Merge branch 'master' into sean
Conflicts: app/styles/containers.less
|  | @ -5,16 +5,10 @@ node_modules | |||
| npm-debug.log | ||||
| 
 | ||||
| # Signing Identity | ||||
| script/identity | ||||
| identity | ||||
| 
 | ||||
| # Resources | ||||
| resources/virtualbox-*.pkg | ||||
| resources/boot2docker* | ||||
| resources/mongod | ||||
| resources/MONGOD_LICENSE.txt | ||||
| resources/node | ||||
| resources/NODE_LICENSE.txt | ||||
| resources/settings.json | ||||
| 
 | ||||
| # Cache | ||||
| cache | ||||
|  |  | |||
|  | @ -15,15 +15,15 @@ Kitematic's documentation and other information can be found at [http://kitemati | |||
| ### Development | ||||
| 
 | ||||
| - `sudo npm install -g less` | ||||
| - `./script/npm install` | ||||
| - `npm install` | ||||
| 
 | ||||
| To run the app in development: | ||||
| 
 | ||||
| - `./script/gulp` | ||||
| - `npm start` | ||||
| 
 | ||||
| ### Building the Mac OS X Package | ||||
| 
 | ||||
| - `./script/release` | ||||
| - `npm run release` | ||||
| 
 | ||||
| ## Uninstalling | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,7 @@ | |||
| var _ = require('underscore'); | ||||
| var React = require('react'); | ||||
| var $ = require('jquery'); | ||||
| var React = require('react/addons'); | ||||
| var Router = require('react-router'); | ||||
| var Convert = require('ansi-to-html'); | ||||
| var convert = new Convert(); | ||||
| var ContainerStore = require('./ContainerStore'); | ||||
| var docker = require('./docker'); | ||||
| var exec = require('exec'); | ||||
|  | @ -17,85 +16,70 @@ var RouteHandler = Router.RouteHandler; | |||
| 
 | ||||
