Pull & Run containers

This commit is contained in:
Jeffrey Morgan
2015-01-17 13:15:26 -05:00
parent 78c3e8fc8c
commit 7e74481e85
40 changed files with 7505 additions and 208 deletions

View File

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

View 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&#39;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
View 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;

View File

@@ -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>
);

View File

@@ -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>
);
}
}
});

View File

@@ -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) {

View File

@@ -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;
}
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Binary file not shown.

BIN
app/images/loading.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

BIN
app/images/loading@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
app/images/official.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

BIN
app/images/official@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
app/images/restarting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
app/images/running.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

BIN
app/images/running@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
app/images/runningwave.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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
View 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); }

View File

@@ -1,3 +1,4 @@
@brand-action: #4A9AEC;
@brand-primary: #49CEF2;
@brand-action: #49CEF2;
@brand-positive: #3AD86D;
@brand-negative: #F74B1F;

View File

@@ -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
};

View File

@@ -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();

Binary file not shown.

View File

@@ -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",