From: Stas Vilchik Date: Mon, 3 Dec 2018 12:50:17 +0000 (+0100) Subject: SONAR-11479 Display the measures of the currently selected directory on the Measures... X-Git-Tag: 7.6~88 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=6208b5a001ccfa21697463e0a780b9b3b3fee6e3;p=sonarqube.git SONAR-11479 Display the measures of the currently selected directory on the Measures page (#1002) --- diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 60317a41e39..7b97f66111c 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -94,6 +94,7 @@ export function getComponentTree( metrics: string[] = [], additional: RequestData = {} ): Promise<{ + baseComponent: T.ComponentMeasure; components: T.ComponentMeasure[]; metrics: T.Metric[]; paging: T.Paging; diff --git a/server/sonar-web/src/main/js/api/measures.ts b/server/sonar-web/src/main/js/api/measures.ts index 324ca966a54..5ebda3a570b 100644 --- a/server/sonar-web/src/main/js/api/measures.ts +++ b/server/sonar-web/src/main/js/api/measures.ts @@ -31,8 +31,11 @@ export function getMeasuresAndMeta( metrics: string[], additional: RequestData = {} ): Promise<{ component: T.ComponentMeasure; metrics?: T.Metric[]; periods?: T.Period[] }> { - const data = { ...additional, component, metricKeys: metrics.join(',') }; - return getJSON('/api/measures/component', data); + return getJSON('/api/measures/component', { + ...additional, + component, + metricKeys: metrics.join(',') + }).catch(throwGlobalError); } interface MeasuresForProjects { diff --git a/server/sonar-web/src/main/js/apps/code/utils.ts b/server/sonar-web/src/main/js/apps/code/utils.ts index baee517fccb..86f015f0e51 100644 --- a/server/sonar-web/src/main/js/apps/code/utils.ts +++ b/server/sonar-web/src/main/js/apps/code/utils.ts @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { without } from 'lodash'; import { addComponent, getComponent as getComponentFromBucket, @@ -59,61 +58,12 @@ const LEAK_METRICS = [ const PAGE_SIZE = 100; -function requestChildren( - componentKey: string, - metrics: string[], - page: number, - branchLike?: T.BranchLike -): Promise { - return getChildren(componentKey, metrics, { - p: page, - ps: PAGE_SIZE, - ...getBranchLikeQuery(branchLike) - }).then(r => { - if (r.paging.total > r.paging.pageSize * r.paging.pageIndex) { - return requestChildren(componentKey, metrics, page + 1, branchLike).then(moreComponents => { - return [...r.components, ...moreComponents]; - }); - } - return r.components; - }); -} - -function requestAllChildren( - componentKey: string, - metrics: string[], - branchLike?: T.BranchLike -): Promise { - return requestChildren(componentKey, metrics, 1, branchLike); -} - interface Children { components: T.ComponentMeasure[]; page: number; total: number; } -interface ExpandRootDirFunc { - (children: Children): Promise; -} - -function expandRootDir(metrics: string[], branchLike?: T.BranchLike): ExpandRootDirFunc { - return function({ components, total, ...other }) { - const rootDir = components.find( - (component: T.ComponentMeasure) => component.qualifier === 'DIR' && component.name === '/' - ); - if (rootDir) { - return requestAllChildren(rootDir.key, metrics, branchLike).then(rootDirComponents => { - const nextComponents = without([...rootDirComponents, ...components], rootDir); - const nextTotal = total + rootDirComponents.length - /* root dir */ 1; - return { components: nextComponents, total: nextTotal, ...other }; - }); - } else { - return Promise.resolve({ components, total, ...other }); - } - }; -} - function prepareChildren(r: any): Children { return { components: r.components, @@ -202,7 +152,6 @@ export function retrieveComponentChildren( ...getBranchLikeQuery(branchLike) }) .then(prepareChildren) - .then(expandRootDir(metrics, branchLike)) .then(r => { addComponentChildren(componentKey, r.components, r.total, r.page); storeChildrenBase(r.components); @@ -268,7 +217,6 @@ export function loadMoreChildren( ...getBranchLikeQuery(branchLike) }) .then(prepareChildren) - .then(expandRootDir(metrics, branchLike)) .then(r => { addComponentChildren(componentKey, r.components, r.total, r.page); storeChildrenBase(r.components); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx index 51de9019d2f..551d4d9f80d 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx @@ -19,9 +19,10 @@ */ import * as React from 'react'; import * as key from 'keymaster'; -import { InjectedRouter } from 'react-router'; +import { withRouter, WithRouterProps } from 'react-router'; import Helmet from 'react-helmet'; -import MeasureContentContainer from './MeasureContentContainer'; +import { keyBy } from 'lodash'; +import MeasureContent from './MeasureContent'; import MeasuresEmpty from './MeasuresEmpty'; import MeasureOverviewContainer from './MeasureOverviewContainer'; import Sidebar from '../sidebar/Sidebar'; @@ -35,7 +36,9 @@ import { hasFullMeasures, getMeasuresPageMetricKeys, groupByDomains, - sortMeasures + sortMeasures, + hasTreemap, + hasTree } from '../utils'; import { isSameBranchLike, @@ -55,62 +58,59 @@ import { removeSideBarClass, removeWhitePageClass } from '../../../helpers/pages'; -import { RawQuery } from '../../../helpers/query'; import '../../../components/search-navigator.css'; import '../style.css'; +import { getAllMetrics } from '../../../api/metrics'; +import { getMeasuresAndMeta } from '../../../api/measures'; +import { enhanceMeasure } from '../../../components/measure/utils'; +import { getLeakPeriod } from '../../../helpers/periods'; -interface Props { +interface Props extends WithRouterProps { branchLike?: T.BranchLike; component: T.ComponentMeasure; - location: { pathname: string; query: RawQuery }; - fetchMeasures: ( - component: string, - metricsKey: string[], - branchLike?: T.BranchLike - ) => Promise<{ - component: T.ComponentMeasure; - measures: T.MeasureEnhanced[]; - leakPeriod?: T.Period; - }>; - fetchMetrics: () => void; - metrics: { [metric: string]: T.Metric }; - metricsKey: string[]; - router: InjectedRouter; } interface State { + leakPeriod?: T.Period; loading: boolean; measures: T.MeasureEnhanced[]; - leakPeriod?: T.Period; + metrics: { [metric: string]: T.Metric }; } -export default class App extends React.PureComponent { +export class App extends React.PureComponent { mounted = false; - - constructor(props: Props) { - super(props); - this.state = { loading: true, measures: [] }; - } + state: State = { + loading: true, + measures: [], + metrics: {} + }; componentDidMount() { this.mounted = true; key.setScope('measures-files'); - this.props.fetchMetrics(); - this.fetchMeasures(this.props); + getAllMetrics().then( + metrics => { + const byKey = keyBy(metrics, 'key'); + this.setState({ metrics: byKey }); + this.fetchMeasures(byKey); + }, + () => {} + ); } - componentWillReceiveProps(nextProps: Props) { + componentDidUpdate(prevProps: Props, prevState: State) { + const prevQuery = parseQuery(prevProps.location.query); + const query = parseQuery(this.props.location.query); + if ( - !isSameBranchLike(nextProps.branchLike, this.props.branchLike) || - nextProps.component.key !== this.props.component.key || - nextProps.metrics !== this.props.metrics + !isSameBranchLike(prevProps.branchLike, this.props.branchLike) || + prevProps.component.key !== this.props.component.key || + prevQuery.selected !== query.selected ) { - this.fetchMeasures(nextProps); + this.fetchMeasures(this.state.metrics); } - } - componentDidUpdate(_prevProps: Props, prevState: State) { if (prevState.measures.length === 0 && this.state.measures.length > 0) { addWhitePageClass(); addSideBarClass(); @@ -124,13 +124,40 @@ export default class App extends React.PureComponent { key.deleteScope('measures-files'); } - fetchMeasures = ({ branchLike, component, fetchMeasures, metrics }: Props) => { - this.setState({ loading: true }); + fetchMeasures(metrics: State['metrics']) { + const { branchLike } = this.props; + const query = parseQuery(this.props.location.query); + const componentKey = query.selected || this.props.component.key; const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike); - fetchMeasures(component.key, filteredKeys, branchLike).then( - ({ measures, leakPeriod }) => { + + const banQualityGate = ({ measures = [], qualifier }: T.ComponentMeasure) => { + const bannedMetrics: string[] = []; + if (!['VW', 'SVW'].includes(qualifier)) { + bannedMetrics.push('alert_status'); + } + if (qualifier === 'APP') { + bannedMetrics.push('releasability_rating', 'releasability_effort'); + } + return measures.filter(measure => !bannedMetrics.includes(measure.metric)); + }; + + getMeasuresAndMeta(componentKey, filteredKeys, { + additionalFields: 'periods', + ...getBranchLikeQuery(branchLike) + }).then( + ({ component, periods }) => { if (this.mounted) { + const measures = banQualityGate(component).map(measure => + enhanceMeasure(measure, metrics) + ); + + const newBugs = measures.find(measure => measure.metric.key === 'new_bugs'); + const applicationPeriods = newBugs ? [{ index: 1 } as T.Period] : []; + const leakPeriod = getLeakPeriod( + component.qualifier === 'APP' ? applicationPeriods : periods + ); + this.setState({ loading: false, leakPeriod, @@ -146,7 +173,7 @@ export default class App extends React.PureComponent { } } ); - }; + } getHelmetTitle = (query: Query, displayOverview: boolean, metric?: T.Metric) => { if (displayOverview && query.metric) { @@ -164,7 +191,7 @@ export default class App extends React.PureComponent { if (displayOverview) { return undefined; } - const metric = this.props.metrics[query.metric]; + const metric = this.state.metrics[query.metric]; if (!metric) { const domainMeasures = groupByDomains(this.state.measures); const firstMeasure = @@ -177,14 +204,21 @@ export default class App extends React.PureComponent { }; updateQuery = (newQuery: Partial) => { - const query = serializeQuery({ - ...parseQuery(this.props.location.query), - ...newQuery - }); + const query: Query = { ...parseQuery(this.props.location.query), ...newQuery }; + + const metric = this.getSelectedMetric(query, false); + if (metric) { + if (query.view === 'treemap' && !hasTreemap(metric.key, metric.type)) { + query.view = 'tree'; + } else if (query.view === 'tree' && !hasTree(metric.key)) { + query.view = 'list'; + } + } + this.props.router.push({ pathname: this.props.location.pathname, query: { - ...query, + ...serializeQuery(query), ...getBranchLikeQuery(this.props.branchLike), id: this.props.component.key } @@ -192,7 +226,7 @@ export default class App extends React.PureComponent { }; renderContent = (displayOverview: boolean, query: Query, metric?: T.Metric) => { - const { branchLike, component, fetchMeasures, metrics } = this.props; + const { branchLike, component } = this.props; const { leakPeriod } = this.state; if (displayOverview) { return ( @@ -201,7 +235,7 @@ export default class App extends React.PureComponent { className="layout-page-main" domain={query.metric} leakPeriod={leakPeriod} - metrics={metrics} + metrics={this.state.metrics} rootComponent={component} router={this.props.router} selected={query.selected} @@ -229,13 +263,11 @@ export default class App extends React.PureComponent { } return ( - { }; render() { - const isLoading = this.state.loading || this.props.metricsKey.length <= 0; - if (isLoading) { + if (this.state.loading) { return ; } + const { branchLike } = this.props; const { measures } = this.state; const query = parseQuery(this.props.location.query); const hasOverview = hasFullMeasures(branchLike); const displayOverview = hasOverview && hasBubbleChart(query.metric); const metric = this.getSelectedMetric(query, displayOverview); + return (
@@ -288,3 +321,5 @@ export default class App extends React.PureComponent { ); } } + +export default withRouter(App); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.tsx deleted file mode 100644 index e7ea87f126e..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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 { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -import { withRouter, WithRouterProps } from 'react-router'; -import App from './App'; -import throwGlobalError from '../../../app/utils/throwGlobalError'; -import { getMetrics, getMetricsKey } from '../../../store/rootReducer'; -import { fetchMetrics } from '../../../store/rootActions'; -import { getMeasuresAndMeta } from '../../../api/measures'; -import { getLeakPeriod } from '../../../helpers/periods'; -import { enhanceMeasure } from '../../../components/measure/utils'; -import { getBranchLikeQuery } from '../../../helpers/branches'; - -interface StateToProps { - metrics: { [metric: string]: T.Metric }; - metricsKey: string[]; -} - -interface DispatchToProps { - fetchMeasures: ( - component: string, - metricsKey: string[], - branchLike?: T.BranchLike - ) => Promise<{ - component: T.ComponentMeasure; - measures: T.MeasureEnhanced[]; - leakPeriod?: T.Period; - }>; - fetchMetrics: () => void; -} - -interface OwnProps { - branchLike?: T.BranchLike; - component: T.ComponentMeasure; -} - -const mapStateToProps = (state: any): StateToProps => ({ - metrics: getMetrics(state), - metricsKey: getMetricsKey(state) -}); - -function banQualityGate({ measures = [], qualifier }: T.ComponentMeasure): T.Measure[] { - const bannedMetrics: string[] = []; - if (!['VW', 'SVW'].includes(qualifier)) { - bannedMetrics.push('alert_status'); - } - if (qualifier === 'APP') { - bannedMetrics.push('releasability_rating', 'releasability_effort'); - } - return measures.filter(measure => !bannedMetrics.includes(measure.metric)); -} - -const fetchMeasures = (component: string, metricsKey: string[], branchLike?: T.BranchLike) => ( - _dispatch: Dispatch, - getState: () => any -) => { - if (metricsKey.length <= 0) { - return Promise.resolve({ component: {}, measures: [], leakPeriod: null }); - } - - return getMeasuresAndMeta(component, metricsKey, { - additionalFields: 'periods', - ...getBranchLikeQuery(branchLike) - }).then(({ component, periods }) => { - const measures = banQualityGate(component).map(measure => - enhanceMeasure(measure, getMetrics(getState())) - ); - - const newBugs = measures.find(measure => measure.metric.key === 'new_bugs'); - const applicationPeriods = newBugs ? [{ index: 1 } as T.Period] : []; - const leakPeriod = getLeakPeriod(component.qualifier === 'APP' ? applicationPeriods : periods); - return { component, measures, leakPeriod }; - }, throwGlobalError); -}; - -const mapDispatchToProps: DispatchToProps = { fetchMeasures: fetchMeasures as any, fetchMetrics }; - -export default withRouter( - connect( - mapStateToProps, - mapDispatchToProps - )(App) -); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx index d771d65dc59..e7202a2a15c 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import * as key from 'keymaster'; import Breadcrumb from './Breadcrumb'; import { getBreadcrumbs } from '../../../api/components'; -import { getBranchLikeQuery } from '../../../helpers/branches'; +import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branches'; interface Props { backToFirst: boolean; @@ -42,13 +42,16 @@ export default class Breadcrumbs extends React.PureComponent { componentDidMount() { this.mounted = true; - this.fetchBreadcrumbs(this.props); + this.fetchBreadcrumbs(); this.attachShortcuts(); } - componentWillReceiveProps(nextProps: Props) { - if (this.props.component !== nextProps.component) { - this.fetchBreadcrumbs(nextProps); + componentDidUpdate(prevProps: Props) { + if ( + this.props.component !== prevProps.component || + !isSameBranchLike(prevProps.branchLike, this.props.branchLike) + ) { + this.fetchBreadcrumbs(); } } @@ -72,7 +75,8 @@ export default class Breadcrumbs extends React.PureComponent { key.unbind('left', 'measures-files'); } - fetchBreadcrumbs = ({ branchLike, component, rootComponent }: Props) => { + fetchBreadcrumbs = () => { + const { branchLike, component, rootComponent } = this.props; const isRoot = component.key === rootComponent.key; if (isRoot) { if (this.mounted) { diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx index 86982a68e0a..4b545348cdd 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx @@ -18,69 +18,72 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { InjectedRouter } from 'react-router'; import Breadcrumbs from './Breadcrumbs'; import MeasureContentHeader from './MeasureContentHeader'; import MeasureHeader from './MeasureHeader'; import MeasureViewSelect from './MeasureViewSelect'; -import MetricNotFound from './MetricNotFound'; import PageActions from './PageActions'; -import FilesView from '../drilldown/FilesView'; +import { complementary } from '../config/complementary'; import CodeView from '../drilldown/CodeView'; +import FilesView from '../drilldown/FilesView'; import TreeMapView from '../drilldown/TreeMapView'; +import { Query, View, isFileType, enhanceComponent, isViewType } from '../utils'; import { getComponentTree } from '../../../api/components'; -import { complementary } from '../config/complementary'; -import { enhanceComponent, isFileType, isViewType, View } from '../utils'; -import { getProjectUrl } from '../../../helpers/urls'; -import { isDiffMetric } from '../../../helpers/measures'; import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; -import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { isDiffMetric, getPeriodValue } from '../../../helpers/measures'; import { RequestData } from '../../../helpers/request'; +import { getProjectUrl } from '../../../helpers/urls'; +import { getMeasures } from '../../../api/measures'; interface Props { branchLike?: T.BranchLike; - className?: string; - component: T.ComponentMeasure; - loading: boolean; - loadingMore: boolean; leakPeriod?: T.Period; - measure?: T.MeasureEnhanced; - metric: T.Metric; + requestedMetric: Pick; metrics: { [metric: string]: T.Metric }; rootComponent: T.ComponentMeasure; router: InjectedRouter; - secondaryMeasure?: T.MeasureEnhanced; - updateLoading: (param: { [key: string]: boolean }) => void; - updateSelected: (component: string) => void; - updateView: (view: View) => void; + selected?: string; + updateQuery: (query: Partial) => void; view: View; } interface State { + baseComponent?: T.ComponentMeasure; components: T.ComponentMeasureEnhanced[]; + loading: boolean; + loadingMoreComponents: boolean; + measure?: T.Measure; metric?: T.Metric; paging?: T.Paging; + secondaryMeasure?: T.Measure; selected?: string; } export default class MeasureContent extends React.PureComponent { container?: HTMLElement | null; mounted = false; - state: State = { components: [] }; + state: State = { + components: [], + loading: true, + loadingMoreComponents: false + }; componentDidMount() { this.mounted = true; - this.fetchComponents(this.props); + this.fetchComponentTree(); } - componentWillReceiveProps(nextProps: Props) { + componentDidUpdate(prevProps: Props) { + const prevComponentKey = prevProps.selected || prevProps.rootComponent.key; + const componentKey = this.props.selected || this.props.rootComponent.key; if ( - !isSameBranchLike(nextProps.branchLike, this.props.branchLike) || - nextProps.component !== this.props.component || - nextProps.metric !== this.props.metric + prevComponentKey !== componentKey || + !isSameBranchLike(prevProps.branchLike, this.props.branchLike) || + prevProps.requestedMetric !== this.props.requestedMetric || + prevProps.view !== this.props.view ) { - this.fetchComponents(nextProps); + this.fetchComponentTree(); } } @@ -88,15 +91,95 @@ export default class MeasureContent extends React.PureComponent { this.mounted = false; } - getSelectedIndex = () => { - const componentKey = isFileType(this.props.component) - ? this.props.component.key - : this.state.selected; - const index = this.state.components.findIndex(component => component.key === componentKey); - return index !== -1 ? index : undefined; + fetchComponentTree = () => { + this.setState({ loading: true }); + const { metricKeys, opts, strategy } = this.getComponentRequestParams( + this.props.view, + this.props.requestedMetric + ); + const componentKey = this.props.selected || this.props.rootComponent.key; + const baseComponentMetrics = [this.props.requestedMetric.key]; + if (this.props.requestedMetric.key === 'ncloc') { + baseComponentMetrics.push('ncloc_language_distribution'); + } + Promise.all([ + getComponentTree(strategy, componentKey, metricKeys, opts), + getMeasures({ componentKey, metricKeys: baseComponentMetrics.join() }) + ]).then( + ([tree, measures]) => { + if (this.mounted) { + const metric = tree.metrics.find(m => m.key === this.props.requestedMetric.key); + const components = tree.components.map(component => + enhanceComponent(component, metric, this.props.metrics) + ); + + const measure = measures.find( + measure => measure.metric === this.props.requestedMetric.key + ); + const secondaryMeasure = measures.find( + measure => measure.metric !== this.props.requestedMetric.key + ); + + this.setState(({ selected }) => ({ + baseComponent: tree.baseComponent, + components, + measure, + metric, + paging: tree.paging, + secondaryMeasure, + selected: + components.length > 0 && components.find(c => c.key === selected) + ? selected + : undefined + })); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); }; - getComponentRequestParams = (view: View, metric: T.Metric, options: Object = {}) => { + fetchMoreComponents = () => { + const { metrics, view } = this.props; + const { baseComponent, metric, paging } = this.state; + if (!baseComponent || !paging || !metric) { + return; + } + const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric, { + p: paging.pageIndex + 1 + }); + this.setState({ loadingMoreComponents: true }); + getComponentTree(strategy, baseComponent.key, metricKeys, opts).then( + r => { + if (metric === this.props.requestedMetric) { + if (this.mounted) { + this.setState(state => ({ + components: [ + ...state.components, + ...r.components.map(component => enhanceComponent(component, metric, metrics)) + ], + paging: r.paging + })); + } + this.setState({ loadingMoreComponents: false }); + } + }, + () => { + if (this.mounted) { + this.setState({ loadingMoreComponents: false }); + } + } + ); + }; + + getComponentRequestParams( + view: View, + metric: Pick, + options: Object = {} + ) { const strategy = view === 'list' ? 'leaves' : 'children'; const metricKeys = [metric.key]; const opts: RequestData = { @@ -133,68 +216,16 @@ export default class MeasureContent extends React.PureComponent { } return { metricKeys, opts: { ...opts, ...options }, strategy }; - }; - - fetchComponents = ({ component, metric, metrics, view }: Props) => { - if (isFileType(component)) { - return; - } + } - const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric); - this.props.updateLoading({ components: true }); - getComponentTree(strategy, component.key, metricKeys, opts).then( - r => { - if (metric === this.props.metric) { - if (this.mounted) { - this.setState(({ selected }: State) => ({ - components: r.components.map(component => - enhanceComponent(component, metric, metrics) - ), - metric: { ...metric, ...r.metrics.find(m => m.key === metric.key) }, - paging: r.paging, - selected: - r.components.length > 0 && r.components.find(c => c.key === selected) - ? selected - : undefined, - view - })); - } - this.props.updateLoading({ components: false }); - } - }, - () => this.props.updateLoading({ components: false }) - ); + updateSelected = (component: string) => { + this.props.updateQuery({ + selected: component !== this.props.rootComponent.key ? component : undefined + }); }; - fetchMoreComponents = () => { - const { component, metric, metrics, view } = this.props; - const { paging } = this.state; - if (!paging) { - return; - } - const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric, { - p: paging.pageIndex + 1 - }); - this.props.updateLoading({ moreComponents: true }); - getComponentTree(strategy, component.key, metricKeys, opts).then( - r => { - if (metric === this.props.metric) { - if (this.mounted) { - this.setState(state => ({ - components: [ - ...state.components, - ...r.components.map(component => enhanceComponent(component, metric, metrics)) - ], - // merge to get the metric best value - metric: { ...metric, ...r.metrics.find(m => m.key === metric.key) }, - paging: r.paging - })); - } - this.props.updateLoading({ moreComponents: false }); - } - }, - () => this.props.updateLoading({ moreComponents: false }) - ); + updateView = (view: View) => { + this.props.updateQuery({ view }); }; onOpenComponent = (componentKey: string) => { @@ -209,26 +240,35 @@ export default class MeasureContent extends React.PureComponent { return; } } - this.setState({ selected: this.props.component.key }); - this.props.updateSelected(componentKey); + this.setState(state => ({ selected: state.baseComponent!.key })); + this.updateSelected(componentKey); if (this.container) { this.container.focus(); } }; - onSelectComponent = (componentKey: string) => this.setState({ selected: componentKey }); + onSelectComponent = (componentKey: string) => { + this.setState({ selected: componentKey }); + }; + + getSelectedIndex = () => { + const componentKey = isFileType(this.state.baseComponent!) + ? this.state.baseComponent!.key + : this.state.selected; + const index = this.state.components.findIndex(component => component.key === componentKey); + return index !== -1 ? index : undefined; + }; renderCode() { return (
); @@ -250,13 +290,14 @@ export default class MeasureContent extends React.PureComponent { fetchMore={this.fetchMoreComponents} handleOpen={this.onOpenComponent} handleSelect={this.onSelectComponent} - loadingMore={this.props.loadingMore} + loadingMore={this.state.loadingMoreComponents} metric={metric} metrics={this.props.metrics} paging={this.state.paging} rootComponent={this.props.rootComponent} selectedIdx={selectedIdx} selectedKey={selectedIdx !== undefined ? this.state.selected : undefined} + view={view} /> ); } else { @@ -272,13 +313,20 @@ export default class MeasureContent extends React.PureComponent { } render() { - const { branchLike, component, measure, metric, rootComponent, view } = this.props; - const isFile = isFileType(component); + const { branchLike, rootComponent, view } = this.props; + const { baseComponent, measure, metric, secondaryMeasure } = this.state; + + if (!baseComponent || !metric) { + return null; + } + + const measureValue = + measure && (isDiffMetric(measure.metric) ? getPeriodValue(measure, 1) : measure.value); + const isFile = isFileType(baseComponent); const selectedIdx = this.getSelectedIndex(); + return ( -
(this.container = container)}> +
(this.container = container)}>
@@ -288,21 +336,22 @@ export default class MeasureContent extends React.PureComponent { backToFirst={view === 'list'} branchLike={branchLike} className="text-ellipsis flex-1" - component={component} + component={baseComponent} handleSelect={this.onOpenComponent} rootComponent={rootComponent} /> } right={
- {!isFile && ( - - )} + {!isFile && + metric && ( + + )} {
- {!metric && } - {metric && ( -
- - - {isFileType(component) ? this.renderCode() : this.renderMeasure()} - -
- )} + +
+ + {isFile ? this.renderCode() : this.renderMeasure()} +
); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.tsx deleted file mode 100644 index cd6edf18d8b..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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 * as React from 'react'; -import { InjectedRouter } from 'react-router'; -import MeasureContent from './MeasureContent'; -import { Query, View } from '../utils'; - -interface Props { - branchLike?: T.BranchLike; - className?: string; - rootComponent: T.ComponentMeasure; - fetchMeasures: ( - component: string, - metricsKey: string[], - branchLike?: T.BranchLike - ) => Promise<{ component: T.ComponentMeasure; measures: T.MeasureEnhanced[] }>; - leakPeriod?: T.Period; - metric: T.Metric; - metrics: { [metric: string]: T.Metric }; - router: InjectedRouter; - selected?: string; - updateQuery: (query: Partial) => void; - view: View; -} - -interface LoadingState { - measure: boolean; - components: boolean; - moreComponents: boolean; -} - -interface State { - component?: T.ComponentMeasure; - loading: LoadingState; - measure?: T.MeasureEnhanced; - secondaryMeasure?: T.MeasureEnhanced; -} - -export default class MeasureContentContainer extends React.PureComponent { - mounted = false; - state: State = { loading: { measure: false, components: false, moreComponents: false } }; - - 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 = ({ branchLike, rootComponent, fetchMeasures, metric, selected }: Props) => { - this.updateLoading({ measure: true }); - - const metricKeys = [metric.key]; - if (metric.key === 'ncloc') { - metricKeys.push('ncloc_language_distribution'); - } - - fetchMeasures(selected || rootComponent.key, metricKeys, branchLike).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 }) - ); - }; - - updateLoading = (loading: Partial) => { - if (this.mounted) { - this.setState(state => ({ loading: { ...state.loading, ...loading } })); - } - }; - - updateSelected = (component: string) => { - this.props.updateQuery({ - selected: component !== this.props.rootComponent.key ? component : undefined - }); - }; - - updateView = (view: View) => { - this.props.updateQuery({ view }); - }; - - render() { - if (!this.state.component) { - return null; - } - - return ( - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx index 2f261e24198..9c1b1898cfa 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx @@ -34,13 +34,13 @@ interface Props { branchLike?: T.BranchLike; component: T.ComponentMeasure; leakPeriod?: T.Period; - measure?: T.MeasureEnhanced; + measureValue?: string; metric: T.Metric; - secondaryMeasure?: T.MeasureEnhanced; + secondaryMeasure?: T.Measure; } export default function MeasureHeader(props: Props) { - const { branchLike, component, leakPeriod, measure, metric, secondaryMeasure } = props; + const { branchLike, component, leakPeriod, measureValue, metric, secondaryMeasure } = props; const isDiff = isDiffMetric(metric.key); const hasHistory = component.qualifier !== 'FIL' && component.qualifier !== 'UTS' && hasFullMeasures(branchLike); @@ -53,20 +53,12 @@ export default function MeasureHeader(props: Props) { {getLocalizedMetricName(metric)} - {isDiff ? ( - - ) : ( - - )} + {!isDiff && @@ -88,7 +80,7 @@ export default function MeasureHeader(props: Props) {
{secondaryMeasure && - secondaryMeasure.metric.key === 'ncloc_language_distribution' && + secondaryMeasure.metric === 'ncloc_language_distribution' && secondaryMeasure.value !== undefined && (
{ componentDidMount() { this.mounted = true; - this.fetchComponents(this.props); + this.fetchComponents(); } - componentWillReceiveProps(nextProps: Props) { + componentDidUpdate(prevProps: Props) { if ( - nextProps.component !== this.props.component || - nextProps.metrics !== this.props.metrics || - nextProps.domain !== this.props.domain + prevProps.component !== this.props.component || + !isSameBranchLike(prevProps.branchLike, this.props.branchLike) || + prevProps.metrics !== this.props.metrics || + prevProps.domain !== this.props.domain ) { - this.fetchComponents(nextProps); + this.fetchComponents(); } } @@ -72,8 +73,8 @@ export default class MeasureOverview extends React.PureComponent { this.mounted = false; } - fetchComponents = (props: Props) => { - const { branchLike, component, domain, metrics } = props; + fetchComponents = () => { + const { branchLike, component, domain, metrics } = this.props; if (isFileType(component)) { this.setState({ components: [], paging: undefined }); return; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx index 200fce078a2..af26325e68a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx @@ -23,7 +23,7 @@ import MeasureOverview from './MeasureOverview'; import { getComponentShow } from '../../../api/components'; import { getProjectUrl } from '../../../helpers/urls'; import { isViewType, Query } from '../utils'; -import { getBranchLikeQuery } from '../../../helpers/branches'; +import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branches'; interface Props { branchLike?: T.BranchLike; @@ -56,17 +56,18 @@ export default class MeasureOverviewContainer extends React.PureComponent { + fetchComponent = () => { + const { branchLike, rootComponent, selected } = this.props; if (!selected || rootComponent.key === selected) { this.setState({ component: rootComponent }); this.updateLoading({ component: false }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.tsx deleted file mode 100644 index 6985137bfbd..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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 * as React from 'react'; -import { translate } from '../../../helpers/l10n'; -import { Alert } from '../../../components/ui/Alert'; - -// TODO seems like this component is used by never rendered in real life -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.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx index de3394ecc88..ad3c05ee775 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx @@ -19,44 +19,66 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import App from '../App'; +import { Location } from 'history'; +import { App } from '../App'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { getMeasuresAndMeta } from '../../../../api/measures'; -const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' }; +jest.mock('../../../../api/metrics', () => ({ + getAllMetrics: jest.fn().mockResolvedValue([ + { + id: '1', + key: 'lines_to_cover', + type: 'INT', + name: 'Lines to Cover', + domain: 'Coverage' + }, + { + id: '2', + key: 'coverage', + type: 'PERCENT', + name: 'Coverage', + domain: 'Coverage' + }, + { + id: '3', + key: 'duplicated_lines_density', + type: 'PERCENT', + name: 'Duplicated Lines (%)', + domain: 'Duplications' + }, + { + id: '4', + key: 'new_bugs', + type: 'INT', + name: 'New Bugs', + domain: 'Reliability' + } + ]) +})); -const METRICS = { - lines_to_cover: { - id: '1', - key: 'lines_to_cover', - type: 'INT', - name: 'Lines to Cover', - domain: 'Coverage' - }, - coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, - duplicated_lines_density: { - id: '3', - key: 'duplicated_lines_density', - type: 'PERCENT', - name: 'Duplicated Lines (%)', - domain: 'Duplications' - }, - new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' } -}; +jest.mock('../../../../api/measures', () => ({ + getMeasuresAndMeta: jest.fn() +})); + +const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' }; const PROPS: App['props'] = { branchLike: { isMain: true, name: 'master' }, component: COMPONENT, - location: { pathname: '/component_measures', query: { metric: 'coverage' } }, - fetchMeasures: jest.fn().mockResolvedValue({ - component: COMPONENT, - measures: [{ metric: 'coverage', value: '80.0' }] - }), - fetchMetrics: jest.fn(), - metrics: METRICS, - metricsKey: ['lines_to_cover', 'coverage', 'duplicated_lines_density', 'new_bugs'], - router: { push: jest.fn() } as any + location: { pathname: '/component_measures', query: { metric: 'coverage' } } as Location, + params: {}, + router: { push: jest.fn() } as any, + routes: [] }; +beforeEach(() => { + (getMeasuresAndMeta as jest.Mock).mockResolvedValue({ + component: { measures: [{ metric: 'coverage', value: '80.0' }] }, + periods: [{ index: '1' }] + }); +}); + it('should render correctly', async () => { const wrapper = shallow(); expect(wrapper.find('.spinner')).toHaveLength(1); @@ -68,7 +90,7 @@ it('should render a measure overview', async () => { const wrapper = shallow( ); expect(wrapper.find('.spinner')).toHaveLength(1); @@ -77,8 +99,11 @@ it('should render a measure overview', async () => { }); it('should render a message when there are no measures', async () => { - const fetchMeasures = jest.fn().mockResolvedValue({ component: COMPONENT, measures: [] }); - const wrapper = shallow(); + (getMeasuresAndMeta as jest.Mock).mockResolvedValue({ + component: { measures: [] }, + periods: [{ index: '1' }] + }); + const wrapper = shallow(); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx index e00c0b57912..4fdc255b5a3 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx @@ -28,13 +28,6 @@ const METRIC = { name: 'Reliability Rating' }; -const MEASURE = { - value: '3.0', - periods: [{ index: 1, value: '0.0' }], - metric: METRIC, - leak: '0.0' -}; - const LEAK_METRIC = { id: '2', key: 'new_reliability_rating', @@ -42,20 +35,11 @@ const LEAK_METRIC = { name: 'Reliability Rating on New Code' }; -const LEAK_MEASURE = { - periods: [{ index: 1, value: '3.0' }], - metric: LEAK_METRIC, - leak: '3.0' -}; +const LEAK_MEASURE = '3.0'; const SECONDARY = { value: 'java=175123;js=26382', - metric: { - id: '3', - key: 'ncloc_language_distribution', - type: 'DATA', - name: 'Lines of Code Per Language' - } + metric: 'ncloc_language_distribution' }; const PROPS = { @@ -66,7 +50,7 @@ const PROPS = { mode: 'previous_version', parameter: '6,4' } as T.Period, - measure: MEASURE, + measureValue: '3.0', metric: METRIC }; @@ -76,7 +60,7 @@ it('should render correctly', () => { it('should render correctly for leak', () => { expect( - shallow() + shallow() ).toMatchSnapshot(); }); @@ -94,7 +78,7 @@ it('should render with short living branch', () => { ) @@ -121,5 +105,5 @@ it('should display secondary measure too', () => { }); it('should work with measure without value', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.tsx.snap index 56e8fe4c14f..e90f3b035ea 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.tsx.snap @@ -73,48 +73,13 @@ exports[`should render correctly 1`] = ` > - void; } diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx index dc27d380146..07c249ce0ed 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx @@ -30,6 +30,7 @@ import { getProjectUrl } from '../../../helpers/urls'; import { translate } from '../../../helpers/l10n'; +import { View } from '../utils'; interface Props { branchLike?: T.BranchLike; @@ -37,6 +38,7 @@ interface Props { onClick: (component: string) => void; metric: T.Metric; rootComponent: T.ComponentMeasure; + view: View; } export default class ComponentCell extends React.PureComponent { @@ -54,33 +56,34 @@ export default class ComponentCell extends React.PureComponent { const { component } = this.props; let head = ''; let tail = component.name; - let branchComponent = null; - if (['DIR', 'FIL', 'UTS'].includes(component.qualifier) && component.path) { + if ( + this.props.view === 'list' && + ['FIL', 'UTS', 'DIR'].includes(component.qualifier) && + component.path + ) { ({ head, tail } = splitPath(component.path)); } - if (this.props.rootComponent.qualifier === 'APP') { - branchComponent = ( - <> - {component.branch ? ( - <> - - {component.branch} - - ) : ( - {translate('branches.main_branch')} - )} - - ); - } + const isApp = this.props.rootComponent.qualifier === 'APP'; + return ( - -   + {head.length > 0 && {head}/} {tail} - {branchComponent} + {isApp && ( + <> + {component.branch ? ( + <> + + {component.branch} + + ) : ( + {translate('branches.main_branch')} + )} + + )} ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx index 1cb3fa627b6..21777417bec 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx @@ -22,6 +22,7 @@ import ComponentsListRow from './ComponentsListRow'; import EmptyResult from './EmptyResult'; import { complementary } from '../config/complementary'; import { getLocalizedMetricName } from '../../../helpers/l10n'; +import { View } from '../utils'; interface Props { branchLike?: T.BranchLike; @@ -31,6 +32,7 @@ interface Props { metrics: { [metric: string]: T.Metric }; rootComponent: T.ComponentMeasure; selectedComponent?: string; + view: View; } export default function ComponentsList({ components, metric, metrics, ...props }: Props) { @@ -40,37 +42,35 @@ export default function ComponentsList({ components, metric, metrics, ...props } const otherMetrics = (complementary[metric.key] || []).map(key => metrics[key]); return ( - - - {otherMetrics.length > 0 && ( - - - - + {components.map(component => ( + + ))} + +
  + + {otherMetrics.length > 0 && ( + + + + + {otherMetrics.map(metric => ( + - {otherMetrics.map(metric => ( - - ))} - - - )} + ))} + + + )} - - {components.map(component => ( - - ))} - -
  + {getLocalizedMetricName(metric)} + {getLocalizedMetricName(metric)} - {getLocalizedMetricName(metric)} -
- +
); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx index 571c0191e3a..eba1ed5536a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import * as classNames from 'classnames'; import ComponentCell from './ComponentCell'; import MeasureCell from './MeasureCell'; +import { View } from '../utils'; interface Props { branchLike?: T.BranchLike; @@ -30,6 +31,7 @@ interface Props { otherMetrics: T.Metric[]; metric: T.Metric; rootComponent: T.ComponentMeasure; + view: View; } export default function ComponentsListRow(props: Props) { @@ -49,6 +51,7 @@ export default function ComponentsListRow(props: Props) { metric={props.metric} onClick={props.onClick} rootComponent={rootComponent} + view={props.view} /> diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx index d4f364f6f34..919bb516797 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx @@ -27,6 +27,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; import { isPeriodBestValue, isDiffMetric, formatMeasure } from '../../../helpers/measures'; import { scrollToElement } from '../../../helpers/scrolling'; import { Alert } from '../../../components/ui/Alert'; +import { View } from '../utils'; interface Props { branchLike?: T.BranchLike; @@ -42,12 +43,15 @@ interface Props { rootComponent: T.ComponentMeasure; selectedKey?: string; selectedIdx?: number; + view: View; } interface State { showBestMeasures: boolean; } +const keyScope = 'measures-files'; + export default class FilesView extends React.PureComponent { listContainer?: HTMLElement | null; @@ -79,22 +83,22 @@ export default class FilesView extends React.PureComponent { } attachShortcuts() { - key('up', 'measures-files', () => { + key('up', keyScope, () => { this.selectPrevious(); return false; }); - key('down', 'measures-files', () => { + key('down', keyScope, () => { this.selectNext(); return false; }); - key('right', 'measures-files', () => { + key('right', keyScope, () => { this.openSelected(); return false; }); } detachShortcuts() { - ['up', 'down', 'right'].forEach(action => key.unbind(action, 'measures-files')); + ['up', 'down', 'right'].forEach(action => key.unbind(action, keyScope)); } getVisibleComponents = (components: T.ComponentMeasureEnhanced[], showBestMeasures: boolean) => { @@ -170,6 +174,7 @@ export default class FilesView extends React.PureComponent { onClick={this.props.handleOpen} rootComponent={this.props.rootComponent} selectedComponent={this.props.selectedKey} + view={this.props.view} /> {hidingBestMeasures && ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentList-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentList-test.tsx index 6ab93f9f2e5..6139ea9bee4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentList-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentList-test.tsx @@ -47,6 +47,7 @@ it('should renders correctly', () => { metrics={METRICS} onClick={jest.fn()} rootComponent={COMPONENTS[0]} + view="tree" /> ) ).toMatchSnapshot(); @@ -61,6 +62,7 @@ it('should renders empty', () => { metrics={METRICS} onClick={jest.fn()} rootComponent={COMPONENTS[0]} + view="tree" /> ) ).toMatchSnapshot(); @@ -75,6 +77,7 @@ it('should renders with multiple measures', () => { metrics={METRICS} onClick={jest.fn()} rootComponent={COMPONENTS[0]} + view="tree" /> ) ).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/FilesView-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/FilesView-test.tsx index 425024d3e86..ae2682cefc9 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/FilesView-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/FilesView-test.tsx @@ -73,6 +73,7 @@ function getWrapper(props = {}) { organization: 'foo', qualifier: 'TRK' }} + view="tree" {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentList-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentList-test.tsx.snap index 743f6ea0ba8..c16ae7b6dff 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentList-test.tsx.snap @@ -1,140 +1,138 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should renders correctly 1`] = ` - - - - + + - -
-
+ } + view="tree" + /> + + `; exports[`should renders empty 1`] = ``; exports[`should renders with multiple measures 1`] = ` - - - - - - + + + - + - + - - - - + + + + + - -
-   - +
+   + + - - Coverage - - + + - - Lines - - + + - - Conditions - -
-
+ } + view="tree" + /> + + `; diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/FilesView-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/FilesView-test.tsx.snap index e9c91714593..0efe85f4ba8 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/FilesView-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/FilesView-test.tsx.snap @@ -42,6 +42,7 @@ exports[`should render with best values hidden 1`] = ` "qualifier": "TRK", } } + view="tree" /> import('./components/AppContainer')) } + indexRoute: { component: lazyLoad(() => import('./components/App')) } }, { path: 'domain/:domainName', diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx index 0086d1f4e76..c6a3e599d70 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import ProjectOverviewFacet from './ProjectOverviewFacet'; import DomainFacet from './DomainFacet'; -import { getDefaultView, groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW, Query } from '../utils'; +import { groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW, Query } from '../utils'; interface Props { hasOverview: boolean; @@ -34,31 +34,12 @@ interface State { } export default class Sidebar extends React.PureComponent { - constructor(props: Props) { - super(props); - this.state = { openFacets: this.getOpenFacets({}, props) }; + static getDerivedStateFromProps(props: Props, state: State) { + return { openFacets: getOpenFacets(state.openFacets, props) }; } - componentWillReceiveProps(nextProps: Props) { - if (nextProps.selectedMetric !== this.props.selectedMetric) { - this.setState(({ openFacets }) => ({ - openFacets: this.getOpenFacets(openFacets, nextProps) - })); - } - } - - getOpenFacets = ( - openFacets: { [metric: string]: boolean }, - { measures, selectedMetric }: Props - ) => { - const newOpenFacets = { ...openFacets }; - const measure = measures.find(measure => measure.metric.key === selectedMetric); - if (measure && measure.metric && measure.metric.domain) { - newOpenFacets[measure.metric.domain] = true; - } else if (KNOWN_DOMAINS.includes(selectedMetric)) { - newOpenFacets[selectedMetric] = true; - } - return newOpenFacets; + state: State = { + openFacets: {} }; toggleFacet = (name: string) => { @@ -67,10 +48,9 @@ export default class Sidebar extends React.PureComponent { })); }; - resetSelection = (metric: string) => ({ selected: undefined, view: getDefaultView(metric) }); - - changeMetric = (metric: string) => - this.props.updateQuery({ metric, ...this.resetSelection(metric) }); + changeMetric = (metric: string) => { + this.props.updateQuery({ metric }); + }; render() { const { hasOverview } = this.props; @@ -98,3 +78,17 @@ export default class Sidebar extends React.PureComponent { ); } } + +function getOpenFacets( + openFacets: { [metric: string]: boolean }, + { measures, selectedMetric }: Props +) { + const newOpenFacets = { ...openFacets }; + const measure = measures.find(measure => measure.metric.key === selectedMetric); + if (measure && measure.metric && measure.metric.domain) { + newOpenFacets[measure.metric.domain] = true; + } else if (KNOWN_DOMAINS.includes(selectedMetric)) { + newOpenFacets[selectedMetric] = true; + } + return newOpenFacets; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.ts b/server/sonar-web/src/main/js/apps/component-measures/utils.ts index 49b0b7a6775..52e39e4e059 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.ts @@ -87,7 +87,7 @@ export function addMeasureCategories(domainName: string, measures: T.MeasureEnha export function enhanceComponent( component: T.ComponentMeasure, - metric: T.Metric | undefined, + metric: Pick | undefined, metrics: { [key: string]: T.Metric } ): T.ComponentMeasureEnhanced { if (!component.measures) { @@ -124,13 +124,6 @@ export const groupByDomains = memoize((measures: T.MeasureEnhanced[]) => { ]); }); -export function getDefaultView(metric: string): View { - if (!hasList(metric)) { - return 'tree'; - } - return DEFAULT_VIEW; -} - export function hasList(metric: string): boolean { return !['releasability_rating', 'releasability_effort'].includes(metric); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx index 4a7c46e3754..5d3a86e8ca4 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx @@ -27,7 +27,6 @@ import Coverage from '../main/Coverage'; import Duplications from '../main/Duplications'; import MetaContainer from '../meta/MetaContainer'; import QualityGate from '../qualityGate/QualityGate'; -import throwGlobalError from '../../../app/utils/throwGlobalError'; import { getMeasuresAndMeta } from '../../../api/measures'; import { getAllTimeMachineData } from '../../../api/time-machine'; import { parseDate } from '../../../helpers/dates'; @@ -150,8 +149,7 @@ export class OverviewApp extends React.PureComponent { }); } }, - error => { - throwGlobalError(error); + () => { if (this.mounted) { this.setState({ loading: false }); }