├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .npmrc ├── .travis.yml ├── LICENSE.txt ├── README.md ├── app.js ├── app ├── .jshintrc ├── actions │ ├── build.js │ ├── buildLog.js │ └── project.js ├── app.js ├── components │ ├── app │ │ ├── index.jade │ │ └── index.js │ ├── builds │ │ ├── item │ │ │ ├── index.jade │ │ │ └── index.js │ │ ├── list │ │ │ ├── index.jade │ │ │ └── index.js │ │ └── view │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ └── sidebar │ │ │ ├── index.jade │ │ │ └── index.js │ ├── common │ │ ├── dateTime │ │ │ ├── index.js │ │ │ └── template.jade │ │ ├── duration │ │ │ ├── index.jade │ │ │ └── index.js │ │ ├── index.js │ │ ├── progress │ │ │ ├── index.jade │ │ │ └── index.js │ │ └── scm │ │ │ ├── index.jade │ │ │ └── index.js │ ├── projects │ │ ├── header │ │ │ ├── index.jade │ │ │ └── index.js │ │ ├── runForm │ │ │ ├── index.jade │ │ │ └── index.js │ │ ├── selector │ │ │ ├── index.jade │ │ │ └── index.js │ │ └── view │ │ │ ├── index.jade │ │ │ └── index.js │ ├── revisions │ │ ├── item │ │ │ ├── index.jade │ │ │ └── index.js │ │ └── list │ │ │ ├── index.jade │ │ │ └── index.js │ ├── root │ │ └── index.js │ └── terminal │ │ ├── index.jade │ │ └── index.js ├── connect.js ├── resources.js ├── stores │ ├── build.js │ ├── buildLog.js │ ├── builds.js │ ├── project.js │ ├── projects.js │ └── terminal.js ├── styles │ ├── common │ │ ├── fonts.less │ │ ├── variables-superhero.less │ │ └── variables.less │ ├── components │ │ ├── builds.less │ │ ├── layout.less │ │ ├── projects.less │ │ └── terminal.less │ └── index.less └── utils.js ├── dataio.js ├── docker ├── .npmrc ├── Dockerfile ├── data │ ├── config.yaml │ ├── preload.json │ └── projects │ │ └── some_project │ │ └── config.yaml ├── entrypoint.sh ├── package-lock.json └── package.json ├── package.json ├── resources ├── builds.js ├── errorHandler.js ├── helpers.js ├── index.js └── projects.js ├── static ├── css │ └── .gitkeep ├── favicon.ico ├── images │ ├── build │ │ ├── progress-small.gif │ │ └── progress.gif │ └── preloader.gif └── js │ └── .gitkeep ├── transforms └── jade.js └── views └── index.jade /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | static/css/**/*.css 3 | static/fonts 4 | static/js/*.js 5 | static/index.html 6 | views/index.js 7 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | views/index.js 3 | static/js/app.build.js 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // environment 3 | "node": true, 4 | // options 5 | "newcap": false, 6 | "supernew": false, 7 | "maxlen": 80, 8 | "smarttabs": true, 9 | "indent": 1, 10 | "globalstrict": true, 11 | "strict": true, 12 | // use es3 to get 'extra comma' at object literal error 13 | "es3": true, 14 | // suppress warnings about switches with just one case 15 | "onecase": true, 16 | "nonew": false, 17 | "trailing": true, 18 | "sub": false, 19 | "loopfunc": true, 20 | "boss": false, 21 | "lastsemic": false, 22 | "quotmark": "single", 23 | "undef": true, 24 | "immed": true 25 | } 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | test 3 | static/js 4 | static/css 5 | app 6 | transforms 7 | views 8 | .jshintrc 9 | *.tgz 10 | .npmrc 11 | docker/ 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | 4 | node_js: 5 | # - "stable" 6 | # - "4.4" 7 | # - "0.12" 8 | - "0.10" 9 | 10 | script: 11 | - npm run lint 12 | # - npm test 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 4 | Oleg Korobenko , 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nci ansible ui 2 | 3 | Simple web interface for running Ansible playbooks. 4 | 5 | It pulls your repository with playbooks and inventories according to project 6 | config (which defines repository path, playbook and inventory directories inside 7 | repository, etc) and allows you to run playbooks with inventories via single 8 | page web interface (with live updates and pretty terminal output). 9 | 10 | 11 | ## Features 12 | 13 | * single page web application which immediately responds to any 14 | user interaction. This app doesn't use http api, it's built using socket.io 15 | * online console output which is very close to terminal emulator 16 | * can run one playbook with different inventories (sequentially) 17 | * works with any Mercurial or Git repositories (no matter if it's a service like 18 | Github, Bitbucket or private server, all you need is authenticate user from 19 | which nci server is running without password e.g. by SSH key) 20 | * minimal dependencies (only NodeJS, SCM client and Ansible are required) 21 | * built on top of [nci](https://github.com/node-ci/nci), can extend 22 | functionality by notification and other plugins 23 | 24 | ![nci-ansible-ui-execution](https://cloud.githubusercontent.com/assets/465522/21159795/e281871a-c19b-11e6-9dea-aac57440dffe.png) 25 | 26 | 27 | ## Installation 28 | 29 | 30 | ### Docker image 31 | 32 | It's recommended setup, image for nci ansible ui contains all dependencies 33 | including ansible. You can try it using command: 34 | 35 | ```sh 36 | docker run --rm -it -p 3000:3000 okvd/nci-ansible-ui 37 | ``` 38 | 39 | That's all, now you can experiment with it by adding/changing projects, 40 | use web interface (on http://127.0.0.1:3000 by default) to run playbooks. 41 | 42 | See [image page](https://hub.docker.com/r/okvd/nci-ansible-ui) for details. 43 | 44 | 45 | ### Native setup 46 | 47 | System requirements: 48 | 49 | * unix-like operating system, not tested on windows 50 | * node.js >= 0.10 51 | * git client >= 1.9 (only for building git projects) 52 | * mercurial client >= 2.8 (only for building mercurial projects) 53 | * ansible 54 | * build tools - gcc, make, etc 55 | (for building [LevelDB](https://github.com/level/leveldown) if binary is not 56 | provided for your platform). E.g. ubuntu `build-essential` package provides 57 | such tools. 58 | 59 | On the system with satisfied requirements clone quick setup repository, 60 | go into it and install dependencies: 61 | 62 | ```sh 63 | git clone https://github.com/node-ci/nci-ansible-ui-quick-setup && 64 | cd nci-ansible-ui-quick-setup && 65 | npm install 66 | ``` 67 | 68 | run server: 69 | 70 | 71 | ```sh 72 | node_modules/.bin/nci 73 | ``` 74 | 75 | Now you can experiment with it by adding/changing projects, 76 | use web interface (on http://127.0.0.1:3000 by default) to run playbooks. 77 | 78 | Sample project works with 79 | [repository](https://github.com/node-ci/nci-ansible-ui-sample-playbook) 80 | which contains sample playbooks (some ping, ps ax and other read commands) and 81 | inventory. Inventory defines localhost as target host with following 82 | settings: 83 | 84 | ```yaml 85 | ansible_host: 127.0.0.1 86 | ansible_user: ansible 87 | ansible_ssh_private_key_file: ~/.ssh/id_rsa_test 88 | ``` 89 | 90 | you should provide such access (ansible will be run by user which started nci 91 | server) in order to run sample project. Localhost 92 | also should be in your known hosts file (you can try this access manually 93 | to get prompt which can add it). 94 | 95 | 96 | ## License 97 | 98 | MIT 99 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | fs = require('fs'), 5 | path = require('path'), 6 | staticPath = path.join(__dirname, 'static'), 7 | indexHtml = fs.readFileSync(staticPath + '/index.html'); 8 | 9 | exports.register = function(originalApp) { 10 | var app = _(originalApp).clone(), 11 | socketio = require('socket.io')(app.httpServer); 12 | 13 | app.dataio = require('./dataio')(socketio); 14 | 15 | // init resources 16 | require('./resources')(app); 17 | 18 | // serve index for all app pages, add this listener after all other 19 | // listeners 20 | app.httpServer.addRequestListener(function(req, res, next) { 21 | if (req.url.indexOf('/data.io.js') === -1) { 22 | 23 | res.setHeader('content-type', 'text/html'); 24 | res.end(indexHtml); 25 | } else { 26 | next(); 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /app/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // environment 3 | "browser": true, 4 | // options 5 | "newcap": false, 6 | "unused": true, 7 | "supernew": false, 8 | "maxlen": 80, 9 | "smarttabs": true, 10 | "indent": 1, 11 | "globalstrict": true, 12 | "strict": true, 13 | // use es3 to get 'extra comma' at object literal error 14 | "es3": true, 15 | // suppress warnings about switches with just one case 16 | "onecase": true, 17 | "nonew": false, 18 | "trailing": true, 19 | "sub": false, 20 | "loopfunc": true, 21 | "boss": false, 22 | "lastsemic": false, 23 | "quotmark": "single", 24 | "undef": true, 25 | "immed": true, 26 | "globals": { 27 | "module": false, 28 | "exports": false, 29 | "require": false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/actions/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reflux = require('reflux'); 4 | 5 | module.exports = Reflux.createActions([ 6 | 'cancel', 7 | 'readTerminalOutput', 8 | 'readAll', 9 | 'read' 10 | ]); 11 | -------------------------------------------------------------------------------- /app/actions/buildLog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reflux = require('reflux'); 4 | 5 | module.exports = Reflux.createActions([ 6 | 'getTail', 7 | 'getLines' 8 | ]); 9 | 10 | -------------------------------------------------------------------------------- /app/actions/project.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reflux = require('reflux'); 4 | 5 | module.exports = Reflux.createActions([ 6 | 'run', 7 | 'readAll', 8 | 'read' 9 | ]); 10 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | ReactDOM = require('react-dom'), 5 | App = require('./components/app'), 6 | RootView = require('./components/root'), 7 | ProjectView = require('./components/projects/view'), 8 | ProjectRunForm = require('./components/projects/runForm'), 9 | BuildView = require('./components/builds/view'), 10 | connect = require('./connect'), 11 | Router = require('react-router'), 12 | Route = React.createFactory(Router.Route); 13 | 14 | var routes = ( 15 | Route( 16 | {handler: App}, 17 | Route({ 18 | name: 'projectRunForm', 19 | path: 'projects/run', 20 | handler: ProjectRunForm 21 | }), 22 | Route({name: 'root', path: '/', handler: RootView}), 23 | Route({name: 'project', path: 'projects/:name', handler: ProjectView}), 24 | Route({name: 'build', path: 'builds/:id', handler: BuildView}) 25 | ) 26 | ); 27 | 28 | connect.io.on('connect', function() { 29 | Router.run(routes, Router.HistoryLocation, function(Handler) { 30 | ReactDOM.render( 31 | React.createElement(Handler), 32 | document.getElementById('content') 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /app/components/app/index.jade: -------------------------------------------------------------------------------- 1 | div 2 | .container-fluid 3 | .page-wrapper 4 | RouteHandler() 5 | -------------------------------------------------------------------------------- /app/components/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | Router = require('react-router'), 5 | template = require('./index.jade'); 6 | 7 | var Component = React.createClass({ 8 | render: function() { 9 | return template({ 10 | Link: Router.Link, 11 | RouteHandler: Router.RouteHandler 12 | }); 13 | } 14 | }); 15 | 16 | module.exports = Component; 17 | -------------------------------------------------------------------------------- /app/components/builds/item/index.jade: -------------------------------------------------------------------------------- 1 | mixin statusText(build) 2 | if build.status === 'in-progress' 3 | span in progress 4 | 5 | if build.status === 'queued' 6 | span queued 7 | 8 | if build.status === 'done' 9 | span done 10 | 11 | if build.status === 'error' 12 | span error 13 | 14 | if build.status === 'canceled' 15 | span canceled 16 | 17 | - var build = this.props.build; 18 | 19 | .builds_item(class="builds_item__#{build.status}") 20 | .builds_inner 21 | .row 22 | .builds_header 23 | if build.number 24 | span(style={fontSize: '15px', color: '#a6a6a6'}) Execution 25 | | 26 | if build.status !== 'queued' 27 | Link(to="build", params={id: build.id}) 28 | span # 29 | span= build.number 30 | else 31 | span # 32 | span= build.number 33 | 34 | if build.waitReason 35 | span ( 36 | span= build.waitReason 37 | span , waiting) 38 | 39 | if build.status === 'in-progress' && build.currentStep 40 | span ( 41 | span= build.currentStep 42 | span ) 43 | 44 | .builds_controls 45 | if build.status === 'in-progress' 46 | .builds_progress 47 | if build.project.avgBuildDuration 48 | Progress(build=build) 49 | 50 | if build.status === 'queued' || build.status === 'in-progress' 51 | .builds_buttons 52 | a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onCancelBuild(build.id)) 53 | i.fa.fa-fw.fa-times(title="Cancel build") 54 | | 55 | | Cancel build 56 | 57 | .builds_content 58 | if build.endDate 59 | span.builds_info 60 | i.fa.fa-fw.fa-clock-o 61 | | finished 62 | DateTime(value=build.endDate) 63 | | , took 64 | | 65 | Duration(value=(build.endDate - build.startDate)) 66 | else 67 | if build.startDate 68 | span.builds_info 69 | i.fa.fa-fw.fa-clock-o 70 | | started 71 | DateTime(value=build.startDate) 72 | else 73 | span.builds_info 74 | i.fa.fa-fw.fa-clock-o 75 | | queued 76 | DateTime(value=build.createDate) 77 | | 78 | if build.scm 79 | span.builds_info 80 | i.fa.fa-fw.fa-comment-o 81 | | 82 | span= utils.prune(build.scm.rev.comment, 40) 83 | -------------------------------------------------------------------------------- /app/components/builds/item/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | React = require('react'), 5 | Router = require('react-router'), 6 | ProjectActions = require('../../../actions/project'), 7 | BuildActions = require('../../../actions/build'), 8 | CommonComponents = require('../../common'), 9 | utils = require('../../../utils'), 10 | template = require('./index.jade'); 11 | 12 | var Component = React.createClass({ 13 | onRebuildProject: function(projectName) { 14 | ProjectActions.run(projectName); 15 | }, 16 | onCancelBuild: function(buildId) { 17 | BuildActions.cancel(buildId); 18 | }, 19 | render: template.locals(_({ 20 | Link: Router.Link, 21 | utils: utils 22 | }).extend(CommonComponents)) 23 | }); 24 | 25 | module.exports = Component; 26 | -------------------------------------------------------------------------------- /app/components/builds/list/index.jade: -------------------------------------------------------------------------------- 1 | - var itemsCount = this.state.items.length; 2 | 3 | if itemsCount 4 | .builds.builds__timeline.builds__timeline-large(class="builds__timeline-#{itemsCount % 2 ? 'left' : 'right'}") 5 | each build, index in this.state.items 6 | Item(build=build, key=build.id) 7 | else 8 | p Build history is empty 9 | 10 | if itemsCount && itemsCount % 20 === 0 11 | .text-center 12 | a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onShowMoreBuilds(this.props.projectName)) 13 | i.fa.fa-fw.fa-refresh(title="Show more builds") 14 | | 15 | | Show more builds 16 | -------------------------------------------------------------------------------- /app/components/builds/list/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | React = require('react'), 5 | Reflux = require('reflux'), 6 | Item = require('../item'), 7 | BuildActions = require('../../../actions/build'), 8 | buildsStore = require('../../../stores/builds'), 9 | template = require('./index.jade'); 10 | 11 | var Component = React.createClass({ 12 | mixins: [ 13 | Reflux.connectFilter(buildsStore, 'items', function(items) { 14 | var projectName = this.props.projectName; 15 | if (projectName) { 16 | return _(items).filter(function(item) { 17 | return item.project && item.project.name === projectName; 18 | }); 19 | } else { 20 | return items; 21 | } 22 | }) 23 | ], 24 | onShowMoreBuilds: function(projectName) { 25 | BuildActions.readAll({ 26 | projectName: projectName, 27 | limit: this.state.items.length + 20 28 | }); 29 | }, 30 | render: template.locals({ 31 | Item: Item 32 | }) 33 | }); 34 | 35 | module.exports = Component; 36 | -------------------------------------------------------------------------------- /app/components/builds/view/index.jade: -------------------------------------------------------------------------------- 1 | mixin statusBadge(build) 2 | if build.status === 'in-progress' 3 | span.label.label-info in progress 4 | 5 | if build.status === 'queued' 6 | span.label.label-default queued 7 | 8 | if build.status === 'done' 9 | span.label.label.sm.label-success done 10 | 11 | if build.status === 'error' 12 | span.label.label-danger error 13 | 14 | if build.status === 'canceled' 15 | - var canceledBy = this.state.build.canceledBy; 16 | span.label.label-warning(title="canceled by #{canceledBy.type}") canceled 17 | 18 | if this.state.build 19 | .row 20 | .col-sm-3.hidden-xs 21 | BuildSidebar(projectName=this.state.build.project.name, currentBuild=this.state.build) 22 | 23 | .col-sm-9 24 | h1.page-header 25 | .pull-right(style={fontSize: '22px'}) 26 | mixin statusBadge(this.state.build) 27 | 28 | if this.state.project.archived 29 | i(title="Project is archived").fa.fa-fw.fa-archive 30 | else 31 | Scm(scm=this.state.build.project.scm.type) 32 | 33 | span= this.state.build.project.name 34 | | 35 | span execution # 36 | span= this.state.build.number 37 | 38 | p 39 | 40 | .small.text-muted 41 | - var node = this.state.build.node; 42 | if node 43 | | On 44 | | 45 | span= node.type 46 | | 47 | | node 48 | if node.type !== node.name 49 | | 50 | | " 51 | span= node.name 52 | | " 53 | - var env = this.state.build.env; 54 | if env 55 | | , within " 56 | span= env.name 57 | | " environment 58 | | , initiated by 59 | | 60 | else 61 | | Initiated by 62 | | 63 | 64 | - var initiator = this.state.build.initiator; 65 | if initiator.type === 'build' 66 | Link(to="project", params={name: initiator.project.name}) 67 | span= initiator.project.name 68 | | 69 | | during the 70 | | 71 | Link(to="build", params={id: initiator.id}) 72 | span execution # 73 | span= initiator.number 74 | else 75 | span= initiator.type 76 | 77 | .build-view_info 78 | .row 79 | .col-md-12 80 | p 81 | i.fa.fa-fw.fa-clock-o 82 | span 83 | if this.state.build.startDate 84 | span Started 85 | DateTime(value=this.state.build.startDate) 86 | else 87 | span Queued 88 | DateTime(value=this.state.build.createDate) 89 | 90 | if this.state.build.endDate 91 | - var durationTitle = _(this.state.build.stepTimings).map(function(stepTiming) { 92 | - return stepTiming.name + ': ' + (stepTiming.duration / 1000).toFixed(1) + ' sec'; 93 | - }).join('\n'); 94 | | , finished 95 | | 96 | DateTime(value=this.state.build.endDate) 97 | | , took 98 | | 99 | Duration(value=(this.state.build.endDate - this.state.build.startDate), title=durationTitle) 100 | 101 | - var scm = this.state.build.scm; 102 | - var rev = scm && scm.rev; 103 | - var changes = scm && scm.changes; 104 | - var targetScmRev = this.state.build.project.scm.rev; 105 | .row 106 | .col-md-12 107 | p 108 | i.fa.fa-fw.fa-code-fork 109 | | Scm target is 110 | | 111 | span= targetScmRev 112 | | , 113 | | 114 | if changes 115 | if changes.length 116 | | changes: 117 | else 118 | | no changes, current revision is 119 | | 120 | | " 121 | RevisionsItem(revision=rev) 122 | | " by 123 | | 124 | = rev.author 125 | else if this.state.build.status === 'in-progress' 126 | | pulling scm changes... 127 | else 128 | | changes are not received 129 | 130 | if changes && changes.length 131 | RevisionsList(revisions=changes) 132 | 133 | //- some wrapper for error block to prevent collapsing of terminal 134 | div 135 | if this.state.build.error 136 | if this.state.build.error.message 137 | h3 138 | | Build 139 | | 140 | if this.state.build.currentStep 141 | | step " 142 | span= this.state.build.currentStep 143 | | " 144 | | 145 | | failed with error: 146 | .alert= this.state.build.error.message 147 | 148 | if this.state.build.error.stderr 149 | h3 stderr: 150 | .alert(style="whiteSpace: pre-wrap;")!= ansiUp.ansi_to_html(this.state.build.error.stderr) 151 | else if this.state.build.status === 'canceled' 152 | h3 153 | | Execution canceled 154 | | 155 | if this.state.build.currentStep 156 | | during step " 157 | span= this.state.build.currentStep 158 | | " 159 | 160 | - var buildParams = this.state.build.params || {}; 161 | - playbook = buildParams.playbook; 162 | 163 | if playbook 164 | h3 165 | i.fa.fa-fw.fa-sliders 166 | | 167 | | Execution parameters 168 | 169 | div 170 | | Playbook: 171 | | 172 | span= playbook.name 173 | 174 | div 175 | | Inventories: 176 | | 177 | span= playbook.inventoryNames.join(', ') 178 | 179 | if playbook.limit 180 | div 181 | | Limit: 182 | | 183 | span(style="wordBreak: break-all;")= playbook.limit 184 | 185 | if playbook.extraVars 186 | div 187 | | Extra vars: 188 | | 189 | span(style="wordBreak: break-all;")= playbook.extraVars 190 | 191 | p 192 | 193 | .row 194 | .col-sm-3 195 | - var runButtonClasses = []; 196 | - if (this.state.project.archived) { 197 | - runButtonClasses.push('disabled'); 198 | - } 199 | 200 | button.btn.btn-default.btn-block( 201 | title="Run again with same parameters", 202 | class=runButtonClasses, 203 | onClick=this.onRunAgain 204 | ) 205 | i.fa.fa-fw.fa-repeat 206 | | 207 | | Run again 208 | 209 | .col-sm-3 210 | button.btn.btn-success.btn-block( 211 | title="Run another project/playbook", 212 | onClick=this.onRunProject 213 | ) 214 | i.fa.fa-fw.fa-play 215 | | 216 | | Run another 217 | 218 | 219 | h3 220 | i.fa.fa-fw.fa-tasks 221 | | 222 | | Execution steps 223 | 224 | - var stepTimings = this.state.build.stepTimings || []; 225 | 226 | - var currentStep = this.state.build.currentStep; 227 | if !this.state.build.completed && currentStep 228 | //- prevent duplication of the same step 229 | - var lastStepTiming = stepTimings[stepTimings.length - 1]; 230 | if !lastStepTiming || lastStepTiming.name !== currentStep 231 | - stepTimings.push({name: currentStep}); 232 | 233 | if stepTimings 234 | each stepTiming, index in stepTimings 235 | div(key='step' + index) 236 | span= '* ' + stepTiming.name 237 | 238 | if !this.state.build.completed && stepTiming.name === currentStep 239 | | 240 | img(src="/images/preloader.gif", width="24", height="24") 241 | 242 | else if !this.state.build.completed 243 | img(src="/images/preloader.gif", width="32", height="32") 244 | 245 | p 246 | 247 | if !this.state.showConsole 248 | .row 249 | .col-sm-6 250 | button.btn.btn-default.btn-block(onClick=this.toggleConsole) 251 | i.fa.fa-fw.fa-terminal 252 | | 253 | | Show console output 254 | 255 | else 256 | p 257 | 258 | .text-center 259 | button.btn.btn-success(onClick=this.onRunProject) 260 | i.fa.fa-fw.fa-play 261 | | 262 | | Run another project/playbook 263 | 264 | if !this.state.showConsole 265 | p 266 | 267 | .text-center 268 | button.btn.btn-default(onClick=this.toggleConsole) 269 | i.fa.fa-fw.fa-terminal 270 | | 271 | | Show console output 272 | 273 | 274 | .build-view_terminal 275 | if this.state.showConsole 276 | h3 277 | i.fa.fa-fw.fa-terminal 278 | | 279 | | Console output 280 | Terminal(build=this.state.build.id, showPreloader=true) 281 | 282 | if this.state.build.completed 283 | if this.state.build.status === 'error' 284 | .text-center.alert.alert-danger 285 | | Execution ended with error, took 286 | | 287 | Duration(value=(this.state.build.endDate - this.state.build.startDate)) 288 | else if this.state.build.status === 'canceled' 289 | .text-center.alert.alert-warning 290 | | Execution canceled, took 291 | | 292 | Duration(value=(this.state.build.endDate - this.state.build.startDate)) 293 | else if this.state.build.status === 'done' 294 | .text-center.alert.alert-success 295 | | Execution successfully completed, took 296 | | 297 | Duration(value=(this.state.build.endDate - this.state.build.startDate)) 298 | 299 | .text-center 300 | button.btn.btn-default(onClick=this.toggleConsole) 301 | i.fa.fa-fw.fa-terminal 302 | | 303 | | Hide console output 304 | -------------------------------------------------------------------------------- /app/components/builds/view/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | React = require('react'), 5 | Router = require('react-router'), 6 | Reflux = require('reflux'), 7 | BuildActions = require('../../../actions/build'), 8 | ProjectActions = require('../../../actions/project'), 9 | buildStore = require('../../../stores/build'), 10 | projectStore = require('../../../stores/project'), 11 | Terminal = require('../../terminal'), 12 | BuildSidebar = require('./sidebar'), 13 | CommonComponents = require('../../common'), 14 | RevisionsItem = require('../../revisions/item'), 15 | RevisionsList = require('../../revisions/list'), 16 | template = require('./index.jade'), 17 | ansiUp = require('ansi_up'), 18 | scrollTop = require('simple-scrolltop'); 19 | 20 | var Component = React.createClass({ 21 | mixins: [Reflux.ListenerMixin, Router.Navigation], 22 | statics: { 23 | willTransitionTo: function(transition, params) { 24 | BuildActions.read(Number(params.id)); 25 | // load builds for sidebar 26 | BuildActions.readAll(); 27 | } 28 | }, 29 | 30 | componentDidMount: function() { 31 | this.listenTo(buildStore, this.updateBuild); 32 | this.listenTo(projectStore, this.updateProject); 33 | }, 34 | 35 | componentWillReceiveProps: function(nextProps) { 36 | // reset console status when go from build page to another build 37 | // page (did mount and mount not called in this case) 38 | if (Number(nextProps.params.id) !== this.state.build.id) { 39 | this.setState({showConsole: this.getInitialState().showConsole}); 40 | } 41 | }, 42 | 43 | updateBuild: function(build) { 44 | if (build) { 45 | // load project config 46 | if ( 47 | _(this.state.project.name).isEmpty() || 48 | this.state.project.name !== build.project.name 49 | ) { 50 | ProjectActions.read({name: build.project.name}); 51 | } 52 | } 53 | this.setState({build: build}); 54 | }, 55 | 56 | updateProject: function(project) { 57 | if (project.name === this.state.build.project.name) { 58 | this.setState({project: project}); 59 | } 60 | }, 61 | 62 | getInitialState: function() { 63 | return { 64 | build: null, 65 | project: {}, 66 | showConsole: false 67 | }; 68 | }, 69 | 70 | toggleConsole: function() { 71 | var consoleState = !this.state.showConsole; 72 | if (consoleState) { 73 | BuildActions.readTerminalOutput(this.state.build); 74 | } 75 | 76 | this.setState({showConsole: consoleState}); 77 | 78 | // scroll to page top after hiding console 79 | if (!consoleState) { 80 | scrollTop(0); 81 | } 82 | }, 83 | 84 | onRunAgain: function() { 85 | var build = this.state.build; 86 | ProjectActions.run(build.project.name, build.params); 87 | 88 | // TODO: go to last build in a durable way 89 | var self = this; 90 | setTimeout(function() { 91 | self.transitionTo('root'); 92 | }, 500); 93 | }, 94 | 95 | onRunProject: function() { 96 | this.transitionTo('projectRunForm'); 97 | }, 98 | 99 | render: template.locals(_({ 100 | Terminal: Terminal, 101 | RevisionsItem: RevisionsItem, 102 | RevisionsList: RevisionsList, 103 | Link: Router.Link, 104 | BuildSidebar: BuildSidebar, 105 | _: _, 106 | ansiUp: ansiUp 107 | }).extend(CommonComponents)) 108 | }); 109 | 110 | module.exports = Component; 111 | -------------------------------------------------------------------------------- /app/components/builds/view/sidebar/index.jade: -------------------------------------------------------------------------------- 1 | .builds.builds__timeline.builds__timeline-small 2 | each item in this.state.items 3 | - var buildItemClasses = ['builds_item__' + item.status]; 4 | - if (item.id === this.props.currentBuild.id) buildItemClasses.push('builds_item__current'); 5 | .builds_item(key=item.id, class=buildItemClasses) 6 | .builds_inner 7 | .row 8 | .builds_header 9 | div 10 | span= item.project.name 11 | 12 | if item.status === 'queued' 13 | span(title='wait reason: ' + item.waitReason) queued 14 | else 15 | Link(to="build", params={id: item.id}) 16 | span execution # 17 | span= item.number 18 | 19 | .builds_controls 20 | if item.status === 'in-progress' 21 | .builds_progress 22 | if item.project.avgBuildDuration 23 | Progress(build=item) 24 | 25 | if item.endDate 26 | div 27 | DateTime(value=item.endDate) 28 | div 29 | | took 30 | | 31 | Duration(value=(item.endDate - item.startDate)) 32 | 33 | if item.status === 'queued' || item.status === 'in-progress' 34 | .builds_buttons 35 | a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onCancelBuild(item.id)) 36 | i.fa.fa-fw.fa-times(title="Cancel execution") 37 | | 38 | | Cancel 39 | -------------------------------------------------------------------------------- /app/components/builds/view/sidebar/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | React = require('react'), 5 | Reflux = require('reflux'), 6 | Router = require('react-router'), 7 | buildsStore = require('../../../../stores/builds'), 8 | template = require('./index.jade'), 9 | CommonComponents = require('../../../common'), 10 | BuildActions = require('../../../../actions/build'); 11 | 12 | module.exports = React.createClass({ 13 | mixins: [ 14 | Reflux.connect(buildsStore, 'items') 15 | ], 16 | onCancelBuild: function(buildId) { 17 | BuildActions.cancel(buildId); 18 | }, 19 | render: template.locals(_({ 20 | Link: Router.Link 21 | }).extend(CommonComponents)) 22 | }); 23 | -------------------------------------------------------------------------------- /app/components/common/dateTime/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | template = require('./template.jade'), 5 | moment = require('moment'); 6 | 7 | var Component = React.createClass({ 8 | render: template.locals({ 9 | moment: moment 10 | }) 11 | }); 12 | 13 | module.exports = Component; 14 | -------------------------------------------------------------------------------- /app/components/common/dateTime/template.jade: -------------------------------------------------------------------------------- 1 | - var date = moment(this.props.value); 2 | span(title= date.format('YYYY-MM-DD HH:mm:ss'))= date.fromNow() 3 | -------------------------------------------------------------------------------- /app/components/common/duration/index.jade: -------------------------------------------------------------------------------- 1 | - var sec = this.props.value / 1000; 2 | - sec = sec >= 1 ? Math.round(sec) : Number(sec.toFixed(2)); 3 | - var min = sec >= 60 ? Math.round(sec / 60) : 0; 4 | - var suffix = this.props.withSuffix ? 'in ' : ''; 5 | - var ending = min === 1 ? '' : 's'; 6 | - var title = this.props.title || sec + ' second' + ending; 7 | pan(title= title)= suffix + (min ? min + ' minute' : sec + ' second') + ending 8 | -------------------------------------------------------------------------------- /app/components/common/duration/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | template = require('./index.jade'), 5 | moment = require('moment'); 6 | 7 | var Component = React.createClass({ 8 | render: template.locals({ 9 | moment: moment 10 | }) 11 | }); 12 | 13 | module.exports = Component; 14 | -------------------------------------------------------------------------------- /app/components/common/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DateTime = require('./dateTime'), 4 | Duration = require('./duration'), 5 | Progress = require('./progress'), 6 | Scm = require('./scm'); 7 | 8 | module.exports = { 9 | DateTime: DateTime, 10 | Scm: Scm, 11 | Duration: Duration, 12 | Progress: Progress 13 | }; 14 | -------------------------------------------------------------------------------- /app/components/common/progress/index.jade: -------------------------------------------------------------------------------- 1 | - var completedPercent = Math.round(this.state.duration / this.state.avgDuration * 100); 2 | - var remainingTime = Math.round((this.state.avgDuration - this.state.duration) / 1000); 3 | - remainingTime = remainingTime > 0 ? remainingTime : 0; 4 | .progress(title= remainingTime ? 'Estimated remaining time: ' + remainingTime + ' sec' : '') 5 | .progress-bar.progress-bar-success.progress-bar-striped.active(style={width: completedPercent + '%'}) 6 | -------------------------------------------------------------------------------- /app/components/common/progress/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | _ = require('underscore'), 5 | template = require('./index.jade'); 6 | 7 | module.exports = React.createClass({ 8 | render: template, 9 | componentDidMount: function() { 10 | var self = this; 11 | var updateCallback = function() { 12 | if (self.props.build.status === 'in-progress') { 13 | if (self.isMounted()) { 14 | self.setState({ 15 | duration: Date.now() - self.props.build.startDate, 16 | avgDuration: self.props.build.project.avgBuildDuration 17 | }); 18 | _.delay(updateCallback, 100); 19 | } 20 | } 21 | }; 22 | 23 | updateCallback(); 24 | }, 25 | getInitialState: function() { 26 | return {}; 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /app/components/common/scm/index.jade: -------------------------------------------------------------------------------- 1 | if this.props.scm 2 | if this.props.scm === 'mercurial' 3 | i.si.si-fw.si-hg 4 | else 5 | i.si.si-fw.si-git 6 | -------------------------------------------------------------------------------- /app/components/common/scm/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | template = require('./index.jade'); 5 | 6 | module.exports = React.createClass({ 7 | render: template 8 | }); 9 | 10 | -------------------------------------------------------------------------------- /app/components/projects/header/index.jade: -------------------------------------------------------------------------------- 1 | h1.page-header.clearfix 2 | .pull-right 3 | 4 | - var scmRev = this.props.project.scm ? this.props.project.scm.rev : ''; 5 | 6 | - var formTitle; 7 | - if (this.props.project.archived) { 8 | - formTitle = 'Archived project can not be run'; 9 | - } 10 | 11 | .form-inline(title=formTitle) 12 | select.form-control( 13 | style="width: 250px;", 14 | onChange=this.onScmBranchChange, 15 | disabled=this.props.project.archived 16 | ) 17 | option(value=scmRev)= scmRev 18 | option(value=-1) Custom revision 19 | 20 | if this.state.scmBranch === '-1' 21 | | 22 | input.form-control( 23 | value= this.state.scmRev, 24 | onChange=this.onScmRevChange, 25 | style="width: 250px;" 26 | ) 27 | 28 | - var buildButtonClasses = []; 29 | - if (this.props.project.archived) { 30 | - buildButtonClasses.push('disabled'); 31 | - } 32 | 33 | if this.props.project.name 34 | | 35 | button.btn.btn-sm.btn-success( 36 | onClick=this.onBuildProject, 37 | class=buildButtonClasses 38 | ) 39 | i.fa.fa-fw.fa-play 40 | | 41 | span Build 42 | 43 | div 44 | if this.props.project.archived 45 | i(title="Project is archived").fa.fa-fw.fa-archive 46 | else 47 | Scm(scm=this.props.project.scm ? this.props.project.scm.type : '') 48 | span= this.props.project.name 49 | -------------------------------------------------------------------------------- /app/components/projects/header/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | ProjectActions = require('../../../actions/project'), 5 | template = require('./index.jade'), 6 | CommonComponents = require('../../common'); 7 | 8 | require('../../../stores/projects'); 9 | 10 | module.exports = React.createClass({ 11 | getInitialState: function() { 12 | return {}; 13 | }, 14 | onScmBranchChange: function(event) { 15 | this.setState({scmBranch: event.target.value}); 16 | }, 17 | onScmRevChange: function(event) { 18 | this.setState({scmRev: event.target.value}); 19 | }, 20 | onBuildProject: function() { 21 | if (this.props.project.name) { 22 | var buildParams = {}; 23 | 24 | var scmBranch = this.state.scmBranch, 25 | scmRev = scmBranch === '-1' ? this.state.scmRev : scmBranch, 26 | projectScmRev = ( 27 | this.props.project.scm ? this.props.project.scm.rev : '' 28 | ); 29 | 30 | if (scmRev && scmRev !== projectScmRev) { 31 | buildParams.scmRev = scmRev; 32 | } 33 | 34 | ProjectActions.run(this.props.project.name, buildParams); 35 | } 36 | }, 37 | render: template.locals({ 38 | Scm: CommonComponents.Scm 39 | }) 40 | }); 41 | -------------------------------------------------------------------------------- /app/components/projects/runForm/index.jade: -------------------------------------------------------------------------------- 1 | div 2 | 3 | .form-horizontal 4 | - var projects = this.state.projects; 5 | - var projectName = this.state.projectName; 6 | - var project, playbook, inventoryNames; 7 | 8 | if projects 9 | .form-group 10 | label.col-md-offset-2.col-md-2.control-label(for="project-name") Project 11 | .col-md-4 12 | select#project-name.form-control( 13 | onChange=this.onProjectNameChange, 14 | value= projectName 15 | ) 16 | option(value="", key="notSet") - select project - 17 | each project in projects 18 | option(value= project.name, key= project.name)= project.name 19 | 20 | - project = _(projects).findWhere({name: projectName}); 21 | 22 | if project 23 | .form-group 24 | label.col-md-offset-2.col-md-2.control-label(for="scm-branch") Branch 25 | .col-md-4 26 | 27 | - var scmRev = project && project.scm ? project.scm.rev : ''; 28 | select#scm-branch.form-control( 29 | onChange=this.onScmBranchChange 30 | ) 31 | option(value=scmRev)= scmRev 32 | option(value=-1) Custom revision 33 | 34 | - var scmBranch = this.state.scmBranch; 35 | if scmBranch === '-1' 36 | | 37 | input.form-control( 38 | value= this.state.scmRev, 39 | onChange=this.onScmRevChange 40 | ) 41 | 42 | if project && project.playbooks && project.playbooks.length 43 | .form-group 44 | label.col-md-offset-2.col-md-2.control-label(for="playbook-name") Playbook 45 | .col-md-4 46 | select#playbook-name.form-control(onChange=this.onPlaybookNameChange) 47 | option(value="", key="notSet") - select playbook - 48 | each playbook in project.playbooks 49 | option(value= playbook.name, key= playbook.name)= playbook.name 50 | 51 | 52 | - var playbookName = this.state.playbookName; 53 | - playbook = _(project.playbooks).findWhere({name: playbookName}); 54 | 55 | if playbookName 56 | - inventoryNames = this.state.inventoryNames; 57 | - var checkedInventoriesCount = 0; 58 | .form-group 59 | label.col-md-offset-2.col-md-2.control-label Inventories 60 | .col-md-4 61 | .row 62 | each inventory in playbook.inventories 63 | .col-md-3(key= 'column-for-' + inventory.name) 64 | label(key= 'label-for-' + inventory.name) 65 | - var checked = _(inventoryNames).contains(inventory.name); 66 | - if (checked) checkedInventoriesCount++; 67 | input( 68 | type="checkbox", 69 | value= inventory.name, 70 | key= inventory.name, 71 | onClick=this.onInventoryNamesChange, 72 | checked= checked 73 | ) 74 | = inventory.name 75 | .row 76 | .col-md-12.text-right 77 | if checkedInventoriesCount === playbook.inventories.length 78 | a( 79 | href="javascript:void(0);", 80 | onClick=this.onUnselectAllInventoryNames 81 | ) Unselect all inventories 82 | else 83 | a( 84 | href="javascript:void(0);", 85 | onClick=this.onSelectAllInventoryNames 86 | ) select all inventories 87 | 88 | .form-group 89 | label.col-md-offset-2.col-md-2.control-label( 90 | for="limit", 91 | title="further limit selected hosts to an additional pattern" 92 | ) Limit 93 | .col-md-4 94 | input#limit.form-control( 95 | type="text", 96 | value= this.state.limit 97 | onChange=this.onLimitChange 98 | ) 99 | 100 | .form-group 101 | label.col-md-offset-2.col-md-2.control-label( 102 | for="extra-vars", 103 | title="set additional variables as key=value (e.g. a=123 b=\"some string\") or YAML/JSON (e.g. {a: 123, b: \"some string\"})" 104 | ) Extra vars 105 | .col-md-4 106 | input#extra-vars.form-control( 107 | type="text", 108 | value= this.state.extraVars 109 | onChange=this.onExtraVarsChange 110 | ) 111 | 112 | .form-group 113 | .col-md-offset-4.col-md-2 114 | button.btn.btn-md.btn-default.btn-block(onClick=this.onCancel) 115 | i.fa.fa-fw.fa-ban 116 | | 117 | span Cancel 118 | 119 | .col-md-2 120 | - var buttonClasses = []; 121 | 122 | - var isValidForm; 123 | - if (project && !project.playbooks) { 124 | - isValidForm = true; 125 | - } else if (project && project.playbooks) { 126 | - isValidForm = playbook && inventoryNames && inventoryNames.length; 127 | - } 128 | 129 | - if (!isValidForm) buttonClasses.push('disabled'); 130 | 131 | button.btn.btn-md.btn-success.btn-block(onClick=this.onRunProject, class= buttonClasses) 132 | i.fa.fa-fw.fa-play 133 | | 134 | span Run 135 | 136 | -------------------------------------------------------------------------------- /app/components/projects/runForm/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | Reflux = require('reflux'), 5 | Router = require('react-router'), 6 | ProjectActions = require('../../../actions/project'), 7 | projectsStore = require('../../../stores/projects'), 8 | template = require('./index.jade'), 9 | _ = require('underscore'); 10 | 11 | require('../../../stores/projects'); 12 | 13 | module.exports = React.createClass({ 14 | mixins: [ 15 | Reflux.connect(projectsStore, 'projects'), 16 | Router.Navigation 17 | ], 18 | statics: { 19 | willTransitionTo: function() { 20 | ProjectActions.readAll(); 21 | } 22 | }, 23 | onProjectNameChange: function(event) { 24 | this.setState({ 25 | projectName: event.target.value, 26 | scmRev: '', 27 | playbookName: '', 28 | inventoryNames: [], 29 | limit: '', 30 | extraVars: '' 31 | }); 32 | }, 33 | onScmBranchChange: function(event) { 34 | this.setState({scmBranch: event.target.value}); 35 | }, 36 | onScmRevChange: function(event) { 37 | this.setState({scmRev: event.target.value}); 38 | }, 39 | onPlaybookNameChange: function(event) { 40 | this.setState({ 41 | playbookName: event.target.value, 42 | inventoryNames: [], 43 | limit: '', 44 | extraVars: '' 45 | }); 46 | }, 47 | onInventoryNamesChange: function(event) { 48 | var input = event.target, 49 | inventoryName = input.value; 50 | 51 | var inventoryNames = this.state.inventoryNames || []; 52 | 53 | if (input.checked) { 54 | inventoryNames.push(inventoryName); 55 | } else { 56 | var index = inventoryNames.indexOf(inventoryName); 57 | if (index !== -1) { 58 | inventoryNames.splice(index, 1); 59 | } 60 | } 61 | 62 | this.setState({inventoryNames: inventoryNames}); 63 | }, 64 | onUnselectAllInventoryNames: function() { 65 | this.setState({inventoryNames: []}); 66 | }, 67 | onSelectAllInventoryNames: function() { 68 | var project = _(this.state.projects).findWhere({ 69 | name: this.state.projectName 70 | }); 71 | var playbook = _(project.playbooks).findWhere({ 72 | name: this.state.playbookName 73 | }); 74 | this.setState({inventoryNames: _(playbook.inventories).pluck('name')}); 75 | }, 76 | onLimitChange: function(event) { 77 | this.setState({limit: event.target.value}); 78 | }, 79 | onExtraVarsChange: function(event) { 80 | this.setState({extraVars: event.target.value}); 81 | }, 82 | onCancel: function() { 83 | this.transitionTo('root'); 84 | }, 85 | onRunProject: function() { 86 | var buildParams = {}; 87 | 88 | if (this.state.playbookName) { 89 | buildParams.playbook = { 90 | name: this.state.playbookName, 91 | inventoryNames: this.state.inventoryNames 92 | }; 93 | 94 | if (this.state.limit) { 95 | buildParams.playbook.limit = this.state.limit; 96 | } 97 | 98 | if (this.state.extraVars) { 99 | buildParams.playbook.extraVars = this.state.extraVars; 100 | } 101 | } 102 | 103 | var project = _(this.state.projects).findWhere({ 104 | name: this.state.projectName 105 | }); 106 | 107 | var scmBranch = this.state.scmBranch, 108 | scmRev = scmBranch === '-1' ? this.state.scmRev : scmBranch, 109 | projectScmRev = project.scm ? project.scm.rev : ''; 110 | 111 | if (scmRev && scmRev !== projectScmRev) { 112 | buildParams.scmRev = scmRev; 113 | } 114 | 115 | ProjectActions.run(project.name, buildParams); 116 | 117 | // TODO: go to last build in a durable way 118 | var self = this; 119 | setTimeout(function() { 120 | self.transitionTo('root'); 121 | }, 500); 122 | }, 123 | render: template.locals({ 124 | _: _ 125 | }) 126 | }); 127 | -------------------------------------------------------------------------------- /app/components/projects/selector/index.jade: -------------------------------------------------------------------------------- 1 | .projects-selector(href="javascript:void(0);") 2 | if !this.state.showSearch 3 | span.projects-selector_preview(onClick=this.onSearchProject) 4 | i.fa.fa-fw.fa-bars 5 | span.projects-selector_preview_text Select a project... 6 | else 7 | input.projects-selector_input( 8 | type="text", 9 | value=this.state.searchQuery, 10 | onChange=this.onSearchChange, 11 | ref=this.onInputMount, 12 | onBlur=this.onBlurSearch 13 | ) 14 | ul.projects-selector_items 15 | each project in this.state.projects 16 | li.projects-selector_item(key=project.name) 17 | Link.projects-selector_item_link(to="project", params={name: project.name}, onMouseDown=this.onSelectProject(project.name)) 18 | Scm(scm=project.scm.type) 19 | span 20 | span= project.name 21 | a.projects-selector_item_run(href="javascript:void(0);", onMouseDown=this.onRunProject(project.name)) 22 | i.fa.fa-fw.fa-play 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/components/projects/selector/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | ReactDOM = require('react-dom'), 5 | Router = require('react-router'), 6 | Reflux = require('reflux'), 7 | CommonComponents = require('../../common'), 8 | ProjectActions = require('../../../actions/project'), 9 | projectsStore = require('../../../stores/projects'), 10 | template = require('./index.jade'); 11 | 12 | module.exports = React.createClass({ 13 | mixins: [Reflux.ListenerMixin, Router.Navigation], 14 | componentDidMount: function() { 15 | this.listenTo(projectsStore, this.updateItems); 16 | }, 17 | getInitialState: function() { 18 | return { 19 | showSearch: false 20 | }; 21 | }, 22 | onRunProject: function(projectName) { 23 | ProjectActions.run(projectName); 24 | this.setState({showSearch: false}); 25 | }, 26 | onSelectProject: function(name) { 27 | this.transitionTo('project', {name: name}); 28 | }, 29 | updateItems: function(projects) { 30 | this.setState({projects: projects}); 31 | }, 32 | onSearchProject: function() { 33 | this.setState({showSearch: true}); 34 | }, 35 | onInputMount: function(component) { 36 | var node = ReactDOM.findDOMNode(component); 37 | if (node) { 38 | node.focus(); 39 | } 40 | }, 41 | onBlurSearch: function() { 42 | this.setState({showSearch: false}); 43 | }, 44 | onSearchChange: function(event) { 45 | var query = event.target.value; 46 | this.setState({searchQuery: query}); 47 | ProjectActions.readAll({nameQuery: query}); 48 | }, 49 | render: template.locals({ 50 | Link: Router.Link, 51 | Scm: CommonComponents.Scm 52 | }) 53 | }); 54 | -------------------------------------------------------------------------------- /app/components/projects/view/index.jade: -------------------------------------------------------------------------------- 1 | div 2 | ProjectHeader(project=this.state.project) 3 | 4 | div.text-muted 5 | - var lastDoneBuild = this.state.project.lastDoneBuild; 6 | p Last successfully built: 7 | if lastDoneBuild 8 | DateTime(value=lastDoneBuild.endDate) 9 | | 10 | | (build # 11 | span= lastDoneBuild.number 12 | | ) 13 | else 14 | | - 15 | 16 | p Current successfully streak: 17 | if lastDoneBuild 18 | span= this.state.project.doneBuildsStreak.buildsCount 19 | else 20 | | - 21 | 22 | p Last build duration: 23 | if lastDoneBuild 24 | Duration(value=(lastDoneBuild.endDate - lastDoneBuild.startDate)) 25 | else 26 | | - 27 | 28 | p Average build duration: 29 | if this.state.project.avgBuildDuration 30 | Duration(value=this.state.project.avgBuildDuration) 31 | else 32 | | - 33 | 34 | h2 35 | i.fa.fa-fw.fa-history 36 | span 37 | span Execution history 38 | 39 | Builds(projectName=this.props.params.name) 40 | -------------------------------------------------------------------------------- /app/components/projects/view/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ProjectHeader = require('../header'), 4 | React = require('react'), 5 | Reflux = require('reflux'), 6 | ProjectActions = require('../../../actions/project'), 7 | BuildActions = require('../../../actions/build'), 8 | projectStore = require('../../../stores/project'), 9 | Builds = require('../../builds/list'), 10 | CommonComponents = require('../../common'), 11 | template = require('./index.jade'); 12 | 13 | module.exports = React.createClass({ 14 | mixins: [ 15 | Reflux.connectFilter(projectStore, 'project', function(project) { 16 | if (project.name === this.props.params.name) { 17 | return project; 18 | } else { 19 | if (this.state) { 20 | return this.state.project; 21 | } else { 22 | return projectStore.getInitialState(); 23 | } 24 | } 25 | }) 26 | ], 27 | statics: { 28 | willTransitionTo: function(transition, params) { 29 | ProjectActions.read({name: params.name}); 30 | BuildActions.readAll({projectName: params.name}); 31 | } 32 | }, 33 | render: template.locals({ 34 | ProjectHeader: ProjectHeader, 35 | Builds: Builds, 36 | DateTime: CommonComponents.DateTime, 37 | Duration: CommonComponents.Duration 38 | }) 39 | }); 40 | -------------------------------------------------------------------------------- /app/components/revisions/item/index.jade: -------------------------------------------------------------------------------- 1 | 2 | - var revision = this.props.revision; 3 | span(title= 'Revision: ' + revision.id + ' by ' + revision.author)= revision.comment 4 | -------------------------------------------------------------------------------- /app/components/revisions/item/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | template = require('./index.jade'); 5 | 6 | module.exports = React.createClass({ 7 | render: template.locals({}) 8 | }); 9 | -------------------------------------------------------------------------------- /app/components/revisions/list/index.jade: -------------------------------------------------------------------------------- 1 | 2 | - var limit = this.state.limit; 3 | - var allRevisions = this.props.revisions; 4 | - var revisions = allRevisions.slice(0, limit); 5 | 6 | each revision, index in revisions 7 | - var number = index + 1; 8 | .row(key= 'scm-revision-' + number) 9 | .col-md-offset-4.col-md-8 10 | p 11 | span= String(number) + '. ' 12 | | 13 | RevisionsItem(revision=revision) 14 | 15 | if revisions.length < allRevisions.length 16 | .row 17 | .col-md-offset-4.col-md-8 18 | a(href="javascript:void(0);", onClick=this.onShowMoreRevisions(null)) 19 | i.fa.fa-fw.fa-refresh(title="Show more builds") 20 | | show more revisions 21 | | 22 | | ( 23 | span= revisions.length 24 | | 25 | | from 26 | | 27 | span= allRevisions.length 28 | | 29 | | are shown) 30 | -------------------------------------------------------------------------------- /app/components/revisions/list/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | template = require('./index.jade'), 5 | RevisionsItem = require('../../revisions/item'); 6 | 7 | module.exports = React.createClass({ 8 | onShowMoreRevisions: function() { 9 | this.setState({ 10 | limit: this.state.limit + this.getInitialState().limit 11 | }); 12 | }, 13 | componentWillReceiveProps: function(nextProps) { 14 | // reset limit when go from build page to another build 15 | // page (did mount and mount not called in this case) 16 | if (nextProps.revisions.length !== this.props.revisions.length) { 17 | this.setState({limit: this.getInitialState().limit}); 18 | } 19 | }, 20 | getInitialState: function() { 21 | return {limit: 20}; 22 | }, 23 | render: template.locals({ 24 | RevisionsItem: RevisionsItem 25 | }) 26 | }); 27 | -------------------------------------------------------------------------------- /app/components/root/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | Router = require('react-router'), 5 | Reflux = require('reflux'), 6 | BuildActions = require('../../actions/build'), 7 | buildsStore = require('../../stores/builds'); 8 | 9 | module.exports = React.createClass({ 10 | mixins: [Reflux.ListenerMixin, Router.Navigation], 11 | componentDidMount: function() { 12 | this.listenTo(buildsStore, this.navigateToBuild); 13 | BuildActions.readAll({limit: 1}); 14 | }, 15 | navigateToBuild: function(builds) { 16 | if (builds.length) { 17 | this.transitionTo('build', {id: builds[0].id}); 18 | } else { 19 | this.transitionTo('projectRunForm'); 20 | } 21 | }, 22 | // dummy render method, coz no view needed 23 | render: function() { 24 | return React.createElement('div'); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /app/components/terminal/index.jade: -------------------------------------------------------------------------------- 1 | .terminal 2 | pre.terminal_code 3 | 4 | .text-center.js-terminal-preloader(style="display: none") 5 | img(src="/images/preloader.gif", width="48", height="48") 6 | 7 | p 8 | -------------------------------------------------------------------------------- /app/components/terminal/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | React = require('react'), 5 | Reflux = require('reflux'), 6 | buildStore = require('../../stores/build'), 7 | terminalStore = require('../../stores/terminal'), 8 | ansiUp = require('ansi_up'), 9 | utils = require('../../utils'), 10 | template = require('./index.jade'), 11 | scrollTop = require('simple-scrolltop'); 12 | 13 | var Component = React.createClass({ 14 | mixins: [Reflux.ListenerMixin], 15 | 16 | shouldScrollBottom: true, 17 | data: [], 18 | linesCount: 0, 19 | 20 | componentDidMount: function() { 21 | this.listenTo(terminalStore, this.updateItems); 22 | this.initialScrollPosition = 120; 23 | if (this.props.showPreloader) { 24 | this.setPreloaderDisplay(true); 25 | 26 | this.listenTo(buildStore, function(build) { 27 | if (build.completed) { 28 | this.setPreloaderDisplay(false); 29 | } 30 | }); 31 | } 32 | 33 | window.onscroll = this.onScroll; 34 | }, 35 | setPreloaderDisplay: function(show) { 36 | var preloader = document.getElementsByClassName('js-terminal-preloader')[0]; 37 | if (show) { 38 | preloader.style.display = 'block'; 39 | } else { 40 | preloader.style.display = 'none'; 41 | } 42 | }, 43 | componentWillUnmount: function() { 44 | window.onscroll = null; 45 | }, 46 | prepareRow: function(row) { 47 | return ansiUp.ansi_to_html(utils.escapeHtml(row.replace('\r', ''))); 48 | }, 49 | prepareOutput: function(output) { 50 | var self = this; 51 | return output.map(function(row) { 52 | return self.prepareRow(row); 53 | }); 54 | }, 55 | getTerminal: function() { 56 | return document.getElementsByClassName('terminal')[0]; 57 | }, 58 | onScroll: function() { 59 | var node = this.getTerminal(); 60 | 61 | this.shouldScrollBottom = window.innerHeight + scrollTop() >= 62 | node.offsetHeight + this.initialScrollPosition; 63 | }, 64 | ensureScrollPosition: function() { 65 | if (this.shouldScrollBottom) { 66 | var node = this.getTerminal(); 67 | 68 | scrollTop(this.initialScrollPosition + node.offsetHeight); 69 | } 70 | }, 71 | makeCodeLineContent: function(line) { 72 | return '' + '' + 73 | '
' + this.prepareRow(line) + '
'; 74 | }, 75 | makeCodeLine: function(line, index) { 76 | return '
' + 77 | this.makeCodeLineContent(line) + '
'; 78 | }, 79 | renderBuffer: _.throttle(function() { 80 | var data = this.data, 81 | currentLinesCount = data.length, 82 | terminal = document.getElementsByClassName('terminal_code')[0], 83 | rows = terminal.childNodes; 84 | 85 | if (rows.length) { 86 | // replace our last node 87 | var index = this.linesCount - 1; 88 | rows[index].innerHTML = this.makeCodeLineContent(data[index]); 89 | } 90 | 91 | var self = this; 92 | terminal.insertAdjacentHTML('beforeend', 93 | _(data.slice(this.linesCount)).map(function(line, index) { 94 | return self.makeCodeLine(line, self.linesCount + index); 95 | }).join('') 96 | ); 97 | 98 | this.linesCount = currentLinesCount; 99 | this.ensureScrollPosition(); 100 | }, 100), 101 | updateItems: function(build) { 102 | // listen just our console update 103 | if (build.buildId === this.props.build) { 104 | this.data = build.data; 105 | this.renderBuffer(); 106 | } 107 | if (this.props.showPreloader && build.buildCompleted) { 108 | this.setPreloaderDisplay(false); 109 | } 110 | }, 111 | shouldComponentUpdate: function() { 112 | return false; 113 | }, 114 | render: template 115 | }); 116 | 117 | module.exports = Component; 118 | -------------------------------------------------------------------------------- /app/connect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var socketio = require('socket.io-client'), 4 | dataio = require('data.io/data.io'), 5 | io = socketio(), 6 | data = dataio(io); 7 | 8 | /* 9 | * Extend Resource 10 | */ 11 | var resource = data.resource('__someResource__'), 12 | resourcePrototype = Object.getPrototypeOf(resource); 13 | 14 | resourcePrototype.disconnect = function() { 15 | this.socket.disconnect(); 16 | this.socket.removeAllListeners(); 17 | }; 18 | 19 | resourcePrototype.connect = function() { 20 | this.socket.connect(); 21 | }; 22 | 23 | resourcePrototype.reconnect = function() { 24 | this.disconnect(); 25 | this.connect(); 26 | }; 27 | 28 | module.exports.io = io; 29 | module.exports.data = data; 30 | -------------------------------------------------------------------------------- /app/resources.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var connect = require('./connect'), 4 | projects = connect.data.resource('projects'), 5 | builds = connect.data.resource('builds'); 6 | 7 | module.exports = { 8 | projects: projects, 9 | builds: builds 10 | }; 11 | -------------------------------------------------------------------------------- /app/stores/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | Reflux = require('reflux'), 5 | BuildActions = require('../actions/build'), 6 | resources = require('../resources'), 7 | resource = resources.builds; 8 | 9 | var Store = Reflux.createStore({ 10 | listenables: BuildActions, 11 | build: null, 12 | 13 | onChange: function(data) { 14 | if (this.build && (data.buildId === this.build.id)) { 15 | _(this.build).extend(data.changes); 16 | this.trigger(this.build); 17 | } 18 | }, 19 | 20 | init: function() { 21 | resource.subscribe('change', this.onChange); 22 | }, 23 | 24 | onRead: function(id) { 25 | var self = this; 26 | resource.sync('read', {id: id}, function(err, build) { 27 | if (err) throw err; 28 | self.build = build; 29 | self.trigger(self.build); 30 | }); 31 | } 32 | }); 33 | 34 | module.exports = Store; 35 | -------------------------------------------------------------------------------- /app/stores/buildLog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reflux = require('reflux'), 4 | BuildLogActions = require('../actions/buildLog'), 5 | resources = require('../resources'), 6 | resource = resources.builds; 7 | 8 | var Store = Reflux.createStore({ 9 | listenables: BuildLogActions, 10 | data: { 11 | lines: [], 12 | total: 0 13 | }, 14 | 15 | getInitialState: function() { 16 | return this.data; 17 | }, 18 | 19 | onGetTail: function(params) { 20 | var self = this; 21 | resource.sync('getBuildLogTail', params, function(err, data) { 22 | if (err) throw err; 23 | self.data = data; 24 | self.trigger(self.data); 25 | }); 26 | }, 27 | 28 | onGetLines: function(params) { 29 | var self = this; 30 | resource.sync('getBuildLogLines', params, function(err, data) { 31 | if (err) throw err; 32 | self.data.lines = data.lines; 33 | self.trigger(self.data); 34 | }); 35 | } 36 | }); 37 | 38 | module.exports = Store; 39 | -------------------------------------------------------------------------------- /app/stores/builds.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | Reflux = require('reflux'), 5 | BuildActions = require('../actions/build'), 6 | resources = require('../resources'), 7 | resource = resources.builds; 8 | 9 | var Store = Reflux.createStore({ 10 | listenables: BuildActions, 11 | builds: [], 12 | 13 | getInitialState: function() { 14 | return this.builds; 15 | }, 16 | 17 | onChanged: function(data) { 18 | var oldBuild = _(this.builds).findWhere({id: data.buildId}); 19 | if (oldBuild) { 20 | _(oldBuild).extend(data.changes); 21 | } else { 22 | this.builds.unshift( 23 | _({id: data.buildId}).extend(data.changes) 24 | ); 25 | } 26 | 27 | this.trigger(this.builds); 28 | }, 29 | 30 | onCancelled: function(data) { 31 | // WORKAROUND: client that trigger `onCancel` gets one `onCancelled` 32 | // call other clients get 2 calls (second with empty data) 33 | if (!data) { 34 | return; 35 | } 36 | 37 | if (data.buildStatus === 'queued') { 38 | var index = _(this.builds).findIndex({id: data.buildId}); 39 | if (index !== -1) { 40 | this.builds.splice(index, 1); 41 | } 42 | 43 | this.trigger(this.builds); 44 | } 45 | }, 46 | 47 | init: function() { 48 | resource.subscribe('change', this.onChanged); 49 | resource.subscribe('cancel', this.onCancelled); 50 | }, 51 | 52 | onReadAll: function(params) { 53 | var self = this; 54 | resource.sync('readAll', params, function(err, builds) { 55 | if (err) throw err; 56 | self.builds = builds; 57 | self.trigger(self.builds); 58 | }); 59 | }, 60 | 61 | onCancel: function(buildId) { 62 | resource.sync('cancel', {buildId: buildId}, function(err) { 63 | if (err) throw err; 64 | }); 65 | } 66 | }); 67 | 68 | module.exports = Store; 69 | 70 | -------------------------------------------------------------------------------- /app/stores/project.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reflux = require('reflux'), 4 | ProjectActions = require('../actions/project'), 5 | resources = require('../resources'), 6 | resource = resources.projects; 7 | 8 | var Store = Reflux.createStore({ 9 | listenables: ProjectActions, 10 | project: {}, 11 | 12 | getInitialState: function() { 13 | return this.project; 14 | }, 15 | 16 | onChange: function(data) { 17 | this.trigger(data.project); 18 | }, 19 | 20 | init: function() { 21 | resource.subscribe('change', this.onChange); 22 | }, 23 | 24 | onRead: function(params) { 25 | var self = this; 26 | resource.sync('read', params, function(err, project) { 27 | if (err) throw err; 28 | self.project = project; 29 | self.trigger(self.project); 30 | }); 31 | } 32 | }); 33 | 34 | module.exports = Store; 35 | -------------------------------------------------------------------------------- /app/stores/projects.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reflux = require('reflux'), 4 | ProjectActions = require('../actions/project'), 5 | resource = require('../resources').projects; 6 | 7 | var Store = Reflux.createStore({ 8 | listenables: ProjectActions, 9 | onRun: function(projectName, buildParams) { 10 | resource.sync('run', { 11 | projectName: projectName, 12 | buildParams: buildParams 13 | }, function(err) { 14 | if (err) throw err; 15 | }); 16 | }, 17 | onReadAll: function(params) { 18 | var self = this; 19 | resource.sync('readAll', params, function(err, projects) { 20 | if (err) throw err; 21 | self.trigger(projects); 22 | }); 23 | } 24 | }); 25 | 26 | module.exports = Store; 27 | -------------------------------------------------------------------------------- /app/stores/terminal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | Reflux = require('reflux'), 5 | BuildActions = require('../actions/build'), 6 | connect = require('../connect').data; 7 | 8 | var Store = Reflux.createStore({ 9 | listenables: BuildActions, 10 | 11 | init: function() { 12 | // the only purpose of this hash to reconnect all the time 13 | // except first, see notes at using 14 | this.connectedResourcesHash = {}; 15 | }, 16 | 17 | onReadTerminalOutput: function(build) { 18 | var self = this, 19 | resourceName = 'build' + build.id; 20 | 21 | // disconnect from all previously connected resources 22 | _(self.connectedResourcesHash).each(function(resource) { 23 | resource.disconnect(); 24 | }); 25 | 26 | var connectToBuildDataResource = function() { 27 | var resource = self.connectedResourcesHash[resourceName]; 28 | // reconnect for get data below (at subscribe), coz 29 | // data emitted only once during connect 30 | if (resource) { 31 | resource.reconnect(); 32 | } else { 33 | resource = connect.resource(resourceName); 34 | self.connectedResourcesHash[resourceName] = resource; 35 | } 36 | 37 | resource.subscribe('data', function(data) { 38 | var lastLine = _(self.lines).last(); 39 | if (lastLine && (_(data.lines).first().number === lastLine.number)) { 40 | self.lines = _(self.lines).initial(); 41 | } 42 | self.lines = self.lines.concat(data.lines); 43 | self.trigger({ 44 | buildId: build.id, 45 | buildCompleted: build.completed, 46 | name: 'Console for build #' + build.id, 47 | data: _(self.lines).chain().pluck('text').map(function(text) { 48 | // TODO: this can break output of non-ansible projects 49 | // prettify ansible output - unescape linebreaks and quotes 50 | return text.replace(/\\n/g, '\n').replace(/\\"/g, ''); 51 | }).value() 52 | }); 53 | }); 54 | }; 55 | 56 | this.lines = []; 57 | this.currentLine = ''; 58 | 59 | // create data resource for completed build 60 | if (build.completed) { 61 | connect.resource('projects').sync( 62 | 'createBuildDataResource', 63 | {buildId: build.id}, 64 | function(err) { 65 | if (err) throw err; 66 | connectToBuildDataResource(); 67 | } 68 | ); 69 | } else { 70 | connectToBuildDataResource(); 71 | } 72 | } 73 | }); 74 | 75 | module.exports = Store; 76 | -------------------------------------------------------------------------------- /app/styles/common/fonts.less: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Ubuntu&subset=latin,cyrillic); 2 | -------------------------------------------------------------------------------- /app/styles/common/variables-superhero.less: -------------------------------------------------------------------------------- 1 | // Superhero 3.3.7 2 | // Variables 3 | // -------------------------------------------------- 4 | 5 | 6 | //== Colors 7 | // 8 | //## Gray and brand colors for use across Bootstrap. 9 | 10 | @gray-base: #000; 11 | @gray-darker: lighten(@gray-base, 13.5%); // #222 12 | @gray-dark: lighten(@gray-base, 20%); // #333 13 | @gray: lighten(@gray-base, 33.5%); // #555 14 | @gray-light: #4E5D6C; // #999 15 | @gray-lighter: #EBEBEB; // #eee 16 | 17 | @brand-primary: #DF691A; 18 | @brand-success: #5cb85c; 19 | @brand-info: #5bc0de; 20 | @brand-warning: #f0ad4e; 21 | @brand-danger: #d9534f; 22 | 23 | 24 | //== Scaffolding 25 | // 26 | //## Settings for some of the most global styles. 27 | 28 | //** Background color for ``. 29 | @body-bg: #2B3E50; 30 | //** Global text color on ``. 31 | @text-color: @gray-lighter; 32 | 33 | //** Global textual link color. 34 | @link-color: @brand-primary; 35 | //** Link hover color set via `darken()` function. 36 | @link-hover-color: @link-color; 37 | //** Link hover decoration. 38 | @link-hover-decoration: underline; 39 | 40 | 41 | //== Typography 42 | // 43 | //## Font, line-height, and color for body text, headings, and more. 44 | 45 | @font-family-sans-serif: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; 46 | @font-family-serif: Georgia, "Times New Roman", Times, serif; 47 | //** Default monospace fonts for ``, ``, and `
`.
 48 | @font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
 49 | @font-family-base:        @font-family-sans-serif;
 50 | 
 51 | @font-size-base:          15px;
 52 | @font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
 53 | @font-size-small:         12px;
 54 | 
 55 | @font-size-h1:            floor((@font-size-base * 2.6)); // ~36px
 56 | @font-size-h2:            floor((@font-size-base * 2.15)); // ~30px
 57 | @font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px
 58 | @font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px
 59 | @font-size-h5:            @font-size-base;
 60 | @font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
 61 | 
 62 | //** Unit-less `line-height` for use in components like buttons.
 63 | @line-height-base:        1.428571429; // 20/14
 64 | //** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
 65 | @line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
 66 | 
 67 | //** By default, this inherits from the ``.
 68 | @headings-font-family:    inherit;
 69 | @headings-font-weight:    400;
 70 | @headings-line-height:    1.1;
 71 | @headings-color:          inherit;
 72 | 
 73 | 
 74 | //== Iconography
 75 | //
 76 | //## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
 77 | 
 78 | //** Load fonts from this directory.
 79 | @icon-font-path:          "../fonts/";
 80 | //** File name for all font files.
 81 | @icon-font-name:          "glyphicons-halflings-regular";
 82 | //** Element ID within SVG icon file.
 83 | @icon-font-svg-id:        "glyphicons_halflingsregular";
 84 | 
 85 | 
 86 | //== Components
 87 | //
 88 | //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
 89 | 
 90 | @padding-base-vertical:     8px;
 91 | @padding-base-horizontal:   16px;
 92 | 
 93 | @padding-large-vertical:    12px;
 94 | @padding-large-horizontal:  24px;
 95 | 
 96 | @padding-small-vertical:    5px;
 97 | @padding-small-horizontal:  10px;
 98 | 
 99 | @padding-xs-vertical:       1px;
100 | @padding-xs-horizontal:     5px;
101 | 
102 | @line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
103 | @line-height-small:         1.5;
104 | 
105 | @border-radius-base:        0;
106 | @border-radius-large:       0;
107 | @border-radius-small:       0;
108 | 
109 | //** Global color for active items (e.g., navs or dropdowns).
110 | @component-active-color:    #fff;
111 | //** Global background color for active items (e.g., navs or dropdowns).
112 | @component-active-bg:       @brand-primary;
113 | 
114 | //** Width of the `border` for generating carets that indicate dropdowns.
115 | @caret-width-base:          4px;
116 | //** Carets increase slightly in size for larger components.
117 | @caret-width-large:         5px;
118 | 
119 | 
120 | //== Tables
121 | //
122 | //## Customizes the `.table` component with basic values, each used across all table variations.
123 | 
124 | //** Padding for ``s and ``s.
125 | @table-cell-padding:            6px;
126 | //** Padding for cells in `.table-condensed`.
127 | @table-condensed-cell-padding:  3px;
128 | 
129 | //** Default background color used for all tables.
130 | @table-bg:                      transparent;
131 | //** Background color used for `.table-striped`.
132 | @table-bg-accent:               @gray-light;
133 | //** Background color used for `.table-hover`.
134 | @table-bg-hover:                darken(@gray-light, 3%);
135 | @table-bg-active:               @table-bg-hover;
136 | 
137 | //** Border color for table and cell borders.
138 | @table-border-color:            @gray-light;  
139 | 
140 | 
141 | //== Buttons
142 | //
143 | //## For each of Bootstrap's buttons, define text, background and border color.
144 | 
145 | @btn-font-weight:                normal;
146 | 
147 | @btn-default-color:              #fff;
148 | @btn-default-bg:                 @gray-light;
149 | @btn-default-border:             transparent;
150 | 
151 | @btn-primary-color:              #fff;
152 | @btn-primary-bg:                 @brand-primary;
153 | @btn-primary-border:             transparent;
154 | 
155 | @btn-success-color:              #fff;
156 | @btn-success-bg:                 @brand-success;
157 | @btn-success-border:             transparent;
158 | 
159 | @btn-info-color:                 #fff;
160 | @btn-info-bg:                    @brand-info;
161 | @btn-info-border:                transparent;
162 | 
163 | @btn-warning-color:              #fff;
164 | @btn-warning-bg:                 @brand-warning;
165 | @btn-warning-border:             transparent;
166 | 
167 | @btn-danger-color:               #fff;
168 | @btn-danger-bg:                  @brand-danger;
169 | @btn-danger-border:              transparent;
170 | 
171 | @btn-link-disabled-color:        @gray-light;
172 | 
173 | // Allows for customizing button radius independently from global border radius
174 | @btn-border-radius-base:         @border-radius-base;
175 | @btn-border-radius-large:        @border-radius-large;
176 | @btn-border-radius-small:        @border-radius-small;
177 | 
178 | 
179 | //== Forms
180 | //
181 | //##
182 | 
183 | //** `` background color
184 | @input-bg:                       #fff;
185 | //** `` background color
186 | @input-bg-disabled:              @gray-lighter;
187 | 
188 | //** Text color for ``s
189 | @input-color:                    @body-bg;
190 | //** `` border color
191 | @input-border:                   transparent;
192 | 
193 | // TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
194 | //** Default `.form-control` border radius
195 | // This has no effect on ``s in CSS.
196 | @input-border-radius:            @border-radius-base;
197 | //** Large `.form-control` border radius
198 | @input-border-radius-large:      @border-radius-large;
199 | //** Small `.form-control` border radius
200 | @input-border-radius-small:      @border-radius-small;
201 | 
202 | //** Border color for inputs on focus
203 | @input-border-focus:             transparent;
204 | 
205 | //** Placeholder text color
206 | @input-color-placeholder:        #ccc;
207 | 
208 | //** Default `.form-control` height
209 | @input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
210 | //** Large `.form-control` height
211 | @input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
212 | //** Small `.form-control` height
213 | @input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
214 | 
215 | //** `.form-group` margin
216 | @form-group-margin-bottom:       15px;
217 | 
218 | @legend-color:                   @text-color;
219 | @legend-border-color:            @gray-light;
220 | 
221 | //** Background color for textual input addons
222 | @input-group-addon-bg:           @btn-default-bg;
223 | //** Border color for textual input addons
224 | @input-group-addon-border-color: transparent;
225 | 
226 | //** Disabled cursor for form controls and buttons.
227 | @cursor-disabled:                not-allowed;
228 | 
229 | 
230 | //== Dropdowns
231 | //
232 | //## Dropdown menu container and contents.
233 | 
234 | //** Background for the dropdown menu.
235 | @dropdown-bg:                    @gray-light;
236 | //** Dropdown menu `border-color`.
237 | @dropdown-border:                transparent;
238 | //** Dropdown menu `border-color` **for IE8**.
239 | @dropdown-fallback-border:       transparent;
240 | //** Divider color for between dropdown items.
241 | @dropdown-divider-bg:            @body-bg;
242 | 
243 | //** Dropdown link text color.
244 | @dropdown-link-color:            @text-color;
245 | //** Hover color for dropdown links.
246 | @dropdown-link-hover-color:      @text-color;
247 | //** Hover background for dropdown links.
248 | @dropdown-link-hover-bg:         darken(@gray-light, 3%);
249 | 
250 | //** Active dropdown menu item text color.
251 | @dropdown-link-active-color:     @component-active-color;
252 | //** Active dropdown menu item background color.
253 | @dropdown-link-active-bg:        @component-active-bg;
254 | 
255 | //** Disabled dropdown menu item background color.
256 | @dropdown-link-disabled-color:   @body-bg;
257 | 
258 | //** Text color for headers within dropdown menus.
259 | @dropdown-header-color:          @body-bg;
260 | 
261 | //** Deprecated `@dropdown-caret-color` as of v3.1.0
262 | @dropdown-caret-color:           #000;
263 | 
264 | 
265 | //-- Z-index master list
266 | //
267 | // Warning: Avoid customizing these values. They're used for a bird's eye view
268 | // of components dependent on the z-axis and are designed to all work together.
269 | //
270 | // Note: These variables are not generated into the Customizer.
271 | 
272 | @zindex-navbar:            1000;
273 | @zindex-dropdown:          1000;
274 | @zindex-popover:           1060;
275 | @zindex-tooltip:           1070;
276 | @zindex-navbar-fixed:      1030;
277 | @zindex-modal-background:  1040;
278 | @zindex-modal:             1050;
279 | 
280 | 
281 | //== Media queries breakpoints
282 | //
283 | //## Define the breakpoints at which your layout will change, adapting to different screen sizes.
284 | 
285 | // Extra small screen / phone
286 | //** Deprecated `@screen-xs` as of v3.0.1
287 | @screen-xs:                  480px;
288 | //** Deprecated `@screen-xs-min` as of v3.2.0
289 | @screen-xs-min:              @screen-xs;
290 | //** Deprecated `@screen-phone` as of v3.0.1
291 | @screen-phone:               @screen-xs-min;
292 | 
293 | // Small screen / tablet
294 | //** Deprecated `@screen-sm` as of v3.0.1
295 | @screen-sm:                  768px;
296 | @screen-sm-min:              @screen-sm;
297 | //** Deprecated `@screen-tablet` as of v3.0.1
298 | @screen-tablet:              @screen-sm-min;
299 | 
300 | // Medium screen / desktop
301 | //** Deprecated `@screen-md` as of v3.0.1
302 | @screen-md:                  992px;
303 | @screen-md-min:              @screen-md;
304 | //** Deprecated `@screen-desktop` as of v3.0.1
305 | @screen-desktop:             @screen-md-min;
306 | 
307 | // Large screen / wide desktop
308 | //** Deprecated `@screen-lg` as of v3.0.1
309 | @screen-lg:                  1200px;
310 | @screen-lg-min:              @screen-lg;
311 | //** Deprecated `@screen-lg-desktop` as of v3.0.1
312 | @screen-lg-desktop:          @screen-lg-min;
313 | 
314 | // So media queries don't overlap when required, provide a maximum
315 | @screen-xs-max:              (@screen-sm-min - 1);
316 | @screen-sm-max:              (@screen-md-min - 1);
317 | @screen-md-max:              (@screen-lg-min - 1);
318 | 
319 | 
320 | //== Grid system
321 | //
322 | //## Define your custom responsive grid.
323 | 
324 | //** Number of columns in the grid.
325 | @grid-columns:              12;
326 | //** Padding between columns. Gets divided in half for the left and right.
327 | @grid-gutter-width:         30px;
328 | // Navbar collapse
329 | //** Point at which the navbar becomes uncollapsed.
330 | @grid-float-breakpoint:     @screen-sm-min;
331 | //** Point at which the navbar begins collapsing.
332 | @grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
333 | 
334 | 
335 | //== Container sizes
336 | //
337 | //## Define the maximum width of `.container` for different screen sizes.
338 | 
339 | // Small screen / tablet
340 | @container-tablet:             (720px + @grid-gutter-width);
341 | //** For `@screen-sm-min` and up.
342 | @container-sm:                 @container-tablet;
343 | 
344 | // Medium screen / desktop
345 | @container-desktop:            (940px + @grid-gutter-width);
346 | //** For `@screen-md-min` and up.
347 | @container-md:                 @container-desktop;
348 | 
349 | // Large screen / wide desktop
350 | @container-large-desktop:      (1140px + @grid-gutter-width);
351 | //** For `@screen-lg-min` and up.
352 | @container-lg:                 @container-large-desktop;
353 | 
354 | 
355 | //== Navbar
356 | //
357 | //##
358 | 
359 | // Basics of a navbar
360 | @navbar-height:                    40px;
361 | @navbar-margin-bottom:             @line-height-computed;
362 | @navbar-border-radius:             @border-radius-base;
363 | @navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
364 | @navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
365 | @navbar-collapse-max-height:       340px;
366 | 
367 | @navbar-default-color:             @text-color;
368 | @navbar-default-bg:                @gray-light;
369 | @navbar-default-border:            transparent;
370 | 
371 | // Navbar links
372 | @navbar-default-link-color:                @text-color;
373 | @navbar-default-link-hover-color:          @navbar-default-link-color;
374 | @navbar-default-link-hover-bg:             @dropdown-link-hover-bg;
375 | @navbar-default-link-active-color:         @navbar-default-link-color;
376 | @navbar-default-link-active-bg:            @dropdown-link-hover-bg;
377 | @navbar-default-link-disabled-color:       #ccc;
378 | @navbar-default-link-disabled-bg:          transparent;
379 | 
380 | // Navbar brand label
381 | @navbar-default-brand-color:               @navbar-default-link-color;
382 | @navbar-default-brand-hover-color:         @navbar-default-link-color;
383 | @navbar-default-brand-hover-bg:            transparent;
384 | 
385 | // Navbar toggle
386 | @navbar-default-toggle-hover-bg:           @navbar-default-link-hover-bg;
387 | @navbar-default-toggle-icon-bar-bg:        @text-color;
388 | @navbar-default-toggle-border-color:       transparent;
389 | 
390 | 
391 | //=== Inverted navbar
392 | // Reset inverted navbar basics
393 | @navbar-inverse-color:                      @text-color;
394 | @navbar-inverse-bg:                         @brand-primary;
395 | @navbar-inverse-border:                     transparent;
396 | 
397 | // Inverted navbar links
398 | @navbar-inverse-link-color:                 @text-color;
399 | @navbar-inverse-link-hover-color:           @navbar-inverse-link-color;
400 | @navbar-inverse-link-hover-bg:              darken(@navbar-inverse-bg, 5%);
401 | @navbar-inverse-link-active-color:          @navbar-inverse-link-color;
402 | @navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 5%);
403 | @navbar-inverse-link-disabled-color:        #444;
404 | @navbar-inverse-link-disabled-bg:           transparent;
405 | 
406 | // Inverted navbar brand label
407 | @navbar-inverse-brand-color:                @navbar-inverse-link-color;
408 | @navbar-inverse-brand-hover-color:          @navbar-inverse-link-color;
409 | @navbar-inverse-brand-hover-bg:             transparent;
410 | 
411 | // Inverted navbar toggle
412 | @navbar-inverse-toggle-hover-bg:            @navbar-inverse-link-hover-bg;
413 | @navbar-inverse-toggle-icon-bar-bg:         @text-color;
414 | @navbar-inverse-toggle-border-color:        transparent;
415 | 
416 | 
417 | //== Navs
418 | //
419 | //##
420 | 
421 | //=== Shared nav styles
422 | @nav-link-padding:                          10px 15px;
423 | @nav-link-hover-bg:                         @gray-light;
424 | 
425 | @nav-disabled-link-color:                   @gray-light;
426 | @nav-disabled-link-hover-color:             @gray-light;
427 | 
428 | //== Tabs
429 | @nav-tabs-border-color:                     transparent;
430 | 
431 | @nav-tabs-link-hover-border-color:          @gray-light;
432 | 
433 | @nav-tabs-active-link-hover-bg:             @body-bg;
434 | @nav-tabs-active-link-hover-color:          @text-color;
435 | @nav-tabs-active-link-hover-border-color:   @gray-light;
436 | 
437 | @nav-tabs-justified-link-border-color:            @gray-light;
438 | @nav-tabs-justified-active-link-border-color:     @gray-light;
439 | 
440 | //== Pills
441 | @nav-pills-border-radius:                   @border-radius-base;
442 | @nav-pills-active-link-hover-bg:            @component-active-bg;
443 | @nav-pills-active-link-hover-color:         @component-active-color;
444 | 
445 | 
446 | //== Pagination
447 | //
448 | //##
449 | 
450 | @pagination-color:                     @text-color;
451 | @pagination-bg:                        @gray-light;
452 | @pagination-border:                    transparent;
453 | 
454 | @pagination-hover-color:               @text-color;
455 | @pagination-hover-bg:                  @table-bg-hover;
456 | @pagination-hover-border:              transparent;
457 | 
458 | @pagination-active-color:              @text-color;
459 | @pagination-active-bg:                 @brand-primary;
460 | @pagination-active-border:             transparent;
461 | 
462 | @pagination-disabled-color:            darken(@table-bg-hover, 10%);
463 | @pagination-disabled-bg:               @gray-light;
464 | @pagination-disabled-border:           transparent;
465 | 
466 | 
467 | //== Pager
468 | //
469 | //##
470 | 
471 | @pager-bg:                             @pagination-bg;
472 | @pager-border:                         @pagination-border;
473 | @pager-border-radius:                  15px;
474 | 
475 | @pager-hover-bg:                       @pagination-hover-bg;
476 | 
477 | @pager-active-bg:                      @pagination-active-bg;
478 | @pager-active-color:                   @pagination-active-color;
479 | 
480 | @pager-disabled-color:                 @pagination-disabled-color;
481 | 
482 | 
483 | //== Jumbotron
484 | //
485 | //##
486 | 
487 | @jumbotron-padding:              30px;
488 | @jumbotron-color:                inherit;
489 | @jumbotron-bg:                   @gray-light;
490 | @jumbotron-heading-color:        inherit;
491 | @jumbotron-font-size:            ceil((@font-size-base * 1.5));
492 | @jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
493 | 
494 | 
495 | //== Form states and alerts
496 | //
497 | //## Define colors for form feedback states and, by default, alerts.
498 | 
499 | @state-success-text:             @text-color;
500 | @state-success-bg:               @brand-success;
501 | @state-success-border:           transparent;
502 | 
503 | @state-info-text:                @text-color;
504 | @state-info-bg:                  @brand-info;
505 | @state-info-border:              transparent;
506 | 
507 | @state-warning-text:             @text-color;
508 | @state-warning-bg:               @brand-warning;
509 | @state-warning-border:           transparent;
510 | 
511 | @state-danger-text:              @text-color;
512 | @state-danger-bg:                @brand-danger;
513 | @state-danger-border:            transparent;
514 | 
515 | 
516 | //== Tooltips
517 | //
518 | //##
519 | 
520 | //** Tooltip max width
521 | @tooltip-max-width:           200px;
522 | //** Tooltip text color
523 | @tooltip-color:               #fff;
524 | //** Tooltip background color
525 | @tooltip-bg:                  #000;
526 | @tooltip-opacity:             .9;
527 | 
528 | //** Tooltip arrow width
529 | @tooltip-arrow-width:         5px;
530 | //** Tooltip arrow color
531 | @tooltip-arrow-color:         @tooltip-bg;
532 | 
533 | 
534 | //== Popovers
535 | //
536 | //##
537 | 
538 | //** Popover body background color
539 | @popover-bg:                          @gray-light;
540 | //** Popover maximum width
541 | @popover-max-width:                   276px;
542 | //** Popover border color
543 | @popover-border-color:                transparent;
544 | //** Popover fallback border color
545 | @popover-fallback-border-color:       transparent;
546 | 
547 | //** Popover title background color
548 | @popover-title-bg:                    @table-bg-hover;
549 | 
550 | //** Popover arrow width
551 | @popover-arrow-width:                 10px;
552 | //** Popover arrow color
553 | @popover-arrow-color:                 @popover-bg;
554 | 
555 | //** Popover outer arrow width
556 | @popover-arrow-outer-width:           (@popover-arrow-width + 1);
557 | //** Popover outer arrow color
558 | @popover-arrow-outer-color:           transparent;
559 | //** Popover outer arrow fallback color
560 | @popover-arrow-outer-fallback-color:  transparent;
561 | 
562 | 
563 | //== Labels
564 | //
565 | //##
566 | 
567 | //** Default label background color
568 | @label-default-bg:            @gray-light;
569 | //** Primary label background color
570 | @label-primary-bg:            @brand-primary;
571 | //** Success label background color
572 | @label-success-bg:            @brand-success;
573 | //** Info label background color
574 | @label-info-bg:               @brand-info;
575 | //** Warning label background color
576 | @label-warning-bg:            @brand-warning;
577 | //** Danger label background color
578 | @label-danger-bg:             @brand-danger;
579 | 
580 | //** Default label text color
581 | @label-color:                 #fff;
582 | //** Default text color of a linked label
583 | @label-link-hover-color:      #fff;
584 | 
585 | 
586 | //== Modals
587 | //
588 | //##
589 | 
590 | //** Padding applied to the modal body
591 | @modal-inner-padding:         20px;
592 | 
593 | //** Padding applied to the modal title
594 | @modal-title-padding:         15px;
595 | //** Modal title line-height
596 | @modal-title-line-height:     @line-height-base;
597 | 
598 | //** Background color of modal content area
599 | @modal-content-bg:                             @gray-light;
600 | //** Modal content border color
601 | @modal-content-border-color:                   transparent;
602 | //** Modal content border color **for IE8**
603 | @modal-content-fallback-border-color:          transparent;
604 | 
605 | //** Modal backdrop background color
606 | @modal-backdrop-bg:           #000;
607 | //** Modal backdrop opacity
608 | @modal-backdrop-opacity:      .5;
609 | //** Modal header border color
610 | @modal-header-border-color:   @body-bg;
611 | //** Modal footer border color
612 | @modal-footer-border-color:   @modal-header-border-color;
613 | 
614 | @modal-lg:                    900px;
615 | @modal-md:                    600px;
616 | @modal-sm:                    300px;
617 | 
618 | 
619 | //== Alerts
620 | //
621 | //## Define alert colors, border radius, and padding.
622 | 
623 | @alert-padding:               15px;
624 | @alert-border-radius:         @border-radius-base;
625 | @alert-link-font-weight:      bold;
626 | 
627 | @alert-success-bg:            @state-success-bg;
628 | @alert-success-text:          @state-success-text;
629 | @alert-success-border:        @state-success-border;
630 | 
631 | @alert-info-bg:               @state-info-bg;
632 | @alert-info-text:             @state-info-text;
633 | @alert-info-border:           @state-info-border;
634 | 
635 | @alert-warning-bg:            @state-warning-bg;
636 | @alert-warning-text:          @state-warning-text;
637 | @alert-warning-border:        @state-warning-border;
638 | 
639 | @alert-danger-bg:             @state-danger-bg;
640 | @alert-danger-text:           @state-danger-text;
641 | @alert-danger-border:         @state-danger-border;
642 | 
643 | 
644 | //== Progress bars
645 | //
646 | //##
647 | 
648 | //** Background color of the whole progress component
649 | @progress-bg:                 @gray-light;
650 | //** Progress bar text color
651 | @progress-bar-color:          #fff;
652 | //** Variable for setting rounded corners on progress bar.
653 | @progress-border-radius:      @border-radius-base;
654 | 
655 | //** Default progress bar color
656 | @progress-bar-bg:             @brand-primary;
657 | //** Success progress bar color
658 | @progress-bar-success-bg:     @brand-success;
659 | //** Warning progress bar color
660 | @progress-bar-warning-bg:     @brand-warning;
661 | //** Danger progress bar color
662 | @progress-bar-danger-bg:      @brand-danger;
663 | //** Info progress bar color
664 | @progress-bar-info-bg:        @brand-info;
665 | 
666 | 
667 | //== List group
668 | //
669 | //##
670 | 
671 | //** Background color on `.list-group-item`
672 | @list-group-bg:                 @gray-light;
673 | //** `.list-group-item` border color
674 | @list-group-border:             transparent;
675 | //** List group border radius
676 | @list-group-border-radius:      @border-radius-base;
677 | 
678 | //** Background color of single list items on hover
679 | @list-group-hover-bg:           @table-bg-hover;
680 | //** Text color of active list items
681 | @list-group-active-color:       @component-active-color;
682 | //** Background color of active list items
683 | @list-group-active-bg:          @component-active-bg;
684 | //** Border color of active list elements
685 | @list-group-active-border:      @list-group-active-bg;
686 | //** Text color for content within active list items
687 | @list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
688 | 
689 | //** Text color of disabled list items
690 | @list-group-disabled-color:      @gray-light;
691 | //** Background color of disabled list items
692 | @list-group-disabled-bg:         @gray-lighter;
693 | //** Text color for content within disabled list items
694 | @list-group-disabled-text-color: @list-group-disabled-color;
695 | 
696 | @list-group-link-color:         @text-color;
697 | @list-group-link-hover-color:   @list-group-link-color;
698 | @list-group-link-heading-color: @text-color;
699 | 
700 | 
701 | //== Panels
702 | //
703 | //##
704 | 
705 | @panel-bg:                    @gray-light;
706 | @panel-body-padding:          15px;
707 | @panel-heading-padding:       10px 15px;
708 | @panel-footer-padding:        @panel-heading-padding;
709 | @panel-border-radius:         @border-radius-base;
710 | 
711 | //** Border color for elements within panels
712 | @panel-inner-border:          transparent;
713 | @panel-footer-bg:             @table-bg-hover;
714 | 
715 | @panel-default-text:          @gray-dark;
716 | @panel-default-border:        transparent;
717 | @panel-default-heading-bg:    #f5f5f5;
718 | 
719 | @panel-primary-text:          #fff;
720 | @panel-primary-border:        transparent;
721 | @panel-primary-heading-bg:    @brand-primary;
722 | 
723 | @panel-success-text:          @state-success-text;
724 | @panel-success-border:        transparent;
725 | @panel-success-heading-bg:    @state-success-bg;
726 | 
727 | @panel-info-text:             @state-info-text;
728 | @panel-info-border:           transparent;
729 | @panel-info-heading-bg:       @state-info-bg;
730 | 
731 | @panel-warning-text:          @state-warning-text;
732 | @panel-warning-border:        transparent;
733 | @panel-warning-heading-bg:    @state-warning-bg;
734 | 
735 | @panel-danger-text:           @state-danger-text;
736 | @panel-danger-border:         transparent;
737 | @panel-danger-heading-bg:     @state-danger-bg;
738 | 
739 | 
740 | //== Thumbnails
741 | //
742 | //##
743 | 
744 | //** Padding around the thumbnail image
745 | @thumbnail-padding:           4px;
746 | //** Thumbnail background color
747 | @thumbnail-bg:                @body-bg;
748 | //** Thumbnail border color
749 | @thumbnail-border:            #ddd;
750 | //** Thumbnail border radius
751 | @thumbnail-border-radius:     @border-radius-base;
752 | 
753 | //** Custom text color for thumbnail captions
754 | @thumbnail-caption-color:     @text-color;
755 | //** Padding around the thumbnail caption
756 | @thumbnail-caption-padding:   9px;
757 | 
758 | 
759 | //== Wells
760 | //
761 | //## 
762 | 
763 | @well-bg:                     @gray-light;
764 | @well-border:                 transparent;
765 | 
766 | 
767 | //== Badges
768 | //
769 | //##
770 | 
771 | @badge-color:                 @text-color;
772 | //** Linked badge text color on hover
773 | @badge-link-hover-color:      #fff;
774 | @badge-bg:                    @gray-light;
775 | 
776 | //** Badge text color in active nav link
777 | @badge-active-color:          @link-color;
778 | //** Badge background color in active nav link
779 | @badge-active-bg:             #fff;
780 | 
781 | @badge-font-weight:           300;
782 | @badge-line-height:           1;
783 | @badge-border-radius:         10px;
784 | 
785 | 
786 | //== Breadcrumbs
787 | //
788 | //##
789 | 
790 | @breadcrumb-padding-vertical:   8px;
791 | @breadcrumb-padding-horizontal: 15px;
792 | //** Breadcrumb background color
793 | @breadcrumb-bg:                 @gray-light;
794 | //** Breadcrumb text color
795 | @breadcrumb-color:              @text-color;
796 | //** Text color of current page in the breadcrumb
797 | @breadcrumb-active-color:       @text-color;
798 | //** Textual separator for between breadcrumb elements
799 | @breadcrumb-separator:          "/";
800 | 
801 | 
802 | //== Carousel
803 | //
804 | //##
805 | 
806 | @carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
807 | 
808 | @carousel-control-color:                      #fff;
809 | @carousel-control-width:                      15%;
810 | @carousel-control-opacity:                    .5;
811 | @carousel-control-font-size:                  20px;
812 | 
813 | @carousel-indicator-active-bg:                #fff;
814 | @carousel-indicator-border-color:             #fff;
815 | 
816 | @carousel-caption-color:                      #fff;
817 | 
818 | 
819 | //== Close
820 | //
821 | //##
822 | 
823 | @close-font-weight:           bold;
824 | @close-color:                 @text-color;
825 | @close-text-shadow:           none;
826 | 
827 | 
828 | //== Code
829 | //
830 | //##
831 | 
832 | @code-color:                  #c7254e;
833 | @code-bg:                     #f9f2f4;
834 | 
835 | @kbd-color:                   #fff;
836 | @kbd-bg:                      #333;
837 | 
838 | @pre-bg:                      #f5f5f5;
839 | @pre-color:                   @gray-dark;
840 | @pre-border-color:            #ccc;
841 | @pre-scrollable-max-height:   340px;
842 | 
843 | 
844 | //== Type
845 | //
846 | //##
847 | 
848 | //** Horizontal offset for forms and lists.
849 | @component-offset-horizontal: 180px;
850 | //** Text muted color
851 | @text-muted:                  @gray-light;
852 | //** Abbreviations and acronyms border color
853 | @abbr-border-color:           @gray-light;
854 | //** Headings small color
855 | @headings-small-color:        @text-color;
856 | //** Blockquote small color
857 | @blockquote-small-color:      @text-color;
858 | //** Blockquote font size
859 | @blockquote-font-size:        (@font-size-base * 1.25);
860 | //** Blockquote border color
861 | @blockquote-border-color:     @gray-light;
862 | //** Page header border color
863 | @page-header-border-color:    @gray-lighter;
864 | //** Width of horizontal description list titles
865 | @dl-horizontal-offset:        @component-offset-horizontal;
866 | //** Point at which .dl-horizontal becomes horizontal
867 | @dl-horizontal-breakpoint:    @grid-float-breakpoint;
868 | //** Horizontal line color.
869 | @hr-border:                   lighten(@gray-light, 5%);
870 | 


--------------------------------------------------------------------------------
/app/styles/common/variables.less:
--------------------------------------------------------------------------------
1 | @font-size-base: 14px;
2 | 


--------------------------------------------------------------------------------
/app/styles/components/builds.less:
--------------------------------------------------------------------------------
  1 | .build-view {
  2 | 	&_info {
  3 | 	}
  4 | 	&_terminal {
  5 | 		margin-top: 20px;
  6 | 	}
  7 | }
  8 | 
  9 | @animation-duration: 1.5s;
 10 | 
 11 | .builds {
 12 | 	&_item {
 13 | 		&:hover {
 14 | 			.builds {
 15 | 				&_buttons {
 16 | 					opacity: 1;
 17 | 				}
 18 | 			}
 19 | 		}
 20 | 	}
 21 | 
 22 | 	&_inner {
 23 | 		background: @well-bg;
 24 | 		padding: 15px;
 25 | 	}
 26 | 
 27 | 	&_header {
 28 | 		.make-xs-column(9);
 29 | 	}
 30 | 
 31 | 	&_controls {
 32 | 		.make-xs-column(3);
 33 | 		.text-right;
 34 | 	}
 35 | 
 36 | 	&_buttons {
 37 | 		transition: opacity 0.2s ease;
 38 | 		opacity: 0;
 39 | 	}
 40 | 
 41 | 	&_progress {
 42 | 		.progress {
 43 | 			height: 18px;
 44 | 			margin-bottom: 0;
 45 | 		}
 46 | 	}
 47 | 
 48 | 	&_status {
 49 | 		float: left;
 50 | 		margin-right: 8px;
 51 | 	}
 52 | 
 53 | 	&_info {
 54 | 		display: inline-block;
 55 | 		margin-right: 10px;
 56 | 	}
 57 | 
 58 | 	&__timeline {
 59 | 		position: relative;
 60 | 
 61 | 		.builds {
 62 | 			&_inner {
 63 | 				border-left: 6px solid darken(@well-bg, 10%);
 64 | 			}
 65 | 
 66 | 			&_header {
 67 | 				margin-bottom: 6px;
 68 | 				font-size: 18px;
 69 | 			}
 70 | 
 71 | 			&_progress {
 72 | 				padding: 3px 0;
 73 | 			}
 74 | 
 75 | 			&_item {
 76 | 				margin: 4px 0;
 77 | 				position: relative;
 78 | 
 79 | 				&:after {
 80 | 					content: '';
 81 | 					position: absolute;
 82 | 					border-radius: 50%;
 83 | 					background: @well-bg;
 84 | 					left: 0;
 85 | 					z-index: 1;
 86 | 				}
 87 | 
 88 | 				&:before {
 89 | 					content: '';
 90 | 					position: absolute;
 91 | 					border: 11px solid transparent;
 92 | 					top: 25px;
 93 | 				}
 94 | 
 95 | 				&__in-progress {
 96 | 					&:after {
 97 | 						background: @label-info-bg;
 98 | 					}
 99 | 				}
100 | 
101 | 				&__done {
102 | 					&:after {
103 | 						background: @brand-success;
104 | 					}
105 | 				}
106 | 
107 | 				&__error {
108 | 					&:after {
109 | 						background: @brand-danger;
110 | 					}
111 | 				}
112 | 
113 | 				&__queued {
114 | 					&:after {
115 | 						background: @label-default-bg;
116 | 					}
117 | 				}
118 | 
119 | 				&__canceled {
120 | 					&:after {
121 | 						background: @brand-warning;
122 | 					}
123 | 				}
124 | 			}
125 | 		}
126 | 
127 | 		&:after {
128 | 			content: '';
129 | 			position: absolute;
130 | 			top: 0;
131 | 			bottom: 0;
132 | 			width: 2px;
133 | 			margin-left: -1px;
134 | 			background: darken(@well-bg, 10%);
135 | 		}
136 | 
137 | 		&-large {
138 | 			.builds {
139 | 				&_item {
140 | 					padding-left: 40px;
141 | 
142 | 					&:after {
143 | 						width: 24px;
144 | 						height: 24px;
145 | 						top: 25px;
146 | 					}
147 | 
148 | 					&:before {
149 | 						left: 20px;
150 | 						border-right-color: darken(@well-bg, 10%);
151 | 						top: 25px;
152 | 					}
153 | 				}
154 | 			}
155 | 
156 | 			&:after {
157 | 				left: 12px;
158 | 			}
159 | 		}
160 | 
161 | 		&-small {
162 | 			.builds {
163 | 				&_item {
164 | 					padding-left: 30px;
165 | 
166 | 					&:after {
167 | 						top: 16px;
168 | 						height: 16px;
169 | 						width: 16px;
170 | 					}
171 | 
172 | 					&:before {
173 | 						left: 10px;
174 | 						border-right-color: darken(@well-bg, 10%);
175 | 						top: 13px;
176 | 					}
177 | 
178 | 					&__current {
179 | 						&:before {
180 | 							left: 10px;
181 | 							border-right-color: @component-active-bg;
182 | 							top: 13px;
183 | 						}
184 | 
185 | 						.builds {
186 | 							&_inner {
187 | 								border-left: 6px solid @component-active-bg;
188 | 							}
189 | 						}
190 | 
191 | 					}
192 | 				}
193 | 
194 | 				&_header {
195 | 					font-size: 14px;
196 | 					.make-xs-column(6);
197 | 					margin-bottom: 0;
198 | 				}
199 | 
200 | 				&_controls {
201 | 					.make-xs-column(6);
202 | 					font-size: 11px;
203 | 				}
204 | 
205 | 				&_progress {
206 | 					padding: 1px 0;
207 | 				}
208 | 			}
209 | 
210 | 			&:after {
211 | 				left: 8px;
212 | 			}
213 | 		}
214 | 	}
215 | }
216 | 
217 | @media (min-width: @screen-sm-min) {
218 | 	.builds {
219 | 		&__timeline {
220 | 			&-large {
221 | 				.builds {
222 | 					&_item {
223 | 						padding-left: 0;
224 | 						display: inline-block;
225 | 						vertical-align: top;
226 | 						width: 50%;
227 | 						margin: 10px 0;
228 | 
229 | 						&:after {
230 | 							left: auto;
231 | 						}
232 | 
233 | 						&:before {
234 | 							left: auto;
235 | 							border-right-color: transparent;
236 | 						}
237 | 					}
238 | 
239 | 					&_inner {
240 | 						border-left: 0;
241 | 					}
242 | 				}
243 | 
244 | 				&:after {
245 | 					left: 50%;
246 | 				}
247 | 			}
248 | 
249 | 			&-left {
250 | 				.builds {
251 | 					&_item {
252 | 						&:nth-child(odd) {
253 | 							padding-right: 30px;
254 | 
255 | 							.builds {
256 | 								&_inner {
257 | 									border-right: 6px solid darken(@well-bg, 10%);
258 | 								}
259 | 							}
260 | 
261 | 							&:after {
262 | 								right: -12px;
263 | 							}
264 | 
265 | 							&:before {
266 | 								right: 10px;
267 | 								border-left-color: darken(@well-bg, 10%);
268 | 							}
269 | 						}
270 | 
271 | 						&:nth-child(even) {
272 | 							padding-left: 30px;
273 | 							top: 50px;
274 | 
275 | 							.builds {
276 | 								&_inner {
277 | 									border-left: 6px solid darken(@well-bg, 10%);
278 | 								}
279 | 							}
280 | 
281 | 							&:after {
282 | 								left: -12px;
283 | 							}
284 | 
285 | 							&:before {
286 | 								left: 10px;
287 | 								border-right-color: darken(@well-bg, 10%);
288 | 							}
289 | 						}
290 | 					}
291 | 				}
292 | 			}
293 | 
294 | 			&-right {
295 | 				.builds {
296 | 					&_item {
297 | 						&:first-child {
298 | 							margin-left: 50%;
299 | 						}
300 | 
301 | 						&:nth-child(even) {
302 | 							padding-right: 30px;
303 | 							top: -50px;
304 | 
305 | 							.builds {
306 | 								&_inner {
307 | 									border-right: 6px solid darken(@well-bg, 10%);
308 | 								}
309 | 							}
310 | 
311 | 							&:after {
312 | 								right: -12px;
313 | 							}
314 | 
315 | 							&:before {
316 | 								right: 10px;
317 | 								border-left-color: darken(@well-bg, 10%);
318 | 							}
319 | 						}
320 | 
321 | 						&:nth-child(odd) {
322 | 							padding-left: 30px;
323 | 
324 | 							.builds {
325 | 								&_inner {
326 | 									border-left: 6px solid darken(@well-bg, 10%);
327 | 								}
328 | 							}
329 | 
330 | 							&:after {
331 | 								left: -12px;
332 | 							}
333 | 
334 | 							&:before {
335 | 								left: 10px;
336 | 								border-right-color: darken(@well-bg, 10%);
337 | 							}
338 | 						}
339 | 					}
340 | 				}
341 | 			}
342 | 		}
343 | 	}
344 | }
345 | 


--------------------------------------------------------------------------------
/app/styles/components/layout.less:
--------------------------------------------------------------------------------
 1 | body {
 2 | 	font-family: 'Open Sans', sans-serif;
 3 | 	min-width: 320px;
 4 | }
 5 | 
 6 | .page-wrapper {
 7 | 	padding: 20px 0;
 8 | }
 9 | 
10 | .page-header {
11 | 	margin-top: 0;
12 | 	border-bottom-color: #d7d7d7;
13 | 
14 | 	.small {
15 | 		font-size: @font-size-base;
16 | 	}
17 | }


--------------------------------------------------------------------------------
/app/styles/components/projects.less:
--------------------------------------------------------------------------------
 1 | .projects-selector {
 2 | 	position: relative;
 3 | 	width: 300px;
 4 | 	color: white;
 5 | 	cursor: pointer;
 6 | 
 7 | 	&_preview {
 8 | 		color: rgba(255, 255, 255, 0.5);
 9 | 		padding: 5px 25px 5px 5px;
10 | 		&_text {
11 | 			padding-left: 10px;
12 | 		}
13 | 	}
14 | 
15 | 	&_input {
16 | 		width: 80%;
17 | 		padding-bottom: 3px;
18 | 		padding-left: 10px;
19 | 		border: none;
20 | 		border-bottom: 1px solid rgba(255, 255, 255, 0.5);
21 | 		background: transparent;
22 | 		outline: none;
23 | 	}
24 | 
25 | 	&_items {
26 | 		position: absolute;
27 | 		width: 100%;
28 | 		padding: 0;
29 | 		padding-top: 15px;
30 | 		left: 0;
31 | 		background: @brand-primary;
32 | 		list-style: none;
33 | 	}
34 | 
35 | 	&_item {
36 | 		.clearfix();
37 | 
38 | 		padding: 10px 10px;
39 | 
40 | 		&_link {
41 | 			float: left;
42 | 			width: 90%;
43 | 			color: white;
44 | 			text-decoration: none;
45 | 			&:hover {
46 | 				color: white;
47 | 				text-decoration: none;
48 | 			}
49 | 		}
50 | 		&_run {
51 | 			float: right;
52 | 			width: 10%;
53 | 			color: rgba(255, 255, 255, 0.5);
54 | 			text-align: right;
55 | 			&:hover {
56 | 				color: white;
57 | 				text-decoration: none;
58 | 			}
59 | 		}
60 | 
61 | 		&:hover {
62 | 			background: lighten(@brand-primary, 10%);
63 | 		}
64 | 	}
65 | }
66 | 


--------------------------------------------------------------------------------
/app/styles/components/terminal.less:
--------------------------------------------------------------------------------
 1 | .terminal {
 2 | 	box-sizing: border-box;
 3 | 	position: relative;
 4 | 
 5 | 	&_code {
 6 | 		display: table;
 7 | 		width: 100%;
 8 | 		padding: 10px 0;
 9 | 		color: #f1f1f1;
10 | 		font-family: monospace;
11 | 		font-size: 12px;
12 | 		line-height: 18px;
13 | 		white-space: pre-wrap;
14 | 		word-wrap: break-word;
15 | 		background-color: #2a2a2a;
16 | 		counter-reset: line-numbering;
17 | 	}
18 | }
19 | 
20 | .code-line {
21 | 	display: table-row;
22 | 
23 | 	&_counter {
24 | 		display: table-cell;
25 | 		width: 1px;
26 | 		vertical-align: top;
27 | 		text-align: right;
28 | 		cursor: pointer;
29 | 		text-decoration: none;
30 | 		color: darken(@gray-lighter, 15%);
31 | 		padding: 0 10px;
32 | 		border-right: 1px solid rgba(255, 255, 255, 0.1);
33 | 		white-space: nowrap;
34 | 
35 | 		&:before {
36 | 			content: counter(line-numbering);
37 | 			counter-increment: line-numbering;
38 | 		}
39 | 
40 | 		&:hover {
41 | 			text-decoration: none;
42 | 			color: @gray-lighter;
43 | 		}
44 | 	}
45 | 
46 | 	&_body {
47 | 		display: table-cell;
48 | 		vertical-align: top;
49 |     		padding: 0 10px;
50 | 	}
51 | 
52 | 	&:hover {
53 | 		background-color: #444;
54 | 	}
55 | }
56 | 


--------------------------------------------------------------------------------
/app/styles/index.less:
--------------------------------------------------------------------------------
 1 | @libPath: "../../node_modules";
 2 | 
 3 | // bootstrap
 4 | @import "@{libPath}/bootstrap/less/bootstrap.less";
 5 | 
 6 | /*//flatly*/
 7 | @import "./common/variables-superhero.less";
 8 | /*@import "./common/variables-lumen.less";*/
 9 | 
10 | //font-awesome
11 | @import "@{libPath}/font-awesome/less/font-awesome.less";
12 | 
13 | //scmicons
14 | @import "@{libPath}/scmicons/less/scmicons.less";
15 | 
16 | //variables
17 | @import "./common/variables.less";
18 | 
19 | //fonts
20 | @import "./common/fonts.less";
21 | 
22 | //layout
23 | @import "./components/layout.less";
24 | 
25 | //components
26 | @import "./components/builds.less";
27 | @import "./components/projects.less";
28 | @import "./components/terminal.less";
29 | 


--------------------------------------------------------------------------------
/app/utils.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | 
 3 | exports.prune = function(str, length) {
 4 | 	var result = '',
 5 | 		words = str.split(' ');
 6 | 
 7 | 	do {
 8 | 		result += words.shift() + ' ';
 9 | 	} while (words.length && result.length < length);
10 | 
11 | 	return result.replace(/ $/, words.length ? '...' : '');
12 | };
13 | 
14 | exports.escapeHtml = function(str) {
15 | 	return str
16 | 		.replace(/&(?!(\w+|\#\d+);)/g, '&')
17 | 		.replace(//g, '>')
19 | 		.replace(/"/g, '"');
20 | };
21 | 


--------------------------------------------------------------------------------
/dataio.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | 
 3 | var dataio = require('data.io'),
 4 | 	Server = require('data.io/lib/server'),
 5 | 	Resource = require('data.io/lib/resource'),
 6 | 	Sync = require('data.io/lib/sync');
 7 | 
 8 | /*
 9 |  * Patch server and resource to provide ability to send data to all clients
10 |  * of the resource
11 |  */
12 | 
13 | Server.prototype.resource = function(name, resource) {
14 | 	var self = this;
15 | 
16 | 	if (resource === undefined) {
17 | 		resource = this.resources[name];
18 | 		if (resource) return resource;
19 | 		resource = new Resource();
20 | 	}
21 | 
22 | 	this.resources[name] = resource;
23 | 
24 | 	this.namespace(name).on('connection', function(client) {
25 | 		self.connect(resource, client);
26 | 	});
27 | 
28 | 	// save link to the namespace at resource
29 | 	resource.namespace = this.namespace(name);
30 | 
31 | 	return resource;
32 | };
33 | 
34 | Resource.prototype.clientEmitSync = function(action, data) {
35 | 	this.namespace.emit('sync', action, data);
36 | };
37 | 
38 | module.exports = function() {
39 | 	return dataio.apply(dataio, arguments);
40 | };
41 | 


--------------------------------------------------------------------------------
/docker/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact=true
2 | 
3 | 


--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
 1 | FROM alpine:3.10.4
 2 | 
 3 | ENV NODE_ENV="production"
 4 | ENV USER="nci-ansible-ui-installer"
 5 | ENV USER_ID=2000
 6 | ENV USER_GORUP_ID=2000
 7 | 
 8 | RUN addgroup -g "$USER_GORUP_ID" "$USER" && \
 9 | 	adduser --disabled-password --ingroup "$USER" --uid "$USER_ID" "$USER";
10 | 
11 | RUN apk add openssh git rsync nodejs npm ansible && \
12 | 	mkdir /var/nci-ansible-ui
13 | 
14 | ADD package.json package-lock.json /var/nci-ansible-ui/
15 | ADD data /var/nci-ansible-ui/data
16 | 
17 | RUN chown -R "$USER":"$USER" /var/nci-ansible-ui
18 | 
19 | USER ${USER}
20 | 
21 | RUN cd /var/nci-ansible-ui && \
22 | 	npm ci --only=prod && \
23 | 
24 | 	ansible --version >> dependencies-info.txt && \
25 | 	echo "nodejs: `node --version`" >> dependencies-info.txt && \
26 | 	npmPackages=`cd /var/nci-ansible-ui && npm ls --prod --depth=0 | tail -n +2` && \
27 | 	echo -e "npm packages:\n$npmPackages" >> dependencies-info.txt;
28 | 
29 | USER root
30 | 
31 | ADD entrypoint.sh /entrypoint.sh
32 | ENTRYPOINT ["/entrypoint.sh"]
33 | 


--------------------------------------------------------------------------------
/docker/data/config.yaml:
--------------------------------------------------------------------------------
 1 | 
 2 | plugins:
 3 |     - nci-projects-reloader
 4 |     - nci-scheduler
 5 |     - nci-static-server
 6 |     - nci-rest-api-server
 7 |     - nci-mail-notification
 8 |     - nci-telegram-notification
 9 |     #ui better be last plugin
10 |     - nci-ansible-ui
11 | 
12 | nodes:
13 |     - type: local
14 |       #allow maximum 3 parallel builds
15 |       maxExecutorsCount: 3
16 | 
17 | http:
18 |     host: 0.0.0.0
19 |     port: 3000
20 |     url: http://127.0.0.1:3000
21 | 
22 |     static:
23 |         #this settings will be consumed by static server plugin 
24 |         locations:
25 |             #serve static for ui plugin
26 |             - url: /favicon.ico
27 |               root: node_modules/nci-ansible-ui/static/
28 |             - url: !!js/regexp ^/(js|css|fonts|images)/
29 |               root: node_modules/nci-ansible-ui/static/
30 | 
31 | storage:
32 |     #use leveldown as db backend
33 |     backend: leveldown
34 | 
35 | 
36 | #configure account for sending notifications
37 | #this settings will be consumed by mail notification plugin
38 | # notify:
39 | #     mail:
40 | #         host: smtp.gmail.com
41 | #         port: 587
42 | #         auth:
43 | #             user: your_sender_login@gmail.com
44 | #             pass: your_sender_password
45 | 
46 | 
47 | 


--------------------------------------------------------------------------------
/docker/data/preload.json:
--------------------------------------------------------------------------------
1 | {
2 |     "plugins": ["nci-yaml-reader"]
3 | }
4 | 
5 | 


--------------------------------------------------------------------------------
/docker/data/projects/some_project/config.yaml:
--------------------------------------------------------------------------------
 1 | 
 2 | scm:
 3 |     type: git
 4 |     repository: https://github.com/node-ci/nci-ansible-ui-sample-playbook
 5 |     rev: master
 6 | 
 7 | #notify when build fails or build status changes (according to previous status)
 8 | #to use email notification notify.mail section in server config should be
 9 | #configured
10 | 
11 | # notify:
12 | #     on:
13 | #         - error
14 | #         - change
15 | #     to:
16 | #         mail:
17 | #             - your_mail@example.com
18 | 
19 | #some shell steps before run playbook with inventories
20 | steps:
21 |     - name: Some action before playbooks
22 |       cmd: echo "do something"
23 | 
24 | playbooks:
25 |     - name: sample_shell_calls
26 |       path: projects/some_project/playbooks/shell/main.yaml
27 |       inventories:
28 |           - name: sample
29 |             path: projects/some_project/inventories/sample/hosts
30 | 
31 |     - name: check_host_availability
32 |       path: projects/some_project/playbooks/ping/main.yaml
33 |       inventories:
34 |           - name: sample
35 |             path: projects/some_project/inventories/sample/hosts
36 | 


--------------------------------------------------------------------------------
/docker/entrypoint.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | if [ `id -u` -ne 0 ]; then
 4 | 	echo "Current user must be root";
 5 | 	exit 1;
 6 | fi;
 7 | 
 8 | if [ "$UID" != "" ] && [ "GUID" != "" ]; then
 9 | 	USER="nci-ansible-ui";
10 | else
11 | 	USER="root";
12 | 	UID=0;
13 | 	GID=0;
14 | fi;
15 | 
16 | # user may already exist if running earlier created container
17 | if ! getent passwd "$UID" > /dev/null 2>&1; then
18 | 	addgroup -g "$GID" "$USER" &&
19 | 	adduser --disabled-password --ingroup "$USER" --uid "$UID" "$USER";
20 | fi;
21 | 
22 | HOME=`getent passwd "$UID" | cut -d: -f6`;
23 | 
24 | echo "*** Running nci";
25 | echo "USER: $USER, UID: $UID, GID: $GID, HOME: $HOME";
26 | cat /var/nci-ansible-ui/dependencies-info.txt;
27 | echo "***";
28 | 
29 | 
30 | cd /var/nci-ansible-ui &&
31 | su "$USER" -c node_modules/.bin/nci;
32 | 


--------------------------------------------------------------------------------
/docker/package-lock.json:
--------------------------------------------------------------------------------
   1 | {
   2 |   "name": "nci-ansible-ui-docker-installation",
   3 |   "version": "0.1.0",
   4 |   "lockfileVersion": 1,
   5 |   "requires": true,
   6 |   "dependencies": {
   7 |     "abstract-leveldown": {
   8 |       "version": "6.2.2",
   9 |       "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.2.tgz",
  10 |       "integrity": "sha512-/a+Iwj0rn//CX0EJOasNyZJd2o8xur8Ce9C57Sznti/Ilt/cb6Qd8/k98A4ZOklXgTG+iAYYUs1OTG0s1eH+zQ==",
  11 |       "requires": {
  12 |         "level-concat-iterator": "~2.0.0",
  13 |         "level-supports": "~1.0.0",
  14 |         "xtend": "~4.0.0"
  15 |       }
  16 |     },
  17 |     "accepts": {
  18 |       "version": "1.3.3",
  19 |       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
  20 |       "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
  21 |       "requires": {
  22 |         "mime-types": "~2.1.11",
  23 |         "negotiator": "0.6.1"
  24 |       }
  25 |     },
  26 |     "addressparser": {
  27 |       "version": "0.3.2",
  28 |       "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-0.3.2.tgz",
  29 |       "integrity": "sha1-WYc/Nej89sc2HBAjkmHXbhU0i7I="
  30 |     },
  31 |     "after": {
  32 |       "version": "0.8.2",
  33 |       "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
  34 |       "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
  35 |     },
  36 |     "anymatch": {
  37 |       "version": "3.1.1",
  38 |       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
  39 |       "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
  40 |       "requires": {
  41 |         "normalize-path": "^3.0.0",
  42 |         "picomatch": "^2.0.4"
  43 |       }
  44 |     },
  45 |     "argparse": {
  46 |       "version": "1.0.10",
  47 |       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
  48 |       "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
  49 |       "requires": {
  50 |         "sprintf-js": "~1.0.2"
  51 |       }
  52 |     },
  53 |     "arraybuffer.slice": {
  54 |       "version": "0.0.6",
  55 |       "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
  56 |       "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco="
  57 |     },
  58 |     "backo2": {
  59 |       "version": "1.0.2",
  60 |       "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
  61 |       "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
  62 |     },
  63 |     "base64-arraybuffer": {
  64 |       "version": "0.1.5",
  65 |       "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
  66 |       "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
  67 |     },
  68 |     "base64id": {
  69 |       "version": "1.0.0",
  70 |       "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
  71 |       "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
  72 |     },
  73 |     "better-assert": {
  74 |       "version": "1.0.2",
  75 |       "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
  76 |       "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
  77 |       "requires": {
  78 |         "callsite": "1.0.0"
  79 |       }
  80 |     },
  81 |     "binary-extensions": {
  82 |       "version": "2.0.0",
  83 |       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
  84 |       "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow=="
  85 |     },
  86 |     "blob": {
  87 |       "version": "0.0.4",
  88 |       "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
  89 |       "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE="
  90 |     },
  91 |     "braces": {
  92 |       "version": "3.0.2",
  93 |       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
  94 |       "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
  95 |       "requires": {
  96 |         "fill-range": "^7.0.1"
  97 |       }
  98 |     },
  99 |     "buildmail": {
 100 |       "version": "1.3.0",
 101 |       "resolved": "https://registry.npmjs.org/buildmail/-/buildmail-1.3.0.tgz",
 102 |       "integrity": "sha1-9eS3UCYUfV6+GiTcYTEqPP9N9yU=",
 103 |       "requires": {
 104 |         "addressparser": "^0.3.2",
 105 |         "hyperquest": "^1.2.0",
 106 |         "libbase64": "^0.1.0",
 107 |         "libmime": "^1.2.0",
 108 |         "libqp": "^1.1.0"
 109 |       }
 110 |     },
 111 |     "callsite": {
 112 |       "version": "1.0.0",
 113 |       "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
 114 |       "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
 115 |     },
 116 |     "charset": {
 117 |       "version": "1.0.1",
 118 |       "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz",
 119 |       "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg=="
 120 |     },
 121 |     "chokidar": {
 122 |       "version": "3.3.1",
 123 |       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
 124 |       "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==",
 125 |       "requires": {
 126 |         "anymatch": "~3.1.1",
 127 |         "braces": "~3.0.2",
 128 |         "fsevents": "~2.1.2",
 129 |         "glob-parent": "~5.1.0",
 130 |         "is-binary-path": "~2.1.0",
 131 |         "is-glob": "~4.0.1",
 132 |         "normalize-path": "~3.0.0",
 133 |         "readdirp": "~3.3.0"
 134 |       }
 135 |     },
 136 |     "clone": {
 137 |       "version": "1.0.4",
 138 |       "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
 139 |       "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
 140 |     },
 141 |     "colors": {
 142 |       "version": "1.1.2",
 143 |       "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
 144 |       "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM="
 145 |     },
 146 |     "component-bind": {
 147 |       "version": "1.0.0",
 148 |       "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
 149 |       "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
 150 |     },
 151 |     "component-emitter": {
 152 |       "version": "1.1.2",
 153 |       "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz",
 154 |       "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM="
 155 |     },
 156 |     "component-inherit": {
 157 |       "version": "0.0.3",
 158 |       "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
 159 |       "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
 160 |     },
 161 |     "conform": {
 162 |       "version": "0.2.12",
 163 |       "resolved": "https://registry.npmjs.org/conform/-/conform-0.2.12.tgz",
 164 |       "integrity": "sha1-cF1u06m+wqne2K0g4SiFJWDwBqI="
 165 |     },
 166 |     "cookie": {
 167 |       "version": "0.3.1",
 168 |       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
 169 |       "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
 170 |     },
 171 |     "core-util-is": {
 172 |       "version": "1.0.2",
 173 |       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
 174 |       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
 175 |     },
 176 |     "cron": {
 177 |       "version": "1.1.1",
 178 |       "resolved": "https://registry.npmjs.org/cron/-/cron-1.1.1.tgz",
 179 |       "integrity": "sha1-AnGdTvSA38juJNgaNgNGC6OQE84=",
 180 |       "requires": {
 181 |         "moment-timezone": "~0.5.5"
 182 |       }
 183 |     },
 184 |     "data.io": {
 185 |       "version": "0.3.0",
 186 |       "resolved": "https://registry.npmjs.org/data.io/-/data.io-0.3.0.tgz",
 187 |       "integrity": "sha1-q1ktf3LjLKBRHX+X1Ipj8R9U7RY=",
 188 |       "requires": {
 189 |         "q": "0.9.x"
 190 |       }
 191 |     },
 192 |     "debug": {
 193 |       "version": "2.3.3",
 194 |       "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
 195 |       "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
 196 |       "requires": {
 197 |         "ms": "0.7.2"
 198 |       }
 199 |     },
 200 |     "deferred-leveldown": {
 201 |       "version": "1.2.2",
 202 |       "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz",
 203 |       "integrity": "sha512-uukrWD2bguRtXilKt6cAWKyoXrTSMo5m7crUdLfWQmu8kIm88w3QZoUL+6nhpfKVmhHANER6Re3sKoNoZ3IKMA==",
 204 |       "requires": {
 205 |         "abstract-leveldown": "~2.6.0"
 206 |       },
 207 |       "dependencies": {
 208 |         "abstract-leveldown": {
 209 |           "version": "2.6.3",
 210 |           "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz",
 211 |           "integrity": "sha512-2++wDf/DYqkPR3o5tbfdhF96EfMApo1GpPfzOsR/ZYXdkSmELlvOOEAl9iKkRsktMPHdGjO4rtkBpf2I7TiTeA==",
 212 |           "requires": {
 213 |             "xtend": "~4.0.0"
 214 |           }
 215 |         }
 216 |       }
 217 |     },
 218 |     "duplexer2": {
 219 |       "version": "0.0.2",
 220 |       "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
 221 |       "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=",
 222 |       "requires": {
 223 |         "readable-stream": "~1.1.9"
 224 |       }
 225 |     },
 226 |     "ecstatic": {
 227 |       "version": "4.1.4",
 228 |       "resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-4.1.4.tgz",
 229 |       "integrity": "sha512-8E4ZLK4uRuB9pwywGpy/B9vcz4gCp6IY7u4cMbeCINr/fjb1v+0wf0Ae2XlfSnG8xZYnE4uaJBjFkYI0bqcIdw==",
 230 |       "requires": {
 231 |         "charset": "^1.0.1",
 232 |         "he": "^1.1.1",
 233 |         "mime": "^2.4.1",
 234 |         "minimist": "^1.1.0",
 235 |         "on-finished": "^2.3.0",
 236 |         "url-join": "^4.0.0"
 237 |       }
 238 |     },
 239 |     "ee-first": {
 240 |       "version": "1.1.1",
 241 |       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
 242 |       "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
 243 |     },
 244 |     "engine.io": {
 245 |       "version": "1.8.2",
 246 |       "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.2.tgz",
 247 |       "integrity": "sha1-a1m+cws0jAElsKRYneHDVavPen4=",
 248 |       "requires": {
 249 |         "accepts": "1.3.3",
 250 |         "base64id": "1.0.0",
 251 |         "cookie": "0.3.1",
 252 |         "debug": "2.3.3",
 253 |         "engine.io-parser": "1.3.2",
 254 |         "ws": "1.1.1"
 255 |       }
 256 |     },
 257 |     "engine.io-client": {
 258 |       "version": "1.8.2",
 259 |       "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.2.tgz",
 260 |       "integrity": "sha1-w4dnVH8qfRhPV1L28K1QEAZwN2Y=",
 261 |       "requires": {
 262 |         "component-emitter": "1.2.1",
 263 |         "component-inherit": "0.0.3",
 264 |         "debug": "2.3.3",
 265 |         "engine.io-parser": "1.3.2",
 266 |         "has-cors": "1.1.0",
 267 |         "indexof": "0.0.1",
 268 |         "parsejson": "0.0.3",
 269 |         "parseqs": "0.0.5",
 270 |         "parseuri": "0.0.5",
 271 |         "ws": "1.1.1",
 272 |         "xmlhttprequest-ssl": "1.5.3",
 273 |         "yeast": "0.1.2"
 274 |       },
 275 |       "dependencies": {
 276 |         "component-emitter": {
 277 |           "version": "1.2.1",
 278 |           "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
 279 |           "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
 280 |         }
 281 |       }
 282 |     },
 283 |     "engine.io-parser": {
 284 |       "version": "1.3.2",
 285 |       "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz",
 286 |       "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=",
 287 |       "requires": {
 288 |         "after": "0.8.2",
 289 |         "arraybuffer.slice": "0.0.6",
 290 |         "base64-arraybuffer": "0.1.5",
 291 |         "blob": "0.0.4",
 292 |         "has-binary": "0.1.7",
 293 |         "wtf-8": "1.0.0"
 294 |       }
 295 |     },
 296 |     "errno": {
 297 |       "version": "0.1.7",
 298 |       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
 299 |       "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
 300 |       "requires": {
 301 |         "prr": "~1.0.1"
 302 |       }
 303 |     },
 304 |     "esprima": {
 305 |       "version": "4.0.1",
 306 |       "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
 307 |       "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
 308 |     },
 309 |     "fill-range": {
 310 |       "version": "7.0.1",
 311 |       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
 312 |       "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
 313 |       "requires": {
 314 |         "to-regex-range": "^5.0.1"
 315 |       }
 316 |     },
 317 |     "fsevents": {
 318 |       "version": "2.1.2",
 319 |       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
 320 |       "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
 321 |       "optional": true
 322 |     },
 323 |     "glob-parent": {
 324 |       "version": "5.1.1",
 325 |       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
 326 |       "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
 327 |       "requires": {
 328 |         "is-glob": "^4.0.1"
 329 |       }
 330 |     },
 331 |     "has-binary": {
 332 |       "version": "0.1.7",
 333 |       "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz",
 334 |       "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=",
 335 |       "requires": {
 336 |         "isarray": "0.0.1"
 337 |       }
 338 |     },
 339 |     "has-cors": {
 340 |       "version": "1.1.0",
 341 |       "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
 342 |       "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
 343 |     },
 344 |     "he": {
 345 |       "version": "1.2.0",
 346 |       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
 347 |       "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
 348 |     },
 349 |     "hyperquest": {
 350 |       "version": "1.3.0",
 351 |       "resolved": "https://registry.npmjs.org/hyperquest/-/hyperquest-1.3.0.tgz",
 352 |       "integrity": "sha1-59WYAwo/wCKbYXJXg7ZBssbxeCo=",
 353 |       "requires": {
 354 |         "duplexer2": "~0.0.2",
 355 |         "through2": "~0.6.3"
 356 |       }
 357 |     },
 358 |     "iconv-lite": {
 359 |       "version": "0.4.24",
 360 |       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
 361 |       "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
 362 |       "requires": {
 363 |         "safer-buffer": ">= 2.1.2 < 3"
 364 |       }
 365 |     },
 366 |     "indexof": {
 367 |       "version": "0.0.1",
 368 |       "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
 369 |       "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
 370 |     },
 371 |     "inherits": {
 372 |       "version": "2.0.4",
 373 |       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
 374 |       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
 375 |     },
 376 |     "ip-address": {
 377 |       "version": "6.1.0",
 378 |       "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-6.1.0.tgz",
 379 |       "integrity": "sha512-u9YYtb1p2fWSbzpKmZ/b3QXWA+diRYPxc2c4y5lFB/MMk5WZ7wNZv8S3CFcIGVJ5XtlaCAl/FQy/D3eQ2XtdOA==",
 380 |       "requires": {
 381 |         "jsbn": "1.1.0",
 382 |         "lodash": "^4.17.15",
 383 |         "sprintf-js": "1.1.2"
 384 |       },
 385 |       "dependencies": {
 386 |         "sprintf-js": {
 387 |           "version": "1.1.2",
 388 |           "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
 389 |           "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
 390 |         }
 391 |       }
 392 |     },
 393 |     "is-binary-path": {
 394 |       "version": "2.1.0",
 395 |       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
 396 |       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
 397 |       "requires": {
 398 |         "binary-extensions": "^2.0.0"
 399 |       }
 400 |     },
 401 |     "is-extglob": {
 402 |       "version": "2.1.1",
 403 |       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
 404 |       "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
 405 |     },
 406 |     "is-glob": {
 407 |       "version": "4.0.1",
 408 |       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
 409 |       "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
 410 |       "requires": {
 411 |         "is-extglob": "^2.1.1"
 412 |       }
 413 |     },
 414 |     "is-number": {
 415 |       "version": "7.0.0",
 416 |       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
 417 |       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
 418 |     },
 419 |     "isarray": {
 420 |       "version": "0.0.1",
 421 |       "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
 422 |       "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
 423 |     },
 424 |     "js-yaml": {
 425 |       "version": "3.13.1",
 426 |       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
 427 |       "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
 428 |       "requires": {
 429 |         "argparse": "^1.0.7",
 430 |         "esprima": "^4.0.0"
 431 |       }
 432 |     },
 433 |     "jsbn": {
 434 |       "version": "1.1.0",
 435 |       "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
 436 |       "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA="
 437 |     },
 438 |     "json3": {
 439 |       "version": "3.3.2",
 440 |       "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
 441 |       "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE="
 442 |     },
 443 |     "junk": {
 444 |       "version": "1.0.3",
 445 |       "resolved": "https://registry.npmjs.org/junk/-/junk-1.0.3.tgz",
 446 |       "integrity": "sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI="
 447 |     },
 448 |     "level-codec": {
 449 |       "version": "7.0.1",
 450 |       "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-7.0.1.tgz",
 451 |       "integrity": "sha512-Ua/R9B9r3RasXdRmOtd+t9TCOEIIlts+TN/7XTT2unhDaL6sJn83S3rUyljbr6lVtw49N3/yA0HHjpV6Kzb2aQ=="
 452 |     },
 453 |     "level-concat-iterator": {
 454 |       "version": "2.0.1",
 455 |       "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz",
 456 |       "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw=="
 457 |     },
 458 |     "level-errors": {
 459 |       "version": "1.0.5",
 460 |       "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-1.0.5.tgz",
 461 |       "integrity": "sha512-/cLUpQduF6bNrWuAC4pwtUKA5t669pCsCi2XbmojG2tFeOr9j6ShtdDCtFFQO1DRt+EVZhx9gPzP9G2bUaG4ig==",
 462 |       "requires": {
 463 |         "errno": "~0.1.1"
 464 |       }
 465 |     },
 466 |     "level-iterator-stream": {
 467 |       "version": "1.3.1",
 468 |       "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz",
 469 |       "integrity": "sha1-5Dt4sagUPm+pek9IXrjqUwNS8u0=",
 470 |       "requires": {
 471 |         "inherits": "^2.0.1",
 472 |         "level-errors": "^1.0.3",
 473 |         "readable-stream": "^1.0.33",
 474 |         "xtend": "^4.0.0"
 475 |       }
 476 |     },
 477 |     "level-supports": {
 478 |       "version": "1.0.1",
 479 |       "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz",
 480 |       "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==",
 481 |       "requires": {
 482 |         "xtend": "^4.0.2"
 483 |       }
 484 |     },
 485 |     "leveldown": {
 486 |       "version": "5.5.1",
 487 |       "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.5.1.tgz",
 488 |       "integrity": "sha512-GoC455/ncfg4yLLItr192HuXpA+CcQ2q9GncXJhewvvlpsBBEegChn5tMPP+kGvJt7u2LuXAd8fY2moQxFD+sQ==",
 489 |       "requires": {
 490 |         "abstract-leveldown": "~6.2.1",
 491 |         "napi-macros": "~2.0.0",
 492 |         "node-gyp-build": "~4.1.0"
 493 |       }
 494 |     },
 495 |     "levelup": {
 496 |       "version": "1.3.9",
 497 |       "resolved": "https://registry.npmjs.org/levelup/-/levelup-1.3.9.tgz",
 498 |       "integrity": "sha512-VVGHfKIlmw8w1XqpGOAGwq6sZm2WwWLmlDcULkKWQXEA5EopA8OBNJ2Ck2v6bdk8HeEZSbCSEgzXadyQFm76sQ==",
 499 |       "requires": {
 500 |         "deferred-leveldown": "~1.2.1",
 501 |         "level-codec": "~7.0.0",
 502 |         "level-errors": "~1.0.3",
 503 |         "level-iterator-stream": "~1.3.0",
 504 |         "prr": "~1.0.1",
 505 |         "semver": "~5.4.1",
 506 |         "xtend": "~4.0.0"
 507 |       }
 508 |     },
 509 |     "libbase64": {
 510 |       "version": "0.1.0",
 511 |       "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz",
 512 |       "integrity": "sha1-YjUag5VjrF/1vSbxL2Dpgwu3UeY="
 513 |     },
 514 |     "libmime": {
 515 |       "version": "1.2.0",
 516 |       "resolved": "https://registry.npmjs.org/libmime/-/libmime-1.2.0.tgz",
 517 |       "integrity": "sha1-jYS087Ils3BEECNu9JSQZDa6dCs=",
 518 |       "requires": {
 519 |         "iconv-lite": "^0.4.13",
 520 |         "libbase64": "^0.1.0",
 521 |         "libqp": "^1.1.0"
 522 |       }
 523 |     },
 524 |     "libqp": {
 525 |       "version": "1.1.0",
 526 |       "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz",
 527 |       "integrity": "sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g="
 528 |     },
 529 |     "lodash": {
 530 |       "version": "4.17.21",
 531 |       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
 532 |       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
 533 |     },
 534 |     "mime": {
 535 |       "version": "2.4.4",
 536 |       "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
 537 |       "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
 538 |     },
 539 |     "mime-db": {
 540 |       "version": "1.43.0",
 541 |       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
 542 |       "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
 543 |     },
 544 |     "mime-types": {
 545 |       "version": "2.1.26",
 546 |       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
 547 |       "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
 548 |       "requires": {
 549 |         "mime-db": "1.43.0"
 550 |       }
 551 |     },
 552 |     "minimist": {
 553 |       "version": "1.2.5",
 554 |       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
 555 |       "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
 556 |     },
 557 |     "moment": {
 558 |       "version": "2.24.0",
 559 |       "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
 560 |       "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
 561 |     },
 562 |     "moment-timezone": {
 563 |       "version": "0.5.28",
 564 |       "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.28.tgz",
 565 |       "integrity": "sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw==",
 566 |       "requires": {
 567 |         "moment": ">= 2.9.0"
 568 |       }
 569 |     },
 570 |     "ms": {
 571 |       "version": "0.7.2",
 572 |       "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
 573 |       "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U="
 574 |     },
 575 |     "napi-macros": {
 576 |       "version": "2.0.0",
 577 |       "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz",
 578 |       "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg=="
 579 |     },
 580 |     "nci": {
 581 |       "version": "1.0.26",
 582 |       "resolved": "https://registry.npmjs.org/nci/-/nci-1.0.26.tgz",
 583 |       "integrity": "sha512-yV8/DDj2lt44moJcK9yKMRi58b2mMpSX3vzRiWYQtftctNRwIw0OXspRIRCVZ+o7j6w53TT0VssJebRThwfkTQ==",
 584 |       "requires": {
 585 |         "colors": "1.1.2",
 586 |         "conform": "0.2.12",
 587 |         "junk": "1.0.3",
 588 |         "nlevel": "1.0.4",
 589 |         "through": "2.3.6",
 590 |         "tree-kill": "1.2.2",
 591 |         "twostep": "0.4.2",
 592 |         "underscore": "1.8.3"
 593 |       }
 594 |     },
 595 |     "nci-ansible-ui": {
 596 |       "version": "1.1.2",
 597 |       "resolved": "https://registry.npmjs.org/nci-ansible-ui/-/nci-ansible-ui-1.1.2.tgz",
 598 |       "integrity": "sha512-hLuXfZlUqIv1YlW6/PQ3YeycQBVt0q0YKVxauz/cyk6JnXTUfpgTodpv60YD6TkPjtOuqMBU6DVnToXh9dfP9w==",
 599 |       "requires": {
 600 |         "data.io": "0.3.0",
 601 |         "socket.io": "1.7.2",
 602 |         "twostep": "0.4.2",
 603 |         "underscore": "1.8.3"
 604 |       }
 605 |     },
 606 |     "nci-mail-notification": {
 607 |       "version": "1.1.2",
 608 |       "resolved": "https://registry.npmjs.org/nci-mail-notification/-/nci-mail-notification-1.1.2.tgz",
 609 |       "integrity": "sha1-q3NDxf1JK1NCrwk/pHSwwG/OPDw=",
 610 |       "requires": {
 611 |         "nodemailer": "1.4.0",
 612 |         "twostep": "0.4.1",
 613 |         "underscore": "1.8.3"
 614 |       },
 615 |       "dependencies": {
 616 |         "twostep": {
 617 |           "version": "0.4.1",
 618 |           "resolved": "https://registry.npmjs.org/twostep/-/twostep-0.4.1.tgz",
 619 |           "integrity": "sha1-nUq6CHGA3aiWHFXcx0pQYQIq63k="
 620 |         }
 621 |       }
 622 |     },
 623 |     "nci-projects-reloader": {
 624 |       "version": "2.0.0",
 625 |       "resolved": "https://registry.npmjs.org/nci-projects-reloader/-/nci-projects-reloader-2.0.0.tgz",
 626 |       "integrity": "sha512-Wx6O5JJuYGc6g5B4yBtx76aVfvSxWLm7XEOpIsnnIa/Rxv+CG6FHBjDwEItvF6bZEqmxIQ0OLLZP21onE2bIWw==",
 627 |       "requires": {
 628 |         "chokidar": "3.3.1"
 629 |       }
 630 |     },
 631 |     "nci-rest-api-server": {
 632 |       "version": "2.0.0",
 633 |       "resolved": "https://registry.npmjs.org/nci-rest-api-server/-/nci-rest-api-server-2.0.0.tgz",
 634 |       "integrity": "sha512-gtJ6AjRIvUzUCNCmVg0nF0hQSCO9zTANnMTjgHSZie0T9ucgX7exrDuLHknUvmUUkCRfKouo9Bo0WYnanQpgnQ==",
 635 |       "requires": {
 636 |         "twostep": "0.4.1",
 637 |         "underscore": "1.8.3"
 638 |       },
 639 |       "dependencies": {
 640 |         "twostep": {
 641 |           "version": "0.4.1",
 642 |           "resolved": "https://registry.npmjs.org/twostep/-/twostep-0.4.1.tgz",
 643 |           "integrity": "sha1-nUq6CHGA3aiWHFXcx0pQYQIq63k="
 644 |         }
 645 |       }
 646 |     },
 647 |     "nci-scheduler": {
 648 |       "version": "1.2.0",
 649 |       "resolved": "https://registry.npmjs.org/nci-scheduler/-/nci-scheduler-1.2.0.tgz",
 650 |       "integrity": "sha512-PFeEHiv3xMv77VFl4W0PcMR4oj6Az2OWWRS2qu1Qg8ppQNkyjBtyCDF6C5LR11s7Gge4gxEoT5L3QMbQQuLhFg==",
 651 |       "requires": {
 652 |         "cron": "1.1.1"
 653 |       }
 654 |     },
 655 |     "nci-static-server": {
 656 |       "version": "2.0.0",
 657 |       "resolved": "https://registry.npmjs.org/nci-static-server/-/nci-static-server-2.0.0.tgz",
 658 |       "integrity": "sha512-bkuZB4QupErad65zD66LrLDKylO/f8p7ixd+XoRVYx4q07pHMY4pDTwoSXH29Niiy7fCFL/67TwX0QTike+tew==",
 659 |       "requires": {
 660 |         "ecstatic": "4.1.4",
 661 |         "underscore": "1.8.3"
 662 |       }
 663 |     },
 664 |     "nci-telegram-notification": {
 665 |       "version": "1.2.2",
 666 |       "resolved": "https://registry.npmjs.org/nci-telegram-notification/-/nci-telegram-notification-1.2.2.tgz",
 667 |       "integrity": "sha512-oQab/bTvoz1YffV4JicFLWtbThDpq+kiIrOp139bXUcBi6b5Yayv36puYMrUAsBse37/InK1MriNJ1Csur0ntQ==",
 668 |       "requires": {
 669 |         "socks5-https-client": "1.2.1",
 670 |         "twostep": "0.4.1",
 671 |         "underscore": "1.8.3"
 672 |       },
 673 |       "dependencies": {
 674 |         "twostep": {
 675 |           "version": "0.4.1",
 676 |           "resolved": "https://registry.npmjs.org/twostep/-/twostep-0.4.1.tgz",
 677 |           "integrity": "sha1-nUq6CHGA3aiWHFXcx0pQYQIq63k="
 678 |         }
 679 |       }
 680 |     },
 681 |     "nci-yaml-reader": {
 682 |       "version": "1.2.2",
 683 |       "resolved": "https://registry.npmjs.org/nci-yaml-reader/-/nci-yaml-reader-1.2.2.tgz",
 684 |       "integrity": "sha512-92BrT2u8Mng2+9D5aGWZvVI/BRF358kh65WR0KiIo+iiUNnb0z3F/vWu7yFK7BRMiO0WPa8tEGBwoVQ9OIz3+g==",
 685 |       "requires": {
 686 |         "js-yaml": "3.13.1",
 687 |         "twostep": "0.4.1",
 688 |         "underscore": "1.8.3"
 689 |       },
 690 |       "dependencies": {
 691 |         "twostep": {
 692 |           "version": "0.4.1",
 693 |           "resolved": "https://registry.npmjs.org/twostep/-/twostep-0.4.1.tgz",
 694 |           "integrity": "sha1-nUq6CHGA3aiWHFXcx0pQYQIq63k="
 695 |         }
 696 |       }
 697 |     },
 698 |     "negotiator": {
 699 |       "version": "0.6.1",
 700 |       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
 701 |       "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
 702 |     },
 703 |     "nlevel": {
 704 |       "version": "1.0.4",
 705 |       "resolved": "https://registry.npmjs.org/nlevel/-/nlevel-1.0.4.tgz",
 706 |       "integrity": "sha1-z1hNEamWNSOLUsP5omYCbmXs65g=",
 707 |       "requires": {
 708 |         "levelup": "1.3.9"
 709 |       }
 710 |     },
 711 |     "node-gyp-build": {
 712 |       "version": "4.1.1",
 713 |       "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz",
 714 |       "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ=="
 715 |     },
 716 |     "nodemailer": {
 717 |       "version": "1.4.0",
 718 |       "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.4.0.tgz",
 719 |       "integrity": "sha1-750OFhHsRQOeZOTdudsVEmQqjUI=",
 720 |       "requires": {
 721 |         "buildmail": "^1.2.4",
 722 |         "hyperquest": "^1.2.0",
 723 |         "libmime": "^1.0.0",
 724 |         "nodemailer-direct-transport": "^1.0.2",
 725 |         "nodemailer-smtp-transport": "^1.0.3"
 726 |       }
 727 |     },
 728 |     "nodemailer-direct-transport": {
 729 |       "version": "1.1.0",
 730 |       "resolved": "https://registry.npmjs.org/nodemailer-direct-transport/-/nodemailer-direct-transport-1.1.0.tgz",
 731 |       "integrity": "sha1-oveHCO5vFuoFc/yClJ0Tj/Fy9iQ=",
 732 |       "requires": {
 733 |         "smtp-connection": "^1.3.1"
 734 |       }
 735 |     },
 736 |     "nodemailer-smtp-transport": {
 737 |       "version": "1.1.0",
 738 |       "resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-1.1.0.tgz",
 739 |       "integrity": "sha1-5sN/MYhaswgOfe089SjErX5pE5g=",
 740 |       "requires": {
 741 |         "clone": "^1.0.2",
 742 |         "nodemailer-wellknown": "^0.1.7",
 743 |         "smtp-connection": "^1.3.7"
 744 |       }
 745 |     },
 746 |     "nodemailer-wellknown": {
 747 |       "version": "0.1.10",
 748 |       "resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz",
 749 |       "integrity": "sha1-WG24EB2zDLRDjrVGc3pBqtDPE9U="
 750 |     },
 751 |     "normalize-path": {
 752 |       "version": "3.0.0",
 753 |       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
 754 |       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
 755 |     },
 756 |     "object-assign": {
 757 |       "version": "4.1.0",
 758 |       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz",
 759 |       "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A="
 760 |     },
 761 |     "object-component": {
 762 |       "version": "0.0.3",
 763 |       "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
 764 |       "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
 765 |     },
 766 |     "on-finished": {
 767 |       "version": "2.3.0",
 768 |       "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
 769 |       "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
 770 |       "requires": {
 771 |         "ee-first": "1.1.1"
 772 |       }
 773 |     },
 774 |     "options": {
 775 |       "version": "0.0.6",
 776 |       "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
 777 |       "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8="
 778 |     },
 779 |     "parsejson": {
 780 |       "version": "0.0.3",
 781 |       "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",
 782 |       "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=",
 783 |       "requires": {
 784 |         "better-assert": "~1.0.0"
 785 |       }
 786 |     },
 787 |     "parseqs": {
 788 |       "version": "0.0.5",
 789 |       "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
 790 |       "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
 791 |       "requires": {
 792 |         "better-assert": "~1.0.0"
 793 |       }
 794 |     },
 795 |     "parseuri": {
 796 |       "version": "0.0.5",
 797 |       "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
 798 |       "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
 799 |       "requires": {
 800 |         "better-assert": "~1.0.0"
 801 |       }
 802 |     },
 803 |     "picomatch": {
 804 |       "version": "2.2.2",
 805 |       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
 806 |       "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
 807 |     },
 808 |     "prr": {
 809 |       "version": "1.0.1",
 810 |       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
 811 |       "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
 812 |     },
 813 |     "q": {
 814 |       "version": "0.9.7",
 815 |       "resolved": "https://registry.npmjs.org/q/-/q-0.9.7.tgz",
 816 |       "integrity": "sha1-TeLmyzspCIyeTLwDv51C+5bOL3U="
 817 |     },
 818 |     "readable-stream": {
 819 |       "version": "1.1.14",
 820 |       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
 821 |       "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
 822 |       "requires": {
 823 |         "core-util-is": "~1.0.0",
 824 |         "inherits": "~2.0.1",
 825 |         "isarray": "0.0.1",
 826 |         "string_decoder": "~0.10.x"
 827 |       }
 828 |     },
 829 |     "readdirp": {
 830 |       "version": "3.3.0",
 831 |       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
 832 |       "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==",
 833 |       "requires": {
 834 |         "picomatch": "^2.0.7"
 835 |       }
 836 |     },
 837 |     "safer-buffer": {
 838 |       "version": "2.1.2",
 839 |       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
 840 |       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
 841 |     },
 842 |     "semver": {
 843 |       "version": "5.4.1",
 844 |       "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
 845 |       "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
 846 |     },
 847 |     "smtp-connection": {
 848 |       "version": "1.3.8",
 849 |       "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.8.tgz",
 850 |       "integrity": "sha1-VYMsIWDPswhuHc2H/RwZ+mG39TY="
 851 |     },
 852 |     "socket.io": {
 853 |       "version": "1.7.2",
 854 |       "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.2.tgz",
 855 |       "integrity": "sha1-g7u98ueSY7N4kA2kA+eEPgXcO3E=",
 856 |       "requires": {
 857 |         "debug": "2.3.3",
 858 |         "engine.io": "1.8.2",
 859 |         "has-binary": "0.1.7",
 860 |         "object-assign": "4.1.0",
 861 |         "socket.io-adapter": "0.5.0",
 862 |         "socket.io-client": "1.7.2",
 863 |         "socket.io-parser": "2.3.1"
 864 |       }
 865 |     },
 866 |     "socket.io-adapter": {
 867 |       "version": "0.5.0",
 868 |       "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz",
 869 |       "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=",
 870 |       "requires": {
 871 |         "debug": "2.3.3",
 872 |         "socket.io-parser": "2.3.1"
 873 |       }
 874 |     },
 875 |     "socket.io-client": {
 876 |       "version": "1.7.2",
 877 |       "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.2.tgz",
 878 |       "integrity": "sha1-Of2ww91FDjIbfkDP2DYS7FM91kQ=",
 879 |       "requires": {
 880 |         "backo2": "1.0.2",
 881 |         "component-bind": "1.0.0",
 882 |         "component-emitter": "1.2.1",
 883 |         "debug": "2.3.3",
 884 |         "engine.io-client": "1.8.2",
 885 |         "has-binary": "0.1.7",
 886 |         "indexof": "0.0.1",
 887 |         "object-component": "0.0.3",
 888 |         "parseuri": "0.0.5",
 889 |         "socket.io-parser": "2.3.1",
 890 |         "to-array": "0.1.4"
 891 |       },
 892 |       "dependencies": {
 893 |         "component-emitter": {
 894 |           "version": "1.2.1",
 895 |           "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
 896 |           "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
 897 |         }
 898 |       }
 899 |     },
 900 |     "socket.io-parser": {
 901 |       "version": "2.3.1",
 902 |       "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz",
 903 |       "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=",
 904 |       "requires": {
 905 |         "component-emitter": "1.1.2",
 906 |         "debug": "2.2.0",
 907 |         "isarray": "0.0.1",
 908 |         "json3": "3.3.2"
 909 |       },
 910 |       "dependencies": {
 911 |         "debug": {
 912 |           "version": "2.2.0",
 913 |           "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
 914 |           "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
 915 |           "requires": {
 916 |             "ms": "0.7.1"
 917 |           }
 918 |         },
 919 |         "ms": {
 920 |           "version": "0.7.1",
 921 |           "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
 922 |           "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
 923 |         }
 924 |       }
 925 |     },
 926 |     "socks5-client": {
 927 |       "version": "1.2.8",
 928 |       "resolved": "https://registry.npmjs.org/socks5-client/-/socks5-client-1.2.8.tgz",
 929 |       "integrity": "sha512-js8WqQ/JjZS3IQwUxRwSThvXzcRIHE8sde8nE5q7nqxiFGb8EoHmNJ9SF2lXqn3ux6pUV3+InH7ng7mANK6XfA==",
 930 |       "requires": {
 931 |         "ip-address": "~6.1.0"
 932 |       }
 933 |     },
 934 |     "socks5-https-client": {
 935 |       "version": "1.2.1",
 936 |       "resolved": "https://registry.npmjs.org/socks5-https-client/-/socks5-https-client-1.2.1.tgz",
 937 |       "integrity": "sha512-FbZ/X/2Xq3DAMhuRA4bnN0jy1QxaPTVPLFvyv6CEj0QDKSTdWp9yRxo1JhqXmWKhPQeJyUMajHJB2UjT43pFcw==",
 938 |       "requires": {
 939 |         "socks5-client": "~1.2.3"
 940 |       }
 941 |     },
 942 |     "sprintf-js": {
 943 |       "version": "1.0.3",
 944 |       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
 945 |       "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
 946 |     },
 947 |     "string_decoder": {
 948 |       "version": "0.10.31",
 949 |       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
 950 |       "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
 951 |     },
 952 |     "through": {
 953 |       "version": "2.3.6",
 954 |       "resolved": "https://registry.npmjs.org/through/-/through-2.3.6.tgz",
 955 |       "integrity": "sha1-JmgcD1JGcQIdTinffDa84tDs8ug="
 956 |     },
 957 |     "through2": {
 958 |       "version": "0.6.5",
 959 |       "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz",
 960 |       "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=",
 961 |       "requires": {
 962 |         "readable-stream": ">=1.0.33-1 <1.1.0-0",
 963 |         "xtend": ">=4.0.0 <4.1.0-0"
 964 |       },
 965 |       "dependencies": {
 966 |         "readable-stream": {
 967 |           "version": "1.0.34",
 968 |           "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
 969 |           "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
 970 |           "requires": {
 971 |             "core-util-is": "~1.0.0",
 972 |             "inherits": "~2.0.1",
 973 |             "isarray": "0.0.1",
 974 |             "string_decoder": "~0.10.x"
 975 |           }
 976 |         }
 977 |       }
 978 |     },
 979 |     "to-array": {
 980 |       "version": "0.1.4",
 981 |       "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
 982 |       "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
 983 |     },
 984 |     "to-regex-range": {
 985 |       "version": "5.0.1",
 986 |       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
 987 |       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
 988 |       "requires": {
 989 |         "is-number": "^7.0.0"
 990 |       }
 991 |     },
 992 |     "tree-kill": {
 993 |       "version": "1.2.2",
 994 |       "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
 995 |       "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
 996 |     },
 997 |     "twostep": {
 998 |       "version": "0.4.2",
 999 |       "resolved": "https://registry.npmjs.org/twostep/-/twostep-0.4.2.tgz",
1000 |       "integrity": "sha1-hLxQh6hxV00ev3vjB54D4SLUFbY="
1001 |     },
1002 |     "ultron": {
1003 |       "version": "1.0.2",
1004 |       "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
1005 |       "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po="
1006 |     },
1007 |     "underscore": {
1008 |       "version": "1.8.3",
1009 |       "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
1010 |       "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
1011 |     },
1012 |     "url-join": {
1013 |       "version": "4.0.1",
1014 |       "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
1015 |       "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
1016 |     },
1017 |     "ws": {
1018 |       "version": "1.1.1",
1019 |       "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.1.tgz",
1020 |       "integrity": "sha1-CC3bbGQehdS7RR8D1S8G6r2x8Bg=",
1021 |       "requires": {
1022 |         "options": ">=0.0.5",
1023 |         "ultron": "1.0.x"
1024 |       }
1025 |     },
1026 |     "wtf-8": {
1027 |       "version": "1.0.0",
1028 |       "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz",
1029 |       "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo="
1030 |     },
1031 |     "xmlhttprequest-ssl": {
1032 |       "version": "1.5.3",
1033 |       "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz",
1034 |       "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0="
1035 |     },
1036 |     "xtend": {
1037 |       "version": "4.0.2",
1038 |       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
1039 |       "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
1040 |     },
1041 |     "yeast": {
1042 |       "version": "0.1.2",
1043 |       "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
1044 |       "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
1045 |     }
1046 |   }
1047 | }
1048 | 


--------------------------------------------------------------------------------
/docker/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "nci-ansible-ui-docker-installation",
 3 |   "version": "0.1.0",
 4 |   "description": "nci-ansible-ui deployment inside docker image",
 5 |   "repository": {
 6 |     "type": "git",
 7 |     "url": "https://github.com/node-ci/nci-ansible-ui.git"
 8 |   },
 9 |   "author": "Oleg Korobenko ",
10 |   "license": "MIT",
11 |   "bugs": {
12 |     "url": "https://github.com/node-ci/nci-ansible-ui/issues"
13 |   },
14 |   "homepage": "https://github.com/node-ci/nci-ansible-ui",
15 |   "dependencies": {
16 |     "leveldown": "5.5.1",
17 |     "nci": "1.0.26",
18 |     "nci-ansible-ui": "1.1.2",
19 |     "nci-mail-notification": "1.1.2",
20 |     "nci-projects-reloader": "2.0.0",
21 |     "nci-rest-api-server": "2.0.0",
22 |     "nci-scheduler": "1.2.0",
23 |     "nci-static-server": "2.0.0",
24 |     "nci-telegram-notification": "1.2.2",
25 |     "nci-yaml-reader": "1.2.2"
26 |   }
27 | }
28 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "nci-ansible-ui",
 3 |   "version": "1.1.2",
 4 |   "description": "Simple web interface for run ansible playbooks",
 5 |   "main": "app.js",
 6 |   "scripts": {
 7 |     "test": "echo \"Error: no test specified\" && exit 1",
 8 |     "build-less": "lessc app/styles/index.less > static/css/index.css",
 9 |     "build-js": "browserify  app/app.js -t ./transforms/jade.js | uglifyjs -mc > static/js/app.build.js",
10 |     "watch-less": "chokidar --initial --silent app/styles/**/*.less app/styles/index.less -c 'npm run build-less'",
11 |     "watch-js": "watchify app/app.js -t ./transforms/jade.js -o static/js/app.build.js -dv",
12 |     "_init-dev": "touch static/js/app.build.js && touch static/css/index.css",
13 |     "dev": "test -z $NODE_ENV && export NODE_ENV=development; nrun _init-dev && parallelshell 'nrun build-fonts' 'nrun watch-less' 'nrun watch-js' 'nrun build-html'",
14 |     "sync": "npm install && npm prune",
15 |     "lint": "jshint ./",
16 |     "build-fonts": "mkdir -p static/fonts/ && parallelshell 'cp ./node_modules/bootstrap/fonts/* ./static/fonts/' 'cp ./node_modules/font-awesome/fonts/* ./static/fonts/' 'cp ./node_modules/scmicons/fonts/* ./static/fonts/'",
17 |     "build-clean": "rm static/index.html",
18 |     "build-html": "jade views/index.jade --obj \"{'env': '$NODE_ENV'}\" -o static/",
19 |     "build": "test -z $NODE_ENV && export NODE_ENV=production; nrun build-fonts && nrun build-js && nrun build-less && nrun build-html",
20 |     "_build-docker-image": "if [ \"$imageTag\" = \"\" ]; then echo \"imageTag is not set\"; exit 1; fi; docker build --tag okvd/nci-ansible-ui:$imageTag ./docker/",
21 |     "build-docker-image": "export imageTag=$npm_package_version && npm run _build-docker-image",
22 |     "build-docker-image-alpha": "export imageTag=$npm_package_version-alphaimage && npm run _build-docker-image",
23 |     "build-docker-image-latest": "export imageTag=latest && npm run _build-docker-image"
24 |   },
25 |   "repository": {
26 |     "type": "git",
27 |     "url": "https://github.com/node-ci/nci-ansible-ui.git"
28 |   },
29 |   "keywords": [
30 |     "nciplugin",
31 |     "ui",
32 |     "interface",
33 |     "ansible",
34 |     "playbook"
35 |   ],
36 |   "contributors": [
37 |     {
38 |       "name": "Oleg Korobenko",
39 |       "email": "oleg.korobenko@gmail.com"
40 |     }
41 |   ],
42 |   "license": "MIT",
43 |   "bugs": {
44 |     "url": "https://github.com/node-ci/nci-ansible-ui/issues"
45 |   },
46 |   "homepage": "https://github.com/node-ci/nci-ansible-ui",
47 |   "dependencies": {
48 |     "data.io": "0.3.0",
49 |     "socket.io": "1.7.2",
50 |     "twostep": "0.4.2",
51 |     "underscore": "1.8.3"
52 |   },
53 |   "devDependencies": {
54 |     "ansi_up": "1.3.0",
55 |     "bootstrap": "3.3.4",
56 |     "browserify": "13.0.0",
57 |     "chokidar-cli": "1.2.0",
58 |     "font-awesome": "4.3.0",
59 |     "jade": "1.11.0",
60 |     "jshint": "2.4.4",
61 |     "less": "3.7.1",
62 |     "moment": "2.10.6",
63 |     "nrun": "0.1.4",
64 |     "parallelshell": "3.0.2",
65 |     "react": "0.14.3",
66 |     "react-dom": "0.14.3",
67 |     "react-jade": "2.5.0",
68 |     "react-router": "0.13.5",
69 |     "reflux": "0.2.7",
70 |     "scmicons": "1.3.0",
71 |     "simple-scrolltop": "1.0.1",
72 |     "socket.io-client": "1.7.2",
73 |     "through": "2.3.8",
74 |     "uglifyjs": "2.4.10",
75 |     "watchify": "3.6.1"
76 |   },
77 |   "peerDependencies": {
78 |     "nci": ">=1.0.1 <1.1.0"
79 |   }
80 | }
81 | 


--------------------------------------------------------------------------------
/resources/builds.js:
--------------------------------------------------------------------------------
  1 | 'use strict';
  2 | 
  3 | var Steppy = require('twostep').Steppy,
  4 | 	_ = require('underscore');
  5 | 
  6 | module.exports = function(app) {
  7 | 	var logger = app.lib.logger('builds resource'),
  8 | 		resource = app.dataio.resource('builds');
  9 | 
 10 | 	resource.use('readAll', function(req, res, next) {
 11 | 		Steppy(
 12 | 			function() {
 13 | 				var data = req.data || {},
 14 | 					getParams = {limit: Number(data.limit) || 20};
 15 | 
 16 | 				if (data.projectName) {
 17 | 					getParams.projectName = data.projectName;
 18 | 				}
 19 | 
 20 | 				app.builds.getRecent(getParams, this.slot());
 21 | 			},
 22 | 			function(err, builds) {
 23 | 				// omit big fields not needed for list
 24 | 				_(builds).each(function(build) {
 25 | 					delete build.stepTimings;
 26 | 					if (build.scm) {
 27 | 						delete build.scm.changes;
 28 | 					}
 29 | 					build.project = _(build.project).pick(
 30 | 						'name', 'scm', 'avgBuildDuration'
 31 | 					);
 32 | 				});
 33 | 
 34 | 				res.send(builds);
 35 | 			},
 36 | 			next
 37 | 		);
 38 | 	});
 39 | 
 40 | 	resource.use('read', function(req, res, next) {
 41 | 		Steppy(
 42 | 			function() {
 43 | 				app.builds.get(req.data.id, this.slot());
 44 | 			},
 45 | 			function(err, build) {
 46 | 				res.send(build);
 47 | 			},
 48 | 			next
 49 | 		);
 50 | 	});
 51 | 
 52 | 	resource.use('getBuildLogTail', function(req, res, next) {
 53 | 		Steppy(
 54 | 			function() {
 55 | 				app.builds.getLogLinesTail({
 56 | 					buildId: req.data.buildId,
 57 | 					limit: req.data.length
 58 | 				}, this.slot());
 59 | 			},
 60 | 			function(err, tail) {
 61 | 				res.send(tail);
 62 | 			},
 63 | 			next
 64 | 		);
 65 | 	});
 66 | 
 67 | 	resource.use('getBuildLogLines', function(req, res, next) {
 68 | 		Steppy(
 69 | 			function() {
 70 | 				app.builds.getLogLines(
 71 | 					_(req.data).pick('buildId', 'from', 'to'),
 72 | 					this.slot()
 73 | 				);
 74 | 			},
 75 | 			function(err, logLinesData) {
 76 | 				res.send(logLinesData);
 77 | 			},
 78 | 			next
 79 | 		);
 80 | 	});
 81 | 
 82 | 	resource.use('cancel', function(req, res, next) {
 83 | 		Steppy(
 84 | 			function() {
 85 | 				var buildId = req.data.buildId;
 86 | 				logger.log('Cancel build: "%s"', buildId);
 87 | 				app.builds.cancel({
 88 | 					buildId: buildId,
 89 | 					canceledBy: {type: 'user'}
 90 | 				}, this.slot());
 91 | 			},
 92 | 			function() {
 93 | 				res.send();
 94 | 			},
 95 | 			next
 96 | 		);
 97 | 	});
 98 | 
 99 | 	return resource;
100 | };
101 | 


--------------------------------------------------------------------------------
/resources/errorHandler.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | 
 3 | module.exports = function(app) {
 4 | 	var logger = app.lib.logger('resources error handler');
 5 | 
 6 | 	return function(err, req, res, next) {
 7 | 		logger.error(
 8 | 			'Error is occurred during requesting ' +
 9 | 			req.resource.namespace.name + ' ' + req.action + ':',
10 | 			err.stack || err
11 | 		);
12 | 	};
13 | };
14 | 


--------------------------------------------------------------------------------
/resources/helpers.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | 
 3 | var Steppy = require('twostep').Steppy;
 4 | 
 5 | var buildDataResourcesHash = {};
 6 | 
 7 | // create resource for build data
 8 | exports.createBuildDataResource = function(app, buildId) {
 9 | 	if (buildId in buildDataResourcesHash) {
10 | 		return;
11 | 	}
12 | 	var buildDataResource = app.dataio.resource('build' + buildId);
13 | 	buildDataResource.on('connection', function(client) {
14 | 		var callback = this.async();
15 | 		Steppy(
16 | 			function() {
17 | 				app.builds.getLogLines({buildId: buildId}, this.slot());
18 | 			},
19 | 			function(err, logLinesData) {
20 | 				client.emit('sync', 'data', {lines: logLinesData.lines});
21 | 
22 | 				this.pass(null);
23 | 			},
24 | 			function(err) {
25 | 				if (err) {
26 | 					var logger = app.lib.logger('create build resource');
27 | 					logger.error(
28 | 						'error during read log for "' + buildId + '":',
29 | 						err.stack || err
30 | 					);
31 | 				}
32 | 				callback();
33 | 			}
34 | 		);
35 | 	});
36 | 	buildDataResourcesHash[buildId] = buildDataResource;
37 | };
38 | 


--------------------------------------------------------------------------------
/resources/index.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | 
 3 | var _ = require('underscore'),
 4 | 	errorHandler = require('./errorHandler'),
 5 | 	helpers = require('./helpers');
 6 | 
 7 | module.exports = function(app) {
 8 | 	_(['builds', 'projects']).each(function(resourceName) {
 9 | 		var resource = require('./' + resourceName)(app);
10 | 		resource.use(errorHandler(app));
11 | 	});
12 | 
13 | 	var buildsResource = app.dataio.resource('builds');
14 | 
15 | 	app.builds.on('buildUpdated', function(build, changes) {
16 | 		if (build.status === 'queued') {
17 | 			helpers.createBuildDataResource(app, build.id);
18 | 		}
19 | 
20 | 		// notify about build's project change, coz building affects project
21 | 		// related stat (last build date, avg build time, etc)
22 | 		if (changes.completed) {
23 | 			var projectsResource = app.dataio.resource('projects');
24 | 			projectsResource.clientEmitSyncChange(build.project.name);
25 | 		}
26 | 
27 | 		buildsResource.clientEmitSync('change', {
28 | 			buildId: build.id,
29 | 			changes: changes
30 | 		});
31 | 	});
32 | 
33 | 	app.builds.on('buildCanceled', function(build) {
34 | 		buildsResource.clientEmitSync('cancel', {
35 | 			buildId: build.id,
36 | 			buildStatus: build.status
37 | 		});
38 | 	});
39 | 
40 | 	app.builds.on('buildLogLines', function(build, lines) {
41 | 		app.dataio.resource('build' + build.id).clientEmitSync(
42 | 			'data',
43 | 			{lines: lines}
44 | 		);
45 | 	});
46 | };
47 | 


--------------------------------------------------------------------------------
/resources/projects.js:
--------------------------------------------------------------------------------
  1 | 'use strict';
  2 | 
  3 | var Steppy = require('twostep').Steppy,
  4 | 	_ = require('underscore'),
  5 | 	helpers = require('./helpers');
  6 | 
  7 | var stringifyArgValue = function(value) {
  8 | 	return '"' + value.replace(/"/g, '\\"') + '"';
  9 | };
 10 | 
 11 | var makeProject = function(project, buildParams) {
 12 | 	var newProject = _(project).clone();
 13 | 
 14 | 	var playbookName = buildParams.playbook && buildParams.playbook.name;
 15 | 	if (playbookName) {
 16 | 
 17 | 		if (!project.playbooks) {
 18 | 			throw new Error(
 19 | 				'No playbooks in the project ' + project.name + ' but ' +
 20 | 				'playbookName is specified'
 21 | 			);
 22 | 		}
 23 | 
 24 | 		var playbook = _(project.playbooks).findWhere({name: playbookName});
 25 | 
 26 | 		if (!playbook) {
 27 | 			throw new Error(
 28 | 				'No playbook ' + playbookName + ' in ' +
 29 | 				project.name + ' project'
 30 | 			);
 31 | 		}
 32 | 
 33 | 		var inventoryNames = buildParams.playbook.inventoryNames,
 34 | 			limit = buildParams.playbook.limit,
 35 | 			extraVars = buildParams.playbook.extraVars;
 36 | 
 37 | 		if (!inventoryNames || !inventoryNames.length) {
 38 | 			throw new Error(
 39 | 				'Inventory not specified for playbook ' + playbook.name +
 40 | 				' (project ' + project.name + ')'
 41 | 			);
 42 | 		}
 43 | 
 44 | 		var inventories = _(inventoryNames).map(function(inventoryName) {
 45 | 			var inventory = _(playbook.inventories).findWhere({
 46 | 				name: inventoryName
 47 | 			});
 48 | 
 49 | 			if (!inventory) {
 50 | 				throw new Error(
 51 | 					'No Inventory ' + inventoryName + ' in ' + playbook.name +
 52 | 					' (project ' + project.name + ')'
 53 | 				);
 54 | 
 55 | 			}
 56 | 
 57 | 			return inventory;
 58 | 		});
 59 | 
 60 | 		var playbookSteps = _(inventories).map(function(inventory) {
 61 | 			var args = [
 62 | 				project.playbookCommand,
 63 | 				playbook.path,
 64 | 				'--inventory-file=' + inventory.path
 65 | 			];
 66 | 
 67 | 			if (limit) {
 68 | 				args.push('--limit=' + limit);
 69 | 			}
 70 | 
 71 | 			if (extraVars) {
 72 | 				args.push('--extra-vars=' + stringifyArgValue(extraVars));
 73 | 			}
 74 | 
 75 | 			var stepName = (
 76 | 				'run playbook ' + playbook.name + ' with ' + inventory.name +
 77 | 				' inventory'
 78 | 			);
 79 | 
 80 | 			var yellow ='\\033[0;33m',
 81 | 				noColor='\\033[0m';
 82 | 
 83 | 			var echoCommand = (
 84 | 				'echo "******************";' +
 85 | 				'echo -e "********* ' + yellow + stepName.toUpperCase() +
 86 | 				noColor + ' *********";' +
 87 | 				'echo "******************";'
 88 | 			);
 89 | 
 90 | 			return {
 91 | 				type: 'shell',
 92 | 				name: stepName,
 93 | 				cmd: echoCommand + args.join(' ')
 94 | 			};
 95 | 		});
 96 | 
 97 | 		newProject.steps = newProject.steps.concat(playbookSteps);
 98 | 	}
 99 | 
100 | 	return newProject;
101 | };
102 | 
103 | var patchDirstributor = function(distributor) {
104 | 	var originalMakeProjet = distributor._makeProject;
105 | 	distributor._makeProject = function(project, buildParams) {
106 | 		var newProject = originalMakeProjet(project, buildParams);
107 | 		newProject = makeProject(newProject, buildParams);
108 | 		return newProject;
109 | 	};
110 | };
111 | 
112 | var extendProject = function(project) {
113 | 	if (project.playbooks) {
114 | 		_(project).defaults({
115 | 			playbookCommand: 'ANSIBLE_FORCE_COLOR=true ansible-playbook'
116 | 		});
117 | 	}
118 | 
119 | 	return project;
120 | };
121 | 
122 | module.exports = function(app) {
123 | 	var logger = app.lib.logger('projects resource'),
124 | 		resource = app.dataio.resource('projects');
125 | 
126 | 	patchDirstributor(app.builds.distributor);
127 | 
128 | 	app.projects.on('projectLoaded', function(project) {
129 | 		extendProject(project);
130 | 	});
131 | 
132 | 	resource.use('createBuildDataResource', function(req, res) {
133 | 		helpers.createBuildDataResource(app, req.data.buildId);
134 | 		res.send();
135 | 	});
136 | 
137 | 	resource.use('readAll', function(req, res) {
138 | 		var filteredProjects = app.projects.getAll(),
139 | 			nameQuery = req.data && req.data.nameQuery;
140 | 
141 | 		filteredProjects = app.projects.filter(function(project) {
142 | 			return !project.archived;
143 | 		});
144 | 
145 | 		if (nameQuery) {
146 | 			filteredProjects = _(filteredProjects).filter(function(project) {
147 | 				return project.name.indexOf(nameQuery) !== -1;
148 | 			});
149 | 		}
150 | 
151 | 		filteredProjects = _(filteredProjects).sortBy('name');
152 | 
153 | 		res.send(filteredProjects);
154 | 	});
155 | 
156 | 	// get project with additional fields
157 | 	var getProject = function(name, callback) {
158 | 		var project;
159 | 		Steppy(
160 | 			function() {
161 | 				project = _(app.projects.get(name)).clone();
162 | 
163 | 				app.builds.getRecent({
164 | 					projectName: project.name,
165 | 					status: 'done',
166 | 					limit: 10
167 | 				}, this.slot());
168 | 
169 | 				app.builds.getDoneStreak({projectName: project.name}, this.slot());
170 | 			},
171 | 			function(err, doneBuilds, doneBuildsStreak) {
172 | 				project.avgBuildDuration = app.builds.getAvgBuildDuration(doneBuilds);
173 | 				project.lastDoneBuild = doneBuilds[0];
174 | 				project.doneBuildsStreak = doneBuildsStreak;
175 | 
176 | 				this.pass(project);
177 | 			},
178 | 			callback
179 | 		);
180 | 	};
181 | 
182 | 	// resource custom method which finds project by name
183 | 	// and emits event about it change to clients
184 | 	resource.clientEmitSyncChange = function(name) {
185 | 		Steppy(
186 | 			function() {
187 | 				getProject(name, this.slot());
188 | 			},
189 | 			function(err, project) {
190 | 				resource.clientEmitSync('change', {project: project});
191 | 			},
192 | 			function(err) {
193 | 				console.error(
194 | 					'Error during sync project change occurred:',
195 | 					err.stack || err
196 | 				);
197 | 			}
198 | 		);
199 | 	};
200 | 
201 | 	resource.use('read', function(req, res) {
202 | 		Steppy(
203 | 			function() {
204 | 				getProject(req.data.name, this.slot());
205 | 			},
206 | 			function(err, project) {
207 | 				res.send(project);
208 | 			}
209 | 		);
210 | 	});
211 | 
212 | 	resource.use('run', function(req, res) {
213 | 		var projectName = req.data.projectName,
214 | 			buildParams = req.data.buildParams;
215 | 
216 | 		logger.log(
217 | 			'Run the project: "%s" with params: %j',
218 | 			projectName,
219 | 			buildParams
220 | 		);
221 | 
222 | 		app.builds.create({
223 | 			projectName: projectName,
224 | 			initiator: {type: 'user'},
225 | 			queueQueued: true,
226 | 			buildParams: buildParams
227 | 		});
228 | 
229 | 		res.send();
230 | 	});
231 | 
232 | 	return resource;
233 | };
234 | 


--------------------------------------------------------------------------------
/static/css/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/node-ci/nci-ansible-ui/c19b30f95133904c8b824d830848602a58ee53ac/static/css/.gitkeep


--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/node-ci/nci-ansible-ui/c19b30f95133904c8b824d830848602a58ee53ac/static/favicon.ico


--------------------------------------------------------------------------------
/static/images/build/progress-small.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/node-ci/nci-ansible-ui/c19b30f95133904c8b824d830848602a58ee53ac/static/images/build/progress-small.gif


--------------------------------------------------------------------------------
/static/images/build/progress.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/node-ci/nci-ansible-ui/c19b30f95133904c8b824d830848602a58ee53ac/static/images/build/progress.gif


--------------------------------------------------------------------------------
/static/images/preloader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/node-ci/nci-ansible-ui/c19b30f95133904c8b824d830848602a58ee53ac/static/images/preloader.gif


--------------------------------------------------------------------------------
/static/js/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/node-ci/nci-ansible-ui/c19b30f95133904c8b824d830848602a58ee53ac/static/js/.gitkeep


--------------------------------------------------------------------------------
/transforms/jade.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | 
 3 | var through = require('through'),
 4 | 	jade = require('react-jade');
 5 | 
 6 | module.exports = function(fileName, options) {
 7 | 	if (!/\.jade$/i.test(fileName)) {
 8 | 		return through();
 9 | 	}
10 | 
11 | 	var template = '';
12 | 	return through(
13 | 		function(chunk) {
14 | 			template += chunk.toString();
15 | 		},
16 | 		function() {
17 | 			options.filename = fileName;
18 | 			options.globalReact = true;
19 | 
20 | 			try {
21 | 				template = jade.compileClient(template, options);
22 | 			} catch (e) {
23 | 				this.emit('error', e);
24 | 				return;
25 | 			}
26 | 
27 | 			var moduleBody = 'var React = require("react");\n' +
28 | 				'module.exports = ' + template;
29 | 
30 | 			this.queue(moduleBody);
31 | 			this.queue(null);
32 | 		}
33 | 	);
34 | };
35 | 


--------------------------------------------------------------------------------
/views/index.jade:
--------------------------------------------------------------------------------
 1 | doctype html
 2 | html
 3 | 	head
 4 | 		meta(charset="utf-8")
 5 | 
 6 | 		title nci-ansible-ui - simple web interface for run ansible playbooks
 7 | 
 8 | 		link(rel="icon", href="/favicon.ico?v1")
 9 | 
10 | 		if env === 'development'
11 | 			link(href="/css/index.css", rel="stylesheet", type="text/css")
12 | 		else
13 | 			style
14 | 				include ../static/css/index.css
15 | 
16 | 	body
17 | 		#content
18 | 
19 | 		if env === 'development'
20 | 			script(type="text/javascript", src="/js/app.build.js")
21 | 		else
22 | 			script
23 | 				include ../static/js/app.build.js
24 | 


--------------------------------------------------------------------------------