[results feed] Add new filter UI using chips in top bar
Replace the existing filter UI popup with chips in a bar at the top.
This also supports filtering on a test name and on individual configurations.
Change-Id: I6b1bf2af21d3a947018c9a36253e0d41c416cb4c
Fixes: https://github.com/dart-lang/dart_ci/issues/88
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/146180
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/results_feed/lib/src/components/app.html b/results_feed/lib/src/components/app.html
index 852b4ca..5d16881 100644
--- a/results_feed/lib/src/components/app.html
+++ b/results_feed/lib/src/components/app.html
@@ -1,4 +1,4 @@
-<header class="shadow blue">
+<header class="blue">
<material-button
icon
class="material-drawer-button"
@@ -6,7 +6,7 @@
<material-icon icon="filter_list"></material-icon>
</material-button>
<span class="title">{{title}}</span>
- <span class="space"></span>
+ <results-filter></results-filter>
<span *ngIf="fetching != null">
<material-icon icon="autorenew"></material-icon>
fetching results...
@@ -17,6 +17,10 @@
{{loginMessage}}
</span>
</header>
+<header class="shadow">
+ <filter-row></filter-row>
+</header>
+
<div class="results-feed-body">
<modal [(visible)]="showFilter">
<material-dialog
diff --git a/results_feed/lib/src/components/app_component.css b/results_feed/lib/src/components/app_component.css
index 6ebbf59..7d0bd18 100644
--- a/results_feed/lib/src/components/app_component.css
+++ b/results_feed/lib/src/components/app_component.css
@@ -7,8 +7,11 @@
header {
flex: none;
width: 100%;
+ min-height: 50px;
margin: 0px;
z-index: 1;
+ display: flex;
+ align-items: center;
}
header.shadow {
@@ -16,10 +19,6 @@
0 1px 10px 0 rgba(0, 0, 0, 0.12);
}
-header span.space {
- flex: 1;
-}
-
header span.button {
font-variant: small-caps;
}
@@ -36,6 +35,10 @@
color: white;
}
+results-filter {
+ flex: 1;
+}
+
.results-feed-body {
flex: auto;
overflow-y: scroll;
diff --git a/results_feed/lib/src/components/app_component.dart b/results_feed/lib/src/components/app_component.dart
index e382b84..8017c8a 100644
--- a/results_feed/lib/src/components/app_component.dart
+++ b/results_feed/lib/src/components/app_component.dart
@@ -15,12 +15,15 @@
import 'package:angular_components/material_icon/material_icon.dart';
import 'package:angular_components/material_toggle/material_toggle.dart';
import 'package:angular_router/angular_router.dart';
-import 'package:dart_results_feed/src/services/filter_component.dart';
+import 'package:dart_results_feed/src/components/results_filter_component.dart';
import 'commit_component.dart';
+import 'filter_row_component.dart';
+import 'results_filter_component.dart';
import '../formatting.dart';
import '../model/commit.dart';
import '../model/comment.dart';
+import '../services/filter_component.dart';
import '../services/filter_service.dart';
import '../services/firestore_service.dart';
import '../services/build_service.dart';
@@ -33,12 +36,14 @@
AutoDismissDirective,
CommitComponent,
FilterComponent,
+ FilterRowComponent,
MaterialIconComponent,
MaterialButtonComponent,
MaterialDialogComponent,
MaterialTemporaryDrawerComponent,
MaterialToggleComponent,
- ModalComponent
+ ModalComponent,
+ ResultsFilterComponent,
],
providers: [
ClassProvider(FilterService),
diff --git a/results_feed/lib/src/components/filter_row_component.css b/results_feed/lib/src/components/filter_row_component.css
new file mode 100644
index 0000000..a0993b4
--- /dev/null
+++ b/results_feed/lib/src/components/filter_row_component.css
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2020, 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.
+ */
+
+material-chip {
+ display: inline-flex;
+}
+
+material-auto-suggest-input {
+ height: 24px;
+ margin-left: 8px;
+}
+
+material-auto-suggest-input ::ng-deep material-input {
+ padding: 0px;
+}
+
+material-icon {
+ color: rgb(63, 81, 181);
+}
\ No newline at end of file
diff --git a/results_feed/lib/src/components/filter_row_component.dart b/results_feed/lib/src/components/filter_row_component.dart
new file mode 100644
index 0000000..ec63789
--- /dev/null
+++ b/results_feed/lib/src/components/filter_row_component.dart
@@ -0,0 +1,94 @@
+// Copyright (c) 2020, 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/material_input/material_auto_suggest_input.dart';
+import 'package:angular_components/material_chips/material_chip.dart';
+import 'package:angular_components/material_chips/material_chips.dart';
+import 'package:angular_components/material_icon/material_icon.dart';
+
+import '../services/build_service.dart';
+import '../services/filter_service.dart';
+
+const allConfigurationGroups = Filter.allConfigurationGroups;
+
+@Component(
+ selector: 'filter-row',
+ exports: [allConfigurationGroups],
+ directives: [
+ coreDirectives,
+ MaterialAutoSuggestInputComponent,
+ MaterialChipComponent,
+ MaterialChipsComponent,
+ MaterialIconComponent,
+ NgModel,
+ ],
+ providers: [
+ popupBindings,
+ ],
+ templateUrl: 'filter_row_component.html',
+ styleUrls: [
+ 'package:angular_components/app_layout/layout.scss.css',
+ 'filter_row_component.css'
+ ])
+class FilterRowComponent implements OnInit {
+ @ViewChild('addFilterTextInput')
+ MaterialAutoSuggestInputComponent addFilterTextInput;
+
+ final FilterService service;
+ final BuildService buildService;
+ bool addingFilter = false;
+ bool addingTestFilter = false;
+ String filterText = '';
+ List<String> selectionOptions = [];
+
+ Filter get filter => service.filter;
+
+ FilterRowComponent(this.service, this.buildService);
+
+ void ngOnInit() async {
+ final configurations = await buildService.configurations;
+ selectionOptions = ['(any test name)']
+ .followedBy({
+ for (final configuration in configurations)
+ configuration.split('-').first + '-'
+ })
+ .followedBy(configurations)
+ .toList();
+ }
+
+ String get test => filter.singleTest;
+
+ Iterable<String> get configurations =>
+ filter.configurationGroups.followedBy(filter.configurations).toList();
+
+ void addFilter() {
+ addingFilter = true;
+ Future.delayed(Duration(), () => addFilterTextInput.focus());
+ }
+
+ void clear() {
+ filterText = '';
+ addingFilter = false;
+ addingTestFilter = false;
+ }
+
+ void selectionChange(event) {
+ if (event is String) {
+ service.addConfiguration(event);
+ clear();
+ }
+ }
+
+ void inputTextChange(event) {
+ filterText = event;
+ addingTestFilter = event is String && event.contains('/');
+ }
+
+ void addTestFilter() {
+ service.setTestFilter(filterText);
+ clear();
+ }
+}
diff --git a/results_feed/lib/src/components/filter_row_component.html b/results_feed/lib/src/components/filter_row_component.html
new file mode 100644
index 0000000..8c581ed
--- /dev/null
+++ b/results_feed/lib/src/components/filter_row_component.html
@@ -0,0 +1,34 @@
+<material-chip
+ *ngIf="test != null"
+ (remove)="service.clearTestFilter()">
+ t: {{test}}
+</material-chip>
+<material-chip
+ *ngFor="let configuration of configurations"
+ (remove)="service.removeConfiguration(configuration)">
+ c: {{configuration}}
+</material-chip>
+<material-chip
+ *ngIf="!addingFilter"
+ [removable]="false"
+ (click)="addFilter()">
+ Add Filter
+</material-chip>
+<material-icon
+ *ngIf="addingTestFilter"
+ baseline
+ icon="check_circle"
+ (click)="addTestFilter()">
+</material-icon>
+<material-auto-suggest-input
+ #addFilterTextInput
+ *ngIf="addingFilter"
+ [displayBottomPanel]="false"
+ [closeOnEnter]="true"
+ [showClearIcon]="true"
+ [selectionOptions]="selectionOptions"
+ [inputText]="filterText"
+ (inputTextChange)="inputTextChange($event)"
+ (selectionChange)="selectionChange($event)"
+ (clear)="clear()">
+</material-auto-suggest-input>
diff --git a/results_feed/lib/src/components/results_filter_component.dart b/results_feed/lib/src/components/results_filter_component.dart
new file mode 100644
index 0000000..1abf2c5
--- /dev/null
+++ b/results_feed/lib/src/components/results_filter_component.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2020, 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/material_chips/material_chip.dart';
+import 'package:angular_components/material_chips/material_chips.dart';
+
+import '../services/filter_service.dart';
+
+const allConfigurationGroups = Filter.allConfigurationGroups;
+
+@Component(selector: 'results-filter', exports: [
+ allConfigurationGroups
+], directives: [
+ coreDirectives,
+ MaterialChipComponent,
+ MaterialChipsComponent,
+], template: '''
+<span></span>
+<span
+ *ngFor="let type of resultTypes"
+ [class.selected]="type == selectedType"
+ (click)="select(type)">
+ {{type}}
+</span>
+<span></span>
+''', styles: [
+ '''
+ :host {
+ display: flex;
+ height: 100%;
+ box-sizing: border-box;}
+ span {
+ color: lightgray;
+ display: inline-flex;
+ font-variant: small-caps;
+ font-size: 20px;
+ font-weight: bold;
+ align-items: center;
+ justify-content: center;
+ height: auto;
+ border-bottom-style: solid;
+ border-width: medium;
+ border-color: transparent;
+ flex: 1 1 auto;
+ }
+ span.selected {
+ color: white;
+ border-color: white;
+ }'''
+])
+class ResultsFilterComponent {
+ static const allResults = 'all results';
+ static const activeFailures = 'active failures';
+ static const unapprovedFailures = 'unapproved failures';
+ static const resultTypes = [allResults, activeFailures, unapprovedFailures];
+
+ final FilterService service;
+ Filter get filter => service.filter;
+
+ ResultsFilterComponent(this.service);
+
+ String get selectedType => filter.showLatestFailures
+ ? filter.showUnapprovedOnly ? unapprovedFailures : activeFailures
+ : allResults;
+
+ void select(String type) {
+ if (type == selectedType) return;
+ service.filter = filter.copy(
+ showLatestFailures: type != allResults,
+ showUnapprovedOnly: type == unapprovedFailures)
+ ..updateUrl();
+ }
+}
diff --git a/results_feed/lib/src/services/build_service.dart b/results_feed/lib/src/services/build_service.dart
index 16e9f29..808c677 100644
--- a/results_feed/lib/src/services/build_service.dart
+++ b/results_feed/lib/src/services/build_service.dart
@@ -28,16 +28,19 @@
BuildService(this._firestoreService);
final FirestoreService _firestoreService;
- final Map<String, Map<int, FutureOr<Build>>> _lookupBuild = {};
- Future<Map<String, String>> _builders;
- Future fetchingBuilders;
+ final Map<String, Map<int, Future<Build>>> _builds = {};
+ Map<String, String> _builders;
+ List<String> _configurations;
+ Future _buildersFetched;
- FutureOr<Build> buildForResult(String configuration, int index) async {
- _builders ??= _fetchBuilders();
- final builder = (await _builders)[configuration];
- final builds = _lookupBuild.putIfAbsent(builder, () => {});
+ Future get _ready => _buildersFetched ??= _fetchBuilders();
- return builds.putIfAbsent(index, _fetchBuild(builder, index));
+ Future<Build> buildForResult(String configuration, int index) async {
+ await _ready;
+ final builder = _builders[configuration];
+ return _builds
+ .putIfAbsent(builder, () => {})
+ .putIfAbsent(index, _fetchBuild(builder, index));
}
Future<Build> Function() _fetchBuild(String builder, int index) => () async {
@@ -46,9 +49,13 @@
return Build.fromDocument(buildDocument);
};
- Future<Map<String, String>> _fetchBuilders() async {
+ Future _fetchBuilders() async {
await _firestoreService.getFirebaseClient();
final builderDocs = await _firestoreService.fetchBuilders();
- return {for (var doc in builderDocs) doc.id: doc.get('builder')};
+ _builders = {for (var doc in builderDocs) doc.id: doc.get('builder')};
}
+
+ FutureOr<List<String>> get configurations =>
+ _configurations ??
+ _ready.then((_) => _configurations = _builders.keys.toList());
}
diff --git a/results_feed/lib/src/services/filter_component.dart b/results_feed/lib/src/services/filter_component.dart
index e8497a2..81c9be5 100644
--- a/results_feed/lib/src/services/filter_component.dart
+++ b/results_feed/lib/src/services/filter_component.dart
@@ -68,8 +68,7 @@
void onSelectionChange(_) {
final values = groupSelector.selectedValues.toList();
- service.filter = filter.copy(configurationGroups: values);
- filter.updateUrl();
+ service.changeFilter(filter.copy(configurationGroups: values));
}
List<Map> changeTypes = [
@@ -94,10 +93,9 @@
void setChangeType(Map event) {
changeType = event;
- service.filter = filter.copy(
+ service.changeFilter(filter.copy(
showLatestFailures: event['latestFailures'],
- showUnapprovedOnly: event['unapprovedOnly']);
- filter.updateUrl();
+ showUnapprovedOnly: event['unapprovedOnly']));
}
String identityFunction(t) => t;
diff --git a/results_feed/lib/src/services/filter_service.dart b/results_feed/lib/src/services/filter_service.dart
index bef557e..2dc2e02 100644
--- a/results_feed/lib/src/services/filter_service.dart
+++ b/results_feed/lib/src/services/filter_service.dart
@@ -11,14 +11,17 @@
final bool showUnapprovedOnly;
final String singleTest; // null by default.
- const Filter._(this.configurations, this.configurationGroups,
- this.showLatestFailures, this.showUnapprovedOnly, this.singleTest);
Filter(this.configurations, this.configurationGroups, this.showLatestFailures,
this.showUnapprovedOnly, this.singleTest);
- static const defaultFilter = Filter._(
+ static final defaultFilter = Filter(
[], [], defaultShowLatestFailures, defaultShowUnapprovedOnly, null);
+ bool get hasFilter =>
+ singleTest != null ||
+ configurations.isNotEmpty ||
+ configurationGroups.isNotEmpty;
+
Filter copy(
{List<String> configurations,
List<String> configurationGroups,
@@ -32,6 +35,9 @@
showUnapprovedOnly ?? this.showUnapprovedOnly,
singleTest ?? this.singleTest); // Cannot reset singleTest to null.
+ Filter clearTest() => Filter(configurations, configurationGroups,
+ showLatestFailures, showUnapprovedOnly, null);
+
String fragment() => [
if (showLatestFailures != defaultShowLatestFailures)
'showLatestFailures=$showLatestFailures',
@@ -94,4 +100,38 @@
FilterService();
Filter filter = Filter.fromUrl();
+
+ void changeFilter(Filter newFilter) {
+ newFilter.updateUrl();
+ if (newFilter.singleTest != filter.singleTest) {
+ window.location.reload();
+ }
+ filter = newFilter;
+ }
+
+ void addConfiguration(String configuration) {
+ if (configuration.endsWith('-')) {
+ changeFilter(filter.copy(
+ configurationGroups: [configuration, ...filter.configurationGroups]));
+ } else {
+ changeFilter(filter
+ .copy(configurations: [configuration, ...filter.configurations]));
+ }
+ }
+
+ void removeConfiguration(String configuration) {
+ changeFilter(filter.copy(
+ configurationGroups: List.from(filter.configurationGroups)
+ ..remove(configuration),
+ configurations: List.from(filter.configurations)
+ ..remove(configuration)));
+ }
+
+ void setTestFilter(String test) {
+ changeFilter(filter.copy(singleTest: test));
+ }
+
+ void clearTestFilter() {
+ changeFilter(filter.clearTest());
+ }
}