From 7989d482532d15c451b2bdd1fbf50606f4847419 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Fri, 8 May 2015 20:14:41 -0700 Subject: [PATCH 1/5] All but environment variables working --- package.json | 2 + src/actions/ContainerActions.js | 33 + src/actions/ContainerServerActions.js | 19 + src/alt.js | 2 + src/app.js | 17 +- src/components/ContainerDetails.react.js | 38 +- .../ContainerDetailsHeader.react.js | 3 + .../ContainerDetailsSubheader.react.js | 58 +- src/components/ContainerHome.react.js | 89 +-- src/components/ContainerHomeFolders.react.js | 16 +- src/components/ContainerHomePreview.react.js | 43 +- src/components/ContainerList.react.js | 13 +- src/components/ContainerListItem.react.js | 13 +- src/components/ContainerSettings.react.js | 2 +- .../ContainerSettingsGeneral.react.js | 168 ++--- .../ContainerSettingsPorts.react.js | 12 +- src/components/Containers.react.js | 89 +-- src/components/ImageCard.react.js | 10 +- src/menutemplate.js | 6 +- src/routes.js | 6 +- src/stores/ContainerStore.js | 603 ++++-------------- src/stores/LogStore.js | 12 +- src/utils/ContainerUtil.js | 7 +- src/utils/DockerUtil.js | 468 ++++++++++++-- styles/left-panel.less | 1 - styles/right-panel.less | 1 - styles/setup.less | 1 - 27 files changed, 799 insertions(+), 933 deletions(-) create mode 100644 src/actions/ContainerActions.js create mode 100644 src/actions/ContainerServerActions.js create mode 100644 src/alt.js diff --git a/package.json b/package.json index 920c0e3df5..7403264e69 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/ContainerActions.js b/src/actions/ContainerActions.js new file mode 100644 index 0000000000..cb97435e2b --- /dev/null +++ b/src/actions/ContainerActions.js @@ -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); diff --git a/src/actions/ContainerServerActions.js b/src/actions/ContainerServerActions.js new file mode 100644 index 0000000000..e1a88e0e71 --- /dev/null +++ b/src/actions/ContainerServerActions.js @@ -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); diff --git a/src/alt.js b/src/alt.js new file mode 100644 index 0000000000..bd2bdab360 --- /dev/null +++ b/src/alt.js @@ -0,0 +1,2 @@ +import Alt from 'alt'; +export default new Alt(); diff --git a/src/app.js b/src/app.js index 5fdac16168..3ef7360b02 100644 --- a/src/app.js +++ b/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(, 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', () => { diff --git a/src/components/ContainerDetails.react.js b/src/components/ContainerDetails.react.js index b18d69819b..58cf2fb628 100644 --- a/src/components/ContainerDetails.react.js +++ b/src/components/ContainerDetails.react.js @@ -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 (
- - - + + +
); } }); -module.exports = ContainerDetail; +module.exports = ContainerDetails; diff --git a/src/components/ContainerDetailsHeader.react.js b/src/components/ContainerDetailsHeader.react.js index 016bb01287..abb2009645 100644 --- a/src/components/ContainerDetailsHeader.react.js +++ b/src/components/ContainerDetailsHeader.react.js @@ -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 = RUNNING; } else if (this.props.container.State.Restarting) { state = RESTARTING; } else if (this.props.container.State.Paused) { state = PAUSED; + } else if (this.props.container.State.Starting) { + state = STARTING; } else if (this.props.container.State.Downloading) { state = DOWNLOADING; } else { diff --git a/src/components/ContainerDetailsSubheader.react.js b/src/components/ContainerDetailsSubheader.react.js index 556f9bc3b2..1466c8a276 100644 --- a/src/components/ContainerDetailsSubheader.react.js +++ b/src/components/ContainerDetailsSubheader.react.js @@ -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; diff --git a/src/components/ContainerHome.react.js b/src/components/ContainerHome.react.js index d760bc5bd4..5cbf3c0a1e 100644 --- a/src/components/ContainerHome.react.js +++ b/src/components/ContainerHome.react.js @@ -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 = (
@@ -83,12 +48,12 @@ var ContainerHome = React.createClass({
); } 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 = (

Downloading Image

- +
); } else { @@ -100,7 +65,7 @@ var ContainerHome = React.createClass({ ); } - } else if (this.state.blocked) { + } else if (this.props.container.State.Waiting) { body = (

Waiting For Another Download

@@ -116,12 +81,12 @@ var ContainerHome = React.createClass({ ); } } else { - if (this.state.defaultPort) { + if (this.props.defaultPort) { body = (
- +
@@ -132,7 +97,7 @@ var ContainerHome = React.createClass({ ); } else { var right; - if (_.keys(this.state.ports) > 0) { + if (_.keys(this.props.ports) > 0) { right = (
diff --git a/src/components/ContainerHomeFolders.react.js b/src/components/ContainerHomeFolders.react.js index db5095f23e..4f769ce766 100644 --- a/src/components/ContainerHomeFolders.react.js +++ b/src/components/ContainerHomeFolders.react.js @@ -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 { diff --git a/src/components/ContainerHomePreview.react.js b/src/components/ContainerHomePreview.react.js index 1d1ac925a8..116b6d5b89 100644 --- a/src/components/ContainerHomePreview.react.js +++ b/src/components/ContainerHomePreview.react.js @@ -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 = (

Web Preview

@@ -83,7 +60,7 @@ var ContainerHomePreview = React.createClass({
); } 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 ( diff --git a/src/components/ContainerList.react.js b/src/components/ContainerList.react.js index 1e6f358d6d..7ff8504b3f 100644 --- a/src/components/ContainerList.react.js +++ b/src/components/ContainerList.react.js @@ -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 ( - + ); }); return ( diff --git a/src/components/ContainerListItem.react.js b/src/components/ContainerListItem.react.js index 827a1abb9c..b8c07387af 100644 --- a/src/components/ContainerListItem.react.js +++ b/src/components/ContainerListItem.react.js @@ -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 ( - +
  • {state}
    diff --git a/src/components/ContainerSettings.react.js b/src/components/ContainerSettings.react.js index 7a25dd73ae..856b1fccc9 100644 --- a/src/components/ContainerSettings.react.js +++ b/src/components/ContainerSettings.react.js @@ -45,7 +45,7 @@ var ContainerSettings = React.createClass({
    - +
  • ); diff --git a/src/components/ContainerSettingsGeneral.react.js b/src/components/ContainerSettingsGeneral.react.js index 4fad44b149..11798c4b3c 100644 --- a/src/components/ContainerSettingsGeneral.react.js +++ b/src/components/ContainerSettingsGeneral.react.js @@ -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 { - 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 (
    ); @@ -226,22 +159,12 @@ var ContainerSettingsGeneral = React.createClass({ {btnSaveName}
    ); - var self = this; - var envVars = _.map(this.state.env, function (val, key) { + var pendingEnvVars = _.map(this.state.pendingEnv, (val, key) => { return (
    - -
    - ); - }); - var pendingEnvVars = _.map(this.state.pendingEnv, function (val, key) { - return ( -
    - - - +
    ); }); @@ -255,7 +178,6 @@ var ContainerSettingsGeneral = React.createClass({
    VALUE
    - {envVars} {pendingEnvVars}
    diff --git a/src/components/ContainerSettingsPorts.react.js b/src/components/ContainerSettingsPorts.react.js index 1992763a4f..c266384d94 100644 --- a/src/components/ContainerSettingsPorts.react.js +++ b/src/components/ContainerSettingsPorts.react.js @@ -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) { diff --git a/src/components/Containers.react.js b/src/components/Containers.react.js index 25e603c5f1..9b6c525b80 100644 --- a/src/components/Containers.react.js +++ b/src/components/Containers.react.js @@ -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({
    - +
    {this.state.currentButtonLabel}
    @@ -179,7 +192,7 @@ var Containers = React.createClass({
    - +
    ); diff --git a/src/components/ImageCard.react.js b/src/components/ImageCard.react.js index 6be5466a3c..a139a2f47d 100644 --- a/src/components/ImageCard.react.js +++ b/src/components/ImageCard.react.js @@ -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'); diff --git a/src/menutemplate.js b/src/menutemplate.js index 6f3d3fa1fd..f673f8d265 100644 --- a/src/menutemplate.js +++ b/src/menutemplate.js @@ -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' diff --git a/src/routes.js b/src/routes.js index a65d0e8395..63bdda0f0b 100644 --- a/src/routes.js +++ b/src/routes.js @@ -29,8 +29,8 @@ var App = React.createClass({ var routes = ( - - + + @@ -43,9 +43,9 @@ var routes = ( - + ); diff --git a/src/stores/ContainerStore.js b/src/stores/ContainerStore.js index 552858aa9b..8bdb4f632f 100644 --- a/src/stores/ContainerStore.js +++ b/src/stores/ContainerStore.js @@ -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); diff --git a/src/stores/LogStore.js b/src/stores/LogStore.js index 7a886ee497..5f81580d78 100644 --- a/src/stores/LogStore.js +++ b/src/stores/LogStore.js @@ -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) { diff --git a/src/utils/ContainerUtil.js b/src/utils/ContainerUtil.js index 2f895ad4c1..2ddfdafcb4 100644 --- a/src/utils/ContainerUtil.js +++ b/src/utils/ContainerUtil.js @@ -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; diff --git a/src/utils/DockerUtil.js b/src/utils/DockerUtil.js index 8c7d3c2404..99e925687d 100644 --- a/src/utils/DockerUtil.js +++ b/src/utils/DockerUtil.js @@ -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(); + } + }); + }); + } +}; diff --git a/styles/left-panel.less b/styles/left-panel.less index 2aa98270fc..769fe64b4b 100644 --- a/styles/left-panel.less +++ b/styles/left-panel.less @@ -1,7 +1,6 @@ /* Sidebar */ .sidebar { - .fade-in(); padding-top: 28px; background-color: white; margin: 0; diff --git a/styles/right-panel.less b/styles/right-panel.less index 167d0c66b4..a72f14a9d0 100644 --- a/styles/right-panel.less +++ b/styles/right-panel.less @@ -1,5 +1,4 @@ .details { - .fade-in(); background-color: @color-background; margin: 0; padding: 0; diff --git a/styles/setup.less b/styles/setup.less index d09abdfd4f..af842c24e0 100644 --- a/styles/setup.less +++ b/styles/setup.less @@ -1,5 +1,4 @@ .setup { - .fade-in(); display: flex; height: 100%; width: 100%; From 0e5ce4cc2e105300ca2452feda7daea980ff2e34 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Sun, 10 May 2015 16:42:57 -0700 Subject: [PATCH 2/5] Last flux changes for containers --- src/actions/ContainerActions.js | 9 +- src/actions/ContainerServerActions.js | 1 + src/components/ContainerHome.react.js | 4 +- src/components/ContainerHomePreview.react.js | 1 + src/components/ContainerListNewItem.react.js | 27 ++-- .../ContainerSettingsGeneral.react.js | 116 +++++++++++------- .../ContainerSettingsVolumes.react.js | 17 +-- src/components/Containers.react.js | 9 +- src/components/ImageCard.react.js | 7 +- src/components/NewContainerPull.react.js | 18 +-- src/stores/ContainerStore.js | 14 ++- src/utils/ContainerUtil.js | 6 +- src/utils/URLUtil.js | 4 +- 13 files changed, 139 insertions(+), 94 deletions(-) diff --git a/src/actions/ContainerActions.js b/src/actions/ContainerActions.js index cb97435e2b..07f75167e9 100644 --- a/src/actions/ContainerActions.js +++ b/src/actions/ContainerActions.js @@ -24,10 +24,17 @@ class ContainerServerActions { } update (name, container) { - console.log(container); this.dispatch({container}); dockerUtil.updateContainer(name, container); } + + clearPending () { + this.dispatch(); + } + + run (name, repo, tag) { + dockerUtil.run(name, repo, tag); + } } export default alt.createActions(ContainerServerActions); diff --git a/src/actions/ContainerServerActions.js b/src/actions/ContainerServerActions.js index e1a88e0e71..3bc0d29ce6 100644 --- a/src/actions/ContainerServerActions.js +++ b/src/actions/ContainerServerActions.js @@ -10,6 +10,7 @@ class ContainerServerActions { 'muted', 'unmuted', 'progress', + 'pending', 'updated', 'waiting' ); diff --git a/src/components/ContainerHome.react.js b/src/components/ContainerHome.react.js index 5cbf3c0a1e..45ffec576b 100644 --- a/src/components/ContainerHome.react.js +++ b/src/components/ContainerHome.react.js @@ -5,8 +5,6 @@ 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 ContainerHome = React.createClass({ @@ -100,7 +98,7 @@ var ContainerHome = React.createClass({ if (_.keys(this.props.ports) > 0) { right = (
    - +
    ); diff --git a/src/components/ContainerHomePreview.react.js b/src/components/ContainerHomePreview.react.js index 116b6d5b89..69bba09568 100644 --- a/src/components/ContainerHomePreview.react.js +++ b/src/components/ContainerHomePreview.react.js @@ -70,6 +70,7 @@ var ContainerHomePreview = React.createClass({ ); }); + preview = (

    IP & Ports

    diff --git a/src/components/ContainerListNewItem.react.js b/src/components/ContainerListNewItem.react.js index ab64e5792e..237e620c4a 100644 --- a/src/components/ContainerListNewItem.react.js +++ b/src/components/ContainerListNewItem.react.js @@ -1,13 +1,10 @@ var $ = require('jquery'); -var React = require('react/addons'); +var React = require('react'); var Router = require('react-router'); -var ContainerStore = require('../stores/ContainerStore'); var metrics = require('../utils/MetricsUtil'); var ContainerListNewItem = React.createClass({ - contextTypes: { - router: React.PropTypes.func - }, + mixins: [Router.Navigation, Router.State], handleItemMouseEnter: function () { var $action = $(this.getDOMNode()).find('.action'); $action.show(); @@ -16,22 +13,20 @@ var ContainerListNewItem = React.createClass({ var $action = $(this.getDOMNode()).find('.action'); $action.hide(); }, - handleDelete: function () { - var self = this; + handleDelete: function (event) { metrics.track('Deleted Container', { from: 'list', type: 'new' }); - var containers = ContainerStore.sorted(); - $(self.getDOMNode()).fadeOut(300, () => { - if (containers.length > 0) { - var name = containers[0].Name; - this.context.router.transitionTo('containerHome', {name: name}); - } - }); + + if (this.props.containers.length > 0 && this.getRoutes()[this.getRoutes().length - 2].name === 'new') { + var name = this.props.containers[0].Name; + this.transitionTo('containerHome', {name}); + } + $(this.getDOMNode()).fadeOut(300); + event.preventDefault(); }, render: function () { - var self = this; var action; if (this.props.containers.length > 0) { action = ( @@ -42,7 +37,7 @@ var ContainerListNewItem = React.createClass({ } return ( -
  • +
  • diff --git a/src/components/ContainerSettingsGeneral.react.js b/src/components/ContainerSettingsGeneral.react.js index 11798c4b3c..f2a910e0ea 100644 --- a/src/components/ContainerSettingsGeneral.react.js +++ b/src/components/ContainerSettingsGeneral.react.js @@ -1,5 +1,4 @@ var _ = require('underscore'); -var $ = require('jquery'); var React = require('react/addons'); var remote = require('remote'); var metrics = require('../utils/MetricsUtil'); @@ -8,22 +7,31 @@ var ContainerUtil = require('../utils/ContainerUtil'); var containerActions = require('../actions/ContainerActions'); var ContainerSettingsGeneral = React.createClass({ + mixins: [React.addons.LinkedStateMixin], + contextTypes: { router: React.PropTypes.func }, getInitialState: function () { + let env = ContainerUtil.env(this.props.container) || []; + env.push(['', '']); + console.log(env); return { slugName: null, nameError: null, - pendingEnv: ContainerUtil.env(this.props.container) || {} + env: env }; }, - willReceiveProps: function () { - this.setState({ - pendingEnv: ContainerUtil.env(this.props.container) || {} - }); + shouldComponentUpdate: function (nextProps, nextState) { + if (nextState.slugName !== this.state.slugName || nextState.nameError !== this.state.nameError) { + return true; + } + if (nextState.env.length === this.state.env.length) { + return false; + } + return true; }, handleNameChange: function (e) { @@ -78,39 +86,54 @@ var ContainerSettingsGeneral = React.createClass({ metrics.track('Changed Container Name'); }, - handleSaveEnvVar: function () { - var $rows = $('.env-vars .keyval-row'); - var envVarList = []; - $rows.each(function () { - var key = $(this).find('.key').val(); - var val = $(this).find('.val').val(); - if (!key.length || !val.length) { - return; - } - envVarList.push(key + '=' + val); - }); + handleSaveEnvVars: function () { metrics.track('Saved Environment Variables'); - containerActions.update(this.props.container.Name, {Env: envVarList}); + let list = []; + _.each(this.state.env, kvp => { + let [key, value] = kvp; + if ((key && key.length) || (value && value.length)) { + list.push(key + '=' + value); + } + }); + containerActions.update(this.props.container.Name, {Env: list}); }, - handleAddPendingEnvVar: function () { - var newKey = $('#new-env-key').val(); - var newVal = $('#new-env-val').val(); - var newEnv = {}; - newEnv[newKey] = newVal; + handleChangeEnvKey: function (index, event) { + let env = _.map(this.state.env, _.clone); + env[index][0] = event.target.value; this.setState({ - pendingEnv: _.extend(this.state.pendingEnv, newEnv) + env: env + }); + }, + + handleChangeEnvVal: function (index, event) { + let env = _.map(this.state.env, _.clone); + env[index][1] = event.target.value; + this.setState({ + env: env + }); + }, + + handleAddEnvVar: function () { + let env = _.map(this.state.env, _.clone); + env.push(['', '']); + this.setState({ + env: env }); - $('#new-env-key').val(''); - $('#new-env-val').val(''); metrics.track('Added Pending Environment Variable'); }, - handleRemovePendingEnvVar: function (key) { - var newEnv = _.omit(this.state.env, key); + handleRemoveEnvVar: function (index) { + let env = _.map(this.state.env, _.clone); + env.splice(index, 1); + if (env.length === 0) { + env.push(['', '']); + } + this.setState({ - env: newEnv + env: env }); + metrics.track('Removed Environment Variable'); }, @@ -131,8 +154,9 @@ var ContainerSettingsGeneral = React.createClass({ render: function () { if (!this.props.container) { - return (
    ); + return false; } + var willBeRenamedAs; var btnSaveName = ( Save @@ -149,7 +173,8 @@ var ContainerSettingsGeneral = React.createClass({

    {this.state.nameError}

    ); } - var rename = ( + + let rename = (

    Container Name

    @@ -159,15 +184,25 @@ var ContainerSettingsGeneral = React.createClass({ {btnSaveName}
    ); - var pendingEnvVars = _.map(this.state.pendingEnv, (val, key) => { + + let vars = _.map(this.state.env, (kvp, index) => { + let [key, val] = kvp; + let icon; + if (index === this.state.env.length - 1) { + icon = ; + } else { + icon = ; + } + return ( -
    - - - +
    { if (!filenames) { return; } @@ -21,11 +21,8 @@ var ContainerSettingsVolumes = React.createClass({ var binds = _.pairs(volumes).map(function (pair) { return pair[1] + ':' + pair[0]; }); - ContainerStore.updateContainer(self.props.container.Name, { - Binds: binds - }, function (err) { - if (err) { console.log(err); } - }); + + containerActions.update(this.props.container.Name, {Binds: binds}); } }); }, @@ -38,11 +35,7 @@ var ContainerSettingsVolumes = React.createClass({ var binds = _.pairs(volumes).map(function (pair) { return pair[1] + ':' + pair[0]; }); - ContainerStore.updateContainer(this.props.container.Name, { - Binds: binds - }, function (err) { - if (err) { console.log(err); } - }); + containerActions.update(this.props.container.Name, {Binds: binds}); }, handleOpenVolumeClick: function (path) { metrics.track('Opened Volume Directory', { diff --git a/src/components/Containers.react.js b/src/components/Containers.react.js index 9b6c525b80..a5829d82ce 100644 --- a/src/components/Containers.react.js +++ b/src/components/Containers.react.js @@ -1,6 +1,6 @@ var $ = require('jquery'); var _ = require('underscore'); -var React = require('react/addons'); +var React = require('react'); var Router = require('react-router'); var containerStore = require('../stores/ContainerStore'); var ContainerList = require('./ContainerList.react'); @@ -62,7 +62,9 @@ var Containers = React.createClass({ }); let name = this.context.router.getCurrentParams().name; - if (name && !containers[name]) { + if (containerStore.getState().pending) { + this.context.router.transitionTo('pull'); + } else if (name && !containers[name]) { if (sorted.length) { this.context.router.transitionTo('containerHome', {name: sorted[0].Name}); } else { @@ -72,7 +74,8 @@ var Containers = React.createClass({ this.setState({ containers: containers, - sorted: sorted + sorted: sorted, + pending: containerStore.getState().pending }); }, diff --git a/src/components/ImageCard.react.js b/src/components/ImageCard.react.js index a139a2f47d..67a0a0a229 100644 --- a/src/components/ImageCard.react.js +++ b/src/components/ImageCard.react.js @@ -1,14 +1,16 @@ var $ = require('jquery'); var React = require('react/addons'); +var Router = require('react-router'); var RetinaImage = require('react-retina-image'); 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 containerActions = require('../actions/ContainerActions'); var containerStore = require('../stores/ContainerStore'); var ImageCard = React.createClass({ + mixins: [Router.Navigation], getInitialState: function () { return { tags: [], @@ -28,7 +30,8 @@ var ImageCard = React.createClass({ from: 'search' }); let name = containerStore.generateName(repository); - dockerUtil.run(name, repository, this.state.chosenTag); + containerActions.run(name, repository, this.state.chosenTag); + this.transitionTo('containerHome', {name}); }, handleTagOverlayClick: function (name) { var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); diff --git a/src/components/NewContainerPull.react.js b/src/components/NewContainerPull.react.js index 3327b399f5..4a9150a962 100644 --- a/src/components/NewContainerPull.react.js +++ b/src/components/NewContainerPull.react.js @@ -1,30 +1,32 @@ var React = require('react/addons'); var Router = require('react-router'); var shell = require('shell'); -var ContainerStore = require('../stores/ContainerStore'); +var containerActions = require('../actions/ContainerActions'); +var containerStore = require('../stores/ContainerStore'); var metrics = require('../utils/MetricsUtil'); module.exports = React.createClass({ mixins: [Router.Navigation], handleOpenClick: function () { - var repo = this.props.pending.repository; + var repo = this.props.pending.repo; if (repo.indexOf('/') === -1) { - shell.openExternal(`https://registry.hub.docker.com/_/${this.props.pending.repository}`); + shell.openExternal(`https://registry.hub.docker.com/_/${this.props.pending.repo}`); } else { - shell.openExternal(`https://registry.hub.docker.com/u/${this.props.pending.repository}`); + shell.openExternal(`https://registry.hub.docker.com/u/${this.props.pending.repo}`); } }, handleCancelClick: function () { metrics.track('Canceled Click-To-Pull'); - ContainerStore.clearPending(); + containerActions.clearPending(); this.context.router.transitionTo('new'); }, handleConfirmClick: function () { metrics.track('Created Container', { from: 'click-to-pull' }); - ContainerStore.clearPending(); - ContainerStore.create(this.props.pending.repository, this.props.pending.tag, function () {}); + containerActions.clearPending(); + let name = containerStore.generateName(this.props.pending.repo); + containerActions.run(name, this.props.pending.repo, this.props.pending.tag); }, render: function () { if (!this.props.pending) { @@ -34,7 +36,7 @@ module.exports = React.createClass({
    -

    You're about to download and run {this.props.pending.repository}:{this.props.pending.tag}.

    +

    You're about to download and run {this.props.pending.repo}:{this.props.pending.tag}.

    Please confirm to create the container.

    Cancel Confirm diff --git a/src/stores/ContainerStore.js b/src/stores/ContainerStore.js index 8bdb4f632f..ddb8a9d676 100644 --- a/src/stores/ContainerStore.js +++ b/src/stores/ContainerStore.js @@ -11,6 +11,9 @@ class ContainerStore { // Blacklist of containers to avoid updating this.muted = {}; + + // Pending container to create + this.pending = null; } start ({name}) { @@ -106,7 +109,7 @@ class ContainerStore { this.setState({containers}); } - error ({ name, error }) { + error ({name, error}) { let containers = this.containers; if (containers[name]) { containers[name].Error = error; @@ -114,6 +117,15 @@ class ContainerStore { this.setState({containers}); } + pending ({repo, tag}) { + let pending = {repo, tag}; + this.setState({pending}); + } + + clearPending () { + this.setState({pending: null}); + } + static generateName (repo) { let base = _.last(repo.split('/')); let count = 1; diff --git a/src/utils/ContainerUtil.js b/src/utils/ContainerUtil.js index 2ddfdafcb4..4d6016ce6e 100644 --- a/src/utils/ContainerUtil.js +++ b/src/utils/ContainerUtil.js @@ -4,13 +4,13 @@ var docker = require('../utils/DockerUtil'); var ContainerUtil = { env: function (container) { if (!container || !container.Config || !container.Config.Env) { - return {}; + return []; } - return _.object(container.Config.Env.map(function (env) { + return _.map(container.Config.Env, env => { var i = env.indexOf('='); var splits = [env.slice(0, i), env.slice(i + 1)]; return splits; - })); + }); }, // TODO: inject host here instead of requiring Docker diff --git a/src/utils/URLUtil.js b/src/utils/URLUtil.js index 9f195ffdb4..2b4e8a8933 100644 --- a/src/utils/URLUtil.js +++ b/src/utils/URLUtil.js @@ -1,6 +1,6 @@ var util = require('./Util'); var parseUri = require('parseUri'); -var containerStore = require('../stores/ContainerStore'); +var containerServerActions = require('../actions/ContainerServerActions'); module.exports = { TYPE_WHITELIST: ['repository'], @@ -52,7 +52,7 @@ module.exports = { } if (type === 'repository' && method === 'run') { - containerStore.setPending(repo, 'latest'); + containerServerActions.pending({repo, tag: 'latest'}); return true; } return false; From b41ba54738f156e1d785503dd129b666c3a83861 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Sun, 10 May 2015 16:50:22 -0700 Subject: [PATCH 3/5] Fix click to pull routing --- src/components/NewContainerPull.react.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/NewContainerPull.react.js b/src/components/NewContainerPull.react.js index 4a9150a962..24dbc63a13 100644 --- a/src/components/NewContainerPull.react.js +++ b/src/components/NewContainerPull.react.js @@ -18,7 +18,7 @@ module.exports = React.createClass({ handleCancelClick: function () { metrics.track('Canceled Click-To-Pull'); containerActions.clearPending(); - this.context.router.transitionTo('new'); + this.transitionTo('new'); }, handleConfirmClick: function () { metrics.track('Created Container', { @@ -27,6 +27,7 @@ module.exports = React.createClass({ containerActions.clearPending(); let name = containerStore.generateName(this.props.pending.repo); containerActions.run(name, this.props.pending.repo, this.props.pending.tag); + this.transitionTo('containerHome', {name}); }, render: function () { if (!this.props.pending) { From 239c54e2ad14020c13f2f9a4a2d1e95ece4fbca8 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Sun, 10 May 2015 18:09:44 -0700 Subject: [PATCH 4/5] Fixing test --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index d980b64020..fcc00df32d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "^((?!-test).)*$" ], "unmockedModulePathPatterns": [ + "alt", "stream", "tty", "net", From 32f129c5bdcad37e6803f144f69f47efcf2b031a Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Mon, 11 May 2015 17:16:50 -0700 Subject: [PATCH 5/5] Error handling, updating state --- package.json | 3 + src/actions/ContainerActions.js | 7 +- src/actions/ContainerServerActions.js | 5 +- src/components/ContainerDetails.react.js | 7 +- .../ContainerDetailsHeader.react.js | 4 +- .../ContainerDetailsSubheader.react.js | 10 +-- src/components/ContainerHome.react.js | 8 +- src/components/ContainerHomeLogs.react.js | 8 ++ .../ContainerSettingsGeneral.react.js | 9 +- .../ContainerSettingsVolumes.react.js | 20 ++--- src/components/Containers.react.js | 2 +- src/stores/ContainerStore.js | 84 +++++++++++-------- src/utils/DockerUtil.js | 12 +-- 13 files changed, 104 insertions(+), 75 deletions(-) diff --git a/package.json b/package.json index fcc00df32d..2fc733d444 100644 --- a/package.json +++ b/package.json @@ -65,12 +65,15 @@ "bugsnag-js": "^2.4.7", "classnames": "^1.2.0", "coveralls": "^2.11.2", + "deep-extend": "^0.4.0", "dockerode": "^2.1.1", "exec": "0.2.0", + "install": "^0.1.8", "jquery": "^2.1.3", "mixpanel": "0.2.0", "mkdirp": "^0.5.0", "node-uuid": "^1.4.3", + "npm": "^2.9.1", "object-assign": "^2.0.0", "parseUri": "^1.2.3-2", "react": "^0.13.1", diff --git a/src/actions/ContainerActions.js b/src/actions/ContainerActions.js index 07f75167e9..e5fd4e1dae 100644 --- a/src/actions/ContainerActions.js +++ b/src/actions/ContainerActions.js @@ -1,7 +1,7 @@ import alt from '../alt'; import dockerUtil from '../utils/DockerUtil'; -class ContainerServerActions { +class ContainerActions { start (name) { this.dispatch({name}); dockerUtil.start(name); @@ -12,7 +12,6 @@ class ContainerServerActions { dockerUtil.destroy(name); } - // TODO: don't require all container data for this method rename (name, newName) { this.dispatch({name, newName}); dockerUtil.rename(name, newName); @@ -24,7 +23,7 @@ class ContainerServerActions { } update (name, container) { - this.dispatch({container}); + this.dispatch({name, container}); dockerUtil.updateContainer(name, container); } @@ -37,4 +36,4 @@ class ContainerServerActions { } } -export default alt.createActions(ContainerServerActions); +export default alt.createActions(ContainerActions); diff --git a/src/actions/ContainerServerActions.js b/src/actions/ContainerServerActions.js index 3bc0d29ce6..bbbf9c82ff 100644 --- a/src/actions/ContainerServerActions.js +++ b/src/actions/ContainerServerActions.js @@ -8,9 +8,10 @@ class ContainerServerActions { 'destroyed', 'error', 'muted', - 'unmuted', - 'progress', 'pending', + 'progress', + 'started', + 'unmuted', 'updated', 'waiting' ); diff --git a/src/components/ContainerDetails.react.js b/src/components/ContainerDetails.react.js index 58cf2fb628..793de1cbd2 100644 --- a/src/components/ContainerDetails.react.js +++ b/src/components/ContainerDetails.react.js @@ -12,14 +12,19 @@ var ContainerDetails = React.createClass({ }, render: function () { + if (!this.props.container) { + return false; + } + let ports = containerUtil.ports(this.props.container); let defaultPort = _.find(_.keys(ports), port => { return util.webPorts.indexOf(port) !== -1; }); + return (
    - +
    ); diff --git a/src/components/ContainerDetailsHeader.react.js b/src/components/ContainerDetailsHeader.react.js index abb2009645..adbf2d2326 100644 --- a/src/components/ContainerDetailsHeader.react.js +++ b/src/components/ContainerDetailsHeader.react.js @@ -7,7 +7,9 @@ var ContainerDetailsHeader = React.createClass({ return false; } - if (this.props.container.State.Running && !this.props.container.State.Paused && !this.props.container.State.ExitCode && !this.props.container.State.Restarting) { + if (this.props.container.State.Updating) { + state = UPDATING; + } else if (this.props.container.State.Running && !this.props.container.State.Paused && !this.props.container.State.ExitCode && !this.props.container.State.Restarting) { state = RUNNING; } else if (this.props.container.State.Restarting) { state = RESTARTING; diff --git a/src/components/ContainerDetailsSubheader.react.js b/src/components/ContainerDetailsSubheader.react.js index efce15965c..9b4cda575f 100644 --- a/src/components/ContainerDetailsSubheader.react.js +++ b/src/components/ContainerDetailsSubheader.react.js @@ -20,31 +20,31 @@ var ContainerDetailsSubheader = React.createClass({ if (!this.props.container) { return false; } - return (!this.props.container.State.Running || !this.props.defaultPort); + return (!this.props.container.State.Running || !this.props.defaultPort || this.props.container.State.Updating); }, disableRestart: function () { if (!this.props.container) { return false; } - return (this.props.container.State.Downloading || this.props.container.State.Restarting); + return (this.props.container.State.Downloading || this.props.container.State.Restarting || this.props.container.State.Updating); }, disableStop: function () { if (!this.props.container) { return false; } - return (this.props.container.State.Downloading || this.props.container.State.ExitCode || !this.props.container.State.Running); + return (this.props.container.State.Downloading || this.props.container.State.ExitCode || !this.props.container.State.Running || this.props.container.State.Updating); }, disableStart: function () { if (!this.props.container) { return false; } - return (this.props.container.State.Downloading || this.props.container.State.Running); + return (this.props.container.State.Downloading || this.props.container.State.Running || this.props.container.State.Updating); }, disableTerminal: function () { if (!this.props.container) { return false; } - return (!this.props.container.State.Running); + return (!this.props.container.State.Running || this.props.container.State.Updating); }, disableTab: function () { if (!this.props.container) { diff --git a/src/components/ContainerHome.react.js b/src/components/ContainerHome.react.js index 45ffec576b..08016322d1 100644 --- a/src/components/ContainerHome.react.js +++ b/src/components/ContainerHome.react.js @@ -35,12 +35,16 @@ var ContainerHome = React.createClass({ }, render: function () { + if (!this.props.container) { + return; + } + let body; - if (this.props.error) { + if (this.props.container.Error) { body = (

    An error occurred:

    -

    {this.props.error.statusCode} {this.props.error.reason} - {this.props.error.json}

    +

    {this.props.container.Error.message}

    If you feel that this error is invalid, please file a ticket on our GitHub repo.

    diff --git a/src/components/ContainerHomeLogs.react.js b/src/components/ContainerHomeLogs.react.js index 40d3fede58..f8d7ac4570 100644 --- a/src/components/ContainerHomeLogs.react.js +++ b/src/components/ContainerHomeLogs.react.js @@ -22,6 +22,14 @@ module.exports = React.createClass({ LogStore.on(LogStore.SERVER_LOGS_EVENT, this.update); LogStore.fetch(this.props.container.Name); }, + + componentWillReceiveProps: function (nextProps) { + if (this.props.container && nextProps.container && this.props.container.Name !== nextProps.container.Name) { + LogStore.detach(this.props.container.Name); + LogStore.fetch(nextProps.container.Name); + } + }, + componentWillUnmount: function() { if (!this.props.container) { return; diff --git a/src/components/ContainerSettingsGeneral.react.js b/src/components/ContainerSettingsGeneral.react.js index f2a910e0ea..f07439bfbb 100644 --- a/src/components/ContainerSettingsGeneral.react.js +++ b/src/components/ContainerSettingsGeneral.react.js @@ -16,7 +16,6 @@ var ContainerSettingsGeneral = React.createClass({ getInitialState: function () { let env = ContainerUtil.env(this.props.container) || []; env.push(['', '']); - console.log(env); return { slugName: null, nameError: null, @@ -28,9 +27,7 @@ var ContainerSettingsGeneral = React.createClass({ if (nextState.slugName !== this.state.slugName || nextState.nameError !== this.state.nameError) { return true; } - if (nextState.env.length === this.state.env.length) { - return false; - } + return true; }, @@ -195,7 +192,7 @@ var ContainerSettingsGeneral = React.createClass({ } return ( -
    ); + return false; } - var self = this; - var volumes = _.map(self.props.container.Volumes, function (val, key) { + + var volumes = _.map(this.props.container.Volumes, (val, key) => { if (!val || val.indexOf(process.env.HOME) === -1) { val = ( No Folder - Change - Remove + Change + Remove ); } else { val = ( - {val.replace(process.env.HOME, '~')} - Change - Remove + {val.replace(process.env.HOME, '~')} + Change + Remove ); } diff --git a/src/components/Containers.react.js b/src/components/Containers.react.js index a5829d82ce..03319d7d20 100644 --- a/src/components/Containers.react.js +++ b/src/components/Containers.react.js @@ -195,7 +195,7 @@ var Containers = React.createClass({
    - +
    ); diff --git a/src/stores/ContainerStore.js b/src/stores/ContainerStore.js index ddb8a9d676..d3b63099a0 100644 --- a/src/stores/ContainerStore.js +++ b/src/stores/ContainerStore.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import deepExtend from 'deep-extend'; import alt from '../alt'; import containerServerActions from '../actions/ContainerServerActions'; import containerActions from '../actions/ContainerActions'; @@ -9,13 +10,18 @@ class ContainerStore { this.bindActions(containerServerActions); this.containers = {}; - // Blacklist of containers to avoid updating - this.muted = {}; - // Pending container to create this.pending = null; } + error ({name, error}) { + let containers = this.containers; + if (containers[name]) { + containers[name].Error = error; + } + this.setState({containers}); + } + start ({name}) { let containers = this.containers; if (containers[name]) { @@ -24,6 +30,15 @@ class ContainerStore { } } + started ({name}) { + let containers = this.containers; + if (containers[name]) { + containers[name].State.Starting = false; + containers[name].State.Updating = false; + this.setState({containers}); + } + } + stop ({name}) { let containers = this.containers; if (containers[name]) { @@ -36,6 +51,11 @@ class ContainerStore { let containers = this.containers; let data = containers[name]; data.Name = newName; + + if (data.State) { + data.State.Updating = true; + } + containers[newName] = data; delete containers[name]; this.setState({containers}); @@ -44,23 +64,32 @@ class ContainerStore { added ({container}) { let containers = this.containers; containers[container.Name] = container; - delete this.muted[container.Name]; + this.setState({containers}); + } + + update ({name, container}) { + let containers = this.containers; + if (containers[name] && containers[name].State && containers[name].State.Updating) { + return; + } + + deepExtend(containers[name], container); + + if (containers[name].State) { + containers[name].State.Updating = true; + } + this.setState({containers}); } updated ({container}) { - if (this.muted[container.Name]) { + let containers = this.containers; + if (!containers[container.Name] || containers[container.Name].State.Updating) { 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}); } @@ -87,20 +116,17 @@ class ContainerStore { let container = _.find(_.values(this.containers), container => { return container.Id === name || container.Name === name; }); - if (container && !this.muted[container.Name]) { + + if (container && container.State && container.State.Updating) { + return; + } + + if (container) { delete containers[container.Name]; this.setState({containers}); } } - muted ({name}) { - this.muted[name] = true; - } - - unmuted ({name}) { - this.muted[name] = false; - } - waiting({name, waiting}) { let containers = this.containers; if (containers[name]) { @@ -109,14 +135,6 @@ class ContainerStore { this.setState({containers}); } - error ({name, error}) { - let containers = this.containers; - if (containers[name]) { - containers[name].Error = error; - } - this.setState({containers}); - } - pending ({repo, tag}) { let pending = {repo, tag}; this.setState({pending}); @@ -127,10 +145,10 @@ class ContainerStore { } static generateName (repo) { - let base = _.last(repo.split('/')); - let count = 1; - let name = base; - let names = _.keys(this.getState().containers); + const base = _.last(repo.split('/')); + const names = _.keys(this.getState().containers); + var count = 1; + var name = base; while (true) { if (names.indexOf(name) === -1) { return name; diff --git a/src/utils/DockerUtil.js b/src/utils/DockerUtil.js index 99e925687d..a2f107d619 100644 --- a/src/utils/DockerUtil.js +++ b/src/utils/DockerUtil.js @@ -79,7 +79,7 @@ export default { containerServerActions.error({name, error}); return; } - containerServerActions.unmuted({name}); + containerServerActions.started({name, error}); this.fetchContainer(name); }); }, @@ -184,9 +184,6 @@ export default { updateContainer (name, data) { let existing = this.client.getContainer(name); existing.inspect((error, existingData) => { - if (error) { - return; - } if (error) { containerServerActions.error({name, error}); return; @@ -202,7 +199,6 @@ export default { } var fullData = _.extend(existingData, data); - containerServerActions.muted({name}); this.createContainer(name, fullData); }); }, @@ -213,7 +209,6 @@ export default { containerServerActions.error({name, error}); return; } - this.fetchAllContainers(); var oldPath = path.join(util.home(), 'Kitematic', name); var newPath = path.join(util.home(), 'Kitematic', newName); @@ -223,7 +218,6 @@ export default { containerServerActions.error({newName, error}); } rimraf(newPath, () => { - console.log('removed'); if (fs.existsSync(oldPath)) { fs.renameSync(oldPath, newPath); } @@ -295,9 +289,7 @@ export default { containerServerActions.destroyed({id: name}); var volumePath = path.join(util.home(), 'Kitematic', name); if (fs.existsSync(volumePath)) { - rimraf(volumePath, function (err) { - console.log(err); - }); + rimraf(volumePath, () => {}); } }); });