blob: 5a997ee967b3bc1f82407dc08be6dfe68de25655 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// This component renders the results list during the approve/comment workflow.
// It is similar to results_panel.dart, which renders the normal view.
import 'package:angular/angular.dart';
import 'package:angular_components/angular_components.dart';
import 'package:angular_components/content/deferred_content.dart';
import 'package:angular_components/laminate/enums/alignment.dart';
import 'package:angular_components/material_checkbox/material_checkbox.dart';
import 'package:angular_components/material_chips/material_chip.dart';
import 'package:angular_components/material_chips/material_chips.dart';
import 'package:angular_components/material_radio/material_radio.dart';
import 'package:angular_components/material_tooltip/material_tooltip.dart';
import 'package:angular_components/material_tooltip/module.dart' as tooltip;
import 'package:angular_forms/angular_forms.dart' show formDirectives;
import 'package:dart_results_feed/src/test_source.dart';
import '../formatting.dart' as formatting;
import '../model/commit.dart';
import '../services/filter_service.dart';
import '../services/try_data_service.dart';
import 'log_component.dart';
@Component(
selector: 'results-selector-panel',
directives: [
coreDirectives,
formDirectives,
DeferredContentDirective,
LogComponent,
MaterialButtonComponent,
MaterialCheckboxComponent,
MaterialChipComponent,
MaterialChipsComponent,
MaterialPaperTooltipComponent,
MaterialRadioComponent,
MaterialRadioGroupComponent,
MaterialTooltipDirective,
MaterialTooltipTargetDirective,
RelativePosition
],
providers: [popupBindings, tooltip.materialTooltipBindings],
templateUrl: 'results_selector_panel.html',
styleUrls: ([
'results.css',
'package:angular_components/css/mdc_web/card/mdc-card.scss.css'
]))
class ResultsSelectorPanel {
ResultsSelectorPanel();
@Input()
set changes(Changes changes) {
_changes = changes;
recomputeChanges();
}
Changes get changes => _changes;
Changes _changes;
// Removes passing changes if failuresOnly is set. Does not handle changing
// failuresOnly from true to false.
void recomputeChanges() {
if (_changes == null) return;
if (failuresOnly) {
_changes = Changes(changes.flat.where((change) => change.failed));
}
configurationCheckboxes.clear();
resultCheckboxes.clear();
checked.clear();
for (final configurationGroup in changes) {
configurationCheckboxes[configurationGroup] = FixedMixedCheckbox();
for (final resultGroup in configurationGroup) {
resultCheckboxes[resultGroup] = FixedMixedCheckbox();
for (final change in resultGroup) {
checked[change] = true;
}
}
}
initializeSelected();
}
@Input()
List<Commit> commits;
/// [range] will be null if these are try results
@Input()
IntRange range;
/// [builds] will be null if these are CI results
@Input()
Map<int, Map<String, TryBuild>> builds;
/// A map from configurations to try builders. Null for CI results.
// TODO(whesse): Make lazy, fetch directly from try data service, not an input.
@Input()
Map<String, String> builders;
@Input()
Filter filter = Filter.defaultFilter;
@Input()
set selected(Set<Change> selected) {
_selected = selected;
initializeSelected();
}
@Input()
set failuresOnly(bool value) {
_failuresOnly = value;
recomputeChanges();
}
bool get failuresOnly => _failuresOnly;
bool _failuresOnly = false;
Set<Change> _selected;
final Map<Change, bool> checked = {};
final Map<List<Change>, FixedMixedCheckbox> resultCheckboxes = {};
final Map<List<List<Change>>, FixedMixedCheckbox> configurationCheckboxes =
{};
int resultLimit = 10;
final preferredTooltipPositions = [
RelativePosition.OffsetBottomLeft,
RelativePosition.OffsetTopLeft
];
Map<String, List<String>> summaries(List<List<Change>> group) {
final first = group.first.first;
final configurations = filter.showLatestFailures
? first.activeConfigurations
: first.configurations;
return configurations.summaries;
}
String buildbucketID(int patchset, String configuration) =>
builds[patchset][builders[configuration]].buildbucketID;
String getTestSource(Change change, bool isTryResult) {
return getTestSourceUrl(commits, change, isTryResult);
}
String approvalContent(Change change) =>
change.approved ? formatting.checkmark : '';
void initializeSelected() {
if (_selected != null && _changes != null) {
_selected.clear();
_selected.addAll(checked.keys);
}
}
bool setCheckbox(Change change, bool event) {
if (checked[change] == event) return false;
checked[change] = event;
if (event) {
_selected.add(change);
} else {
_selected.remove(change);
}
return true;
}
void onChange(bool event, Change change, List<Change> resultGroup,
List<List<Change>> configurationGroup) {
if (setCheckbox(change, event)) {
configurationCheckboxes[configurationGroup].setMixed();
resultCheckboxes[resultGroup].setMixed();
}
}
void onResultChange(String event, List<Change> resultGroup,
List<List<Change>> configurationGroup) {
final checkbox = resultCheckboxes[resultGroup];
if (checkbox.eventMatchesState(event)) return;
assert(event != 'mixed');
final newChecked = event == 'true';
checkbox.setState(newChecked, false);
for (final change in resultGroup) {
setCheckbox(change, newChecked);
}
configurationCheckboxes[configurationGroup].setMixed();
}
void onConfigurationChange(
String event, List<List<Change>> configurationGroup) {
final checkbox = configurationCheckboxes[configurationGroup];
if (checkbox.eventMatchesState(event)) return;
assert(event != 'mixed');
final newChecked = event == 'true';
checkbox.setState(newChecked, false);
for (final subgroup in configurationGroup) {
resultCheckboxes[subgroup].setState(newChecked, false);
for (final change in subgroup) {
setCheckbox(change, newChecked);
}
}
}
}
class FixedMixedCheckbox {
bool checked = true;
bool indeterminate = false;
// Model change indeterminate <-> checked generates a bad 'unchecked' event.
// Workaround for issue https://github.com/dart-lang/angular_components/issues/434
bool expectBadEvent = false;
bool eventMatchesState(String event) {
if (event == 'mixed' && indeterminate ||
event == 'true' && checked ||
event == 'false' && !indeterminate && !checked ||
event == 'false' && expectBadEvent) {
expectBadEvent = false;
return true;
}
return false;
}
void setState(bool newChecked, bool newIndeterminate) {
assert(!newChecked || !newIndeterminate);
if (newChecked && indeterminate || checked && newIndeterminate) {
expectBadEvent = true;
}
checked = newChecked;
indeterminate = newIndeterminate;
}
void setMixed() => setState(false, true);
}