├── LICENSE.txt ├── .nuget ├── NuGet.exe ├── NuGet.Config └── NuGet.targets ├── libs └── ITCloud.Web.Routing.dll ├── TeamCityDashboard ├── images │ ├── q42.png │ ├── grid-unit.png │ └── transparent.gif ├── fonts │ ├── Segoe UI.ttf │ ├── SegoeWP.ttf │ ├── segoeui.ttf │ ├── segoeuil.ttf │ ├── Segoe UI Bold.ttf │ ├── SegoeWP-Black.ttf │ ├── SegoeWP-Light.ttf │ └── SegoeWP-Semibold.ttf ├── Global.asax ├── packages.config ├── Web.Release.config ├── Interfaces │ ├── IBuildRun.cs │ ├── IBuildConfig.cs │ ├── IProject.cs │ ├── ICacheService.cs │ └── ICodeStatistics.cs ├── Models │ ├── ProjectVisible.cs │ ├── CodeStatistics.cs │ ├── BuildConfig.cs │ └── Project.cs ├── Properties │ ├── PublishProfiles │ │ └── Publish to disk.pubxml │ └── AssemblyInfo.cs ├── Controllers │ ├── PushEvent.cs │ └── DashboardController.cs ├── Web.Debug.config ├── Services │ ├── WebCacheService.cs │ ├── AbstractHttpDataService.cs │ ├── SonarDataService.cs │ ├── GithubDataService.cs │ └── TeamCityDataService.cs ├── Scripts │ ├── jquery.fn.sortelements.js │ ├── jquery.timeago.js │ ├── metro-grid.js │ ├── jquery.masonry.min.js │ └── jquery.crypt.js ├── Global.asax.cs ├── parameters.xml ├── Web.example.config ├── css │ ├── styles.css │ └── styles.scss ├── TeamCityDashboard.csproj └── Views │ └── Dashboard │ └── Index.aspx ├── packages └── repositories.config ├── .gitignore ├── TeamCityDashboard.sln └── README.md /LICENSE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/LICENSE.txt -------------------------------------------------------------------------------- /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/.nuget/NuGet.exe -------------------------------------------------------------------------------- /libs/ITCloud.Web.Routing.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/libs/ITCloud.Web.Routing.dll -------------------------------------------------------------------------------- /TeamCityDashboard/images/q42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/images/q42.png -------------------------------------------------------------------------------- /TeamCityDashboard/fonts/Segoe UI.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/fonts/Segoe UI.ttf -------------------------------------------------------------------------------- /TeamCityDashboard/fonts/SegoeWP.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/fonts/SegoeWP.ttf -------------------------------------------------------------------------------- /TeamCityDashboard/fonts/segoeui.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/fonts/segoeui.ttf -------------------------------------------------------------------------------- /TeamCityDashboard/fonts/segoeuil.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/fonts/segoeuil.ttf -------------------------------------------------------------------------------- /TeamCityDashboard/images/grid-unit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/images/grid-unit.png -------------------------------------------------------------------------------- /TeamCityDashboard/fonts/Segoe UI Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/fonts/Segoe UI Bold.ttf -------------------------------------------------------------------------------- /TeamCityDashboard/fonts/SegoeWP-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/fonts/SegoeWP-Black.ttf -------------------------------------------------------------------------------- /TeamCityDashboard/fonts/SegoeWP-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/fonts/SegoeWP-Light.ttf -------------------------------------------------------------------------------- /TeamCityDashboard/images/transparent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/images/transparent.gif -------------------------------------------------------------------------------- /TeamCityDashboard/Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Codebehind="Global.asax.cs" Inherits="TeamCityDashboard.TeamCityDashboardApplication" Language="C#" %> 2 | -------------------------------------------------------------------------------- /TeamCityDashboard/fonts/SegoeWP-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchie84/teamcity-dashboard/HEAD/TeamCityDashboard/fonts/SegoeWP-Semibold.ttf -------------------------------------------------------------------------------- /packages/repositories.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /TeamCityDashboard/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /TeamCityDashboard/Web.Release.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | bin 3 | obj 4 | logs 5 | 6 | # mstest test results 7 | TestResults 8 | *.suo 9 | *.user 10 | packages/* 11 | !packages/repositories.config 12 | web.config 13 | *.Publish.xml 14 | *.ncrunchsolution 15 | -------------------------------------------------------------------------------- /TeamCityDashboard/Interfaces/IBuildRun.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace TeamCityDashboard.Interfaces 7 | { 8 | public interface IBuildRun 9 | { 10 | IBuildConfig BuildConfig { get; } 11 | string Id { get; } 12 | string Name { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TeamCityDashboard/Models/ProjectVisible.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | 6 | namespace TeamCityDashboard.Models 7 | { 8 | /// 9 | /// hackish object to cache boolean value 10 | /// 11 | public class ProjectVisible 12 | { 13 | public bool Visible { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /TeamCityDashboard/Interfaces/IBuildConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace TeamCityDashboard.Interfaces 7 | { 8 | public interface IBuildConfig 9 | { 10 | string Id { get; } 11 | string Name { get; } 12 | string Url { get; } 13 | DateTime? CurrentBuildDate { get; } 14 | bool CurrentBuildIsSuccesfull { get; } 15 | IEnumerable PossibleBuildBreakerEmailAddresses { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TeamCityDashboard/Interfaces/IProject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace TeamCityDashboard.Interfaces 7 | { 8 | public interface IProject 9 | { 10 | string Id { get; } 11 | string Name { get; } 12 | string Url { get; } 13 | string IconUrl { get; } 14 | string SonarProjectKey { get; } 15 | IEnumerable BuildConfigs { get; } 16 | DateTime? LastBuildDate { get; } 17 | ICodeStatistics Statistics { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TeamCityDashboard/Models/CodeStatistics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using TeamCityDashboard.Interfaces; 6 | 7 | namespace TeamCityDashboard.Models 8 | { 9 | public class CodeStatistics : ICodeStatistics 10 | { 11 | public double CodeCoveragePercentage { get; set; } 12 | public double CyclomaticComplexityClass { get; set; } 13 | public double CyclomaticComplexityFunction { get; set; } 14 | public int NonCommentingLinesOfCode { get; set; } 15 | public double CommentLinesPercentage { get; set; } 16 | public int CommentLines { get; set; } 17 | public int AmountOfUnitTests { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /TeamCityDashboard/Models/BuildConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using TeamCityDashboard.Interfaces; 6 | 7 | namespace TeamCityDashboard.Models 8 | { 9 | [System.Diagnostics.DebuggerDisplay("BuildConfig - {Id} - {Name} - CurrentBuildDate {CurrentBuildDate}")] 10 | public class BuildConfig : IBuildConfig 11 | { 12 | public string Id { get; set; } 13 | public string Name { get; set; } 14 | public bool CurrentBuildIsSuccesfull { get; set; } 15 | public string Url { get; set; } 16 | public IEnumerable PossibleBuildBreakerEmailAddresses { get; set; } 17 | public DateTime? CurrentBuildDate { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /TeamCityDashboard/Interfaces/ICacheService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace TeamCityDashboard.Interfaces 7 | { 8 | public interface ICacheService 9 | { 10 | /// 11 | /// Set item in cache, if secondsToCache <= 0 then does not expire 12 | /// 13 | /// 14 | /// 15 | /// 16 | void Set(string cacheId, object item, int secondsToCache); 17 | T Get(string cacheId) where T : class; 18 | T Get(string cacheId, Func getItemCallback, int secondsToCache) where T : class; 19 | void Delete(string cacheId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TeamCityDashboard/Models/Project.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using TeamCityDashboard.Interfaces; 6 | 7 | namespace TeamCityDashboard.Models 8 | { 9 | [System.Diagnostics.DebuggerDisplay("Project {Id} - {Name}")] 10 | public class Project : IProject 11 | { 12 | public string Id { get; set; } 13 | public string Name { get; set; } 14 | public string Url { get; set; } 15 | public string IconUrl { get; set; } 16 | public DateTime? LastBuildDate { get; set; } 17 | public IEnumerable BuildConfigs { get; set; } 18 | 19 | // sonar specific part 20 | 21 | public string SonarProjectKey { get; set; } 22 | public ICodeStatistics Statistics{ get; set;} 23 | 24 | public object[][] CoverageGraph { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /TeamCityDashboard/Properties/PublishProfiles/Publish to disk.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Package 9 | Release 10 | Any CPU 11 | 12 | False 13 | ..\TeamCityDashboard.zip 14 | true 15 | TeamcityDashboard 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /TeamCityDashboard/Controllers/PushEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace TeamCityDashboard.Models 7 | { 8 | public class PushEvent 9 | { 10 | /// 11 | /// The SHA of the HEAD commit on the repository. 12 | /// 13 | public string EventId { get; set; } 14 | 15 | /// 16 | /// event=>actor=>login 17 | /// 18 | public string ActorUsername { get; set; } 19 | 20 | /// 21 | /// event=>actor=>gravatar_id 22 | /// 23 | public string ActorGravatarId { get; set; } 24 | 25 | /// 26 | /// event=>payload=>size 27 | /// 28 | public int AmountOfCommits { get; set; } 29 | 30 | /// 31 | /// event=>repo=>name 32 | /// 33 | public string RepositoryName { get; set; } 34 | 35 | /// 36 | /// event=>payload=>ref 37 | /// 38 | public string BranchName { get; set; } 39 | 40 | /// 41 | /// event=>created_at 42 | /// 43 | public DateTime Created { get; set; } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /TeamCityDashboard.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{50908BAA-7A53-45ED-88A1-7849F19FA23B}" 5 | ProjectSection(SolutionItems) = preProject 6 | .nuget\NuGet.exe = .nuget\NuGet.exe 7 | .nuget\NuGet.targets = .nuget\NuGet.targets 8 | EndProjectSection 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamCityDashboard", "TeamCityDashboard\TeamCityDashboard.csproj", "{B43EC6D7-11D6-4E8D-B943-3918CAF79E63}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {B43EC6D7-11D6-4E8D-B943-3918CAF79E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {B43EC6D7-11D6-4E8D-B943-3918CAF79E63}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {B43EC6D7-11D6-4E8D-B943-3918CAF79E63}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {B43EC6D7-11D6-4E8D-B943-3918CAF79E63}.Release|Any CPU.Build.0 = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /TeamCityDashboard/Interfaces/ICodeStatistics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace TeamCityDashboard.Interfaces 7 | { 8 | public interface ICodeStatistics 9 | { 10 | /// 11 | /// sonar key=coverage 12 | /// 13 | double CodeCoveragePercentage { get; } 14 | //int CodeCoveragePercentage { get; } 15 | 16 | /// 17 | /// sonar key=class_complexity 18 | /// 19 | double CyclomaticComplexityClass { get; } 20 | 21 | /// 22 | /// sonar key=function_complexity 23 | /// 24 | double CyclomaticComplexityFunction { get; } 25 | 26 | /// 27 | /// sonar key=ncloc 28 | /// 29 | int NonCommentingLinesOfCode { get; } 30 | 31 | /// 32 | /// sonar key=comment_lines_density 33 | /// 34 | double CommentLinesPercentage { get; } 35 | 36 | /// 37 | /// sonar key=comment_lines 38 | /// 39 | int CommentLines { get; } 40 | 41 | /// 42 | /// sonar key=tests 43 | /// 44 | int AmountOfUnitTests { get; } 45 | 46 | /// 47 | /// sonar key=lcom4 48 | /// 49 | //double LackOfCohesionOfMethods { get; } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /TeamCityDashboard/Web.Debug.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /TeamCityDashboard/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("TeamCityDashboard")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Q42")] 12 | [assembly: AssemblyProduct("TeamCityDashboard")] 13 | [assembly: AssemblyCopyright("Copyright © Q42 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("9e57d01f-524b-4539-8163-1213e8e9edb4")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // You can specify all the values or you can default the Revision and Build Numbers 31 | // by using the '*' as shown below: 32 | [assembly: AssemblyVersion("1.0.0.0")] 33 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /TeamCityDashboard/Services/WebCacheService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using TeamCityDashboard.Interfaces; 6 | using System.Web.Caching; 7 | 8 | namespace TeamCityDashboard.Services 9 | { 10 | public class WebCacheService : ICacheService 11 | { 12 | public const int CACHE_PERMANENTLY = 0; 13 | 14 | public T Get(string cacheId) where T : class 15 | { 16 | return HttpRuntime.Cache.Get(cacheId) as T; 17 | } 18 | 19 | public T Get(string cacheId, Func getItemCallback, int secondsToCache) where T : class 20 | { 21 | T item = Get(cacheId); 22 | if (item == null) 23 | { 24 | item = getItemCallback(); 25 | Set(cacheId, item, secondsToCache); 26 | } 27 | return item; 28 | } 29 | 30 | public void Delete(string cacheId) 31 | { 32 | HttpRuntime.Cache.Remove(cacheId); 33 | } 34 | 35 | /// 36 | /// Set item in cache, if secondsToCache <= 0 then does not expire 37 | /// 38 | /// 39 | /// 40 | /// 41 | public void Set(string cacheId, object item, int secondsToCache) 42 | { 43 | if (item == null) 44 | throw new NotImplementedException("No NULL values can be cached at the moment."); 45 | 46 | if (secondsToCache <= CACHE_PERMANENTLY) 47 | { 48 | //never remove from cache 49 | HttpRuntime.Cache.Insert(cacheId, item, null, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, null); 50 | } 51 | else 52 | { 53 | HttpRuntime.Cache.Insert(cacheId, item, null, DateTime.Now.AddSeconds(secondsToCache), System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /TeamCityDashboard/Scripts/jquery.fn.sortelements.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery.fn.sortElements 3 | * http://james.padolsey.com/javascript/sorting-elements-with-jquery/ 4 | * -------------- 5 | * @param Function comparator: 6 | * Exactly the same behaviour as [1,2,3].sort(comparator) 7 | * 8 | * @param Function getSortable 9 | * A function that should return the element that is 10 | * to be sorted. The comparator will run on the 11 | * current collection, but you may want the actual 12 | * resulting sort to occur on a parent or another 13 | * associated element. 14 | * 15 | * E.g. $('td').sortElements(comparator, function(){ 16 | * return this.parentNode; 17 | * }) 18 | * 19 | * The 's parent () will be sorted instead 20 | * of the itself. 21 | */ 22 | jQuery.fn.sortElements = (function(){ 23 | 24 | var sort = [].sort; 25 | 26 | return function(comparator, getSortable) { 27 | 28 | getSortable = getSortable || function(){return this;}; 29 | 30 | var placements = this.map(function(){ 31 | 32 | var sortElement = getSortable.call(this), 33 | parentNode = sortElement.parentNode, 34 | 35 | // Since the element itself will change position, we have 36 | // to have some way of storing its original position in 37 | // the DOM. The easiest way is to have a 'flag' node: 38 | nextSibling = parentNode.insertBefore( 39 | document.createTextNode(''), 40 | sortElement.nextSibling 41 | ); 42 | 43 | return function() { 44 | 45 | if (parentNode === this) { 46 | throw new Error( 47 | "You can't sort elements if any one is a descendant of another." 48 | ); 49 | } 50 | 51 | // Insert before flag: 52 | parentNode.insertBefore(this, nextSibling); 53 | // Remove flag: 54 | parentNode.removeChild(nextSibling); 55 | 56 | }; 57 | 58 | }); 59 | 60 | return sort.call(this, comparator).each(function(i){ 61 | placements[i].call(getSortable.call(this)); 62 | }); 63 | 64 | }; 65 | 66 | })(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Teamcity Dashboard 2 | ======== 3 | :warning: This project has not been updated in 5 years. YMMV. 4 | 5 | Dashboard to display useful information from TeamCity and if available merges it with Sonar results. If somebody broke the build it tries to display their gravatar. 6 | 7 | Installation 8 | ------ 9 | * Compile project (.Net 4.0). This will automatically copy the web.example.config => web.config; 10 | * Modify the web.config to point to your teamcity, sonar & github + valid credentials; 11 | * Hook up the site in IIS (.Net 4.0); 12 | * Visit the dashboard on the configured URL. 13 | 14 | Configuration 15 | ------ 16 | The site only shows non-archived projects which contain at least one build config where ‘enable status widget’ is checked. If you want to hide specific build steps (i.e. false positives) disable the 'status' widget in the specific build step. When a build is broken the dashboard tries to retrace which user broke the build by traversing back in history until it finds a succesful build. It then will display the user(s) gravatar of the configured email address. 17 | 18 | Sonar integration 19 | ------ 20 | The sonar integration is done via configuration in TeamCity. Add the following parameter to your project configuration in TeamCity: 21 | * `sonar.project.key` containing the exact project key as found in Sonar 22 | 23 | Github integration 24 | ------ 25 | For displaying events in your organization (pushes?) to github you can authenticate the dashboard to access your github account. You need to generate an Oauth2 token which is quite easy: 26 | `curl -i -u "user:pass" https://api.github.com/authorizations -d '{"scopes":["repo"]}'` 27 | (Documentation: http://developer.github.com/v3/oauth/#create-a-new-authorization). I do not know exactly which `scopes` are required for what but the `repo` value let me access the (private) events of my organization which was what i needed. 28 | 29 | Project logo 30 | ------ 31 | If you want to display a logo in the interface you can also configure this in TeamCity. Add the following parameter to your project's configuration in TeamCity: 32 | * `dashboard.project.logo.url` containing the URL to a image of your application. 33 | 34 | Retrieval of the users Gravatar 35 | ------ 36 | * The dashboard can only find the email address of a user if the user has mapped its VCS username to its account. If you see thing in Teamcity like 'changes (4) crunchie84@github' it might not be linked. This can be configured in the user his settings in TeamCity. 37 | 38 | About the font: Segoe UI 39 | ======== 40 | The font is copyright by Microsoft. Please visit this link http://www.microsoft.com/typography/fonts/family.aspx?FID=331 to determine how you can get a licensed version. Fallback is Serif. 41 | -------------------------------------------------------------------------------- /TeamCityDashboard/Global.asax.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Web; 7 | using System.Web.Mvc; 8 | using System.Web.Routing; 9 | using ITCloud.Web.Routing; 10 | using log4net; 11 | 12 | namespace TeamCityDashboard 13 | { 14 | public class TeamCityDashboardApplication : System.Web.HttpApplication 15 | { 16 | private static readonly ILog log = LogManager.GetLogger(typeof(TeamCityDashboardApplication)); 17 | 18 | public static void RegisterGlobalFilters(GlobalFilterCollection filters) 19 | { 20 | filters.Add(new HandleErrorAttribute()); 21 | } 22 | 23 | public static void RegisterRoutes(RouteCollection routes) 24 | { 25 | routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 26 | 27 | // discover UrlRoute attributes on MVC controller classes in the project 28 | routes.DiscoverMvcControllerRoutes(); 29 | 30 | //routes.MapRoute( 31 | // "Default", // Route name 32 | // "{controller}/{action}/{id}", // URL with parameters 33 | // new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults 34 | //); 35 | 36 | } 37 | 38 | protected void Application_Start() 39 | { 40 | log4net.Config.XmlConfigurator.Configure(); 41 | 42 | log.Info("Teamcity dashboard starting..."); 43 | 44 | string version = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; 45 | if (string.IsNullOrWhiteSpace(version)) 46 | version = Assembly.GetExecutingAssembly().GetName().Version.ToString(); 47 | log.InfoFormat("Current version: '{0}'", version); 48 | 49 | AreaRegistration.RegisterAllAreas(); 50 | 51 | RegisterGlobalFilters(GlobalFilters.Filters); 52 | RegisterRoutes(RouteTable.Routes); 53 | 54 | log.Info("Application started"); 55 | } 56 | 57 | protected void Application_Error(object sender, EventArgs e) 58 | { 59 | if (log.IsErrorEnabled) 60 | { 61 | HttpApplication application = (HttpApplication)sender; 62 | if (application.Context == null) 63 | return; 64 | 65 | var exception = application.Context.Error; 66 | 67 | string logMessage = "An uncaught exception occurred"; 68 | try 69 | { 70 | if (Request != null && Request.Url != null) 71 | logMessage += string.Format(" in {0} {1} (referrer={2})", Request.HttpMethod, Request.Url.PathAndQuery, Request.UrlReferrer); 72 | } 73 | catch (HttpException) 74 | { 75 | // an HttpException could occur with message: "Request is not available in this context" but then we ignore it 76 | } 77 | log.Error(logMessage, exception); 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /TeamCityDashboard/Services/AbstractHttpDataService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text; 7 | using System.Web; 8 | using System.Xml; 9 | using TeamCityDashboard.Interfaces; 10 | 11 | namespace TeamCityDashboard.Services 12 | { 13 | public abstract class AbstractHttpDataService 14 | { 15 | protected readonly string BaseUrl; 16 | protected readonly string UserName; 17 | protected readonly string Password; 18 | 19 | protected readonly ICacheService CacheService; 20 | 21 | public AbstractHttpDataService(string baseUrl, string username, string password, ICacheService cacheService) 22 | { 23 | this.BaseUrl = baseUrl; 24 | this.UserName = username; 25 | this.Password = password; 26 | this.CacheService = cacheService; 27 | } 28 | 29 | /// 30 | /// Duration to cache some things which almost never change 31 | /// 32 | protected const int CACHE_DURATION = 3 * 60 * 60;//3 hours 33 | 34 | /// 35 | /// retrieve the content of given url and parse it to xmldocument. throws httpexception if raised 36 | /// 37 | /// 38 | /// 39 | /// original code on http://www.stickler.de/en/information/code-snippets/httpwebrequest-basic-authentication.aspx 40 | protected XmlDocument GetPageContents(string relativeUrl) 41 | { 42 | XmlDocument result = new XmlDocument(); 43 | result.LoadXml(GetContents(relativeUrl)); 44 | return result; 45 | } 46 | 47 | /// 48 | /// retrieve the content of given url. throws httpexception if raised 49 | /// 50 | /// 51 | /// 52 | protected string GetContents(string relativeUrl) 53 | { 54 | try 55 | { 56 | Uri uri = new Uri(string.Format("{0}{1}", BaseUrl, relativeUrl)); 57 | WebRequest myWebRequest = HttpWebRequest.Create(uri); 58 | 59 | HttpWebRequest myHttpWebRequest = (HttpWebRequest)myWebRequest; 60 | 61 | NetworkCredential myNetworkCredential = new NetworkCredential(UserName, Password); 62 | CredentialCache myCredentialCache = new CredentialCache(); 63 | myCredentialCache.Add(uri, "Basic", myNetworkCredential); 64 | 65 | myHttpWebRequest.PreAuthenticate = true; 66 | myHttpWebRequest.Credentials = myCredentialCache; 67 | 68 | using (WebResponse myWebResponse = myWebRequest.GetResponse()) 69 | { 70 | using (Stream responseStream = myWebResponse.GetResponseStream()) 71 | { 72 | StreamReader myStreamReader = new StreamReader(responseStream, Encoding.Default); 73 | return myStreamReader.ReadToEnd(); 74 | } 75 | } 76 | } 77 | catch (Exception e) 78 | { 79 | throw new HttpException(string.Format("Error while retrieving url '{0}': {1}", relativeUrl, e.Message), e); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /TeamCityDashboard/parameters.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /TeamCityDashboard/Web.example.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | false 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /TeamCityDashboard/css/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Segoe UI; 3 | src: url("../fonts/SegoeWP.ttf"); } 4 | 5 | @font-face { 6 | font-family: Segoe UI Light; 7 | src: url("../fonts/SegoeWP-Light.ttf"); } 8 | 9 | @font-face { 10 | font-family: Segoe UI Semibold; 11 | src: url("../fonts/SegoeWP-Semibold.ttf"); } 12 | 13 | body { 14 | margin: 0; 15 | x-background-image: url(../images/grid-unit.png); 16 | background-color: #000; 17 | color: #fff; 18 | font-family: Segoe UI, Sans-Serif; 19 | -webkit-overflow-scrolling: auto; } 20 | 21 | a { 22 | text-decoration: none; 23 | color: #fff; } 24 | 25 | h1, 26 | .xx-large { 27 | font-family: Segoe UI Light, Sans-Serif; 28 | font-size: 42pt; 29 | line-height: 48pt; 30 | margin: 0; } 31 | 32 | h2, 33 | .x-large { 34 | font-family: Segoe UI Light, Sans-Serif; 35 | font-size: 20pt; 36 | line-height: 24pt; 37 | margin: 0; } 38 | 39 | h3, 40 | .large { 41 | font-family: Segoe UI, Sans-Serif; 42 | font-size: 19px; 43 | line-height: 22px; 44 | margin: 0; 45 | letter-spacing: 1px; } 46 | 47 | p, 48 | .medium { 49 | font-family: Arial, Segoe UI, Sans-Serif; 50 | font-size: 11pt; 51 | line-height: 15pt; } 52 | 53 | h4, 54 | .small { 55 | font-family: Arial, Segoe UI, Sans-Serif; 56 | font-size: 10pt; 57 | font-weight: normal; 58 | line-height: 15pt; } 59 | 60 | #title { 61 | margin: 20px 0 10px 100px; } 62 | 63 | #projectsContainer { 64 | margin-left: 100px; 65 | margin-right: 100px; } 66 | 67 | #pushMessagesContainer { 68 | margin: 20px 0 0 100px; } 69 | #pushMessagesContainer h2 { 70 | margin-bottom: 10px; } 71 | #pushMessagesContainer .item { 72 | margin-right: 10px; } 73 | 74 | .item { 75 | float: left; 76 | margin-bottom: 10px; 77 | width: 250px; 78 | /*default width; we try to make them 120 with JS*/ 79 | min-height: 120px; 80 | overflow: hidden; } 81 | .item.event { 82 | background-color: rgba(6, 82, 196, 0.8); } 83 | .item.successful { 84 | background-color: #363; 85 | height: 120px; } 86 | .item.failing { 87 | background: #822; } 88 | .item .item-images { 89 | position: relative; 90 | margin-bottom: 10px; } 91 | .item .item-images.images-uneven-rows { 92 | /* Add extra margin bottom to negate the uneven rows slidly misaligning the blocks*/ 93 | overflow: hidden; 94 | margin-bottom: 5px; } 95 | .item .item-images .full-size { 96 | display: block; 97 | width: 250px; 98 | height: 250px; } 99 | .item .item-images .half-size { 100 | display: block; 101 | float: left; 102 | width: 125px; 103 | height: 125px; } 104 | .item .item-text { 105 | position: relative; 106 | clear: left; 107 | display: block; 108 | padding: 10px 10px 5px 10px; 109 | box-sizing: border-box; 110 | height: 120px; 111 | overflow: hidden; } 112 | .item .item-text .details { 113 | float: left; 114 | width: 145px; } 115 | .item .item-text .details .small { 116 | line-height: 14pt; 117 | margin: 5px 0; } 118 | .item .item-text .logo, 119 | .item .item-text .pusher { 120 | margin: 0 15px 5px 0; 121 | max-width: 60px; 122 | max-height: 60px; 123 | float: left; } 124 | .item .item-text .statistics-container .statistic .value { 125 | font-size: 11pt; 126 | float: right; } 127 | .item .extra-text { 128 | position: relative; 129 | display: none; 130 | padding: 0; 131 | box-sizing: border-box; } 132 | .item .extra-text h3 { 133 | margin: 10px 0 0 20px; 134 | font-weight: normal; } 135 | .item .extra-text .chart { 136 | width: 250px; 137 | height: 85px; } 138 | 139 | -------------------------------------------------------------------------------- /TeamCityDashboard/Services/SonarDataService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Web; 6 | using TeamCityDashboard.Interfaces; 7 | using TeamCityDashboard.Models; 8 | 9 | namespace TeamCityDashboard.Services 10 | { 11 | public class SonarDataService : AbstractHttpDataService 12 | { 13 | public SonarDataService(string baseUrl, string username, string password, ICacheService cacheService) : base(baseUrl, username, password, cacheService) { } 14 | 15 | //http://sonar.q42.net/api/resources?resource=NegenTwee:PartnerApi&metrics=ncloc,coverage&verbose=true&includetrends=true 16 | private const string PROJECT_TRENDS_URL = @"/api/resources?resource={0}&metrics={1}&verbose=true&includetrends=true&format=xml"; 17 | private const string PROJECT_METRICS_CSV = @"coverage,class_complexity,function_complexity,ncloc,comment_lines_density,comment_lines,tests"; 18 | 19 | /// 20 | /// create coverage csv report 21 | /// 2013-01-01T00:00:00+0100 22 | /// 23 | private const string PROJECT_TIMEMACHINE_URL = @"/api/timemachine?resource={0}&metrics=coverage&fromDateTime={1}&format=csv"; 24 | 25 | public ICodeStatistics GetProjectStatistics(string projectKey) 26 | { 27 | CodeStatistics result = CacheService.Get("sonar-stats-" + projectKey, () => { 28 | var data = GetPageContents(string.Format(CultureInfo.InvariantCulture, PROJECT_TRENDS_URL, projectKey, PROJECT_METRICS_CSV)); 29 | 30 | //TODO this code can be greatly improved - error checking etc 31 | var stats = new CodeStatistics 32 | { 33 | AmountOfUnitTests = (int)double.Parse(data.SelectSingleNode("resources/resource/msr[key='tests']/val").InnerText, CultureInfo.InvariantCulture), 34 | NonCommentingLinesOfCode = (int)double.Parse(data.SelectSingleNode("resources/resource/msr[key='ncloc']/val").InnerText, CultureInfo.InvariantCulture), 35 | CommentLines = (int)double.Parse(data.SelectSingleNode("resources/resource/msr[key='comment_lines']/val").InnerText, CultureInfo.InvariantCulture), 36 | CommentLinesPercentage = double.Parse(data.SelectSingleNode("resources/resource/msr[key='comment_lines_density']/val").InnerText, CultureInfo.InvariantCulture), 37 | CyclomaticComplexityClass = double.Parse(data.SelectSingleNode("resources/resource/msr[key='class_complexity']/val").InnerText, CultureInfo.InvariantCulture), 38 | CyclomaticComplexityFunction = double.Parse(data.SelectSingleNode("resources/resource/msr[key='function_complexity']/val").InnerText, CultureInfo.InvariantCulture) 39 | }; 40 | 41 | if (data.SelectSingleNode("resources/resource/msr[key='coverage']/val") != null) 42 | stats.CodeCoveragePercentage = double.Parse(data.SelectSingleNode("resources/resource/msr[key='coverage']/val").InnerText, CultureInfo.InvariantCulture); 43 | 44 | return stats; 45 | }, 3600); 46 | 47 | return result; 48 | } 49 | 50 | public IEnumerable> GetProjectCoverage(string projectKey) 51 | { 52 | string url = string.Format(CultureInfo.InvariantCulture, PROJECT_TIMEMACHINE_URL, projectKey, DateTime.Now.AddMonths(-2).ToString("s", CultureInfo.InvariantCulture)); 53 | string csv = GetContents(url); 54 | var lines = from line 55 | in csv.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).Skip(1) 56 | let columns = line.Split(',') 57 | select new KeyValuePair(parseDateTime(columns.First()), double.Parse(columns.Skip(1).First(), CultureInfo.InvariantCulture)); 58 | 59 | return lines; 60 | } 61 | 62 | private static DateTime parseDateTime(string date) 63 | { 64 | DateTime theDate; 65 | if (DateTime.TryParseExact(date, @"yyyy-MM-dd\THH:mm:sszz\0\0", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out theDate)) 66 | { 67 | return theDate; 68 | } 69 | return DateTime.MinValue; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /TeamCityDashboard/css/styles.scss: -------------------------------------------------------------------------------- 1 | @font-face 2 | { 3 | font-family: Segoe UI; 4 | src: url('../fonts/SegoeWP.ttf'); 5 | } 6 | @font-face 7 | { 8 | font-family: Segoe UI Light; 9 | src: url('../fonts/SegoeWP-Light.ttf'); 10 | } 11 | @font-face 12 | { 13 | font-family: Segoe UI Semibold; 14 | src: url('../fonts/SegoeWP-Semibold.ttf'); 15 | } 16 | 17 | body 18 | { 19 | margin: 0; 20 | x-background-image: url(../images/grid-unit.png); 21 | background-color: #000; 22 | color: #fff; 23 | font-family: Segoe UI, Sans-Serif; 24 | -webkit-overflow-scrolling: auto; 25 | } 26 | 27 | a 28 | { 29 | text-decoration: none; 30 | color: #fff; 31 | } 32 | 33 | h1, 34 | .xx-large 35 | { 36 | font-family: Segoe UI Light, Sans-Serif; 37 | font-size: 42pt; 38 | line-height: 48pt; 39 | margin:0; 40 | } 41 | 42 | h2, 43 | .x-large 44 | { 45 | font-family: Segoe UI Light, Sans-Serif; 46 | font-size: 20pt; 47 | line-height: 24pt; 48 | margin:0; 49 | 50 | } 51 | 52 | h3, 53 | .large 54 | { 55 | font-family: Segoe UI, Sans-Serif; 56 | font-size: 19px; 57 | line-height: 22px; 58 | margin:0; 59 | letter-spacing:1px; 60 | } 61 | 62 | p, 63 | .medium 64 | { 65 | font-family: Arial, Segoe UI, Sans-Serif; 66 | font-size: 11pt; 67 | line-height: 15pt; 68 | } 69 | 70 | h4, 71 | .small 72 | { 73 | font-family: Arial, Segoe UI, Sans-Serif; 74 | font-size: 10pt; 75 | font-weight: normal; 76 | line-height: 15pt; 77 | } 78 | 79 | #title 80 | { 81 | margin: 20px 0 10px 100px; 82 | } 83 | 84 | #projectsContainer 85 | { 86 | margin-left: 100px; 87 | margin-right: 100px; 88 | } 89 | #pushMessagesContainer 90 | { 91 | margin: 20px 0 0 100px; 92 | 93 | h2 94 | { 95 | margin-bottom: 10px; 96 | } 97 | .item 98 | { 99 | margin-right: 10px; 100 | } 101 | } 102 | 103 | 104 | .item 105 | { 106 | float: left; 107 | margin-bottom: 10px; 108 | width: 250px; /*default width; we try to make them 120 with JS*/ 109 | min-height: 120px; 110 | overflow: hidden; 111 | 112 | &.event 113 | { 114 | background-color: rgba(6, 82, 196, 0.8); 115 | } 116 | 117 | &.successful 118 | { 119 | background-color: #363; 120 | height: 120px; 121 | } 122 | 123 | &.failing 124 | { 125 | background: #822; 126 | } 127 | 128 | .item-images 129 | { 130 | position: relative; 131 | margin-bottom: 10px; 132 | 133 | &.images-uneven-rows 134 | { 135 | /* Add extra margin bottom to negate the uneven rows slidly misaligning the blocks*/ 136 | overflow: hidden; 137 | margin-bottom: 5px; 138 | } 139 | 140 | .full-size 141 | { 142 | display: block; 143 | width: 250px; 144 | height: 250px; 145 | } 146 | 147 | .half-size 148 | { 149 | display: block; 150 | float: left; 151 | width: 125px; 152 | height: 125px; 153 | } 154 | } 155 | 156 | .item-text 157 | { 158 | position: relative; 159 | clear: left; 160 | display: block; 161 | padding: 10px 10px 5px 10px; 162 | box-sizing: border-box; 163 | height: 120px; 164 | overflow:hidden; 165 | 166 | .details 167 | { 168 | float:left; 169 | width:145px; 170 | 171 | .small 172 | { 173 | line-height: 14pt; 174 | margin:5px 0; 175 | } 176 | } 177 | 178 | .logo, 179 | .pusher 180 | { 181 | margin:0 15px 5px 0; 182 | max-width: 60px; 183 | max-height: 60px; 184 | float:left; 185 | } 186 | 187 | .statistics-container 188 | { 189 | .statistic .value 190 | { 191 | font-size: 11pt; 192 | float: right; 193 | } 194 | } 195 | } 196 | 197 | .extra-text 198 | { 199 | position: relative; 200 | display: none; 201 | padding: 0; 202 | box-sizing: border-box; 203 | 204 | h3 205 | { 206 | margin: 10px 0 0 20px; 207 | font-weight: normal; 208 | } 209 | 210 | .chart 211 | { 212 | width: 250px; 213 | height: 85px; 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 8 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 9 | $([System.IO.Path]::Combine($(SolutionDir), "packages")) 10 | 11 | 12 | $(SolutionDir).nuget 13 | packages.config 14 | $(SolutionDir)packages 15 | 16 | 17 | $(NuGetToolsPath)\nuget.exe 18 | "$(NuGetExePath)" 19 | mono --runtime=v4.0.30319 $(NuGetExePath) 20 | 21 | $(TargetDir.Trim('\\')) 22 | 23 | 24 | "" 25 | 26 | 27 | false 28 | 29 | 30 | false 31 | 32 | 33 | $(NuGetCommand) install "$(PackagesConfig)" -source $(PackageSources) -o "$(PackagesDir)" 34 | $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols 35 | 36 | 37 | 38 | 39 | $(PrepareForBuildDependsOn); 40 | RestorePackages; 41 | 42 | 46 | 47 | 48 | 49 | $(PrepareForBuildDependsOn); 50 | BuildPackage; 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | -------------------------------------------------------------------------------- /TeamCityDashboard/Scripts/jquery.timeago.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Timeago is a jQuery plugin that makes it easy to support automatically 3 | * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). 4 | * 5 | * @name timeago 6 | * @version 0.11.4 7 | * @requires jQuery v1.2.3+ 8 | * @author Ryan McGeary 9 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * For usage and examples, visit: 12 | * http://timeago.yarp.com/ 13 | * 14 | * Copyright (c) 2008-2012, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) 15 | */ 16 | (function($) { 17 | $.timeago = function(timestamp) { 18 | if (timestamp instanceof Date) { 19 | return inWords(timestamp); 20 | } else if (typeof timestamp === "string") { 21 | return inWords($.timeago.parse(timestamp)); 22 | } else if (typeof timestamp === "number") { 23 | return inWords(new Date(timestamp)); 24 | } else { 25 | return inWords($.timeago.datetime(timestamp)); 26 | } 27 | }; 28 | var $t = $.timeago; 29 | 30 | $.extend($.timeago, { 31 | settings: { 32 | refreshMillis: 60000, 33 | allowFuture: false, 34 | strings: { 35 | prefixAgo: null, 36 | prefixFromNow: null, 37 | suffixAgo: "ago", 38 | suffixFromNow: "from now", 39 | seconds: "less than a minute", 40 | minute: "about a minute", 41 | minutes: "%d minutes", 42 | hour: "about an hour", 43 | hours: "about %d hours", 44 | day: "a day", 45 | days: "%d days", 46 | month: "about a month", 47 | months: "%d months", 48 | year: "about a year", 49 | years: "%d years", 50 | wordSeparator: " ", 51 | numbers: [] 52 | } 53 | }, 54 | inWords: function(distanceMillis) { 55 | var $l = this.settings.strings; 56 | var prefix = $l.prefixAgo; 57 | var suffix = $l.suffixAgo; 58 | if (this.settings.allowFuture) { 59 | if (distanceMillis < 0) { 60 | prefix = $l.prefixFromNow; 61 | suffix = $l.suffixFromNow; 62 | } 63 | } 64 | 65 | var seconds = Math.abs(distanceMillis) / 1000; 66 | var minutes = seconds / 60; 67 | var hours = minutes / 60; 68 | var days = hours / 24; 69 | var years = days / 365; 70 | 71 | function substitute(stringOrFunction, number) { 72 | var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; 73 | var value = ($l.numbers && $l.numbers[number]) || number; 74 | return string.replace(/%d/i, value); 75 | } 76 | 77 | var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || 78 | seconds < 90 && substitute($l.minute, 1) || 79 | minutes < 45 && substitute($l.minutes, Math.round(minutes)) || 80 | minutes < 90 && substitute($l.hour, 1) || 81 | hours < 24 && substitute($l.hours, Math.round(hours)) || 82 | hours < 42 && substitute($l.day, 1) || 83 | days < 30 && substitute($l.days, Math.round(days)) || 84 | days < 45 && substitute($l.month, 1) || 85 | days < 365 && substitute($l.months, Math.round(days / 30)) || 86 | years < 1.5 && substitute($l.year, 1) || 87 | substitute($l.years, Math.round(years)); 88 | 89 | var separator = $l.wordSeparator === undefined ? " " : $l.wordSeparator; 90 | return $.trim([prefix, words, suffix].join(separator)); 91 | }, 92 | parse: function(iso8601) { 93 | var s = $.trim(iso8601); 94 | s = s.replace(/\.\d+/,""); // remove milliseconds 95 | s = s.replace(/-/,"/").replace(/-/,"/"); 96 | s = s.replace(/T/," ").replace(/Z/," UTC"); 97 | s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 98 | return new Date(s); 99 | }, 100 | datetime: function(elem) { 101 | var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); 102 | return $t.parse(iso8601); 103 | }, 104 | isTime: function(elem) { 105 | // jQuery's `is()` doesn't play well with HTML5 in IE 106 | return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); 107 | } 108 | }); 109 | 110 | $.fn.timeago = function() { 111 | var self = this; 112 | self.each(refresh); 113 | 114 | var $s = $t.settings; 115 | if ($s.refreshMillis > 0) { 116 | setInterval(function() { self.each(refresh); }, $s.refreshMillis); 117 | } 118 | return self; 119 | }; 120 | 121 | function refresh() { 122 | var data = prepareData(this); 123 | if (!isNaN(data.datetime)) { 124 | $(this).text(inWords(data.datetime)); 125 | } 126 | return this; 127 | } 128 | 129 | function prepareData(element) { 130 | element = $(element); 131 | if (!element.data("timeago")) { 132 | element.data("timeago", { datetime: $t.datetime(element) }); 133 | var text = $.trim(element.text()); 134 | if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { 135 | element.attr("title", text); 136 | } 137 | } 138 | return element.data("timeago"); 139 | } 140 | 141 | function inWords(date) { 142 | return $t.inWords(distance(date)); 143 | } 144 | 145 | function distance(date) { 146 | return (new Date().getTime() - date.getTime()); 147 | } 148 | 149 | // fix for IE6 suckage 150 | document.createElement("abbr"); 151 | document.createElement("time"); 152 | }(jQuery)); 153 | -------------------------------------------------------------------------------- /TeamCityDashboard/Scripts/metro-grid.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Windows 8 Metro Grid implementation by Tom Lokhorst 3 | * 4 | */ 5 | function MetroGrid() { 6 | } 7 | 8 | MetroGrid.prototype = { 9 | init: function ($grid) { 10 | this.$grid = $grid; 11 | 12 | // dummy group for space 13 | //this.layout(); 14 | 15 | //$(window).bind('resize', this.layout.bind(this)); 16 | }, 17 | 18 | animate: function () { 19 | var $items = this.$grid.find('.item:has(.item-text)'); 20 | var $item = $($items[Math.floor(Math.random() * $items.length)]); 21 | 22 | if ($item != null) { 23 | this.animateStep($item).then(function (step) { 24 | if (step) 25 | setTimeout(this.animateStep.bind(this, $item, step), (Math.random() * 2 + 3) * 1000); 26 | }.bind(this)); 27 | } 28 | 29 | setTimeout(this.animate.bind(this), (Math.random() * 3 + 2) * 1000);//2 tot 5 seconds between animations 30 | }, 31 | 32 | animateStep: function ($item, gotoStep) { 33 | return $.Deferred(function (dfd) { 34 | var step = gotoStep || $item.data('step') || 'uninitialized'; 35 | 36 | //if somehow we are triggert twice at the same time we bail out the second time 37 | if (step == 'animating') return; 38 | $item.data('step', 'animating'); 39 | 40 | 41 | if (step == 'uninitialized') { 42 | //hard position the extra text outside of the box and make it visible 43 | $itemText = $item.find('.item-text'); 44 | var mtop = $itemText.position() 45 | ? $item.height() - $itemText.position().top - $itemText.height() - 10 46 | : 0; 47 | mtop = Math.floor(mtop / 120) * 120; 48 | $item.find('.extra-text') 49 | .css({ marginTop: mtop }) 50 | .show(); 51 | 52 | step = 'start'; 53 | } 54 | 55 | if (step == 'start') { 56 | //if we have multiple images they will round-robin fade in fade out and change position 57 | var $images = $item.find('.item-images img'); 58 | if (!$item.hasClass('failing') || $images.length < 2) { 59 | step = 'up'; 60 | } 61 | else { 62 | var fst = Math.floor(Math.random() * $images.length); 63 | var snd = (fst + 1) % $images.length; 64 | 65 | var $fst = $($images[fst]); 66 | var $snd = $($images[snd]); 67 | 68 | $fst.animate({ opacity: 0, }, 'slow', function () { 69 | $snd.animate({ opacity: 0, }, 'slow', function () { 70 | $fst.swap($snd); 71 | 72 | $snd.animate({ opacity: 1, }, 'slow', function () { 73 | $fst.animate({ opacity: 1, }, 'slow', function () { 74 | setTimeout(function () { 75 | $item.data('step', 'up'); 76 | dfd.resolve(); 77 | }, 1000); 78 | }); 79 | }); 80 | }); 81 | }); 82 | } 83 | } 84 | 85 | if (step == '?') { 86 | if (!$item.hasClass('failing')) 87 | step = 'up'; 88 | } 89 | 90 | //now animate the extra-text portion to top 91 | if (step == 'up') { 92 | if (!$item.find('.extra-text').length) { 93 | //we dont animate up or down, ready with animation, ready for next cycle 94 | $item.data('step', 'start'); 95 | dfd.resolve(); 96 | return; 97 | } 98 | 99 | //130 = images should not display margin 100 | $item.children() 101 | .animate({ top: -(($item.hasClass('failing') && $item.find('.item-images img').length) ? 130 : 120) }, 'slow', function () { 102 | setTimeout(function () { 103 | dfd.resolve('down'); 104 | }.bind(this), 1000); 105 | }.bind(this)); 106 | } 107 | 108 | //now animate back to the bottom part 109 | if (step == 'down') { 110 | $item.children() 111 | .animate({ top: 0 }, 'slow', function () { 112 | setTimeout(function () { 113 | //ready for next animation cycle 114 | $item.data('step', 'start'); 115 | dfd.resolve(); 116 | }.bind(this), 1000); 117 | }.bind(this)); 118 | } 119 | }.bind(this)).promise(); 120 | }, 121 | }; 122 | 123 | if (!Function.prototype.bind) { 124 | Function.prototype.bind = function (oThis) { 125 | if (typeof this !== "function") { 126 | // closest thing possible to the ECMAScript 5 internal IsCallable function 127 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 128 | } 129 | 130 | var aArgs = Array.prototype.slice.call(arguments, 1), 131 | fToBind = this, 132 | fNOP = function () { }, 133 | fBound = function () { 134 | return fToBind.apply(this instanceof fNOP 135 | ? this 136 | : oThis || window, 137 | aArgs.concat(Array.prototype.slice.call(arguments))); 138 | }; 139 | 140 | fNOP.prototype = this.prototype; 141 | fBound.prototype = new fNOP(); 142 | 143 | return fBound; 144 | }; 145 | } 146 | 147 | jQuery.fn.swap = function (b) { 148 | b = jQuery(b)[0]; 149 | var a = this[0]; 150 | 151 | var t = a.parentNode.insertBefore(document.createTextNode(""), a); 152 | b.parentNode.insertBefore(a, b); 153 | t.parentNode.insertBefore(b, t); 154 | t.parentNode.removeChild(t); 155 | 156 | return this; 157 | }; 158 | 159 | -------------------------------------------------------------------------------- /TeamCityDashboard/Controllers/DashboardController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using ITCloud.Web.Routing; 7 | using TeamCityDashboard.Interfaces; 8 | using TeamCityDashboard.Services; 9 | using System.Configuration; 10 | using System.Diagnostics; 11 | using System.Reflection; 12 | 13 | namespace TeamCityDashboard.Controllers 14 | { 15 | public class DashboardController : Controller 16 | { 17 | private static TeamCityDataService TeamCityDataService = null; 18 | private static SonarDataService SonarDataService = null; 19 | private static GithubDataService GithubDataService = null; 20 | 21 | private static string _version; 22 | public string ProjectVersion 23 | { 24 | get 25 | { 26 | if (string.IsNullOrWhiteSpace(_version)) 27 | { 28 | _version = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; 29 | if (string.IsNullOrWhiteSpace(_version)) 30 | _version = Assembly.GetExecutingAssembly().GetName().Version.ToString(); 31 | } 32 | return _version; 33 | } 34 | } 35 | 36 | public DashboardController() 37 | { 38 | ICacheService cacheService = new WebCacheService(); 39 | 40 | //singletonish ftw 41 | if (TeamCityDataService == null) 42 | { 43 | TeamCityDataService = new TeamCityDataService( 44 | ConfigurationManager.AppSettings["teamcity.baseUrl"], 45 | ConfigurationManager.AppSettings["teamcity.username"], 46 | ConfigurationManager.AppSettings["teamcity.password"], 47 | cacheService 48 | ); 49 | } 50 | 51 | //singletonish ftw 52 | if (SonarDataService == null) 53 | { 54 | SonarDataService = new SonarDataService( 55 | ConfigurationManager.AppSettings["sonar.baseUrl"], 56 | ConfigurationManager.AppSettings["sonar.username"], 57 | ConfigurationManager.AppSettings["sonar.password"], 58 | cacheService 59 | ); 60 | } 61 | 62 | if (GithubDataService == null) 63 | { 64 | GithubDataService = new TeamCityDashboard.Services.GithubDataService( 65 | (string)ConfigurationManager.AppSettings["github.oauth2token"], 66 | (string)ConfigurationManager.AppSettings["github.api.events.url"] 67 | , cacheService 68 | ); 69 | } 70 | } 71 | 72 | [UrlRoute(Name = "VersionString", Path = "version")] 73 | [HttpGet()] 74 | public ActionResult Version() 75 | { 76 | return new JsonResult() 77 | { 78 | JsonRequestBehavior = JsonRequestBehavior.AllowGet, 79 | ContentEncoding = System.Text.Encoding.UTF8, 80 | Data = ProjectVersion 81 | }; 82 | } 83 | 84 | [UrlRoute(Name = "Data", Path = "data")] 85 | [HttpGet()] 86 | public ActionResult Data() 87 | { 88 | var projectsWithSonarDataAdded = from proj in TeamCityDataService.GetActiveProjects() 89 | select new TeamCityDashboard.Models.Project 90 | { 91 | Id = proj.Id, 92 | Name = proj.Name, 93 | BuildConfigs = proj.BuildConfigs, 94 | SonarProjectKey = proj.SonarProjectKey, 95 | Url = proj.Url, 96 | LastBuildDate = proj.LastBuildDate, 97 | IconUrl = proj.IconUrl, 98 | Statistics = string.IsNullOrWhiteSpace(proj.SonarProjectKey) ? (ICodeStatistics)null : SonarDataService.GetProjectStatistics(proj.SonarProjectKey), 99 | CoverageGraph = string.IsNullOrWhiteSpace(proj.SonarProjectKey) ? null : getProjectGraph(proj.SonarProjectKey) 100 | }; 101 | 102 | return new JsonResult() 103 | { 104 | JsonRequestBehavior = JsonRequestBehavior.AllowGet, 105 | ContentEncoding = System.Text.Encoding.UTF8, 106 | Data = projectsWithSonarDataAdded 107 | }; 108 | } 109 | 110 | private object[][] getProjectGraph(string sonarKey) 111 | { 112 | var data = SonarDataService.GetProjectCoverage(sonarKey); 113 | return (from kvp in data select new object[] { kvp.Key, kvp.Value }).ToArray(); 114 | } 115 | 116 | [UrlRoute(Name = "projectGraphData", Path = "projectgraph")] 117 | [HttpGet()] 118 | public ActionResult ProjectGraph(string sonarKey) 119 | { 120 | var data = SonarDataService.GetProjectCoverage(sonarKey); 121 | 122 | return new JsonResult() 123 | { 124 | JsonRequestBehavior = JsonRequestBehavior.AllowGet, 125 | ContentEncoding = System.Text.Encoding.UTF8, 126 | Data = (from kvp in data select new object[] { kvp.Key, kvp.Value }).ToArray() 127 | }; 128 | } 129 | 130 | [UrlRoute(Name = "Github push events", Path = "pushevents")] 131 | [HttpGet()] 132 | public ActionResult PushEvents() 133 | { 134 | return new JsonResult() 135 | { 136 | JsonRequestBehavior = JsonRequestBehavior.AllowGet, 137 | ContentEncoding = System.Text.Encoding.UTF8, 138 | Data = GithubDataService.GetRecentEvents() 139 | }; 140 | } 141 | 142 | [UrlRoute(Name = "Home", Path = "")] 143 | [HttpGet()] 144 | public ActionResult Index() 145 | { 146 | return View(); 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /TeamCityDashboard/Services/GithubDataService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text; 7 | using System.Web; 8 | using log4net; 9 | using Newtonsoft.Json.Linq; 10 | using TeamCityDashboard.Interfaces; 11 | using TeamCityDashboard.Models; 12 | 13 | namespace TeamCityDashboard.Services 14 | { 15 | public class GithubDataService 16 | { 17 | private static readonly ILog log = LogManager.GetLogger(typeof(GithubDataService)); 18 | 19 | private readonly ICacheService cacheService; 20 | private readonly string oauth2token; 21 | private readonly string eventsurl; 22 | 23 | private const string API_BASE_URL = @"https://api.github.com"; 24 | 25 | public GithubDataService(string oauth2token, string eventsurl, ICacheService cacheService) 26 | { 27 | if (cacheService == null) 28 | throw new ArgumentNullException("cacheService"); 29 | if (string.IsNullOrWhiteSpace(oauth2token)) 30 | throw new ArgumentNullException("oauth2token"); 31 | if (string.IsNullOrWhiteSpace(eventsurl)) 32 | throw new ArgumentNullException("eventsurl"); 33 | 34 | this.cacheService = cacheService; 35 | this.oauth2token = oauth2token; 36 | this.eventsurl = eventsurl; 37 | } 38 | 39 | /// 40 | /// always returns (max) 5 recent events old -> first ordered 41 | /// 42 | /// 43 | public IEnumerable GetRecentEvents() 44 | { 45 | try 46 | { 47 | string response = getEventsApiContents(); 48 | if (!string.IsNullOrWhiteSpace(response)) 49 | { 50 | //parse result, re-cache it 51 | var latestPushEvents = parseGithubPushEventsJson(response); 52 | cacheService.Set("latest-pushevents", latestPushEvents, WebCacheService.CACHE_PERMANENTLY); 53 | } 54 | return getRecentPushEventsFromCache(); 55 | } 56 | catch (Exception ex) 57 | { 58 | log.Error(ex); 59 | return Enumerable.Empty(); 60 | } 61 | } 62 | 63 | private static List parseGithubPushEventsJson(string json) 64 | { 65 | JArray events = JArray.Parse(json); 66 | var parsedEvents = (from evt in events 67 | where (string)evt["type"] == "PushEvent" 68 | select parseGithubPushEvent(evt)); 69 | 70 | log.DebugFormat("Retrieved {0} push events from github", parsedEvents.Count()); 71 | 72 | var latestFivePushEvents = parsedEvents.OrderByDescending(pe => pe.Created).Take(5).OrderBy(pe => pe.Created).ToList(); 73 | 74 | return latestFivePushEvents; 75 | } 76 | 77 | private static PushEvent parseGithubPushEvent(JToken evt) 78 | { 79 | string repositoryName = ((string) evt["repo"]["name"]); 80 | if(repositoryName.Contains('/')) 81 | repositoryName = repositoryName.Substring(1 + repositoryName.IndexOf('/')); 82 | 83 | return new PushEvent 84 | { 85 | RepositoryName = repositoryName, 86 | BranchName = ((string)evt["payload"]["ref"]).Replace("refs/heads/", ""), 87 | EventId = evt["id"].ToString(), 88 | ActorUsername = (string)evt["actor"]["login"], 89 | ActorGravatarId = (string)evt["actor"]["gravatar_id"], 90 | AmountOfCommits = (int)evt["payload"]["size"], 91 | Created = (DateTime)evt["created_at"] 92 | }; 93 | } 94 | 95 | private IEnumerable getRecentPushEventsFromCache() 96 | { 97 | var latestPushEvents = cacheService.Get>("latest-pushevents"); 98 | if (latestPushEvents == null) 99 | { 100 | log.Error("We could not find pushEvents in the cache AND github did not return any. Possible error?"); 101 | return Enumerable.Empty(); 102 | } 103 | 104 | return latestPushEvents; 105 | } 106 | 107 | private string lastReceivedEventsETAG; 108 | private string getEventsApiContents(bool ignoreCache=false) 109 | { 110 | try 111 | { 112 | Uri uri = new Uri(string.Format("{0}{1}", API_BASE_URL, eventsurl)); 113 | HttpWebRequest myHttpWebRequest = (HttpWebRequest)HttpWebRequest.Create(uri); 114 | myHttpWebRequest.UserAgent = "TeamCity CI Dashboard - https://github.com/crunchie84/teamcity-dashboard"; 115 | myHttpWebRequest.Headers.Add("Authorization", "bearer " + oauth2token); 116 | 117 | if (!string.IsNullOrWhiteSpace(lastReceivedEventsETAG) && !ignoreCache) 118 | myHttpWebRequest.Headers.Add("If-None-Match", lastReceivedEventsETAG); 119 | 120 | using (HttpWebResponse myWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse()) 121 | { 122 | if (myWebResponse.StatusCode == HttpStatusCode.OK) 123 | { 124 | //if we do not save the returned ETag we will always get the full list with latest changes instead of the real delta since we started polling. 125 | lastReceivedEventsETAG = myWebResponse.Headers.Get("ETag"); 126 | 127 | using (Stream responseStream = myWebResponse.GetResponseStream()) 128 | { 129 | StreamReader myStreamReader = new StreamReader(responseStream, Encoding.Default); 130 | return myStreamReader.ReadToEnd(); 131 | } 132 | } 133 | } 134 | } 135 | catch (HttpException ex) 136 | { 137 | if (ex.GetHttpCode() != (int)HttpStatusCode.NotModified) 138 | throw; 139 | } 140 | catch (WebException ex) 141 | { 142 | var response = ex.Response as HttpWebResponse; 143 | if(response == null || response.StatusCode != HttpStatusCode.NotModified) 144 | throw; 145 | } 146 | catch (Exception ex) 147 | { 148 | throw new HttpException(string.Format("Error while retrieving url '{0}': {1}", eventsurl, ex.Message), ex); 149 | } 150 | return string.Empty; 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /TeamCityDashboard/Scripts/jquery.masonry.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Masonry v2.1.07 3 | * A dynamic layout plugin for jQuery 4 | * The flip-side of CSS Floats 5 | * http://masonry.desandro.com 6 | * 7 | * Licensed under the MIT license. 8 | * Copyright 2012 David DeSandro 9 | */ (function (a, b, c) { 10 | "use strict"; 11 | var d = b.event, 12 | e = b.event.handle ? "handle" : "dispatch", 13 | f; 14 | d.special.smartresize = { 15 | setup: function () { 16 | b(this).bind("resize", d.special.smartresize.handler) 17 | }, 18 | teardown: function () { 19 | b(this).unbind("resize", d.special.smartresize.handler) 20 | }, 21 | handler: function (a, b) { 22 | var c = this, 23 | g = arguments; 24 | a.type = "smartresize", f && clearTimeout(f), f = setTimeout(function () { 25 | d[e].apply(c, g) 26 | }, b === "execAsap" ? 0 : 100) 27 | } 28 | }, b.fn.smartresize = function (a) { 29 | return a ? this.bind("smartresize", a) : this.trigger("smartresize", ["execAsap"]) 30 | }, b.Mason = function (a, c) { 31 | this.element = b(c), this._create(a), this._init() 32 | }, b.Mason.settings = { 33 | isResizable: !0, 34 | isAnimated: !1, 35 | animationOptions: { 36 | queue: !1, 37 | duration: 500 38 | }, 39 | gutterWidth: 0, 40 | isRTL: !1, 41 | isFitWidth: !1, 42 | containerStyle: { 43 | position: "relative" 44 | } 45 | }, b.Mason.prototype = { 46 | _filterFindBricks: function (a) { 47 | var b = this.options.itemSelector; 48 | return b ? a.filter(b).add(a.find(b)) : a 49 | }, 50 | _getBricks: function (a) { 51 | var b = this._filterFindBricks(a).css({ 52 | position: "absolute" 53 | }).addClass("masonry-brick"); 54 | return b 55 | }, 56 | _create: function (c) { 57 | this.options = b.extend(!0, {}, b.Mason.settings, c), this.styleQueue = []; 58 | var d = this.element[0].style; 59 | this.originalStyle = { 60 | height: d.height || "" 61 | }; 62 | var e = this.options.containerStyle; 63 | for (var f in e) this.originalStyle[f] = d[f] || ""; 64 | this.element.css(e), this.horizontalDirection = this.options.isRTL ? "right" : "left"; 65 | var g = this.element.css("padding-" + this.horizontalDirection), 66 | h = this.element.css("padding-top"); 67 | this.offset = { 68 | x: g ? parseInt(g, 10) : 0, 69 | y: h ? parseInt(h, 10) : 0 70 | }, this.isFluid = this.options.columnWidth && typeof this.options.columnWidth == "function"; 71 | var i = this; 72 | setTimeout(function () { 73 | i.element.addClass("masonry") 74 | }, 0), this.options.isResizable && b(a).bind("smartresize.masonry", function () { 75 | i.resize() 76 | }), this.reloadItems() 77 | }, 78 | _init: function (a) { 79 | this._getColumns(), this._reLayout(a) 80 | }, 81 | option: function (a, c) { 82 | b.isPlainObject(a) && (this.options = b.extend(!0, this.options, a)) 83 | }, 84 | layout: function (a, b) { 85 | for (var c = 0, d = a.length; c < d; c++) this._placeBrick(a[c]); 86 | var e = {}; 87 | e.height = Math.max.apply(Math, this.colYs); 88 | if (this.options.isFitWidth) { 89 | var f = 0; 90 | c = this.cols; 91 | while (--c) { 92 | if (this.colYs[c] !== 0) break; 93 | f++ 94 | } 95 | e.width = (this.cols - f) * this.columnWidth - this.options.gutterWidth 96 | } 97 | this.styleQueue.push({ 98 | $el: this.element, 99 | style: e 100 | }); 101 | var g = this.isLaidOut ? this.options.isAnimated ? "animate" : "css" : "css", 102 | h = this.options.animationOptions, 103 | i; 104 | for (c = 0, d = this.styleQueue.length; c < d; c++) i = this.styleQueue[c], i.$el[g](i.style, h); 105 | this.styleQueue = [], b && b.call(a), this.isLaidOut = !0 106 | }, 107 | _getColumns: function () { 108 | var a = this.options.isFitWidth ? this.element.parent() : this.element, 109 | b = a.width(); 110 | this.columnWidth = this.isFluid ? this.options.columnWidth(b) : this.options.columnWidth || this.$bricks.outerWidth(!0) || b, this.columnWidth += this.options.gutterWidth, this.cols = Math.floor((b + this.options.gutterWidth) / this.columnWidth), this.cols = Math.max(this.cols, 1) 111 | }, 112 | _placeBrick: function (a) { 113 | var c = b(a), 114 | d, e, f, g, h; 115 | d = Math.ceil(c.outerWidth(!0) / this.columnWidth), d = Math.min(d, this.cols); 116 | if (d === 1) f = this.colYs; 117 | else { 118 | e = this.cols + 1 - d, f = []; 119 | for (h = 0; h < e; h++) g = this.colYs.slice(h, h + d), f[h] = Math.max.apply(Math, g) 120 | } 121 | var i = Math.min.apply(Math, f), 122 | j = 0; 123 | for (var k = 0, l = f.length; k < l; k++) if (f[k] === i) { 124 | j = k; 125 | break 126 | } 127 | var m = { 128 | top: i + this.offset.y 129 | }; 130 | m[this.horizontalDirection] = this.columnWidth * j + this.offset.x, this.styleQueue.push({ 131 | $el: c, 132 | style: m 133 | }); 134 | var n = i + c.outerHeight(!0), 135 | o = this.cols + 1 - l; 136 | for (k = 0; k < o; k++) this.colYs[j + k] = n 137 | }, 138 | resize: function () { 139 | var a = this.cols; 140 | this._getColumns(), (this.isFluid || this.cols !== a) && this._reLayout() 141 | }, 142 | _reLayout: function (a) { 143 | var b = this.cols; 144 | this.colYs = []; 145 | while (b--) this.colYs.push(0); 146 | this.layout(this.$bricks, a) 147 | }, 148 | reloadItems: function () { 149 | this.$bricks = this._getBricks(this.element.children()) 150 | }, 151 | reload: function (a) { 152 | this.reloadItems(), this._init(a) 153 | }, 154 | appended: function (a, b, c) { 155 | if (b) { 156 | this._filterFindBricks(a).css({ 157 | top: this.element.height() 158 | }); 159 | var d = this; 160 | setTimeout(function () { 161 | d._appended(a, c) 162 | }, 1) 163 | } else this._appended(a, c) 164 | }, 165 | _appended: function (a, b) { 166 | var c = this._getBricks(a); 167 | this.$bricks = this.$bricks.add(c), this.layout(c, b) 168 | }, 169 | remove: function (a) { 170 | this.$bricks = this.$bricks.not(a), a.remove() 171 | }, 172 | destroy: function () { 173 | this.$bricks.removeClass("masonry-brick").each(function () { 174 | this.style.position = "", this.style.top = "", this.style.left = "" 175 | }); 176 | var c = this.element[0].style; 177 | for (var d in this.originalStyle) c[d] = this.originalStyle[d]; 178 | this.element.unbind(".masonry").removeClass("masonry").removeData("masonry"), b(a).unbind(".masonry") 179 | } 180 | }, b.fn.imagesLoaded = function (a) { 181 | function h() { 182 | a.call(c, d) 183 | } 184 | function i(a) { 185 | var c = a.target; 186 | c.src !== f && b.inArray(c, g) === -1 && (g.push(c), --e <= 0 && (setTimeout(h), d.unbind(".imagesLoaded", i))) 187 | } 188 | var c = this, 189 | d = c.find("img").add(c.filter("img")), 190 | e = d.length, 191 | f = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==", 192 | g = []; 193 | return e || h(), d.bind("load.imagesLoaded error.imagesLoaded", i).each(function () { 194 | var a = this.src; 195 | this.src = f, this.src = a 196 | }), c 197 | }; 198 | var g = function (b) { 199 | a.console && a.console.error(b) 200 | }; 201 | b.fn.masonry = function (a) { 202 | if (typeof a == "string") { 203 | var c = Array.prototype.slice.call(arguments, 1); 204 | this.each(function () { 205 | var d = b.data(this, "masonry"); 206 | if (!d) { 207 | g("cannot call methods on masonry prior to initialization; attempted to call method '" + a + "'"); 208 | return 209 | } 210 | if (!b.isFunction(d[a]) || a.charAt(0) === "_") { 211 | g("no such method '" + a + "' for masonry instance"); 212 | return 213 | } 214 | d[a].apply(d, c) 215 | }) 216 | } else this.each(function () { 217 | var c = b.data(this, "masonry"); 218 | c ? (c.option(a || {}), c._init()) : b.data(this, "masonry", new b.Mason(a, this)) 219 | }); 220 | return this 221 | } 222 | })(window, jQuery); -------------------------------------------------------------------------------- /TeamCityDashboard/TeamCityDashboard.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 8 | 0.0.0.0 9 | obj\$(Configuration)\Package\TeamCityDashboard-$(build_number).zip 10 | False 11 | 12 | 13 | 2.0 14 | {B43EC6D7-11D6-4E8D-B943-3918CAF79E63} 15 | {E53F8FEA-EAE0-44A6-8774-FFD645390401};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} 16 | Library 17 | Properties 18 | TeamCityDashboard 19 | TeamCityDashboard 20 | v4.5 21 | false 22 | false 23 | ..\ 24 | true 25 | 26 | 27 | 28 | 29 | 4.0 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | true 38 | full 39 | false 40 | bin\ 41 | DEBUG;TRACE 42 | prompt 43 | 4 44 | false 45 | 46 | 47 | pdbonly 48 | true 49 | true 50 | bin\ 51 | TRACE 52 | prompt 53 | 4 54 | false 55 | 56 | 57 | 58 | ..\libs\ITCloud.Web.Routing.dll 59 | 60 | 61 | ..\packages\log4net.2.0.0\lib\net40-full\log4net.dll 62 | 63 | 64 | ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Global.asax 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | styles.scss 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | Web.config 127 | 128 | 129 | Web.config 130 | 131 | 132 | 133 | 134 | True 135 | False 136 | Nested 137 | False 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 10.0 156 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 157 | 158 | 159 | 160 | 161 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | True 175 | 176 | 177 | 178 | 179 | 180 | REM copy .example files to .config files if needed 181 | IF NOT EXIST "$(projectDir)\Web.Config" IF EXIST "$(projectDir)\Web.example.config" COPY "$(projectDir)\Web.example.config" "$(projectDir)\Web.Config" 182 | 183 | 184 | -------------------------------------------------------------------------------- /TeamCityDashboard/Services/TeamCityDataService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using TeamCityDashboard.Interfaces; 7 | using System.Net; 8 | using System.IO; 9 | using System.Text; 10 | using System.Xml; 11 | using TeamCityDashboard.Models; 12 | using System.Globalization; 13 | 14 | namespace TeamCityDashboard.Services 15 | { 16 | public class TeamCityDataService : AbstractHttpDataService 17 | { 18 | public TeamCityDataService(string baseUrl, string username, string password, ICacheService cacheService) : base(baseUrl, username, password, cacheService) { } 19 | 20 | /// 21 | /// url to retrieve list of projects in TeamCity 22 | /// 23 | private const string URL_PROJECTS_LIST = @"/httpAuth/app/rest/projects"; 24 | 25 | /// 26 | /// url to retrieve details of given {0} project (buildtypes etc) 27 | /// 28 | private const string URL_PROJECT_DETAILS = @"/httpAuth/app/rest/projects/id:{0}"; 29 | 30 | /// 31 | /// retrieve the first 100 builds of the given buildconfig and retrieve the status of it 32 | /// 33 | private const string URL_BUILDS_LIST = @"/httpAuth/app/rest/buildTypes/id:{0}/builds"; 34 | 35 | /// 36 | /// retrieve details of the given build ({0}) and verify that the /buildType/settings/property[@name='allowExternalStatus'] == 'true' 37 | /// 38 | private const string URL_BUILD_DETAILS = @"/httpAuth/app/rest/buildTypes/id:{0}"; 39 | 40 | /// 41 | /// url to retrieve the changes commited in the given {0} buildrunId 42 | /// 43 | private const string URL_BUILD_CHANGES = @"/httpAuth/app/rest/changes?build=id:{0}"; 44 | 45 | /// 46 | /// Url to retrieve the details of the given {0} changeId 47 | /// 48 | private const string URL_CHANGE_DETAILS = @"/httpAuth/app/rest/changes/id:{0}"; 49 | 50 | /// 51 | /// url to retrieve the emailaddress of the given {0} userId 52 | /// 53 | private const string URL_USER_EMAILADDRESS = @"/httpAuth/app/rest/users/id:{0}/email"; 54 | 55 | /// 56 | /// take all failed projects with at least one build config visible AND max 15 successfull build configs 57 | /// 58 | /// 59 | public IEnumerable GetActiveProjects() 60 | { 61 | var projects = getNonArchivedProjects().ToList(); 62 | 63 | var failing = projects.Where(p => p.BuildConfigs.Any(c => !c.CurrentBuildIsSuccesfull)); 64 | var success = projects.Where(p => p.BuildConfigs.All(c => c.CurrentBuildIsSuccesfull)); 65 | 66 | int amountToTake = Math.Max(15, failing.Count()); 67 | 68 | //only display the most recent 15 build projects together with the failing ones OR if we have more failing display those 69 | return failing.Concat(success.OrderByDescending(p => p.LastBuildDate)).Take(amountToTake); 70 | } 71 | 72 | private IEnumerable getNonArchivedProjects() 73 | { 74 | XmlDocument projectsPageContent = GetPageContents(URL_PROJECTS_LIST); 75 | if (projectsPageContent == null) 76 | yield break; 77 | 78 | foreach (XmlElement el in projectsPageContent.SelectNodes("//project")) 79 | { 80 | var project = ParseProjectDetails(el.GetAttribute("id"), el.GetAttribute("name")); 81 | if (project == null) 82 | continue; 83 | yield return project; 84 | } 85 | } 86 | 87 | /// 88 | /// only retrieve non-archived projects etc 89 | /// 90 | /// 91 | /// 92 | /// 93 | private IProject ParseProjectDetails(string projectId, string projectName) 94 | { 95 | //determine details, archived? buildconfigs 96 | XmlDocument projectDetails = CacheService.Get("project-details-" + projectId, () => { 97 | return GetPageContents(string.Format(URL_PROJECT_DETAILS, projectId)); 98 | }, 15 * 60); 99 | 100 | if (projectDetails == null) 101 | return null; 102 | 103 | if (projectDetails.DocumentElement.GetAttribute("archived") == "true") 104 | return null;//not needed 105 | 106 | List buildConfigs = new List(); 107 | foreach (XmlElement buildType in projectDetails.SelectNodes("project/buildTypes/buildType")) 108 | { 109 | var buildConfigDetails = ParseBuildConfigDetails(buildType.GetAttribute("id"), buildType.GetAttribute("name")); 110 | if (buildConfigDetails != null) 111 | buildConfigs.Add(buildConfigDetails); 112 | } 113 | 114 | if (buildConfigs.Count == 0) 115 | return null;//do not report 'empty' projects' 116 | 117 | return new Project 118 | { 119 | Id = projectId, 120 | Name = projectName, 121 | Url = projectDetails.DocumentElement.GetAttribute("webUrl"), 122 | IconUrl = parseProjectProperty(projectDetails, "dashboard.project.logo.url"), 123 | SonarProjectKey = parseProjectProperty(projectDetails, "sonar.project.key"), 124 | BuildConfigs = buildConfigs, 125 | LastBuildDate = (buildConfigs.Where(b => b.CurrentBuildDate.HasValue).Max(b => b.CurrentBuildDate.Value)) 126 | }; 127 | } 128 | 129 | private static string parseProjectProperty(XmlDocument projectDetails, string propertyName) 130 | { 131 | var propertyElement = projectDetails.SelectSingleNode(string.Format("project/parameters/property[@name='{0}']/@value", propertyName)); 132 | if (propertyElement != null) 133 | return propertyElement.Value; 134 | 135 | return null; 136 | } 137 | 138 | /// 139 | /// Retrieve the build configdetails with the given ID. return NULL if the given config is not visible in the widget interface 140 | /// 141 | /// 142 | /// 143 | /// 144 | private IBuildConfig ParseBuildConfigDetails(string id, string name) 145 | { 146 | //do we need to show this buildCOnfig? 147 | bool isVisibleExternally = CacheService.Get("build-visible-widgetinterface-" + id, () => IsBuildVisibleOnExternalWidgetInterface(id), CACHE_DURATION).Visible; 148 | if (!isVisibleExternally) 149 | return null; 150 | 151 | ///retrieve details of last 100 builds and find out if the last (=first row) was succesfull or iterate untill we found the first breaker? 152 | XmlDocument buildResultsDoc = GetPageContents(string.Format(URL_BUILDS_LIST, id)); 153 | XmlElement lastBuild = buildResultsDoc.DocumentElement.FirstChild as XmlElement; 154 | 155 | 156 | DateTime? currentBuildDate = null; 157 | bool currentBuildSuccesfull = true; 158 | List buildBreakerEmailaddress = new List(); 159 | 160 | if (lastBuild != null) 161 | { 162 | currentBuildSuccesfull = lastBuild.GetAttribute("status") == "SUCCESS";//we default to true 163 | 164 | //try to parse last date 165 | string buildDate = lastBuild.GetAttribute("startDate"); 166 | DateTime theDate; 167 | 168 | //TeamCity date format => 20121101T134409+0100 169 | if (DateTime.TryParseExact(buildDate, @"yyyyMMdd\THHmmsszz\0\0", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out theDate)) 170 | { 171 | currentBuildDate = theDate; 172 | } 173 | 174 | //if the last build was not successfull iterate back untill we found one which was successfull so we know who might have broke it. 175 | if (!currentBuildSuccesfull) 176 | { 177 | XmlNode lastSuccessfullBuild = buildResultsDoc.DocumentElement.SelectSingleNode("build[@status='SUCCESS']"); 178 | if (lastSuccessfullBuild != null) 179 | { 180 | XmlElement breakingBuild = lastSuccessfullBuild.PreviousSibling as XmlElement; 181 | if (breakingBuild == null) 182 | buildBreakerEmailaddress.Add("no-breaking-build-after-succes-should-not-happen"); 183 | else 184 | buildBreakerEmailaddress = CacheService.Get>( 185 | "buildbreakers-build-" + breakingBuild.GetAttribute("id"), 186 | () => ParseBuildBreakerDetails(breakingBuild.GetAttribute("id")), 187 | CACHE_DURATION 188 | ).Distinct().ToList(); 189 | } 190 | else 191 | { 192 | //IF NO previous pages with older builds available then we can assume this is the first build and it broke. show image of that one. 193 | //TODO we could iterate older builds to find the breaker via above logic 194 | } 195 | } 196 | } 197 | 198 | return new BuildConfig 199 | { 200 | Id = id, 201 | Name = name, 202 | Url = new Uri(string.Format("{0}/viewType.html?buildTypeId={1}&tab=buildTypeStatusDiv", BaseUrl, id)).ToString(), 203 | CurrentBuildIsSuccesfull = currentBuildSuccesfull, 204 | CurrentBuildDate = currentBuildDate, 205 | PossibleBuildBreakerEmailAddresses = buildBreakerEmailaddress 206 | }; 207 | } 208 | 209 | private ProjectVisible IsBuildVisibleOnExternalWidgetInterface(string id) 210 | { 211 | XmlDocument buildConfigDetails = GetPageContents(string.Format(URL_BUILD_DETAILS, id)); 212 | XmlElement externalStatusEl = buildConfigDetails.SelectSingleNode("buildType/settings/property[@name='allowExternalStatus']") as XmlElement; 213 | return new ProjectVisible 214 | { 215 | Visible = externalStatusEl != null && externalStatusEl.GetAttribute("value") == "true"//no external status visible 216 | }; 217 | } 218 | 219 | /// 220 | /// retrieve the emailaddress of the user who changed something in the given buildId because he most likely broke it 221 | /// 222 | /// 223 | /// 224 | private IEnumerable ParseBuildBreakerDetails(string buildId) 225 | { 226 | //retrieve changes 227 | XmlDocument buildChangesDoc = GetPageContents(string.Format(URL_BUILD_CHANGES, buildId)); 228 | foreach (XmlElement el in buildChangesDoc.SelectNodes("//change")) 229 | { 230 | //retrieve change details 231 | string changeId = el.GetAttribute("id"); 232 | if (string.IsNullOrEmpty(changeId)) 233 | throw new ArgumentNullException(string.Format("@id of change within buildId {0} should not be NULL", buildId)); 234 | 235 | //retrieve userid who changed something//details 236 | XmlDocument changeDetailsDoc = GetPageContents(string.Format(URL_CHANGE_DETAILS, changeId)); 237 | XmlElement userDetails = (changeDetailsDoc.SelectSingleNode("change/user") as XmlElement); 238 | if (userDetails == null) 239 | continue;//sometimes a change is not linked to a user who commited it.. ? 240 | 241 | string userId = userDetails.GetAttribute("id"); 242 | if (userId == null) 243 | throw new ArgumentNullException(string.Format("No userId given in changeId {0}", changeId)); 244 | 245 | //retrieve email 246 | string email = CacheService.Get("user-email-" + userId, () => GetUserEmailAddress(userId), CACHE_DURATION); 247 | if (!string.IsNullOrEmpty(email)) 248 | yield return email.ToLower().Trim(); 249 | } 250 | } 251 | 252 | private string GetUserEmailAddress(string userId) 253 | { 254 | return GetContents(string.Format(URL_USER_EMAILADDRESS, userId)); 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /TeamCityDashboard/Views/Dashboard/Index.aspx: -------------------------------------------------------------------------------- 1 | <%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %> 2 | 3 | 4 | Q42 Continuous Integration 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 311 | 312 |
313 |

