Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
3a2bc1c5
Unverified
Commit
3a2bc1c5
authored
Oct 13, 2021
by
Alex
Committed by
GitHub
Oct 13, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
added conductor_status widget (#91445)
parent
01afd64b
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
365 additions
and
24 deletions
+365
-24
main.dart
dev/conductor/ui/lib/main.dart
+1
-3
conductor_status.dart
dev/conductor/ui/lib/widgets/conductor_status.dart
+182
-5
main_test.dart
dev/conductor/ui/test/main_test.dart
+2
-16
conductor_status_test.dart
dev/conductor/ui/test/widgets/conductor_status_test.dart
+180
-0
No files found.
dev/conductor/ui/lib/main.dart
View file @
3a2bc1c5
...
@@ -25,8 +25,7 @@ Future<void> main() async {
...
@@ -25,8 +25,7 @@ Future<void> main() async {
throw
Exception
(
'The conductor only supports MacOS and Linux desktop'
);
throw
Exception
(
'The conductor only supports MacOS and Linux desktop'
);
}
}
final
File
_stateFile
=
_fs
.
file
(
_stateFilePath
);
final
File
_stateFile
=
_fs
.
file
(
_stateFilePath
);
final
pb
.
ConductorState
?
state
=
final
pb
.
ConductorState
?
state
=
_stateFile
.
existsSync
()
?
readStateFromFile
(
_stateFile
)
:
null
;
_stateFile
.
existsSync
()
?
readStateFromFile
(
_stateFile
)
:
null
;
WidgetsFlutterBinding
.
ensureInitialized
();
WidgetsFlutterBinding
.
ensureInitialized
();
runApp
(
MyApp
(
state
));
runApp
(
MyApp
(
state
));
...
@@ -44,7 +43,6 @@ class MyApp extends StatelessWidget {
...
@@ -44,7 +43,6 @@ class MyApp extends StatelessWidget {
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
return
MaterialApp
(
return
MaterialApp
(
title:
_title
,
title:
_title
,
// TODO(Yugue): Add theming, https://github.com/flutter/flutter/issues/90982.
home:
Scaffold
(
home:
Scaffold
(
appBar:
AppBar
(
appBar:
AppBar
(
title:
const
Text
(
_title
),
title:
const
Text
(
_title
),
...
...
dev/conductor/ui/lib/widgets/conductor_status.dart
View file @
3a2bc1c5
...
@@ -6,7 +6,7 @@ import 'package:conductor_core/conductor_core.dart';
...
@@ -6,7 +6,7 @@ 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:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
/// Display
the current conductor state
/// Display
s the current conductor state.
class
ConductorStatus
extends
StatefulWidget
{
class
ConductorStatus
extends
StatefulWidget
{
const
ConductorStatus
({
const
ConductorStatus
({
Key
?
key
,
Key
?
key
,
...
@@ -19,15 +19,192 @@ class ConductorStatus extends StatefulWidget {
...
@@ -19,15 +19,192 @@ class ConductorStatus extends StatefulWidget {
@override
@override
ConductorStatusState
createState
()
=>
ConductorStatusState
();
ConductorStatusState
createState
()
=>
ConductorStatusState
();
static
final
List
<
String
>
headerElements
=
<
String
>[
'Conductor Version'
,
'Release Channel'
,
'Release Version'
,
'Release Started at'
,
'Release Updated at'
,
'Dart SDK Revision'
,
];
}
}
class
ConductorStatusState
extends
State
<
ConductorStatus
>
{
class
ConductorStatusState
extends
State
<
ConductorStatus
>
{
/// Returns the conductor state in a Map<K, V> format for the desktop app to consume.
Map
<
String
,
Object
>
presentStateDesktop
(
pb
.
ConductorState
state
)
{
final
List
<
Map
<
String
,
String
>>
engineCherrypicks
=
<
Map
<
String
,
String
>>[];
for
(
final
pb
.
Cherrypick
cherrypick
in
state
.
engine
.
cherrypicks
)
{
engineCherrypicks
.
add
(<
String
,
String
>{
'trunkRevision'
:
cherrypick
.
trunkRevision
,
'state'
:
'
${cherrypick.state}
'
});
}
final
List
<
Map
<
String
,
String
>>
frameworkCherrypicks
=
<
Map
<
String
,
String
>>[];
for
(
final
pb
.
Cherrypick
cherrypick
in
state
.
framework
.
cherrypicks
)
{
frameworkCherrypicks
.
add
(<
String
,
String
>{
'trunkRevision'
:
cherrypick
.
trunkRevision
,
'state'
:
'
${cherrypick.state}
'
});
}
return
<
String
,
Object
>{
'Conductor Version'
:
state
.
conductorVersion
,
'Release Channel'
:
state
.
releaseChannel
,
'Release Version'
:
state
.
releaseVersion
,
'Release Started at'
:
DateTime
.
fromMillisecondsSinceEpoch
(
state
.
createdDate
.
toInt
()).
toString
(),
'Release Updated at'
:
DateTime
.
fromMillisecondsSinceEpoch
(
state
.
lastUpdatedDate
.
toInt
()).
toString
(),
'Engine Candidate Branch'
:
state
.
engine
.
candidateBranch
,
'Engine Starting Git HEAD'
:
state
.
engine
.
startingGitHead
,
'Engine Current Git HEAD'
:
state
.
engine
.
currentGitHead
,
'Engine Path to Checkout'
:
state
.
engine
.
checkoutPath
,
'Engine LUCI Dashboard'
:
luciConsoleLink
(
state
.
releaseChannel
,
'engine'
),
'Engine Cherrypicks'
:
engineCherrypicks
,
'Dart SDK Revision'
:
state
.
engine
.
dartRevision
,
'Framework Candidate Branch'
:
state
.
framework
.
candidateBranch
,
'Framework Starting Git HEAD'
:
state
.
framework
.
startingGitHead
,
'Framework Current Git HEAD'
:
state
.
framework
.
currentGitHead
,
'Framework Path to Checkout'
:
state
.
framework
.
checkoutPath
,
'Framework LUCI Dashboard'
:
luciConsoleLink
(
state
.
releaseChannel
,
'flutter'
),
'Framework Cherrypicks'
:
frameworkCherrypicks
,
'Current Phase'
:
state
.
currentPhase
,
};
}
@override
@override
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
return
SelectableText
(
late
final
Map
<
String
,
Object
>
currentStatus
;
widget
.
releaseState
!=
null
if
(
widget
.
releaseState
==
null
)
{
?
presentState
(
widget
.
releaseState
!)
return
SelectableText
(
'No persistent state file found at
${widget.stateFilePath}
'
);
:
'No persistent state file found at
${widget.stateFilePath}
'
,
}
else
{
currentStatus
=
presentStateDesktop
(
widget
.
releaseState
!);
}
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
<
Widget
>[
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
<
Widget
>[
Table
(
columnWidths:
const
<
int
,
TableColumnWidth
>{
0
:
FixedColumnWidth
(
200.0
),
1
:
FixedColumnWidth
(
400.0
),
},
children:
<
TableRow
>[
for
(
String
headerElement
in
ConductorStatus
.
headerElements
)
TableRow
(
children:
<
Widget
>[
Text
(
'
$headerElement
:'
),
SelectableText
((
currentStatus
[
headerElement
]
==
null
||
currentStatus
[
headerElement
]
==
''
)
?
'Unknown'
:
currentStatus
[
headerElement
]!
as
String
),
],
),
],
),
const
SizedBox
(
height:
20.0
),
Wrap
(
children:
<
Widget
>[
Column
(
children:
<
Widget
>[
CherrypickTable
(
engineOrFramework:
'engine'
,
currentStatus:
currentStatus
),
],
),
const
SizedBox
(
width:
20.0
),
Column
(
children:
<
Widget
>[
CherrypickTable
(
engineOrFramework:
'framework'
,
currentStatus:
currentStatus
),
],
),
],
)
],
)
],
);
}
}
/// Displays explanations for each status type as a tooltip.
class
StatusTooltip
extends
StatefulWidget
{
const
StatusTooltip
({
Key
?
key
,
this
.
engineOrFramework
,
})
:
super
(
key:
key
);
final
String
?
engineOrFramework
;
@override
State
<
StatusTooltip
>
createState
()
=>
_StatusTooltipState
();
}
class
_StatusTooltipState
extends
State
<
StatusTooltip
>
{
@override
Widget
build
(
BuildContext
context
)
{
return
Row
(
children:
<
Widget
>[
const
Text
(
'Status'
),
const
SizedBox
(
width:
10.0
),
Tooltip
(
padding:
const
EdgeInsets
.
all
(
10.0
),
message:
'''
PENDING: The cherrypick has not yet been applied.
PENDING_WITH_CONFLICT: The cherrypick has not been applied and will require manual resolution.
COMPLETED: The cherrypick has been successfully applied to the local checkout.
ABANDONED: The cherrypick will NOT be applied in this release.'''
,
child:
Icon
(
Icons
.
info
,
size:
16.0
,
key:
Key
(
'
${widget.engineOrFramework}
ConductorStatusTooltip'
),
),
),
],
);
}
}
/// Widget for showing the engine and framework cherrypicks applied to the current release.
///
/// Shows the cherrypicks' SHA and status in two separate table DataRow cells.
class
CherrypickTable
extends
StatefulWidget
{
const
CherrypickTable
({
Key
?
key
,
required
this
.
engineOrFramework
,
required
this
.
currentStatus
,
})
:
super
(
key:
key
);
final
String
engineOrFramework
;
final
Map
<
String
,
Object
>
currentStatus
;
@override
CherrypickTableState
createState
()
=>
CherrypickTableState
();
}
class
CherrypickTableState
extends
State
<
CherrypickTable
>
{
@override
Widget
build
(
BuildContext
context
)
{
final
List
<
Map
<
String
,
String
>>
cherrypicks
=
widget
.
engineOrFramework
==
'engine'
?
widget
.
currentStatus
[
'Engine Cherrypicks'
]!
as
List
<
Map
<
String
,
String
>>
:
widget
.
currentStatus
[
'Framework Cherrypicks'
]!
as
List
<
Map
<
String
,
String
>>;
return
DataTable
(
dataRowHeight:
30.0
,
headingRowHeight:
30.0
,
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
Colors
.
grey
)),
columns:
<
DataColumn
>[
DataColumn
(
label:
Text
(
'
${widget.engineOrFramework == 'engine' ? 'Engine' : 'Framework'}
Cherrypicks'
)),
DataColumn
(
label:
StatusTooltip
(
engineOrFramework:
widget
.
engineOrFramework
)),
],
rows:
cherrypicks
.
map
((
Map
<
String
,
String
>
cherrypick
)
{
return
DataRow
(
cells:
<
DataCell
>[
DataCell
(
SelectableText
(
cherrypick
[
'trunkRevision'
]!),
),
DataCell
(
SelectableText
(
cherrypick
[
'state'
]!),
),
],
);
}).
toList
(),
);
);
}
}
}
}
dev/conductor/ui/test/main_test.dart
View file @
3a2bc1c5
...
@@ -4,31 +4,17 @@
...
@@ -4,31 +4,17 @@
import
'dart:io'
show
Platform
;
import
'dart:io'
show
Platform
;
import
'package:conductor_core/proto.dart'
as
pb
;
import
'package:conductor_ui/main.dart'
;
import
'package:conductor_ui/main.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
void
main
(
)
{
group
(
'Main app'
,
()
{
group
(
'Main app'
,
()
{
testWidgets
(
'
Handles null state
'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'
Scaffold Initialization
'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MyApp
(
null
));
await
tester
.
pumpWidget
(
const
MyApp
(
null
));
expect
(
find
.
textContaining
(
'Flutter Desktop Conductor'
),
findsOneWidget
);
expect
(
find
.
textContaining
(
'Flutter Desktop Conductor'
),
findsOneWidget
);
expect
(
find
.
textContaining
(
'No persistent state file found at'
),
findsOneWidget
);
expect
(
find
.
textContaining
(
'Desktop app for managing a release'
),
findsOneWidget
);
});
testWidgets
(
'App prints release channel from state file'
,
(
WidgetTester
tester
)
async
{
const
String
channelName
=
'dev'
;
final
pb
.
ConductorState
state
=
pb
.
ConductorState
(
releaseChannel:
channelName
,
);
await
tester
.
pumpWidget
(
MyApp
(
state
));
expect
(
find
.
textContaining
(
'Flutter Desktop Conductor'
),
findsOneWidget
);
expect
(
find
.
textContaining
(
'Conductor version'
),
findsOneWidget
);
expect
(
find
.
text
(
'1'
),
findsNothing
);
});
});
},
skip:
Platform
.
isWindows
);
// This app does not support Windows [intended]
},
skip:
Platform
.
isWindows
);
// This app does not support Windows [intended]
}
}
dev/conductor/ui/test/widgets/conductor_status_test.dart
0 → 100644
View file @
3a2bc1c5
// Copyright 2014 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:conductor_core/proto.dart'
as
pb
;
import
'package:conductor_ui/widgets/conductor_status.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
group
(
'conductor_status'
,
()
{
testWidgets
(
'Conductor_status displays nothing found when there is no state file'
,
(
WidgetTester
tester
)
async
{
const
String
testPath
=
'./testPath'
;
await
tester
.
pumpWidget
(
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setState
)
{
return
MaterialApp
(
home:
Material
(
child:
Column
(
children:
const
<
Widget
>[
ConductorStatus
(
stateFilePath:
testPath
,
),
],
),
),
);
},
),
);
expect
(
find
.
text
(
'No persistent state file found at
$testPath
'
),
findsOneWidget
);
expect
(
find
.
text
(
'Conductor version:'
),
findsNothing
);
});
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
(
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setState
)
{
return
MaterialApp
(
home:
Material
(
child:
Column
(
children:
<
Widget
>[
ConductorStatus
(
releaseState:
state
,
stateFilePath:
testPath
,
),
],
),
),
);
},
),
);
expect
(
find
.
text
(
'No persistent state file found at
$testPath
'
),
findsNothing
);
for
(
final
String
headerElement
in
ConductorStatus
.
headerElements
)
{
expect
(
find
.
text
(
'
$headerElement
:'
),
findsOneWidget
);
}
expect
(
find
.
text
(
conductorVersion
),
findsOneWidget
);
expect
(
find
.
text
(
releaseChannel
),
findsOneWidget
);
expect
(
find
.
text
(
releaseVersion
),
findsOneWidget
);
expect
(
find
.
text
(
'Release Started at:'
),
findsOneWidget
);
expect
(
find
.
text
(
'Release Updated at:'
),
findsOneWidget
);
expect
(
find
.
text
(
dartRevision
),
findsOneWidget
);
expect
(
find
.
text
(
engineCherrypick1
),
findsOneWidget
);
expect
(
find
.
text
(
engineCherrypick2
),
findsOneWidget
);
expect
(
find
.
text
(
frameworkCherrypick
),
findsOneWidget
);
final
TestGesture
gesture
=
await
tester
.
createGesture
(
kind:
PointerDeviceKind
.
mouse
);
addTearDown
(
gesture
.
removePointer
);
await
gesture
.
addPointer
(
location:
Offset
.
zero
);
/// Tests the tooltip is displaying status explanations upon cursor hovering.
///
/// Before hovering, status explanations are not found.
/// When the cursor hovers over the info icon, the explanations are displayed and found.
expect
(
find
.
textContaining
(
'PENDING: The cherrypick has not yet been applied.'
),
findsNothing
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
byKey
(
const
Key
(
'engineConductorStatusTooltip'
))));
await
tester
.
pumpAndSettle
();
expect
(
find
.
textContaining
(
'PENDING: The cherrypick has not yet been applied.'
),
findsOneWidget
);
});
testWidgets
(
'Conductor_status displays correct status with a null state file except a releaseChannel'
,
(
WidgetTester
tester
)
async
{
const
String
testPath
=
'./testPath'
;
const
String
releaseChannel
=
'beta'
;
final
pb
.
ConductorState
state
=
pb
.
ConductorState
(
releaseChannel:
releaseChannel
,
);
await
tester
.
pumpWidget
(
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setState
)
{
return
MaterialApp
(
home:
Material
(
child:
Column
(
children:
<
Widget
>[
ConductorStatus
(
releaseState:
state
,
stateFilePath:
testPath
,
),
],
),
),
);
},
),
);
expect
(
find
.
text
(
'No persistent state file found at
$testPath
'
),
findsNothing
);
for
(
final
String
headerElement
in
ConductorStatus
.
headerElements
)
{
expect
(
find
.
text
(
'
$headerElement
:'
),
findsOneWidget
);
}
expect
(
find
.
text
(
releaseChannel
),
findsOneWidget
);
expect
(
find
.
text
(
'Unknown'
),
findsNWidgets
(
3
));
final
TestGesture
gesture
=
await
tester
.
createGesture
(
kind:
PointerDeviceKind
.
mouse
);
addTearDown
(
gesture
.
removePointer
);
await
gesture
.
addPointer
(
location:
Offset
.
zero
);
/// Tests the tooltip is displaying status explanations upon cursor hovering.
///
/// Before hovering, status explanations are not found.
/// When the cursor hovers over the info icon, the explanations are displayed and found.
expect
(
find
.
textContaining
(
'PENDING: The cherrypick has not yet been applied.'
),
findsNothing
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
byKey
(
const
Key
(
'engineConductorStatusTooltip'
))));
await
tester
.
pumpAndSettle
();
expect
(
find
.
textContaining
(
'PENDING: The cherrypick has not yet been applied.'
),
findsOneWidget
);
});
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment