]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9472 Change the rendering of best values on the Measures page
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Fri, 20 Apr 2018 05:38:14 +0000 (07:38 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 24 Apr 2018 18:20:46 +0000 (20:20 +0200)
12 files changed:
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java
server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchActionTest.java
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js
server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js
server/sonar-web/src/main/js/helpers/measures.ts
server/sonar-web/src/main/js/store/metrics/actions.js
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-ws/src/main/protobuf/ws-measures.proto

index 5ba9c3f92281ebfa11a0d8db77a80151bbb8bbe5..3adddc099656e1c745783e2077d390ebd119669c 100644 (file)
@@ -185,6 +185,7 @@ public class ComponentTreeAction implements MeasuresWsAction {
       .setHandler(this)
       .addPagingParams(100, MAX_SIZE)
       .setChangelog(
+        new Change("7.2", "field 'bestValue' is added to the response"),
         new Change("6.3", format("Number of metric keys is limited to %s", MAX_METRIC_KEYS)),
         new Change("6.6", "the response field id is deprecated. Use key instead."),
         new Change("6.6", "the response field refId is deprecated. Use refKey instead."));
@@ -382,7 +383,7 @@ public class ComponentTreeAction implements MeasuresWsAction {
   }
 
   private static Measures.Component.Builder toWsComponent(ComponentDto component, Map<MetricDto, ComponentTreeData.Measure> measures,
-                                                          Map<String, ComponentDto> referenceComponentsByUuid) {
+    Map<String, ComponentDto> referenceComponentsByUuid) {
     Measures.Component.Builder wsComponent = componentDtoToWsComponent(component);
     ComponentDto referenceComponent = referenceComponentsByUuid.get(component.getCopyResourceUuid());
     if (referenceComponent != null) {
@@ -406,16 +407,16 @@ public class ComponentTreeAction implements MeasuresWsAction {
       Optional<SnapshotDto> baseSnapshot = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, baseComponent.projectUuid());
       if (!baseSnapshot.isPresent()) {
         return ComponentTreeData.builder()
-                .setBaseComponent(baseComponent)
-                .build();
+          .setBaseComponent(baseComponent)
+          .build();
       }
 
       ComponentTreeQuery componentTreeQuery = toComponentTreeQuery(wsRequest, baseComponent);
       List<ComponentDto> components = searchComponents(dbSession, componentTreeQuery);
       List<MetricDto> metrics = searchMetrics(dbSession, wsRequest);
       Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric = searchMeasuresByComponentUuidAndMetric(dbSession, baseComponent, componentTreeQuery,
-              components,
-              metrics);
+        components,
+        metrics);
 
       components = filterComponents(components, measuresByComponentUuidAndMetric, metrics, wsRequest);
       components = sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric);
@@ -424,14 +425,14 @@ public class ComponentTreeAction implements MeasuresWsAction {
       components = paginateComponents(components, wsRequest);
 
       return ComponentTreeData.builder()
-              .setBaseComponent(baseComponent)
-              .setComponentsFromDb(components)
-              .setComponentCount(componentCount)
-              .setMeasuresByComponentUuidAndMetric(measuresByComponentUuidAndMetric)
-              .setMetrics(metrics)
-              .setPeriods(snapshotToWsPeriods(baseSnapshot.get()))
-              .setReferenceComponentsByUuid(searchReferenceComponentsById(dbSession, components))
-              .build();
+        .setBaseComponent(baseComponent)
+        .setComponentsFromDb(components)
+        .setComponentCount(componentCount)
+        .setMeasuresByComponentUuidAndMetric(measuresByComponentUuidAndMetric)
+        .setMetrics(metrics)
+        .setPeriods(snapshotToWsPeriods(baseSnapshot.get()))
+        .setReferenceComponentsByUuid(searchReferenceComponentsById(dbSession, components))
+        .build();
     }
   }
 
@@ -451,15 +452,15 @@ public class ComponentTreeAction implements MeasuresWsAction {
 
   private Map<String, ComponentDto> searchReferenceComponentsById(DbSession dbSession, List<ComponentDto> components) {
     List<String> referenceComponentUUids = components.stream()
-            .map(ComponentDto::getCopyResourceUuid)
-            .filter(Objects::nonNull)
-            .collect(MoreCollectors.toList(components.size()));
+      .map(ComponentDto::getCopyResourceUuid)
+      .filter(Objects::nonNull)
+      .collect(MoreCollectors.toList(components.size()));
     if (referenceComponentUUids.isEmpty()) {
       return emptyMap();
     }
 
     return FluentIterable.from(dbClient.componentDao().selectByUuids(dbSession, referenceComponentUUids))
-            .uniqueIndex(ComponentDto::uuid);
+      .uniqueIndex(ComponentDto::uuid);
   }
 
   private List<ComponentDto> searchComponents(DbSession dbSession, ComponentTreeQuery componentTreeQuery) {
@@ -476,16 +477,16 @@ public class ComponentTreeAction implements MeasuresWsAction {
     if (metrics.size() < metricKeys.size()) {
       List<String> foundMetricKeys = Lists.transform(metrics, MetricDto::getKey);
       Set<String> missingMetricKeys = Sets.difference(
-              new LinkedHashSet<>(metricKeys),
-              new LinkedHashSet<>(foundMetricKeys));
+        new LinkedHashSet<>(metricKeys),
+        new LinkedHashSet<>(foundMetricKeys));
 
       throw new NotFoundException(format("The following metric keys are not found: %s", COMMA_JOINER.join(missingMetricKeys)));
     }
     String forbiddenMetrics = metrics.stream()
-            .filter(metric -> ComponentTreeAction.FORBIDDEN_METRIC_TYPES.contains(metric.getValueType()))
-            .map(MetricDto::getKey)
-            .sorted()
-            .collect(MoreCollectors.join(COMMA_JOINER));
+      .filter(metric -> ComponentTreeAction.FORBIDDEN_METRIC_TYPES.contains(metric.getValueType()))
+      .map(MetricDto::getKey)
+      .sorted()
+      .collect(MoreCollectors.join(COMMA_JOINER));
     checkArgument(forbiddenMetrics.isEmpty(), "Metrics %s can't be requested in this web service. Please use api/measures/component", forbiddenMetrics);
     return metrics;
   }
@@ -495,19 +496,19 @@ public class ComponentTreeAction implements MeasuresWsAction {
 
     Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDto::getId);
     MeasureTreeQuery measureQuery = MeasureTreeQuery.builder()
-            .setStrategy(MeasureTreeQuery.Strategy.valueOf(componentTreeQuery.getStrategy().name()))
-            .setNameOrKeyQuery(componentTreeQuery.getNameOrKeyQuery())
-            .setQualifiers(componentTreeQuery.getQualifiers())
-            .setMetricIds(new ArrayList<>(metricsById.keySet()))
-            .build();
+      .setStrategy(MeasureTreeQuery.Strategy.valueOf(componentTreeQuery.getStrategy().name()))
+      .setNameOrKeyQuery(componentTreeQuery.getNameOrKeyQuery())
+      .setQualifiers(componentTreeQuery.getQualifiers())
+      .setMetricIds(new ArrayList<>(metricsById.keySet()))
+      .build();
 
     Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric = HashBasedTable.create(components.size(), metrics.size());
     dbClient.liveMeasureDao().selectTreeByQuery(dbSession, baseComponent, measureQuery, result -> {
       LiveMeasureDto measureDto = result.getResultObject();
       measuresByComponentUuidAndMetric.put(
-              measureDto.getComponentUuid(),
-              metricsById.get(measureDto.getMetricId()),
-              ComponentTreeData.Measure.createFromMeasureDto(measureDto));
+        measureDto.getComponentUuid(),
+        metricsById.get(measureDto.getMetricId()),
+        ComponentTreeData.Measure.createFromMeasureDto(measureDto));
     });
 
     addBestValuesToMeasures(measuresByComponentUuidAndMetric, components, metrics);
@@ -523,11 +524,11 @@ public class ComponentTreeAction implements MeasuresWsAction {
    * </ul>
    */
   private static void addBestValuesToMeasures(Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric, List<ComponentDto> components,
-                                              List<MetricDto> metrics) {
+    List<MetricDto> metrics) {
     List<MetricDtoWithBestValue> metricDtosWithBestValueMeasure = metrics.stream()
-            .filter(MetricDtoFunctions.isOptimizedForBestValue())
-            .map(new MetricDtoToMetricDtoWithBestValue())
-            .collect(MoreCollectors.toList(metrics.size()));
+      .filter(MetricDtoFunctions.isOptimizedForBestValue())
+      .map(new MetricDtoToMetricDtoWithBestValue())
+      .collect(MoreCollectors.toList(metrics.size()));
     if (metricDtosWithBestValueMeasure.isEmpty()) {
       return;
     }
@@ -537,7 +538,7 @@ public class ComponentTreeAction implements MeasuresWsAction {
       for (MetricDtoWithBestValue metricWithBestValue : metricDtosWithBestValueMeasure) {
         if (measuresByComponentUuidAndMetric.get(component.uuid(), metricWithBestValue.getMetric()) == null) {
           measuresByComponentUuidAndMetric.put(component.uuid(), metricWithBestValue.getMetric(),
-                  ComponentTreeData.Measure.createFromMeasureDto(metricWithBestValue.getBestValue()));
+            ComponentTreeData.Measure.createFromMeasureDto(metricWithBestValue.getBestValue()));
         }
       }
     });
@@ -554,9 +555,9 @@ public class ComponentTreeAction implements MeasuresWsAction {
     checkState(metricToSort.isPresent(), "Metric '%s' not found", metricKeyToSort, wsRequest.getMetricKeys());
 
     return components
-            .stream()
-            .filter(new HasMeasure(measuresByComponentUuidAndMetric, metricToSort.get(), wsRequest.getMetricPeriodSort()))
-            .collect(MoreCollectors.toList(components.size()));
+      .stream()
+      .filter(new HasMeasure(measuresByComponentUuidAndMetric, metricToSort.get(), wsRequest.getMetricPeriodSort()))
+      .collect(MoreCollectors.toList(components.size()));
   }
 
   private static boolean componentWithMeasuresOnly(ComponentTreeRequest wsRequest) {
@@ -564,15 +565,15 @@ public class ComponentTreeAction implements MeasuresWsAction {
   }
 
   private static List<ComponentDto> sortComponents(List<ComponentDto> components, ComponentTreeRequest wsRequest, List<MetricDto> metrics,
-                                                   Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
+    Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
     return ComponentTreeSort.sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric);
   }
 
   private static List<ComponentDto> paginateComponents(List<ComponentDto> components, ComponentTreeRequest wsRequest) {
     return components.stream()
-            .skip(offset(wsRequest.getPage(), wsRequest.getPageSize()))
-            .limit(wsRequest.getPageSize())
-            .collect(MoreCollectors.toList(wsRequest.getPageSize()));
+      .skip(offset(wsRequest.getPage(), wsRequest.getPageSize()))
+      .limit(wsRequest.getPageSize())
+      .collect(MoreCollectors.toList(wsRequest.getPageSize()));
   }
 
   @CheckForNull
@@ -600,8 +601,8 @@ public class ComponentTreeAction implements MeasuresWsAction {
     List<String> childrenQualifiers = childrenQualifiers(wsRequest, baseComponent.qualifier());
 
     ComponentTreeQuery.Builder componentTreeQueryBuilder = ComponentTreeQuery.builder()
-            .setBaseUuid(baseComponent.uuid())
-            .setStrategy(STRATEGIES.get(wsRequest.getStrategy()));
+      .setBaseUuid(baseComponent.uuid())
+      .setStrategy(STRATEGIES.get(wsRequest.getStrategy()));
 
     if (wsRequest.getQuery() != null) {
       componentTreeQueryBuilder.setNameOrKeyQuery(wsRequest.getQuery());
index b7b768040c1c491baa409c7bdfa8ccf94ccc40a1..6070c001b95beef81f3cd8a816ebefeadae838ac 100644 (file)
 package org.sonar.server.measure.ws;
 
 import javax.annotation.Nullable;
+import org.sonar.core.util.Protobuf;
 import org.sonar.db.measure.LiveMeasureDto;
 import org.sonar.db.measure.MeasureDto;
 import org.sonar.db.metric.MetricDto;
 import org.sonarqube.ws.Measures;
 import org.sonarqube.ws.Measures.Measure;
 
+import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.server.measure.ws.MeasureValueFormatter.formatMeasureValue;
 import static org.sonar.server.measure.ws.MeasureValueFormatter.formatNumericalValue;
 
@@ -49,18 +51,22 @@ class MeasureDtoToWsMeasure {
 
   static void updateMeasureBuilder(Measure.Builder measureBuilder, MetricDto metric, double doubleValue, @Nullable String stringValue, double variation) {
     measureBuilder.setMetric(metric.getKey());
+    Double bestValue = metric.getBestValue();
     // a measure value can be null, new_violations metric for example
     if (!Double.isNaN(doubleValue) || stringValue != null) {
       measureBuilder.setValue(formatMeasureValue(doubleValue, stringValue, metric));
+      setNullable(bestValue, v -> measureBuilder.setBestValue(doubleValue == v));
     }
 
     Measures.PeriodValue.Builder periodBuilder = Measures.PeriodValue.newBuilder();
     if (Double.isNaN(variation)) {
       return;
     }
-    measureBuilder.getPeriodsBuilder().addPeriodsValue(periodBuilder
+    Measures.PeriodValue.Builder builderForValue = periodBuilder
       .clear()
       .setIndex(1)
-      .setValue(formatNumericalValue(variation, metric)));
+      .setValue(formatNumericalValue(variation, metric));
+    setNullable(bestValue, v -> builderForValue.setBestValue(variation == v));
+    measureBuilder.getPeriodsBuilder().addPeriodsValue(builderForValue);
   }
 }
index b900442a8ec663b7450b5074f348a239a44321de..057002c7ccd5bb5f1fdd2f888b6227cdbbf1167f 100644 (file)
@@ -263,8 +263,8 @@ public class ComponentActionTest {
       .executeProtobuf(ComponentWsResponse.class);
 
     assertThat(response.getComponent().getMeasuresList())
-      .extracting(Measures.Measure::getMetric, Measures.Measure::getValue)
-      .containsExactly(tuple(metric.getKey(), "7"));
+      .extracting(Measures.Measure::getMetric, Measures.Measure::getValue, Measures.Measure::getBestValue)
+      .containsExactly(tuple(metric.getKey(), "7", true));
   }
 
   @Test
index cc42a73db20d5a7470e01ca8f91e87422eaa0328..424c2607084fbada77203340846fea8c8563ca50 100644 (file)
@@ -51,9 +51,11 @@ import org.sonar.server.ws.WsActionTester;
 import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Measures;
 import org.sonarqube.ws.Measures.ComponentTreeWsResponse;
+import org.sonarqube.ws.Measures.PeriodValue;
 
 import static java.lang.Double.parseDouble;
 import static java.lang.String.format;
+import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.tuple;
 import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING_KEY;
@@ -251,14 +253,56 @@ public class ComponentTreeActionTest {
     assertThat(response.getComponentsList().get(0).getMeasuresList()).extracting("metric").containsOnly("coverage");
     // file measures
     List<Measures.Measure> fileMeasures = response.getComponentsList().get(1).getMeasuresList();
-    assertThat(fileMeasures).extracting("metric").containsOnly("ncloc", "coverage", "new_violations");
-    assertThat(fileMeasures).extracting("value").containsOnly("100", "15.5", "");
+    assertThat(fileMeasures)
+      .extracting(Measures.Measure::getMetric, Measures.Measure::getValue, Measures.Measure::getBestValue, Measures.Measure::hasBestValue)
+      .containsExactlyInAnyOrder(tuple("ncloc", "100", true, true),
+        tuple("coverage", "15.5", false, false),
+        tuple("new_violations", "", false, false));
 
     List<Common.Metric> metrics = response.getMetrics().getMetricsList();
     assertThat(metrics).extracting("bestValue").contains("100", "");
     assertThat(metrics).extracting("worstValue").contains("1000");
   }
 
+  @Test
+  public void return_is_best_value_on_leak_measures() {
+    ComponentDto project = db.components().insertPrivateProject();
+    db.components().insertSnapshot(project);
+    userSession.anonymous().addProjectPermission(UserRole.USER, project);
+    ComponentDto file = newFileDto(project, null);
+    db.components().insertComponent(file);
+
+    MetricDto matchingBestValue = db.measures().insertMetric(m -> m
+      .setKey("new_lines")
+      .setValueType(INT.name())
+      .setBestValue(100d));
+    MetricDto doesNotMatchBestValue = db.measures().insertMetric(m -> m
+      .setKey("new_lines_2")
+      .setValueType(INT.name())
+      .setBestValue(100d));
+    MetricDto noBestValue = db.measures().insertMetric(m -> m
+      .setKey("new_violations")
+      .setValueType(INT.name())
+      .setBestValue(null));
+    db.measures().insertLiveMeasure(file, matchingBestValue, m -> m.setValue(null).setData((String) null).setVariation(100d));
+    db.measures().insertLiveMeasure(file, doesNotMatchBestValue, m -> m.setValue(null).setData((String) null).setVariation(10d));
+    db.measures().insertLiveMeasure(file, noBestValue, m -> m.setValue(null).setData((String) null).setVariation(42.0d));
+
+    ComponentTreeWsResponse response = ws.newRequest()
+      .setParam(PARAM_COMPONENT, project.getKey())
+      .setParam(PARAM_METRIC_KEYS, "new_lines,new_lines_2,new_violations")
+      .executeProtobuf(ComponentTreeWsResponse.class);
+
+    // file measures
+    List<Measures.Measure> fileMeasures = response.getComponentsList().get(0).getMeasuresList();
+    assertThat(fileMeasures)
+      .extracting(Measures.Measure::getMetric, m -> m.getPeriods().getPeriodsValueList())
+      .containsExactlyInAnyOrder(
+        tuple(matchingBestValue.getKey(), singletonList(PeriodValue.newBuilder().setIndex(1).setValue("100").setBestValue(true).build())),
+        tuple(doesNotMatchBestValue.getKey(), singletonList(PeriodValue.newBuilder().setIndex(1).setValue("10").setBestValue(false).build())),
+        tuple(noBestValue.getKey(), singletonList(PeriodValue.newBuilder().setIndex(1).setValue("42").build())));
+  }
+
   @Test
   public void use_best_value_for_rating() {
     ComponentDto project = db.components().insertPrivateProject();
index 46f0cdd01ecb53fd85e1e6efbee4d8fcbe01df7b..b39cf629302588b1c81fea5877a208733fbab411 100644 (file)
@@ -44,6 +44,7 @@ import org.sonarqube.ws.Measures.Measure;
 import org.sonarqube.ws.Measures.SearchWsResponse;
 
 import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Lists.transform;
 import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
@@ -126,6 +127,29 @@ public class SearchActionTest {
     assertThat(measure.getValue()).isEqualTo("15.5");
   }
 
+  @Test
+  public void return_best_value() {
+    ComponentDto project = db.components().insertPrivateProject(db.getDefaultOrganization());
+    userSession.addProjectPermission(UserRole.USER, project);
+    MetricDto matchBestValue = db.measures().insertMetric(m -> m.setValueType(FLOAT.name()).setBestValue(15.5d));
+    db.measures().insertLiveMeasure(project, matchBestValue, m -> m.setValue(15.5d));
+    MetricDto doesNotMatchBestValue = db.measures().insertMetric(m -> m.setValueType(INT.name()).setBestValue(50d));
+    db.measures().insertLiveMeasure(project, doesNotMatchBestValue, m -> m.setValue(40d));
+    MetricDto noBestValue = db.measures().insertMetric(m -> m.setValueType(INT.name()).setBestValue(null));
+    db.measures().insertLiveMeasure(project, noBestValue, m -> m.setValue(123d));
+
+    SearchWsResponse result = call(singletonList(project.getDbKey()),
+      asList(matchBestValue.getKey(), doesNotMatchBestValue.getKey(), noBestValue.getKey()));
+
+    List<Measure> measures = result.getMeasuresList();
+    assertThat(measures)
+      .extracting(Measure::getMetric, Measure::getValue, Measure::getBestValue, Measure::hasBestValue)
+      .containsExactlyInAnyOrder(
+        tuple(matchBestValue.getKey(), "15.5", true, true),
+        tuple(doesNotMatchBestValue.getKey(), "40", false, true),
+        tuple(noBestValue.getKey(), "123", false, false));
+  }
+
   @Test
   public void return_measures_on_leak_period() {
     OrganizationDto organization = db.organizations().insert();
@@ -189,7 +213,6 @@ public class SearchActionTest {
     assertThat(measure.getValue()).isEqualTo("15.5");
   }
 
-
   @Test
   public void return_measures_on_application() {
     OrganizationDto organization = db.organizations().insert();
@@ -232,7 +255,7 @@ public class SearchActionTest {
     ComponentDto project2 = db.components().insertPrivateProject(db.getDefaultOrganization());
     db.measures().insertLiveMeasure(project1, metric, m -> m.setValue(15.5d));
     db.measures().insertLiveMeasure(project2, metric, m -> m.setValue(42.0d));
-    Arrays.stream(new ComponentDto[]{project1}).forEach(p -> userSession.addProjectPermission(UserRole.USER, p));
+    Arrays.stream(new ComponentDto[] {project1}).forEach(p -> userSession.addProjectPermission(UserRole.USER, p));
 
     SearchWsResponse result = call(asList(project1.getDbKey(), project2.getDbKey()), singletonList(metric.getKey()));
 
index a4847ed1848d42a47ec301ce3b797d84ccc70e94..a7cd534be4cb24eeec90e9acfa1f0c76db3fbe00 100644 (file)
@@ -62,6 +62,7 @@ import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'
 |}; */
 
 /*:: type State = {
+  bestValue?: string,
   components: Array<ComponentEnhanced>,
   metric: ?Metric,
   paging?: Paging,
@@ -117,6 +118,7 @@ export default class MeasureContent extends React.PureComponent {
     const metricKeys = [metric.key];
     const opts /*: Object */ = {
       ...getBranchLikeQuery(this.props.branchLike),
+      additionalFields: 'metrics',
       metricSortFilter: 'withMeasuresOnly'
     };
     const isDiff = isDiffMetric(metric.key);
@@ -151,6 +153,7 @@ export default class MeasureContent extends React.PureComponent {
         if (metric === this.props.metric) {
           if (this.mounted) {
             this.setState(({ selected } /*: State */) => ({
+              bestValue: r.metrics[0].bestValue,
               components: r.components.map(component =>
                 enhanceComponent(component, metric, metrics)
               ),
@@ -185,6 +188,7 @@ export default class MeasureContent extends React.PureComponent {
         if (metric === this.props.metric) {
           if (this.mounted) {
             this.setState(state => ({
+              bestValue: r.metrics[0].bestValue,
               components: [
                 ...state.components,
                 ...r.components.map(component => enhanceComponent(component, metric, metrics))
@@ -245,6 +249,7 @@ export default class MeasureContent extends React.PureComponent {
         const selectedIdx = this.getSelectedIndex();
         return (
           <FilesView
+            bestValue={this.state.bestValue}
             branchLike={this.props.branchLike}
             components={this.state.components}
             fetchMore={this.fetchMoreComponents}
index cfc0aa28aa253fc2c0254588e7730a7d82f2660c..fbd26322b636fa2f5ca0e409845663361f776dab 100644 (file)
@@ -22,11 +22,13 @@ import React from 'react';
 import ComponentsListRow from './ComponentsListRow';
 import EmptyResult from './EmptyResult';
 import { complementary } from '../config/complementary';
-import { getLocalizedMetricName } from '../../../helpers/l10n';
+import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric, isPeriodBestValue } from '../../../helpers/measures';
 /*:: import type { Component, ComponentEnhanced } from '../types'; */
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 
 /*:: type Props = {|
+  bestValue?: string,
   branchLike?: { id?: string; name: string },
   components: Array<ComponentEnhanced>,
   onClick: string => void,
@@ -36,54 +38,108 @@ import { getLocalizedMetricName } from '../../../helpers/l10n';
   selectedComponent?: ?string
 |}; */
 
-export default function ComponentsList(
-  {
-    branchLike,
-    components,
-    onClick,
-    metrics,
-    metric,
-    rootComponent,
-    selectedComponent
-  } /*: Props */
-) {
-  if (!components.length) {
-    return <EmptyResult />;
+/*:: type State = {
+  hideBest: boolean
+}; */
+
+export default class ComponentsList extends React.PureComponent {
+  /*:: props: Props; */
+  state /*: State */ = {
+    hideBest: true
+  };
+
+  componentWillReceiveProps(nextProps /*: Props */) {
+    if (nextProps.metric !== this.props.metric) {
+      this.setState({ hideBest: true });
+    }
+  }
+
+  displayAll = (event /*: Event */) => {
+    event.preventDefault();
+    this.setState({ hideBest: false });
+  };
+
+  hasBestValue(component /*: Component*/, otherMetrics /*: Array<Metric> */) {
+    const { metric } = this.props;
+    const focusedMeasure = component.measures.find(measure => measure.metric.key === metric.key);
+    if (isDiffMetric(focusedMeasure.metric.key)) {
+      return isPeriodBestValue(focusedMeasure, 1);
+    }
+    return focusedMeasure.bestValue;
   }
 
-  const otherMetrics = (complementary[metric.key] || []).map(key => metrics[key]);
-  return (
-    <table className="data zebra zebra-hover">
-      {otherMetrics.length > 0 && (
-        <thead>
-          <tr>
-            <th>&nbsp;</th>
-            <th className="text-right">
-              <span className="small">{getLocalizedMetricName(metric)}</span>
-            </th>
-            {otherMetrics.map(metric => (
-              <th className="text-right" key={metric.key}>
-                <span className="small">{getLocalizedMetricName(metric)}</span>
-              </th>
-            ))}
-          </tr>
-        </thead>
-      )}
+  renderComponent(component /*: Component*/, otherMetrics /*: Array<Metric> */) {
+    const { branchLike, metric, selectedComponent, onClick, rootComponent } = this.props;
+    return (
+      <ComponentsListRow
+        branchLike={branchLike}
+        component={component}
+        isSelected={component.key === selectedComponent}
+        key={component.id}
+        metric={metric}
+        onClick={onClick}
+        otherMetrics={otherMetrics}
+        rootComponent={rootComponent}
+      />
+    );
+  }
 
-      <tbody>
-        {components.map(component => (
-          <ComponentsListRow
-            branchLike={branchLike}
-            component={component}
-            isSelected={component.key === selectedComponent}
-            key={component.id}
-            metric={metric}
-            onClick={onClick}
-            otherMetrics={otherMetrics}
-            rootComponent={rootComponent}
-          />
-        ))}
-      </tbody>
-    </table>
-  );
+  renderHiddenLink(hiddenCount /*: number*/, colCount /*: number*/) {
+    return (
+      <div className="alert alert-info spacer-top">
+        {translateWithParameters(
+          'component_measures.hidden_best_score_metrics',
+          hiddenCount,
+          formatMeasure(this.props.bestValue, this.props.metric.type)
+        )}
+        <a className="spacer-left" href="#" onClick={this.displayAll}>
+          {translate('show_all')}
+        </a>
+      </div>
+    );
+  }
+
+  render() {
+    const { components, metric, metrics } = this.props;
+    if (!components.length) {
+      return <EmptyResult />;
+    }
+
+    const otherMetrics = (complementary[metric.key] || []).map(key => metrics[key]);
+    const notBestComponents = components.filter(
+      component => !this.hasBestValue(component, otherMetrics)
+    );
+    const hiddenCount = components.length - notBestComponents.length;
+    const shouldHideBest = this.state.hideBest && hiddenCount !== components.length;
+    return (
+      <React.Fragment>
+        <table className="data zebra zebra-hover">
+          {otherMetrics.length > 0 && (
+            <thead>
+              <tr>
+                <th>&nbsp;</th>
+                <th className="text-right">
+                  <span className="small">{getLocalizedMetricName(metric)}</span>
+                </th>
+                {otherMetrics.map(metric => (
+                  <th className="text-right" key={metric.key}>
+                    <span className="small">{getLocalizedMetricName(metric)}</span>
+                  </th>
+                ))}
+              </tr>
+            </thead>
+          )}
+
+          <tbody>
+            {(shouldHideBest ? notBestComponents : components).map(component =>
+              this.renderComponent(component, otherMetrics)
+            )}
+          </tbody>
+        </table>
+        {shouldHideBest &&
+          hiddenCount > 0 &&
+          this.renderHiddenLink(hiddenCount, otherMetrics.length + 3)}
+      </React.Fragment>
+    );
+  }
 }
index 4c1fbb8ce082b18f4b1b1e7e2388d3dec176334f..93f464ef47bcdae709e3b51ac9998ab2745d785e 100644 (file)
@@ -28,6 +28,7 @@ import { scrollToElement } from '../../../helpers/scrolling';
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 
 /*:: type Props = {|
+  bestValue?: string,
   branchLike?: { id?: string; name: string },
   components: Array<ComponentEnhanced>,
   fetchMore: () => void,
@@ -124,6 +125,7 @@ export default class ListView extends React.PureComponent {
     return (
       <div ref={elem => (this.listContainer = elem)}>
         <ComponentsList
+          bestValue={this.props.bestValue}
           branchLike={this.props.branchLike}
           components={this.props.components}
           metric={this.props.metric}
index 928fc79a103b5a2ac08f5c14b7a38793fbc43b0d..596ea2a36854f2bb9ed66f1f7c3f1b9fafee9f72 100644 (file)
@@ -25,6 +25,7 @@ const HOURS_IN_DAY = 8;
 export interface MeasurePeriod {
   index: number;
   value: string;
+  bestValue?: boolean;
 }
 
 export interface MeasureIntern {
@@ -90,6 +91,15 @@ export function getPeriodValue(
   return period ? period.value : undefined;
 }
 
+export function isPeriodBestValue(
+  measure: Measure | MeasureEnhanced,
+  periodIndex: number
+): boolean {
+  const { periods } = measure;
+  const period = periods && periods.find(period => period.index === periodIndex);
+  return (period && period.bestValue) || false;
+}
+
 /** Check if metric is differential */
 export function isDiffMetric(metricKey: string): boolean {
   return metricKey.indexOf('new_') === 0;
index 31f4cc198e71e8791aadf582847a2bb90e2e54a4..a271928ce3b4fd8bbb39560603249560ebb96710 100644 (file)
@@ -19,6 +19,7 @@
  */
 // @flow
 /*:: export type Metric = {
+  bestValue?: string,
   custom?: boolean,
   decimalScale?: number,
   description?: string,
index 6b82d0d0497e0a0719b0b20e704b27918c60e4a8..b1f662821025647bfcfae5080eee94a017cc1397 100644 (file)
@@ -233,6 +233,7 @@ short_number_suffix.g=G
 short_number_suffix.k=k
 short_number_suffix.m=M
 show_more=Show More
+show_all=Show All
 should_be_unique=Should be unique
 since_x=since {0}
 since_previous_analysis=since previous analysis
@@ -2441,6 +2442,7 @@ component_measures.not_found=The requested measure was not found.
 component_measures.to_select_files=to select files
 component_measures.to_navigate=to navigate
 component_measures.to_navigate_files=to next/previous file
+component_measures.hidden_best_score_metrics=There are {0} hidden components with a score of {1}.
 
 component_measures.overview.project_overview.facet=Project Overview
 component_measures.overview.project_overview.title=Risk
index c861426c3ea2698cad57db4761832c303b567f76..0385751c43bc4ea8b828058ebe27db234800e3e0 100644 (file)
@@ -99,6 +99,7 @@ message Measure {
   optional string value = 2;
   optional PeriodsValue periods = 3;
   optional string component = 4;
+  optional bool bestValue = 5;
 }
 
 message PeriodsValue {
@@ -108,4 +109,5 @@ message PeriodsValue {
 message PeriodValue {
   optional int32 index = 1;
   optional string value = 2;
+  optional bool bestValue = 3;
 }