Merge branch 'master' into feature/move-to-common-js

This commit is contained in:
Vladimir Polyakov 2015-12-22 21:50:13 +03:00
commit 761012f23e
23 changed files with 881 additions and 281 deletions

View File

@ -16,7 +16,7 @@ work in progress...
* ~~Persistent build and console output information~~ * ~~Persistent build and console output information~~
* ~~Project relations (blocks, triggers, etc)~~ * ~~Project relations (blocks, triggers, etc)~~
* ~~Writes to stderr must not break the build~~ * ~~Writes to stderr must not break the build~~
* Mail and jabber notifications (with commits, current step and error) * ~~Mail and jabber notifications~~
* ~~Rename notification strategies according to statuses~~ * ~~Rename notification strategies according to statuses~~
* ~~Work with git~~ * ~~Work with git~~
* ~~Build every commit, commit with tag, etc~~ * ~~Build every commit, commit with tag, etc~~
@ -33,12 +33,15 @@ work in progress...
* ~~git checkout before reset~~ * ~~git checkout before reset~~
* slow move out from build page (with lot of output) to main page - several sec * slow move out from build page (with lot of output) to main page - several sec
* when long line appear console output row numbers not on the same line with * ~~when long line appear console output row numbers not on the same line with
content content~~
* some "undefined" comments in scm changes * some "undefined" comments in scm changes
* projects list scroll * projects list scroll
* Error during send: TypeError: Cannot read property 'changes' of undefined * ~~Error during send: TypeError: Cannot read property 'changes' of undefined~~
* Builds loss * ~~Builds loss~~
* ~~error on git after change branch: fatal: ambiguous argument '18a8ea4..branch':
unknown revision or path not in the working tree.~~
* "Uncaught TypeError: Cannot read property 'name' of undefined" at item.js (jade)
## Feature requests ## Feature requests
@ -46,7 +49,7 @@ content
* ~~should write at the end of build console out that build is done (or error)~~ * ~~should write at the end of build console out that build is done (or error)~~
* ~~share workspace files at static~~ * ~~share workspace files at static~~
* "clear workspace" button * "clear workspace" button
* show more builds button (or infinity scroll) on start page * ~~show more builds button (or infinity scroll) on start page~~
* ~~hide console output by default (when go on completed build page you scroll * ~~hide console output by default (when go on completed build page you scroll
down to the output which could be very long)~~ down to the output which could be very long)~~
* speed up build points animation at ff (maybe borrow something from animate.css?) * speed up build points animation at ff (maybe borrow something from animate.css?)

2
app.js
View File

