diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-09-23 17:42:18 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-09-30 17:23:37 +0200 |
commit | 40bb40f37b47e8d5825ef349ec68c70e6b1d06c8 (patch) | |
tree | b572767548b0f5d44ad0cd026cee8ae8d6729ceb | |
parent | e9756989048439cb1c4b8eabf5fafb868b93f67e (diff) | |
download | sonarqube-40bb40f37b47e8d5825ef349ec68c70e6b1d06c8.tar.gz sonarqube-40bb40f37b47e8d5825ef349ec68c70e6b1d06c8.zip |
SONAR-8170 SONAR-8171 Reorganize My Account space
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 |