diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-02 16:42:00 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-14 11:44:44 +0200 |
commit | d0d3a549fc0b1acf602511bd17e99923023b0d12 (patch) | |
tree | 5fa0106fe71b3293bd05ae9c2dff009826aa1bae /server/sonar-web/src/main/js/apps | |
parent | 5be60c5d3348076336e5a79e6308104db52f27dc (diff) | |
download | sonarqube-d0d3a549fc0b1acf602511bd17e99923023b0d12.tar.gz sonarqube-d0d3a549fc0b1acf602511bd17e99923023b0d12.zip |
SONAR-9608 SONAR-9635 Make tree view and file view use the same base component
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
16 files changed, 493 insertions, 281 deletions
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 2524de80259..e742c486be0 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,7 +20,7 @@ // @flow import React from 'react'; import Helmet from 'react-helmet'; -import MeasureContent from './MeasureContent'; +import MeasureContentContainer from './MeasureContentContainer'; import Sidebar from '../sidebar/Sidebar'; import { parseQuery, serializeQuery } from '../utils'; import { translate } from '../../../helpers/l10n'; @@ -153,8 +153,8 @@ export default class App extends React.PureComponent { </div> {metric != null && - <MeasureContent - className="layout-page-main-inner" + <MeasureContentContainer + className="layout-page-main" currentUser={this.props.currentUser} rootComponent={this.props.component} fetchMeasures={this.props.fetchMeasures} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js index 320a9ea93e9..78a035729e8 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js @@ -23,13 +23,12 @@ import Breadcrumb from './Breadcrumb'; import { getBreadcrumbs } from '../../../api/components'; import type { Component } from '../types'; -type Props = { +type Props = {| className?: string, component: Component, handleSelect: Component => void, - rootComponent: Component, - view: string -}; + rootComponent: Component +|}; type State = { breadcrumbs: Array<Component> @@ -57,9 +56,9 @@ export default class Breadcrumbs extends React.PureComponent { this.mounted = false; } - fetchBreadcrumbs = ({ component, rootComponent, view }: Props) => { + fetchBreadcrumbs = ({ component, rootComponent }: Props) => { const isRoot = component.key === rootComponent.key; - if (isRoot || view === 'list') { + if (isRoot) { if (this.mounted) { this.setState({ breadcrumbs: isRoot ? [component] : [rootComponent, component] }); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js b/server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js new file mode 100644 index 00000000000..4dc60a071d1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js @@ -0,0 +1,45 @@ +/* + * 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'; +import { formatMeasure } from '../../../helpers/measures'; + +type Props = { + className?: string, + current: ?number, + total: number +}; + +export default function FilesCounter({ className, current, total }: Props) { + return ( + <span className={className}> + <strong> + {current != null && + <span> + {formatMeasure(current, 'INT')} + {' / '} + </span>} + {formatMeasure(total, 'INT')} + </strong>{' '} + {translate('component_measures.files')} + </span> + ); +} 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 index a0447e10030..cf99ff49311 100644 --- 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 @@ -22,69 +22,60 @@ import React from 'react'; import moment from 'moment'; import Breadcrumbs from './Breadcrumbs'; import Favorite from '../../../components/controls/Favorite'; -import ListView from './drilldown/ListView'; +import FilesView from './drilldown/FilesView'; import MeasureHeader from './MeasureHeader'; import MeasureViewSelect from './MeasureViewSelect'; import MetricNotFound from './MetricNotFound'; import PageActions from './PageActions'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; +import { getComponentTree } from '../../../api/components'; +import { complementary } from '../config/complementary'; +import { enhanceComponent, isFileType } from '../utils'; import { isDiffMetric } from '../../../helpers/measures'; -import type { Component, Period, Query } from '../types'; +import type { Component, ComponentEnhanced, Paging, Period } from '../types'; import type { MeasureEnhanced } from '../../../components/measure/types'; import type { Metric } from '../../../store/metrics/actions'; type Props = { className?: string, + component: Component, currentUser: { isLoggedIn: boolean }, - rootComponent: Component, - fetchMeasures: ( - Component, - Array<string> - ) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>, + loading: boolean, leakPeriod?: Period, + measure: ?MeasureEnhanced, metric: Metric, metrics: { [string]: Metric }, - selected: ?string, - updateQuery: Query => void, + rootComponent: Component, + secondaryMeasure: ?MeasureEnhanced, + updateLoading: ({ [string]: boolean }) => void, + updateSelected: Component => void, + updateView: string => void, view: string }; type State = { - component: ?Component, - loading: { - measure: boolean, - components: boolean - }, - measure: ?MeasureEnhanced, - secondaryMeasure: ?MeasureEnhanced + components: Array<ComponentEnhanced>, + metric: ?Metric, + paging?: Paging }; export default class MeasureContent extends React.PureComponent { mounted: boolean; props: Props; state: State = { - component: null, - loading: { - measure: false, - components: false - }, - measure: null, - secondaryMeasure: null + components: [], + metric: null, + paging: null }; componentDidMount() { this.mounted = true; - this.fetchMeasure(this.props); + this.fetchComponents(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); + if (nextProps.component !== this.props.component || nextProps.metric !== this.props.metric) { + this.fetchComponents(nextProps); } } @@ -92,56 +83,89 @@ export default class MeasureContent extends React.PureComponent { this.mounted = false; } - fetchMeasure = ({ rootComponent, fetchMeasures, metric, selected }: Props) => { - this.updateLoading({ measure: true }); + 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 } }; + }; - 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'); + fetchComponents = ({ component, metric, view }: Props) => { + if (isFileType(component)) { + return this.setState({ components: [], metric: null, paging: null }); } - fetchMeasures(selected || rootComponent.key, metricKeys).then( - ({ component, measures }) => { + const strategy = view === 'list' ? 'leaves' : 'children'; + const { metricKeys, opts } = this.getComponentRequestParams(metric); + this.props.updateLoading({ components: true }); + getComponentTree(strategy, component.key, metricKeys, opts).then( + r => { 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.setState({ + components: r.components.map(component => enhanceComponent(component, metric)), + metric, + paging: r.paging + }); } + this.props.updateLoading({ components: false }); }, - () => this.updateLoading({ measure: false }) + () => this.props.updateLoading({ components: false }) ); }; - handleSelect = (component: Component) => - this.props.updateQuery({ - selected: component.key !== this.props.rootComponent.key ? component.key : null - }); - - updateLoading = (loading: { [string]: boolean }) => { - if (this.mounted) { - this.setState(state => ({ loading: { ...state.loading, ...loading } })); + fetchMoreComponents = () => { + const { component, metric, view } = this.props; + const { paging } = this.state; + if (!paging) { + return; } + const strategy = view === 'list' ? 'leaves' : 'children'; + const { metricKeys, opts } = this.getComponentRequestParams(metric, { + p: paging.pageIndex + 1 + }); + this.props.updateLoading({ components: true }); + getComponentTree(strategy, 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 }) + ); }; - updateView = (view: string) => this.props.updateQuery({ view }); - renderContent() { - const { component } = this.state; - if (!component) { - return null; - } - - const { leakPeriod, metric, rootComponent, view } = this.props; - const isFile = component.key !== rootComponent.key && component.qualifier === 'FIL'; + const { component, leakPeriod, view } = this.props; - if (isFile) { + if (isFileType(component)) { const leakPeriodDate = - isDiffMetric(metric.key) && leakPeriod != null ? moment(leakPeriod.date).toDate() : null; + isDiffMetric(this.props.metric.key) && leakPeriod != null + ? moment(leakPeriod.date).toDate() + : null; let filterLine; if (leakPeriodDate != null) { @@ -161,38 +185,40 @@ export default class MeasureContent extends React.PureComponent { ); } - if (view === 'list') { + const { metric } = this.state; + if (metric == null) { + return null; + } + + if (['list', 'tree'].includes(view)) { return ( - <ListView - component={component} - handleSelect={this.handleSelect} + <FilesView + components={this.state.components} + fetchMore={this.fetchMoreComponents} + handleSelect={this.props.updateSelected} metric={metric} metrics={this.props.metrics} - updateLoading={this.updateLoading} + paging={this.state.paging} /> ); } } render() { - const { currentUser, metric, rootComponent, view } = this.props; - const { component, loading, measure } = this.state; + const { component, currentUser, measure, metric, rootComponent, view } = this.props; const isLoggedIn = currentUser && currentUser.isLoggedIn; return ( - <div className="layout-page-main"> + <div className={this.props.className}> <div className="layout-page-header-panel layout-page-main-header issues-main-header"> <div className="layout-page-header-panel-inner layout-page-main-header-inner"> <div className="layout-page-main-inner clearfix"> - {component && - <Breadcrumbs - className="measure-breadcrumbs spacer-right text-ellipsis" - component={component} - handleSelect={this.handleSelect} - rootComponent={rootComponent} - view={view} - />} - {component && - component.key !== rootComponent.key && + <Breadcrumbs + className="measure-breadcrumbs spacer-right text-ellipsis" + component={component} + handleSelect={this.props.updateSelected} + rootComponent={rootComponent} + /> + {component.key !== rootComponent.key && isLoggedIn && <Favorite favorite={component.isFavorite === true} @@ -201,13 +227,15 @@ export default class MeasureContent extends React.PureComponent { />} <MeasureViewSelect className="measure-view-select" - metric={this.props.metric} - handleViewChange={this.updateView} + metric={metric} + handleViewChange={this.props.updateView} view={view} /> <PageActions - loading={loading.measure || loading.components} - isFile={component && component.qualifier === 'FIL'} + current={this.state.components.length} + loading={this.props.loading} + isFile={isFileType(component)} + paging={this.state.paging} view={view} /> </div> @@ -217,14 +245,12 @@ export default class MeasureContent extends React.PureComponent { {metric != null && measure != null && <div className="layout-page-main-inner"> - {component && - <MeasureHeader - component={component} - leakPeriod={this.props.leakPeriod} - measure={measure} - secondaryMeasure={this.state.secondaryMeasure} - updateQuery={this.props.updateQuery} - />} + <MeasureHeader + component={component} + leakPeriod={this.props.leakPeriod} + measure={measure} + secondaryMeasure={this.props.secondaryMeasure} + /> {this.renderContent()} </div>} </div> diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js new file mode 100644 index 00000000000..747a7af5c33 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js @@ -0,0 +1,148 @@ +/* + * 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 MeasureContent from './MeasureContent'; +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, + currentUser: { isLoggedIn: boolean }, + rootComponent: Component, + fetchMeasures: ( + Component, + Array<string> + ) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>, + leakPeriod?: Period, + metric: Metric, + metrics: { [string]: Metric }, + selected: ?string, + updateQuery: Query => void, + view: string +}; + +type State = { + component: ?Component, + loading: { + measure: boolean, + components: boolean + }, + measure: ?MeasureEnhanced, + secondaryMeasure: ?MeasureEnhanced +}; + +export default class MeasureContentContainer 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 }) + ); + }; + + updateLoading = (loading: { [string]: boolean }) => { + if (this.mounted) { + this.setState(state => ({ loading: { ...state.loading, ...loading } })); + } + }; + + updateSelected = (component: Component) => + this.props.updateQuery({ + selected: component.key !== this.props.rootComponent.key ? component.key : null + }); + + updateView = (view: string) => this.props.updateQuery({ view }); + + render() { + if (!this.state.component) { + return null; + } + + return ( + <MeasureContent + className={this.props.className} + component={this.state.component} + currentUser={this.props.currentUser} + loading={this.state.loading.measure || this.state.loading.components} + leakPeriod={this.props.leakPeriod} + measure={this.state.measure} + metric={this.props.metric} + metrics={this.props.metrics} + rootComponent={this.props.rootComponent} + secondaryMeasure={this.state.secondaryMeasure} + updateLoading={this.updateLoading} + updateSelected={this.updateSelected} + updateView={this.updateView} + view={this.props.view} + /> + ); + } +} 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 index a29d87081cd..e27bfd396f3 100644 --- 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 @@ -32,12 +32,12 @@ import { isDiffMetric } from '../../../helpers/measures'; import type { Component, Period } from '../types'; import type { MeasureEnhanced } from '../../../components/measure/types'; -type Props = { +type Props = {| component: Component, leakPeriod?: Period, measure: MeasureEnhanced, secondaryMeasure: ?MeasureEnhanced -}; +|}; export default function MeasureHeader({ component, leakPeriod, measure, secondaryMeasure }: Props) { const metric = measure.metric; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js b/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js index 22df52e5ea9..297d351d79d 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js @@ -20,11 +20,15 @@ // @flow import React from 'react'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import FilesCounter from './FilesCounter'; import { translate } from '../../../helpers/l10n'; +import type { Paging } from '../types'; type Props = {| + current: ?number, loading: boolean, isFile: ?boolean, + paging: ?Paging, view: string |}; @@ -61,14 +65,20 @@ export default class PageActions extends React.PureComponent { } render() { - const { isFile, view } = this.props; + const { isFile, paging, view } = this.props; const showShortcuts = ['list', 'tree'].includes(view); return ( <div className="pull-right"> {!isFile && showShortcuts && this.renderShortcuts()} {isFile && this.renderFileShortcuts()} - <div className="measure-details-page-spinner"> - <DeferredSpinner className="pull-right" loading={this.props.loading} /> + <div className="measure-details-page-actions"> + <DeferredSpinner loading={this.props.loading} /> + {paging != null && + <FilesCounter + className="spacer-left" + current={this.props.current} + total={paging.total} + />} </div> </div> ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js new file mode 100644 index 00000000000..55a575c6b48 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.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. + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import FilesCounter from '../FilesCounter'; + +it('should display x files on y total', () => { + expect(shallow(<FilesCounter current={12} total={123455} />)).toMatchSnapshot(); +}); + +it('should display only total of files', () => { + expect(shallow(<FilesCounter current={null} total={123455} />)).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js index d3770a5fa0e..a75ddac8ecb 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js @@ -32,3 +32,17 @@ it('should display correctly for a file', () => { it('should not display shortcuts for treemap', () => { expect(shallow(<PageActions loading={true} isFile={false} view="treemap" />)).toMatchSnapshot(); }); + +it('should display the total of files', () => { + expect( + shallow( + <PageActions + current={12} + loading={true} + isFile={false} + view="treemap" + paging={{ total: 120 }} + /> + ) + ).toMatchSnapshot(); +}); 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 99335765bdc..e6c6e72a116 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 @@ -35,8 +35,8 @@ exports[`should render correctly 1`] = ` </div> </div> </div> - <MeasureContent - className="layout-page-main-inner" + <MeasureContentContainer + className="layout-page-main" fetchMeasures={[Function]} leakPeriod={null} metric={ diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap new file mode 100644 index 00000000000..bb01a6121da --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display only total of files 1`] = ` +<span> + <strong> + 123,455 + </strong> + + component_measures.files +</span> +`; + +exports[`should display x files on y total 1`] = ` +<span> + <strong> + <span> + 12 + / + </span> + 123,455 + </strong> + + component_measures.files +</span> +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap index 26a060b6fb2..5121c8d6e90 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap @@ -17,10 +17,9 @@ exports[`should display correctly for a file 1`] = ` </span> </span> <div - className="measure-details-page-spinner" + className="measure-details-page-actions" > <DeferredSpinner - className="pull-right" loading={false} timeout={100} /> @@ -65,10 +64,9 @@ exports[`should display correctly for a project 1`] = ` </span> </span> <div - className="measure-details-page-spinner" + className="measure-details-page-actions" > <DeferredSpinner - className="pull-right" loading={true} timeout={100} /> @@ -76,15 +74,34 @@ exports[`should display correctly for a project 1`] = ` </div> `; +exports[`should display the total of files 1`] = ` +<div + className="pull-right" +> + <div + className="measure-details-page-actions" + > + <DeferredSpinner + loading={true} + timeout={100} + /> + <FilesCounter + className="spacer-left" + current={12} + total={120} + /> + </div> +</div> +`; + exports[`should not display shortcuts for treemap 1`] = ` <div className="pull-right" > <div - className="measure-details-page-spinner" + className="measure-details-page-actions" > <DeferredSpinner - className="pull-right" loading={true} timeout={100} /> diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js new file mode 100644 index 00000000000..2e2d12dff72 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js @@ -0,0 +1,53 @@ +/* + * 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 ComponentsList from './ComponentsList'; +import ListFooter from '../../../../components/controls/ListFooter'; +import type { Component, ComponentEnhanced, Paging } from '../../types'; +import type { Metric } from '../../../../store/metrics/actions'; + +type Props = { + components: Array<ComponentEnhanced>, + fetchMore: () => void, + handleSelect: Component => void, + metric: Metric, + metrics: { [string]: Metric }, + paging: ?Paging +}; + +export default function ListView(props: Props) { + return ( + <div> + <ComponentsList + components={props.components} + metrics={props.metrics} + metric={props.metric} + onClick={props.handleSelect} + /> + {props.paging && + <ListFooter + count={props.components.length} + total={props.paging.total} + loadMore={props.fetchMore} + />} + </div> + ); +} 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 deleted file mode 100644 index b9f94f2c715..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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 ComponentsList from './ComponentsList'; -import ListFooter from '../../../../components/controls/ListFooter'; -import { getComponentTree } from '../../../../api/components'; -import { complementary } from '../../config/complementary'; -import { enhanceComponent } from '../../utils'; -import { isDiffMetric } from '../../../../helpers/measures'; -import type { Component, ComponentEnhanced, Paging } from '../../types'; -import type { Metric } from '../../../../store/metrics/actions'; - -type Props = { - component: Component, - handleSelect: Component => void, - metric: Metric, - metrics: { [string]: Metric }, - updateLoading: ({ [string]: boolean }) => void -}; - -type State = { - components: Array<ComponentEnhanced>, - 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 }: Props) => { - 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; - } - - return ( - <div> - <ComponentsList - components={components} - metrics={this.props.metrics} - metric={metric} - onClick={this.props.handleSelect} - /> - {paging && - <ListFooter - count={components.length} - total={paging.total} - loadMore={this.fetchMoreComponents} - />} - </div> - ); - } -} 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 238b36f5be3..d5aabbb7ac5 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 @@ -16,10 +16,13 @@ white-space: nowrap; } -.measure-details-page-spinner { +.measure-details-page-actions { display: inline-block; - min-width: 20px; + min-width: 80px; text-align: right; +} + +.measure-details-page-actions .spinner { vertical-align: text-bottom; } 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 7e3a1d07cb5..74f1c8c4335 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 @@ -80,6 +80,9 @@ export const enhanceComponent = (component: Component, metric: Metric): Componen return { ...component, value, leak, measures: enhancedMeasures }; }; +export const isFileType = (component: Component): boolean => + ['FIL', 'UTS'].includes(component.qualifier); + export const groupByDomains = memoize((measures: Array<MeasureEnhanced>): Array<{ name: string, measures: Array<MeasureEnhanced> |