Merge pull request #179 from kitematic/container-store-refactor

Container store refactor
This commit is contained in:
Jeffrey Morgan
2015-02-16 13:10:05 -08:00
15 changed files with 198 additions and 884 deletions

View File

@@ -38,8 +38,8 @@
"node_modules/6to5"
]
},
"docker-version": "1.4.1",
"boot2docker-version": "1.4.1",
"docker-version": "1.5.0",
"boot2docker-version": "1.5.0",
"atom-shell-version": "0.21.1",
"virtualbox-version": "4.3.20",
"virtualbox-filename": "VirtualBox-4.3.20.pkg",

View File

@@ -19,7 +19,7 @@ var ContainerDetail = React.createClass({
},
init: function () {
var currentRoute = _.last(this.getRoutes()).name;
if (currentRoute === 'containerDetail') {
if (currentRoute === 'containerDetails') {
this.transitionTo('containerHome', {name: this.getParams().name});
}
},

View File

@@ -1,640 +0,0 @@
var _ = require('underscore');
var $ = require('jquery');
var React = require('react/addons');
var Router = require('react-router');
var exec = require('exec');
var path = require('path');
var remote = require('remote');
var rimraf = require('rimraf');
var fs = require('fs');
var dialog = remote.require('dialog');
var ContainerStore = require('./ContainerStore');
var ContainerUtil = require('./ContainerUtil');
var boot2docker = require('./Boot2Docker');
var ContainerDetailsHeader = require('./ContainerDetailsHeader.react');
var ContainerHome = require('./ContainerHome.react');
var RetinaImage = require('react-retina-image');
var Radial = require('./Radial.react');
var _oldHeight = 0;
var ContainerDetailsbak = React.createClass({
mixins: [Router.State, Router.Navigation],
PAGE_HOME: 'home',
PAGE_LOGS: 'logs',
PAGE_SETTINGS: 'settings',
PAGE_PORTS: 'ports',
PAGE_VOLUMES: 'volumes',
getInitialState: function () {
return {
logs: [],
page: this.PAGE_HOME,
env: {},
pendingEnv: {},
ports: {},
volumes: {},
defaultPort: null
};
},
componentWillReceiveProps: function () {
this.init();
},
componentDidMount: function () {
this.init();
ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
},
componentWillUnmount: function () {
ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
},
componentDidUpdate: function () {
// Scroll logs to bottom
var parent = $('.details-logs');
if (parent.length) {
if (parent.scrollTop() >= _oldHeight) {
parent.stop();
parent.scrollTop(parent[0].scrollHeight - parent.height());
}
_oldHeight = parent[0].scrollHeight - parent.height();
}
},
init: function () {
var container = ContainerStore.container(this.getParams().name);
if (!container) {
return;
}
this.setState({
progress: ContainerStore.progress(this.getParams().name),
env: ContainerUtil.env(container),
page: this.PAGE_HOME
});
var ports = ContainerUtil.ports(container);
var webPorts = ['80', '8000', '8080', '3000', '5000', '2368'];
this.setState({
ports: ports,
defaultPort: _.find(_.keys(ports), function (port) {
return webPorts.indexOf(port) !== -1;
})
});
this.updateLogs();
},
updateLogs: function (name) {
if (name && name !== this.getParams().name) {
return;
}
this.setState({
logs: ContainerStore.logs(this.getParams().name)
});
},
updateProgress: function (name) {
if (name === this.getParams().name) {
this.setState({
progress: ContainerStore.progress(name)
});
}
},
disableRun: function () {
return (!this.props.container.State.Running || !this.state.defaultPort);
},
disableRestart: function () {
return (this.props.container.State.Downloading || this.props.container.State.Restarting);
},
disableTerminal: function () {
return (!this.props.container.State.Running);
},
disableTab: function () {
return (this.props.container.State.Downloading);
},
showHome: function () {
if (!this.disableTab()) {
/*this.setState({
page: this.PAGE_HOME
});*/
this.transitionTo('containerHome', {name: this.getParams().name});
}
},
showLogs: function () {
if (!this.disableTab()) {
this.setState({
page: this.PAGE_LOGS
});
}
},
showPorts: function () {
this.setState({
page: this.PAGE_PORTS
});
},
showVolumes: function () {
this.setState({
page: this.PAGE_VOLUMES
});
},
showSettings: function () {
if (!this.disableTab()) {
this.setState({
page: this.PAGE_SETTINGS
});
}
},
handleRun: function () {
if (this.state.defaultPort && !this.disableRun()) {
exec(['open', this.state.ports[this.state.defaultPort].url], function (err) {
if (err) { throw err; }
});
}
},
handleRestart: function () {
if (!this.disableRestart()) {
ContainerStore.restart(this.props.container.Name, function (err) {
console.log(err);
});
}
},
handleTerminal: function () {
if (!this.disableTerminal()) {
var container = this.props.container;
var terminal = path.join(process.cwd(), 'resources', 'terminal');
var cmd = [terminal, boot2docker.command().replace(/ /g, '\\\\\\\\ ').replace(/\(/g, '\\\\\\\\(').replace(/\)/g, '\\\\\\\\)'), 'ssh', '-t', 'sudo', 'docker', 'exec', '-i', '-t', container.Name, 'sh'];
exec(cmd, function (stderr, stdout, code) {
console.log(stderr);
console.log(stdout);
if (code) {
console.log(stderr);
}
});
}
},
handleViewLink: function (url) {
exec(['open', url], function (err) {
if (err) { throw err; }
});
},
handleChangeDefaultPort: function (port, e) {
if (e.target.checked) {
this.setState({
defaultPort: null
});
} else {
this.setState({
defaultPort: port
});
}
},
handleChooseVolumeClick: function (dockerVol) {
var self = this;
dialog.showOpenDialog({properties: ['openDirectory', 'createDirectory']}, function (filenames) {
if (!filenames) {
return;
}
var directory = filenames[0];
if (directory) {
var volumes = _.clone(self.props.container.Volumes);
volumes[dockerVol] = directory;
var binds = _.pairs(volumes).map(function (pair) {
return pair[1] + ':' + pair[0];
});
ContainerStore.updateContainer(self.props.container.Name, {
Binds: binds
}, function (err) {
if (err) { console.log(err); }
});
}
});
},
handleOpenVolumeClick: function (path) {
exec(['open', path], function (err) {
if (err) { throw err; }
});
},
handleSaveContainerName: function () {
var newName = $('#input-container-name').val();
if (newName === this.props.container.Name) {
return;
}
if (fs.existsSync(path.join(process.env.HOME, 'Kitematic', this.props.container.Name))) {
fs.renameSync(path.join(process.env.HOME, 'Kitematic', this.props.container.Name), path.join(process.env.HOME, 'Kitematic', newName));
}
ContainerStore.updateContainer(this.props.container.Name, {
name: newName
}, function (err) {
this.transitionTo('container', {name: newName});
if (err) {
console.error(err);
}
}.bind(this));
},
handleSaveEnvVar: function () {
var $rows = $('.env-vars .keyval-row');
var envVarList = [];
$rows.each(function () {
var key = $(this).find('.key').val();
var val = $(this).find('.val').val();
if (!key.length || !val.length) {
return;
}
envVarList.push(key + '=' + val);
});
var self = this;
ContainerStore.updateContainer(self.props.container.Name, {
Env: envVarList
}, function (err) {
if (err) {
console.error(err);
} else {
self.setState({
pendingEnv: {}
});
$('#new-env-key').val('');
$('#new-env-val').val('');
}
});
},
handleAddPendingEnvVar: function () {
var newKey = $('#new-env-key').val();
var newVal = $('#new-env-val').val();
var newEnv = {};
newEnv[newKey] = newVal;
this.setState({
pendingEnv: _.extend(this.state.pendingEnv, newEnv)
});
$('#new-env-key').val('');
$('#new-env-val').val('');
},
handleRemoveEnvVar: function (key) {
var newEnv = _.omit(this.state.env, key);
this.setState({
env: newEnv
});
},
handleRemovePendingEnvVar: function (key) {
var newEnv = _.omit(this.state.pendingEnv, key);
this.setState({
pendingEnv: newEnv
});
},
handleDeleteContainer: function () {
dialog.showMessageBox({
message: 'Are you sure you want to delete this container?',
buttons: ['Delete', 'Cancel']
}, function (index) {
var volumePath = path.join(process.env.HOME, 'Kitematic', this.props.container.Name);
if (fs.existsSync(volumePath)) {
rimraf(volumePath, function (err) {
console.log(err);
});
}
if (index === 0) {
ContainerStore.remove(this.props.container.Name, function (err) {
console.error(err);
});
}
}.bind(this));
},
handleItemMouseEnterRun: function () {
var $action = $(this.getDOMNode()).find('.action .run');
$action.css("visibility", "visible");
},
handleItemMouseLeaveRun: function () {
var $action = $(this.getDOMNode()).find('.action .run');
$action.css("visibility", "hidden");
},
handleItemMouseEnterRestart: function () {
var $action = $(this.getDOMNode()).find('.action .restart');
$action.css("visibility", "visible");
},
handleItemMouseLeaveRestart: function () {
var $action = $(this.getDOMNode()).find('.action .restart');
$action.css("visibility", "hidden");
},
handleItemMouseEnterTerminal: function () {
var $action = $(this.getDOMNode()).find('.action .terminal');
$action.css("visibility", "visible");
},
handleItemMouseLeaveTerminal: function () {
var $action = $(this.getDOMNode()).find('.action .terminal');
$action.css("visibility", "hidden");
},
render: function () {
var self = this;
if (!this.state) {
return <div></div>;
}
var logs = this.state.logs.map(function (l, i) {
return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>;
});
if (!this.props.container) {
return false;
}
var button;
if (this.state.progress === 1) {
button = <a className="btn btn-primary" onClick={this.handleClick}>View</a>;
} else {
button = <a className="btn btn-primary disabled" onClick={this.handleClick}>View</a>;
}
var envVars = _.map(this.state.env, function (val, key) {
return (
<div key={key} className="keyval-row">
<input type="text" className="key line" defaultValue={key}></input>
<input type="text" className="val line" defaultValue={val}></input>
<a onClick={self.handleRemoveEnvVar.bind(self, key)} className="only-icon btn btn-action small"><span className="icon icon-cross"></span></a>
</div>
);
});
var pendingEnvVars = _.map(this.state.pendingEnv, function (val, key) {
return (
<div key={key} className="keyval-row">
<input type="text" className="key line" defaultValue={key}></input>
<input type="text" className="val line" defaultValue={val}></input>
<a onClick={self.handleRemovePendingEnvVar.bind(self, key)} className="only-icon btn btn-action small"><span className="icon icon-arrow-undo"></span></a>
</div>
);
});
var disabledClass = '';
if (!this.props.container.State.Running) {
disabledClass = 'disabled';
}
/*var buttonClass = React.addons.classSet({
btn: true,
'btn-action': true,
'with-icon': true,
disabled: !this.props.container.State.Running
});
var restartButtonClass = React.addons.classSet({
btn: true,
'btn-action': true,
'with-icon': true,
disabled: this.props.container.State.Downloading || this.props.container.State.Restarting
});
var viewButtonClass = React.addons.classSet({
btn: true,
'btn-action': true,
'with-icon': true,
disabled: !this.props.container.State.Running || !this.state.defaultPort
});
var kitematicVolumes = _.pairs(this.props.container.Volumes).filter(function (pair) {
return pair[1].indexOf(path.join(process.env.HOME, 'Kitematic')) !== -1;
});
var volumesButtonClass = React.addons.classSet({
btn: true,
'btn-action': true,
'with-icon': true,
disabled: !kitematicVolumes.length
});
var textButtonClasses = React.addons.classSet({
'btn': true,
'btn-action': true,
'only-icon': true,
'active': this.state.page === this.PAGE_LOGS,
disabled: this.props.container.State.Downloading
});
var gearButtonClass = React.addons.classSet({
'btn': true,
'btn-action': true,
'only-icon': true,
'active': this.state.page === this.PAGE_SETTINGS,
disabled: this.props.container.State.Downloading
});*/
var ports = _.map(_.pairs(self.state.ports), function (pair) {
var key = pair[0];
var val = pair[1];
return (
<div key={key} className="table-values">
<span className="value-left">{key}</span><span className="icon icon-arrow-right"></span>
<a className="value-right" onClick={self.handleViewLink.bind(self, val.url)}>{val.display}</a>
</div>
);
});
var volumes = _.map(self.props.container.Volumes, function (val, key) {
if (!val || val.indexOf(process.env.HOME) === -1) {
val = 'No Host Folder';
}
return (
<div key={key} className="table-values">
<span className="value-left">{key}</span><span className="icon icon-arrow-right"></span>
<a className="value-right">{val.replace(process.env.HOME, '~')}</a>
</div>
);
});
var body;
if (this.props.container.State.Downloading) {
if (this.state.progress) {
body = (
<div className="details-progress">
<h2>Downloading Image</h2>
<Radial progress={Math.round(this.state.progress * 100)}/>
</div>
);
} else {
body = (
<div className="details-progress">
<h2>Connecting to Docker Hub</h2>
<Radial spin="true" progress="90"/>
</div>
);
}
} else {
if (this.state.page === this.PAGE_HOME) {
body = (
<ContainerHome ports={this.state.ports} defaultPort={this.state.defaultPort} logs={logs} container={this.props.container} />
);
} else if (this.state.page === this.PAGE_LOGS) {
body = (
<div className="details-panel details-logs logs">
{logs}
</div>
);
} else if (this.state.page === this.PAGE_PORTS) {
body = (
<div className="details-panel">
<div className="ports">
<h3>Configure Ports</h3>
<div className="table">
<div className="table-labels">
<div className="label-left">DOCKER PORT</div>
<div className="label-right">MAC PORT</div>
</div>
{ports}
</div>
</div>
</div>
);
} else if (this.state.page === this.PAGE_VOLUMES) {
body = (
<div className="details-panel">
<div className="volumes">
<h3>Configure Volumes</h3>
<div className="table">
<div className="table-labels">
<div className="label-left">DOCKER FOLDER</div>
<div className="label-right">MAC FOLDER</div>
</div>
{volumes}
</div>
</div>
</div>
);
} else {
var rename = (
<div className="settings-section">
<h3>Container Name</h3>
<div className="container-name">
<input id="input-container-name" type="text" className="line" placeholder="Container Name" defaultValue={this.props.container.Name}></input>
</div>
<a className="btn btn-action" onClick={this.handleSaveContainerName}>Save</a>
</div>
);
body = (
<div className="details-panel">
<div className="settings">
{rename}
<div className="settings-section">
<h3>Environment Variables</h3>
<div className="env-vars-labels">
<div className="label-key">KEY</div>
<div className="label-val">VALUE</div>
</div>
<div className="env-vars">
{envVars}
{pendingEnvVars}
<div className="keyval-row">
<input id="new-env-key" type="text" className="key line"></input>
<input id="new-env-val" type="text" className="val line"></input>
<a onClick={this.handleAddPendingEnvVar} className="only-icon btn btn-positive small"><span className="icon icon-add-1"></span></a>
</div>
</div>
<a className="btn btn-action" onClick={this.handleSaveEnvVar}>Save</a>
</div>
<div className="settings-section">
<h3>Delete Container</h3>
<a className="btn btn-action" onClick={this.handleDeleteContainer}>Delete Container</a>
</div>
</div>
</div>
);
}
}
var tabHomeClasses = React.addons.classSet({
'tab': true,
'active': this.state.page === this.PAGE_HOME,
disabled: this.disableTab()
});
var tabLogsClasses = React.addons.classSet({
'tab': true,
'active': this.state.page === this.PAGE_LOGS,
disabled: this.disableTab()
});
var tabSettingsClasses = React.addons.classSet({
'tab': true,
'active': this.state.page === this.PAGE_SETTINGS,
disabled: this.disableTab()
});
/*var ports = _.map(_.pairs(self.state.ports), function (pair, index, list) {
var key = pair[0];
var val = pair[1];
return (
<div key={key} className="table-values">
<span className="value-left">{key}</span><span className="icon icon-arrow-right"></span>
<a className="value-right" onClick={self.handleViewLink.bind(self, val.url)}>{val.display}</a>
<input onChange={self.handleChangeDefaultPort.bind(self, key)} type="checkbox" checked={self.state.defaultPort === key}/> <label>Default</label>
</div>
);
});
var volumes = _.map(self.props.container.Volumes, function (val, key) {
if (!val || val.indexOf(process.env.HOME) === -1) {
val = <span>No folder<a className="btn btn-primary btn-xs" onClick={self.handleChooseVolumeClick.bind(self, key)}>Choose</a></span>;
} else {
val = <span><a className="value-right" onClick={self.handleOpenVolumeClick.bind(self, val)}>{val.replace(process.env.HOME, '~')}</a> <a className="btn btn-primary btn-xs" onClick={self.handleChooseVolumeClick.bind(self, key)}>Choose</a></span>;
}
return (
<div key={key} className="table-values">
<span className="value-left">{key}</span><span className="icon icon-arrow-right"></span>
{val}
</div>
);
});*/
/* var view;
if (this.state.defaultPort) {
view = (
<div className="action btn-group">
<a className={viewButtonClass} onClick={this.handleView}><span className="icon icon-preview-2"></span><span className="content">View</span></a>
<a className={dropdownViewButtonClass} onClick={this.handleViewDropdown}><span className="icon-dropdown icon icon-arrow-37"></span></a>
</div>
);
} else {
view = (
<div className="action">
<a className={dropdownViewButtonClass} onClick={this.handleViewDropdown}><span className="icon icon-preview-2"></span> <span className="content">Ports</span> <span className="icon-dropdown icon icon-arrow-37"></span></a>
</div>
);
}*/
var runActionClass = React.addons.classSet({
action: true,
disabled: this.disableRun()
});
var restartActionClass = React.addons.classSet({
action: true,
disabled: this.disableRestart()
});
var terminalActionClass = React.addons.classSet({
action: true,
disabled: this.disableTerminal()
});
return (
<div className="details">
<ContainerDetailsHeader container={this.props.container} />
<div className="details-subheader">
<div className="details-header-actions">
<div className={runActionClass} onMouseEnter={this.handleItemMouseEnterRun} onMouseLeave={this.handleItemMouseLeaveRun}>
<span className="action-icon" onClick={this.handleRun}><RetinaImage src="button-run.png"/></span>
<span className="btn-label run">Run</span>
</div>
<div className={restartActionClass} onMouseEnter={this.handleItemMouseEnterRestart} onMouseLeave={this.handleItemMouseLeaveRestart}>
<span className="action-icon" onClick={this.handleRestart}><RetinaImage src="button-restart.png"/></span>
<span className="btn-label restart">Restart</span>
</div>
<div className={terminalActionClass} onMouseEnter={this.handleItemMouseEnterTerminal} onMouseLeave={this.handleItemMouseLeaveTerminal}>
<span className="action-icon" onClick={this.handleTerminal}><RetinaImage src="button-terminal.png"/></span>
<span className="btn-label terminal">Terminal</span>
</div>
</div>
<div className="details-subheader-tabs">
<span className={tabHomeClasses} onClick={this.showHome}>Home</span>
<span className={tabLogsClasses} onClick={this.showLogs}>Logs</span>
<span className={tabSettingsClasses} onClick={this.showSettings}>Settings</span>
</div>
</div>
{body}
</div>
);
}
});
module.exports = ContainerDetailsbak;

View File

@@ -90,7 +90,7 @@ var ContainerHome = React.createClass({
<ContainerHomePreview />
</div>
<div className="right">
<ContainerHomeLogs />
<ContainerHomeLogs/>
<ContainerHomeFolders container={this.props.container} />
</div>
</div>
@@ -116,7 +116,7 @@ var ContainerHome = React.createClass({
<div className="details-panel home">
<div className="content">
<div className="left">
<ContainerHomeLogs />
<ContainerHomeLogs/>
</div>
{right}
</div>

View File

@@ -1,6 +1,6 @@
var $ = require('jquery');
var React = require('react/addons');
var ContainerStore = require('./ContainerStore');
var LogStore = require('./LogStore');
var Router = require('react-router');
var ContainerHomeLogs = React.createClass({
@@ -15,10 +15,10 @@ var ContainerHomeLogs = React.createClass({
},
componentDidMount: function() {
this.init();
ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
LogStore.on(LogStore.SERVER_LOGS_EVENT, this.updateLogs);
},
componentWillUnmount: function() {
ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.updateLogs);
},
componentDidUpdate: function () {
// Scroll logs to bottom
@@ -39,7 +39,7 @@ var ContainerHomeLogs = React.createClass({
return;
}
this.setState({
logs: ContainerStore.logs(this.getParams().name)
logs: LogStore.logs(this.getParams().name)
});
},
handleClickLogs: function () {

View File

@@ -30,19 +30,7 @@ var ContainerListItem = React.createClass({
render: function () {
var self = this;
var container = this.props.container;
var downloadingImage = null, downloading = false;
var env = container.Config.Env;
if (env.length) {
var obj = _.object(env.map(function (e) {
return e.split('=');
}));
if (obj.KITEMATIC_DOWNLOADING) {
downloading = true;
}
downloadingImage = obj.KITEMATIC_DOWNLOADING_IMAGE || null;
}
var imageName = downloadingImage || container.Config.Image;
var imageName = container.Config.Image;
// Synchronize all animations
var style = {
@@ -50,7 +38,7 @@ var ContainerListItem = React.createClass({
};
var state;
if (downloading) {
if (container.State.Downloading) {
state = <div className="state state-downloading"><div style={style} className="downloading-arrow"></div></div>;
} else if (container.State.Running && !container.State.Paused) {
state = <div className="state state-running"><div style={style} className="runningwave"></div></div>;
@@ -66,7 +54,7 @@ var ContainerListItem = React.createClass({
}
return (
<Router.Link data-container={name} to="containerDetail" params={{name: container.Name}}>
<Router.Link data-container={name} to="containerDetails" params={{name: container.Name}}>
<li onMouseEnter={self.handleItemMouseEnter} onMouseLeave={self.handleItemMouseLeave}>
{state}
<div className="info">

View File

@@ -1,6 +1,6 @@
var $ = require('jquery');
var React = require('react/addons');
var ContainerStore = require('./ContainerStore');
var LogStore = require('./LogStore');
var Router = require('react-router');
var ContainerLogs = React.createClass({
@@ -15,10 +15,10 @@ var ContainerLogs = React.createClass({
},
componentDidMount: function() {
this.init();
ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
LogStore.on(LogStore.SERVER_LOGS_EVENT, this.updateLogs);
},
componentWillUnmount: function() {
ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.updateLogs);
},
componentDidUpdate: function () {
// Scroll logs to bottom
@@ -39,7 +39,7 @@ var ContainerLogs = React.createClass({
return;
}
this.setState({
logs: ContainerStore.logs(this.getParams().name)
logs: LogStore.logs(this.getParams().name)
});
},
render: function () {

View File

@@ -1,48 +1,20 @@
var $ = require('jquery');
var _ = require('underscore');
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var path = require('path');
var assign = require('object-assign');
var Convert = require('ansi-to-html');
var docker = require('./Docker');
var registry = require('./Registry');
var ContainerUtil = require('./ContainerUtil');
var convert = new Convert();
var _recommended = [];
var _placeholders = {};
var _containers = {};
var _progress = {};
var _logs = {};
var _streams = {};
var _muted = {};
var ContainerStore = assign(Object.create(EventEmitter.prototype), {
CLIENT_CONTAINER_EVENT: 'client_container_event',
CLIENT_RECOMMENDED_EVENT: 'client_recommended_event',
SERVER_CONTAINER_EVENT: 'server_container_event',
SERVER_PROGRESS_EVENT: 'server_progress_event',
SERVER_LOGS_EVENT: 'server_logs_event',
_pullScratchImage: function (callback) {
var image = docker.client().getImage('scratch:latest');
image.inspect(function (err, data) {
if (!data) {
docker.client().pull('scratch:latest', function (err, stream) {
if (err) {
callback(err);
return;
}
stream.setEncoding('utf8');
stream.on('data', function () {});
stream.on('end', function () {
callback();
});
});
} else {
callback();
}
});
},
_pullImage: function (repository, tag, callback, progressCallback) {
registry.layers(repository, tag, function (err, layerSizes) {
@@ -89,7 +61,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
var totalReceived = chunks.reduce(function (pv, sv) {
return pv + sv;
});
}, 0);
var totalProgress = totalReceived / totalBytes;
progressCallback(totalProgress);
@@ -101,12 +73,6 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
});
});
},
_escapeHTML: function (html) {
var text = document.createTextNode(html);
var div = document.createElement('div');
div.appendChild(text);
return div.innerHTML;
},
_createContainer: function (name, containerData, callback) {
var existing = docker.client().getContainer(name);
var self = this;
@@ -118,14 +84,8 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
if (containerData.Config && containerData.Config.Image) {
containerData.Image = containerData.Config.Image;
}
existing.kill(function (err) {
if (err) {
console.log(err);
}
existing.remove(function (err) {
if (err) {
console.log(err);
}
existing.kill(function () {
existing.remove(function () {
docker.client().getImage(containerData.Image).inspect(function (err, data) {
if (err) {
callback(err);
@@ -161,31 +121,6 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
});
});
},
_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) {
if (err) {
callback(err);
return;
}
self.fetchContainer(name, callback);
});
});
},
_generateName: function (repository) {
var base = _.last(repository.split('/'));
var count = 1;
@@ -201,18 +136,22 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
}
},
_resumePulling: function () {
var downloading = _.filter(_.values(_containers), function (container) {
var downloading = _.filter(_.values(this.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) {
docker.client().pull(container.Config.Image, function (err, stream) {
delete _placeholders[container.Name];
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
stream.setEncoding('utf8');
stream.on('data', function () {});
stream.on('end', function () {
self._createContainer(container.Name, {Image: container.KitematicDownloadingImage}, function () {});
self._createContainer(container.Name, {Image: container.Config.Image}, function () {
self.emit(self.CLIENT_CONTAINER_EVENT, container.Name);
});
});
});
});
@@ -262,13 +201,18 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
} else {
callback();
}
var placeholderData = JSON.parse(localStorage.getItem('store.placeholders'));
console.log(placeholderData);
console.log(_.keys(_containers));
if (placeholderData) {
_placeholders = _.omit(placeholderData, _.keys(_containers));
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
}
console.log(_placeholders);
this.emit(this.CLIENT_CONTAINER_EVENT);
this._resumePulling();
this._startListeningToEvents();
}.bind(this));
this.fetchRecommended(function () {
this.emit(this.CLIENT_RECOMMENDED_EVENT);
}.bind(this));
},
fetchContainer: function (id, callback) {
docker.client().getContainer(id).inspect(function (err, container) {
@@ -281,15 +225,6 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
}
// Fix leading slash in container names
container.Name = container.Name.replace('/', '');
// Add Downloading State (stored in environment variables) to containers for Kitematic
var env = ContainerUtil.env(container);
container.State.Downloading = !!env.KITEMATIC_DOWNLOADING;
container.KitematicDownloadingImage = env.KITEMATIC_DOWNLOADING_IMAGE;
this.fetchLogs(container.Name, function () {
}.bind(this));
_containers[container.Name] = container;
callback(null, container);
}
@@ -311,103 +246,42 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
});
});
},
fetchRecommended: function (callback) {
if (_recommended.length) {
return;
}
$.ajax({
url: 'https://kitematic.com/recommended.json',
cache: false,
dataType: 'json',
success: function (res) {
var recommended = res.repos;
async.map(recommended, function (rec, callback) {
$.get('https://registry.hub.docker.com/v1/search?q=' + rec.repo, function (data) {
var results = data.results;
var result = _.find(results, function (r) {
return r.name === rec.repo;
});
callback(null, _.extend(result, rec));
});
}, function (err, results) {
_recommended = results.filter(function(r) { return !!r; });
callback();
});
},
error: function (err) {
console.log(err);
}
});
},
fetchLogs: function (name, callback) {
var index = 0;
var self = this;
docker.client().getContainer(name).logs({
follow: true,
stdout: true,
stderr: true,
timestamps: true
}, function (err, stream) {
callback(err);
if (_streams[name]) {
return;
}
_streams[name] = stream;
if (err) {
return;
}
_logs[name] = [];
stream.setEncoding('utf8');
var timeout;
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);
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
timeout = setTimeout(function () {
timeout = null;
self.emit(self.SERVER_LOGS_EVENT, name);
}, 100);
_logs[name].push(convert.toHtml(self._escapeHTML(msg)));
}
index += 1;
});
stream.on('end', function () {
delete _streams[name];
});
});
},
create: function (repository, tag, callback) {
tag = tag || 'latest';
var self = this;
var imageName = repository + ':' + tag;
var containerName = this._generateName(repository);
// Pull image
self._createPlaceholderContainer(imageName, containerName, function (err, container) {
if (err) {
callback(err);
return;
_placeholders[containerName] = {
Name: containerName,
Image: imageName,
Config: {
Image: imageName,
},
State: {
Downloading: true
}
_containers[containerName] = container;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create');
_muted[containerName] = true;
_progress[containerName] = 0;
self._pullImage(repository, tag, function () {
self._createContainer(containerName, {Image: imageName}, function () {
delete _progress[containerName];
_muted[containerName] = false;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName);
});
}, function (progress) {
_progress[containerName] = progress;
self.emit(self.SERVER_PROGRESS_EVENT, containerName);
};
console.log(_placeholders);
console.log(JSON.stringify(_placeholders));
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create');
_muted[containerName] = true;
_progress[containerName] = 0;
self._pullImage(repository, tag, function () {
delete _placeholders[containerName];
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
self._createContainer(containerName, {Image: imageName}, function () {
delete _progress[containerName];
_muted[containerName] = false;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName);
});
callback(null, containerName);
}, function (progress) {
_progress[containerName] = progress;
self.emit(self.SERVER_PROGRESS_EVENT, containerName);
});
callback(null, containerName);
},
updateContainer: function (name, data, callback) {
_muted[name] = true;
@@ -428,6 +302,10 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
});
},
remove: function (name, callback) {
if (_placeholders[name]) {
delete _placeholders[name];
return;
}
var container = docker.client().getContainer(name);
if (_containers[name].State.Paused) {
container.unpause(function (err) {
@@ -467,25 +345,19 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
}
},
containers: function() {
return _containers;
return _.extend(_containers, _placeholders);
},
container: function (name) {
return _containers[name];
return this.containers()[name];
},
sorted: function () {
return _.values(_containers).sort(function (a, b) {
return _.values(this.containers()).sort(function (a, b) {
return a.Name.localeCompare(b.Name);
});
},
recommended: function () {
return _recommended;
},
progress: function (name) {
return _progress[name];
},
logs: function (name) {
return _logs[name] || [];
}
});
module.exports = ContainerStore;

View File

@@ -13,6 +13,9 @@ var ContainerUtil = {
}));
},
ports: function (container) {
if (!container.NetworkSettings) {
return {};
}
var res = {};
var ip = docker.host;
_.each(container.NetworkSettings.Ports, function (value, key) {

70
src/LogStore.js Normal file
View File

@@ -0,0 +1,70 @@
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var Convert = require('ansi-to-html');
var docker = require('./Docker');
var _convert = new Convert();
var _logs = {};
var _streams = {};
var LogStore = assign(Object.create(EventEmitter.prototype), {
SERVER_LOGS_EVENT: 'server_logs_event',
_escapeHTML: function (html) {
var text = document.createTextNode(html);
var div = document.createElement('div');
div.appendChild(text);
return div.innerHTML;
},
fetchLogs: function (name) {
if (!name || !docker.client()) {
return;
}
var index = 0;
var self = this;
docker.client().getContainer(name).logs({
follow: true,
stdout: true,
stderr: true,
timestamps: true
}, function (err, stream) {
if (_streams[name]) {
return;
}
_streams[name] = stream;
if (err) {
return;
}
_logs[name] = [];
stream.setEncoding('utf8');
var timeout;
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);
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
timeout = setTimeout(function () {
timeout = null;
self.emit(self.SERVER_LOGS_EVENT, name);
}, 100);
_logs[name].push(_convert.toHtml(self._escapeHTML(msg)));
}
index += 1;
});
stream.on('end', function () {
delete _streams[name];
});
});
},
logs: function (name) {
if (!_streams[name]) {
this.fetchLogs(name);
}
return _logs[name] || [];
}
});
module.exports = LogStore;

View File

@@ -34,26 +34,14 @@ bugsnag.notifyReleaseStages = ['production'];
bugsnag.appVersion = app.getVersion();
router.run(Handler => React.render(<Handler/>, document.body));
if (!window.location.hash.length || window.location.hash === '#/') {
SetupStore.run().then(boot2docker.ip).then(ip => {
console.log(ip);
docker.setHost(ip);
ContainerStore.init(function (err) {
if (err) { console.log(err); }
router.transitionTo('containers');
});
}).catch(err => {
bugsnag.notify(err);
SetupStore.run().then(boot2docker.ip).then(ip => {
console.log(ip);
docker.setHost(ip);
ContainerStore.init(function (err) {
if (err) { console.log(err); }
router.transitionTo('containers');
});
} else {
console.log('Skipping installer.');
router.transitionTo('containers');
boot2docker.ip().then(ip => {
docker.setHost(ip);
ContainerStore.init(function (err) {
if (err) { console.log(err); }
});
}).catch(err => {
bugsnag.notify(err);
});
}
}).catch(err => {
console.log(err);
bugsnag.notify(err);
});

View File

@@ -5,13 +5,16 @@ var RetinaImage = require('react-retina-image');
var ContainerStore = require('./ContainerStore');
var Radial = require('./Radial.react');
var assign = require('object-assign');
var Promise = require('bluebird');
var _recommended = [];
var NewContainer = React.createClass({
_searchRequest: null,
getInitialState: function () {
return {
query: '',
results: [],
results: _recommended,
loading: false,
tags: {},
active: null,
@@ -23,14 +26,8 @@ var NewContainer = React.createClass({
creating: []
});
this.refs.searchInput.getDOMNode().focus();
ContainerStore.on(ContainerStore.CLIENT_RECOMMENDED_EVENT, this.update);
this.update();
},
update: function () {
if (!this.state.query.length) {
this.setState({
results: ContainerStore.recommended()
});
if (!_recommended.length) {
this.recommended();
}
},
search: function (query) {
@@ -59,6 +56,40 @@ var NewContainer = React.createClass({
}
});
},
recommended: function () {
if (this._searchRequest) {
this._searchRequest.abort();
this._searchRequest = null;
}
if (_recommended.length) {
return;
}
Promise.resolve($.ajax({
url: 'https://kitematic.com/recommended.json',
cache: false,
dataType: 'json',
})).then(res => res.repos).map(repo => {
return $.get('https://registry.hub.docker.com/v1/search?q=' + repo.repo).then(data => {
var results = data.results;
var result = _.find(results, function (r) {
return r.name === repo.repo;
});
return _.extend(result, repo);
});
}).then(results => {
_recommended = results.filter(r => !!r);
if (!this.state.query.length) {
if (this.isMounted()) {
this.setState({
results: _recommended
});
}
}
}).catch(err => {
console.log(err);
});
},
handleChange: function (e) {
var query = e.target.value;
@@ -70,7 +101,7 @@ var NewContainer = React.createClass({
if (!query.length) {
this.setState({
query: query,
results: ContainerStore.recommended()
results: _recommended
});
} else {
var self = this;

View File

@@ -27,7 +27,7 @@ var App = React.createClass({
var routes = (
<Route name="app" path="/" handler={App}>
<Route name="containers" handler={Containers}>
<Route name="containerDetail" path="/containers/:name" handler={ContainerDetails}>
<Route name="containerDetails" path="/containers/:name" handler={ContainerDetails}>
<Route name="containerHome" path="/containers/:name/home" handler={ContainerHome} />
<Route name="containerLogs" path="/containers/:name/logs" handler={ContainerLogs}/>
<Route name="containerSettings" path="/containers/:name/settings" handler={ContainerSettings}>

View File

@@ -165,7 +165,9 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), {
run: Promise.coroutine(function* () {
yield this.updateBinaries();
var steps = yield this.requiredSteps();
console.log(steps);
for (let step of steps) {
console.log(step.name);
_currentStep = step;
step.percent = 0;
while (true) {
@@ -181,6 +183,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), {
break;
} catch (err) {
if (err) {
console.log(err);
_error = err;
this.emit(this.ERROR_EVENT);
} else {

View File

@@ -7,7 +7,6 @@ module.exports = {
exec: function (args, options) {
options = options || {};
return new Promise((resolve, reject) => {
console.log(options);
exec(args, options, (stderr, stdout, code) => {
if (code) {
reject(stderr);