[current results ui] Each test is a single row before expanding
Change-Id: I4bec1e3a46952e045e8c19543170575e0030a25c
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/159622
Reviewed-by: Alexander Thomas <athom@google.com>
Reviewed-by: Karl Klose <karlklose@google.com>
diff --git a/ui_current_results/lib/main.dart b/ui_current_results/lib/main.dart
index 5efcdd4..c03a6e8 100644
--- a/ui_current_results/lib/main.dart
+++ b/ui_current_results/lib/main.dart
@@ -72,6 +72,7 @@
),
),
persistentFooterButtons: [
+ ApiPortalLink(),
JsonLink(),
textPopup(),
],
@@ -97,10 +98,30 @@
}
}
+class ApiPortalLink extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Consumer<Filter>(
+ builder: (context, Filter filter, child) {
+ return FlatButton(
+ child: Text('API portal'),
+ onPressed: () => html.window.open(
+ 'https://endpointsportal.dart-ci-staging.cloud.goog'
+ '/docs/current-results-rest-zlujsyuhha-uc.a.run.app/g'
+ '/routes/v1/results/get',
+ '_blank'),
+ );
+ },
+ );
+ }
+}
+
class JsonLink extends StatelessWidget {
@override
- Widget build(BuildContext context) => Consumer<Filter>(
- builder: (context, filter, child) => FlatButton(
+ Widget build(BuildContext context) {
+ return Consumer<Filter>(
+ builder: (context, filter, child) {
+ return FlatButton(
child: Text('json'),
onPressed: () => html.window.open(
Uri.https(apiHost, 'v1/results', {
@@ -108,8 +129,10 @@
'pageSize': '4000'
}).toString(),
'_blank'),
- ),
- );
+ );
+ },
+ );
+ }
}
Widget textPopup() {
diff --git a/ui_current_results/lib/query.dart b/ui_current_results/lib/query.dart
index 8e47075..9270fc9 100644
--- a/ui_current_results/lib/query.dart
+++ b/ui_current_results/lib/query.dart
@@ -2,7 +2,6 @@
// 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';
@@ -21,6 +20,7 @@
Filter filter;
bool showAll = true;
List<String> names = [];
+ Map<String, Map<String, int>> counts = {};
Map<String, Map<ChangeInResult, List<Result>>> grouped = {};
bool partialResults = true;
@@ -35,9 +35,19 @@
..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)));
+ for (final result in results) {
+ grouped
+ .putIfAbsent(result.name, () => <ChangeInResult, List<Result>>{})
+ .putIfAbsent(ChangeInResult(result), () => <Result>[])
+ .add(result);
+ }
+ for (final name in grouped.keys) {
+ final count = counts[name] = <String, int>{};
+ for (final change in grouped[name].keys) {
+ count.putIfAbsent(change.kind, () => 0);
+ count[change.kind] += grouped[name][change].length;
+ }
+ }
names = grouped.keys.toList()..sort();
partialResults = results.length == fetchLimit;
notifyListeners();
@@ -45,25 +55,27 @@
}
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;
+ final String result;
+ final String expected;
+ final bool flaky;
+ final String text;
bool get matches => result == expected;
- Color get backgroundColor =>
- flaky ? gold : matches ? Colors.lightGreen : lightCoral;
+ String get kind => flaky ? 'flaky' : matches ? 'pass' : 'fail';
- ChangeInResult._(this.result, this.expected, this.flaky);
- static ChangeInResult fromResult(Result result) =>
- ChangeInResult._(result.result, result.expected, result.flaky);
+ ChangeInResult(Result result)
+ : this._(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;
+ ChangeInResult._(this.result, this.expected, this.flaky)
+ : text = flaky
+ ? "flaky (latest result $result expected $expected"
+ : "$result (expected $expected)";
+
+ @override
+ String toString() => text;
+ @override
+ bool operator ==(Object other) => text == (other as ChangeInResult)?.text;
+ @override
+ int get hashCode => text.hashCode;
}
String resultAsCommaSeparated(Result result) => [
diff --git a/ui_current_results/lib/results.dart b/ui_current_results/lib/results.dart
index 6c2fa13..0ead7a5 100644
--- a/ui_current_results/lib/results.dart
+++ b/ui_current_results/lib/results.dart
@@ -5,26 +5,46 @@
import 'dart:html' as html;
import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
import 'query.dart';
-class ResultsPanel extends StatelessWidget {
+class ResultsPanel extends StatefulWidget {
final QueryResults queryResults;
final bool showAll;
ResultsPanel(this.queryResults, {this.showAll = true});
@override
+ ResultsPanelState createState() => ResultsPanelState();
+}
+
+class ResultsPanelState extends State<ResultsPanel> {
+ static const Color lightCoral = Color.fromARGB(255, 240, 128, 128);
+ static const Color gold = Color.fromARGB(255, 255, 215, 0);
+ static const resultColors = {
+ 'pass': Colors.lightGreen,
+ 'flaky': gold,
+ 'fail': lightCoral,
+ };
+
+ static const kinds = ['pass', 'fail', 'flaky'];
+ List<bool> expanded = [];
+
+ @override
Widget build(BuildContext context) {
+ if (expanded.length != widget.queryResults.names.length) {
+ expanded = List<bool>.filled(widget.queryResults.names.length, false);
+ }
return Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 800.0,
child: Container(
- decoration: BoxDecoration(border: Border.all(color: Colors.black)),
+ // decoration: BoxDecoration(border: Border.all(color: Colors.black)),
child: ListView.builder(
- itemCount: queryResults.names.length,
- itemBuilder: itemBuilder(queryResults),
+ itemCount: widget.queryResults.names.length,
+ itemBuilder: itemBuilder(widget.queryResults),
),
),
),
@@ -35,70 +55,83 @@
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.
+ final counts = results.counts[name];
+ if (!widget.showAll &&
+ changeGroups.keys.every((change) => change.matches)) {
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),
- ),
+ Container(
+ height: 28.0,
+ padding: EdgeInsets.only(top: 0.0, left: 8.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ IconButton(
+ icon: Icon(
+ expanded[index] ? Icons.expand_less : Icons.expand_more),
+ onPressed: () =>
+ setState(() => expanded[index] = !expanded[index]),
+ ),
+ for (final kind in kinds)
+ Container(
+ width: 20,
+ alignment: Alignment.bottomCenter,
+ color: counts.containsKey(kind)
+ ? resultColors[kind]
+ : Colors.white,
+ child: Text('${counts[kind] ?? ''}',
+ style: TextStyle(fontSize: 14.0)),
+ ),
+ Flexible(
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ reverse: true,
+ child: SelectableText(
+ results.partialResults ? '$name (partial results)' : name,
+ style: TextStyle(fontSize: 16.0),
),
),
- IconButton(
+ ),
+ IconButton(
icon: Icon(Icons.history),
onPressed: () => html.window.open(
Uri(
path: '/',
- fragment: showAll
+ fragment: widget.showAll
? 'showLatestFailures=false&test=$name'
: 'test=$name')
.toString(),
- '_blank'),
- ),
- ],
- ),
+ '_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,
- ),
+ if (expanded[index])
+ for (final change in changeGroups.keys
+ .where((key) => widget.showAll || !key.matches))
+ Container(
+ alignment: Alignment.topLeft,
+ padding: EdgeInsets.only(left: 48.0),
+ constraints: BoxConstraints.loose(Size.fromWidth(500.0)),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ padding: EdgeInsets.only(top: 12.0),
+ child: Text(
+ '$change (${changeGroups[change].length} configurations)',
+ style: TextStyle(
+ backgroundColor: resultColors[change.kind],
+ fontSize: 16.0)),
+ ),
+ for (final result in changeGroups[change])
+ SelectableText(result.configuration),
+ ],
),
- 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),
- ],
),
- ),
+ if (expanded[index]) SizedBox(height: 12.0),
],
);
};