| var ContainerDetails = React.createClass({ | ||||
|   mixins: [Router.State], | ||||
|   _oldHeight: 0, | ||||
|   PAGE_LOGS: 'logs', | ||||
|   PAGE_SETTINGS: 'settings', | ||||
|   getInitialState: function () { | ||||
|     return { | ||||
|       logs: [] | ||||
|       logs: [], | ||||
|       page: this.PAGE_LOGS | ||||
|     }; | ||||
|   }, | ||||
|   componentWillReceiveProps: function () { | ||||
|     this.update(); | ||||
|     this.setState({ | ||||
|       logs: [] | ||||
|       page: this.PAGE_LOGS | ||||
|     }); | ||||
|     var self = this; | ||||
|     var logs = []; | ||||
|     var index = 0; | ||||
|     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(); | ||||
|     ContainerStore.fetchLogs(this.getParams().name, function () { | ||||
|       this.updateLogs(); | ||||
|     }.bind(this)); | ||||
|   }, | ||||
|   componentDidMount: function () { | ||||
|     ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.update); | ||||
|     ContainerStore.addChangeListener(ContainerStore.PROGRESS, this.update); | ||||
|     ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); | ||||
|     ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); | ||||
|   }, | ||||
|   componentWillUnmount: function () { | ||||
|     ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.update); | ||||
|     ContainerStore.removeChangeListener(ContainerStore.PROGRESS, this.update); | ||||
|     ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); | ||||
|     ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); | ||||
|   }, | ||||
|   update: function () { | ||||
|     var name = this.getParams().name; | ||||
|   componentDidUpdate: function () { | ||||
|     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({ | ||||
|       container: ContainerStore.container(name), | ||||
|       progress: ContainerStore.progress(name) | ||||
|       logs: ContainerStore.logs(this.getParams().name) | ||||
|     }); | ||||
|   }, | ||||
|   _escapeHTML: function (html) { | ||||
|     var text = document.createTextNode(html); | ||||
|     var div = document.createElement('div'); | ||||
|     div.appendChild(text); | ||||
|     return div.innerHTML; | ||||
|   updateProgress: function (name) { | ||||
|     console.log('progress', name, ContainerStore.progress(name)); | ||||
|     if (name === this.getParams().name) { | ||||
|       this.setState({ | ||||
|         progress: ContainerStore.progress(name) | ||||
|       }); | ||||
|     } | ||||
|   }, | ||||
|   showLogs: function () { | ||||
|     this.setState({ | ||||
|       page: this.PAGE_LOGS | ||||
|     }); | ||||
|   }, | ||||
|   showSettings: function () { | ||||
|     this.setState({ | ||||
|       page: this.PAGE_SETTINGS | ||||
|     }); | ||||
|   }, | ||||
|   handleClick: function (name) { | ||||
|     var container = this.state.container; | ||||
|     var container = this.props.container; | ||||
|     boot2docker.ip(function (err, ip) { | ||||
|       var ports = _.map(container.NetworkSettings.Ports, function (value, key) { | ||||
|         var portProtocolPair = key.split('/'); | ||||
|  | @ -113,7 +97,6 @@ var ContainerDetails = React.createClass({ | |||
|         } | ||||
|         return res; | ||||
|       }); | ||||
|       console.log(ports); | ||||
|       exec(['open', ports[0].url], function (err) { | ||||
|         if (err) { throw err; } | ||||
|       }); | ||||
|  | @ -130,28 +113,19 @@ var ContainerDetails = React.createClass({ | |||
|       return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>; | ||||
|     }); | ||||
| 
 | ||||
|     if (!this.state.container) { | ||||
|     if (!this.props.container) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     var state; | ||||
|     if (this.state.container.State.Running) { | ||||
|     if (this.props.container.State.Running) { | ||||
|       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>; | ||||
|     } else if (this.state.container.State.Paused) { | ||||
|     } else if (this.props.container.State.Paused) { | ||||
|       state = <h2 className="status paused">paused</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>; | ||||
|     } else if (this.props.container.State.Downloading) { | ||||
|       state = <h2 className="status">downloading</h2>; | ||||
|     } | ||||
| 
 | ||||
|     var button; | ||||
|  | @ -161,42 +135,75 @@ var ContainerDetails = React.createClass({ | |||
|       button = <a className="btn btn-primary disabled" onClick={this.handleClick}>View</a>; | ||||
|     } | ||||
| 
 | ||||
|     var name = this.state.container.Name.replace('/', ''); | ||||
|     var image = this.state.container.Config.Image; | ||||
|     var body; | ||||
|     if (this.props.container.State.Downloading) { | ||||
|       body = ( | ||||
|         <div className="details-progress"> | ||||
|           <ProgressBar now={this.state.progress * 100} label="%(percent)s%" /> | ||||
|         </div> | ||||
|       ); | ||||
|     } else { | ||||
|       if (this.state.page === this.PAGE_LOGS) { | ||||
|         body = ( | ||||
|           <div className="details-logs"> | ||||
|             <div className="logs"> | ||||
|               {logs} | ||||
|             </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"> | ||||
|           <h1>{name}</h1>{state}<h2 className="image-label">Image</h2><h2 className="image">{image}</h2> | ||||
|         </div> | ||||
|         <div className="details-actions"> | ||||
|           <div className="action btn-group"> | ||||
|             <a className="btn btn-action with-icon" onClick={this.handleClick}><span className="icon icon-preview-2"></span> View</a> | ||||
|             <a className="btn btn-action with-icon dropdown-toggle"><span className="icon-dropdown icon icon-arrow-37"></span></a> | ||||
|           <div className="details-header-info"> | ||||
|             <h1>{name}</h1>{state}<h2 className="image-label">Image</h2><h2 className="image">{image}</h2> | ||||
|           </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"> | ||||
|           <h4>Container Logs</h4> | ||||
|           <div className="logs"> | ||||
|             {logs} | ||||
|           <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> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -7,40 +7,16 @@ var Modal = require('react-bootstrap/Modal'); | |||
| var RetinaImage = require('react-retina-image'); | ||||
| var ModalTrigger = require('react-bootstrap/ModalTrigger'); | ||||
| var ContainerModal = require('./ContainerModal.react'); | ||||
| var ContainerStore = require('./ContainerStore'); | ||||
| var Header = require('./Header.react'); | ||||
| var docker = require('./docker'); | ||||
| 
 | ||||
| var Link = Router.Link; | ||||
| var RouteHandler = Router.RouteHandler; | ||||
| var Navigation= Router.Navigation; | ||||
| 
 | ||||
| var ContainerList = React.createClass({ | ||||
|   getInitialState: function () { | ||||
|     return { | ||||
|       containers: [] | ||||
|     }; | ||||
|   }, | ||||
|   componentDidMount: function () { | ||||
|     this.updateContainers(); | ||||
|     ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.updateContainers); | ||||
|   }, | ||||
|   componentWillMount: function () { | ||||
|     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 () { | ||||
|     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 env = container.Config.Env; | ||||
|       if (env.length) { | ||||
|  | @ -76,22 +52,20 @@ var ContainerList = React.createClass({ | |||
|         state = <div className="state state-stopped"></div>; | ||||
|       } | ||||
| 
 | ||||
|       var name = container.Name.replace('/', ''); | ||||
| 
 | ||||
|       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> | ||||
|             {state} | ||||
|             <div className="info"> | ||||
|               <div className="name"> | ||||
|                 {name} | ||||
|                 {container.Name} | ||||
|               </div> | ||||
|               <div className="image"> | ||||
|                 {imageName} | ||||
|               </div> | ||||
|             </div> | ||||
|           </li> | ||||
|         </Link> | ||||
|         </Router.Link> | ||||
|       ); | ||||
|     }); | ||||
|     return ( | ||||
|  |  | |||
|  | @ -1,36 +1,51 @@ | |||
| var async = require('async'); | ||||
| var $ = require('jquery'); | ||||
| var React = require('react'); | ||||
| var Router = require('react-router'); | ||||
| var React = require('react/addons'); | ||||
| var Modal = require('react-bootstrap/Modal'); | ||||
| var RetinaImage = require('react-retina-image'); | ||||
| var ContainerStore = require('./ContainerStore'); | ||||
| var OverlayTrigger = require('react-bootstrap/OverlayTrigger'); | ||||
| var Popover = require('react-bootstrap/Popover'); | ||||
| 
 | ||||
| var Navigation = Router.Navigation; | ||||
| 
 | ||||
| var ContainerModal = React.createClass({ | ||||
|   mixins: [Navigation], | ||||
|   _searchRequest: null, | ||||
|   getInitialState: function () { | ||||
|     return { | ||||
|       query: '', | ||||
|       results: [], | ||||
|       recommended: ContainerStore.recommended() | ||||
|       results: ContainerStore.recommended(), | ||||
|       loading: false, | ||||
|     }; | ||||
|   }, | ||||
|   componentDidMount: function () { | ||||
|     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) { | ||||
|     if (this._searchRequest) { | ||||
|       this._searchRequest.abort(); | ||||
|       this._searchRequest = null; | ||||
|     } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       loading: true | ||||
|     }); | ||||
| 
 | ||||
|     var self = this; | ||||
|     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; | ||||
|       if (self.isMounted()) { | ||||
|         self.setState(result); | ||||
|         console.log(result); | ||||
|       } | ||||
|     }); | ||||
|   }, | ||||
|  | @ -41,85 +56,106 @@ var ContainerModal = React.createClass({ | |||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (this._searchRequest) { | ||||
|       console.log('Cancel'); | ||||
|       this._searchRequest.abort(); | ||||
|       this._searchRequest = null; | ||||
|     } | ||||
|     clearTimeout(this.timeout); | ||||
|     var self = this; | ||||
|     this.timeout = setTimeout(function () { | ||||
|       self.search(query); | ||||
|     }, 250); | ||||
|     if (!query.length) { | ||||
|       this.setState({ | ||||
|         query: query, | ||||
|         results: ContainerStore.recommended() | ||||
|       }); | ||||
|     } else { | ||||
|       var self = this; | ||||
|       this.timeout = setTimeout(function () { | ||||
|         self.search(query); | ||||
|       }, 200); | ||||
|     } | ||||
|   }, | ||||
|   handleClick: function (event) { | ||||
|     var name = event.target.getAttribute('name'); | ||||
|     var self = this; | ||||
|     ContainerStore.create(name, 'latest', function (err, containerName) { | ||||
|       // this.transitionTo('containers', {container: containerName});
 | ||||
|       self.props.onRequestHide(); | ||||
|     }.bind(this)); | ||||
|     }); | ||||
|   }, | ||||
|   render: function () { | ||||
|     var self = this; | ||||
|     var data = this.state.results.slice(0, 7); | ||||
| 
 | ||||
|     var data; | ||||
|     if (this.state.query) { | ||||
|       data = this.state.results.splice(0, 7); | ||||
|     } else { | ||||
|       data = this.state.recommended; | ||||
|     } | ||||
|     var results = data.map(function (r) { | ||||
|       var name; | ||||
|       if (r.is_official) { | ||||
|         name = <span><RetinaImage src="official.png"/>{r.name}</span>; | ||||
|       } else { | ||||
|         name = <span>{r.name}</span>; | ||||
|       } | ||||
|       return ( | ||||
|         <li key={r.name}> | ||||
|           <div className="info"> | ||||
|             <div className="name"> | ||||
|               {name} | ||||
|     var results; | ||||
|     if (data.length) { | ||||
|       var items = data.map(function (r) { | ||||
|         var name; | ||||
|         if (r.is_official) { | ||||
|           name = <span><RetinaImage src="official.png"/>{r.name}</span>; | ||||
|         } else { | ||||
|           name = <span>{r.name}</span>; | ||||
|         } | ||||
|         return ( | ||||
|           <li key={r.name}> | ||||
|             <div className="info"> | ||||
|               <div className="name"> | ||||
|                 {name} | ||||
|               </div> | ||||
|               <div className="properties"> | ||||
|                 <div className="icon icon-star-9"></div> | ||||
|                 <div className="star-count">{r.star_count}</div> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div className="stars"> | ||||
|               <div className="icon icon-star-9"></div> | ||||
|               <div className="star-count">{r.star_count}</div> | ||||
|             <div className="action"> | ||||
|               <div className="btn-group"> | ||||
|                 <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-58"></span></a> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div className="action"> | ||||
|             <div className="btn-group"> | ||||
|               <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> | ||||
|             </div> | ||||
|           </div> | ||||
|         </li> | ||||
|           </li> | ||||
|         ); | ||||
|       }); | ||||
| 
 | ||||
|       results = ( | ||||
|         <div className="result-list"> | ||||
|           <ul> | ||||
|             {items} | ||||
|           </ul> | ||||
|         </div> | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     var title; | ||||
|     if (this.state.query) { | ||||
|       title = <h4 className="title">Results</h4>; | ||||
|     } 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 ( | ||||
|       <Modal {...this.props} animation={false} className="create-modal"> | ||||
|         <div className="modal-body"> | ||||
|           <section className="search"> | ||||
|             <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="search-bar"> | ||||
|               <input type="search" ref="searchInput" className="form-control" placeholder="Find an existing image" onChange={this.handleChange}/> | ||||
|               <div className={magnifierClasses}></div> | ||||
|               <RetinaImage className={loadingClasses} src="loading.png"/> | ||||
|             </div> | ||||
|             <div className="question"> | ||||
|               <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> | ||||
|               </OverlayTrigger> | ||||
|             </div> | ||||
|             <div className="results"> | ||||
|               {title} | ||||
|               <ul> | ||||
|                 {results} | ||||
|               </ul> | ||||
|               <div className="title">{title}</div> | ||||
|               {results} | ||||
|             </div> | ||||
|           </section> | ||||
|           <aside className="custom"> | ||||
|  |  | |||
|  | @ -1,21 +1,25 @@ | |||
| var EventEmitter = require('events').EventEmitter; | ||||
| 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 registry = require('./registry'); | ||||
| var $ = require('jquery'); | ||||
| var _ = require('underscore'); | ||||
| 
 | ||||
| // Merge our store with Node's Event Emitter
 | ||||
| var _recommended = []; | ||||
| var _containers = {}; | ||||
| var _progress = {}; | ||||
| var _logs = {}; | ||||
| 
 | ||||
| var ContainerStore = assign(EventEmitter.prototype, { | ||||
|   CONTAINERS: 'containers', | ||||
|   PROGRESS: 'progress', | ||||
|   LOGS: 'logs', | ||||
|   RECOMMENDED: 'recommended', | ||||
|   _recommended: [], | ||||
|   _containers: {}, | ||||
|   _progress: {}, | ||||
|   _logs: {}, | ||||
|   CLIENT_CONTAINER_EVENT: 'client_container', | ||||
|   SERVER_CONTAINER_EVENT: 'server_container', | ||||
|   SERVER_PROGRESS_EVENT: 'server_progress', | ||||
|   SERVER_RECOMMENDED_EVENT: 'server_recommended_event', | ||||
|   SERVER_LOGS_EVENT: 'server_logs', | ||||
|   _pullScratchImage: function (callback) { | ||||
|     var image = docker.client().getImage('scratch:latest'); | ||||
|     image.inspect(function (err, data) { | ||||
|  | @ -36,32 +40,98 @@ var ContainerStore = assign(EventEmitter.prototype, { | |||
|       } | ||||
|     }); | ||||
|   }, | ||||
|   _pullImage: function (repository, tag, callback, progressCallback) { | ||||
|     registry.layers(repository, tag, function (err, layerSizes) { | ||||
| 
 | ||||
|       // TODO: Support v2 registry API
 | ||||
|       // TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
 | ||||
|       // Use the per-layer pull progress % to update the total progress.
 | ||||
|       docker.client().listImages({all: 1}, function(err, images) { | ||||
|         var existingIds = new Set(images.map(function (image) { | ||||
|           return image.Id.slice(0, 12); | ||||
|         })); | ||||
|         var layersToDownload = layerSizes.filter(function (layerSize) { | ||||
|           return !existingIds.has(layerSize.Id); | ||||
|         }); | ||||
| 
 | ||||
|         var totalBytes = layersToDownload.map(function (s) { return s.size; }).reduce(function (pv, sv) { return pv + sv; }, 0); | ||||
|         docker.client().pull(repository + ':' + tag, function (err, stream) { | ||||
|           stream.setEncoding('utf8'); | ||||
| 
 | ||||
|           var layerProgress = layersToDownload.reduce(function (r, layer) { | ||||
|             if (_.findWhere(images, {Id: layer.Id})) { | ||||
|               r[layer.Id] = 100; | ||||
|             } else { | ||||
|               r[layer.Id] = 0; | ||||
|             } | ||||
|             return r; | ||||
|           }, {}); | ||||
| 
 | ||||
|           stream.on('data', function (str) { | ||||
|             var data = JSON.parse(str); | ||||
|             console.log(data); | ||||
| 
 | ||||
|             if (data.status === 'Already exists') { | ||||
|               layerProgress[data.id] = 1; | ||||
|             } else if (data.status === 'Downloading') { | ||||
|               var current = data.progressDetail.current; | ||||
|               var total = data.progressDetail.total; | ||||
|               var layerFraction = current / total; | ||||
|               layerProgress[data.id] = layerFraction; | ||||
|             } | ||||
| 
 | ||||
|             var chunks = layersToDownload.map(function (s) { | ||||
|               return layerProgress[s.Id] * s.size; | ||||
|             }); | ||||
| 
 | ||||
|             var totalReceived = chunks.reduce(function (pv, sv) { | ||||
|               return pv + sv; | ||||
|             }); | ||||
| 
 | ||||
|             var totalProgress = totalReceived / totalBytes; | ||||
|             progressCallback(totalProgress); | ||||
|           }); | ||||
|           stream.on('end', function () { | ||||
|             callback(); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }, | ||||
|   _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) { | ||||
|       console.log('Placeholder removed.'); | ||||
|       docker.client().createContainer({ | ||||
|         Image: image, | ||||
|         Tty: false, | ||||
|         name: name | ||||
|         name: name, | ||||
|         User: 'root' | ||||
|       }, 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); | ||||
|           if (err) { | ||||
|             callback(err); | ||||
|             return; | ||||
|           } | ||||
|           self.fetchContainer(name, callback); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }, | ||||
|   _createPlaceholderContainer: function (imageName, name, callback) { | ||||
|     console.log('_createPlaceholderContainer', imageName, name); | ||||
|     var self = this; | ||||
|     this._pullScratchImage(function (err) { | ||||
|       if (err) { | ||||
|         callback(err); | ||||
|  | @ -77,7 +147,11 @@ var ContainerStore = assign(EventEmitter.prototype, { | |||
|         Cmd: 'placeholder', | ||||
|         name: name | ||||
|       }, function (err, container) { | ||||
|         callback(err, container); | ||||
|         if (err) { | ||||
|           callback(err); | ||||
|           return; | ||||
|         } | ||||
|         self.fetchContainer(name, callback); | ||||
|       }); | ||||
|     }); | ||||
|   }, | ||||
|  | @ -86,7 +160,7 @@ var ContainerStore = assign(EventEmitter.prototype, { | |||
|     var count = 1; | ||||
|     var name = base; | ||||
|     while (true) { | ||||
|       var exists = _.findWhere(_.values(this._containers), {Name: '/' + name}) || _.findWhere(_.values(this._containers), {Name: name}); | ||||
|       var exists = _.findWhere(_.values(_containers), {Name: name}) || _.findWhere(_.values(_containers), {Name: name}); | ||||
|       if (!exists) { | ||||
|         return name; | ||||
|       } else { | ||||
|  | @ -95,82 +169,97 @@ var ContainerStore = assign(EventEmitter.prototype, { | |||
|       } | ||||
|     } | ||||
|   }, | ||||
|   init: function (callback) { | ||||
|     // TODO: Load cached data from db on loading
 | ||||
|   _resumePulling: function () { | ||||
|     var downloading = _.filter(_.values(_containers), function (container) { | ||||
|       return container.State.Downloading; | ||||
|     }); | ||||
| 
 | ||||
|     // Refresh with docker & hook into events
 | ||||
|     // Recover any pulls that were happening
 | ||||
|     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) { | ||||
|     downloading.forEach(function (container) { | ||||
|       docker.client().pull(container.KitematicDownloadingImage, 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.'); | ||||
|             }); | ||||
|           } | ||||
|         stream.on('data', function (data) {}); | ||||
|         stream.on('end', function () { | ||||
|           self._createContainer(container.KitematicDownloadingImage, container.Name, function () {}); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }, | ||||
|   update: function (callback) { | ||||
|   _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) { | ||||
|         docker.client().getContainer(container.Id).inspect(function (err, data) { | ||||
|           callback(err, data); | ||||
|       async.map(containers, function (container, callback) { | ||||
|         self.fetchContainer(container.Id, function (err) { | ||||
|           callback(err); | ||||
|         }); | ||||
|       }, 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); | ||||
|         callback(err); | ||||
|       }); | ||||
|     }); | ||||
|   }, | ||||
|   updateRecommended: function (callback) { | ||||
|   fetchRecommended: function (callback) { | ||||
|     if (_recommended.length) { | ||||
|      return; | ||||
|    } | ||||
|     var self = this; | ||||
|     $.ajax({ | ||||
|       url: 'https://kitematic.com/recommended.json', | ||||
|  | @ -185,7 +274,7 @@ var ContainerStore = assign(EventEmitter.prototype, { | |||
|             })); | ||||
|           }); | ||||
|         }, function (err, results) { | ||||
|           self._recommended = results; | ||||
|           _recommended = results; | ||||
|           callback(); | ||||
|         }); | ||||
|       }, | ||||
|  | @ -194,6 +283,54 @@ var ContainerStore = assign(EventEmitter.prototype, { | |||
|       } | ||||
|     }); | ||||
|   }, | ||||
|   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; | ||||
|  | @ -205,104 +342,57 @@ var ContainerStore = assign(EventEmitter.prototype, { | |||
|       if (!data) { | ||||
|         // Pull image
 | ||||
|         self._createPlaceholderContainer(imageName, containerName, function (err, container) { | ||||
|           if (err) { | ||||
|             console.log(err); | ||||
|           } | ||||
|           registry.layers(repository, tag, function (err, layerSizes) { | ||||
|             if (err) { | ||||
|               callback(err); | ||||
|             } | ||||
| 
 | ||||
|             // TODO: Support v2 registry API
 | ||||
|             // TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
 | ||||
|             // Use the per-layer pull progress % to update the total progress.
 | ||||
|             docker.client().listImages({all: 1}, function(err, images) { | ||||
|               var existingIds = new Set(images.map(function (image) { | ||||
|                 return image.Id.slice(0, 12); | ||||
|               })); | ||||
|               var layersToDownload = layerSizes.filter(function (layerSize) { | ||||
|                 return !existingIds.has(layerSize.Id); | ||||
|               }); | ||||
| 
 | ||||
|               var totalBytes = layersToDownload.map(function (s) { return s.size; }).reduce(function (pv, sv) { return pv + sv; }, 0); | ||||
|               docker.client().pull(imageName, function (err, stream) { | ||||
|                 callback(null, containerName); | ||||
|                 stream.setEncoding('utf8'); | ||||
| 
 | ||||
|                 var layerProgress = layersToDownload.reduce(function (r, layer) { | ||||
|                   if (_.findWhere(images, {Id: layer.Id})) { | ||||
|                     r[layer.Id] = 100; | ||||
|                   } else { | ||||
|                     r[layer.Id] = 0; | ||||
|                   } | ||||
|                   return r; | ||||
|                 }, {}); | ||||
| 
 | ||||
|                 self._progress[containerName] = 0; | ||||
| 
 | ||||
|                 stream.on('data', function (str) { | ||||
|                   console.log(str); | ||||
|                   var data = JSON.parse(str); | ||||
| 
 | ||||
|                   if (data.status === 'Already exists') { | ||||
|                     layerProgress[data.id] = 1; | ||||
|                   } else if (data.status === 'Downloading') { | ||||
|                     var current = data.progressDetail.current; | ||||
|                     var total = data.progressDetail.total; | ||||
|                     var layerFraction = current / total; | ||||
|                     layerProgress[data.id] = layerFraction; | ||||
|                   } | ||||
| 
 | ||||
|                   var chunks = layersToDownload.map(function (s) { | ||||
|                     return layerProgress[s.Id] * s.size; | ||||
|                   }); | ||||
| 
 | ||||
|                   var totalReceived = chunks.reduce(function (pv, sv) { | ||||
|                     return pv + sv; | ||||
|                   }); | ||||
| 
 | ||||
|                   var totalProgress = totalReceived / totalBytes; | ||||
|                   self._progress[containerName] = totalProgress; | ||||
|                   self.emit(self.PROGRESS); | ||||
|                 }); | ||||
|                 stream.on('end', function () { | ||||
|                   self._createContainer(imageName, containerName, function () { | ||||
|                     delete self._progress[containerName]; | ||||
|                   }); | ||||
|                 }); | ||||
|               }); | ||||
|           _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 { | ||||
|         // 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); | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   }, | ||||
|   containers: function() { | ||||
|     return this._containers; | ||||
|     return _containers; | ||||
|   }, | ||||
|   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 () { | ||||
|     return this._recommended; | ||||
|     return _recommended; | ||||
|   }, | ||||
|   progress: function (name) { | ||||
|     return this._progress[name]; | ||||
|     return _progress[name]; | ||||
|   }, | ||||
|   logs: function (name) { | ||||
|     return logs[name]; | ||||
|   }, | ||||
|   addChangeListener: function(eventType, callback) { | ||||
|     this.on(eventType, callback); | ||||
|   }, | ||||
|   removeChangeListener: function(eventType, callback) { | ||||
|     this.removeListener(eventType, callback); | ||||
|   }, | ||||
|     return _logs[name] || []; | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| module.exports = ContainerStore; | ||||
|  |  | |||
|  | @ -12,15 +12,51 @@ var _ = require('underscore'); | |||
| var docker = require('./docker'); | ||||
| var $ = require('jquery'); | ||||
| 
 | ||||
| var Link = Router.Link; | ||||
| var RouteHandler = Router.RouteHandler; | ||||
| 
 | ||||
| var Containers = React.createClass({ | ||||
|   mixins: [Router.Navigation, Router.State], | ||||
|   getInitialState: function () { | ||||
|     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) { | ||||
|     if (e.target.scrollTop > 0 && !this.state.sidebarOffset) { | ||||
|       this.setState({ | ||||
|  | @ -37,6 +73,7 @@ var Containers = React.createClass({ | |||
|     if (this.state.sidebarOffset) { | ||||
|       sidebarHeaderClass += ' sep'; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className="containers"> | ||||
|         <Header/> | ||||
|  | @ -46,17 +83,15 @@ var Containers = React.createClass({ | |||
|               <h4>My Containers</h4> | ||||
|               <div className="create"> | ||||
|                 <ModalTrigger modal={<ContainerModal/>}> | ||||
|                   <div className="wrapper"> | ||||
|                     <a className="btn btn-action only-icon"><span className="icon icon-add-1"></span></a> | ||||
|                   </div> | ||||
|                   <a className="btn btn-action only-icon"><span className="icon icon-add-1"></span></a> | ||||
|                 </ModalTrigger> | ||||
|               </div> | ||||
|             </section> | ||||
|             <section className="sidebar-containers" onScroll={this.handleScroll}> | ||||
|               <ContainerList/> | ||||
|               <ContainerList containers={this.state.sorted}/> | ||||
|             </section> | ||||
|           </div> | ||||
|           <RouteHandler/> | ||||
|           <Router.RouteHandler container={this.state.containers[this.getParams().name]}/> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -2,6 +2,11 @@ var React = require('react/addons'); | |||
| var remote = require('remote'); | ||||
| 
 | ||||
| var Header = React.createClass({ | ||||
|   getInitialState: function () { | ||||
|     return { | ||||
|       fullscreen: false | ||||
|     }; | ||||
|   }, | ||||
|   componentDidMount: function () { | ||||
|     document.addEventListener('keyup', this.handleDocumentKeyUp, false); | ||||
|   }, | ||||
|  | @ -22,14 +27,16 @@ var Header = React.createClass({ | |||
|   }, | ||||
|   handleFullscreen: function () { | ||||
|     remote.getCurrentWindow().setFullScreen(!remote.getCurrentWindow().isFullScreen()); | ||||
|     this.forceUpdate(); | ||||
|     this.setState({ | ||||
|       fullscreen: remote.getCurrentWindow().isFullScreen() | ||||
|     }); | ||||
|   }, | ||||
|   handleFullscreenHover: function () { | ||||
|     this.update(); | ||||
|   }, | ||||
|   render: function () { | ||||
|     var buttons; | ||||
|     if (remote.getCurrentWindow().isFullScreen()) { | ||||
|     if (this.state.fullscreen) { | ||||
|       return ( | ||||
|         <div className="header no-drag"> | ||||
|           <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 Radial = require('./Radial.react.js'); | ||||
| var async = require('async'); | ||||
|  | @ -134,6 +134,12 @@ var setupSteps = [ | |||
| 
 | ||||
| var Setup = React.createClass({ | ||||
|   mixins: [ Router.Navigation ], | ||||
|   getInitialState: function () { | ||||
|     return { | ||||
|       message: '', | ||||
|       progress: 0 | ||||
|     }; | ||||
|   }, | ||||
|   render: function () { | ||||
|     var radial; | ||||
|     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 | 
							
								
								
									
										71
									
								
								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 Router = require('react-router'); | ||||
| var RetinaImage = require('react-retina-image'); | ||||
| var Raven = require('raven'); | ||||
| var async = require('async'); | ||||
| var docker = require('./docker.js'); | ||||
| var boot2docker = require('./boot2docker.js'); | ||||
| var Setup = require('./Setup.react'); | ||||
| var Containers = require('./Containers.react'); | ||||
| var ContainerDetails = require('./ContainerDetails.react'); | ||||
| var docker = require('./docker'); | ||||
| var router = require('./router'); | ||||
| var boot2docker = require('./boot2docker'); | ||||
| var ContainerStore = require('./ContainerStore'); | ||||
| var Radial = require('./Radial.react'); | ||||
| var app = require('remote').require('app'); | ||||
| 
 | ||||
| var Route = Router.Route; | ||||
| var NotFoundRoute = Router.NotFoundRoute; | ||||
|  | @ -17,53 +18,23 @@ var DefaultRoute = Router.DefaultRoute; | |||
| var Link = Router.Link; | ||||
| var RouteHandler = Router.RouteHandler; | ||||
| 
 | ||||
| var App = React.createClass({ | ||||
|   render: function () { | ||||
|     return ( | ||||
|       <RouteHandler/> | ||||
|     ); | ||||
|   } | ||||
| }); | ||||
| Bugsnag.apiKey = 'fc51aab02ce9dd1bb6ebc9fe2f4d43d7'; | ||||
| Bugsnag.autoNotify = true; | ||||
| Bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production'; | ||||
| Bugsnag.notifyReleaseStages = []; | ||||
| Bugsnag.appVersion = app.getVersion(); | ||||
| 
 | ||||
| var NoContainers = React.createClass({ | ||||
|   render: function () { | ||||
|     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 !== '#/') { | ||||
| if (window.location.hash === '#/') { | ||||
|   router.run(function (Handler) { | ||||
|     React.render(<Handler/>, document.body); | ||||
|   }); | ||||
| } else { | ||||
|   boot2docker.ip(function (err, ip) { | ||||
|     docker.setHost(ip); | ||||
|     ContainerStore.init(function () { | ||||
|       Router.run(routes, function (Handler) { | ||||
|       router.run(function (Handler) { | ||||
|         React.render(<Handler/>, document.body); | ||||
|       }); | ||||
|     }); | ||||
|   } else { | ||||
|     Router.run(routes, function (Handler) { | ||||
|       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; | ||||
|         border-bottom: 1px solid transparent; | ||||
|         transition: border-bottom 0.25s; | ||||
|         padding: 0px 28px 0px 10px; | ||||
|         padding: 0px 10px 0px 10px; | ||||
| 
 | ||||
|         &.sep { | ||||
|           border-bottom: 1px solid #eee; | ||||
|  | @ -32,20 +32,26 @@ | |||
|         h4 { | ||||
|           align-self: flex-start; | ||||
|           padding: 0 24px; | ||||
|           margin: 10px 0 0; | ||||
|           margin: 14px 0 0; | ||||
|           display: inline-block; | ||||
|           font-size: 14px; | ||||
|           position: relative; | ||||
|         } | ||||
| 
 | ||||
|         .create { | ||||
|           flex: 1 auto; | ||||
|           text-align: right; | ||||
| 
 | ||||
|           .wrapper { | ||||
|             text-align: center; | ||||
|             display: inline-block; | ||||
|           .btn { | ||||
|             margin-top: 4px; | ||||
|             padding: 4px 7px; | ||||
|             font-size: 16px; | ||||
|             position: relative; | ||||
|             .icon { | ||||
|               position: relative; | ||||
|               top: 3px; | ||||
|               left: 1px; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|  | @ -64,6 +70,7 @@ | |||
|           margin: 0; | ||||
|           min-width: 240px; | ||||
|           padding: 0; | ||||
|           margin-top: 4px; | ||||
| 
 | ||||
|           display: flex; | ||||
|           flex-direction: column; | ||||
|  | @ -72,9 +79,12 @@ | |||
|             color: inherit; | ||||
|             flex-shrink: 0; | ||||
|             cursor: default; | ||||
|             margin: 0px 5px 0px 20px; | ||||
|             margin: 0px 3px 0px 8px; | ||||
|             outline: none; | ||||
|             padding: 4px 5px; | ||||
| 
 | ||||
|             &.active { | ||||
| <<<<<<< HEAD | ||||
|               background: @brand-primary; | ||||
|               li { | ||||
|                 .name { | ||||
|  | @ -111,6 +121,38 @@ | |||
|               } | ||||
|               .state-stopped { | ||||
|                 .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 { | ||||
|       margin: 0; | ||||
|       padding: 0; | ||||
|  | @ -235,64 +293,72 @@ | |||
|       display: flex; | ||||
|       flex-direction: column; | ||||
| 
 | ||||
|       .details-actions { | ||||
|         flex: 0 auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         padding: 20px 40px 10px 40px; | ||||
|         position: relative; | ||||
|         border-bottom: 1px solid transparent; | ||||
|         transition: border-bottom 0.25s; | ||||
|         .action { | ||||
|           margin-right: 24px; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       .details-tabs { | ||||
|         .tabs { | ||||
|           z-index: 0; | ||||
|           float: right; | ||||
|           margin-right: 40px; | ||||
|           margin-top: -42px; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       .details-header { | ||||
|         flex: 0 auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         flex-direction: column; | ||||
|         padding: 4px 40px 10px 40px; | ||||
|         position: relative; | ||||
|         a { | ||||
|           position: absolute; | ||||
|           right: 30px; | ||||
|           top: -4px; | ||||
|         } | ||||
|         h1 { | ||||
|           margin: 0; | ||||
|           font-size: 20px; | ||||
|           margin: 0; | ||||
|           color: @gray-darkest; | ||||
|         } | ||||
|         h2 { | ||||
|           &.status { | ||||
|             margin: 8px 0px 0px 16px; | ||||
|             text-transform: uppercase; | ||||
|             font-weight: bold; | ||||
|             font-size: 10px; | ||||
|             &.running { | ||||
|               color: @brand-positive; | ||||
|         border-bottom: 1px solid #eee; | ||||
| 
 | ||||
|         .details-header-actions { | ||||
|           flex: 0 auto; | ||||
|           display: flex; | ||||
|           flex-direction: row; | ||||
|           margin-top: 24px; | ||||
|           margin-bottom: 6px; | ||||
|           position: relative; | ||||
|           border-bottom: 1px solid transparent; | ||||
|           transition: border-bottom 0.25s; | ||||
|           .action { | ||||
|             flex: 0 auto; | ||||
|             margin-right: 24px; | ||||
|           } | ||||
|           .details-header-actions-rhs { | ||||
|             flex: 1 auto; | ||||
|             display: flex; | ||||
|             align-items: right; | ||||
|             justify-content: flex-end; | ||||
|             a.btn { | ||||
|               z-index: 0; | ||||
|             } | ||||
|           } | ||||
|           &.image-label { | ||||
|             margin: 8px 0px 0px 30px; | ||||
|             font-size: 10px; | ||||
|             color: @gray-lighter; | ||||
|         } | ||||
| 
 | ||||
|         .details-header-info { | ||||
|           display: flex; | ||||
|           flex-direction: row; | ||||
|           a { | ||||
|             position: absolute; | ||||
|             right: 30px; | ||||
|             top: -4px; | ||||
|           } | ||||
|           &.image { | ||||
|             margin: 5px 0px 0px 16px; | ||||
|             font-size: 14px; | ||||
|             color: @gray-normal; | ||||
|           h1 { | ||||
|             margin: 0; | ||||
|             font-size: 20px; | ||||
|             margin: 0; | ||||
|             color: @gray-darkest; | ||||
|           } | ||||
|           h2 { | ||||
|             &.status { | ||||
|               margin: 8px 0px 0px 16px; | ||||
|               text-transform: uppercase; | ||||
|               font-weight: bold; | ||||
|               font-size: 10px; | ||||
|               &.running { | ||||
|                 color: @brand-positive; | ||||
|               } | ||||
|             } | ||||
|             &.image-label { | ||||
|               margin: 8px 0px 0px 30px; | ||||
|               font-size: 10px; | ||||
|               color: @gray-lighter; | ||||
|             } | ||||
|             &.image { | ||||
|               margin: 5px 0px 0px 16px; | ||||
|               font-size: 14px; | ||||
|               color: @gray-normal; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | @ -306,11 +372,12 @@ | |||
|         flex: 1; | ||||
|         overflow: auto; | ||||
|         h4 { | ||||
|           margin-top: 30px; | ||||
|           font-size: 14px; | ||||
|           margin-top: 16px; | ||||
|           margin-left: 40px; | ||||
|         } | ||||
|         .logs { | ||||
|           user-select: text; | ||||
|           -webkit-user-select: text; | ||||
|           font-family: Menlo; | ||||
|           font-size: 12px; | ||||
|           padding: 18px 45px; | ||||
|  |  | |||
|  | @ -3,9 +3,10 @@ | |||
| .header { | ||||
|   min-width: 100%; | ||||
|   flex: 0; | ||||
|   min-height: 48px; | ||||
|   min-height: 50px; | ||||
|   -webkit-app-region: drag; | ||||
|   -webkit-user-select: none; | ||||
|   // border-bottom: 1px solid #efefef; | ||||
| 
 | ||||
|   &.no-drag { | ||||
|     -webkit-app-region: no-drag; | ||||
|  |  | |||
|  | @ -13,8 +13,13 @@ html, body { | |||
|   width: 100%; | ||||
|   overflow: hidden; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   user-select: none; | ||||
|   -webkit-user-select: none; | ||||
|   font-family: 'Clear Sans', sans-serif; | ||||
| 
 | ||||
|   cursor: default; | ||||
|   img { | ||||
|     pointer-events: none; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| ::-webkit-scrollbar { | ||||
|  | @ -49,8 +54,10 @@ html, body { | |||
|     box-shadow: 0 2px 5px rgba(0, 0, 0, 0.10); | ||||
|     border: none; //1px solid #ccc; | ||||
|     height: 610px; | ||||
|     display: flex; | ||||
|   } | ||||
|   .modal-body { | ||||
|     flex: 1 auto; | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     padding: 32px 32px; | ||||
|  | @ -62,19 +69,10 @@ html, body { | |||
|     } | ||||
| 
 | ||||
|     section.search { | ||||
|       flex: 0 auto; | ||||
|       min-width: 404px; | ||||
|       padding-right: 32px; | ||||
|       border-right: 1px solid #eee; | ||||
| 
 | ||||
|       .search-icon { | ||||
|         font-size: 20px; | ||||
|         color: @gray-normal; | ||||
|         position: absolute; | ||||
|         top: 40px; | ||||
|         left: 45px; | ||||
|       } | ||||
| 
 | ||||
|       .question { | ||||
|         a { | ||||
|           transition: all 0.3s ease 0s; | ||||
|  | @ -89,31 +87,62 @@ html, body { | |||
|         text-align: right; | ||||
|       } | ||||
| 
 | ||||
|       input { | ||||
|         border-radius: 20px; | ||||
|         font-size: 13px; | ||||
|         height: 38px; | ||||
|         padding: 8px 16px 8px 40px; | ||||
|         font-weight: 300; | ||||
|         color: @gray-darkest; | ||||
|         margin-bottom: 3px; | ||||
|         border-color: lighten(@gray-lighter, 10%); | ||||
| 
 | ||||
|         &:focus { | ||||
|           box-shadow: none; | ||||
|           border-color: @gray-lighter; | ||||
|       .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 { | ||||
|           border-radius: 20px; | ||||
|           font-size: 13px; | ||||
|           height: 38px; | ||||
|           padding: 8px 16px 8px 40px; | ||||
|           color: @gray-darkest; | ||||
|           margin-bottom: 3px; | ||||
|           border-color: @gray-lightest; | ||||
|           box-shadow: none; | ||||
| 
 | ||||
|         &::-webkit-input-placeholder { | ||||
|           color: #ddd; | ||||
|           font-weight: 300; | ||||
|           &:focus { | ||||
|             box-shadow: none; | ||||
|             border-color: @gray-lighter; | ||||
|           } | ||||
| 
 | ||||
|           &::-webkit-input-placeholder { | ||||
|             color: #ddd; | ||||
|             font-weight: 300; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       .results { | ||||
|         overflow: auto; | ||||
| 
 | ||||
|         .no-results { | ||||
|           text-align: center; | ||||
|           h3 { | ||||
|             color: #ABC0C0; | ||||
|             font-size: 16px; | ||||
|             margin-top: 160px; | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         .title { | ||||
|           flex: 0 auto; | ||||
|           margin-top: 16px; | ||||
|         } | ||||
| 
 | ||||
|  | @ -147,7 +176,7 @@ html, body { | |||
|                 overflow: hidden; | ||||
|                 text-overflow: ellipsis; | ||||
|               } | ||||
|               .stars { | ||||
|               .properties { | ||||
|                 color: @gray-lighter; | ||||
|                 margin-top: 2px; | ||||
| 
 | ||||
|  | @ -188,7 +217,7 @@ html, body { | |||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| @-webkit-keyframes translatedownload { | ||||
| @-webkit-keyframes spin { | ||||
|   from { | ||||
|     -webkit-transform: rotate(0deg); | ||||
|   } | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ h4 { | |||
| 
 | ||||
| // Mixin for generating new styles | ||||
| .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 | ||||
|   border-color: @btn-color; | ||||
|   color: @btn-color; | ||||
|  | @ -42,18 +42,22 @@ h4 { | |||
|   &:focus { | ||||
|     border-color: darken(@btn-color, 10%); | ||||
|     color: darken(@btn-color, 10%); | ||||
|     cursor: default; | ||||
|     box-shadow: none; | ||||
|   } | ||||
| 
 | ||||
|   &:active { | ||||
|     background-color: lighten(@btn-color, 45%); | ||||
|     border-color: darken(@btn-color, 10%); | ||||
|     color: darken(@btn-color, 10%); | ||||
|     box-shadow: none; | ||||
|   } | ||||
| 
 | ||||
|   &.active { | ||||
|     background-color: @btn-color; | ||||
|     color: white; | ||||
|     box-shadow: none; | ||||
|     box-shadow: none; | ||||
|   } | ||||
| 
 | ||||
|   &:disabled, | ||||
|  | @ -71,12 +75,6 @@ h4 { | |||
|         margin-right: 4px; | ||||
|       } | ||||
|     } | ||||
|     &.only-icon { | ||||
|       padding: 0px 14px 0px 14px; | ||||
|       .icon:before { | ||||
|         top: 5px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -92,37 +90,25 @@ h4 { | |||
|   text-shadow: none; | ||||
|   padding: 6px 14px 6px 14px; | ||||
|   height: 32px; | ||||
|   .icon:before { | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
|   cursor: default; | ||||
| 
 | ||||
|   .content { | ||||
|     position: relative; | ||||
|     top: 5px; | ||||
|     font-size: 20px; | ||||
|     margin-right: 4px; | ||||
|     top: -4px; | ||||
|     margin-left: 5px; | ||||
|     margin-right: 5px; | ||||
|   } | ||||
|   &.with-icon { | ||||
|     padding: 0px 14px 6px 14px; | ||||
|   } | ||||
|   &.only-icon { | ||||
|     padding: 0px 5px 10px 5px; | ||||
|     .icon:before { | ||||
|       position: relative; | ||||
|       margin-right: 0px; | ||||
|     } | ||||
|   } | ||||
|   .icon-dropdown { | ||||
|     &.icon:before { | ||||
|       font-size: 10px; | ||||
|       position: relative; | ||||
|       top: 1px; | ||||
|       margin-left: 4px; | ||||
|       margin-right: 0px; | ||||
|     } | ||||
| 
 | ||||
|   .icon { | ||||
|     position: relative; | ||||
|     font-size: 16px; | ||||
|   } | ||||
| 
 | ||||
|   // Remove the gradient for the pressed/active state | ||||
|   &:active, | ||||
|   &.active { | ||||
|     background-image: none; | ||||
|     box-shadow: none; | ||||
|   } | ||||
| 
 | ||||
|   &:focus, | ||||
|  |  | |||
|  | @ -16,6 +16,8 @@ if (argv.test) { | |||
|   console.log('Running tests'); | ||||
| } | ||||
| 
 | ||||
| process.env.NODE_PATH = __dirname + '/../node_modules'; | ||||
| 
 | ||||
| app.on('activate-with-no-open-windows', function () { | ||||
|   if (mainWindow) { | ||||
|     mainWindow.show(); | ||||
|  | @ -25,13 +27,14 @@ app.on('activate-with-no-open-windows', function () { | |||
| 
 | ||||
| app.on('ready', function() { | ||||
|   var windowOptions = { | ||||
|     width: 1200, | ||||
|     height: 800, | ||||
|     'min-width': 960, | ||||
|     width: 1000, | ||||
|     height: 700, | ||||
|     'min-width': 1000, | ||||
|     'min-height': 700, | ||||
|     resizable: true, | ||||
|     frame: false | ||||
|   }; | ||||
| 
 | ||||
|   mainWindow = new BrowserWindow(windowOptions); | ||||
|   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 | ||||
							
								
								
									
										67
									
								
								gulpfile.js
								
								
								
								
							
							
						
						|  | @ -4,8 +4,7 @@ var browserify = require('browserify'); | |||
| var watchify = require('watchify'); | ||||
| var reactify = require('reactify'); | ||||
| var gulpif = require('gulp-if'); | ||||
| var uglify = require('gulp-uglify'); | ||||
| var streamify = require('gulp-streamify'); | ||||
| var uglify = require('gulp-uglifyjs'); | ||||
| var notify = require('gulp-notify'); | ||||
| var concat = require('gulp-concat'); | ||||
| var less = require('gulp-less'); | ||||
|  | @ -22,56 +21,29 @@ var ecstatic = require('ecstatic'); | |||
| var downloadatomshell = require('gulp-download-atom-shell'); | ||||
| var packagejson = require('./package.json'); | ||||
| var http = require('http'); | ||||
| var react = require('gulp-react'); | ||||
| var fs = require('fs'); | ||||
| 
 | ||||
| var dependencies = Object.keys(packagejson.dependencies); | ||||
| var devDependencies = Object.keys(packagejson.devDependencies); | ||||
| 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, | ||||
|   filename: 'Kitematic.app', | ||||
|   name: 'Kitematic', | ||||
|   signing_identity: process.env.XCODE_SIGNING_IDENTITY | ||||
|   name: 'Kitematic' | ||||
|   //signing_identity: fs.readFileSync('./identity')
 | ||||
| }; | ||||
| 
 | ||||
| gulp.task('js', function () { | ||||
|   var bundler = browserify({ | ||||
|     entries: ['./app/main.js'], // Only need initial file, browserify finds the rest
 | ||||
|     transform: [reactify], // We want to convert JSX to normal javascript
 | ||||
|     debug: options.dev, // Gives us sourcemapping
 | ||||
|     builtins: false, | ||||
|     commondir: false, | ||||
|     insertGlobals: false, | ||||
|     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(gulpif(options.dev && !options.test, livereload())); | ||||
|   }; | ||||
| 
 | ||||
|   if (options.dev) { | ||||
|     bundler = watchify(bundler); | ||||
|     bundler.on('update', bundle); | ||||
|   } | ||||
| 
 | ||||
|   return bundle(); | ||||
|   gulp.src('./app/**/*.js') | ||||
|     .pipe(plumber(function(error) { | ||||
|       gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message)); | ||||
|       // emit the end event, to properly end the task
 | ||||
|       this.emit('end'); | ||||
|     })) | ||||
|     .pipe(react()) | ||||
|     .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) | ||||
|     .pipe(gulpif(options.dev, livereload())); | ||||
| }); | ||||
| 
 | ||||
| gulp.task('specs', function () { | ||||
|  | @ -109,7 +81,7 @@ gulp.task('images', function() { | |||
|       svgoPlugins: [{removeViewBox: false}] | ||||
|     })) | ||||
|     .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 () { | ||||
|  | @ -138,16 +110,16 @@ gulp.task('download', function (cb) { | |||
| gulp.task('copy', function () { | ||||
|   gulp.src('./app/index.html') | ||||
|     .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/**') | ||||
|     .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) { | ||||
|   var stream = gulp.src('').pipe(shell([ | ||||
|     'rm -rf ./dist/osx', | ||||
|     'rm -Rf ./dist', | ||||
|     'mkdir -p ./dist/osx', | ||||
|     'cp -R ./cache/Atom.app ./dist/osx/<%= filename %>', | ||||
|     '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.watch('./app/**/*.js', ['js']); | ||||
|   gulp.watch('./app/**/*.html', ['copy']); | ||||
|   gulp.watch('./app/styles/**/*.less', ['styles']); | ||||
|   gulp.watch('./app/images/**', ['images']); | ||||
|  |  | |||
							
								
								
									
										17
									
								
								package.json
								
								
								
								
							
							
						
						|  | @ -11,7 +11,10 @@ | |||
|   }, | ||||
|   "bugs": "https://github.com/kitematic/kitematic/issues", | ||||
|   "scripts": { | ||||
|     "start": "./script/run" | ||||
|     "start": "gulp", | ||||
|     "preinstall": "./deps", | ||||
|     "test": "gulp test", | ||||
|     "release": ". ./script/identity && gulp release" | ||||
|   }, | ||||
|   "licenses": [ | ||||
|     { | ||||
|  | @ -24,26 +27,22 @@ | |||
|   "dependencies": { | ||||
|     "ansi-to-html": "0.2.0", | ||||
|     "async": "^0.9.0", | ||||
|     "bugsnag-js": "git+https://git@github.com/bugsnag/bugsnag-js", | ||||
|     "dockerode": "2.0.4", | ||||
|     "exec": "0.1.2", | ||||
|     "flux-react": "^2.6.1", | ||||
|     "gulp-react": "^2.0.0", | ||||
|     "jquery": "^2.1.3", | ||||
|     "leveldown": "^1.0.0", | ||||
|     "levelup": "git+https://github.com/kitematic/node-levelup.git", | ||||
|     "minimist": "^1.1.0", | ||||
|     "moment": "2.8.1", | ||||
|     "ncp": "0.6.0", | ||||
|     "node-uuid": "1.4.1", | ||||
|     "open": "0.0.5", | ||||
|     "raven": "^0.7.2", | ||||
|     "react": "^0.12.1", | ||||
|     "react": "^0.12.2", | ||||
|     "react-bootstrap": "^0.13.2", | ||||
|     "react-retina-image": "^1.1.2", | ||||
|     "react-router": "^0.11.6", | ||||
|     "request": "2.42.0", | ||||
|     "request-progress": "0.3.1", | ||||
|     "retina.js": "^1.1.0", | ||||
|     "tar": "0.1.20", | ||||
|     "underscore": "^1.7.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|  | @ -65,9 +64,9 @@ | |||
|     "gulp-sourcemaps": "^1.2.8", | ||||
|     "gulp-streamify": "0.0.5", | ||||
|     "gulp-uglify": "^0.3.1", | ||||
|     "gulp-uglifyjs": "^0.5.0", | ||||
|     "gulp-util": "^3.0.0", | ||||
|     "jasmine-tagged": "^1.1.2", | ||||
|     "object-assign": "^2.0.0", | ||||
|     "reactify": "^0.15.2", | ||||
|     "run-sequence": "^1.0.2", | ||||
|     "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 | ||||