| // 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. |
| |
| 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 '../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() |
| ChangeGroup commit; |
| |
| /// [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 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); |
| } |