From d7b669175e4e40341f6f1553ebe8ed84a9980ce2 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Thu, 27 Jul 2017 17:21:25 +0200 Subject: [PATCH] SONAR-9608 SONAR-9634 Create the measure list view --- .../details/MeasureDetailsHeader.js | 2 +- .../__tests__/utils-test.js | 14 +- .../apps/component-measures/components/App.js | 61 ++++-- .../components/AppContainer.js | 53 +++-- .../components/LeakPeriodLegend.js | 61 ++++++ .../components/MeasureContent.js | 160 +++++++++++++++ .../components/MeasureHeader.js | 86 ++++++++ .../components/MetricNotFound.js | 32 +++ .../components/__tests__/App-test.js | 20 +- .../__tests__/LeakPeriodLegend-test.js | 61 ++++++ .../__tests__/MeasureHeader-test.js | 78 +++++++ .../__tests__/__snapshots__/App-test.js.snap | 72 ++++--- .../LeakPeriodLegend-test.js.snap | 30 +++ .../__snapshots__/MeasureHeader-test.js.snap | 149 ++++++++++++++ .../components/drilldown/ComponentCell.js | 106 ++++++++++ .../components/drilldown/ComponentsList.js | 84 ++++++++ .../components/drilldown/ComponentsListRow.js | 70 +++++++ .../drilldown/EmptyComponentsList.js | 30 +++ .../components/drilldown/ListView.js | 193 ++++++++++++++++++ .../components/drilldown/MeasureCell.js | 39 ++++ .../apps/component-measures/config/bubbles.js | 27 +++ .../config/complementary.js | 38 ++++ .../main/js/apps/component-measures/style.css | 47 +++-- .../main/js/apps/component-measures/types.js | 25 ++- .../main/js/apps/component-measures/utils.js | 25 ++- .../src/main/js/components/measure/utils.js | 16 ++ ...tribution.js => ComplexityDistribution.js} | 7 +- .../src/main/js/store/metrics/actions.js | 4 +- .../src/main/js/store/metrics/reducer.js | 4 +- .../src/main/js/store/rootReducer.js | 2 + 30 files changed, 1473 insertions(+), 123 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/drilldown/EmptyComponentsList.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/drilldown/MeasureCell.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/config/complementary.js rename server/sonar-web/src/main/js/components/shared/{complexity-distribution.js => ComplexityDistribution.js} (90%) diff --git a/server/sonar-web/src/main/js/apps/component-measures-old/details/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures-old/details/MeasureDetailsHeader.js index f2281c24eba..8a6254d140b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures-old/details/MeasureDetailsHeader.js +++ b/server/sonar-web/src/main/js/apps/component-measures-old/details/MeasureDetailsHeader.js @@ -25,7 +25,7 @@ import LeakPeriodLegend from '../components/LeakPeriodLegend'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import HistoryIcon from '../../../components/icons-components/HistoryIcon'; import Tooltip from '../../../components/controls/Tooltip'; -import { ComplexityDistribution } from '../../../components/shared/complexity-distribution'; +import ComplexityDistribution from '../../../components/shared/ComplexityDistribution'; import { isDiffMetric } from '../../../helpers/measures'; import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; import { getComponentMeasureHistory } from '../../../helpers/urls'; diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js index 62eaf3c7a56..00be69c8786 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js @@ -92,30 +92,32 @@ describe('groupByDomains', () => { describe('parseQuery', () => { it('should correctly parse the url query', () => { - expect(utils.parseQuery({})).toEqual({ metric: '', view: utils.DEFAULT_VIEW }); - expect(utils.parseQuery({ metric: 'foo', view: 'tree' })).toEqual({ + expect(utils.parseQuery({})).toEqual({ metric: '', selected: '', view: utils.DEFAULT_VIEW }); + expect(utils.parseQuery({ metric: 'foo', selected: 'bar', view: 'tree' })).toEqual({ metric: 'foo', + selected: 'bar', view: 'tree' }); }); it('should be memoized', () => { - const query = { metric: 'foo', view: 'tree' }; + const query = { metric: 'foo', selected: 'bar', view: 'tree' }; expect(utils.parseQuery(query)).toBe(utils.parseQuery(query)); }); }); describe('serializeQuery', () => { it('should correctly serialize the query', () => { - expect(utils.serializeQuery({ metric: '', view: 'list' })).toEqual({}); - expect(utils.serializeQuery({ metric: 'foo', view: 'tree' })).toEqual({ + expect(utils.serializeQuery({ metric: '', selected: '', view: 'list' })).toEqual({}); + expect(utils.serializeQuery({ metric: 'foo', selected: 'bar', view: 'tree' })).toEqual({ metric: 'foo', + selected: 'bar', view: 'tree' }); }); it('should be memoized', () => { - const query = { metric: 'foo', view: 'tree' }; + const query = { metric: 'foo', selected: 'bar', view: 'tree' }; expect(utils.serializeQuery(query)).toBe(utils.serializeQuery(query)); }); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js index 02d5b590ef4..7b9ac879874 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js @@ -20,12 +20,13 @@ // @flow import React from 'react'; import Helmet from 'react-helmet'; +import MeasureContent from './MeasureContent'; import Sidebar from '../sidebar/Sidebar'; import { parseQuery, serializeQuery } from '../utils'; import { translate } from '../../../helpers/l10n'; import type { Component, Query, Period } from '../types'; import type { RawQuery } from '../../../helpers/query'; -import type { Metrics } from '../../../store/metrics/actions'; +import type { Metric } from '../../../store/metrics/actions'; import type { MeasureEnhanced } from '../../../components/measure/types'; import '../style.css'; @@ -34,10 +35,11 @@ type Props = {| location: { pathname: string, query: RawQuery }, fetchMeasures: ( Component, - Metrics - ) => Promise<{ measures: Array, periods: Array }>, + Array + ) => Promise<{ component: Component, measures: Array, leakPeriod: ?Period }>, fetchMetrics: () => void, - metrics: Metrics, + metrics: { [string]: Metric }, + metricsKey: Array, router: { push: ({ pathname: string, query?: RawQuery }) => void } @@ -46,7 +48,7 @@ type Props = {| type State = {| loading: boolean, measures: Array, - periods: Array + leakPeriod: ?Period |}; export default class App extends React.PureComponent { @@ -59,7 +61,7 @@ export default class App extends React.PureComponent { this.state = { loading: true, measures: [], - periods: [] + leakPeriod: null }; } @@ -83,12 +85,27 @@ export default class App extends React.PureComponent { } } - fetchMeasures = ({ component, fetchMeasures, metrics }: Props) => { + componentWillUnmount() { + this.mounted = false; + const footer = document.getElementById('footer'); + if (footer) { + footer.classList.remove('search-navigator-footer'); + } + } + + fetchMeasures = ({ component, fetchMeasures, metrics, metricsKey }: Props) => { this.setState({ loading: true }); - fetchMeasures(component, metrics).then( - ({ measures, periods }) => { + const filterdKeys = metricsKey.filter( + key => !metrics[key].hidden && !['DATA', 'DISTRIB'].includes(metrics[key].type) + ); + fetchMeasures(component.key, filterdKeys).then( + ({ measures, leakPeriod }) => { if (this.mounted) { - this.setState({ loading: false, measures, periods }); + this.setState({ + loading: false, + leakPeriod, + measures: measures.filter(measure => measure.value != null || measure.leak != null) + }); } }, () => this.setState({ loading: false }) @@ -110,10 +127,12 @@ export default class App extends React.PureComponent { }; render() { - if (this.state.loading) { + const isLoading = this.state.loading || this.props.metricsKey.length <= 0; + if (isLoading) { return ; } const query = parseQuery(this.props.location.query); + const metric = this.props.metrics[query.metric]; return (
@@ -132,15 +151,17 @@ export default class App extends React.PureComponent {
-
-
-
-
Page Actions
-
-
- -
Main
-
+ {metric != null && + } ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js index 063a5bc2124..f8979c7eea7 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js @@ -22,21 +22,27 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import App from './App'; import throwGlobalError from '../../../app/utils/throwGlobalError'; -import { getComponent, getMetrics, getMetricByKey } from '../../../store/rootReducer'; +import { + getComponent, + getMetrics, + getMetricByKey, + getMetricsKey +} from '../../../store/rootReducer'; import { fetchMetrics } from '../../../store/rootActions'; import { getMeasuresAndMeta } from '../../../api/measures'; -import { getLeakValue } from '../utils'; -import type { Component } from '../types'; -import type { Metrics } from '../../../store/metrics/actions'; +import { getLeakPeriod } from '../../../helpers/periods'; +import { enhanceMeasure } from '../../../components/measure/utils'; +import type { Component, Period } from '../types'; import type { Measure, MeasureEnhanced } from '../../../components/measure/types'; const mapStateToProps = (state, ownProps) => ({ component: getComponent(state, ownProps.location.query.id), - metrics: getMetrics(state) + metrics: getMetrics(state), + metricsKey: getMetricsKey(state) }); -const banQualityGate = (component: Component, measures: Array): Array => { - let newMeasures = [...measures]; +const banQualityGate = (component: Component): Array => { + let newMeasures = [...component.measures]; if (!['VW', 'SVW', 'APP'].includes(component.qualifier)) { newMeasures = newMeasures.filter(measure => measure.metric !== 'alert_status'); } @@ -49,36 +55,23 @@ const banQualityGate = (component: Component, measures: Array): Array ( +const fetchMeasures = (component: string, metrics: Array) => ( dispatch, getState -): Promise> => { - const metricKeys = metrics.filter(key => { - const metric = getMetricByKey(getState(), key); - return !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type); - }); - - if (metricKeys.length <= 0) { - return Promise.resolve([]); +): Promise<{ component: Component, measures: Array, leakPeriod: ?Period }> => { + if (metrics.length <= 0) { + return Promise.resolve({ component: {}, measures: [], leakPeriod: null }); } - return getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => { - const measures: Array = banQualityGate(component, r.component.measures) - .map(measure => { - const metric = getMetricByKey(getState(), measure.metric); - const leak = getLeakValue(measure); - return { value: measure.value, periods: measure.periods, metric, leak }; - }) - .filter(measure => { - const hasValue = measure.value != null; - const hasLeakValue = measure.leak != null; - return hasValue || hasLeakValue; - }); + return getMeasuresAndMeta(component, metrics, { additionalFields: 'periods' }).then(r => { + const measures: Array = banQualityGate(r.component).map(measure => + enhanceMeasure(measure, getMetricByKey(getState(), measure.metric)) + ); const newBugs = measures.find(measure => measure.metric.key === 'new_bugs'); const applicationPeriods = newBugs ? [{ index: 1 }] : []; - const periods = component.qualifier === 'APP' ? applicationPeriods : r.periods; - return { measures, periods }; + const periods = r.component.qualifier === 'APP' ? applicationPeriods : r.periods; + return { component: r.component, measures, leakPeriod: getLeakPeriod(periods) }; }, throwGlobalError); }; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js new file mode 100644 index 00000000000..1d36e84a078 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +import React from 'react'; +import moment from 'moment'; +import Tooltip from '../../../components/controls/Tooltip'; +import { getPeriodLabel, getPeriodDate } from '../../../helpers/periods'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import type { Component, Period } from '../types'; + +export default function LeakPeriodLegend({ + component, + period +}: { + component: Component, + period: Period +}) { + if (component.qualifier === 'APP') { + return ( +
+ {translate('issues.leak_period')} +
+ ); + } + + const label = ( +
+ {translateWithParameters('overview.leak_period_x', getPeriodLabel(period))} +
+ ); + + if (period.mode === 'days') { + return label; + } + + const date = getPeriodDate(period); + const fromNow = moment(date).fromNow(); + const tooltip = fromNow + ', ' + moment(date).format('LL'); + return ( + + {label} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js new file mode 100644 index 00000000000..68bc0bb3eac --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js @@ -0,0 +1,160 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +import React from 'react'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import ListView from './drilldown/ListView'; +import MeasureHeader from './MeasureHeader'; +import MetricNotFound from './MetricNotFound'; +import type { Component, Period, Query } from '../types'; +import type { MeasureEnhanced } from '../../../components/measure/types'; +import type { Metric } from '../../../store/metrics/actions'; + +type Props = { + className?: string, + rootComponent: Component, + fetchMeasures: ( + Component, + Array + ) => Promise<{ component: Component, measures: Array }>, + leakPeriod?: Period, + metric: Metric, + metrics: { [string]: Metric }, + selected: ?string, + updateQuery: Query => void +}; + +type State = { + component: ?Component, + loading: { + measure: boolean, + components: boolean + }, + measure: ?MeasureEnhanced, + secondaryMeasure: ?MeasureEnhanced +}; + +export default class MeasureContent extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { + component: null, + loading: { + measure: false, + components: false + }, + measure: null, + secondaryMeasure: null + }; + + componentDidMount() { + this.mounted = true; + this.fetchMeasure(this.props); + } + + componentWillReceiveProps(nextProps: Props) { + const { component } = this.state; + const componentChanged = + !component || + nextProps.rootComponent.key !== component.key || + nextProps.selected !== component.key; + if (componentChanged || nextProps.metric !== this.props.metric) { + this.fetchMeasure(nextProps); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchMeasure = ({ rootComponent, fetchMeasures, metric, selected }: Props) => { + this.updateLoading({ measure: true }); + + const metricKeys = [metric.key]; + if (metric.key === 'ncloc') { + metricKeys.push('ncloc_language_distribution'); + } else if (metric.key === 'function_complexity') { + metricKeys.push('function_complexity_distribution'); + } else if (metric.key === 'file_complexity') { + metricKeys.push('file_complexity_distribution'); + } + + fetchMeasures(selected || rootComponent.key, metricKeys).then( + ({ component, measures }) => { + if (this.mounted) { + const measure = measures.find(measure => measure.metric.key === metric.key); + const secondaryMeasure = measures.find(measure => measure.metric.key !== metric.key); + this.setState({ component, measure, secondaryMeasure }); + this.updateLoading({ measure: false }); + } + }, + () => this.updateLoading({ measure: false }) + ); + }; + + handleSelect = (component: Component) => this.props.updateQuery({ selected: component.key }); + + updateLoading = (loading: { [string]: boolean }) => { + if (this.mounted) { + this.setState(state => ({ loading: { ...state.loading, ...loading } })); + } + }; + + render() { + const { metric } = this.props; + const { loading, measure } = this.state; + + return ( +
+
+
+
+ Page Actions + +
+
+
+ {metric != null && measure != null + ?
+ + +
+ : } +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js new file mode 100644 index 00000000000..c58748cda20 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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 ComplexityDistribution from '../../../components/shared/ComplexityDistribution'; +import HistoryIcon from '../../../components/icons-components/HistoryIcon'; +import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; +import LanguageDistribution from '../../../components/charts/LanguageDistribution'; +import LeakPeriodLegend from './LeakPeriodLegend'; +import Measure from '../../../components/measure/Measure'; +import Tooltip from '../../../components/controls/Tooltip'; +import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; +import { getComponentMeasureHistory } from '../../../helpers/urls'; +import { isDiffMetric } from '../../../helpers/measures'; +import type { Component, Period } from '../types'; +import type { MeasureEnhanced } from '../../../components/measure/types'; + +type Props = { + component: Component, + leakPeriod?: Period, + measure: MeasureEnhanced, + secondaryMeasure: ?MeasureEnhanced +}; + +export default function MeasureHeader({ component, leakPeriod, measure, secondaryMeasure }: Props) { + const metric = measure.metric; + const isDiff = isDiffMetric(metric.key); + return ( +
+
+ + {getLocalizedMetricName(metric)} + + + {isDiff + ? + : } + + + {!isDiff && + + + + + } + {secondaryMeasure && + secondaryMeasure.metric.key === 'ncloc_language_distribution' && +
+ +
} + + {secondaryMeasure && + secondaryMeasure.metric.key === 'function_complexity_distribution' && +
+ +
} + + {secondaryMeasure && + secondaryMeasure.metric.key === 'file_complexity_distribution' && +
+ +
} +
+ {leakPeriod != null && } +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js b/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js new file mode 100644 index 00000000000..2dff010b0a0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +import React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default function MetricNotFound({ className }: { className?: string }) { + return ( +
+
+ {translate('component_measures.not_found')} +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js index a17ab39c8f1..7561113bd27 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js @@ -21,24 +21,30 @@ import React from 'react'; import { shallow } from 'enzyme'; import App from '../App'; -const METRICS = [ - { key: 'lines_to_cover', type: 'INT', name: 'Lines to Cover', domain: 'Coverage' }, - { key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, - { +const METRICS = { + lines_to_cover: { + key: 'lines_to_cover', + type: 'INT', + name: 'Lines to Cover', + domain: 'Coverage' + }, + coverage: { key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, + duplicated_lines_density: { key: 'duplicated_lines_density', type: 'PERCENT', name: 'Duplicated Lines (%)', domain: 'Duplications' }, - { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' } -]; + new_bugs: { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' } +}; const PROPS = { component: { key: 'foo' }, - location: { pathname: '/component_measures', query: {} }, + location: { pathname: '/component_measures', query: { metric: 'coverage' } }, fetchMeasures: () => {}, fetchMetrics: () => {}, metrics: METRICS, + metricsKey: ['lines_to_cover', 'coverage', 'duplicated_lines_density', 'new_bugs'], router: { push: () => {} } }; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js new file mode 100644 index 00000000000..4f7fce011d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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 { shallow } from 'enzyme'; +import LeakPeriodLegend from '../LeakPeriodLegend'; + +const PROJECT = { + key: 'foo', + qualifier: 'TRK' +}; + +const APP = { + key: 'bar', + qualifier: 'APP' +}; + +const PERIOD = { + date: '2017-05-16T13:50:02+0200', + index: 1, + mode: 'previous_version', + parameter: '6,4' +}; + +const PERIOD_DAYS = { + date: '2017-05-16T13:50:02+0200', + index: 1, + mode: 'days', + parameter: '18' +}; + +jest.mock('moment', () => () => ({ + format: () => 'March 1, 2017 9:36 AM', + fromNow: () => 'a month ago', + toDate: () => 'date' +})); + +it('should render correctly', () => { + expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); +}); + +it('should render correctly for APP', () => { + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js new file mode 100644 index 00000000000..8914cf0a86e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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 { shallow } from 'enzyme'; +import MeasureHeader from '../MeasureHeader'; + +const MEASURE = { + value: '3.0', + periods: [{ index: 1, value: '0.0' }], + metric: { + key: 'reliability_rating', + type: 'RATING', + name: 'Reliability Rating' + }, + leak: '0.0' +}; + +const LEAK_MEASURE = { + periods: [{ index: 1, value: '3.0' }], + metric: { + key: 'new_reliability_rating', + type: 'RATING', + name: 'Reliability Rating on New Code' + }, + leak: '3.0' +}; + +const SECONDARY = { + value: 'java=175123;js=26382', + metric: { + key: 'ncloc_language_distribution', + type: 'DATA', + name: 'Lines of Code Per Language' + }, + leak: null +}; + +const PROPS = { + component: { key: 'foo' }, + leakPeriod: { + date: '2017-05-16T13:50:02+0200', + index: 1, + mode: 'previous_version', + parameter: '6,4' + }, + measure: MEASURE, + secondaryMeasure: null +}; + +it('should render correctly', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should render correctly for leak', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should display secondary measure too', () => { + const wrapper = shallow(); + expect(wrapper.find('LanguageDistribution')).toHaveLength(1); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap index c74cb34eff9..1eeed263913 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap @@ -28,34 +28,60 @@ exports[`should render correctly 1`] = ` > -
-
-
-
- Page Actions -
-
-
-
- Main -
-
+ `; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap new file mode 100644 index 00000000000..0f0e328d85e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + +
+ overview.leak_period_x.overview.period.previous_version.6,4 +
+
+`; + +exports[`should render correctly 2`] = ` +
+ overview.leak_period_x.overview.period.days.18 +
+`; + +exports[`should render correctly for APP 1`] = ` +
+ issues.leak_period +
+`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap new file mode 100644 index 00000000000..3e66e8fa4fc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap @@ -0,0 +1,149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +
+
+ + Reliability Rating + + + + + + + + + + +
+ +
+`; + +exports[`should render correctly for leak 1`] = ` +
+
+ + Reliability Rating on New Code + + + + + +
+ +
+`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js new file mode 100644 index 00000000000..a3b54e1955e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +import React from 'react'; +import classNames from 'classnames'; +import QualifierIcon from '../../../../components/shared/QualifierIcon'; +import { splitPath } from '../../../../helpers/path'; +import { getComponentUrl } from '../../../../helpers/urls'; +import type { Component } from '../../types'; + +type Props = { + component: Component, + isSelected: boolean, + onClick: Component => void +}; + +export default class ComponentCell extends React.PureComponent { + props: Props; + + handleClick = (e: MouseEvent) => { + const isLeftClickEvent = e.button === 0; + const isModifiedEvent = !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); + + if (isLeftClickEvent && !isModifiedEvent) { + e.preventDefault(); + this.props.onClick(); + } + }; + + renderInner() { + const { component } = this.props; + let head = ''; + let tail = component.name; + + if (['DIR', 'FIL', 'UTS'].includes(component.qualifier)) { + const parts = splitPath(component.path); + head = parts.head; + tail = parts.tail; + } + return ( + + +   + {head.length > 0 && + + {head}/ + } + {tail} + + ); + } + + render() { + const { component } = this.props; + const linkClassName = classNames('link-no-underline', { + selected: this.props.isSelected + }); + + return ( + +
+ {component.refId == null || component.qualifier === 'DEV_PRJ' + ? + {this.renderInner()} + + : + + + + {this.renderInner()} + } +
+ + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js new file mode 100644 index 00000000000..ba8d42d84c9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js @@ -0,0 +1,84 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +import React from 'react'; +import ComponentsListRow from './ComponentsListRow'; +import EmptyComponentsList from './EmptyComponentsList'; +import { complementary } from '../../config/complementary'; +import { getLocalizedMetricName } from '../../../../helpers/l10n'; +import type { Component } from '../../types'; +import type { Metric } from '../../../../store/metrics/actions'; + +type Props = { + components: Array, + onClick: Component => void, + metric: Metric, + metrics: { [string]: Metric }, + selectedComponent?: ?string +}; + +export default function ComponentsList({ + components, + onClick, + metrics, + metric, + selectedComponent +}: Props) { + if (!components.length) { + return ; + } + + const otherMetrics = (complementary[metric.key] || []).map(key => metrics[key]); + return ( + + {otherMetrics.length > 0 && + + + + + {otherMetrics.map(metric => + + )} + + } + + + {components.map(component => + + )} + +
  + + {getLocalizedMetricName(metric)} + + + + {getLocalizedMetricName(metric)} + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js new file mode 100644 index 00000000000..deab1a98453 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +import React from 'react'; +import ComponentCell from './ComponentCell'; +import MeasureCell from './MeasureCell'; +import type { Component } from '../../types'; +import type { Metric } from '../../../../store/metrics/actions'; + +type Props = { + component: Component, + isSelected: boolean, + onClick: Component => void, + otherMetrics: Array, + metric: Metric +}; + +export default class ComponentsListRow extends React.PureComponent { + props: Props; + + handleClick = () => this.props.onClick(this.props.component); + + render() { + const { component } = this.props; + const otherMeasures = this.props.otherMetrics.map(metric => { + const measure = component.measures.find(measure => measure.metric === metric.key); + return { ...measure, metric }; + }); + return ( + + + + + + {otherMeasures.map(measure => + + )} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/EmptyComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/EmptyComponentsList.js new file mode 100644 index 00000000000..bcdeee0e710 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/EmptyComponentsList.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +import React from 'react'; +import { translate } from '../../../../helpers/l10n'; + +export default function EmptyComponentsList() { + return ( +
+ {translate('no_results')} +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js new file mode 100644 index 00000000000..d567f772759 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js @@ -0,0 +1,193 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +import React from 'react'; +import moment from 'moment'; +import ComponentsList from './ComponentsList'; +import ListFooter from '../../../../components/controls/ListFooter'; +import SourceViewer from '../../../../components/SourceViewer/SourceViewer'; +import { getComponentTree } from '../../../../api/components'; +import { complementary } from '../../config/complementary'; +import { isDiffMetric } from '../../../../helpers/measures'; +import { enhanceComponent } from '../../utils'; +import type { Component, ComponentEnhanced, Paging, Period } from '../../types'; +import type { Metric } from '../../../../store/metrics/actions'; + +type Props = { + component: Component, + handleSelect: Component => void, + leakPeriod?: Period, + loading: boolean, + metric: Metric, + metrics: { [string]: Metric }, + selectedComponent: ?string, + updateLoading: ({ [string]: boolean }) => void +}; + +type State = { + components: Array, + metric: ?Metric, + paging?: Paging +}; + +export default class ListView extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { + components: [], + metric: null, + paging: null + }; + + componentDidMount() { + this.mounted = true; + this.fetchComponents(this.props); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.component !== this.props.component || nextProps.metric !== this.props.metric) { + this.fetchComponents(nextProps); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + getComponentRequestParams = (metric: Metric, options: Object = {}) => { + const metricKeys = [metric.key, ...(complementary[metric.key] || [])]; + let opts: Object = { + asc: metric.direction === 1, + ps: 100, + metricSortFilter: 'withMeasuresOnly', + metricSort: metric.key + }; + if (isDiffMetric(metric.key)) { + opts = { + ...opts, + s: 'metricPeriod,name', + metricPeriodSort: 1 + }; + } else { + opts = { + ...opts, + s: 'metric,name' + }; + } + return { metricKeys, opts: { ...opts, ...options } }; + }; + + fetchComponents = ({ component, metric, selectedComponent }: Props) => { + if (selectedComponent) { + this.setState({ metric }); + return; + } + const { metricKeys, opts } = this.getComponentRequestParams(metric); + this.props.updateLoading({ components: true }); + getComponentTree('leaves', component.key, metricKeys, opts).then( + r => { + if (this.mounted) { + this.setState({ + components: r.components.map(component => enhanceComponent(component, metric)), + metric, + paging: r.paging + }); + } + this.props.updateLoading({ components: false }); + }, + () => this.props.updateLoading({ components: false }) + ); + }; + + fetchMoreComponents = () => { + const { component, metric } = this.props; + const { paging } = this.state; + if (!paging) { + return; + } + const { metricKeys, opts } = this.getComponentRequestParams(metric, { + p: paging.pageIndex + 1 + }); + this.props.updateLoading({ components: true }); + getComponentTree('leaves', component.key, metricKeys, opts).then( + r => { + if (this.mounted) { + this.setState(state => ({ + components: [ + ...state.components, + ...r.components.map(component => enhanceComponent(component, metric)) + ], + metric, + paging: r.paging + })); + } + this.props.updateLoading({ components: false }); + }, + () => this.props.updateLoading({ components: false }) + ); + }; + + render() { + const { components, metric, paging } = this.state; + if (metric == null) { + return null; + } + + const { leakPeriod, selectedComponent } = this.props; + if (selectedComponent) { + const leakPeriodDate = + isDiffMetric(metric.key) && leakPeriod != null ? moment(leakPeriod.date).toDate() : null; + + let filterLine; + if (leakPeriodDate != null) { + filterLine = line => { + if (line.scmDate) { + const scmDate = moment(line.scmDate).toDate(); + return scmDate >= leakPeriodDate; + } else { + return false; + } + }; + } + return ( +
+ +
+ ); + } + + return ( +
+ + {paging && + } +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/MeasureCell.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/MeasureCell.js new file mode 100644 index 00000000000..be6348e5105 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/MeasureCell.js @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +import React from 'react'; +import Measure from '../../../../components/measure/Measure'; +import type { Component } from '../../types'; +import type { Metric } from '../../../../store/metrics/actions'; + +type Props = { + component: Component, + metric: Metric +}; + +export default function MeasureCell({ component, metric }: Props) { + return ( + + + + + + ); +} 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 new file mode 100644 index 00000000000..3fd1e2a2055 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +export const bubbles = { + 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' }, + Coverage: { x: 'complexity', y: 'coverage', size: 'uncovered_lines' }, + Duplications: { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' } +}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/complementary.js b/server/sonar-web/src/main/js/apps/component-measures/config/complementary.js new file mode 100644 index 00000000000..e50c2238aac --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/config/complementary.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +// @flow +export const complementary = { + coverage: ['uncovered_lines', 'uncovered_conditions'], + line_coverage: ['uncovered_lines'], + branch_coverage: ['uncovered_conditions'], + uncovered_lines: ['line_coverage'], + uncovered_conditions: ['branch_coverage'], + + new_coverage: ['new_uncovered_lines', 'new_uncovered_conditions'], + new_line_coverage: ['new_uncovered_lines'], + new_branch_coverage: ['new_uncovered_conditions'], + new_uncovered_lines: ['new_line_coverage'], + new_uncovered_conditions: ['new_branch_coverage'], + + duplicated_lines_density: ['duplicated_lines'], + new_duplicated_lines_density: ['new_duplicated_lines'], + duplicated_lines: ['duplicated_lines_density'], + new_duplicated_lines: ['new_duplicated_lines_density'] +}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css index fc294c55dee..1e3aa63a269 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/style.css +++ b/server/sonar-web/src/main/js/apps/component-measures/style.css @@ -1,19 +1,45 @@ -.measures-domains-leak-header { - float: right; +.domain-measures-leak { background-color: #fbf3d5; border: 1px solid #eae3c7; - padding: 4px 10px; - white-space: nowrap; + padding: 4px 6px; } -.domain-measures-leak { - background-color: #fbf3d5; - border: 1px solid #eae3c7; +.facet .domain-measures-leak { padding: 4px 4px; margin: -5px -5px; } -.domain-measures-value .rating { +.domain-measures-leak-header { + background-color: #fbf3d5; + border: 1px solid #eae3c7; + padding: 4px 10px; + white-space: nowrap; +} + +.measure-details-header { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; +} + +.measure-details-metric { + display: flex; + align-items: center; +} + +.measure-details-secondary { + display: inline-block; + width: 260px; + margin-left: 4px; +} + +.measure-details-secondary .bar-chart { + margin-top: -10px; +} + +.domain-measures-value .rating, +.measure-details-value .rating { width: 18px; height: 18px; line-height: 18px; @@ -21,8 +47,3 @@ margin-bottom: -2px; font-size: 12px; } - -.domain-measures-leak .rating { - margin-top: -3px; - margin-bottom: -3px; -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/types.js b/server/sonar-web/src/main/js/apps/component-measures/types.js index 501e60ed0c1..340baf37de1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/types.js +++ b/server/sonar-web/src/main/js/apps/component-measures/types.js @@ -17,7 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export type Component = { +import type { Measure, MeasureEnhanced } from '../../components/measure/types'; + +type ComponentIntern = { isFavorite?: boolean, isRecentlyBrowsed?: boolean, key: string, @@ -28,9 +30,18 @@ export type Component = { qualifier: string }; -export type Query = { - metric: ?string, - view: string +export type Component = ComponentIntern & { measures?: Array }; + +export type ComponentEnhanced = ComponentIntern & { + value?: ?string, + leak?: ?string, + measures: Array +}; + +export type Paging = { + pageIndex: number, + pageSize: number, + total: number }; export type Period = { @@ -39,3 +50,9 @@ export type Period = { mode: string, parameter?: string }; + +export type Query = { + metric: ?string, + selected: ?string, + view: string +}; 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 2622792dcda..f6fcfdabc3d 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 @@ -22,9 +22,12 @@ import { groupBy, memoize, sortBy, toPairs } from 'lodash'; import { getLocalizedMetricName } from '../../helpers/l10n'; import { cleanQuery, parseAsString, serializeString } from '../../helpers/query'; import { domains } from './config/domains'; -import type { Query } from './types'; +import { bubbles } from './config/bubbles'; +import { enhanceMeasure } from '../../components/measure/utils'; +import type { Component, ComponentEnhanced, Query } from './types'; import type { RawQuery } from '../../helpers/query'; -import type { Measure, MeasureEnhanced } from '../../components/measure/types'; +import type { Metric } from '../../store/metrics/actions'; +import type { MeasureEnhanced } from '../../components/measure/types'; export const DEFAULT_VIEW = 'list'; const KNOWN_DOMAINS = [ @@ -69,13 +72,13 @@ export function sortMeasures( ]); } -export function getLeakValue(measure: ?Measure): ?string { - if (!measure || !measure.periods) { - return null; - } - const period = measure.periods.find(period => period.index === 1); - return period ? period.value : null; -} +export const enhanceComponent = (component: Component, metric: Metric): ComponentEnhanced => { + const enhancedMeasures = component.measures.map(measure => enhanceMeasure(measure, metric)); + const measure = enhancedMeasures.find(measure => measure.metric.key === metric.key); + const value = measure ? measure.value : null; + const leak = measure ? measure.leak : null; + return { ...component, value, leak, measures: enhancedMeasures }; +}; export const groupByDomains = memoize((measures: Array): Array<{ name: string, @@ -96,14 +99,18 @@ export const groupByDomains = memoize((measures: Array): Array< ]); }); +export const hasBubbleChart = (domainName: string): boolean => bubbles[domainName] != null; + export const parseQuery = memoize((urlQuery: RawQuery): Query => ({ metric: parseAsString(urlQuery['metric']), + selected: parseAsString(urlQuery['selected']), view: parseAsString(urlQuery['view']) || DEFAULT_VIEW })); export const serializeQuery = memoize((query: Query): RawQuery => { return cleanQuery({ metric: serializeString(query.metric), + selected: serializeString(query.selected), view: query.view === DEFAULT_VIEW ? null : serializeString(query.view) }); }); diff --git a/server/sonar-web/src/main/js/components/measure/utils.js b/server/sonar-web/src/main/js/components/measure/utils.js index bf98a58ee7b..74cc616d680 100644 --- a/server/sonar-web/src/main/js/components/measure/utils.js +++ b/server/sonar-web/src/main/js/components/measure/utils.js @@ -24,10 +24,18 @@ import { getRatingTooltip as nextGetRatingTooltip, isDiffMetric } from '../../helpers/measures'; +import type { Measure, MeasureEnhanced } from './types'; import type { Metric } from '../../store/metrics/actions'; const KNOWN_RATINGS = ['sqale_rating', 'reliability_rating', 'security_rating']; +export const enhanceMeasure = (measure: Measure, metric: Metric): MeasureEnhanced => ({ + value: measure.value, + periods: measure.periods, + metric, + leak: getLeakValue(measure) +}); + export function formatLeak(value: ?string, metric: Metric, options: Object) { if (isDiffMetric(metric.key)) { return formatMeasure(value, metric.type, options); @@ -36,6 +44,14 @@ export function formatLeak(value: ?string, metric: Metric, options: Object) { } } +export function getLeakValue(measure: ?Measure): ?string { + if (!measure || !measure.periods) { + return null; + } + const period = measure.periods.find(period => period.index === 1); + return period ? period.value : null; +} + export function getRatingTooltip(metricKey: string, value: ?string) { const finalMetricKey = isDiffMetric(metricKey) ? metricKey.substr(4) : metricKey; if (KNOWN_RATINGS.includes(finalMetricKey)) { diff --git a/server/sonar-web/src/main/js/components/shared/complexity-distribution.js b/server/sonar-web/src/main/js/components/shared/ComplexityDistribution.js similarity index 90% rename from server/sonar-web/src/main/js/components/shared/complexity-distribution.js rename to server/sonar-web/src/main/js/components/shared/ComplexityDistribution.js index 7853ee91800..0dd2ccc4514 100644 --- a/server/sonar-web/src/main/js/components/shared/complexity-distribution.js +++ b/server/sonar-web/src/main/js/components/shared/ComplexityDistribution.js @@ -25,7 +25,7 @@ import { translateWithParameters } from '../../helpers/l10n'; const HEIGHT = 80; -export class ComplexityDistribution extends React.PureComponent { +export default class ComplexityDistribution extends React.PureComponent { static propTypes = { distribution: PropTypes.string.isRequired, of: PropTypes.string.isRequired @@ -61,11 +61,8 @@ export class ComplexityDistribution extends React.PureComponent { }; render() { - // TODO remove inline styling return ( -
+
{this.renderBarChart()}
); diff --git a/server/sonar-web/src/main/js/store/metrics/actions.js b/server/sonar-web/src/main/js/store/metrics/actions.js index d76e54233fb..c144c2e1527 100644 --- a/server/sonar-web/src/main/js/store/metrics/actions.js +++ b/server/sonar-web/src/main/js/store/metrics/actions.js @@ -32,11 +32,9 @@ export type Metric = { type: string }; -export type Metrics = Array; - export const RECEIVE_METRICS = 'RECEIVE_METRICS'; -export const receiveMetrics = (metrics: Metrics) => ({ +export const receiveMetrics = (metrics: Array) => ({ type: RECEIVE_METRICS, metrics }); diff --git a/server/sonar-web/src/main/js/store/metrics/reducer.js b/server/sonar-web/src/main/js/store/metrics/reducer.js index 0c0950c6864..edc8750186c 100644 --- a/server/sonar-web/src/main/js/store/metrics/reducer.js +++ b/server/sonar-web/src/main/js/store/metrics/reducer.js @@ -44,6 +44,6 @@ const keys = (state: StateKeys = [], action = {}) => { export default combineReducers({ byKey, keys }); -export const getMetrics = (state: State) => state.keys; - +export const getMetrics = (state: State) => state.byKey; export const getMetricByKey = (state: State, key: string) => state.byKey[key]; +export const getMetricsKey = (state: State) => state.keys; diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index e38f544dabf..7da4acd3dfc 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -93,6 +93,8 @@ export const getMetrics = state => fromMetrics.getMetrics(state.metrics); export const getMetricByKey = (state, key) => fromMetrics.getMetricByKey(state.metrics, key); +export const getMetricsKey = state => fromMetrics.getMetricsKey(state.metrics); + export const getGlobalNotifications = state => fromNotifications.getGlobal(state.notifications); export const getProjectsWithNotifications = state => -- 2.39.5