/*
* 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
}
}