diff --git a/lib/distributor.js b/lib/distributor.js index a0fe966..3ce3dcb 100644 --- a/lib/distributor.js +++ b/lib/distributor.js @@ -3,6 +3,7 @@ var Steppy = require('twostep').Steppy, _ = require('underscore'), Node = require('./node').Node, + getAvgProjectBuildDuration = require('./project').getAvgProjectBuildDuration, EventEmitter = require('events').EventEmitter, inherits = require('util').inherits, notifier = require('./notifier'), @@ -226,8 +227,10 @@ Distributor.prototype.run = function(params, callback) { } else { this.pass(null); } + + getAvgProjectBuildDuration(params.projectName, this.slot()); }, - function(err, hasScmChanges) { + function(err, hasScmChanges, avgProjectBuildDuration) { if (params.withScmChangesOnly && !hasScmChanges) { logger.log( 'Building of "%s" skipped coz no scm changes', @@ -236,6 +239,8 @@ Distributor.prototype.run = function(params, callback) { return callback(); } + project.avgBuildDuration = avgProjectBuildDuration; + self._updateBuild({}, { project: project, initiator: params.initiator, diff --git a/lib/project.js b/lib/project.js index 7cd58ed..eaa7839 100644 --- a/lib/project.js +++ b/lib/project.js @@ -5,6 +5,7 @@ var Steppy = require('twostep').Steppy, path = require('path'), _ = require('underscore'), reader = require('./reader'), + db = require('../db'), utils = require('./utils'); @@ -99,3 +100,27 @@ exports.create = function(baseDir, config, callback) { callback ); }; + +exports.getAvgProjectBuildDuration = function(projectName, callback) { + Steppy( + function() { + // get last done builds to calc avg build time + db.builds.find({ + start: { + projectName: projectName, + status: 'done', + descCreateDate: '' + }, + limit: 10 + }, this.slot()); + }, + function(err, doneBuilds) { + var durationsSum = _(doneBuilds).reduce(function(memo, build) { + return memo + (build.endDate - build.startDate); + }, 0); + + this.pass(Math.round(durationsSum / doneBuilds.length)); + }, + callback + ); +}; diff --git a/resources/builds.js b/resources/builds.js index 5b4b0a3..9ff15f4 100644 --- a/resources/builds.js +++ b/resources/builds.js @@ -30,7 +30,9 @@ module.exports = function(app) { _(builds).each(function(build) { delete build.stepTimings; delete build.scm.changes; - build.project = _(build.project).pick('name', 'scm'); + build.project = _(build.project).pick( + 'name', 'scm', 'avgBuildDuration' + ); }); res.send(builds); diff --git a/resources/projects.js b/resources/projects.js index 7520733..88f6420 100644 --- a/resources/projects.js +++ b/resources/projects.js @@ -2,6 +2,8 @@ var Steppy = require('twostep').Steppy, _ = require('underscore'), + getAvgProjectBuildDuration = + require('../lib/project').getAvgProjectBuildDuration, createBuildDataResource = require('../distributor').createBuildDataResource, logger = require('../lib/logger')('projects resource'), db = require('../db'); @@ -26,14 +28,16 @@ module.exports = function(app) { function() { project = _(app.projects).findWhere(req.data); - // get last done builds to calc avg build time + getAvgProjectBuildDuration(project.name, this.slot()); + + // get last done build db.builds.find({ start: { projectName: project.name, status: 'done', descCreateDate: '' }, - limit: 10 + limit: 1 }, this.slot()); // tricky but effective streak counting inside filter goes below @@ -60,17 +64,9 @@ module.exports = function(app) { doneBuildsStreakCallback(err, doneBuildsStreak); }); }, - function(err, doneBuilds, doneBuildsStreak) { - project.lastDoneBuild = doneBuilds[0]; - - var durationsSum = _(doneBuilds).reduce(function(memo, build) { - return memo + (build.endDate - build.startDate); - }, 0); - - project.avgBuildDuration = Math.round( - durationsSum / doneBuilds.length - ); - + function(err, avgProjectBuildDuration, lastDoneBuilds, doneBuildsStreak) { + project.lastDoneBuild = lastDoneBuilds[0]; + project.avgBuildDuration = avgProjectBuildDuration; project.doneBuildsStreak = doneBuildsStreak; res.send(project); diff --git a/static/css/sources/components/builds.less b/static/css/sources/components/builds.less index bded1e7..d86da6d 100644 --- a/static/css/sources/components/builds.less +++ b/static/css/sources/components/builds.less @@ -14,15 +14,12 @@ .row(); padding: 15px 0; margin-bottom: 3px; + background: lighten(@well-bg, 3%); - &__in-progress { - background: lighten(@brand-info, 40%); - } - &__done { - background: lighten(@brand-success, 50%); - } - &__error { - background: lighten(@brand-danger, 30%); + &_status { + float: left; + padding-top: 14px; + margin-right: 13px; } &_info { @@ -34,8 +31,21 @@ .make-md-column(3); .make-xs-column(3); .text-right; - margin-top: 8px; + + &_buttons { + margin-top: 8px; + transition: opacity 0.2s ease; + opacity: 0; + } + + &_progress { + padding-top: 14px; + .progress { + margin-bottom: 0; + } + } } + &_content { .make-md-column(9); .make-xs-column(9); @@ -48,4 +58,72 @@ font-size: inherit; } } + + &:hover { + .build_controls_buttons { + opacity: 1; + } + } } + +@animation-duration: 1.5s; + +.status { + width: 25px; + height: 25px; + border-radius: 50%; + + &__in-progress { + background: @brand-info; + + -webkit-animation: pulsate @animation-duration ease-out; + -webkit-animation-iteration-count: infinite; + -moz-animation: pulsate @animation-duration ease-out; + -moz-animation-iteration-count: infinite; + -o-animation: pulsate @animation-duration ease-out; + -o-animation-iteration-count: infinite; + animation: pulsate @animation-duration ease-out; + animation-iteration-count: infinite; + } + &__done { + background: @link-color; + } + &__error { + background: @brand-danger; + } + &__queued { + background: @brand-primary; + } +} + +.pulsate-frames() { + .transform(@scaleX, @scaleY) { + -webkit-transform: scale(@scaleX, @scaleY); opacity: 1; + -moz-transform: scale(@scaleX, @scaleY); opacity: 1; + -ms-transform: scale(@scaleX, @scaleY); opacity: 1; + -o-transform: scale(@scaleX, @scaleY); opacity: 1; + transform: scale(@scaleX, @scaleY); opacity: 1; + } + + 0% { + .transform(1.0, 1.0); + } + 25% { + opacity: 1.0; + } + 50% { + .transform(0.75, 0.75); + } + 50% { + opacity: 1.0; + } + 100% { + .transform(1.0, 1.0); + } +} + +@-webkit-keyframes pulsate {.pulsate-frames} +@-moz-keyframes pulsate {.pulsate-frames} +@-ms-keyframes pulsate {.pulsate-frames} +@-o-keyframes pulsate {.pulsate-frames} +@keyframes pulsate {.pulsate-frames} diff --git a/static/js/app/components/builds/item.jade b/static/js/app/components/builds/item.jade index a4dfcd2..acf9d80 100644 --- a/static/js/app/components/builds/item.jade +++ b/static/js/app/components/builds/item.jade @@ -13,16 +13,27 @@ mixin statusText(build) - var build = this.props.build; -.build(class="build__#{build.status}") +.build(class="") .build_content + .build_status + .status(class="status__#{build.status}") div.build_header + if build.project + span + Scm(scm=build.project.scm.type) + | + Link(to="project", params={name: build.project.name}) + span= build.project.name + | if build.number + span(style={fontSize: '15px', color: '#a6a6a6'}) build + | if build.status !== 'queued' Link(to="build", params={id: build.id}) - span Build # + span # span= build.number else - span Build # + span # span= build.number if build.waitReason @@ -36,12 +47,6 @@ mixin statusText(build) span ) div - if build.project - span.build_info - Scm(scm=build.project.scm.type) - | - Link(to="project", params={name: build.project.name}) - span= build.project.name if build.endDate span.build_info i.fa.fa-fw.fa-clock-o @@ -69,11 +74,13 @@ mixin statusText(build) .build_controls if build.completed - a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onRebuildProject(build.project.name)) - i.fa.fa-fw.fa-repeat(title="Rebuild") - | - | Build again - else - //-.progress - //-.progress-bar.progress-bar-success(style={width: '60%'}) + .build_controls_buttons + a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onRebuildProject(build.project.name)) + i.fa.fa-fw.fa-repeat(title="Rebuild") + | + | Build again + if build.status === 'in-progress' + .build_controls_progress + if build.project.avgBuildDuration + Progress(build=build) diff --git a/static/js/app/components/builds/item.js b/static/js/app/components/builds/item.js index 420e2bb..3e60ed0 100644 --- a/static/js/app/components/builds/item.js +++ b/static/js/app/components/builds/item.js @@ -12,6 +12,7 @@ define([ template = template.locals({ DateTime: CommonComponents.DateTime, Duration: CommonComponents.Duration, + Progress: CommonComponents.Progress, Scm: CommonComponents.Scm, Terminal: TerminalComponent, Link: Router.Link @@ -24,7 +25,6 @@ define([ }; }, onRebuildProject: function(projectName) { - console.log('onRebuildProject'); ProjectActions.run(projectName) }, onShowTerminal: function(build) { diff --git a/static/js/app/components/common/index.js b/static/js/app/components/common/index.js index bbb451e..0903ec0 100644 --- a/static/js/app/components/common/index.js +++ b/static/js/app/components/common/index.js @@ -3,11 +3,13 @@ define([ './dateTime/index', './scm/index', - './duration/index' -], function(DateTime, Scm, Duration) { + './duration/index', + './progress/index' +], function(DateTime, Scm, Duration, Progress) { return { DateTime: DateTime, Scm: Scm, - Duration: Duration + Duration: Duration, + Progress: Progress }; }); diff --git a/static/js/app/components/common/progress/index.jade b/static/js/app/components/common/progress/index.jade new file mode 100644 index 0000000..ef90fb2 --- /dev/null +++ b/static/js/app/components/common/progress/index.jade @@ -0,0 +1,2 @@ +.progress + .progress-bar.progress-bar-success(style={width: this.state.percent + '%'}) diff --git a/static/js/app/components/common/progress/index.js b/static/js/app/components/common/progress/index.js new file mode 100644 index 0000000..cd9a258 --- /dev/null +++ b/static/js/app/components/common/progress/index.js @@ -0,0 +1,31 @@ +'use strict'; + +define([ + 'underscore', + 'react', + 'templates/app/components/common/progress/index' +], function(_, React, template) { + return React.createClass({ + render: template, + _computePercent: function() { + var build = this.props.build; + return Math.round((Date.now() - build.startDate) / + build.project.avgBuildDuration * 100); + }, + componentDidMount: function() { + var self = this, + updateCallback = function() { + if (self.props.build.status === 'in-progress') { + self.setState({percent: self._computePercent()}); + _.delay(updateCallback, 100); + } + } + updateCallback(); + }, + getInitialState: function() { + return { + percent: this._computePercent() + } + } + }); +});