From: Stas Vilchik Date: Fri, 23 Oct 2015 14:09:18 +0000 (+0200) Subject: SONAR-6357 add detailed "Size" panel for the "Overview" main page X-Git-Tag: 5.3-RC1~454 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=3c38542ce87deca7dab67fbe7cc87a3b4692beb7;p=sonarqube.git SONAR-6357 add detailed "Size" panel for the "Overview" main page --- diff --git a/server/sonar-web/src/main/js/api/languages.js b/server/sonar-web/src/main/js/api/languages.js new file mode 100644 index 00000000000..49b32957b73 --- /dev/null +++ b/server/sonar-web/src/main/js/api/languages.js @@ -0,0 +1,6 @@ +import { getJSON } from '../helpers/request.js'; + +export function getLanguages () { + let url = baseUrl + '/api/languages/list'; + return getJSON(url).then(r => r.languages); +} diff --git a/server/sonar-web/src/main/js/api/measures.js b/server/sonar-web/src/main/js/api/measures.js index b79b56a5ef2..e416e3a8882 100644 --- a/server/sonar-web/src/main/js/api/measures.js +++ b/server/sonar-web/src/main/js/api/measures.js @@ -7,7 +7,7 @@ export function getMeasures (componentKey, metrics) { let msr = r[0].msr || []; let measures = {}; msr.forEach(measure => { - measures[measure.key] = measure.val; + measures[measure.key] = measure.val || measure.data; }); return measures; }); diff --git a/server/sonar-web/src/main/js/api/metrics.js b/server/sonar-web/src/main/js/api/metrics.js new file mode 100644 index 00000000000..66bf7482c00 --- /dev/null +++ b/server/sonar-web/src/main/js/api/metrics.js @@ -0,0 +1,8 @@ +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); +} diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/coverage/bubble-chart.js index fde275f7b6c..826aca98364 100644 --- a/server/sonar-web/src/main/js/apps/overview/coverage/bubble-chart.js +++ b/server/sonar-web/src/main/js/apps/overview/coverage/bubble-chart.js @@ -1,105 +1,12 @@ -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
- -
; - } - - 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('
'); - return `
${inner}
`; - }); - return ; - } - render () { - return
-
-

Project Files

-
    -
  • X: Complexity
  • -
  • Y: Coverage
  • -
  • Size: Technical Debt
  • -
-
-
- {this.renderBubbleChart()} -
-
; + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/main.js b/server/sonar-web/src/main/js/apps/overview/coverage/main.js index 88533e138c7..084a7f94ea0 100644 --- a/server/sonar-web/src/main/js/apps/overview/coverage/main.js +++ b/server/sonar-web/src/main/js/apps/overview/coverage/main.js @@ -6,10 +6,8 @@ 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 { +export default class extends React.Component { render () { return
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/timeline.js b/server/sonar-web/src/main/js/apps/overview/coverage/timeline.js index 58e9a9d98e9..60e14a00b6b 100644 --- a/server/sonar-web/src/main/js/apps/overview/coverage/timeline.js +++ b/server/sonar-web/src/main/js/apps/overview/coverage/timeline.js @@ -1,164 +1,20 @@ -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
- -
; - } - - 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 ; - } - - renderTimelineMetricSelect () { - if (this.state.loading) { - return null; - } - - let issueOptions = COVERAGE_METRICS - .map(metric => ); - let debtOptions = TESTS_METRICS - .map(metric => ); - - return ; - } - render () { - return
-
-

Project History

- {this.renderTimelineMetricSelect()} -
-
- {this.renderLineChart()} -
-
; + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/treemap.js b/server/sonar-web/src/main/js/apps/overview/coverage/treemap.js index ee4af18e832..251d5f0751e 100644 --- a/server/sonar-web/src/main/js/apps/overview/coverage/treemap.js +++ b/server/sonar-web/src/main/js/apps/overview/coverage/treemap.js @@ -1,89 +1,20 @@ -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
- -
; - } - - 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('
'); - return `
${inner}
`; - }); - return ; - } +export class CoverageTreemap extends React.Component { render () { - return
-
-

Project Components

-
    -
  • Size: Lines
  • -
  • Color: Coverage
  • -
-
-
- {this.renderTreemap()} -
-
; + let scale = d3.scale.linear() + .domain([0, 25, 50, 75, 100]) + .range(COLORS_5); + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js new file mode 100644 index 00000000000..730795e11ac --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js @@ -0,0 +1,125 @@ +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('
'); + return `
${inner}
`; + } + + renderLoading () { + return
+ +
; + } + + 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 ; + } + + render () { + return
+
+

Project Files

+
    +
  • X: {this.state.xMetric.name}
  • +
  • Y: {this.state.yMetric.name}
  • +
  • Size: {this.getSizeMetricsTitle()}
  • +
+
+
+ {this.renderBubbleChart()} +
+
; + } +} + +DomainBubbleChart.propTypes = { + xMetric: React.PropTypes.string.isRequired, + yMetric: React.PropTypes.string.isRequired, + sizeMetrics: React.PropTypes.arrayOf(React.PropTypes.string).isRequired +}; diff --git a/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js b/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js new file mode 100644 index 00000000000..16feb0425ae --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js @@ -0,0 +1,53 @@ +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 + {metricObject.name} + + + {format(this.state.measures[metric], metricObject.type)} + + + ; + }); + return + {rows} +
; + } +} + +DomainMeasuresList.propTypes = { + metricsToDisplay: React.PropTypes.arrayOf(React.PropTypes.string).isRequired +}; diff --git a/server/sonar-web/src/main/js/apps/overview/domain/timeline.js b/server/sonar-web/src/main/js/apps/overview/domain/timeline.js new file mode 100644 index 00000000000..ff671f1fc71 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/domain/timeline.js @@ -0,0 +1,153 @@ +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
+ +
; + } + + 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 ; + } + + renderMetricOption (metric) { + return ; + } + + 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 {options}; + }); + } else { + inner = groupedMetrics[0].metrics.map(this.renderMetricOption); + } + return ; + } + + render () { + return
+
+

Project History

+ {this.renderTimelineMetricSelect()} +
+
+ {this.renderLineChart()} +
+
; + } +} + +DomainTimeline.propTypes = { + metrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + initialMetric: React.PropTypes.string.isRequired +}; diff --git a/server/sonar-web/src/main/js/apps/overview/domain/treemap.js b/server/sonar-web/src/main/js/apps/overview/domain/treemap.js new file mode 100644 index 00000000000..1592b58a8e2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/domain/treemap.js @@ -0,0 +1,100 @@ +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('
'); + return `
${inner}
`; + } + + renderLoading () { + return
+ +
; + } + + 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 ; + } + + render () { + let color = this.props.colorMetric ?
  • Color: {this.state.colorMetric.name}
  • : null; + return
    +
    +

    Project Components

    +
      +
    • Size: {this.state.sizeMetric.name}
    • + {color} +
    +
    +
    + {this.renderTreemap()} +
    +
    ; + } +} + +DomainTreemap.propTypes = { + sizeMetric: React.PropTypes.string.isRequired, + colorMetric: React.PropTypes.string, + scale: React.PropTypes.func +}; diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js index 4612cdf7bb4..299a47978f1 100644 --- a/server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js +++ b/server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js @@ -1,101 +1,12 @@ -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
    - -
    ; - } - - 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('
    '); - return `
    ${inner}
    `; - }); - return ; - } - render () { - return
    -
    -

    Project Files

    -
      -
    • X: Lines of Code
    • -
    • Y: Duplicated Blocks
    • -
    • Size: Duplicated Lines
    • -
    -
    -
    - {this.renderBubbleChart()} -
    -
    ; + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/main.js b/server/sonar-web/src/main/js/apps/overview/duplications/main.js index 3cda843b5cb..a029f6ebb32 100644 --- a/server/sonar-web/src/main/js/apps/overview/duplications/main.js +++ b/server/sonar-web/src/main/js/apps/overview/duplications/main.js @@ -5,10 +5,8 @@ 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 { +export default class extends React.Component { render () { return
    diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/timeline.js b/server/sonar-web/src/main/js/apps/overview/duplications/timeline.js index 81c8d63d8bc..abd6987af7d 100644 --- a/server/sonar-web/src/main/js/apps/overview/duplications/timeline.js +++ b/server/sonar-web/src/main/js/apps/overview/duplications/timeline.js @@ -1,131 +1,16 @@ -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
    - -
    ; - } - - 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 ; - } - - renderTimelineMetricSelect () { - if (this.state.loading) { - return null; - } - - let options = DUPLICATIONS_METRICS - .map(metric => ); - - return ; - } - render () { - return
    -
    -

    Project History

    - {this.renderTimelineMetricSelect()} -
    -
    - {this.renderLineChart()} -
    -
    ; + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/treemap.js b/server/sonar-web/src/main/js/apps/overview/duplications/treemap.js index 0b3b00e17d6..019e78e4cab 100644 --- a/server/sonar-web/src/main/js/apps/overview/duplications/treemap.js +++ b/server/sonar-web/src/main/js/apps/overview/duplications/treemap.js @@ -1,88 +1,20 @@ -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
    - -
    ; - } - - 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('
    '); - return `
    ${inner}
    `; - }); - return ; - } +export class DuplicationsTreemap extends React.Component { render () { - return
    -
    -

    Project Components

    -
      -
    • Size: Lines of Code
    • -
    • Color: Duplications
    • -
    -
    -
    - {this.renderTreemap()} -
    -
    ; + let scale = d3.scale.linear() + .domain([0, 25, 50, 75, 100]) + .range(COLORS_5); + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/formatting.js b/server/sonar-web/src/main/js/apps/overview/formatting.js index c35de0f91fd..7f05531226c 100644 --- a/server/sonar-web/src/main/js/apps/overview/formatting.js +++ b/server/sonar-web/src/main/js/apps/overview/formatting.js @@ -12,7 +12,6 @@ const METRIC_TYPES = { 'sqale_index': 'SHORT_WORK_DUR', 'sqale_debt_ratio': 'PERCENT', 'sqale_rating': 'RATING', - 'lines': 'SHORT_INT', 'coverage': 'PERCENT', 'line_coverage': 'PERCENT', @@ -50,7 +49,27 @@ const METRIC_TYPES = { '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) { diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js index 9488219a2a9..967f752fb88 100644 --- a/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js +++ b/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js @@ -9,8 +9,10 @@ export default React.createClass({ lines = this.props.measures['lines'], files = this.props.measures['files']; + let active = this.props.section === 'size'; + return ( - +
    diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js b/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js new file mode 100644 index 00000000000..1a1dcefcc47 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js @@ -0,0 +1,21 @@ +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); + }); +} diff --git a/server/sonar-web/src/main/js/apps/overview/issues/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/issues/bubble-chart.js index b37c420abcf..f68dae5458e 100644 --- a/server/sonar-web/src/main/js/apps/overview/issues/bubble-chart.js +++ b/server/sonar-web/src/main/js/apps/overview/issues/bubble-chart.js @@ -1,105 +1,12 @@ -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
    - -
    ; - } - - 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('
    '); - return `
    ${inner}
    `; - }); - return ; - } - render () { - return
    -
    -

    Project Files

    -
      -
    • X: Issues
    • -
    • Y: Technical Debt
    • -
    • Size: Blocker & Critical Issues
    • -
    -
    -
    - {this.renderBubbleChart()} -
    -
    ; + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/issues/timeline.js b/server/sonar-web/src/main/js/apps/overview/issues/timeline.js index 5266dad14ae..3ca3f56b129 100644 --- a/server/sonar-web/src/main/js/apps/overview/issues/timeline.js +++ b/server/sonar-web/src/main/js/apps/overview/issues/timeline.js @@ -1,147 +1,16 @@ -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
    - -
    ; - } - - 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 ; - } - - renderTimelineMetricSelect () { - if (this.state.loading) { - return null; - } - - let issueOptions = ISSUES_METRICS - .map(metric => ); - let debtOptions = DEBT_METRICS - .map(metric => ); - - return ; - } - render () { - return
    -
    -

    Project History

    - {this.renderTimelineMetricSelect()} -
    -
    - {this.renderLineChart()} -
    -
    ; + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/issues/treemap.js b/server/sonar-web/src/main/js/apps/overview/issues/treemap.js index 665406c41d9..6cab86725a0 100644 --- a/server/sonar-web/src/main/js/apps/overview/issues/treemap.js +++ b/server/sonar-web/src/main/js/apps/overview/issues/treemap.js @@ -1,99 +1,20 @@ -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
    - -
    ; - } - - 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('
    '); - return `
    ${inner}
    `; - }); - return ; - } +export class IssuesTreemap extends React.Component { render () { - return
    -
    -

    Project Components

    -
      -
    • Size: Lines
    • -
    • Color: SQALE Rating
    • -
    -
    -
    - {this.renderTreemap()} -
    -
    ; + let scale = d3.scale.ordinal() + .domain([1, 2, 3, 4, 5]) + .range(COLORS_5); + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/main.js b/server/sonar-web/src/main/js/apps/overview/main.js index c0520880841..f2111487d3f 100644 --- a/server/sonar-web/src/main/js/apps/overview/main.js +++ b/server/sonar-web/src/main/js/apps/overview/main.js @@ -1,11 +1,15 @@ 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(); @@ -13,6 +17,14 @@ export default class Overview extends React.Component { 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; @@ -24,16 +36,23 @@ export default class Overview extends React.Component { } render () { + if (!this.state.metrics) { + return null; + } + let child; switch (this.state.section) { case 'issues': - child = ; + child = ; break; case 'coverage': - child = ; + child = ; break; case 'duplications': - child = ; + child = ; + break; + case 'size': + child = ; break; default: child = null; diff --git a/server/sonar-web/src/main/js/apps/overview/size/comments-details.js b/server/sonar-web/src/main/js/apps/overview/size/comments-details.js new file mode 100644 index 00000000000..1f2058276c8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/size/comments-details.js @@ -0,0 +1,19 @@ +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
    +

    Comments

    + +
    ; + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/size/complexity-details.js b/server/sonar-web/src/main/js/apps/overview/size/complexity-details.js new file mode 100644 index 00000000000..bd1ba93b2ae --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/size/complexity-details.js @@ -0,0 +1,21 @@ +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
    +

    Complexity

    + +
    ; + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js b/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js new file mode 100644 index 00000000000..8ff3da2740d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js @@ -0,0 +1,68 @@ +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
    + +
    ; + } + + 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 ; + } + + render () { + return
    +
    +

     

    +
      +
    • X: Complexity/file
    • +
    • Size: Number of Files
    • +
    +
    +
    + {this.renderBarChart()} +
    +
    ; + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js b/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js new file mode 100644 index 00000000000..ab724920dc4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js @@ -0,0 +1,81 @@ +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
    + +
    ; + } + + 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 ; + } + + render () { + return
    +
    +

     

    +
      +
    • Size: Lines of Code
    • +
    +
    +
    + {this.renderBarChart()} +
    +
    ; + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/size/main.js b/server/sonar-web/src/main/js/apps/overview/size/main.js new file mode 100644 index 00000000000..72f892c9db6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/size/main.js @@ -0,0 +1,44 @@ +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
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + +
    ; + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/size/size-details.js b/server/sonar-web/src/main/js/apps/overview/size/size-details.js new file mode 100644 index 00000000000..c7362244222 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/size/size-details.js @@ -0,0 +1,24 @@ +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
    +

    Size

    + +
    ; + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/size/timeline.js b/server/sonar-web/src/main/js/apps/overview/size/timeline.js new file mode 100644 index 00000000000..21978726ccf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/size/timeline.js @@ -0,0 +1,16 @@ +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 ; + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/size/treemap.js b/server/sonar-web/src/main/js/apps/overview/size/treemap.js new file mode 100644 index 00000000000..ddfc2671db6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/size/treemap.js @@ -0,0 +1,10 @@ +import React from 'react'; + +import { DomainTreemap } from '../domain/treemap'; + + +export class SizeTreemap extends React.Component { + render () { + return ; + } +} diff --git a/server/sonar-web/src/main/js/components/charts/bar-chart.js b/server/sonar-web/src/main/js/components/charts/bar-chart.js new file mode 100644 index 00000000000..347d45d9673 --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/bar-chart.js @@ -0,0 +1,107 @@ +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 {tick}; + }); + return {ticks}; + } + + 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 {value}; + }); + return {ticks}; + } + + 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 ; + }); + return {bars}; + } + + render () { + if (!this.state.width || !this.state.height) { + return
    ; + } + + 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 + + {this.renderXTicks(xScale, yScale)} + {this.renderXValues(xScale, yScale)} + {this.renderBars(xScale, yScale)} + + ; + } +} + +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 +}; diff --git a/server/sonar-web/src/main/js/components/charts/bubble-chart.js b/server/sonar-web/src/main/js/components/charts/bubble-chart.js index e2c5d200f57..82edf22ce4e 100644 --- a/server/sonar-web/src/main/js/components/charts/bubble-chart.js +++ b/server/sonar-web/src/main/js/components/charts/bubble-chart.js @@ -172,9 +172,8 @@ export class BubbleChart extends React.Component { let bubbles = this.props.items .map((item, index) => { - let tooltip = index < this.props.tooltips.length ? this.props.tooltips[index] : null; return ; }); diff --git a/server/sonar-web/src/main/js/components/charts/treemap.js b/server/sonar-web/src/main/js/components/charts/treemap.js index db6ceb8d364..d00ce0be0fc 100644 --- a/server/sonar-web/src/main/js/components/charts/treemap.js +++ b/server/sonar-web/src/main/js/components/charts/treemap.js @@ -111,13 +111,11 @@ export class Treemap extends React.Component { .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}
    ${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}
    ${node.label.substr(prefixLength)}` : node.label; return ; + tooltip={node.tooltip}/>; }); return
    @@ -138,6 +136,5 @@ export class Treemap extends React.Component { } 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 }; diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less index 4e0edc3fcba..9b43ba81fdf 100644 --- a/server/sonar-web/src/main/less/pages/overview.less +++ b/server/sonar-web/src/main/less/pages/overview.less @@ -309,8 +309,21 @@ } } +.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; }