@@ -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()); |
@@ -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); | |||
} | |||
} |
@@ -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 |
@@ -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(); |
@@ -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())); | |||
@@ -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} |
@@ -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> </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> </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> | |||
); | |||
} | |||
} |
@@ -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} |
@@ -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; |
@@ -19,6 +19,7 @@ | |||
*/ | |||
// @flow | |||
/*:: export type Metric = { | |||
bestValue?: string, | |||
custom?: boolean, | |||
decimalScale?: number, | |||
description?: string, |
@@ -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 |
@@ -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; | |||
} |