blob: bef6530ff3d494b9ebc52894af9b4342d5487e1d [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 'dart:async';
import 'package:cocoon_common/rpc_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../state/build.dart';
import '../widgets/scaffold.dart';
/// Shows diagnostic information about tree health, and allows manual disabling.
final class TreeStatusPage extends StatefulWidget {
const TreeStatusPage({super.key, this.queryParameters});
static const String routeSegment = 'status';
static const String routeName = '/$routeSegment';
final Map<String, String>? queryParameters;
@override
State<TreeStatusPage> createState() => _TreeStatusPageState();
}
class _TreeStatusPageState extends State<TreeStatusPage> {
late List<TreeStatusChange> changes;
late String? currentRepo;
@override
void initState() {
changes = [];
if (widget.queryParameters case final queryParameters?) {
currentRepo = queryParameters['repo'];
}
super.initState();
}
@override
void didChangeDependencies() {
unawaited(_fetchTreeStatusChanges());
super.didChangeDependencies();
}
Future<void> _fetchTreeStatusChanges() async {
final buildState = Provider.of<BuildState>(context, listen: false);
final response = await buildState.cocoonService.fetchTreeStatusChanges(
idToken: await buildState.authService.idToken,
repo: currentRepo ?? buildState.currentRepo,
);
if (response.data case final data?) {
setState(() {
changes = data;
});
}
}
void _updateNavigation(
BuildContext context, {
required String repo,
required String branch,
}) {
final queryParameters = {
...?widget.queryParameters,
'repo': repo,
'branch': branch,
};
final uri = Uri(
path: TreeStatusPage.routeName,
queryParameters: queryParameters,
);
final buildState = Provider.of<BuildState>(context, listen: false);
buildState.updateCurrentRepoBranch(repo, branch);
unawaited(Navigator.pushNamed(context, uri.toString()));
}
@override
Widget build(BuildContext context) {
final markedFailing = changes.firstOrNull?.status == TreeStatus.failure;
return CocoonScaffold(
title: const Text('Tree Status'),
body: CustomScrollView(
slivers: [
SliverAppBar(
automaticallyImplyLeading: false,
actions: [
ElevatedButton.icon(
onPressed: () async {
// Launch a dialog that takes an optional reason.
final reason = await showDialog<String>(
context: context,
builder: (_) => const _ConfirmChangeDialog(),
);
if (reason == null) {
return;
}
final buildState = Provider.of<BuildState>(
context,
listen: false,
);
await buildState.cocoonService.updateTreeStatus(
idToken: await buildState.authService.idToken,
repo: buildState.currentRepo,
status:
markedFailing ? TreeStatus.success : TreeStatus.failure,
reason: reason,
);
await _fetchTreeStatusChanges();
},
label:
markedFailing
? const Text('Enable Tree')
: const Text('Disable Tree'),
icon:
markedFailing
? const Icon(Icons.check, color: Colors.green)
: const Icon(Icons.close, color: Colors.red),
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate((_, i) {
final item = changes[i];
return ListTile(
leading:
item.status == TreeStatus.success
? const Icon(Icons.check, color: Colors.green)
: const Icon(Icons.error, color: Colors.red),
title: Text(item.authoredBy),
subtitle: Text(
item.reason != null ? 'Reason: ${item.reason}' : '',
),
trailing: Text(item.createdOn.toString()),
);
}, childCount: changes.length),
),
],
),
onUpdateNavigation: ({required branch, required repo}) {
_updateNavigation(context, repo: repo, branch: branch);
},
);
}
}
final class _ConfirmChangeDialog extends StatefulWidget {
const _ConfirmChangeDialog();
@override
State<_ConfirmChangeDialog> createState() => _ConfirmChangeDialogState();
}
class _ConfirmChangeDialogState extends State<_ConfirmChangeDialog> {
final _reasonController = TextEditingController();
@override
void dispose() {
_reasonController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Reason'),
content: TextField(
controller: _reasonController,
decoration: const InputDecoration(hintText: 'Enter reason (optional)'),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(_reasonController.text);
},
child: const Text('Submit'),
),
],
);
}
}