Переглянути джерело

SONAR-6331 add project overview page

tags/5.3-RC1
Stas Vilchik 8 роки тому
джерело
коміт
187f501d31
42 змінених файлів з 1418 додано та 8 видалено
  1. 2
    1
      server/sonar-web/package.json
  2. 26
    0
      server/sonar-web/src/main/js/apps/overview/app.js
  3. 7
    0
      server/sonar-web/src/main/js/apps/overview/card.js
  4. 7
    0
      server/sonar-web/src/main/js/apps/overview/cards.js
  5. 13
    0
      server/sonar-web/src/main/js/apps/overview/empty.js
  6. 33
    0
      server/sonar-web/src/main/js/apps/overview/gate-condition.js
  7. 21
    0
      server/sonar-web/src/main/js/apps/overview/gate-conditions.js
  8. 14
    0
      server/sonar-web/src/main/js/apps/overview/gate-empty.js
  9. 25
    0
      server/sonar-web/src/main/js/apps/overview/gate.js
  10. 34
    0
      server/sonar-web/src/main/js/apps/overview/helpers/donut.js
  11. 95
    0
      server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js
  12. 8
    0
      server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js
  13. 11
    0
      server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js
  14. 11
    0
      server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js
  15. 11
    0
      server/sonar-web/src/main/js/apps/overview/helpers/measure.js
  16. 18
    0
      server/sonar-web/src/main/js/apps/overview/helpers/period-label.js
  17. 8
    0
      server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js
  18. 12
    0
      server/sonar-web/src/main/js/apps/overview/helpers/rating.js
  19. 46
    0
      server/sonar-web/src/main/js/apps/overview/leak-coverage.js
  20. 42
    0
      server/sonar-web/src/main/js/apps/overview/leak-dups.js
  21. 69
    0
      server/sonar-web/src/main/js/apps/overview/leak-issues.js
  22. 30
    0
      server/sonar-web/src/main/js/apps/overview/leak-size.js
  23. 33
    0
      server/sonar-web/src/main/js/apps/overview/leak.js
  24. 102
    0
      server/sonar-web/src/main/js/apps/overview/main.js
  25. 68
    0
      server/sonar-web/src/main/js/apps/overview/meta.js
  26. 47
    0
      server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js
  27. 47
    0
      server/sonar-web/src/main/js/apps/overview/nutshell-dups.js
  28. 68
    0
      server/sonar-web/src/main/js/apps/overview/nutshell-issues.js
  29. 35
    0
      server/sonar-web/src/main/js/apps/overview/nutshell-size.js
  30. 23
    0
      server/sonar-web/src/main/js/apps/overview/nutshell.js
  31. 1
    1
      server/sonar-web/src/main/js/helpers/Url.js
  32. 1
    1
      server/sonar-web/src/main/js/libs/application.js
  33. 7
    1
      server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
  34. 2
    2
      server/sonar-web/src/main/less/components/measures.less
  35. 1
    0
      server/sonar-web/src/main/less/components/navbar.less
  36. 1
    1
      server/sonar-web/src/main/less/components/ui.less
  37. 1
    0
      server/sonar-web/src/main/less/pages.less
  38. 195
    0
      server/sonar-web/src/main/less/pages/overview.less
  39. 1
    1
      server/sonar-web/src/main/less/variables.less
  40. 29
    0
      server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb
  41. 181
    0
      server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb
  42. 32
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 1
server/sonar-web/package.json Переглянути файл

@@ -52,7 +52,8 @@
},
"browserify-shim": {
"jquery": "global:jQuery",
"underscore": "global:_"
"underscore": "global:_",
"d3": "global:d3"
},
"browserify": {
"transform": [

+ 26
- 0
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));

+ 7
- 0
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>;
}
});

+ 7
- 0
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>;
}
});

+ 13
- 0
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>
);
}
});

+ 33
- 0
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}/>&nbsp;
<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>&nbsp;
<span className="overview-gate-condition-itself">
{window.t('quality_gates.operator', this.props.condition.op, 'short')}&nbsp;
<Measure value={threshold} type={this.props.condition.metric.type}/>
</span>
</div>
</div>
);
}
});

