diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-11-09 14:07:58 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-11-09 17:24:30 +0100 |
commit | 5a0ff33ea0c8496aa923dba9e64ca642d79898ed (patch) | |
tree | 90fd3fc7abbbcc4d79b1bc8c26c876b6976b6e4d | |
parent | ae7efa83ddab2008efab798ef9b8b119009aca69 (diff) | |
download | sonarqube-5a0ff33ea0c8496aa923dba9e64ca642d79898ed.tar.gz sonarqube-5a0ff33ea0c8496aa923dba9e64ca642d79898ed.zip |
SONAR-6361 add detailed "Duplications" panel for the "Overview" main page
12 files changed, 178 insertions, 113 deletions
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 index 58c65c45ad1..e112d8a230d 100644 --- 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 @@ -101,7 +101,7 @@ export class DomainBubbleChart extends React.Component { } render () { - return <div className="overview-bubble-chart overview-domain-dark"> + return <div className="overview-domain overview-domain-chart"> <div className="overview-domain-header"> <h2 className="overview-title">Project Files</h2> <ul className="list-inline small"> @@ -110,7 +110,7 @@ export class DomainBubbleChart extends React.Component { <li>Size: {this.getSizeMetricsTitle()}</li> </ul> </div> - <div> + <div className="overview-bubble-chart"> {this.renderBubbleChart()} </div> </div>; 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 deleted file mode 100644 index 299a47978f1..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { DomainBubbleChart } from '../domain/bubble-chart'; - - -export class DuplicationsBubbleChart extends React.Component { - render () { - return <DomainBubbleChart {...this.props} - xMetric="ncloc" - yMetric="duplicated_blocks" - sizeMetrics={['duplicated_lines']}/>; - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js b/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js deleted file mode 100644 index b1b9ec79138..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { DomainMeasuresList } from '../domain/measures-list'; - - -const METRICS = [ - 'duplicated_blocks', - 'duplicated_files', - 'duplicated_lines', - 'duplicated_lines_density' -]; - - -export class DuplicationsDetails extends React.Component { - render () { - return <div className="overview-domain-section"> - <h2 className="overview-title">Duplications</h2> - <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/> - </div>; - } -} 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 02aa9eceecc..a3aa1d482dd 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 @@ -1,31 +1,117 @@ +import d3 from 'd3'; import React from 'react'; -import { DuplicationsDetails } from './duplications-details'; -import { DuplicationsBubbleChart } from './bubble-chart'; -import { DuplicationsTimeline } from './timeline'; -import { DuplicationsTreemap } from './treemap'; +import { getMeasuresAndVariations } from '../../../api/measures'; +import { DetailedMeasure } from '../common-components'; +import { DomainTimeline } from '../timeline/domain-timeline'; +import { DomainTreemap } from '../domain/treemap'; +import { DomainBubbleChart } from '../domain/bubble-chart'; +import { getPeriodLabel, getPeriodDate } from './../helpers/period-label'; +import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; +import { filterMetrics, filterMetricsForDomains } from '../helpers/metrics'; +import { Legend } from '../common-components'; +import { CHART_COLORS_RANGE_PERCENT } from '../../../helpers/constants'; -export default class extends React.Component { - render () { - return <div className="overview-detailed-page"> - <div className="overview-domain-header"> - <h2 className="overview-title">Duplications</h2> - </div> +export const DuplicationsMain = React.createClass({ + mixins: [TooltipsMixin], + + getInitialState() { + return { + ready: false, + leakPeriodLabel: getPeriodLabel(this.props.component.periods, this.props.leakPeriodIndex), + leakPeriodDate: getPeriodDate(this.props.component.periods, this.props.leakPeriodIndex) + }; + }, + + componentDidMount() { + this.requestMeasures().then(r => { + let measures = this.getMeasuresValues(r, 'value'); + let leak = this.getMeasuresValues(r, 'var' + this.props.leakPeriodIndex); + this.setState({ ready: true, measures, leak }); + }); + }, + + getMeasuresValues (measures, fieldKey) { + let values = {}; + Object.keys(measures).forEach(measureKey => { + values[measureKey] = measures[measureKey][fieldKey]; + }); + return values; + }, + + getMetricsForDomain() { + return this.props.metrics + .filter(metric => ['Duplication'].indexOf(metric.domain) !== -1) + .map(metric => metric.key); + }, + + getMetricsForTimeline() { + return filterMetricsForDomains(this.props.metrics, ['Duplication']); + }, + + getAllMetricsForTimeline() { + return filterMetrics(this.props.metrics); + }, - <a className="overview-detailed-page-back" href="#"> - <i className="icon-chevron-left"/> - </a> + requestMeasures () { + return getMeasuresAndVariations(this.props.component.key, this.getMetricsForDomain()); + }, - <DuplicationsTimeline {...this.props}/> - <div className="flex-columns"> - <div className="flex-column flex-column-half"> - <DuplicationsDetails {...this.props}/> + renderLoading () { + return <div className="text-center"> + <i className="spinner spinner-margin"/> + </div>; + }, + + renderLegend () { + return <Legend leakPeriodDate={this.state.leakPeriodDate} leakPeriodLabel={this.state.leakPeriodLabel}/>; + }, + + renderMeasures() { + let metrics = filterMetricsForDomains(this.props.metrics, ['Duplication']) + .map(metric => { + return <DetailedMeasure key={metric.key} {...this.props} {...this.state} metric={metric.key} + type={metric.type}/>; + }); + return <div>{metrics}</div>; + }, + + render () { + if (!this.state.ready) { + return this.renderLoading(); + } + let treemapScale = d3.scale.linear() + .domain([0, 100]) + .range(CHART_COLORS_RANGE_PERCENT); + return <div className="overview-detailed-page"> + <div className="overview-domain-charts"> + <div className="overview-domain"> + <div className="overview-domain-header"> + <div className="overview-title">Duplications Overview</div> + {this.renderLegend()} + </div> + <div className="overview-detailed-measures-list"> + {this.renderMeasures()} + </div> </div> + <DomainBubbleChart {...this.props} + xMetric="ncloc" + yMetric="duplicated_blocks" + sizeMetrics={['duplicated_lines']}/> </div> - <DuplicationsBubbleChart {...this.props}/> - <DuplicationsTreemap {...this.props}/> + <div className="overview-domain-charts"> + <DomainTimeline {...this.props} {...this.state} + initialMetric="duplicated_lines_density" + metrics={this.getMetricsForTimeline()} + allMetrics={this.getAllMetricsForTimeline()}/> + <DomainTreemap {...this.props} + sizeMetric="ncloc" + colorMetric="duplicated_lines_density" + scale={treemapScale}/> + </div> </div>; + } -} +}); 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 deleted file mode 100644 index abd6987af7d..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/duplications/timeline.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; - -import { DomainTimeline } from '../domain/timeline'; -import { filterMetricsForDomains } from '../helpers/metrics'; - - -const DOMAINS = ['Duplication']; - - -export class DuplicationsTimeline extends React.Component { - render () { - return <DomainTimeline {...this.props} - initialMetric="duplicated_lines_density" - metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>; - } -} 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 deleted file mode 100644 index 019e78e4cab..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/duplications/treemap.js +++ /dev/null @@ -1,20 +0,0 @@ -import d3 from 'd3'; -import React from 'react'; - -import { DomainTreemap } from '../domain/treemap'; - - -const COLORS_5 = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000']; - - -export class DuplicationsTreemap extends React.Component { - render () { - 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}/>; - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/main/duplications.js b/server/sonar-web/src/main/js/apps/overview/main/duplications.js index 4ae63e785bd..db4c6e6c320 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/duplications.js +++ b/server/sonar-web/src/main/js/apps/overview/main/duplications.js @@ -31,7 +31,7 @@ export const GeneralDuplications = React.createClass({ render () { return <Domain> - <DomainHeader title="Duplications"/> + <DomainHeader title="Duplications" linkTo="/duplications"/> <DomainPanel domain="duplications"> <DomainNutshell> diff --git a/server/sonar-web/src/main/js/apps/overview/overview.js b/server/sonar-web/src/main/js/apps/overview/overview.js index dae5318a477..21c5eb3ccc0 100644 --- a/server/sonar-web/src/main/js/apps/overview/overview.js +++ b/server/sonar-web/src/main/js/apps/overview/overview.js @@ -4,6 +4,7 @@ import Gate from './main/gate/gate'; import GeneralMain from './main/main'; import Meta from './meta'; import { SizeMain } from './size/main'; +import { DuplicationsMain } from './duplications/main'; import { getMetrics } from '../../api/metrics'; import { RouterMixin } from '../../components/router/router'; @@ -46,6 +47,12 @@ export const Overview = React.createClass({ </div>; }, + renderDuplications () { + return <div className="overview"> + <DuplicationsMain {...this.props} {...this.state}/> + </div>; + }, + render () { if (!this.state.ready) { return this.renderLoading(); @@ -55,6 +62,8 @@ export const Overview = React.createClass({ return this.renderMain(); case '/size': return this.renderSize(); + case '/duplications': + return this.renderDuplications(); default: throw new Error('Unknown route: ' + this.state.route); } 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 2dd929c7c4c..391bf25c786 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 @@ -1,3 +1,4 @@ +import _ from 'underscore'; import d3 from 'd3'; import React from 'react'; @@ -93,8 +94,7 @@ export const BubbleChart = React.createClass({ let x = xScale(tick); let y1 = yScale.range()[0]; let y2 = yScale.range()[1]; - return <line key={index} x1={x} x2={x} y1={y1} y2={y2} className="bubble-chart-grid" - shapeRendering="crispEdges" strokeWidth="0.3"/>; + return <line key={index} x1={x} x2={x} y1={y1} y2={y2} className="bubble-chart-grid"/>; }); return <g ref="xGrid">{lines}</g>; @@ -109,8 +109,7 @@ export const BubbleChart = React.createClass({ let y = yScale(tick); let x1 = xScale.range()[0]; let x2 = xScale.range()[1]; - return <line key={index} x1={x1} x2={x2} y1={y} y2={y} className="bubble-chart-grid" - shapeRendering="crispEdges" strokeWidth="0.3"/>; + return <line key={index} x1={x1} x2={x2} y1={y} y2={y} className="bubble-chart-grid"/>; }); return <g ref="yGrid">{lines}</g>; @@ -156,27 +155,27 @@ export const BubbleChart = React.createClass({ let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2]; let xScale = d3.scale.linear() - .domain([0, d3.max(this.props.items, d => d.x)]) - .range([0, availableWidth]) - .nice(); + .domain([0, d3.max(this.props.items, d => d.x)]) + .range([0, availableWidth]) + .nice(); let yScale = d3.scale.linear() - .domain([0, d3.max(this.props.items, d => d.y)]) - .range([availableHeight, 0]) - .nice(); + .domain([0, d3.max(this.props.items, d => d.y)]) + .range([availableHeight, 0]) + .nice(); let sizeScale = d3.scale.linear() - .domain([0, d3.max(this.props.items, d => d.size)]) - .range(this.props.sizeRange); + .domain([0, d3.max(this.props.items, d => d.size)]) + .range(this.props.sizeRange); xScale.range(this.getXRange(xScale, sizeScale, availableWidth)); yScale.range(this.getYRange(yScale, sizeScale, availableHeight)); - let bubbles = this.props.items - .map((item, index) => { - return <Bubble key={index} - tooltip={item.tooltip} - link={item.link} - x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>; - }); + let bubbles = _.sortBy(this.props.items, (a, b) => b.size - a.size) + .map((item, index) => { + return <Bubble key={index} + tooltip={item.tooltip} + link={item.link} + x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>; + }); return <svg className="bubble-chart" width={this.state.width} height={this.state.height}> <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}> diff --git a/server/sonar-web/src/main/js/helpers/constants.js b/server/sonar-web/src/main/js/helpers/constants.js index 392adf2202a..d1ba85c5395 100644 --- a/server/sonar-web/src/main/js/helpers/constants.js +++ b/server/sonar-web/src/main/js/helpers/constants.js @@ -1,2 +1,4 @@ export const SEVERITIES = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED']; + +export const CHART_COLORS_RANGE_PERCENT = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000']; diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less index 1379e215a94..e85da65f77f 100644 --- a/server/sonar-web/src/main/less/pages/overview.less +++ b/server/sonar-web/src/main/less/pages/overview.less @@ -236,6 +236,7 @@ .overview-detailed-page { flex: 1; + animation: fadeIn 0.5s forwards; } .overview-detailed-measures-list { @@ -316,9 +317,14 @@ .overview-domain-charts { display: flex; - .overview-domain-chart { + & > .overview-domain, + & > .overview-domain-chart { flex: 1; } + + & > .overview-domain { + max-width: 560px; + } } .overview-domain-chart { @@ -445,6 +451,38 @@ align-items: center; } +.overview-bubble-chart { + padding: 10px; + border: 1px solid @barBorderColor; + box-sizing: border-box; + background-color: #fff; + + .bubble-chart-bubble { + fill: @blue; + fill-opacity: 0.2; + stroke: @blue; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { fill-opacity: 0.8; } + } + + .bubble-chart-grid { + shape-rendering: crispedges; + stroke: #eee; + } + + .bubble-chart-tick { + fill: @secondFontColor; + font-size: 11px; + text-anchor: middle; + } + + .bubble-chart-tick-y { + text-anchor: end; + } +} + /* * Responsive Stuff */ diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb index b9447a88eb5..d1004193763 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb @@ -61,7 +61,7 @@ index: '<%= index -%>', mode: '<%= @snapshot.period_mode(index) -%>', modeParam: '<%= @snapshot.period_param(index) -%>', - date: '<%= @snapshot.period_datetime(index).to_date.strftime('%FT%T%z') -%>' + date: '<%= @snapshot.period_datetime(index).strftime('%FT%T%z') -%>' }, <% end %> <% end %> |