diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-03-04 13:52:10 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-03-07 16:10:23 +0100 |
commit | 063357cfe66f882459a9304e9793ba1039b89207 (patch) | |
tree | 9e45658a7bc9db3281bec310f69862ce4439e329 /server/sonar-web/src/main/js | |
parent | b91bac7f106204167511b593b8a49e433abf9afc (diff) | |
download | sonarqube-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')
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]; +} |