@ -32,7 +32,7 @@ var server = http.createServer(function(req, res) {
} }
if (req.url.indexOf('/data.io.js') === -1) { if (req.url.indexOf('/data.io.js') === -1) {
if (/(js|css|fonts)/.test(req.url)) { if (/(js|css|fonts|images)/.test(req.url)) {
staticServer.serve(req, res); staticServer.serve(req, res);
} else { } else {
// serve index for all app pages // serve index for all app pages

View File

@ -13,42 +13,61 @@ mixin statusText(build)
- var build = this.props.build; - var build = this.props.build;
.build(class="") .builds_item(class="builds_item__#{build.status}")
.build_content .builds_inner
.build_status .row
.status(class="status__#{build.status}") .builds_header
div.build_header if build.project
if build.project span
span Scm(scm=build.project.scm.type)
Scm(scm=build.project.scm.type) |
Link(to="project", params={name: build.project.name})
span= build.project.name
| |
Link(to="project", params={name: build.project.name}) if build.number
span= build.project.name span(style={fontSize: '15px', color: '#a6a6a6'}) build
| |
if build.number if build.status !== 'queued'
span(style={fontSize: '15px', color: '#a6a6a6'}) build Link(to="build", params={id: build.id})
| span #
if build.status !== 'queued' span= build.number
Link(to="build", params={id: build.id}) else
span # span #
span= build.number span= build.number
else
span #
span= build.number
if build.waitReason if build.waitReason
span ( span (
span= build.waitReason span= build.waitReason
span , waiting) span , waiting)
if build.status === 'in-progress' && build.currentStep if build.status === 'in-progress' && build.currentStep
span ( span (
span= build.currentStep span= build.currentStep
span ) span )
div .builds_controls
if build.completed
.builds_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'
.builds_progress
if build.project.avgBuildDuration
Progress(build=build)
if build.status === 'queued'
.builds_buttons
a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onCancelBuild(build.id))
i.fa.fa-fw.fa-times(title="Cancel build")
|
| Cancel build
.builds_content
if build.endDate if build.endDate
span.build_info span.builds_info
i.fa.fa-fw.fa-clock-o i.fa.fa-fw.fa-clock-o
| finished | finished
DateTime(value=build.endDate) DateTime(value=build.endDate)
@ -56,39 +75,18 @@ mixin statusText(build)
Duration(value=(build.endDate - build.startDate), withSuffix=true) Duration(value=(build.endDate - build.startDate), withSuffix=true)
else else
if build.startDate if build.startDate
span.build_info span.builds_info
i.fa.fa-fw.fa-clock-o i.fa.fa-fw.fa-clock-o
| started | started
DateTime(value=build.startDate) DateTime(value=build.startDate)
else else
span.build_info span.builds_info
i.fa.fa-fw.fa-clock-o i.fa.fa-fw.fa-clock-o
| queued | queued
DateTime(value=build.createDate) DateTime(value=build.createDate)
| |
if build.scm if build.scm
span.build_info span.builds_info
i.fa.fa-fw.fa-comment-o i.fa.fa-fw.fa-comment-o
| |
span= utils.prune(build.scm.rev.comment, 40) span= utils.prune(build.scm.rev.comment, 40)
|
.build_controls
if build.completed
.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)
if build.status === 'queued'
.build_controls_buttons
a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onCancelBuild(build.id))
i.fa.fa-fw.fa-times(title="Cancel build")
|
| Cancel build

View File

@ -1,6 +1,15 @@
.builds - var itemsCount = this.state.items.length;
if !this.state.items.length
p Build history is empty
each build, index in this.state.items
Item(build=build, key=build.id)
if itemsCount
.builds.builds__timeline.builds__timeline-large(class="builds__timeline-#{itemsCount % 2 ? 'left' : 'right'}")
each build, index in this.state.items
Item(build=build, key=build.id)
else
p Build history is empty
if itemsCount && itemsCount % 20 === 0
.text-center
a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onShowMoreBuilds(this.props.projectName))
i.fa.fa-fw.fa-plus(title="Show more builds")
|
| Show more builds

View File

@ -15,29 +15,30 @@ mixin statusBadge(build)
if this.state.build if this.state.build
.col-sm-3.hidden-xs .col-sm-3.hidden-xs
BuildSidebar(projectName=this.state.build.project.name) BuildSidebar(projectName=this.state.build.project.name)
.col-sm-9 .col-sm-9
h1 h1.page-header
.pull-right(style={fontSize: '22px'}) .pull-right(style={fontSize: '22px'})
mixin statusBadge(this.state.build) mixin statusBadge(this.state.build)
span Build # span Build #
span= this.state.build.number span= this.state.build.number
.text-muted(style={marginTop: '-10px'}) .small.text-muted
| Initiated by | Initiated by
- var initiator = this.state.build.initiator; - var initiator = this.state.build.initiator;
if initiator.type === 'build' if initiator.type === 'build'
Link(to="project", params={name: initiator.project.name}) Link(to="project", params={name: initiator.project.name})
span= initiator.project.name span= initiator.project.name
| |
| during the | during the
| |
Link(to="build", params={id: initiator.id}) Link(to="build", params={id: initiator.id})
span build # span build #
span= initiator.number span= initiator.number
else else
span= initiator.type span= initiator.type
hr //- hr
.build-view_info .build-view_info
if this.state.build.error if this.state.build.error
@ -133,5 +134,5 @@ mixin statusBadge(build)
button.btn.btn-primary(onClick=this.toggleConsole) button.btn.btn-primary(onClick=this.toggleConsole)
i.fa.fa-fw.fa-refresh i.fa.fa-fw.fa-refresh
| |
| Load console output... | Show full console output

View File

