aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-09-23 17:42:18 +0200
committerStas Vilchik <vilchiks@gmail.com>2016-09-30 17:23:37 +0200
commit40bb40f37b47e8d5825ef349ec68c70e6b1d06c8 (patch)
treeb572767548b0f5d44ad0cd026cee8ae8d6729ceb
parente9756989048439cb1c4b8eabf5fafb868b93f67e (diff)
downloadsonarqube-40bb40f37b47e8d5825ef349ec68c70e6b1d06c8.tar.gz
sonarqube-40bb40f37b47e8d5825ef349ec68c70e6b1d06c8.zip
SONAR-8170 SONAR-8171 Reorganize My Account space
-rw-r--r--it/it-tests/src/test/java/it/user/MyAccountPageTest.java31
-rw-r--r--it/it-tests/src/test/java/pageobjects/MyActivityPage.java48
-rw-r--r--it/it-tests/src/test/java/pageobjects/Navigation.java4
-rw-r--r--it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html2
-rw-r--r--it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html2
-rw-r--r--it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_issues.html55
-rw-r--r--it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html4
-rw-r--r--it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html2
-rw-r--r--it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_user_details.html17
-rw-r--r--server/sonar-web/config/webpack/webpack.config.base.js2
-rw-r--r--server/sonar-web/src/main/js/api/favorites.js4
-rw-r--r--server/sonar-web/src/main/js/app/components/App.js41
-rw-r--r--server/sonar-web/src/main/js/app/index.js (renamed from server/sonar-web/src/main/js/apps/account/app.js)36
-rw-r--r--server/sonar-web/src/main/js/app/store/favorites/actions.js25
-rw-r--r--server/sonar-web/src/main/js/app/store/favorites/reducer.js47
-rw-r--r--server/sonar-web/src/main/js/app/store/measures/actions.js27
-rw-r--r--server/sonar-web/src/main/js/app/store/measures/reducer.js44
-rw-r--r--server/sonar-web/src/main/js/app/store/rootReducer.js43
-rw-r--r--server/sonar-web/src/main/js/app/store/users/actions.js31
-rw-r--r--server/sonar-web/src/main/js/app/store/users/reducer.js52
-rw-r--r--server/sonar-web/src/main/js/app/styles.css42
-rw-r--r--server/sonar-web/src/main/js/apps/account/account.css175
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Account.js60
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/AccountApp.js91
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/FavoriteIssueFilters.js61
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/FavoriteMeasureFilters.js61
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Favorites.js61
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Home.js91
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/IssueWidgets.js243
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Nav.js65
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Security.js44
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/UserCard.js43
-rw-r--r--server/sonar-web/src/main/js/apps/account/home/components/FavoriteProjects.js145
-rw-r--r--server/sonar-web/src/main/js/apps/account/home/components/IssuesActivity.js84
-rw-r--r--server/sonar-web/src/main/js/apps/account/home/components/MyActivity.js53
-rw-r--r--server/sonar-web/src/main/js/apps/account/home/components/MyActivityContainer.js28
-rw-r--r--server/sonar-web/src/main/js/apps/account/home/store/actions.js56
-rw-r--r--server/sonar-web/src/main/js/apps/account/home/store/reducer.js32
-rw-r--r--server/sonar-web/src/main/js/apps/account/issues-app.js106
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js (renamed from server/sonar-web/src/main/js/apps/account/components/GlobalNotifications.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/Notifications.js (renamed from server/sonar-web/src/main/js/apps/account/components/Notifications.js)34
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.js (renamed from server/sonar-web/src/main/js/apps/account/components/NotificationsContainer.js)2
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js (renamed from server/sonar-web/src/main/js/apps/account/components/NotificationsList.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/ProjectNotification.js (renamed from server/sonar-web/src/main/js/apps/account/components/ProjectNotification.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js (renamed from server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js)2
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/Profile.js63
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js (renamed from server/sonar-web/src/main/js/apps/account/components/UserExternalIdentity.js)9
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserGroups.js (renamed from server/sonar-web/src/main/js/apps/account/components/Issues.js)35
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserScmAccounts.js55
-rw-r--r--server/sonar-web/src/main/js/apps/account/projects/Projects.js21
-rw-r--r--server/sonar-web/src/main/js/apps/account/projects/ProjectsContainer.js4
-rw-r--r--server/sonar-web/src/main/js/apps/account/projects/ProjectsSearch.js65
-rw-r--r--server/sonar-web/src/main/js/apps/account/routes.js37
-rw-r--r--server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs5
-rw-r--r--server/sonar-web/src/main/js/helpers/request.js2
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.js16
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb2
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/account/index.html.erb22
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties17
59 files changed, 1373 insertions, 1076 deletions
diff --git a/it/it-tests/src/test/java/it/user/MyAccountPageTest.java b/it/it-tests/src/test/java/it/user/MyAccountPageTest.java
index 8f7b479d8ee..63c11f9549d 100644
--- a/it/it-tests/src/test/java/it/user/MyAccountPageTest.java
+++ b/it/it-tests/src/test/java/it/user/MyAccountPageTest.java
@@ -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();
@@ -58,6 +67,20 @@ public class MyAccountPageTest {
}
@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",
"/user/MyAccountPageTest/should_display_user_details.html"
@@ -74,14 +97,6 @@ public class MyAccountPageTest {
}
@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
Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("should_display_projects",
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
index 00000000000..8bcb376a85b
--- /dev/null
+++ b/it/it-tests/src/test/java/pageobjects/MyActivityPage.java
@@ -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);
+ }
+}
diff --git a/it/it-tests/src/test/java/pageobjects/Navigation.java b/it/it-tests/src/test/java/pageobjects/Navigation.java
index 071df520c6e..8342474a417 100644
--- a/it/it-tests/src/test/java/pageobjects/Navigation.java
+++ b/it/it-tests/src/test/java/pageobjects/Navigation.java
@@ -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);
}
diff --git a/it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html b/it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html
index 60751a0adef..f073c8718fb 100644
--- a/it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html
+++ b/it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details.html
@@ -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/ExternalAuthenticationTest/external-user-details2.html b/it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html
index a245110f624..ca5bd80f9c9 100644
--- a/it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html
+++ b/it/it-tests/src/test/resources/user/ExternalAuthenticationTest/external-user-details2.html
@@ -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
index ab2e1c438f3..00000000000
--- a/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_issues.html
+++ /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>
diff --git a/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html b/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html
index 4060bf595a6..31d0dbb7197 100644
--- a/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html
+++ b/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html
@@ -41,12 +41,12 @@
</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>
diff --git a/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html b/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html
index 5f580876c51..b4e9b0f9e1e 100644
--- a/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html
+++ b/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html
@@ -41,7 +41,7 @@
</tr>
<tr>
<td>open</td>
- <td>/account/projects</td>
+ <td>/account/projects/</td>
<td></td>
</tr>
<tr>
diff --git a/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_user_details.html b/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_user_details.html
index 1036819238c..71efda227c6 100644
--- a/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_user_details.html
+++ b/it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_user_details.html
@@ -41,7 +41,7 @@
</tr>
<tr>
<td>open</td>
- <td>/sonar/account/</td>
+ <td>/sonar/account/profile/</td>
<td></td>
</tr>
<tr>
@@ -99,21 +99,6 @@
<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>
diff --git a/server/sonar-web/config/webpack/webpack.config.base.js b/server/sonar-web/config/webpack/webpack.config.base.js
index e5ae5336916..245c6ba7ca4 100644
--- a/server/sonar-web/config/webpack/webpack.config.base.js
+++ b/server/sonar-web/config/webpack/webpack.config.base.js
@@ -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',
diff --git a/server/sonar-web/src/main/js/api/favorites.js b/server/sonar-web/src/main/js/api/favorites.js
index eff40c309fd..d3b0e2eed44 100644
--- a/server/sonar-web/src/main/js/api/favorites.js
+++ b/server/sonar-web/src/main/js/api/favorites.js
@@ -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
index 00000000000..b4fac8bf062
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/App.js
@@ -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/apps/account/app.js b/server/sonar-web/src/main/js/app/index.js
index e7d86d442ab..35a9525f137 100644
--- a/server/sonar-web/src/main/js/apps/account/app.js
+++ b/server/sonar-web/src/main/js/app/index.js
@@ -19,33 +19,31 @@
*/
import React from 'react';
import { render } from 'react-dom';
-import { Router, Route, IndexRoute, Redirect, useRouterHistory } from 'react-router';
+import { Router, Route, 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';
+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 + '/account'
+ basename: window.baseUrl + '/'
});
- 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}/>
+ const store = configureStore(rootReducer);
- <Redirect from="/index" to="/"/>
- </Route>
- </Router>
+ 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
index 00000000000..2fc7db49085
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/store/favorites/actions.js
@@ -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
index 00000000000..29d19fece37
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/store/favorites/reducer.js
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { 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
index 00000000000..7445f7fae44
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/store/measures/actions.js
@@ -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
index 00000000000..5197ebf8dd5
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/store/measures/reducer.js
@@ -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
index 00000000000..de0ad66b3f9
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/store/rootReducer.js
@@ -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
index 00000000000..5ed397862c7
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/store/users/actions.js
@@ -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
index 00000000000..dec079465f2
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/store/users/reducer.js
@@ -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
index 00000000000..1fa93c779ad
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/styles.css
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/apps/account/account.css b/server/sonar-web/src/main/js/apps/account/account.css
index b2a1839311c..130a53eabf3 100644
--- a/server/sonar-web/src/main/js/apps/account/account.css
+++ b/server/sonar-web/src/main/js/apps/account/account.css
@@ -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 {
@@ -44,12 +88,17 @@
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 {
@@ -67,10 +116,6 @@
.account-project-card {
position: relative;
display: block;
- padding: 10px 15px;
- border: 1px solid #e6e6e6;
- border-radius: 3px;
- background-color: #fff;
}
.account-project-name {
@@ -113,3 +158,99 @@
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/components/Account.js b/server/sonar-web/src/main/js/apps/account/components/Account.js
new file mode 100644
index 00000000000..35e273a1d32
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/components/Account.js
@@ -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
index 84757dfd6fc..00000000000
--- a/server/sonar-web/src/main/js/apps/account/components/AccountApp.js
+++ /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
index 87d5840c7e3..00000000000
--- a/server/sonar-web/src/main/js/apps/account/components/FavoriteIssueFilters.js
+++ /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
index cc512ab1796..00000000000
--- a/server/sonar-web/src/main/js/apps/account/components/FavoriteMeasureFilters.js
+++ /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
index 6bfe92bd928..00000000000
--- a/server/sonar-web/src/main/js/apps/account/components/Favorites.js
+++ /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/Home.js b/server/sonar-web/src/main/js/apps/account/components/Home.js
deleted file mode 100644
index 98ae36986f9..00000000000
--- a/server/sonar-web/src/main/js/apps/account/components/Home.js
+++ /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
index 0230acb750b..00000000000
--- a/server/sonar-web/src/main/js/apps/account/components/IssueWidgets.js
+++ /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/Nav.js b/server/sonar-web/src/main/js/apps/account/components/Nav.js
index 59948f363f2..d7e4b1215de 100644
--- a/server/sonar-web/src/main/js/apps/account/components/Nav.js
+++ b/server/sonar-web/src/main/js/apps/account/components/Nav.js
@@ -18,47 +18,34 @@
* 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/Security.js b/server/sonar-web/src/main/js/apps/account/components/Security.js
index 3c78f8aad2c..9accad023f5 100644
--- a/server/sonar-web/src/main/js/apps/account/components/Security.js
+++ b/server/sonar-web/src/main/js/apps/account/components/Security.js
@@ -18,34 +18,40 @@
* 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);
diff --git a/server/sonar-web/src/main/js/apps/account/components/UserCard.js b/server/sonar-web/src/main/js/apps/account/components/UserCard.js
index a62526dc145..c9fc9674173 100644
--- a/server/sonar-web/src/main/js/apps/account/components/UserCard.js
+++ b/server/sonar-web/src/main/js/apps/account/components/UserCard.js
@@ -19,32 +19,25 @@
*/
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/home/components/FavoriteProjects.js b/server/sonar-web/src/main/js/apps/account/home/components/FavoriteProjects.js
new file mode 100644
index 00000000000..c8cf522e686
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/home/components/FavoriteProjects.js
@@ -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
index 00000000000..26200df619f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/home/components/IssuesActivity.js
@@ -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
index 00000000000..43d13d066b2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/home/components/MyActivity.js
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import 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
index 00000000000..2e1b98636ff
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/home/components/MyActivityContainer.js
@@ -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
index 00000000000..3bb645a926d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/home/store/actions.js
@@ -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
index 00000000000..3cf1b341303
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/home/store/reducer.js
@@ -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
index 273782b9e0f..00000000000
--- a/server/sonar-web/src/main/js/apps/account/issues-app.js
+++ /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/components/GlobalNotifications.js b/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js
index 180f22468f8..180f22468f8 100644
--- a/server/sonar-web/src/main/js/apps/account/components/GlobalNotifications.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js
diff --git a/server/sonar-web/src/main/js/apps/account/components/Notifications.js b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.js
index 04e5fc7124f..96eb2b21109 100644
--- a/server/sonar-web/src/main/js/apps/account/components/Notifications.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.js
@@ -27,32 +27,30 @@ const Notifications = ({ globalNotifications, projectNotifications, onAddProject
const channels = globalNotifications[0].channels.map(c => c.id);
return (
- <div className="page page-limited">
+ <div>
<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>
+ <GlobalNotifications
+ notifications={globalNotifications}
+ channels={channels}/>
+
+ <hr className="account-separator"/>
+
+ <ProjectNotifications
+ notifications={projectNotifications}
+ channels={channels}
+ onAddProject={onAddProject}
+ onRemoveProject={onRemoveProject}/>
- <p className="big-spacer-top panel panel-vertical bordered-top text-right">
+ <hr className="account-separator"/>
+
+ <div className="text-center">
<button id="submit-notifications" type="submit">
{translate('my_profile.notifications.submit')}
</button>
- </p>
+ </div>
</form>
</div>
);
diff --git a/server/sonar-web/src/main/js/apps/account/components/NotificationsContainer.js b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.js
index 20ba78576e8..47933f2ebb0 100644
--- a/server/sonar-web/src/main/js/apps/account/components/NotificationsContainer.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.js
@@ -66,7 +66,7 @@ export default class NotificationsContainer extends React.Component {
translate('my_account.notifications');
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/components/NotificationsList.js b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js
index a8044b4b142..a8044b4b142 100644
--- a/server/sonar-web/src/main/js/apps/account/components/NotificationsList.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js
diff --git a/server/sonar-web/src/main/js/apps/account/components/ProjectNotification.js b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotification.js
index 95fb143d8de..95fb143d8de 100644
--- a/server/sonar-web/src/main/js/apps/account/components/ProjectNotification.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotification.js
diff --git a/server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js
index acdd691e348..0f51df5f786 100644
--- a/server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js
@@ -66,7 +66,7 @@ export default function ProjectNotifications ({ notifications, channels, onAddPr
onRemoveProject={onRemoveProject}/>
))}
- <div className="huge-spacer-top panel bg-muted">
+ <div className="spacer-top panel bg-muted">
<span className="text-middle spacer-right">
Set notifications for:
</span>
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
index 00000000000..776b328676e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/Profile.js
@@ -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/components/UserExternalIdentity.js b/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js
index c26d0418270..5fa90240cef 100644
--- a/server/sonar-web/src/main/js/apps/account/components/UserExternalIdentity.js
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js
@@ -69,16 +69,15 @@ export default class UserExternalIdentity extends React.Component {
if (!identityProvider) {
return (
- <span className="note">
+ <div>
{user.externalProvider}{': '}{user.externalIdentity}
- </span>
+ </div>
);
}
return (
- <div
- className="identity-provider"
- style={{ backgroundColor: identityProvider.backgroundColor }}>
+ <div className="identity-provider"
+ style={{ backgroundColor: identityProvider.backgroundColor }}>
<img src={window.baseUrl + identityProvider.iconPath} width="14" height="14"/>
{' '}
{user.externalIdentity}
diff --git a/server/sonar-web/src/main/js/apps/account/components/Issues.js b/server/sonar-web/src/main/js/apps/account/profile/UserGroups.js
index 626c8107068..2e17ca06066 100644
--- a/server/sonar-web/src/main/js/apps/account/components/Issues.js
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserGroups.js
@@ -17,34 +17,27 @@
* 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 React from 'react';
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();
- }
+export default class UserGroups extends React.Component {
+ static propTypes = {
+ groups: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
+ };
render () {
- const title = translate('my_account.page') + ' - ' +
- translate('issues.page');
+ const { groups } = this.props;
return (
<div>
- <Helmet
- title={title}
- titleTemplate="SonarQube - %s"/>
- <div ref="container"></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
index 00000000000..2c25e40b3cd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserScmAccounts.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/account/projects/Projects.js b/server/sonar-web/src/main/js/apps/account/projects/Projects.js
index 9f0d367c289..30c56708215 100644
--- a/server/sonar-web/src/main/js/apps/account/projects/Projects.js
+++ b/server/sonar-web/src/main/js/apps/account/projects/Projects.js
@@ -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 && (
diff --git a/server/sonar-web/src/main/js/apps/account/projects/ProjectsContainer.js b/server/sonar-web/src/main/js/apps/account/projects/ProjectsContainer.js
index 7beadce026b..e7b5cf5faf9 100644
--- a/server/sonar-web/src/main/js/apps/account/projects/ProjectsContainer.js
+++ b/server/sonar-web/src/main/js/apps/account/projects/ProjectsContainer.js
@@ -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
index b0250ecafe7..00000000000
--- a/server/sonar-web/src/main/js/apps/account/projects/ProjectsSearch.js
+++ /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
index 00000000000..e16ea776ae4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/routes.js
@@ -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>
+);
diff --git a/server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs b/server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs
index 53c0d18271c..0000cfea8b3 100644
--- a/server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs
+++ b/server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs
@@ -47,13 +47,12 @@
</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>
diff --git a/server/sonar-web/src/main/js/helpers/request.js b/server/sonar-web/src/main/js/helpers/request.js
index 97e002f0d58..74a48d84334 100644
--- a/server/sonar-web/src/main/js/helpers/request.js
+++ b/server/sonar-web/src/main/js/helpers/request.js
@@ -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));
}
diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js
index 561413c59d9..35307e33a54 100644
--- a/server/sonar-web/src/main/js/helpers/urls.js
+++ b/server/sonar-web/src/main/js/helpers/urls.js
@@ -27,6 +27,18 @@ export function getComponentUrl (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
* @param {object} query
@@ -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';
+};
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb
index 8e8fee2d57f..dca6c9cde7a 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb
@@ -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
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/account/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/account/index.html.erb
index bcad3768198..2a9a0762193 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/account/index.html.erb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/account/index.html.erb
@@ -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 %>
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index a2f80f1c0ba..86acc414ecb 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -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