Pull & Run containers
@@ -6,15 +6,33 @@ var NotFoundRoute = Router.NotFoundRoute;
|
||||
var DefaultRoute = Router.DefaultRoute;
|
||||
var Link = Router.Link;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
|
||||
var Convert = require('ansi-to-html');
|
||||
var convert = new Convert();
|
||||
|
||||
var ContainerStore = require('./ContainerStore.js');
|
||||
var docker = require('./docker.js');
|
||||
|
||||
var Container = React.createClass({
|
||||
var ContainerDetails = React.createClass({
|
||||
mixins: [Router.State],
|
||||
componentDidMount: function () {
|
||||
ContainerStore.addChangeListener(this.update);
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
ContainerStore.removeChangeListener(this.update);
|
||||
},
|
||||
update: function () {
|
||||
var containerId = this.getParams().Id;
|
||||
this.setState({
|
||||
container: ContainerStore.containers()[containerId]
|
||||
});
|
||||
},
|
||||
_escapeHTML: function (html) {
|
||||
var text = document.createTextNode(html);
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(text);
|
||||
return div.innerHTML;
|
||||
},
|
||||
componentWillReceiveProps: function () {
|
||||
this.update();
|
||||
var self = this;
|
||||
var logs = [];
|
||||
var index = 0;
|
||||
@@ -29,7 +47,7 @@ var Container = React.createClass({
|
||||
if (index % 2 === 1) {
|
||||
var time = buf.substr(0,buf.indexOf(' '));
|
||||
var msg = buf.substr(buf.indexOf(' ')+1);
|
||||
logs.push(convert.toHtml(msg));
|
||||
logs.push(convert.toHtml(self._escapeHTML(msg)));
|
||||
}
|
||||
index += 1;
|
||||
});
|
||||
@@ -47,7 +65,7 @@ var Container = React.createClass({
|
||||
if (index % 2 === 1) {
|
||||
var time = buf.substr(0,buf.indexOf(' '));
|
||||
var msg = buf.substr(buf.indexOf(' ')+1);
|
||||
logs.push(convert.toHtml(msg));
|
||||
logs.push(convert.toHtml(self._escapeHTML(msg)));
|
||||
self.setState({logs: logs});
|
||||
}
|
||||
index += 1;
|
||||
@@ -63,12 +81,9 @@ var Container = React.createClass({
|
||||
return false;
|
||||
}
|
||||
|
||||
var container = _.find(this.props.containers, function (container) {
|
||||
return container.Id === self.getParams().Id;
|
||||
});
|
||||
// console.log(container);
|
||||
|
||||
if (!container || !this.state) {
|
||||
if (!this.state) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
@@ -77,23 +92,25 @@ var Container = React.createClass({
|
||||
});
|
||||
|
||||
var state;
|
||||
if (container.State.Running) {
|
||||
if (this.state.container.State.Running) {
|
||||
state = <h2 className="status">running</h2>;
|
||||
} else if (container.State.Restarting) {
|
||||
} else if (this.state.container.State.Restarting) {
|
||||
state = <h2 className="status">restarting</h2>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="details">
|
||||
<div className="details-header">
|
||||
<h1>{container.Name.replace('/', '')}</h1>{state}
|
||||
<h1>{this.state.container.Name.replace('/', '')}</h1>
|
||||
</div>
|
||||
<div className="logs">
|
||||
{logs}
|
||||
<div className="details-logs">
|
||||
<div className="logs">
|
||||
{logs}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Container;
|
||||
module.exports = ContainerDetails;
|
||||
93
app/ContainerModal.react.js
Normal file
@@ -0,0 +1,93 @@
|
||||
var React = require('react');
|
||||
var Router = require('react-router');
|
||||
var Modal = require('react-bootstrap/Modal');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var $ = require('jquery');
|
||||
var ContainerStore = require('./ContainerStore.js');
|
||||
|
||||
var ContainerModal = React.createClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
query: '',
|
||||
results: []
|
||||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.refs.searchInput.getDOMNode().focus();
|
||||
},
|
||||
search: function (query) {
|
||||
var self = this;
|
||||
$.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) {
|
||||
self.setState(result);
|
||||
console.log(result);
|
||||
});
|
||||
},
|
||||
handleChange: function (e) {
|
||||
var query = e.target.value;
|
||||
|
||||
if (query === this.state.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
var self = this;
|
||||
this.timeout = setTimeout(function () {
|
||||
self.search(query);
|
||||
}, 250);
|
||||
},
|
||||
handleClick: function (event) {
|
||||
var name = event.target.getAttribute('name');
|
||||
ContainerStore.create(name);
|
||||
},
|
||||
render: function () {
|
||||
var top = this.state.results.splice(0, 7);
|
||||
var self = this;
|
||||
var results = top.map(function (r) {
|
||||
var name;
|
||||
if (r.is_official) {
|
||||
name = <span><RetinaImage src="official.png"/>{r.name}</span>;
|
||||
} else {
|
||||
name = <span>{r.name}</span>;
|
||||
}
|
||||
return (
|
||||
<li key={r.name}>
|
||||
<div className="info">
|
||||
<div className="name">
|
||||
{name}
|
||||
</div>
|
||||
<div className="stars">
|
||||
<div className="icon icon-star-9"></div>
|
||||
<div className="star-count">{r.star_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="action">
|
||||
<button className="btn btn-primary" name={r.name} onClick={self.handleClick}>Create</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Modal {...this.props} animation={false} className="create-modal">
|
||||
<div className="modal-body">
|
||||
<section className="search">
|
||||
<input type="search" ref="searchInput" className="form-control" placeholder="Find an existing image" onChange={this.handleChange}/>
|
||||
<div className="question">
|
||||
<a href="#"><span>What's an image?</span></a>
|
||||
</div>
|
||||
<div className="results">
|
||||
<div className="title">Results</div>
|
||||
<ul>
|
||||
{results}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<aside className="custom">
|
||||
<div className="title">Create a Custom Container</div>
|
||||
</aside>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerModal;
|
||||
120
app/ContainerStore.js
Normal file
@@ -0,0 +1,120 @@
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var async = require('async');
|
||||
var assign = require('react/lib/Object.assign');
|
||||
var docker = require('./docker.js');
|
||||
var $ = require('jquery');
|
||||
|
||||
// Merge our store with Node's Event Emitter
|
||||
var ContainerStore = assign(EventEmitter.prototype, {
|
||||
_containers: {},
|
||||
init: function () {
|
||||
// TODO: Load cached data from leveldb
|
||||
// Check if the pulled image is working
|
||||
|
||||
// Refresh with docker & hook into events
|
||||
var self = this;
|
||||
this.update(function (err) {
|
||||
docker.client().getEvents(function (err, stream) {
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
self.update(function (err) {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
update: function (callback) {
|
||||
var self = this;
|
||||
docker.client().listContainers({all: true}, function (err, containers) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
async.map(containers, function(container, callback) {
|
||||
docker.client().getContainer(container.Id).inspect(function (err, data) {
|
||||
callback(err, data);
|
||||
});
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
var containers = {};
|
||||
results.map(function (r) {
|
||||
containers[r.Id] = r;
|
||||
});
|
||||
self._containers = containers;
|
||||
self.emit('change');
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
},
|
||||
_createContainer: function (image) {
|
||||
docker.client().createContainer({
|
||||
Image: image,
|
||||
Tty: false
|
||||
}, function (err, container) {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
console.log('Created container: ' + container.id);
|
||||
container.start({
|
||||
PublishAllPorts: true
|
||||
}, function (err) {
|
||||
if (err) { callback(err, null); return; }
|
||||
console.log('Started container: ' + container.id);
|
||||
callback(null, container);
|
||||
});
|
||||
});
|
||||
},
|
||||
create: function (repository, tag, callback) {
|
||||
tag = tag || 'latest';
|
||||
var name = repository + ':' + tag;
|
||||
// Check if image is not local or already being downloaded
|
||||
console.log('Creating container.');
|
||||
var self = this;
|
||||
var image = docker.client().getImage(name);
|
||||
image.inspect(function (err, data) {
|
||||
/*$.get('https://registry.hub.docker.com/v1/repositories/' + repository + '/tags/' + tag, function (data) {
|
||||
|
||||
});*/
|
||||
if (data === null) {
|
||||
// Pull image
|
||||
docker.client().pull(name, function (err, stream) {
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
console.log(data);
|
||||
});
|
||||
stream.on('end', function () {
|
||||
self._createContainer(name);
|
||||
});
|
||||
});
|
||||
|
||||
// Create placeholder container
|
||||
} else {
|
||||
// If not then directly create the container
|
||||
self._createContainer(name);
|
||||
}
|
||||
});
|
||||
// If so then create a container w/ kitematic-only 'downloading state'
|
||||
// Pull image
|
||||
// When image is done pulling then
|
||||
},
|
||||
|
||||
// Returns all shoes
|
||||
containers: function() {
|
||||
return this._containers;
|
||||
},
|
||||
|
||||
addChangeListener: function(callback) {
|
||||
this.on('change', callback);
|
||||
},
|
||||
|
||||
removeChangeListener: function(callback) {
|
||||
this.removeListener('change', callback);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerStore;
|
||||
@@ -1,43 +1,75 @@
|
||||
var React = require('react');
|
||||
var Router = require('react-router');
|
||||
var Modal = require('react-bootstrap/Modal');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var ModalTrigger = require('react-bootstrap/ModalTrigger');
|
||||
var ContainerModal = require('./ContainerModal.react.js');
|
||||
var ContainerStore = require('./ContainerStore.js');
|
||||
var Route = Router.Route;
|
||||
var NotFoundRoute = Router.NotFoundRoute;
|
||||
var DefaultRoute = Router.DefaultRoute;
|
||||
var Link = Router.Link;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
var Navigation= Router.Navigation;
|
||||
|
||||
var Header = require('./Header.react.js');
|
||||
|
||||
var async = require('async');
|
||||
var _ = require('underscore');
|
||||
var docker = require('./docker.js');
|
||||
|
||||
|
||||
var ContainerList = React.createClass({
|
||||
mixins: [Navigation],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
containers: []
|
||||
};
|
||||
},
|
||||
handleClick: function () {
|
||||
console.log('hi');
|
||||
},
|
||||
componentDidMount: function () {
|
||||
ContainerStore.addChangeListener(this.update);
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
ContainerStore.removeChangeListener(this.update);
|
||||
},
|
||||
update: function () {
|
||||
var containers = _.values(ContainerStore.containers()).sort(function (a, b) {
|
||||
return a.Name.localeCompare(b.Name);
|
||||
});
|
||||
console.log(containers);
|
||||
if (containers.length > 0) {
|
||||
this.transitionTo('container', {Id: containers[0].Id, container: containers[0]});
|
||||
}
|
||||
this.setState({
|
||||
containers: containers
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var containers = this.props.containers.map(function (container) {
|
||||
var containers = this.state.containers.map(function (container) {
|
||||
var state;
|
||||
if (container.State.Running) {
|
||||
state = <span className="status">running</span>;
|
||||
} else if (container.State.Restarting) {
|
||||
state = <span className="status">restarting</span>;
|
||||
state = <div className="state state-running"><div className="runningwave"></div></div>;
|
||||
} else {
|
||||
state = <div className="state state-restarting"></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link key={container.Id} to="container" params={{Id: container.Id, container: container}}>
|
||||
<Link key={container.Id} to="container" params={{Id: container.Id, container: container}} onClick={this.handleClick}>
|
||||
<li>
|
||||
<div className="name">
|
||||
{container.Name.replace('/', '')}
|
||||
</div>
|
||||
<div className="image">
|
||||
{state} - {container.Config.Image}
|
||||
{state}
|
||||
<div className="info">
|
||||
<div className="name">
|
||||
{container.Name.replace('/', '')}
|
||||
</div>
|
||||
<div className="image">
|
||||
{container.Config.Image}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<ul className="container-list">
|
||||
<ul>
|
||||
{containers}
|
||||
</ul>
|
||||
);
|
||||
@@ -45,49 +77,50 @@ var ContainerList = React.createClass({
|
||||
});
|
||||
|
||||
var Containers = React.createClass({
|
||||
mixins: [Navigation],
|
||||
getInitialState: function() {
|
||||
return {containers: [], index: null};
|
||||
getInitialState: function () {
|
||||
return {
|
||||
sidebarOffset: 0
|
||||
};
|
||||
},
|
||||
update: function () {
|
||||
var self = this;
|
||||
docker.client().listContainers({all: true}, function (err, containers) {
|
||||
async.map(containers, function(container, callback) {
|
||||
docker.client().getContainer(container.Id).inspect(function (err, data) {
|
||||
callback(null, data);
|
||||
});
|
||||
}, function (err, results) {
|
||||
if (results.length > 0) {
|
||||
self.transitionTo('container', {Id: results[0].Id, container: results[0]});
|
||||
}
|
||||
self.setState({containers: results});
|
||||
handleScroll: function (e) {
|
||||
if (e.target.scrollTop > 0 && !this.state.sidebarOffset) {
|
||||
this.setState({
|
||||
sidebarOffset: e.target.scrollTop
|
||||
});
|
||||
});
|
||||
} else if (e.target.scrollTop === 0 && this.state.sidebarOffset) {
|
||||
this.setState({
|
||||
sidebarOffset: 0
|
||||
});
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.update();
|
||||
var self = this;
|
||||
docker.client().getEvents(function (err, stream) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
self.update();
|
||||
});
|
||||
});
|
||||
handleClick: function () {
|
||||
// ContainerStore.create('jbfink/wordpress', 'latest');
|
||||
},
|
||||
render: function () {
|
||||
var sidebarHeaderClass = 'sidebar-header';
|
||||
if (this.state.sidebarOffset) {
|
||||
sidebarHeaderClass += ' sep';
|
||||
}
|
||||
return (
|
||||
<div className="containers">
|
||||
<div className="containers" onClick={this.handleClick}>
|
||||
<Header/>
|
||||
<div className="containers-body">
|
||||
<div className="sidebar">
|
||||
<ContainerList containers={this.state.containers}/>
|
||||
</div>
|
||||
<div className="details container">
|
||||
<RouteHandler containers={this.state.containers}/>
|
||||
<section className={sidebarHeaderClass}>
|
||||
<h3>containers</h3>
|
||||
<div className="create">
|
||||
<ModalTrigger modal={<ContainerModal/>}>
|
||||
<div className="wrapper">
|
||||
<span className="icon icon-add-3"></span>
|
||||
</div>
|
||||
</ModalTrigger>
|
||||
</div>
|
||||
</section>
|
||||
<section className="sidebar-containers" onScroll={this.handleScroll}>
|
||||
<ContainerList/>
|
||||
</section>
|
||||
</div>
|
||||
<RouteHandler/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,18 @@ var React = require('react/addons');
|
||||
var remote = require('remote');
|
||||
|
||||
var Header = React.createClass({
|
||||
componentDidMount: function () {
|
||||
document.addEventListener('keyup', this.handleDocumentKeyUp, false);
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
document.removeEventListener('keyup', this.handleDocumentKeyUp, false);
|
||||
},
|
||||
handleDocumentKeyUp: function (e) {
|
||||
if (e.keyCode === 27 && remote.getCurrentWindow().isFullScreen()) {
|
||||
remote.getCurrentWindow().setFullScreen(false);
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
handleClose: function () {
|
||||
remote.getCurrentWindow().hide();
|
||||
},
|
||||
@@ -9,30 +21,35 @@ var Header = React.createClass({
|
||||
remote.getCurrentWindow().minimize();
|
||||
},
|
||||
handleFullscreen: function () {
|
||||
var isFullscreen = remote.getCurrentWindow().isFullScreen();
|
||||
remote.getCurrentWindow().setFullScreen(!isFullscreen);
|
||||
remote.getCurrentWindow().setFullScreen(!remote.getCurrentWindow().isFullScreen());
|
||||
this.forceUpdate();
|
||||
},
|
||||
handleFullscreenHover: function () {
|
||||
this.update();
|
||||
},
|
||||
render: function () {
|
||||
var fullscreenButton;
|
||||
var buttons;
|
||||
if (remote.getCurrentWindow().isFullScreen()) {
|
||||
fullscreenButton = <div className="button button-fullscreenclose" onClick={this.handleFullscreen}></div>;
|
||||
} else {
|
||||
fullscreenButton = <div className="button button-fullscreen" onClick={this.handleFullscreen}></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
<div className="buttons">
|
||||
<div className="button button-close" onClick={this.handleClose}></div>
|
||||
<div className="button button-minimize" onClick={this.handleMinimize}></div>
|
||||
{fullscreenButton}
|
||||
return (
|
||||
<div className="header no-drag">
|
||||
<div className="buttons">
|
||||
<div className="button button-close disabled"></div>
|
||||
<div className="button button-minimize disabled"></div>
|
||||
<div className="button button-fullscreenclose enabled" onClick={this.handleFullscreen}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="header">
|
||||
<div className="buttons">
|
||||
<div className="button button-close enabled" onClick={this.handleClose}></div>
|
||||
<div className="button button-minimize enabled" onClick={this.handleMinimize}></div>
|
||||
<div className="button button-fullscreen enabled" onClick={this.handleFullscreen}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ var homeDir = function () {
|
||||
|
||||
var Boot2Docker = {
|
||||
version: function () {
|
||||
return JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'))['boot2docker-version'];
|
||||
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'))['boot2docker-version'];
|
||||
},
|
||||
cliVersion: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'version'], function (err, out) {
|
||||
|
||||
@@ -4,15 +4,14 @@ var dockerode = require('dockerode');
|
||||
|
||||
var Docker = {
|
||||
host: null,
|
||||
_client: null,
|
||||
setHost: function(host) {
|
||||
this.host = host;
|
||||
},
|
||||
client: function () {
|
||||
var certDir = path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.boot2docker/certs/boot2docker-vm');
|
||||
if (!fs.existsSync(certDir)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
return new dockerode({
|
||||
this._client = new dockerode({
|
||||
protocol: 'https',
|
||||
host: this.host,
|
||||
port: 2376,
|
||||
@@ -20,6 +19,9 @@ var Docker = {
|
||||
cert: fs.readFileSync(path.join(certDir, 'cert.pem')),
|
||||
key: fs.readFileSync(path.join(certDir, 'key.pem'))
|
||||
});
|
||||
},
|
||||
client: function () {
|
||||
return this._client;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
BIN
app/fonts/clearsans-bold-webfont.ttf
Executable file
BIN
app/fonts/clearsans-bolditalic-webfont.ttf
Executable file
BIN
app/fonts/clearsans-italic-webfont.ttf
Executable file
BIN
app/fonts/clearsans-light-webfont.ttf
Executable file
BIN
app/fonts/clearsans-medium-webfont.ttf
Executable file
BIN
app/fonts/clearsans-mediumitalic-webfont.ttf
Executable file
BIN
app/fonts/clearsans-regular-webfont.ttf
Executable file
BIN
app/fonts/clearsans-thin-webfont.ttf
Executable file
BIN
app/fonts/streamline-24px.eot
Normal file
1652
app/fonts/streamline-24px.svg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
app/fonts/streamline-24px.ttf
Normal file
BIN
app/fonts/streamline-24px.woff
Normal file
BIN
app/images/loading.png
Normal file
|
After Width: | Height: | Size: 807 B |
BIN
app/images/loading@2x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/images/official.png
Normal file
|
After Width: | Height: | Size: 609 B |
BIN
app/images/official@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/images/restarting.png
Normal file
|
After Width: | Height: | Size: 641 B |
BIN
app/images/restarting@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/images/running.png
Normal file
|
After Width: | Height: | Size: 536 B |
BIN
app/images/running@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/images/runningwave.png
Normal file
|
After Width: | Height: | Size: 355 B |
BIN
app/images/runningwave@2x.png
Normal file
|
After Width: | Height: | Size: 654 B |
@@ -5,7 +5,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<script src="main.js"></script>
|
||||
<script src="https://cdn.ravenjs.com/1.1.15/jquery,native/raven.min.js"></script>
|
||||
<script src="http://localhost:35729/livereload.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,12 +7,14 @@ var DefaultRoute = Router.DefaultRoute;
|
||||
var Link = Router.Link;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
|
||||
var Raven = require('raven');
|
||||
var async = require('async');
|
||||
var docker = require('./docker.js');
|
||||
var boot2docker = require('./boot2docker.js');
|
||||
var Setup = require('./Setup.react');
|
||||
var Containers = require('./Containers.react');
|
||||
var Container = require('./Container.react');
|
||||
var ContainerDetails = require('./ContainerDetails.react');
|
||||
var ContainerStore = require('./ContainerStore.js');
|
||||
var Radial = require('./Radial.react');
|
||||
|
||||
var NoContainers = React.createClass({
|
||||
@@ -26,6 +28,9 @@ var NoContainers = React.createClass({
|
||||
});
|
||||
|
||||
var App = React.createClass({
|
||||
componentWillMount: function () {
|
||||
ContainerStore.init();
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<RouteHandler/>
|
||||
@@ -36,7 +41,7 @@ var App = React.createClass({
|
||||
var routes = (
|
||||
<Route name="app" path="/" handler={App}>
|
||||
<Route name="containers" handler={Containers}>
|
||||
<Route name="container" path=":Id" handler={Container}>
|
||||
<Route name="container" path=":Id" handler={ContainerDetails}>
|
||||
</Route>
|
||||
<DefaultRoute handler={NoContainers}/>
|
||||
</Route>
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
|
||||
|
||||
//** Load fonts from this directory.
|
||||
@icon-font-path: "../fonts/";
|
||||
@icon-font-path: "../fonts";
|
||||
//** File name for all font files.
|
||||
@icon-font-name: "glyphicons-halflings-regular";
|
||||
//** Element ID within SVG icon file.
|
||||
|
||||
4966
app/styles/icons.less
Normal file
@@ -1,123 +1,261 @@
|
||||
@import "bootstrap/bootstrap.less";
|
||||
@import "clearsans.less";
|
||||
@import "theme.less";
|
||||
@import "icons.less";
|
||||
@import "retina.less";
|
||||
@import "setup.less";
|
||||
@import "radial.less";
|
||||
|
||||
.header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid #eee;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
.buttons {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 16px;
|
||||
left: 20px;
|
||||
|
||||
.buttons {
|
||||
&:hover {
|
||||
.button-minimize.enabled {
|
||||
.at2x('minimize.png', 10px, 10px);
|
||||
}
|
||||
.button-close.enabled {
|
||||
.at2x('close.png', 10px, 10px);
|
||||
}
|
||||
.button-fullscreen.enabled {
|
||||
.at2x('fullscreen.png', 10px, 10px);
|
||||
}
|
||||
.button-fullscreenclose.enabled {
|
||||
.at2x('fullscreenclose.png', 10px, 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 16px;
|
||||
left: 20px;
|
||||
background: white;
|
||||
margin-right: 9px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border: 1px solid #CCD3D5;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0px 1px 1px 0px rgba(234,234,234,0.50);
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover {
|
||||
.button-minimize {
|
||||
.at2x('minimize.png', 10px, 10px);
|
||||
}
|
||||
.button-close {
|
||||
.at2x('close.png', 10px, 10px);
|
||||
}
|
||||
.button-fullscreen {
|
||||
.at2x('fullscreen.png', 10px, 10px);
|
||||
}
|
||||
.button-fullscreenclose {
|
||||
.at2x('fullscreenclose.png', 10px, 10px);
|
||||
}
|
||||
&.disabled {
|
||||
border: 1px solid #E8EEEF;
|
||||
}
|
||||
|
||||
.button {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
background: white;
|
||||
margin-right: 9px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border: 1px solid #CACDD0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0px 1px 1px 0px rgba(234,234,234,0.50);
|
||||
-webkit-app-region: no-drag;
|
||||
&.enabled:hover {
|
||||
box-shadow: 0px 1px 1px 0px rgba(195,198,201,0.50);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 1px 1px 0px rgba(195,198,201,0.50);
|
||||
}
|
||||
|
||||
&:hover:active {
|
||||
cursor: default;
|
||||
-webkit-filter: brightness(92%);
|
||||
}
|
||||
&.enabled:hover:active {
|
||||
cursor: default;
|
||||
-webkit-filter: brightness(92%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
min-width: 256px;
|
||||
.header {
|
||||
min-width: 100%;
|
||||
flex: 0;
|
||||
min-height: 48px;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
|
||||
&:hover {
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
padding: 14px 24px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.image {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
&.no-drag {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
.containers {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.containers-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: row;
|
||||
|
||||
.sidebar {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
min-width: 256px;
|
||||
// border-right: 1px solid #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 240px;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
border-right: 1px solid #eee;
|
||||
|
||||
.sidebar-header {
|
||||
flex: 0 auto;
|
||||
min-width: 240px;
|
||||
display: flex;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-bottom 0.25s;
|
||||
|
||||
&.sep {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
h3 {
|
||||
align-self: flex-start;
|
||||
color: #CCD3D5;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
padding: 0 24px;
|
||||
margin: 10px 0 0;
|
||||
font-variant: small-caps;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.create {
|
||||
flex: 1 auto;
|
||||
text-align: right;
|
||||
|
||||
.wrapper {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
|
||||
span.icon {
|
||||
margin-top: 5px;
|
||||
margin-left: auto;
|
||||
display: inline-block;
|
||||
border-radius: 20px;
|
||||
font-size: 26px;
|
||||
color: @brand-primary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
span.icon {
|
||||
color: darken(@brand-primary, 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-containers {
|
||||
position: relative;
|
||||
flex: 1 auto;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.sep {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 240px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
flex-shrink: 0;
|
||||
cursor: default;
|
||||
|
||||
/*&:hover {
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
background: @brand-primary;
|
||||
|
||||
li {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
li > .info > .name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
li > .info > .image {
|
||||
color: #fff;
|
||||
}
|
||||
}*/
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
vertical-align: middle;
|
||||
|
||||
margin: 11px 24px 0px;
|
||||
border-bottom: 1px solid #efefef;
|
||||
padding-bottom: 11px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
|
||||
.info {
|
||||
font-size: 13px;
|
||||
margin-left: 12px;
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: #555;
|
||||
}
|
||||
.image {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.state {
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.state-running {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
.at2x('running.png', 20px, 20px);
|
||||
overflow: hidden;
|
||||
// -webkit-mask-image: -webkit-radial-gradient(circle, white, black);
|
||||
|
||||
.runningwave {
|
||||
position: absolute;
|
||||
width: 44px;
|
||||
height: 20px;
|
||||
left: -20px;
|
||||
.at2x('runningwave.png', 20px, 20px);
|
||||
// background-repeat: repeat;
|
||||
-webkit-animation-name: translate;
|
||||
-webkit-animation-duration: 6.0s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-timing-function: linear;
|
||||
}
|
||||
}
|
||||
|
||||
.state-restarting {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
.at2x('restarting.png', 20px, 20px);
|
||||
background-repeat: repeat-x;
|
||||
-webkit-animation-name: rotate;
|
||||
-webkit-animation-duration: 3.0s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-timing-function: linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
@@ -127,27 +265,28 @@
|
||||
}
|
||||
|
||||
.details {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
|
||||
.details-header {
|
||||
padding: 14px 45px;
|
||||
flex: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0px 45px 14px;
|
||||
background: white;
|
||||
width: 100%;
|
||||
h1 {
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
h2 {
|
||||
margin-left: 18px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
font-variant: small-caps;
|
||||
|
||||
&.status {
|
||||
@@ -160,14 +299,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.logs {
|
||||
user-select: text;
|
||||
.details-logs {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
font-family: Menlo;
|
||||
font-size: 12px;
|
||||
padding: 44px 45px;
|
||||
p {
|
||||
margin: 6px;
|
||||
.logs {
|
||||
user-select: text;
|
||||
font-family: Menlo;
|
||||
font-size: 12px;
|
||||
padding: 44px 45px;
|
||||
color: #595D5E;
|
||||
white-space: pre-wrap;
|
||||
p {
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,8 +322,176 @@ html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
user-select: none;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
font-family: 'Helvetica Neue', sans-serif;
|
||||
font-family: 'Clear Sans', sans-serif;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
margin: 3px;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
background-clip: padding-box;
|
||||
width: 7px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.create-modal {
|
||||
@modal-padding: 32px;
|
||||
@search-width: 372px;
|
||||
@custom-width: 270px;
|
||||
.modal-dialog {
|
||||
margin-top: 8%;
|
||||
width: calc(@modal-padding + @search-width + 2 * @modal-padding + @custom-width);
|
||||
}
|
||||
.modal-content {
|
||||
//box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.10);
|
||||
border: none; //1px solid #ccc;
|
||||
height: 610px;
|
||||
}
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 32px 32px;
|
||||
|
||||
.title {
|
||||
color: #CCD3D5;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
aside.custom {
|
||||
flex: 0 auto;
|
||||
padding-left: 32px;
|
||||
min-width: 270px;
|
||||
}
|
||||
|
||||
section.search {
|
||||
flex: 0 auto;
|
||||
min-width: 404px;
|
||||
padding-right: 32px;
|
||||
border-right: 1px solid #eee;
|
||||
|
||||
.question {
|
||||
a {
|
||||
color: #CCD3D5;
|
||||
}
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
height: 38px;
|
||||
padding: 8px 16px;
|
||||
font-weight: 400;
|
||||
color: #666;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: #ddd;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
overflow: auto;
|
||||
|
||||
.title {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
color: #555;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
.info {
|
||||
.name {
|
||||
max-width: 278px;
|
||||
img {
|
||||
margin-right: 6px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.stars {
|
||||
color: #A7A7A7;
|
||||
margin-top: 2px;
|
||||
|
||||
.star-count {
|
||||
font-size: 10px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
left: 1px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
flex: 0 auto;
|
||||
}
|
||||
.action {
|
||||
text-align: right;
|
||||
flex: 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-backdrop.in {
|
||||
background: rgba(227,230,230,0.95);
|
||||
opacity: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotate {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes translate {
|
||||
from {
|
||||
-webkit-transform: translateX(0px);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translateX(20px);
|
||||
}
|
||||
}
|
||||
|
||||
76
app/styles/theme.less
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Load core variables and mixins
|
||||
// --------------------------------------------------
|
||||
|
||||
@import "bootstrap/variables.less";
|
||||
@import "bootstrap/mixins.less";
|
||||
|
||||
|
||||
//
|
||||
// Buttons
|
||||
// --------------------------------------------------
|
||||
|
||||
// Common styles
|
||||
.btn-default,
|
||||
.btn-primary,
|
||||
.btn-success,
|
||||
.btn-info,
|
||||
.btn-warning,
|
||||
.btn-danger {
|
||||
text-shadow: 0 -1px 0 rgba(0,0,0,.2);
|
||||
@shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);
|
||||
.box-shadow(@shadow);
|
||||
|
||||
// Reset the shadow
|
||||
&:active,
|
||||
&.active {
|
||||
.box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
|
||||
}
|
||||
|
||||
.badge {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Mixin for generating new styles
|
||||
.btn-styles(@btn-color: #555) {
|
||||
#gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));
|
||||
.reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners
|
||||
background-repeat: repeat-x;
|
||||
border-color: darken(@btn-color, 14%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: darken(@btn-color, 12%);
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
background-color: darken(@btn-color, 12%);
|
||||
border-color: darken(@btn-color, 14%);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
background-color: darken(@btn-color, 12%);
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Common styles
|
||||
.btn {
|
||||
// Remove the gradient for the pressed/active state
|
||||
&:active,
|
||||
&.active {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the mixin to the buttons
|
||||
.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }
|
||||
.btn-primary { .btn-styles(@btn-primary-bg); }
|
||||
.btn-success { .btn-styles(@btn-success-bg); }
|
||||
.btn-info { .btn-styles(@btn-info-bg); }
|
||||
.btn-warning { .btn-styles(@btn-warning-bg); }
|
||||
.btn-danger { .btn-styles(@btn-danger-bg); }
|
||||
@@ -1,3 +1,4 @@
|
||||
@brand-action: #4A9AEC;
|
||||
@brand-primary: #49CEF2;
|
||||
@brand-action: #49CEF2;
|
||||
@brand-positive: #3AD86D;
|
||||
@brand-negative: #F74B1F;
|
||||
|
||||
@@ -28,8 +28,8 @@ app.on('ready', function() {
|
||||
var windowOptions = {
|
||||
width: 1200,
|
||||
height: 800,
|
||||
'min-width': 1080,
|
||||
'min-height': 560,
|
||||
'min-width': 960,
|
||||
'min-height': 700,
|
||||
resizable: true,
|
||||
frame: false
|
||||
};
|
||||
|
||||
@@ -215,7 +215,7 @@ gulp.task('test', ['download', 'copy', 'js', 'images', 'styles', 'specs'], funct
|
||||
gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () {
|
||||
gulp.watch('./app/**/*.html', ['copy']);
|
||||
gulp.watch('./app/styles/**/*.less', ['styles']);
|
||||
gulp.watch('./app/images/**/*.png', ['images']);
|
||||
gulp.watch('./app/images/**', ['images']);
|
||||
|
||||
livereload.listen();
|
||||
|
||||
|
||||
BIN
kitematic.icns
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Kitematic",
|
||||
"version": "0.4.4",
|
||||
"version": "0.5.0",
|
||||
"author": "Kitematic",
|
||||
"description": "Simple Docker App management for Mac OS X.",
|
||||
"homepage": "https://kitematic.com/",
|
||||
@@ -20,15 +20,14 @@
|
||||
}
|
||||
],
|
||||
"boot2docker-version": "1.3.2",
|
||||
"atom-shell-version": "0.20.3",
|
||||
"atom-shell-version": "0.20.6",
|
||||
"dependencies": {
|
||||
"ansi-to-html": "0.2.0",
|
||||
"async": "^0.9.0",
|
||||
"dockerode": "2.0.4",
|
||||
"exec": "0.1.2",
|
||||
"flux-react": "^2.6.1",
|
||||
"ftscroller": "^0.5.1",
|
||||
"iscroll": "^5.1.3",
|
||||
"jquery": "^2.1.3",
|
||||
"leveldown": "^1.0.0",
|
||||
"levelup": "git+https://github.com/kitematic/node-levelup.git",
|
||||
"minimist": "^1.1.0",
|
||||
@@ -36,11 +35,16 @@
|
||||
"ncp": "0.6.0",
|
||||
"node-uuid": "1.4.1",
|
||||
"open": "0.0.5",
|
||||
"raven": "^0.7.2",
|
||||
"react": "^0.12.1",
|
||||
"react-bootstrap": "^0.13.2",
|
||||
"react-retina-image": "^1.1.2",
|
||||
"react-router": "^0.11.6",
|
||||
"request": "2.42.0",
|
||||
"request-progress": "0.3.1",
|
||||
"retina.js": "^1.1.0",
|
||||
"tar": "0.1.20"
|
||||
"tar": "0.1.20",
|
||||
"underscore": "^1.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^6.2.0",
|
||||
|
||||