@ -1,17 +1,18 @@
.builds(style={paddingTop: '20px'}) .builds.builds__timeline.builds__timeline-small
each item in this.state.items each item in this.state.items
.build.build__small(key=item.id) .builds_item(key=item.id, class="builds_item__#{item.status}")
.build_content .builds_inner
.build_status .row
.status.status__small(class="status__#{item.status}") .builds_header
.build_header Link(to="build", params={id: item.id})
Link(to="build", params={id: item.id}) span build #
span build # span= item.number
span= item.number
.build_controls .builds_controls
if item.status === 'in-progress' if item.status === 'in-progress'
.build_controls_progress .builds_progress
if item.project.avgBuildDuration if item.project.avgBuildDuration
Progress(build=item) Progress(build=item)
if item.endDate
DateTime(value=item.endDate) if !item.endDate
DateTime(value=item.endDate)

View File

@ -1,2 +1,2 @@
.progress .progress
.progress-bar.progress-bar-success(style={width: this.state.percent + '%'}) .progress-bar.progress-bar-success.progress-bar-striped.active(style={width: this.state.percent + '%'})

View File

@ -1,5 +1,4 @@
.main-row div
.row h1.page-header Builds history
.col-md-8.col-sm-12
h2 Builds history BuildsList()
BuildsList()

View File

@ -1,58 +1,56 @@
.row div
.col-md-8 h1.page-header.clearfix
h1.clearfix .pull-right
.pull-right button.btn.btn-sm.btn-primary.dropdown-toggle(
button.btn.btn-sm.btn-primary.dropdown-toggle( data-toggle="dropdown",
data-toggle="dropdown", aria-expanded="false",
aria-expanded="false", disabled="true"
disabled="true" )
) | target revision:
| target revision: span= this.state.project.scm ? this.state.project.scm.rev : ''
span= this.state.project.scm ? this.state.project.scm.rev : '' |
| if this.state.project.name
if this.state.project.name button.btn.btn-sm.btn-success(onClick=this.onBuildProject)
button.btn.btn-sm.btn-success(onClick=this.onBuildProject) i.fa.fa-fw.fa-play
i.fa.fa-fw.fa-play
|
span Build
div
Scm(scm=this.state.project.scm ? this.state.project.scm.type : '')
span= this.state.project.name
hr
div.text-muted
- var lastDoneBuild = this.state.project.lastDoneBuild;
p Last successfully built:
if lastDoneBuild
DateTime(value=lastDoneBuild.endDate)
| |
| (build # span Build
span= lastDoneBuild.number div
| ) Scm(scm=this.state.project.scm ? this.state.project.scm.type : '')
else span= this.state.project.name
| -
p Current successfully streak: div.text-muted
if lastDoneBuild - var lastDoneBuild = this.state.project.lastDoneBuild;
span= this.state.project.doneBuildsStreak p Last successfully built:
else if lastDoneBuild
| - DateTime(value=lastDoneBuild.endDate)
|
| (build #
span= lastDoneBuild.number
| )
else
| -
p Last build duration: p Current successfully streak:
if lastDoneBuild if lastDoneBuild
Duration(value=(lastDoneBuild.endDate - lastDoneBuild.startDate)) span= this.state.project.doneBuildsStreak
else else
| - | -
p Average build duration: p Last build duration:
if this.state.project.avgBuildDuration if lastDoneBuild
Duration(value=this.state.project.avgBuildDuration) Duration(value=(lastDoneBuild.endDate - lastDoneBuild.startDate))
else else
| - | -
h2 p Average build duration:
i.fa.fa-fw.fa-history if this.state.project.avgBuildDuration
span Duration(value=this.state.project.avgBuildDuration)
span Build history else
Builds(projectName=this.props.params.name) | -
h2
i.fa.fa-fw.fa-history
span
span Build history
Builds(projectName=this.props.params.name)

View File

@ -1,3 +1,3 @@
.terminal .terminal
pre.terminal_code pre.terminal_code
.terminal_footer(style={height: '30px'}) .terminal_footer

View File

