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 (
|
React.render(<Handler/>, document.body);
|
||||||
<div>
|
});
|
||||||
No Containers
|
} else {
|
||||||
</div>
|
boot2docker.ip(function (err, ip) {
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
docker.setHost(ip);
|
||||||
ContainerStore.init(function () {
|
ContainerStore.init(function () {
|
||||||
Router.run(routes, function (Handler) {
|
router.run(function (Handler) {
|
||||||
React.render(<Handler/>, document.body);
|
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;
|
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
|
|