// 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/conductor_core.dart'; import 'package:conductor_core/proto.dart' as pb; import 'package:flutter/material.dart'; /// Displays the current conductor state. class ConductorStatus extends StatefulWidget { const ConductorStatus({ Key? key, this.releaseState, required this.stateFilePath, }) : super(key: key); final pb.ConductorState? releaseState; final String stateFilePath; @override State createState() => ConductorStatusState(); static final List headerElements = [ 'Conductor Version', 'Release Channel', 'Release Version', 'Release Started at', 'Release Updated at', 'Dart SDK Revision', ]; static final List engineRepoElements = [ 'Engine Candidate Branch', 'Engine Starting Git HEAD', 'Engine Current Git HEAD', 'Engine Path to Checkout', 'Engine LUCI Dashboard', ]; static final List frameworkRepoElements = [ 'Framework Candidate Branch', 'Framework Starting Git HEAD', 'Framework Current Git HEAD', 'Framework Path to Checkout', 'Framework LUCI Dashboard', ]; } class ConductorStatusState extends State { /// Returns the conductor state in a Map format for the desktop app to consume. Map presentStateDesktop(pb.ConductorState state) { final List> engineCherrypicks = >[]; for (final pb.Cherrypick cherrypick in state.engine.cherrypicks) { engineCherrypicks .add({'trunkRevision': cherrypick.trunkRevision, 'state': '${cherrypick.state}'}); } final List> frameworkCherrypicks = >[]; for (final pb.Cherrypick cherrypick in state.framework.cherrypicks) { frameworkCherrypicks .add({'trunkRevision': cherrypick.trunkRevision, 'state': '${cherrypick.state}'}); } return { '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 Widget build(BuildContext context) { late final Map currentStatus; if (widget.releaseState == null) { return SelectableText('No persistent state file found at ${widget.stateFilePath}'); } else { currentStatus = presentStateDesktop(widget.releaseState!); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Table( columnWidths: const { 0: FixedColumnWidth(200.0), }, children: [ for (String headerElement in ConductorStatus.headerElements) TableRow( children: [ Text('$headerElement:'), SelectableText((currentStatus[headerElement] == null || currentStatus[headerElement] == '') ? 'Unknown' : currentStatus[headerElement]! as String), ], ), ], ), const SizedBox(height: 20.0), Wrap( children: [ Column( children: [ RepoInfoExpansion(engineOrFramework: 'engine', currentStatus: currentStatus), const SizedBox(height: 10.0), CherrypickTable(engineOrFramework: 'engine', currentStatus: currentStatus), ], ), const SizedBox(width: 20.0), Column( children: [ RepoInfoExpansion(engineOrFramework: 'framework', currentStatus: currentStatus), const SizedBox(height: 10.0), 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 createState() => _StatusTooltipState(); } class _StatusTooltipState extends State { @override Widget build(BuildContext context) { return Row( children: [ 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 currentStatus; @override State createState() => CherrypickTableState(); } class CherrypickTableState extends State { @override Widget build(BuildContext context) { final List> cherrypicks = widget.engineOrFramework == 'engine' ? widget.currentStatus['Engine Cherrypicks']! as List> : widget.currentStatus['Framework Cherrypicks']! as List>; return DataTable( dataRowHeight: 30.0, headingRowHeight: 30.0, decoration: BoxDecoration(border: Border.all(color: Colors.grey)), columns: [ DataColumn(label: Text('${widget.engineOrFramework == 'engine' ? 'Engine' : 'Framework'} Cherrypicks')), DataColumn(label: StatusTooltip(engineOrFramework: widget.engineOrFramework)), ], rows: cherrypicks.map((Map cherrypick) { return DataRow( cells: [ DataCell( SelectableText(cherrypick['trunkRevision']!), ), DataCell( SelectableText(cherrypick['state']!), ), ], ); }).toList(), ); } } /// 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 currentStatus; @override State createState() => RepoInfoExpansionState(); } class RepoInfoExpansionState extends State { 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( 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 { 0: FixedColumnWidth(240.0), }, children: [ for (String repoElement in widget.engineOrFramework == 'engine' ? ConductorStatus.engineRepoElements : ConductorStatus.frameworkRepoElements) TableRow( decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.grey))), children: [ Text('$repoElement:'), SelectableText( (widget.currentStatus[repoElement] == null || widget.currentStatus[repoElement] == '') ? 'Unknown' : widget.currentStatus[repoElement]! as String), ], ), ], ), ), ), ], ), ); } }