Error handling, updating state

This commit is contained in:
Jeffrey Morgan 2015-05-11 17:16:50 -07:00
parent 239c54e2ad
commit 32f129c5bd
13 changed files with 104 additions and 75 deletions

View File

@ -65,12 +65,15 @@
"bugsnag-js": "^2.4.7", "bugsnag-js": "^2.4.7",
"classnames": "^1.2.0", "classnames": "^1.2.0",
"coveralls": "^2.11.2", "coveralls": "^2.11.2",
"deep-extend": "^0.4.0",
"dockerode": "^2.1.1", "dockerode": "^2.1.1",
"exec": "0.2.0", "exec": "0.2.0",
"install": "^0.1.8",
"jquery": "^2.1.3", "jquery": "^2.1.3",
"mixpanel": "0.2.0", "mixpanel": "0.2.0",
"mkdirp": "^0.5.0", "mkdirp": "^0.5.0",
"node-uuid": "^1.4.3", "node-uuid": "^1.4.3",
"npm": "^2.9.1",
"object-assign": "^2.0.0", "object-assign": "^2.0.0",
"parseUri": "^1.2.3-2", "parseUri": "^1.2.3-2",
"react": "^0.13.1", "react": "^0.13.1",

View File

@ -1,7 +1,7 @@
import alt from '../alt'; import alt from '../alt';
import dockerUtil from '../utils/DockerUtil'; import dockerUtil from '../utils/DockerUtil';
class ContainerServerActions { class ContainerActions {
start (name) { start (name) {
this.dispatch({name}); this.dispatch({name});
dockerUtil.start(name); dockerUtil.start(name);
@ -12,7 +12,6 @@ class ContainerServerActions {
dockerUtil.destroy(name); dockerUtil.destroy(name);
} }
// TODO: don't require all container data for this method
rename (name, newName) { rename (name, newName) {
this.dispatch({name, newName}); this.dispatch({name, newName});
dockerUtil.rename(name, newName); dockerUtil.rename(name, newName);
@ -24,7 +23,7 @@ class ContainerServerActions {
} }
update (name, container) { update (name, container) {
this.dispatch({container}); this.dispatch({name, container});
dockerUtil.updateContainer(name, container); dockerUtil.updateContainer(name, container);
} }
@ -37,4 +36,4 @@ class ContainerServerActions {
} }
} }
export default alt.createActions(ContainerServerActions); export default alt.createActions(ContainerActions);

View File

@ -8,9 +8,10 @@ class ContainerServerActions {
'destroyed', 'destroyed',
'error', 'error',
'muted', 'muted',
'unmuted',
'progress',
'pending', 'pending',
'progress',
'started',
'unmuted',
'updated', 'updated',
'waiting' 'waiting'
); );

View File

