]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7980 Rewrite the "Licenses" page (#1207)
authorStas Vilchik <vilchiks@gmail.com>
Tue, 6 Sep 2016 14:02:40 +0000 (16:02 +0200)
committerGitHub <noreply@github.com>
Tue, 6 Sep 2016 14:02:40 +0000 (16:02 +0200)
34 files changed:
it/it-tests/src/test/java/it/settings/LicensesPageTest.java [new file with mode: 0644]
it/it-tests/src/test/java/it/settings/SettingsTestRestartingOrchestrator.java
it/it-tests/src/test/java/pageobjects/Navigation.java
it/it-tests/src/test/java/pageobjects/licenses/LicenseItem.java [new file with mode: 0644]
it/it-tests/src/test/java/pageobjects/licenses/LicensesPage.java [new file with mode: 0644]
it/it-tests/src/test/resources/settings/SettingsTest/display-license.html [deleted file]
it/it-tests/src/test/resources/settings/SettingsTest/display-untyped-license.html [deleted file]
it/it-tests/src/test/resources/settings/SettingsTest/ignore-corrupted-license.html [deleted file]
server/sonar-web/src/main/js/api/licenses.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/app.js
server/sonar-web/src/main/js/apps/settings/licenses/LicenseChangeForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/LicenseRowContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/LicenseStatus.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/LicenseValueView.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/LicenseValueView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/LicensesApp.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/LicensesAppHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/LicensesList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/LicensesListContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseChangeForm-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseStatus-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesApp-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesAppHeader-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesList-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/store/licenses/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/store/licenses/reducer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/store/rootReducer.js
server/sonar-web/src/main/js/main/nav/links-mixin.js
server/sonar-web/src/main/js/main/nav/settings/settings-nav.js
server/sonar-web/src/main/less/init/forms.less
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/it/it-tests/src/test/java/it/settings/LicensesPageTest.java b/it/it-tests/src/test/java/it/settings/LicensesPageTest.java
new file mode 100644 (file)
index 0000000..4fe4885
--- /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.
+ */
+package it.settings;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.ws.Settings.ValuesWsResponse;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.setting.ValuesRequest;
+import pageobjects.Navigation;
+import pageobjects.licenses.LicenseItem;
+import pageobjects.licenses.LicensesPage;
+
+import static com.codeborne.selenide.Condition.text;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.pluginArtifact;
+
+public class LicensesPageTest {
+  private static Orchestrator orchestrator;
+  private static WsClient wsClient;
+
+  @Rule
+  public Navigation nav = Navigation.get(orchestrator);
+
+  @BeforeClass
+  public static void start() {
+    orchestrator = Orchestrator.builderEnv()
+      .addPlugin(pluginArtifact("license-plugin"))
+      .build();
+    orchestrator.start();
+
+    wsClient = newAdminWsClient(orchestrator);
+  }
+
+  @AfterClass
+  public static void stop() {
+    if (orchestrator != null) {
+      orchestrator.stop();
+    }
+  }
+
+  @Test
+  public void display_licenses() {
+    LicensesPage page = nav.logIn().asAdmin().openLicenses();
+
+    page.getLicenses().shouldHaveSize(2);
+    page.getLicensesAsItems().get(0).getName().shouldHave(text("typed.license.secured"));
+    page.getLicensesAsItems().get(1).getName().shouldHave(text("untyped.license.secured"));
+  }
+
+  @Test
+  public void change_licenses() {
+    String EXAMPLE_LICENSE = "TmFtZTogRGV2ZWxvcHBlcnMKUGx1Z2luOiBhdXRvY29udHJvbApFeHBpcmVzOiAyMDEyLTA0LTAxCktleTogNjI5N2MxMzEwYzg2NDZiZTE5MDU1MWE4ZmZmYzk1OTBmYzEyYTIyMgo=";
+
+    LicensesPage page = nav.logIn().asAdmin().openLicenses();
+    LicenseItem licenseItem = page.getLicenseByKey("typed.license.secured");
+    licenseItem.setLicense(EXAMPLE_LICENSE);
+
+    ValuesWsResponse response = wsClient.settingsService()
+      .values(ValuesRequest.builder().setKeys("typed.license.secured").build());
+    assertThat(response.getSettings(0).getValue()).isEqualTo(EXAMPLE_LICENSE);
+  }
+}
index 67b2eefa62f9fd6dbc67422e5bed4f9754da55a5..7280a23305dcb3417baab64dff1ea599e3047f6a 100644 (file)
@@ -72,12 +72,6 @@ public class SettingsTestRestartingOrchestrator {
       // test encryption
       "/settings/SettingsTest/generate-secret-key.html",
       "/settings/SettingsTest/encrypt-text.html"
-
-      // test licenses
-      // TODO enable when license page will be rewritten
-      // "/settings/SettingsTest/ignore-corrupted-license.html",
-      // "/settings/SettingsTest/display-license.html",
-      // "/settings/SettingsTest/display-untyped-license.html"
     ).build();
     new SeleneseTest(selenese).runOn(orchestrator);
   }
