aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-03-04 13:52:10 +0100
committerStas Vilchik <vilchiks@gmail.com>2016-03-07 16:10:23 +0100
commit063357cfe66f882459a9304e9793ba1039b89207 (patch)
tree9e45658a7bc9db3281bec310f69862ce4439e329 /server/sonar-web/src/main/js
parentb91bac7f106204167511b593b8a49e433abf9afc (diff)
downloadsonarqube-063357cfe66f882459a9304e9793ba1039b89207.tar.gz
sonarqube-063357cfe66f882459a9304e9793ba1039b89207.zip
SONAR-7406 Display bubble chart of selected measure on the "Measures" page
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app.js4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/bubbles.js40
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js6
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/IconBubbles.js35
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/IconHistory.js35
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js164
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js18
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/hooks.js13
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/styles.css30
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.js5
10 files changed, 344 insertions, 6 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures/app.js b/server/sonar-web/src/main/js/apps/component-measures/app.js
index 4f39850bd89..6a140f23c06 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/app.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/app.js
@@ -28,8 +28,9 @@ import MeasureDetails from './components/MeasureDetails';
import MeasureDrilldownTree from './components/MeasureDrilldownTree';
import MeasureDrilldownList from './components/MeasureDrilldownList';
import MeasureHistory from './components/MeasureHistory';
+import MeasureBubbleChart from './components/MeasureBubbleChart';
-import { checkHistoryExistence } from './hooks';
+import { checkHistoryExistence, checkBubbleChartExistence } from './hooks';
import './styles.css';
@@ -59,6 +60,7 @@ window.sonarqube.appStarted.then(options => {
<Route path="tree" component={MeasureDrilldownTree}/>
<Route path="list" component={MeasureDrilldownList}/>
<Route path="history" component={MeasureHistory} onEnter={checkHistoryExistence}/>
+ <Route path="bubbles" component={MeasureBubbleChart} onEnter={checkBubbleChartExistence}/>
</Route>
</Route>
diff --git a/server/sonar-web/src/main/js/apps/component-measures/bubbles.js b/server/sonar-web/src/main/js/apps/component-measures/bubbles.js
new file mode 100644
index 00000000000..bfbaed418b7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/bubbles.js
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+const bubblesConfig = {
+ 'code_smells': { x: 'ncloc', y: 'sqale_index', size: 'code_smells' },
+ 'sqale_index': { x: 'ncloc', y: 'code_smells', size: 'sqale_index' },
+
+ 'coverage': { x: 'complexity', y: 'coverage', size: 'uncovered_lines' },
+ 'it_coverage': { x: 'complexity', y: 'it_coverage', size: 'it_uncovered_lines' },
+ 'overall_coverage': { x: 'complexity', y: 'overall_coverage', size: 'overall_uncovered_lines' },
+
+ 'uncovered_lines': { x: 'complexity', y: 'coverage', size: 'uncovered_lines' },
+ 'it_uncovered_lines': { x: 'complexity', y: 'it_coverage', size: 'it_uncovered_lines' },
+ 'overall_uncovered_lines': { x: 'complexity', y: 'overall_coverage', size: 'overall_uncovered_lines' },
+
+ 'uncovered_conditions': { x: 'complexity', y: 'coverage', size: 'uncovered_conditions' },
+ 'it_uncovered_conditions': { x: 'complexity', y: 'it_coverage', size: 'it_uncovered_conditions' },
+ 'overall_uncovered_conditions': { x: 'complexity', y: 'overall_coverage', size: 'overall_uncovered_conditions' },
+
+ 'duplicated_lines': { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' },
+ 'duplicated_blocks': { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' }
+};
+
+export default bubblesConfig;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js
index 5a12088b4c3..c71d8c9fca7 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.js
@@ -30,7 +30,8 @@ export default class ComponentMeasuresApp extends React.Component {
getChildContext () {
return {
- component: this.props.component
+ component: this.props.component,
+ metrics: this.state.metrics
};
}
@@ -69,5 +70,6 @@ export default class ComponentMeasuresApp extends React.Component {
}
ComponentMeasuresApp.childContextTypes = {
- component: React.PropTypes.object
+ component: React.PropTypes.object,
+ metrics: React.PropTypes.array
};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/IconBubbles.js b/server/sonar-web/src/main/js/apps/component-measures/components/IconBubbles.js
new file mode 100644
index 00000000000..b5411d4cc81
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/IconBubbles.js
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+
+export default function IconBubbles () {
+ /* eslint max-len: 0 */
+ return (
+ <svg className="measure-tab-icon"
+ viewBox="0 0 512 448"
+ fillRule="evenodd"
+ clipRule="evenodd"
+ strokeLinejoin="round"
+ strokeMiterlimit="1.414">
+ <path
+ d="M352 256c52.984 0 96 43.016 96 96s-43.016 96-96 96-96-43.016-96-96 43.016-96 96-96zM128 96c70.645 0 128 57.355 128 128 0 70.645-57.355 128-128 128C57.355 352 0 294.645 0 224 0 153.355 57.355 96 128 96zM352 0c52.984 0 96 43.016 96 96s-43.016 96-96 96-96-43.016-96-96 43.016-96 96-96z"/>
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/IconHistory.js b/server/sonar-web/src/main/js/apps/component-measures/components/IconHistory.js
new file mode 100644
index 00000000000..85dac4e9dec
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/IconHistory.js
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+
+export default function IconHistory () {
+ /* eslint max-len: 0 */
+ return (
+ <svg className="measure-tab-icon"
+ viewBox="0 0 512 448"
+ fillRule="evenodd"
+ clipRule="evenodd"
+ strokeLinejoin="round"
+ strokeMiterlimit="1.414">
+ <path
+ d="M512 384v32H0V32h32v352h480zM480 72v108.75q0 5.25-4.875 7.375t-8.875-1.875L436 156 277.75 314.25q-2.5 2.5-5.75 2.5t-5.75-2.5L208 256 104 360l-48-48 146.25-146.25q2.5-2.5 5.75-2.5t5.75 2.5L272 224l116-116-30.25-30.25q-4-4-1.875-8.875T363.25 64H472q3.5 0 5.75 2.25T480 72z"/>
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js
new file mode 100644
index 00000000000..5654e20cd14
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureBubbleChart.js
@@ -0,0 +1,164 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+
+import Spinner from './Spinner';
+import { BubbleChart } from '../../../components/charts/bubble-chart';
+import bubbles from '../bubbles';
+import { getFiles } from '../../../api/components';
+import { formatMeasure } from '../../../helpers/measures';
+import Workspace from '../../../components/workspace/main';
+
+const HEIGHT = 360;
+const BUBBLES_LIMIT = 500;
+
+function getMeasure (component, metric) {
+ return Number(component.measures[metric]) || 0;
+}
+
+export default class MeasureBubbleChart extends React.Component {
+ state = {
+ fetching: true,
+ files: []
+ };
+
+ componentWillMount () {
+ const { metric } = this.props;
+ const { metrics } = this.context;
+ const conf = bubbles[metric.key];
+
+ this.xMetric = metrics.find(m => m.key === conf.x);
+ this.yMetric = metrics.find(m => m.key === conf.y);
+ this.sizeMetric = metrics.find(m => m.key === conf.size);
+ }
+
+ componentDidMount () {
+ this.mounted = true;
+ this.fetchFiles();
+ }
+
+ componentDidUpdate (nextProps, nextState, nextContext) {
+ if ((nextProps.metric !== this.props.metric) ||
+ (nextContext.component !== this.context.component)) {
+ this.fetchFiles();
+ }
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ fetchFiles () {
+ const { component } = this.context;
+ const metrics = [this.xMetric.key, this.yMetric.key, this.sizeMetric.key];
+ const options = {
+ s: 'metric',
+ metricSort: this.sizeMetric.key,
+ asc: false,
+ ps: BUBBLES_LIMIT
+ };
+
+ getFiles(component.key, metrics, options).then(r => {
+ const files = r.map(file => {
+ const measures = {};
+
+ file.measures.forEach(measure => {
+ measures[measure.metric] = measure.value;
+ });
+ return { ...file, measures };
+ });
+
+ this.setState({
+ files,
+ fetching: false,
+ total: files.length
+ });
+ });
+ }
+
+ getTooltip (component) {
+ const inner = [
+ component.name,
+ `${this.xMetric.name}: ${formatMeasure(getMeasure(component, this.xMetric.key), this.xMetric.type)}`,
+ `${this.yMetric.name}: ${formatMeasure(getMeasure(component, this.yMetric.key), this.yMetric.type)}`,
+ `${this.sizeMetric.name}: ${formatMeasure(getMeasure(component, this.sizeMetric.key), this.sizeMetric.type)}`
+ ].join('<br>');
+
+ return `<div class="text-left">${inner}</div>`;
+ }
+
+ handleBubbleClick (id) {
+ Workspace.openComponent({ uuid: id });
+ }
+
+ renderBubbleChart () {
+ const items = this.state.files.map(file => {
+ return {
+ x: getMeasure(file, this.xMetric.key),
+ y: getMeasure(file, this.yMetric.key),
+ size: getMeasure(file, this.sizeMetric.key),
+ link: file.id,
+ tooltip: this.getTooltip(file)
+ };
+ });
+
+ const formatXTick = (tick) => formatMeasure(tick, this.xMetric.type);
+ const formatYTick = (tick) => formatMeasure(tick, this.yMetric.type);
+
+ return (
+ <BubbleChart
+ items={items}
+ height={HEIGHT}
+ padding={[25, 60, 50, 60]}
+ formatXTick={formatXTick}
+ formatYTick={formatYTick}
+ onBubbleClick={this.handleBubbleClick.bind(this)}/>
+ );
+ }
+
+ render () {
+ const { fetching } = this.state;
+
+ if (fetching) {
+ return (
+ <div className="measure-details-bubble-chart">
+ <div className="note text-center" style={{ lineHeight: `${HEIGHT}px` }}>
+ <Spinner/>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="measure-details-bubble-chart">
+ {this.renderBubbleChart()}
+
+ <div className="measure-details-bubble-chart-axis x">{this.xMetric.name}</div>
+ <div className="measure-details-bubble-chart-axis y">{this.yMetric.name}</div>
+ <div className="measure-details-bubble-chart-axis size">Size: {this.sizeMetric.name}</div>
+ </div>
+ );
+ }
+}
+
+MeasureBubbleChart.contextTypes = {
+ component: React.PropTypes.object,
+ metrics: React.PropTypes.array
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js
index 839e1869862..c3d98277f83 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js
@@ -22,8 +22,10 @@ import { Link } from 'react-router';
import IconList from './IconList';
import IconTree from './IconTree';
+import IconHistory from './IconHistory';
+import IconBubbles from './IconBubbles';
-import { hasHistory } from '../utils';
+import { hasHistory, hasBubbleChart } from '../utils';
import { translate } from '../../../helpers/l10n';
export default class MeasureDrilldown extends React.Component {
@@ -44,6 +46,7 @@ export default class MeasureDrilldown extends React.Component {
{translate('component_measures.tab.tree')}
</Link>
</li>
+
<li>
<Link
activeClassName="active"
@@ -52,15 +55,28 @@ export default class MeasureDrilldown extends React.Component {
{translate('component_measures.tab.list')}
</Link>
</li>
+
{hasHistory(metric.key) && (
<li>
<Link
activeClassName="active"
to={{ pathname: `${metric.key}/history`, query: { id: component.key } }}>
+ <IconHistory/>
{translate('component_measures.tab.history')}
</Link>
</li>
)}
+
+ {hasBubbleChart(metric.key) && (
+ <li>
+ <Link
+ activeClassName="active"
+ to={{ pathname: `${metric.key}/bubbles`, query: { id: component.key } }}>
+ <IconBubbles/>
+ {translate('component_measures.tab.bubbles')}
+ </Link>
+ </li>
+ )}
</ul>
{child}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/hooks.js b/server/sonar-web/src/main/js/apps/component-measures/hooks.js
index d9d58658fc4..03653fa3067 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/hooks.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/hooks.js
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { hasHistory } from './utils';
+import { hasHistory, hasBubbleChart } from './utils';
export function checkHistoryExistence (nextState, replace) {
const { metricKey } = nextState.params;
@@ -29,3 +29,14 @@ export function checkHistoryExistence (nextState, replace) {
});
}
}
+
+export function checkBubbleChartExistence (nextState, replace) {
+ const { metricKey } = nextState.params;
+
+ if (!hasBubbleChart(metricKey)) {
+ replace({
+ pathname: metricKey,
+ query: nextState.location.query
+ });
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/styles.css b/server/sonar-web/src/main/js/apps/component-measures/styles.css
index 72c75b3503d..040ac570462 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/styles.css
+++ b/server/sonar-web/src/main/js/apps/component-measures/styles.css
@@ -46,7 +46,6 @@
background-color: #f5eed0;
}
-
.measure-details {
margin-top: 10px;
}
@@ -204,3 +203,32 @@
.measure-details-history {
padding: 10px;
}
+
+.measure-details-bubble-chart {
+ position: relative;
+ max-width: 960px;
+ margin: 10px auto;
+ padding: 30px 0 30px 50px;
+}
+
+.measure-details-bubble-chart-axis {
+ position: absolute;
+ color: #777;
+ font-size: 12px;
+}
+
+.measure-details-bubble-chart-axis.x {
+ top: 50%;
+ left: -20px;
+ transform: rotate(-90deg);
+}
+
+.measure-details-bubble-chart-axis.y {
+ left: 50%;
+ bottom: 10px;
+}
+
+.measure-details-bubble-chart-axis.size {
+ left: 50%;
+ top: 10px;
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.js b/server/sonar-web/src/main/js/apps/component-measures/utils.js
index f94af95f5c3..0a067cdebb1 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/utils.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/utils.js
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import bubbles from './bubbles';
import { formatMeasure, formatMeasureVariation } from '../../helpers/measures';
export function getLeakValue (measure) {
@@ -86,3 +87,7 @@ export function enhanceWithSingleMeasure (components) {
export function hasHistory (metricKey) {
return metricKey.indexOf('new_') !== 0;
}
+
+export function hasBubbleChart (metricKey) {
+ return !!bubbles[metricKey];
+}