summaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-11-30 15:38:42 +0100
committerStas Vilchik <vilchiks@gmail.com>2015-11-30 15:38:52 +0100
commit8daa623f4787c12a42cf3687430702ea294583f5 (patch)
treef70c21913c26c469840810cf58d02052b8557e20 /server/sonar-web
parentb0bb06b6b6522c403c2935830db4ae155caa6840 (diff)
downloadsonarqube-8daa623f4787c12a42cf3687430702ea294583f5.tar.gz
sonarqube-8daa623f4787c12a42cf3687430702ea294583f5.zip
SONAR-6946 Add an ability to drilldown on the overview treemap
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/domain-treemap.js62
-rw-r--r--server/sonar-web/src/main/js/apps/overview/domains/coverage-domain.js6
-rw-r--r--server/sonar-web/src/main/js/apps/overview/domains/duplications-domain.js2
-rw-r--r--server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js46
-rw-r--r--server/sonar-web/src/main/js/components/charts/treemap.js45
-rw-r--r--server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js11
-rw-r--r--server/sonar-web/src/main/js/helpers/constants.js1
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/dashboard/no_dashboard.html.erb1
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>&nbsp;</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 = {