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%;