mirror of
https://gitlab.silvrtree.co.uk/martind2000/nci.git
synced 2025-02-05 04:20:16 +00:00
Merge branch 'master' into feature/move-to-common-js
This commit is contained in:
commit
761012f23e
15
README.md
15
README.md
@ -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
2
app.js
@ -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
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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 + '%'})
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
.main-row
|
div
|
||||||
.row
|
h1.page-header Builds history
|
||||||
.col-md-8.col-sm-12
|
|
||||||
h2 Builds history
|
BuildsList()
|
||||||
BuildsList()
|
|
||||||
|
@ -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)
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
.terminal
|
.terminal
|
||||||
pre.terminal_code
|
pre.terminal_code
|
||||||
.terminal_footer(style={height: '30px'})
|
.terminal_footer
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
4
db.js
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
@ -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
BIN
static/images/preloader.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
39
static/js/app/components/builds/list.js
Normal file
39
static/js/app/components/builds/list.js
Normal 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;
|
||||||
|
});
|
66
static/js/app/components/builds/view.js
Normal file
66
static/js/app/components/builds/view.js
Normal 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;
|
||||||
|
});
|
131
static/js/app/components/terminal/terminal.js
Normal file
131
static/js/app/components/terminal/terminal.js
Normal 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;
|
||||||
|
});
|
66
static/js/app/stores/terminal.js
Normal file
66
static/js/app/stores/terminal.js
Normal 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;
|
||||||
|
});
|
@ -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();
|
||||||
|
Loading…
Reference in New Issue
Block a user