]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9614 Add buttons to navigate through open files on measures page
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 9 Aug 2017 07:17:18 +0000 (09:17 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 14 Aug 2017 09:44:44 +0000 (11:44 +0200)
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap
server/sonar-web/src/main/js/apps/component-measures/style.css

index 66e9fe4775631f564319cff3f147018f8c38185d..8cde6abfd8e0b667259311a6b76db9a531b48299 100644 (file)
@@ -89,9 +89,10 @@ export default class MeasureContent extends React.PureComponent {
   }
 
   getSelectedIndex = (): ?number => {
-    const index = this.state.components.findIndex(
-      component => component.key === this.state.selected
-    );
+    const componentKey = isFileType(this.props.component)
+      ? this.props.component.key
+      : this.state.selected;
+    const index = this.state.components.findIndex(component => component.key === componentKey);
     return index !== -1 ? index : null;
   };
 
@@ -121,7 +122,7 @@ export default class MeasureContent extends React.PureComponent {
 
   fetchComponents = ({ component, metric, metrics, view }: Props) => {
     if (isFileType(component)) {
-      return this.setState({ components: [], metric: null, paging: null, view: null });
+      return this.setState({ metric: null, view: null });
     }
 
     const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric);
@@ -244,6 +245,7 @@ export default class MeasureContent extends React.PureComponent {
     const { component, currentUser, measure, metric, rootComponent, view } = this.props;
     const isLoggedIn = currentUser && currentUser.isLoggedIn;
     const isFile = isFileType(component);
+    const selectedIdx = this.getSelectedIndex();
     return (
       <div className={this.props.className}>
         <div className="layout-page-header-panel layout-page-main-header issues-main-header">
@@ -269,7 +271,7 @@ export default class MeasureContent extends React.PureComponent {
                   view={view}
                 />}
               <PageActions
-                current={this.getSelectedIndex() + 1}
+                current={selectedIdx + 1}
                 loading={this.props.loading}
                 isFile={isFile}
                 paging={this.state.paging}
@@ -284,9 +286,12 @@ export default class MeasureContent extends React.PureComponent {
           <div className="layout-page-main-inner">
             <MeasureHeader
               component={component}
+              components={this.state.components}
+              handleSelect={this.props.updateSelected}
               leakPeriod={this.props.leakPeriod}
               measure={measure}
               secondaryMeasure={this.props.secondaryMeasure}
+              selectedIdx={selectedIdx}
             />
             {this.renderContent()}
           </div>}
index b08974be9b6d2494f8315173f862ad97e35db429..5c3a3a1828267e45e55a55a33b3b9f770541e62a 100644 (file)
@@ -17,6 +17,7 @@
  * 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 { Link } from 'react-router';
 import ComplexityDistribution from '../../../components/shared/ComplexityDistribution';
@@ -26,7 +27,8 @@ import LanguageDistribution from '../../../components/charts/LanguageDistributio
 import LeakPeriodLegend from './LeakPeriodLegend';
 import Measure from '../../../components/measure/Measure';
 import Tooltip from '../../../components/controls/Tooltip';
-import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { isFileType } from '../utils';
+import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
 import { getComponentMeasureHistory } from '../../../helpers/urls';
 import { isDiffMetric } from '../../../helpers/measures';
 import type { Component, Period } from '../types';
@@ -34,57 +36,119 @@ import type { MeasureEnhanced } from '../../../components/measure/types';
 
 type Props = {|
   component: Component,
+  components: Array<Component>,
   leakPeriod?: Period,
+  handleSelect: string => void,
   measure: MeasureEnhanced,
-  secondaryMeasure: ?MeasureEnhanced
+  secondaryMeasure: ?MeasureEnhanced,
+  selectedIdx: ?number
 |};
 
-export default function MeasureHeader({ component, leakPeriod, measure, secondaryMeasure }: Props) {
-  const metric = measure.metric;
-  const isDiff = isDiffMetric(metric.key);
-  const hasHistory = ['TRK', 'VW', 'SVW', 'APP'].includes(component.qualifier);
-  return (
-    <div className="measure-details-header big-spacer-bottom">
-      <div className="measure-details-primary">
-        <div className="measure-details-metric">
-          <IssueTypeIcon query={metric.key} className="little-spacer-right text-text-bottom" />
-          {getLocalizedMetricName(metric)}
-          <span className="measure-details-value spacer-left">
-            <strong>
-              {isDiff
-                ? <Measure className="domain-measures-leak" measure={measure} metric={metric} />
-                : <Measure measure={measure} metric={metric} />}
-            </strong>
-          </span>
-          {!isDiff &&
-            hasHistory &&
-            <Tooltip
-              placement="right"
-              overlay={translate('component_measures.show_metric_history')}>
-              <Link
-                className="js-show-history spacer-left button button-small button-compact"
-                to={getComponentMeasureHistory(component.key, metric.key)}>
-                <HistoryIcon />
-              </Link>
-            </Tooltip>}
+export default class MeasureHeader extends React.PureComponent {
+  props: Props;
+
+  handleSelectPrevious = (e: Event & { target: HTMLElement }) => {
+    e.target.blur();
+    if (this.props.selectedIdx != null) {
+      const prevComponent = this.props.components[this.props.selectedIdx - 1];
+      if (prevComponent) {
+        this.props.handleSelect(prevComponent.key);
+      }
+    }
+  };
+
+  handleSelectNext = (e: Event & { target: HTMLElement }) => {
+    e.target.blur();
+    if (this.props.selectedIdx != null) {
+      const prevComponent = this.props.components[this.props.selectedIdx + 1];
+      if (prevComponent) {
+        this.props.handleSelect(prevComponent.key);
+      }
+    }
+  };
+
+  renderFileNav() {
+    const { components, selectedIdx } = this.props;
+    if (selectedIdx == null) {
+      return null;
+    }
+    const hasPrevious = selectedIdx > 0;
+    const hasNext = selectedIdx < components.length - 1;
+    return (
+      <div className="display-inline-block">
+        {components.length > 0 &&
+          <span className="note spacer-right">
+            {translateWithParameters(
+              'component_measures.x_of_y',
+              selectedIdx + 1,
+              components.length
+            )}
+          </span>}
+        <div className="button-group">
+          {hasPrevious && <button onClick={this.handleSelectPrevious}>&lt;</button>}
+          {hasNext && <button onClick={this.handleSelectNext}>&gt;</button>}
+        </div>
+      </div>
+    );
+  }
+
+  render() {
+    const { component, components, leakPeriod, measure, secondaryMeasure } = this.props;
+    const metric = measure.metric;
+    const isDiff = isDiffMetric(metric.key);
+    const hasHistory = ['TRK', 'VW', 'SVW', 'APP'].includes(component.qualifier);
+    const hasComponents = components && components.length > 1;
+    return (
+      <div className="measure-details-header big-spacer-bottom">
+        <div className="measure-details-primary">
+          <div className="measure-details-metric">
+            <IssueTypeIcon query={metric.key} className="little-spacer-right text-text-bottom" />
+            {getLocalizedMetricName(metric)}
+            <span className="measure-details-value spacer-left">
+              <strong>
+                {isDiff
+                  ? <Measure className="domain-measures-leak" measure={measure} metric={metric} />
+                  : <Measure measure={measure} metric={metric} />}
+              </strong>
+            </span>
+            {!isDiff &&
+              hasHistory &&
+              <Tooltip
+                placement="right"
+                overlay={translate('component_measures.show_metric_history')}>
+                <Link
+                  className="js-show-history spacer-left button button-small button-compact"
+                  to={getComponentMeasureHistory(component.key, metric.key)}>
+                  <HistoryIcon />
+                </Link>
+              </Tooltip>}
+          </div>
+          <div className="measure-details-primary-actions">
+            {hasComponents && isFileType(component) && this.renderFileNav()}
+            {leakPeriod != null &&
+              <LeakPeriodLegend
+                className="spacer-left"
+                component={component}
+                period={leakPeriod}
+              />}
+          </div>
         </div>
-        {leakPeriod != null && <LeakPeriodLegend component={component} period={leakPeriod} />}
+        {secondaryMeasure &&
+          secondaryMeasure.metric.key === 'ncloc_language_distribution' &&
+          <div className="measure-details-secondary">
+            <LanguageDistribution alignTicks={true} distribution={secondaryMeasure.value} />
+          </div>}
+        {secondaryMeasure &&
+          secondaryMeasure.metric.key === 'function_complexity_distribution' &&
+          <div className="measure-details-secondary">
+            <ComplexityDistribution distribution={secondaryMeasure.value} of="function" />
+          </div>}
+        {secondaryMeasure &&
+          secondaryMeasure.metric.key === 'file_complexity_distribution' &&
+          <div className="measure-details-secondary">
+            <ComplexityDistribution distribution={secondaryMeasure.value} of="file" />
+          </div>}
       </div>
-      {secondaryMeasure &&
-        secondaryMeasure.metric.key === 'ncloc_language_distribution' &&
-        <div className="measure-details-secondary">
-          <LanguageDistribution alignTicks={true} distribution={secondaryMeasure.value} />
-        </div>}
-      {secondaryMeasure &&
-        secondaryMeasure.metric.key === 'function_complexity_distribution' &&
-        <div className="measure-details-secondary">
-          <ComplexityDistribution distribution={secondaryMeasure.value} of="function" />
-        </div>}
-      {secondaryMeasure &&
-        secondaryMeasure.metric.key === 'file_complexity_distribution' &&
-        <div className="measure-details-secondary">
-          <ComplexityDistribution distribution={secondaryMeasure.value} of="file" />
-        </div>}
-    </div>
-  );
+    );
+  }
 }
index 34ea5cd64d1b244351910d18ecb6db80a07573e6..46734ad83cadbab683a95da3d32aeab6bbc5c8c3 100644 (file)
@@ -54,6 +54,8 @@ const SECONDARY = {
 
 const PROPS = {
   component: { key: 'foo', qualifier: 'TRK' },
+  components: [],
+  handleSelect: () => {},
   leakPeriod: {
     date: '2017-05-16T13:50:02+0200',
     index: 1,
@@ -61,7 +63,9 @@ const PROPS = {
     parameter: '6,4'
   },
   measure: MEASURE,
-  secondaryMeasure: null
+  paging: null,
+  secondaryMeasure: null,
+  selectedIdx: null
 };
 
 it('should render correctly', () => {
@@ -76,3 +80,17 @@ it('should display secondary measure too', () => {
   const wrapper = shallow(<MeasureHeader {...PROPS} secondaryMeasure={SECONDARY} />);
   expect(wrapper.find('LanguageDistribution')).toHaveLength(1);
 });
+
+it('shohuld display correctly for open file', () => {
+  const wrapper = shallow(
+    <MeasureHeader
+      {...PROPS}
+      component={{ key: 'bar', qualifier: 'FIL' }}
+      components={[{ key: 'foo' }, { key: 'bar' }, { key: 'baz' }]}
+      selectedIdx={1}
+    />
+  );
+  expect(wrapper.find('.measure-details-primary-actions')).toMatchSnapshot();
+  wrapper.setProps({ components: [{ key: 'foo' }, { key: 'bar' }] });
+  expect(wrapper.find('.measure-details-primary-actions')).toMatchSnapshot();
+});
index 775d3af72a4c01de7bed579392920ead338c12eb..fae4fa473aac454a816bca37dd623037b9c053c6 100644 (file)
@@ -1,5 +1,94 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`shohuld display correctly for open file 1`] = `
+<div
+  className="measure-details-primary-actions"
+>
+  <div
+    className="display-inline-block"
+  >
+    <span
+      className="note spacer-right"
+    >
+      component_measures.x_of_y.2.3
+    </span>
+    <div
+      className="button-group"
+    >
+      <button
+        onClick={[Function]}
+      >
+        &lt;
+      </button>
+      <button
+        onClick={[Function]}
+      >
+        &gt;
+      </button>
+    </div>
+  </div>
+  <LeakPeriodLegend
+    className="spacer-left"
+    component={
+      Object {
+        "key": "bar",
+        "qualifier": "FIL",
+      }
+    }
+    period={
+      Object {
+        "date": "2017-05-16T13:50:02+0200",
+        "index": 1,
+        "mode": "previous_version",
+        "parameter": "6,4",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`shohuld display correctly for open file 2`] = `
+<div
+  className="measure-details-primary-actions"
+>
+  <div
+    className="display-inline-block"
+  >
+    <span
+      className="note spacer-right"
+    >
+      component_measures.x_of_y.2.2
+    </span>
+    <div
+      className="button-group"
+    >
+      <button
+        onClick={[Function]}
+      >
+        &lt;
+      </button>
+    </div>
+  </div>
+  <LeakPeriodLegend
+    className="spacer-left"
+    component={
+      Object {
+        "key": "bar",
+        "qualifier": "FIL",
+      }
+    }
+    period={
+      Object {
+        "date": "2017-05-16T13:50:02+0200",
+        "index": 1,
+        "mode": "previous_version",
+        "parameter": "6,4",
+      }
+    }
+  />
+</div>
+`;
+
 exports[`should render correctly 1`] = `
 <div
   className="measure-details-header big-spacer-bottom"
@@ -70,22 +159,27 @@ exports[`should render correctly 1`] = `
         </Link>
       </Tooltip>
     </div>
-    <LeakPeriodLegend
-      component={
-        Object {
-          "key": "foo",
-          "qualifier": "TRK",
+    <div
+      className="measure-details-primary-actions"
+    >
+      <LeakPeriodLegend
+        className="spacer-left"
+        component={
+          Object {
+            "key": "foo",
+            "qualifier": "TRK",
+          }
         }
-      }
-      period={
-        Object {
-          "date": "2017-05-16T13:50:02+0200",
-          "index": 1,
-          "mode": "previous_version",
-          "parameter": "6,4",
+        period={
+          Object {
+            "date": "2017-05-16T13:50:02+0200",
+            "index": 1,
+            "mode": "previous_version",
+            "parameter": "6,4",
+          }
         }
-      }
-    />
+      />
+    </div>
   </div>
 </div>
 `;
@@ -138,22 +232,27 @@ exports[`should render correctly for leak 1`] = `
         </strong>
       </span>
     </div>
-    <LeakPeriodLegend
-      component={
-        Object {
-          "key": "foo",
-          "qualifier": "TRK",
+    <div
+      className="measure-details-primary-actions"
+    >
+      <LeakPeriodLegend
+        className="spacer-left"
+        component={
+          Object {
+            "key": "foo",
+            "qualifier": "TRK",
+          }
         }
-      }
-      period={
-        Object {
-          "date": "2017-05-16T13:50:02+0200",
-          "index": 1,
-          "mode": "previous_version",
-          "parameter": "6,4",
+        period={
+          Object {
+            "date": "2017-05-16T13:50:02+0200",
+            "index": 1,
+            "mode": "previous_version",
+            "parameter": "6,4",
+          }
         }
-      }
-    />
+      />
+    </div>
   </div>
 </div>
 `;
index 2dba101338e407d1e23b5be1b3ad5d4737a9054a..699bd21b96eb0d5fc6d6ada94e2014aca3c7276c 100644 (file)
@@ -19,6 +19,7 @@
 }
 
 .domain-measures-leak-header {
+  display: inline-block;
   background-color: #fbf3d5;
   border: 1px solid #eae3c7;
   padding: 4px 10px;
   align-items: center;
 }
 
+.measure-details-primary-actions {
+  display: flex;
+  align-items: center;
+}
+
 .measure-details-secondary {
   display: inline-block;
   width: 260px;