]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7252 display widgets to my issues on "My Account" page
authorStas Vilchik <vilchiks@gmail.com>
Mon, 1 Feb 2016 13:08:20 +0000 (14:08 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 1 Feb 2016 15:36:00 +0000 (16:36 +0100)
server/sonar-web/src/main/js/apps/account/components/Home.js
server/sonar-web/src/main/js/apps/account/components/IssueWidgets.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/styles/account.css
server/sonar-web/src/main/js/components/charts/bar-chart.js
server/sonar-web/src/main/less/init/misc.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 2561d8864af012eb5efecebfa30562cb92124ca9..593618038db963d6e6326d545184f2b6fa3d214f 100644 (file)
@@ -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 }) => (
         </div>
 
         <div className="column-third">
-          <section>
-            <h2 className="spacer-bottom">{translate('issues.page')}</h2>
-            <p>Some cool issue widgets go here...</p>
-          </section>
+          <IssueWidgets/>
         </div>
 
         <div className="column-third">
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 (file)
index 0000000..3fec0fe
--- /dev/null
@@ -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 (
+        <section className="abs-width-300 huge-spacer-top account-bar-chart">
+          <h4 className="spacer-bottom">
+            {translate('my_account.issue_widget.by_creation_date')}
+          </h4>
+          <BarChart
+              data={data}
+              xTicks={xTicks}
+              xValues={xValues}
+              barsWidth={20}
+              padding={[25, 0, 25, 0]}
+              height={80}
+              onBarClick={this.handleByDateClick.bind(this)}/>
+        </section>
+    );
+  }
+
+  render () {
+    return (
+        <section>
+          <h2 className="spacer-bottom">{translate('issues.page')}</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>
+                  </tbody>
+                </table>
+              </section>
+          )}
+
+          {!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>
+          )}
+
+          {!this.state.loading && this.renderByDate()}
+        </section>
+    );
+  }
+}
index 4105365b0cbb2d3c47e8f6d323d7cb0dc77ee32e..28eb45f4a5e69bac694649d5fc61c253efb55d86 100644 (file)
 .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;
+}
index e70c8529f0c629a7fc8fc5edf0f32b4fc0cd5d76..c73405d370bd8f9f392e3a2b22371f85421e9662 100644 (file)
@@ -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}</text>;
     });
     return <g>{ticks}</g>;
@@ -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}</text>;
     });
     return <g>{ticks}</g>;
@@ -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 <g>{bars}</g>;
   },
index 2595e67a970f8ee838ae3b4d4fa7b94d387c6ed2..2fc4f1bd403cf696256d0ec0de6d4b9fb25b9159 100644 (file)
@@ -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 {
index b09e66c3d48aafc943c3c623deb3c87e4011656f..e1b2c0c7d601072ba1f9704299c18246a1f283be 100644 (file)
@@ -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