blob: e2d01e3ccfca006abad7916ed7ca35beaa0a8883 [file] [log] [blame]
// Copyright 2019 The Flutter Authors. 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:url_launcher/url_launcher.dart';
import 'package:cocoon_service/protos.dart' show Commit, Task;
import '../agent_dashboard_page.dart';
import '../logic/qualified_task.dart';
import '../state/build.dart';
import 'luci_task_attempt_summary.dart';
import 'progress_button.dart';
import 'task_attempt_summary.dart';
import 'task_box.dart';
/// Displays the information from [Task] and allows interacting with a [Task].
///
/// This is intended to be inserted in an [OverlayEntry] as it requires
/// [closeCallback] that will remove the widget from the tree.
class TaskOverlayEntry extends StatelessWidget {
const TaskOverlayEntry({
Key key,
@required this.position,
@required this.task,
@required this.showSnackBarCallback,
@required this.closeCallback,
@required this.buildState,
@required this.commit,
}) : assert(position != null),
assert(buildState != null),
assert(task != null),
assert(showSnackBarCallback != null),
assert(closeCallback != null),
super(key: key);
/// The global position where to show the task overlay.
final Offset position;
/// The [Task] to display in the overlay.
final Task task;
final ShowSnackBarCallback showSnackBarCallback;
/// This callback removes the parent overlay from the widget tree.
///
/// On a click that is outside the area of the overlay (the rest of the screen),
/// this callback is called closing the overlay.
final VoidCallback closeCallback;
/// A reference to the [BuildState] for performing operations on this [Task].
final BuildState buildState;
/// [Commit] for cirrus tasks to show log.
final Commit commit;
@override
Widget build(BuildContext context) {
// If this is ever positioned not at the top-left of the viewport, then
// we should make sure to convert the position to the Overlay's coordinate
// space otherwise it'll be misaligned.
return Stack(
children: <Widget>[
// This is a focus container to emphasize the cell that this
// [Overlay] is currently showing information from.
Positioned(
top: position.dy,
left: position.dx,
width: TaskBox.cellSize,
height: TaskBox.cellSize,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 4.0),
color: Colors.white70,
),
),
),
// This is the area a user can click (the rest of the screen) to close the overlay.
GestureDetector(
onTap: closeCallback,
behavior: HitTestBehavior.opaque,
child: const SizedBox.expand(),
),
Positioned(
// Move this overlay to be where the parent is
// TODO(ianh): This will go past the edge of the page if it's near the right margin;
// we should do something like positionDependentBox.
top: position.dy + TaskBox.cellSize / 2.0,
left: position.dx + TaskBox.cellSize / 2.0,
child: TaskOverlayContents(
showSnackBarCallback: showSnackBarCallback,
buildState: buildState,
task: task,
commit: commit,
closeCallback: closeCallback,
),
),
],
);
}
}
/// Displays the information from [Task] and allows interacting with a [Task].
///
/// This is intended to be inserted in [TaskOverlayEntry].
///
/// Offers the functionality of opening the log for this [Task] and rerunning
/// this [Task] through the build system.
class TaskOverlayContents extends StatelessWidget {
const TaskOverlayContents({
Key key,
@required this.showSnackBarCallback,
@required this.buildState,
@required this.task,
@required this.closeCallback,
this.commit,
}) : assert(showSnackBarCallback != null),
assert(buildState != null),
assert(task != null),
super(key: key);
final ShowSnackBarCallback showSnackBarCallback;
/// A reference to the [BuildState] for performing operations on this [Task].
final BuildState buildState;
/// The [Task] to display in the overlay
final Task task;
/// [Commit] for cirrus tasks to show log.
final Commit commit;
/// This callback removes the parent overlay from the widget tree.
///
/// This is used in this scope to close this overlay on redirection to view
/// the agent for this task in the agent dashboard.
final void Function() closeCallback;
@visibleForTesting
static const String rerunErrorMessage = 'Failed to rerun task.';
@visibleForTesting
static const String rerunSuccessMessage = 'Devicelab is rerunning the task. This can take a minute to propagate.';
@visibleForTesting
static const Duration rerunSnackBarDuration = Duration(seconds: 15);
@visibleForTesting
static const String downloadLogErrorMessage = 'Failed to download task log.';
@visibleForTesting
static const Duration downloadLogSnackBarDuration = Duration(seconds: 15);
/// A lookup table to define the [Icon] for this task, based on
/// the values returned by [TaskBox.effectiveTaskStatus].
static const Map<String, Icon> statusIcon = <String, Icon>{
TaskBox.statusFailed: Icon(Icons.clear, color: Colors.red, size: 32),
TaskBox.statusNew: Icon(Icons.new_releases, color: Colors.blue, size: 32),
TaskBox.statusInProgress: Icon(Icons.autorenew, color: Colors.blue, size: 32),
TaskBox.statusSucceeded: Icon(Icons.check_circle, color: Colors.green, size: 32),
TaskBox.statusSucceededButFlaky: Icon(Icons.check_circle_outline, size: 32),
TaskBox.statusUnderperformed: Icon(Icons.new_releases, color: Colors.orange, size: 32),
TaskBox.statusUnderperformedInProgress: Icon(Icons.autorenew, color: Colors.orange, size: 32),
};
@override
Widget build(BuildContext context) {
final DateTime createTime = DateTime.fromMillisecondsSinceEpoch(task.createTimestamp.toInt());
final DateTime startTime = DateTime.fromMillisecondsSinceEpoch(task.startTimestamp.toInt());
final DateTime endTime = DateTime.fromMillisecondsSinceEpoch(task.endTimestamp.toInt());
final Duration queueDuration = startTime.difference(createTime);
final Duration runDuration = endTime.difference(startTime);
final String taskStatus = TaskBox.effectiveTaskStatus(task);
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Tooltip(
message: taskStatus,
child: Padding(
padding: const EdgeInsets.only(left: 8.0, top: 10.0, right: 12.0),
child: statusIcon[taskStatus],
),
),
Expanded(
child: ListBody(
children: <Widget>[
SelectableText(
task.name,
style: Theme.of(context).textTheme.bodyText1,
),
if (QualifiedTask.fromTask(task).isDevicelab)
Text(
'Attempts: ${task.attempts}\n'
'Run time: ${runDuration.inMinutes} minutes\n'
'Queue time: ${queueDuration.inSeconds} seconds\n'
'Flaky: ${task.isFlaky}',
style: Theme.of(context).textTheme.bodyText2,
)
else
Text(
'Task was run outside of devicelab',
style: Theme.of(context).textTheme.bodyText2,
),
if (QualifiedTask.fromTask(task).isDevicelab) TaskAttemptSummary(task: task),
if (QualifiedTask.fromTask(task).isLuci) LuciTaskAttemptSummary(task: task),
],
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
if (QualifiedTask.fromTask(task).isDevicelab)
RaisedButton(
child: Text.rich(
TextSpan(
text: 'SHOW ',
children: <TextSpan>[
TextSpan(
text: task.reservedForAgentId,
style: const TextStyle(fontStyle: FontStyle.italic),
),
],
),
),
onPressed: () {
// Close the current overlay
closeCallback();
// Open the agent dashboard
Navigator.pushNamed(
context,
AgentDashboardPage.routeName,
arguments: task.reservedForAgentId,
);
},
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ProgressButton(
child: const Text('DOWNLOAD ALL LOGS'),
onPressed: _viewLog,
),
),
if (QualifiedTask.fromTask(task).isDevicelab)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ProgressButton(
child: const Text('RERUN'),
onPressed: _rerunTask,
),
),
],
),
],
),
),
),
);
}
Future<void> _rerunTask() async {
final bool success = await buildState.rerunTask(task);
final Text snackBarText = success ? const Text(rerunSuccessMessage) : const Text(rerunErrorMessage);
showSnackBarCallback(
SnackBar(
content: snackBarText,
duration: rerunSnackBarDuration,
),
);
}
/// If [task] is in the devicelab, download the log. Otherwise, open the
/// url closest to where the log will be.
///
/// If a devicelab log fails to download, show an error snack bar.
Future<void> _viewLog() async {
if (QualifiedTask.fromTask(task).isDevicelab) {
final bool success = await buildState.downloadLog(task, commit);
if (!success) {
/// Only show [SnackBar] on failure since the user's device will
/// indicate a download has been made.
showSnackBarCallback(
const SnackBar(
content: Text(downloadLogErrorMessage),
duration: rerunSnackBarDuration,
),
);
}
return;
}
/// Tasks outside of devicelab have public logs that we just redirect to.
launch(logUrl(task, commit: commit));
}
}