import { Treemap } from '../../../components/charts/treemap';
import { getChildren } from '../../../api/components';
import { formatMeasure } from '../../../helpers/measures';
+import { getComponentUrl } from '../../../helpers/urls';
const HEIGHT = 302;
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 => {
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],
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>;
}
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 () {
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';
}
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';
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">
--- /dev/null
+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>;
+ }
+});
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';
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 () {
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>;
}
});
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)
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}
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>;
}
});
this.initTooltips();
},
+ componentWillUpdate() {
+ this.destroyTooltips();
+ },
+
componentDidUpdate () {
this.initTooltips();
},
$('[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');
+ }
}
};
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'];
<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 = {