index 28a99e4b3f8770500d4df5b345ac45e38333d7a4..018fdbaaa1a7065c8c414c13dee3deb0bd540d0d 100644 (file)
@@ -29,6 +29,7 @@ import java.net.URLEncoder;
 import javax.annotation.Nullable;
 import org.junit.rules.ExternalResource;
 import org.openqa.selenium.By;
+import pageobjects.licenses.LicensesPage;
 import pageobjects.settings.SettingsPage;
 
 import static com.codeborne.selenide.Selenide.$;
@@ -95,6 +96,10 @@ public class Navigation extends ExternalResource {
     return open(url, SettingsPage.class);
   }
 
+  public LicensesPage openLicenses() {
+    return open("/settings/licenses", LicensesPage.class);
+  }
+
   public void open(String relativeUrl) {
     Selenide.open(relativeUrl);
   }
diff --git a/it/it-tests/src/test/java/pageobjects/licenses/LicenseItem.java b/it/it-tests/src/test/java/pageobjects/licenses/LicenseItem.java
new file mode 100644 (file)
index 0000000..2df9869
--- /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.
+ */
+package pageobjects.licenses;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class LicenseItem {
+
+  private final SelenideElement elt;
+
+  public LicenseItem(SelenideElement elt) {
+    this.elt = elt;
+  }
+
+  public SelenideElement getName() {
+    return elt.find(".js-product");
+  }
+
+  public LicenseItem setLicense(String value) {
+    elt.find(".js-change").click();
+    $("#license-input").shouldBe(visible).val(value);
+    $(".js-modal-submit").click();
+    $("#license-input").shouldNotBe(visible);
+    return this;
+  }
+}
diff --git a/it/it-tests/src/test/java/pageobjects/licenses/LicensesPage.java b/it/it-tests/src/test/java/pageobjects/licenses/LicensesPage.java
new file mode 100644 (file)
index 0000000..129d6c6
--- /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.
+ */
+package pageobjects.licenses;
+
+import com.codeborne.selenide.ElementsCollection;
+import com.codeborne.selenide.SelenideElement;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class LicensesPage {
+
+  public LicensesPage() {
+    $("#licenses-page").shouldBe(visible);
+  }
+
+  public ElementsCollection getLicenses() {
+    return $$(".js-license");
+  }
+
+  public List<LicenseItem> getLicensesAsItems() {
+    return getLicenses()
+      .stream()
+      .map(LicenseItem::new)
+      .collect(Collectors.toList());
+  }
+
+  public LicenseItem getLicenseByKey(String key) {
+    SelenideElement element = $(".js-license[data-license-key=\"" + key + "\"]");
+    return new LicenseItem(element);
+  }
+}
diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/display-license.html b/it/it-tests/src/test/resources/settings/SettingsTest/display-license.html
deleted file mode 100644 (file)
index f2f1d31..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-    <title>display-license</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-    <tbody>
-    <tr>
-        <td>open</td>
-        <td>/sessions/logout</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>open</td>
-        <td>/settings?category=general</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>type</td>
-        <td>id=login</td>
-        <td>admin</td>
-    </tr>
-    <tr>
-        <td>type</td>
-        <td>id=password</td>
-        <td>admin</td>
-    </tr>
-    <tr>
-        <td>clickAndWait</td>
-        <td>name=commit</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>waitForElementPresent</td>
-        <td>css=.js-user-authenticated</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>type</td>
-        <td>id=input_typed.license.secured</td>
-        <td>TmFtZTogRGV2ZWxvcHBlcnMKUGx1Z2luOiBhdXRvY29udHJvbApFeHBpcmVzOiAyMDEyLTA0LTAxCktleTogNjI5N2MxMzEwYzg2NDZiZTE5MDU1MWE4ZmZmYzk1OTBmYzEyYTIyMgo=</td>
-    </tr>
-    <tr>
-        <td>click</td>
-        <td>id=submit_settings</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>waitForText</td>
-        <td>block_typed.license.secured</td>
-        <td>*autocontrol*</td>
-    </tr>
-    <tr>
-        <td>waitForText</td>
-        <td>block_typed.license.secured</td>
-        <td>*Developpers*</td>
-    </tr>
-    <tr>
-        <td>waitForText</td>
-        <td>block_typed.license.secured</td>
-        <td>*2012*</td>
-    </tr>
-    </tbody>
-</table>
-</body>
-</html>
diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/display-untyped-license.html b/it/it-tests/src/test/resources/settings/SettingsTest/display-untyped-license.html
deleted file mode 100644 (file)
index ae353cf..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-    <title>display-untyped-license</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-    <tbody>
-    <tr>
-        <td>open</td>
-        <td>/sessions/logout</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>open</td>
-        <td>/settings?category=general</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>type</td>
-        <td>id=login</td>
-        <td>admin</td>
-    </tr>
-    <tr>
-        <td>type</td>
-        <td>id=password</td>
-        <td>admin</td>
-    </tr>
-    <tr>
-        <td>clickAndWait</td>
-        <td>name=commit</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>waitForElementPresent</td>
-        <td>css=.js-user-authenticated</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>type</td>
-        <td>id=input_untyped.license.secured</td>
-        <td>TmFtZTogRGV2ZWxvcHBlcnMKUGx1Z2luOiBhdXRvY29udHJvbApFeHBpcmVzOiAyMDEyLTA0LTAxCktleTogNjI5N2MxMzEwYzg2NDZiZTE5MDU1MWE4ZmZmYzk1OTBmYzEyYTIyMgo=</td>
-    </tr>
-    <tr>
-        <td>click</td>
-        <td>submit_settings</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>waitForText</td>
-        <td>block_untyped.license.secured</td>
-        <td>*autocontrol*</td>
-    </tr>
-    <tr>
-        <td>waitForText</td>
-        <td>block_untyped.license.secured</td>
-        <td>*Developpers*</td>
-    </tr>
-    <tr>
-        <td>waitForText</td>
-        <td>block_untyped.license.secured</td>
-        <td>*2012*</td>
-    </tr>
-    </tbody>
-</table>
-</body>
-</html>
diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/ignore-corrupted-license.html b/it/it-tests/src/test/resources/settings/SettingsTest/ignore-corrupted-license.html
deleted file mode 100644 (file)
index e05b572..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-    <title>ignore-corrupted-license</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-    <tbody>
-    <tr>
-        <td>open</td>
-        <td>/sessions/logout</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>open</td>
-        <td>/settings?category=general</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>type</td>
-        <td>id=login</td>
-        <td>admin</td>
-    </tr>
-    <tr>
-        <td>type</td>
-        <td>id=password</td>
-        <td>admin</td>
-    </tr>
-    <tr>
-        <td>clickAndWait</td>
-        <td>name=commit</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>waitForElementPresent</td>
-        <td>css=.js-user-authenticated</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>type</td>
-        <td>id=input_typed.license.secured</td>
-        <td>ABCDE</td>
-    </tr>
-    <tr>
-        <td>click</td>
-        <td>id=submit_settings</td>
-        <td></td>
-    </tr>
-    <tr>
-        <td>waitForText</td>
-        <td>block_typed.license.secured</td>
-        <td>*Product*-*Organization*-*Expiration*-*</td>
-    </tr>
-    </tbody>
-</table>
-</body>
-</html>
diff --git a/server/sonar-web/src/main/js/api/licenses.js b/server/sonar-web/src/main/js/api/licenses.js
new file mode 100644 (file)
index 0000000..466c67d
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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, post } from '../helpers/request';
+
+export const getLicenses = () =>
+    getJSON('/api/licenses/list').then(r => r.licenses);
+
+export const setLicense = (key, value) => {
+  const url = '/api/settings/set';
+  const data = { key, value };
+  return post(url, data);
+};
+
+export const resetLicense = key => {
+  const url = '/api/settings/reset';
+  const data = { keys: key };
+  return post(url, data);
+};
index cf9dfcf07adf2b013be3e1118ea066715befd42a..51c70703f9d7d4a5f4c04904d9b3ccb8760c2bb4 100644 (file)
@@ -23,6 +23,7 @@ import { Provider } from 'react-redux';
 import { Router, Route, Redirect, useRouterHistory } from 'react-router';
 import { createHistory } from 'history';
 import App from './components/App';
