--- /dev/null
+import { getJSON } from '../helpers/request.js';
+
+export function getLanguages () {
+ let url = baseUrl + '/api/languages/list';
+ return getJSON(url).then(r => r.languages);
+}
let msr = r[0].msr || [];
let measures = {};
msr.forEach(measure => {
- measures[measure.key] = measure.val;
+ measures[measure.key] = measure.val || measure.data;
});
return measures;
});
--- /dev/null
+import _ from 'underscore';
+import { getJSON } from '../helpers/request.js';
+
+export function getMetrics () {
+ let url = baseUrl + '/api/metrics/search';
+ let data = { ps: 9999 };
+ return getJSON(url, data).then(r => r.metrics);
+}
-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;
-}
+import { DomainBubbleChart } from '../domain/bubble-chart';
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>;
+ return <DomainBubbleChart {...this.props}
+ xMetric="complexity"
+ yMetric="coverage"
+ sizeMetrics={['sqale_index']}/>;
}
}
import { CoverageTimeline } from './timeline';
import { CoverageTreemap } from './treemap';
-import { getSeverities, getTags, getAssignees } from '../../../api/issues';
-
-export default class CoverageDomain extends React.Component {
+export default class extends React.Component {
render () {
return <div className="overview-domain">
-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';
+import { DomainTimeline } from '../domain/timeline';
+import { filterMetricsForDomains } from '../helpers/metrics';
-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 DOMAINS = [
+ 'Tests',
+ 'Tests (Integration)',
+ 'Tests (Overall)'
];
-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>;
+ return <DomainTimeline {...this.props}
+ initialMetric="coverage"
+ metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
}
}
-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';
+import { DomainTreemap } from '../domain/treemap';
-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}/>;
- }
+export class CoverageTreemap extends React.Component {
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>;
+ let scale = d3.scale.linear()
+ .domain([0, 25, 50, 75, 100])
+ .range(COLORS_5);
+ return <DomainTreemap {...this.props}
+ sizeMetric="ncloc"
+ colorMetric="coverage"
+ scale={scale}/>;
}
}
--- /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';
+
+
+const HEIGHT = 360;
+
+
+function getMeasure (component, metric) {
+ return component.measures[metric] || 0;
+}
+
+
+export class DomainBubbleChart extends React.Component {
+ constructor (props) {
+ super(props);
+ this.state = {
+ loading: true,
+ files: [],
+ xMetric: this.getMetricObject(props.metrics, props.xMetric),
+ yMetric: this.getMetricObject(props.metrics, props.yMetric),
+ sizeMetrics: props.sizeMetrics.map(this.getMetricObject.bind(null, props.metrics))
+ };
+ }
+
+ componentDidMount () {
+ this.requestFiles();
+ }
+
+ requestFiles () {
+ let metrics = [].concat(this.props.xMetric, this.props.yMetric, this.props.sizeMetrics);
+ return getFiles(this.props.component.key, 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 });
+ });
+ }
+
+ getMetricObject (metrics, metricKey) {
+ return _.findWhere(metrics, { key: metricKey });
+ }
+
+ getSizeMetricsValue (component) {
+ return this.props.sizeMetrics.reduce((previousValue, currentValue) => {
+ return previousValue + getMeasure(component, currentValue);
+ }, 0);
+ }
+
+ getSizeMetricsTitle () {
+ return this.state.sizeMetrics.map(metric => metric.name).join(' & ');
+ }
+
+ getTooltip (component) {
+ let sizeMetricsTitle = this.getSizeMetricsTitle();
+ let sizeMetricsType = this.state.sizeMetrics[0].type;
+
+ let inner = [
+ component.name,
+ `${this.state.xMetric.name}: ${window.formatMeasure(getMeasure(component, this.props.xMetric), this.state.xMetric.type)}`,
+ `${this.state.yMetric.name}: ${window.formatMeasure(getMeasure(component, this.props.yMetric), this.state.yMetric.type)}`,
+ `${sizeMetricsTitle}: ${window.formatMeasure(this.getSizeMetricsValue(component), sizeMetricsType)}`
+ ].join('<br>');
+ return `<div class="text-left">${inner}</div>`;
+ }
+
+ 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, this.props.xMetric),
+ y: getMeasure(component, this.props.yMetric),
+ size: this.getSizeMetricsValue(component),
+ link: getProjectUrl(component.key),
+ tooltip: this.getTooltip(component)
+ };
+ });
+ let xGrid = this.state.files.map(component => getMeasure(component, this.props.xMetric));
+ let formatXTick = (tick) => window.formatMeasure(tick, this.state.xMetric.type);
+ let formatYTick = (tick) => window.formatMeasure(tick, this.state.yMetric.type);
+ return <BubbleChart items={items}
+ xGrid={xGrid}
+ height={HEIGHT}
+ padding={[25, 30, 50, 60]}
+ formatXTick={formatXTick}
+ formatYTick={formatYTick}/>;
+ }
+
+ 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: {this.state.xMetric.name}</li>
+ <li>Y: {this.state.yMetric.name}</li>
+ <li>Size: {this.getSizeMetricsTitle()}</li>
+ </ul>
+ </div>
+ <div>
+ {this.renderBubbleChart()}
+ </div>
+ </div>;
+ }
+}
+
+DomainBubbleChart.propTypes = {
+ xMetric: React.PropTypes.string.isRequired,
+ yMetric: React.PropTypes.string.isRequired,
+ sizeMetrics: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
+};
--- /dev/null
+import _ from 'underscore';
+import React from 'react';
+
+import DrilldownLink from '../helpers/drilldown-link';
+import { getMeasures } from '../../../api/measures';
+
+
+function format (value, type) {
+ return value != null ? window.formatMeasure(value, type) : '—';
+}
+
+
+export class DomainMeasuresList extends React.Component {
+ constructor () {
+ super();
+ this.state = { measures: {} };
+ }
+
+ componentDidMount () {
+ this.requestDetails();
+ }
+
+ requestDetails () {
+ return getMeasures(this.props.component.key, this.props.metricsToDisplay).then(measures => {
+ this.setState({ measures });
+ });
+ }
+
+ getMetricObject (metricKey) {
+ return _.findWhere(this.props.metrics, { key: metricKey });
+ }
+
+ render () {
+ let rows = this.props.metricsToDisplay.map(metric => {
+ let metricObject = this.getMetricObject(metric);
+ return <tr key={metric}>
+ <td>{metricObject.name}</td>
+ <td className="thin nowrap text-right">
+ <DrilldownLink component={this.props.component.key} metric={metric}>
+ {format(this.state.measures[metric], metricObject.type)}
+ </DrilldownLink>
+ </td>
+ </tr>;
+ });
+ return <table className="data zebra">
+ <tbody>{rows}</tbody>
+ </table>;
+ }
+}
+
+DomainMeasuresList.propTypes = {
+ metricsToDisplay: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
+};
--- /dev/null
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { LineChart } from '../../../components/charts/line-chart';
+import { getTimeMachineData } from '../../../api/time-machine';
+import { getEvents } from '../../../api/events';
+
+
+const HEIGHT = 280;
+
+
+export class DomainTimeline extends React.Component {
+ constructor (props) {
+ super(props);
+ this.state = { loading: true, currentMetric: props.initialMetric };
+ }
+
+ 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);
+ }
+
+ groupMetricsByDomain () {
+ return _.sortBy(
+ _.map(
+ _.groupBy(this.props.metrics, 'domain'),
+ (metricList, domain) => {
+ return {
+ domain: domain,
+ metrics: _.sortBy(metricList, 'name')
+ };
+ }),
+ 'domain'
+ );
+ }
+
+ 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 currentMetricType = _.findWhere(this.props.metrics, { key: this.state.currentMetric }).type;
+
+ 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 => window.formatMeasure(event.value, currentMetricType));
+
+ // 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]}/>;
+ }
+
+ renderMetricOption (metric) {
+ return <option key={metric.key} value={metric.key}>{metric.name}</option>;
+ }
+
+ renderTimelineMetricSelect () {
+ if (this.state.loading) {
+ return null;
+ }
+ let groupedMetrics = this.groupMetricsByDomain();
+ let inner;
+ if (groupedMetrics.length > 1) {
+ inner = groupedMetrics.map(metricGroup => {
+ let options = metricGroup.metrics.map(this.renderMetricOption);
+ return <optgroup key={metricGroup.domain} label={metricGroup.domain}>{options}</optgroup>;
+ });
+ } else {
+ inner = groupedMetrics[0].metrics.map(this.renderMetricOption);
+ }
+ return <select ref="metricSelect"
+ className="overview-timeline-select"
+ onChange={this.handleMetricChange.bind(this)}
+ value={this.state.currentMetric}>{inner}</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>;
+ }
+}
+
+DomainTimeline.propTypes = {
+ metrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ initialMetric: React.PropTypes.string.isRequired
+};
--- /dev/null
+import _ from 'underscore';
+import React from 'react';
+
+import { Treemap } from '../../../components/charts/treemap';
+import { getChildren } from '../../../api/components';
+
+const HEIGHT = 360;
+
+
+export class DomainTreemap extends React.Component {
+ constructor (props) {
+ super(props);
+ this.state = {
+ loading: true,
+ files: [],
+ sizeMetric: this.getMetricObject(props.metrics, props.sizeMetric),
+ colorMetric: props.colorMetric ? this.getMetricObject(props.metrics, props.colorMetric) : null
+ };
+ }
+
+ componentDidMount () {
+ this.requestComponents();
+ }
+
+ requestComponents () {
+ let metrics = [this.props.sizeMetric, this.props.colorMetric];
+ return getChildren(this.props.component.key, 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 });
+ });
+ }
+
+ getMetricObject (metrics, metricKey) {
+ return _.findWhere(metrics, { key: metricKey });
+ }
+
+ getTooltip (component) {
+ let inner = [
+ component.name,
+ `${this.state.sizeMetric.name}: ${window.formatMeasure(component.measures[this.props.sizeMetric], this.state.sizeMetric.type)}`
+ ];
+ if (this.state.colorMetric) {
+ inner.push(`${this.state.colorMetric.name}: ${window.formatMeasure(component.measures[this.props.colorMetric], this.state.colorMetric.type)}`);
+ }
+ inner = inner.join('<br>');
+ return `<div class="text-left">${inner}</div>`;
+ }
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ }
+
+ renderTreemap () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+
+ // TODO filter out zero sized components
+ let items = this.state.components.map(component => {
+ let colorMeasure = this.props.colorMetric ? component.measures[this.props.colorMetric] : null;
+ return {
+ size: component.measures[this.props.sizeMetric],
+ color: colorMeasure != null ? this.props.scale(colorMeasure) : '#777',
+ tooltip: this.getTooltip(component),
+ label: component.name
+ };
+ });
+ return <Treemap items={items} height={HEIGHT}/>;
+ }
+
+ render () {
+ let color = this.props.colorMetric ? <li>Color: {this.state.colorMetric.name}</li> : null;
+ 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: {this.state.sizeMetric.name}</li>
+ {color}
+ </ul>
+ </div>
+ <div>
+ {this.renderTreemap()}
+ </div>
+ </div>;
+ }
+}
+
+DomainTreemap.propTypes = {
+ sizeMetric: React.PropTypes.string.isRequired,
+ colorMetric: React.PropTypes.string,
+ scale: React.PropTypes.func
+};
-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 = 'ncloc';
-const Y_METRIC = 'duplicated_blocks';
-const SIZE_METRIC = 'duplicated_lines';
-const COMPONENTS_METRICS = [X_METRIC, Y_METRIC, SIZE_METRIC];
-const HEIGHT = 360;
-
-
-function formatInt (d) {
- return window.formatMeasure(d, 'SHORT_INT');
-}
-
-function getMeasure (component, metric) {
- return component.measures[metric] || 0;
-}
+import { DomainBubbleChart } from '../domain/bubble-chart';
export class DuplicationsBubbleChart 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,
- `Lines of Code: ${formatMeasure(getMeasure(component, X_METRIC), X_METRIC)}`,
- `Duplicated Blocks: ${formatMeasure(getMeasure(component, Y_METRIC), Y_METRIC)}`,
- `Duplicated Lines: ${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={formatInt}/>;
- }
-
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: Lines of Code</li>
- <li>Y: Duplicated Blocks</li>
- <li>Size: Duplicated Lines</li>
- </ul>
- </div>
- <div>
- {this.renderBubbleChart()}
- </div>
- </div>;
+ return <DomainBubbleChart {...this.props}
+ xMetric="ncloc"
+ yMetric="duplicated_blocks"
+ sizeMetrics={['duplicated_lines']}/>;
}
}
import { DuplicationsTimeline } from './timeline';
import { DuplicationsTreemap } from './treemap';
-import { getSeverities, getTags, getAssignees } from '../../../api/issues';
-
-export default class DuplicationsDomain extends React.Component {
+export default class extends React.Component {
render () {
return <div className="overview-domain">
<DuplicationsTimeline {...this.props}/>
-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';
+import { DomainTimeline } from '../domain/timeline';
+import { filterMetricsForDomains } from '../helpers/metrics';
-const DUPLICATIONS_METRICS = [
- 'duplicated_blocks',
- 'duplicated_files',
- 'duplicated_lines',
- 'duplicated_lines_density'
-];
-
-const HEIGHT = 280;
+const DOMAINS = ['Duplication'];
export class DuplicationsTimeline extends React.Component {
- constructor () {
- super();
- this.state = { loading: true, currentMetric: DUPLICATIONS_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 options = DUPLICATIONS_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}>{options}</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>;
+ return <DomainTimeline {...this.props}
+ initialMetric="duplicated_lines_density"
+ metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
}
}
-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';
+import { DomainTreemap } from '../domain/treemap';
-const COMPONENTS_METRICS = [
- 'ncloc',
- 'duplicated_lines_density'
-];
-
-const HEIGHT = 360;
const COLORS_5 = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000'];
-export class DuplicationsTreemap 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 duplications = component.measures['duplicated_lines_density'];
- return {
- size: component.measures['ncloc'],
- color: duplications != null ? colorScale(duplications) : '#777'
- };
- });
- let labels = this.state.components.map(component => component.name);
- let tooltips = this.state.components.map(component => {
- let inner = [
- component.name,
- `Lines of Code: ${formatMeasure(component.measures['ncloc'], 'ncloc')}`,
- `Duplications: ${formatMeasure(component.measures['duplicated_lines_density'], 'duplicated_lines_density')}`
- ].join('<br>');
- return `<div class="text-left">${inner}</div>`;
- });
- return <Treemap items={items} labels={labels} tooltips={tooltips} height={HEIGHT}/>;
- }
+export class DuplicationsTreemap extends React.Component {
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 of Code</li>
- <li>Color: Duplications</li>
- </ul>
- </div>
- <div>
- {this.renderTreemap()}
- </div>
- </div>;
+ let scale = d3.scale.linear()
+ .domain([0, 25, 50, 75, 100])
+ .range(COLORS_5);
+ return <DomainTreemap {...this.props}
+ sizeMetric="ncloc"
+ colorMetric="duplicated_lines_density"
+ scale={scale}/>;
}
}
'sqale_index': 'SHORT_WORK_DUR',
'sqale_debt_ratio': 'PERCENT',
'sqale_rating': 'RATING',
- 'lines': 'SHORT_INT',
'coverage': 'PERCENT',
'line_coverage': 'PERCENT',
'duplicated_lines': 'INT',
'duplicated_lines_density': 'PERCENT',
- 'complexity': 'INT'
+ 'ncloc': 'SHORT_INT',
+ 'classes': 'SHORT_INT',
+ 'lines': 'SHORT_INT',
+ 'generated_ncloc': 'SHORT_INT',
+ 'generated_lines': 'SHORT_INT',
+ 'directories': 'SHORT_INT',
+ 'files': 'SHORT_INT',
+ 'functions': 'SHORT_INT',
+ 'statements': 'SHORT_INT',
+ 'public_api': 'SHORT_INT',
+
+ 'complexity': 'SHORT_INT',
+ 'class_complexity': 'SHORT_INT',
+ 'file_complexity': 'SHORT_INT',
+ 'function_complexity': 'SHORT_INT',
+
+ 'comment_lines_density': 'PERCENT',
+ 'comment_lines': 'SHORT_INT',
+ 'commented_out_code_lines': 'SHORT_INT',
+ 'public_documented_api_density': 'PERCENT',
+ 'public_undocumented_api': 'SHORT_INT'
};
export function formatMeasure (value, metric) {
lines = this.props.measures['lines'],
files = this.props.measures['files'];
+ let active = this.props.section === 'size';
+
return (
- <Card>
+ <Card linkTo="size" active={active} onRoute={this.props.onRoute}>
<div className="measures">
<div className="measure measure-big" data-metric="lines">
<span className="measure-value">
--- /dev/null
+function hasRightDomain (metric, domains) {
+ return domains.indexOf(metric.domain) !== -1;
+}
+
+function isNotHidden (metric) {
+ return !metric.hidden;
+}
+
+function hasSimpleType (metric) {
+ return metric.type !== 'DATA' && metric.type !== 'DISTRIB';
+}
+
+function isNotDifferential (metric) {
+ return metric.key.indexOf('new_') !== 0;
+}
+
+export function filterMetricsForDomains (metrics, domains) {
+ return metrics.filter(metric => {
+ return hasRightDomain(metric, domains) && isNotHidden(metric) && hasSimpleType(metric) && isNotDifferential(metric);
+ });
+}
-import _ from 'underscore';
import React from 'react';
-import { BubbleChart } from '../../../components/charts/bubble-chart';
-import { getProjectUrl } from '../../../helpers/Url';
-import { getFiles } from '../../../api/components';
-
-
-const X_METRIC = 'violations';
-const Y_METRIC = 'sqale_index';
-const SIZE_METRIC_1 = 'blocker_violations';
-const SIZE_METRIC_2 = 'critical_violations';
-const COMPONENTS_METRICS = [X_METRIC, Y_METRIC, SIZE_METRIC_1, SIZE_METRIC_2];
-const HEIGHT = 360;
-
-
-function formatIssues (d) {
- return window.formatMeasure(d, 'SHORT_INT');
-}
-
-function formatDebt (d) {
- return window.formatMeasure(d, 'SHORT_WORK_DUR');
-}
-
-function getMeasure (component, metric) {
- return component.measures[metric] || 0;
-}
+import { DomainBubbleChart } from '../domain/bubble-chart';
export class IssuesBubbleChart 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_1) + getMeasure(component, SIZE_METRIC_2),
- 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,
- `Issues: ${formatIssues(getMeasure(component, X_METRIC))}`,
- `Technical Debt: ${formatDebt(getMeasure(component, Y_METRIC))}`,
- `Blocker & Critical Issues: ${formatIssues(getMeasure(component, SIZE_METRIC_1) + getMeasure(component, SIZE_METRIC_2))}`
- ].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={formatIssues}
- formatYTick={formatDebt}/>;
- }
-
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: Issues</li>
- <li>Y: Technical Debt</li>
- <li>Size: Blocker & Critical Issues</li>
- </ul>
- </div>
- <div>
- {this.renderBubbleChart()}
- </div>
- </div>;
+ return <DomainBubbleChart {...this.props}
+ xMetric="violations"
+ yMetric="sqale_index"
+ sizeMetrics={['blocker_violations', 'critical_violations']}/>;
}
}
-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';
+import { DomainTimeline } from '../domain/timeline';
+import { filterMetricsForDomains } from '../helpers/metrics';
-const ISSUES_METRICS = [
- 'violations',
- 'blocker_violations',
- 'critical_violations',
- 'major_violations',
- 'minor_violations',
- 'info_violations',
- 'confirmed_issues',
- 'false_positive_issues',
- 'open_issues',
- 'reopened_issues'
-];
-
-const DEBT_METRICS = [
- 'sqale_index',
- 'sqale_debt_ratio'
-];
-
-const HEIGHT = 280;
+const DOMAINS = ['Issues', 'Technical Debt'];
export class IssuesTimeline extends React.Component {
- constructor () {
- super();
- this.state = { loading: true, currentMetric: ISSUES_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 = ISSUES_METRICS
- .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
- let debtOptions = DEBT_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="Issues">{issueOptions}</optgroup>
- <optgroup label="Technical Debt">{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>;
+ return <DomainTimeline {...this.props}
+ initialMetric="violations"
+ metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
}
}
-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';
+import { DomainTreemap } from '../domain/treemap';
-const COMPONENTS_METRICS = [
- 'lines',
- 'sqale_rating'
-];
-const HEIGHT = 360;
+const COLORS_5 = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000'];
-export class IssuesTreemap 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 });
- });
- }
-
- // TODO use css
- getRatingColor (rating) {
- switch (rating) {
- case 1:
- return '#00AA00';
- case 2:
- return '#80CC00';
- case 3:
- return '#FFEE00';
- case 4:
- return '#F77700';
- case 5:
- return '#EE0000';
- default:
- return '#777';
- }
- }
-
- renderLoading () {
- return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
- <i className="spinner"/>
- </div>;
- }
-
- renderTreemap () {
- if (this.state.loading) {
- return this.renderLoading();
- }
-
- let items = this.state.components.map(component => {
- return {
- size: component.measures['lines'],
- color: this.getRatingColor(component.measures['sqale_rating'])
- };
- });
- 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')}`,
- `SQALE Rating: ${formatMeasure(component.measures['sqale_rating'], 'sqale_rating')}`
- ].join('<br>');
- return `<div class="text-left">${inner}</div>`;
- });
- return <Treemap items={items} labels={labels} tooltips={tooltips} height={HEIGHT}/>;
- }
+export class IssuesTreemap extends React.Component {
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: SQALE Rating</li>
- </ul>
- </div>
- <div>
- {this.renderTreemap()}
- </div>
- </div>;
+ let scale = d3.scale.ordinal()
+ .domain([1, 2, 3, 4, 5])
+ .range(COLORS_5);
+ return <DomainTreemap {...this.props}
+ sizeMetric="ncloc"
+ colorMetric="sqale_rating"
+ scale={scale}/>;
}
}
import React from 'react';
import offset from 'document-offset';
+
import GeneralMain from './general/main';
import IssuesMain from './issues/main';
import CoverageMain from './coverage/main';
import DuplicationsMain from './duplications/main';
+import SizeMain from './size/main';
import Meta from './meta';
+import { getMetrics } from '../../api/metrics';
+
export default class Overview extends React.Component {
constructor () {
super();
this.state = { section: hash.length ? hash.substr(1) : null };
}
+ componentDidMount () {
+ this.requestMetrics();
+ }
+
+ requestMetrics () {
+ return getMetrics().then(metrics => this.setState({ metrics }));
+ }
+
handleRoute (section, el) {
this.setState({ section }, () => this.scrollToEl(el));
window.location.href = '#' + section;
}
render () {
+ if (!this.state.metrics) {
+ return null;
+ }
+
let child;
switch (this.state.section) {
case 'issues':
- child = <IssuesMain {...this.props}/>;
+ child = <IssuesMain {...this.props} {...this.state}/>;
break;
case 'coverage':
- child = <CoverageMain {...this.props}/>;
+ child = <CoverageMain {...this.props} {...this.state}/>;
break;
case 'duplications':
- child = <DuplicationsMain {...this.props}/>;
+ child = <DuplicationsMain {...this.props} {...this.state}/>;
+ break;
+ case 'size':
+ child = <SizeMain {...this.props} {...this.state}/>;
break;
default:
child = null;
--- /dev/null
+import React from 'react';
+
+import { DomainMeasuresList } from '../domain/measures-list';
+
+
+const METRICS = [
+ 'comment_lines',
+ 'comment_lines_density'
+];
+
+
+export class CommentsDetails extends React.Component {
+ render () {
+ return <div className="overview-domain-section">
+ <h2 className="overview-title">Comments</h2>
+ <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+
+import { DomainMeasuresList } from '../domain/measures-list';
+
+
+const METRICS = [
+ 'complexity',
+ 'class_complexity',
+ 'file_complexity',
+ 'function_complexity'
+];
+
+
+export class ComplexityDetails extends React.Component {
+ render () {
+ return <div className="overview-domain-section">
+ <h2 className="overview-title">Complexity</h2>
+ <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+
+import { BarChart } from '../../../components/charts/bar-chart';
+import { getMeasures } from '../../../api/measures';
+
+
+const HEIGHT = 120;
+const COMPLEXITY_DISTRIBUTION_METRIC = 'file_complexity_distribution';
+
+
+export class ComplexityDistribution extends React.Component {
+ constructor (props) {
+ super(props);
+ this.state = { loading: true };
+ }
+
+ componentDidMount () {
+ this.requestData();
+ }
+
+ requestData () {
+ return getMeasures(this.props.component.key, [COMPLEXITY_DISTRIBUTION_METRIC]).then(measures => {
+ this.setState({ loading: false, distribution: measures[COMPLEXITY_DISTRIBUTION_METRIC] });
+ });
+ }
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ }
+
+ renderBarChart () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+
+ let data = this.state.distribution.split(';').map((point, index) => {
+ let tokens = point.split('=');
+ return { x: index, y: parseInt(tokens[1], 10), value: parseInt(tokens[0], 10) };
+ });
+
+ let xTicks = data.map(point => point.value);
+
+ let xValues = data.map(point => window.formatMeasure(point.y, 'INT'));
+
+ return <BarChart data={data}
+ xTicks={xTicks}
+ xValues={xValues}
+ height={HEIGHT}
+ padding={[25, 30, 50, 30]}/>;
+ }
+
+ render () {
+ return <div className="overview-bar-chart">
+ <div className="overview-domain-header">
+ <h2 className="overview-title"> </h2>
+ <ul className="list-inline small">
+ <li>X: Complexity/file</li>
+ <li>Size: Number of Files</li>
+ </ul>
+ </div>
+ <div>
+ {this.renderBarChart()}
+ </div>
+ </div>;
+ }
+}
--- /dev/null
+import _ from 'underscore';
+import React from 'react';
+
+import { BarChart } from '../../../components/charts/bar-chart';
+import { getMeasures } from '../../../api/measures';
+import { getLanguages } from '../../../api/languages';
+
+
+const HEIGHT = 180;
+const COMPLEXITY_DISTRIBUTION_METRIC = 'ncloc_language_distribution';
+
+
+export class LanguageDistribution extends React.Component {
+ constructor (props) {
+ super(props);
+ this.state = { loading: true };
+ }
+
+ componentDidMount () {
+ this.requestData();
+ }
+
+ requestData () {
+ return Promise.all([
+ getMeasures(this.props.component.key, [COMPLEXITY_DISTRIBUTION_METRIC]),
+ getLanguages()
+ ]).then(responses => {
+ this.setState({
+ loading: false,
+ distribution: responses[0][COMPLEXITY_DISTRIBUTION_METRIC],
+ languages: responses[1]
+ });
+ });
+ }
+
+ getLanguageName (langKey) {
+ let lang = _.findWhere(this.state.languages, { key: langKey });
+ return lang ? lang.name : window.t('unknown');
+ }
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ }
+
+ renderBarChart () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+
+ let data = this.state.distribution.split(';').map((d, index) => {
+ let tokens = d.split('=');
+ return { x: index, y: parseInt(tokens[1], 10), lang: tokens[0] };
+ });
+
+ let xTicks = data.map(d => this.getLanguageName(d.lang));
+
+ let xValues = data.map(d => window.formatMeasure(d.y, 'INT'));
+
+ return <BarChart data={data}
+ xTicks={xTicks}
+ xValues={xValues}
+ height={HEIGHT}
+ padding={[25, 30, 50, 30]}/>;
+ }
+
+ render () {
+ return <div className="overview-bar-chart">
+ <div className="overview-domain-header">
+ <h2 className="overview-title"> </h2>
+ <ul className="list-inline small">
+ <li>Size: Lines of Code</li>
+ </ul>
+ </div>
+ <div>
+ {this.renderBarChart()}
+ </div>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+
+import { SizeTimeline } from './timeline';
+import { SizeDetails } from './size-details';
+import { ComplexityDetails } from './complexity-details';
+import { CommentsDetails } from './comments-details';
+import { ComplexityDistribution } from './complexity-distribution';
+import { LanguageDistribution } from './language-distribution';
+import { SizeTreemap } from './treemap';
+
+
+export default class extends React.Component {
+ render () {
+ return <div className="overview-domain">
+ <SizeTimeline {...this.props}/>
+
+ <div className="flex-columns">
+ <div className="flex-column flex-column-third">
+ <SizeDetails {...this.props}/>
+ </div>
+ <div className="flex-column flex-column-two-thirds">
+ <LanguageDistribution {...this.props}/>
+ </div>
+ </div>
+
+ <div className="flex-columns">
+ <div className="flex-column flex-column-third">
+ <ComplexityDetails {...this.props}/>
+ </div>
+ <div className="flex-column flex-column-two-thirds">
+ <ComplexityDistribution {...this.props}/>
+ </div>
+ </div>
+
+ <div className="flex-columns">
+ <div className="flex-column flex-column-third">
+ <CommentsDetails {...this.props}/>
+ </div>
+ </div>
+
+ <SizeTreemap {...this.props}/>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+
+import { DomainMeasuresList } from '../domain/measures-list';
+
+
+const METRICS = [
+ 'ncloc',
+ 'lines',
+ 'files',
+ 'directories',
+ 'functions',
+ 'classes',
+ 'accessors'
+];
+
+
+export class SizeDetails extends React.Component {
+ render () {
+ return <div className="overview-domain-section">
+ <h2 className="overview-title">Size</h2>
+ <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+
+import { DomainTimeline } from '../domain/timeline';
+import { filterMetricsForDomains } from '../helpers/metrics';
+
+
+const DOMAINS = ['Size', 'Complexity', 'Documentation'];
+
+
+export class SizeTimeline extends React.Component {
+ render () {
+ return <DomainTimeline {...this.props}
+ initialMetric="ncloc"
+ metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
+ }
+}
--- /dev/null
+import React from 'react';
+
+import { DomainTreemap } from '../domain/treemap';
+
+
+export class SizeTreemap extends React.Component {
+ render () {
+ return <DomainTreemap {...this.props} sizeMetric="ncloc"/>;
+ }
+}
--- /dev/null
+import d3 from 'd3';
+import React from 'react';
+
+export class BarChart extends React.Component {
+ constructor (props) {
+ super();
+ this.state = { width: props.width, height: props.height };
+ }
+
+ componentDidMount () {
+ if (!this.props.width || !this.props.height) {
+ this.handleResize();
+ window.addEventListener('resize', this.handleResize.bind(this));
+ }
+ }
+
+ componentWillUnmount () {
+ if (!this.props.width || !this.props.height) {
+ window.removeEventListener('resize', this.handleResize.bind(this));
+ }
+ }
+
+ handleResize () {
+ let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+ let newWidth = this.props.width || boundingClientRect.width;
+ let newHeight = this.props.height || boundingClientRect.height;
+ this.setState({ width: newWidth, height: newHeight });
+ }
+
+ renderXTicks (xScale, yScale) {
+ if (!this.props.xTicks.length) {
+ return null;
+ }
+ let ticks = this.props.xTicks.map((tick, index) => {
+ let point = this.props.data[index];
+ let x = Math.round(xScale(point.x) + xScale.rangeBand() / 2 + this.props.barsWidth / 2);
+ let y = yScale.range()[0];
+ return <text key={index} className="bar-chart-tick" x={x} y={y} dy="1.5em">{tick}</text>;
+ });
+ return <g>{ticks}</g>;
+ }
+
+ renderXValues (xScale, yScale) {
+ if (!this.props.xValues.length) {
+ return null;
+ }
+ let ticks = this.props.xValues.map((value, index) => {
+ let point = this.props.data[index];
+ let x = Math.round(xScale(point.x) + xScale.rangeBand() / 2 + this.props.barsWidth / 2);
+ let y = yScale(point.y);
+ return <text key={index} className="bar-chart-tick" x={x} y={y} dy="-1em">{value}</text>;
+ });
+ return <g>{ticks}</g>;
+ }
+
+ renderBars (xScale, yScale) {
+ let bars = this.props.data.map((d, index) => {
+ let x = Math.round(xScale(d.x) + xScale.rangeBand() / 2);
+ let maxY = yScale.range()[0];
+ let y = Math.round(yScale(d.y)) - /* minimum bar height */ 1;
+ let height = maxY - y;
+ return <rect key={index} className="bar-chart-bar"
+ x={x} y={y} width={this.props.barsWidth} height={height}/>;
+ });
+ return <g>{bars}</g>;
+ }
+
+ render () {
+ if (!this.state.width || !this.state.height) {
+ return <div/>;
+ }
+
+ let availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3];
+ let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
+
+ let maxY = d3.max(this.props.data, d => d.y);
+ let xScale = d3.scale.ordinal()
+ .domain(this.props.data.map(d => d.x))
+ .rangeRoundBands([0, availableWidth]);
+ let yScale = d3.scale.linear()
+ .domain([0, maxY])
+ .range([availableHeight, 0]);
+
+ return <svg className="bar-chart" width={this.state.width} height={this.state.height}>
+ <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+ {this.renderXTicks(xScale, yScale)}
+ {this.renderXValues(xScale, yScale)}
+ {this.renderBars(xScale, yScale)}
+ </g>
+ </svg>;
+ }
+}
+
+BarChart.defaultProps = {
+ xTicks: [],
+ xValues: [],
+ padding: [10, 10, 10, 10],
+ barsWidth: 40
+};
+
+BarChart.propTypes = {
+ data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
+ xValues: React.PropTypes.arrayOf(React.PropTypes.any),
+ padding: React.PropTypes.arrayOf(React.PropTypes.number),
+ barsWidth: React.PropTypes.number
+};
let bubbles = this.props.items
.map((item, index) => {
- let tooltip = index < this.props.tooltips.length ? this.props.tooltips[index] : null;
return <Bubble key={index}
- tooltip={tooltip}
+ tooltip={item.tooltip}
link={item.link}
x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>;
});
.nodes({ children: this.props.items })
.filter(d => !d.children);
- let prefix = mostCommitPrefix(this.props.labels),
+ let prefix = mostCommitPrefix(this.props.items.map(item => item.label)),
prefixLength = prefix.length;
let rectangles = nodes.map((node, index) => {
- let label = prefixLength ? `${prefix}<br>${this.props.labels[index].substr(prefixLength)}` :
- this.props.labels[index];
- let tooltip = index < this.props.tooltips.length ? this.props.tooltips[index] : null;
+ let label = prefixLength ? `${prefix}<br>${node.label.substr(prefixLength)}` : node.label;
return <TreemapRect key={index}
x={node.x}
y={node.y}
fill={node.color}
label={label}
prefix={prefix}
- tooltip={tooltip}/>;
+ tooltip={node.tooltip}/>;
});
return <div className="sonar-d3">
}
Treemap.propTypes = {
- labels: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
- tooltips: React.PropTypes.arrayOf(React.PropTypes.string)
+ items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
};
}
}
+.overview-bar-chart {
+ .bar-chart-bar {
+ fill: @blue;
+ }
+
+ .bar-chart-tick {
+ fill: @baseFontColor;
+ font-size: 11px;
+ text-anchor: middle;
+ }
+}
+
.overview-treemap {
.overview-domain-header {
+ padding-top: 0;
padding-left: 0;
padding-right: 0;
}