@ -6,20 +6,48 @@
} }
} }
.builds { @animation-duration: 1.5s;
padding: 0 15px;
}
.build { .builds {
.row(); &_item {
padding: 15px 0; &:hover {
margin-bottom: 3px; .builds {
background: @well-bg; &_buttons {
opacity: 1;
}
}
}
}
&_inner {
background: @well-bg;
padding: 15px;
}
&_header {
.make-xs-column(9);
}
&_controls {
.make-xs-column(3);
.text-right;
}
&_buttons {
transition: opacity 0.2s ease;
opacity: 0;
}
&_progress {
.progress {
height: 18px;
margin-bottom: 0;
}
}
&_status { &_status {
float: left; float: left;
padding-top: 14px; margin-right: 8px;
margin-right: 13px;
} }
&_info { &_info {
@ -27,126 +55,301 @@
margin-right: 10px; margin-right: 10px;
} }
&_controls { &__timeline {
.make-md-column(3); position: relative;
.make-xs-column(3);
.text-right;
&_buttons { .builds {
margin-top: 8px; &_inner {
transition: opacity 0.2s ease; border-left: 6px solid darken(@well-bg, 10%);
opacity: 0; }
&_header {
margin-bottom: 6px;
font-size: 18px;
}
&_progress {
padding: 3px 0;
}
&_item {
margin: 4px 0;
position: relative;
&:after {
content: '';
position: absolute;
border-radius: 50%;
background: @well-bg;
left: 0;
z-index: 1;
}
&:before {
content: '';
position: absolute;
border: 11px solid transparent;
top: 25px;
}
&__in-progress {
&:after {
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 {
&:after {
background: @link-color;
}
}
&__error {
&:after {
background: @brand-danger;
}
}
&__queued {
&:after {
background: @brand-primary;
}
}
}
} }
&_progress { &:after {
padding-top: 14px; content: '';
.progress { position: absolute;
height: 18px; top: 0;
margin-bottom: 0; bottom: 0;
width: 2px;
margin-left: -1px;
background: darken(@well-bg, 10%);
}
&-large {
.builds {
&_item {
padding-left: 40px;
&:after {
width: 24px;
height: 24px;
top: 25px;
}
&:before {
left: 20px;
border-right-color: darken(@well-bg, 10%);
top: 25px;
}
}
}
&:after {
left: 12px;
}
}
&-small {
.builds {
&_item {
padding-left: 30px;
&:after {
top: 16px;
height: 16px;
width: 16px;
}
&:before {
left: 10px;
border-right-color: darken(@well-bg, 10%);
top: 13px;
}
}
&_header {
font-size: 14px;
.make-xs-column(7);
margin-bottom: 0;
}
&_controls {
.make-xs-column(5);
font-size: 12px;
}
&_progress {
padding: 1px 0;
}
}
&:after {
left: 8px;
} }
} }
} }
}
&_content { @media (min-width: @screen-sm-min) {
.make-md-column(9); .builds {
.make-xs-column(9); &__timeline {
} &-large {
.builds {
&_item {
padding-left: 0;
display: inline-block;
vertical-align: top;
width: 50%;
margin: 10px 0;
&_header { &:after {
margin-bottom: 6px; left: auto;
font-size: 18px; }
a {
font-size: inherit;
}
}
&:hover { &:before {
.build_controls_buttons { left: auto;
opacity: 1; border-right-color: transparent;
} }
} }
&__small { &_inner {
.build_status { border-left: 0;
padding-top: 5px; }
} }
.build_header {
font-size: 14px; &:after {
} left: 50%;
.build_content { }
.make-xs-column(7); }
}
.build_controls { &-left {
.make-xs-column(5); .builds {
font-size: 12px; &_item {
&_progress { &:nth-child(odd) {
padding-top: 4px; padding-right: 30px;
.progress {
height: 12px; .builds {
&_inner {
border-right: 6px solid darken(@well-bg, 10%);
}
}
&:after {
right: -12px;
}
&:before {
right: 10px;
border-left-color: darken(@well-bg, 10%);
}
}
&:nth-child(even) {
padding-left: 30px;
top: 50px;
.builds {
&_inner {
border-left: 6px solid darken(@well-bg, 10%);
}
}
&:after {
left: -12px;
}
&:before {
left: 10px;
border-right-color: darken(@well-bg, 10%);
}
}
}
}
}
&-right {
.builds {
&_item {
&:first-child {
margin-left: 50%;
}
&:nth-child(even) {
padding-right: 30px;
top: -50px;
.builds {
&_inner {
border-right: 6px solid darken(@well-bg, 10%);
}
}
&:after {
right: -12px;
}
&:before {
right: 10px;
border-left-color: darken(@well-bg, 10%);
}
}
&:nth-child(odd) {
padding-left: 30px;
.builds {
&_inner {
border-left: 6px solid darken(@well-bg, 10%);
}
}
&:after {
left: -12px;
}
&:before {
left: 10px;
border-right-color: darken(@well-bg, 10%);
}
}
}
} }
} }
} }
} }
} }
@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;
}
&__small {
width: 10px;
height: 10px;
}
}
.pulsate-frames() { .pulsate-frames() {
.transform(@scaleX, @scaleY) { .transform(@scaleX, @scaleY) {
-webkit-transform: scale(@scaleX, @scaleY); opacity: 1; -webkit-transform: scale3d(@scaleX, @scaleY, 1);
-moz-transform: scale(@scaleX, @scaleY); opacity: 1; -moz-transform: scale3d(@scaleX, @scaleY, 1);
-ms-transform: scale(@scaleX, @scaleY); opacity: 1; -ms-transform: scale3d(@scaleX, @scaleY, 1);
-o-transform: scale(@scaleX, @scaleY); opacity: 1; -o-transform: scale3d(@scaleX, @scaleY, 1);
transform: scale(@scaleX, @scaleY); opacity: 1; transform: scale3d(@scaleX, @scaleY, 1);
} }
0% { 0% {
.transform(1.0, 1.0); .transform(1, 1);
}
25% {
opacity: 1.0;
} }
50% { 50% {
.transform(0.75, 0.75); .transform(0.75, 0.75);
} }
50% {
opacity: 1.0;
}
100% { 100% {
.transform(1.0, 1.0); .transform(1, 1);
} }
} }

