diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-10-19 16:40:37 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-10-20 11:03:41 +0200 |
commit | 187f501d31cba9ff220c44bbcd222768a4eee40e (patch) | |
tree | 51e71412637be96d9458c761f3d09dc236570d22 /server | |
parent | 26817f2f1b31d777b681cc08f3a06db201521007 (diff) | |
download | sonarqube-187f501d31cba9ff220c44bbcd222768a4eee40e.tar.gz sonarqube-187f501d31cba9ff220c44bbcd222768a4eee40e.zip |
SONAR-6331 add project overview page
Diffstat (limited to 'server')
41 files changed, 1386 insertions, 8 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 0b9f0251293..2d21537cd6d 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -52,7 +52,8 @@ }, "browserify-shim": { "jquery": "global:jQuery", - "underscore": "global:_" + "underscore": "global:_", + "d3": "global:d3" }, "browserify": { "transform": [ 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> + ); + } +}); diff --git a/server/sonar-web/src/main/js/helpers/Url.js b/server/sonar-web/src/main/js/helpers/Url.js index 80e24f0fc28..6a57b3537f7 100644 --- a/server/sonar-web/src/main/js/helpers/Url.js +++ b/server/sonar-web/src/main/js/helpers/Url.js @@ -2,5 +2,5 @@ export function getProjectUrl(project) { if (typeof project !== 'string') { throw new TypeError('Project ID or KEY should be passed'); } - return `${window.baseUrl}/dashboard?id=${encodeURIComponent(project)}`; + return `${window.baseUrl}/overview?id=${encodeURIComponent(project)}`; } diff --git a/server/sonar-web/src/main/js/libs/application.js b/server/sonar-web/src/main/js/libs/application.js index 255af4f496c..5338ae64886 100644 --- a/server/sonar-web/src/main/js/libs/application.js +++ b/server/sonar-web/src/main/js/libs/application.js @@ -362,7 +362,7 @@ function closeModalWindow () { */ function shouldDisplayAbout (days, hours, minutes) { var hasDays = days > 0, - fewDays = days < 1000, + fewDays = days < 5, hasHours = hours > 0, hasMinutes = minutes > 0; return (hasDays && fewDays && hasHours) || (!hasDays && hasHours && hasMinutes); diff --git a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js index b7db14235b9..be59409f008 100644 --- a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js +++ b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js @@ -17,7 +17,7 @@ export default React.createClass({ return params.period ? `&period=${params.period}` : ''; }, - renderOverviewLink() { + renderMainDashboardLink() { if (_.size(this.props.component.dashboards) === 0) { return null; } @@ -34,6 +34,11 @@ export default React.createClass({ }); }, + renderOverviewLink() { + let url = `/overview?id=${encodeURIComponent(this.props.component.key)}`; + return this.renderLink(url, window.t('overview.page'), '/overview'); + }, + renderComponentsLink() { const url = `/components/index?id=${encodeURIComponent(this.props.component.key)}`; return this.renderLink(url, window.t('components.page'), '/components'); @@ -239,6 +244,7 @@ export default React.createClass({ render() { return ( <ul className="nav navbar-nav nav-tabs"> + {this.renderMainDashboardLink()} {this.renderOverviewLink()} {this.renderComponentsLink()} {this.renderComponentIssuesLink()} diff --git a/server/sonar-web/src/main/less/components/measures.less b/server/sonar-web/src/main/less/components/measures.less index a112b884fc4..17292f6eabc 100644 --- a/server/sonar-web/src/main/less/components/measures.less +++ b/server/sonar-web/src/main/less/components/measures.less @@ -58,7 +58,7 @@ .measure-value { color: @darkBlue; font-size: @bigFontSize; - font-weight: 300; + font-weight: 400; } .measure-bar { @@ -95,7 +95,7 @@ .measure-name { margin-top: 2px; font-size: 15px; - font-weight: 300; + font-weight: 400; } .measure-value { diff --git a/server/sonar-web/src/main/less/components/navbar.less b/server/sonar-web/src/main/less/components/navbar.less index 141c2b08f31..f6e613f933e 100644 --- a/server/sonar-web/src/main/less/components/navbar.less +++ b/server/sonar-web/src/main/less/components/navbar.less @@ -189,6 +189,7 @@ top: @navbarGlobalHeight; z-index: @navbar-context-z-index; height: @navbarContextHeight; + padding-top: 5px; background-color: @navbarContextBackground; .nav-tabs { diff --git a/server/sonar-web/src/main/less/components/ui.less b/server/sonar-web/src/main/less/components/ui.less index 6e8ebe364ef..1af0fed2673 100644 --- a/server/sonar-web/src/main/less/components/ui.less +++ b/server/sonar-web/src/main/less/components/ui.less @@ -168,7 +168,7 @@ > li:first-child { font-size: 18px; - font-weight: 300; + font-weight: 400; > a { color: @baseFontColor; diff --git a/server/sonar-web/src/main/less/pages.less b/server/sonar-web/src/main/less/pages.less index 4eee2d92015..0dce505a7a8 100644 --- a/server/sonar-web/src/main/less/pages.less +++ b/server/sonar-web/src/main/less/pages.less @@ -9,3 +9,4 @@ @import "pages/maintenance"; @import "pages/login"; @import "pages/api-documentation"; +@import "pages/overview"; diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less new file mode 100644 index 00000000000..da3055a4485 --- /dev/null +++ b/server/sonar-web/src/main/less/pages/overview.less @@ -0,0 +1,195 @@ +@import (reference) "../variables"; +@import (reference) "../mixins"; +@import (reference) "../init/type"; + +.overview { + display: flex; + width: 100%; + min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})"; +} + +.overview-main { + flex: 1; + box-sizing: border-box; + background-color: #fff; +} + +.overview-gate { + .clearfix; + padding: 50px 30px; +} + +.overview-gate-box { + float: left; + .size(120px, 70px); + padding: 10px; + .box-sizing(border-box); + line-height: 24px; + color: #fff; + font-size: 16px; + font-weight: 300; +} + +.overview-gate-box-error { background-color: @red; } + +.overview-gate-box-warn { background-color: @orange; } + +.overview-gate-box-ok { background-color: @green; } + +.overview-gate-conditions { + line-height: 70px; + font-size: 0; + white-space: nowrap; + overflow: hidden; + + & > li { + display: inline-block; + vertical-align: middle; + padding: 0 20px; + .box-sizing(border-box); + font-size: @baseFontSize; + line-height: 1; + } +} + +.overview-gate-condition-metric { + //color: mix(@baseFontColor, @barBackgroundColor, 70%); + font-size: 15px; + font-weight: 400; + //letter-spacing: 0.03em; +} + +.overview-gate-condition-value { + margin-top: 8px; + font-weight: 300; + font-size: 22px; + + i { + position: relative; + top: -1px; + } +} + +.overview-gate-condition-itself { + padding-left: 4px; + color: mix(@baseFontColor, @barBackgroundColor, 70%); + font-size: 13px; + font-weight: 400; +} + +.overview-gate-condition-level { + margin-top: 8px; +} + +.overview-leak { + padding: 50px 30px; + border-top: 1px solid @barBorderColor; + border-bottom: 1px solid @barBorderColor; +} + +.overview-title { + font-size: 18px; + font-weight: 400; + + & > .badge { + position: relative; + top: -2px; + margin-left: 15px; + padding: 8px 15px; + font-size: 16px; + letter-spacing: 0.04em; + } +} + +.overview-title + .overview-cards:not(:empty) { + margin-top: 20px; +} + +.overview-leak-period { + margin-left: 10px; + font-size: 14px; +} + +.overview-nutshell { + padding: 50px 30px; +} + +.overview-cards { + display: flex; +} + +.overview-card { + flex: 1 0 25%; + box-sizing: border-box; + + .overview-gate & { + flex-grow: 0; + } + + .overview-main & { + font-size: 14px; + } + + .measures-chart { + width: auto; + text-align: left; + } + + .measures-chart-indent { + padding-left: 67px; + } + + .measure-big + .measure-big { + margin-left: 30px; + } + + .list-inline { + margin-left: -10px; + margin-right: -10px; + + & > li { + padding-left: 10px; + padding-right: 10px; + } + } +} + +.overview-measure { + font-size: 28px; +} + +.overview-measure-label { + font-size: 16px; +} + +.overview-meta { + width: 240px; + padding: 50px 30px; + border-left: 1px solid @barBorderColor; + box-sizing: border-box; + background-color: @barBackgroundColor; + + .panel { + border: none !important; + } +} + +.overview-meta .overview-card { + width: auto; + margin-bottom: 30px; +} + +.overview-meta-description { + line-height: 1.5; +} + +.overview-meta-header { + color: #797979; +} + +.overview-meta-list { + & > li { + padding-bottom: 4px; + .text-ellipsis; + } +} diff --git a/server/sonar-web/src/main/less/variables.less b/server/sonar-web/src/main/less/variables.less index a92cdd372f2..b21f2353e3c 100644 --- a/server/sonar-web/src/main/less/variables.less +++ b/server/sonar-web/src/main/less/variables.less @@ -155,7 +155,7 @@ */ @navbarGlobalHeight: 30px; -@navbarContextHeight: 60px; +@navbarContextHeight: 65px; @pageFooterHeight: 60px; diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb new file mode 100644 index 00000000000..746bc651be8 --- /dev/null +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb @@ -0,0 +1,29 @@ +# +# SonarQube, open source software quality management tool. +# Copyright (C) 2008-2014 SonarSource +# mailto:contact AT sonarsource DOT com +# +# SonarQube 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. +# +# SonarQube 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. +# +class OverviewController < ApplicationController + before_filter :init_resource_for_user_role + + SECTION=Navigation::SECTION_RESOURCE + + def index + + end + +end diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb new file mode 100644 index 00000000000..41fe8714401 --- /dev/null +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb @@ -0,0 +1,181 @@ +<% + links_size = @resource.project_links.size + + profiles = [] + if @snapshot + qprofiles_measure = @snapshot.measure(Metric::QUALITY_PROFILES) + if qprofiles_measure && !qprofiles_measure.data.blank? + profiles = JSON.parse qprofiles_measure.data + end + end + profiles_size = profiles.size + + is_gate_default = false + gate = nil + gate_id = Property.value('sonar.qualitygate', @resource && @resource.id, nil) + unless gate_id + gate_id=Property.value('sonar.qualitygate', nil, nil) + is_gate_default = false || gate_id + end + if gate_id + gate = Internal.quality_gates.get(gate_id.to_i) + end +%> + +<% + if @snapshot + m = @snapshot.measure(Metric::QUALITY_GATE_DETAILS) + if m && !m.data.blank? + details = JSON.parse m.data + m.alert_status = details['level'] + raw_conditions = details['conditions'] + conditions = [] + missing_metric = false + raw_conditions.each do |condition| + if metric(condition['metric']).nil? + missing_metric = true + else + conditions << condition + end + end + alert_metric = metric(Metric::ALERT_STATUS) + end + end +%> + +<% content_for :extra_script do %> + <script> + (function () { + var component = { + id: '<%= escape_javascript @resource.uuid %>', + key: '<%= escape_javascript @resource.key %>', + description: '<%= escape_javascript @resource.description %>', + hasSnapshot: <%= @snapshot ? true : false %>, + periods: [ + <% + if @snapshot && @snapshot.project_snapshot.periods? + (1..5).each do |index| + if @snapshot.period_mode(index) + %> + { + index: '<%= index -%>', + mode: '<%= @snapshot.period_mode(index) -%>', + modeParam: '<%= @snapshot.period_param(index) -%>', + date: '<%= @snapshot.period_datetime(index).to_date.strftime('%FT%T%z') -%>' + }, + <% end %> + <% end %> + <% end %> + ], + links: [ + <% @resource.project_links.sort.each_with_index do |link, index| %> + { + name: '<%= escape_javascript link.name -%>', + type: '<%= escape_javascript link.link_type -%>', + href: '<%= escape_javascript link.href -%>' + }<% if index < links_size - 1 %>, <% end -%> + <% end %> + ], + profiles: [ + <% profiles.each_with_index do |profile, index| %> + { + name: '<%= escape_javascript profile['name'] -%>', + key: '<%= escape_javascript profile['key']-%>', + language: '<%= escape_javascript Api::Utils.language_name(profile['language']) -%>' + }<% if index < profiles_size - 1 %>, <% end -%> + <% end %> + ], + <% if gate %> + gate: { + name: '<%= escape_javascript gate.getName() -%>', + key: <%= escape_javascript gate_id -%>, + isDefault: <%= is_gate_default -%> + } + <% end %> + }; + + <% if m %> + var gate = { + level: '<%= m.alert_status -%>', + conditions: [ + <% conditions.sort_by {|condition| [ -condition['level'].length, metric(condition['metric']).short_name] }.each do |condition| %> + <% metric = metric(condition['metric']) %> + { + level: '<%= escape_javascript condition['level'] %>', + metric: { + name: '<%= escape_javascript metric.name %>', + type: '<%= escape_javascript metric.value_type %>' + }, + op: '<%= escape_javascript condition['op'] %>', + period: '<%= condition['period'] %>', + warning: '<%= escape_javascript condition['warning'] %>', + error: '<%= escape_javascript condition['error'] %>', + actual: '<%= escape_javascript condition['actual'] %>', + }, + <% end %> + ] + }; + <% else %> + var gate = null; + <% end %> + + var measures = { + <% if @snapshot %> + + // issues + <% if @snapshot.measure('sqale_rating') %> + sqaleRating: '<%= @snapshot.measure('sqale_rating').value -%>', + <% else %> + sqaleRating: 'A', + <% end %> + + // coverage + <% if @snapshot.measure('overall_coverage') %> + coverage: '<%= @snapshot.measure('overall_coverage').value -%>', + <% end %> + <% if @snapshot.measure('tests') %> + tests: '<%= @snapshot.measure('tests').value -%>', + <% end %> + + // duplications + duplications: '<%= @snapshot.measure('duplicated_lines_density').value -%>', + duplicatedLines: '<%= @snapshot.measure('duplicated_lines').value -%>', + duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').value -%>', + + // size + lines: '<%= @snapshot.measure('lines').value -%>', + files: '<%= @snapshot.measure('files').value -%>' + <% end %> + }; + + var leak = { + <% if @snapshot %> + // coverage + <% if @snapshot.measure('new_overall_coverage') %> + newCoverage: '<%= @snapshot.measure('new_overall_coverage').variation(3) -%>', + <% end %> + <% if @snapshot.measure('tests') %> + tests: '<%= @snapshot.measure('tests').variation(3) -%>', + <% end %> + + // duplications + duplications: '<%= @snapshot.measure('duplicated_lines_density').variation(3) -%>', + duplicatedLines: '<%= @snapshot.measure('duplicated_lines').variation(3) -%>', + duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').variation(3) -%>', + + // size + lines: '<%= @snapshot.measure('lines').variation(3) -%>', + files: '<%= @snapshot.measure('files').variation(3) -%>' + <% end %> + }; + + window.sonarqube.overview = { + component: component, + gate: gate, + measures: measures, + leak: leak + }; + })(); + </script> + <script src="<%= ApplicationController.root_context -%>/js/bundles/overview.js?v=<%= sonar_version -%>"></script> +<% end %> |