diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-11-30 15:38:42 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-11-30 15:38:52 +0100 |
commit | 8daa623f4787c12a42cf3687430702ea294583f5 (patch) | |
tree | f70c21913c26c469840810cf58d02052b8557e20 /server/sonar-web | |
parent | b0bb06b6b6522c403c2935830db4ae155caa6840 (diff) | |
download | sonarqube-8daa623f4787c12a42cf3687430702ea294583f5.tar.gz sonarqube-8daa623f4787c12a42cf3687430702ea294583f5.zip |
SONAR-6946 Add an ability to drilldown on the overview treemap
Diffstat (limited to 'server/sonar-web')
8 files changed, 147 insertions, 27 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/components/domain-treemap.js b/server/sonar-web/src/main/js/apps/overview/components/domain-treemap.js index 0014cd54e9f..29d08065f0b 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/domain-treemap.js +++ b/server/sonar-web/src/main/js/apps/overview/components/domain-treemap.js @@ -4,6 +4,7 @@ import React from 'react'; import { Treemap } from '../../../components/charts/treemap'; import { getChildren } from '../../../api/components'; import { formatMeasure } from '../../../helpers/measures'; +import { getComponentUrl } from '../../../helpers/urls'; const HEIGHT = 302; @@ -16,17 +17,18 @@ export class DomainTreemap extends React.Component { loading: true, files: [], sizeMetric: this.getMetricObject(props.metrics, props.sizeMetric), - colorMetric: props.colorMetric ? this.getMetricObject(props.metrics, props.colorMetric) : null + colorMetric: props.colorMetric ? this.getMetricObject(props.metrics, props.colorMetric) : null, + breadcrumbs: [] }; } componentDidMount () { - this.requestComponents(); + this.requestComponents(this.props.component.key); } - requestComponents () { + requestComponents (componentKey) { let metrics = [this.props.sizeMetric, this.props.colorMetric]; - return getChildren(this.props.component.key, metrics).then(r => { + return getChildren(componentKey, metrics).then(r => { let components = r.map(component => { let measures = {}; (component.msr || []).forEach(measure => { @@ -45,7 +47,8 @@ export class DomainTreemap extends React.Component { getTooltip (component) { let inner = [ component.name, - `${this.state.sizeMetric.name}: ${formatMeasure(component.measures[this.props.sizeMetric], this.state.sizeMetric.type)}` + `${this.state.sizeMetric.name}: + ${formatMeasure(component.measures[this.props.sizeMetric], this.state.sizeMetric.type)}` ]; if (this.state.colorMetric) { let measure = component.measures[this.props.colorMetric], @@ -56,15 +59,31 @@ export class DomainTreemap extends React.Component { return `<div class="text-left">${inner}</div>`; } - renderLoading () { - return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}> - <i className="spinner"/> - </div>; + handleRectangleClick (node) { + this.requestComponents(node.key).then(() => { + let nextBreadcrumbs = [...this.state.breadcrumbs]; + let index = _.findIndex(this.state.breadcrumbs, b => b.key === node.key); + if (index !== -1) { + nextBreadcrumbs = nextBreadcrumbs.slice(0, index); + } + nextBreadcrumbs = [...nextBreadcrumbs, { + key: node.key, + name: node.name, + qualifier: node.qualifier + }]; + this.setState({ breadcrumbs: nextBreadcrumbs }); + }); + } + + handleReset() { + this.requestComponents(this.props.component.key).then(() => { + this.setState({ breadcrumbs: [] }); + }); } - renderWhenNoData () { + renderLoading () { return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}> - {window.t('no_data')} + <i className="spinner"/> </div>; } @@ -73,21 +92,30 @@ export class DomainTreemap extends React.Component { return this.renderLoading(); } - if (!this.state.components.length) { - return this.renderWhenNoData(); - } - // 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 { + key: component.key, + name: component.name, + qualifier: component.qualifier, size: component.measures[this.props.sizeMetric], color: colorMeasure != null ? this.props.scale(colorMeasure) : '#777', tooltip: this.getTooltip(component), - label: component.name + label: component.name, + link: getComponentUrl(component.key) }; }); - return <Treemap items={items} height={HEIGHT}/>; + + const canBeClicked = node => node.qualifier !== 'FIL' && node.qualifier !== 'UTS'; + + return <Treemap + items={items} + breadcrumbs={this.state.breadcrumbs} + height={HEIGHT} + canBeClicked={canBeClicked} + onRectangleClick={this.handleRectangleClick.bind(this)} + onReset={this.handleReset.bind(this)}/>; } render () { diff --git a/server/sonar-web/src/main/js/apps/overview/domains/coverage-domain.js b/server/sonar-web/src/main/js/apps/overview/domains/coverage-domain.js index 6b0cbc391bc..d79f7846f51 100644 --- a/server/sonar-web/src/main/js/apps/overview/domains/coverage-domain.js +++ b/server/sonar-web/src/main/js/apps/overview/domains/coverage-domain.js @@ -10,7 +10,7 @@ import { getPeriodLabel, getPeriodDate } from './../helpers/periods'; import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; import { filterMetrics, filterMetricsForDomains } from '../helpers/metrics'; import { DomainLeakTitle } from '../main/components'; -import { CHART_COLORS_RANGE_PERCENT } from '../../../helpers/constants'; +import { CHART_REVERSED_COLORS_RANGE_PERCENT } from '../../../helpers/constants'; import { CoverageMeasuresList } from '../components/coverage-measures-list'; @@ -83,8 +83,8 @@ export const CoverageMain = React.createClass({ } let treemapScale = d3.scale.linear() - .domain([0, 100]) - .range(CHART_COLORS_RANGE_PERCENT); + .domain([0, 25, 50, 75, 100]) + .range(CHART_REVERSED_COLORS_RANGE_PERCENT); let coverageMetric = this.state.coverageMetricPrefix + 'coverage', uncoveredLinesMetric = this.state.coverageMetricPrefix + 'uncovered_lines'; diff --git a/server/sonar-web/src/main/js/apps/overview/domains/duplications-domain.js b/server/sonar-web/src/main/js/apps/overview/domains/duplications-domain.js index 946b6f1663b..fe4cae3afac 100644 --- a/server/sonar-web/src/main/js/apps/overview/domains/duplications-domain.js +++ b/server/sonar-web/src/main/js/apps/overview/domains/duplications-domain.js @@ -82,7 +82,7 @@ export const DuplicationsMain = React.createClass({ return this.renderLoading(); } let treemapScale = d3.scale.linear() - .domain([0, 100]) + .domain([0, 25, 50, 75, 100]) .range(CHART_COLORS_RANGE_PERCENT); return <div className="overview-detailed-page"> <div className="overview-cards-list"> diff --git a/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js b/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js new file mode 100644 index 00000000000..bbd6b2bf1f6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js @@ -0,0 +1,46 @@ +import React from 'react'; + +import QualifierIcon from '../shared/qualifier-icon'; + + +export const TreemapBreadcrumbs = React.createClass({ + propTypes: { + breadcrumbs: React.PropTypes.arrayOf(React.PropTypes.shape({ + key: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired, + qualifier: React.PropTypes.string.isRequired + }).isRequired).isRequired + }, + + handleItemClick(item, e) { + e.preventDefault(); + this.props.onRectangleClick(item); + }, + + handleReset(e) { + e.preventDefault(); + this.props.onReset(); + }, + + renderHome() { + return <span className="treemap-breadcrumbs-item"> + <a onClick={this.handleReset} className="icon-home" href="#"/> + </span>; + }, + + renderBreadcrumbsItems(b) { + return <span key={b.key} className="treemap-breadcrumbs-item" title={b.name}> + <i className="icon-chevron-right"/> + <QualifierIcon qualifier={b.qualifier}/> + <a onClick={this.handleItemClick.bind(this, b)} href="#">{b.name}</a> + </span>; + }, + + render() { + let breadcrumbs = this.props.breadcrumbs.map(this.renderBreadcrumbsItems); + return <div className="treemap-breadcrumbs"> + {this.props.breadcrumbs.length ? this.renderHome() : null} + {breadcrumbs} + </div>; + } +}); 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 de56351ff7e..fe036d3fb9d 100644 --- a/server/sonar-web/src/main/js/components/charts/treemap.js +++ b/server/sonar-web/src/main/js/components/charts/treemap.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import d3 from 'd3'; import React from 'react'; +import { TreemapBreadcrumbs } from './treemap-breadcrumbs'; import { ResizeMixin } from './../mixins/resize-mixin'; import { TooltipsMixin } from './../mixins/tooltips-mixin'; @@ -35,7 +36,19 @@ export const TreemapRect = React.createClass({ height: React.PropTypes.number.isRequired, fill: React.PropTypes.string.isRequired, label: React.PropTypes.string.isRequired, - prefix: React.PropTypes.string + prefix: React.PropTypes.string, + onClick: React.PropTypes.func + }, + + renderLink() { + if (!this.props.link) { + return null; + } + + return <a onClick={e => e.stopPropagation()} + className="treemap-link" + href={this.props.link} + style={{ fontSize: 12 }}><i className="icon-link"/></a>; }, render () { @@ -53,12 +66,14 @@ export const TreemapRect = React.createClass({ height: this.props.height, backgroundColor: this.props.fill, fontSize: SIZE_SCALE(this.props.width / this.props.label.length), - lineHeight: `${this.props.height}px` + lineHeight: `${this.props.height}px`, + cursor: typeof this.props.onClick === 'function' ? 'pointer' : 'default' }; let isTextVisible = this.props.width >= 40 && this.props.height >= 40; - return <div className="treemap-cell" {...tooltipAttrs} style={cellStyles}> + return <div className="treemap-cell" {...tooltipAttrs} style={cellStyles} onClick={this.props.onClick}> <div className="treemap-inner" dangerouslySetInnerHTML={{ __html: this.props.label }} style={{ maxWidth: this.props.width, visibility: isTextVisible ? 'visible': 'hidden' }}/> + {this.renderLink()} </div>; } }); @@ -69,18 +84,32 @@ export const Treemap = React.createClass({ propTypes: { items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - height: React.PropTypes.number + height: React.PropTypes.number, + onRectangleClick: React.PropTypes.func }, getInitialState() { return { width: this.props.width, height: this.props.height }; }, + renderWhenNoData () { + return <div className="sonar-d3"> + <div className="treemap-container" style={{ width: this.state.width, height: this.state.height }}> + {window.t('no_data')} + </div> + <TreemapBreadcrumbs {...this.props}/> + </div> + }, + render () { - if (!this.state.width || !this.state.height || !this.props.items.length) { + if (!this.state.width || !this.state.height) { return <div> </div>; } + if (!this.props.items.length) { + return this.renderWhenNoData(); + } + let treemap = d3.layout.treemap() .round(true) .value(d => d.size) @@ -96,6 +125,7 @@ export const Treemap = React.createClass({ let rectangles = nodes.map((node, index) => { let label = prefixLength ? `${prefix}<br>${node.label.substr(prefixLength)}` : node.label; + const onClick = this.props.canBeClicked(node) ? () => this.props.onRectangleClick(node) : null; return <TreemapRect key={index} x={node.x} y={node.y} @@ -104,13 +134,16 @@ export const Treemap = React.createClass({ fill={node.color} label={label} prefix={prefix} - tooltip={node.tooltip}/>; + tooltip={node.tooltip} + link={node.link} + onClick={onClick}/>; }); return <div className="sonar-d3"> <div className="treemap-container" style={{ width: this.state.width, height: this.state.height }}> {rectangles} </div> + <TreemapBreadcrumbs {...this.props}/> </div>; } }); diff --git a/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js index 240edee02c5..af0c2be023e 100644 --- a/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js +++ b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js @@ -7,6 +7,10 @@ export const TooltipsMixin = { this.initTooltips(); }, + componentWillUpdate() { + this.destroyTooltips(); + }, + componentDidUpdate () { this.initTooltips(); }, @@ -16,5 +20,12 @@ export const TooltipsMixin = { $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this)) .tooltip({ container: 'body', placement: 'bottom', html: true }); } + }, + + destroyTooltips () { + if ($.fn.tooltip) { + $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this)) + .tooltip('destroy'); + } } }; diff --git a/server/sonar-web/src/main/js/helpers/constants.js b/server/sonar-web/src/main/js/helpers/constants.js index d1ba85c5395..80653c27a68 100644 --- a/server/sonar-web/src/main/js/helpers/constants.js +++ b/server/sonar-web/src/main/js/helpers/constants.js @@ -2,3 +2,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']; +export const CHART_REVERSED_COLORS_RANGE_PERCENT = ['#ee0000', '#f77700', '#ffee00', '#80cc00', '#00aa00']; diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/dashboard/no_dashboard.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/dashboard/no_dashboard.html.erb index 72416ed82eb..74d50a2f7a0 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/dashboard/no_dashboard.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/dashboard/no_dashboard.html.erb @@ -6,6 +6,7 @@ <script> (function () { jQuery('.navbar-context').remove(); + jQuery('#sidebar').remove(); jQuery('.page-wrapper-context').addClass('page-wrapper-global').removeClass('page-wrapper-context'); window.sonarqube.el = '#source-viewer'; window.sonarqube.file = { |