aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-08-02 16:42:00 +0200
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-08-14 11:44:44 +0200
commitd0d3a549fc0b1acf602511bd17e99923023b0d12 (patch)
tree5fa0106fe71b3293bd05ae9c2dff009826aa1bae /server/sonar-web/src/main/js/apps
parent5be60c5d3348076336e5a79e6308104db52f27dc (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/App.js6
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js11
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js45
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js218
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js148
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js16
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js30
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js14
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap25
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap29
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js53
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js161
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/style.css7
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.js3
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>