},
"browserify-shim": {
"jquery": "global:jQuery",
- "underscore": "global:_"
+ "underscore": "global:_",
+ "d3": "global:d3"
},
"browserify": {
"transform": [
--- /dev/null
+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));
--- /dev/null
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ return <li className="overview-card">{this.props.children}</li>;
+ }
+});
--- /dev/null
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ return <ul className="overview-cards">{this.props.children}</ul>;
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+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();
+};
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+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>;
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
--- /dev/null
+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>
+ );
+ }
+});
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)}`;
}
*/
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);
return params.period ? `&period=${params.period}` : '';
},
- renderOverviewLink() {
+ renderMainDashboardLink() {
if (_.size(this.props.component.dashboards) === 0) {
return null;
}
});
},
+ 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');
render() {
return (
<ul className="nav navbar-nav nav-tabs">
+ {this.renderMainDashboardLink()}
{this.renderOverviewLink()}
{this.renderComponentsLink()}
{this.renderComponentIssuesLink()}
.measure-value {
color: @darkBlue;
font-size: @bigFontSize;
- font-weight: 300;
+ font-weight: 400;
}
.measure-bar {
.measure-name {
margin-top: 2px;
font-size: 15px;
- font-weight: 300;
+ font-weight: 400;
}
.measure-value {
top: @navbarGlobalHeight;
z-index: @navbar-context-z-index;
height: @navbarContextHeight;
+ padding-top: 5px;
background-color: @navbarContextBackground;
.nav-tabs {
> li:first-child {
font-size: 18px;
- font-weight: 300;
+ font-weight: 400;
> a {
color: @baseFontColor;
@import "pages/maintenance";
@import "pages/login";
@import "pages/api-documentation";
+@import "pages/overview";
--- /dev/null
+@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;
+ }
+}
*/
@navbarGlobalHeight: 30px;
-@navbarContextHeight: 60px;
+@navbarContextHeight: 65px;
@pageFooterHeight: 60px;
--- /dev/null
+#
+# 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
--- /dev/null
+<%
+ 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 %>
#
#------------------------------------------------------------------------------
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