]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8170 SONAR-8171 Reorganize My Account space
authorStas Vilchik <vilchiks@gmail.com>
Fri, 23 Sep 2016 15:42:18 +0000 (17:42 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Fri, 30 Sep 2016 15:23:37 +0000 (17:23 +0200)
68 files changed:
it/it-tests/src/test/java/it/user/MyAccountPageTest.java
it/it-tests/src/test/java/pageobjects/MyActivityPage.java [new file with mode: 0644]
it/it-tests/src/test/java/pageobjects/Navigation.java
it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html
it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html
it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_issues.html [deleted file]
it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html
it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html
it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_user_details.html
server/sonar-web/config/webpack/webpack.config.base.js
server/sonar-web/src/main/js/api/favorites.js
server/sonar-web/src/main/js/app/components/App.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/index.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/store/favorites/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/store/favorites/reducer.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/store/measures/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/store/measures/reducer.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/store/rootReducer.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/store/users/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/store/users/reducer.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/styles.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/account.css
server/sonar-web/src/main/js/apps/account/app.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/Account.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/components/AccountApp.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/FavoriteIssueFilters.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/FavoriteMeasureFilters.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/Favorites.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/GlobalNotifications.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/Home.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/IssueWidgets.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/Issues.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/Nav.js
server/sonar-web/src/main/js/apps/account/components/Notifications.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/NotificationsContainer.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/NotificationsList.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/ProjectNotification.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js [deleted file]
server/sonar-web/src/main/js/apps/account/components/Security.js
server/sonar-web/src/main/js/apps/account/components/UserCard.js
server/sonar-web/src/main/js/apps/account/components/UserExternalIdentity.js [deleted file]
server/sonar-web/src/main/js/apps/account/home/components/FavoriteProjects.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/home/components/IssuesActivity.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/home/components/MyActivity.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/home/components/MyActivityContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/home/store/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/home/store/reducer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/issues-app.js [deleted file]
server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/notifications/Notifications.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/notifications/ProjectNotification.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/profile/Profile.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/profile/UserGroups.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/profile/UserScmAccounts.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/projects/Projects.js
server/sonar-web/src/main/js/apps/account/projects/ProjectsContainer.js
server/sonar-web/src/main/js/apps/account/projects/ProjectsSearch.js [deleted file]
server/sonar-web/src/main/js/apps/account/routes.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs
server/sonar-web/src/main/js/helpers/request.js
server/sonar-web/src/main/js/helpers/urls.js
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/views/account/index.html.erb
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 8f7b479d8ee189d43ed0334c64f398601da9d243..63c11f9549df2ca59c12fc07889953490e424d24 100644 (file)
@@ -27,11 +27,17 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.sonarqube.ws.client.PostRequest;
 import org.sonarqube.ws.client.WsClient;
+import pageobjects.MyActivityPage;
+import pageobjects.Navigation;
 import util.selenium.SeleneseTest;
 
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.WebDriverRunner.url;
+import static org.assertj.core.api.Assertions.assertThat;
 import static util.ItUtils.newAdminWsClient;
 import static util.ItUtils.projectDir;
 
@@ -41,6 +47,9 @@ public class MyAccountPageTest {
   public static Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
   private static WsClient adminWsClient;
 
+  @Rule
+  public Navigation nav = Navigation.get(orchestrator);
+
   @BeforeClass
   public static void setUp() {
     orchestrator.resetData();
@@ -57,6 +66,20 @@ public class MyAccountPageTest {
     deactivateUser("account-user");
   }
 
+  @Test
+  public void should_open_by_default() {
+    nav.logIn().asAdmin().openHomepage();
+    assertThat(url()).contains("/account");
+  }
+
+  @Test
+  public void should_display_activity () {
+    MyActivityPage page = nav.logIn().asAdmin().openMyActivity();
+    page.getAllIssues().shouldBe(visible);
+    page.getRecentIssues().shouldBe(visible);
+    page.assertNoFavoriteProjects();
+  }
+
   @Test
   public void should_display_user_details() throws Exception {
     Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("should_display_user_details",
@@ -73,14 +96,6 @@ public class MyAccountPageTest {
     new SeleneseTest(selenese).runOn(orchestrator);
   }
 
-  @Test
-  public void should_display_issues() throws Exception {
-    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("should_display_issues",
-      "/user/MyAccountPageTest/should_display_issues.html"
-    ).build();
-    new SeleneseTest(selenese).runOn(orchestrator);
-  }
-
   @Test
   public void should_display_projects() throws Exception {
     // first, try on empty instance
diff --git a/it/it-tests/src/test/java/pageobjects/MyActivityPage.java b/it/it-tests/src/test/java/pageobjects/MyActivityPage.java
new file mode 100644 (file)
index 0000000..8bcb376
--- /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.
+ */
+package pageobjects;
+
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class MyActivityPage {
+
+  public MyActivityPage() {
+    $("#my-activity-page").shouldBe(visible);
+  }
+
+  public SelenideElement getRecentIssues() {
+    return $("#recent-issues");
+  }
+
+  public SelenideElement getAllIssues() {
+    return $("#all-issues");
+  }
+
+  public SelenideElement getFavoriteProjects() {
+    return $("#favorite-projects");
+  }
+
+  public void assertNoFavoriteProjects() {
+    $("#no-favorite-projects").shouldBe(visible);
+  }
+}
index 071df520c6eefde7bace198f6ec99e257421371f..8342474a41796b5aa180482b0240b2ff467c7acd 100644 (file)
@@ -108,6 +108,10 @@ public class Navigation extends ExternalResource {
     return open("/settings/server_id", ServerIdPage.class);
   }
 
+  public MyActivityPage openMyActivity() {
+    return open("/account", MyActivityPage.class);
+  }
+
   public void open(String relativeUrl) {
     Selenide.open(relativeUrl);
   }
index 60751a0adefd2e68ac3d7ffefb7a9e8b7d06a8cb..f073c8718fb98b95929105c368885112f2050f75 100644 (file)
@@ -40,7 +40,7 @@
   </tr>
   <tr>
     <td>open</td>
-    <td>/account/index</td>
+    <td>/account/profile</td>
     <td></td>
   </tr>
   <tr>
index a245110f6241d50d32ccfbd9917b20ec8a29c329..ca5bd80f9c9c5c4ed9569fb60d5adc21d5d0b799 100644 (file)
@@ -40,7 +40,7 @@
   </tr>
   <tr>
     <td>open</td>
-    <td>/account/index</td>
+    <td>/account/profile</td>
     <td></td>
   </tr>
   <tr>
diff --git a/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_issues.html b/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_issues.html
deleted file mode 100644 (file)
index ab2e1c4..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <link rel="selenium.base" href="http://localhost:49506"/>
-  <title>should_display_issues</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">should_display_issues</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-       <td>open</td>
-       <td>/sonar/sessions/login</td>
-       <td></td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>id=login</td>
-       <td>account-user</td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>id=password</td>
-       <td>password</td>
-</tr>
-<tr>
-       <td>clickAndWait</td>
-       <td>commit</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>css=.js-user-authenticated</td>
-       <td></td>
-</tr>
-<tr>
-       <td>open</td>
-       <td>/sonar/account/issues</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>css=[data-unresolved]</td>
-       <td></td>
-</tr>
-</tbody>
-</table>
-</body>
-</html>
index 4060bf595a61692b99e89ba98bf6afe54cc76c16..31d0dbb719798c4c7db55ece3e28a9f0915a0ced 100644 (file)
 </tr>
 <tr>
        <td>open</td>
-       <td>/account/projects</td>
+       <td>/account/projects/</td>
        <td></td>
 </tr>
 <tr>
        <td>waitForElementPresent</td>
-       <td>css=.account-projects</td>
+       <td>css=#account-projects</td>
        <td></td>
 </tr>
 <tr>
index 5f580876c514e5c563812447af5ed400c1572ee5..b4e9b0f9e1efadc139beeff866d0dbb0c3dcb0a8 100644 (file)
@@ -41,7 +41,7 @@
 </tr>
 <tr>
        <td>open</td>
-       <td>/account/projects</td>
+       <td>/account/projects/</td>
        <td></td>
 </tr>
 <tr>
index 1036819238cb5cf308b84faeba0190f8d53abfd9..71efda227c6dbc4447f92965a4b07f509fc85f88 100644 (file)
@@ -41,7 +41,7 @@
 </tr>
 <tr>
        <td>open</td>
-       <td>/sonar/account/</td>
+       <td>/sonar/account/profile/</td>
        <td></td>
 </tr>
 <tr>
        <td>css=#avatar img</td>
        <td></td>
 </tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>id=favorite-components</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>id=favorite-issue-filters</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>id=favorite-measure-filters</td>
-       <td></td>
-</tr>
 </tbody>
 </table>
 </body>
index e5ae5336916c303948a063e9c3ab12e78e1357e9..245c6ba7ca4e0cfdb8bf534ab4afaacc56bde97a 100644 (file)
@@ -23,8 +23,8 @@ module.exports = {
     'sonar': './src/main/js/libs/sonar.js',
 
     'main': './src/main/js/main/app.js',
+    'app': './src/main/js/app/index.js',
 
-    'account': './src/main/js/apps/account/app.js',
     'background-tasks': './src/main/js/apps/background-tasks/app.js',
     'code': './src/main/js/apps/code/app.js',
     'coding-rules': './src/main/js/apps/coding-rules/app.js',
index eff40c309fdb81d8a690ca42552c11060aa1e62b..d3b0e2eed44a03fbdc6a0d54817f0723b5e71009 100644 (file)
@@ -17,7 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { post, requestDelete } from '../helpers/request';
+import { post, requestDelete, getJSON } from '../helpers/request';
+
+export const getFavorites = () => getJSON('/api/favourites');
 
 export function addFavorite (componentKey) {
   const url = '/api/favourites';
diff --git a/server/sonar-web/src/main/js/app/components/App.js b/server/sonar-web/src/main/js/app/components/App.js
new file mode 100644 (file)
index 0000000..b4fac8b
--- /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 { connect } from 'react-redux';
+import { fetchCurrentUser } from '../store/users/actions';
+
+class App extends React.Component {
+  static propTypes = {
+    fetchCurrentUser: React.PropTypes.func.isRequired
+  };
+
+  componentDidMount () {
+    this.props.fetchCurrentUser();
+  }
+
+  render () {
+    return this.props.children;
+  }
+}
+
+export default connect(
+    () => ({}),
+    { fetchCurrentUser }
+)(App);
diff --git a/server/sonar-web/src/main/js/app/index.js b/server/sonar-web/src/main/js/app/index.js
new file mode 100644 (file)
index 0000000..35a9525
--- /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 { render } from 'react-dom';
+import { Router, Route, useRouterHistory } from 'react-router';
+import { createHistory } from 'history';
+import { Provider } from 'react-redux';
+import App from './components/App';
+import accountRoutes from '../apps/account/routes';
+import configureStore from '../components/store/configureStore';
+import rootReducer from './store/rootReducer';
+import './styles.css';
+
+window.sonarqube.appStarted.then(options => {
+  const el = document.querySelector(options.el);
+
+  const history = useRouterHistory(createHistory)({
+    basename: window.baseUrl + '/'
+  });
+
+  const store = configureStore(rootReducer);
+
+  render((
+      <Provider store={store}>
+        <Router history={history}>
+          <Route path="/" component={App}>
+            {accountRoutes}
+          </Route>
+        </Router>
+      </Provider>
+  ), el);
+});
diff --git a/server/sonar-web/src/main/js/app/store/favorites/actions.js b/server/sonar-web/src/main/js/app/store/favorites/actions.js
new file mode 100644 (file)
index 0000000..2fc7db4
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+export const RECEIVE_FAVORITES = 'RECEIVE_FAVORITES';
+
+export const receiveFavorites = favorites => ({
+  type: RECEIVE_FAVORITES,
+  favorites
+});
diff --git a/server/sonar-web/src/main/js/app/store/favorites/reducer.js b/server/sonar-web/src/main/js/app/store/favorites/reducer.js
new file mode 100644 (file)
index 0000000..29d19fe
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { combineReducers } from 'redux';
+import keyBy from 'lodash/keyBy';
+import { RECEIVE_FAVORITES } from './actions';
+
+const favoritesByKey = (state = {}, action = {}) => {
+  if (action.type === RECEIVE_FAVORITES) {
+    const byKey = keyBy(action.favorites, 'key');
+    return { ...state, ...byKey };
+  }
+
+  return state;
+};
+
+const favoriteKeys = (state = null, action = {}) => {
+  if (action.type === RECEIVE_FAVORITES) {
+    return action.favorites.map(f => f.key);
+  }
+
+  return state;
+};
+
+export default combineReducers({ favoritesByKey, favoriteKeys });
+
+export const getFavorites = state => (
+    state.favoriteKeys ?
+        state.favoriteKeys.map(key => state.favoritesByKey[key]) :
+        null
+);
diff --git a/server/sonar-web/src/main/js/app/store/measures/actions.js b/server/sonar-web/src/main/js/app/store/measures/actions.js
new file mode 100644 (file)
index 0000000..7445f7f
--- /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.
+ */
+export const RECEIVE_COMPONENT_MEASURE = 'RECEIVE_COMPONENT_MEASURE';
+
+export const receiveComponentMeasure = (componentKey, metricKey, value) => ({
+  type: RECEIVE_COMPONENT_MEASURE,
+  componentKey,
+  metricKey,
+  value
+});
diff --git a/server/sonar-web/src/main/js/app/store/measures/reducer.js b/server/sonar-web/src/main/js/app/store/measures/reducer.js
new file mode 100644 (file)
index 0000000..5197ebf
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { RECEIVE_COMPONENT_MEASURE } from './actions';
+
+const byMetricKey = (state = {}, action = {}) => {
+  if (action.type === RECEIVE_COMPONENT_MEASURE) {
+    return { ...state, [action.metricKey]: action.value };
+  }
+
+  return state;
+};
+
+const reducer = (state = {}, action = {}) => {
+  if (action.type === RECEIVE_COMPONENT_MEASURE) {
+    const component = state[action.componentKey];
+    return { ...state, [action.componentKey]: byMetricKey(component, action) };
+  }
+
+  return state;
+};
+
+export default reducer;
+
+export const getComponentMeasure = (state, componentKey, metricKey) => {
+  const component = state[componentKey];
+  return component && component[metricKey];
+};
diff --git a/server/sonar-web/src/main/js/app/store/rootReducer.js b/server/sonar-web/src/main/js/app/store/rootReducer.js
new file mode 100644 (file)
index 0000000..de0ad66
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { combineReducers } from 'redux';
+import users, * as fromUsers from './users/reducer';
+import favorites, * as fromFavorites from './favorites/reducer';
+import measures, * as fromMeasures from './measures/reducer';
+
+import issuesActivity, * as fromIssuesActivity from '../../apps/account/home/store/reducer';
+
+export default combineReducers({ users, favorites, issuesActivity, measures });
+
+export const getCurrentUser = state => (
+    fromUsers.getCurrentUser(state.users)
+);
+
+export const getFavorites = state => (
+    fromFavorites.getFavorites(state.favorites)
+);
+
+export const getIssuesActivity = state => (
+    fromIssuesActivity.getIssuesActivity(state.issuesActivity)
+);
+
+export const getComponentMeasure = (state, componentKey, metricKey) => (
+    fromMeasures.getComponentMeasure(state.measures, componentKey, metricKey)
+);
diff --git a/server/sonar-web/src/main/js/app/store/users/actions.js b/server/sonar-web/src/main/js/app/store/users/actions.js
new file mode 100644 (file)
index 0000000..5ed3978
--- /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 { getCurrentUser } from '../../../api/users';
+
+export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER';
+
+export const receiveCurrentUser = user => ({
+  type: RECEIVE_CURRENT_USER,
+  user
+});
+
+export const fetchCurrentUser = () => dispatch => {
+  getCurrentUser().then(user => dispatch(receiveCurrentUser(user)));
+};
diff --git a/server/sonar-web/src/main/js/app/store/users/reducer.js b/server/sonar-web/src/main/js/app/store/users/reducer.js
new file mode 100644 (file)
index 0000000..dec0794
--- /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 { combineReducers } from 'redux';
+import uniq from 'lodash/uniq';
+import { RECEIVE_CURRENT_USER } from './actions';
+
+const usersByLogin = (state = {}, action = {}) => {
+  if (action.type === RECEIVE_CURRENT_USER) {
+    return { ...state, [action.user.login]: action.user };
+  }
+
+  return state;
+};
+
+const userLogins = (state = [], action = {}) => {
+  if (action.type === RECEIVE_CURRENT_USER) {
+    return uniq([...state, action.user.login]);
+  }
+
+  return state;
+};
+
+const currentUser = (state = null, action = {}) => {
+  if (action.type === RECEIVE_CURRENT_USER) {
+    return action.user.login;
+  }
+
+  return state;
+};
+
+export default combineReducers({ usersByLogin, userLogins, currentUser });
+
+export const getCurrentUser = state => (
+    state.currentUser ? state.usersByLogin[state.currentUser] : null
+);
diff --git a/server/sonar-web/src/main/js/app/styles.css b/server/sonar-web/src/main/js/app/styles.css
new file mode 100644 (file)
index 0000000..1fa93c7
--- /dev/null
@@ -0,0 +1,42 @@
+.boxed-group {
+  margin-bottom: 20px;
+  border: 1px solid #e6e6e6;
+  border-radius: 2px;
+  background-color: #fff;
+}
+
+.boxed-group > h2 {
+  line-height: 24px;
+  padding: 15px 20px 0;
+}
+
+.boxed-group hr {
+  height: 0;
+  border-top: 1px solid #efefef;
+  margin: 15px -20px;
+}
+
+.boxed-group-actions {
+  float: right;
+  margin-top: 15px;
+  margin-right: 20px;
+}
+
+.boxed-group-inner {
+  padding: 15px 20px;
+}
+
+.boxed-group-inner:empty {
+  padding-top: 0;
+}
+
+.boxed-group-list {
+  margin-top: -8px;
+  margin-bottom: -8px;
+}
+
+.boxed-group-list > li {
+  margin-left: -20px;
+  margin-right: -20px;
+  padding: 8px 20px;
+}
index b2a1839311cdc314f7c402222df67d57a2eb7f3d..130a53eabf35d27d473cd24b9fc8e228c7effd57 100644 (file)
@@ -1,29 +1,73 @@
+.account-container {
+  width: 600px;
+  margin-left: auto;
+  margin-right: auto;
+}
+
 .account-header {
-  position: fixed;
-  top: 30px;
-  left: 0;
-  right: 0;
-  z-index: 420;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  border-bottom: 1px solid #e6e6e6;
   background-color: #f3f3f3;
 }
 
-.account-nav { }
+.account-nav {
+  float: right;
+  padding-top: 11px;
+}
 
 .account-nav .nav-tabs {
   width: 100%;
+  border-bottom: none;
+}
+
+.account-nav .navbar-nav > li > a {
+  padding-top: 8px;
+  padding-bottom: 8px;
 }
 
 .account-user {
-  padding: 10px;
+  float: left;
 }
 
-.account-nav-avatar {
+.account-user > a {
+  display: block;
   float: left;
+  margin: -10px -15px -10px -10px;
+  padding: 10px 15px 10px 10px;
+  border-bottom: none;
+  border-radius: 3px;
+}
+
+.account-user > a:hover,
+.account-user > a:active {
+  background-color: rgba(0, 0, 0, 0.075);
+}
+
+.account-user h1 {
+  line-height: 60px;
+}
+
+.account-user-avatar {
   margin-right: 20px;
 }
 
-.account-page {
-  padding-top: 102px;
+.account-user-avatar > img {
+  border-radius: 60px;
+}
+
+.account-user-avatar:empty {
+  display: none;
+}
+
+.account-body {
+  padding: 40px 0;
+}
+
+.account-separator {
+  height: 0;
+  margin: 40px 0;
+  border-top: 1px solid #e6e6e6;
 }
 
 .account-bar-chart .bar-chart-bar {
   text-anchor: start;
 }
 
-.account-projects {
-  max-width: 960px;
+.account-projects-list {
+  margin-left: -20px;
+  margin-right: -20px;
+}
+
+.account-projects-list > li {
+  padding: 15px 20px;
 }
 
 .account-projects-list > li + li {
-  margin-top: 10px;
+  border-top: 1px solid #e6e6e6;
 }
 
 .account-project-side {
 .account-project-card {
   position: relative;
   display: block;
-  padding: 10px 15px;
-  border: 1px solid #e6e6e6;
-  border-radius: 3px;
-  background-color: #fff;
 }
 
 .account-project-name {
   color: #777;
   font-size: 12px;
 }
+
+.my-activity-issues {
+  position: relative;
+  display: flex;
+  justify-content: center;
+  margin-bottom: 70px;
+}
+
+.my-activity-issues:after {
+  position: absolute;
+  z-index: 5;
+  top: -15px;
+  left: 50%;
+  width: 1px;
+  height: 100px;
+  background-color: #e6e6e6;
+  transform: rotate(30deg);
+  content: "";
+}
+
+.my-activity-issues > a {
+  display: block;
+  padding: 15px 20px;
+  border: none;
+  border-radius: 2px;
+  color: #444;
+}
+
+.my-activity-issues > a:hover {
+  background-color: #f3f3f3;
+}
+
+.my-activity-recent-issues {
+  margin-right: 50px;
+  text-align: right;
+}
+
+.my-activity-recent-issues .my-activity-issues-note {
+  text-align: left;
+}
+
+.my-activity-all-issues {
+  margin-left: 50px;
+}
+
+.my-activity-issues-number {
+  display: inline-block;
+  vertical-align: middle;
+  line-height: 36px;
+  font-size: 36px;
+  font-weight: 300;
+}
+
+.my-activity-issues-note {
+  display: inline-block;
+  vertical-align: middle;
+  padding-left: 10px;
+  padding-top: 2px;
+  line-height: 16px;
+  font-size: 12px;
+}
+
+.my-activity-projects {
+  width: 360px;
+  margin: 0 auto;
+  padding: 40px 0;
+}
+
+.my-activity-projects-header {
+  line-height: 24px;
+  margin-bottom: 15px;
+  padding: 0 10px;
+}
+
+.my-activity-projects > ul > li + li {
+  border-top: 1px solid #e6e6e6;
+}
+
+.my-activity-projects > ul > li > a {
+  display: block;
+  padding: 15px 10px;
+  border: none;
+}
+
+.my-activity-projects > ul > li > a:hover {
+  background-color: #f3f3f3;
+}
+
+.my-activity-projects .level {
+  width: 60px;
+}
+
+.my-activity-projects .more {
+  margin-top: 30px;
+  text-align: center;
+}
diff --git a/server/sonar-web/src/main/js/apps/account/app.js b/server/sonar-web/src/main/js/apps/account/app.js
deleted file mode 100644 (file)
index e7d86d4..0000000
+++ /dev/null
@@ -1,51 +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 React from 'react';
-import { render } from 'react-dom';
-import { Router, Route, IndexRoute, Redirect, useRouterHistory } from 'react-router';
-import { createHistory } from 'history';
-import AccountApp from './components/AccountApp';
-import Home from './components/Home';
-import NotificationsContainer from './components/NotificationsContainer';
-import Security from './components/Security';
-import Issues from './components/Issues';
-import ProjectsContainer from './projects/ProjectsContainer';
-
-window.sonarqube.appStarted.then(options => {
-  const el = document.querySelector(options.el);
-
-  const history = useRouterHistory(createHistory)({
-    basename: window.baseUrl + '/account'
-  });
-
-  render((
-      <Router history={history}>
-        <Route path="/" component={AccountApp}>
-          <IndexRoute component={Home}/>
-          <Route path="issues" component={Issues}/>
-          <Route path="notifications" component={NotificationsContainer}/>
-          <Route path="security" component={Security}/>
-          <Route path="projects" component={ProjectsContainer}/>
-
-          <Redirect from="/index" to="/"/>
-        </Route>
-      </Router>
-  ), el);
-});
diff --git a/server/sonar-web/src/main/js/apps/account/components/Account.js b/server/sonar-web/src/main/js/apps/account/components/Account.js
new file mode 100644 (file)
index 0000000..35e273a
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { connect } from 'react-redux';
+import Nav from './Nav';
+import UserCard from './UserCard';
+import { getCurrentUser } from '../../../app/store/rootReducer';
+import '../account.css';
+
+class Account extends React.Component {
+  render () {
+    const { currentUser, children } = this.props;
+
+    if (currentUser == null) {
+      return (
+          <div id="account-page">
+            <div className="text-center">
+              <i className="spinner"/>
+            </div>
+          </div>
+      );
+    }
+
+    return (
+        <div id="account-page">
+          <header className="account-header">
+            <div className="account-container clearfix">
+              <UserCard user={currentUser}/>
+              <Nav user={currentUser}/>
+            </div>
+          </header>
+
+          {children}
+        </div>
+    );
+  }
+}
+
+export default connect(
+    state => ({
+      currentUser: getCurrentUser(state)
+    })
+)(Account);
diff --git a/server/sonar-web/src/main/js/apps/account/components/AccountApp.js b/server/sonar-web/src/main/js/apps/account/components/AccountApp.js
deleted file mode 100644 (file)
index 84757df..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 React, { Component, cloneElement } from 'react';
-import Nav from './Nav';
-import { getCurrentUser } from '../../../api/users';
-import { getIssueFilters } from '../../../api/issues';
-import '../account.css';
-
-export default class AccountApp extends Component {
-  state = {
-    loading: true
-  };
-
-  componentDidMount () {
-    this.mounted = true;
-    Promise.all([
-      this.loadUser(),
-      this.loadFavoriteIssueFilters()
-    ]).then(() => this.finishLoading());
-  }
-
-  componentWillUnmount () {
-    this.mounted = false;
-  }
-
-  loadUser () {
-    return getCurrentUser().then(user => {
-      if (this.mounted) {
-        this.setState({ user });
-      }
-    });
-  }
-
-  loadFavoriteIssueFilters () {
-    return getIssueFilters().then(issueFilters => {
-      const favoriteIssueFilters = issueFilters.filter(f => f.favorite);
-      this.setState({ issueFilters: favoriteIssueFilters });
-    });
-  }
-
-  finishLoading () {
-    if (this.mounted) {
-      this.setState({ loading: false });
-    }
-  }
-
-  render () {
-    const { user, issueFilters, loading } = this.state;
-
-    if (loading) {
-      return (
-          <div>
-            <i className="spinner spinner-margin"/>
-          </div>
-      );
-    }
-
-    const { favorites } = window.sonarqube.user;
-    const measureFilters = window.sonarqube.user.favoriteMeasureFilters;
-    const children = cloneElement(this.props.children, {
-      measureFilters,
-      user,
-      favorites,
-      issueFilters
-    });
-
-    return (
-        <div className="account-page">
-          <Nav user={user}/>
-          {children}
-        </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/account/components/FavoriteIssueFilters.js b/server/sonar-web/src/main/js/apps/account/components/FavoriteIssueFilters.js
deleted file mode 100644 (file)
index 87d5840..0000000
+++ /dev/null
@@ -1,61 +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 React from 'react';
-
-import FavoriteIssueFilter from '../../../components/controls/FavoriteIssueFilter';
-import { translate } from '../../../helpers/l10n';
-
-const FavoriteIssueFilters = ({ issueFilters }) => (
-    <section className="huge-spacer-top">
-      <h2 className="spacer-bottom">
-        {translate('my_account.favorite_issue_filters')}
-      </h2>
-
-      {!issueFilters.length && (
-          <p className="note">
-            {translate('my_account.no_favorite_issue_filters')}
-          </p>
-      )}
-
-      <table id="favorite-issue-filters" className="data">
-        <tbody>
-          {issueFilters.map(f => (
-              <tr key={f.name}>
-                <td className="thin">
-                  <FavoriteIssueFilter filter={f} favorite={true}/>
-                </td>
-                <td>
-                  <a href={`${window.baseUrl}/issues/search#id=${f.id}`}>
-                    {f.name}
-                  </a>
-                </td>
-              </tr>
-          ))}
-        </tbody>
-      </table>
-
-      <div className="spacer-top small">
-        <a href={`${window.baseUrl}/issues/manage`}>{translate('see_all')}</a>
-      </div>
-
-    </section>
-);
-
-export default FavoriteIssueFilters;
diff --git a/server/sonar-web/src/main/js/apps/account/components/FavoriteMeasureFilters.js b/server/sonar-web/src/main/js/apps/account/components/FavoriteMeasureFilters.js
deleted file mode 100644 (file)
index cc512ab..0000000
+++ /dev/null
@@ -1,61 +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 React from 'react';
-
-import FavoriteMeasureFilter from '../../../components/controls/FavoriteMeasureFilter';
-import { translate } from '../../../helpers/l10n';
-
-const FavoriteMeasureFilters = ({ measureFilters }) => (
-    <section className="huge-spacer-top">
-      <h2 className="spacer-bottom">
-        {translate('my_account.favorite_measure_filters')}
-      </h2>
-
-      {!measureFilters.length && (
-          <p className="note">
-            {translate('my_account.no_favorite_measure_filters')}
-          </p>
-      )}
-
-      <table id="favorite-measure-filters" className="data">
-        <tbody>
-          {measureFilters.map(f => (
-              <tr key={f.name}>
-                <td className="thin">
-                  <FavoriteMeasureFilter filter={f} favorite={true}/>
-                </td>
-                <td>
-                  <a href={`${window.baseUrl}/measures/filter/${f.id}`}>
-                    {f.name}
-                  </a>
-                </td>
-              </tr>
-          ))}
-        </tbody>
-      </table>
-
-      <div className="spacer-top small">
-        <a href={`${window.baseUrl}/measures/manage`}>{translate('see_all')}</a>
-      </div>
-
-    </section>
-);
-
-export default FavoriteMeasureFilters;
diff --git a/server/sonar-web/src/main/js/apps/account/components/Favorites.js b/server/sonar-web/src/main/js/apps/account/components/Favorites.js
deleted file mode 100644 (file)
index 6bfe92b..0000000
+++ /dev/null
@@ -1,61 +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 React from 'react';
-
-import Favorite from '../../../components/controls/Favorite';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
-import { translate } from '../../../helpers/l10n';
-import { getComponentUrl } from '../../../helpers/urls';
-
-const Favorites = ({ favorites }) => (
-    <section>
-      <h2 className="spacer-bottom">
-        {translate('my_account.favorite_components')}
-      </h2>
-
-      {!favorites.length && (
-          <p className="note">
-            {translate('my_account.no_favorite_components')}
-          </p>
-      )}
-
-      <table id="favorite-components" className="data">
-        <tbody>
-          {favorites.map(f => (
-              <tr key={f.key}>
-                <td className="thin">
-                  <Favorite component={f.key} favorite={true}/>
-                </td>
-                <td>
-                  <a href={getComponentUrl(f.key)} className="link-with-icon">
-                    <QualifierIcon qualifier={f.qualifier}/>
-                    {' '}
-                    <span>{f.name}</span>
-                  </a>
-                </td>
-              </tr>
-          ))}
-        </tbody>
-      </table>
-
-    </section>
-);
-
-export default Favorites;
diff --git a/server/sonar-web/src/main/js/apps/account/components/GlobalNotifications.js b/server/sonar-web/src/main/js/apps/account/components/GlobalNotifications.js
deleted file mode 100644 (file)
index 180f224..0000000
+++ /dev/null
@@ -1,51 +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 React from 'react';
-
-import NotificationsList from './NotificationsList';
-import { translate } from '../../../helpers/l10n';
-
-export default function GlobalNotifications ({ notifications, channels }) {
-  return (
-      <section>
-        <h2 className="spacer-bottom">
-          {translate('my_profile.overall_notifications.title')}
-        </h2>
-
-        <table className="form">
-          <thead>
-            <tr>
-              <th></th>
-              {channels.map(channel => (
-                  <th key={channel} className="text-center">
-                    <h4>{translate('notification.channel', channel)}</h4>
-                  </th>
-              ))}
-            </tr>
-          </thead>
-
-          <NotificationsList
-              notifications={notifications}
-              checkboxId={(d, c) => `global_notifs_${d}_${c}`}
-              checkboxName={(d, c) => `global_notifs[${d}.${c}]`}/>
-        </table>
-      </section>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/account/components/Home.js b/server/sonar-web/src/main/js/apps/account/components/Home.js
deleted file mode 100644 (file)
index 98ae369..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 React from 'react';
-import Helmet from 'react-helmet';
-
-import Favorites from './Favorites';
-import FavoriteIssueFilters from './FavoriteIssueFilters';
-import FavoriteMeasureFilters from './FavoriteMeasureFilters';
-import IssueWidgets from './IssueWidgets';
-import { translate } from '../../../helpers/l10n';
-
-const Home = ({ user, favorites, issueFilters, measureFilters }) => (
-    <div className="page page-limited">
-      <Helmet
-          title={translate('my_account.page')}
-          titleTemplate="SonarQube - %s"/>
-
-      <div className="columns">
-        <div className="column-third">
-          <Favorites favorites={favorites}/>
-          {issueFilters && <FavoriteIssueFilters issueFilters={issueFilters}/>}
-          {measureFilters && <FavoriteMeasureFilters measureFilters={measureFilters}/>}
-        </div>
-
-        <div className="column-third">
-          <IssueWidgets/>
-        </div>
-
-        <div className="column-third">
-          <section>
-            <h2 className="spacer-bottom">{translate('my_profile.groups')}</h2>
-            <ul id="groups">
-              {user.groups.map(group => (
-                  <li
-                      key={group}
-                      className="little-spacer-bottom text-ellipsis"
-                      title={group}>
-                    {group}
-                  </li>
-              ))}
-            </ul>
-          </section>
-
-          <section className="huge-spacer-top">
-            <h2 className="spacer-bottom">{translate('my_profile.scm_accounts')}</h2>
-            <ul id="scm-accounts">
-              <li
-                  className="little-spacer-bottom text-ellipsis"
-                  title={user.login}>
-                {user.login}
-              </li>
-              {user.email && (
-                  <li
-                      className="little-spacer-bottom text-ellipsis"
-                      title={user.email}>
-                    {user.email}
-                  </li>
-              )}
-              {user.scmAccounts.map(scmAccount => (
-                  <li
-                      key={scmAccount}
-                      className="little-spacer-bottom text-ellipsis"
-                      title={scmAccount}>
-                    {scmAccount}
-                  </li>
-              ))}
-            </ul>
-          </section>
-        </div>
-      </div>
-    </div>
-);
-
-export default Home;
diff --git a/server/sonar-web/src/main/js/apps/account/components/IssueWidgets.js b/server/sonar-web/src/main/js/apps/account/components/IssueWidgets.js
deleted file mode 100644 (file)
index 0230acb..0000000
+++ /dev/null
@@ -1,243 +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 moment from 'moment';
-import React, { Component } from 'react';
-
-import SeverityHelper from '../../../components/shared/severity-helper';
-import { BarChart } from '../../../components/charts/bar-chart';
-import { getFacets, getFacet } from '../../../api/issues';
-import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
-
-const BASE_QUERY = { resolved: false, assignees: '__me__' };
-
-function getTotalUrl () {
-  return window.baseUrl + '/account/issues#resolved=false';
-}
-
-function getToFixUrl () {
-  return window.baseUrl + '/account/issues#resolved=false|statuses=CONFIRMED';
-}
-
-function getToReviewUrl () {
-  return window.baseUrl + '/account/issues#resolved=false|statuses=' + encodeURIComponent('OPEN,REOPENED');
-}
-
-function getSeverityUrl (severity) {
-  return window.baseUrl + '/account/issues#resolved=false|severities=' + severity;
-}
-
-function getProjectUrl (project) {
-  return window.baseUrl + '/account/issues#resolved=false|projectUuids=' + project;
-}
-
-function getPeriodUrl (createdAfter, createdBefore) {
-  return window.baseUrl + `/account/issues#resolved=false|createdAfter=${createdAfter}|createdBefore=${createdBefore}`;
-}
-
-export default class IssueWidgets extends Component {
-  state = {
-    loading: true
-  };
-
-  componentDidMount () {
-    this.fetchIssues();
-  }
-
-  fetchIssues () {
-    Promise.all([
-      this.fetchFacets(),
-      this.fetchByDate()
-    ]).then(responses => {
-      const facets = responses[0];
-      const byDate = responses[1];
-
-      this.setState({
-        loading: false,
-        total: facets.total,
-        severities: facets.severities,
-        projects: facets.projects,
-        toFix: facets.toFix,
-        toReview: facets.toReview,
-        byDate
-      });
-    });
-  }
-
-  fetchFacets () {
-    return getFacets(BASE_QUERY, ['statuses', 'severities', 'projectUuids']).then(r => {
-      const severities = _.sortBy(
-          _.findWhere(r.facets, { property: 'severities' }).values,
-          (facet) => window.severityComparator(facet.val)
-      );
-
-      const projects = _.findWhere(r.facets, { property: 'projectUuids' }).values.map(p => {
-        const base = _.findWhere(r.response.components, { uuid: p.val });
-        return Object.assign({}, p, base);
-      });
-
-      const statuses = _.findWhere(r.facets, { property: 'statuses' }).values;
-      const toFix = _.findWhere(statuses, { val: 'CONFIRMED' }).count;
-      const toReview = _.findWhere(statuses, { val: 'OPEN' }).count +
-          _.findWhere(statuses, { val: 'REOPENED' }).count;
-
-      const total = r.response.total;
-
-      return { severities, projects, toFix, toReview, total };
-    });
-  }
-
-  fetchByDate () {
-    return getFacet(Object.assign({ createdInLast: '1w' }, BASE_QUERY), 'createdAt').then(r => r.facet);
-  }
-
-  handleByDateClick ({ value }) {
-    const created = moment(value);
-    const createdAfter = created.format('YYYY-MM-DD');
-    const createdBefore = created.add(1, 'days').format('YYYY-MM-DD');
-    window.location = getPeriodUrl(createdAfter, createdBefore);
-  }
-
-  renderByDate () {
-    const data = this.state.byDate.map((d, x) => {
-      return { x, y: d.count, value: d.val };
-    });
-    const xTicks = this.state.byDate.map(d => moment(d.val).format('dd'));
-    const xValues = this.state.byDate.map(d => d.count);
-
-    return (
-        <section className="abs-width-300 huge-spacer-top account-bar-chart">
-          <h4 className="spacer-bottom">
-            {translate('my_account.issue_widget.leak_last_week')}
-          </h4>
-          <BarChart
-              data={data}
-              xTicks={xTicks}
-              xValues={xValues}
-              barsWidth={20}
-              padding={[25, 10, 25, 10]}
-              height={80}
-              onBarClick={this.handleByDateClick.bind(this)}/>
-        </section>
-    );
-  }
-
-  render () {
-    return (
-        <section>
-          <h2 className="spacer-bottom">{translate('my_account.my_issues')}</h2>
-
-          {this.state.loading && (
-              <i className="spinner"/>
-          )}
-
-          {!this.state.loading && (
-              <section className="abs-width-300">
-                <table className="data zebra">
-                  <tbody>
-                    <tr>
-                      <td>
-                        <strong>{translate('total')}</strong>
-                      </td>
-                      <td className="thin nowrap text-right">
-                        <a href={getTotalUrl()}>
-                          {formatMeasure(this.state.total, 'SHORT_INT')}
-                        </a>
-                      </td>
-                    </tr>
-                    <tr>
-                      <td>
-                        <span className="spacer-left">{translate('my_account.to_review')}</span>
-                      </td>
-                      <td className="thin nowrap text-right">
-                        <a href={getToReviewUrl()}>
-                          {formatMeasure(this.state.toReview, 'SHORT_INT')}
-                        </a>
-                      </td>
-                    </tr>
-                    <tr>
-                      <td>
-                        <span className="spacer-left">{translate('my_account.to_fix')}</span>
-                      </td>
-                      <td className="thin nowrap text-right">
-                        <a href={getToFixUrl()}>
-                          {formatMeasure(this.state.toFix, 'SHORT_INT')}
-                        </a>
-                      </td>
-                    </tr>
-                  </tbody>
-                </table>
-              </section>
-          )}
-
-          {!this.state.loading && this.renderByDate()}
-
-          {!this.state.loading && (
-              <section className="abs-width-300 huge-spacer-top">
-                <h4 className="spacer-bottom">
-                  {translate('my_account.issue_widget.by_severity')}
-                </h4>
-                <table className="data zebra">
-                  <tbody>
-                    {this.state.severities.map(s => (
-                        <tr key={s.val}>
-                          <td>
-                            <SeverityHelper severity={s.val}/>
-                          </td>
-                          <td className="thin nowrap text-right">
-                            <a href={getSeverityUrl(s.val)}>
-                              {formatMeasure(s.count, 'SHORT_INT')}
-                            </a>
-                          </td>
-                        </tr>
-                    ))}
-                  </tbody>
-                </table>
-              </section>
-          )}
-
-          {!this.state.loading && (
-              <section className="abs-width-300 huge-spacer-top">
-                <h4 className="spacer-bottom">
-                  {translate('my_account.issue_widget.by_project')}
-                </h4>
-                <table className="data zebra">
-                  <tbody>
-                    {this.state.projects.map(p => (
-                        <tr key={p.val}>
-                          <td>
-                            {p.name}
-                          </td>
-                          <td className="thin nowrap text-right">
-                            <a href={getProjectUrl(p.val)}>
-                              {formatMeasure(p.count, 'SHORT_INT')}
-                            </a>
-                          </td>
-                        </tr>
-                    ))}
-                  </tbody>
-                </table>
-              </section>
-          )}
-        </section>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/account/components/Issues.js b/server/sonar-web/src/main/js/apps/account/components/Issues.js
deleted file mode 100644 (file)
index 626c810..0000000
+++ /dev/null
@@ -1,51 +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 React, { Component } from 'react';
-import Helmet from 'react-helmet';
-
-import IssuesApp from '../issues-app';
-import { translate } from '../../../helpers/l10n';
-
-export default class Issues extends Component {
-  componentDidMount () {
-    this.issuesApp = IssuesApp;
-    this.issuesApp.start({
-      el: this.refs.container
-    });
-  }
-
-  componentWillUnmount () {
-    this.issuesApp.stop();
-  }
-
-  render () {
-    const title = translate('my_account.page') + ' - ' +
-        translate('issues.page');
-
-    return (
-        <div>
-          <Helmet
-              title={title}
-              titleTemplate="SonarQube - %s"/>
-          <div ref="container"></div>
-        </div>
-    );
-  }
-}
index 59948f363f27bab8959abf90f021f3e03d651edf..d7e4b1215deb48610fcb75bb8a611292d6823036 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import React from 'react';
-import { IndexLink } from 'react-router';
-
-import UserCard from './UserCard';
+import { Link } from 'react-router';
 import { translate } from '../../../helpers/l10n';
 
-const Nav = ({ user }) => (
-    <header className="account-header">
-      <UserCard user={user}/>
-
-      <nav className="account-nav clearfix">
-        <ul className="nav navbar-nav nav-tabs">
-          <li>
-            <IndexLink to="/" activeClassName="active">
-              <i className="icon-home"/>
-            </IndexLink>
-          </li>
-          <li>
-            <a
-                className={window.location.pathname === `${window.baseUrl}/account/issues` && 'active'}
-                href={`${window.baseUrl}/account/issues`}>
-              {translate('issues.page')}
-            </a>
-          </li>
-          <li>
-            <IndexLink to="projects" activeClassName="active">
-              {translate('my_account.projects')}
-            </IndexLink>
-          </li>
-          <li>
-            <IndexLink to="notifications" activeClassName="active">
-              {translate('my_account.notifications')}
-            </IndexLink>
-          </li>
-          <li>
-            <IndexLink to="security" activeClassName="active">
-              {translate('my_account.security')}
-            </IndexLink>
-          </li>
-        </ul>
-      </nav>
-    </header>
+const Nav = () => (
+    <nav className="account-nav clearfix">
+      <ul className="nav navbar-nav nav-tabs">
+        <li>
+          <Link to="/account/profile/" activeClassName="active">
+            {translate('my_account.profile')}
+          </Link>
+        </li>
+        <li>
+          <Link to="/account/security/" activeClassName="active">
+            {translate('my_account.security')}
+          </Link>
+        </li>
+        <li>
+          <Link to="/account/notifications/" activeClassName="active">
+            {translate('my_account.notifications')}
+          </Link>
+        </li>
+        <li>
+          <Link to="/account/projects/" activeClassName="active">
+            {translate('my_account.projects')}
+          </Link>
+        </li>
+      </ul>
+    </nav>
 );
 
 export default Nav;
diff --git a/server/sonar-web/src/main/js/apps/account/components/Notifications.js b/server/sonar-web/src/main/js/apps/account/components/Notifications.js
deleted file mode 100644 (file)
index 04e5fc7..0000000
+++ /dev/null
@@ -1,61 +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 React from 'react';
-
-import GlobalNotifications from './GlobalNotifications';
-import ProjectNotifications from './ProjectNotifications';
-import { translate } from '../../../helpers/l10n';
-
-const Notifications = ({ globalNotifications, projectNotifications, onAddProject, onRemoveProject }) => {
-  const channels = globalNotifications[0].channels.map(c => c.id);
-
-  return (
-      <div className="page page-limited">
-        <p className="big-spacer-bottom">
-          {translate('notification.dispatcher.information')}
-        </p>
-        <form id="notif_form" method="post" action={`${window.baseUrl}/account/update_notifications`}>
-          <div className="columns columns-overflow-visible">
-            <div className="column-half">
-              <GlobalNotifications
-                  notifications={globalNotifications}
-                  channels={channels}/>
-            </div>
-
-            <div className="column-half">
-              <ProjectNotifications
-                  notifications={projectNotifications}
-                  channels={channels}
-                  onAddProject={onAddProject}
-                  onRemoveProject={onRemoveProject}/>
-            </div>
-          </div>
-
-          <p className="big-spacer-top panel panel-vertical bordered-top text-right">
-            <button id="submit-notifications" type="submit">
-              {translate('my_profile.notifications.submit')}
-            </button>
-          </p>
-        </form>
-      </div>
-  );
-};
-
-export default Notifications;
diff --git a/server/sonar-web/src/main/js/apps/account/components/NotificationsContainer.js b/server/sonar-web/src/main/js/apps/account/components/NotificationsContainer.js
deleted file mode 100644 (file)
index 20ba785..0000000
+++ /dev/null
@@ -1,82 +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 React from 'react';
-import Helmet from 'react-helmet';
-import Notifications from './Notifications';
-import { translate } from '../../../helpers/l10n';
-
-export default class NotificationsContainer extends React.Component {
-  state = {
-    globalNotifications: window.sonarqube.notifications.global,
-    projectNotifications: window.sonarqube.notifications.project
-  };
-
-  componentWillMount () {
-    this.handleAddProject = this.handleAddProject.bind(this);
-    this.handleRemoveProject = this.handleRemoveProject.bind(this);
-  }
-
-  handleAddProject (project) {
-    const { projectNotifications } = this.state;
-    const found = projectNotifications
-        .find(notification => notification.project.internalId === project.internalId);
-
-    if (!found) {
-      const newProjectNotification = {
-        project,
-        notifications: window.sonarqube.notifications.projectDispatchers.map(dispatcher => {
-          const channels = window.sonarqube.notifications.channels.map(channel => {
-            return { id: channel, checked: false };
-          });
-          return { dispatcher, channels };
-        })
-      };
-
-      this.setState({
-        projectNotifications: [...projectNotifications, newProjectNotification]
-      });
-    }
-  }
-
-  handleRemoveProject (project) {
-    const projectNotifications = this.state.projectNotifications
-        .filter(notification => notification.project.internalId !== project.internalId);
-    this.setState({ projectNotifications });
-  }
-
-  render () {
-    const title = translate('my_account.page') + ' - ' +
-        translate('my_account.notifications');
-
-    return (
-        <div>
-          <Helmet
-              title={title}
-              titleTemplate="SonarQube - %s"/>
-
-          <Notifications
-              globalNotifications={this.state.globalNotifications}
-              projectNotifications={this.state.projectNotifications}
-              onAddProject={this.handleAddProject}
-              onRemoveProject={this.handleRemoveProject}/>
-        </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/account/components/NotificationsList.js b/server/sonar-web/src/main/js/apps/account/components/NotificationsList.js
deleted file mode 100644 (file)
index a8044b4..0000000
+++ /dev/null
@@ -1,42 +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 React from 'react';
-
-import { translate } from '../../../helpers/l10n';
-
-export default function NotificationsList ({ notifications, checkboxName, checkboxId }) {
-  return (
-      <tbody>
-        {notifications.map(notification => (
-            <tr key={notification.dispatcher}>
-              <td>{translate('notification.dispatcher', notification.dispatcher)}</td>
-              {notification.channels.map(channel => (
-                  <td key={channel.id} className="text-center">
-                    <input defaultChecked={channel.checked}
-                           id={checkboxId(notification.dispatcher, channel.id)}
-                           name={checkboxName(notification.dispatcher, channel.id)}
-                           type="checkbox"/>
-                  </td>
-              ))}
-            </tr>
-        ))}
-      </tbody>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/account/components/ProjectNotification.js b/server/sonar-web/src/main/js/apps/account/components/ProjectNotification.js
deleted file mode 100644 (file)
index 95fb143..0000000
+++ /dev/null
@@ -1,73 +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 classNames from 'classnames';
-import React, { Component } from 'react';
-
-import NotificationsList from './NotificationsList';
-import { translate } from '../../../helpers/l10n';
-
-export default class ProjectNotification extends Component {
-  state = {
-    toDelete: false
-  };
-
-  handleRemoveProject (e) {
-    e.preventDefault();
-    if (this.state.toDelete) {
-      const { data, onRemoveProject } = this.props;
-      onRemoveProject(data.project);
-    } else {
-      this.setState({ toDelete: true });
-    }
-  }
-
-  render () {
-    const { data, channels } = this.props;
-    const buttonClassName = classNames('big-spacer-left', 'button-red', {
-      'active': this.state.toDelete
-    });
-
-    return (
-        <table key={data.project.internalId} className="form big-spacer-bottom">
-          <thead>
-            <tr>
-              <th>
-                <h4 className="display-inline-block">{data.project.name}</h4>
-                <button
-                    onClick={this.handleRemoveProject.bind(this)}
-                    className={buttonClassName}>
-                  {this.state.toDelete ? 'Sure?' : translate('delete')}
-                </button>
-              </th>
-              {channels.map(channel => (
-                  <th key={channel} className="text-center">
-                    <h4>{translate('notification.channel', channel)}</h4>
-                  </th>
-              ))}
-            </tr>
-          </thead>
-          <NotificationsList
-              notifications={data.notifications}
-              checkboxId={(d, c) => `project_notifs_${data.project.internalId}_${d}_${c}`}
-              checkboxName={(d, c) => `project_notifs[${data.project.internalId}][${d}][${c}]`}/>
-        </table>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js b/server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js
deleted file mode 100644 (file)
index acdd691..0000000
+++ /dev/null
@@ -1,82 +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 React from 'react';
-import Select from 'react-select';
-
-import ProjectNotification from './ProjectNotification';
-import { translate } from '../../../helpers/l10n';
-import { getProjectsWithInternalId } from '../../../api/components';
-
-export default function ProjectNotifications ({ notifications, channels, onAddProject, onRemoveProject }) {
-  const loadOptions = (query) => {
-    return getProjectsWithInternalId(query)
-        .then(results => results.map(r => {
-          return {
-            value: r.id,
-            label: r.text
-          };
-        }))
-        .then(options => {
-          return { options };
-        });
-  };
-
-  const handleAddProject = (selected) => {
-    const project = {
-      internalId: selected.value,
-      name: selected.label
-    };
-    onAddProject(project);
-  };
-
-  return (
-      <section>
-        <h2 className="spacer-bottom">
-          {translate('my_profile.per_project_notifications.title')}
-        </h2>
-
-        {!notifications.length && (
-            <div className="note">
-              {translate('my_account.no_project_notifications')}
-            </div>
-        )}
-
-        {notifications.map(p => (
-            <ProjectNotification
-                key={p.project.internalId}
-                data={p}
-                channels={channels}
-                onRemoveProject={onRemoveProject}/>
-        ))}
-
-        <div className="huge-spacer-top panel bg-muted">
-          <span className="text-middle spacer-right">
-            Set notifications for:
-          </span>
-          <Select.Async
-              name="new_project"
-              style={{ width: '150px' }}
-              loadOptions={loadOptions}
-              onChange={handleAddProject}
-              placeholder="Search Project"/>
-        </div>
-      </section>
-  );
-}
index 3c78f8aad2cfda87fa701f05fe8e1abc267929f8..9accad023f5df9f10c5c3579d7e6d5c3ab0aadc8 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import React from 'react';
+import { connect } from 'react-redux';
 import Helmet from 'react-helmet';
 import Password from './Password';
 import Tokens from './Tokens';
 import { translate } from '../../../helpers/l10n';
+import { getCurrentUser } from '../../../app/store/rootReducer';
 
-const Security = ({ user }) => {
-  const title = translate('my_account.page') + ' - ' +
-      translate('my_account.security');
+class Security extends React.Component {
+  render () {
+    const { user } = this.props;
 
-  return (
-      <div className="page page-limited">
-        <Helmet
-            title={title}
-            titleTemplate="SonarQube - %s"/>
+    const title = translate('my_account.page') + ' - ' +
+        translate('my_account.security');
 
-        <div className="columns">
-          <div className="column-half">
-            <Tokens user={user}/>
-          </div>
+    return (
+        <div className="account-body account-container">
+          <Helmet
+              title={title}
+              titleTemplate="SonarQube - %s"/>
+
+          <Tokens user={user}/>
+
+          {user.local && (
+              <hr className="account-separator"/>
+          )}
 
           {user.local && (
-              <div className="column-half">
-                <Password user={user}/>
-              </div>
+              <Password user={user}/>
           )}
         </div>
-      </div>
-  );
-};
+    );
+  }
+}
 
-export default Security;
+export default connect(
+    state => ({ user: getCurrentUser(state) })
+)(Security);
index a62526dc145ad3b4666a9fabf175e3bc31dd5335..c9fc96741737e47cec3110437196b263de4dfdb8 100644 (file)
  */
 import React from 'react';
 import { IndexLink } from 'react-router';
-
-import UserExternalIdentity from './UserExternalIdentity';
 import Avatar from '../../../components/ui/Avatar';
 
-const UserCard = ({ user }) => {
-  return (
-      <section className="account-user clearfix">
-        <div id="avatar" className="account-nav-avatar">
-          <IndexLink to="/" className="link-no-underline">
-            <Avatar email={user.email} size={48}/>
-          </IndexLink>
-        </div>
-        <div>
-          <IndexLink to="/" className="link-no-underline">
-            <h1 id="name" className="display-inline-block">{user.name}</h1>
+export default class UserCard extends React.Component {
+  static propTypes = {
+    user: React.PropTypes.object.isRequired
+  };
+
+  render () {
+    const { user } = this.props;
+
+    return (
+        <div className="account-user">
+          <IndexLink to="/account/">
+            <div id="avatar" className="pull-left account-user-avatar">
+              <Avatar email={user.email} size={60}/>
+            </div>
+            <h1 id="name" className="pull-left">{user.name}</h1>
           </IndexLink>
-          <span id="login" className="note big-spacer-left">{user.login}</span>
-          {!user.local && user.externalProvider !== 'sonarqube' && (
-              <span id="identity-provider" className="big-spacer-left">
-                <UserExternalIdentity user={user}/>
-              </span>
-          )}
         </div>
-        <div id="email" className="little-spacer-top">{user.email}</div>
-      </section>
-  );
-};
-
-export default UserCard;
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/account/components/UserExternalIdentity.js b/server/sonar-web/src/main/js/apps/account/components/UserExternalIdentity.js
deleted file mode 100644 (file)
index c26d041..0000000
+++ /dev/null
@@ -1,88 +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 React from 'react';
-
-import { getIdentityProviders } from '../../../api/users';
-
-export default class UserExternalIdentity extends React.Component {
-  state = {
-    loading: true
-  };
-
-  componentDidMount () {
-    this.mounted = true;
-    this.fetchIdentityProviders();
-  }
-
-  componentDidUpdate (nextProps) {
-    if (nextProps.user !== this.props.user) {
-      this.this.fetchIdentityProviders();
-    }
-  }
-
-  componentWillUnmount () {
-    this.mounted = false;
-  }
-
-  fetchIdentityProviders () {
-    this.setState({ loading: true });
-    getIdentityProviders()
-        .then(r => r.identityProviders)
-        .then(providers => {
-          if (this.mounted) {
-            const identityProvider = providers
-                .find(provider => provider.key === this.props.user.externalProvider);
-            this.setState({ loading: false, identityProvider });
-          }
-        })
-        .catch(() => {
-          if (this.mounted) {
-            this.setState({ loading: false });
-          }
-        });
-  }
-
-  render () {
-    const { user } = this.props;
-    const { loading, identityProvider } = this.state;
-
-    if (loading) {
-      return null;
-    }
-
-    if (!identityProvider) {
-      return (
-          <span className="note">
-            {user.externalProvider}{': '}{user.externalIdentity}
-          </span>
-      );
-    }
-
-    return (
-        <div
-            className="identity-provider"
-            style={{ backgroundColor: identityProvider.backgroundColor }}>
-          <img src={window.baseUrl + identityProvider.iconPath} width="14" height="14"/>
-          {' '}
-          {user.externalIdentity}
-        </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/account/home/components/FavoriteProjects.js b/server/sonar-web/src/main/js/apps/account/home/components/FavoriteProjects.js
new file mode 100644 (file)
index 0000000..c8cf522
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { connect } from 'react-redux';
+import sortBy from 'lodash/sortBy';
+import Favorite from '../../../../components/controls/Favorite';
+import Level from '../../../../components/ui/Level';
+import { TooltipsContainer } from '../../../../components/mixins/tooltips-mixin';
+import { getFavorites, getComponentMeasure } from '../../../../app/store/rootReducer';
+import { getComponentUrl, getProjectsUrl } from '../../../../helpers/urls';
+import { fetchFavoriteProjects } from '../store/actions';
+import { translate } from '../../../../helpers/l10n';
+
+class FavoriteProjects extends React.Component {
+  static propTypes = {
+    favorites: React.PropTypes.array,
+    fetchFavoriteProjects: React.PropTypes.func.isRequired
+  };
+
+  componentDidMount () {
+    this.props.fetchFavoriteProjects();
+  }
+
+  renderList () {
+    const { favorites } = this.props;
+
+    if (!favorites) {
+      return null;
+    }
+
+    if (favorites.length === 0) {
+      return (
+          <div id="no-favorite-projects" className="boxed-group boxed-group-inner markdown text-center">
+            <p className="note">{translate('my_activity.no_favorite_projects')}</p>
+            <p>{translate('my_activity.no_favorite_projects.engagement')}</p>
+          </div>
+      );
+    }
+
+    const sorted = sortBy(favorites, project => project.name.toLowerCase());
+
+    return (
+        <ul id="favorite-projects">
+          {sorted.map(project => (
+              <li key={project.key}>
+                <div className="pull-left" style={{ padding: '15px 15px 15px 10px' }}>
+                  <Favorite favorite={true} component={project.key}/>
+                </div>
+
+                <a href={getComponentUrl(project.key)}>
+                  {project.qualityGate != null && (
+                      <span className="pull-right">
+                        <Level level={project.qualityGate}/>
+                      </span>
+                  )}
+                  <strong>{project.name}</strong>
+                </a>
+              </li>
+          ))}
+        </ul>
+    );
+  }
+
+  renderQualityGateTitle () {
+    const { favorites } = this.props;
+
+    const shouldBeRendered = favorites != null && favorites.some(f => f.qualityGate != null);
+
+    if (!shouldBeRendered) {
+      return null;
+    }
+
+    return (
+        <TooltipsContainer>
+          <div className="pull-right note">
+            {translate('overview.quality_gate')}
+            <i className="little-spacer-left icon-help"
+               title={translate('quality_gates.intro.1')}
+               data-toggle="tooltip"/>
+          </div>
+        </TooltipsContainer>
+    );
+  }
+
+  render () {
+    const { favorites } = this.props;
+
+    return (
+        <div className="my-activity-projects">
+          <div className="my-activity-projects-header">
+            {this.renderQualityGateTitle()}
+            <h2>{translate('my_activity.my_favorite_projects')}</h2>
+          </div>
+
+          {favorites == null && (
+              <div className="text-center">
+                <i className="spinner"/>
+              </div>
+          )}
+
+          {this.renderList()}
+
+          <div className="more">
+            <a className="button" href={getProjectsUrl()}>
+              {translate('my_activity.explore_projects')}
+            </a>
+          </div>
+        </div>
+    );
+  }
+}
+
+const mapStateToProps = state => {
+  const fromState = getFavorites(state);
+  const favorites = fromState == null ? null : fromState
+      .filter(component => component.qualifier === 'TRK')
+      .map(component => ({
+        ...component,
+        qualityGate: getComponentMeasure(state, component.key, 'alert_status')
+      }));
+
+  return { favorites };
+};
+
+export default connect(
+    mapStateToProps,
+    { fetchFavoriteProjects }
+)(FavoriteProjects);
diff --git a/server/sonar-web/src/main/js/apps/account/home/components/IssuesActivity.js b/server/sonar-web/src/main/js/apps/account/home/components/IssuesActivity.js
new file mode 100644 (file)
index 0000000..26200df
--- /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 React from 'react';
+import { connect } from 'react-redux';
+import { fetchIssuesActivity } from '../store/actions';
+import { getIssuesActivity } from '../../../../app/store/rootReducer';
+import { getIssuesUrl } from '../../../../helpers/urls';
+import { translate } from '../../../../helpers/l10n';
+
+class IssuesActivity extends React.Component {
+  componentDidMount () {
+    this.props.fetchIssuesActivity();
+  }
+
+  getColorClass (number) {
+    if (number == null) {
+      return '';
+    }
+    return number > 0 ? 'text-danger' : 'text-success';
+  }
+
+  renderRecentIssues () {
+    const number = this.props.issuesActivity && this.props.issuesActivity.recent;
+    const url = getIssuesUrl({ resolved: 'false', assignees: '__me__', createdInLast: '1w' });
+
+    return (
+        <a className="my-activity-recent-issues" href={url}>
+          <div id="recent-issues" className={'my-activity-issues-number ' + this.getColorClass(number)}>
+            {number != null ? number : ' ' }
+          </div>
+          <div className="my-activity-issues-note">
+            {translate('my_activity.my_issues')}<br/>{translate('my_activity.last_week')}
+          </div>
+        </a>
+    );
+  }
+
+  renderAllIssues () {
+    const number = this.props.issuesActivity && this.props.issuesActivity.all;
+    const url = getIssuesUrl({ resolved: 'false', assignees: '__me__' });
+
+    return (
+        <a className="my-activity-all-issues" href={url}>
+          <div id="all-issues" className={'my-activity-issues-number ' + this.getColorClass(number)}>
+            {number != null ? number : ' ' }
+          </div>
+          <div className="my-activity-issues-note">
+            {translate('my_activity.my_issues')}<br/>{translate('my_activity.all_time')}
+          </div>
+        </a>
+    );
+  }
+
+  render () {
+    return (
+        <div className="my-activity-issues">
+          {this.renderRecentIssues()}
+          {this.renderAllIssues()}
+        </div>
+    );
+  }
+}
+
+export default connect(
+    state => ({ issuesActivity: getIssuesActivity(state) }),
+    { fetchIssuesActivity }
+)(IssuesActivity);
diff --git a/server/sonar-web/src/main/js/apps/account/home/components/MyActivity.js b/server/sonar-web/src/main/js/apps/account/home/components/MyActivity.js
new file mode 100644 (file)
index 0000000..43d13d0
--- /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 IssuesActivity from './IssuesActivity';
+import FavoriteProjects from './FavoriteProjects';
+
+export default class MyActivity extends React.Component {
+  static propTypes = {
+    currentUser: React.PropTypes.object
+  };
+
+  render () {
+    const { currentUser } = this.props;
+
+    return (
+        <div id="my-activity-page">
+
+          {currentUser == null ? (
+
+              <div className="account-body text-center">
+                <i className="spinner"/>
+              </div>
+
+          ) : (
+
+              <div className="account-body">
+                <IssuesActivity/>
+                <FavoriteProjects/>
+              </div>
+
+          )}
+
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/account/home/components/MyActivityContainer.js b/server/sonar-web/src/main/js/apps/account/home/components/MyActivityContainer.js
new file mode 100644 (file)
index 0000000..2e1b986
--- /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 { connect } from 'react-redux';
+import MyActivity from './MyActivity';
+import { getCurrentUser } from '../../../../app/store/rootReducer';
+
+const mapStateToProps = state => ({
+  currentUser: getCurrentUser(state)
+});
+
+export default connect(mapStateToProps)(MyActivity);
diff --git a/server/sonar-web/src/main/js/apps/account/home/store/actions.js b/server/sonar-web/src/main/js/apps/account/home/store/actions.js
new file mode 100644 (file)
index 0000000..3bb645a
--- /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 { getIssuesCount } from '../../../../api/issues';
+import { getFavorites } from '../../../../api/favorites';
+import { receiveFavorites } from '../../../../app/store/favorites/actions';
+import { getMeasures } from '../../../../api/measures';
+import { receiveComponentMeasure } from '../../../../app/store/measures/actions';
+
+export const RECEIVE_ISSUES_ACTIVITY = 'myActivity/RECEIVE_ISSUES_ACTIVITY';
+
+const receiveIssuesActivity = (recent, all) => ({
+  type: RECEIVE_ISSUES_ACTIVITY,
+  recent,
+  all
+});
+
+export const fetchIssuesActivity = () => dispatch => {
+  const query = { resolved: 'false', assignees: '__me__' };
+  Promise.all([
+    getIssuesCount(query),
+    getIssuesCount({ ...query, createdInLast: '1w' })
+  ]).then(responses => dispatch(receiveIssuesActivity(responses[1].issues, responses[0].issues)));
+};
+
+export const fetchFavoriteProjects = () => dispatch => {
+  getFavorites().then(favorites => {
+    dispatch(receiveFavorites(favorites));
+
+    const projects = favorites.filter(component => component.qualifier === 'TRK');
+    Promise.all(projects.map(project => getMeasures(project.key, ['alert_status'])))
+        .then(responses => {
+          responses.forEach((measures, index) => {
+            measures.forEach(measure => {
+              dispatch(receiveComponentMeasure(projects[index].key, measure.metric, measure.value));
+            });
+          });
+        });
+  });
+};
diff --git a/server/sonar-web/src/main/js/apps/account/home/store/reducer.js b/server/sonar-web/src/main/js/apps/account/home/store/reducer.js
new file mode 100644 (file)
index 0000000..3cf1b34
--- /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 { RECEIVE_ISSUES_ACTIVITY } from './actions';
+
+const reducer = (state = null, action = {}) => {
+  if (action.type === RECEIVE_ISSUES_ACTIVITY) {
+    return { all: action.all, recent: action.recent };
+  }
+
+  return state;
+};
+
+export default reducer;
+
+export const getIssuesActivity = state => state;
diff --git a/server/sonar-web/src/main/js/apps/account/issues-app.js b/server/sonar-web/src/main/js/apps/account/issues-app.js
deleted file mode 100644 (file)
index 273782b..0000000
+++ /dev/null
@@ -1,106 +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';
-import Marionette from 'backbone.marionette';
-import State from '../issues/models/state';
-import Layout from '../issues/layout';
-import Issues from '../issues/models/issues';
-import Facets from '../../components/navigator/models/facets';
-import Filters from '../issues/models/filters';
-import Controller from '../issues/controller';
-import Router from '../issues/router';
-import WorkspaceListView from '../issues/workspace-list-view';
-import WorkspaceHeaderView from '../issues/workspace-header-view';
-import FacetsView from './../issues/facets-view';
-
-const App = new Marionette.Application();
-
-const init = function (options) {
-  this.config = options.config;
-  this.state = new State({
-    isContext: true,
-    contextQuery: { assignees: '__me__' }
-  });
-  this.updateContextFacets();
-  this.list = new Issues();
-  this.facets = new Facets();
-  this.filters = new Filters();
-
-  this.layout = new Layout({ app: this });
-  this.layout.$el.appendTo(options.el);
-  this.layout.render();
-  $('#footer').addClass('search-navigator-footer');
-
-  this.controller = new Controller({ app: this });
-
-  this.issuesView = new WorkspaceListView({
-    app: this,
-    collection: this.list
-  });
-  this.layout.workspaceListRegion.show(this.issuesView);
-  this.issuesView.bindScrollEvents();
-
-  this.workspaceHeaderView = new WorkspaceHeaderView({
-    app: this,
-    collection: this.list
-  });
-  this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView);
-
-  this.facetsView = new FacetsView({
-    app: this,
-    collection: this.facets
-  });
-  this.layout.facetsRegion.show(this.facetsView);
-
-  this.controller.fetchFilters().done(function () {
-    key.setScope('list');
-    App.router = new Router({ app: App });
-    Backbone.history.start();
-  });
-};
-
-App.getContextQuery = function () {
-  return { assignees: '__me__' };
-};
-
-App.updateContextFacets = function () {
-  const facets = this.state.get('facets');
-  const allFacets = this.state.get('allFacets');
-  const facetsFromServer = this.state.get('facetsFromServer');
-  return this.state.set({
-    facets,
-    allFacets: _.difference(allFacets, ['assignees']),
-    facetsFromServer: _.difference(facetsFromServer, ['assignees'])
-  });
-};
-
-App.stop = function () {
-  App.layout.destroy();
-  Backbone.history.stop();
-  $('#footer').removeClass('search-navigator-footer');
-};
-
-App.on('start', function (options) {
-  init.call(App, options);
-});
-
-export default App;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js b/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js
new file mode 100644 (file)
index 0000000..180f224
--- /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 React from 'react';
+
+import NotificationsList from './NotificationsList';
+import { translate } from '../../../helpers/l10n';
+
+export default function GlobalNotifications ({ notifications, channels }) {
+  return (
+      <section>
+        <h2 className="spacer-bottom">
+          {translate('my_profile.overall_notifications.title')}
+        </h2>
+
+        <table className="form">
+          <thead>
+            <tr>
+              <th></th>
+              {channels.map(channel => (
+                  <th key={channel} className="text-center">
+                    <h4>{translate('notification.channel', channel)}</h4>
+                  </th>
+              ))}
+            </tr>
+          </thead>
+
+          <NotificationsList
+              notifications={notifications}
+              checkboxId={(d, c) => `global_notifs_${d}_${c}`}
+              checkboxName={(d, c) => `global_notifs[${d}.${c}]`}/>
+        </table>
+      </section>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/Notifications.js b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.js
new file mode 100644 (file)
index 0000000..96eb2b2
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+
+import GlobalNotifications from './GlobalNotifications';
+import ProjectNotifications from './ProjectNotifications';
+import { translate } from '../../../helpers/l10n';
+
+const Notifications = ({ globalNotifications, projectNotifications, onAddProject, onRemoveProject }) => {
+  const channels = globalNotifications[0].channels.map(c => c.id);
+
+  return (
+      <div>
+        <p className="big-spacer-bottom">
+          {translate('notification.dispatcher.information')}
+        </p>
+        <form id="notif_form" method="post" action={`${window.baseUrl}/account/update_notifications`}>
+          <GlobalNotifications
+              notifications={globalNotifications}
+              channels={channels}/>
+
+          <hr className="account-separator"/>
+
+          <ProjectNotifications
+              notifications={projectNotifications}
+              channels={channels}
+              onAddProject={onAddProject}
+              onRemoveProject={onRemoveProject}/>
+
+          <hr className="account-separator"/>
+
+          <div className="text-center">
+            <button id="submit-notifications" type="submit">
+              {translate('my_profile.notifications.submit')}
+            </button>
+          </div>
+        </form>
+      </div>
+  );
+};
+
+export default Notifications;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.js b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.js
new file mode 100644 (file)
index 0000000..47933f2
--- /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 Helmet from 'react-helmet';
+import Notifications from './Notifications';
+import { translate } from '../../../helpers/l10n';
+
+export default class NotificationsContainer extends React.Component {
+  state = {
+    globalNotifications: window.sonarqube.notifications.global,
+    projectNotifications: window.sonarqube.notifications.project
+  };
+
+  componentWillMount () {
+    this.handleAddProject = this.handleAddProject.bind(this);
+    this.handleRemoveProject = this.handleRemoveProject.bind(this);
+  }
+
+  handleAddProject (project) {
+    const { projectNotifications } = this.state;
+    const found = projectNotifications
+        .find(notification => notification.project.internalId === project.internalId);
+
+    if (!found) {
+      const newProjectNotification = {
+        project,
+        notifications: window.sonarqube.notifications.projectDispatchers.map(dispatcher => {
+          const channels = window.sonarqube.notifications.channels.map(channel => {
+            return { id: channel, checked: false };
+          });
+          return { dispatcher, channels };
+        })
+      };
+
+      this.setState({
+        projectNotifications: [...projectNotifications, newProjectNotification]
+      });
+    }
+  }
+
+  handleRemoveProject (project) {
+    const projectNotifications = this.state.projectNotifications
+        .filter(notification => notification.project.internalId !== project.internalId);
+    this.setState({ projectNotifications });
+  }
+
+  render () {
+    const title = translate('my_account.page') + ' - ' +
+        translate('my_account.notifications');
+
+    return (
+        <div className="account-body account-container">
+          <Helmet
+              title={title}
+              titleTemplate="SonarQube - %s"/>
+
+          <Notifications
+              globalNotifications={this.state.globalNotifications}
+              projectNotifications={this.state.projectNotifications}
+              onAddProject={this.handleAddProject}
+              onRemoveProject={this.handleRemoveProject}/>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js
new file mode 100644 (file)
index 0000000..a8044b4
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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 function NotificationsList ({ notifications, checkboxName, checkboxId }) {
+  return (
+      <tbody>
+        {notifications.map(notification => (
+            <tr key={notification.dispatcher}>
+              <td>{translate('notification.dispatcher', notification.dispatcher)}</td>
+              {notification.channels.map(channel => (
+                  <td key={channel.id} className="text-center">
+                    <input defaultChecked={channel.checked}
+                           id={checkboxId(notification.dispatcher, channel.id)}
+                           name={checkboxName(notification.dispatcher, channel.id)}
+                           type="checkbox"/>
+                  </td>
+              ))}
+            </tr>
+        ))}
+      </tbody>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotification.js b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotification.js
new file mode 100644 (file)
index 0000000..95fb143
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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 classNames from 'classnames';
+import React, { Component } from 'react';
+
+import NotificationsList from './NotificationsList';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProjectNotification extends Component {
+  state = {
+    toDelete: false
+  };
+
+  handleRemoveProject (e) {
+    e.preventDefault();
+    if (this.state.toDelete) {
+      const { data, onRemoveProject } = this.props;
+      onRemoveProject(data.project);
+    } else {
+      this.setState({ toDelete: true });
+    }
+  }
+
+  render () {
+    const { data, channels } = this.props;
+    const buttonClassName = classNames('big-spacer-left', 'button-red', {
+      'active': this.state.toDelete
+    });
+
+    return (
+        <table key={data.project.internalId} className="form big-spacer-bottom">
+          <thead>
+            <tr>
+              <th>
+                <h4 className="display-inline-block">{data.project.name}</h4>
+                <button
+                    onClick={this.handleRemoveProject.bind(this)}
+                    className={buttonClassName}>
+                  {this.state.toDelete ? 'Sure?' : translate('delete')}
+                </button>
+              </th>
+              {channels.map(channel => (
+                  <th key={channel} className="text-center">
+                    <h4>{translate('notification.channel', channel)}</h4>
+                  </th>
+              ))}
+            </tr>
+          </thead>
+          <NotificationsList
+              notifications={data.notifications}
+              checkboxId={(d, c) => `project_notifs_${data.project.internalId}_${d}_${c}`}
+              checkboxName={(d, c) => `project_notifs[${data.project.internalId}][${d}][${c}]`}/>
+        </table>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js
new file mode 100644 (file)
index 0000000..0f51df5
--- /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 Select from 'react-select';
+
+import ProjectNotification from './ProjectNotification';
+import { translate } from '../../../helpers/l10n';
+import { getProjectsWithInternalId } from '../../../api/components';
+
+export default function ProjectNotifications ({ notifications, channels, onAddProject, onRemoveProject }) {
+  const loadOptions = (query) => {
+    return getProjectsWithInternalId(query)
+        .then(results => results.map(r => {
+          return {
+            value: r.id,
+            label: r.text
+          };
+        }))
+        .then(options => {
+          return { options };
+        });
+  };
+
+  const handleAddProject = (selected) => {
+    const project = {
+      internalId: selected.value,
+      name: selected.label
+    };
+    onAddProject(project);
+  };
+
+  return (
+      <section>
+        <h2 className="spacer-bottom">
+          {translate('my_profile.per_project_notifications.title')}
+        </h2>
+
+        {!notifications.length && (
+            <div className="note">
+              {translate('my_account.no_project_notifications')}
+            </div>
+        )}
+
+        {notifications.map(p => (
+            <ProjectNotification
+                key={p.project.internalId}
+                data={p}
+                channels={channels}
+                onRemoveProject={onRemoveProject}/>
+        ))}
+
+        <div className="spacer-top panel bg-muted">
+          <span className="text-middle spacer-right">
+            Set notifications for:
+          </span>
+          <Select.Async
+              name="new_project"
+              style={{ width: '150px' }}
+              loadOptions={loadOptions}
+              onChange={handleAddProject}
+              placeholder="Search Project"/>
+        </div>
+      </section>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/account/profile/Profile.js b/server/sonar-web/src/main/js/apps/account/profile/Profile.js
new file mode 100644 (file)
index 0000000..776b328
--- /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 React from 'react';
+import { connect } from 'react-redux';
+import UserExternalIdentity from './UserExternalIdentity';
+import UserGroups from './UserGroups';
+import UserScmAccounts from './UserScmAccounts';
+import { getCurrentUser } from '../../../app/store/rootReducer';
+
+class Profile extends React.Component {
+  render () {
+    const { user } = this.props;
+
+    return (
+        <div className="account-body account-container">
+          <div className="spacer-bottom">
+            Login: <strong id="login">{user.login}</strong>
+          </div>
+
+          {!user.local && user.externalProvider !== 'sonarqube' && (
+              <div id="identity-provider" className="spacer-bottom">
+                <UserExternalIdentity user={user}/>
+              </div>
+          )}
+
+          {!!user.email && (
+              <div className="spacer-bottom">
+                Email: <strong id="email">{user.email}</strong>
+              </div>
+          )}
+
+          <hr className="account-separator"/>
+
+          <UserGroups groups={user.groups}/>
+
+          <hr className="account-separator"/>
+
+          <UserScmAccounts user={user} scmAccounts={user.scmAccounts}/>
+        </div>
+    );
+  }
+}
+
+export default connect(
+    state => ({ user: getCurrentUser(state) })
+)(Profile);
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js b/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js
new file mode 100644 (file)
index 0000000..5fa9024
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * 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 { getIdentityProviders } from '../../../api/users';
+
+export default class UserExternalIdentity extends React.Component {
+  state = {
+    loading: true
+  };
+
+  componentDidMount () {
+    this.mounted = true;
+    this.fetchIdentityProviders();
+  }
+
+  componentDidUpdate (nextProps) {
+    if (nextProps.user !== this.props.user) {
+      this.this.fetchIdentityProviders();
+    }
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  fetchIdentityProviders () {
+    this.setState({ loading: true });
+    getIdentityProviders()
+        .then(r => r.identityProviders)
+        .then(providers => {
+          if (this.mounted) {
+            const identityProvider = providers
+                .find(provider => provider.key === this.props.user.externalProvider);
+            this.setState({ loading: false, identityProvider });
+          }
+        })
+        .catch(() => {
+          if (this.mounted) {
+            this.setState({ loading: false });
+          }
+        });
+  }
+
+  render () {
+    const { user } = this.props;
+    const { loading, identityProvider } = this.state;
+
+    if (loading) {
+      return null;
+    }
+
+    if (!identityProvider) {
+      return (
+          <div>
+            {user.externalProvider}{': '}{user.externalIdentity}
+          </div>
+      );
+    }
+
+    return (
+        <div className="identity-provider"
+             style={{ backgroundColor: identityProvider.backgroundColor }}>
+          <img src={window.baseUrl + identityProvider.iconPath} width="14" height="14"/>
+          {' '}
+          {user.externalIdentity}
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserGroups.js b/server/sonar-web/src/main/js/apps/account/profile/UserGroups.js
new file mode 100644 (file)
index 0000000..2e17ca0
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default class UserGroups extends React.Component {
+  static propTypes = {
+    groups: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
+  };
+
+  render () {
+    const { groups } = this.props;
+
+    return (
+        <div>
+          <h2 className="spacer-bottom">{translate('my_profile.groups')}</h2>
+          <ul id="groups">
+            {groups.map(group => (
+                <li key={group} className="little-spacer-bottom" title={group}>
+                  {group}
+                </li>
+            ))}
+          </ul>
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserScmAccounts.js b/server/sonar-web/src/main/js/apps/account/profile/UserScmAccounts.js
new file mode 100644 (file)
index 0000000..2c25e40
--- /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 React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default class UserScmAccounts extends React.Component {
+  static propTypes = {
+    user: React.PropTypes.object.isRequired,
+    scmAccounts: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
+  };
+
+  render () {
+    const { user, scmAccounts } = this.props;
+
+    return (
+        <div>
+          <h2 className="spacer-bottom">{translate('my_profile.scm_accounts')}</h2>
+          <ul id="scm-accounts">
+            <li className="little-spacer-bottom text-ellipsis" title={user.login}>
+              {user.login}
+            </li>
+
+            {user.email && (
+                <li className="little-spacer-bottom text-ellipsis" title={user.email}>
+                  {user.email}
+                </li>
+            )}
+
+            {scmAccounts.map(scmAccount => (
+                <li key={scmAccount} className="little-spacer-bottom" title={scmAccount}>
+                  {scmAccount}
+                </li>
+            ))}
+          </ul>
+        </div>
+    );
+  }
+}
index 9f0d367c2898febab7f553752169011ea23442d8..30c56708215eb6aa5fdc66bdf15103f5cece16c8 100644 (file)
@@ -19,7 +19,6 @@
  */
 import React from 'react';
 import ProjectCard from './ProjectCard';
-import ProjectsSearch from './ProjectsSearch';
 import ListFooter from '../../../components/controls/ListFooter';
 import { projectsListType } from './propTypes';
 import { translate } from '../../../helpers/l10n';
@@ -37,23 +36,15 @@ export default class Projects extends React.Component {
     const { projects } = this.props;
 
     return (
-        <div className="page page-limited account-projects">
-          <header className="page-header">
-            <h1 className="page-title">
-              My Projects
-            </h1>
-            <div className="pull-right">
-              <ProjectsSearch onSearch={this.props.search}/>
-            </div>
-            <div className="page-description">
-              {translate('my_account.projects.description')}
-            </div>
-          </header>
-
-          {projects.length === 0 && (
+        <div id="account-projects">
+          {projects.length === 0 ? (
               <div className="js-no-results">
                 {translate('my_account.projects.no_results')}
               </div>
+          ) : (
+              <p>
+                {translate('my_account.projects.description')}
+              </p>
           )}
 
           {projects.length > 0 && (
index 7beadce026bb331e2b108a66d0e4258563e804d5..e7b5cf5faf90e62814a11763beb4436cc768f276 100644 (file)
@@ -33,7 +33,6 @@ export default class ProjectsContainer extends React.Component {
   componentWillMount () {
     this.loadMore = this.loadMore.bind(this);
     this.search = this.search.bind(this);
-    document.querySelector('html').classList.add('dashboard-page');
   }
 
   componentDidMount () {
@@ -43,7 +42,6 @@ export default class ProjectsContainer extends React.Component {
 
   componentWillUnmount () {
     this.mounted = false;
-    document.querySelector('html').classList.remove('dashboard-page');
   }
 
   loadProjects (page = this.state.page, query = this.state.query) {
@@ -89,7 +87,7 @@ export default class ProjectsContainer extends React.Component {
         translate('my_account.projects');
 
     return (
-        <div>
+        <div className="account-body account-container">
           <Helmet
               title={title}
               titleTemplate="SonarQube - %s"/>
diff --git a/server/sonar-web/src/main/js/apps/account/projects/ProjectsSearch.js b/server/sonar-web/src/main/js/apps/account/projects/ProjectsSearch.js
deleted file mode 100644 (file)
index b0250ec..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 React from 'react';
-import debounce from 'lodash/debounce';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-export default class ProjectsSearch extends React.Component {
-  static propTypes = {
-    onSearch: React.PropTypes.func.isRequired
-  };
-
-  componentWillMount () {
-    this.handleChange = this.handleChange.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-    this.onSearch = debounce(this.props.onSearch, 250);
-  }
-
-  handleChange () {
-    const { value } = this.refs.input;
-    if (value.length > 2 || value.length === 0) {
-      this.onSearch(value);
-    }
-  }
-
-  handleSubmit (e) {
-    e.preventDefault();
-    this.handleChange();
-  }
-
-  render () {
-    return (
-        <div>
-          <form onSubmit={this.handleSubmit}>
-            <input
-                ref="input"
-                type="search"
-                className="input-large"
-                placeholder={translate('search_verb')}
-                onChange={this.handleChange}/>
-            <div className="note little-spacer-top text-right">
-              {translateWithParameters(
-                  'my_account.projects.x_characters_min', 3)}
-            </div>
-          </form>
-        </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/account/routes.js b/server/sonar-web/src/main/js/apps/account/routes.js
new file mode 100644 (file)
index 0000000..e16ea77
--- /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 { Route, IndexRoute } from 'react-router';
+import MyActivityContainer from './home/components/MyActivityContainer';
+import Account from './components/Account';
+import ProjectsContainer from './projects/ProjectsContainer';
+import NotificationsContainer from './notifications/NotificationsContainer';
+import Security from './components/Security';
+import Profile from './profile/Profile';
+
+export default (
+    <Route path="account" component={Account}>
+      <IndexRoute component={MyActivityContainer}/>
+      <Route path="profile" component={Profile}/>
+      <Route path="security" component={Security}/>
+      <Route path="notifications" component={NotificationsContainer}/>
+      <Route path="projects" component={ProjectsContainer}/>
+    </Route>
+);
index 53c0d18271cea8e07aac4a4ed03aa566216a8527..0000cfea8b3abd0e6976688da3e9b7c72644dcae 100644 (file)
   </table>
 {{/notNull}}
 
-<h4 class="big-spacer-top spacer-bottom">Generate Tokens</h4>
-
 {{#each errors}}
   <div class="alert alert-danger">{{msg}}</div>
 {{/each}}
 
-<form class="js-generate-token-form">
+<form class="js-generate-token-form spacer-top panel bg-muted">
+  <label>Generate New Token:</label>
   <input type="text" required maxlength="100" placeholder="Enter Token Name">
   <button>Generate</button>
 </form>
index 97e002f0d5843f1aec989a76b724f2e4ebdb88a0..74a48d84334d38cf8fc809f67f56c3981141c1b6 100644 (file)
@@ -205,5 +205,5 @@ export function requestDelete (url, data) {
  * @returns {Promise}
  */
 export function delay (response) {
-  return new Promise(resolve => setTimeout(() => resolve(response), 500));
+  return new Promise(resolve => setTimeout(() => resolve(response), 1200));
 }
index 561413c59d9772adb13aaf9c99195b57e18c5244..35307e33a540c1b45c1acc389153ec1ee4922bad 100644 (file)
@@ -26,6 +26,18 @@ export function getComponentUrl (componentKey) {
   return window.baseUrl + '/dashboard?id=' + encodeURIComponent(componentKey);
 }
 
+/**
+ * Generate URL for a global issues page
+ * @param {object} query
+ * @returns {string}
+ */
+export function getIssuesUrl (query) {
+  const serializedQuery = Object.keys(query).map(criterion => (
+      `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}`
+  )).join('|');
+  return window.baseUrl + '/issues/search#' + serializedQuery;
+}
+
 /**
  * Generate URL for a component's issues page
  * @param {string} componentKey
@@ -101,3 +113,7 @@ export function getDeprecatedActiveRulesUrl (query = {}) {
   const baseQuery = { activation: 'true', statuses: 'DEPRECATED' };
   return getRulesUrl({ ...query, ...baseQuery });
 }
+
+export const getProjectsUrl = () => {
+  return window.baseUrl + '/measures/search?qualifiers[]=TRK';
+};
index 8e8fee2d57f4002df5311881e63f5ceafce2cb9d..dca6c9cde7a16ddbd7b169273ffc21b261af7a28 100644 (file)
@@ -60,7 +60,7 @@ class AccountController < ApplicationController
       end
     end
 
-    redirect_to "#{ApplicationController.root_context}/account/notifications"
+    redirect_to "#{ApplicationController.root_context}/account/notifications/"
   end
 
   private
index bcad3768198be9ef058bac71b68e3ed87670eed7..2a9a07621934a0e156eb15cd6b065d17baa0ff6e 100644 (file)
@@ -1,25 +1,5 @@
 <% content_for :extra_script do %>
   <script>
-    window.sonarqube.user = {
-      favorites: [
-        <% current_user.favourites.each_with_index do |f, index| %>
-        {
-          id: '<%= escape_javascript f.uuid -%>',
-          key: '<%= escape_javascript f.key -%>',
-          name: '<%= escape_javascript f.name -%>',
-          qualifier: '<%= escape_javascript f.qualifier -%>'
-        },
-        <% end %>
-      ],
-      favoriteMeasureFilters: [
-        <% current_user.favourited_measure_filters.each do |filter| %>
-        {
-          id: <%= filter.id -%>,
-          name: '<%= escape_javascript filter.name -%>'
-        },
-        <% end %>
-      ]
-    };
     window.sonarqube.notifications = {
       channels: [
         <% for channel in @channels -%>
@@ -92,5 +72,5 @@
       ]
     };
   </script>
-  <script src="<%= ApplicationController.root_context -%>/js/bundles/account.js?v=<%= sonar_version -%>"></script>
+  <script src="<%= ApplicationController.root_context -%>/js/bundles/app.js?v=<%= sonar_version -%>"></script>
 <% end %>
index a2f80f1c0ba798af1417efcee6a435b384edd974..86acc414ecb5a6c0e96cdd030b08f8e94359c5ae 100644 (file)
@@ -596,7 +596,6 @@ more_actions=More Actions
 move_left=Move left
 move_right=Move right
 my_account.favorite_components=Favorite Components
-my_account.favorite_issue_filters=Favorite Issue Filters
 my_account.favorite_measure_filters=Favorite Measure Filters
 my_account.issue_widget.by_project=My Issues by Project
 my_account.issue_widget.by_severity=My Issues by Severity
@@ -611,7 +610,6 @@ my_account.page=My Account
 my_account.security=Security
 my_account.tokens_description=If you want to enforce security by not providing credentials of a real SonarQube user to run your code scan or to invoke web services, you can provide a User Token as a replacement of the user login. This will increase the security of your installation by not letting your analysis user's password going through your network.
 my_account.to_fix=To Fix
-my_account.to_review=To Review
 my_profile.add_project=Add project
 my_profile.email=Email
 my_profile.favorites.title=Favorites
@@ -3134,6 +3132,7 @@ my_account.favorite_measure_filters=Favorite Measure Filters
 my_account.no_favorite_measure_filters=You do not have favorite measure filters yet.
 my_account.notifications=Notifications
 my_account.no_project_notifications=You have not set project notifications yet.
+my_account.profile=Profile
 my_account.security=Security
 my_account.tokens_description=If you want to enforce security by not providing credentials of a real SonarQube user to run your code scan or to invoke web services, you can provide a User Token as a replacement of the user login. This will increase the security of your installation by not letting your analysis user's password going through your network.
 my_account.my_issues=My Issues
@@ -4071,3 +4070,17 @@ component_measures.legend.color_x=Color: {0}
 component_measures.legend.size_x=Size: {0}
 component_measures.x_of_y={0} of {1}
 component_measures.no_history=There is no historical data.
+
+
+#------------------------------------------------------------------------------
+#
+# MY ACTIVITY
+#
+#------------------------------------------------------------------------------
+my_activity.my_issues=My Issues
+my_activity.last_week=Last Week
+my_activity.all_time=All Time
+my_activity.my_favorite_projects=My Favorite Projects
+my_activity.no_favorite_projects=You don't have any favorite projects yet.
+my_activity.no_favorite_projects.engagement=Discover and favorite projects you are interested in to have a quick access to them.
+my_activity.explore_projects=Explore Projects