--- /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 = '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;
+}
+
+
+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>;
+ }
+}
--- /dev/null
+import React from 'react';
+import { getMeasures } from '../../../api/measures';
+import { formatMeasure } from '../formatting';
+
+
+const METRICS = [
+ 'duplicated_blocks',
+ 'duplicated_files',
+ 'duplicated_lines',
+ 'duplicated_lines_density'
+];
+
+
+export class DuplicationsDetails 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">Details</h2>
+ <table className="data zebra">
+ <tbody>
+ <tr>
+ <td>Duplications</td>
+ <td className="thin nowrap text-right">
+ {formatMeasure(this.state.measures['duplicated_lines_density'], 'duplicated_lines_density')}
+ </td>
+ </tr>
+ <tr>
+ <td>Blocks</td>
+ <td className="thin nowrap text-right">
+ {formatMeasure(this.state.measures['duplicated_blocks'], 'duplicated_blocks')}
+ </td>
+ </tr>
+ <tr>
+ <td>Files</td>
+ <td className="thin nowrap text-right">
+ {formatMeasure(this.state.measures['duplicated_files'], 'duplicated_files')}
+ </td>
+ </tr>
+ <tr>
+ <td>Lines</td>
+ <td className="thin nowrap text-right">
+ {formatMeasure(this.state.measures['duplicated_lines'], 'duplicated_lines')}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>;
+ }
+}
--- /dev/null
+import React from 'react';
+
+import { DuplicationsDetails } from './duplications-details';
+import { DuplicationsBubbleChart } from './bubble-chart';
+import { DuplicationsTimeline } from './timeline';
+import { DuplicationsTreemap } from './treemap';
+
+import { getSeverities, getTags, getAssignees } from '../../../api/issues';
+
+
+export default class DuplicationsDomain extends React.Component {
+ render () {
+ return <div className="overview-domain">
+ <DuplicationsTimeline {...this.props}/>
+ <div className="flex-columns">
+ <div className="flex-column flex-column-half">
+ <DuplicationsDetails {...this.props}/>
+ </div>
+ </div>
+
+ <DuplicationsBubbleChart {...this.props}/>
+ <DuplicationsTreemap {...this.props}/>
+ </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 DUPLICATIONS_METRICS = [
+ 'duplicated_blocks',
+ 'duplicated_files',
+ 'duplicated_lines',
+ 'duplicated_lines_density'
+];
+
+const HEIGHT = 280;
+
+
+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>;
+ }
+}
--- /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 = [
+ '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}/>;
+ }
+
+ 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>;
+ }
+}
'test_execution_time': 'MILLISEC',
'test_success_density': 'PERCENT',
+ 'duplicated_blocks': 'INT',
+ 'duplicated_files': 'INT',
+ 'duplicated_lines': 'INT',
+ 'duplicated_lines_density': 'PERCENT',
+
'complexity': 'INT'
};
return null;
}
+ let active = this.props.section === 'duplications';
+
return (
- <Card>
+ <Card linkTo="duplications" active={active} onRoute={this.props.onRoute}>
<div className="measures">
<div className="measures-chart">
<Donut data={donutData} size="47"/>
import GeneralMain from './general/main';
import IssuesMain from './issues/main';
import CoverageMain from './coverage/main';
+import DuplicationsMain from './duplications/main';
import Meta from './meta';
export default class Overview extends React.Component {
case 'coverage':
child = <CoverageMain {...this.props}/>;
break;
+ case 'duplications':
+ child = <DuplicationsMain {...this.props}/>;
+ break;
default:
child = null;
}