blob: b2e5db41ffac9ac9d2e8aa297188318b832d1ac7 [file] [log] [blame]
// 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:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart' as url_launcher;
import 'filter.dart';
import 'query.dart';
import 'results.dart';
void main() {
runApp(const Providers());
}
class CurrentResultsApp extends StatelessWidget {
const CurrentResultsApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Current Results',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.compact,
),
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
final parameters = settings.name!.substring(1).split('&');
final terms = parameters
.firstWhere((parameter) => parameter.startsWith('filter='),
orElse: () => 'filter=')
.split('=')[1];
final filter = Filter(terms);
final showAll = parameters.contains('showAll');
final flakes = parameters.contains('flaky');
final tab = showAll
? 2
: flakes
? 1
: 0;
return NoTransitionPageRoute(
builder: (context) {
Provider.of<QueryResults>(context, listen: false).fetch(filter);
// Not allowed to set state of tab controller in this builder.
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<TabController>(context, listen: false).index = tab;
});
return const CurrentResultsScaffold();
},
settings: settings,
maintainState: false,
);
},
);
}
}
/// Provides access to an QueryResults object which notifies when new
/// results are fetched, a DefaultTabController widget used by the
/// TabBar, and to the TabController object created by that
/// DefaultTabController widget.
class Providers extends StatelessWidget {
const Providers({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => QueryResults(),
child: DefaultTabController(
length: 3,
initialIndex: 0,
child: Builder(
// ChangeNotifierProvider.value in a Builder is needed to make
// the TabController available for widgets to observe.
builder: (context) => ChangeNotifierProvider<TabController>.value(
value: DefaultTabController.of(context),
child: const CurrentResultsApp(),
),
),
),
);
}
}
class CurrentResultsScaffold extends StatelessWidget {
const CurrentResultsScaffold({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topLeft,
child: Scaffold(
appBar: AppBar(
leading: const Center(
child: FetchingProgress(),
),
title: const Text(
'Current Results',
style: TextStyle(fontSize: 24.0),
),
actions: [
Tooltip(
message: 'Send feeback!',
child: IconButton(
icon: const Icon(Icons.bug_report),
splashRadius: 20,
onPressed: () {
url_launcher.launchUrl(
Uri.https('github.com', '/dart-lang/dart_ci/issues'));
},
),
),
],
bottom: TabBar(
tabs: const [
Tab(text: 'FAILURES'),
Tab(text: 'FLAKES'),
Tab(text: 'ALL'),
],
onTap: (int tab) {
// We cannot compare to the previous value, it is gone.
pushRoute(context, tab: tab);
},
),
),
persistentFooterButtons: const [
ResultsSummary(),
TestSummary(),
ApiPortalLink(),
JsonLink(),
TextPopup(),
],
body: const SelectionArea(
child: Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: FilterUI(),
),
Divider(
color: Colors.black12,
height: 20,
),
Expanded(
child: ResultsPanel(),
),
],
)),
),
);
}
}
class ApiPortalLink extends StatelessWidget {
const ApiPortalLink();
@override
Widget build(BuildContext context) {
return TextButton(
child: const Text('API portal'),
onPressed: () => url_launcher.launchUrl(Uri.https(
'endpointsportal.dart-ci.cloud.goog',
'/docs/current-results-qvyo5rktwa-uc.a.run.app/g'
'/routes/v1/results/get',
)),
);
}
}
class JsonLink extends StatelessWidget {
const JsonLink();
@override
Widget build(BuildContext context) {
return Consumer<QueryResults>(
builder: (context, results, child) {
return TextButton(
child: const Text('JSON'),
onPressed: () => url_launcher.launchUrl(
Uri.https(apiHost, 'v1/results', {
'filter': results.filter.terms.join(','),
'pageSize': '4000',
}),
),
);
},
);
}
}
class TextPopup extends StatelessWidget {
const TextPopup();
@override
Widget build(BuildContext context) {
return Consumer<QueryResults>(
builder: (context, QueryResults results, child) {
return Tooltip(
message: 'Results query as text',
waitDuration: const Duration(milliseconds: 500),
child: TextButton(
child: const Text('Text'),
onPressed: () => showDialog(
context: context,
builder: (BuildContext context) {
final text = [resultTextHeader]
.followedBy(results.names
.expand((name) => results.grouped[name]!.values)
.expand((list) => list)
.map(resultAsCommaSeparated))
.join('\n');
return AlertDialog(
title: const Text('Results query as text'),
content: SelectableText(text),
actions: <Widget>[
TextButton(
child: const Text('Copy and dismiss'),
onPressed: () {
Clipboard.setData(ClipboardData(text: text));
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Dismiss'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
),
),
);
},
);
}
}
class NoTransitionPageRoute extends MaterialPageRoute {
NoTransitionPageRoute({
required WidgetBuilder builder,
RouteSettings? settings,
bool maintainState = true,
}) : super(
builder: builder,
settings: settings,
maintainState: maintainState,
);
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return child;
}
}
void pushRoute(context, {Iterable<String>? terms, int? tab}) {
if (terms == null && tab == null) {
throw ArgumentError('pushRoute calls must have a named argument');
}
tab ??= Provider.of<TabController>(context, listen: false).index;
terms ??= Provider.of<QueryResults>(context, listen: false).filter.terms;
final tabItems = [
if (tab == 2) 'showAll',
if (tab == 1) 'flaky',
];
Navigator.pushNamed(
context,
[
'/filter=${terms.join(',')}',
...tabItems,
].join('&'),
);
}