+import LicensesApp from './licenses/LicensesApp';
 import rootReducer from './store/rootReducer';
 import configureStore from '../../components/store/configureStore';
 
@@ -44,6 +45,9 @@ window.sonarqube.appStarted.then(options => {
         <Router history={history}>
           <Redirect from="/index" to="/"/>
           <Route path="/" component={withComponent(App)}/>
+          {options.component == null && (
+              <Route path="/licenses" component={LicensesApp}/>
+          )}
         </Router>
       </Provider>
   ), el);
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseChangeForm.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseChangeForm.js
new file mode 100644 (file)
index 0000000..8976c35
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import LicenseValueView from './LicenseValueView';
+import { translate } from '../../../helpers/l10n';
+
+export default class LicenseChangeForm extends React.Component {
+  static propTypes = {
+    license: React.PropTypes.object.isRequired,
+    onChange: React.PropTypes.func.isRequired
+  };
+
+  onClick (e) {
+    e.preventDefault();
+    e.target.blur();
+
+    const { license, onChange } = this.props;
+
+    new LicenseValueView({
+      productName: license.name || license.key,
+      value: license.value,
+      onChange
+    }).render();
+  }
+
+  render () {
+    return (
+        <button className="js-change" onClick={e => this.onClick(e)}>{translate('change_verb')}</button>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js
new file mode 100644 (file)
index 0000000..2b095f0
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import shallowCompare from 'react-addons-shallow-compare';
+import moment from 'moment';
+import LicenseStatus from './LicenseStatus';
+import LicenseChangeForm from './LicenseChangeForm';
+
+export default class LicenseRow extends React.Component {
+  static propTypes = {
+    license: React.PropTypes.object.isRequired,
+    setLicense: React.PropTypes.func.isRequired
+  };
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return shallowCompare(this, nextProps, nextState);
+  }
+
+  handleSet (value) {
+    return this.props.setLicense(this.props.license.key, value).catch(() => { /* do nothing */ });
+  }
+
+  render () {
+    const { license } = this.props;
+
+    return (
+        <tr className="js-license" data-license-key={license.key}>
+          <td className="text-middle"><LicenseStatus license={license}/></td>
+          <td className="js-product text-middle">
+            <div className={license.invalidProduct ? 'text-danger' : null}>
+              {license.name || license.key}
+            </div>
+          </td>
+          <td className="js-organization text-middle">{license.organization}</td>
+          <td className="js-expiration text-middle">
+            <div className={license.invalidExpiration ? 'text-danger' : null}>
+              {moment(license.expiration).format('LL')}
+            </div>
+          </td>
+          <td className="js-type text-middle">{license.type}</td>
+          <td className="js-server-id text-middle">
+            <div className={license.invalidServerId ? 'text-danger' : null}>
+              {license.serverId}
+            </div>
+          </td>
+          <td className="text-right">
+            <LicenseChangeForm license={license} onChange={value => this.handleSet(value)}/>
+          </td>
+        </tr>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRowContainer.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRowContainer.js
new file mode 100644 (file)
index 0000000..af004df
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+import LicenseRow from './LicenseRow';
+import { getLicenseByKey } from '../store/rootReducer';
+import { setLicense } from '../store/licenses/actions';
+
+const mapStateToProps = (state, ownProps) => ({
+  license: getLicenseByKey(state, ownProps.licenseKey)
+});
+
+export default connect(
+    mapStateToProps,
+    { setLicense }
+)(LicenseRow);
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseStatus.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseStatus.js
new file mode 100644 (file)
index 0000000..6574356
--- /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';
+
+export default class LicenseStatus extends React.Component {
+  static propTypes = {
+    license: React.PropTypes.object.isRequired
+  };
+
+  render () {
+    const { license } = this.props;
+
+    if (license.value == null) {
+      return null;
+    }
+
+    const isInvalid = !!license.invalidProduct || !!license.invalidExpiration || !!license.invalidServerId;
+    if (isInvalid) {
+      return <i className="icon-alert-error"/>;
+    }
+
+    return <i className="icon-check"/>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseValueView.hbs b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseValueView.hbs
new file mode 100644 (file)
index 0000000..c472b28
--- /dev/null
@@ -0,0 +1,16 @@
+<form>
+  <div class="modal-head">
+    <h2>{{tp 'licenses.change_license_for_x' productName}}</h2>
+  </div>
+  <div class="modal-body">
+    <div class="js-modal-messages"></div>
+    <label for="license-input">{{t 'licenses.license_input_label'}}</label>
+    <textarea class="width-100 spacer-top" rows="7" id="license-input">{{value}}</textarea>
+    <div class="spacer-top note">{{t 'licenses.license_input_note'}}</div>
+  </div>
+  <div class="modal-foot">
+    <i class="js-modal-spinner spinner spacer-right hidden"></i>
+    <button class="js-modal-submit">{{t 'save'}}</button>
+    <a href="#" class="js-modal-close">{{t 'cancel'}}</a>
+  </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseValueView.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseValueView.js
new file mode 100644 (file)
index 0000000..4e80de4
--- /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 ModalForm from '../../../components/common/modal-form';
+import Template from './LicenseValueView.hbs';
+
+export default ModalForm.extend({
+  template: Template,
+
+  onFormSubmit () {
+    ModalForm.prototype.onFormSubmit.apply(this, arguments);
+    this.disableForm();
+    this.showSpinner();
+
+    const value = this.$('textarea').val();
+    this.options.onChange(value).then(() => this.destroy());
+  },
+
+  serializeData () {
+    return {
+      productName: this.options.productName,
+      value: this.options.value
+    };
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicensesApp.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicensesApp.js
new file mode 100644 (file)
index 0000000..0568461
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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 GlobalMessagesContainer from '../components/GlobalMessagesContainer';
+import LicensesAppHeader from './LicensesAppHeader';
+import LicensesListContainer from './LicensesListContainer';
+
+export default class LicensesApp extends React.Component {
+  render () {
+    return (
+        <div id="licenses-page" className="page page-limited">
+          <LicensesAppHeader/>
+          <GlobalMessagesContainer/>
+          <LicensesListContainer/>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicensesAppHeader.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicensesAppHeader.js
new file mode 100644 (file)
index 0000000..a504570
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default class LicensesAppHeader extends React.Component {
+  render () {
+    return (
+        <header className="page-header">
+          <h1 className="page-title">{translate('property.category.licenses')}</h1>
+          <div className="page-description"
+               dangerouslySetInnerHTML={{ __html: translate('property.category.licenses.description') }}/>
+        </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicensesList.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicensesList.js
new file mode 100644 (file)
index 0000000..84bfb60
--- /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 shallowCompare from 'react-addons-shallow-compare';
+import LicenseRowContainer from './LicenseRowContainer';
+import { translate } from '../../../helpers/l10n';
+
+export default class LicensesList extends React.Component {
+  static propTypes = {
+    licenses: React.PropTypes.array.isRequired,
+    fetchLicenses: React.PropTypes.func.isRequired
+  };
+
+  componentDidMount () {
+    this.props.fetchLicenses().catch(() => { /* do nothing */ });
+  }
+
+  shouldComponentUpdate (nextProps, nextState) {
+    return shallowCompare(this, nextProps, nextState);
+  }
+
+  render () {
+    return (
+        <table className="data zebra zebra-hover" style={{ tableLayout: 'fixed' }}>
+          <thead>
+            <tr>
+              <th width={40}>&nbsp;</th>
+              <th>{translate('licenses.list.product')}</th>
+              <th width={150}>{translate('licenses.list.organization')}</th>
+              <th width={150}>{translate('licenses.list.expiration')}</th>
+              <th width={150}>{translate('licenses.list.type')}</th>
+              <th width={150}>{translate('licenses.list.server')}</th>
+              <th width={80}>&nbsp;</th>
+            </tr>
+          </thead>
+          <tbody>
+            {this.props.licenses.map(licenseKey => (
+                <LicenseRowContainer key={licenseKey} licenseKey={licenseKey}/>
+            ))}
+          </tbody>
+        </table>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicensesListContainer.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicensesListContainer.js
new file mode 100644 (file)
index 0000000..bd692b7
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+import LicensesList from './LicensesList';
+import { fetchLicenses } from '../store/licenses/actions';
+import { getAllLicenseKeys } from '../store/rootReducer';
+
+const mapStateToProps = state => ({
+  licenses: getAllLicenseKeys(state)
+});
+
+export default connect(
+    mapStateToProps,
+    { fetchLicenses }
+)(LicensesList);
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseChangeForm-test.js b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseChangeForm-test.js
new file mode 100644 (file)
index 0000000..7d52015
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import LicenseChangeForm from '../LicenseChangeForm';
+
+it('should render button', () => {
+  const form = shallow(<LicenseChangeForm license={{}} onChange={jest.fn()}/>);
+  expect(form.is('button')).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js
new file mode 100644 (file)
index 0000000..87a75c9
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import LicenseRow from '../LicenseRow';
+import LicenseStatus from '../LicenseStatus';
+import LicenseChangeForm from '../LicenseChangeForm';
+
+it('should render status', () => {
+  const license = {};
+  const licenseStatus = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find(LicenseStatus);
+  expect(licenseStatus.length).toBe(1);
+  expect(licenseStatus.prop('license')).toBe(license);
+});
+
+it('should render product', () => {
+  const license = { name: 'foo' };
+  const licenseProduct = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find('.js-product');
+  expect(licenseProduct.length).toBe(1);
+  expect(licenseProduct.text()).toContain('foo');
+});
+
+it('should render invalid product', () => {
+  const license = { product: 'foo', invalidProduct: true };
+  const licenseProduct = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find('.js-product');
+  expect(licenseProduct.find('.text-danger').length).toBe(1);
+});
+
+it('should render key when no name', () => {
+  const license = { key: 'foo.secured' };
+  const licenseProduct = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find('.js-product');
+  expect(licenseProduct.length).toBe(1);
+  expect(licenseProduct.text()).toContain('foo.secured');
+});
+
+it('should render organization', () => {
+  const license = { organization: 'org' };
+  const licenseOrg = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find('.js-organization');
+  expect(licenseOrg.length).toBe(1);
+  expect(licenseOrg.text()).toContain('org');
+});
+
+it('should render expiration', () => {
+  const license = { expiration: '2015-01-01' };
+  const licenseExpiration = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find('.js-expiration');
+  expect(licenseExpiration.length).toBe(1);
+  expect(licenseExpiration.text()).toContain('2015');
+});
+
+it('should render invalid expiration', () => {
+  const license = { expiration: '2015-01-01', invalidExpiration: true };
+  const licenseExpiration = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find('.js-expiration');
+  expect(licenseExpiration.find('.text-danger').length).toBe(1);
+});
+
+it('should render type', () => {
+  const license = { type: 'PRODUCTION' };
+  const licenseType = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find('.js-type');
+  expect(licenseType.length).toBe(1);
+  expect(licenseType.text()).toContain('PRODUCTION');
+});
+
+it('should render server id', () => {
+  const license = { serverId: 'bar' };
+  const licenseServerId = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find('.js-server-id');
+  expect(licenseServerId.length).toBe(1);
+  expect(licenseServerId.text()).toContain('bar');
+});
+
+it('should render invalid server id', () => {
+  const license = { serverId: 'bar', invalidServerId: true };
+  const licenseServerId = shallow(<LicenseRow license={license} setLicense={jest.fn()}/>).find('.js-server-id');
+  expect(licenseServerId.find('.text-danger').length).toBe(1);
+});
+
+it('should render change form', () => {
+  const license = { key: 'foo' };
+  const setLicense = jest.fn(() => Promise.resolve());
+  const licenseChangeForm = shallow(<LicenseRow license={license} setLicense={setLicense}/>).find(LicenseChangeForm);
+  expect(licenseChangeForm.length).toBe(1);
+  expect(licenseChangeForm.prop('license')).toBe(license);
+  expect(typeof licenseChangeForm.prop('onChange')).toBe('function');
+
+  licenseChangeForm.prop('onChange')('license-hash');
+  expect(setLicense).toBeCalledWith('foo', 'license-hash');
+});
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseStatus-test.js b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseStatus-test.js
new file mode 100644 (file)
index 0000000..106f1d6
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import LicenseStatus from '../LicenseStatus';
+
+it('should render nothing when no value', () => {
+  const status = shallow(<LicenseStatus license={{}}/>);
+  expect(status.node).toBeNull();
+});
+
+it('should render ok', () => {
+  const status = shallow(<LicenseStatus license={{ value: 'foo' }}/>);
+  expect(status.is('.icon-check')).toBe(true);
+});
+
+it('should render error when invalid product', () => {
+  const status = shallow(<LicenseStatus license={{ value: 'foo', invalidProduct: true }}/>);
+  expect(status.is('.icon-check')).toBe(false);
+  expect(status.is('.icon-alert-error')).toBe(true);
+});
+
+it('should render error when invalid expiration', () => {
+  const status = shallow(<LicenseStatus license={{ value: 'foo', invalidExpiration: true }}/>);
+  expect(status.is('.icon-check')).toBe(false);
+  expect(status.is('.icon-alert-error')).toBe(true);
+});
+
+it('should render error when invalid server id', () => {
+  const status = shallow(<LicenseStatus license={{ value: 'foo', invalidServerId: true }}/>);
+  expect(status.is('.icon-check')).toBe(false);
+  expect(status.is('.icon-alert-error')).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesApp-test.js b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesApp-test.js
new file mode 100644 (file)
index 0000000..ab23f0f
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import LicensesApp from '../LicensesApp';
+import GlobalMessagesContainer from '../../components/GlobalMessagesContainer';
+import LicensesAppHeader from '../LicensesAppHeader';
+import LicensesListContainer from '../LicensesListContainer';
+
+it('should render', () => {
+  const app = shallow(<LicensesApp/>);
+  expect(app.find(GlobalMessagesContainer).length).toBe(1);
+  expect(app.find(LicensesAppHeader).length).toBe(1);
+  expect(app.find(LicensesListContainer).length).toBe(1);
+});
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesAppHeader-test.js b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesAppHeader-test.js
new file mode 100644 (file)
index 0000000..a5b04e0
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import LicensesAppHeader from '../LicensesAppHeader';
+
+it('should render', () => {
+  const header = shallow(<LicensesAppHeader/>);
+  expect(header.find('.page-title').length).toBe(1);
+  expect(header.find('.page-description').length).toBe(1);
+});
diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesList-test.js b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesList-test.js
new file mode 100644 (file)
index 0000000..2b3fb17
--- /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 { shallow, mount } from 'enzyme';
+import LicensesList from '../LicensesList';
+import LicenseRowContainer from '../LicenseRowContainer';
+
+it('should render', () => {
+  const list = shallow(<LicensesList licenses={[]} fetchLicenses={jest.fn()}/>);
+  expect(list.is('table')).toBe(true);
+});
+
+it('should fetch licenses', () => {
+  const fetchLicenses = jest.fn(() => Promise.resolve());
+  mount(<LicensesList licenses={[]} fetchLicenses={fetchLicenses}/>);
+  expect(fetchLicenses).toBeCalled();
+});
+
+it('should render rows', () => {
+  const list = shallow(<LicensesList licenses={['foo', 'bar']} fetchLicenses={jest.fn()}/>);
+  expect(list.find(LicenseRowContainer).length).toBe(2);
+  expect(list.find(LicenseRowContainer).at(0).prop('licenseKey')).toBe('foo');
+  expect(list.find(LicenseRowContainer).at(1).prop('licenseKey')).toBe('bar');
+});
diff --git a/server/sonar-web/src/main/js/apps/settings/store/licenses/actions.js b/server/sonar-web/src/main/js/apps/settings/store/licenses/actions.js
new file mode 100644 (file)
index 0000000..d94b51a
--- /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 * as licenses from '../../../../api/licenses';
+import { parseError } from '../../../code/utils';
+import { addGlobalSuccessMessage, addGlobalErrorMessage } from '../../../../components/store/globalMessages';
+import { translate } from '../../../../helpers/l10n';
+
+export const RECEIVE_LICENSES = 'RECEIVE_LICENSES';
+
+const receiveLicenses = licenses => ({
+  type: RECEIVE_LICENSES,
+  licenses
+});
+
+export const fetchLicenses = () => dispatch =>
+    licenses.getLicenses()
+        .then(licenses => {
+          dispatch(receiveLicenses(licenses));
+        })
+        .catch(e => {
+          parseError(e).then(message => dispatch(addGlobalErrorMessage(key, message)));
+          return Promise.reject();
+        });
+
+export const setLicense = (key, value) => dispatch => {
+  const request = value ? licenses.setLicense(key, value) : licenses.resetLicense(key);
+
+  return request
+      .then(() => {
+        dispatch(fetchLicenses());
+        dispatch(addGlobalSuccessMessage(translate('licenses.success_message')));
+      })
+      .catch(e => {
+        parseError(e).then(message => dispatch(addGlobalErrorMessage(message)));
+        return Promise.reject();
+      });
+};
diff --git a/server/sonar-web/src/main/js/apps/settings/store/licenses/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/licenses/reducer.js
new file mode 100644 (file)
index 0000000..e51c543
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import keyBy from 'lodash/keyBy';
+import { RECEIVE_LICENSES } from './actions';
+
+const reducer = (state = {}, action = {}) => {
+  if (action.type === RECEIVE_LICENSES) {
+    const licensesByKey = keyBy(action.licenses, 'key');
+    return { ...state, ...licensesByKey };
+  }
+
+  return state;
+};
+
+export default reducer;
+
+export const getLicenseByKey = (state, key) => state[key];
+
+export const getAllLicenseKeys = state => Object.keys(state);
index 603cb3b753ab62530d9da9eccf684f8f8647c117..e886b4616c01e902d37f4d35610e8765d30c200d 100644 (file)
@@ -21,12 +21,14 @@ import { combineReducers } from 'redux';
 import definitions, * as fromDefinitions from './definitions/reducer';
 import values, * as fromValues from './values/reducer';
 import settingsPage, * as fromSettingsPage from './settingsPage/reducer';
+import licenses, * as fromLicenses from './licenses/reducer';
 import globalMessages, * as fromGlobalMessages from '../../../components/store/globalMessages';
 
 const rootReducer = combineReducers({
   definitions,
   values,
   settingsPage,
+  licenses,
   globalMessages
 });
 
@@ -50,6 +52,10 @@ export const getChangedValue = (state, key) => fromSettingsPage.getChangedValue(
 
 export const isLoading = (state, key) => fromSettingsPage.isLoading(state.settingsPage, key);
 
+export const getLicenseByKey = (state, key) => fromLicenses.getLicenseByKey(state.licenses, key);
+
+export const getAllLicenseKeys = state => fromLicenses.getAllLicenseKeys(state.licenses);
+
 export const getValidationMessage = (state, key) => fromSettingsPage.getValidationMessage(state.settingsPage, key);
 
 export const getGlobalMessages = state => fromGlobalMessages.getGlobalMessages(state.globalMessages);
index 4b39a6738e280ddcd0de0013ec45f1ce733253a8..56f465f973dc57734fec09e040c1d78358d640fd 100644 (file)
@@ -30,7 +30,7 @@ export default {
     const fullUrl = window.baseUrl + url;
     const check = _.isFunction(highlightUrl) ? highlightUrl : this.activeLink;
     return (
-        <li key={url} className={check(highlightUrl)}>
+        <li key={url} className={check(fullUrl)}>
           <a href={fullUrl}>{title}</a>
         </li>
     );
index fe673cbadb339389148070bf00c4ff806259acb3..d9125a3644ed00a275ae9ffec4a9db5d80c09f12 100644 (file)
@@ -76,7 +76,8 @@ export default React.createClass({
                 <i className="icon-dropdown"></i>
               </a>
               <ul className="dropdown-menu">
-                {this.renderLink('/settings', translate('settings.page'))}
+                {this.renderLink('/settings', translate('settings.page'), url => window.location.pathname === url)}
+                {this.renderLink('/settings/licenses', translate('property.category.licenses'))}
                 {this.renderLink('/metrics', 'Custom Metrics')}
                 {this.renderLink('/admin_dashboards',
                     translate('default_dashboards.page'))}
@@ -120,8 +121,7 @@ export default React.createClass({
                 <i className="icon-dropdown"></i>
               </a>
               <ul className="dropdown-menu">
-                {this.renderLink('/updatecenter',
-                    translate('update_center.page'))}
+                {this.renderLink('/updatecenter', translate('update_center.page'))}
                 {this.renderLink('/system', translate('system_info.page'))}
               </ul>
             </li>
index bdc1c17ced310b9c9daca6f846dbf769dd693db9..384455b0b9847982ae69f5836486a5e2c5360288 100644 (file)
@@ -70,6 +70,10 @@ input[type="search"]::-webkit-search-decoration {
 
 textarea {
   padding: 3px;
+
+  &.width-100 {
+    max-width: 100%;
+  }
 }
 
 select {
index ee4e72f070e7f93617887a0675e91de6fc303154..809467058139f11555d5d9cf60341b0508d904ad 100644 (file)
@@ -24,4 +24,8 @@ class SettingsController < ApplicationController
   def index
 
   end
+
+  def licenses
+    render :action => 'index'
+  end
 end
index 8b42dba8ba569f0891ba051d15f7a9c175e97b66..dc1353f58f33f673d6f7c1751a68c583ae6b660f 100644 (file)
@@ -2074,6 +2074,15 @@ server_id_configuration.ip.desc=A server ID is linked to the IP address of the h
 server_id_configuration.generation_error=Organisation and/or IP address are not valid.
 server_id_configuration.fields_cannot_be_blank=Organisation and IP address cannot be blank.
 server_id_configuration.does_not_match_organisation_pattern=Organisation does not match the required pattern.
+licenses.list.product=Product
+licenses.list.organization=Organization
+licenses.list.expiration=Expiration
+licenses.list.type=Type
+licenses.list.server=Server
+licenses.change_license_for_x=Change License for {0}
+licenses.license_input_label=Insert the license text below:
+licenses.license_input_note=Keep empty if you want to unset this license.
+licenses.success_message=New license has been set.
 
 
 #------------------------------------------------------------------------------