mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Release Desktop UI Repo Info Widget (#91769)
This commit is contained in:
parent
ae19c8f66a
commit
a6f375abdc
@ -55,7 +55,7 @@ class MyApp extends StatelessWidget {
|
|||||||
const SelectableText(
|
const SelectableText(
|
||||||
'Desktop app for managing a release of the Flutter SDK, currently in development',
|
'Desktop app for managing a release of the Flutter SDK, currently in development',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20.0),
|
const SizedBox(height: 10.0),
|
||||||
MainProgression(
|
MainProgression(
|
||||||
releaseState: state,
|
releaseState: state,
|
||||||
stateFilePath: _stateFilePath,
|
stateFilePath: _stateFilePath,
|
||||||
|
@ -28,6 +28,22 @@ class ConductorStatus extends StatefulWidget {
|
|||||||
'Release Updated at',
|
'Release Updated at',
|
||||||
'Dart SDK Revision',
|
'Dart SDK Revision',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static final List<String> engineRepoElements = <String>[
|
||||||
|
'Engine Candidate Branch',
|
||||||
|
'Engine Starting Git HEAD',
|
||||||
|
'Engine Current Git HEAD',
|
||||||
|
'Engine Path to Checkout',
|
||||||
|
'Engine LUCI Dashboard',
|
||||||
|
];
|
||||||
|
|
||||||
|
static final List<String> frameworkRepoElements = <String>[
|
||||||
|
'Framework Candidate Branch',
|
||||||
|
'Framework Starting Git HEAD',
|
||||||
|
'Framework Current Git HEAD',
|
||||||
|
'Framework Path to Checkout',
|
||||||
|
'Framework LUCI Dashboard',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConductorStatusState extends State<ConductorStatus> {
|
class ConductorStatusState extends State<ConductorStatus> {
|
||||||
@ -86,7 +102,6 @@ class ConductorStatusState extends State<ConductorStatus> {
|
|||||||
Table(
|
Table(
|
||||||
columnWidths: const <int, TableColumnWidth>{
|
columnWidths: const <int, TableColumnWidth>{
|
||||||
0: FixedColumnWidth(200.0),
|
0: FixedColumnWidth(200.0),
|
||||||
1: FixedColumnWidth(400.0),
|
|
||||||
},
|
},
|
||||||
children: <TableRow>[
|
children: <TableRow>[
|
||||||
for (String headerElement in ConductorStatus.headerElements)
|
for (String headerElement in ConductorStatus.headerElements)
|
||||||
@ -105,19 +120,23 @@ class ConductorStatusState extends State<ConductorStatus> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
RepoInfoExpansion(engineOrFramework: 'engine', currentStatus: currentStatus),
|
||||||
|
const SizedBox(height: 10.0),
|
||||||
CherrypickTable(engineOrFramework: 'engine', currentStatus: currentStatus),
|
CherrypickTable(engineOrFramework: 'engine', currentStatus: currentStatus),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20.0),
|
const SizedBox(width: 20.0),
|
||||||
Column(
|
Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
RepoInfoExpansion(engineOrFramework: 'framework', currentStatus: currentStatus),
|
||||||
|
const SizedBox(height: 10.0),
|
||||||
CherrypickTable(engineOrFramework: 'framework', currentStatus: currentStatus),
|
CherrypickTable(engineOrFramework: 'framework', currentStatus: currentStatus),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -208,3 +227,80 @@ class CherrypickTableState extends State<CherrypickTable> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Widget to display repo info related to the engine and framework.
|
||||||
|
///
|
||||||
|
/// Click to show/hide the repo info in a dropdown fashion. By default the section is hidden.
|
||||||
|
class RepoInfoExpansion extends StatefulWidget {
|
||||||
|
const RepoInfoExpansion({
|
||||||
|
Key? key,
|
||||||
|
required this.engineOrFramework,
|
||||||
|
required this.currentStatus,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String engineOrFramework;
|
||||||
|
final Map<String, Object> currentStatus;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RepoInfoExpansionState createState() => RepoInfoExpansionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RepoInfoExpansionState extends State<RepoInfoExpansion> {
|
||||||
|
bool _isExpanded = false;
|
||||||
|
|
||||||
|
/// Show/hide [ExpansionPanel].
|
||||||
|
void showHide() {
|
||||||
|
setState(() {
|
||||||
|
_isExpanded = !_isExpanded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 500.0,
|
||||||
|
child: ExpansionPanelList(
|
||||||
|
expandedHeaderPadding: EdgeInsets.zero,
|
||||||
|
expansionCallback: (int index, bool isExpanded) {
|
||||||
|
showHide();
|
||||||
|
},
|
||||||
|
children: <ExpansionPanel>[
|
||||||
|
ExpansionPanel(
|
||||||
|
isExpanded: _isExpanded,
|
||||||
|
headerBuilder: (BuildContext context, bool isExpanded) {
|
||||||
|
return ListTile(
|
||||||
|
key: Key('${widget.engineOrFramework}RepoInfoDropdown'),
|
||||||
|
title: Text('${widget.engineOrFramework == 'engine' ? 'Engine' : 'Framework'} Repo Info'),
|
||||||
|
onTap: () {
|
||||||
|
showHide();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(15.0),
|
||||||
|
child: Table(
|
||||||
|
columnWidths: const <int, TableColumnWidth>{
|
||||||
|
0: FixedColumnWidth(240.0),
|
||||||
|
},
|
||||||
|
children: <TableRow>[
|
||||||
|
for (String repoElement in widget.engineOrFramework == 'engine'
|
||||||
|
? ConductorStatus.engineRepoElements
|
||||||
|
: ConductorStatus.frameworkRepoElements)
|
||||||
|
TableRow(
|
||||||
|
decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.grey))),
|
||||||
|
children: <Widget>[
|
||||||
|
Text('$repoElement:'),
|
||||||
|
SelectableText(
|
||||||
|
(widget.currentStatus[repoElement] == null || widget.currentStatus[repoElement] == '')
|
||||||
|
? 'Unknown'
|
||||||
|
: widget.currentStatus[repoElement]! as String),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -37,7 +37,7 @@ class MainProgression extends StatefulWidget {
|
|||||||
class MainProgressionState extends State<MainProgression> {
|
class MainProgressionState extends State<MainProgression> {
|
||||||
int _completedStep = 0;
|
int _completedStep = 0;
|
||||||
|
|
||||||
// Move forward the stepper to the next step of the release.
|
/// Move forward the stepper to the next step of the release.
|
||||||
void nextStep() {
|
void nextStep() {
|
||||||
if (_completedStep < MainProgression._stepTitles.length - 1) {
|
if (_completedStep < MainProgression._stepTitles.length - 1) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -74,6 +74,7 @@ class MainProgressionState extends State<MainProgression> {
|
|||||||
releaseState: widget.releaseState,
|
releaseState: widget.releaseState,
|
||||||
stateFilePath: widget.stateFilePath,
|
stateFilePath: widget.stateFilePath,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 20.0),
|
||||||
Stepper(
|
Stepper(
|
||||||
controlsBuilder: (BuildContext context, ControlsDetails details) => Row(),
|
controlsBuilder: (BuildContext context, ControlsDetails details) => Row(),
|
||||||
physics: const ScrollPhysics(),
|
physics: const ScrollPhysics(),
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:conductor_core/conductor_core.dart';
|
||||||
import 'package:conductor_core/proto.dart' as pb;
|
import 'package:conductor_core/proto.dart' as pb;
|
||||||
import 'package:conductor_ui/widgets/conductor_status.dart';
|
import 'package:conductor_ui/widgets/conductor_status.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
@ -10,6 +11,57 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('conductor_status', () {
|
group('conductor_status', () {
|
||||||
|
late pb.ConductorState state;
|
||||||
|
|
||||||
|
const String testPath = './testPath';
|
||||||
|
const String conductorVersion = 'v1.0';
|
||||||
|
const String releaseChannel = 'beta';
|
||||||
|
const String releaseVersion = '1.2.0-3.4.pre';
|
||||||
|
const String engineCandidateBranch = 'flutter-1.2-candidate.3';
|
||||||
|
const String frameworkCandidateBranch = 'flutter-1.2-candidate.4';
|
||||||
|
const String workingBranch = 'cherrypicks-$engineCandidateBranch';
|
||||||
|
const String dartRevision = 'fe9708ab688dcda9923f584ba370a66fcbc3811f';
|
||||||
|
const String engineCherrypick1 = 'a5a25cd702b062c24b2c67b8d30b5cb33e0ef6f0';
|
||||||
|
const String engineCherrypick2 = '94d06a2e1d01a3b0c693b94d70c5e1df9d78d249';
|
||||||
|
const String frameworkCherrypick = '768cd702b691584b2c67b8d30b5cb33e0ef6f0';
|
||||||
|
const String engineStartingGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4';
|
||||||
|
const String engineCurrentGitHead = '23otn2o3itn2o3int2oi3tno23itno2i3tn';
|
||||||
|
const String engineCheckoutPath = '/Users/alexchen/Desktop/flutter_conductor_checkouts/engine';
|
||||||
|
const String frameworkStartingGitHead = 'df6981e98rh49er8h149er8h19er8h1';
|
||||||
|
const String frameworkCurrentGitHead = '239tnint023t09j2039tj0239tn';
|
||||||
|
const String frameworkCheckoutPath = '/Users/alexchen/Desktop/flutter_conductor_checkouts/framework';
|
||||||
|
final String engineLUCIDashboard = luciConsoleLink(releaseChannel, 'engine');
|
||||||
|
final String frameworkLUCIDashboard = luciConsoleLink(releaseChannel, 'flutter');
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
state = pb.ConductorState(
|
||||||
|
engine: pb.Repository(
|
||||||
|
candidateBranch: engineCandidateBranch,
|
||||||
|
cherrypicks: <pb.Cherrypick>[
|
||||||
|
pb.Cherrypick(trunkRevision: engineCherrypick1),
|
||||||
|
pb.Cherrypick(trunkRevision: engineCherrypick2),
|
||||||
|
],
|
||||||
|
dartRevision: dartRevision,
|
||||||
|
workingBranch: workingBranch,
|
||||||
|
startingGitHead: engineStartingGitHead,
|
||||||
|
currentGitHead: engineCurrentGitHead,
|
||||||
|
checkoutPath: engineCheckoutPath,
|
||||||
|
),
|
||||||
|
framework: pb.Repository(
|
||||||
|
candidateBranch: frameworkCandidateBranch,
|
||||||
|
cherrypicks: <pb.Cherrypick>[
|
||||||
|
pb.Cherrypick(trunkRevision: frameworkCherrypick),
|
||||||
|
],
|
||||||
|
workingBranch: workingBranch,
|
||||||
|
startingGitHead: frameworkStartingGitHead,
|
||||||
|
currentGitHead: frameworkCurrentGitHead,
|
||||||
|
checkoutPath: frameworkCheckoutPath,
|
||||||
|
),
|
||||||
|
conductorVersion: conductorVersion,
|
||||||
|
releaseChannel: releaseChannel,
|
||||||
|
releaseVersion: releaseVersion,
|
||||||
|
);
|
||||||
|
});
|
||||||
testWidgets('Conductor_status displays nothing found when there is no state file', (WidgetTester tester) async {
|
testWidgets('Conductor_status displays nothing found when there is no state file', (WidgetTester tester) async {
|
||||||
const String testPath = './testPath';
|
const String testPath = './testPath';
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -17,7 +69,7 @@ void main() {
|
|||||||
builder: (BuildContext context, StateSetter setState) {
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Material(
|
home: Material(
|
||||||
child: Column(
|
child: ListView(
|
||||||
children: const <Widget>[
|
children: const <Widget>[
|
||||||
ConductorStatus(
|
ConductorStatus(
|
||||||
stateFilePath: testPath,
|
stateFilePath: testPath,
|
||||||
@ -35,57 +87,12 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Conductor_status displays correct status with a state file', (WidgetTester tester) async {
|
testWidgets('Conductor_status displays correct status with a state file', (WidgetTester tester) async {
|
||||||
const String testPath = './testPath';
|
|
||||||
const String conductorVersion = 'v1.0';
|
|
||||||
const String releaseChannel = 'beta';
|
|
||||||
const String releaseVersion = '1.2.0-3.4.pre';
|
|
||||||
const String candidateBranch = 'flutter-1.2-candidate.3';
|
|
||||||
const String workingBranch = 'cherrypicks-$candidateBranch';
|
|
||||||
const String dartRevision = 'fe9708ab688dcda9923f584ba370a66fcbc3811f';
|
|
||||||
const String engineCherrypick1 = 'a5a25cd702b062c24b2c67b8d30b5cb33e0ef6f0';
|
|
||||||
const String engineCherrypick2 = '94d06a2e1d01a3b0c693b94d70c5e1df9d78d249';
|
|
||||||
const String frameworkCherrypick = '768cd702b691584b2c67b8d30b5cb33e0ef6f0';
|
|
||||||
const String engineStartingGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4';
|
|
||||||
const String engineCurrentGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4';
|
|
||||||
const String engineCheckoutPath = '/Users/alexchen/Desktop/flutter_conductor_checkouts/engine';
|
|
||||||
const String frameworkStartingGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4';
|
|
||||||
const String frameworkCurrentGitHead = '083049e6cae311910c6a6619a6681b7eba4035b4';
|
|
||||||
const String frameworkCheckoutPath = '/Users/alexchen/Desktop/flutter_conductor_checkouts/framework';
|
|
||||||
|
|
||||||
final pb.ConductorState state = pb.ConductorState(
|
|
||||||
engine: pb.Repository(
|
|
||||||
candidateBranch: candidateBranch,
|
|
||||||
cherrypicks: <pb.Cherrypick>[
|
|
||||||
pb.Cherrypick(trunkRevision: engineCherrypick1),
|
|
||||||
pb.Cherrypick(trunkRevision: engineCherrypick2),
|
|
||||||
],
|
|
||||||
dartRevision: dartRevision,
|
|
||||||
workingBranch: workingBranch,
|
|
||||||
startingGitHead: engineStartingGitHead,
|
|
||||||
currentGitHead: engineCurrentGitHead,
|
|
||||||
checkoutPath: engineCheckoutPath,
|
|
||||||
),
|
|
||||||
framework: pb.Repository(
|
|
||||||
candidateBranch: candidateBranch,
|
|
||||||
cherrypicks: <pb.Cherrypick>[
|
|
||||||
pb.Cherrypick(trunkRevision: frameworkCherrypick),
|
|
||||||
],
|
|
||||||
workingBranch: workingBranch,
|
|
||||||
startingGitHead: frameworkStartingGitHead,
|
|
||||||
currentGitHead: frameworkCurrentGitHead,
|
|
||||||
checkoutPath: frameworkCheckoutPath,
|
|
||||||
),
|
|
||||||
conductorVersion: conductorVersion,
|
|
||||||
releaseChannel: releaseChannel,
|
|
||||||
releaseVersion: releaseVersion,
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
StatefulBuilder(
|
StatefulBuilder(
|
||||||
builder: (BuildContext context, StateSetter setState) {
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Material(
|
home: Material(
|
||||||
child: Column(
|
child: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConductorStatus(
|
ConductorStatus(
|
||||||
releaseState: state,
|
releaseState: state,
|
||||||
@ -130,9 +137,7 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('Conductor_status displays correct status with a null state file except a releaseChannel',
|
testWidgets('Conductor_status displays correct status with a null state file except a releaseChannel',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
const String testPath = './testPath';
|
final pb.ConductorState stateIncomplete = pb.ConductorState(
|
||||||
const String releaseChannel = 'beta';
|
|
||||||
final pb.ConductorState state = pb.ConductorState(
|
|
||||||
releaseChannel: releaseChannel,
|
releaseChannel: releaseChannel,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -141,10 +146,10 @@ void main() {
|
|||||||
builder: (BuildContext context, StateSetter setState) {
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Material(
|
home: Material(
|
||||||
child: Column(
|
child: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConductorStatus(
|
ConductorStatus(
|
||||||
releaseState: state,
|
releaseState: stateIncomplete,
|
||||||
stateFilePath: testPath,
|
stateFilePath: testPath,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -160,7 +165,7 @@ void main() {
|
|||||||
expect(find.text('$headerElement:'), findsOneWidget);
|
expect(find.text('$headerElement:'), findsOneWidget);
|
||||||
}
|
}
|
||||||
expect(find.text(releaseChannel), findsOneWidget);
|
expect(find.text(releaseChannel), findsOneWidget);
|
||||||
expect(find.text('Unknown'), findsNWidgets(3));
|
expect(find.text('Unknown'), findsNWidgets(11));
|
||||||
|
|
||||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
addTearDown(gesture.removePointer);
|
addTearDown(gesture.removePointer);
|
||||||
@ -176,5 +181,52 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.textContaining('PENDING: The cherrypick has not yet been applied.'), findsOneWidget);
|
expect(find.textContaining('PENDING: The cherrypick has not yet been applied.'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Repo Info section displays corresponding info in a dropdown fashion', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
ConductorStatus(
|
||||||
|
releaseState: state,
|
||||||
|
stateFilePath: testPath,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('No persistent state file found at $testPath'), findsNothing);
|
||||||
|
for (final String repoElement in ConductorStatus.engineRepoElements) {
|
||||||
|
expect(find.text('$repoElement:'), findsOneWidget);
|
||||||
|
}
|
||||||
|
for (final String repoElement in ConductorStatus.frameworkRepoElements) {
|
||||||
|
expect(find.text('$repoElement:'), findsOneWidget);
|
||||||
|
}
|
||||||
|
expect(find.text(engineCandidateBranch), findsOneWidget);
|
||||||
|
expect(find.text(engineStartingGitHead), findsOneWidget);
|
||||||
|
expect(find.text(engineCurrentGitHead), findsOneWidget);
|
||||||
|
expect(find.text(engineCheckoutPath), findsOneWidget);
|
||||||
|
expect(find.text(engineLUCIDashboard), findsOneWidget);
|
||||||
|
|
||||||
|
expect(find.text(frameworkCandidateBranch), findsOneWidget);
|
||||||
|
expect(find.text(frameworkStartingGitHead), findsOneWidget);
|
||||||
|
expect(find.text(frameworkCurrentGitHead), findsOneWidget);
|
||||||
|
expect(find.text(frameworkCheckoutPath), findsOneWidget);
|
||||||
|
expect(find.text(frameworkLUCIDashboard), findsOneWidget);
|
||||||
|
|
||||||
|
expect(tester.widget<ExpansionPanelList>(find.byType(ExpansionPanelList).first).children[0].isExpanded,
|
||||||
|
equals(false));
|
||||||
|
await tester.tap(find.byKey(const Key('engineRepoInfoDropdown')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.widget<ExpansionPanelList>(find.byType(ExpansionPanelList).first).children[0].isExpanded,
|
||||||
|
equals(true));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user