mirror of
https://github.com/docker/docs.git
synced 2026-03-27 14:28:47 +07:00
All but environment variables working
This commit is contained in:
@@ -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",
|
||||
|
||||
33
src/actions/ContainerActions.js
Normal file
33
src/actions/ContainerActions.js
Normal 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);
|
||||
19
src/actions/ContainerServerActions.js
Normal file
19
src/actions/ContainerServerActions.js
Normal 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
2
src/alt.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Alt from 'alt';
|
||||
export default new Alt();
|
||||
17
src/app.js
17
src/app.js
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -45,7 +45,7 @@ var ContainerSettings = React.createClass({
|
||||
</Router.Link>
|
||||
</ul>
|
||||
</div>
|
||||
<Router.RouteHandler container={container}/>
|
||||
<Router.RouteHandler {...this.props}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* Sidebar */
|
||||
|
||||
.sidebar {
|
||||
.fade-in();
|
||||
padding-top: 28px;
|
||||
background-color: white;
|
||||
margin: 0;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.details {
|
||||
.fade-in();
|
||||
background-color: @color-background;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.setup {
|
||||
.fade-in();
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user