[results feed] Add filter for individual configurations

Also changes configuration group filter to be inactive when empty, and
default to empty

Change-Id: I114b308f0dda6c60303dcb90c007854c3acc1002
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/142040
Reviewed-by: Jonas Termansen <sortie@google.com>
diff --git a/results_feed/lib/src/model/commit.dart b/results_feed/lib/src/model/commit.dart
index fa20e26..83a8f5e 100644
--- a/results_feed/lib/src/model/commit.dart
+++ b/results_feed/lib/src/model/commit.dart
@@ -153,8 +153,9 @@
   }
 
   bool show(Filter filter) =>
-      filter.allGroups ||
+      filter.configurationGroups.isEmpty && filter.configurations.isEmpty ||
       configurations.any((configuration) =>
+          filter.configurations.contains(configuration) ||
           filter.configurationGroups.any(configuration.startsWith));
 }
 
diff --git a/results_feed/lib/src/services/filter_component.dart b/results_feed/lib/src/services/filter_component.dart
index 1a88f7a..e8497a2 100644
--- a/results_feed/lib/src/services/filter_component.dart
+++ b/results_feed/lib/src/services/filter_component.dart
@@ -67,13 +67,6 @@
   }
 
   void onSelectionChange(_) {
-    if (groupSelector.selectedValues.isEmpty) {
-      // Do not allow deselecting the last selected group.
-      // Selecting synchronously or with Future.microtask don't show in UI.
-      final recheck = service.filter.configurationGroups.first;
-      Future(() => groupSelector.select(recheck));
-      return;
-    }
     final values = groupSelector.selectedValues.toList();
     service.filter = filter.copy(configurationGroups: values);
     filter.updateUrl();
diff --git a/results_feed/lib/src/services/filter_service.dart b/results_feed/lib/src/services/filter_service.dart
index 1570063..8fe78c3 100644
--- a/results_feed/lib/src/services/filter_service.dart
+++ b/results_feed/lib/src/services/filter_service.dart
@@ -5,36 +5,39 @@
 import 'dart:html';
 
 class Filter {
+  final List<String> configurations;
   final List<String> configurationGroups;
   final bool showLatestFailures;
   final bool showUnapprovedOnly;
 
-  const Filter._(this.configurationGroups, this.showLatestFailures,
-      this.showUnapprovedOnly);
-  Filter(this.configurationGroups, this.showLatestFailures,
+  const Filter._(this.configurations, this.configurationGroups,
+      this.showLatestFailures, this.showUnapprovedOnly);
+  Filter(this.configurations, this.configurationGroups, this.showLatestFailures,
       this.showUnapprovedOnly);
 
-  static const defaultFilter = Filter._(allConfigurationGroups,
-      defaultShowLatestFailures, defaultShowUnapprovedOnly);
+  static const defaultFilter =
+      Filter._([], [], defaultShowLatestFailures, defaultShowUnapprovedOnly);
 
   Filter copy(
-          {List<String> configurationGroups,
+          {List<String> configurations,
+          List<String> configurationGroups,
           bool showLatestFailures,
           bool showUnapprovedOnly}) =>
       Filter(
+          configurations ?? this.configurations,
           configurationGroups ?? this.configurationGroups,
           showLatestFailures ?? this.showLatestFailures,
           showUnapprovedOnly ?? this.showUnapprovedOnly);
 
-  bool get allGroups =>
-      configurationGroups.length == allConfigurationGroups.length;
-
   String fragment() => [
         if (showLatestFailures != defaultShowLatestFailures)
           'showLatestFailures=$showLatestFailures',
         if (showUnapprovedOnly != defaultShowUnapprovedOnly)
           'showUnapprovedOnly=$showUnapprovedOnly',
-        if (!allGroups) 'configurationGroups=${configurationGroups.join(',')}'
+        if (configurations.isNotEmpty)
+          'configurations=${configurations.join(',')}',
+        if (configurationGroups.isNotEmpty)
+          'configurationGroups=${configurationGroups.join(',')}'
       ].join('&');
 
   void updateUrl() {
@@ -43,7 +46,7 @@
 
   factory Filter.fromUrl() {
     final fragment = Uri.parse(window.location.href).fragment;
-    Filter result = defaultFilter;
+    var result = defaultFilter;
     if (fragment.isEmpty) return result;
     for (final setting in fragment.split('&')) {
       final key = setting.split('=').first;
@@ -52,6 +55,9 @@
         result = result.copy(showLatestFailures: value == 'true');
       } else if (key == 'showUnapprovedOnly') {
         result = result.copy(showUnapprovedOnly: value == 'true');
+      } else if (key == 'configurations') {
+        final configurations = value.split(',');
+        result = result.copy(configurations: configurations);
       } else if (key == 'configurationGroups') {
         final configurationGroups = value.split(',');
         result = result.copy(configurationGroups: configurationGroups);