+ 21
- 0
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>;
}
});

+ 14
- 0
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>
);
}
});

+ 25
- 0
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>
);
}
});

+ 34
- 0
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>;
}
});

+ 95
- 0
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>;
}
});

+ 8
- 0
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>;
}
});

+ 11
- 0
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>;
}
});

+ 11
- 0
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>;
}
});

+ 11
- 0
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>;
}
});

+ 18
- 0
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();
};

+ 8
- 0
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>;
}
});

+ 12
- 0
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>;
}
});

+ 46
- 0
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>&nbsp;
<span>{window.t('overview.metric.tests')}</span>
</li>
</ul>
</Card>
);
}
});

+ 42
- 0
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>&nbsp;
<span>{window.t('overview.metric.duplicated_lines')}</span>
</li>
</ul>
</Card>
);
}
});

+ 69
- 0
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>&nbsp;
<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>&nbsp;
<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>&nbsp;
<IssuesLink component={this.props.component.key}
params={{ resolved: 'false', createdAfter: periodDate, statuses: 'OPEN,REOPENED' }}>
<MeasureVariation value={issuesToReview} type="SHORT_INT"/>
</IssuesLink>
</li>
</ul>
</Card>
);
}
});

+ 30
- 0
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>
);
}
});

+ 33
- 0
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>
);
}
});

+ 102
- 0
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>
);
}
});

+ 68
- 0
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>
);
}
});

+ 47
- 0
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>&nbsp;
<span>{window.t('overview.metric.tests')}</span>
</li>
</ul>
</Card>
);
}
});

+ 47
- 0
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>&nbsp;
<span>{window.t('overview.metric.duplicated_lines')}</span>
</li>
</ul>
</Card>
);
}
});

+ 68
- 0
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>&nbsp;
<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>&nbsp;
<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>&nbsp;
<IssuesLink component={this.props.component.key} params={{ resolved: 'false', statuses: 'OPEN,REOPENED' }}>
<Measure value={issuesToReview} type="SHORT_INT"/>
</IssuesLink>
</li>
</ul>
</Card>
);
}
});

+ 35
- 0
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>
);
}
});

+ 23
- 0
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>
);
}
});

+ 1
- 1
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)}`;
}

+ 1
- 1
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);

+ 7
- 1
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()}

+ 2
- 2
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 {

+ 1
- 0
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 {

+ 1
- 1
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;

+ 1
- 0
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";

+ 195
- 0
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;
}
}

+ 1
- 1
server/sonar-web/src/main/less/variables.less Переглянути файл

@@ -155,7 +155,7 @@
*/

@navbarGlobalHeight: 30px;
@navbarContextHeight: 60px;
@navbarContextHeight: 65px;
@pageFooterHeight: 60px;



+ 29
- 0
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

+ 181
- 0
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 %>

+ 32
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -3092,3 +3092,35 @@ background_tasks.in_progress_duration=Duration of the current task in progress.
#
#------------------------------------------------------------------------------
system.log_level.warning=Current level has performance impacts, please make sure to get back to INFO level once your investigation is done. Please note that when you restart the server, the level will automatically be reset to INFO.



#------------------------------------------------------------------------------
#
# OVERVIEW
#
#------------------------------------------------------------------------------
overview.quality_gate=Quality Gate
overview.quality_profiles=Quality Profiles
overview.water_leak=Water Leak
overview.project_in_a_nutshell=Project In a Nutshell

overview.metric.new_coverage=New Coverage
overview.metric.tests=tests
overview.metric.duplications=Duplications
overview.metric.duplicated_lines=lines
overview.metric.debt=Debt
overview.metric.issues=Issues
overview.metric.new_debt=New Debt
overview.metric.new_issues=New Issues
overview.metric.lines=Lines
overview.metric.files=Files
overview.metric.coverage=Coverage

overview.period.previous_version=since {0}
overview.period.previous_analysis=since previous analysis
overview.period.days=last {0} days

overview.gate.ERROR=Failed
overview.gate.WARN=Warning
overview.gate.OK=Passed

Завантаження…
Відмінити
Зберегти