[current_results] Add experiment filter

Results can now be filtered by the experiments that were used to run the
test using the syntax 'experiment:<name>'. If multiple experiment
filters are used, all results are returned that match one of them.

This CL also updates the current_results_ui copies of the query.pb*
files to the version used in current_results, and contains the
required package version changes.

Change-Id: I4a84ee0d779307c0cd0d8577fbf0bb8dc9ffe65e
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/198461
Reviewed-by: William Hesse <whesse@google.com>
Commit-Queue: Karl Klose <karlklose@google.com>
diff --git a/current_results/lib/protos/query.proto b/current_results/lib/protos/query.proto
index 1882de3..0c32ec4 100644
--- a/current_results/lib/protos/query.proto
+++ b/current_results/lib/protos/query.proto
@@ -83,6 +83,7 @@
   string expected = 4;
   bool flaky = 5;
   int32 time_ms = 6;
+  repeated string experiments = 7;
 }
 
 message ListTestsRequest {
diff --git a/current_results/lib/src/generated/query.pb.dart b/current_results/lib/src/generated/query.pb.dart
index 7978ec6..12a9ddd 100644
--- a/current_results/lib/src/generated/query.pb.dart
+++ b/current_results/lib/src/generated/query.pb.dart
@@ -267,6 +267,7 @@
         const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'expected')
     ..aOB(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'flaky')
     ..a<$core.int>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'timeMs', $pb.PbFieldType.O3)
+    ..pPS(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'experiments')
     ..hasRequiredFields = false;
 
   Result._() : super();
@@ -277,6 +278,7 @@
     $core.String? expected,
     $core.bool? flaky,
     $core.int? timeMs,
+    $core.Iterable<$core.String>? experiments,
   }) {
     final _result = create();
     if (name != null) {
@@ -297,6 +299,9 @@
     if (timeMs != null) {
       _result.timeMs = timeMs;
     }
+    if (experiments != null) {
+      _result.experiments.addAll(experiments);
+    }
     return _result;
   }
   factory Result.fromBuffer($core.List<$core.int> i,
@@ -396,6 +401,9 @@
   $core.bool hasTimeMs() => $_has(5);
   @$pb.TagNumber(6)
   void clearTimeMs() => clearField(6);
+
+  @$pb.TagNumber(7)
+  $core.List<$core.String> get experiments => $_getList(6);
 }
 
 class ListTestsRequest extends $pb.GeneratedMessage {
diff --git a/current_results/lib/src/generated/query.pbjson.dart b/current_results/lib/src/generated/query.pbjson.dart
index 81d6802..67cf213 100644
--- a/current_results/lib/src/generated/query.pbjson.dart
+++ b/current_results/lib/src/generated/query.pbjson.dart
@@ -65,12 +65,13 @@
     const {'1': 'expected', '3': 4, '4': 1, '5': 9, '10': 'expected'},
     const {'1': 'flaky', '3': 5, '4': 1, '5': 8, '10': 'flaky'},
     const {'1': 'time_ms', '3': 6, '4': 1, '5': 5, '10': 'timeMs'},
+    const {'1': 'experiments', '3': 7, '4': 3, '5': 9, '10': 'experiments'},
   ],
 };
 
 /// Descriptor for `Result`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List resultDescriptor = $convert.base64Decode(
-    'CgZSZXN1bHQSEgoEbmFtZRgBIAEoCVIEbmFtZRIkCg1jb25maWd1cmF0aW9uGAIgASgJUg1jb25maWd1cmF0aW9uEhYKBnJlc3VsdBgDIAEoCVIGcmVzdWx0EhoKCGV4cGVjdGVkGAQgASgJUghleHBlY3RlZBIUCgVmbGFreRgFIAEoCFIFZmxha3kSFwoHdGltZV9tcxgGIAEoBVIGdGltZU1z');
+    'CgZSZXN1bHQSEgoEbmFtZRgBIAEoCVIEbmFtZRIkCg1jb25maWd1cmF0aW9uGAIgASgJUg1jb25maWd1cmF0aW9uEhYKBnJlc3VsdBgDIAEoCVIGcmVzdWx0EhoKCGV4cGVjdGVkGAQgASgJUghleHBlY3RlZBIUCgVmbGFreRgFIAEoCFIFZmxha3kSFwoHdGltZV9tcxgGIAEoBVIGdGltZU1zEiAKC2V4cGVyaW1lbnRzGAcgAygJUgtleHBlcmltZW50cw==');
 @$core.Deprecated('Use listTestsRequestDescriptor instead')
 const ListTestsRequest$json = const {
   '1': 'ListTestsRequest',
diff --git a/current_results/lib/src/result.dart b/current_results/lib/src/result.dart
index 52c2ec3..d4e145a 100644
--- a/current_results/lib/src/result.dart
+++ b/current_results/lib/src/result.dart
@@ -13,9 +13,10 @@
   final bool flaky;
   final String expected;
   final Duration time;
+  final List<String> experiments;
 
   Result(this.name, this.configuration, this.commitHash, this.result,
-      this.flaky, this.expected, this.time);
+      this.flaky, this.expected, this.time, this.experiments);
 
   Result.fromApi(api.Result other)
       : this(
@@ -25,9 +26,13 @@
             unique(other.result),
             other.flaky,
             unique(other.expected),
-            Duration(milliseconds: other.timeMs));
+            Duration(milliseconds: other.timeMs),
+            other.experiments.isEmpty
+                ? const []
+                : other.experiments.map(unique).toList(growable: false));
 
-  Result.nameOnly(String name) : this(name, null, null, null, null, null, null);
+  Result.nameOnly(String name)
+      : this(name, null, null, null, null, null, null, null);
 
   static final uniqueStrings = <String>{};
 
@@ -41,7 +46,8 @@
     ..result = result
     ..timeMs = time.inMilliseconds
     ..expected = expected
-    ..flaky = flaky;
+    ..flaky = flaky
+    ..experiments.addAll(experiments);
 
   static query_api.Result toApi(Result result) => result.toQueryResult();
 
@@ -53,6 +59,7 @@
         'flaky': flaky,
         'expected': expected,
         'time': time,
+        'experiments': experiments,
       };
 
   @override
diff --git a/current_results/lib/src/slice.dart b/current_results/lib/src/slice.dart
index ac8e0be..f139389 100644
--- a/current_results/lib/src/slice.dart
+++ b/current_results/lib/src/slice.dart
@@ -24,16 +24,23 @@
 
 /// Returns the range from [sorted] of Result entries that are at or after
 /// [startResult] and begin with [prefixResult].
