From ec156cb28a340cc394ec28ccf5d10033f80168b6 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 1 Feb 2016 14:08:20 +0100 Subject: [PATCH] SONAR-7252 display widgets to my issues on "My Account" page --- .../main/js/apps/account/components/Home.js | 6 +- .../apps/account/components/IssueWidgets.js | 211 ++++++++++++++++++ .../main/js/apps/account/styles/account.css | 18 ++ .../main/js/components/charts/bar-chart.js | 15 +- server/sonar-web/src/main/less/init/misc.less | 1 + .../resources/org/sonar/l10n/core.properties | 3 + 6 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/account/components/IssueWidgets.js 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 index 2561d8864af..593618038db 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Home.js +++ b/server/sonar-web/src/main/js/apps/account/components/Home.js @@ -22,6 +22,7 @@ import React from 'react'; 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 }) => ( @@ -34,10 +35,7 @@ const Home = ({ user, favorites, issueFilters, measureFilters }) => (
-
-

{translate('issues.page')}

-

Some cool issue widgets go here...

-
+
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 new file mode 100644 index 00000000000..3fec0fe2cff --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/components/IssueWidgets.js @@ -0,0 +1,211 @@ +/* + * 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 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, + byDate + }); + }); + } + + fetchFacets () { + return getFacets(BASE_QUERY, ['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 total = r.response.total; + + return { severities, projects, 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 ( +
+

+ {translate('my_account.issue_widget.by_creation_date')} +

+ +
+ ); + } + + render () { + return ( +
+

{translate('issues.page')}

+ + {this.state.loading && ( + + )} + + {!this.state.loading && ( +
+ + + + + + + +
+ {translate('total')} + + + {formatMeasure(this.state.total, 'SHORT_INT')} + +
+
+ )} + + {!this.state.loading && ( +
+

+ {translate('my_account.issue_widget.by_severity')} +

+ + + {this.state.severities.map(s => ( + + + + + ))} + +
+ + + + {formatMeasure(s.count, 'SHORT_INT')} + +
+
+ )} + + {!this.state.loading && ( +
+

+ {translate('my_account.issue_widget.by_project')} +

+ + + {this.state.projects.map(p => ( + + + + + ))} + +
+ {p.name} + + + {formatMeasure(p.count, 'SHORT_INT')} + +
+
+ )} + + {!this.state.loading && this.renderByDate()} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/account/styles/account.css b/server/sonar-web/src/main/js/apps/account/styles/account.css index 4105365b0cb..28eb45f4a5e 100644 --- a/server/sonar-web/src/main/js/apps/account/styles/account.css +++ b/server/sonar-web/src/main/js/apps/account/styles/account.css @@ -27,3 +27,21 @@ .account-page { padding-top: 102px; } + +.account-bar-chart .bar-chart-bar { + fill: #4b9fd5; +} + +.account-bar-chart .bar-chart-tick { + fill: #777; + font-size: 12px; + text-anchor: middle; +} + +.account-bar-chart .histogram-tick { + text-anchor: end; +} + +.account-bar-chart .histogram-value { + text-anchor: start; +} diff --git a/server/sonar-web/src/main/js/components/charts/bar-chart.js b/server/sonar-web/src/main/js/components/charts/bar-chart.js index e70c8529f0c..c73405d370b 100644 --- a/server/sonar-web/src/main/js/components/charts/bar-chart.js +++ b/server/sonar-web/src/main/js/components/charts/bar-chart.js @@ -30,7 +30,8 @@ export const BarChart = React.createClass({ xValues: React.PropTypes.arrayOf(React.PropTypes.any), height: React.PropTypes.number, padding: React.PropTypes.arrayOf(React.PropTypes.number), - barsWidth: React.PropTypes.number + barsWidth: React.PropTypes.number, + onBarClick: React.PropTypes.func }, mixins: [ResizeMixin, TooltipsMixin], @@ -48,6 +49,10 @@ export const BarChart = React.createClass({ return { width: this.props.width, height: this.props.height }; }, + handleClick(point) { + this.props.onBarClick(point); + }, + renderXTicks (xScale, yScale) { if (!this.props.xTicks.length) { return null; @@ -67,6 +72,8 @@ export const BarChart = React.createClass({ x={x} y={y} dy="1.5em" + onClick={this.props.onBarClick && this.handleClick.bind(this, point)} + style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }} {...tooltipAtts}>{tick}; }); return {ticks}; @@ -91,6 +98,8 @@ export const BarChart = React.createClass({ x={x} y={y} dy="-1em" + onClick={this.props.onBarClick && this.handleClick.bind(this, point)} + style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }} {...tooltipAtts}>{value}; }); return {ticks}; @@ -113,7 +122,9 @@ export const BarChart = React.createClass({ x={x} y={y} width={this.props.barsWidth} - height={height}/>; + height={height} + onClick={this.props.onBarClick && this.handleClick.bind(this, d)} + style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}/>; }); return {bars}; }, diff --git a/server/sonar-web/src/main/less/init/misc.less b/server/sonar-web/src/main/less/init/misc.less index 2595e67a970..2fc4f1bd403 100644 --- a/server/sonar-web/src/main/less/init/misc.less +++ b/server/sonar-web/src/main/less/init/misc.less @@ -89,6 +89,7 @@ td.big-spacer-top { padding-top: 16px; } .width-10 { width: 10%; } .abs-width-240 { width: 240px; } +.abs-width-300 { width: 300px; } .abs-width-400 { width: 400px; } .justify { 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 b09e66c3d48..e1b2c0c7d60 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2166,6 +2166,9 @@ my_account.notifications=Notifications my_account.no_project_notifications=You have not set project notifications yet. 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.issue_widget.by_creation_date=By Creation Date +my_account.issue_widget.by_project=By Project +my_account.issue_widget.by_severity=By Severity -- 2.39.5