@ -12,14 +12,19 @@ var ContainerDetails = React.createClass({
}, },
render: function () { render: function () {
if (!this.props.container) {
return false;
}
let ports = containerUtil.ports(this.props.container); let ports = containerUtil.ports(this.props.container);
let defaultPort = _.find(_.keys(ports), port => { let defaultPort = _.find(_.keys(ports), port => {
return util.webPorts.indexOf(port) !== -1; return util.webPorts.indexOf(port) !== -1;
}); });
return ( return (
<div className="details"> <div className="details">
<ContainerDetailsHeader {...this.props} defaultPort={defaultPort} ports={ports}/> <ContainerDetailsHeader {...this.props} defaultPort={defaultPort} ports={ports}/>
<ContainerDetailsSubheader {...this.props}/> <ContainerDetailsSubheader {...this.props} defaultPort={defaultPort} ports={ports}/>
<Router.RouteHandler {...this.props} defaultPort={defaultPort} ports={ports}/> <Router.RouteHandler {...this.props} defaultPort={defaultPort} ports={ports}/>
</div> </div>
); );

View File

@ -7,7 +7,9 @@ var ContainerDetailsHeader = React.createClass({
return false; 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 = <span className="status downloading">UPDATING</span>;
} else if (this.props.container.State.Running && !this.props.container.State.Paused && !this.props.container.State.ExitCode && !this.props.container.State.Restarting) {
state = <span className="status running">RUNNING</span>; state = <span className="status running">RUNNING</span>;
} else if (this.props.container.State.Restarting) { } else if (this.props.container.State.Restarting) {
state = <span className="status restarting">RESTARTING</span>; state = <span className="status restarting">RESTARTING</span>;

View File

@ -20,31 +20,31 @@ var ContainerDetailsSubheader = React.createClass({
if (!this.props.container) { if (!this.props.container) {
return false; 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 () { disableRestart: function () {
if (!this.props.container) { if (!this.props.container) {
return false; 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 () { disableStop: function () {
if (!this.props.container) { if (!this.props.container) {
return false; 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 () { disableStart: function () {
if (!this.props.container) { if (!this.props.container) {
return false; 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 () { disableTerminal: function () {
if (!this.props.container) { if (!this.props.container) {
return false; return false;
} }
return (!this.props.container.State.Running); return (!this.props.container.State.Running || this.props.container.State.Updating);
}, },
disableTab: function () { disableTab: function () {
if (!this.props.container) { if (!this.props.container) {

View File

@ -35,12 +35,16 @@ var ContainerHome = React.createClass({
}, },
render: function () { render: function () {
if (!this.props.container) {
return;
}
let body; let body;
if (this.props.error) { if (this.props.container.Error) {
body = ( body = (
<div className="details-progress"> <div className="details-progress">
<h3>An error occurred:</h3> <h3>An error occurred:</h3>
<h2>{this.props.error.statusCode} {this.props.error.reason} - {this.props.error.json}</h2> <h2>{this.props.container.Error.message}</h2>
<h3>If you feel that this error is invalid, please <a onClick={this.handleErrorClick}>file a ticket on our GitHub repo.</a></h3> <h3>If you feel that this error is invalid, please <a onClick={this.handleErrorClick}>file a ticket on our GitHub repo.</a></h3>
<Radial progress={100} error={true} thick={true} transparent={true}/> <Radial progress={100} error={true} thick={true} transparent={true}/>
</div> </div>

View File

@ -22,6 +22,14 @@ module.exports = React.createClass({
LogStore.on(LogStore.SERVER_LOGS_EVENT, this.update); LogStore.on(LogStore.SERVER_LOGS_EVENT, this.update);
LogStore.fetch(this.props.container.Name); 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() { componentWillUnmount: function() {
if (!this.props.container) { if (!this.props.container) {
return; return;

View File

@ -16,7 +16,6 @@ var ContainerSettingsGeneral = React.createClass({
getInitialState: function () { getInitialState: function () {
let env = ContainerUtil.env(this.props.container) || []; let env = ContainerUtil.env(this.props.container) || [];
env.push(['', '']); env.push(['', '']);
console.log(env);
return { return {
slugName: null, slugName: null,
nameError: null, nameError: null,
@ -28,9 +27,7 @@ var ContainerSettingsGeneral = React.createClass({
if (nextState.slugName !== this.state.slugName || nextState.nameError !== this.state.nameError) { if (nextState.slugName !== this.state.slugName || nextState.nameError !== this.state.nameError) {
return true; return true;
} }
if (nextState.env.length === this.state.env.length) {
return false;
}
return true; return true;
}, },
@ -195,7 +192,7 @@ var ContainerSettingsGeneral = React.createClass({
} }
return ( return (
<div key={index + ':' + key + '=' + val} className="keyval-row"> <div className="keyval-row">
<input type="text" className="key line" defaultValue={key} onChange={this.handleChangeEnvKey.bind(this, index)}></input> <input type="text" className="key line" defaultValue={key} onChange={this.handleChangeEnvKey.bind(this, index)}></input>
<input type="text" className="val line" defaultValue={val} onChange={this.handleChangeEnvVal.bind(this, index)}></input> <input type="text" className="val line" defaultValue={val} onChange={this.handleChangeEnvVal.bind(this, index)}></input>
{icon} {icon}
@ -215,7 +212,7 @@ var ContainerSettingsGeneral = React.createClass({
<div className="env-vars"> <div className="env-vars">
{vars} {vars}
</div> </div>
<a className="btn btn-action" onClick={this.handleSaveEnvVars}>Save</a> <a className="btn btn-action" disabled={this.props.container.State.Updating} onClick={this.handleSaveEnvVars}>Save</a>
</div> </div>
<div className="settings-section"> <div className="settings-section">
<h3>Delete Container</h3> <h3>Delete Container</h3>

View File

@ -22,7 +22,7 @@ var ContainerSettingsVolumes = React.createClass({
return pair[1] + ':' + pair[0]; return pair[1] + ':' + pair[0];
}); });
containerActions.update(this.props.container.Name, {Binds: binds}); containerActions.update(this.props.container.Name, {Binds: binds, Volumes: volumes});
} }
}); });
}, },
@ -35,7 +35,7 @@ var ContainerSettingsVolumes = React.createClass({
var binds = _.pairs(volumes).map(function (pair) { var binds = _.pairs(volumes).map(function (pair) {
return pair[1] + ':' + pair[0]; return pair[1] + ':' + pair[0];
}); });
containerActions.update(this.props.container.Name, {Binds: binds}); containerActions.update(this.props.container.Name, {Binds: binds, Volumes: volumes});
}, },
handleOpenVolumeClick: function (path) { handleOpenVolumeClick: function (path) {
metrics.track('Opened Volume Directory', { metrics.track('Opened Volume Directory', {
@ -45,24 +45,24 @@ var ContainerSettingsVolumes = React.createClass({
}, },
render: function () { render: function () {
if (!this.props.container) { if (!this.props.container) {
return (<div></div>); 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) { if (!val || val.indexOf(process.env.HOME) === -1) {
val = ( val = (
<span> <span>
<a className="value-right">No Folder</a> <a className="value-right">No Folder</a>
<a className="btn btn-action small" onClick={self.handleChooseVolumeClick.bind(self, key)}>Change</a> <a className="btn btn-action small" disabled={this.props.container.State.Updating} onClick={this.handleChooseVolumeClick.bind(this, key)}>Change</a>
<a className="btn btn-action small" onClick={self.handleRemoveVolumeClick.bind(self, key)}>Remove</a> <a className="btn btn-action small" disabled={this.props.container.State.Updating} onClick={this.handleRemoveVolumeClick.bind(this, key)}>Remove</a>
</span> </span>
); );
} else { } else {
val = ( val = (
<span> <span>
<a className="value-right" onClick={self.handleOpenVolumeClick.bind(self, val)}>{val.replace(process.env.HOME, '~')}</a> <a className="value-right" onClick={this.handleOpenVolumeClick.bind(this, val)}>{val.replace(process.env.HOME, '~')}</a>
<a className="btn btn-action small" onClick={self.handleChooseVolumeClick.bind(self, key)}>Change</a> <a className="btn btn-action small" disabled={this.props.container.State.Updating} onClick={this.handleChooseVolumeClick.bind(this, key)}>Change</a>
<a className="btn btn-action small" onClick={self.handleRemoveVolumeClick.bind(self, key)}>Remove</a> <a className="btn btn-action small" disabled={this.props.container.State.Updating} onClick={this.handleRemoveVolumeClick.bind(this, key)}>Remove</a>
</span> </span>
); );
} }

View File

@ -195,7 +195,7 @@ var Containers = React.createClass({
<div className="sidebar-buttons-padding"></div> <div className="sidebar-buttons-padding"></div>
</section> </section>
</div> </div>
<Router.RouteHandler pending={this.state.pending} containers={this.state.containers} container={container} error={this.state.error}/> <Router.RouteHandler pending={this.state.pending} containers={this.state.containers} container={container}/>
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,5 @@
import _ from 'underscore'; import _ from 'underscore';
import deepExtend from 'deep-extend';
import alt from '../alt'; import alt from '../alt';
import containerServerActions from '../actions/ContainerServerActions'; import containerServerActions from '../actions/ContainerServerActions';
import containerActions from '../actions/ContainerActions'; import containerActions from '../actions/ContainerActions';
@ -9,13 +10,18 @@ class ContainerStore {
this.bindActions(containerServerActions); this.bindActions(containerServerActions);
this.containers = {}; this.containers = {};
// Blacklist of containers to avoid updating
this.muted = {};
// Pending container to create // Pending container to create
this.pending = null; this.pending = null;
} }
error ({name, error}) {
let containers = this.containers;
if (containers[name]) {
containers[name].Error = error;
}
this.setState({containers});
}
start ({name}) { start ({name}) {
let containers = this.containers; let containers = this.containers;
if (containers[name]) { 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}) { stop ({name}) {
let containers = this.containers; let containers = this.containers;
if (containers[name]) { if (containers[name]) {
@ -36,6 +51,11 @@ class ContainerStore {
let containers = this.containers; let containers = this.containers;
let data = containers[name]; let data = containers[name];
data.Name = newName; data.Name = newName;
if (data.State) {
data.State.Updating = true;
}
containers[newName] = data; containers[newName] = data;
delete containers[name]; delete containers[name];
this.setState({containers}); this.setState({containers});
@ -44,23 +64,32 @@ class ContainerStore {
added ({container}) { added ({container}) {
let containers = this.containers; let containers = this.containers;
containers[container.Name] = container; 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}); this.setState({containers});
} }
updated ({container}) { updated ({container}) {
if (this.muted[container.Name]) { let containers = this.containers;
if (!containers[container.Name] || containers[container.Name].State.Updating) {
return; return;
} }
let containers = this.containers;
if (!containers[container.Name]) {
return;
}
containers[container.Name] = container; containers[container.Name] = container;
if (container.State.Running) {
delete container.State.Starting;
}
this.setState({containers}); this.setState({containers});
} }
@ -87,20 +116,17 @@ class ContainerStore {
let container = _.find(_.values(this.containers), container => { let container = _.find(_.values(this.containers), container => {
return container.Id === name || container.Name === name; 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]; delete containers[container.Name];
this.setState({containers}); this.setState({containers});
} }
} }
muted ({name}) {
this.muted[name] = true;
}
unmuted ({name}) {
this.muted[name] = false;
}
waiting({name, waiting}) { waiting({name, waiting}) {
let containers = this.containers; let containers = this.containers;
if (containers[name]) { if (containers[name]) {
@ -109,14 +135,6 @@ class ContainerStore {
this.setState({containers}); this.setState({containers});
} }
error ({name, error}) {
let containers = this.containers;
if (containers[name]) {
containers[name].Error = error;
}
this.setState({containers});
}
pending ({repo, tag}) { pending ({repo, tag}) {
let pending = {repo, tag}; let pending = {repo, tag};
this.setState({pending}); this.setState({pending});
@ -127,10 +145,10 @@ class ContainerStore {
} }
static generateName (repo) { static generateName (repo) {
let base = _.last(repo.split('/')); const base = _.last(repo.split('/'));
let count = 1; const names = _.keys(this.getState().containers);
let name = base; var count = 1;
let names = _.keys(this.getState().containers); var name = base;
while (true) { while (true) {
if (names.indexOf(name) === -1) { if (names.indexOf(name) === -1) {
return name; return name;

View File

@ -79,7 +79,7 @@ export default {
containerServerActions.error({name, error}); containerServerActions.error({name, error});
return; return;
} }
containerServerActions.unmuted({name}); containerServerActions.started({name, error});
this.fetchContainer(name); this.fetchContainer(name);
}); });
}, },
@ -184,9 +184,6 @@ export default {
updateContainer (name, data) { updateContainer (name, data) {
let existing = this.client.getContainer(name); let existing = this.client.getContainer(name);
existing.inspect((error, existingData) => { existing.inspect((error, existingData) => {
if (error) {
return;
}
if (error) { if (error) {
containerServerActions.error({name, error}); containerServerActions.error({name, error});
return; return;
@ -202,7 +199,6 @@ export default {
} }
var fullData = _.extend(existingData, data); var fullData = _.extend(existingData, data);
containerServerActions.muted({name});
this.createContainer(name, fullData); this.createContainer(name, fullData);
}); });
}, },
@ -213,7 +209,6 @@ export default {
containerServerActions.error({name, error}); containerServerActions.error({name, error});
return; return;
} }
this.fetchAllContainers();
var oldPath = path.join(util.home(), 'Kitematic', name); var oldPath = path.join(util.home(), 'Kitematic', name);
var newPath = path.join(util.home(), 'Kitematic', newName); var newPath = path.join(util.home(), 'Kitematic', newName);
@ -223,7 +218,6 @@ export default {
containerServerActions.error({newName, error}); containerServerActions.error({newName, error});
} }
rimraf(newPath, () => { rimraf(newPath, () => {
console.log('removed');
if (fs.existsSync(oldPath)) { if (fs.existsSync(oldPath)) {
fs.renameSync(oldPath, newPath); fs.renameSync(oldPath, newPath);
} }
@ -295,9 +289,7 @@ export default {
containerServerActions.destroyed({id: name}); containerServerActions.destroyed({id: name});
var volumePath = path.join(util.home(), 'Kitematic', name); var volumePath = path.join(util.home(), 'Kitematic', name);
if (fs.existsSync(volumePath)) { if (fs.existsSync(volumePath)) {
rimraf(volumePath, function (err) { rimraf(volumePath, () => {});
console.log(err);
});
} }
}); });
}); });