diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
29 files changed, 964 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/app.js b/server/sonar-web/src/main/js/apps/overview/app.js new file mode 100644 index 00000000000..80bc990a965 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/app.js @@ -0,0 +1,26 @@ +import $ from 'jquery'; +import _ from 'underscore'; +import React from 'react'; +import Main from './main'; +import Empty from './empty'; + +class App { + start(options) { + let opts = _.extend({}, options, window.sonarqube.overview); + _.extend(opts.component, options.component); + $('html').toggleClass('dashboard-page', opts.component.hasSnapshot); + window.requestMessages().done(() => { + let el = document.querySelector(opts.el); + let inner = opts.component.hasSnapshot ? ( + <Main + component={opts.component} + gate={opts.gate} + measures={opts.measures} + leak={opts.leak}/> + ) : <Empty/>; + React.render(inner, el); + }); + } +} + +window.sonarqube.appStarted.then(options => new App().start(options)); diff --git a/server/sonar-web/src/main/js/apps/overview/card.js b/server/sonar-web/src/main/js/apps/overview/card.js new file mode 100644 index 00000000000..a22146246d3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/card.js @@ -0,0 +1,7 @@ +import React from 'react'; + +export default React.createClass({ + render() { + return <li className="overview-card">{this.props.children}</li>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/cards.js b/server/sonar-web/src/main/js/apps/overview/cards.js new file mode 100644 index 00000000000..3d69cf8bf3a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/cards.js @@ -0,0 +1,7 @@ +import React from 'react'; + +export default React.createClass({ + render() { + return <ul className="overview-cards">{this.props.children}</ul>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/empty.js b/server/sonar-web/src/main/js/apps/overview/empty.js new file mode 100644 index 00000000000..78c5320a1f8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/empty.js @@ -0,0 +1,13 @@ +import React from 'react'; + +export default React.createClass({ + render() { + return ( + <div className="panel"> + <div className="alert alert-warning"> + {window.t('provisioning.no_analysis')} + </div> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/gate-condition.js new file mode 100644 index 00000000000..2cc428253e6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/gate-condition.js @@ -0,0 +1,33 @@ +import React from 'react'; +import Measure from './helpers/measure'; +import { periodLabel, getPeriodDate } from './helpers/period-label'; +import DrilldownLink from './helpers/drilldown-link'; + +export default React.createClass({ + render() { + let metricName = window.t('metric', this.props.condition.metric.name, 'name'), + threshold = this.props.condition.level === 'ERROR' ? + this.props.condition.error : this.props.condition.warning, + iconClassName = 'icon-alert-' + this.props.condition.level.toLowerCase(), + period = this.props.condition.period ? + `(${periodLabel(this.props.component.periods, this.props.condition.period)})` : null, + periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period); + + return ( + <div> + <h4 className="overview-gate-condition-metric">{metricName}<br/><span className="nowrap">{period}</span></h4> + <div className="overview-gate-condition-value"> + <i className={iconClassName}/> + <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name} + period={this.props.condition.period} periodDate={periodDate}> + <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/> + </DrilldownLink> + <span className="overview-gate-condition-itself"> + {window.t('quality_gates.operator', this.props.condition.op, 'short')} + <Measure value={threshold} type={this.props.condition.metric.type}/> + </span> + </div> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/gate-conditions.js new file mode 100644 index 00000000000..65b92e42a71 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/gate-conditions.js @@ -0,0 +1,21 @@ +import React from 'react'; +import Cards from './cards'; +import Card from './card'; +import GateCondition from './gate-condition'; + +export default React.createClass({ + render() { + let conditions = this.props.gate.conditions + .filter((c) => { + return c.level !== 'OK'; + }) + .map((c) => { + return ( + <Card key={c.metric.name}> + <GateCondition condition={c} component={this.props.component}/> + </Card> + ); + }); + return <Cards>{conditions}</Cards>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/gate-empty.js new file mode 100644 index 00000000000..56cdb552a05 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/gate-empty.js @@ -0,0 +1,14 @@ +import React from 'react'; + +export default React.createClass({ + render() { + let qualityGatesUrl = window.baseUrl + '/quality_gates'; + + return ( + <div className="overview-gate"> + <h2 className="overview-title">{window.t('overview.quality_gate')}</h2> + <p className="big-spacer-top">You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate.js b/server/sonar-web/src/main/js/apps/overview/gate.js new file mode 100644 index 00000000000..2436d2d468e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/gate.js @@ -0,0 +1,25 @@ +import React from 'react'; +import GateConditions from './gate-conditions'; +import GateEmpty from './gate-empty'; + +export default React.createClass({ + render() { + if (!this.props.gate || !this.props.gate.level) { + return this.props.component.qualifier === 'TRK' ? <GateEmpty/> : null; + } + + let + badgeClassName = 'badge badge-' + this.props.gate.level.toLowerCase(), + badgeText = window.t('overview.gate', this.props.gate.level); + + return ( + <div className="overview-gate"> + <h2 className="overview-title"> + {window.t('overview.quality_gate')} + <span className={badgeClassName}>{badgeText}</span> + </h2> + <GateConditions gate={this.props.gate} component={this.props.component}/> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/donut.js b/server/sonar-web/src/main/js/apps/overview/helpers/donut.js new file mode 100644 index 00000000000..368a5222c64 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/donut.js @@ -0,0 +1,34 @@ +import d3 from 'd3'; +import React from 'react'; + +let Sector = React.createClass({ + render() { + let arc = d3.svg.arc() + .outerRadius(this.props.radius) + .innerRadius(this.props.radius - this.props.thickness); + return <path d={arc(this.props.data)} style={{ fill: this.props.fill }}/>; + } +}); + +export default React.createClass({ + getDefaultProps() { + return { + size: 30, + thickness: 6 + }; + }, + + render() { + let radius = this.props.size / 2; + let pie = d3.layout.pie() + .sort(null) + .value(d => d.value); + let data = this.props.data; + let sectors = pie(data).map((d, i) => { + return <Sector key={i} data={d} fill={data[i].fill} radius={radius} thickness={this.props.thickness}/>; + }); + return <svg width={this.props.size} height={this.props.size}> + <g transform={`translate(${radius}, ${radius})`}>{sectors}</g> + </svg>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js new file mode 100644 index 00000000000..338a34bc016 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js @@ -0,0 +1,95 @@ +import _ from 'underscore'; +import moment from 'moment'; +import React from 'react'; +import IssuesLink from './issues-link'; + +export default React.createClass({ + isIssueMeasure() { + const ISSUE_MEASURES = [ + 'violations', + 'blocker_violations', + 'critical_violations', + 'major_violations', + 'minor_violations', + 'info_violations', + 'new_blocker_violations', + 'new_critical_violations', + 'new_major_violations', + 'new_minor_violations', + 'new_info_violations', + 'open_issues', + 'reopened_issues', + 'confirmed_issues', + 'false_positive_issues' + ]; + return ISSUE_MEASURES.indexOf(this.props.metric) !== -1; + }, + + propsToIssueParams() { + let params = {}; + if (this.props.periodDate) { + params.createdAfter = moment(this.props.periodDate).format('YYYY-MM-DDTHH:mm:ssZZ'); + } + switch (this.props.metric) { + case 'blocker_violations': + case 'new_blocker_violations': + _.extend(params, { resolved: 'false', severities: 'BLOCKER' }); + break; + case 'critical_violations': + case 'new_critical_violations': + _.extend(params, { resolved: 'false', severities: 'CRITICAL' }); + break; + case 'major_violations': + case 'new_major_violations': + _.extend(params, { resolved: 'false', severities: 'MAJOR' }); + break; + case 'minor_violations': + case 'new_minor_violations': + _.extend(params, { resolved: 'false', severities: 'MINOR' }); + break; + case 'info_violations': + case 'new_info_violations': + _.extend(params, { resolved: 'false', severities: 'INFO' }); + break; + case 'open_issues': + _.extend(params, { resolved: 'false', statuses: 'OPEN' }); + break; + case 'reopened_issues': + _.extend(params, { resolved: 'false', statuses: 'REOPENED' }); + break; + case 'confirmed_issues': + _.extend(params, { resolved: 'false', statuses: 'CONFIRMED' }); + break; + case 'false_positive_issues': + _.extend(params, { resolutions: 'FALSE-POSITIVE' }); + break; + default: + _.extend(params, { resolved: 'false' }); + } + return params; + }, + + renderIssuesLink() { + return <IssuesLink component={this.props.component} params={this.propsToIssueParams()}> + {this.props.children} + </IssuesLink>; + }, + + render() { + if (this.isIssueMeasure()) { + return this.renderIssuesLink(); + } + + let params = { id: this.props.component, metric: this.props.metric }; + if (this.props.period) { + params.period = this.props.period; + } + + let query = Object.keys(params).map(key => { + return `${key}=${encodeURIComponent(params[key])}`; + }).join('&'), + url = `${baseUrl}/drilldown/measures?${query}`; + + return <a href={url}>{this.props.children}</a>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js new file mode 100644 index 00000000000..79878ac9fd6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js @@ -0,0 +1,8 @@ +import React from 'react'; + +export default React.createClass({ + render() { + let url = `${baseUrl}/quality_gates/show/${this.props.gate}`; + return <a href={url}>{this.props.children}</a>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js new file mode 100644 index 00000000000..d3118579036 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default React.createClass({ + render() { + let params = Object.keys(this.props.params).map((key) => { + return `${key}=${encodeURIComponent(this.props.params[key])}`; + }).join('|'), + url = `${baseUrl}/component_issues/index?id=${encodeURIComponent(this.props.component)}#${params}`; + return <a href={url}>{this.props.children}</a>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js b/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js new file mode 100644 index 00000000000..0e2e1fa75c4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default React.createClass({ + render() { + if (this.props.value == null || isNaN(this.props.value)) { + return null; + } + let formatted = window.formatMeasureVariation(this.props.value, this.props.type); + return <span>{formatted}</span>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/measure.js b/server/sonar-web/src/main/js/apps/overview/helpers/measure.js new file mode 100644 index 00000000000..dafc98a82c4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/measure.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default React.createClass({ + render() { + if (this.props.value == null || isNaN(this.props.value)) { + return null; + } + let formatted = window.formatMeasure(this.props.value, this.props.type); + return <span>{formatted}</span>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js new file mode 100644 index 00000000000..109a9df9a57 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js @@ -0,0 +1,18 @@ +import _ from 'underscore'; +import moment from 'moment'; + +export let periodLabel = (periods, periodIndex) => { + let period = _.findWhere(periods, { index: periodIndex }); + if (!period) { + return null; + } + return window.tp(`overview.period.${period.mode}`, period.modeParam); +}; + +export let getPeriodDate = (periods, periodIndex) => { + let period = _.findWhere(periods, { index: periodIndex }); + if (!period) { + return null; + } + return moment(period.date).toDate(); +}; diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js new file mode 100644 index 00000000000..22065abad56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js @@ -0,0 +1,8 @@ +import React from 'react'; + +export default React.createClass({ + render() { + let url = `${baseUrl}/profiles/show?key=${encodeURIComponent(this.props.profile)}`; + return <a href={url}>{this.props.children}</a>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/rating.js b/server/sonar-web/src/main/js/apps/overview/helpers/rating.js new file mode 100644 index 00000000000..e160019e9c6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/rating.js @@ -0,0 +1,12 @@ +import React from 'react'; + +export default React.createClass({ + render() { + if (this.props.value == null || isNaN(this.props.value)) { + return null; + } + let formatted = window.formatMeasure(this.props.value, 'RATING'); + let className = 'rating rating-' + formatted; + return <span className={className}>{formatted}</span>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak-coverage.js b/server/sonar-web/src/main/js/apps/overview/leak-coverage.js new file mode 100644 index 00000000000..0d502439796 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak-coverage.js @@ -0,0 +1,46 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import MeasureVariation from './helpers/measure-variation'; +import DrilldownLink from './helpers/drilldown-link'; +import Donut from './helpers/donut'; + +export default React.createClass({ + render() { + let + newCoverage = parseInt(this.props.leak.newCoverage, 10), + tests = this.props.leak.tests, + donutData = [ + { value: newCoverage, fill: '#85bb43' }, + { value: 100 - newCoverage, fill: '#d4333f' } + ]; + + if (newCoverage == null || isNaN(newCoverage)) { + return null; + } + + return ( + <Card> + <div className="measures"> + <div className="measures-chart"> + <Donut data={donutData} size="47"/> + </div> + <div className="measure measure-big" data-metric="new_coverage"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="new_coverage" period="3"> + <Measure value={newCoverage} type="PERCENT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.new_coverage')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top measures-chart-indent"> + <li> + <span><MeasureVariation value={tests} type="SHORT_INT"/></span> + <span>{window.t('overview.metric.tests')}</span> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak-dups.js b/server/sonar-web/src/main/js/apps/overview/leak-dups.js new file mode 100644 index 00000000000..a34555fd892 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak-dups.js @@ -0,0 +1,42 @@ +import React from 'react'; +import Card from './card'; +import MeasureVariation from './helpers/measure-variation'; +import Donut from './helpers/donut'; + +export default React.createClass({ + render() { + let + density = this.props.leak.duplications, + lines = this.props.leak.duplicatedLines, + donutData = [ + { value: density, fill: '#f3ca8e' }, + { value: 100 - density, fill: '#e6e6e6' } + ]; + + if (density == null) { + return null; + } + + return ( + <Card> + <div className="measures"> + <div className="measures-chart"> + <Donut data={donutData} size="47"/> + </div> + <div className="measure measure-big" data-metric="duplicated_lines_density"> + <span className="measure-value"> + <MeasureVariation value={density} type="PERCENT"/> + </span> + <span className="measure-name">{window.t('overview.metric.duplications')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top measures-chart-indent"> + <li> + <span><MeasureVariation value={lines} type="SHORT_INT"/></span> + <span>{window.t('overview.metric.duplicated_lines')}</span> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak-issues.js b/server/sonar-web/src/main/js/apps/overview/leak-issues.js new file mode 100644 index 00000000000..61443d6f1ce --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak-issues.js @@ -0,0 +1,69 @@ +import moment from 'moment'; +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import MeasureVariation from './helpers/measure-variation'; +import IssuesLink from './helpers/issues-link'; +import SeverityIcon from '../../components/shared/severity-icon'; +import StatusIcon from '../../components/shared/status-icon'; +import {getPeriodDate} from './helpers/period-label'; + +export default React.createClass({ + render() { + let + newDebt = this.props.leak.newDebt, + issues = this.props.leak.newIssues, + blockerIssues = this.props.leak.newBlockerIssues, + criticalIssues = this.props.leak.newCriticalIssues, + issuesToReview = this.props.leak.newOpenIssues + this.props.leak.newReopenedIssues, + periodDate = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ'); + + return ( + <Card> + <div className="measures"> + <div className="measure measure-big" data-metric="sqale_index"> + <span className="measure-value"> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate, facetMode: 'debt' }}> + <Measure value={newDebt} type="SHORT_WORK_DUR"/> + </IssuesLink> + </span> + <span className="measure-name">{window.t('overview.metric.new_debt')}</span> + </div> + <div className="measure measure-big" data-metric="violations"> + <span className="measure-value"> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate }}> + <Measure value={issues} type="SHORT_INT"/> + </IssuesLink> + </span> + <span className="measure-name">{window.t('overview.metric.new_issues')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top"> + <li> + <span><SeverityIcon severity="BLOCKER"/></span> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate, severities: 'BLOCKER' }}> + <MeasureVariation value={blockerIssues} type="SHORT_INT"/> + </IssuesLink> + </li> + <li> + <span><SeverityIcon severity="CRITICAL"/></span> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate, severities: 'CRITICAL' }}> + <MeasureVariation value={criticalIssues} type="SHORT_INT"/> + </IssuesLink> + </li> + <li> + <span><StatusIcon status="OPEN"/></span> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate, statuses: 'OPEN,REOPENED' }}> + <MeasureVariation value={issuesToReview} type="SHORT_INT"/> + </IssuesLink> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak-size.js b/server/sonar-web/src/main/js/apps/overview/leak-size.js new file mode 100644 index 00000000000..d7df01caf33 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak-size.js @@ -0,0 +1,30 @@ +import React from 'react'; +import Card from './card'; +import MeasureVariation from './helpers/measure-variation'; + +export default React.createClass({ + render() { + let + lines = this.props.leak.lines, + files = this.props.leak.files; + + return ( + <Card> + <div className="measures"> + <div className="measure measure-big" data-metric="lines"> + <span className="measure-value"> + <MeasureVariation value={lines} type="SHORT_INT"/> + </span> + <span className="measure-name">{window.t('overview.metric.lines')}</span> + </div> + <div className="measure measure-big" data-metric="files"> + <span className="measure-value"> + <MeasureVariation value={files} type="SHORT_INT"/> + </span> + <span className="measure-name">{window.t('overview.metric.files')}</span> + </div> + </div> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak.js b/server/sonar-web/src/main/js/apps/overview/leak.js new file mode 100644 index 00000000000..f629153f2ff --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak.js @@ -0,0 +1,33 @@ +import _ from 'underscore'; +import React from 'react'; +import Cards from './cards'; +import LeakIssues from './leak-issues'; +import LeakCoverage from './leak-coverage'; +import LeakSize from './leak-size'; +import LeakDups from './leak-dups'; +import {periodLabel} from './helpers/period-label'; + +export default React.createClass({ + render() { + if (_.size(this.props.component.periods) < 3) { + return null; + } + + let period = periodLabel(this.props.component.periods, '3'); + + return ( + <div className="overview-leak"> + <h2 className="overview-title"> + {window.t('overview.water_leak')} + <span className="overview-leak-period">{period}</span> + </h2> + <Cards> + <LeakIssues component={this.props.component} leak={this.props.leak} measures={this.props.measures}/> + <LeakCoverage component={this.props.component} leak={this.props.leak}/> + <LeakDups component={this.props.component} leak={this.props.leak}/> + <LeakSize component={this.props.component} leak={this.props.leak}/> + </Cards> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/main.js b/server/sonar-web/src/main/js/apps/overview/main.js new file mode 100644 index 00000000000..dc1025a8911 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/main.js @@ -0,0 +1,102 @@ +import $ from 'jquery'; +import _ from 'underscore'; +import moment from 'moment'; +import React from 'react'; +import Gate from './gate'; +import Leak from './leak'; +import Nutshell from './nutshell'; +import Meta from './meta'; +import {getPeriodDate} from './helpers/period-label'; + +export default React.createClass({ + getInitialState() { + return { leak: this.props.leak, measures: this.props.measures }; + }, + + componentDidMount() { + if (this._hasWaterLeak()) { + this.requestLeakIssues(); + this.requestLeakDebt(); + } + this.requestNutshellIssues(); + this.requestNutshellDebt(); + }, + + _hasWaterLeak() { + return !!_.findWhere(this.props.component.periods, { index: '3' }); + }, + + _requestIssues(data) { + let url = `${baseUrl}/api/issues/search`; + data.ps = 1; + data.componentUuids = this.props.component.id; + return $.get(url, data); + }, + + requestLeakIssues() { + let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ'); + this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities,statuses' }).done(r => { + let + severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values, + statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values; + + this.setState({ + leak: _.extend({}, this.state.leak, { + newIssues: r.total, + newBlockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count, + newCriticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count, + newOpenIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count, + newReopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count + }) + }); + }); + }, + + requestNutshellIssues() { + this._requestIssues({ resolved: 'false', facets: 'severities,statuses' }).done(r => { + let + severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values, + statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values; + + this.setState({ + measures: _.extend({}, this.state.measures, { + issues: r.total, + blockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count, + criticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count, + openIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count, + reopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count + }) + }); + }); + }, + + requestLeakDebt() { + let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ'); + this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities', facetMode: 'debt' }).done(r => { + this.setState({ + leak: _.extend({}, this.state.leak, { newDebt: r.debtTotal }) + }); + }); + }, + + requestNutshellDebt() { + this._requestIssues({ resolved: 'false', facets: 'severities', facetMode: 'debt' }).done(r => { + this.setState({ + measures: _.extend({}, this.state.measures, { debt: r.debtTotal }) + }); + }); + }, + + render() { + return ( + <div className="overview"> + <div className="overview-main"> + <Gate component={this.props.component} gate={this.props.gate}/> + <Leak component={this.props.component} leak={this.state.leak} measures={this.state.measures}/> + <Nutshell component={this.props.component} measures={this.state.measures}/> + </div> + <Meta component={this.props.component}/> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta.js b/server/sonar-web/src/main/js/apps/overview/meta.js new file mode 100644 index 00000000000..310aadd8ae6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta.js @@ -0,0 +1,68 @@ +import _ from 'underscore'; +import React from 'react'; +import ProfileLink from './helpers/profile-link'; +import GateLink from './helpers/gate-link'; + +export default React.createClass({ + render() { + let + profiles = (this.props.component.profiles || []).map(profile => { + return ( + <li key={profile.key}> + <span className="note spacer-right">({profile.language})</span> + <ProfileLink profile={profile.key}>{profile.name}</ProfileLink> + </li> + ); + }), + links = (this.props.component.links || []).map(link => { + let iconClassName = `spacer-right icon-color-link icon-${link.type}`; + return ( + <li key={link.type}> + <i className={iconClassName}/> + <a href={link.href} target="_blank">{link.name}</a> + </li> + ); + }); + + let descriptionCard = this.props.component.description ? ( + <div className="overview-card"> + <div className="overview-meta-description">{this.props.component.description}</div> + </div> + ) : null, + + linksCard = _.size(this.props.component.links) > 0 ? ( + <div className="overview-card"> + <ul className="overview-meta-list">{links}</ul> + </div> + ) : null, + + profilesCard = _.size(this.props.component.profiles) > 0 ? ( + <div className="overview-card"> + <h4 className="overview-meta-header">{window.t('overview.quality_profiles')}</h4> + <ul className="overview-meta-list">{profiles}</ul> + </div> + ) : null, + + gateCard = this.props.component.gate ? ( + <div className="overview-card"> + <h4 className="overview-meta-header">{window.t('overview.quality_gate')}</h4> + <ul className="overview-meta-list"> + <li> + {this.props.component.gate.isDefault ? + <span className="note spacer-right">(Default)</span> : null} + <GateLink gate={this.props.component.gate.key}>{this.props.component.gate.name}</GateLink> + </li> + </ul> + </div> + ) : null; + + return ( + <div className="overview-meta"> + {descriptionCard} + {linksCard} + {profilesCard} + {gateCard} + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js b/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js new file mode 100644 index 00000000000..e478a715259 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js @@ -0,0 +1,47 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import DrilldownLink from './helpers/drilldown-link'; +import Donut from './helpers/donut'; + +export default React.createClass({ + render() { + let + coverage = this.props.measures.coverage, + tests = this.props.measures.tests, + donutData = [ + { value: coverage, fill: '#85bb43' }, + { value: 100 - coverage, fill: '#d4333f' } + ]; + + if (coverage == null) { + return null; + } + + return ( + <Card> + <div className="measures"> + <div className="measures-chart"> + <Donut data={donutData} size="47"/> + </div> + <div className="measure measure-big"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="overall_coverage"> + <Measure value={coverage} type="PERCENT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.coverage')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top measures-chart-indent"> + <li> + <DrilldownLink component={this.props.component.key} metric="tests"> + <Measure value={tests} type="SHORT_INT"/> + </DrilldownLink> + <span>{window.t('overview.metric.tests')}</span> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-dups.js b/server/sonar-web/src/main/js/apps/overview/nutshell-dups.js new file mode 100644 index 00000000000..fd93f144b0a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell-dups.js @@ -0,0 +1,47 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import DrilldownLink from './helpers/drilldown-link'; +import Donut from './helpers/donut'; + +export default React.createClass({ + render() { + let + density = this.props.measures.duplications, + lines = this.props.measures.duplicatedLines, + donutData = [ + { value: density, fill: '#f3ca8e' }, + { value: 100 - density, fill: '#e6e6e6' } + ]; + + if (density == null) { + return null; + } + + return ( + <Card> + <div className="measures"> + <div className="measures-chart"> + <Donut data={donutData} size="47"/> + </div> + <div className="measure measure-big"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density"> + <Measure value={density} type="PERCENT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.duplications')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top measures-chart-indent"> + <li> + <DrilldownLink component={this.props.component.key} metric="duplicated_lines"> + <Measure value={lines} type="SHORT_INT"/> + </DrilldownLink> + <span>{window.t('overview.metric.duplicated_lines')}</span> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-issues.js b/server/sonar-web/src/main/js/apps/overview/nutshell-issues.js new file mode 100644 index 00000000000..f9ae3340dbf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell-issues.js @@ -0,0 +1,68 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import Rating from './helpers/rating'; +import IssuesLink from './helpers/issues-link'; +import DrilldownLink from './helpers/drilldown-link'; +import SeverityIcon from '../../components/shared/severity-icon'; +import StatusIcon from '../../components/shared/status-icon'; + +export default React.createClass({ + render() { + let + debt = this.props.measures.debt, + rating = this.props.measures.sqaleRating, + issues = this.props.measures.issues, + blockerIssues = this.props.measures.blockerIssues, + criticalIssues = this.props.measures.criticalIssues, + issuesToReview = this.props.measures.openIssues + this.props.measures.reopenedIssues; + + return ( + <Card> + <div className="measures"> + <div className="measure measure-big" data-metric="sqale_rating"> + <DrilldownLink component={this.props.component.key} metric="sqale_rating"> + <Rating value={rating}/> + </DrilldownLink> + </div> + <div className="measure measure-big" data-metric="sqale_index"> + <span className="measure-value"> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}> + <Measure value={debt} type="SHORT_WORK_DUR"/> + </IssuesLink> + </span> + <span className="measure-name">{window.t('overview.metric.debt')}</span> + </div> + <div className="measure measure-big" data-metric="violations"> + <span className="measure-value"> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}> + <Measure value={issues} type="SHORT_INT"/> + </IssuesLink> + </span> + <span className="measure-name">{window.t('overview.metric.issues')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top"> + <li> + <span><SeverityIcon severity="BLOCKER"/></span> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'BLOCKER' }}> + <Measure value={blockerIssues} type="SHORT_INT"/> + </IssuesLink> + </li> + <li> + <span><SeverityIcon severity="CRITICAL"/></span> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'CRITICAL' }}> + <Measure value={criticalIssues} type="SHORT_INT"/> + </IssuesLink> + </li> + <li> + <span><StatusIcon status="OPEN"/></span> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', statuses: 'OPEN,REOPENED' }}> + <Measure value={issuesToReview} type="SHORT_INT"/> + </IssuesLink> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-size.js b/server/sonar-web/src/main/js/apps/overview/nutshell-size.js new file mode 100644 index 00000000000..5d41e291c80 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell-size.js @@ -0,0 +1,35 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import DrilldownLink from './helpers/drilldown-link'; + +export default React.createClass({ + render() { + let + lines = this.props.measures['lines'], + files = this.props.measures['files']; + + return ( + <Card> + <div className="measures"> + <div className="measure measure-big" data-metric="lines"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="lines"> + <Measure value={lines} type="SHORT_INT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.lines')}</span> + </div> + <div className="measure measure-big" data-metric="files"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="files"> + <Measure value={files} type="SHORT_INT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.files')}</span> + </div> + </div> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell.js b/server/sonar-web/src/main/js/apps/overview/nutshell.js new file mode 100644 index 00000000000..f8c7dc39f9e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell.js @@ -0,0 +1,23 @@ +import React from 'react'; +import Cards from './cards'; +import NutshellIssues from './nutshell-issues'; +import NutshellCoverage from './nutshell-coverage'; +import NutshellSize from './nutshell-size'; +import NutshellDups from './nutshell-dups'; + +export default React.createClass({ + render() { + let props = { measures: this.props.measures, component: this.props.component }; + return ( + <div className="overview-nutshell"> + <h2 className="overview-title">{window.t('overview.project_in_a_nutshell')}</h2> + <Cards> + <NutshellIssues {...props}/> + <NutshellCoverage {...props}/> + <NutshellDups {...props}/> + <NutshellSize {...props}/> + </Cards> + </div> + ); + } +}); |