--- /dev/null
+import { getJSON } from '../helpers/request.js';
+
+export function getMeasures (componentKey, metrics) {
+ let url = baseUrl + '/api/resources/index';
+ let data = { resource: componentKey, metrics: metrics.join(',') };
+ return getJSON(url, data).then(r => {
+ let msr = r[0].msr || [];
+ let measures = {};
+ msr.forEach(measure => {
+ measures[measure.key] = measure.val;
+ });
+ return measures;
+ });
+}
--- /dev/null
+import _ from 'underscore';
+import React from 'react';
+import { BubbleChart } from '../../../components/charts/bubble-chart';
+import { getProjectUrl } from '../../../helpers/Url';
+import { getFiles } from '../../../api/components';
+import { formatMeasure } from '../formatting';
+
+
+const X_METRIC = 'complexity';
+const Y_METRIC = 'coverage';
+const SIZE_METRIC = 'sqale_index';
+const COMPONENTS_METRICS = [X_METRIC, Y_METRIC, SIZE_METRIC];
+const HEIGHT = 360;
+
+
+function formatInt (d) {
+ return window.formatMeasure(d, 'SHORT_INT');
+}
+
+function formatPercent (d) {
+ return window.formatMeasure(d, 'PERCENT');
+}
+
+function getMeasure (component, metric) {
+ return component.measures[metric] || 0;
+}
+
+
+export class CoverageBubbleChart extends React.Component {
+ constructor () {
+ super();
+ this.state = { loading: true, files: [] };
+ }
+
+ componentDidMount () {
+ this.requestFiles();
+ }
+
+ requestFiles () {
+ return getFiles(this.props.component.key, COMPONENTS_METRICS).then(r => {
+ let files = r.map(file => {
+ let measures = {};
+ (file.msr || []).forEach(measure => {
+ measures[measure.key] = measure.val;
+ });
+ return _.extend(file, { measures });
+ });
+ this.setState({ loading: false, files });
+ });
+ }
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ }
+
+ renderBubbleChart () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+
+ let items = this.state.files.map(component => {
+ return {
+ x: getMeasure(component, X_METRIC),
+ y: getMeasure(component, Y_METRIC),
+ size: getMeasure(component, SIZE_METRIC),
+ link: getProjectUrl(component.key)
+ };
+ });
+ let xGrid = this.state.files.map(component => component.measures[X_METRIC]);
+ let tooltips = this.state.files.map(component => {
+ let inner = [
+ component.name,
+ `Complexity: ${formatMeasure(getMeasure(component, X_METRIC), X_METRIC)}`,
+ `Coverage: ${formatMeasure(getMeasure(component, Y_METRIC), Y_METRIC)}`,
+ `Technical Debt: ${formatMeasure(getMeasure(component, SIZE_METRIC), SIZE_METRIC)}`
+ ].join('<br>');
+ return `<div class="text-left">${inner}</div>`;
+ });
+ return <BubbleChart items={items}
+ xGrid={xGrid}
+ tooltips={tooltips}
+ height={HEIGHT}
+ padding={[25, 30, 50, 60]}
+ formatXTick={formatInt}
+ formatYTick={formatPercent}/>;
+ }
+
+ render () {
+ return <div className="overview-bubble-chart overview-domain-dark">
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Project Files</h2>
+ <ul className="list-inline small">
+ <li>X: Complexity</li>
+ <li>Y: Coverage</li>
+ <li>Size: Technical Debt</li>
+ </ul>
+ </div>
+ <div>
+ {this.renderBubbleChart()}
+ </div>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+import { getMeasures } from '../../../api/measures';
+
+
+const METRICS = [
+ 'coverage',
+ 'line_coverage',
+ 'branch_coverage',
+ 'it_coverage',
+ 'it_line_coverage',
+ 'it_branch_coverage',
+ 'overall_coverage',
+ 'overall_line_coverage',
+ 'overall_branch_coverage'
+];
+
+
+function formatCoverage (value) {
+ return value != null ? window.formatMeasure(value, 'PERCENT') : '—';
+}
+
+
+export class CoverageDetails extends React.Component {
+ constructor () {
+ super();
+ this.state = { measures: {} };
+ }
+
+ componentDidMount () {
+ this.requestDetails();
+ }
+
+ requestDetails () {
+ return getMeasures(this.props.component.key, METRICS).then(measures => {
+ this.setState({ measures });
+ });
+ }
+
+ renderCoverage (coverage, lineCoverage, branchCoverage) {
+ return <table className="data zebra">
+ <tbody>
+ <tr>
+ <td>Coverage</td>
+ <td className="thin nowrap text-right">
+ {formatCoverage(coverage)}
+ </td>
+ </tr>
+ <tr>
+ <td>Line Coverage</td>
+ <td className="thin nowrap text-right">
+ {formatCoverage(lineCoverage)}
+ </td>
+ </tr>
+ <tr>
+ <td>Branch Coverage</td>
+ <td className="thin nowrap text-right">
+ {formatCoverage(branchCoverage)}
+ </td>
+ </tr>
+ </tbody>
+ </table>;
+ }
+
+ renderUTCoverage () {
+ if (this.state.measures['coverage'] == null) {
+ return null;
+ }
+ return <div className="big-spacer-top">
+ <h4 className="spacer-bottom">Unit Tests</h4>
+ {this.renderCoverage(
+ this.state.measures['coverage'],
+ this.state.measures['line_coverage'],
+ this.state.measures['branch_coverage'])}
+ </div>;
+ }
+
+ renderITCoverage () {
+ if (this.state.measures['it_coverage'] == null) {
+ return null;
+ }
+ return <div className="big-spacer-top">
+ <h4 className="spacer-bottom">Integration Tests</h4>
+ {this.renderCoverage(
+ this.state.measures['it_coverage'],
+ this.state.measures['it_line_coverage'],
+ this.state.measures['it_branch_coverage'])}
+ </div>;
+ }
+
+ renderOverallCoverage () {
+ if (this.state.measures['coverage'] == null ||
+ this.state.measures['it_coverage'] == null ||
+ this.state.measures['overall_coverage'] == null) {
+ return null;
+ }
+ return <div className="big-spacer-top">
+ <h4 className="spacer-bottom">Overall</h4>
+ {this.renderCoverage(
+ this.state.measures['overall_coverage'],
+ this.state.measures['overall_line_coverage'],
+ this.state.measures['overall_branch_coverage'])}
+ </div>;
+ }
+
+ render () {
+ return <div className="overview-domain-section">
+ <h2 className="overview-title">Coverage Details</h2>
+ {this.renderUTCoverage()}
+ {this.renderITCoverage()}
+ {this.renderOverallCoverage()}
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+
+import { CoverageDetails } from './coverage-details';
+import { TestsDetails } from './tests-details';
+import { CoverageBubbleChart } from './bubble-chart';
+import { CoverageTimeline } from './timeline';
+import { CoverageTreemap } from './treemap';
+
+import { getSeverities, getTags, getAssignees } from '../../../api/issues';
+
+
+export default class CoverageDomain extends React.Component {
+ render () {
+ return <div className="overview-domain">
+
+ <CoverageTimeline {...this.props}/>
+
+ <div className="flex-columns">
+ <div className="flex-column flex-column-half">
+ <CoverageDetails {...this.props}/>
+ </div>
+ <div className="flex-column flex-column-half">
+ <TestsDetails {...this.props}/>
+ </div>
+ </div>
+
+ <CoverageBubbleChart {...this.props}/>
+ <CoverageTreemap {...this.props}/>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+import { getMeasures } from '../../../api/measures';
+import { formatMeasure } from '../formatting';
+
+
+const METRICS = [
+ 'tests',
+ 'skipped_tests',
+ 'test_errors',
+ 'test_failures',
+ 'test_execution_time',
+ 'test_success_density'
+];
+
+
+function format (value, metric) {
+ return value != null ? formatMeasure(value, metric) : '—';
+}
+
+
+export class TestsDetails extends React.Component {
+ constructor () {
+ super();
+ this.state = { measures: {} };
+ }
+
+ componentDidMount () {
+ this.requestDetails();
+ }
+
+ requestDetails () {
+ return getMeasures(this.props.component.key, METRICS).then(measures => {
+ this.setState({ measures });
+ });
+ }
+
+ render () {
+ return <div className="overview-domain-section">
+ <h2 className="overview-title">Tests Details</h2>
+ <table className="data zebra">
+ <tbody>
+ <tr>
+ <td>Tests</td>
+ <td className="thin nowrap text-right">
+ {format(this.state.measures['tests'], 'tests')}
+ </td>
+ </tr>
+ <tr>
+ <td>Skipped Tests</td>
+ <td className="thin nowrap text-right">
+ {format(this.state.measures['skipped_tests'], 'skipped_tests')}
+ </td>
+ </tr>
+ <tr>
+ <td>Test Errors</td>
+ <td className="thin nowrap text-right">
+ {format(this.state.measures['test_errors'], 'test_errors')}
+ </td>
+ </tr>
+ <tr>
+ <td>Test Failures</td>
+ <td className="thin nowrap text-right">
+ {format(this.state.measures['test_failures'], 'test_failures')}
+ </td>
+ </tr>
+ <tr>
+ <td>Tests Execution Time</td>
+ <td className="thin nowrap text-right">
+ {format(this.state.measures['test_execution_time'], 'test_execution_time')}
+ </td>
+ </tr>
+ <tr>
+ <td>Tests Success</td>
+ <td className="thin nowrap text-right">
+ {format(this.state.measures['test_success_density'], 'test_success_density')}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>;
+ }
+}
--- /dev/null
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { LineChart } from '../../../components/charts/line-chart';
+import { formatMeasure } from '../formatting';
+import { getTimeMachineData } from '../../../api/time-machine';
+import { getEvents } from '../../../api/events';
+
+
+const COVERAGE_METRICS = [
+ 'coverage',
+ 'line_coverage',
+ 'branch_coverage',
+ 'lines_to_cover',
+ 'conditions_to_cover',
+ 'uncovered_lines',
+ 'uncovered_conditions',
+
+ 'it_coverage',
+ 'it_line_coverage',
+ 'it_branch_coverage',
+ 'it_lines_to_cover',
+ 'it_conditions_to_cover',
+ 'it_uncovered_lines',
+ 'it_uncovered_conditions',
+
+ 'overall_coverage',
+ 'overall_line_coverage',
+ 'overall_branch_coverage',
+ 'overall_lines_to_cover',
+ 'overall_conditions_to_cover',
+ 'overall_uncovered_lines',
+ 'overall_uncovered_conditions'
+];
+
+const TESTS_METRICS = [
+ 'tests',
+ 'skipped_tests',
+ 'test_errors',
+ 'test_failures',
+ 'test_execution_time',
+ 'test_success_density'
+];
+
+const HEIGHT = 280;
+
+
+export class CoverageTimeline extends React.Component {
+ constructor () {
+ super();
+ this.state = { loading: true, currentMetric: COVERAGE_METRICS[0] };
+ }
+
+ componentDidMount () {
+ Promise.all([
+ this.requestTimeMachineData(),
+ this.requestEvents()
+ ]).then(() => this.setState({ loading: false }));
+ }
+
+ requestTimeMachineData () {
+ return getTimeMachineData(this.props.component.key, this.state.currentMetric).then(r => {
+ let snapshots = r[0].cells.map(cell => {
+ return { date: moment(cell.d).toDate(), value: cell.v[0] };
+ });
+ this.setState({ snapshots });
+ });
+ }
+
+ requestEvents () {
+ return getEvents(this.props.component.key, 'Version').then(r => {
+ let events = r.map(event => {
+ return { version: event.n, date: moment(event.dt).toDate() };
+ });
+ events = _.sortBy(events, 'date');
+ this.setState({ events });
+ });
+ }
+
+ prepareEvents () {
+ let events = this.state.events;
+ let snapshots = this.state.snapshots;
+ return events
+ .map(event => {
+ let snapshot = snapshots.find(s => s.date.getTime() === event.date.getTime());
+ event.value = snapshot && snapshot.value;
+ return event;
+ })
+ .filter(event => event.value != null);
+ }
+
+ handleMetricChange () {
+ let metric = React.findDOMNode(this.refs.metricSelect).value;
+ this.setState({ currentMetric: metric }, this.requestTimeMachineData);
+ }
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ }
+
+ renderLineChart () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+
+ let events = this.prepareEvents();
+
+ let data = events.map((event, index) => {
+ return { x: index, y: event.value };
+ });
+
+ let xTicks = events.map(event => event.version.substr(0, 6));
+
+ let xValues = events.map(event => formatMeasure(event.value, this.state.currentMetric));
+
+ // TODO use leak period
+ let backdropConstraints = [
+ this.state.events.length - 2,
+ this.state.events.length - 1
+ ];
+
+ return <LineChart data={data}
+ xTicks={xTicks}
+ xValues={xValues}
+ backdropConstraints={backdropConstraints}
+ height={HEIGHT}
+ interpolate="linear"
+ padding={[25, 30, 50, 30]}/>;
+ }
+
+ renderTimelineMetricSelect () {
+ if (this.state.loading) {
+ return null;
+ }
+
+ let issueOptions = COVERAGE_METRICS
+ .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
+ let debtOptions = TESTS_METRICS
+ .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
+
+ return <select ref="metricSelect"
+ className="overview-timeline-select"
+ onChange={this.handleMetricChange.bind(this)}
+ value={this.state.currentMetric}>
+ <optgroup label="Coverage">{issueOptions}</optgroup>
+ <optgroup label="Tests">{debtOptions}</optgroup>
+ </select>;
+ }
+
+ render () {
+ return <div className="overview-timeline overview-domain-dark">
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Project History</h2>
+ {this.renderTimelineMetricSelect()}
+ </div>
+ <div>
+ {this.renderLineChart()}
+ </div>
+ </div>;
+ }
+}
--- /dev/null
+import _ from 'underscore';
+import d3 from 'd3';
+import React from 'react';
+
+import { Treemap } from '../../../components/charts/treemap';
+import { formatMeasure } from '../formatting';
+import { getChildren } from '../../../api/components';
+
+const COMPONENTS_METRICS = [
+ 'lines',
+ 'coverage'
+];
+
+const HEIGHT = 360;
+
+const COLORS_5 = ['#ee0000', '#f77700', '#ffee00', '#80cc00', '#00aa00'];
+
+export class CoverageTreemap extends React.Component {
+ constructor () {
+ super();
+ this.state = { loading: true, components: [] };
+ }
+
+ componentDidMount () {
+ this.requestComponents();
+ }
+
+ requestComponents () {
+ return getChildren(this.props.component.key, COMPONENTS_METRICS).then(r => {
+ let components = r.map(component => {
+ let measures = {};
+ (component.msr || []).forEach(measure => {
+ measures[measure.key] = measure.val;
+ });
+ return _.extend(component, { measures });
+ });
+ this.setState({ loading: false, components });
+ });
+ }
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ }
+
+ renderTreemap () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+
+ let colorScale = d3.scale.linear().domain([0, 25, 50, 75, 100]);
+ colorScale.range(COLORS_5);
+
+ let items = this.state.components.map(component => {
+ let coverage = component.measures['coverage'];
+ console.log(coverage);
+ return {
+ size: component.measures['lines'],
+ color: coverage != null ? colorScale(coverage) : '#777'
+ };
+ });
+ let labels = this.state.components.map(component => component.name);
+ let tooltips = this.state.components.map(component => {
+ let inner = [
+ component.name,
+ `Lines: ${formatMeasure(component.measures['lines'], 'lines')}`,
+ `Coverage: ${formatMeasure(component.measures['coverage'], 'coverage')}`
+ ].join('<br>');
+ return `<div class="text-left">${inner}</div>`;
+ });
+ return <Treemap items={items} labels={labels} tooltips={tooltips} height={HEIGHT}/>;
+ }
+
+ render () {
+ return <div className="overview-domain-section overview-treemap">
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Project Components</h2>
+ <ul className="list-inline small">
+ <li>Size: Lines</li>
+ <li>Color: Coverage</li>
+ </ul>
+ </div>
+ <div>
+ {this.renderTreemap()}
+ </div>
+ </div>;
+ }
+}
'sqale_index': 'SHORT_WORK_DUR',
'sqale_debt_ratio': 'PERCENT',
'sqale_rating': 'RATING',
- 'lines': 'SHORT_INT'
+ 'lines': 'SHORT_INT',
+
+ 'coverage': 'PERCENT',
+ 'line_coverage': 'PERCENT',
+ 'branch_coverage': 'PERCENT',
+ 'lines_to_cover': 'SHORT_INT',
+ 'conditions_to_cover': 'SHORT_INT',
+ 'uncovered_lines': 'SHORT_INT',
+ 'uncovered_conditions': 'SHORT_INT',
+
+ 'it_coverage': 'PERCENT',
+ 'it_line_coverage': 'PERCENT',
+ 'it_branch_coverage': 'PERCENT',
+ 'it_lines_to_cover': 'SHORT_INT',
+ 'it_conditions_to_cover': 'SHORT_INT',
+ 'it_uncovered_lines': 'SHORT_INT',
+ 'it_uncovered_conditions': 'SHORT_INT',
+
+ 'overall_coverage': 'PERCENT',
+ 'overall_line_coverage': 'PERCENT',
+ 'overall_branch_coverage': 'PERCENT',
+ 'overall_lines_to_cover': 'SHORT_INT',
+ 'overall_conditions_to_cover': 'SHORT_INT',
+ 'overall_uncovered_lines': 'SHORT_INT',
+ 'overall_uncovered_conditions': 'SHORT_INT',
+
+ 'tests': 'SHORT_INT',
+ 'skipped_tests': 'SHORT_INT',
+ 'test_errors': 'SHORT_INT',
+ 'test_failures': 'SHORT_INT',
+ 'test_execution_time': 'MILLISEC',
+ 'test_success_density': 'PERCENT',
+
+ 'complexity': 'INT'
};
export function formatMeasure (value, metric) {
return null;
}
+ let active = this.props.section === 'coverage';
+
return (
- <Card>
+ <Card linkTo="coverage" active={active} onRoute={this.props.onRoute}>
<div className="measures">
<div className="measures-chart">
<Donut data={donutData} size="47"/>
import offset from 'document-offset';
import GeneralMain from './general/main';
import IssuesMain from './issues/main';
+import CoverageMain from './coverage/main';
import Meta from './meta';
export default class Overview extends React.Component {
case 'issues':
child = <IssuesMain {...this.props}/>;
break;
+ case 'coverage':
+ child = <CoverageMain {...this.props}/>;
+ break;
default:
child = null;
}
return l10nKey !== result ? result : value;
};
+
+ /**
+ * Format a milliseconds measure
+ * @param {number} value
+ */
+ var millisecondsFormatter = function (value) {
+ return value + ' ms';
+ };
+
/**
* Format a measure according to its type
* @param measure
'WORK_DUR': durationFormatter,
'SHORT_WORK_DUR': shortDurationFormatter,
'RATING': ratingFormatter,
- 'LEVEL': levelFormatter
+ 'LEVEL': levelFormatter,
+ 'MILLISEC': millisecondsFormatter
};
if (measure != null && type != null) {
formatted = formatters[type] != null ? formatters[type](measure) : measure;
.flex-column-third {
width: ~"calc(100% / 3)";
}
+
+.flex-column-two-thirds {
+ width: ~"calc(100% / 3 * 2)";
+}