Actual Continuous Integration Status

314 |
315 | 316 |
317 |
318 | 319 | 320 |
321 |

Pushes to GitHub

322 |
323 |
324 | 325 | -------------------------------------------------------------------------------- /TeamCityDashboard/Scripts/jquery.crypt.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cryptography Plug-in 3 | * version: 1.0.0 (24 Sep 2008) 4 | * copyright 2008 Scott Thompson http://www.itsyndicate.ca - scott@itsyndicate.ca 5 | * http://www.opensource.org/licenses/mit-license.php 6 | */ 7 | /* 8 | * A set of functions to do some basic cryptography encoding/decoding 9 | * I compiled from some javascripts I found into a jQuery plug-in. 10 | * Thanks go out to the original authors. 11 | * 12 | * Also a big thanks to Wade W. Hedgren http://homepages.uc.edu/~hedgreww 13 | * for the 1.1.1 upgrade to conform correctly to RFC4648 Sec5 url save base64 14 | * 15 | * Changelog: 1.1.0 16 | * - rewrote plugin to use only one item in the namespace 17 | * 18 | * Changelog: 1.1.1 19 | * - added code to base64 to allow URL and Filename Safe Alphabet (RFC4648 Sec5) 20 | * 21 | * --- Base64 Encoding and Decoding code was written by 22 | * 23 | * Base64 code from Tyler Akins -- http://rumkin.com 24 | * and is placed in the public domain 25 | * 26 | * 27 | * --- MD5 and SHA1 Functions based upon Paul Johnston's javascript libraries. 28 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 29 | * Digest Algorithm, as defined in RFC 1321. 30 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. 31 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 32 | * Distributed under the BSD License 33 | * See http://pajhome.org.uk/crypt/md5 for more info. 34 | * 35 | * xTea Encrypt and Decrypt 36 | * copyright 2000-2005 Chris Veness 37 | * http://www.movable-type.co.uk 38 | * 39 | * 40 | * Examples: 41 | * 42 | var md5 = $().crypt({method:"md5",source:$("#phrase").val()}); 43 | var sha1 = $().crypt({method:"sha1",source:$("#phrase").val()}); 44 | var b64 = $().crypt({method:"b64enc",source:$("#phrase").val()}); 45 | var b64dec = $().crypt({method:"b64dec",source:b64}); 46 | var xtea = $().crypt({method:"xteaenc",source:$("#phrase").val(),keyPass:$("#passPhrase").val()}); 47 | var xteadec = $().crypt({method:"xteadec",source:xtea,keyPass:$("#passPhrase").val()}); 48 | var xteab64 = $().crypt({method:"xteab64enc",source:$("#phrase").val(),keyPass:$("#passPhrase").val()}); 49 | var xteab64dec = $().crypt({method:"xteab64dec",source:xteab64,keyPass:$("#passPhrase").val()}); 50 | 51 | You can also pass source this way. 52 | var md5 = $("#idOfSource").crypt({method:"md5"}); 53 | * 54 | */ 55 | (function($){ 56 | $.fn.crypt = function(options) { 57 | var defaults = { 58 | b64Str : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 59 | strKey : "123", 60 | method : "md5", 61 | source : "", 62 | chrsz : 8, /* md5 - bits per input character. 8 - ASCII; 16 - Unicode */ 63 | hexcase : 0 /* md5 - hex output format. 0 - lowercase; 1 - uppercase */ 64 | }; 65 | 66 | // code to enable URL and Filename Safe Alphabet (RFC4648 Sec5) 67 | if (typeof(options.urlsafe) == 'undefined'){ 68 | defaults.b64Str += '+/='; 69 | options.urlsafe = false; 70 | }else if (options.urlsafe){ 71 | defaults.b64Str += '-_='; 72 | }else{ 73 | defaults.b64Str += '+/='; 74 | } 75 | 76 | var opts = $.extend(defaults, options); 77 | 78 | // support for $("#name").crypt..... 79 | if (!opts.source) { 80 | var $this = $(this); 81 | // determine if it's a div or a textarea 82 | if ($this.html()) opts.source = $this.html(); 83 | else if ($this.val()) opts.source = $this.val(); 84 | else {alert("Please provide source text");return false;}; 85 | }; 86 | 87 | if (opts.method == 'md5') { 88 | return md5(opts); 89 | } else if (opts.method == 'sha1') { 90 | return sha1(opts); 91 | } else if (opts.method == 'b64enc') { 92 | return b64enc(opts); 93 | } else if (opts.method == 'b64dec') { 94 | return b64dec(opts); 95 | } else if (opts.method == 'xteaenc') { 96 | return xteaenc(opts); 97 | } else if (opts.method == 'xteadec') { 98 | return xteadec(opts); 99 | } else if (opts.method == 'xteab64enc') { 100 | var tmpenc = xteaenc(opts); 101 | opts.method = "b64enc"; 102 | opts.source = tmpenc; 103 | return b64enc(opts); 104 | } else if (opts.method == 'xteab64dec') { 105 | var tmpdec = b64dec(opts); 106 | opts.method = "xteadec"; 107 | opts.source = tmpdec; 108 | return xteadec(opts); 109 | } 110 | 111 | 112 | function b64enc(params) { 113 | 114 | var output = ""; 115 | var chr1, chr2, chr3; 116 | var enc1, enc2, enc3, enc4; 117 | var i = 0; 118 | 119 | do { 120 | chr1 = params.source.charCodeAt(i++); 121 | chr2 = params.source.charCodeAt(i++); 122 | chr3 = params.source.charCodeAt(i++); 123 | 124 | enc1 = chr1 >> 2; 125 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 126 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 127 | enc4 = chr3 & 63; 128 | 129 | if (isNaN(chr2)) { 130 | enc3 = enc4 = 64; 131 | } else if (isNaN(chr3)) { 132 | enc4 = 64; 133 | }; 134 | 135 | output += params.b64Str.charAt(enc1) 136 | + params.b64Str.charAt(enc2) 137 | + params.b64Str.charAt(enc3) 138 | + params.b64Str.charAt(enc4); 139 | 140 | 141 | } while (i < params.source.length); 142 | 143 | return output; 144 | 145 | }; 146 | 147 | function b64dec(params) { 148 | 149 | var output = ""; 150 | var chr1, chr2, chr3; 151 | var enc1, enc2, enc3, enc4; 152 | var i = 0; 153 | 154 | // remove all characters that are not A-Z, a-z, 0-9, !, -, or _ 155 | 156 | // remove all characters that are not A-Z, a-z, 0-9, !, -, or _ 157 | // params.source = params.source.replace(/[^A-Za-z0-9!_-]/g, ""); 158 | 159 | var re = new RegExp ('[^A-Za-z0-9' + params.b64Str.substr(-3) + ']', 'g'); 160 | params.source = params.source.replace(re, ""); 161 | 162 | do { 163 | enc1 = params.b64Str.indexOf(params.source.charAt(i++)); 164 | enc2 = params.b64Str.indexOf(params.source.charAt(i++)); 165 | enc3 = params.b64Str.indexOf(params.source.charAt(i++)); 166 | enc4 = params.b64Str.indexOf(params.source.charAt(i++)); 167 | 168 | chr1 = (enc1 << 2) | (enc2 >> 4); 169 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 170 | chr3 = ((enc3 & 3) << 6) | enc4; 171 | 172 | output = output + String.fromCharCode(chr1); 173 | 174 | if (enc3 != 64) { 175 | output = output + String.fromCharCode(chr2); 176 | } 177 | if (enc4 != 64) { 178 | output = output + String.fromCharCode(chr3); 179 | } 180 | } while (i < params.source.length); 181 | 182 | return output; 183 | }; 184 | 185 | 186 | function md5(params) { 187 | /* This is a trimmed version of Paul Johnsons JavaScript 188 | * 189 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 190 | * Digest Algorithm, as defined in RFC 1321. 191 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. 192 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 193 | * Distributed under the BSD License 194 | * See http://pajhome.org.uk/crypt/md5 for more info. 195 | */ 196 | 197 | //var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 198 | //var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 199 | 200 | return binl2hex(core_md5(str2binl(params.source), params.source.length * params.chrsz)); 201 | 202 | /* 203 | * Convert an array of little-endian words to a hex string. 204 | */ 205 | function binl2hex(binarray) 206 | { 207 | var hex_tab = params.hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 208 | var str = ""; 209 | for(var i = 0; i < binarray.length * 4; i++) 210 | { 211 | str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + 212 | hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); 213 | }; 214 | return str; 215 | }; 216 | 217 | /* 218 | * Calculate the HMAC-MD5, of a key and some data 219 | */ 220 | function core_hmac_md5(key, data) 221 | { 222 | var bkey = str2binl(key); 223 | if(bkey.length > 16) bkey = core_md5(bkey, key.length * params.chrsz); 224 | 225 | var ipad = Array(16), opad = Array(16); 226 | for(var i = 0; i < 16; i++) 227 | { 228 | ipad[i] = bkey[i] ^ 0x36363636; 229 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 230 | }; 231 | 232 | var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * params.chrsz); 233 | return core_md5(opad.concat(hash), 512 + 128); 234 | }; 235 | 236 | /* 237 | * Convert a string to an array of little-endian words 238 | * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. 239 | */ 240 | function str2binl(str) 241 | { 242 | var bin = Array(); 243 | var mask = (1 << params.chrsz) - 1; 244 | for(var i = 0; i < str.length * params.chrsz; i += params.chrsz) 245 | bin[i>>5] |= (str.charCodeAt(i / params.chrsz) & mask) << (i%32); 246 | return bin; 247 | } 248 | 249 | 250 | /* 251 | * Bitwise rotate a 32-bit number to the left. 252 | */ 253 | function bit_rol(num, cnt) 254 | { 255 | return (num << cnt) | (num >>> (32 - cnt)); 256 | } 257 | 258 | 259 | /* 260 | * These functions implement the four basic operations the algorithm uses. 261 | */ 262 | function md5_cmn(q, a, b, x, s, t) 263 | { 264 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); 265 | } 266 | function md5_ff(a, b, c, d, x, s, t) 267 | { 268 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); 269 | } 270 | function md5_gg(a, b, c, d, x, s, t) 271 | { 272 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); 273 | } 274 | function md5_hh(a, b, c, d, x, s, t) 275 | { 276 | return md5_cmn(b ^ c ^ d, a, b, x, s, t); 277 | } 278 | function md5_ii(a, b, c, d, x, s, t) 279 | { 280 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); 281 | } 282 | 283 | /* 284 | * Calculate the MD5 of an array of little-endian words, and a bit length 285 | */ 286 | function core_md5(x, len) 287 | { 288 | /* append padding */ 289 | x[len >> 5] |= 0x80 << ((len) % 32); 290 | x[(((len + 64) >>> 9) << 4) + 14] = len; 291 | 292 | var a = 1732584193; 293 | var b = -271733879; 294 | var c = -1732584194; 295 | var d = 271733878; 296 | 297 | for(var i = 0; i < x.length; i += 16) 298 | { 299 | var olda = a; 300 | var oldb = b; 301 | var oldc = c; 302 | var oldd = d; 303 | 304 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); 305 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); 306 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); 307 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); 308 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); 309 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); 310 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); 311 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); 312 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); 313 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); 314 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063); 315 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); 316 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); 317 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); 318 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); 319 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); 320 | 321 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); 322 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); 323 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); 324 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); 325 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); 326 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); 327 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); 328 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); 329 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); 330 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); 331 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); 332 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); 333 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); 334 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); 335 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); 336 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); 337 | 338 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); 339 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); 340 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); 341 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); 342 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); 343 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); 344 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); 345 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); 346 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); 347 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); 348 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); 349 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); 350 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); 351 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); 352 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); 353 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); 354 | 355 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); 356 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); 357 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); 358 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); 359 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); 360 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); 361 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); 362 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); 363 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); 364 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); 365 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); 366 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); 367 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); 368 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); 369 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); 370 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); 371 | 372 | a = safe_add(a, olda); 373 | b = safe_add(b, oldb); 374 | c = safe_add(c, oldc); 375 | d = safe_add(d, oldd); 376 | }; 377 | return Array(a, b, c, d); 378 | 379 | }; 380 | 381 | }; 382 | 383 | /* 384 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 385 | * to work around bugs in some JS interpreters. (used by md5 and sha1) 386 | */ 387 | function safe_add(x, y) 388 | { 389 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 390 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 391 | return (msw << 16) | (lsw & 0xFFFF); 392 | }; 393 | 394 | function sha1(params) { 395 | return binb2hex(core_sha1(str2binb(params.source),params.source.length * params.chrsz)); 396 | 397 | /* 398 | * Calculate the SHA-1 of an array of big-endian words, and a bit length 399 | */ 400 | function core_sha1(x, len) 401 | { 402 | /* append padding */ 403 | x[len >> 5] |= 0x80 << (24 - len % 32); 404 | x[((len + 64 >> 9) << 4) + 15] = len; 405 | 406 | var w = Array(80); 407 | var a = 1732584193; 408 | var b = -271733879; 409 | var c = -1732584194; 410 | var d = 271733878; 411 | var e = -1009589776; 412 | 413 | for(var i = 0; i < x.length; i += 16) 414 | { 415 | var olda = a; 416 | var oldb = b; 417 | var oldc = c; 418 | var oldd = d; 419 | var olde = e; 420 | 421 | for(var j = 0; j < 80; j++) 422 | { 423 | if(j < 16) w[j] = x[i + j]; 424 | else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); 425 | var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), 426 | safe_add(safe_add(e, w[j]), sha1_kt(j))); 427 | e = d; 428 | d = c; 429 | c = rol(b, 30); 430 | b = a; 431 | a = t; 432 | } 433 | 434 | a = safe_add(a, olda); 435 | b = safe_add(b, oldb); 436 | c = safe_add(c, oldc); 437 | d = safe_add(d, oldd); 438 | e = safe_add(e, olde); 439 | } 440 | return Array(a, b, c, d, e); 441 | 442 | } 443 | /* 444 | * Bitwise rotate a 32-bit number to the left. 445 | */ 446 | function rol(num, cnt) 447 | { 448 | return (num << cnt) | (num >>> (32 - cnt)); 449 | } 450 | 451 | /* 452 | * Determine the appropriate additive constant for the current iteration 453 | */ 454 | function sha1_kt(t) 455 | { 456 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : 457 | (t < 60) ? -1894007588 : -899497514; 458 | } 459 | /* 460 | * Perform the appropriate triplet combination function for the current 461 | * iteration 462 | */ 463 | function sha1_ft(t, b, c, d) 464 | { 465 | if(t < 20) return (b & c) | ((~b) & d); 466 | if(t < 40) return b ^ c ^ d; 467 | if(t < 60) return (b & c) | (b & d) | (c & d); 468 | return b ^ c ^ d; 469 | } 470 | 471 | /* 472 | * Convert an array of big-endian words to a hex string. 473 | */ 474 | function binb2hex(binarray) 475 | { 476 | var hex_tab = params.hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 477 | var str = ""; 478 | for(var i = 0; i < binarray.length * 4; i++) 479 | { 480 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + 481 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); 482 | } 483 | return str; 484 | } 485 | 486 | 487 | /* 488 | * Convert an 8-bit or 16-bit string to an array of big-endian words 489 | * In 8-bit function, characters >255 have their hi-byte silently ignored. 490 | */ 491 | function str2binb(str) 492 | { 493 | var bin = Array(); 494 | var mask = (1 << params.chrsz) - 1; 495 | for(var i = 0; i < str.length * params.chrsz; i += params.chrsz) 496 | bin[i>>5] |= (str.charCodeAt(i / params.chrsz) & mask) << (32 - params.chrsz - i%32); 497 | return bin; 498 | } 499 | 500 | }; 501 | 502 | function xteaenc(params) { 503 | var v = new Array(2), k = new Array(4), s = "", i; 504 | 505 | params.source = escape(params.source); // use escape() so only have single-byte chars to encode 506 | 507 | // build key directly from 1st 16 chars of strKey 508 | for (var i=0; i<4; i++) k[i] = Str4ToLong(params.strKey.slice(i*4,(i+1)*4)); 509 | 510 | for (i=0; i>>5)+z ^ sum+k[sum & 3]; 529 | sum += delta; 530 | z += (y<<4 ^ y>>>5)+y ^ sum+k[sum>>>11 & 3]; 531 | // note: unsigned right-shift '>>>' is used in place of original '>>', due to lack 532 | // of 'unsigned' type declaration in JavaScript (thanks to Karsten Kraus for this) 533 | } 534 | v[0] = y;v[1] = z; 535 | } 536 | }; 537 | 538 | function xteadec(params) { 539 | var v = new Array(2), k = new Array(4), s = "", i; 540 | 541 | for (var i=0; i<4; i++) k[i] = Str4ToLong(params.strKey.slice(i*4,(i+1)*4)); 542 | 543 | ciphertext = unescCtrlCh(params.source); 544 | for (i=0; i>>5)+y ^ sum+k[sum>>>11 & 3]; 563 | sum -= delta; 564 | y -= (z<<4 ^ z>>>5)+z ^ sum+k[sum & 3]; 565 | } 566 | v[0] = y;v[1] = z; 567 | } 568 | 569 | }; 570 | 571 | // xtea supporting functions 572 | function Str4ToLong(s) { // convert 4 chars of s to a numeric long 573 | var v = 0; 574 | for (var i=0; i<4; i++) v |= s.charCodeAt(i) << i*8; 575 | return isNaN(v) ? 0 : v; 576 | }; 577 | 578 | function LongToStr4(v) { // convert a numeric long to 4 char string 579 | var s = String.fromCharCode(v & 0xFF, v>>8 & 0xFF, v>>16 & 0xFF, v>>24 & 0xFF); 580 | return s; 581 | }; 582 | 583 | function escCtrlCh(str) { // escape control chars which might cause problems with encrypted texts 584 | return str.replace(/[\0\t\n\v\f\r\xa0'"!]/g, function(c) {return '!' + c.charCodeAt(0) + '!';}); 585 | }; 586 | 587 | function unescCtrlCh(str) { // unescape potentially problematic nulls and control characters 588 | return str.replace(/!\d\d?\d?!/g, function(c) {return String.fromCharCode(c.slice(1,-1));}); 589 | }; 590 | 591 | 592 | 593 | }; 594 | })(jQuery); 595 | 596 | 597 | --------------------------------------------------------------------------------