diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-03-24 11:20:18 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-03-25 13:02:42 +0100 |
commit | bdd14b31bcd4ff599dd2dffd133d80f43b8970c3 (patch) | |
tree | 55d5ac42dd12c67853794c163cd3c4603f3dca36 /server/sonar-web | |
parent | 5869fc40c78cd2e1b7b29a7d74670c0e5a6c1afa (diff) | |
download | sonarqube-bdd14b31bcd4ff599dd2dffd133d80f43b8970c3.tar.gz sonarqube-bdd14b31bcd4ff599dd2dffd133d80f43b8970c3.zip |
SONAR-7402 improve display of measures home page
Diffstat (limited to 'server/sonar-web')
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); }); }); }); |