All but environment variables working

This commit is contained in:
Jeffrey Morgan
2015-05-08 20:14:41 -07:00
parent e348ff938b
commit 7989d48253
27 changed files with 799 additions and 933 deletions

View File

@@ -56,6 +56,7 @@
"virtualbox-checksum": "668f61c95efe37f8fc65cafe95b866fba64e37f2492dfc1e2b44a7ac3dcafa3b",
"virtualbox-checksum-win": "9cb265babf307d825f5178693af95ffca077f80ae22cf43868c3538c159123ff",
"dependencies": {
"alt": "^0.16.2",
"ansi-to-html": "0.3.0",
"any-promise": "^0.1.0",
"async": "^0.9.0",
@@ -67,6 +68,7 @@
"exec": "0.2.0",
"jquery": "^2.1.3",
"mixpanel": "0.2.0",
"mkdirp": "^0.5.0",
"node-uuid": "^1.4.3",
"object-assign": "^2.0.0",
"parseUri": "^1.2.3-2",

View File

@@ -0,0 +1,33 @@
import alt from '../alt';
import dockerUtil from '../utils/DockerUtil';
class ContainerServerActions {
start (name) {
this.dispatch({name});
dockerUtil.start(name);
}
destroy (name) {
this.dispatch({name});
dockerUtil.destroy(name);
}
// TODO: don't require all container data for this method
rename (name, newName) {
this.dispatch({name, newName});
dockerUtil.rename(name, newName);
}
stop (name) {
this.dispatch({name});
dockerUtil.stop(name);
}
update (name, container) {
console.log(container);
this.dispatch({container});
dockerUtil.updateContainer(name, container);
}
}
export default alt.createActions(ContainerServerActions);

View File

@@ -0,0 +1,19 @@
import alt from '../alt';
class ContainerServerActions {
constructor () {
this.generateActions(
'added',
'allUpdated',
'destroyed',
'error',
'muted',
'unmuted',
'progress',
'updated',
'waiting'
);
}
}
export default alt.createActions(ContainerServerActions);

2
src/alt.js Normal file
View File

@@ -0,0 +1,2 @@
import Alt from 'alt';
export default new Alt();

View File

@@ -1,10 +1,8 @@
require.main.paths.splice(0, 0, process.env.NODE_PATH);
var remote = require('remote');
var ContainerStore = require('./stores/ContainerStore');
var Menu = remote.require('menu');
var React = require('react');
var SetupStore = require('./stores/SetupStore');
var bugsnag = require('bugsnag-js');
var ipc = require('ipc');
var machine = require('./utils/DockerMachineUtil');
var metrics = require('./utils/MetricsUtil');
@@ -14,6 +12,7 @@ var webUtil = require('./utils/WebUtil');
var urlUtil = require ('./utils/URLUtil');
var app = remote.require('app');
var request = require('request');
var docker = require('./utils/DockerUtil');
webUtil.addWindowSizeSaving();
webUtil.addLiveReload();
@@ -31,23 +30,15 @@ setInterval(function () {
router.run(Handler => React.render(<Handler/>, document.body));
SetupStore.setup().then(() => {
if (ContainerStore.pending()) {
router.transitionTo('pull');
} else {
router.transitionTo('new');
}
Menu.setApplicationMenu(Menu.buildFromTemplate(template()));
ContainerStore.on(ContainerStore.SERVER_ERROR_EVENT, (err) => {
bugsnag.notify(err);
});
ContainerStore.init(function () {});
docker.init();
router.transitionTo('search');
}).catch(err => {
metrics.track('Setup Failed', {
step: 'catch',
message: err.message
});
console.log(err);
bugsnag.notify(err);
throw err;
});
ipc.on('application:quitting', () => {

View File

@@ -1,39 +1,29 @@
var _ = require('underscore');
var React = require('react/addons');
var Router = require('react-router');
var ContainerDetailsHeader = require('./ContainerDetailsHeader.react');
var ContainerDetailsSubheader = require('./ContainerDetailsSubheader.react');
var Router = require('react-router');
var containerUtil = require('../utils/ContainerUtil');
var util = require('../utils/Util');
var _ = require('underscore');
var ContainerDetail = React.createClass({
var ContainerDetails = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
getInitialState: function () {
return {
currentRoute: null
};
},
componentWillReceiveProps: function () {
this.init();
},
componentDidMount: function () {
this.init();
},
init: function () {
var currentRoute = _.last(this.context.router.getCurrentRoutes()).name;
if (currentRoute === 'containerDetails') {
this.context.router.transitionTo('containerHome', {name: this.context.router.getCurrentParams().name});
}
},
render: function () {
let ports = containerUtil.ports(this.props.container);
let defaultPort = _.find(_.keys(ports), port => {
return util.webPorts.indexOf(port) !== -1;
});
return (
<div className="details">
<ContainerDetailsHeader container={this.props.container}/>
<ContainerDetailsSubheader container={this.props.container} />
<Router.RouteHandler container={this.props.container} error={this.props.error}/>
<ContainerDetailsHeader {...this.props} defaultPort={defaultPort} ports={ports}/>
<ContainerDetailsSubheader {...this.props}/>
<Router.RouteHandler {...this.props} defaultPort={defaultPort} ports={ports}/>
</div>
);
}
});
module.exports = ContainerDetail;
module.exports = ContainerDetails;

View File

@@ -6,12 +6,15 @@ var ContainerDetailsHeader = React.createClass({
if (!this.props.container) {
return false;
}
if (this.props.container.State.Running && !this.props.container.State.Paused && !this.props.container.State.ExitCode && !this.props.container.State.Restarting) {
state = <span className="status running">RUNNING</span>;
} else if (this.props.container.State.Restarting) {
state = <span className="status restarting">RESTARTING</span>;
} else if (this.props.container.State.Paused) {
state = <span className="status paused">PAUSED</span>;
} else if (this.props.container.State.Starting) {
state = <span className="status running">STARTING</span>;
} else if (this.props.container.State.Downloading) {
state = <span className="status downloading">DOWNLOADING</span>;
} else {

View File

@@ -1,53 +1,26 @@
var _ = require('underscore');
var $ = require('jquery');
var _ = require('underscore');
var React = require('react');
var exec = require('exec');
var shell = require('shell');
var metrics = require('../utils/MetricsUtil');
var ContainerStore = require('../stores/ContainerStore');
var ContainerUtil = require('../utils/ContainerUtil');
var machine = require('../utils/DockerMachineUtil');
var RetinaImage = require('react-retina-image');
var webPorts = require('../utils/Util').webPorts;
var classNames = require('classnames');
var resources = require('../utils/ResourcesUtil');
var dockerUtil = require('../utils/DockerUtil');
var containerActions = require('../actions/ContainerActions');
var ContainerDetailsSubheader = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
getInitialState: function () {
return {
defaultPort: null
};
},
componentWillReceiveProps: function () {
this.init();
},
componentDidMount: function () {
this.init();
},
init: function () {
this.setState({
currentRoute: _.last(this.context.router.getCurrentRoutes()).name
});
var container = ContainerStore.container(this.context.router.getCurrentParams().name);
if (!container) {
return;
}
var ports = ContainerUtil.ports(container);
this.setState({
ports: ports,
defaultPort: _.find(_.keys(ports), function (port) {
return webPorts.indexOf(port) !== -1;
})
});
},
disableRun: function () {
if (!this.props.container) {
return false;
}
return (!this.props.container.State.Running || !this.state.defaultPort);
return (!this.props.container.State.Running || !this.props.defaultPort);
},
disableRestart: function () {
if (!this.props.container) {
@@ -100,32 +73,29 @@ var ContainerDetailsSubheader = React.createClass({
}
},
handleRun: function () {
if (this.state.defaultPort && !this.disableRun()) {
if (this.props.defaultPort && !this.disableRun()) {
metrics.track('Opened In Browser', {
from: 'header'
});
shell.openExternal(this.state.ports[this.state.defaultPort].url);
shell.openExternal(this.props.ports[this.props.defaultPort].url);
}
},
handleRestart: function () {
if (!this.disableRestart()) {
metrics.track('Restarted Container');
ContainerStore.restart(this.props.container.Name, function () {
});
dockerUtil.restart(this.props.container.Name);
}
},
handleStop: function () {
if (!this.disableStop()) {
metrics.track('Stopped Container');
ContainerStore.stop(this.props.container.Name, function () {
});
containerActions.stop(this.props.container.Name);
}
},
handleStart: function () {
if (!this.disableStart()) {
metrics.track('Started Container');
ContainerStore.start(this.props.container.Name, function () {
});
containerActions.start(this.props.container.Name);
}
},
handleTerminal: function () {
@@ -207,19 +177,23 @@ var ContainerDetailsSubheader = React.createClass({
action: true,
disabled: this.disableTerminal()
});
var currentRoutes = _.map(this.context.router.getCurrentRoutes(), r => r.name);
var currentRoute = _.last(currentRoutes);
var tabHomeClasses = classNames({
'tab': true,
'active': this.state.currentRoute === 'containerHome',
'active': currentRoute === 'containerHome',
disabled: this.disableTab()
});
var tabLogsClasses = classNames({
'tab': true,
'active': this.state.currentRoute === 'containerLogs',
'active': currentRoute === 'containerLogs',
disabled: this.disableTab()
});
var tabSettingsClasses = classNames({
'tab': true,
'active': this.state.currentRoute && (this.state.currentRoute.indexOf('containerSettings') >= 0),
'active': currentRoutes && (currentRoutes.indexOf('containerSettings') >= 0),
disabled: this.disableTab()
});
var startStopToggle;

View File

@@ -1,78 +1,43 @@
var _ = require('underscore');
var $ = require('jquery');
var React = require('react/addons');
var ContainerStore = require('../stores/ContainerStore');
var Radial = require('./Radial.react');
var ContainerHomePreview = require('./ContainerHomePreview.react');
var ContainerHomeLogs = require('./ContainerHomeLogs.react');
var ContainerHomeFolders = require('./ContainerHomeFolders.react');
var containerUtil = require('../utils/ContainerUtil');
var util = require ('../utils/Util');
var shell = require('shell');
var ContainerUtil = require('../utils/ContainerUtil');
var util = require('../utils/Util');
var resizeWindow = function () {
$('.left .wrapper').height(window.innerHeight - 240);
$('.right .wrapper').height(window.innerHeight / 2 - 100);
};
var ContainerHome = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
getInitialState: function () {
return {
ports: {},
defaultPort: null,
progress: 0
};
componentDidMount: function() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
},
componentWillUnmount: function() {
window.removeEventListener('resize', this.handleResize);
},
componentDidUpdate: function () {
this.handleResize();
},
handleResize: function () {
resizeWindow();
$('.left .wrapper').height(window.innerHeight - 240);
$('.right .wrapper').height(window.innerHeight / 2 - 100);
},
handleErrorClick: function () {
shell.openExternal('https://github.com/kitematic/kitematic/issues/new');
},
componentWillReceiveProps: function () {
this.init();
},
componentDidMount: function() {
this.init();
ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
resizeWindow();
window.addEventListener('resize', this.handleResize);
},
componentWillUnmount: function() {
ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
window.removeEventListener('resize', this.handleResize);
},
componentDidUpdate: function () {
resizeWindow();
},
init: function () {
var container = ContainerStore.container(this.context.router.getCurrentParams().name);
if (!container) {
return;
}
var ports = ContainerUtil.ports(container);
this.setState({
ports: ports,
defaultPort: _.find(_.keys(ports), function (port) {
return util.webPorts.indexOf(port) !== -1;
}),
progress: ContainerStore.progress(this.context.router.getCurrentParams().name),
blocked: ContainerStore.blocked(this.context.router.getCurrentParams().name)
});
},
updateProgress: function (name) {
if (name === this.context.router.getCurrentParams().name) {
this.setState({
blocked: ContainerStore.blocked(name),
progress: ContainerStore.progress(name)
});
}
},
render: function () {
var body;
let body;
if (this.props.error) {
body = (
<div className="details-progress">
@@ -83,12 +48,12 @@ var ContainerHome = React.createClass({
</div>
);
} else if (this.props.container && this.props.container.State.Downloading) {
if (this.state.progress !== undefined) {
if (this.state.progress > 0) {
if (this.props.container.Progress !== undefined) {
if (this.props.container.Progress > 0) {
body = (
<div className="details-progress">
<h2>Downloading Image</h2>
<Radial progress={Math.min(Math.round(this.state.progress * 100), 99)} thick={true} gray={true}/>
<Radial progress={Math.min(Math.round(this.props.container.Progress), 99)} thick={true} gray={true}/>
</div>
);
} else {
@@ -100,7 +65,7 @@ var ContainerHome = React.createClass({
);
}
} else if (this.state.blocked) {
} else if (this.props.container.State.Waiting) {
body = (
<div className="details-progress">
<h2>Waiting For Another Download</h2>
@@ -116,12 +81,12 @@ var ContainerHome = React.createClass({
);
}
} else {
if (this.state.defaultPort) {
if (this.props.defaultPort) {
body = (
<div className="details-panel home">
<div className="content">
<div className="left">
<ContainerHomePreview />
<ContainerHomePreview ports={this.props.ports} defaultPort={this.props.defaultPort} />
</div>
<div className="right">
<ContainerHomeLogs container={this.props.container}/>
@@ -132,7 +97,7 @@ var ContainerHome = React.createClass({
);
} else {
var right;
if (_.keys(this.state.ports) > 0) {
if (_.keys(this.props.ports) > 0) {
right = (
<div className="right">
<ContainerHomePreview />

View File

@@ -5,8 +5,9 @@ var path = require('path');
var shell = require('shell');
var util = require('../utils/Util');
var metrics = require('../utils/MetricsUtil');
var ContainerStore = require('../stores/ContainerStore');
var containerActions = require('../actions/ContainerActions');
var dialog = require('remote').require('dialog');
var mkdirp = require('mkdirp');
var ContainerHomeFolder = React.createClass({
contextTypes: {
@@ -37,15 +38,14 @@ var ContainerHomeFolder = React.createClass({
}
return pair[1] + ':' + pair[0];
});
ContainerStore.updateContainer(this.props.container.Name, {
Binds: binds
}, (err) => {
if (err) {
console.log(err);
return;
mkdirp(newHostVolume, function (err) {
console.log(err);
if (!err) {
shell.showItemInFolder(newHostVolume);
}
shell.showItemInFolder(newHostVolume);
});
containerActions.update(this.props.container.Name, {Binds: binds});
}
});
} else {

View File

@@ -1,28 +1,14 @@
var _ = require('underscore');
var React = require('react/addons');
var ContainerStore = require('../stores/ContainerStore');
var ContainerUtil = require('../utils/ContainerUtil');
var request = require('request');
var shell = require('shell');
var metrics = require('../utils/MetricsUtil');
var webPorts = require('../utils/Util').webPorts;
var ContainerHomePreview = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
getInitialState: function () {
return {
ports: {},
defaultPort: null
};
},
componentWillReceiveProps: function () {
this.init();
},
componentDidMount: function() {
this.init();
},
reload: function () {
var webview = document.getElementById('webview');
if (webview) {
@@ -38,40 +24,31 @@ var ContainerHomePreview = React.createClass({
});
}
},
componentWillUnmount: function() {
clearInterval(this.timer);
},
init: function () {
var container = ContainerStore.container(this.context.router.getCurrentParams().name);
if (!container) {
return;
}
var ports = ContainerUtil.ports(container);
this.setState({
ports: ports,
defaultPort: _.find(_.keys(ports), function (port) {
return webPorts.indexOf(port) !== -1;
})
});
},
handleClickPreview: function () {
if (this.state.defaultPort) {
if (this.props.defaultPort) {
metrics.track('Opened In Browser', {
from: 'preview'
});
shell.openExternal(this.state.ports[this.state.defaultPort].url);
shell.openExternal(this.props.ports[this.props.defaultPort].url);
}
},
handleClickNotShowingCorrectly: function () {
metrics.track('Viewed Port Settings', {
from: 'preview'
});
this.context.router.transitionTo('containerSettingsPorts', {name: this.context.router.getCurrentParams().name});
},
render: function () {
var preview;
if (this.state.defaultPort) {
var frame = React.createElement('webview', {className: 'frame', id: 'webview', src: this.state.ports[this.state.defaultPort].url, autosize: 'on'});
if (this.props.defaultPort) {
var frame = React.createElement('webview', {className: 'frame', id: 'webview', src: this.props.ports[this.props.defaultPort].url, autosize: 'on'});
preview = (
<div className="web-preview wrapper">
<h4>Web Preview</h4>
@@ -83,7 +60,7 @@ var ContainerHomePreview = React.createClass({
</div>
);
} else {
var ports = _.map(_.pairs(this.state.ports), function (pair) {
var ports = _.map(_.pairs(this.props.ports), function (pair) {
var key = pair[0];
var val = pair[1];
return (

View File

@@ -4,19 +4,12 @@ var ContainerListNewItem = require('./ContainerListNewItem.react');
var ContainerList = React.createClass({
componentWillMount: function () {
this._start = Date.now();
this.start = Date.now();
},
render: function () {
var self = this;
var containers = this.props.containers.map(function (container) {
var containerId = container.Id;
if (!containerId && container.State.Downloading) {
// Fall back to the container image name when there is no id. (when the
// image is downloading).
containerId = container.Image;
}
var containers = this.props.containers.map(container => {
return (
<ContainerListItem key={containerId} container={container} start={self._start} />
<ContainerListItem key={container.Id} container={container} start={this.start} />
);
});
return (

View File

@@ -4,9 +4,9 @@ var Router = require('react-router');
var remote = require('remote');
var dialog = remote.require('dialog');
var metrics = require('../utils/MetricsUtil');
var ContainerStore = require('../stores/ContainerStore');
var OverlayTrigger = require('react-bootstrap').OverlayTrigger;
var Tooltip = require('react-bootstrap').Tooltip;
var containerActions = require('../actions/ContainerActions');
var ContainerListItem = React.createClass({
handleItemMouseEnter: function () {
@@ -29,12 +29,7 @@ var ContainerListItem = React.createClass({
from: 'list',
type: 'existing'
});
ContainerStore.remove(this.props.container.Name, () => {
var containers = ContainerStore.sorted();
if (containers.length === 0) {
$(document.body).find('.new-container-item').parent().fadeIn();
}
});
containerActions.destroy(this.props.container.Name);
}
}.bind(this));
},
@@ -56,7 +51,7 @@ var ContainerListItem = React.createClass({
// Synchronize all animations
var style = {
WebkitAnimationDelay: (self.props.start - Date.now()) + 'ms'
WebkitAnimationDelay: 0 + 'ms'
};
var state;
@@ -101,7 +96,7 @@ var ContainerListItem = React.createClass({
}
return (
<Router.Link data-container={name} to="containerDetails" params={{name: container.Name}}>
<Router.Link to="container" params={{name: container.Name}}>
<li onMouseEnter={self.handleItemMouseEnter} onMouseLeave={self.handleItemMouseLeave}>
{state}
<div className="info">

View File

@@ -45,7 +45,7 @@ var ContainerSettings = React.createClass({
</Router.Link>
</ul>
</div>
<Router.RouteHandler container={container}/>
<Router.RouteHandler {...this.props}/>
</div>
</div>
);

View File

@@ -1,80 +1,61 @@
var _ = require('underscore');
var $ = require('jquery');
var React = require('react/addons');
var path = require('path');
var remote = require('remote');
var rimraf = require('rimraf');
var fs = require('fs');
var metrics = require('../utils/MetricsUtil');
var dialog = remote.require('dialog');
var ContainerStore = require('../stores/ContainerStore');
var ContainerUtil = require('../utils/ContainerUtil');
var containerNameSlugify = function (text) {
text = text.replace(/^\s+|\s+$/g, ''); // Trim
text = text.toLowerCase();
// Remove Accents
var from = "àáäâèéëêìíïîòóöôùúüûñç·/,:;";
var to = "aaaaeeeeiiiioooouuuunc-----";
for (var i=0, l=from.length ; i<l ; i++) {
text = text.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
}
text = text.replace(/[^a-z0-9-_.\s]/g, '') // Remove invalid chars
.replace(/\s+/g, '-') // Collapse whitespace and replace by -
.replace(/-+/g, '-') // Collapse dashes
.replace(/_+/g, '_'); // Collapse underscores
return text;
};
var containerActions = require('../actions/ContainerActions');
var ContainerSettingsGeneral = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
getInitialState: function () {
return {
slugName: null,
nameError: null,
env: {},
pendingEnv: {}
pendingEnv: ContainerUtil.env(this.props.container) || {}
};
},
componentWillReceiveProps: function () {
this.init();
},
componentDidMount: function() {
this.init();
},
init: function () {
var container = ContainerStore.container(this.context.router.getCurrentParams().name);
if (!container) {
return;
}
willReceiveProps: function () {
this.setState({
env: ContainerUtil.env(container),
nameError: null
pendingEnv: ContainerUtil.env(this.props.container) || {}
});
},
handleNameChange: function (e) {
var newName = e.target.value;
if (newName === this.state.slugName) {
let name = e.target.value;
if (name === this.state.slugName) {
return;
}
if (!newName.length) {
this.setState({
slugName: null
});
} else {
this.setState({
slugName: containerNameSlugify(newName),
nameError: null
});
name = name.replace(/^\s+|\s+$/g, ''); // Trim
name = name.toLowerCase();
// Remove Accents
let from = "àáäâèéëêìíïîòóöôùúüûñç·/,:;";
let to = "aaaaeeeeiiiioooouuuunc-----";
for (var i=0, l=from.length ; i<l ; i++) {
name = name.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
}
name = name.replace(/[^a-z0-9-_.\s]/g, '') // Remove invalid chars
.replace(/\s+/g, '-') // Collapse whitespace and replace by -
.replace(/-+/g, '-') // Collapse dashes
.replace(/_+/g, '_'); // Collapse underscores
this.setState({
slugName: name
});
},
handleNameOnKeyUp: function (e) {
if (e.keyCode === 13 && this.state.slugName) {
this.handleSaveContainerName();
}
},
handleSaveContainerName: function () {
var newName = this.state.slugName;
if (newName === this.props.container.Name) {
@@ -84,43 +65,19 @@ var ContainerSettingsGeneral = React.createClass({
this.setState({
slugName: null
});
var oldName = this.props.container.Name;
if (ContainerStore.container(newName)) {
if (this.props.containers[newName]) {
this.setState({
nameError: 'A container already exists with this name.'
});
return;
}
ContainerStore.rename(oldName, newName, err => {
if (err) {
this.setState({
nameError: err.message
});
return;
}
metrics.track('Changed Container Name');
this.context.router.transitionTo('containerSettingsGeneral', {name: newName});
var oldPath = path.join(process.env.HOME, 'Kitematic', oldName);
var newPath = path.join(process.env.HOME, 'Kitematic', newName);
rimraf(newPath, () => {
if (fs.existsSync(oldPath)) {
fs.renameSync(oldPath, newPath);
}
var binds = _.pairs(this.props.container.Volumes).map(function (pair) {
return pair[1] + ':' + pair[0];
});
var newBinds = binds.map(b => {
return b.replace(path.join(process.env.HOME, 'Kitematic', oldName), path.join(process.env.HOME, 'Kitematic', newName));
});
ContainerStore.updateContainer(newName, {Binds: newBinds}, err => {
rimraf(oldPath, () => {});
if (err) {
console.log(err);
}
});
});
});
containerActions.rename(this.props.container.Name, newName);
this.context.router.transitionTo('containerSettingsGeneral', {name: newName});
metrics.track('Changed Container Name');
},
handleSaveEnvVar: function () {
var $rows = $('.env-vars .keyval-row');
var envVarList = [];
@@ -132,22 +89,10 @@ var ContainerSettingsGeneral = React.createClass({
}
envVarList.push(key + '=' + val);
});
var self = this;
metrics.track('Saved Environment Variables');
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('');
}
});
containerActions.update(this.props.container.Name, {Env: envVarList});
},
handleAddPendingEnvVar: function () {
var newKey = $('#new-env-key').val();
var newVal = $('#new-env-val').val();
@@ -160,42 +105,30 @@ var ContainerSettingsGeneral = React.createClass({
$('#new-env-val').val('');
metrics.track('Added Pending Environment Variable');
},
handleRemoveEnvVar: function (key) {
handleRemovePendingEnvVar: function (key) {
var newEnv = _.omit(this.state.env, key);
this.setState({
env: newEnv
});
metrics.track('Removed Environment Variable');
},
handleRemovePendingEnvVar: function (key) {
var newEnv = _.omit(this.state.pendingEnv, key);
this.setState({
pendingEnv: newEnv
});
metrics.track('Removed Pending Environment Variable');
},
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);
});
}
}, index => {
if (index === 0) {
metrics.track('Deleted Container', {
from: 'settings',
type: 'existing'
});
ContainerStore.remove(this.props.container.Name, function (err) {
console.error(err);
});
containerActions.destroy(this.props.container.Name);
}
}.bind(this));
});
},
render: function () {
if (!this.props.container) {
return (<div></div>);
@@ -226,22 +159,12 @@ var ContainerSettingsGeneral = React.createClass({
{btnSaveName}
</div>
);
var self = this;
var envVars = _.map(this.state.env, function (val, key) {
var pendingEnvVars = _.map(this.state.pendingEnv, (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>
<a onClick={this.handleRemovePendingEnvVar.bind(this, key)} className="only-icon btn btn-action small"><span className="icon icon-cross"></span></a>
</div>
);
});
@@ -255,7 +178,6 @@ var ContainerSettingsGeneral = React.createClass({
<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>

View File

@@ -1,7 +1,6 @@
var _ = require('underscore');
var React = require('react/addons');
var shell = require('shell');
var ContainerStore = require('../stores/ContainerStore');
var ContainerUtil = require('../utils/ContainerUtil');
var metrics = require('../utils/MetricsUtil');
var webPorts = require('../utils/Util').webPorts;
@@ -16,18 +15,11 @@ var ContainerSettingsPorts = React.createClass({
defaultPort: null
};
},
componentWillReceiveProps: function () {
this.init();
},
componentDidMount: function() {
this.init();
},
init: function () {
var container = ContainerStore.container(this.context.router.getCurrentParams().name);
if (!container) {
if (!this.props.container) {
return;
}
var ports = ContainerUtil.ports(container);
var ports = ContainerUtil.ports(this.props.container);
this.setState({
ports: ports,
defaultPort: _.find(_.keys(ports), function (port) {

View File

@@ -1,7 +1,8 @@
var $ = require('jquery');
var _ = require('underscore');
var React = require('react/addons');
var Router = require('react-router');
var ContainerStore = require('../stores/ContainerStore');
var containerStore = require('../stores/ContainerStore');
var ContainerList = require('./ContainerList.react');
var Header = require('./Header.react');
var ipc = require('ipc');
@@ -16,22 +17,19 @@ var Containers = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
getInitialState: function () {
return {
sidebarOffset: 0,
containers: ContainerStore.containers(),
sorted: ContainerStore.sorted(),
containers: {},
sorted: [],
updateAvailable: false,
currentButtonLabel: '',
error: ContainerStore.error(),
downloading: ContainerStore.downloading()
currentButtonLabel: ''
};
},
componentDidMount: function () {
this.update();
ContainerStore.on(ContainerStore.SERVER_ERROR_EVENT, this.updateError);
ContainerStore.on(ContainerStore.SERVER_CONTAINER_EVENT, this.update);
ContainerStore.on(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient);
containerStore.listen(this.update);
ipc.on('application:update-available', () => {
this.setState({
@@ -40,41 +38,44 @@ var Containers = React.createClass({
});
autoUpdater.checkForUpdates();
},
componentDidUnmount: function () {
ContainerStore.removeListener(ContainerStore.SERVER_CONTAINER_EVENT, this.update);
ContainerStore.removeListener(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient);
containerStore.unlisten(this.update);
},
updateError: function (err) {
this.setState({
error: err
update: function () {
let containers = containerStore.getState().containers;
let sorted = _.values(containers).sort(function (a, b) {
if (a.State.Downloading && !b.State.Downloading) {
return -1;
} else if (!a.State.Downloading && b.State.Downloading) {
return 1;
} else {
if (a.State.Running && !b.State.Running) {
return -1;
} else if (!a.State.Running && b.State.Running) {
return 1;
} else {
return a.Name.localeCompare(b.Name);
}
}
});
},
update: function (name, status) {
var sorted = ContainerStore.sorted();
this.setState({
containers: ContainerStore.containers(),
sorted: sorted,
pending: ContainerStore.pending(),
downloading: ContainerStore.downloading()
});
if (status === 'destroy') {
let name = this.context.router.getCurrentParams().name;
if (name && !containers[name]) {
if (sorted.length) {
this.context.router.transitionTo('containerHome', {name: sorted[0].Name});
} else {
this.context.router.transitionTo('containers');
this.context.router.transitionTo('search');
}
}
this.setState({
containers: containers,
sorted: sorted
});
},
updateFromClient: function (name, status) {
this.update(name, status);
if (status === 'create') {
this.context.router.transitionTo('containerHome', {name: name});
} else if (status === 'pending' && ContainerStore.pending()) {
this.context.router.transitionTo('pull');
} else if (status === 'destroy') {
this.onDestroy();
}
},
handleScroll: function (e) {
if (e.target.scrollTop > 0 && !this.state.sidebarOffset) {
this.setState({
@@ -86,63 +87,75 @@ var Containers = React.createClass({
});
}
},
handleNewContainer: function () {
$(this.getDOMNode()).find('.new-container-item').parent().fadeIn();
this.context.router.transitionTo('new');
metrics.track('Pressed New Container');
},
handleAutoUpdateClick: function () {
metrics.track('Restarted to Update');
ipc.send('application:quit-install');
},
handleClickPreferences: function () {
metrics.track('Opened Preferences', {
from: 'app'
});
this.context.router.transitionTo('preferences');
},
handleClickDockerTerminal: function () {
metrics.track('Opened Docker Terminal', {
from: 'app'
});
machine.dockerTerminal();
},
handleClickReportIssue: function () {
metrics.track('Opened Issue Reporter', {
from: 'app'
});
shell.openExternal('https://github.com/kitematic/kitematic/issues/new');
},
handleMouseEnterDockerTerminal: function () {
this.setState({
currentButtonLabel: 'Open terminal to use Docker command line.'
});
},
handleMouseLeaveDockerTerminal: function () {
this.setState({
currentButtonLabel: ''
});
},
handleMouseEnterReportIssue: function () {
this.setState({
currentButtonLabel: 'Report an issue or suggest feedback.'
});
},
handleMouseLeaveReportIssue: function () {
this.setState({
currentButtonLabel: ''
});
},
handleMouseEnterPreferences: function () {
this.setState({
currentButtonLabel: 'Change app preferences.'
});
},
handleMouseLeavePreferences: function () {
this.setState({
currentButtonLabel: ''
});
},
render: function () {
var sidebarHeaderClass = 'sidebar-header';
if (this.state.sidebarOffset) {
@@ -168,7 +181,7 @@ var Containers = React.createClass({
</div>
</section>
<section className="sidebar-containers" onScroll={this.handleScroll}>
<ContainerList downloading={this.state.downloading} containers={this.state.sorted} newContainer={this.state.newContainer} />
<ContainerList containers={this.state.sorted} newContainer={this.state.newContainer} />
<div className="sidebar-buttons">
<div className="btn-label">{this.state.currentButtonLabel}</div>
<span className="btn-sidebar" onClick={this.handleClickDockerTerminal} onMouseEnter={this.handleMouseEnterDockerTerminal} onMouseLeave={this.handleMouseLeaveDockerTerminal}><RetinaImage src="docker-terminal.png"/></span>
@@ -179,7 +192,7 @@ var Containers = React.createClass({
<div className="sidebar-buttons-padding"></div>
</section>
</div>
<Router.RouteHandler pending={this.state.pending} container={container} error={this.state.error}/>
<Router.RouteHandler pending={this.state.pending} containers={this.state.containers} container={container} error={this.state.error}/>
</div>
</div>
);

View File

@@ -1,11 +1,12 @@
var $ = require('jquery');
var React = require('react/addons');
var RetinaImage = require('react-retina-image');
var ContainerStore = require('../stores/ContainerStore');
var metrics = require('../utils/MetricsUtil');
var OverlayTrigger = require('react-bootstrap').OverlayTrigger;
var Tooltip = require('react-bootstrap').Tooltip;
var util = require('../utils/Util');
var dockerUtil = require('../utils/DockerUtil');
var containerStore = require('../stores/ContainerStore');
var ImageCard = React.createClass({
getInitialState: function () {
@@ -22,13 +23,12 @@ var ImageCard = React.createClass({
$tagOverlay.fadeOut(300);
metrics.track('Selected Image Tag');
},
handleClick: function (name) {
handleClick: function (repository) {
metrics.track('Created Container', {
from: 'search'
});
ContainerStore.create(name, this.state.chosenTag, function () {
$(document.body).find('.new-container-item').parent().fadeOut();
}.bind(this));
let name = containerStore.generateName(repository);
dockerUtil.run(name, repository, this.state.chosenTag);
},
handleTagOverlayClick: function (name) {
var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');

View File

@@ -5,7 +5,7 @@ var router = require('./router');
var util = require('./utils/Util');
var metrics = require('./utils/MetricsUtil');
var machine = require('./utils/DockerMachineUtil');
var docker = require('./utils/DockerUtil');
import docker from './utils/DockerUtil';
// main.js
var MenuTemplate = function () {
@@ -23,7 +23,7 @@ var MenuTemplate = function () {
{
label: 'Preferences',
accelerator: util.CommandOrCtrl() + '+,',
enabled: !!docker.host(),
enabled: !!docker.host,
click: function () {
metrics.track('Opened Preferences', {
from: 'menu'
@@ -76,7 +76,7 @@ var MenuTemplate = function () {
{
label: 'Open Docker Command Line Terminal',
accelerator: util.CommandOrCtrl() + '+Shift+T',
enabled: !!docker.host(),
enabled: !!docker.host,
click: function() {
metrics.track('Opened Docker Terminal', {
from: 'menu'

View File

@@ -29,8 +29,8 @@ var App = React.createClass({
var routes = (
<Route name="app" path="/" handler={App}>
<Route name="containers" handler={Containers}>
<Route name="containerDetails" path="containers/details/:name" handler={ContainerDetails}>
<Route name="containerHome" path="containers/details/:name/home" handler={ContainerHome} />
<Route name="container" path="containers/details/:name" handler={ContainerDetails}>
<DefaultRoute name="containerHome" handler={ContainerHome} />
<Route name="containerLogs" path="containers/details/:name/logs" handler={ContainerLogs}/>
<Route name="containerSettings" path="containers/details/:name/settings" handler={ContainerSettings}>
<Route name="containerSettingsGeneral" path="containers/details/:name/settings/general" handler={ContainerSettingsGeneral}/>
@@ -43,9 +43,9 @@ var routes = (
<Route name="pull" path="containers/new/pull" handler={NewContainerPull}></Route>
</Route>
<Route name="preferences" path="/preferences" handler={Preferences}/>
<Redirect to="new"/>
</Route>
<DefaultRoute name="setup" handler={Setup}/>
<Redirect from="containers/details/:name" to="containerHome"/>
</Route>
);

View File

@@ -1,520 +1,133 @@
var _ = require('underscore');
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var assign = require('object-assign');
var docker = require('../utils/DockerUtil');
var metrics = require('../utils/MetricsUtil');
var registry = require('../utils/RegistryUtil');
var logStore = require('../stores/LogStore');
var bugsnag = require('bugsnag-js');
import _ from 'underscore';
import alt from '../alt';
import containerServerActions from '../actions/ContainerServerActions';
import containerActions from '../actions/ContainerActions';
var _placeholders = {};
var _containers = {};
var _progress = {};
var _muted = {};
var _blocked = {};
var _error = null;
var _pending = null;
class ContainerStore {
constructor () {
this.bindActions(containerActions);
this.bindActions(containerServerActions);
this.containers = {};
var ContainerStore = assign(Object.create(EventEmitter.prototype), {
CLIENT_CONTAINER_EVENT: 'client_container_event',
SERVER_CONTAINER_EVENT: 'server_container_event',
SERVER_PROGRESS_EVENT: 'server_progress_event',
SERVER_ERROR_EVENT: 'server_error_event',
_pullImage: function (repository, tag, callback, progressCallback, blockedCallback) {
registry.layers(repository, tag, (err, layerSizes) => {
// Blacklist of containers to avoid updating
this.muted = {};
}
// TODO: Support v2 registry API
// TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
// Use the per-layer pull progress % to update the total progress.
docker.client().listImages({all: 1}, (err, images) => {
images = images || [];
var existingIds = new Set(images.map(function (image) {
return image.Id.slice(0, 12);
}));
var layersToDownload = layerSizes.filter(function (layerSize) {
return !existingIds.has(layerSize.Id) && !isNaN(layerSize.size);
});
start ({name}) {
let containers = this.containers;
if (containers[name]) {
containers[name].State.Starting = true;
this.setState({containers});
}
}
var totalBytes = layersToDownload.map(function (s) { return s.size; }).reduce(function (pv, sv) { return pv + sv; }, 0);
docker.client().pull(repository + ':' + tag, (err, stream) => {
if (err) {
callback(err);
return;
}
stream.setEncoding('utf8');
stop ({name}) {
let containers = this.containers;
if (containers[name]) {
containers[name].State.Running = false;
this.setState({containers});
}
}
var layerProgress = layersToDownload.reduce(function (r, layer) {
if (_.findWhere(images, {Id: layer.Id})) {
r[layer.Id] = 100;
} else {
r[layer.Id] = 0;
}
return r;
}, {});
rename ({name, newName}) {
let containers = this.containers;
let data = containers[name];
data.Name = newName;
containers[newName] = data;
delete containers[name];
this.setState({containers});
}
stream.on('data', str => {
var data = JSON.parse(str);
added ({container}) {
let containers = this.containers;
containers[container.Name] = container;
delete this.muted[container.Name];
this.setState({containers});
}
if (data.error) {
_error = data.error;
callback(data.error);
return;
}
updated ({container}) {
if (this.muted[container.Name]) {
return;
}
if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) {
blockedCallback();
return;
}
let containers = this.containers;
if (!containers[container.Name]) {
return;
}
containers[container.Name] = container;
if (container.State.Running) {
delete container.State.Starting;
}
this.setState({containers});
}
if (data.status === 'Already exists') {
layerProgress[data.id] = 1;
} else if (data.status === 'Downloading') {
var current = data.progressDetail.current;
var total = data.progressDetail.total;
if (total <= 0) {
progressCallback(0);
return;
} else {
layerProgress[data.id] = current / total;
}
allUpdated ({containers}) {
this.setState({containers});
}
var chunks = layersToDownload.map(function (s) {
var progress = layerProgress[s.Id] || 0;
return progress * s.size;
});
progress ({name, progress}) {
let containers = this.containers;
if (containers[name]) {
containers[name].Progress = progress;
}
this.setState({containers});
}
var totalReceived = chunks.reduce(function (pv, sv) {
return pv + sv;
}, 0);
destroy ({name}) {
let containers = this.containers;
delete containers[name];
this.setState({containers});
}
var totalProgress = totalReceived / totalBytes;
progressCallback(totalProgress);
}
});
stream.on('end', function () {
callback(_error);
_error = null;
});
});
});
destroyed ({name}) {
let containers = this.containers;
let container = _.find(_.values(this.containers), container => {
return container.Id === name || container.Name === name;
});
},
_startContainer: function (name, containerData, callback) {
var self = this;
var binds = containerData.Binds || [];
var startopts = {
Binds: binds
};
if (containerData.NetworkSettings && containerData.NetworkSettings.Ports) {
startopts.PortBindings = containerData.NetworkSettings.Ports;
} else{
startopts.PublishAllPorts = true;
if (container && !this.muted[container.Name]) {
delete containers[container.Name];
this.setState({containers});
}
var container = docker.client().getContainer(name);
container.start(startopts, function (err) {
if (err) {
callback(err);
return;
}
self.fetchContainer(name, callback);
logStore.fetch(name);
});
},
_createContainer: function (name, containerData, callback) {
var existing = docker.client().getContainer(name);
var self = this;
if (!containerData.name && containerData.Name) {
containerData.name = containerData.Name;
} else if (!containerData.name) {
containerData.name = name;
}
muted ({name}) {
this.muted[name] = true;
}
unmuted ({name}) {
this.muted[name] = false;
}
waiting({name, waiting}) {
let containers = this.containers;
if (containers[name]) {
containers[name].State.Waiting = waiting;
}
if (containerData.Config && containerData.Config.Image) {
containerData.Image = containerData.Config.Image;
this.setState({containers});
}
error ({ name, error }) {
let containers = this.containers;
if (containers[name]) {
containers[name].Error = error;
}
if (!containerData.Env && containerData.Config && containerData.Config.Env) {
containerData.Env = containerData.Config.Env;
}
existing.kill(function () {
existing.remove(function () {
docker.client().createContainer(containerData, function (err) {
if (err) {
callback(err, null);
return;
}
self._startContainer(name, containerData, callback);
});
});
});
},
_generateName: function (repository) {
var base = _.last(repository.split('/'));
var count = 1;
var name = base;
this.setState({containers});
}
static generateName (repo) {
let base = _.last(repo.split('/'));
let count = 1;
let name = base;
let names = _.keys(this.getState().containers);
while (true) {
if (!this.containers()[name]) {
if (names.indexOf(name) === -1) {
return name;
} else {
count++;
name = base + '-' + count;
}
}
},
_resumePulling: function (callback) {
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) {
_progress[container.Name] = 99;
docker.client().pull(container.Config.Image, function (err, stream) {
if (err) {
callback(err);
return;
}
stream.setEncoding('utf8');
stream.on('data', function () {});
stream.on('end', function () {
delete _placeholders[container.Name];
delete _progress[container.Name];
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
self._createContainer(container.Name, {Image: container.Config.Image}, err => {
if (err) {
callback(err);
return;
}
self.emit(self.SERVER_PROGRESS_EVENT, container.Name);
self.emit(self.CLIENT_CONTAINER_EVENT, container.Name);
});
});
});
});
},
_startListeningToEvents: function (callback) {
docker.client().getEvents((err, stream) => {
if (err) {
callback(err);
return;
}
if (stream) {
stream.setEncoding('utf8');
stream.on('data', this._dockerEvent.bind(this));
}
});
},
_dockerEvent: function (json) {
var data = JSON.parse(json);
console.log(data);
if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete') {
return;
}
// If the event is delete, remove the container
if (data.status === 'destroy') {
var container = _.findWhere(_.values(_containers), {Id: data.id});
if (container) {
delete _containers[container.Name];
if (!_muted[container.Name]) {
this.emit(this.SERVER_CONTAINER_EVENT, container.Name, data.status);
}
} else {
this.emit(this.SERVER_CONTAINER_EVENT, data.status);
}
} else {
this.fetchContainer(data.id, err => {
if (err) {
return;
}
var container = _.findWhere(_.values(_containers), {Id: data.id});
if (!container || _muted[container.Name]) {
return;
}
this.emit(this.SERVER_CONTAINER_EVENT, container ? container.Name : null, data.status);
});
}
},
init: function (callback) {
this.fetchAllContainers(err => {
if (err) {
_error = err;
this.emit(this.SERVER_ERROR_EVENT, err);
bugsnag.notify(err, 'Container Store failed to init', err);
callback(err);
return;
} else {
callback();
}
var placeholderData = JSON.parse(localStorage.getItem('store.placeholders'));
if (placeholderData) {
_placeholders = _.omit(placeholderData, _.keys(_containers));
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
}
this.emit(this.CLIENT_CONTAINER_EVENT);
this._resumePulling(err => {
_error = err;
this.emit(this.SERVER_ERROR_EVENT, err);
bugsnag.notify(err, 'Container Store failed to resume pulling', err);
});
this._startListeningToEvents(err => {
_error = err;
this.emit(this.SERVER_ERROR_EVENT, err);
bugsnag.notify(err, 'Container Store failed to listen to events', err);
});
});
},
fetchContainer: function (id, callback) {
docker.client().getContainer(id).inspect((err, container) => {
if (err) {
callback(err);
} else {
if (container.Config.Image === container.Image.slice(0, 12) || container.Config.Image === container.Image) {
callback();
return;
}
// Fix leading slash in container names
container.Name = container.Name.replace('/', '');
_containers[container.Name] = container;
callback(null, container);
}
});
},
fetchAllContainers: function (callback) {
docker.client().listContainers({all: true}, (err, containers) => {
if (err) {
callback(err);
return;
}
var names = new Set(_.map(containers, container => container.Names[0].replace('/', '')));
_.each(_.keys(_containers), name => {
if (!names.has(name)) {
delete _containers[name];
}
});
async.each(containers, (container, callback) => {
this.fetchContainer(container.Id, function (err) {
callback(err);
});
}, function (err) {
callback(err);
});
});
},
create: function (repository, tag, callback) {
tag = tag || 'latest';
var imageName = repository + ':' + tag;
var containerName = this._generateName(repository);
_placeholders[containerName] = {
Id: require('crypto').randomBytes(32).toString('hex'),
Name: containerName,
Image: imageName,
Config: {
Image: imageName,
},
State: {
Downloading: true
}
};
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
this.emit(this.CLIENT_CONTAINER_EVENT, containerName, 'create');
_muted[containerName] = true;
this._pullImage(repository, tag, err => {
if (err) {
_error = err;
this.emit(this.SERVER_ERROR_EVENT, err);
bugsnag.notify(err, 'Container Store failed to create container', err);
return;
}
_error = null;
_blocked[containerName] = false;
if (!_placeholders[containerName]) {
return;
}
delete _placeholders[containerName];
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
this._createContainer(containerName, {Image: imageName}, err => {
if (err) {
console.log(err);
_error = err;
this.emit(this.SERVER_ERROR_EVENT, err);
return;
}
_error = null;
metrics.track('Container Finished Creating');
delete _progress[containerName];
_muted[containerName] = false;
this.emit(this.CLIENT_CONTAINER_EVENT, containerName);
});
}, progress => {
_blocked[containerName] = false;
_progress[containerName] = progress;
this.emit(this.SERVER_PROGRESS_EVENT, containerName);
}, () => {
_blocked[containerName] = true;
this.emit(this.SERVER_PROGRESS_EVENT, containerName);
});
callback(null, containerName);
},
updateContainer: function (name, data, callback) {
_muted[name] = true;
if (!data.name) {
data.name = data.Name;
}
var fullData = assign(_containers[name], data);
this._createContainer(name, fullData, function () {
_muted[name] = false;
this.emit(this.CLIENT_CONTAINER_EVENT, name);
callback();
}.bind(this));
},
rename: function (name, newName, callback) {
docker.client().getContainer(name).rename({name: newName}, err => {
if (err && err.statusCode !== 204) {
callback(err);
return;
}
this.fetchAllContainers(err => {
this.emit(this.CLIENT_CONTAINER_EVENT);
callback(err);
});
});
},
restart: function (name, callback) {
var container = docker.client().getContainer(name);
_muted[name] = true;
container.stop(err => {
if (err && err.statusCode !== 304) {
_muted[name] = false;
callback(err);
} else {
var data = _containers[name];
this._startContainer(name, data, err => {
_muted[name] = false;
this.emit(this.CLIENT_CONTAINER_EVENT, name, 'start');
callback(err);
});
}
});
},
stop: function (name, callback) {
var container = docker.client().getContainer(name);
_muted[name] = true;
container.stop(err => {
if (err && err.statusCode !== 304) {
_muted[name] = false;
callback(err);
} else {
_muted[name] = false;
this.fetchContainer(name, callback);
}
});
},
start: function (name, callback) {
var container = docker.client().getContainer(name);
container.start(err => {
if (err && err.statusCode !== 304) {
callback(err);
} else {
this.fetchContainer(name, callback);
}
});
},
remove: function (name, callback) {
if (_placeholders[name]) {
delete _placeholders[name];
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
this.emit(this.CLIENT_CONTAINER_EVENT, name, 'destroy');
callback();
return;
}
var container = docker.client().getContainer(name);
if (_containers[name] && _containers[name].State.Paused) {
container.unpause(function (err) {
if (err) {
callback(err);
return;
} else {
container.kill(function (err) {
if (err) {
callback(err);
return;
} else {
container.remove(function (err) {
if (err) {
callback(err);
return;
}
});
}
});
}
});
} else {
container.kill(function (err) {
if (err) {
callback(err);
return;
} else {
container.remove(function (err) {
callback(err);
});
}
});
}
},
containers: function() {
return _.extend(_.clone(_containers), _placeholders);
},
container: function (name) {
return this.containers()[name];
},
sorted: function () {
return _.values(this.containers()).sort(function (a, b) {
if (a.State.Downloading && !b.State.Downloading) {
return -1;
} else if (!a.State.Downloading && b.State.Downloading) {
return 1;
} else {
if (a.State.Running && !b.State.Running) {
return -1;
} else if (!a.State.Running && b.State.Running) {
return 1;
} else {
return a.Name.localeCompare(b.Name);
}
}
});
},
progress: function (name) {
return _progress[name];
},
blocked: function (name) {
return !!_blocked[name];
},
error: function () {
return _error;
},
downloading: function () {
return !!_.keys(_placeholders).length;
},
pending: function () {
return _pending;
},
setPending: function (repository, tag) {
_pending = {
repository: repository,
tag: tag
};
this.emit(this.CLIENT_CONTAINER_EVENT, null, 'pending');
},
clearPending: function () {
_pending = null;
this.emit(this.CLIENT_CONTAINER_EVENT, null, 'pending');
}
});
}
module.exports = ContainerStore;
export default alt.createStore(ContainerStore);

View File

@@ -19,10 +19,10 @@ module.exports = assign(Object.create(EventEmitter.prototype), {
return div.innerHTML;
},
fetch: function (name) {
if (!name || !docker.client()) {
if (!name || !docker.client) {
return;
}
docker.client().getContainer(name).logs({
docker.client.getContainer(name).logs({
stdout: true,
stderr: true,
timestamps: false,
@@ -34,7 +34,7 @@ module.exports = assign(Object.create(EventEmitter.prototype), {
}
var logs = [];
var outstream = new stream.PassThrough();
docker.client().modem.demuxStream(logStream, outstream, outstream);
docker.client.modem.demuxStream(logStream, outstream, outstream);
outstream.on('data', (chunk) => {
logs.push(_convert.toHtml(this._escape(chunk)));
});
@@ -46,10 +46,10 @@ module.exports = assign(Object.create(EventEmitter.prototype), {
});
},
attach: function (name) {
if (!name || !docker.client() || _streams[name]) {
if (!name || !docker.client || _streams[name]) {
return;
}
docker.client().getContainer(name).attach({
docker.client.getContainer(name).attach({
stdout: true,
stderr: true,
logs: false,
@@ -60,7 +60,7 @@ module.exports = assign(Object.create(EventEmitter.prototype), {
}
_streams[name] = logStream;
var outstream = new stream.PassThrough();
docker.client().modem.demuxStream(logStream, outstream, outstream);
docker.client.modem.demuxStream(logStream, outstream, outstream);
outstream.on('data', (chunk) => {
_logs[name].push(_convert.toHtml(this._escape(chunk)));
if (_logs[name].length > MAX_LOG_SIZE) {

View File

@@ -12,13 +12,14 @@ var ContainerUtil = {
return splits;
}));
},
// TODO (jeffdm): inject host here instead of requiring Docker
// TODO: inject host here instead of requiring Docker
ports: function (container) {
if (!container.NetworkSettings) {
if (!container || !container.NetworkSettings) {
return {};
}
var res = {};
var ip = docker.host();
var ip = docker.host;
_.each(container.NetworkSettings.Ports, function (value, key) {
var dockerPort = key.split('/')[0];
var localUrl = null;

View File

@@ -1,19 +1,32 @@
var fs = require('fs');
var path = require('path');
var dockerode = require('dockerode');
var Promise = require('bluebird');
var util = require('./Util');
import async from 'async';
import fs from 'fs';
import path from 'path';
import dockerode from 'dockerode';
import _ from 'underscore';
import util from './Util';
import registry from '../utils/RegistryUtil';
import metrics from '../utils/MetricsUtil';
import containerServerActions from '../actions/ContainerServerActions';
import Promise from 'bluebird';
import rimraf from 'rimraf';
var Docker = {
_host: null,
_client: null,
setup: function(ip, name) {
var certDir = path.join(util.home(), '.docker/machine/machines', name);
if (!fs.existsSync(certDir)) {
return;
export default {
host: null,
client: null,
placeholders: {},
setup (ip, name) {
if (!ip || !name) {
throw new Error('Falsy ip or machine name passed to init');
}
this._host = ip;
this._client = new dockerode({
let certDir = path.join(util.home(), '.docker/machine/machines/', name);
if (!fs.existsSync(certDir)) {
throw new Error('Certificate directory does not exist');
}
this.host = ip;
this.client = new dockerode({
protocol: 'https',
host: ip,
port: 2376,
@@ -22,38 +35,409 @@ var Docker = {
key: fs.readFileSync(path.join(certDir, 'key.pem'))
});
},
client: function () {
return this._client;
init () {
this.placeholders = JSON.parse(localStorage.getItem('placeholders')) || {};
this.fetchAllContainers();
this.listen();
// Resume pulling containers that were previously being pulled
_.each(_.values(this.placeholders), container => {
containerServerActions.added({container});
this.client.pull(container.Config.Image, (error, stream) => {
if (error) {
containerServerActions.error({name: container.Name, error});
return;
}
stream.setEncoding('utf8');
stream.on('data', function () {});
stream.on('end', () => {
delete this.placeholders[container.Name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
this.createContainer(container.Name, {Image: container.Config.Image});
});
});
});
},
host: function () {
return this._host;
startContainer (name, containerData) {
let startopts = {
Binds: containerData.Binds || []
};
if (containerData.NetworkSettings && containerData.NetworkSettings.Ports) {
startopts.PortBindings = containerData.NetworkSettings.Ports;
} else {
startopts.PublishAllPorts = true;
}
let container = this.client.getContainer(name);
container.start(startopts, (error) => {
if (error) {
containerServerActions.error({name, error});
return;
}
containerServerActions.unmuted({name});
this.fetchContainer(name);
});
},
waitForConnection: Promise.coroutine(function * (tries, delay) {
tries = tries || 10;
delay = delay || 1000;
var tryCount = 1;
while (true) {
try {
yield new Promise((resolve, reject) => {
this._client.listContainers((err) => {
if (err) {
reject(err);
createContainer (name, containerData) {
containerData.name = containerData.Name || name;
if (containerData.Config && containerData.Config.Image) {
containerData.Image = containerData.Config.Image;
}
if (!containerData.Env && containerData.Config && containerData.Config.Env) {
containerData.Env = containerData.Config.Env;
}
let existing = this.client.getContainer(name);
existing.kill(() => {
existing.remove(() => {
this.client.createContainer(containerData, (error) => {
if (error) {
containerServerActions.error({name, error});
return;
}
metrics.track('Container Finished Creating');
this.startContainer(name, containerData);
});
});
});
},
fetchContainer (id) {
this.client.getContainer(id).inspect((error, container) => {
if (error) {
containerServerActions.error({name: id, error});
} else {
container.Name = container.Name.replace('/', '');
containerServerActions.updated({container});
}
});
},
fetchAllContainers () {
this.client.listContainers({all: true}, (err, containers) => {
if (err) {
return;
}
async.map(containers, (container, callback) => {
this.client.getContainer(container.Id).inspect((error, container) => {
container.Name = container.Name.replace('/', '');
callback(null, container);
});
}, (err, containers) => {
if (err) {
// TODO: add a global error handler for this
return;
}
containerServerActions.allUpdated({containers: _.indexBy(containers.concat(_.values(this.placeholders)), 'Name')});
});
});
},
run (name, repository, tag) {
tag = tag || 'latest';
let imageName = repository + ':' + tag;
let placeholderData = {
Id: require('crypto').randomBytes(32).toString('hex'),
Name: name,
Image: imageName,
Config: {
Image: imageName,
},
State: {
Downloading: true
}
};
containerServerActions.added({container: placeholderData});
this.placeholders[name] = placeholderData;
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
this.pullImage(repository, tag, error => {
if (error) {
containerServerActions.error({name, error});
return;
}
if (!this.placeholders[name]) {
return;
}
delete this.placeholders[name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
this.createContainer(name, {Image: imageName});
}, progress => {
containerServerActions.progress({name, progress});
}, () => {
containerServerActions.waiting({name, waiting: true});
});
},
updateContainer (name, data) {
let existing = this.client.getContainer(name);
existing.inspect((error, existingData) => {
if (error) {
return;
}
if (error) {
containerServerActions.error({name, error});
return;
}
existingData.name = existingData.Name || name;
if (existingData.Config && existingData.Config.Image) {
existingData.Image = existingData.Config.Image;
}
if (!existingData.Env && existingData.Config && existingData.Config.Env) {
existingData.Env = existingData.Config.Env;
}
var fullData = _.extend(existingData, data);
containerServerActions.muted({name});
this.createContainer(name, fullData);
});
},
rename (name, newName) {
this.client.getContainer(name).rename({name: newName}, error => {
if (error && error.statusCode !== 204) {
containerServerActions.error({name, error});
return;
}
this.fetchAllContainers();
var oldPath = path.join(util.home(), 'Kitematic', name);
var newPath = path.join(util.home(), 'Kitematic', newName);
this.client.getContainer(newName).inspect((error, container) => {
if (error) {
// TODO: handle error
containerServerActions.error({newName, error});
}
rimraf(newPath, () => {
console.log('removed');
if (fs.existsSync(oldPath)) {
fs.renameSync(oldPath, newPath);
}
var binds = _.pairs(container.Volumes).map(function (pair) {
return pair[1] + ':' + pair[0];
});
var newBinds = binds.map(b => {
return b.replace(path.join(util.home(), 'Kitematic', name), path.join(util.home(), 'Kitematic', newName));
});
this.updateContainer(newName, {Binds: newBinds});
rimraf(oldPath, () => {});
});
});
});
},
restart (name) {
let container = this.client.getContainer(name);
container.stop(error => {
if (error && error.statusCode !== 304) {
containerServerActions.error({name, error});
return;
}
container.inspect((error, data) => {
if (error) {
containerServerActions.error({name, error});
}
this.startContainer(name, data);
});
});
},
stop (name) {
this.client.getContainer(name).stop(error => {
if (error && error.statusCode !== 304) {
containerServerActions.error({name, error});
return;
}
this.fetchContainer(name);
});
},
start (name) {
this.client.getContainer(name).start(error => {
if (error && error.statusCode !== 304) {
containerServerActions.error({name, error});
return;
}
this.fetchContainer(name);
});
},
destroy (name) {
if (this.placeholders[name]) {
containerServerActions.destroyed({id: name});
delete this.placeholders[name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
return;
}
let container = this.client.getContainer(name);
container.unpause(function () {
container.kill(function (error) {
if (error) {
containerServerActions.error({name, error});
return;
}
container.remove(function () {
containerServerActions.destroyed({id: name});
var volumePath = path.join(util.home(), 'Kitematic', name);
if (fs.existsSync(volumePath)) {
rimraf(volumePath, function (err) {
console.log(err);
});
}
});
});
});
},
listen () {
this.client.getEvents((error, stream) => {
if (error || !stream) {
// TODO: Add app-wide error handler
return;
}
stream.setEncoding('utf8');
stream.on('data', json => {
let data = JSON.parse(json);
console.log(data);
if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete') {
return;
}
if (data.status === 'destroy') {
containerServerActions.destroyed({name: data.id});
} else if (data.status === 'create') {
this.fetchAllContainers();
} else {
this.fetchContainer(data.id);
}
});
});
},
pullImage (repository, tag, callback, progressCallback, blockedCallback) {
registry.layers(repository, tag, (err, layerSizes) => {
// TODO: Support v2 registry API
// TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
// Use the per-layer pull progress % to update the total progress.
this.client.listImages({all: 1}, (err, images) => {
images = images || [];
let existingIds = new Set(images.map(function (image) {
return image.Id.slice(0, 12);
}));
let layersToDownload = layerSizes.filter(function (layerSize) {
return !existingIds.has(layerSize.Id);
});
this.client.pull(repository + ':' + tag, (err, stream) => {
if (err) {
callback(err);
return;
}
stream.setEncoding('utf8');
let layerProgress = layersToDownload.reduce(function (r, layer) {
if (_.findWhere(images, {Id: layer.Id})) {
r[layer.Id] = 1;
} else {
resolve();
r[layer.Id] = 0;
}
return r;
}, {});
let timeout = null;
stream.on('data', str => {
var data = JSON.parse(str);
if (data.error) {
return;
}
if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) {
blockedCallback();
return;
}
if (data.status === 'Already exists') {
layerProgress[data.id] = 1;
} else if (data.status === 'Downloading') {
let current = data.progressDetail.current;
let total = data.progressDetail.total;
if (total <= 0) {
progressCallback(0);
return;
} else {
layerProgress[data.id] = current / total;
}
let sum = _.values(layerProgress).reduce((pv, sv) => pv + sv, 0);
let numlayers = _.keys(layerProgress).length;
var totalProgress = sum / numlayers * 100;
if (!timeout) {
progressCallback(totalProgress);
timeout = setTimeout(() => {
timeout = null;
}, 100);
}
}
});
stream.on('end', function () {
callback();
});
});
break;
} catch (err) {
tryCount += 1;
yield Promise.delay(delay);
if (tryCount > tries) {
throw new Error('Cannot connect to the Docker Engine. Either the VM is not responding or the connection may be blocked (VPN or Proxy): ' + err.message);
}
continue;
}
}
}),
};
});
});
},
module.exports = Docker;
// TODO: move this to machine health checks
waitForConnection (tries, delay) {
tries = tries || 10;
delay = delay || 1000;
let tryCount = 1, connected = false;
return new Promise((resolve, reject) => {
async.until(() => connected, callback => {
this.client.listContainers(error => {
if (error) {
if (tryCount > tries) {
callback(Error('Cannot connect to the Docker Engine. Either the VM is not responding or the connection may be blocked (VPN or Proxy): ' + error.message));
} else {
tryCount += 1;
setTimeout(callback, delay);
}
} else {
connected = true;
callback();
}
});
}, error => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
};

View File

@@ -1,7 +1,6 @@
/* Sidebar */
.sidebar {
.fade-in();
padding-top: 28px;
background-color: white;
margin: 0;

View File

@@ -1,5 +1,4 @@
.details {
.fade-in();
background-color: @color-background;
margin: 0;
padding: 0;

View File

@@ -1,5 +1,4 @@
.setup {
.fade-in();
display: flex;
height: 100%;
width: 100%;