Merge branch 'master' of github.com:kitematic/kitematic

This commit is contained in:
Jeffrey Morgan 2015-09-08 14:43:09 -07:00
commit a8a72a16c9
11 changed files with 161 additions and 50 deletions

View File

@ -1,5 +1,8 @@
root: true root: true
plugins:
- react
ecmaFeatures: ecmaFeatures:
modules: true modules: true
jsx: true jsx: true

View File

@ -65,7 +65,8 @@
"babel": "^5.1.10", "babel": "^5.1.10",
"babel-jest": "^5.2.0", "babel-jest": "^5.2.0",
"electron-prebuilt": "^0.27.3", "electron-prebuilt": "^0.27.3",
"eslint": "^1.0.0", "eslint": "^1.3.1",
"eslint-plugin-react": "^3.3.0",
"grunt": "^0.4.5", "grunt": "^0.4.5",
"grunt-babel": "^5.0.1", "grunt-babel": "^5.0.1",
"grunt-chmod": "^1.0.3", "grunt-chmod": "^1.0.3",

View File

@ -87,6 +87,16 @@ var ContainerDetailsSubheader = React.createClass({
containerActions.start(this.props.container.Name); containerActions.start(this.props.container.Name);
} }
}, },
handleDocs: function () {
let repoUri = 'https://hub.docker.com/r/';
let imageName = this.props.container.Config.Image.split(':')[0];
if (imageName.indexOf('/') === -1) {
repoUri = repoUri + 'library/' + imageName;
} else {
repoUri = repoUri + imageName;
}
shell.openExternal(repoUri);
},
handleTerminal: function () { handleTerminal: function () {
if (!this.disableTerminal()) { if (!this.disableTerminal()) {
metrics.track('Terminaled Into Container'); metrics.track('Terminaled Into Container');
@ -119,6 +129,10 @@ var ContainerDetailsSubheader = React.createClass({
action: true, action: true,
disabled: this.disableTerminal() disabled: this.disableTerminal()
}); });
var docsActionClass = classNames({
action: true,
disabled: false
});
var currentRoutes = _.map(this.context.router.getCurrentRoutes(), r => r.name); var currentRoutes = _.map(this.context.router.getCurrentRoutes(), r => r.name);
var currentRoute = _.last(currentRoutes); var currentRoute = _.last(currentRoutes);
@ -149,6 +163,7 @@ var ContainerDetailsSubheader = React.createClass({
</div> </div>
); );
} }
return ( return (
<div className="details-subheader"> <div className="details-subheader">
<div className="details-header-actions"> <div className="details-header-actions">
@ -161,6 +176,10 @@ var ContainerDetailsSubheader = React.createClass({
<div className="action-icon" onClick={this.handleTerminal}><span className="icon icon-docker-exec"></span></div> <div className="action-icon" onClick={this.handleTerminal}><span className="icon icon-docker-exec"></span></div>
<div className="btn-label">EXEC</div> <div className="btn-label">EXEC</div>
</div> </div>
<div className={docsActionClass}>
<div className="action-icon" onClick={this.handleDocs}><span className="icon icon-open-external"></span></div>
<div className="btn-label">DOCS</div>
</div>
</div> </div>
<div className="details-subheader-tabs"> <div className="details-subheader-tabs">
<span className={tabHomeClasses} onClick={this.showHome}>Home</span> <span className={tabHomeClasses} onClick={this.showHome}>Home</span>

View File

@ -46,7 +46,7 @@ var ContainerHome = React.createClass({
render: function () { render: function () {
if (!this.props.container) { if (!this.props.container) {
return; return '';
} }
let body; let body;
@ -70,6 +70,9 @@ var ContainerHome = React.createClass({
} }
sum = sum / this.props.container.Progress.amount; sum = sum / this.props.container.Progress.amount;
if (isNaN(sum)) {
sum = 0;
}
body = ( body = (
<div className="details-progress"> <div className="details-progress">

View File

@ -2,6 +2,8 @@ import _ from 'underscore';
import React from 'react/addons'; import React from 'react/addons';
import shell from 'shell'; import shell from 'shell';
import ContainerUtil from '../utils/ContainerUtil'; import ContainerUtil from '../utils/ContainerUtil';
import containerActions from '../actions/ContainerActions';
import containerStore from '../stores/ContainerStore';
import metrics from '../utils/MetricsUtil'; import metrics from '../utils/MetricsUtil';
import {webPorts} from '../utils/Util'; import {webPorts} from '../utils/Util';
@ -11,50 +13,88 @@ var ContainerSettingsPorts = React.createClass({
}, },
getInitialState: function () { getInitialState: function () {
return { return {
ports: {}, ports: ContainerUtil.ports(this.props.container)
defaultPort: null
}; };
}, },
componentDidMount: function() {
if (!this.props.container) {
return;
}
var ports = ContainerUtil.ports(this.props.container);
this.setState({
ports: ports,
defaultPort: _.find(_.keys(ports), function (port) {
return webPorts.indexOf(port) !== -1;
})
});
},
handleViewLink: function (url) { handleViewLink: function (url) {
metrics.track('Opened In Browser', { metrics.track('Opened In Browser', {
from: 'settings' from: 'settings'
}); });
shell.openExternal(url); shell.openExternal(url);
}, },
handleChangeDefaultPort: function (port, e) { handleChangePort: function(key, e) {
if (e.target.checked) { var ports = this.state.ports;
this.setState({ var port = e.target.value;
defaultPort: port
// save updated port
ports[key] = _.extend(ports[key], {
url: 'http://' + ports[key]['ip'] + ':' + port,
port: port,
error: null
}); });
} else {
this.setState({ // basic validation, if number is integer, if its in range, if there
defaultPort: null // is no collision with ports of other containers and also if there is no
// collision with ports for current containser
const name = this.props.container.Name;
const containers = containerStore.getState().containers;
const container = ContainerUtil.isPortCollision(name, containers, port);
const duplicates = _.filter(ports, (v, i) => {
return (i != key && _.isEqual(v.port, port));
}); });
if (!port.match(/^[0-9]+$/g)) {
ports[key].error = 'Needs to be an integer.';
} }
else if (port <= 0 || port > 65535) {
ports[key].error = 'Needs to be in range <1,65535>.';
}
else if (container) {
ports[key].error = 'Collision with port at container "'+ container.Name +'"';
}
else if (duplicates.length > 0) {
ports[key].error = "Collision with other port at container.";
}
else if (port == 22 || port == 2376) {
ports[key].error = "Ports 22 and 2376 are reserved ports for Kitematic/Docker.";
}
this.setState({ ports: ports });
},
handleSave: function() {
containerActions.update(this.props.container.Name, {
NetworkSettings: {
Ports: _.reduce(this.state.ports, function(res, value, key) {
res[key + '/tcp'] = [{
HostIp: value.ip,
HostPort: value.port,
}];
return res;
}, {})
}
});
}, },
render: function () { render: function () {
if (!this.props.container) { if (!this.props.container) {
return false; return false;
} }
var isUpdating = (this.props.container.State.Updating);
var isValid = true;
var ports = _.map(_.pairs(this.state.ports), pair => { var ports = _.map(_.pairs(this.state.ports), pair => {
var key = pair[0]; var key = pair[0];
var val = pair[1]; var {ip, port, url, error} = pair[1];
isValid = (error) ? false : isValid;
let ipLink = (this.props.container.State.Running && !this.props.container.State.Paused && !this.props.container.State.ExitCode && !this.props.container.State.Restarting) ? (<a onClick={this.handleViewLink.bind(this, url)}>{ip}</a>):({ip});
return ( return (
<tr key={key}> <tr key={key}>
<td>{key}</td> <td>{key}</td>
<td><a onClick={this.handleViewLink.bind(this, val.url)}>{val.display}</a></td> <td className="bind">
{ipLink}:
<input
type="text"
disabled={isUpdating}
onChange={this.handleChangePort.bind(this, key)}
defaultValue={port} />
</td>
<td className="error">{error}</td>
</tr> </tr>
); );
}); });
@ -66,13 +106,19 @@ var ContainerSettingsPorts = React.createClass({
<thead> <thead>
<tr> <tr>
<th>DOCKER PORT</th> <th>DOCKER PORT</th>
<th>ACCESS URL</th> <th>MAC IP:PORT</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ports} {ports}
</tbody> </tbody>
</table> </table>
<a className="btn btn-action"
disabled={isUpdating || !isValid}
onClick={this.handleSave}>
Save
</a>
</div> </div>
</div> </div>
); );

View File

@ -28,21 +28,42 @@ var ContainerUtil = {
} }
var res = {}; var res = {};
var ip = docker.host; var ip = docker.host;
_.each(container.NetworkSettings.Ports, function (value, key) { var ports = (container.NetworkSettings.Ports) ? container.NetworkSettings.Ports : (container.HostConfig.PortBindings) ? container.HostConfig.PortBindings : container.Config.ExposedPorts;
_.each(ports, function (value, key) {
var dockerPort = key.split('/')[0]; var dockerPort = key.split('/')[0];
var localUrl = null; var localUrl = null;
var localUrlDisplay = null; var localUrlDisplay = null;
var port = null;
if (value && value.length) { if (value && value.length) {
var port = value[0].HostPort; var port = value[0].HostPort;
localUrl = 'http://' + ip + ':' + port; localUrl = 'http://' + ip + ':' + port;
localUrlDisplay = ip + ':' + port;
} }
res[dockerPort] = { res[dockerPort] = {
url: localUrl, url: localUrl,
display: localUrlDisplay ip: ip,
port: port
}; };
}); });
return res; return res;
},
/**
* Check if there is port colision with other containers
* @param {String} name name of the current container
* @param {Array} containers array of all containers
* @param {String} port
* @return {Object|null} return nothing or container with colision
*/
isPortCollision: function (name, containers, port) {
var interfaces = {};
_.forEach(containers, container => {
if (container.Name != name) {
_.forEach(this.ports(container), (ip) => {
interfaces[ip + ':' + port] = container;
});
}
});
return interfaces[docker.host + ':' + port];
} }
}; };

View File

@ -262,7 +262,7 @@ export default {
}, },
restart (name) { restart (name) {
this.client.getContainer(name).stop(stopError => { this.client.getContainer(name).stop({t: 10}, stopError => {
if (stopError && stopError.statusCode !== 304) { if (stopError && stopError.statusCode !== 304) {
containerServerActions.error({name, stopError}); containerServerActions.error({name, stopError});
return; return;
@ -278,7 +278,7 @@ export default {
}, },
stop (name) { stop (name) {
this.client.getContainer(name).stop(error => { this.client.getContainer(name).stop({t: 10}, error => {
if (error && error.statusCode !== 304) { if (error && error.statusCode !== 304) {
containerServerActions.error({name, error}); containerServerActions.error({name, error});
return; return;

View File

@ -130,11 +130,9 @@ module.exports = {
} }
if (v1parts[i] === v2parts[i]) { if (v1parts[i] === v2parts[i]) {
continue; continue;
} } else if (v1parts[i] > v2parts[i]) {
else if (v1parts[i] > v2parts[i]) {
return 1; return 1;
} } else {
else {
return -1; return -1;
} }
} }
@ -158,5 +156,5 @@ module.exports = {
linuxToWindowsPath: function (linuxAbsPath) { linuxToWindowsPath: function (linuxAbsPath) {
return linuxAbsPath.replace('/c', 'C:').split('/').join('\\'); return linuxAbsPath.replace('/c', 'C:').split('/').join('\\');
}, },
webPorts: ['80', '8000', '8080', '3000', '5000', '2368', '9200', '8983'] webPorts: ['80', '8000', '8080', '8888', '3000', '5000', '2368', '9200', '8983']
}; };

View File

@ -112,7 +112,27 @@
} }
.table { .table {
&.ports { &.ports {
max-width: 500px; input {
width: 50px;
border: 0;
}
tr {
td {
&:first-child {
width: 120px;
}
&.bind {
width: 190px;
}
&.error {
text-align: left;
color: red;
padding-left: 10px;
padding-right: 10px;
border: 0;
}
}
}
} }
&.volumes { &.volumes {
max-width: 100%; max-width: 100%;