-Iterable<Result> getResultRange(
-    List<Result> sorted, Result startResult, Result prefixResult) {
-  var start = lowerBound<Result>(sorted, startResult, compare: compareNames);
+/// If [experiments] is not empty, only results with one of the contained
+/// experiment names are included.
+Iterable<Result> getResultRange(List<Result> sorted, Result startResult,
+    Result prefixResult, Set<String> experiments) {
+  final start = lowerBound<Result>(sorted, startResult, compare: compareNames);
   if (start >= sorted.length) return [];
   if (!sorted[start].name.startsWith(prefixResult.name)) return [];
   var end = start + 1;
   if (end < sorted.length && sorted[end].name.startsWith(prefixResult.name)) {
     end = lowerBound<Result>(sorted, prefixResult, compare: isAfterPrefix);
   }
-  return sorted.getRange(start, end);
+  var range = sorted.getRange(start, end);
+  if (experiments.isNotEmpty) {
+    range = range.where((result) => experiments
+        .any((experiment) => result.experiments.contains(experiment)));
+  }
+  return range;
 }
 
 /// Holds the test results for all configurations.
@@ -49,16 +56,19 @@
   final _lastFetched = <String, DateTime>{};
 
   /// A sorted list of all test names seen. Names are not removed from this list.
-  List<String> testNames = [];
+  List<String> _testNames = [];
+
   int _size = 0;
 
   int get size => _size;
 
   void add(List<String> lines) {
     if (lines.isEmpty) return;
-    final results = lines.map((line) => Result.fromApi(api.Result()
-      ..mergeFromProto3Json(json.decode(line),
-          supportNamesWithUnderscores: true)));
+    final results = lines
+        .map((line) => Result.fromApi(api.Result()
+          ..mergeFromProto3Json(json.decode(line),
+              supportNamesWithUnderscores: true)))
+        .toList();
     final configuration = results.first.configuration;
     if (results.any((result) => result.configuration != configuration)) {
       print('Loaded results list with multiple configurations: '
@@ -75,9 +85,9 @@
   }
 
   void collectTestNames() {
-    testNames.clear();
+    _testNames.clear();
     for (final results in _stored.values) {
-      testNames = _mergeIfNeeded(testNames, results);
+      _testNames = _mergeIfNeeded(_testNames, results);
     }
   }
 
@@ -119,7 +129,6 @@
   }
 
   query_api.GetResultsResponse results(query_api.GetResultsRequest query) {
-    final response = query_api.GetResultsResponse();
     final limit = min(100000, query.pageSize == 0 ? 100000 : query.pageSize);
     final pageStart =
         query.pageToken.isEmpty ? null : PageStart.parse(query.pageToken);
@@ -127,13 +136,19 @@
         query.filter.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty);
     final configurationSet = <String>{};
     final testPrefixes = <String>[];
-    for (final prefix in filterTerms) {
-      final matchingConfigurations = _stored.keys
-          .where((configuration) => configuration.startsWith(prefix));
-      if (matchingConfigurations.isEmpty) {
-        testPrefixes.add(prefix);
+    final experiments = <String>{};
+    for (final term in filterTerms) {
+      const experimentPrefix = 'experiment:';
+      if (term.startsWith(experimentPrefix)) {
+        experiments.add(term.substring(experimentPrefix.length));
       } else {
-        configurationSet.addAll(matchingConfigurations);
+        final matchingConfigurations = _stored.keys
+            .where((configuration) => configuration.startsWith(term));
+        if (matchingConfigurations.isEmpty) {
+          testPrefixes.add(term);
+        } else {
+          configurationSet.addAll(matchingConfigurations);
+        }
       }
     }
     testPrefixes.sort();
@@ -150,11 +165,12 @@
         (configurationSet.isEmpty ? _stored.keys : configurationSet).toList()
           ..sort();
 
+    final response = query_api.GetResultsResponse();
     for (final prefix in testPrefixes) {
-      response.results.addAll(getSortedResults(
-              prefix, configurations, pageStart,
-              needed: limit - response.results.length)
-          .map(Result.toApi));
+      var sortedResults = getSortedResults(
+          prefix, configurations, experiments, pageStart,
+          needed: limit - response.results.length);
+      response.results.addAll(sortedResults.map(Result.toApi));
       if (response.results.length == limit) {
         response.nextPageToken = PageStart(
                 response.results.last.name, response.results.last.configuration)
@@ -167,10 +183,12 @@
 
   /// Returns up to [needed] results from the configurations in [configurations],
   /// with test names that start with [prefix], sorted by test name.
+  /// If [experiments] is not empty, only results with one of the contained
+  /// experiment names are included.
   /// If [pageStart] is not null, test names before pageStart.name are
   /// filtered out.
-  List<Result> getSortedResults(
-      String prefix, List<String> configurations, PageStart pageStart,
+  List<Result> getSortedResults(String prefix, List<String> configurations,
+      Set<String> experiments, PageStart pageStart,
       {int needed}) {
     final prefixResult = Result.nameOnly(prefix);
     var startResult;
@@ -185,8 +203,8 @@
     var results = <Result>[];
 
     for (final configuration in configurations) {
-      var configurationRange =
-          getResultRange(_stored[configuration], startResult, prefixResult);
+      var configurationRange = getResultRange(
+          _stored[configuration], startResult, prefixResult, experiments);
 
       if (configurationRange.isEmpty) continue;
       if (pageStart != null &&
@@ -214,9 +232,9 @@
     if (limit == 0) limit = 20;
     final prefix = query.prefix;
     final response = query_api.ListTestsResponse();
-    final start = lowerBound(testNames, prefix);
-    final end = min(start + limit, testNames.length);
-    for (final name in testNames.getRange(start, end)) {
+    final start = lowerBound(_testNames, prefix);
+    final end = min(start + limit, _testNames.length);
+    for (final name in _testNames.getRange(start, end)) {
       if (name.startsWith(prefix)) {
         response.names.add(name);
       } else {
diff --git a/current_results_ui/lib/src/generated/query.pb.dart b/current_results_ui/lib/src/generated/query.pb.dart
index c5e7833..12a9ddd 100644
--- a/current_results_ui/lib/src/generated/query.pb.dart
+++ b/current_results_ui/lib/src/generated/query.pb.dart
@@ -2,37 +2,116 @@
 //  Generated code. Do not modify.
 //  source: query.proto
 //
-// @dart = 2.3
-// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
 
-import 'dart:async' as $async;
 import 'dart:core' as $core;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
-import 'google/protobuf/empty.pb.dart' as $0;
+class Empty extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'Empty',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
+      createEmptyInstance: create)
+    ..hasRequiredFields = false;
+
+  Empty._() : super();
+  factory Empty() => create();
+  factory Empty.fromBuffer($core.List<$core.int> i,
+          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+      create()..mergeFromBuffer(i, r);
+  factory Empty.fromJson($core.String i,
+          [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+      create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
+  Empty clone() => Empty()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
+  Empty copyWith(void Function(Empty) updates) =>
+      super.copyWith((message) => updates(message as Empty))
+          as Empty; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static Empty create() => Empty._();
+  Empty createEmptyInstance() => create();
+  static $pb.PbList<Empty> createRepeated() => $pb.PbList<Empty>();
+  @$core.pragma('dart2js:noInline')
+  static Empty getDefault() =>
+      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Empty>(create);
+  static Empty? _defaultInstance;
+}
 
 class GetResultsRequest extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('GetResultsRequest',
-      package: const $pb.PackageName('current_results'),
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'GetResultsRequest',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
       createEmptyInstance: create)
-    ..pPS(1, 'names')
-    ..pPS(2, 'configurations')
-    ..a<$core.int>(3, 'pageSize', $pb.PbFieldType.O3)
-    ..aOS(4, 'pageToken')
+    ..aOS(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'filter')
+    ..a<$core.int>(
+        2,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'pageSize',
+        $pb.PbFieldType.O3)
+    ..aOS(
+        3,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'pageToken')
     ..hasRequiredFields = false;
 
   GetResultsRequest._() : super();
-  factory GetResultsRequest() => create();
+  factory GetResultsRequest({
+    $core.String? filter,
+    $core.int? pageSize,
+    $core.String? pageToken,
+  }) {
+    final _result = create();
+    if (filter != null) {
+      _result.filter = filter;
+    }
+    if (pageSize != null) {
+      _result.pageSize = pageSize;
+    }
+    if (pageToken != null) {
+      _result.pageToken = pageToken;
+    }
+    return _result;
+  }
   factory GetResultsRequest.fromBuffer($core.List<$core.int> i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromBuffer(i, r);
   factory GetResultsRequest.fromJson($core.String i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
   GetResultsRequest clone() => GetResultsRequest()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
   GetResultsRequest copyWith(void Function(GetResultsRequest) updates) =>
-      super.copyWith((message) => updates(message as GetResultsRequest));
+      super.copyWith((message) => updates(message as GetResultsRequest))
+          as GetResultsRequest; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static GetResultsRequest create() => GetResultsRequest._();
@@ -42,58 +121,96 @@
   @$core.pragma('dart2js:noInline')
   static GetResultsRequest getDefault() => _defaultInstance ??=
       $pb.GeneratedMessage.$_defaultFor<GetResultsRequest>(create);
-  static GetResultsRequest _defaultInstance;
+  static GetResultsRequest? _defaultInstance;
 
   @$pb.TagNumber(1)
-  $core.List<$core.String> get names => $_getList(0);
+  $core.String get filter => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set filter($core.String v) {
+    $_setString(0, v);
+  }
+
+  @$pb.TagNumber(1)
+  $core.bool hasFilter() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearFilter() => clearField(1);
 
   @$pb.TagNumber(2)
-  $core.List<$core.String> get configurations => $_getList(1);
-
-  @$pb.TagNumber(3)
-  $core.int get pageSize => $_getIZ(2);
-  @$pb.TagNumber(3)
+  $core.int get pageSize => $_getIZ(1);
+  @$pb.TagNumber(2)
   set pageSize($core.int v) {
-    $_setSignedInt32(2, v);
+    $_setSignedInt32(1, v);
   }
 
-  @$pb.TagNumber(3)
-  $core.bool hasPageSize() => $_has(2);
-  @$pb.TagNumber(3)
-  void clearPageSize() => clearField(3);
+  @$pb.TagNumber(2)
+  $core.bool hasPageSize() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearPageSize() => clearField(2);
 
-  @$pb.TagNumber(4)
-  $core.String get pageToken => $_getSZ(3);
-  @$pb.TagNumber(4)
+  @$pb.TagNumber(3)
+  $core.String get pageToken => $_getSZ(2);
+  @$pb.TagNumber(3)
   set pageToken($core.String v) {
-    $_setString(3, v);
+    $_setString(2, v);
   }
 
-  @$pb.TagNumber(4)
-  $core.bool hasPageToken() => $_has(3);
-  @$pb.TagNumber(4)
-  void clearPageToken() => clearField(4);
+  @$pb.TagNumber(3)
+  $core.bool hasPageToken() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearPageToken() => clearField(3);
 }
 
 class GetResultsResponse extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('GetResultsResponse',
-      package: const $pb.PackageName('current_results'),
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'GetResultsResponse',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
       createEmptyInstance: create)
-    ..pc<Result>(1, 'results', $pb.PbFieldType.PM, subBuilder: Result.create)
-    ..aOS(2, 'nextPageToken')
+    ..pc<Result>(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'results',
+        $pb.PbFieldType.PM,
+        subBuilder: Result.create)
+    ..aOS(2,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'nextPageToken')
     ..hasRequiredFields = false;
 
   GetResultsResponse._() : super();
-  factory GetResultsResponse() => create();
+  factory GetResultsResponse({
+    $core.Iterable<Result>? results,
+    $core.String? nextPageToken,
+  }) {
+    final _result = create();
+    if (results != null) {
+      _result.results.addAll(results);
+    }
+    if (nextPageToken != null) {
+      _result.nextPageToken = nextPageToken;
+    }
+    return _result;
+  }
   factory GetResultsResponse.fromBuffer($core.List<$core.int> i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromBuffer(i, r);
   factory GetResultsResponse.fromJson($core.String i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
   GetResultsResponse clone() => GetResultsResponse()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
   GetResultsResponse copyWith(void Function(GetResultsResponse) updates) =>
-      super.copyWith((message) => updates(message as GetResultsResponse));
+      super.copyWith((message) => updates(message as GetResultsResponse))
+          as GetResultsResponse; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static GetResultsResponse create() => GetResultsResponse._();
@@ -103,7 +220,7 @@
   @$core.pragma('dart2js:noInline')
   static GetResultsResponse getDefault() => _defaultInstance ??=
       $pb.GeneratedMessage.$_defaultFor<GetResultsResponse>(create);
-  static GetResultsResponse _defaultInstance;
+  static GetResultsResponse? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.List<Result> get results => $_getList(0);
@@ -122,28 +239,87 @@
 }
 
 class Result extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('Result',
-      package: const $pb.PackageName('current_results'),
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'Result',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
       createEmptyInstance: create)
-    ..aOS(1, 'name')
-    ..aOS(2, 'configuration')
-    ..aOS(3, 'result')
-    ..aOS(4, 'expected')
-    ..aOB(5, 'flaky')
-    ..a<$core.int>(6, 'timeMs', $pb.PbFieldType.O3)
+    ..aOS(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'name')
+    ..aOS(
+        2,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'configuration')
+    ..aOS(
+        3,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'result')
+    ..aOS(4,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'expected')
+    ..aOB(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'flaky')
+    ..a<$core.int>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'timeMs', $pb.PbFieldType.O3)
+    ..pPS(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'experiments')
     ..hasRequiredFields = false;
 
   Result._() : super();
-  factory Result() => create();
+  factory Result({
+    $core.String? name,
+    $core.String? configuration,
+    $core.String? result,
+    $core.String? expected,
+    $core.bool? flaky,
+    $core.int? timeMs,
+    $core.Iterable<$core.String>? experiments,
+  }) {
+    final _result = create();
+    if (name != null) {
+      _result.name = name;
+    }
+    if (configuration != null) {
+      _result.configuration = configuration;
+    }
+    if (result != null) {
+      _result.result = result;
+    }
+    if (expected != null) {
+      _result.expected = expected;
+    }
+    if (flaky != null) {
+      _result.flaky = flaky;
+    }
+    if (timeMs != null) {
+      _result.timeMs = timeMs;
+    }
+    if (experiments != null) {
+      _result.experiments.addAll(experiments);
+    }
+    return _result;
+  }
   factory Result.fromBuffer($core.List<$core.int> i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromBuffer(i, r);
   factory Result.fromJson($core.String i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
   Result clone() => Result()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
   Result copyWith(void Function(Result) updates) =>
-      super.copyWith((message) => updates(message as Result));
+      super.copyWith((message) => updates(message as Result))
+          as Result; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static Result create() => Result._();
@@ -152,7 +328,7 @@
   @$core.pragma('dart2js:noInline')
   static Result getDefault() =>
       _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Result>(create);
-  static Result _defaultInstance;
+  static Result? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get name => $_getSZ(0);
@@ -225,27 +401,64 @@
   $core.bool hasTimeMs() => $_has(5);
   @$pb.TagNumber(6)
   void clearTimeMs() => clearField(6);
+
+  @$pb.TagNumber(7)
+  $core.List<$core.String> get experiments => $_getList(6);
 }
 
 class ListTestsRequest extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListTestsRequest',
-      package: const $pb.PackageName('current_results'),
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'ListTestsRequest',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
       createEmptyInstance: create)
-    ..aOS(1, 'prefix')
-    ..a<$core.int>(2, 'limit', $pb.PbFieldType.O3)
+    ..aOS(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'prefix')
+    ..a<$core.int>(
+        2,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'limit',
+        $pb.PbFieldType.O3)
     ..hasRequiredFields = false;
 
   ListTestsRequest._() : super();
-  factory ListTestsRequest() => create();
+  factory ListTestsRequest({
+    $core.String? prefix,
+    $core.int? limit,
+  }) {
+    final _result = create();
+    if (prefix != null) {
+      _result.prefix = prefix;
+    }
+    if (limit != null) {
+      _result.limit = limit;
+    }
+    return _result;
+  }
   factory ListTestsRequest.fromBuffer($core.List<$core.int> i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromBuffer(i, r);
   factory ListTestsRequest.fromJson($core.String i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
   ListTestsRequest clone() => ListTestsRequest()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
   ListTestsRequest copyWith(void Function(ListTestsRequest) updates) =>
-      super.copyWith((message) => updates(message as ListTestsRequest));
+      super.copyWith((message) => updates(message as ListTestsRequest))
+          as ListTestsRequest; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ListTestsRequest create() => ListTestsRequest._();
@@ -255,7 +468,7 @@
   @$core.pragma('dart2js:noInline')
   static ListTestsRequest getDefault() => _defaultInstance ??=
       $pb.GeneratedMessage.$_defaultFor<ListTestsRequest>(create);
-  static ListTestsRequest _defaultInstance;
+  static ListTestsRequest? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get prefix => $_getSZ(0);
@@ -283,23 +496,48 @@
 }
 
 class ListTestsResponse extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListTestsResponse',
-      package: const $pb.PackageName('current_results'),
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'ListTestsResponse',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
       createEmptyInstance: create)
-    ..pPS(1, 'names')
+    ..pPS(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'names')
     ..hasRequiredFields = false;
 
   ListTestsResponse._() : super();
-  factory ListTestsResponse() => create();
+  factory ListTestsResponse({
+    $core.Iterable<$core.String>? names,
+  }) {
+    final _result = create();
+    if (names != null) {
+      _result.names.addAll(names);
+    }
+    return _result;
+  }
   factory ListTestsResponse.fromBuffer($core.List<$core.int> i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromBuffer(i, r);
   factory ListTestsResponse.fromJson($core.String i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
   ListTestsResponse clone() => ListTestsResponse()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
   ListTestsResponse copyWith(void Function(ListTestsResponse) updates) =>
-      super.copyWith((message) => updates(message as ListTestsResponse));
+      super.copyWith((message) => updates(message as ListTestsResponse))
+          as ListTestsResponse; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ListTestsResponse create() => ListTestsResponse._();
@@ -309,33 +547,57 @@
   @$core.pragma('dart2js:noInline')
   static ListTestsResponse getDefault() => _defaultInstance ??=
       $pb.GeneratedMessage.$_defaultFor<ListTestsResponse>(create);
-  static ListTestsResponse _defaultInstance;
+  static ListTestsResponse? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.List<$core.String> get names => $_getList(0);
 }
 
 class ListConfigurationsRequest extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListConfigurationsRequest',
-      package: const $pb.PackageName('current_results'),
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'ListConfigurationsRequest',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
       createEmptyInstance: create)
-    ..aOS(1, 'prefix')
+    ..aOS(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'prefix')
     ..hasRequiredFields = false;
 
   ListConfigurationsRequest._() : super();
-  factory ListConfigurationsRequest() => create();
+  factory ListConfigurationsRequest({
+    $core.String? prefix,
+  }) {
+    final _result = create();
+    if (prefix != null) {
+      _result.prefix = prefix;
+    }
+    return _result;
+  }
   factory ListConfigurationsRequest.fromBuffer($core.List<$core.int> i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromBuffer(i, r);
   factory ListConfigurationsRequest.fromJson($core.String i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
   ListConfigurationsRequest clone() =>
       ListConfigurationsRequest()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
   ListConfigurationsRequest copyWith(
           void Function(ListConfigurationsRequest) updates) =>
-      super
-          .copyWith((message) => updates(message as ListConfigurationsRequest));
+      super.copyWith((message) => updates(message as ListConfigurationsRequest))
+          as ListConfigurationsRequest; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ListConfigurationsRequest create() => ListConfigurationsRequest._();
@@ -345,7 +607,7 @@
   @$core.pragma('dart2js:noInline')
   static ListConfigurationsRequest getDefault() => _defaultInstance ??=
       $pb.GeneratedMessage.$_defaultFor<ListConfigurationsRequest>(create);
-  static ListConfigurationsRequest _defaultInstance;
+  static ListConfigurationsRequest? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get prefix => $_getSZ(0);
@@ -362,26 +624,50 @@
 
 class ListConfigurationsResponse extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(
-      'ListConfigurationsResponse',
-      package: const $pb.PackageName('current_results'),
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'ListConfigurationsResponse',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
       createEmptyInstance: create)
-    ..pPS(1, 'configurations')
+    ..pPS(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'configurations')
     ..hasRequiredFields = false;
 
   ListConfigurationsResponse._() : super();
-  factory ListConfigurationsResponse() => create();
+  factory ListConfigurationsResponse({
+    $core.Iterable<$core.String>? configurations,
+  }) {
+    final _result = create();
+    if (configurations != null) {
+      _result.configurations.addAll(configurations);
+    }
+    return _result;
+  }
   factory ListConfigurationsResponse.fromBuffer($core.List<$core.int> i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromBuffer(i, r);
   factory ListConfigurationsResponse.fromJson($core.String i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
   ListConfigurationsResponse clone() =>
       ListConfigurationsResponse()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
   ListConfigurationsResponse copyWith(
           void Function(ListConfigurationsResponse) updates) =>
       super.copyWith(
-          (message) => updates(message as ListConfigurationsResponse));
+              (message) => updates(message as ListConfigurationsResponse))
+          as ListConfigurationsResponse; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ListConfigurationsResponse create() => ListConfigurationsResponse._();
@@ -391,31 +677,57 @@
   @$core.pragma('dart2js:noInline')
   static ListConfigurationsResponse getDefault() => _defaultInstance ??=
       $pb.GeneratedMessage.$_defaultFor<ListConfigurationsResponse>(create);
-  static ListConfigurationsResponse _defaultInstance;
+  static ListConfigurationsResponse? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.List<$core.String> get configurations => $_getList(0);
 }
 
 class FetchResponse extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('FetchResponse',
-      package: const $pb.PackageName('current_results'),
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'FetchResponse',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
       createEmptyInstance: create)
-    ..pc<ConfigurationUpdate>(1, 'updates', $pb.PbFieldType.PM,
+    ..pc<ConfigurationUpdate>(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'updates',
+        $pb.PbFieldType.PM,
         subBuilder: ConfigurationUpdate.create)
     ..hasRequiredFields = false;
 
   FetchResponse._() : super();
-  factory FetchResponse() => create();
+  factory FetchResponse({
+    $core.Iterable<ConfigurationUpdate>? updates,
+  }) {
+    final _result = create();
+    if (updates != null) {
+      _result.updates.addAll(updates);
+    }
+    return _result;
+  }
   factory FetchResponse.fromBuffer($core.List<$core.int> i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromBuffer(i, r);
   factory FetchResponse.fromJson($core.String i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
   FetchResponse clone() => FetchResponse()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
   FetchResponse copyWith(void Function(FetchResponse) updates) =>
-      super.copyWith((message) => updates(message as FetchResponse));
+      super.copyWith((message) => updates(message as FetchResponse))
+          as FetchResponse; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static FetchResponse create() => FetchResponse._();
@@ -425,30 +737,55 @@
   @$core.pragma('dart2js:noInline')
   static FetchResponse getDefault() => _defaultInstance ??=
       $pb.GeneratedMessage.$_defaultFor<FetchResponse>(create);
-  static FetchResponse _defaultInstance;
+  static FetchResponse? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.List<ConfigurationUpdate> get updates => $_getList(0);
 }
 
 class ConfigurationUpdate extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ConfigurationUpdate',
-      package: const $pb.PackageName('current_results'),
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'ConfigurationUpdate',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'current_results'),
       createEmptyInstance: create)
-    ..aOS(1, 'configuration')
+    ..aOS(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'configuration')
     ..hasRequiredFields = false;
 
   ConfigurationUpdate._() : super();
-  factory ConfigurationUpdate() => create();
+  factory ConfigurationUpdate({
+    $core.String? configuration,
+  }) {
+    final _result = create();
+    if (configuration != null) {
+      _result.configuration = configuration;
+    }
+    return _result;
+  }
   factory ConfigurationUpdate.fromBuffer($core.List<$core.int> i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromBuffer(i, r);
   factory ConfigurationUpdate.fromJson($core.String i,
           [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
       create()..mergeFromJson(i, r);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+      'Will be removed in next major version')
   ConfigurationUpdate clone() => ConfigurationUpdate()..mergeFromMessage(this);
+  @$core.Deprecated('Using this can add significant overhead to your binary. '
+      'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+      'Will be removed in next major version')
   ConfigurationUpdate copyWith(void Function(ConfigurationUpdate) updates) =>
-      super.copyWith((message) => updates(message as ConfigurationUpdate));
+      super.copyWith((message) => updates(message as ConfigurationUpdate))
+          as ConfigurationUpdate; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
   static ConfigurationUpdate create() => ConfigurationUpdate._();
@@ -458,7 +795,7 @@
   @$core.pragma('dart2js:noInline')
   static ConfigurationUpdate getDefault() => _defaultInstance ??=
       $pb.GeneratedMessage.$_defaultFor<ConfigurationUpdate>(create);
-  static ConfigurationUpdate _defaultInstance;
+  static ConfigurationUpdate? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get configuration => $_getSZ(0);
@@ -472,42 +809,3 @@
   @$pb.TagNumber(1)
   void clearConfiguration() => clearField(1);
 }
-
-class QueryApi {
-  $pb.RpcClient _client;
-  QueryApi(this._client);
-
-  $async.Future<GetResultsResponse> getResults(
-      $pb.ClientContext ctx, GetResultsRequest request) {
-    var emptyResponse = GetResultsResponse();
-    return _client.invoke<GetResultsResponse>(
-        ctx, 'Query', 'GetResults', request, emptyResponse);
-  }
-
-  $async.Future<ListTestsResponse> listTests(
-      $pb.ClientContext ctx, ListTestsRequest request) {
-    var emptyResponse = ListTestsResponse();
-    return _client.invoke<ListTestsResponse>(
-        ctx, 'Query', 'ListTests', request, emptyResponse);
-  }
-
-  $async.Future<ListTestsResponse> listTestPathCompletions(
-      $pb.ClientContext ctx, ListTestsRequest request) {
-    var emptyResponse = ListTestsResponse();
-    return _client.invoke<ListTestsResponse>(
-        ctx, 'Query', 'ListTestPathCompletions', request, emptyResponse);
-  }
-
-  $async.Future<ListConfigurationsResponse> listConfigurations(
-      $pb.ClientContext ctx, ListConfigurationsRequest request) {
-    var emptyResponse = ListConfigurationsResponse();
-    return _client.invoke<ListConfigurationsResponse>(
-        ctx, 'Query', 'ListConfigurations', request, emptyResponse);
-  }
-
-  $async.Future<FetchResponse> fetch($pb.ClientContext ctx, $0.Empty request) {
-    var emptyResponse = FetchResponse();
-    return _client.invoke<FetchResponse>(
-        ctx, 'Query', 'Fetch', request, emptyResponse);
-  }
-}
diff --git a/current_results_ui/lib/src/generated/query.pbenum.dart b/current_results_ui/lib/src/generated/query.pbenum.dart
index 277e345..9937c96 100644
--- a/current_results_ui/lib/src/generated/query.pbenum.dart
+++ b/current_results_ui/lib/src/generated/query.pbenum.dart
@@ -2,5 +2,5 @@
 //  Generated code. Do not modify.
 //  source: query.proto
 //
-// @dart = 2.3
-// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
diff --git a/current_results_ui/lib/src/generated/query.pbjson.dart b/current_results_ui/lib/src/generated/query.pbjson.dart
index 9e4ee01..67cf213 100644
--- a/current_results_ui/lib/src/generated/query.pbjson.dart
+++ b/current_results_ui/lib/src/generated/query.pbjson.dart
@@ -2,27 +2,35 @@
 //  Generated code. Do not modify.
 //  source: query.proto
 //
-// @dart = 2.3
-// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
 
-import 'google/protobuf/empty.pbjson.dart' as $0;
+import 'dart:core' as $core;
+import 'dart:convert' as $convert;
+import 'dart:typed_data' as $typed_data;
 
+@$core.Deprecated('Use emptyDescriptor instead')
+const Empty$json = const {
+  '1': 'Empty',
+};
+
+/// Descriptor for `Empty`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List emptyDescriptor =
+    $convert.base64Decode('CgVFbXB0eQ==');
+@$core.Deprecated('Use getResultsRequestDescriptor instead')
 const GetResultsRequest$json = const {
   '1': 'GetResultsRequest',
   '2': const [
-    const {'1': 'names', '3': 1, '4': 3, '5': 9, '10': 'names'},
-    const {
-      '1': 'configurations',
-      '3': 2,
-      '4': 3,
-      '5': 9,
-      '10': 'configurations'
-    },
-    const {'1': 'page_size', '3': 3, '4': 1, '5': 5, '10': 'pageSize'},
-    const {'1': 'page_token', '3': 4, '4': 1, '5': 9, '10': 'pageToken'},
+    const {'1': 'filter', '3': 1, '4': 1, '5': 9, '10': 'filter'},
+    const {'1': 'page_size', '3': 2, '4': 1, '5': 5, '10': 'pageSize'},
+    const {'1': 'page_token', '3': 3, '4': 1, '5': 9, '10': 'pageToken'},
   ],
 };
 
+/// Descriptor for `GetResultsRequest`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List getResultsRequestDescriptor = $convert.base64Decode(
+    'ChFHZXRSZXN1bHRzUmVxdWVzdBIWCgZmaWx0ZXIYASABKAlSBmZpbHRlchIbCglwYWdlX3NpemUYAiABKAVSCHBhZ2VTaXplEh0KCnBhZ2VfdG9rZW4YAyABKAlSCXBhZ2VUb2tlbg==');
+@$core.Deprecated('Use getResultsResponseDescriptor instead')
 const GetResultsResponse$json = const {
   '1': 'GetResultsResponse',
   '2': const [
@@ -44,6 +52,10 @@
   ],
 };
 
+/// Descriptor for `GetResultsResponse`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List getResultsResponseDescriptor = $convert.base64Decode(
+    'ChJHZXRSZXN1bHRzUmVzcG9uc2USMQoHcmVzdWx0cxgBIAMoCzIXLmN1cnJlbnRfcmVzdWx0cy5SZXN1bHRSB3Jlc3VsdHMSJgoPbmV4dF9wYWdlX3Rva2VuGAIgASgJUg1uZXh0UGFnZVRva2Vu');
+@$core.Deprecated('Use resultDescriptor instead')
 const Result$json = const {
   '1': 'Result',
   '2': const [
@@ -53,9 +65,14 @@
     const {'1': 'expected', '3': 4, '4': 1, '5': 9, '10': 'expected'},
     const {'1': 'flaky', '3': 5, '4': 1, '5': 8, '10': 'flaky'},
     const {'1': 'time_ms', '3': 6, '4': 1, '5': 5, '10': 'timeMs'},
+    const {'1': 'experiments', '3': 7, '4': 3, '5': 9, '10': 'experiments'},
   ],
 };
 
+/// Descriptor for `Result`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List resultDescriptor = $convert.base64Decode(
+    'CgZSZXN1bHQSEgoEbmFtZRgBIAEoCVIEbmFtZRIkCg1jb25maWd1cmF0aW9uGAIgASgJUg1jb25maWd1cmF0aW9uEhYKBnJlc3VsdBgDIAEoCVIGcmVzdWx0EhoKCGV4cGVjdGVkGAQgASgJUghleHBlY3RlZBIUCgVmbGFreRgFIAEoCFIFZmxha3kSFwoHdGltZV9tcxgGIAEoBVIGdGltZU1zEiAKC2V4cGVyaW1lbnRzGAcgAygJUgtleHBlcmltZW50cw==');
+@$core.Deprecated('Use listTestsRequestDescriptor instead')
 const ListTestsRequest$json = const {
   '1': 'ListTestsRequest',
   '2': const [
@@ -64,6 +81,10 @@
   ],
 };
 
+/// Descriptor for `ListTestsRequest`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List listTestsRequestDescriptor = $convert.base64Decode(
+    'ChBMaXN0VGVzdHNSZXF1ZXN0EhYKBnByZWZpeBgBIAEoCVIGcHJlZml4EhQKBWxpbWl0GAIgASgFUgVsaW1pdA==');
+@$core.Deprecated('Use listTestsResponseDescriptor instead')
 const ListTestsResponse$json = const {
   '1': 'ListTestsResponse',
   '2': const [
@@ -71,6 +92,10 @@
   ],
 };
 
+/// Descriptor for `ListTestsResponse`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List listTestsResponseDescriptor = $convert
+    .base64Decode('ChFMaXN0VGVzdHNSZXNwb25zZRIUCgVuYW1lcxgBIAMoCVIFbmFtZXM=');
+@$core.Deprecated('Use listConfigurationsRequestDescriptor instead')
 const ListConfigurationsRequest$json = const {
   '1': 'ListConfigurationsRequest',
   '2': const [
@@ -78,6 +103,11 @@
   ],
 };
 
+/// Descriptor for `ListConfigurationsRequest`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List listConfigurationsRequestDescriptor =
+    $convert.base64Decode(
+        'ChlMaXN0Q29uZmlndXJhdGlvbnNSZXF1ZXN0EhYKBnByZWZpeBgBIAEoCVIGcHJlZml4');
+@$core.Deprecated('Use listConfigurationsResponseDescriptor instead')
 const ListConfigurationsResponse$json = const {
   '1': 'ListConfigurationsResponse',
   '2': const [
@@ -91,6 +121,11 @@
   ],
 };
 
+/// Descriptor for `ListConfigurationsResponse`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List listConfigurationsResponseDescriptor =
+    $convert.base64Decode(
+        'ChpMaXN0Q29uZmlndXJhdGlvbnNSZXNwb25zZRImCg5jb25maWd1cmF0aW9ucxgBIAMoCVIOY29uZmlndXJhdGlvbnM=');
+@$core.Deprecated('Use fetchResponseDescriptor instead')
 const FetchResponse$json = const {
   '1': 'FetchResponse',
   '2': const [
@@ -105,6 +140,10 @@
   ],
 };
 
+/// Descriptor for `FetchResponse`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List fetchResponseDescriptor = $convert.base64Decode(
+    'Cg1GZXRjaFJlc3BvbnNlEj4KB3VwZGF0ZXMYASADKAsyJC5jdXJyZW50X3Jlc3VsdHMuQ29uZmlndXJhdGlvblVwZGF0ZVIHdXBkYXRlcw==');
+@$core.Deprecated('Use configurationUpdateDescriptor instead')
 const ConfigurationUpdate$json = const {
   '1': 'ConfigurationUpdate',
   '2': const [
@@ -112,47 +151,6 @@
   ],
 };
 
-const QueryServiceBase$json = const {
-  '1': 'Query',
-  '2': const [
-    const {
-      '1': 'GetResults',
-      '2': '.current_results.GetResultsRequest',
-      '3': '.current_results.GetResultsResponse'
-    },
-    const {
-      '1': 'ListTests',
-      '2': '.current_results.ListTestsRequest',
-      '3': '.current_results.ListTestsResponse'
-    },
-    const {
-      '1': 'ListTestPathCompletions',
-      '2': '.current_results.ListTestsRequest',
-      '3': '.current_results.ListTestsResponse'
-    },
-    const {
-      '1': 'ListConfigurations',
-      '2': '.current_results.ListConfigurationsRequest',
-      '3': '.current_results.ListConfigurationsResponse'
-    },
-    const {
-      '1': 'Fetch',
-      '2': '.google.protobuf.Empty',
-      '3': '.current_results.FetchResponse'
-    },
-  ],
-};
-
-const QueryServiceBase$messageJson = const {
-  '.current_results.GetResultsRequest': GetResultsRequest$json,
-  '.current_results.GetResultsResponse': GetResultsResponse$json,
-  '.current_results.Result': Result$json,
-  '.current_results.ListTestsRequest': ListTestsRequest$json,
-  '.current_results.ListTestsResponse': ListTestsResponse$json,
-  '.current_results.ListConfigurationsRequest': ListConfigurationsRequest$json,
-  '.current_results.ListConfigurationsResponse':
-      ListConfigurationsResponse$json,
-  '.google.protobuf.Empty': $0.Empty$json,
-  '.current_results.FetchResponse': FetchResponse$json,
-  '.current_results.ConfigurationUpdate': ConfigurationUpdate$json,
-};
+/// Descriptor for `ConfigurationUpdate`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List configurationUpdateDescriptor = $convert.base64Decode(
+    'ChNDb25maWd1cmF0aW9uVXBkYXRlEiQKDWNvbmZpZ3VyYXRpb24YASABKAlSDWNvbmZpZ3VyYXRpb24=');
diff --git a/current_results_ui/lib/src/generated/query.pbserver.dart b/current_results_ui/lib/src/generated/query.pbserver.dart
deleted file mode 100644
index ab2b362..0000000
--- a/current_results_ui/lib/src/generated/query.pbserver.dart
+++ /dev/null
@@ -1,69 +0,0 @@
-///
-//  Generated code. Do not modify.
-//  source: query.proto
-//
-// @dart = 2.3
-// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type
-
-import 'dart:async' as $async;
-
-import 'package:protobuf/protobuf.dart' as $pb;
-
-import 'dart:core' as $core;
-import 'query.pb.dart' as $1;
-import 'google/protobuf/empty.pb.dart' as $0;
-import 'query.pbjson.dart';
-
-export 'query.pb.dart';
-
-abstract class QueryServiceBase extends $pb.GeneratedService {
-  $async.Future<$1.GetResultsResponse> getResults(
-      $pb.ServerContext ctx, $1.GetResultsRequest request);
-  $async.Future<$1.ListTestsResponse> listTests(
-      $pb.ServerContext ctx, $1.ListTestsRequest request);
-  $async.Future<$1.ListTestsResponse> listTestPathCompletions(
-      $pb.ServerContext ctx, $1.ListTestsRequest request);
-  $async.Future<$1.ListConfigurationsResponse> listConfigurations(
-      $pb.ServerContext ctx, $1.ListConfigurationsRequest request);
-  $async.Future<$1.FetchResponse> fetch(
-      $pb.ServerContext ctx, $0.Empty request);
-
-  $pb.GeneratedMessage createRequest($core.String method) {
-    switch (method) {
-      case 'GetResults':
-        return $1.GetResultsRequest();
-      case 'ListTests':
-        return $1.ListTestsRequest();
-      case 'ListTestPathCompletions':
-        return $1.ListTestsRequest();
-      case 'ListConfigurations':
-        return $1.ListConfigurationsRequest();
-      case 'Fetch':
-        return $0.Empty();
-      default:
-        throw $core.ArgumentError('Unknown method: $method');
-    }
-  }
-
-  $async.Future<$pb.GeneratedMessage> handleCall($pb.ServerContext ctx,
-      $core.String method, $pb.GeneratedMessage request) {
-    switch (method) {
-      case 'GetResults':
-        return this.getResults(ctx, request);
-      case 'ListTests':
-        return this.listTests(ctx, request);
-      case 'ListTestPathCompletions':
-        return this.listTestPathCompletions(ctx, request);
-      case 'ListConfigurations':
-        return this.listConfigurations(ctx, request);
-      case 'Fetch':
-        return this.fetch(ctx, request);
-      default:
-        throw $core.ArgumentError('Unknown method: $method');
-    }
-  }
-
-  $core.Map<$core.String, $core.dynamic> get $json => QueryServiceBase$json;
-  $core.Map<$core.String, $core.Map<$core.String, $core.dynamic>>
-      get $messageJson => QueryServiceBase$messageJson;
-}
diff --git a/current_results_ui/pubspec.lock b/current_results_ui/pubspec.lock
index f30336a..433ec1a 100644
--- a/current_results_ui/pubspec.lock
+++ b/current_results_ui/pubspec.lock
@@ -1,34 +1,41 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  archive:
+    dependency: transitive
+    description:
+      name: archive
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.2"
   async:
     dependency: transitive
     description:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.5.0-nullsafety.3"
+    version: "2.5.0"
   boolean_selector:
     dependency: transitive
     description:
       name: boolean_selector
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.1.0-nullsafety.3"
+    version: "2.1.0"
   characters:
     dependency: transitive
     description:
       name: characters
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0-nullsafety.5"
+    version: "1.1.0"
   charcode:
     dependency: transitive
     description:
       name: charcode
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0-nullsafety.3"
+    version: "1.2.0"
   clippy:
     dependency: "direct main"
     description:
@@ -42,42 +49,35 @@
       name: clock
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0-nullsafety.3"
+    version: "1.1.0"
   collection:
     dependency: transitive
     description:
       name: collection
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.15.0-nullsafety.5"
-  convert:
-    dependency: transitive
-    description:
-      name: convert
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.1.1"
+    version: "1.15.0"
   crypto:
     dependency: transitive
     description:
       name: crypto
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.1.5"
+    version: "3.0.1"
   fake_async:
     dependency: transitive
     description:
       name: fake_async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0-nullsafety.3"
+    version: "1.2.0"
   fixnum:
     dependency: transitive
     description:
       name: fixnum
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.10.11"
+    version: "1.0.0"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -94,84 +94,84 @@
       name: googleapis_auth
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.2.12"
+    version: "1.1.0"
   grpc:
     dependency: "direct main"
     description:
       name: grpc
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.2.0"
+    version: "3.0.0"
   http:
     dependency: "direct main"
     description:
       name: http
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.2"
+    version: "0.13.3"
   http2:
     dependency: transitive
     description:
       name: http2
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.0.0"
+    version: "2.0.0"
   http_parser:
     dependency: transitive
     description:
       name: http_parser
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.1.4"
+    version: "4.0.0"
   matcher:
     dependency: transitive
     description:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.10-nullsafety.3"
+    version: "0.12.10"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0-nullsafety.6"
+    version: "1.3.0"
   nested:
     dependency: transitive
     description:
       name: nested
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.0.4"
+    version: "1.0.0"
   path:
     dependency: transitive
     description:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.0-nullsafety.3"
+    version: "1.8.0"
   pedantic:
     dependency: transitive
     description:
       name: pedantic
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.9.2"
+    version: "1.11.0"
   protobuf:
     dependency: "direct main"
     description:
       name: protobuf
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.0.1"
+    version: "2.0.0"
   provider:
     dependency: "direct main"
     description:
       name: provider
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "4.3.2+1"
+    version: "4.3.3"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -183,56 +183,56 @@
       name: source_span
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.0-nullsafety.4"
+    version: "1.8.1"
   stack_trace:
     dependency: transitive
     description:
       name: stack_trace
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.10.0-nullsafety.6"
+    version: "1.10.0"
   stream_channel:
     dependency: transitive
     description:
       name: stream_channel
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.1.0-nullsafety.3"
+    version: "2.1.0"
   string_scanner:
     dependency: transitive
     description:
       name: string_scanner
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0-nullsafety.3"
+    version: "1.1.0"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0-nullsafety.3"
+    version: "1.2.0"
   test_api:
     dependency: transitive
     description:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.2.19-nullsafety.6"
+    version: "0.3.0"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0-nullsafety.5"
+    version: "1.3.0"
   vector_math:
     dependency: transitive
     description:
       name: vector_math
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.1.0-nullsafety.5"
+    version: "2.1.0"
 sdks:
-  dart: ">=2.12.0-0.0 <3.0.0"
+  dart: ">=2.12.0 <3.0.0"
   flutter: ">=1.16.0"
diff --git a/current_results_ui/pubspec.yaml b/current_results_ui/pubspec.yaml
index f3d781f..f3c88ab 100644
--- a/current_results_ui/pubspec.yaml
+++ b/current_results_ui/pubspec.yaml
@@ -12,9 +12,9 @@
   flutter:
     sdk: flutter
 
-  http: ^0.12.1
-  grpc: ^2.1.3
-  protobuf: ^1.0.0
+  http: ^0.13.1
+  grpc: ^3.0.0
+  protobuf: ^2.0.0
   provider: ^4.3.2
 
 dev_dependencies: