Merge branch 'master' into sean
Conflicts: app/styles/containers.less
|  | @ -5,16 +5,10 @@ node_modules | ||||||
| npm-debug.log | npm-debug.log | ||||||
| 
 | 
 | ||||||
| # Signing Identity | # Signing Identity | ||||||
| script/identity | identity | ||||||
| 
 | 
 | ||||||
| # Resources | # Resources | ||||||
| resources/virtualbox-*.pkg |  | ||||||
| resources/boot2docker* | resources/boot2docker* | ||||||
| resources/mongod |  | ||||||
| resources/MONGOD_LICENSE.txt |  | ||||||
| resources/node |  | ||||||
| resources/NODE_LICENSE.txt |  | ||||||
| resources/settings.json |  | ||||||
| 
 | 
 | ||||||
| # Cache | # Cache | ||||||
| cache | cache | ||||||
|  |  | ||||||
|  | @ -15,15 +15,15 @@ Kitematic's documentation and other information can be found at [http://kitemati | ||||||
| ### Development | ### Development | ||||||
| 
 | 
 | ||||||
| - `sudo npm install -g less` | - `sudo npm install -g less` | ||||||
| - `./script/npm install` | - `npm install` | ||||||
| 
 | 
 | ||||||
| To run the app in development: | To run the app in development: | ||||||
| 
 | 
 | ||||||
| - `./script/gulp` | - `npm start` | ||||||
| 
 | 
 | ||||||
| ### Building the Mac OS X Package | ### Building the Mac OS X Package | ||||||
| 
 | 
 | ||||||
| - `./script/release` | - `npm run release` | ||||||
| 
 | 
 | ||||||
| ## Uninstalling | ## Uninstalling | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,7 @@ | ||||||
| var _ = require('underscore'); | var _ = require('underscore'); | ||||||
| var React = require('react'); | var $ = require('jquery'); | ||||||
|  | var React = require('react/addons'); | ||||||
| var Router = require('react-router'); | var Router = require('react-router'); | ||||||
| var Convert = require('ansi-to-html'); |  | ||||||
| var convert = new Convert(); |  | ||||||
| var ContainerStore = require('./ContainerStore'); | var ContainerStore = require('./ContainerStore'); | ||||||
| var docker = require('./docker'); | var docker = require('./docker'); | ||||||
| var exec = require('exec'); | var exec = require('exec'); | ||||||
|  | @ -17,85 +16,70 @@ var RouteHandler = Router.RouteHandler; | ||||||
| 
 | 
 | ||||||
| var ContainerDetails = React.createClass({ | var ContainerDetails = React.createClass({ | ||||||
|   mixins: [Router.State], |   mixins: [Router.State], | ||||||
|  |   _oldHeight: 0, | ||||||
|  |   PAGE_LOGS: 'logs', | ||||||
|  |   PAGE_SETTINGS: 'settings', | ||||||
|   getInitialState: function () { |   getInitialState: function () { | ||||||
|     return { |     return { | ||||||
|       logs: [] |       logs: [], | ||||||
|  |       page: this.PAGE_LOGS | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   componentWillReceiveProps: function () { |   componentWillReceiveProps: function () { | ||||||
|     this.update(); |  | ||||||
|     this.setState({ |     this.setState({ | ||||||
|       logs: [] |       page: this.PAGE_LOGS | ||||||
|     }); |     }); | ||||||
|     var self = this; |     ContainerStore.fetchLogs(this.getParams().name, function () { | ||||||
|     var logs = []; |       this.updateLogs(); | ||||||
|     var index = 0; |     }.bind(this)); | ||||||
|     docker.client().getContainer(this.getParams().name).logs({ |  | ||||||
|       follow: false, |  | ||||||
|       stdout: true, |  | ||||||
|       stderr: true, |  | ||||||
|       timestamps: true |  | ||||||
|     }, function (err, stream) { |  | ||||||
|       stream.setEncoding('utf8'); |  | ||||||
|       stream.on('data', function (buf) { |  | ||||||
|         // Every other message is a header
 |  | ||||||
|         if (index % 2 === 1) { |  | ||||||
|           var time = buf.substr(0,buf.indexOf(' ')); |  | ||||||
|           var msg = buf.substr(buf.indexOf(' ')+1); |  | ||||||
|           logs.push(convert.toHtml(self._escapeHTML(msg))); |  | ||||||
|         } |  | ||||||
|         index += 1; |  | ||||||
|       }); |  | ||||||
|       stream.on('end', function (buf) { |  | ||||||
|         self.setState({logs: logs}); |  | ||||||
|         docker.client().getContainer(self.getParams().name).logs({ |  | ||||||
|           follow: true, |  | ||||||
|           stdout: true, |  | ||||||
|           stderr: true, |  | ||||||
|           timestamps: true, |  | ||||||
|           tail: 0 |  | ||||||
|         }, function (err, stream) { |  | ||||||
|           stream.setEncoding('utf8'); |  | ||||||
|           stream.on('data', function (buf) { |  | ||||||
|             // Every other message is a header
 |  | ||||||
|             if (index % 2 === 1) { |  | ||||||
|               var time = buf.substr(0,buf.indexOf(' ')); |  | ||||||
|               var msg = buf.substr(buf.indexOf(' ')+1); |  | ||||||
|               logs.push(convert.toHtml(self._escapeHTML(msg))); |  | ||||||
|               self.setState({logs: logs}); |  | ||||||
|             } |  | ||||||
|             index += 1; |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|   componentWillMount: function () { |  | ||||||
|     this.update(); |  | ||||||
|   }, |   }, | ||||||
|   componentDidMount: function () { |   componentDidMount: function () { | ||||||
|     ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.update); |     ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); | ||||||
|     ContainerStore.addChangeListener(ContainerStore.PROGRESS, this.update); |     ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); | ||||||
|   }, |   }, | ||||||
|   componentWillUnmount: function () { |   componentWillUnmount: function () { | ||||||
|     ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.update); |     ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); | ||||||
|     ContainerStore.removeChangeListener(ContainerStore.PROGRESS, this.update); |     ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); | ||||||
|   }, |   }, | ||||||
|   update: function () { |   componentDidUpdate: function () { | ||||||
|     var name = this.getParams().name; |     var parent = $('.details-logs'); | ||||||
|  |     if (!parent.length) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (parent.scrollTop() >= this._oldHeight) { | ||||||
|  |       parent.stop(); | ||||||
|  |       parent.scrollTop(parent[0].scrollHeight - parent.height()); | ||||||
|  |     } | ||||||
|  |     this._oldHeight = parent[0].scrollHeight - parent.height(); | ||||||
|  |   }, | ||||||
|  |   updateLogs: function (name) { | ||||||
|  |     if (name && name !== this.getParams().name) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     this.setState({ |     this.setState({ | ||||||
|       container: ContainerStore.container(name), |       logs: ContainerStore.logs(this.getParams().name) | ||||||
|       progress: ContainerStore.progress(name) |  | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|   _escapeHTML: function (html) { |   updateProgress: function (name) { | ||||||
|     var text = document.createTextNode(html); |     console.log('progress', name, ContainerStore.progress(name)); | ||||||
|     var div = document.createElement('div'); |     if (name === this.getParams().name) { | ||||||
|     div.appendChild(text); |       this.setState({ | ||||||
|     return div.innerHTML; |         progress: ContainerStore.progress(name) | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   showLogs: function () { | ||||||
|  |     this.setState({ | ||||||
|  |       page: this.PAGE_LOGS | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   showSettings: function () { | ||||||
|  |     this.setState({ | ||||||
|  |       page: this.PAGE_SETTINGS | ||||||
|  |     }); | ||||||
|   }, |   }, | ||||||
|   handleClick: function (name) { |   handleClick: function (name) { | ||||||
|     var container = this.state.container; |     var container = this.props.container; | ||||||
|     boot2docker.ip(function (err, ip) { |     boot2docker.ip(function (err, ip) { | ||||||
|       var ports = _.map(container.NetworkSettings.Ports, function (value, key) { |       var ports = _.map(container.NetworkSettings.Ports, function (value, key) { | ||||||
|         var portProtocolPair = key.split('/'); |         var portProtocolPair = key.split('/'); | ||||||
|  | @ -113,7 +97,6 @@ var ContainerDetails = React.createClass({ | ||||||
|         } |         } | ||||||
|         return res; |         return res; | ||||||
|       }); |       }); | ||||||
|       console.log(ports); |  | ||||||
|       exec(['open', ports[0].url], function (err) { |       exec(['open', ports[0].url], function (err) { | ||||||
|         if (err) { throw err; } |         if (err) { throw err; } | ||||||
|       }); |       }); | ||||||
|  | @ -130,28 +113,19 @@ var ContainerDetails = React.createClass({ | ||||||
|       return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>; |       return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (!this.state.container) { |     if (!this.props.container) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var state; |     var state; | ||||||
|     if (this.state.container.State.Running) { |     if (this.props.container.State.Running) { | ||||||
|       state = <h2 className="status running">running</h2>; |       state = <h2 className="status running">running</h2>; | ||||||
|     } else if (this.state.container.State.Restarting) { |     } else if (this.props.container.State.Restarting) { | ||||||
|       state = <h2 className="status restarting">restarting</h2>; |       state = <h2 className="status restarting">restarting</h2>; | ||||||
|     } else if (this.state.container.State.Paused) { |     } else if (this.props.container.State.Paused) { | ||||||
|       state = <h2 className="status paused">paused</h2>; |       state = <h2 className="status paused">paused</h2>; | ||||||
|     } |     } else if (this.props.container.State.Downloading) { | ||||||
| 
 |       state = <h2 className="status">downloading</h2>; | ||||||
|     var progress; |  | ||||||
|     if (this.state.progress > 0 && this.state.progress != 1) { |  | ||||||
|       progress = ( |  | ||||||
|         <div className="details-progress"> |  | ||||||
|           <ProgressBar now={this.state.progress * 100} label="%(percent)s%" /> |  | ||||||
|         </div> |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       progress = <div></div>; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var button; |     var button; | ||||||
|  | @ -161,42 +135,75 @@ var ContainerDetails = React.createClass({ | ||||||
|       button = <a className="btn btn-primary disabled" onClick={this.handleClick}>View</a>; |       button = <a className="btn btn-primary disabled" onClick={this.handleClick}>View</a>; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var name = this.state.container.Name.replace('/', ''); |     var body; | ||||||
|     var image = this.state.container.Config.Image; |     if (this.props.container.State.Downloading) { | ||||||
| 
 |       body = ( | ||||||
|     return ( |         <div className="details-progress"> | ||||||
|       <div className="details"> |           <ProgressBar now={this.state.progress * 100} label="%(percent)s%" /> | ||||||
|         <div className="details-header"> |  | ||||||
|           <h1>{name}</h1>{state}<h2 className="image-label">Image</h2><h2 className="image">{image}</h2> |  | ||||||
|         </div> |         </div> | ||||||
|         <div className="details-actions"> |       ); | ||||||
|           <div className="action btn-group"> |     } else { | ||||||
|             <a className="btn btn-action with-icon" onClick={this.handleClick}><span className="icon icon-preview-2"></span> View</a> |       if (this.state.page === this.PAGE_LOGS) { | ||||||
|             <a className="btn btn-action with-icon dropdown-toggle"><span className="icon-dropdown icon icon-arrow-37"></span></a> |         body = ( | ||||||
|           </div> |  | ||||||
|           <div className="action"> |  | ||||||
|             <a className="btn btn-action with-icon dropdown-toggle" onClick={this.handleClick}><span className="icon icon-folder-1"></span> Volume <span className="icon-dropdown icon icon-arrow-37"></span></a> |  | ||||||
|           </div> |  | ||||||
|           <div className="action"> |  | ||||||
|             <a className="btn btn-action with-icon" onClick={this.handleClick}><span className="icon icon-refresh"></span> Restart</a> |  | ||||||
|           </div> |  | ||||||
|           <div className="action"> |  | ||||||
|             <a className="btn btn-action with-icon" onClick={this.handleClick}><span className="icon icon-window-code-3"></span> Terminal</a> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         <div className="details-tabs"> |  | ||||||
|           <div className="tabs btn-group"> |  | ||||||
|             <a className="btn btn-action only-icon active"><span className="icon icon-text-wrapping-2"></span></a> |  | ||||||
|             <a className="btn btn-action only-icon"><span className="icon icon-setting-gear"></span></a> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         {progress} |  | ||||||
|           <div className="details-logs"> |           <div className="details-logs"> | ||||||
|           <h4>Container Logs</h4> |  | ||||||
|             <div className="logs"> |             <div className="logs"> | ||||||
|               {logs} |               {logs} | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         body = ( | ||||||
|  |           <div className="details-logs"> | ||||||
|  |             <div className="settings"> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var textButtonClasses = React.addons.classSet({ | ||||||
|  |       'btn': true, | ||||||
|  |       'btn-action': true, | ||||||
|  |       'only-icon': true, | ||||||
|  |       'active': this.state.page === this.PAGE_LOGS | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     var gearButtonClass = React.addons.classSet({ | ||||||
|  |       'btn': true, | ||||||
|  |       'btn-action': true, | ||||||
|  |       'only-icon': true, | ||||||
|  |       'active': this.state.page === this.PAGE_SETTINGS | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     var name = this.props.container.Name; | ||||||
|  |     var image = this.props.container.Config.Image; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |       <div className="details"> | ||||||
|  |         <div className="details-header"> | ||||||
|  |           <div className="details-header-info"> | ||||||
|  |             <h1>{name}</h1>{state}<h2 className="image-label">Image</h2><h2 className="image">{image}</h2> | ||||||
|  |           </div> | ||||||
|  |           <div className="details-header-actions"> | ||||||
|  |             <div className="action btn-group"> | ||||||
|  |               <a className="btn btn-action with-icon" onClick={this.handleClick}><span className="icon icon-preview-2"></span><span className="content">View</span></a><a className="btn btn-action with-icon dropdown-toggle"><span className="icon-dropdown icon icon-arrow-37"></span></a> | ||||||
|  |             </div> | ||||||
|  |             <div className="action"> | ||||||
|  |               <a className="btn btn-action with-icon dropdown-toggle" onClick={this.handleClick}><span className="icon icon-folder-1"></span> <span className="content">Volumes</span> <span className="icon-dropdown icon icon-arrow-37"></span></a> | ||||||
|  |             </div> | ||||||
|  |             <div className="action"> | ||||||
|  |               <a className="btn btn-action with-icon" onClick={this.handleClick}><span className="icon icon-refresh"></span> <span className="content">Restart</span></a> | ||||||
|  |             </div> | ||||||
|  |             <div className="action"> | ||||||
|  |               <a className="btn btn-action with-icon" onClick={this.handleClick}><span className="icon icon-window-code-3"></span> <span className="content">Terminal</span></a> | ||||||
|  |             </div> | ||||||
|  |             <div className="details-header-actions-rhs tabs btn-group"> | ||||||
|  |               <a className={textButtonClasses} onClick={this.showLogs}><span className="icon icon-text-wrapping-2"></span></a> | ||||||
|  |               <a className={gearButtonClass} onClick={this.showSettings}><span className="icon icon-setting-gear"></span></a> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         {body} | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -7,40 +7,16 @@ var Modal = require('react-bootstrap/Modal'); | ||||||
| var RetinaImage = require('react-retina-image'); | var RetinaImage = require('react-retina-image'); | ||||||
| var ModalTrigger = require('react-bootstrap/ModalTrigger'); | var ModalTrigger = require('react-bootstrap/ModalTrigger'); | ||||||
| var ContainerModal = require('./ContainerModal.react'); | var ContainerModal = require('./ContainerModal.react'); | ||||||
| var ContainerStore = require('./ContainerStore'); |  | ||||||
| var Header = require('./Header.react'); | var Header = require('./Header.react'); | ||||||
| var docker = require('./docker'); | var docker = require('./docker'); | ||||||
| 
 | 
 | ||||||
| var Link = Router.Link; |  | ||||||
| var RouteHandler = Router.RouteHandler; |  | ||||||
| var Navigation= Router.Navigation; |  | ||||||
| 
 |  | ||||||
| var ContainerList = React.createClass({ | var ContainerList = React.createClass({ | ||||||
|   getInitialState: function () { |  | ||||||
|     return { |  | ||||||
|       containers: [] |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   componentDidMount: function () { |  | ||||||
|     this.updateContainers(); |  | ||||||
|     ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.updateContainers); |  | ||||||
|   }, |  | ||||||
|   componentWillMount: function () { |   componentWillMount: function () { | ||||||
|     this._start = Date.now(); |     this._start = Date.now(); | ||||||
|   }, |   }, | ||||||
|   componentWillUnmount: function () { |  | ||||||
|     ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.updateContainers); |  | ||||||
|   }, |  | ||||||
|   updateContainers: function () { |  | ||||||
|     // Sort by name
 |  | ||||||
|     var containers = _.values(ContainerStore.containers()).sort(function (a, b) { |  | ||||||
|       return a.Name.localeCompare(b.Name); |  | ||||||
|     }); |  | ||||||
|     this.setState({containers: containers}); |  | ||||||
|   }, |  | ||||||
|   render: function () { |   render: function () { | ||||||
|     var self = this; |     var self = this; | ||||||
|     var containers = this.state.containers.map(function (container) { |     var containers = this.props.containers.map(function (container) { | ||||||
|       var downloadingImage = null, downloading = false; |       var downloadingImage = null, downloading = false; | ||||||
|       var env = container.Config.Env; |       var env = container.Config.Env; | ||||||
|       if (env.length) { |       if (env.length) { | ||||||
|  | @ -76,22 +52,20 @@ var ContainerList = React.createClass({ | ||||||
|         state = <div className="state state-stopped"></div>; |         state = <div className="state state-stopped"></div>; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       var name = container.Name.replace('/', ''); |  | ||||||
| 
 |  | ||||||
|       return ( |       return ( | ||||||
|         <Link key={name} data-container={name} to="container" params={{name: name}}> |         <Router.Link key={container.Name} data-container={name} to="container" params={{name: container.Name}}> | ||||||
|           <li> |           <li> | ||||||
|             {state} |             {state} | ||||||
|             <div className="info"> |             <div className="info"> | ||||||
|               <div className="name"> |               <div className="name"> | ||||||
|                 {name} |                 {container.Name} | ||||||
|               </div> |               </div> | ||||||
|               <div className="image"> |               <div className="image"> | ||||||
|                 {imageName} |                 {imageName} | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </li> |           </li> | ||||||
|         </Link> |         </Router.Link> | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|     return ( |     return ( | ||||||
|  |  | ||||||
|  | @ -1,36 +1,51 @@ | ||||||
| var async = require('async'); | var async = require('async'); | ||||||
| var $ = require('jquery'); | var $ = require('jquery'); | ||||||
| var React = require('react'); | var React = require('react/addons'); | ||||||
| var Router = require('react-router'); |  | ||||||
| var Modal = require('react-bootstrap/Modal'); | var Modal = require('react-bootstrap/Modal'); | ||||||
| var RetinaImage = require('react-retina-image'); | var RetinaImage = require('react-retina-image'); | ||||||
| var ContainerStore = require('./ContainerStore'); | var ContainerStore = require('./ContainerStore'); | ||||||
| var OverlayTrigger = require('react-bootstrap/OverlayTrigger'); | var OverlayTrigger = require('react-bootstrap/OverlayTrigger'); | ||||||
| var Popover = require('react-bootstrap/Popover'); | var Popover = require('react-bootstrap/Popover'); | ||||||
| 
 | 
 | ||||||
| var Navigation = Router.Navigation; |  | ||||||
| 
 |  | ||||||
| var ContainerModal = React.createClass({ | var ContainerModal = React.createClass({ | ||||||
|   mixins: [Navigation], |  | ||||||
|   _searchRequest: null, |   _searchRequest: null, | ||||||
|   getInitialState: function () { |   getInitialState: function () { | ||||||
|     return { |     return { | ||||||
|       query: '', |       query: '', | ||||||
|       results: [], |       results: ContainerStore.recommended(), | ||||||
|       recommended: ContainerStore.recommended() |       loading: false, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   componentDidMount: function () { |   componentDidMount: function () { | ||||||
|     this.refs.searchInput.getDOMNode().focus(); |     this.refs.searchInput.getDOMNode().focus(); | ||||||
|  |     ContainerStore.on(ContainerStore.SERVER_RECOMMENDED_EVENT, this.update); | ||||||
|  |   }, | ||||||
|  |   update: function () { | ||||||
|  |     if (!this.state.query.length) { | ||||||
|  |       this.setState({ | ||||||
|  |         results: ContainerStore.recommended() | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|   search: function (query) { |   search: function (query) { | ||||||
|  |     if (this._searchRequest) { | ||||||
|  |       this._searchRequest.abort(); | ||||||
|  |       this._searchRequest = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.setState({ | ||||||
|  |       loading: true | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     var self = this; |     var self = this; | ||||||
|     this._searchRequest = $.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) { |     this._searchRequest = $.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) { | ||||||
|       self._searchRequest.abort(); |       self.setState({ | ||||||
|  |         query: query, | ||||||
|  |         loading: false | ||||||
|  |       }); | ||||||
|       self._searchRequest = null; |       self._searchRequest = null; | ||||||
|       if (self.isMounted()) { |       if (self.isMounted()) { | ||||||
|         self.setState(result); |         self.setState(result); | ||||||
|         console.log(result); |  | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|  | @ -41,35 +56,33 @@ var ContainerModal = React.createClass({ | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (this._searchRequest) { |  | ||||||
|       console.log('Cancel'); |  | ||||||
|       this._searchRequest.abort(); |  | ||||||
|       this._searchRequest = null; |  | ||||||
|     } |  | ||||||
|     clearTimeout(this.timeout); |     clearTimeout(this.timeout); | ||||||
|  |     if (!query.length) { | ||||||
|  |       this.setState({ | ||||||
|  |         query: query, | ||||||
|  |         results: ContainerStore.recommended() | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|       var self = this; |       var self = this; | ||||||
|       this.timeout = setTimeout(function () { |       this.timeout = setTimeout(function () { | ||||||
|         self.search(query); |         self.search(query); | ||||||
|     }, 250); |       }, 200); | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|   handleClick: function (event) { |   handleClick: function (event) { | ||||||
|     var name = event.target.getAttribute('name'); |     var name = event.target.getAttribute('name'); | ||||||
|     var self = this; |     var self = this; | ||||||
|     ContainerStore.create(name, 'latest', function (err, containerName) { |     ContainerStore.create(name, 'latest', function (err, containerName) { | ||||||
|       // this.transitionTo('containers', {container: containerName});
 |  | ||||||
|       self.props.onRequestHide(); |       self.props.onRequestHide(); | ||||||
|     }.bind(this)); |     }); | ||||||
|   }, |   }, | ||||||
|   render: function () { |   render: function () { | ||||||
|     var self = this; |     var self = this; | ||||||
|  |     var data = this.state.results.slice(0, 7); | ||||||
| 
 | 
 | ||||||
|     var data; |     var results; | ||||||
|     if (this.state.query) { |     if (data.length) { | ||||||
|       data = this.state.results.splice(0, 7); |       var items = data.map(function (r) { | ||||||
|     } else { |  | ||||||
|       data = this.state.recommended; |  | ||||||
|     } |  | ||||||
|     var results = data.map(function (r) { |  | ||||||
|         var name; |         var name; | ||||||
|         if (r.is_official) { |         if (r.is_official) { | ||||||
|           name = <span><RetinaImage src="official.png"/>{r.name}</span>; |           name = <span><RetinaImage src="official.png"/>{r.name}</span>; | ||||||
|  | @ -82,7 +95,7 @@ var ContainerModal = React.createClass({ | ||||||
|               <div className="name"> |               <div className="name"> | ||||||
|                 {name} |                 {name} | ||||||
|               </div> |               </div> | ||||||
|             <div className="stars"> |               <div className="properties"> | ||||||
|                 <div className="icon icon-star-9"></div> |                 <div className="icon icon-star-9"></div> | ||||||
|                 <div className="star-count">{r.star_count}</div> |                 <div className="star-count">{r.star_count}</div> | ||||||
|               </div> |               </div> | ||||||
|  | @ -90,36 +103,59 @@ var ContainerModal = React.createClass({ | ||||||
|             <div className="action"> |             <div className="action"> | ||||||
|               <div className="btn-group"> |               <div className="btn-group"> | ||||||
|                 <a className="btn btn-action" name={r.name} onClick={self.handleClick}>Create</a> |                 <a className="btn btn-action" name={r.name} onClick={self.handleClick}>Create</a> | ||||||
|               <a className="btn btn-action with-icon dropdown-toggle"><span className="icon-dropdown icon icon-arrow-37"></span></a> |                 <a className="btn btn-action with-icon dropdown-toggle"><span className="icon-dropdown icon icon-arrow-58"></span></a> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </li> |           </li> | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|     var title; |       results = ( | ||||||
|     if (this.state.query) { |         <div className="result-list"> | ||||||
|       title = <h4 className="title">Results</h4>; |           <ul> | ||||||
|  |             {items} | ||||||
|  |           </ul> | ||||||
|  |         </div> | ||||||
|  |       ); | ||||||
|     } else { |     } else { | ||||||
|       title = <h4 className="title">Recommended</h4>; |       results = ( | ||||||
|  |         <div className="no-results"> | ||||||
|  |           <h3> | ||||||
|  |             No Results | ||||||
|  |           </h3> | ||||||
|  |         </div> | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     var title = this.state.query ? 'Results' : 'Recommended'; | ||||||
|  |     var loadingClasses = React.addons.classSet({ | ||||||
|  |       hidden: !this.state.loading, | ||||||
|  |       loading: true | ||||||
|  |     }); | ||||||
|  |     var magnifierClasses = React.addons.classSet({ | ||||||
|  |       hidden: this.state.loading, | ||||||
|  |       icon: true, | ||||||
|  |       'icon-magnifier': true, | ||||||
|  |       'search-icon': true | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|       <Modal {...this.props} animation={false} className="create-modal"> |       <Modal {...this.props} animation={false} className="create-modal"> | ||||||
|         <div className="modal-body"> |         <div className="modal-body"> | ||||||
|           <section className="search"> |           <section className="search"> | ||||||
|  |             <div className="search-bar"> | ||||||
|               <input type="search" ref="searchInput" className="form-control" placeholder="Find an existing image" onChange={this.handleChange}/> |               <input type="search" ref="searchInput" className="form-control" placeholder="Find an existing image" onChange={this.handleChange}/> | ||||||
|             <div className="icon icon-magnifier search-icon"></div> |               <div className={magnifierClasses}></div> | ||||||
|  |               <RetinaImage className={loadingClasses} src="loading.png"/> | ||||||
|  |             </div> | ||||||
|             <div className="question"> |             <div className="question"> | ||||||
|               <OverlayTrigger trigger="hover" placement="bottom" overlay={<Popover>An image is a template which a container can be created from.</Popover>}> |               <OverlayTrigger trigger="hover" placement="bottom" overlay={<Popover>An image is a template which a container can be created from.</Popover>}> | ||||||
|                 <a><span>What's an image?</span></a> |                 <a><span>What's an image?</span></a> | ||||||
|               </OverlayTrigger> |               </OverlayTrigger> | ||||||
|             </div> |             </div> | ||||||
|             <div className="results"> |             <div className="results"> | ||||||
|               {title} |               <div className="title">{title}</div> | ||||||
|               <ul> |  | ||||||
|               {results} |               {results} | ||||||
|               </ul> |  | ||||||
|             </div> |             </div> | ||||||
|           </section> |           </section> | ||||||
|           <aside className="custom"> |           <aside className="custom"> | ||||||
|  |  | ||||||
|  | @ -1,21 +1,25 @@ | ||||||
| var EventEmitter = require('events').EventEmitter; | var EventEmitter = require('events').EventEmitter; | ||||||
| var async = require('async'); | var async = require('async'); | ||||||
| var assign = require('react/lib/Object.assign'); | var assign = require('object-assign'); | ||||||
|  | var Stream = require('stream'); | ||||||
|  | var Convert = require('ansi-to-html'); | ||||||
|  | var convert = new Convert(); | ||||||
| var docker = require('./docker'); | var docker = require('./docker'); | ||||||
| var registry = require('./registry'); | var registry = require('./registry'); | ||||||
| var $ = require('jquery'); | var $ = require('jquery'); | ||||||
| var _ = require('underscore'); | var _ = require('underscore'); | ||||||
| 
 | 
 | ||||||
| // Merge our store with Node's Event Emitter
 | var _recommended = []; | ||||||
|  | var _containers = {}; | ||||||
|  | var _progress = {}; | ||||||
|  | var _logs = {}; | ||||||
|  | 
 | ||||||
| var ContainerStore = assign(EventEmitter.prototype, { | var ContainerStore = assign(EventEmitter.prototype, { | ||||||
|   CONTAINERS: 'containers', |   CLIENT_CONTAINER_EVENT: 'client_container', | ||||||
|   PROGRESS: 'progress', |   SERVER_CONTAINER_EVENT: 'server_container', | ||||||
|   LOGS: 'logs', |   SERVER_PROGRESS_EVENT: 'server_progress', | ||||||
|   RECOMMENDED: 'recommended', |   SERVER_RECOMMENDED_EVENT: 'server_recommended_event', | ||||||
|   _recommended: [], |   SERVER_LOGS_EVENT: 'server_logs', | ||||||
|   _containers: {}, |  | ||||||
|   _progress: {}, |  | ||||||
|   _logs: {}, |  | ||||||
|   _pullScratchImage: function (callback) { |   _pullScratchImage: function (callback) { | ||||||
|     var image = docker.client().getImage('scratch:latest'); |     var image = docker.client().getImage('scratch:latest'); | ||||||
|     image.inspect(function (err, data) { |     image.inspect(function (err, data) { | ||||||
|  | @ -36,182 +40,8 @@ var ContainerStore = assign(EventEmitter.prototype, { | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|   _createContainer: function (image, name, callback) { |   _pullImage: function (repository, tag, callback, progressCallback) { | ||||||
|     var existing = docker.client().getContainer(name); |  | ||||||
|     existing.remove(function (err, data) { |  | ||||||
|       console.log('Placeholder removed.'); |  | ||||||
|       docker.client().createContainer({ |  | ||||||
|         Image: image, |  | ||||||
|         Tty: false, |  | ||||||
|         name: name |  | ||||||
|       }, function (err, container) { |  | ||||||
|         if (err) { |  | ||||||
|           callback(err, null); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         console.log('Created container: ' + container.id); |  | ||||||
|         container.start({ |  | ||||||
|           PublishAllPorts: true |  | ||||||
|         }, function (err) { |  | ||||||
|           if (err) { callback(err, null); return; } |  | ||||||
|           console.log('Started container: ' + container.id); |  | ||||||
|           callback(null, container); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|   _createPlaceholderContainer: function (imageName, name, callback) { |  | ||||||
|     console.log('_createPlaceholderContainer', imageName, name); |  | ||||||
|     this._pullScratchImage(function (err) { |  | ||||||
|       if (err) { |  | ||||||
|         callback(err); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       docker.client().createContainer({ |  | ||||||
|         Image: 'scratch:latest', |  | ||||||
|         Tty: false, |  | ||||||
|         Env: [ |  | ||||||
|           'KITEMATIC_DOWNLOADING=true', |  | ||||||
|           'KITEMATIC_DOWNLOADING_IMAGE=' + imageName |  | ||||||
|         ], |  | ||||||
|         Cmd: 'placeholder', |  | ||||||
|         name: name |  | ||||||
|       }, function (err, container) { |  | ||||||
|         callback(err, container); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|   _generateName: function (repository) { |  | ||||||
|     var base = _.last(repository.split('/')); |  | ||||||
|     var count = 1; |  | ||||||
|     var name = base; |  | ||||||
|     while (true) { |  | ||||||
|       var exists = _.findWhere(_.values(this._containers), {Name: '/' + name}) || _.findWhere(_.values(this._containers), {Name: name}); |  | ||||||
|       if (!exists) { |  | ||||||
|         return name; |  | ||||||
|       } else { |  | ||||||
|         count++; |  | ||||||
|         name = base + '-' + count; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   init: function (callback) { |  | ||||||
|     // TODO: Load cached data from db on loading
 |  | ||||||
| 
 |  | ||||||
|     // Refresh with docker & hook into events
 |  | ||||||
|     var self = this; |  | ||||||
|     this.update(function (err) { |  | ||||||
|       self.updateRecommended(function (err) { |  | ||||||
|         callback(); |  | ||||||
|       }); |  | ||||||
|       var downloading = _.filter(_.values(self._containers), function (container) { |  | ||||||
|         var env = container.Config.Env; |  | ||||||
|         return _.indexOf(env, 'KITEMATIC_DOWNLOADING=true') !== -1; |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       // Recover any pulls that were happening
 |  | ||||||
|       downloading.forEach(function (container) { |  | ||||||
|         var env = _.object(container.Config.Env.map(function (e) { |  | ||||||
|           return e.split('='); |  | ||||||
|         })); |  | ||||||
|         docker.client().pull(env.KITEMATIC_DOWNLOADING_IMAGE, function (err, stream) { |  | ||||||
|           stream.setEncoding('utf8'); |  | ||||||
|           stream.on('data', function (data) { |  | ||||||
|             console.log(data); |  | ||||||
|           }); |  | ||||||
|           stream.on('end', function () { |  | ||||||
|             self._createContainer(env.KITEMATIC_DOWNLOADING_IMAGE, container.Name.replace('/', ''), function () { |  | ||||||
| 
 |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       docker.client().getEvents(function (err, stream) { |  | ||||||
|         stream.setEncoding('utf8'); |  | ||||||
|         stream.on('data', function (data) { |  | ||||||
|           console.log(data); |  | ||||||
| 
 |  | ||||||
|           // TODO: Dont refresh on deleting placeholder containers
 |  | ||||||
|           var deletingPlaceholder = data.status === 'destroy' && self.container(data.id) && self.container(data.id).Config.Env.indexOf('KITEMATIC_DOWNLOADING=true') !== -1; |  | ||||||
|           console.log(deletingPlaceholder); |  | ||||||
|           if (!deletingPlaceholder) { |  | ||||||
|             self.update(function (err) { |  | ||||||
|               console.log('Updated container data.'); |  | ||||||
|             }); |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|   update: function (callback) { |  | ||||||
|     var self = this; |  | ||||||
|     docker.client().listContainers({all: true}, function (err, containers) { |  | ||||||
|       if (err) { |  | ||||||
|         callback(err); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       async.map(containers, function(container, callback) { |  | ||||||
|         docker.client().getContainer(container.Id).inspect(function (err, data) { |  | ||||||
|           callback(err, data); |  | ||||||
|         }); |  | ||||||
|       }, function (err, results) { |  | ||||||
|         if (err) { |  | ||||||
|           callback(err); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         var containers = {}; |  | ||||||
|         results.forEach(function (r) { |  | ||||||
|           containers[r.Name.replace('/', '')] = r; |  | ||||||
|         }); |  | ||||||
|         self._containers = containers; |  | ||||||
|         self.emit(self.CONTAINERS); |  | ||||||
|         callback(null); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|   updateRecommended: function (callback) { |  | ||||||
|     var self = this; |  | ||||||
|     $.ajax({ |  | ||||||
|       url: 'https://kitematic.com/recommended.json', |  | ||||||
|       dataType: 'json', |  | ||||||
|       success: function (res, status) { |  | ||||||
|         var recommended = res.recommended; |  | ||||||
|         async.map(recommended, function (repository, callback) { |  | ||||||
|           $.get('https://registry.hub.docker.com/v1/search?q=' + repository, function (data) { |  | ||||||
|             var results = data.results; |  | ||||||
|             callback(null, _.find(results, function (r) { |  | ||||||
|               return r.name === repository; |  | ||||||
|             })); |  | ||||||
|           }); |  | ||||||
|         }, function (err, results) { |  | ||||||
|           self._recommended = results; |  | ||||||
|           callback(); |  | ||||||
|         }); |  | ||||||
|       }, |  | ||||||
|       error: function (err) { |  | ||||||
|         console.log(err); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|   create: function (repository, tag, callback) { |  | ||||||
|     tag = tag || 'latest'; |  | ||||||
|     var self = this; |  | ||||||
|     var imageName = repository + ':' + tag; |  | ||||||
|     var containerName = this._generateName(repository); |  | ||||||
|     var image = docker.client().getImage(imageName); |  | ||||||
| 
 |  | ||||||
|     image.inspect(function (err, data) { |  | ||||||
|       if (!data) { |  | ||||||
|         // Pull image
 |  | ||||||
|         self._createPlaceholderContainer(imageName, containerName, function (err, container) { |  | ||||||
|           if (err) { |  | ||||||
|             console.log(err); |  | ||||||
|           } |  | ||||||
|     registry.layers(repository, tag, function (err, layerSizes) { |     registry.layers(repository, tag, function (err, layerSizes) { | ||||||
|             if (err) { |  | ||||||
|               callback(err); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|       // TODO: Support v2 registry API
 |       // TODO: Support v2 registry API
 | ||||||
|       // TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
 |       // TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
 | ||||||
|  | @ -225,8 +55,7 @@ var ContainerStore = assign(EventEmitter.prototype, { | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         var totalBytes = layersToDownload.map(function (s) { return s.size; }).reduce(function (pv, sv) { return pv + sv; }, 0); |         var totalBytes = layersToDownload.map(function (s) { return s.size; }).reduce(function (pv, sv) { return pv + sv; }, 0); | ||||||
|               docker.client().pull(imageName, function (err, stream) { |         docker.client().pull(repository + ':' + tag, function (err, stream) { | ||||||
|                 callback(null, containerName); |  | ||||||
|           stream.setEncoding('utf8'); |           stream.setEncoding('utf8'); | ||||||
| 
 | 
 | ||||||
|           var layerProgress = layersToDownload.reduce(function (r, layer) { |           var layerProgress = layersToDownload.reduce(function (r, layer) { | ||||||
|  | @ -238,11 +67,9 @@ var ContainerStore = assign(EventEmitter.prototype, { | ||||||
|             return r; |             return r; | ||||||
|           }, {}); |           }, {}); | ||||||
| 
 | 
 | ||||||
|                 self._progress[containerName] = 0; |  | ||||||
| 
 |  | ||||||
|           stream.on('data', function (str) { |           stream.on('data', function (str) { | ||||||
|                   console.log(str); |  | ||||||
|             var data = JSON.parse(str); |             var data = JSON.parse(str); | ||||||
|  |             console.log(data); | ||||||
| 
 | 
 | ||||||
|             if (data.status === 'Already exists') { |             if (data.status === 'Already exists') { | ||||||
|               layerProgress[data.id] = 1; |               layerProgress[data.id] = 1; | ||||||
|  | @ -262,47 +89,310 @@ var ContainerStore = assign(EventEmitter.prototype, { | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             var totalProgress = totalReceived / totalBytes; |             var totalProgress = totalReceived / totalBytes; | ||||||
|                   self._progress[containerName] = totalProgress; |             progressCallback(totalProgress); | ||||||
|                   self.emit(self.PROGRESS); |  | ||||||
|           }); |           }); | ||||||
|           stream.on('end', function () { |           stream.on('end', function () { | ||||||
|                   self._createContainer(imageName, containerName, function () { |             callback(); | ||||||
|                     delete self._progress[containerName]; |  | ||||||
|           }); |           }); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |   }, | ||||||
|  |   _escapeHTML: function (html) { | ||||||
|  |     var text = document.createTextNode(html); | ||||||
|  |     var div = document.createElement('div'); | ||||||
|  |     div.appendChild(text); | ||||||
|  |     return div.innerHTML; | ||||||
|  |   }, | ||||||
|  |   _createContainer: function (image, name, callback) { | ||||||
|  |     var existing = docker.client().getContainer(name); | ||||||
|  |     var self = this; | ||||||
|  |     existing.remove(function (err, data) { | ||||||
|  |       docker.client().createContainer({ | ||||||
|  |         Image: image, | ||||||
|  |         Tty: false, | ||||||
|  |         name: name, | ||||||
|  |         User: 'root' | ||||||
|  |       }, function (err, container) { | ||||||
|  |         if (err) { | ||||||
|  |           callback(err, null); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         container.start({ | ||||||
|  |           PublishAllPorts: true | ||||||
|  |         }, function (err) { | ||||||
|  |           if (err) { | ||||||
|  |             callback(err); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |           self.fetchContainer(name, callback); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   _createPlaceholderContainer: function (imageName, name, callback) { | ||||||
|  |     var self = this; | ||||||
|  |     this._pullScratchImage(function (err) { | ||||||
|  |       if (err) { | ||||||
|  |         callback(err); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       docker.client().createContainer({ | ||||||
|  |         Image: 'scratch:latest', | ||||||
|  |         Tty: false, | ||||||
|  |         Env: [ | ||||||
|  |           'KITEMATIC_DOWNLOADING=true', | ||||||
|  |           'KITEMATIC_DOWNLOADING_IMAGE=' + imageName | ||||||
|  |         ], | ||||||
|  |         Cmd: 'placeholder', | ||||||
|  |         name: name | ||||||
|  |       }, function (err, container) { | ||||||
|  |         if (err) { | ||||||
|  |           callback(err); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         self.fetchContainer(name, callback); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   _generateName: function (repository) { | ||||||
|  |     var base = _.last(repository.split('/')); | ||||||
|  |     var count = 1; | ||||||
|  |     var name = base; | ||||||
|  |     while (true) { | ||||||
|  |       var exists = _.findWhere(_.values(_containers), {Name: name}) || _.findWhere(_.values(_containers), {Name: name}); | ||||||
|  |       if (!exists) { | ||||||
|  |         return name; | ||||||
|  |       } else { | ||||||
|  |         count++; | ||||||
|  |         name = base + '-' + count; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   _resumePulling: function () { | ||||||
|  |     var downloading = _.filter(_.values(_containers), function (container) { | ||||||
|  |       return container.State.Downloading; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Recover any pulls that were happening
 | ||||||
|  |     var self = this; | ||||||
|  |     downloading.forEach(function (container) { | ||||||
|  |       docker.client().pull(container.KitematicDownloadingImage, function (err, stream) { | ||||||
|  |         stream.setEncoding('utf8'); | ||||||
|  |         stream.on('data', function (data) {}); | ||||||
|  |         stream.on('end', function () { | ||||||
|  |           self._createContainer(container.KitematicDownloadingImage, container.Name, function () {}); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   _startListeningToEvents: function () { | ||||||
|  |     docker.client().getEvents(function (err, stream) { | ||||||
|  |       if (stream) { | ||||||
|  |         stream.setEncoding('utf8'); | ||||||
|  |         stream.on('data', this._dockerEvent.bind(this)); | ||||||
|  |       } | ||||||
|  |     }.bind(this)); | ||||||
|  |   }, | ||||||
|  |   _dockerEvent: function (json) { | ||||||
|  |     var data = JSON.parse(json); | ||||||
|  |     console.log(data); | ||||||
|  | 
 | ||||||
|  |     // If the event is delete, remove the container
 | ||||||
|  |     if (data.status === 'destroy') { | ||||||
|  |       var container = _.findWhere(_.values(_containers), {Id: data.id}); | ||||||
|  |       delete _containers[container.Name]; | ||||||
|  |       this.emit(this.SERVER_CONTAINER_EVENT, container.Name, data.status); | ||||||
|  |     } else { | ||||||
|  |       this.fetchContainer(data.id, function (err) { | ||||||
|  |         var container = _.findWhere(_.values(_containers), {Id: data.id}); | ||||||
|  |         this.emit(this.SERVER_CONTAINER_EVENT, container ? container.Name : null, data.status); | ||||||
|  |       }.bind(this)); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   init: function (callback) { | ||||||
|  |     // TODO: Load cached data from db on loading
 | ||||||
|  |     this.fetchAllContainers(function (err) { | ||||||
|  |       callback(); | ||||||
|  |       this.emit(this.CLIENT_CONTAINER_EVENT); | ||||||
|  |       this.fetchRecommended(function (err) { | ||||||
|  |         this.emit(this.SERVER_RECOMMENDED_EVENT); | ||||||
|  |       }.bind(this)); | ||||||
|  |       this._resumePulling(); | ||||||
|  |       this._startListeningToEvents(); | ||||||
|  |     }.bind(this)); | ||||||
|  |   }, | ||||||
|  |   fetchContainer: function (id, callback) { | ||||||
|  |     docker.client().getContainer(id).inspect(function (err, container) { | ||||||
|  |       if (err) { | ||||||
|  |         callback(err); | ||||||
|  |       } else { | ||||||
|  |         // Fix leading slash in container names
 | ||||||
|  |         container.Name = container.Name.replace('/', ''); | ||||||
|  | 
 | ||||||
|  |         // Add Downloading State (stored in environment variables) to containers for Kitematic
 | ||||||
|  |         var env = _.object(container.Config.Env.map(function (e) { return e.split('='); })); | ||||||
|  |         container.State.Downloading = !!env.KITEMATIC_DOWNLOADING; | ||||||
|  |         container.KitematicDownloadingImage = env.KITEMATIC_DOWNLOADING_IMAGE; | ||||||
|  | 
 | ||||||
|  |         _containers[container.Name] = container; | ||||||
|  |         callback(null, container); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   fetchAllContainers: function (callback) { | ||||||
|  |     var self = this; | ||||||
|  |     docker.client().listContainers({all: true}, function (err, containers) { | ||||||
|  |       if (err) { | ||||||
|  |         callback(err); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       async.map(containers, function (container, callback) { | ||||||
|  |         self.fetchContainer(container.Id, function (err) { | ||||||
|  |           callback(err); | ||||||
|  |         }); | ||||||
|  |       }, function (err, results) { | ||||||
|  |         callback(err); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   fetchRecommended: function (callback) { | ||||||
|  |     if (_recommended.length) { | ||||||
|  |      return; | ||||||
|  |    } | ||||||
|  |     var self = this; | ||||||
|  |     $.ajax({ | ||||||
|  |       url: 'https://kitematic.com/recommended.json', | ||||||
|  |       dataType: 'json', | ||||||
|  |       success: function (res, status) { | ||||||
|  |         var recommended = res.recommended; | ||||||
|  |         async.map(recommended, function (repository, callback) { | ||||||
|  |           $.get('https://registry.hub.docker.com/v1/search?q=' + repository, function (data) { | ||||||
|  |             var results = data.results; | ||||||
|  |             callback(null, _.find(results, function (r) { | ||||||
|  |               return r.name === repository; | ||||||
|  |             })); | ||||||
|  |           }); | ||||||
|  |         }, function (err, results) { | ||||||
|  |           _recommended = results; | ||||||
|  |           callback(); | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |       error: function (err) { | ||||||
|  |         console.log(err); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   fetchLogs: function (name, callback) { | ||||||
|  |     if (_logs[name]) { | ||||||
|  |       callback(); | ||||||
|  |     } | ||||||
|  |     _logs[name] = []; | ||||||
|  |     var index = 0; | ||||||
|  |     var self = this; | ||||||
|  |     docker.client().getContainer(name).logs({ | ||||||
|  |       follow: false, | ||||||
|  |       stdout: true, | ||||||
|  |       stderr: true, | ||||||
|  |       timestamps: true | ||||||
|  |     }, function (err, stream) { | ||||||
|  |       stream.setEncoding('utf8'); | ||||||
|  |       stream.on('data', function (buf) { | ||||||
|  |         // Every other message is a header
 | ||||||
|  |         if (index % 2 === 1) { | ||||||
|  |           var time = buf.substr(0,buf.indexOf(' ')); | ||||||
|  |           var msg = buf.substr(buf.indexOf(' ')+1); | ||||||
|  |           _logs[name].push(convert.toHtml(self._escapeHTML(msg))); | ||||||
|  |           self.emit(self.SERVER_LOGS_EVENT, name); | ||||||
|  |         } | ||||||
|  |         index += 1; | ||||||
|  |       }); | ||||||
|  |       stream.on('end', function (buf) { | ||||||
|  |         callback(); | ||||||
|  |         docker.client().getContainer(name).logs({ | ||||||
|  |           follow: true, | ||||||
|  |           stdout: true, | ||||||
|  |           stderr: true, | ||||||
|  |           timestamps: true, | ||||||
|  |           tail: 0 | ||||||
|  |         }, function (err, stream) { | ||||||
|  |           stream.setEncoding('utf8'); | ||||||
|  |           stream.on('data', function (buf) { | ||||||
|  |             // Every other message is a header
 | ||||||
|  |             if (index % 2 === 1) { | ||||||
|  |               var time = buf.substr(0,buf.indexOf(' ')); | ||||||
|  |               var msg = buf.substr(buf.indexOf(' ')+1); | ||||||
|  |               _logs[name].push(convert.toHtml(self._escapeHTML(msg))); | ||||||
|  |               self.emit(self.SERVER_LOGS_EVENT, name); | ||||||
|  |             } | ||||||
|  |             index += 1; | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   create: function (repository, tag, callback) { | ||||||
|  |     tag = tag || 'latest'; | ||||||
|  |     var self = this; | ||||||
|  |     var imageName = repository + ':' + tag; | ||||||
|  |     var containerName = this._generateName(repository); | ||||||
|  |     var image = docker.client().getImage(imageName); | ||||||
|  | 
 | ||||||
|  |     image.inspect(function (err, data) { | ||||||
|  |       if (!data) { | ||||||
|  |         // Pull image
 | ||||||
|  |         self._createPlaceholderContainer(imageName, containerName, function (err, container) { | ||||||
|  |           _containers[containerName] = container; | ||||||
|  |           self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create'); | ||||||
|  |           _progress[containerName] = 0; | ||||||
|  |           self._pullImage(repository, tag, function () { | ||||||
|  |             self._createContainer(imageName, containerName, function (err, container) { | ||||||
|  |               delete _progress[containerName]; | ||||||
|  |             }); | ||||||
|  |           }, function (progress) { | ||||||
|  |             _progress[containerName] = progress; | ||||||
|  |             self.emit(self.SERVER_PROGRESS_EVENT, containerName); | ||||||
|  |           }); | ||||||
|  |           callback(null, containerName); | ||||||
|  |         }); | ||||||
|       } else { |       } else { | ||||||
|         // If not then directly create the container
 |         // If not then directly create the container
 | ||||||
|         self._createContainer(imageName, containerName, function () { |         self._createContainer(imageName, containerName, function (err, container) { | ||||||
|  |           self.emit(ContainerStore.CLIENT_CONTAINER_EVENT, containerName, 'create'); | ||||||
|           callback(null, containerName); |           callback(null, containerName); | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|   containers: function() { |   containers: function() { | ||||||
|     return this._containers; |     return _containers; | ||||||
|   }, |   }, | ||||||
|   container: function (name) { |   container: function (name) { | ||||||
|     return this._containers[name]; |     return _containers[name]; | ||||||
|  |   }, | ||||||
|  |   sorted: function () { | ||||||
|  |     return _.values(_containers).sort(function (a, b) { | ||||||
|  |       var active = function (container) { | ||||||
|  |         return container.State.Running || container.State.Restarting || container.State.Downloading; | ||||||
|  |       }; | ||||||
|  |       if (active(a) && !active(b)) { | ||||||
|  |         return -1; | ||||||
|  |       } else if (!active(a) && active(b)) { | ||||||
|  |         return 1; | ||||||
|  |       } else { | ||||||
|  |         return a.Name.localeCompare(b.Name); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|   }, |   }, | ||||||
|   recommended: function () { |   recommended: function () { | ||||||
|     return this._recommended; |     return _recommended; | ||||||
|   }, |   }, | ||||||
|   progress: function (name) { |   progress: function (name) { | ||||||
|     return this._progress[name]; |     return _progress[name]; | ||||||
|   }, |   }, | ||||||
|   logs: function (name) { |   logs: function (name) { | ||||||
|     return logs[name]; |     return _logs[name] || []; | ||||||
|   }, |   } | ||||||
|   addChangeListener: function(eventType, callback) { |  | ||||||
|     this.on(eventType, callback); |  | ||||||
|   }, |  | ||||||
|   removeChangeListener: function(eventType, callback) { |  | ||||||
|     this.removeListener(eventType, callback); |  | ||||||
|   }, |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| module.exports = ContainerStore; | module.exports = ContainerStore; | ||||||
|  |  | ||||||
|  | @ -12,15 +12,51 @@ var _ = require('underscore'); | ||||||
| var docker = require('./docker'); | var docker = require('./docker'); | ||||||
| var $ = require('jquery'); | var $ = require('jquery'); | ||||||
| 
 | 
 | ||||||
| var Link = Router.Link; |  | ||||||
| var RouteHandler = Router.RouteHandler; |  | ||||||
| 
 |  | ||||||
| var Containers = React.createClass({ | var Containers = React.createClass({ | ||||||
|  |   mixins: [Router.Navigation, Router.State], | ||||||
|   getInitialState: function () { |   getInitialState: function () { | ||||||
|     return { |     return { | ||||||
|       sidebarOffset: 0 |       sidebarOffset: 0, | ||||||
|  |       containers: ContainerStore.containers(), | ||||||
|  |       sorted: ContainerStore.sorted(), | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |   componentDidMount: function () { | ||||||
|  |     this.update(); | ||||||
|  |     ContainerStore.on(ContainerStore.SERVER_CONTAINER_EVENT, this.update); | ||||||
|  |     ContainerStore.on(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient); | ||||||
|  | 
 | ||||||
|  |     if (this.state.sorted.length) { | ||||||
|  |       this.transitionTo('container', {name: this.state.sorted[0].Name}); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   componentDidUnmount: function () { | ||||||
|  |     ContainerStore.removeListener(ContainerStore.SERVER_CONTAINER_EVENT, this.update); | ||||||
|  |     ContainerStore.removeListener(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient); | ||||||
|  |   }, | ||||||
|  |   update: function (name, status) { | ||||||
|  |     this.setState({ | ||||||
|  |       containers: ContainerStore.containers(), | ||||||
|  |       sorted: ContainerStore.sorted() | ||||||
|  |     }); | ||||||
|  |     if (status === 'destroy') { | ||||||
|  |       if (this.state.sorted.length) { | ||||||
|  |         this.transitionTo('container', {name: this.state.sorted[0].Name}); | ||||||
|  |       } else { | ||||||
|  |         this.transitionTo('containers'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   updateFromClient: function (name, status) { | ||||||
|  |     this.setState({ | ||||||
|  |       containers: ContainerStore.containers(), | ||||||
|  |       sorted: ContainerStore.sorted() | ||||||
|  |     }); | ||||||
|  |     if (status === 'create') { | ||||||
|  |       console.log('transition'); | ||||||
|  |       this.transitionTo('container', {name: name}); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|   handleScroll: function (e) { |   handleScroll: function (e) { | ||||||
|     if (e.target.scrollTop > 0 && !this.state.sidebarOffset) { |     if (e.target.scrollTop > 0 && !this.state.sidebarOffset) { | ||||||
|       this.setState({ |       this.setState({ | ||||||
|  | @ -37,6 +73,7 @@ var Containers = React.createClass({ | ||||||
|     if (this.state.sidebarOffset) { |     if (this.state.sidebarOffset) { | ||||||
|       sidebarHeaderClass += ' sep'; |       sidebarHeaderClass += ' sep'; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div className="containers"> |       <div className="containers"> | ||||||
|         <Header/> |         <Header/> | ||||||
|  | @ -46,17 +83,15 @@ var Containers = React.createClass({ | ||||||
|               <h4>My Containers</h4> |               <h4>My Containers</h4> | ||||||
|               <div className="create"> |               <div className="create"> | ||||||
|                 <ModalTrigger modal={<ContainerModal/>}> |                 <ModalTrigger modal={<ContainerModal/>}> | ||||||
|                   <div className="wrapper"> |  | ||||||
|                   <a className="btn btn-action only-icon"><span className="icon icon-add-1"></span></a> |                   <a className="btn btn-action only-icon"><span className="icon icon-add-1"></span></a> | ||||||
|                   </div> |  | ||||||
|                 </ModalTrigger> |                 </ModalTrigger> | ||||||
|               </div> |               </div> | ||||||
|             </section> |             </section> | ||||||
|             <section className="sidebar-containers" onScroll={this.handleScroll}> |             <section className="sidebar-containers" onScroll={this.handleScroll}> | ||||||
|               <ContainerList/> |               <ContainerList containers={this.state.sorted}/> | ||||||
|             </section> |             </section> | ||||||
|           </div> |           </div> | ||||||
|           <RouteHandler/> |           <Router.RouteHandler container={this.state.containers[this.getParams().name]}/> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  | @ -2,6 +2,11 @@ var React = require('react/addons'); | ||||||
| var remote = require('remote'); | var remote = require('remote'); | ||||||
| 
 | 
 | ||||||
| var Header = React.createClass({ | var Header = React.createClass({ | ||||||
|  |   getInitialState: function () { | ||||||
|  |     return { | ||||||
|  |       fullscreen: false | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   componentDidMount: function () { |   componentDidMount: function () { | ||||||
|     document.addEventListener('keyup', this.handleDocumentKeyUp, false); |     document.addEventListener('keyup', this.handleDocumentKeyUp, false); | ||||||
|   }, |   }, | ||||||
|  | @ -22,14 +27,16 @@ var Header = React.createClass({ | ||||||
|   }, |   }, | ||||||
|   handleFullscreen: function () { |   handleFullscreen: function () { | ||||||
|     remote.getCurrentWindow().setFullScreen(!remote.getCurrentWindow().isFullScreen()); |     remote.getCurrentWindow().setFullScreen(!remote.getCurrentWindow().isFullScreen()); | ||||||
|     this.forceUpdate(); |     this.setState({ | ||||||
|  |       fullscreen: remote.getCurrentWindow().isFullScreen() | ||||||
|  |     }); | ||||||
|   }, |   }, | ||||||
|   handleFullscreenHover: function () { |   handleFullscreenHover: function () { | ||||||
|     this.update(); |     this.update(); | ||||||
|   }, |   }, | ||||||
|   render: function () { |   render: function () { | ||||||
|     var buttons; |     var buttons; | ||||||
|     if (remote.getCurrentWindow().isFullScreen()) { |     if (this.state.fullscreen) { | ||||||
|       return ( |       return ( | ||||||
|         <div className="header no-drag"> |         <div className="header no-drag"> | ||||||
|           <div className="buttons"> |           <div className="buttons"> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,14 @@ | ||||||
|  | var React = require('react/addons'); | ||||||
|  | var RetinaImage = require('react-retina-image'); | ||||||
|  | 
 | ||||||
|  | var NoContainers = React.createClass({ | ||||||
|  |   render: function () { | ||||||
|  |     return ( | ||||||
|  |       <div className="no-containers"> | ||||||
|  |         <h3>No Containers</h3> | ||||||
|  |       </div> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | module.exports = NoContainers; | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| var React = require('react'); | var React = require('react/addons'); | ||||||
| var Router = require('react-router'); | var Router = require('react-router'); | ||||||
| var Radial = require('./Radial.react.js'); | var Radial = require('./Radial.react.js'); | ||||||
| var async = require('async'); | var async = require('async'); | ||||||
|  | @ -134,6 +134,12 @@ var setupSteps = [ | ||||||
| 
 | 
 | ||||||
| var Setup = React.createClass({ | var Setup = React.createClass({ | ||||||
|   mixins: [ Router.Navigation ], |   mixins: [ Router.Navigation ], | ||||||
|  |   getInitialState: function () { | ||||||
|  |     return { | ||||||
|  |       message: '', | ||||||
|  |       progress: 0 | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   render: function () { |   render: function () { | ||||||
|     var radial; |     var radial; | ||||||
|     if (this.state.progress) { |     if (this.state.progress) { | ||||||
|  |  | ||||||
| After Width: | Height: | Size: 208 B | 
| After Width: | Height: | Size: 412 B | 
| Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 272 B | 
| Before Width: | Height: | Size: 555 B After Width: | Height: | Size: 563 B | 
| After Width: | Height: | Size: 410 B | 
| After Width: | Height: | Size: 852 B | 
| Before Width: | Height: | Size: 619 B After Width: | Height: | Size: 618 B | 
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 807 B After Width: | Height: | Size: 572 B | 
| Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.4 KiB | 
| After Width: | Height: | Size: 2.5 KiB | 
| After Width: | Height: | Size: 5.6 KiB | 
| After Width: | Height: | Size: 349 B | 
| After Width: | Height: | Size: 729 B | 
							
								
								
									
										69
									
								
								app/main.js
								
								
								
								
							
							
						
						|  | @ -1,15 +1,16 @@ | ||||||
|  | var module = require('module'); | ||||||
|  | require.main.paths.splice(0, 0, process.env.NODE_PATH); | ||||||
|  | 
 | ||||||
|  | var Bugsnag = require('bugsnag-js'); | ||||||
| var React = require('react'); | var React = require('react'); | ||||||
| var Router = require('react-router'); | var Router = require('react-router'); | ||||||
| var RetinaImage = require('react-retina-image'); | var RetinaImage = require('react-retina-image'); | ||||||
| var Raven = require('raven'); |  | ||||||
| var async = require('async'); | var async = require('async'); | ||||||
| var docker = require('./docker.js'); | var docker = require('./docker'); | ||||||
| var boot2docker = require('./boot2docker.js'); | var router = require('./router'); | ||||||
| var Setup = require('./Setup.react'); | var boot2docker = require('./boot2docker'); | ||||||
| var Containers = require('./Containers.react'); |  | ||||||
| var ContainerDetails = require('./ContainerDetails.react'); |  | ||||||
| var ContainerStore = require('./ContainerStore'); | var ContainerStore = require('./ContainerStore'); | ||||||
| var Radial = require('./Radial.react'); | var app = require('remote').require('app'); | ||||||
| 
 | 
 | ||||||
| var Route = Router.Route; | var Route = Router.Route; | ||||||
| var NotFoundRoute = Router.NotFoundRoute; | var NotFoundRoute = Router.NotFoundRoute; | ||||||
|  | @ -17,53 +18,23 @@ var DefaultRoute = Router.DefaultRoute; | ||||||
| var Link = Router.Link; | var Link = Router.Link; | ||||||
| var RouteHandler = Router.RouteHandler; | var RouteHandler = Router.RouteHandler; | ||||||
| 
 | 
 | ||||||
| var App = React.createClass({ | Bugsnag.apiKey = 'fc51aab02ce9dd1bb6ebc9fe2f4d43d7'; | ||||||
|   render: function () { | Bugsnag.autoNotify = true; | ||||||
|     return ( | Bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production'; | ||||||
|       <RouteHandler/> | Bugsnag.notifyReleaseStages = []; | ||||||
|     ); | Bugsnag.appVersion = app.getVersion(); | ||||||
|   } |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| var NoContainers = React.createClass({ | if (window.location.hash === '#/') { | ||||||
|   render: function () { |   router.run(function (Handler) { | ||||||
|     return ( |  | ||||||
|       <div> |  | ||||||
|         No Containers |  | ||||||
|       </div> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| var routes = ( |  | ||||||
|   <Route name="app" path="/" handler={App}> |  | ||||||
|     <Route name="containers" handler={Containers}> |  | ||||||
|       <Route name="container" path=":name" handler={ContainerDetails}> |  | ||||||
|       </Route> |  | ||||||
|       <DefaultRoute handler={NoContainers}/> |  | ||||||
|     </Route> |  | ||||||
|     <DefaultRoute handler={Setup}/> |  | ||||||
|     <Route name="setup" handler={Setup}> |  | ||||||
|     </Route> |  | ||||||
|   </Route> |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| boot2docker.ip(function (err, ip) { |  | ||||||
|   if (window.location.hash !== '#/') { |  | ||||||
|     docker.setHost(ip); |  | ||||||
|     ContainerStore.init(function () { |  | ||||||
|       Router.run(routes, function (Handler) { |  | ||||||
|     React.render(<Handler/>, document.body); |     React.render(<Handler/>, document.body); | ||||||
|   }); |   }); | ||||||
|     }); |  | ||||||
| } else { | } else { | ||||||
|     Router.run(routes, function (Handler) { |   boot2docker.ip(function (err, ip) { | ||||||
|  |     docker.setHost(ip); | ||||||
|  |     ContainerStore.init(function () { | ||||||
|  |       router.run(function (Handler) { | ||||||
|         React.render(<Handler/>, document.body); |         React.render(<Handler/>, document.body); | ||||||
|       }); |       }); | ||||||
|   } |  | ||||||
|     }); |     }); | ||||||
| 
 |   }); | ||||||
| if (process.env.NODE_ENV !== 'development') { |  | ||||||
|   Raven.config('https://0a5f032d745d4acaae94ce46f762c586@app.getsentry.com/35057', { |  | ||||||
|   }).install(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,8 @@ | ||||||
|  | var Router = require('react-router'); | ||||||
|  | var routes = require('./routes'); | ||||||
|  | 
 | ||||||
|  | var router = Router.create({ | ||||||
|  |   routes: routes | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | module.exports = router; | ||||||
|  | @ -0,0 +1,33 @@ | ||||||
|  | var React = require('react/addons'); | ||||||
|  | var Setup = require('./Setup.react'); | ||||||
|  | var Containers = require('./Containers.react'); | ||||||
|  | var ContainerDetails = require('./ContainerDetails.react'); | ||||||
|  | var NoContainers = require('./NoContainers.react'); | ||||||
|  | var Router = require('react-router'); | ||||||
|  | 
 | ||||||
|  | var Route = Router.Route; | ||||||
|  | var DefaultRoute = Router.DefaultRoute; | ||||||
|  | var RouteHandler = Router.RouteHandler; | ||||||
|  | 
 | ||||||
|  | var App = React.createClass({ | ||||||
|  |   render: function () { | ||||||
|  |     return ( | ||||||
|  |       <RouteHandler/> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | var routes = ( | ||||||
|  |   <Route name="app" path="/" handler={App}> | ||||||
|  |     <Route name="containers" handler={Containers}> | ||||||
|  |       <Route name="container" path=":name" handler={ContainerDetails}> | ||||||
|  |       </Route> | ||||||
|  |       <DefaultRoute handler={NoContainers}/> | ||||||
|  |     </Route> | ||||||
|  |     <DefaultRoute handler={Setup}/> | ||||||
|  |     <Route name="setup" handler={Setup}> | ||||||
|  |     </Route> | ||||||
|  |   </Route> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | module.exports = routes; | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
|         display: flex; |         display: flex; | ||||||
|         border-bottom: 1px solid transparent; |         border-bottom: 1px solid transparent; | ||||||
|         transition: border-bottom 0.25s; |         transition: border-bottom 0.25s; | ||||||
|         padding: 0px 28px 0px 10px; |         padding: 0px 10px 0px 10px; | ||||||
| 
 | 
 | ||||||
|         &.sep { |         &.sep { | ||||||
|           border-bottom: 1px solid #eee; |           border-bottom: 1px solid #eee; | ||||||
|  | @ -32,20 +32,26 @@ | ||||||
|         h4 { |         h4 { | ||||||
|           align-self: flex-start; |           align-self: flex-start; | ||||||
|           padding: 0 24px; |           padding: 0 24px; | ||||||
|           margin: 10px 0 0; |           margin: 14px 0 0; | ||||||
|           display: inline-block; |           display: inline-block; | ||||||
|  |           font-size: 14px; | ||||||
|           position: relative; |           position: relative; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         .create { |         .create { | ||||||
|           flex: 1 auto; |           flex: 1 auto; | ||||||
|           text-align: right; |           text-align: right; | ||||||
| 
 |           .btn { | ||||||
|           .wrapper { |             margin-top: 4px; | ||||||
|             text-align: center; |             padding: 4px 7px; | ||||||
|             display: inline-block; |             font-size: 16px; | ||||||
|  |             position: relative; | ||||||
|  |             .icon { | ||||||
|  |               position: relative; | ||||||
|  |               top: 3px; | ||||||
|  |               left: 1px; | ||||||
|  |             } | ||||||
|           } |           } | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  | @ -64,6 +70,7 @@ | ||||||
|           margin: 0; |           margin: 0; | ||||||
|           min-width: 240px; |           min-width: 240px; | ||||||
|           padding: 0; |           padding: 0; | ||||||
|  |           margin-top: 4px; | ||||||
| 
 | 
 | ||||||
|           display: flex; |           display: flex; | ||||||
|           flex-direction: column; |           flex-direction: column; | ||||||
|  | @ -72,9 +79,12 @@ | ||||||
|             color: inherit; |             color: inherit; | ||||||
|             flex-shrink: 0; |             flex-shrink: 0; | ||||||
|             cursor: default; |             cursor: default; | ||||||
|             margin: 0px 5px 0px 20px; |             margin: 0px 3px 0px 8px; | ||||||
|  |             outline: none; | ||||||
|  |             padding: 4px 5px; | ||||||
| 
 | 
 | ||||||
|             &.active { |             &.active { | ||||||
|  | <<<<<<< HEAD | ||||||
|               background: @brand-primary; |               background: @brand-primary; | ||||||
|               li { |               li { | ||||||
|                 .name { |                 .name { | ||||||
|  | @ -111,6 +121,38 @@ | ||||||
|               } |               } | ||||||
|               .state-stopped { |               .state-stopped { | ||||||
|                 .at2x('still-white.png', 20px, 20px); |                 .at2x('still-white.png', 20px, 20px); | ||||||
|  | ======= | ||||||
|  |               li { | ||||||
|  |                 border-bottom: none; | ||||||
|  |                 border-radius: 40px; | ||||||
|  |                 background: @brand-primary; | ||||||
|  |                 .name { | ||||||
|  |                   color: white; | ||||||
|  |                 } | ||||||
|  |                 .image { | ||||||
|  |                   color: white; | ||||||
|  |                   opacity: 0.9; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 .state-running { | ||||||
|  |                   .at2x('running-white.png', 20px, 20px); | ||||||
|  | 
 | ||||||
|  |                   .runningwave { | ||||||
|  |                     .at2x('runningwave-white.png', 20px, 20px); | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |                 .state-stopped { | ||||||
|  |                   .at2x('stopped-white.png', 20px, 20px); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 .state-downloading { | ||||||
|  |                   .at2x('downloading-white.png', 20px, 20px); | ||||||
|  | 
 | ||||||
|  |                   .downloading-arrow { | ||||||
|  |                     .at2x('downloading-arrow-white.png', 20px, 20px); | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  | >>>>>>> master | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -225,6 +267,22 @@ | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     .no-containers { | ||||||
|  |       flex: 1 auto; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       flex-direction: column; | ||||||
|  |       position: relative; | ||||||
|  | 
 | ||||||
|  |       h3 { | ||||||
|  |         position: relative; | ||||||
|  |         top: -44px; | ||||||
|  |         font-size: 18px; | ||||||
|  |         color: #C7D7D7; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     .details { |     .details { | ||||||
|       margin: 0; |       margin: 0; | ||||||
|       padding: 0; |       padding: 0; | ||||||
|  | @ -235,34 +293,41 @@ | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
| 
 | 
 | ||||||
|       .details-actions { |       .details-header { | ||||||
|  |         flex: 0 auto; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         padding: 4px 40px 10px 40px; | ||||||
|  |         position: relative; | ||||||
|  |         border-bottom: 1px solid #eee; | ||||||
|  | 
 | ||||||
|  |         .details-header-actions { | ||||||
|           flex: 0 auto; |           flex: 0 auto; | ||||||
|           display: flex; |           display: flex; | ||||||
|           flex-direction: row; |           flex-direction: row; | ||||||
|         padding: 20px 40px 10px 40px; |           margin-top: 24px; | ||||||
|  |           margin-bottom: 6px; | ||||||
|           position: relative; |           position: relative; | ||||||
|           border-bottom: 1px solid transparent; |           border-bottom: 1px solid transparent; | ||||||
|           transition: border-bottom 0.25s; |           transition: border-bottom 0.25s; | ||||||
|           .action { |           .action { | ||||||
|  |             flex: 0 auto; | ||||||
|             margin-right: 24px; |             margin-right: 24px; | ||||||
|           } |           } | ||||||
|       } |           .details-header-actions-rhs { | ||||||
| 
 |             flex: 1 auto; | ||||||
|       .details-tabs { |             display: flex; | ||||||
|         .tabs { |             align-items: right; | ||||||
|  |             justify-content: flex-end; | ||||||
|  |             a.btn { | ||||||
|               z-index: 0; |               z-index: 0; | ||||||
|           float: right; |             } | ||||||
|           margin-right: 40px; |  | ||||||
|           margin-top: -42px; |  | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|       .details-header { |         .details-header-info { | ||||||
|         flex: 0 auto; |  | ||||||
|           display: flex; |           display: flex; | ||||||
|           flex-direction: row; |           flex-direction: row; | ||||||
|         padding: 4px 40px 10px 40px; |  | ||||||
|         position: relative; |  | ||||||
|           a { |           a { | ||||||
|             position: absolute; |             position: absolute; | ||||||
|             right: 30px; |             right: 30px; | ||||||
|  | @ -296,6 +361,7 @@ | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       .details-progress { |       .details-progress { | ||||||
|         margin: 26% auto 0; |         margin: 26% auto 0; | ||||||
|  | @ -306,11 +372,12 @@ | ||||||
|         flex: 1; |         flex: 1; | ||||||
|         overflow: auto; |         overflow: auto; | ||||||
|         h4 { |         h4 { | ||||||
|           margin-top: 30px; |           font-size: 14px; | ||||||
|  |           margin-top: 16px; | ||||||
|           margin-left: 40px; |           margin-left: 40px; | ||||||
|         } |         } | ||||||
|         .logs { |         .logs { | ||||||
|           user-select: text; |           -webkit-user-select: text; | ||||||
|           font-family: Menlo; |           font-family: Menlo; | ||||||
|           font-size: 12px; |           font-size: 12px; | ||||||
|           padding: 18px 45px; |           padding: 18px 45px; | ||||||
|  |  | ||||||
|  | @ -3,9 +3,10 @@ | ||||||
| .header { | .header { | ||||||
|   min-width: 100%; |   min-width: 100%; | ||||||
|   flex: 0; |   flex: 0; | ||||||
|   min-height: 48px; |   min-height: 50px; | ||||||
|   -webkit-app-region: drag; |   -webkit-app-region: drag; | ||||||
|   -webkit-user-select: none; |   -webkit-user-select: none; | ||||||
|  |   // border-bottom: 1px solid #efefef; | ||||||
| 
 | 
 | ||||||
|   &.no-drag { |   &.no-drag { | ||||||
|     -webkit-app-region: no-drag; |     -webkit-app-region: no-drag; | ||||||
|  |  | ||||||
|  | @ -13,8 +13,13 @@ html, body { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|   -webkit-font-smoothing: antialiased; |   -webkit-font-smoothing: antialiased; | ||||||
|   user-select: none; |   -webkit-user-select: none; | ||||||
|   font-family: 'Clear Sans', sans-serif; |   font-family: 'Clear Sans', sans-serif; | ||||||
|  | 
 | ||||||
|  |   cursor: default; | ||||||
|  |   img { | ||||||
|  |     pointer-events: none; | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ::-webkit-scrollbar { | ::-webkit-scrollbar { | ||||||
|  | @ -49,8 +54,10 @@ html, body { | ||||||
|     box-shadow: 0 2px 5px rgba(0, 0, 0, 0.10); |     box-shadow: 0 2px 5px rgba(0, 0, 0, 0.10); | ||||||
|     border: none; //1px solid #ccc; |     border: none; //1px solid #ccc; | ||||||
|     height: 610px; |     height: 610px; | ||||||
|  |     display: flex; | ||||||
|   } |   } | ||||||
|   .modal-body { |   .modal-body { | ||||||
|  |     flex: 1 auto; | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: row; |     flex-direction: row; | ||||||
|     padding: 32px 32px; |     padding: 32px 32px; | ||||||
|  | @ -62,19 +69,10 @@ html, body { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     section.search { |     section.search { | ||||||
|       flex: 0 auto; |  | ||||||
|       min-width: 404px; |       min-width: 404px; | ||||||
|       padding-right: 32px; |       padding-right: 32px; | ||||||
|       border-right: 1px solid #eee; |       border-right: 1px solid #eee; | ||||||
| 
 | 
 | ||||||
|       .search-icon { |  | ||||||
|         font-size: 20px; |  | ||||||
|         color: @gray-normal; |  | ||||||
|         position: absolute; |  | ||||||
|         top: 40px; |  | ||||||
|         left: 45px; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .question { |       .question { | ||||||
|         a { |         a { | ||||||
|           transition: all 0.3s ease 0s; |           transition: all 0.3s ease 0s; | ||||||
|  | @ -89,15 +87,35 @@ html, body { | ||||||
|         text-align: right; |         text-align: right; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       .search-bar { | ||||||
|  |         position: relative; | ||||||
|  |         .loading { | ||||||
|  |           position: absolute; | ||||||
|  |           left: 13px; | ||||||
|  |           top: 10px; | ||||||
|  |           width: 20px; | ||||||
|  |           height: 20px; | ||||||
|  |           -webkit-animation-name: spin; | ||||||
|  |           -webkit-animation-duration: 1.8s; | ||||||
|  |           -webkit-animation-iteration-count: infinite; | ||||||
|  |           -webkit-animation-timing-function: linear; | ||||||
|  |         } | ||||||
|  |         .search-icon { | ||||||
|  |           font-size: 20px; | ||||||
|  |           color: @gray-lighter; | ||||||
|  |           position: absolute; | ||||||
|  |           top: 9px; | ||||||
|  |           left: 14px; | ||||||
|  |         } | ||||||
|         input { |         input { | ||||||
|           border-radius: 20px; |           border-radius: 20px; | ||||||
|           font-size: 13px; |           font-size: 13px; | ||||||
|           height: 38px; |           height: 38px; | ||||||
|           padding: 8px 16px 8px 40px; |           padding: 8px 16px 8px 40px; | ||||||
|         font-weight: 300; |  | ||||||
|           color: @gray-darkest; |           color: @gray-darkest; | ||||||
|           margin-bottom: 3px; |           margin-bottom: 3px; | ||||||
|         border-color: lighten(@gray-lighter, 10%); |           border-color: @gray-lightest; | ||||||
|  |           box-shadow: none; | ||||||
| 
 | 
 | ||||||
|           &:focus { |           &:focus { | ||||||
|             box-shadow: none; |             box-shadow: none; | ||||||
|  | @ -109,11 +127,22 @@ html, body { | ||||||
|             font-weight: 300; |             font-weight: 300; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       .results { |       .results { | ||||||
|         overflow: auto; |         overflow: auto; | ||||||
| 
 | 
 | ||||||
|  |         .no-results { | ||||||
|  |           text-align: center; | ||||||
|  |           h3 { | ||||||
|  |             color: #ABC0C0; | ||||||
|  |             font-size: 16px; | ||||||
|  |             margin-top: 160px; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         .title { |         .title { | ||||||
|  |           flex: 0 auto; | ||||||
|           margin-top: 16px; |           margin-top: 16px; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -147,7 +176,7 @@ html, body { | ||||||
|                 overflow: hidden; |                 overflow: hidden; | ||||||
|                 text-overflow: ellipsis; |                 text-overflow: ellipsis; | ||||||
|               } |               } | ||||||
|               .stars { |               .properties { | ||||||
|                 color: @gray-lighter; |                 color: @gray-lighter; | ||||||
|                 margin-top: 2px; |                 margin-top: 2px; | ||||||
| 
 | 
 | ||||||
|  | @ -188,7 +217,7 @@ html, body { | ||||||
|   height: 100%; |   height: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @-webkit-keyframes translatedownload { | @-webkit-keyframes spin { | ||||||
|   from { |   from { | ||||||
|     -webkit-transform: rotate(0deg); |     -webkit-transform: rotate(0deg); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -33,7 +33,7 @@ h4 { | ||||||
| 
 | 
 | ||||||
| // Mixin for generating new styles | // Mixin for generating new styles | ||||||
| .btn-styles(@btn-color: @gray-normal) { | .btn-styles(@btn-color: @gray-normal) { | ||||||
|   transition: all 0.3s ease 0s; |   transition: all 0.1s; | ||||||
|   .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners |   .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners | ||||||
|   border-color: @btn-color; |   border-color: @btn-color; | ||||||
|   color: @btn-color; |   color: @btn-color; | ||||||
|  | @ -42,18 +42,22 @@ h4 { | ||||||
|   &:focus { |   &:focus { | ||||||
|     border-color: darken(@btn-color, 10%); |     border-color: darken(@btn-color, 10%); | ||||||
|     color: darken(@btn-color, 10%); |     color: darken(@btn-color, 10%); | ||||||
|  |     cursor: default; | ||||||
|  |     box-shadow: none; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &:active { |   &:active { | ||||||
|     background-color: lighten(@btn-color, 45%); |     background-color: lighten(@btn-color, 45%); | ||||||
|     border-color: darken(@btn-color, 10%); |     border-color: darken(@btn-color, 10%); | ||||||
|     color: darken(@btn-color, 10%); |     color: darken(@btn-color, 10%); | ||||||
|  |     box-shadow: none; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &.active { |   &.active { | ||||||
|     background-color: @btn-color; |     background-color: @btn-color; | ||||||
|     color: white; |     color: white; | ||||||
|     box-shadow: none; |     box-shadow: none; | ||||||
|  |     box-shadow: none; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &:disabled, |   &:disabled, | ||||||
|  | @ -71,12 +75,6 @@ h4 { | ||||||
|         margin-right: 4px; |         margin-right: 4px; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     &.only-icon { |  | ||||||
|       padding: 0px 14px 0px 14px; |  | ||||||
|       .icon:before { |  | ||||||
|         top: 5px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -92,37 +90,25 @@ h4 { | ||||||
|   text-shadow: none; |   text-shadow: none; | ||||||
|   padding: 6px 14px 6px 14px; |   padding: 6px 14px 6px 14px; | ||||||
|   height: 32px; |   height: 32px; | ||||||
|   .icon:before { |   cursor: default; | ||||||
|     width: 20px; | 
 | ||||||
|     height: 20px; |   .content { | ||||||
|     position: relative; |     position: relative; | ||||||
|     top: 5px; |     top: -4px; | ||||||
|     font-size: 20px; |     margin-left: 5px; | ||||||
|     margin-right: 4px; |     margin-right: 5px; | ||||||
|   } |   } | ||||||
|   &.with-icon { | 
 | ||||||
|     padding: 0px 14px 6px 14px; |   .icon { | ||||||
|   } |  | ||||||
|   &.only-icon { |  | ||||||
|     padding: 0px 5px 10px 5px; |  | ||||||
|     .icon:before { |  | ||||||
|     position: relative; |     position: relative; | ||||||
|       margin-right: 0px; |     font-size: 16px; | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   .icon-dropdown { |  | ||||||
|     &.icon:before { |  | ||||||
|       font-size: 10px; |  | ||||||
|       position: relative; |  | ||||||
|       top: 1px; |  | ||||||
|       margin-left: 4px; |  | ||||||
|       margin-right: 0px; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   // Remove the gradient for the pressed/active state |   // Remove the gradient for the pressed/active state | ||||||
|   &:active, |   &:active, | ||||||
|   &.active { |   &.active { | ||||||
|     background-image: none; |     background-image: none; | ||||||
|  |     box-shadow: none; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &:focus, |   &:focus, | ||||||
|  |  | ||||||
|  | @ -16,6 +16,8 @@ if (argv.test) { | ||||||
|   console.log('Running tests'); |   console.log('Running tests'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | process.env.NODE_PATH = __dirname + '/../node_modules'; | ||||||
|  | 
 | ||||||
| app.on('activate-with-no-open-windows', function () { | app.on('activate-with-no-open-windows', function () { | ||||||
|   if (mainWindow) { |   if (mainWindow) { | ||||||
|     mainWindow.show(); |     mainWindow.show(); | ||||||
|  | @ -25,13 +27,14 @@ app.on('activate-with-no-open-windows', function () { | ||||||
| 
 | 
 | ||||||
| app.on('ready', function() { | app.on('ready', function() { | ||||||
|   var windowOptions = { |   var windowOptions = { | ||||||
|     width: 1200, |     width: 1000, | ||||||
|     height: 800, |     height: 700, | ||||||
|     'min-width': 960, |     'min-width': 1000, | ||||||
|     'min-height': 700, |     'min-height': 700, | ||||||
|     resizable: true, |     resizable: true, | ||||||
|     frame: false |     frame: false | ||||||
|   }; |   }; | ||||||
|  | 
 | ||||||
|   mainWindow = new BrowserWindow(windowOptions); |   mainWindow = new BrowserWindow(windowOptions); | ||||||
|   mainWindow.hide(); |   mainWindow.hide(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,18 @@ | ||||||
|  | #!/bin/bash | ||||||
|  | 
 | ||||||
|  | BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | ||||||
|  | export BOOT2DOCKER_CLI_VERSION=$(node -pe "JSON.parse(process.argv[1])['boot2docker-version']" "$(cat $BASE/package.json)") | ||||||
|  | export BOOT2DOCKER_CLI_VERSION_FILE=boot2docker-$BOOT2DOCKER_CLI_VERSION | ||||||
|  | 
 | ||||||
|  | mkdir -p $BASE/cache | ||||||
|  | 
 | ||||||
|  | pushd $BASE/resources > /dev/null | ||||||
|  | 
 | ||||||
|  | if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then | ||||||
|  |   echo "-----> Downloading Boot2docker CLI..." | ||||||
|  |   rm -rf boot2docker-* | ||||||
|  |   curl -L -o $BOOT2DOCKER_CLI_VERSION_FILE https://github.com/boot2docker/boot2docker-cli/releases/download/v${BOOT2DOCKER_CLI_VERSION}/boot2docker-v${BOOT2DOCKER_CLI_VERSION}-darwin-amd64 | ||||||
|  |   chmod +x $BOOT2DOCKER_CLI_VERSION_FILE | ||||||
|  | fi | ||||||
|  | 
 | ||||||
|  | popd > /dev/null | ||||||
							
								
								
									
										65
									
								
								gulpfile.js
								
								
								
								
							
							
						
						|  | @ -4,8 +4,7 @@ var browserify = require('browserify'); | ||||||
| var watchify = require('watchify'); | var watchify = require('watchify'); | ||||||
| var reactify = require('reactify'); | var reactify = require('reactify'); | ||||||
| var gulpif = require('gulp-if'); | var gulpif = require('gulp-if'); | ||||||
| var uglify = require('gulp-uglify'); | var uglify = require('gulp-uglifyjs'); | ||||||
| var streamify = require('gulp-streamify'); |  | ||||||
| var notify = require('gulp-notify'); | var notify = require('gulp-notify'); | ||||||
| var concat = require('gulp-concat'); | var concat = require('gulp-concat'); | ||||||
| var less = require('gulp-less'); | var less = require('gulp-less'); | ||||||
|  | @ -22,56 +21,29 @@ var ecstatic = require('ecstatic'); | ||||||
| var downloadatomshell = require('gulp-download-atom-shell'); | var downloadatomshell = require('gulp-download-atom-shell'); | ||||||
| var packagejson = require('./package.json'); | var packagejson = require('./package.json'); | ||||||
| var http = require('http'); | var http = require('http'); | ||||||
|  | var react = require('gulp-react'); | ||||||
|  | var fs = require('fs'); | ||||||
| 
 | 
 | ||||||
| var dependencies = Object.keys(packagejson.dependencies); | var dependencies = Object.keys(packagejson.dependencies); | ||||||
| var devDependencies = Object.keys(packagejson.devDependencies); | var devDependencies = Object.keys(packagejson.devDependencies); | ||||||
| var options = { | var options = { | ||||||
|   dev: process.argv.indexOf('release') === -1, |   dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1, | ||||||
|   test: process.argv.indexOf('test') !== -1, |   test: process.argv.indexOf('test') !== -1, | ||||||
|   filename: 'Kitematic.app', |   filename: 'Kitematic.app', | ||||||
|   name: 'Kitematic', |   name: 'Kitematic' | ||||||
|   signing_identity: process.env.XCODE_SIGNING_IDENTITY |   //signing_identity: fs.readFileSync('./identity')
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| gulp.task('js', function () { | gulp.task('js', function () { | ||||||
|   var bundler = browserify({ |   gulp.src('./app/**/*.js') | ||||||
|     entries: ['./app/main.js'], // Only need initial file, browserify finds the rest
 |     .pipe(plumber(function(error) { | ||||||
|     transform: [reactify], // We want to convert JSX to normal javascript
 |       gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message)); | ||||||
|     debug: options.dev, // Gives us sourcemapping
 |       // emit the end event, to properly end the task
 | ||||||
|     builtins: false, |       this.emit('end'); | ||||||
|     commondir: false, |     })) | ||||||
|     insertGlobals: false, |     .pipe(react()) | ||||||
|     detectGlobals: false, |  | ||||||
|     bundleExternal: false, |  | ||||||
|     cache: {}, packageCache: {}, fullPaths: options.dev // Requirement of watchify
 |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   // We set our dependencies as externals on our app bundler when developing
 |  | ||||||
|   dependencies.forEach(function (dep) { |  | ||||||
|     bundler.external(dep); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   devDependencies.forEach(function (dep) { |  | ||||||
|     bundler.external(dep); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   bundler.external('./app'); |  | ||||||
| 
 |  | ||||||
|   var bundle = function () { |  | ||||||
|     return bundler.bundle() |  | ||||||
|       .on('error', gutil.log) |  | ||||||
|       .pipe(source('main.js')) |  | ||||||
|       .pipe(gulpif(!options.dev, streamify(uglify()))) |  | ||||||
|     .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) |     .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) | ||||||
|       .pipe(gulpif(options.dev && !options.test, livereload())); |     .pipe(gulpif(options.dev, livereload())); | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   if (options.dev) { |  | ||||||
|     bundler = watchify(bundler); |  | ||||||
|     bundler.on('update', bundle); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return bundle(); |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| gulp.task('specs', function () { | gulp.task('specs', function () { | ||||||
|  | @ -109,7 +81,7 @@ gulp.task('images', function() { | ||||||
|       svgoPlugins: [{removeViewBox: false}] |       svgoPlugins: [{removeViewBox: false}] | ||||||
|     })) |     })) | ||||||
|     .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) |     .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) | ||||||
|     .pipe(gulpif(options.dev && !options.test, livereload())); |     .pipe(gulpif(options.dev, livereload())); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| gulp.task('styles', function () { | gulp.task('styles', function () { | ||||||
|  | @ -138,16 +110,16 @@ gulp.task('download', function (cb) { | ||||||
| gulp.task('copy', function () { | gulp.task('copy', function () { | ||||||
|   gulp.src('./app/index.html') |   gulp.src('./app/index.html') | ||||||
|     .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) |     .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) | ||||||
|     .pipe(gulpif(options.dev && !options.test, livereload())); |     .pipe(gulpif(options.dev, livereload())); | ||||||
| 
 | 
 | ||||||
|   gulp.src('./app/fonts/**') |   gulp.src('./app/fonts/**') | ||||||
|     .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) |     .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) | ||||||
|     .pipe(gulpif(options.dev && !options.test, livereload())); |     .pipe(gulpif(options.dev, livereload())); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| gulp.task('dist', function (cb) { | gulp.task('dist', function (cb) { | ||||||
|   var stream = gulp.src('').pipe(shell([ |   var stream = gulp.src('').pipe(shell([ | ||||||
|     'rm -rf ./dist/osx', |     'rm -Rf ./dist', | ||||||
|     'mkdir -p ./dist/osx', |     'mkdir -p ./dist/osx', | ||||||
|     'cp -R ./cache/Atom.app ./dist/osx/<%= filename %>', |     'cp -R ./cache/Atom.app ./dist/osx/<%= filename %>', | ||||||
|     'mv ./dist/osx/<%= filename %>/Contents/MacOS/Atom ./dist/osx/<%= filename %>/Contents/MacOS/<%= name %>', |     'mv ./dist/osx/<%= filename %>/Contents/MacOS/Atom ./dist/osx/<%= filename %>/Contents/MacOS/<%= name %>', | ||||||
|  | @ -213,6 +185,7 @@ gulp.task('test', ['download', 'copy', 'js', 'images', 'styles', 'specs'], funct | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () { | gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () { | ||||||
|  |   gulp.watch('./app/**/*.js', ['js']); | ||||||
|   gulp.watch('./app/**/*.html', ['copy']); |   gulp.watch('./app/**/*.html', ['copy']); | ||||||
|   gulp.watch('./app/styles/**/*.less', ['styles']); |   gulp.watch('./app/styles/**/*.less', ['styles']); | ||||||
|   gulp.watch('./app/images/**', ['images']); |   gulp.watch('./app/images/**', ['images']); | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								package.json
								
								
								
								
							
							
						
						|  | @ -11,7 +11,10 @@ | ||||||
|   }, |   }, | ||||||
|   "bugs": "https://github.com/kitematic/kitematic/issues", |   "bugs": "https://github.com/kitematic/kitematic/issues", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "./script/run" |     "start": "gulp", | ||||||
|  |     "preinstall": "./deps", | ||||||
|  |     "test": "gulp test", | ||||||
|  |     "release": ". ./script/identity && gulp release" | ||||||
|   }, |   }, | ||||||
|   "licenses": [ |   "licenses": [ | ||||||
|     { |     { | ||||||
|  | @ -24,26 +27,22 @@ | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "ansi-to-html": "0.2.0", |     "ansi-to-html": "0.2.0", | ||||||
|     "async": "^0.9.0", |     "async": "^0.9.0", | ||||||
|  |     "bugsnag-js": "git+https://git@github.com/bugsnag/bugsnag-js", | ||||||
|     "dockerode": "2.0.4", |     "dockerode": "2.0.4", | ||||||
|     "exec": "0.1.2", |     "exec": "0.1.2", | ||||||
|     "flux-react": "^2.6.1", |     "gulp-react": "^2.0.0", | ||||||
|     "jquery": "^2.1.3", |     "jquery": "^2.1.3", | ||||||
|     "leveldown": "^1.0.0", |  | ||||||
|     "levelup": "git+https://github.com/kitematic/node-levelup.git", |  | ||||||
|     "minimist": "^1.1.0", |     "minimist": "^1.1.0", | ||||||
|     "moment": "2.8.1", |     "moment": "2.8.1", | ||||||
|     "ncp": "0.6.0", |  | ||||||
|     "node-uuid": "1.4.1", |     "node-uuid": "1.4.1", | ||||||
|     "open": "0.0.5", |     "open": "0.0.5", | ||||||
|     "raven": "^0.7.2", |     "react": "^0.12.2", | ||||||
|     "react": "^0.12.1", |  | ||||||
|     "react-bootstrap": "^0.13.2", |     "react-bootstrap": "^0.13.2", | ||||||
|     "react-retina-image": "^1.1.2", |     "react-retina-image": "^1.1.2", | ||||||
|     "react-router": "^0.11.6", |     "react-router": "^0.11.6", | ||||||
|     "request": "2.42.0", |     "request": "2.42.0", | ||||||
|     "request-progress": "0.3.1", |     "request-progress": "0.3.1", | ||||||
|     "retina.js": "^1.1.0", |     "retina.js": "^1.1.0", | ||||||
|     "tar": "0.1.20", |  | ||||||
|     "underscore": "^1.7.0" |     "underscore": "^1.7.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  | @ -65,9 +64,9 @@ | ||||||
|     "gulp-sourcemaps": "^1.2.8", |     "gulp-sourcemaps": "^1.2.8", | ||||||
|     "gulp-streamify": "0.0.5", |     "gulp-streamify": "0.0.5", | ||||||
|     "gulp-uglify": "^0.3.1", |     "gulp-uglify": "^0.3.1", | ||||||
|  |     "gulp-uglifyjs": "^0.5.0", | ||||||
|     "gulp-util": "^3.0.0", |     "gulp-util": "^3.0.0", | ||||||
|     "jasmine-tagged": "^1.1.2", |     "jasmine-tagged": "^1.1.2", | ||||||
|     "object-assign": "^2.0.0", |  | ||||||
|     "reactify": "^0.15.2", |     "reactify": "^0.15.2", | ||||||
|     "run-sequence": "^1.0.2", |     "run-sequence": "^1.0.2", | ||||||
|     "vinyl-source-stream": "^0.1.1", |     "vinyl-source-stream": "^0.1.1", | ||||||
|  |  | ||||||
							
								
								
									
										35
									
								
								script/env
								
								
								
								
							
							
						
						|  | @ -1,35 +0,0 @@ | ||||||
| #!/bin/bash |  | ||||||
| 
 |  | ||||||
| DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" |  | ||||||
| BASE=$DIR/.. |  | ||||||
| export NODE_VERSION="0.11.14" |  | ||||||
| export NPM="$BASE/cache/node-v$NODE_VERSION/bin/npm" |  | ||||||
| export NODE="$BASE/cache/node-v$NODE_VERSION/bin/node" |  | ||||||
| export PATH="$BASE/cache/node-v$NODE_VERSION/bin/:$BASE/node_modules/.bin:$PATH" |  | ||||||
| export NODE_PATH="$BASE/node_modules" |  | ||||||
| export BOOT2DOCKER_CLI_VERSION=$($NODE -pe "JSON.parse(process.argv[1])['boot2docker-version']" "$(cat $BASE/package.json)") |  | ||||||
| export BOOT2DOCKER_CLI_VERSION_FILE=boot2docker-$BOOT2DOCKER_CLI_VERSION |  | ||||||
| 
 |  | ||||||
| mkdir -p $BASE/cache |  | ||||||
| 
 |  | ||||||
| pushd $BASE/cache > /dev/null |  | ||||||
| 
 |  | ||||||
| if [ ! -f "$NODE" ]; then |  | ||||||
|   curl -L -o node-v$NODE_VERSION-darwin-x64.tar.gz http://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-x64.tar.gz |  | ||||||
|   mkdir -p node-v$NODE_VERSION |  | ||||||
|   tar -xzf node-v$NODE_VERSION-darwin-x64.tar.gz --strip-components 1 -C node-v$NODE_VERSION |  | ||||||
|   rm -rf node-v$NODE_VERSION-darwin-x64.tar.gz |  | ||||||
| fi |  | ||||||
| 
 |  | ||||||
| popd > /dev/null |  | ||||||
| 
 |  | ||||||
| pushd $BASE/resources > /dev/null |  | ||||||
| 
 |  | ||||||
| if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then |  | ||||||
|   cecho "-----> Downloading Boot2docker CLI..." $purple |  | ||||||
|   rm -rf boot2docker-* |  | ||||||
|   curl -L -o $BOOT2DOCKER_CLI_VERSION_FILE https://github.com/boot2docker/boot2docker-cli/releases/download/v${BOOT2DOCKER_CLI_VERSION}/boot2docker-v${BOOT2DOCKER_CLI_VERSION}-darwin-amd64 |  | ||||||
|   chmod +x $BOOT2DOCKER_CLI_VERSION_FILE |  | ||||||
| fi |  | ||||||
| 
 |  | ||||||
| popd > /dev/null |  | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| #!/bin/bash |  | ||||||
| DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" |  | ||||||
| source $DIR/env |  | ||||||
| 
 |  | ||||||
| gulp $* |  | ||||||
							
								
								
									
										11
									
								
								script/npm
								
								
								
								
							
							
						
						|  | @ -1,11 +0,0 @@ | ||||||
| #!/bin/bash |  | ||||||
| 
 |  | ||||||
| DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" |  | ||||||
| source $DIR/env |  | ||||||
| 
 |  | ||||||
| ATOM_SHELL_VERSION=$($NODE -pe "JSON.parse(process.argv[1])['atom-shell-version']" "$(cat package.json)") |  | ||||||
| export npm_config_disturl=https://gh-contractor-zcbenz.s3.amazonaws.com/atom-shell/dist |  | ||||||
| export npm_config_target=$ATOM_SHELL_VERSION |  | ||||||
| export npm_config_arch=ia64 |  | ||||||
| 
 |  | ||||||
| HOME=~/.atom-shell-gyp $NPM $* |  | ||||||
|  | @ -1,10 +0,0 @@ | ||||||
| #!/bin/bash |  | ||||||
| 
 |  | ||||||
| DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" |  | ||||||
| source $DIR/env |  | ||||||
| 
 |  | ||||||
| if [ -f $DIR/identity ]; then |  | ||||||
|   source $DIR/identity |  | ||||||
| fi |  | ||||||
| 
 |  | ||||||
| gulp release |  | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| #!/bin/bash |  | ||||||
| DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" |  | ||||||
| source $DIR/env |  | ||||||
| 
 |  | ||||||
| gulp test |  | ||||||