summaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-03-24 11:20:18 +0100
committerStas Vilchik <vilchiks@gmail.com>2016-03-25 13:02:42 +0100
commitbdd14b31bcd4ff599dd2dffd133d80f43b8970c3 (patch)
tree55d5ac42dd12c67853794c163cd3c4603f3dca36 /server/sonar-web
parent5869fc40c78cd2e1b7b29a7d74670c0e5a6c1afa (diff)
downloadsonarqube-bdd14b31bcd4ff599dd2dffd133d80f43b8970c3.tar.gz
sonarqube-bdd14b31bcd4ff599dd2dffd133d80f43b8970c3.zip
SONAR-7402 improve display of measures home page
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/.eslintrc13
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/SourceViewer.js1
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app.js14
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app/actions.js5
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app/reducer.js7
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/Measure.js49
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js (renamed from server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js)41
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/MeasureBubbleChartContainer.js (renamed from server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js)3
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js22
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/config/domains.js235
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js27
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js1
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js15
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js10
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js15
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js42
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js12
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js97
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/DomainMeasures.js84
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/DomainMeasuresContainer.js42
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/Home.js78
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/HomeContainer.js44
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/MainMeasures.js50
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/MeasuresList.js58
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/home/reducer.js29
-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.css45
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.js4
-rw-r--r--server/sonar-web/src/main/js/components/shared/Level.js34
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.js2
-rw-r--r--server/sonar-web/src/main/less/components/ui.less58
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/drilldown_controller.rb2
-rw-r--r--server/sonar-web/tests/helpers/urls-test.js4
34 files changed, 880 insertions, 280 deletions
diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc
index 21fa38c27a1..b22ddbb9776 100644
--- a/server/sonar-web/.eslintrc
+++ b/server/sonar-web/.eslintrc
@@ -1,5 +1,9 @@
{
- "extends": "eslint:recommended",
+ "extends": [
+ "eslint:recommended",
+ "plugin:import/errors",
+ "plugin:import/warnings"
+ ],
"ecmaFeatures": {
"jsx": true,
@@ -71,11 +75,6 @@
"react/prop-types": 0,
"react/react-in-jsx-scope": 2,
"react/self-closing-comp": 2,
- "react/sort-comp": 1,
-
- "import/no-unresolved": 2,
- "import/export": 2,
- "import/no-duplicates": 2,
- "import/imports-first": 2
+ "react/sort-comp": 1
}
}
diff --git a/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js b/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js
index 0f0431a9e3d..d79983625c6 100644
--- a/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js
+++ b/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js
@@ -60,7 +60,6 @@ export default class SourceViewer extends Component {
if (period) {
const periodDate = getPeriodDate(period);
const periodLabel = getPeriodLabel(period);
- console.log(periodDate, periodLabel);
this.sourceViewer.filterLinesByDate(periodDate, periodLabel);
}
}
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 f3340803c41..264b835b95e 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
@@ -24,17 +24,18 @@ import { createHistory } from 'history';
import { Provider } from 'react-redux';
import AppContainer from './app/AppContainer';
+import HomeContainer from './home/HomeContainer';
import AllMeasuresContainer from './home/AllMeasuresContainer';
+import DomainMeasuresContainer from './home/DomainMeasuresContainer';
import MeasureDetailsContainer from './details/MeasureDetailsContainer';
import ListViewContainer from './details/drilldown/ListViewContainer';
import TreeViewContainer from './details/drilldown/TreeViewContainer';
import MeasureHistoryContainer from './details/history/MeasureHistoryContainer';
-import MeasureBubbleChartContainer from './details/bubbleChart/MeasureBubbleChartContainer';
import MeasureTreemapContainer from './details/treemap/MeasureTreemapContainer';
import configureStore from './store/configureStore';
-import { checkHistoryExistence, checkBubbleChartExistence } from './hooks';
+import { checkHistoryExistence } from './hooks';
import './styles.css';
@@ -59,13 +60,16 @@ window.sonarqube.appStarted.then(options => {
<Redirect from="/index" to="/"/>
<Route path="/" component={AppContainer}>
- <IndexRoute component={AllMeasuresContainer}/>
- <Route path=":metricKey" component={MeasureDetailsContainer}>
+ <Route component={HomeContainer}>
+ <IndexRoute component={AllMeasuresContainer}/>
+ <Route path="domain/:domainName" component={DomainMeasuresContainer}/>
+ </Route>
+
+ <Route path="metric/:metricKey" component={MeasureDetailsContainer}>
<IndexRedirect to="list"/>
<Route path="list" component={ListViewContainer}/>
<Route path="tree" component={TreeViewContainer}/>
<Route path="history" component={MeasureHistoryContainer} onEnter={checkHistoryExistence}/>
- <Route path="bubbles" component={MeasureBubbleChartContainer} onEnter={checkBubbleChartExistence}/>
<Route path="treemap" component={MeasureTreemapContainer}/>
</Route>
</Route>
diff --git a/server/sonar-web/src/main/js/apps/component-measures/app/actions.js b/server/sonar-web/src/main/js/apps/component-measures/app/actions.js
index e38934a9bee..137b9592f72 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/app/actions.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/app/actions.js
@@ -24,6 +24,7 @@ import { getMetrics } from '../../../api/metrics';
*/
export const DISPLAY_HOME = 'app/DISPLAY_HOME';
+export const DISPLAY_DOMAIN = 'app/DISPLAY_DOMAIN';
export const RECEIVE_METRICS = 'app/RECEIVE_METRICS';
@@ -35,6 +36,10 @@ export function displayHome () {
return { type: DISPLAY_HOME };
}
+export function displayDomain (domainName) {
+ return { type: DISPLAY_DOMAIN, domainName };
+}
+
function receiveMetrics (metrics) {
return { type: RECEIVE_METRICS, metrics };
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js b/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js
index 2484e2d0563..f481ff479a7 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/app/reducer.js
@@ -17,14 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { RECEIVE_METRICS } from './actions';
+import { DISPLAY_DOMAIN, RECEIVE_METRICS } from './actions';
const initialState = {
- metrics: undefined
+ metrics: undefined,
+ lastDisplayedDomain: undefined
};
export default function appReducer (state = initialState, action = {}) {
switch (action.type) {
+ case DISPLAY_DOMAIN:
+ return { ...state, lastDisplayedDomain: action.domainName };
case RECEIVE_METRICS:
return { ...state, metrics: action.metrics };
default:
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Measure.js b/server/sonar-web/src/main/js/apps/component-measures/components/Measure.js
new file mode 100644
index 00000000000..755d5eb20d9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/Measure.js
@@ -0,0 +1,49 @@
+/*
+ * 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 { Rating } from '../../../components/shared/rating';
+import Level from '../../../components/shared/Level';
+import { formatMeasure } from '../../../helpers/measures';
+import { formatLeak, isDiffMetric } from '../utils';
+
+const Measure = ({ measure, metric }) => {
+ const finalMetric = metric || measure.metric;
+
+ if (finalMetric.type === 'RATING') {
+ return <Rating value={measure.value}/>;
+ }
+
+ if (finalMetric.type === 'LEVEL') {
+ return <Level level={measure.value}/>;
+ }
+
+ const formattedValue = isDiffMetric(finalMetric) ?
+ formatLeak(measure.leak, finalMetric) :
+ formatMeasure(measure.value, finalMetric.type);
+
+ return (
+ <span>
+ {formattedValue}
+ </span>
+ );
+};
+
+export default Measure;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js
index a43e93f53e7..2fcfb83778b 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/BubbleChart.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js
@@ -19,7 +19,7 @@
*/
import React from 'react';
-import Spinner from './../../components/Spinner';
+import Spinner from './../Spinner';
import { BubbleChart as OriginalBubbleChart } from '../../../../components/charts/bubble-chart';
import bubbles from '../../config/bubbles';
import { getComponentLeaves } from '../../../../api/components';
@@ -35,17 +35,12 @@ function getMeasure (component, metric) {
export default class BubbleChart extends React.Component {
state = {
- fetching: true,
+ fetching: 0,
files: []
};
componentWillMount () {
- const { metric, metrics } = this.props;
- 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);
+ this.updateMetrics(this.props);
}
componentDidMount () {
@@ -53,8 +48,12 @@ export default class BubbleChart extends React.Component {
this.fetchFiles();
}
+ componentWillUpdate (nextProps) {
+ this.updateMetrics(nextProps);
+ }
+
componentDidUpdate (nextProps) {
- if (nextProps.metric !== this.props.metric) {
+ if (nextProps.domainName !== this.props.domainName) {
this.fetchFiles();
}
}
@@ -63,6 +62,14 @@ export default class BubbleChart extends React.Component {
this.mounted = false;
}
+ updateMetrics (props) {
+ const { metrics, domainName } = props;
+ const conf = bubbles[domainName];
+ 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);
+ }
+
fetchFiles () {
const { component } = this.props;
const metrics = [this.xMetric.key, this.yMetric.key, this.sizeMetric.key];
@@ -73,6 +80,10 @@ export default class BubbleChart extends React.Component {
ps: BUBBLES_LIMIT
};
+ if (this.mounted) {
+ this.setState({ fetching: this.state.fetching + 1 });
+ }
+
getComponentLeaves(component.key, metrics, options).then(r => {
const files = r.components.map(file => {
const measures = {};
@@ -83,11 +94,13 @@ export default class BubbleChart extends React.Component {
return { ...file, measures };
});
- this.setState({
- files,
- fetching: false,
- total: files.length
- });
+ if (this.mounted) {
+ this.setState({
+ files,
+ fetching: this.state.fetching - 1,
+ total: files.length
+ });
+ }
});
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/MeasureBubbleChartContainer.js
index 0dba5dcada8..f219bfe01cc 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/bubbleChart/MeasureBubbleChartContainer.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/MeasureBubbleChartContainer.js
@@ -24,8 +24,7 @@ import MeasureBubbleChart from './BubbleChart';
const mapStateToProps = state => {
return {
component: state.app.component,
- metrics: state.app.metrics,
- metric: state.details.metric
+ metrics: state.app.metrics
};
};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js
index bfbaed418b7..a9b0702c0a3 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js
@@ -18,23 +18,11 @@
* 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' }
+ 'Reliability': { x: 'ncloc', y: 'reliability_remediation_effort', size: 'bugs' },
+ 'Security': { x: 'ncloc', y: 'security_remediation_effort', size: 'vulnerabilities' },
+ 'Maintainability': { x: 'ncloc', y: 'sqale_index', size: 'code_smells' },
+ 'Tests': { x: 'complexity', y: 'coverage', size: 'uncovered_lines' },
+ 'Duplication': { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' }
};
export default bubblesConfig;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/domains.js b/server/sonar-web/src/main/js/apps/component-measures/config/domains.js
index 3e91106645a..931dbcdba57 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/config/domains.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/config/domains.js
@@ -17,105 +17,156 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-export default {
- 'Issues': [
- 'violations',
- 'new_violations',
- 'blocker_violations',
- 'new_blocker_violations',
- 'critical_violations',
- 'new_critical_violations',
- 'major_violations',
- 'new_major_violations',
- 'minor_violations',
- 'new_minor_violations',
- 'info_violations',
- 'new_info_violations',
- 'open_issues',
- 'reopened_issues',
- 'confirmed_issues',
- 'false_positive_issues'
- ],
+export const domains = {
+ 'Reliability': {
+ main: [
+ 'bugs',
+ 'new_bugs',
+ 'reliability_rating'
+ ],
+ order: [
+ 'bugs',
+ 'new_bugs',
+ 'reliability_rating',
+ 'reliability_remediation_effort',
+ 'new_reliability_remediation_effort',
+ 'effort_to_reach_reliability_rating_a'
+ ]
+ },
- 'Maintainability': [
- 'code_smells',
- 'new_code_smells',
- 'sqale_index',
- 'new_technical_debt',
- 'sqale_rating',
- 'sqale_debt_ratio',
- 'new_sqale_debt_ratio',
- 'effort_to_reach_maintainability_rating_a'
- ],
+ 'Security': {
+ main: [
+ 'vulnerabilities',
+ 'new_vulnerabilities',
+ 'security_rating'
+ ],
+ order: [
+ 'vulnerabilities',
+ 'new_vulnerabilities',
+ 'security_remediation_effort',
+ 'new_security_remediation_effort',
+ 'effort_to_reach_security_rating_a'
+ ]
+ },
- 'Reliability': [
- 'bugs',
- 'new_bugs',
- 'reliability_rating',
- 'reliability_remediation_effort',
- 'new_reliability_remediation_effort',
- 'effort_to_reach_reliability_rating_a'
- ],
+ 'Maintainability': {
+ main: [
+ 'code_smells',
+ 'new_code_smells',
+ 'sqale_index',
+ 'new_technical_debt',
+ 'sqale_rating'
+ ],
+ order: [
+ 'code_smells',
+ 'new_code_smells',
+ 'sqale_index',
+ 'new_technical_debt',
+ 'sqale_debt_ratio',
+ 'new_sqale_debt_ratio',
+ 'effort_to_reach_maintainability_rating_a'
+ ]
+ },
- 'Security': [
- 'vulnerabilities',
- 'new_vulnerabilities',
- 'security_rating',
- 'security_remediation_effort',
- 'new_security_remediation_effort',
- 'effort_to_reach_security_rating_a'
- ],
+ 'Tests': {
+ main: [
+ 'overall_coverage',
+ 'new_overall_coverage',
+ 'coverage',
+ 'new_coverage',
+ 'it_coverage',
+ 'new_it_coverage',
+ 'tests'
+ ],
+ order: [
+ 'overall_coverage',
+ 'new_overall_coverage',
+ 'overall_line_coverage',
+ 'new_overall_line_coverage',
+ 'overall_branch_coverage',
+ 'new_overall_branch_coverage',
+ 'overall_uncovered_lines',
+ 'new_overall_uncovered_lines',
+ 'overall_uncovered_conditions',
+ 'new_overall_uncovered_conditions',
+ 'new_overall_lines_to_cover',
- 'Size': [
- 'ncloc',
- 'lines'
- ],
+ 'coverage',
+ 'new_coverage',
+ 'line_coverage',
+ 'new_line_coverage',
+ 'branch_coverage',
+ 'new_branch_coverage',
+ 'uncovered_lines',
+ 'new_uncovered_lines',
+ 'uncovered_conditions',
+ 'new_uncovered_conditions',
+ 'new_lines_to_cover',
- 'Tests': [
- 'overall_coverage',
- 'new_overall_coverage',
- 'overall_line_coverage',
- 'new_overall_line_coverage',
- 'overall_branch_coverage',
- 'new_overall_branch_coverage',
- 'overall_uncovered_lines',
- 'new_overall_uncovered_lines',
- 'overall_uncovered_conditions',
- 'new_overall_uncovered_conditions',
- 'new_overall_lines_to_cover',
+ 'it_coverage',
+ 'new_it_coverage',
+ 'it_line_coverage',
+ 'new_it_line_coverage',
+ 'it_branch_coverage',
+ 'new_it_branch_coverage',
+ 'it_uncovered_lines',
+ 'new_it_uncovered_lines',
+ 'it_uncovered_conditions',
+ 'new_it_uncovered_conditions',
+ 'new_it_lines_to_cover',
- 'coverage',
- 'new_coverage',
- 'line_coverage',
- 'new_line_coverage',
- 'branch_coverage',
- 'new_branch_coverage',
- 'uncovered_lines',
- 'new_uncovered_lines',
- 'uncovered_conditions',
- 'new_uncovered_conditions',
- 'new_lines_to_cover',
+ 'lines_to_cover',
- 'it_coverage',
- 'new_it_coverage',
- 'it_line_coverage',
- 'new_it_line_coverage',
- 'it_branch_coverage',
- 'new_it_branch_coverage',
- 'it_uncovered_lines',
- 'new_it_uncovered_lines',
- 'it_uncovered_conditions',
- 'new_it_uncovered_conditions',
- 'new_it_lines_to_cover',
+ 'tests',
+ 'test_success',
+ 'test_errors',
+ 'test_failures',
+ 'skipped_tests',
+ 'test_success_density',
+ 'test_execution_time'
+ ]
+ },
- 'lines_to_cover',
+ 'Duplication': {
+ main: [
+ 'duplicated_lines_density',
+ 'duplicated_blocks'
+ ],
+ order: [
+ 'duplicated_lines_density',
+ 'duplicated_blocks',
+ 'duplicated_lines',
+ 'duplicated_files'
+ ]
+ },
- 'tests',
- 'test_success',
- 'test_errors',
- 'test_failures',
- 'skipped_tests',
- 'test_success_density',
- 'test_execution_time'
- ]
+ 'Size': {
+ main: [
+ 'ncloc',
+ 'files'
+ ],
+ order: []
+ },
+
+ 'Issues': {
+ main: [],
+ order: [
+ 'violations',
+ 'new_violations',
+ 'blocker_violations',
+ 'new_blocker_violations',
+ 'critical_violations',
+ 'new_critical_violations',
+ 'major_violations',
+ 'new_major_violations',
+ 'minor_violations',
+ 'new_minor_violations',
+ 'info_violations',
+ 'new_info_violations',
+ 'open_issues',
+ 'reopened_issues',
+ 'confirmed_issues',
+ 'false_positive_issues'
+ ]
+ }
};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js
index 8cd30894cdd..93ccd1e32e0 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetails.js
@@ -18,14 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import { IndexLink } from 'react-router';
+import { Link, IndexLink } from 'react-router';
import Spinner from './../components/Spinner';
import MeasureDetailsHeader from './MeasureDetailsHeader';
import MeasureDrilldown from './drilldown/MeasureDrilldown';
-
import { getLeakPeriod, getPeriodDate, getPeriodLabel } from '../../../helpers/periods';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
export default class MeasureDetails extends React.Component {
componentWillMount () {
@@ -55,7 +54,7 @@ export default class MeasureDetails extends React.Component {
}
render () {
- const { component, metric, secondaryMeasure, measure, periods, children } = this.props;
+ const { component, metric, secondaryMeasure, measure, periods, lastDisplayedDomain, children } = this.props;
if (measure == null) {
return <Spinner/>;
@@ -68,12 +67,20 @@ export default class MeasureDetails extends React.Component {
return (
<section id="component-measures-details" className="page page-container page-limited">
- <IndexLink
- to={{ pathname: '/', query: { id: component.key } }}
- id="component-measures-back-to-all-measures"
- className="small text-muted">
- {translate('component_measures.back_to_all_measures')}
- </IndexLink>
+ {lastDisplayedDomain ? (
+ <Link
+ to={{ pathname: `domain/${lastDisplayedDomain}`, query: { id: component.key } }}
+ className="small text-muted">
+ {translateWithParameters('component_measures.back_to_domain_measures', lastDisplayedDomain)}
+ </Link>
+ ) : (
+ <IndexLink
+ to={{ pathname: '/', query: { id: component.key } }}
+ id="component-measures-back-to-all-measures"
+ className="small text-muted">
+ {translate('component_measures.back_to_all_measures')}
+ </IndexLink>
+ )}
<MeasureDetailsHeader
measure={measure}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js
index 80000e23ff3..3d009be366b 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsContainer.js
@@ -25,6 +25,7 @@ import { fetchMeasure } from './actions';
const mapStateToProps = state => {
return {
component: state.app.component,
+ lastDisplayedDomain: state.app.lastDisplayedDomain,
metrics: state.app.metrics,
metric: state.details.metric,
measure: state.details.measure,
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js
index 13e3080a352..3020d94a017 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/MeasureDetailsHeader.js
@@ -19,10 +19,10 @@
*/
import React from 'react';
+import Measure from './../components/Measure';
import LanguageDistribution from './../components/LanguageDistribution';
import { ComplexityDistribution } from '../../overview/components/complexity-distribution';
import { formatLeak } from '../utils';
-import { formatMeasure } from '../../../helpers/measures';
import { translateWithParameters } from '../../../helpers/l10n';
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
@@ -41,7 +41,7 @@ export default function MeasureDetailsHeader ({ measure, metric, secondaryMeasur
<div className="measure-details-value">
{measure.value != null && (
<div className="measure-details-value-absolute">
- {formatMeasure(measure.value, metric.type)}
+ <Measure measure={measure} metric={metric}/>
</div>
)}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js
index 97c25a1b723..1ade9e39ba4 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js
@@ -22,6 +22,7 @@ import classNames from 'classnames';
import ComponentsList from './ComponentsList';
import ListHeader from './ListHeader';
+import Spinner from '../../components/Spinner';
import SourceViewer from '../../../code/components/SourceViewer';
import ListFooter from '../../../../components/shared/list-footer';
@@ -104,11 +105,15 @@ export default class ListView extends React.Component {
{!selected && (
<div className={classNames({ 'new-loading': fetching })}>
- <ComponentsList
- components={components}
- selected={selected}
- metric={metric}
- onClick={this.handleClick.bind(this)}/>
+ {(!fetching || components.length !== 0) ? (
+ <ComponentsList
+ components={components}
+ selected={selected}
+ metric={metric}
+ onClick={this.handleClick.bind(this)}/>
+ ) : (
+ <Spinner/>
+ )}
<ListFooter
count={components.length}
total={total}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js
index 3df7565a694..fe2946c15b8 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/MeasureDrilldown.js
@@ -41,7 +41,7 @@ export default class MeasureDrilldown extends React.Component {
<li>
<Link
activeClassName="active"
- to={{ pathname: `${metric.key}/list`, query: { id: component.key } }}>
+ to={{ pathname: `metric/${metric.key}/list`, query: { id: component.key } }}>
<IconList/>
{translate('component_measures.tab.list')}
</Link>
@@ -50,7 +50,7 @@ export default class MeasureDrilldown extends React.Component {
<li>
<Link
activeClassName="active"
- to={{ pathname: `${metric.key}/tree`, query: { id: component.key } }}>
+ to={{ pathname: `metric/${metric.key}/tree`, query: { id: component.key } }}>
<IconTree/>
{translate('component_measures.tab.tree')}
</Link>
@@ -60,7 +60,7 @@ export default class MeasureDrilldown extends React.Component {
<li>
<Link
activeClassName="active"
- to={{ pathname: `${metric.key}/bubbles`, query: { id: component.key } }}>
+ to={{ pathname: `metric/${metric.key}/bubbles`, query: { id: component.key } }}>
<IconBubbles/>
{translate('component_measures.tab.bubbles')}
</Link>
@@ -71,7 +71,7 @@ export default class MeasureDrilldown extends React.Component {
<li>
<Link
activeClassName="active"
- to={{ pathname: `${metric.key}/treemap`, query: { id: component.key } }}>
+ to={{ pathname: `metric/${metric.key}/treemap`, query: { id: component.key } }}>
<IconTreemap/>
{translate('component_measures.tab.treemap')}
</Link>
@@ -82,7 +82,7 @@ export default class MeasureDrilldown extends React.Component {
<li>
<Link
activeClassName="active"
- to={{ pathname: `${metric.key}/history`, query: { id: component.key } }}>
+ to={{ pathname: `metric/${metric.key}/history`, query: { id: component.key } }}>
<IconHistory/>
{translate('component_measures.tab.history')}
</Link>
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js
index 1d10919bd2f..7a4048589e1 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js
@@ -21,6 +21,7 @@ import React from 'react';
import ComponentsList from './ComponentsList';
import ListHeader from './ListHeader';
+import Spinner from '../../components/Spinner';
import SourceViewer from '../../../code/components/SourceViewer';
import ListFooter from '../../../../components/shared/list-footer';
@@ -102,11 +103,15 @@ export default class TreeView extends React.Component {
{!selected && (
<div>
- <ComponentsList
- components={components}
- selected={selected}
- metric={metric}
- onClick={this.handleClick.bind(this)}/>
+ {(!fetching || components.length !== 0) ? (
+ <ComponentsList
+ components={components}
+ selected={selected}
+ metric={metric}
+ onClick={this.handleClick.bind(this)}/>
+ ) : (
+ <Spinner/>
+ )}
<ListFooter
count={components.length}
total={total}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js
index c0bc2f6162c..1d30a1f4cb0 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasures.js
@@ -17,48 +17,34 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import _ from 'underscore';
import React from 'react';
-import Spinner from './../components/Spinner';
import AllMeasuresDomain from './AllMeasuresDomain';
import { getLeakPeriodLabel } from '../../../helpers/periods';
export default class AllMeasures extends React.Component {
componentDidMount () {
this.props.onDisplay();
- this.props.fetchMeasures();
}
- render () {
- const { component, measures, periods, fetching } = this.props;
-
- if (fetching) {
- return <Spinner/>;
- }
-
- const domains = _.sortBy(_.pairs(_.groupBy(measures, measure => measure.metric.domain)).map(r => {
- const [name, measures] = r;
- const sortedMeasures = _.sortBy(measures, measure => measure.metric.name);
-
- return { name, measures: sortedMeasures };
- }), 'name');
+ componentDidUpdate () {
+ this.props.onDisplay();
+ }
+ render () {
+ const { component, domains, periods } = this.props;
const leakPeriodLabel = getLeakPeriodLabel(periods);
return (
- <section id="component-measures-home" className="page page-container page-limited">
- <ul className="measures-domains">
- {domains.map((domain, index) => (
- <AllMeasuresDomain
- key={domain.name}
- domain={domain}
- component={component}
- displayLeakHeader={index === 0}
- leakPeriodLabel={leakPeriodLabel}/>
- ))}
- </ul>
- </section>
+ <ul className="measures-domains">
+ {domains.map(domain => (
+ <AllMeasuresDomain
+ key={domain.name}
+ domain={domain}
+ component={component}
+ leakPeriodLabel={leakPeriodLabel}/>
+ ))}
+ </ul>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js
index c12fac1ac13..8cdcb3d40b7 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresContainer.js
@@ -20,23 +20,19 @@
import { connect } from 'react-redux';
import AllMeasures from './AllMeasures';
-import { fetchMeasures } from './actions';
-import { displayHome } from '../app/actions';
+import { displayDomain } from '../app/actions';
const mapStateToProps = state => {
return {
component: state.app.component,
- metrics: state.app.metrics,
- measures: state.home.measures,
- periods: state.home.periods,
- fetching: state.status.fetching
+ domains: state.home.domains,
+ periods: state.home.periods
};
};
const mapDispatchToProps = dispatch => {
return {
- onDisplay: () => dispatch(displayHome()),
- fetchMeasures: () => dispatch(fetchMeasures())
+ onDisplay: () => dispatch(displayDomain(undefined))
};
};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js
index 67fe53d31d7..5bf2d5585b1 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/AllMeasuresDomain.js
@@ -20,64 +20,55 @@
import sortBy from '../../../../../../node_modules/lodash/sortBy';
import partition from '../../../../../../node_modules/lodash/partition';
import React from 'react';
-import { Link } from 'react-router';
-import domains from '../config/domains';
-import { formatLeak } from '../utils';
-import { formatMeasure } from '../../../helpers/measures';
-import { translateWithParameters } from '../../../helpers/l10n';
+import MeasuresList from './MeasuresList';
+import { domains } from '../config/domains';
-export default function AllMeasuresDomain ({ domain, component, displayLeakHeader, leakPeriodLabel }) {
- const hasLeak = !!leakPeriodLabel;
- const { measures } = domain;
- const knownMetrics = domains[domain.name] || [];
+const sortMeasures = (measures, order) => {
+ const [known, unknown] = partition(measures, measure => order.includes(measure.metric.key));
+ return [
+ ...sortBy(known, measure => order.indexOf(measure.metric.key)),
+ ...sortBy(unknown, measure => measure.metric.name)
+ ];
+};
- const [knownMeasures, otherMeasures] =
- partition(measures, measure => knownMetrics.indexOf(measure.metric.key) !== -1);
+export default class AllMeasuresDomain extends React.Component {
+ render () {
+ const { domain, component, leakPeriodLabel, displayHeader } = this.props;
- const finalMeasures = [
- ...sortBy(knownMeasures, measure => knownMetrics.indexOf(measure.metric.key)),
- ...sortBy(otherMeasures, measure => measure.metric.name)
- ];
+ const hasLeak = !!leakPeriodLabel;
+ const { measures } = domain;
+ const domainConfig = domains[domain.name] || { main: [], order: [] };
+ const mainMetrics = domainConfig.main;
+ const orderedMeasures = domainConfig.order;
+ const [mainMeasures, otherMeasures] = partition(measures,
+ measure => mainMetrics.indexOf(measure.metric.key) !== -1);
+ const sortedMainMeasures = sortMeasures(mainMeasures, orderedMeasures);
+ const sortedOtherMeasures = sortMeasures(otherMeasures, orderedMeasures);
+ const finalMeasures = [...sortedMainMeasures, ...sortedOtherMeasures];
- return (
- <li>
- <header className="page-header">
- <h3 className="page-title">{domain.name}</h3>
- {displayLeakHeader && hasLeak && (
- <div className="measures-domains-leak-header">
- {translateWithParameters('overview.leak_period_x', leakPeriodLabel)}
- </div>
+ return (
+ <li>
+ {displayHeader && (
+ <header className="page-header">
+ <h3 className="page-title">{domain.name}</h3>
+ </header>
)}
- </header>
- <ul className="domain-measures">
- {finalMeasures.map(measure => (
- <li key={measure.metric.key} id={`measure-${measure.metric.key}`}>
- <Link to={{ pathname: measure.metric.key, query: { id: component.key } }}>
- <div className="domain-measures-name">
- <span id={`measure-${measure.metric.key}-name`}>
- {measure.metric.name}
- </span>
- </div>
- <div className="domain-measures-value">
- {measure.value != null && (
- <span id={`measure-${measure.metric.key}-value`}>
- {formatMeasure(measure.value, measure.metric.type)}
- </span>
- )}
- </div>
- {hasLeak && measure.leak != null && (
- <div className="domain-measures-value domain-measures-leak">
- <span id={`measure-${measure.metric.key}-leak`}>
- {formatLeak(measure.leak, measure.metric)}
- </span>
- </div>
- )}
- </Link>
- </li>
- ))}
- </ul>
- </li>
- );
+ <MeasuresList
+ measures={finalMeasures}
+ hasLeak={hasLeak}
+ component={component}/>
+ </li>
+ );
+ }
}
+
+AllMeasuresDomain.defaultProps = {
+ displayHeader: true
+};
+
+AllMeasuresDomain.propTypes = {
+ displayHeader: React.PropTypes.bool
+};
+
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/DomainMeasures.js b/server/sonar-web/src/main/js/apps/component-measures/home/DomainMeasures.js
new file mode 100644
index 00000000000..8dcd15583ee
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/DomainMeasures.js
@@ -0,0 +1,84 @@
+/*
+ * 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 sortBy from '../../../../../../node_modules/lodash/sortBy';
+import partition from '../../../../../../node_modules/lodash/partition';
+import React from 'react';
+
+import MainMeasures from './MainMeasures';
+import MeasuresList from './MeasuresList';
+import MeasureBubbleChartContainer from '../components/bubbleChart/MeasureBubbleChartContainer';
+import { getLeakPeriodLabel } from '../../../helpers/periods';
+import { hasBubbleChart } from '../utils';
+import { domains as domainsConf } from '../config/domains';
+
+const sortMeasures = (measures, order) => {
+ const [known, unknown] = partition(measures, measure => order.includes(measure.metric.key));
+ return [
+ ...sortBy(known, measure => order.indexOf(measure.metric.key)),
+ ...sortBy(unknown, measure => measure.metric.name)
+ ];
+};
+
+export default class DomainMeasures extends React.Component {
+ componentDidMount () {
+ this.props.onDisplay(this.props.params.domainName);
+ }
+
+ componentDidUpdate () {
+ this.props.onDisplay(this.props.params.domainName);
+ }
+
+ render () {
+ const { component, domains, periods } = this.props;
+ const { domainName } = this.props.params;
+ const domain = domains.find(d => d.name === domainName);
+ const { measures } = domain;
+ const leakPeriodLabel = getLeakPeriodLabel(periods);
+
+ const conf = domainsConf[domainName];
+ const mainMetrics = conf ? conf.main : [];
+ const order = conf ? conf.order : [];
+ const [mainMeasures, otherMeasures] = partition(measures, m => mainMetrics.includes(m.metric.key));
+ const sortedMainMeasures = sortMeasures(mainMeasures, order);
+ const sortedOtherMeasures = sortMeasures(otherMeasures, order);
+
+ return (
+ <section id="component-measures-domain">
+ {mainMeasures.length > 0 && (
+ <MainMeasures
+ measures={sortedMainMeasures}
+ component={component}
+ hasLeak={leakPeriodLabel != null}/>
+ )}
+
+ {otherMeasures.length > 0 && (
+ <MeasuresList
+ measures={sortedOtherMeasures}
+ component={component}
+ hasLeak={leakPeriodLabel != null}/>
+ )}
+
+ {hasBubbleChart(domainName) && (
+ <MeasureBubbleChartContainer domainName={domainName}/>
+ )}
+ </section>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/DomainMeasuresContainer.js b/server/sonar-web/src/main/js/apps/component-measures/home/DomainMeasuresContainer.js
new file mode 100644
index 00000000000..0ff458548b3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/DomainMeasuresContainer.js
@@ -0,0 +1,42 @@
+/*
+ * 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 { connect } from 'react-redux';
+
+import DomainMeasures from './DomainMeasures';
+import { displayDomain } from '../app/actions';
+
+const mapStateToProps = state => {
+ return {
+ component: state.app.component,
+ domains: state.home.domains,
+ periods: state.home.periods
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onDisplay: domainName => dispatch(displayDomain(domainName))
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(DomainMeasures);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/Home.js b/server/sonar-web/src/main/js/apps/component-measures/home/Home.js
new file mode 100644
index 00000000000..a9e4dd58e9f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/Home.js
@@ -0,0 +1,78 @@
+/*
+ * 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 { Link, IndexLink } from 'react-router';
+
+import { getLeakPeriodLabel } from '../../../helpers/periods';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+export default class Home extends React.Component {
+ componentDidMount () {
+ this.props.onDisplay();
+ this.props.fetchMeasures();
+ }
+
+ render () {
+ const { component, domains, periods } = this.props;
+
+ if (domains == null) {
+ return null;
+ }
+
+ const leakPeriodLabel = getLeakPeriodLabel(periods);
+
+ return (
+ <section id="component-measures-home" className="page page-container page-limited">
+ <header id="component-measures-home-header" className="home-header">
+ <nav className="nav-pills pull-left">
+ <ul>
+ <li>
+ <IndexLink
+ to={{ pathname: '/', query: { id: component.key } }}
+ activeClassName="active">
+ {translate('all')}
+ </IndexLink>
+ </li>
+ {domains.map(domain => (
+ <li key={domain.name}>
+ <Link
+ to={{ pathname: `domain/${domain.name}`, query: { id: component.key } }}
+ activeClassName="active">
+ {domain.name}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </nav>
+
+ {leakPeriodLabel != null && (
+ <div className="measures-domains-leak-header">
+ {translateWithParameters('overview.leak_period_x', leakPeriodLabel)}
+ </div>
+ )}
+ </header>
+
+ <main id="component-measures-home-main">
+ {this.props.children}
+ </main>
+ </section>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/HomeContainer.js b/server/sonar-web/src/main/js/apps/component-measures/home/HomeContainer.js
new file mode 100644
index 00000000000..a5473ec3dba
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/HomeContainer.js
@@ -0,0 +1,44 @@
+/*
+ * 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 { connect } from 'react-redux';
+
+import Home from './Home';
+import { fetchMeasures } from './actions';
+import { displayHome } from '../app/actions';
+
+const mapStateToProps = state => {
+ return {
+ component: state.app.component,
+ domains: state.home.domains,
+ periods: state.home.periods
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onDisplay: () => dispatch(displayHome()),
+ fetchMeasures: () => dispatch(fetchMeasures())
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(Home);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/MainMeasures.js b/server/sonar-web/src/main/js/apps/component-measures/home/MainMeasures.js
new file mode 100644
index 00000000000..172fb67896b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/MainMeasures.js
@@ -0,0 +1,50 @@
+/*
+ * 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 { Link } from 'react-router';
+import classNames from 'classnames';
+
+import Measure from '../components/Measure';
+import { isDiffMetric } from '../utils';
+
+const MainMeasures = ({ measures, component }) => {
+ return (
+ <ul className="domain-main-measures">
+ {measures.map(measure => (
+ <li key={measure.metric.key}>
+ <div className={classNames('measure-details-value', {
+ 'measure-details-value-absolute': !isDiffMetric(measure.metric),
+ 'measure-details-value-leak': isDiffMetric(measure.metric)
+ })}>
+ <Link to={{ pathname: `metric/${measure.metric.key}`, query: { id: component.key } }}>
+ <Measure measure={measure}/>
+ </Link>
+ </div>
+
+ <div className="domain-main-measures-label">
+ {measure.metric.name}
+ </div>
+ </li>
+ ))}
+ </ul>
+ );
+};
+
+export default MainMeasures;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/MeasuresList.js b/server/sonar-web/src/main/js/apps/component-measures/home/MeasuresList.js
new file mode 100644
index 00000000000..4bf7833968d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/MeasuresList.js
@@ -0,0 +1,58 @@
+/*
+ * 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 { Link } from 'react-router';
+
+import Measure from '../components/Measure';
+import { formatLeak } from '../utils';
+
+const MeasuresList = ({ measures, hasLeak, component }) => {
+ return (
+ <ul className="domain-measures">
+ {measures.map(measure => (
+ <li key={measure.metric.key} id={`measure-${measure.metric.key}`}>
+ <Link to={{ pathname: `metric/${measure.metric.key}`, query: { id: component.key } }}>
+ <div className="domain-measures-name">
+ <span id={`measure-${measure.metric.key}-name`}>
+ {measure.metric.name}
+ </span>
+ </div>
+ <div className="domain-measures-value">
+ {measure.value != null && (
+ <span id={`measure-${measure.metric.key}-value`}>
+ <Measure measure={measure}/>
+ </span>
+ )}
+ </div>
+ {hasLeak && measure.leak != null && (
+ <div className="domain-measures-value domain-measures-leak">
+ <span id={`measure-${measure.metric.key}-leak`}>
+ {formatLeak(measure.leak, measure.metric)}
+ </span>
+ </div>
+ )}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ );
+};
+
+export default MeasuresList;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js b/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js
index e80c7b1129b..9ba2b5f86ec 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/home/reducer.js
@@ -17,17 +17,44 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import _ from 'underscore';
+import sortBy from '../../../../../../node_modules/lodash/sortBy';
+import partition from '../../../../../../node_modules/lodash/partition';
+
import { RECEIVE_MEASURES } from './actions';
const initialState = {
measures: undefined,
+ domains: undefined,
periods: undefined
};
+function groupByDomains (measures) {
+ const KNOWN_DOMAINS = ['Reliability', 'Security', 'Maintainability', 'Tests', 'Duplication', 'Size', 'Complexity'];
+
+ const domains = _.sortBy(_.pairs(_.groupBy(measures, measure => measure.metric.domain)).map(r => {
+ const [name, measures] = r;
+ const sortedMeasures = _.sortBy(measures, measure => measure.metric.name);
+
+ return { name, measures: sortedMeasures };
+ }), 'name');
+ const [knownDomains, unknownDomains] =
+ partition(domains, domain => KNOWN_DOMAINS.includes(domain.name));
+ return [
+ ...sortBy(knownDomains, domain => KNOWN_DOMAINS.indexOf(domain.name)),
+ ...sortBy(unknownDomains, domain => domain.name)
+ ];
+}
+
export default function (state = initialState, action = {}) {
switch (action.type) {
case RECEIVE_MEASURES:
- return { ...state, measures: action.measures, periods: action.periods };
+ return {
+ ...state,
+ measures: action.measures,
+ domains: groupByDomains(action.measures),
+ periods: action.periods
+ };
default:
return state;
}
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 03653fa3067..d9d58658fc4 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, hasBubbleChart } from './utils';
+import { hasHistory } from './utils';
export function checkHistoryExistence (nextState, replace) {
const { metricKey } = nextState.params;
@@ -29,14 +29,3 @@ 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 198f0aeea91..6864b465258 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
@@ -1,17 +1,23 @@
+.home-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 30px;
+}
+
.measures-domains {
}
.measures-domains > li {
- padding-bottom: 40px;
- break-inside: avoid;
+ padding-bottom: 20px;
}
.measures-domains-leak-header {
- float: right;
line-height: 22px;
padding: 0 10px;
border: 1px solid #eae3c7;
background-color: #fbf3d5;
+ white-space: nowrap;
}
.domain-measures {
@@ -28,6 +34,10 @@
background-color: #f8f8f8;
}
+.domain-measures > li > a:hover {
+ background-color: #ecf6fe !important;
+}
+
.domain-measures-name,
.domain-measures-value {
padding: 7px 10px;
@@ -57,6 +67,32 @@
background-color: #f5eed0;
}
+.domain-main-measures {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 0 10px;
+}
+
+.domain-main-measures > li {
+ margin-right: 40px;
+ margin-bottom: 30px;
+}
+
+.domain-main-measures-value {
+ line-height: 1;
+ font-size: 24px;
+ font-weight: 300;
+}
+
+.domain-main-measures-leak {
+
+}
+
+.domain-main-measures-label {
+ margin-top: 4px;
+ white-space: nowrap;
+}
+
.measure-details {
margin-top: 10px;
}
@@ -232,7 +268,7 @@
.measure-details-bubble-chart {
position: relative;
margin: 10px 0;
- padding: 30px 0 30px 50px;
+ padding: 30px 0 30px 60px;
}
.measure-details-bubble-chart-axis {
@@ -265,7 +301,6 @@
margin-bottom: 10px;
}
-
.component-measures-breadcrumbs {
display: flex;
flex-wrap: wrap;
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 5349f97864a..3e54ffe49da 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
@@ -91,8 +91,8 @@ export function hasHistory (metricKey) {
return metricKey.indexOf('new_') !== 0;
}
-export function hasBubbleChart (metricKey) {
- return !!bubbles[metricKey];
+export function hasBubbleChart (domainName) {
+ return !!bubbles[domainName];
}
export function hasTreemap (metric) {
diff --git a/server/sonar-web/src/main/js/components/shared/Level.js b/server/sonar-web/src/main/js/components/shared/Level.js
new file mode 100644
index 00000000000..cd92ed80d0e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/Level.js
@@ -0,0 +1,34 @@
+/*
+ * 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 { formatMeasure } from '../../helpers/measures';
+
+const Level = ({ level }) => {
+ const formatted = formatMeasure(level, 'LEVEL');
+ const className = 'level level-' + level;
+ return (
+ <span className={className}>
+ {formatted}
+ </span>
+ );
+};
+
+export default Level;
diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js
index be2f332895b..5279154290a 100644
--- a/server/sonar-web/src/main/js/helpers/urls.js
+++ b/server/sonar-web/src/main/js/helpers/urls.js
@@ -48,7 +48,7 @@ export function getComponentIssuesUrl (componentKey, query) {
* @returns {string}
*/
export function getComponentDrilldownUrl (componentKey, metric) {
- return `/component_measures/${metric}?id=${encodeURIComponent(componentKey)}`;
+ return `/component_measures/metric/${metric}?id=${encodeURIComponent(componentKey)}`;
}
diff --git a/server/sonar-web/src/main/less/components/ui.less b/server/sonar-web/src/main/less/components/ui.less
index 84c6235101a..15f4bb163b5 100644
--- a/server/sonar-web/src/main/less/components/ui.less
+++ b/server/sonar-web/src/main/less/components/ui.less
@@ -69,6 +69,36 @@
a & { border-bottom-color: #EE0000; }
}
+.level {
+ display: inline-block;
+ height: 1.3em;
+ line-height: 1.3;
+ padding: 0 0.3em;
+ color: #fff;
+ font-weight: 300;
+ text-align: center;
+
+ a > & {
+ margin-bottom: -1px;
+ border-bottom: 1px solid;
+ transition: all 0.2s ease;
+
+ &:hover { opacity: 0.8; }
+ }
+}
+
+.level-OK {
+ background-color: @green;
+}
+
+.level-WARN {
+ background-color: @orange;
+}
+
+.level-ERROR {
+ background-color: @red;
+}
+
.processes-container {
position: fixed;
@@ -231,6 +261,34 @@
}
}
+.nav-pills {
+ & > ul {
+ display: flex;
+ flex-wrap: wrap;
+
+ & > li > a {
+ display: inline-block;
+ vertical-align: middle;
+ padding: 3px 10px;
+ border: 1px solid transparent;
+ border-radius: 24px;
+ background-color: #fff;
+ color: @darkBlue;
+ transition: none;
+
+ &:hover {
+ border-color: @darkBlue;
+ }
+ }
+
+ & > li.active > a,
+ & > li > a.active {
+ background-color: @darkBlue;
+ color: #fff;
+ }
+ }
+}
+
.flash {
background-color: transparent;
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/drilldown_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/drilldown_controller.rb
index 3ef48fcabb2..7d6c243c921 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/drilldown_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/drilldown_controller.rb
@@ -24,7 +24,7 @@ class DrilldownController < ApplicationController
def measures
metric = params[:metric] || 'ncloc'
- return redirect_to("/component_measures/#{metric}?id=#{url_encode(@resource.key)}")
+ return redirect_to("/component_measures/metric/#{metric}?id=#{url_encode(@resource.key)}")
end
def issues
diff --git a/server/sonar-web/tests/helpers/urls-test.js b/server/sonar-web/tests/helpers/urls-test.js
index b99f948c4b2..1cb46cd7952 100644
--- a/server/sonar-web/tests/helpers/urls-test.js
+++ b/server/sonar-web/tests/helpers/urls-test.js
@@ -56,12 +56,12 @@ describe('URLs', function () {
describe('#getComponentDrilldownUrl', function () {
it('should return component drilldown url', function () {
expect(getComponentDrilldownUrl(SIMPLE_COMPONENT_KEY, METRIC)).to.equal(
- '/component_measures/' + METRIC + '?id=' + SIMPLE_COMPONENT_KEY);
+ '/component_measures/metric/' + METRIC + '?id=' + SIMPLE_COMPONENT_KEY);
});
it('should encode component key', function () {
expect(getComponentDrilldownUrl(COMPLEX_COMPONENT_KEY, METRIC)).to.equal(
- '/component_measures/' + METRIC + '?id=' + COMPLEX_COMPONENT_KEY_ENCODED);
+ '/component_measures/metric/' + METRIC + '?id=' + COMPLEX_COMPONENT_KEY_ENCODED);
});
});
});