From: Pascal Mugnier Date: Fri, 20 Apr 2018 05:38:14 +0000 (+0200) Subject: SONAR-9472 Change the rendering of best values on the Measures page X-Git-Tag: 7.5~1332 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=a7d7420a719ee56590e5c09d70bfd1a75a14abdf;p=sonarqube.git SONAR-9472 Change the rendering of best values on the Measures page --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java index 5ba9c3f9228..3adddc09965 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java @@ -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 measures, - Map referenceComponentsByUuid) { + Map 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 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 components = searchComponents(dbSession, componentTreeQuery); List metrics = searchMetrics(dbSession, wsRequest); Table 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 searchReferenceComponentsById(DbSession dbSession, List components) { List 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 searchComponents(DbSession dbSession, ComponentTreeQuery componentTreeQuery) { @@ -476,16 +477,16 @@ public class ComponentTreeAction implements MeasuresWsAction { if (metrics.size() < metricKeys.size()) { List foundMetricKeys = Lists.transform(metrics, MetricDto::getKey); Set 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 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 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 { * */ private static void addBestValuesToMeasures(Table measuresByComponentUuidAndMetric, List components, - List metrics) { + List metrics) { List 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 sortComponents(List components, ComponentTreeRequest wsRequest, List metrics, - Table measuresByComponentUuidAndMetric) { + Table measuresByComponentUuidAndMetric) { return ComponentTreeSort.sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric); } private static List paginateComponents(List 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 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()); diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java index b7b768040c1..6070c001b95 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/MeasureDtoToWsMeasure.java @@ -20,12 +20,14 @@ 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); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java index b900442a8ec..057002c7ccd 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java @@ -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 diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java index cc42a73db20..424c2607084 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java @@ -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 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 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 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(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchActionTest.java index 46f0cdd01ec..b39cf629302 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchActionTest.java @@ -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 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())); 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 a4847ed1848..a7cd534be4c 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 @@ -62,6 +62,7 @@ import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches' |}; */ /*:: type State = { + bestValue?: string, components: Array, 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 ( , 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 ; +/*:: 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 */) { + 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 ( - - {otherMetrics.length > 0 && ( - - - - - {otherMetrics.map(metric => ( - - ))} - - - )} + renderComponent(component /*: Component*/, otherMetrics /*: Array */) { + const { branchLike, metric, selectedComponent, onClick, rootComponent } = this.props; + return ( + + ); + } - - {components.map(component => ( - - ))} - -
  - {getLocalizedMetricName(metric)} - - {getLocalizedMetricName(metric)} -
- ); + renderHiddenLink(hiddenCount /*: number*/, colCount /*: number*/) { + return ( +
+ {translateWithParameters( + 'component_measures.hidden_best_score_metrics', + hiddenCount, + formatMeasure(this.props.bestValue, this.props.metric.type) + )} + + {translate('show_all')} + +
+ ); + } + + render() { + const { components, metric, metrics } = this.props; + if (!components.length) { + return ; + } + + 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 ( + + + {otherMetrics.length > 0 && ( + + + + + {otherMetrics.map(metric => ( + + ))} + + + )} + + + {(shouldHideBest ? notBestComponents : components).map(component => + this.renderComponent(component, otherMetrics) + )} + +
  + {getLocalizedMetricName(metric)} + + {getLocalizedMetricName(metric)} +
+ {shouldHideBest && + hiddenCount > 0 && + this.renderHiddenLink(hiddenCount, otherMetrics.length + 3)} +
+ ); + } } diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js index 4c1fbb8ce08..93f464ef47b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js @@ -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, fetchMore: () => void, @@ -124,6 +125,7 @@ export default class ListView extends React.PureComponent { return (
(this.listContainer = elem)}> period.index === periodIndex); + return (period && period.bestValue) || false; +} + /** Check if metric is differential */ export function isDiffMetric(metricKey: string): boolean { return metricKey.indexOf('new_') === 0; diff --git a/server/sonar-web/src/main/js/store/metrics/actions.js b/server/sonar-web/src/main/js/store/metrics/actions.js index 31f4cc198e7..a271928ce3b 100644 --- a/server/sonar-web/src/main/js/store/metrics/actions.js +++ b/server/sonar-web/src/main/js/store/metrics/actions.js @@ -19,6 +19,7 @@ */ // @flow /*:: export type Metric = { + bestValue?: string, custom?: boolean, decimalScale?: number, description?: string, diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 6b82d0d0497..b1f66282102 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 diff --git a/sonar-ws/src/main/protobuf/ws-measures.proto b/sonar-ws/src/main/protobuf/ws-measures.proto index c861426c3ea..0385751c43b 100644 --- a/sonar-ws/src/main/protobuf/ws-measures.proto +++ b/sonar-ws/src/main/protobuf/ws-measures.proto @@ -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; }