[current results ui] Initial version of current results Flutter web interface

This is the initial version of the current results web app.

Change-Id: I4c1da75f1e8ec1e02e2703e96ab5d132f6f3a6c4
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/158441
Reviewed-by: Karl Klose <karlklose@google.com>
diff --git a/ui_current_results/lib/filter.dart b/ui_current_results/lib/filter.dart
new file mode 100644
index 0000000..10911fc
--- /dev/null
+++ b/ui_current_results/lib/filter.dart
@@ -0,0 +1,69 @@
+// 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:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class Filter extends ChangeNotifier {
+  List<String> terms = [
+    'dartk-',
+    'app_jitk',
+    'service/de',
+  ];
+
+  void addAll(String value) {
+    for (final term in value.split(',')) {
+      final trimmed = term.trim();
+      if (trimmed.isEmpty) continue;
+      if (terms.contains(trimmed)) continue;
+      terms.add(trimmed);
+    }
+    notifyListeners();
+  }
+
+  void remove(String term) {
+    terms.remove(term);
+    notifyListeners();
+  }
+}
+
+class FilterUI extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Consumer<Filter>(
+      builder: (context, filter, child) {
+        return Padding(
+          padding: EdgeInsets.all(8.0),
+          child: Wrap(
+            spacing: 12.0,
+            alignment: WrapAlignment.start,
+            children: [
+              for (final term in filter.terms)
+                InputChip(
+                    label: Text(term), onDeleted: () => filter.remove(term)),
+              AddWidget(filter),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+class AddWidget extends StatelessWidget {
+  final Filter filter;
+  AddWidget(this.filter);
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      width: 200.0,
+      height: 40.0,
+      child: TextField(
+        decoration: InputDecoration(hintText: 'Test or configuration prefix'),
+        onSubmitted: (value) => filter.addAll(value),
+      ),
+    );
+  }
+}
diff --git a/ui_current_results/lib/main.dart b/ui_current_results/lib/main.dart
new file mode 100644
index 0000000..5efcdd4
--- /dev/null
+++ b/ui_current_results/lib/main.dart
@@ -0,0 +1,149 @@
+// 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:clippy/browser.dart' as clippy;
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'dart:html' as html;
+
+import 'filter.dart';
+import 'query.dart';
+import 'results.dart';
+
+void main() {
+  runApp(CurrentResultsAppProviders());
+}
+
+class CurrentResultsAppProviders extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+        title: 'Current Results',
+        theme: ThemeData(
+          primarySwatch: Colors.blue,
+          visualDensity: VisualDensity.compact,
+        ),
+        home: MultiProvider(
+            providers: [
+              ChangeNotifierProvider(create: (context) => Filter()),
+              ChangeNotifierProxyProvider<Filter, QueryResults>(
+                  create: (context) => QueryResults(),
+                  update: (context, filter, queryResults) {
+                    return queryResults
+                      ..filter = filter
+                      ..fetchCurrentResults();
+                  })
+            ],
+            child: DefaultTabController(
+              length: 2,
+              child: CurrentResultsApp(),
+            )));
+  }
+}
+
+class CurrentResultsApp extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Align(
+      alignment: Alignment.topLeft,
+      child: Container(
+        constraints: BoxConstraints(maxWidth: 808.0),
+        decoration: BoxDecoration(
+            border: Border(right: BorderSide(color: Colors.black))),
+        child: Scaffold(
+          appBar: AppBar(
+            leading: Image.asset('dart_64.png', width: 16.0, height: 32.0),
+            title: Text(
+              'Current Results',
+              style: TextStyle(
+                fontSize: 24.0,
+                color: Color.fromARGB(255, 63, 81, 181),
+              ),
+            ),
+            backgroundColor: Colors.white,
+            bottom: TabBar(
+              tabs: [
+                Tab(text: 'ALL'),
+                Tab(text: 'FAILURES'),
+              ],
+              indicatorColor: Color.fromARGB(255, 63, 81, 181),
+              labelColor: Color.fromARGB(255, 63, 81, 181),
+            ),
+          ),
+          persistentFooterButtons: [
+            JsonLink(),
+            textPopup(),
+          ],
+          body: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              FilterUI(),
+              Expanded(
+                child: Consumer<QueryResults>(
+                  builder: (context, results, child) => TabBarView(
+                    children: [
+                      ResultsPanel(results, showAll: true),
+                      ResultsPanel(results, showAll: false)
+                    ],
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class JsonLink extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) => Consumer<Filter>(
+        builder: (context, filter, child) => FlatButton(
+          child: Text('json'),
+          onPressed: () => html.window.open(
+              Uri.https(apiHost, 'v1/results', {
+                'filter': filter.terms.join(','),
+                'pageSize': '4000'
+              }).toString(),
+              '_blank'),
+        ),
+      );
+}
+
+Widget textPopup() {
+  return Consumer<QueryResults>(
+    builder: (context, QueryResults results, child) {
+      return FlatButton(
+        child: Text('text'),
+        onPressed: () => showDialog(
+          context: context,
+          builder: (BuildContext context) {
+            final text = [resultTextHeader]
+                .followedBy(
+                    results.resultsObject.results.map(resultAsCommaSeparated))
+                .join('\n');
+            return AlertDialog(
+              title: Text('Results query as text'),
+              content: SelectableText(text),
+              actions: <Widget>[
+                FlatButton(
+                  child: Text('Copy and dismiss'),
+                  onPressed: () {
+                    clippy.write(text);
+                    Navigator.of(context).pop();
+                  },
+                ),
+                FlatButton(
+                  child: Text('Dismiss'),
+                  onPressed: () => Navigator.of(context).pop(),
+                ),
+              ],
+            );
+          },
+        ),
+      );
+    },
+  );
+}
diff --git a/ui_current_results/lib/query.dart b/ui_current_results/lib/query.dart
new file mode 100644
index 0000000..8e47075
--- /dev/null
+++ b/ui_current_results/lib/query.dart
@@ -0,0 +1,78 @@
+// 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:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+
+import 'src/generated/query.pb.dart';
+import 'filter.dart';
+
+const String apiHost = 'current-results-rest-zlujsyuhha-uc.a.run.app';
+// Current endpoints proxy is limited to 1 MB response size,
+// so we limit results fetched to 4000.
+// Implement paging on the service in the future to remove this limit.
+const int fetchLimit = 4000;
+const int maxFetchedResults = 100 * fetchLimit;
+
+class QueryResults extends ChangeNotifier {
+  Filter filter;
+  bool showAll = true;
+  List<String> names = [];
+  Map<String, Map<ChangeInResult, List<Result>>> grouped = {};
+  bool partialResults = true;
+
+  GetResultsResponse resultsObject = GetResultsResponse.create();
+
+  void fetchCurrentResults() async {
+    final client = http.Client();
+    final resultsQuery = Uri.https(apiHost, 'v1/results',
+        {'filter': filter.terms.join(','), 'pageSize': '$fetchLimit'});
+    final resultsResponse = await client.get(resultsQuery);
+    resultsObject = GetResultsResponse.create()
+      ..mergeFromProto3Json(json.decode(resultsResponse.body));
+    final results = resultsObject.results;
+
+    grouped = groupBy<Result, String>(results, (Result result) => result.name)
+        .map((String name, List<Result> list) => MapEntry(name,
+            groupBy<Result, ChangeInResult>(list, ChangeInResult.fromResult)));
+    names = grouped.keys.toList()..sort();
+    partialResults = results.length == fetchLimit;
+    notifyListeners();
+  }
+}
+
+class ChangeInResult {
+  static const Color lightCoral = Color.fromARGB(255, 240, 128, 128);
+  static const Color gold = Color.fromARGB(255, 255, 215, 0);
+
+  String result;
+  String expected;
+  bool flaky;
+  bool get matches => result == expected;
+  Color get backgroundColor =>
+      flaky ? gold : matches ? Colors.lightGreen : lightCoral;
+
+  ChangeInResult._(this.result, this.expected, this.flaky);
+  static ChangeInResult fromResult(Result result) =>
+      ChangeInResult._(result.result, result.expected, result.flaky);
+
+  String toString() => flaky
+      ? "flaky (latest result $result expected $expected"
+      : "$result (expected $expected)";
+  bool operator ==(Object other) => toString() == other.toString();
+  int get hashCode => toString().hashCode;
+}
+
+String resultAsCommaSeparated(Result result) => [
+      result.name,
+      result.configuration,
+      result.result,
+      result.expected,
+      result.flaky,
+      result.timeMs
+    ].join(',');
+
+String resultTextHeader = "name,configuration,result,expected,flaky,timeMs";
diff --git a/ui_current_results/lib/results.dart b/ui_current_results/lib/results.dart
new file mode 100644
index 0000000..6c2fa13
--- /dev/null
+++ b/ui_current_results/lib/results.dart
@@ -0,0 +1,106 @@
+// 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 'dart:html' as html;
+
+import 'package:flutter/material.dart';
+
+import 'query.dart';
+
+class ResultsPanel extends StatelessWidget {
+  final QueryResults queryResults;
+  final bool showAll;
+
+  ResultsPanel(this.queryResults, {this.showAll = true});
+
+  @override
+  Widget build(BuildContext context) {
+    return Align(
+      alignment: Alignment.topLeft,
+      child: SizedBox(
+        width: 800.0,
+        child: Container(
+          decoration: BoxDecoration(border: Border.all(color: Colors.black)),
+          child: ListView.builder(
+            itemCount: queryResults.names.length,
+            itemBuilder: itemBuilder(queryResults),
+          ),
+        ),
+      ),
+    );
+  }
+
+  IndexedWidgetBuilder itemBuilder(QueryResults results) {
+    return (BuildContext context, int index) {
+      final name = results.names[index];
+      final changeGroups = results.grouped[name];
+      if (!showAll && changeGroups.keys.every((change) => change.matches)) {
+        // Inserting this seems to break history icons on remaining tests.
+        return Container(height: 0.0, width: 0.0);
+      }
+      return Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          SizedOverflowBox(
+            size: Size(600.0, 18.0),
+            alignment: Alignment.topLeft,
+            child: Padding(
+              padding: EdgeInsets.only(top: 0.0, left: 8.0),
+              child: Row(
+                children: [
+                  Flexible(
+                    child: SingleChildScrollView(
+                      scrollDirection: Axis.horizontal,
+                      reverse: true,
+                      child: SelectableText(
+                        results.partialResults
+                            ? '$name (partial results)'
+                            : name,
+                        style: TextStyle(fontSize: 16.0),
+                      ),
+                    ),
+                  ),
+                  IconButton(
+                    icon: Icon(Icons.history),
+                    onPressed: () => html.window.open(
+                        Uri(
+                                path: '/',
+                                fragment: showAll
+                                    ? 'showLatestFailures=false&test=$name'
+                                    : 'test=$name')
+                            .toString(),
+                        '_blank'),
+                  ),
+                ],
+              ),
+            ),
+          ),
+          for (final change
+              in changeGroups.keys.where((key) => showAll || !key.matches))
+            Container(
+              alignment: Alignment.topLeft,
+              constraints: BoxConstraints.loose(Size.fromWidth(500.0)),
+              child: ExpansionTile(
+                key: Key(name + change.toString()),
+                title: Text(
+                  '$change (${changeGroups[change].length} configurations)',
+                  style: TextStyle(
+                    backgroundColor: change.backgroundColor,
+                    fontSize: 14.0,
+                  ),
+                ),
+                expandedAlignment: Alignment.topLeft,
+                expandedCrossAxisAlignment: CrossAxisAlignment.start,
+                childrenPadding: EdgeInsets.only(left: 48.0, bottom: 4.0),
+                children: [
+                  for (final result in changeGroups[change])
+                    SelectableText(result.configuration),
+                ],
+              ),
+            ),
+        ],
+      );
+    };
+  }
+}
diff --git a/ui_current_results/lib/src/generated/google/protobuf/empty.pb.dart b/ui_current_results/lib/src/generated/google/protobuf/empty.pb.dart
new file mode 100644
index 0000000..bc2a099
--- /dev/null
+++ b/ui_current_results/lib/src/generated/google/protobuf/empty.pb.dart
@@ -0,0 +1,38 @@
+///
+//  Generated code. Do not modify.
+//  source: google/protobuf/empty.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:core' as $core;
+
+import 'package:protobuf/protobuf.dart' as $pb;
+
+class Empty extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('Empty',
+      package: const $pb.PackageName('google.protobuf'),
+      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);
+  Empty clone() => Empty()..mergeFromMessage(this);
+  Empty copyWith(void Function(Empty) updates) =>
+      super.copyWith((message) => updates(message as Empty));
+  $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;
+}
diff --git a/ui_current_results/lib/src/generated/google/protobuf/empty.pbenum.dart b/ui_current_results/lib/src/generated/google/protobuf/empty.pbenum.dart
new file mode 100644
index 0000000..0fb3ebb
--- /dev/null
+++ b/ui_current_results/lib/src/generated/google/protobuf/empty.pbenum.dart
@@ -0,0 +1,6 @@
+///
+//  Generated code. Do not modify.
+//  source: google/protobuf/empty.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
diff --git a/ui_current_results/lib/src/generated/google/protobuf/empty.pbjson.dart b/ui_current_results/lib/src/generated/google/protobuf/empty.pbjson.dart
new file mode 100644
index 0000000..588c6a0
--- /dev/null
+++ b/ui_current_results/lib/src/generated/google/protobuf/empty.pbjson.dart
@@ -0,0 +1,10 @@
+///
+//  Generated code. Do not modify.
+//  source: google/protobuf/empty.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
+
+const Empty$json = const {
+  '1': 'Empty',
+};
diff --git a/ui_current_results/lib/src/generated/google/protobuf/empty.pbserver.dart b/ui_current_results/lib/src/generated/google/protobuf/empty.pbserver.dart
new file mode 100644
index 0000000..7a64fc1
--- /dev/null
+++ b/ui_current_results/lib/src/generated/google/protobuf/empty.pbserver.dart
@@ -0,0 +1,8 @@
+///
+//  Generated code. Do not modify.
+//  source: google/protobuf/empty.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
+
+export 'empty.pb.dart';
diff --git a/ui_current_results/lib/src/generated/query.pb.dart b/ui_current_results/lib/src/generated/query.pb.dart
new file mode 100644
index 0000000..c5e7833
--- /dev/null
+++ b/ui_current_results/lib/src/generated/query.pb.dart
@@ -0,0 +1,513 @@
+///
+//  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 'dart:core' as $core;
+
+import 'package:protobuf/protobuf.dart' as $pb;
+
+import 'google/protobuf/empty.pb.dart' as $0;
+
+class GetResultsRequest extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('GetResultsRequest',
+      package: const $pb.PackageName('current_results'),
+      createEmptyInstance: create)
+    ..pPS(1, 'names')
+    ..pPS(2, 'configurations')
+    ..a<$core.int>(3, 'pageSize', $pb.PbFieldType.O3)
+    ..aOS(4, 'pageToken')
+    ..hasRequiredFields = false;
+
+  GetResultsRequest._() : super();
+  factory GetResultsRequest() => create();
+  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);
+  GetResultsRequest clone() => GetResultsRequest()..mergeFromMessage(this);
+  GetResultsRequest copyWith(void Function(GetResultsRequest) updates) =>
+      super.copyWith((message) => updates(message as GetResultsRequest));
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static GetResultsRequest create() => GetResultsRequest._();
+  GetResultsRequest createEmptyInstance() => create();
+  static $pb.PbList<GetResultsRequest> createRepeated() =>
+      $pb.PbList<GetResultsRequest>();
+  @$core.pragma('dart2js:noInline')
+  static GetResultsRequest getDefault() => _defaultInstance ??=
+      $pb.GeneratedMessage.$_defaultFor<GetResultsRequest>(create);
+  static GetResultsRequest _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.List<$core.String> get names => $_getList(0);
+
+  @$pb.TagNumber(2)
+  $core.List<$core.String> get configurations => $_getList(1);
+
+  @$pb.TagNumber(3)
+  $core.int get pageSize => $_getIZ(2);
+  @$pb.TagNumber(3)
+  set pageSize($core.int v) {
+    $_setSignedInt32(2, v);
+  }
+
+  @$pb.TagNumber(3)
+  $core.bool hasPageSize() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearPageSize() => clearField(3);
+
+  @$pb.TagNumber(4)
+  $core.String get pageToken => $_getSZ(3);
+  @$pb.TagNumber(4)
+  set pageToken($core.String v) {
+    $_setString(3, v);
+  }
+
+  @$pb.TagNumber(4)
+  $core.bool hasPageToken() => $_has(3);
+  @$pb.TagNumber(4)
+  void clearPageToken() => clearField(4);
+}
+
+class GetResultsResponse extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('GetResultsResponse',
+      package: const $pb.PackageName('current_results'),
+      createEmptyInstance: create)
+    ..pc<Result>(1, 'results', $pb.PbFieldType.PM, subBuilder: Result.create)
+    ..aOS(2, 'nextPageToken')
+    ..hasRequiredFields = false;
+
+  GetResultsResponse._() : super();
+  factory GetResultsResponse() => create();
+  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);
+  GetResultsResponse clone() => GetResultsResponse()..mergeFromMessage(this);
+  GetResultsResponse copyWith(void Function(GetResultsResponse) updates) =>
+      super.copyWith((message) => updates(message as GetResultsResponse));
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static GetResultsResponse create() => GetResultsResponse._();
+  GetResultsResponse createEmptyInstance() => create();
+  static $pb.PbList<GetResultsResponse> createRepeated() =>
+      $pb.PbList<GetResultsResponse>();
+  @$core.pragma('dart2js:noInline')
+  static GetResultsResponse getDefault() => _defaultInstance ??=
+      $pb.GeneratedMessage.$_defaultFor<GetResultsResponse>(create);
+  static GetResultsResponse _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.List<Result> get results => $_getList(0);
+
+  @$pb.TagNumber(2)
+  $core.String get nextPageToken => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set nextPageToken($core.String v) {
+    $_setString(1, v);
+  }
+
+  @$pb.TagNumber(2)
+  $core.bool hasNextPageToken() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearNextPageToken() => clearField(2);
+}
+
+class Result extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('Result',
+      package: const $pb.PackageName('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)
+    ..hasRequiredFields = false;
+
+  Result._() : super();
+  factory Result() => create();
+  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);
+  Result clone() => Result()..mergeFromMessage(this);
+  Result copyWith(void Function(Result) updates) =>
+      super.copyWith((message) => updates(message as Result));
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static Result create() => Result._();
+  Result createEmptyInstance() => create();
+  static $pb.PbList<Result> createRepeated() => $pb.PbList<Result>();
+  @$core.pragma('dart2js:noInline')
+  static Result getDefault() =>
+      _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Result>(create);
+  static Result _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get name => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set name($core.String v) {
+    $_setString(0, v);
+  }
+
+  @$pb.TagNumber(1)
+  $core.bool hasName() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearName() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get configuration => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set configuration($core.String v) {
+    $_setString(1, v);
+  }
+
+  @$pb.TagNumber(2)
+  $core.bool hasConfiguration() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearConfiguration() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.String get result => $_getSZ(2);
+  @$pb.TagNumber(3)
+  set result($core.String v) {
+    $_setString(2, v);
+  }
+
+  @$pb.TagNumber(3)
+  $core.bool hasResult() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearResult() => clearField(3);
+
+  @$pb.TagNumber(4)
+  $core.String get expected => $_getSZ(3);
+  @$pb.TagNumber(4)
+  set expected($core.String v) {
+    $_setString(3, v);
+  }
+
+  @$pb.TagNumber(4)
+  $core.bool hasExpected() => $_has(3);
+  @$pb.TagNumber(4)
+  void clearExpected() => clearField(4);
+
+  @$pb.TagNumber(5)
+  $core.bool get flaky => $_getBF(4);
+  @$pb.TagNumber(5)
+  set flaky($core.bool v) {
+    $_setBool(4, v);
+  }
+
+  @$pb.TagNumber(5)
+  $core.bool hasFlaky() => $_has(4);
+  @$pb.TagNumber(5)
+  void clearFlaky() => clearField(5);
+
+  @$pb.TagNumber(6)
+  $core.int get timeMs => $_getIZ(5);
+  @$pb.TagNumber(6)
+  set timeMs($core.int v) {
+    $_setSignedInt32(5, v);
+  }
+
+  @$pb.TagNumber(6)
+  $core.bool hasTimeMs() => $_has(5);
+  @$pb.TagNumber(6)
+  void clearTimeMs() => clearField(6);
+}
+
+class ListTestsRequest extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListTestsRequest',
+      package: const $pb.PackageName('current_results'),
+      createEmptyInstance: create)
+    ..aOS(1, 'prefix')
+    ..a<$core.int>(2, 'limit', $pb.PbFieldType.O3)
+    ..hasRequiredFields = false;
+
+  ListTestsRequest._() : super();
+  factory ListTestsRequest() => create();
+  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);
+  ListTestsRequest clone() => ListTestsRequest()..mergeFromMessage(this);
+  ListTestsRequest copyWith(void Function(ListTestsRequest) updates) =>
+      super.copyWith((message) => updates(message as ListTestsRequest));
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static ListTestsRequest create() => ListTestsRequest._();
+  ListTestsRequest createEmptyInstance() => create();
+  static $pb.PbList<ListTestsRequest> createRepeated() =>
+      $pb.PbList<ListTestsRequest>();
+  @$core.pragma('dart2js:noInline')
+  static ListTestsRequest getDefault() => _defaultInstance ??=
+      $pb.GeneratedMessage.$_defaultFor<ListTestsRequest>(create);
+  static ListTestsRequest _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get prefix => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set prefix($core.String v) {
+    $_setString(0, v);
+  }
+
+  @$pb.TagNumber(1)
+  $core.bool hasPrefix() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearPrefix() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.int get limit => $_getIZ(1);
+  @$pb.TagNumber(2)
+  set limit($core.int v) {
+    $_setSignedInt32(1, v);
+  }
+
+  @$pb.TagNumber(2)
+  $core.bool hasLimit() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearLimit() => clearField(2);
+}
+
+class ListTestsResponse extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo('ListTestsResponse',
+      package: const $pb.PackageName('current_results'),
+      createEmptyInstance: create)
+    ..pPS(1, 'names')
+    ..hasRequiredFields = false;
+
+  ListTestsResponse._() : super();
+  factory ListTestsResponse() => create();
+  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);
+  ListTestsResponse clone() => ListTestsResponse()..mergeFromMessage(this);
+  ListTestsResponse copyWith(void Function(ListTestsResponse) updates) =>
+      super.copyWith((message) => updates(message as ListTestsResponse));
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static ListTestsResponse create() => ListTestsResponse._();
+  ListTestsResponse createEmptyInstance() => create();
+  static $pb.PbList<ListTestsResponse> createRepeated() =>
+      $pb.PbList<ListTestsResponse>();
+  @$core.pragma('dart2js:noInline')
+  static ListTestsResponse getDefault() => _defaultInstance ??=
+      $pb.GeneratedMessage.$_defaultFor<ListTestsResponse>(create);
+  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'),
+      createEmptyInstance: create)
+    ..aOS(1, 'prefix')
+    ..hasRequiredFields = false;
+
+  ListConfigurationsRequest._() : super();
+  factory ListConfigurationsRequest() => create();
+  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);
+  ListConfigurationsRequest clone() =>
+      ListConfigurationsRequest()..mergeFromMessage(this);
+  ListConfigurationsRequest copyWith(
+          void Function(ListConfigurationsRequest) updates) =>
+      super
+          .copyWith((message) => updates(message as ListConfigurationsRequest));
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static ListConfigurationsRequest create() => ListConfigurationsRequest._();
+  ListConfigurationsRequest createEmptyInstance() => create();
+  static $pb.PbList<ListConfigurationsRequest> createRepeated() =>
+      $pb.PbList<ListConfigurationsRequest>();
+  @$core.pragma('dart2js:noInline')
+  static ListConfigurationsRequest getDefault() => _defaultInstance ??=
+      $pb.GeneratedMessage.$_defaultFor<ListConfigurationsRequest>(create);
+  static ListConfigurationsRequest _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get prefix => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set prefix($core.String v) {
+    $_setString(0, v);
+  }
+
+  @$pb.TagNumber(1)
+  $core.bool hasPrefix() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearPrefix() => clearField(1);
+}
+
+class ListConfigurationsResponse extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      'ListConfigurationsResponse',
+      package: const $pb.PackageName('current_results'),
+      createEmptyInstance: create)
+    ..pPS(1, 'configurations')
+    ..hasRequiredFields = false;
+
+  ListConfigurationsResponse._() : super();
+  factory ListConfigurationsResponse() => create();
+  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);
+  ListConfigurationsResponse clone() =>
+      ListConfigurationsResponse()..mergeFromMessage(this);
+  ListConfigurationsResponse copyWith(
+          void Function(ListConfigurationsResponse) updates) =>
+      super.copyWith(
+          (message) => updates(message as ListConfigurationsResponse));
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static ListConfigurationsResponse create() => ListConfigurationsResponse._();
+  ListConfigurationsResponse createEmptyInstance() => create();
+  static $pb.PbList<ListConfigurationsResponse> createRepeated() =>
+      $pb.PbList<ListConfigurationsResponse>();
+  @$core.pragma('dart2js:noInline')
+  static ListConfigurationsResponse getDefault() => _defaultInstance ??=
+      $pb.GeneratedMessage.$_defaultFor<ListConfigurationsResponse>(create);
+  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'),
+      createEmptyInstance: create)
+    ..pc<ConfigurationUpdate>(1, 'updates', $pb.PbFieldType.PM,
+        subBuilder: ConfigurationUpdate.create)
+    ..hasRequiredFields = false;
+
+  FetchResponse._() : super();
+  factory FetchResponse() => create();
+  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);
+  FetchResponse clone() => FetchResponse()..mergeFromMessage(this);
+  FetchResponse copyWith(void Function(FetchResponse) updates) =>
+      super.copyWith((message) => updates(message as FetchResponse));
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static FetchResponse create() => FetchResponse._();
+  FetchResponse createEmptyInstance() => create();
+  static $pb.PbList<FetchResponse> createRepeated() =>
+      $pb.PbList<FetchResponse>();
+  @$core.pragma('dart2js:noInline')
+  static FetchResponse getDefault() => _defaultInstance ??=
+      $pb.GeneratedMessage.$_defaultFor<FetchResponse>(create);
+  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'),
+      createEmptyInstance: create)
+    ..aOS(1, 'configuration')
+    ..hasRequiredFields = false;
+
+  ConfigurationUpdate._() : super();
+  factory ConfigurationUpdate() => create();
+  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);
+  ConfigurationUpdate clone() => ConfigurationUpdate()..mergeFromMessage(this);
+  ConfigurationUpdate copyWith(void Function(ConfigurationUpdate) updates) =>
+      super.copyWith((message) => updates(message as ConfigurationUpdate));
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static ConfigurationUpdate create() => ConfigurationUpdate._();
+  ConfigurationUpdate createEmptyInstance() => create();
+  static $pb.PbList<ConfigurationUpdate> createRepeated() =>
+      $pb.PbList<ConfigurationUpdate>();
+  @$core.pragma('dart2js:noInline')
+  static ConfigurationUpdate getDefault() => _defaultInstance ??=
+      $pb.GeneratedMessage.$_defaultFor<ConfigurationUpdate>(create);
+  static ConfigurationUpdate _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get configuration => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set configuration($core.String v) {
+    $_setString(0, v);
+  }
+
+  @$pb.TagNumber(1)
+  $core.bool hasConfiguration() => $_has(0);
+  @$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/ui_current_results/lib/src/generated/query.pbenum.dart b/ui_current_results/lib/src/generated/query.pbenum.dart
new file mode 100644
index 0000000..277e345
--- /dev/null
+++ b/ui_current_results/lib/src/generated/query.pbenum.dart
@@ -0,0 +1,6 @@
+///
+//  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
diff --git a/ui_current_results/lib/src/generated/query.pbjson.dart b/ui_current_results/lib/src/generated/query.pbjson.dart
new file mode 100644
index 0000000..9e4ee01
--- /dev/null
+++ b/ui_current_results/lib/src/generated/query.pbjson.dart
@@ -0,0 +1,158 @@
+///
+//  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 'google/protobuf/empty.pbjson.dart' as $0;
+
+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 GetResultsResponse$json = const {
+  '1': 'GetResultsResponse',
+  '2': const [
+    const {
+      '1': 'results',
+      '3': 1,
+      '4': 3,
+      '5': 11,
+      '6': '.current_results.Result',
+      '10': 'results'
+    },
+    const {
+      '1': 'next_page_token',
+      '3': 2,
+      '4': 1,
+      '5': 9,
+      '10': 'nextPageToken'
+    },
+  ],
+};
+
+const Result$json = const {
+  '1': 'Result',
+  '2': const [
+    const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
+    const {'1': 'configuration', '3': 2, '4': 1, '5': 9, '10': 'configuration'},
+    const {'1': 'result', '3': 3, '4': 1, '5': 9, '10': 'result'},
+    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 ListTestsRequest$json = const {
+  '1': 'ListTestsRequest',
+  '2': const [
+    const {'1': 'prefix', '3': 1, '4': 1, '5': 9, '10': 'prefix'},
+    const {'1': 'limit', '3': 2, '4': 1, '5': 5, '10': 'limit'},
+  ],
+};
+
+const ListTestsResponse$json = const {
+  '1': 'ListTestsResponse',
+  '2': const [
+    const {'1': 'names', '3': 1, '4': 3, '5': 9, '10': 'names'},
+  ],
+};
+
+const ListConfigurationsRequest$json = const {
+  '1': 'ListConfigurationsRequest',
+  '2': const [
+    const {'1': 'prefix', '3': 1, '4': 1, '5': 9, '10': 'prefix'},
+  ],
+};
+
+const ListConfigurationsResponse$json = const {
+  '1': 'ListConfigurationsResponse',
+  '2': const [
+    const {
+      '1': 'configurations',
+      '3': 1,
+      '4': 3,
+      '5': 9,
+      '10': 'configurations'
+    },
+  ],
+};
+
+const FetchResponse$json = const {
+  '1': 'FetchResponse',
+  '2': const [
+    const {
+      '1': 'updates',
+      '3': 1,
+      '4': 3,
+      '5': 11,
+      '6': '.current_results.ConfigurationUpdate',
+      '10': 'updates'
+    },
+  ],
+};
+
+const ConfigurationUpdate$json = const {
+  '1': 'ConfigurationUpdate',
+  '2': const [
+    const {'1': 'configuration', '3': 1, '4': 1, '5': 9, '10': 'configuration'},
+  ],
+};
+
+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,
+};
diff --git a/ui_current_results/lib/src/generated/query.pbserver.dart b/ui_current_results/lib/src/generated/query.pbserver.dart
new file mode 100644
index 0000000..ab2b362
--- /dev/null
+++ b/ui_current_results/lib/src/generated/query.pbserver.dart
@@ -0,0 +1,69 @@
+///
+//  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;
+}