/* * Copyright (c) 2009 Craig Sutherland * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ using System; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using FastForward.WinCore; using ThoughtWorks.CruiseControl.Remote; using ThoughtWorks.CruiseControl.Remote.Monitor; namespace FastForward.Monitor { /// /// Displays the current status information from the server. /// public partial class CurrentStatusForm : Form { #region Private fields private Project project; private object lockObject = new object(); private bool isRefreshing = false; private int percentage = 0; private Color colour = Color.YellowGreen; #endregion #region Constructors /// /// Initialise a new . /// /// /// public CurrentStatusForm(Project project, Form owner) { InitializeComponent(); this.project = project; Text = "Project Status: " + project.Name; RefreshStatus(); Owner = owner; } #endregion #region Private methods #region RefreshStatus() /// /// Refresh the status of the items. /// private void RefreshStatus() { var canRefresh = false; // Check if a refresh is already running lock (lockObject) { canRefresh = !isRefreshing; isRefreshing = true; } // Trigger the refresh if (canRefresh) { refreshTimer.Enabled = false; refreshWorder.RunWorkerAsync(); } } #endregion #region DisplayStatus() /// /// Display the status of the project. /// /// private void DisplayStatus(ProjectStatusSnapshot status) { var server = new ItemStatus(); server.ChildItems.Add(status); UpdateNode(server, statusTree.Nodes); if (statusTree.SelectedNode == null) { statusTree.SelectedNode = statusTree.Nodes[0]; } else { DisplayItem((statusTree.SelectedNode as StatusTreeNode).Status); } } #endregion #region UpdateNode() /// /// Update a node in the tree. /// /// /// private void UpdateNode(ItemStatus status, TreeNodeCollection nodes) { // Get the existing nodes var oldNodes = (from record in nodes.Cast() select record.Name).ToList(); // Update or add the nodes foreach (var item in status.ChildItems) { var expand = false; var key = item.Identifier.ToString(); StatusTreeNode node; if (oldNodes.Contains(key)) { oldNodes.Remove(key); node = nodes[key] as StatusTreeNode; } else { node = new StatusTreeNode(); nodes.Add(node); expand = true; } node.UpdateStatus(item); UpdateNode(item, node.Nodes); if (expand) node.Expand(); } // Remove any old nodes foreach (var oldNode in oldNodes) { nodes.RemoveByKey(oldNode); } } #endregion #region DisplayItem() /// /// Display an item in the property grid. /// /// private void DisplayItem(ItemStatus status) { itemPropertyGrid.SelectedObject = new StatusDisplayWrapper(status); percentage = 0; colour = Color.Gray; switch (status.Status) { case ItemBuildStatus.CompletedFailed: colour = Color.Red; percentage = 100; break; case ItemBuildStatus.CompletedSuccess: colour = Color.Green; percentage = 100; break; case ItemBuildStatus.Cancelled: percentage = 100; break; case ItemBuildStatus.Running: percentage = 50; break; } itemProgress.ForeColor = colour; var childCount = status.ChildItems.Count; if (childCount != 0) { var completedCount = (from record in status.ChildItems where record.Status == ItemBuildStatus.CompletedFailed || record.Status == ItemBuildStatus.CompletedSuccess || record.Status == ItemBuildStatus.Cancelled select record).Count(); if (completedCount == childCount) { percentage = 100; } else { percentage = completedCount * 100 / childCount; } } itemProgress.Refresh(); } #endregion #endregion #region Event handlers #region refreshTimer_Tick() /// /// Update the display because the timer has fired. /// /// /// private void refreshTimer_Tick(object sender, EventArgs e) { RefreshStatus(); } #endregion #region refreshButton_Click /// /// Update the display because the user clicked on the button. /// /// /// private void refreshButton_Click(object sender, EventArgs e) { RefreshStatus(); } #endregion #region refreshWorder_DoWork /// /// Do the actual work of refreshing the display. /// /// /// private void refreshWorder_DoWork(object sender, DoWorkEventArgs e) { try { // Retrieve the current status var status = project.RetrieveCurrentStatus(); // Perform the update of the display this.SafeInvoke(DisplayStatus, status); } catch (NotImplementedException) { // The underlying client hasn't implement this method } catch (CommunicationsException) { // This could be a communications error, or the server does not implement the // relevant interfaces - either way the display will not work } } #endregion #region refreshWorder_RunWorkerCompleted() /// /// Update the display when the background worker has finished. /// /// /// private void refreshWorder_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { refreshTimer.Enabled = true; statusLabel.Text = "Last updated " + DateTime.Now.ToLongTimeString(); lock (lockObject) { isRefreshing = false; } } #endregion #region statusTree_AfterSelect() /// /// Handle the user selecting a different node in the tree. /// /// /// private void statusTree_AfterSelect(object sender, TreeViewEventArgs e) { var currentNode = e.Node as StatusTreeNode; DisplayItem(currentNode.Status); } #endregion #region itemProgress_Paint() /// /// Display the side progress bar. /// /// /// private void itemProgress_Paint(object sender, PaintEventArgs e) { var width = itemProgress.Width; var height = itemProgress.Height; using (var background = new SolidBrush(itemProgress.BackColor)) { e.Graphics.FillRectangle(background, 0, 0, width, Height); } if (percentage > 0) { height = height * percentage / 100; using (var background = new SolidBrush(colour)) { e.Graphics.FillRectangle(background, 0, itemProgress.Height - height, width, Height); } } } #endregion #endregion #region Private classes #region StatusTreeNode /// /// A node in the progress tree, /// private class StatusTreeNode : TreeNode { #region Public properties #region Status /// /// The status being displayed. /// public ItemStatus Status { get; private set; } #endregion #endregion #region Public methods #region UpdateStatus() /// /// Update the node. /// /// public void UpdateStatus(ItemStatus status) { Status = status; Name = status.Identifier.ToString(); Text = status.Name; var image = status.Status.ToString(); ImageKey = image; SelectedImageKey = image; } #endregion #endregion } #endregion #region StatusDisplayWrapper /// /// A wrapper for a status to display in the property grid. /// private class StatusDisplayWrapper { #region Private fields private ItemStatus status; #endregion #region Constructors /// /// Initialise a new . /// /// public StatusDisplayWrapper(ItemStatus status) { this.status = status; } #endregion #region Public properties #region Description /// /// The description of the item. /// [Description("The description of the item (optional).")] [Category("Item Details")] public string Description { get { return status.Description; } set { } } #endregion #region Name /// /// The name of the item. /// [Description("The name of the item.")] [Category("Item Details")] public string Name { get { return status.Name; } set { } } #endregion #region ErrorMessage /// /// Any error details on why the item failed. /// [DisplayName("Error Message")] [Description("Any error details on why the item failed - only set if the status is failed.")] [Category("Item Details")] public string ErrorMessage { get { return status.Error; } set { } } #endregion #region Status /// /// The current status of the item. /// [Description("The current status of the item.")] [Category("Item Details")] public string Status { get { switch (status.Status) { case ItemBuildStatus.CompletedSuccess: return "Success"; case ItemBuildStatus.CompletedFailed: return "Failed"; default: return status.Status.ToString(); } } set { } } #endregion #region TimeStarted /// /// The time this item started running. /// [DisplayName("Started")] [Description("The time this item started running.")] [Category("Timings")] public string TimeStarted { get { return FormatDate(status.TimeStarted); } set { } } #endregion #region TimeCompleted /// /// The time this item completed running. /// [DisplayName("Completed")] [Description("The time this item completed running.")] [Category("Timings")] public string TimeCompleted { get { return FormatDate(status.TimeCompleted); } set { } } #endregion #region TimeElapsed /// /// Either the time this item has been running (for currently active items) or the time /// it took this item to run (for completed items). /// [DisplayName("Elapsed")] [Description("Either the time this item has been running (for currently active items) or the time it took this item to run (for completed items).")] [Category("Timings")] public string TimeElapsed { get { if (status.TimeStarted.HasValue) { var elapsed = (status.TimeCompleted ?? DateTime.Now) - status.TimeStarted.Value; var builder = new StringBuilder(); if (elapsed.Hours > 0) builder.AppendFormat("{0}h ", elapsed.Hours); if (elapsed.Minutes > 0) builder.AppendFormat("{0}m ", elapsed.Minutes); builder.AppendFormat("{0}.{1}s", elapsed.Seconds, elapsed.Milliseconds / 100); return builder.ToString(); } else { return string.Empty; } } set { } } #endregion #region TimeEstimated /// /// The time this item is estimated to complete running. /// [DisplayName("Estimated Completion")] [Description("The time this item is estimated to complete running.")] [Category("Timings")] public string TimeEstimated { get { return FormatDate(status.TimeOfEstimatedCompletion); } set { } } #endregion #endregion #region Private methods #region FormatDate() /// /// Format a date. /// /// /// private string FormatDate(DateTime? value) { if (value.HasValue) { if (value.Value.Date == DateTime.Today) { return value.Value.ToLongTimeString(); } else if (value.Value == DateTime.Today.AddDays(-1)) { return value.Value.ToLongTimeString() + " [yesterday]"; } else { return value.Value.ToLongTimeString() + " [" + value.Value.ToLongDateString() + "]"; } } else { return string.Empty; } } #endregion #endregion } #endregion #endregion } }