]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9608 SONAR-9635 Make tree view and file view use the same base component
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 2 Aug 2017 14:42:00 +0000 (16:42 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 14 Aug 2017 09:44:44 +0000 (11:44 +0200)
17 files changed:
server/sonar-web/src/main/js/apps/component-measures/components/App.js
server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js
server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap
server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/style.css
server/sonar-web/src/main/js/apps/component-measures/utils.js
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 2524de802592818d18acba306039e262fce65029..e742c486be052fd1476e8a8d67083e36009010d2 100644 (file)
@@ -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}
index 320a9ea93e9d4b1feaaca36b32be311cc9a42260..78a035729e8f6f2b11af4e9d24ea1fab47747e7b 100644 (file)
@@ -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 (file)
index 0000000..4dc60a0
--- /dev/null
@@ -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>
+  );
+}
index a0447e1003037a63090b82de2d490c1c28a36382..cf99ff493114516d87051c9ba42e5dbcf4049e5f 100644 (file)
@@ -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 (file)
index 0000000..747a7af
--- /dev/null
@@ -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}
+      />
+    );
+  }
+}
index a29d87081cdf96bf7869fe5079b659a1b51b168f..e27bfd396f3897dd729273905c6c02e73cc4277f 100644 (file)
@@ -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;
index 22df52e5ea992c25d9a2afb247f818bdbf5e9954..297d351d79d701940079844817601eae1ce7de1d 100644 (file)
 // @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 (file)
index 0000000..55a575c
--- /dev/null
@@ -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();
+});
index d3770a5fa0ee4a9d281277026ba0bc171f0eda41..a75ddac8ecbd5eb897ffea17869efcece3233048 100644 (file)
@@ -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();
+});
index 99335765bdc4813b127ca843a53da260d9ccbd22..e6c6e72a116afcb38c936a3f2c5f0b61ea896913 100644 (file)
@@ -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 (file)
index 0000000..bb01a61
--- /dev/null
@@ -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>
+`;
index 26a060b6fb22d42e75403d1fefbf08ccee07df9a..5121c8d6e90bcc061ed2d1ca960c2945baab4b37 100644 (file)
@@ -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 (file)
index 0000000..2e2d12d
--- /dev/null
@@ -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 (file)
index b9f94f2..0000000
+++ /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>
-    );
-  }
-}
index 238b36f5be3e4842c5bc741311252878f8ee0a12..d5aabbb7ac58edc0e14bd67d08c1eb291597a24b 100644 (file)
   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;
 }
 
index 7e3a1d07cb5ddd09ad58652f1660f53dd10018ed..74f1c8c433537e68824dca272bb33de0238c4bcf 100644 (file)
@@ -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>
index e75da8525f745fe50e64e4525966df1242508030..f4bf4662093c8038d6d338487afd3e64559730ee 100644 (file)
@@ -2884,6 +2884,7 @@ code.open_component_page=Open Component's Page
 component_measures.all_measures=All Measures
 component_measures.domain_measures={0} Measures
 component_measures.back_to_list=Back to List
+component_measures.files=files
 component_measures.show_metric_history=Show history of this metric
 component_measures.tab.tree=Tree
 component_measures.tab.list=List