View File

@ -1,7 +1,18 @@
body { body {
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
padding-top: @navbar-height;
min-width: 320px;
} }
.page-wrapper { .page-wrapper {
margin-top: @navbar-height + 15px; padding: 20px 0;
} }
.page-header {
margin-top: 0;
border-bottom-color: #d7d7d7;
.small {
font-size: @font-size-base;
}
}

4
db.js
View File

@ -210,7 +210,9 @@ nlevel.DocsSection.prototype._beforePut = function(docs, callback) {
// update createDate before put to provide latest date for last id // update createDate before put to provide latest date for last id
// it's rquired for correct generateIds function // it's rquired for correct generateIds function
_(docs).each(function(doc) { _(docs).each(function(doc) {
doc.createDate = Date.now(); if (!doc.id) {
doc.createDate = Date.now();
}
}); });
self.beforePut(docs, callback); self.beforePut(docs, callback);

View File

@ -182,10 +182,21 @@ Distributor.prototype._onBuildComplete = function(build, callback) {
Distributor.prototype._updateBuild = function(build, changes, callback) { Distributor.prototype._updateBuild = function(build, changes, callback) {
var self = this; var self = this;
callback = callback || _.noop; callback = callback || _.noop;
var isWithNumber = Boolean(build.number); var isWithId = Boolean(build.id),
isWithNumber = Boolean(build.number);
Steppy( Steppy(
function() { function() {
if (build.id && changes.status && changes.status !== build.status) {
logger.log(
'Build #%s (project "%s") change status: %s -> %s',
build.id,
build.project.name,
build.status,
changes.status
);
}
_(build).extend(changes); _(build).extend(changes);
// skip saving to db of unimportant data // skip saving to db of unimportant data
@ -196,6 +207,14 @@ Distributor.prototype._updateBuild = function(build, changes, callback) {
} }
}, },
function() { function() {
if (!isWithId && build.id) {
logger.log(
'Build #%s (project "%s") %s',
build.id,
build.project.name,
build.status
);
}
// if number appear after save to db then add it to changes // if number appear after save to db then add it to changes
// TODO: might be better to generate number right there (instead // TODO: might be better to generate number right there (instead

View File

@ -133,12 +133,26 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) {
var self = this; var self = this;
Steppy( Steppy(
function() { function() {
// get current rev to update on it after git log
self.getCurrent(this.slot());
},
function(err, currentRev) {
this.pass(currentRev);
// update to rev2 to prevent git error when switch to branch that
// doesn't exist locally: "unknown revision or path not in the
// working tree"
self.update(rev2, this.slot());
},
function(err, currentRev) {
this.pass(currentRev);
self.run({cmd: 'git', args: [ self.run({cmd: 'git', args: [
'log', rev1 ? rev1 + '..' + rev2 : rev2, 'log', rev1 ? rev1 + '..' + rev2 : rev2,
'--pretty=' + self._revTemplate '--pretty=' + self._revTemplate
]}, this.slot()); ]}, this.slot());
}, },
function(err, stdout) { function(err, currentRev, stdout) {
// always skip last line - it's empty // always skip last line - it's empty
var rows = stdout.split('\n').slice(0, -1); var rows = stdout.split('\n').slice(0, -1);
@ -146,7 +160,12 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) {
return self._parseRev(str); return self._parseRev(str);
}); });
this.pass(currentRev, changes);
},
function(err, currentRev, changes) {
this.pass(changes); this.pass(changes);
self.update(currentRev.id, this.slot());
}, },
callback callback
); );

View File

@ -15,13 +15,9 @@ inherits(Scm, ParentScm);
Scm.prototype.defaultRev = 'default'; Scm.prototype.defaultRev = 'default';
// use 2 invisible separators as fields separator Scm.prototype._arraysSeparator = '\u2064' + '\u2064';
Scm.prototype._fieldsSeparator = String.fromCharCode(2063); Scm.prototype._fieldsSeparator = '\u2063' + '\u2063';
Scm.prototype._fieldsSeparator += Scm.prototype._fieldsSeparator; Scm.prototype._linesSeparator = '\u2028' + '\u2028';
// use 2 vertical tabs as arrays separator
Scm.prototype._arraysSeparator = String.fromCharCode(11);
Scm.prototype._arraysSeparator += Scm.prototype._arraysSeparator;
Scm.prototype._revTemplate = [ Scm.prototype._revTemplate = [
'{node|short}', '{node|short}',
@ -94,13 +90,15 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) {
function() { function() {
self.run({cmd: 'hg', args: [ self.run({cmd: 'hg', args: [
'log', '--rev', rev2 + ':' + rev1, 'log', '--rev', rev2 + ':' + rev1,
'--template', self._revTemplate + '\n' '--template', self._revTemplate + self._linesSeparator
]}, this.slot()); ]}, this.slot());
}, },
function(err, stdout) { function(err, stdout) {
// always skip last line - it's empty and also skip first // always skip last line - it's empty and also skip first
// rev if we see range // rev if we see range
var rows = stdout.split('\n').slice(0, rev1 === rev2 ? -1 : -2); var rows = stdout.split(self._linesSeparator).slice(
0, rev1 === rev2 ? -1 : -2
);
var changes = _(rows).map(function(str) { var changes = _(rows).map(function(str) {
return self._parseRev(str); return self._parseRev(str);

View File

@ -1,6 +1,6 @@
{ {
"name": "nci", "name": "nci",
"version": "0.3.6", "version": "0.4.1",
"description": "Continuous integration server written in node.js", "description": "Continuous integration server written in node.js",
"bin": { "bin": {
"nci": "bin/nci" "nci": "bin/nci"

BIN
static/images/preloader.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,39 @@
'use strict';
define([
'react',
'reflux',
'underscore',
'./item',
'app/actions/build',
'app/stores/builds',
'templates/app/components/builds/list'
], function(React, Reflux, _, Item, BuildActions, buildsStore, template) {
template = template.locals({
Item: Item
});
var Component = React.createClass({
mixins: [
Reflux.connectFilter(buildsStore, 'items', function(items) {
var projectName = this.props.projectName;
if (projectName) {
return _(items).filter(function(item) {
return item.project && item.project.name === projectName;
});
} else {
return items;
}
})
],
onShowMoreBuilds: function(projectName) {
BuildActions.readAll({
projectName: projectName,
limit: this.state.items.length + 20
});
},
render: template
});
return Component;
});

View File

@ -0,0 +1,66 @@
'use strict';
define([
'react',
'react-router',
'reflux',
'app/actions/build',
'app/stores/build',
'app/components/terminal/terminal',
'app/components/buildSidebar/index',
'templates/app/components/builds/view',
'app/components/common/index'
], function(
React, Router, Reflux, BuildActions, buildStore, TerminalComponent,
BuildSidebar, template, CommonComponents
) {
template = template.locals({
DateTime: CommonComponents.DateTime,
Duration: CommonComponents.Duration,
Scm: CommonComponents.Scm,
Terminal: TerminalComponent,
Link: Router.Link,
BuildSidebar: BuildSidebar
});
var Component = React.createClass({
mixins: [Reflux.ListenerMixin],
statics: {
willTransitionTo: function(transition, params, query) {
BuildActions.read(Number(params.id));
}
},
componentDidMount: function() {
this.listenTo(buildStore, this.updateBuild);
},
componentWillReceiveProps: function(nextProps) {
// reset console status when go from build page to another build
// page (did mount and mount not called in this case)
if (Number(nextProps.params.id) !== this.state.build.id) {
this.setState({showConsole: this.getInitialState().showConsole});
}
},
updateBuild: function(build) {
if (build) {
BuildActions.readAll({projectName: build.project.name});
}
this.setState({build: build});
},
render: template,
getInitialState: function() {
return {
build: null,
showConsole: false
};
},
toggleConsole: function() {
var consoleState = !this.state.showConsole;
if (consoleState) {
BuildActions.readTerminalOutput(this.state.build);
}
this.setState({showConsole: consoleState});
}
});
return Component;
});

View File

@ -0,0 +1,131 @@
'use strict';
define([
'underscore',
'react',
'reflux',
'app/stores/terminal',
'app/stores/build',
'ansi_up',
'templates/app/components/terminal/terminal'
], function(
_,
React,
Reflux,
terminalStore,
buildStore,
ansiUp,
template
) {
var Component = React.createClass({
mixins: [Reflux.ListenerMixin],
shouldScrollBottom: true,
data: [],
linesCount: 0,
componentDidMount: function() {
this.listenTo(terminalStore, this.updateItems);
var node = document.getElementsByClassName('terminal')[0];
this.initialScrollPosition = node.getBoundingClientRect().top;
if (this.props.showPreloader) {
this.getTerminal().insertAdjacentHTML('afterend',
'<img src="/images/preloader.gif" class="terminal_preloader"/>'
);
this.listenTo(buildStore, function(build) {
if (build.completed) {
this.removePreloader();
}
});
}
window.onscroll = this.onScroll;
},
removePreloader: function() {
var preloader = document.getElementsByClassName(
'terminal_preloader'
)[0];
preloader.parentNode.removeChild(preloader);
},
componentWillUnmount: function() {
window.onscroll = null;
},
prepareRow: function(row) {
return ansiUp.ansi_to_html(row.replace('\r', ''));
},
prepareOutput: function(output) {
var self = this;
return output.map(function(row) {
return self.prepareRow(row);
});
},
getTerminal: function() {
return document.getElementsByClassName('terminal')[0];
},
getBody: function() {
return document.getElementsByTagName('body')[0];
},
onScroll: function() {
var node = this.getTerminal(),
body = this.getBody();
this.shouldScrollBottom = window.innerHeight + body.scrollTop >=
node.offsetHeight + this.initialScrollPosition;
},
ensureScrollPosition: function() {
if (this.shouldScrollBottom) {
var node = this.getTerminal(),
body = this.getBody();
body.scrollTop = this.initialScrollPosition + node.offsetHeight;
}
},
makeCodeLineContent: function(line) {
return '<span class="code-line_counter">' + '</span>' +
'<div class="code-line_body">' + this.prepareRow(line) + '</div>';
},
makeCodeLine: function(line, index) {
return '<div class="code-line" data-number="' + index + '">' +
this.makeCodeLineContent(line) + '</div>';
},
renderBuffer: _.throttle(function() {
var data = this.data,
currentLinesCount = data.length,
terminal = document.getElementsByClassName('terminal_code')[0],
rows = terminal.childNodes;
if (rows.length) {
// replace our last node
var index = this.linesCount - 1;
rows[index].innerHTML = this.makeCodeLineContent(data[index]);
}
var self = this;
terminal.insertAdjacentHTML('beforeend',
_(data.slice(this.linesCount)).map(function(line, index) {
return self.makeCodeLine(line, self.linesCount + index);
}).join('')
);
this.linesCount = currentLinesCount;
this.ensureScrollPosition();
}, 100),
updateItems: function(build) {
// listen just our console update
if (build.buildId === this.props.build) {
this.data = build.data;
this.renderBuffer();
}
if (this.props.showPreloader && build.buildCompleted) {
this.removePreloader();
}
},
shouldComponentUpdate: function() {
return false;
},
render: template
});
return Component;
});

View File

@ -0,0 +1,66 @@
'use strict';
define([
'underscore', 'reflux', 'app/actions/build', 'app/connect'
], function(
_, Reflux, BuildActions, connect
) {
var Store = Reflux.createStore({
listenables: BuildActions,
init: function() {
// the only purpose of this hash to reconnect all the time
// except first, see notes at using
this.connectedResourcesHash = {};
},
onReadTerminalOutput: function(build) {
var self = this,
output = [],
resourceName = 'build' + build.id;
var connectToBuildDataResource = function() {
// reconnect for get data below (at subscribe), coz
// data emitted only once during connect
if (self.connectedResourcesHash[resourceName]) {
connect.resource(resourceName).reconnect();
} else {
self.connectedResourcesHash[resourceName] = 1;
}
connect.resource(resourceName).subscribe('data', function(data) {
var lastLine = _(self.lines).last();
if (lastLine && (_(data.lines).first().number === lastLine.number)) {
self.lines = _(self.lines).initial();
}
self.lines = self.lines.concat(data.lines);
self.trigger({
buildId: build.id,
buildCompleted: build.completed,
name: 'Console for build #' + build.id,
data: _(self.lines).pluck('text')
});
});
};
this.lines = [];
this.currentLine = '';
// create data resource for completed build
if (build.completed) {
connect.resource('projects').sync(
'createBuildDataResource',
{buildId: build.id},
function(err) {
if (err) throw err;
connectToBuildDataResource();
}
);
} else {
connectToBuildDataResource();
}
}
});
return Store;
});

View File

@ -37,7 +37,7 @@ describe('Db concurrency', function() {
); );
}); });
describe('prallel builds put should produce different ids', function() { describe('prallel builds add should produce different ids', function() {
var expectedIds = []; var expectedIds = [];
var builds = _(100).chain().range().map(function(number) { var builds = _(100).chain().range().map(function(number) {
@ -45,7 +45,7 @@ describe('Db concurrency', function() {
return makeBuild({project: {name: 'project' + number}}); return makeBuild({project: {name: 'project' + number}});
}).value(); }).value();
it('put two builds in parallel without errors', function(done) { it('put builds in parallel without errors', function(done) {
Steppy( Steppy(
function() { function() {
var putGroup = this.makeGroup(); var putGroup = this.makeGroup();
@ -68,6 +68,43 @@ describe('Db concurrency', function() {
}); });
}); });
describe('prallel builds add/update should produce different ids', function() {
var expectedIds = [];
var builds = _(200).chain().range().map(function(number) {
expectedIds.push(number + 1);
return makeBuild({project: {name: 'project' + number}});
}).value();
it('put builds in parallel without errors', function(done) {
Steppy(
function() {
var putGroup = this.makeGroup();
_(builds.slice(0, 190)).each(function(build) {
db.builds.put(build, putGroup.slot());
});
},
function() {
var putGroup = this.makeGroup();
_(builds).each(function(build) {
db.builds.put(build, putGroup.slot());
});
},
done
);
});
it('shoud have all ' + expectedIds.length +' ids ', function() {
expect(_(builds).chain().pluck('id').sortBy().value()).eql(
expectedIds
);
});
after(function(done) {
db.builds.del(expectedIds, done);
});
});
describe('prallel builds put should produce different numbers', function() { describe('prallel builds put should produce different numbers', function() {
var expectedIds = []; var expectedIds = [];
@ -79,7 +116,7 @@ describe('Db concurrency', function() {
}); });
}).value(); }).value();
it('put three builds in parallel without errors', function(done) { it('put builds in parallel without errors', function(done) {
Steppy( Steppy(
function() { function() {
var putGroup = this.makeGroup(); var putGroup = this.makeGroup();