diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps/account')
34 files changed, 885 insertions, 997 deletions
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/app.js b/server/sonar-web/src/main/js/apps/account/app.js deleted file mode 100644 index e7d86d442ab..00000000000 --- a/server/sonar-web/src/main/js/apps/account/app.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { render } from 'react-dom'; -import { Router, Route, IndexRoute, Redirect, useRouterHistory } from 'react-router'; -import { createHistory } from 'history'; -import AccountApp from './components/AccountApp'; -import Home from './components/Home'; -import NotificationsContainer from './components/NotificationsContainer'; -import Security from './components/Security'; -import Issues from './components/Issues'; -import ProjectsContainer from './projects/ProjectsContainer'; - -window.sonarqube.appStarted.then(options => { - const el = document.querySelector(options.el); - - const history = useRouterHistory(createHistory)({ - basename: window.baseUrl + '/account' - }); - - render(( - <Router history={history}> - <Route path="/" component={AccountApp}> - <IndexRoute component={Home}/> - <Route path="issues" component={Issues}/> - <Route path="notifications" component={NotificationsContainer}/> - <Route path="security" component={Security}/> - <Route path="projects" component={ProjectsContainer}/> - - <Redirect from="/index" to="/"/> - </Route> - </Router> - ), el); -}); diff --git a/server/sonar-web/src/main/js/apps/account/components/Account.js b/server/sonar-web/src/main/js/apps/account/components/Account.js new file mode 100644 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> |