Refactored out Meteor into React.js. WIP
|
@ -1,10 +1,11 @@
|
|||
.DS_Store
|
||||
.demeteorized
|
||||
build
|
||||
dist
|
||||
node_modules
|
||||
package
|
||||
cache
|
||||
bin
|
||||
npm-debug.log
|
||||
|
||||
# Signing Identity
|
||||
script/identity
|
||||
|
||||
# Resources
|
||||
resources/virtualbox-*.pkg
|
||||
|
@ -17,5 +18,3 @@ resources/settings.json
|
|||
|
||||
# Cache
|
||||
cache
|
||||
|
||||
script/sign.sh
|
||||
|
|
15
README.md
|
@ -10,21 +10,18 @@ Kitematic is a simple application for managing Docker containers on Mac OS X.
|
|||
|
||||
Kitematic's documentation and other information can be found at [http://kitematic.com/docs](http://kitematic.com/docs).
|
||||
|
||||
## Development
|
||||
### Development
|
||||
|
||||
- Install any version of Node.js
|
||||
- Install meteor.js `curl https://install.meteor.com/ | sh`.
|
||||
- Install meteorite `npm install meteorite -g`
|
||||
- Install demeteorizer `npm install demeteorizer -g`
|
||||
- Run `./script/setup.sh` to download the binary requirements (things like virtualbox).
|
||||
- `sudo npm install -g less`
|
||||
- `./script/npm install`
|
||||
|
||||
### Running the development Server
|
||||
To run the app in development:
|
||||
|
||||
- `./script/run.sh`
|
||||
- `./script/gulp`
|
||||
|
||||
### Building the Mac OS X Package
|
||||
|
||||
- `./script/dist.sh`
|
||||
- `./script/release`
|
||||
|
||||
## Uninstalling
|
||||
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
var _ = require('underscore');
|
||||
var React = require('react');
|
||||
var Router = require('react-router');
|
||||
var Convert = require('ansi-to-html');
|
||||
var convert = new Convert();
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var docker = require('./docker');
|
||||
var exec = require('exec');
|
||||
var boot2docker = require('./boot2docker');
|
||||
var ProgressBar = require('react-bootstrap/ProgressBar');
|
||||
|
||||
var Route = Router.Route;
|
||||
var NotFoundRoute = Router.NotFoundRoute;
|
||||
var DefaultRoute = Router.DefaultRoute;
|
||||
var Link = Router.Link;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
|
||||
var ContainerDetails = React.createClass({
|
||||
mixins: [Router.State],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
logs: []
|
||||
};
|
||||
},
|
||||
componentWillReceiveProps: function () {
|
||||
this.update();
|
||||
this.setState({
|
||||
logs: []
|
||||
});
|
||||
var self = this;
|
||||
var logs = [];
|
||||
var index = 0;
|
||||
docker.client().getContainer(this.getParams().name).logs({
|
||||
follow: false,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true
|
||||
}, function (err, stream) {
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (buf) {
|
||||
// Every other message is a header
|
||||
if (index % 2 === 1) {
|
||||
var time = buf.substr(0,buf.indexOf(' '));
|
||||
var msg = buf.substr(buf.indexOf(' ')+1);
|
||||
logs.push(convert.toHtml(self._escapeHTML(msg)));
|
||||
}
|
||||
index += 1;
|
||||
});
|
||||
stream.on('end', function (buf) {
|
||||
self.setState({logs: logs});
|
||||
docker.client().getContainer(self.getParams().name).logs({
|
||||
follow: true,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
tail: 0
|
||||
}, function (err, stream) {
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (buf) {
|
||||
// Every other message is a header
|
||||
if (index % 2 === 1) {
|
||||
var time = buf.substr(0,buf.indexOf(' '));
|
||||
var msg = buf.substr(buf.indexOf(' ')+1);
|
||||
logs.push(convert.toHtml(self._escapeHTML(msg)));
|
||||
self.setState({logs: logs});
|
||||
}
|
||||
index += 1;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
componentWillMount: function () {
|
||||
this.update();
|
||||
},
|
||||
componentDidMount: function () {
|
||||
ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.update);
|
||||
ContainerStore.addChangeListener(ContainerStore.PROGRESS, this.update);
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.update);
|
||||
ContainerStore.removeChangeListener(ContainerStore.PROGRESS, this.update);
|
||||
},
|
||||
update: function () {
|
||||
var name = this.getParams().name;
|
||||
this.setState({
|
||||
container: ContainerStore.container(name),
|
||||
progress: ContainerStore.progress(name)
|
||||
});
|
||||
},
|
||||
_escapeHTML: function (html) {
|
||||
var text = document.createTextNode(html);
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(text);
|
||||
return div.innerHTML;
|
||||
},
|
||||
handleClick: function (name) {
|
||||
var container = this.state.container;
|
||||
boot2docker.ip(function (err, ip) {
|
||||
var ports = _.map(container.NetworkSettings.Ports, function (value, key) {
|
||||
var portProtocolPair = key.split('/');
|
||||
var res = {
|
||||
'port': portProtocolPair[0],
|
||||
'protocol': portProtocolPair[1]
|
||||
};
|
||||
if (value && value.length) {
|
||||
var port = value[0].HostPort;
|
||||
res.host = ip;
|
||||
res.port = port;
|
||||
res.url = 'http://' + ip + ':' + port;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
console.log(ports);
|
||||
exec(['open', ports[0].url], function (err) {
|
||||
if (err) { throw err; }
|
||||
});
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var self = this;
|
||||
|
||||
if (!this.state) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
var logs = this.state.logs.map(function (l, i) {
|
||||
return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>;
|
||||
});
|
||||
|
||||
if (!this.state.container) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var state;
|
||||
if (this.state.container.State.Running) {
|
||||
state = <h2 className="status">running</h2>;
|
||||
} else if (this.state.container.State.Restarting) {
|
||||
state = <h2 className="status">restarting</h2>;
|
||||
}
|
||||
|
||||
var progress;
|
||||
if (this.state.progress > 0 && this.state.progress != 1) {
|
||||
progress = (
|
||||
<div className="details-progress">
|
||||
<ProgressBar now={this.state.progress * 100} label="%(percent)s%" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
progress = <div></div>;
|
||||
}
|
||||
|
||||
var button;
|
||||
if (this.state.progress === 1) {
|
||||
button = <a className="btn btn-primary" onClick={this.handleClick}>View</a>;
|
||||
} else {
|
||||
button = <a className="btn btn-primary disabled" onClick={this.handleClick}>View</a>;
|
||||
}
|
||||
|
||||
var name = this.state.container.Name.replace('/', '');
|
||||
|
||||
return (
|
||||
<div className="details">
|
||||
<div className="details-header">
|
||||
<h1>{name}</h1> <a className="btn btn-primary" onClick={this.handleClick}>View</a>
|
||||
</div>
|
||||
{progress}
|
||||
<div className="details-logs">
|
||||
<div className="logs">
|
||||
{logs}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerDetails;
|
|
@ -0,0 +1,105 @@
|
|||
var async = require('async');
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
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');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var Header = require('./Header.react');
|
||||
var docker = require('./docker');
|
||||
|
||||
var Link = Router.Link;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
var Navigation= Router.Navigation;
|
||||
|
||||
var ContainerList = React.createClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
containers: []
|
||||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.updateContainers();
|
||||
ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.updateContainers);
|
||||
},
|
||||
componentWillMount: function () {
|
||||
this._start = Date.now();
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.updateContainers);
|
||||
},
|
||||
updateContainers: function () {
|
||||
// Sort by name
|
||||
var containers = _.values(ContainerStore.containers()).sort(function (a, b) {
|
||||
return a.Name.localeCompare(b.Name);
|
||||
});
|
||||
this.setState({containers: containers});
|
||||
},
|
||||
render: function () {
|
||||
var self = this;
|
||||
var containers = this.state.containers.map(function (container) {
|
||||
var downloadingImage = null, downloading = false;
|
||||
var env = container.Config.Env;
|
||||
if (env.length) {
|
||||
var obj = _.object(env.map(function (e) {
|
||||
return e.split('=');
|
||||
}));
|
||||
if (obj.KITEMATIC_DOWNLOADING) {
|
||||
downloading = true;
|
||||
}
|
||||
downloadingImage = obj.KITEMATIC_DOWNLOADING_IMAGE || null;
|
||||
}
|
||||
|
||||
var imageName = downloadingImage || container.Config.Image;
|
||||
|
||||
// Synchronize all animations
|
||||
var style = {
|
||||
WebkitAnimationDelay: (self._start - Date.now()) + 'ms'
|
||||
};
|
||||
|
||||
var state;
|
||||
if (downloading) {
|
||||
state = <div className="state state-downloading"><div style={style} className="downloading-arrow"></div></div>;
|
||||
} else if (container.State.Running && !container.State.Paused) {
|
||||
state = <div className="state state-running"><div style={style} className="runningwave"></div></div>;
|
||||
} else if (container.State.Restarting) {
|
||||
state = <div className="state state-restarting" style={style}></div>;
|
||||
} else if (container.State.Paused) {
|
||||
state = <div className="state state-paused"></div>;
|
||||
} else if (container.State.ExitCode) {
|
||||
// state = <div className="state state-error"></div>;
|
||||
state = <div className="state state-stopped"></div>;
|
||||
} else {
|
||||
state = <div className="state state-stopped"></div>;
|
||||
}
|
||||
|
||||
var name = container.Name.replace('/', '');
|
||||
|
||||
return (
|
||||
<Link key={name} data-container={name} to="container" params={{name: name}}>
|
||||
<li>
|
||||
{state}
|
||||
<div className="info">
|
||||
<div className="name">
|
||||
{name}
|
||||
</div>
|
||||
<div className="image">
|
||||
{imageName}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<ul>
|
||||
{containers}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerList;
|
|
@ -0,0 +1,126 @@
|
|||
var async = require('async');
|
||||
var $ = require('jquery');
|
||||
var React = require('react');
|
||||
var Router = require('react-router');
|
||||
var Modal = require('react-bootstrap/Modal');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
|
||||
var Navigation = Router.Navigation;
|
||||
|
||||
var ContainerModal = React.createClass({
|
||||
mixins: [Navigation],
|
||||
_searchRequest: null,
|
||||
getInitialState: function () {
|
||||
return {
|
||||
query: '',
|
||||
results: [],
|
||||
recommended: ContainerStore.recommended()
|
||||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.refs.searchInput.getDOMNode().focus();
|
||||
},
|
||||
search: function (query) {
|
||||
var self = this;
|
||||
this._searchRequest = $.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) {
|
||||
self._searchRequest.abort();
|
||||
self._searchRequest = null;
|
||||
if (self.isMounted()) {
|
||||
self.setState(result);
|
||||
console.log(result);
|
||||
}
|
||||
});
|
||||
},
|
||||
handleChange: function (e) {
|
||||
var query = e.target.value;
|
||||
|
||||
if (query === this.state.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._searchRequest) {
|
||||
console.log('Cancel');
|
||||
this._searchRequest.abort();
|
||||
this._searchRequest = null;
|
||||
}
|
||||
clearTimeout(this.timeout);
|
||||
var self = this;
|
||||
this.timeout = setTimeout(function () {
|
||||
self.search(query);
|
||||
}, 250);
|
||||
},
|
||||
handleClick: function (event) {
|
||||
var name = event.target.getAttribute('name');
|
||||
var self = this;
|
||||
ContainerStore.create(name, 'latest', function (err, containerName) {
|
||||
// this.transitionTo('containers', {container: containerName});
|
||||
self.props.onRequestHide();
|
||||
}.bind(this));
|
||||
},
|
||||
render: function () {
|
||||
var self = this;
|
||||
|
||||
var data;
|
||||
if (this.state.query) {
|
||||
data = this.state.results.splice(0, 7);
|
||||
} else {
|
||||
data = this.state.recommended;
|
||||
}
|
||||
var results = data.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>
|
||||
);
|
||||
});
|
||||
|
||||
var title;
|
||||
if (this.state.query) {
|
||||
title = <div className="title">Results</div>;
|
||||
} else {
|
||||
title = <div className="title">Recommended</div>;
|
||||
}
|
||||
|
||||
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">
|
||||
{title}
|
||||
<ul>
|
||||
{results}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<aside className="custom">
|
||||
<div className="title">Create a Custom Container</div>
|
||||
</aside>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerModal;
|
|
@ -0,0 +1,308 @@
|
|||
var EventEmitter = require('events').EventEmitter;
|
||||
var async = require('async');
|
||||
var assign = require('react/lib/Object.assign');
|
||||
var docker = require('./docker');
|
||||
var registry = require('./registry');
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
|
||||
// Merge our store with Node's Event Emitter
|
||||
var ContainerStore = assign(EventEmitter.prototype, {
|
||||
CONTAINERS: 'containers',
|
||||
PROGRESS: 'progress',
|
||||
LOGS: 'logs',
|
||||
RECOMMENDED: 'recommended',
|
||||
_recommended: [],
|
||||
_containers: {},
|
||||
_progress: {},
|
||||
_logs: {},
|
||||
_pullScratchImage: function (callback) {
|
||||
var image = docker.client().getImage('scratch:latest');
|
||||
image.inspect(function (err, data) {
|
||||
if (!data) {
|
||||
docker.client().pull('scratch:latest', function (err, stream) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {});
|
||||
stream.on('end', function () {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
_createContainer: function (image, name, callback) {
|
||||
var existing = docker.client().getContainer(name);
|
||||
existing.remove(function (err, data) {
|
||||
console.log('Placeholder removed.');
|
||||
docker.client().createContainer({
|
||||
Image: image,
|
||||
Tty: false,
|
||||
name: name
|
||||
}, 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
_createPlaceholderContainer: function (imageName, name, callback) {
|
||||
console.log('_createPlaceholderContainer', imageName, name);
|
||||
this._pullScratchImage(function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
docker.client().createContainer({
|
||||
Image: 'scratch:latest',
|
||||
Tty: false,
|
||||
Env: [
|
||||
'KITEMATIC_DOWNLOADING=true',
|
||||
'KITEMATIC_DOWNLOADING_IMAGE=' + imageName
|
||||
],
|
||||
Cmd: 'placeholder',
|
||||
name: name
|
||||
}, function (err, container) {
|
||||
callback(err, container);
|
||||
});
|
||||
});
|
||||
},
|
||||
_generateName: function (repository) {
|
||||
var base = _.last(repository.split('/'));
|
||||
var count = 1;
|
||||
var name = base;
|
||||
while (true) {
|
||||
var exists = _.findWhere(_.values(this._containers), {Name: '/' + name}) || _.findWhere(_.values(this._containers), {Name: name});
|
||||
if (!exists) {
|
||||
return name;
|
||||
} else {
|
||||
count++;
|
||||
name = base + '-' + count;
|
||||
}
|
||||
}
|
||||
},
|
||||
init: function (callback) {
|
||||
// TODO: Load cached data from db on loading
|
||||
|
||||
// Refresh with docker & hook into events
|
||||
var self = this;
|
||||
this.update(function (err) {
|
||||
self.updateRecommended(function (err) {
|
||||
callback();
|
||||
});
|
||||
var downloading = _.filter(_.values(self._containers), function (container) {
|
||||
var env = container.Config.Env;
|
||||
return _.indexOf(env, 'KITEMATIC_DOWNLOADING=true') !== -1;
|
||||
});
|
||||
|
||||
// Recover any pulls that were happening
|
||||
downloading.forEach(function (container) {
|
||||
var env = _.object(container.Config.Env.map(function (e) {
|
||||
return e.split('=');
|
||||
}));
|
||||
docker.client().pull(env.KITEMATIC_DOWNLOADING_IMAGE, function (err, stream) {
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
console.log(data);
|
||||
});
|
||||
stream.on('end', function () {
|
||||
self._createContainer(env.KITEMATIC_DOWNLOADING_IMAGE, container.Name.replace('/', ''), function () {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
docker.client().getEvents(function (err, stream) {
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
console.log(data);
|
||||
|
||||
// TODO: Dont refresh on deleting placeholder containers
|
||||
var deletingPlaceholder = data.status === 'destroy' && self.container(data.id) && self.container(data.id).Config.Env.indexOf('KITEMATIC_DOWNLOADING=true') !== -1;
|
||||
console.log(deletingPlaceholder);
|
||||
if (!deletingPlaceholder) {
|
||||
self.update(function (err) {
|
||||
console.log('Updated container data.');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
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.forEach(function (r) {
|
||||
containers[r.Name.replace('/', '')] = r;
|
||||
});
|
||||
self._containers = containers;
|
||||
self.emit(self.CONTAINERS);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
},
|
||||
updateRecommended: function (callback) {
|
||||
var self = this;
|
||||
$.ajax({
|
||||
url: 'https://kitematic.com/recommended.json',
|
||||
dataType: 'json',
|
||||
success: function (res, status) {
|
||||
var recommended = res.recommended;
|
||||
async.map(recommended, function (repository, callback) {
|
||||
$.get('https://registry.hub.docker.com/v1/search?q=' + repository, function (data) {
|
||||
var results = data.results;
|
||||
callback(null, _.find(results, function (r) {
|
||||
return r.name === repository;
|
||||
}));
|
||||
});
|
||||
}, function (err, results) {
|
||||
self._recommended = results;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
error: function (err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
create: function (repository, tag, callback) {
|
||||
tag = tag || 'latest';
|
||||
var self = this;
|
||||
var imageName = repository + ':' + tag;
|
||||
var containerName = this._generateName(repository);
|
||||
var image = docker.client().getImage(imageName);
|
||||
|
||||
image.inspect(function (err, data) {
|
||||
if (!data) {
|
||||
// Pull image
|
||||
self._createPlaceholderContainer(imageName, containerName, function (err, container) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
registry.layers(repository, tag, function (err, layerSizes) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
}
|
||||
|
||||
// TODO: Support v2 registry API
|
||||
// TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
|
||||
// Use the per-layer pull progress % to update the total progress.
|
||||
docker.client().listImages({all: 1}, function(err, images) {
|
||||
var existingIds = new Set(images.map(function (image) {
|
||||
return image.Id.slice(0, 12);
|
||||
}));
|
||||
var layersToDownload = layerSizes.filter(function (layerSize) {
|
||||
return !existingIds.has(layerSize.Id);
|
||||
});
|
||||
|
||||
var totalBytes = layersToDownload.map(function (s) { return s.size; }).reduce(function (pv, sv) { return pv + sv; }, 0);
|
||||
docker.client().pull(imageName, function (err, stream) {
|
||||
callback(null, containerName);
|
||||
stream.setEncoding('utf8');
|
||||
|
||||
var layerProgress = layersToDownload.reduce(function (r, layer) {
|
||||
if (_.findWhere(images, {Id: layer.Id})) {
|
||||
r[layer.Id] = 100;
|
||||
} else {
|
||||
r[layer.Id] = 0;
|
||||
}
|
||||
return r;
|
||||
}, {});
|
||||
|
||||
self._progress[containerName] = 0;
|
||||
|
||||
stream.on('data', function (str) {
|
||||
console.log(str);
|
||||
var data = JSON.parse(str);
|
||||
|
||||
if (data.status === 'Already exists') {
|
||||
layerProgress[data.id] = 1;
|
||||
} else if (data.status === 'Downloading') {
|
||||
var current = data.progressDetail.current;
|
||||
var total = data.progressDetail.total;
|
||||
var layerFraction = current / total;
|
||||
layerProgress[data.id] = layerFraction;
|
||||
}
|
||||
|
||||
var chunks = layersToDownload.map(function (s) {
|
||||
return layerProgress[s.Id] * s.size;
|
||||
});
|
||||
|
||||
var totalReceived = chunks.reduce(function (pv, sv) {
|
||||
return pv + sv;
|
||||
});
|
||||
|
||||
var totalProgress = totalReceived / totalBytes;
|
||||
self._progress[containerName] = totalProgress;
|
||||
self.emit(self.PROGRESS);
|
||||
});
|
||||
stream.on('end', function () {
|
||||
self._createContainer(imageName, containerName, function () {
|
||||
delete self._progress[containerName];
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// If not then directly create the container
|
||||
self._createContainer(imageName, containerName, function () {
|
||||
callback(null, containerName);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
containers: function() {
|
||||
return this._containers;
|
||||
},
|
||||
container: function (name) {
|
||||
return this._containers[name];
|
||||
},
|
||||
recommended: function () {
|
||||
return this._recommended;
|
||||
},
|
||||
progress: function (name) {
|
||||
return this._progress[name];
|
||||
},
|
||||
logs: function (name) {
|
||||
return logs[name];
|
||||
},
|
||||
addChangeListener: function(eventType, callback) {
|
||||
this.on(eventType, callback);
|
||||
},
|
||||
removeChangeListener: function(eventType, callback) {
|
||||
this.removeListener(eventType, callback);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = ContainerStore;
|
|
@ -0,0 +1,66 @@
|
|||
var React = require('react/addons');
|
||||
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');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var ContainerList = require('./ContainerList.react');
|
||||
var Header = require('./Header.react');
|
||||
var async = require('async');
|
||||
var _ = require('underscore');
|
||||
var docker = require('./docker');
|
||||
var $ = require('jquery');
|
||||
|
||||
var Link = Router.Link;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
|
||||
var Containers = React.createClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
sidebarOffset: 0
|
||||
};
|
||||
},
|
||||
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
|
||||
});
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
var sidebarHeaderClass = 'sidebar-header';
|
||||
if (this.state.sidebarOffset) {
|
||||
sidebarHeaderClass += ' sep';
|
||||
}
|
||||
return (
|
||||
<div className="containers">
|
||||
<Header/>
|
||||
<div className="containers-body">
|
||||
<div className="sidebar">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Containers;
|
|
@ -0,0 +1,56 @@
|
|||
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();
|
||||
},
|
||||
handleMinimize: function () {
|
||||
remote.getCurrentWindow().minimize();
|
||||
},
|
||||
handleFullscreen: function () {
|
||||
remote.getCurrentWindow().setFullScreen(!remote.getCurrentWindow().isFullScreen());
|
||||
this.forceUpdate();
|
||||
},
|
||||
handleFullscreenHover: function () {
|
||||
this.update();
|
||||
},
|
||||
render: function () {
|
||||
var buttons;
|
||||
if (remote.getCurrentWindow().isFullScreen()) {
|
||||
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>
|
||||
);
|
||||
} 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Header;
|
|
@ -0,0 +1,37 @@
|
|||
var React = require('react/addons');
|
||||
|
||||
var Radial = React.createClass({
|
||||
render: function () {
|
||||
var percentage;
|
||||
if (this.props.progress && !this.props.spin) {
|
||||
percentage = (
|
||||
<div className="percentage"></div>
|
||||
);
|
||||
} else {
|
||||
percentage = <div></div>;
|
||||
}
|
||||
var classes = React.addons.classSet({
|
||||
'radial-progress': true,
|
||||
'radial-spinner': this.props.spin
|
||||
});
|
||||
return (
|
||||
<div className={classes} data-progress={this.props.progress}>
|
||||
<div className="circle">
|
||||
<div className="mask full">
|
||||
<div className="fill"></div>
|
||||
</div>
|
||||
<div className="mask half">
|
||||
<div className="fill"></div>
|
||||
<div className="fill fix"></div>
|
||||
</div>
|
||||
<div className="shadow"></div>
|
||||
</div>
|
||||
<div className="inset">
|
||||
{percentage}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Radial;
|
|
@ -0,0 +1,196 @@
|
|||
var React = require('react');
|
||||
var Router = require('react-router');
|
||||
var Radial = require('./Radial.react.js');
|
||||
var async = require('async');
|
||||
var assign = require('object-assign');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var boot2docker = require('./boot2docker.js');
|
||||
var virtualbox = require('./virtualbox.js');
|
||||
var util = require('./util.js');
|
||||
var docker = require('./docker.js');
|
||||
var ContainerStore = require('./ContainerStore.js');
|
||||
|
||||
var setupSteps = [
|
||||
{
|
||||
run: function (callback, progressCallback) {
|
||||
var installed = virtualbox.installed();
|
||||
if (!installed) {
|
||||
util.download('https://s3.amazonaws.com/kite-installer/' + virtualbox.INSTALLER_FILENAME, path.join(process.cwd(), 'resources', virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) {
|
||||
if (err) {callback(err); return;}
|
||||
virtualbox.install(function (err) {
|
||||
if (!virtualbox.installed()) {
|
||||
callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.');
|
||||
} else {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
}, function (progress) {
|
||||
progressCallback(progress);
|
||||
});
|
||||
} else {
|
||||
virtualbox.version(function (err, installedVersion) {
|
||||
if (err) {callback(err); return;}
|
||||
if (util.compareVersions(installedVersion, virtualbox.REQUIRED_VERSION) < 0) {
|
||||
// Download a newer version of Virtualbox
|
||||
util.downloadFile(Setup.BASE_URL + virtualbox.INSTALLER_FILENAME, path.join(util.getResourceDir(), virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) {
|
||||
if (err) {callback(err); return;}
|
||||
virtualbox.kill(function (err) {
|
||||
if (err) {callback(err); return;}
|
||||
virtualbox.install(function (err) {
|
||||
if (err) {callback(err); return;}
|
||||
virtualbox.version(function (err, installedVersion) {
|
||||
if (err) {callback(err); return;}
|
||||
if (util.compareVersions(installedVersion, virtualbox.REQUIRED_VERSION) < 0) {
|
||||
callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.');
|
||||
} else {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}, function (progress) {
|
||||
progressCallback(progress);
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
message: 'Downloading VirtualBox...'
|
||||
},
|
||||
{
|
||||
run: function (callback) {
|
||||
virtualbox.deleteVM('kitematic-vm', function (err, removed) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
},
|
||||
message: 'Cleaning up existing Docker VM...'
|
||||
},
|
||||
|
||||
// Initialize Boot2Docker if necessary.
|
||||
{
|
||||
run: function (callback) {
|
||||
boot2docker.exists(function (err, exists) {
|
||||
if (err) { callback(err); return; }
|
||||
if (!exists) {
|
||||
boot2docker.init(function (err) {
|
||||
callback(err);
|
||||
});
|
||||
} else {
|
||||
if (!boot2docker.sshKeyExists()) {
|
||||
callback('Boot2Docker SSH key doesn\'t exist. Fix by removing the existing Boot2Docker VM and re-run the installer. This usually occurs because an old version of Boot2Docker is installed.');
|
||||
} else {
|
||||
boot2docker.isoVersion(function (err, version) {
|
||||
if (err || util.compareVersions(version, boot2docker.version()) < 0) {
|
||||
boot2docker.stop(function(err) {
|
||||
boot2docker.upgrade(function (err) {
|
||||
callback(err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
message: 'Setting up the Docker VM...'
|
||||
},
|
||||
{
|
||||
run: function (callback) {
|
||||
boot2docker.waitWhileStatus('saving', function (err) {
|
||||
boot2docker.status(function (err, status) {
|
||||
if (err) {callback(err); return;}
|
||||
if (status !== 'running') {
|
||||
boot2docker.start(function (err) {
|
||||
callback(err);
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
message: 'Starting the Docker VM...'
|
||||
},
|
||||
{
|
||||
run: function (callback) {
|
||||
boot2docker.ip(function (err, ip) {
|
||||
if (err) {callback(err); return;}
|
||||
console.log('Setting host IP to: ' + ip);
|
||||
// Docker.setHost(ip);
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
message: 'Detecting Docker VM...'
|
||||
}
|
||||
];
|
||||
|
||||
var Setup = React.createClass({
|
||||
mixins: [ Router.Navigation ],
|
||||
render: function () {
|
||||
var radial;
|
||||
if (this.state.progress) {
|
||||
radial = <Radial progress={this.state.progress}/>;
|
||||
} else {
|
||||
radial = <Radial spin="true" progress="92"/>;
|
||||
}
|
||||
return (
|
||||
<div className="setup">
|
||||
{radial}
|
||||
<p>{this.state.message}</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
componentWillMount: function () {
|
||||
this.setState({});
|
||||
},
|
||||
componentDidMount: function () {
|
||||
var self = this;
|
||||
this.setup(function (err) {
|
||||
boot2docker.ip(function (err, ip) {
|
||||
docker.setHost(ip);
|
||||
self.transitionTo('containers');
|
||||
});
|
||||
});
|
||||
},
|
||||
setup: function (callback) {
|
||||
var self = this;
|
||||
var currentStep = 0;
|
||||
async.eachSeries(setupSteps, function (step, callback) {
|
||||
console.log('Performing step ' + currentStep);
|
||||
self.setState({progress: 0});
|
||||
self.setState({message: step.message});
|
||||
step.run(function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
currentStep += 1;
|
||||
callback();
|
||||
}
|
||||
}, function (progress) {
|
||||
self.setState({progress: progress});
|
||||
});
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
// if any of the steps fail
|
||||
console.log('Kitematic setup failed at step ' + currentStep);
|
||||
console.log(err);
|
||||
self.setState({error: err});
|
||||
callback(err);
|
||||
} else {
|
||||
// Setup Finished
|
||||
console.log('Setup finished.');
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Setup;
|
|
@ -0,0 +1,218 @@
|
|||
var exec = require('exec');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var async = require('async');
|
||||
|
||||
var cmdExec = function (cmd, callback) {
|
||||
exec(cmd, function (stderr, stdout, code) {
|
||||
if (code !== 0) {
|
||||
callback('Exit code ' + code + ': ' + stderr);
|
||||
} else {
|
||||
callback(null, stdout);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var homeDir = function () {
|
||||
return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
|
||||
};
|
||||
|
||||
var Boot2Docker = {
|
||||
version: function () {
|
||||
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'))['boot2docker-version'];
|
||||
},
|
||||
cliVersion: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'version'], function (err, out) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
var match = out.match(/version: v(\d+\.\d+\.\d+)/);
|
||||
if (!match || match.length < 2) {
|
||||
callback('Could not parse the boot2docker cli version.');
|
||||
} else {
|
||||
callback(null, match[1]);
|
||||
}
|
||||
});
|
||||
},
|
||||
isoVersion: function (callback) {
|
||||
fs.readFile(path.join(homeDir(), '.boot2docker', 'boot2docker.iso'), 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
var match = data.match(/Boot2Docker-v(\d+\.\d+\.\d+)/);
|
||||
if (!match) {
|
||||
callback('Could not parse boot2docker iso version');
|
||||
return;
|
||||
}
|
||||
callback (null, match[1]);
|
||||
});
|
||||
},
|
||||
command: function () {
|
||||
return path.join(process.cwd(), 'resources', 'boot2docker-' + this.version());
|
||||
},
|
||||
exists: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'info'], callback);
|
||||
},
|
||||
status: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'status'], function (err, out) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
callback(null, out.trim());
|
||||
});
|
||||
},
|
||||
init: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'init'], callback);
|
||||
},
|
||||
start: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'start'], callback);
|
||||
},
|
||||
stop: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'stop'], callback);
|
||||
},
|
||||
upgrade: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'upgrade'], callback);
|
||||
},
|
||||
ip: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'ip'], callback);
|
||||
},
|
||||
erase: function (callback) {
|
||||
var VMFileLocation = path.join(homeDir(), 'VirtualBox\\ VMs/boot2docker-vm');
|
||||
cmdExec(['rm', '-rf', VMFileLocation], callback);
|
||||
},
|
||||
state: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'info'], function (err, out) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var info = JSON.parse(out);
|
||||
callback(null, info.State);
|
||||
} catch (e) {
|
||||
callback(e, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
disk: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'ssh', 'df'], function (err, out) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var lines = out.split('\n');
|
||||
var dataline = _.find(lines, function (line) {
|
||||
return line.indexOf('/dev/sda1') !== -1;
|
||||
});
|
||||
var tokens = dataline.split(' ');
|
||||
tokens = tokens.filter(function (token) {
|
||||
return token !== '';
|
||||
});
|
||||
var usedGb = parseInt(tokens[2], 10) / 1000000;
|
||||
var totalGb = parseInt(tokens[3], 10) / 1000000;
|
||||
var percent = parseInt(tokens[4].replace('%', ''), 10);
|
||||
callback(null, {
|
||||
used_gb: usedGb.toFixed(2),
|
||||
total_gb: totalGb.toFixed(2),
|
||||
percent: percent
|
||||
});
|
||||
} catch (error) {
|
||||
callback(error, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
memory: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'ssh', 'free -m'], function (err, out) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var lines = out.split('\n');
|
||||
var dataline = _.find(lines, function (line) {
|
||||
return line.indexOf('-/+ buffers') !== -1;
|
||||
});
|
||||
var tokens = dataline.split(' ');
|
||||
tokens = tokens.filter(function(token) {
|
||||
return token !== '';
|
||||
});
|
||||
var usedGb = parseInt(tokens[2], 10) / 1000;
|
||||
var freeGb = parseInt(tokens[3], 10) / 1000;
|
||||
var totalGb = usedGb + freeGb;
|
||||
var percent = Math.round(usedGb / totalGb * 100);
|
||||
callback(null, {
|
||||
used_gb: usedGb.toFixed(2),
|
||||
total_gb: totalGb.toFixed(2),
|
||||
free_gb: freeGb.toFixed(2),
|
||||
percent: percent
|
||||
});
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
createScratchImage: function (callback) {
|
||||
cmdExec([Boot2Docker.command(), 'ssh', 'tar cv --files-from /dev/null | docker import - scratch'], function (err, out) {
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
stats: function (callback) {
|
||||
var self = this;
|
||||
self.state(function (err, state) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if (state === 'poweroff') {
|
||||
callback(null, {state: state});
|
||||
return;
|
||||
}
|
||||
self.memoryUsage(function (err, mem) {
|
||||
if (err) {
|
||||
callback(null, {state: state});
|
||||
return;
|
||||
}
|
||||
self.diskUsage(function (err, disk) {
|
||||
if (err) {
|
||||
callback(null, {state: state, memory: mem});
|
||||
return;
|
||||
}
|
||||
callback(null, {
|
||||
state: state,
|
||||
memory: mem,
|
||||
disk: disk
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
sshKeyExists: function () {
|
||||
return fs.existsSync(path.join(homeDir(), '.ssh', 'id_boot2docker'));
|
||||
},
|
||||
|
||||
// Todo: move me to setup
|
||||
waitWhileStatus: function (status, callback) {
|
||||
var current = status;
|
||||
async.whilst(function () {
|
||||
return current === status;
|
||||
}, function (callback) {
|
||||
Boot2Docker.status(function (err, vmStatus) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
current = vmStatus.trim();
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Boot2Docker;
|
|
@ -0,0 +1,28 @@
|
|||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var dockerode = require('dockerode');
|
||||
|
||||
var Docker = {
|
||||
host: null,
|
||||
_client: null,
|
||||
setHost: function(host) {
|
||||
this.host = host;
|
||||
var certDir = path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.boot2docker/certs/boot2docker-vm');
|
||||
if (!fs.existsSync(certDir)) {
|
||||
return;
|
||||
}
|
||||
this._client = new dockerode({
|
||||
protocol: 'https',
|
||||
host: this.host,
|
||||
port: 2376,
|
||||
ca: fs.readFileSync(path.join(certDir, 'ca.pem')),
|
||||
cert: fs.readFileSync(path.join(certDir, 'cert.pem')),
|
||||
key: fs.readFileSync(path.join(certDir, 'key.pem'))
|
||||
});
|
||||
},
|
||||
client: function () {
|
||||
return this._client;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Docker;
|
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 243 B |
After Width: | Height: | Size: 355 B |
After Width: | Height: | Size: 276 B |
After Width: | Height: | Size: 555 B |
After Width: | Height: | Size: 619 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 584 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 240 B |
After Width: | Height: | Size: 321 B |
After Width: | Height: | Size: 238 B |
After Width: | Height: | Size: 807 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 106 B |
After Width: | Height: | Size: 115 B |
After Width: | Height: | Size: 609 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 726 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 641 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 536 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 355 B |
After Width: | Height: | Size: 654 B |
After Width: | Height: | Size: 571 B |
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="main.css"/>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self' http://localhost:35729; style-src 'self' 'unsafe-inline';">
|
||||
</head>
|
||||
<body>
|
||||
<script src="main.js"></script>
|
||||
<script src="http://localhost:35729/livereload.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,69 @@
|
|||
var React = require('react');
|
||||
var Router = require('react-router');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
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 ContainerDetails = require('./ContainerDetails.react');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var Radial = require('./Radial.react');
|
||||
|
||||
var Route = Router.Route;
|
||||
var NotFoundRoute = Router.NotFoundRoute;
|
||||
var DefaultRoute = Router.DefaultRoute;
|
||||
var Link = Router.Link;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
|
||||
var App = React.createClass({
|
||||
render: function () {
|
||||
return (
|
||||
<RouteHandler/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var NoContainers = React.createClass({
|
||||
render: function () {
|
||||
return (
|
||||
<div>
|
||||
No Containers
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var routes = (
|
||||
<Route name="app" path="/" handler={App}>
|
||||
<Route name="containers" handler={Containers}>
|
||||
<Route name="container" path=":name" handler={ContainerDetails}>
|
||||
</Route>
|
||||
<DefaultRoute handler={NoContainers}/>
|
||||
</Route>
|
||||
<DefaultRoute handler={Setup}/>
|
||||
<Route name="setup" handler={Setup}>
|
||||
</Route>
|
||||
</Route>
|
||||
);
|
||||
|
||||
boot2docker.ip(function (err, ip) {
|
||||
if (window.location.hash !== '#/') {
|
||||
docker.setHost(ip);
|
||||
ContainerStore.init(function () {
|
||||
Router.run(routes, function (Handler) {
|
||||
React.render(<Handler/>, document.body);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Router.run(routes, function (Handler) {
|
||||
React.render(<Handler/>, document.body);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
Raven.config('https://0a5f032d745d4acaae94ce46f762c586@app.getsentry.com/35057', {
|
||||
}).install();
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
var async = require('async');
|
||||
var $ = require('jquery');
|
||||
|
||||
var Registry = {
|
||||
token: function(repository, callback) {
|
||||
$.ajax({
|
||||
url: 'https://registry.hub.docker.com/v1/repositories/' + repository + '/images',
|
||||
headers: {
|
||||
'X-Docker-Token': true,
|
||||
},
|
||||
success: function (res, status, xhr) {
|
||||
callback(null, xhr.getResponseHeader('X-Docker-Token'));
|
||||
},
|
||||
error: function (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
ancestry: function (imageId, token, callback) {
|
||||
$.ajax({
|
||||
url: 'https://registry-1.docker.io/v1/images/' + imageId + '/ancestry',
|
||||
headers: {
|
||||
Authorization: 'Token ' + token
|
||||
},
|
||||
success: function (layers, status, xhr) {
|
||||
callback(null, layers);
|
||||
},
|
||||
error: function (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
imageId: function (repository, tag, token, callback) {
|
||||
$.ajax({
|
||||
url: 'https://registry-1.docker.io/v1/repositories/' + repository + '/tags/' + tag,
|
||||
headers: {
|
||||
Authorization: 'Token ' + token
|
||||
},
|
||||
success: function (res, status, xhr) {
|
||||
callback(null, res);
|
||||
},
|
||||
error: function (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Returns an array [{Id: <12 character image ID, size: size of layer in bytes}]
|
||||
layers: function (repository, tag, callback) {
|
||||
var self = this;
|
||||
this.token(repository, function (err, token) {
|
||||
self.imageId(repository, tag, token, function (err, imageId) {
|
||||
self.ancestry(imageId, token, function (err, layers) {
|
||||
async.map(layers, function (layer, callback) {
|
||||
$.ajax({
|
||||
url: 'https://registry-1.docker.io/v1/images/' + layer + '/json',
|
||||
headers: {
|
||||
Authorization: 'Token ' + token
|
||||
},
|
||||
success: function (res, status, xhr) {
|
||||
var size = xhr.getResponseHeader('X-Docker-Size');
|
||||
callback(null, {
|
||||
Id: layer.slice(0, 12),
|
||||
size: parseInt(size, 10)
|
||||
});
|
||||
},
|
||||
error: function (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
callback('Could not sum' + err);
|
||||
return;
|
||||
}
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Registry;
|
|
@ -44,11 +44,17 @@
|
|||
}
|
||||
|
||||
// Account for badges in navs
|
||||
a.list-group-item.active > &,
|
||||
.list-group-item.active > &,
|
||||
.nav-pills > .active > a > & {
|
||||
color: @badge-active-color;
|
||||
background-color: @badge-active-bg;
|
||||
}
|
||||
.list-group-item > & {
|
||||
float: right;
|
||||
}
|
||||
.list-group-item > & + & {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.nav-pills > li > a > & {
|
||||
margin-left: 3px;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// Core variables and mixins
|
||||
@import "variables.less";
|
||||
@import "mixins.less";
|
||||
|
||||
// Reset and dependencies
|
||||
@import "normalize.less";
|
||||
@import "print.less";
|
||||
@import "glyphicons.less";
|
||||
|
||||
// Core CSS
|
||||
@import "scaffolding.less";
|
||||
@import "type.less";
|
||||
@import "code.less";
|
||||
@import "grid.less";
|
||||
@import "tables.less";
|
||||
@import "forms.less";
|
||||
@import "buttons.less";
|
||||
|
||||
// Components
|
||||
@import "component-animations.less";
|
||||
@import "dropdowns.less";
|
||||
@import "button-groups.less";
|
||||
@import "input-groups.less";
|
||||
@import "navs.less";
|
||||
@import "navbar.less";
|
||||
@import "breadcrumbs.less";
|
||||
@import "pagination.less";
|
||||
@import "pager.less";
|
||||
@import "labels.less";
|
||||
@import "badges.less";
|
||||
@import "jumbotron.less";
|
||||
@import "thumbnails.less";
|
||||
@import "alerts.less";
|
||||
@import "progress-bars.less";
|
||||
@import "media.less";
|
||||
@import "list-group.less";
|
||||
@import "panels.less";
|
||||
@import "responsive-embed.less";
|
||||
@import "wells.less";
|
||||
@import "close.less";
|
||||
|
||||
// Components w/ JavaScript
|
||||
@import "modals.less";
|
||||
@import "tooltip.less";
|
||||
@import "popovers.less";
|
||||
@import "carousel.less";
|
||||
|
||||
// Utility classes
|
||||
@import "utilities.less";
|
||||
@import "responsive-utilities.less";
|
|
@ -18,10 +18,6 @@
|
|||
&.active {
|
||||
z-index: 2;
|
||||
}
|
||||
&:focus {
|
||||
// Remove focus outline when dropdown JS adds it after closing the menu
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,7 +194,6 @@
|
|||
}
|
||||
|
||||
|
||||
|
||||
// Justified button groups
|
||||
// ----------------------
|
||||
|
||||
|
@ -226,15 +221,23 @@
|
|||
// Checkbox and radio options
|
||||
//
|
||||
// In order to support the browser's form validation feedback, powered by the
|
||||
// `required` attribute, we have to "hide" the inputs via `opacity`. We cannot
|
||||
// use `display: none;` or `visibility: hidden;` as that also hides the popover.
|
||||
// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use
|
||||
// `display: none;` or `visibility: hidden;` as that also hides the popover.
|
||||
// Simply visually hiding the inputs via `opacity` would leave them clickable in
|
||||
// certain cases which is prevented by using `clip` and `pointer-events`.
|
||||
// This way, we ensure a DOM element is visible to position the popover from.
|
||||
//
|
||||
// See https://github.com/twbs/bootstrap/pull/12794 for more.
|
||||
// See https://github.com/twbs/bootstrap/pull/12794 and
|
||||
// https://github.com/twbs/bootstrap/pull/14559 for more information.
|
||||
|
||||
[data-toggle="buttons"] > .btn > input[type="radio"],
|
||||
[data-toggle="buttons"] > .btn > input[type="checkbox"] {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
.opacity(0);
|
||||
[data-toggle="buttons"] {
|
||||
> .btn,
|
||||
> .btn-group > .btn {
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: absolute;
|
||||
clip: rect(0,0,0,0);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
font-weight: @btn-font-weight;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
|
||||
border: 1px solid transparent;
|
||||
|
@ -22,13 +23,15 @@
|
|||
&,
|
||||
&:active,
|
||||
&.active {
|
||||
&:focus {
|
||||
&:focus,
|
||||
&.focus {
|
||||
.tab-focus();
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
&:focus,
|
||||
&.focus {
|
||||
color: @btn-default-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -43,7 +46,7 @@
|
|||
&.disabled,
|
||||
&[disabled],
|
||||
fieldset[disabled] & {
|
||||
cursor: not-allowed;
|
||||
cursor: @cursor-disabled;
|
||||
pointer-events: none; // Future-proof disabling of clicks
|
||||
.opacity(.65);
|
||||
.box-shadow(none);
|
||||
|
@ -85,11 +88,11 @@
|
|||
.btn-link {
|
||||
color: @link-color;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
border-radius: 0;
|
||||
|
||||
&,
|
||||
&:active,
|
||||
&.active,
|
||||
&[disabled],
|
||||
fieldset[disabled] & {
|
||||
background-color: transparent;
|
|
@ -24,6 +24,30 @@
|
|||
&:extend(.img-responsive);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// WebKit CSS3 transforms for supported devices
|
||||
@media all and (transform-3d), (-webkit-transform-3d) {
|
||||
transition: transform .6s ease-in-out;
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000;
|
||||
|
||||
&.next,
|
||||
&.active.right {
|
||||
transform: translate3d(100%, 0, 0);
|
||||
left: 0;
|
||||
}
|
||||
&.prev,
|
||||
&.active.left {
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
left: 0;
|
||||
}
|
||||
&.next.left,
|
||||
&.prev.right,
|
||||
&.active {
|
||||
transform: translate3d(0, 0, 0);
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .active,
|
|
@ -32,6 +32,7 @@ kbd {
|
|||
kbd {
|
||||
padding: 0;
|
||||
font-size: 100%;
|
||||
font-weight: bold;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
|
@ -17,8 +17,9 @@
|
|||
|
||||
.collapse {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
|
||||
&.in { display: block; }
|
||||
&.in { display: block; visibility: visible; }
|
||||
tr&.in { display: table-row; }
|
||||
tbody&.in { display: table-row-group; }
|
||||
}
|
||||
|
@ -27,5 +28,7 @@
|
|||
position: relative;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
.transition(height .35s ease);
|
||||
.transition-property(~"height, visibility");
|
||||
.transition-duration(.35s);
|
||||
.transition-timing-function(ease);
|
||||
}
|
|
@ -103,16 +103,15 @@
|
|||
&:focus {
|
||||
color: @dropdown-link-disabled-color;
|
||||
}
|
||||
}
|
||||
// Nuke hover/focus effects
|
||||
.dropdown-menu > .disabled > a {
|
||||
|
||||
// Nuke hover/focus effects
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
background-image: none; // Remove CSS gradient
|
||||
.reset-filter();
|
||||
cursor: not-allowed;
|
||||
cursor: @cursor-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,7 +197,7 @@
|
|||
|
||||
// Component alignment
|
||||
//
|
||||
// Reiterate per navbar.import.less and the modified component alignment there.
|
||||
// Reiterate per navbar.less and the modified component alignment there.
|
||||
|
||||
@media (min-width: @grid-float-breakpoint) {
|
||||
.navbar-right {
|
||||
|
@ -212,4 +211,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +141,7 @@ output {
|
|||
&[disabled],
|
||||
&[readonly],
|
||||
fieldset[disabled] & {
|
||||
cursor: not-allowed;
|
||||
cursor: @cursor-disabled;
|
||||
background-color: @input-bg-disabled;
|
||||
opacity: 1; // iOS fix for unreadable disabled content
|
||||
}
|
||||
|
@ -168,23 +168,27 @@ input[type="search"] {
|
|||
// Special styles for iOS temporal inputs
|
||||
//
|
||||
// In Mobile Safari, setting `display: block` on temporal inputs causes the
|
||||
// text within the input to become vertically misaligned.
|
||||
// As a workaround, we set a pixel line-height that matches the
|
||||
// given height of the input. Since this fucks up everything else, we have to
|
||||
// appropriately reset it for Internet Explorer and the size variations.
|
||||
// text within the input to become vertically misaligned. As a workaround, we
|
||||
// set a pixel line-height that matches the given height of the input, but only
|
||||
// for Safari.
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
line-height: @input-height-base;
|
||||
// IE8+ misaligns the text within date inputs, so we reset
|
||||
line-height: @line-height-base ~"\0";
|
||||
|
||||
&.input-sm {
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
line-height: @input-height-base;
|
||||
}
|
||||
input[type="date"].input-sm,
|
||||
input[type="time"].input-sm,
|
||||
input[type="datetime-local"].input-sm,
|
||||
input[type="month"].input-sm {
|
||||
line-height: @input-height-small;
|
||||
}
|
||||
&.input-lg {
|
||||
input[type="date"].input-lg,
|
||||
input[type="time"].input-lg,
|
||||
input[type="datetime-local"].input-lg,
|
||||
input[type="month"].input-lg {
|
||||
line-height: @input-height-large;
|
||||
}
|
||||
}
|
||||
|
@ -208,11 +212,11 @@ input[type="month"] {
|
|||
.checkbox {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-height: @line-height-computed; // clear the floating input if there is no label text
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
label {
|
||||
min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text
|
||||
padding-left: 20px;
|
||||
margin-bottom: 0;
|
||||
font-weight: normal;
|
||||
|
@ -258,7 +262,7 @@ input[type="checkbox"] {
|
|||
&[disabled],
|
||||
&.disabled,
|
||||
fieldset[disabled] & {
|
||||
cursor: not-allowed;
|
||||
cursor: @cursor-disabled;
|
||||
}
|
||||
}
|
||||
// These classes are used directly on <label>s
|
||||
|
@ -266,7 +270,7 @@ input[type="checkbox"] {
|
|||
.checkbox-inline {
|
||||
&.disabled,
|
||||
fieldset[disabled] & {
|
||||
cursor: not-allowed;
|
||||
cursor: @cursor-disabled;
|
||||
}
|
||||
}
|
||||
// These classes are used on elements with <label> descendants
|
||||
|
@ -275,7 +279,7 @@ input[type="checkbox"] {
|
|||
&.disabled,
|
||||
fieldset[disabled] & {
|
||||
label {
|
||||
cursor: not-allowed;
|
||||
cursor: @cursor-disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -306,12 +310,14 @@ input[type="checkbox"] {
|
|||
// Build on `.form-control` with modifier classes to decrease or increase the
|
||||
// height and font-size of form controls.
|
||||
|
||||
.input-sm {
|
||||
.input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);
|
||||
.input-sm,
|
||||
.form-group-sm .form-control {
|
||||
.input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @input-border-radius-small);
|
||||
}
|
||||
|
||||
.input-lg {
|
||||
.input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);
|
||||
.input-lg,
|
||||
.form-group-lg .form-control {
|
||||
.input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @input-border-radius-large);
|
||||
}
|
||||
|
||||
|
||||
|
@ -331,7 +337,7 @@ input[type="checkbox"] {
|
|||
// Feedback icon (requires .glyphicon classes)
|
||||
.form-control-feedback {
|
||||
position: absolute;
|
||||
top: (@line-height-computed + 5); // Height of the `label` and its margin
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2; // Ensure icon is above input groups
|
||||
display: block;
|
||||
|
@ -339,6 +345,7 @@ input[type="checkbox"] {
|
|||
height: @input-height-base;
|
||||
line-height: @input-height-base;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.input-lg + .form-control-feedback {
|
||||
width: @input-height-large;
|
||||
|
@ -362,10 +369,15 @@ input[type="checkbox"] {
|
|||
.form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg);
|
||||
}
|
||||
|
||||
// Reposition feedback icon if input has visible label above
|
||||
.has-feedback label {
|
||||
|
||||
// Reposition feedback icon if label is hidden with "screenreader only" state
|
||||
.has-feedback label.sr-only ~ .form-control-feedback {
|
||||
top: 0;
|
||||
& ~ .form-control-feedback {
|
||||
top: (@line-height-computed + 5); // Height of the `label` and its margin
|
||||
}
|
||||
&.sr-only ~ .form-control-feedback {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -382,7 +394,6 @@ input[type="checkbox"] {
|
|||
}
|
||||
|
||||
|
||||
|
||||
// Inline forms
|
||||
//
|
||||
// Make forms appear inline(-block) by adding the `.form-inline` class. Inline
|
||||
|
@ -392,7 +403,7 @@ input[type="checkbox"] {
|
|||
// Requires wrapping inputs and labels with `.form-group` for proper display of
|
||||
// default HTML form controls and our custom form controls (e.g., input groups).
|
||||
//
|
||||
// Heads up! This is mixin-ed into `.navbar-form` in navbars.import.less.
|
||||
// Heads up! This is mixin-ed into `.navbar-form` in navbars.less.
|
||||
|
||||
.form-inline {
|
||||
|
||||
|
@ -412,6 +423,11 @@ input[type="checkbox"] {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Make static controls behave like regular ones
|
||||
.form-control-static {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: inline-table;
|
||||
vertical-align: middle;
|
||||
|
@ -453,10 +469,7 @@ input[type="checkbox"] {
|
|||
margin-left: 0;
|
||||
}
|
||||
|
||||
// Validation states
|
||||
//
|
||||
// Reposition the icon because it's now within a grid column and columns have
|
||||
// `position: relative;` on them. Also accounts for the grid gutter padding.
|
||||
// Re-override the feedback icon.
|
||||
.has-feedback .form-control-feedback {
|
||||
top: 0;
|
||||
}
|
||||
|
@ -509,7 +522,6 @@ input[type="checkbox"] {
|
|||
// Reposition the icon because it's now within a grid column and columns have
|
||||
// `position: relative;` on them. Also accounts for the grid gutter padding.
|
||||
.has-feedback .form-control-feedback {
|
||||
top: 0;
|
||||
right: (@grid-gutter-width / 2);
|
||||
}
|
||||
|
||||
|
@ -523,9 +535,6 @@ input[type="checkbox"] {
|
|||
padding-top: ((@padding-large-vertical * @line-height-large) + 1);
|
||||
}
|
||||
}
|
||||
.form-control {
|
||||
&:extend(.input-lg);
|
||||
}
|
||||
}
|
||||
.form-group-sm {
|
||||
@media (min-width: @screen-sm-min) {
|
||||
|
@ -533,8 +542,5 @@ input[type="checkbox"] {
|
|||
padding-top: (@padding-small-vertical + 1);
|
||||
}
|
||||
}
|
||||
.form-control {
|
||||
&:extend(.input-sm);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,7 +33,8 @@
|
|||
// Individual icons
|
||||
.glyphicon-asterisk { &:before { content: "\2a"; } }
|
||||
.glyphicon-plus { &:before { content: "\2b"; } }
|
||||
.glyphicon-euro { &:before { content: "\20ac"; } }
|
||||
.glyphicon-euro,
|
||||
.glyphicon-eur { &:before { content: "\20ac"; } }
|
||||
.glyphicon-minus { &:before { content: "\2212"; } }
|
||||
.glyphicon-cloud { &:before { content: "\2601"; } }
|
||||
.glyphicon-envelope { &:before { content: "\2709"; } }
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
|
||||
.jumbotron {
|
||||
padding: @jumbotron-padding;
|
||||
padding: @jumbotron-padding (@jumbotron-padding / 2);
|
||||
margin-bottom: @jumbotron-padding;
|
||||
color: @jumbotron-color;
|
||||
background-color: @jumbotron-bg;
|
||||
|
@ -23,7 +23,8 @@
|
|||
border-top-color: darken(@jumbotron-bg, 10%);
|
||||
}
|
||||
|
||||
.container & {
|
||||
.container &,
|
||||
.container-fluid & {
|
||||
border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container
|
||||
}
|
||||
|
||||
|
@ -32,10 +33,10 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: @screen-sm-min) {
|
||||
padding-top: (@jumbotron-padding * 1.6);
|
||||
padding-bottom: (@jumbotron-padding * 1.6);
|
||||
padding: (@jumbotron-padding * 1.6) 0;
|
||||
|
||||
.container & {
|
||||
.container &,
|
||||
.container-fluid & {
|
||||
padding-left: (@jumbotron-padding * 2);
|
||||
padding-right: (@jumbotron-padding * 2);
|
||||
}
|
|
@ -35,14 +35,6 @@
|
|||
margin-bottom: 0;
|
||||
.border-bottom-radius(@list-group-border-radius);
|
||||
}
|
||||
|
||||
// Align badges within list items
|
||||
> .badge {
|
||||
float: right;
|
||||
}
|
||||
> .badge + .badge {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -74,6 +66,7 @@ a.list-group-item {
|
|||
&.disabled:focus {
|
||||
background-color: @list-group-disabled-bg;
|
||||
color: @list-group-disabled-color;
|
||||
cursor: @cursor-disabled;
|
||||
|
||||
// Force color to inherit for custom content
|
||||
.list-group-item-heading {
|
|
@ -0,0 +1,47 @@
|
|||
.media {
|
||||
// Proper spacing between instances of .media
|
||||
margin-top: 15px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.media-right,
|
||||
.media > .pull-right {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.media-left,
|
||||
.media > .pull-left {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.media-left,
|
||||
.media-right,
|
||||
.media-body {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.media-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.media-bottom {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
// Reset margins on headings for tighter default spacing
|
||||
.media-heading {
|
||||
margin-top: 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
// Media list variation
|
||||
//
|
||||
// Undo default ul/ol styles
|
||||
.media-list {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Mixins
|
||||
// --------------------------------------------------
|
||||
|
||||
// Utilities
|
||||
@import "mixins/hide-text.less";
|
||||
@import "mixins/opacity.less";
|
||||
@import "mixins/image.less";
|
||||
@import "mixins/labels.less";
|
||||
@import "mixins/reset-filter.less";
|
||||
@import "mixins/resize.less";
|
||||
@import "mixins/responsive-visibility.less";
|
||||
@import "mixins/size.less";
|
||||
@import "mixins/tab-focus.less";
|
||||
@import "mixins/text-emphasis.less";
|
||||
@import "mixins/text-overflow.less";
|
||||
@import "mixins/vendor-prefixes.less";
|
||||
|
||||
// Components
|
||||
@import "mixins/alerts.less";
|
||||
@import "mixins/buttons.less";
|
||||
@import "mixins/panels.less";
|
||||
@import "mixins/pagination.less";
|
||||
@import "mixins/list-group.less";
|
||||
@import "mixins/nav-divider.less";
|
||||
@import "mixins/forms.less";
|
||||
@import "mixins/progress-bar.less";
|
||||
@import "mixins/table-row.less";
|
||||
|
||||
// Skins
|
||||
@import "mixins/background-variant.less";
|
||||
@import "mixins/border-radius.less";
|
||||
@import "mixins/gradients.less";
|
||||
|
||||
// Layout
|
||||
@import "mixins/clearfix.less";
|
||||
@import "mixins/center-block.less";
|
||||
@import "mixins/nav-vertical-align.less";
|
||||
@import "mixins/grid-framework.less";
|
||||
@import "mixins/grid.less";
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.focus,
|
||||
&:active,
|
||||
&.active,
|
||||
.open > .dropdown-toggle& {
|
||||
|
@ -28,6 +29,7 @@
|
|||
&,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.focus,
|
||||
&:active,
|
||||
&.active {
|
||||
background-color: @background;
|
|
@ -1,6 +1,6 @@
|
|||
// Form validation states
|
||||
//
|
||||
// Used in forms.import.less to generate the form validation CSS for warnings, errors,
|
||||
// Used in forms.less to generate the form validation CSS for warnings, errors,
|
||||
// and successes.
|
||||
|
||||
.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) {
|
||||
|
@ -10,7 +10,11 @@
|
|||
.radio,
|
||||
.checkbox,
|
||||
.radio-inline,
|
||||
.checkbox-inline {
|
||||
.checkbox-inline,
|
||||
&.radio label,
|
||||
&.checkbox label,
|
||||
&.radio-inline label,
|
||||
&.checkbox-inline label {
|
||||
color: @text-color;
|
||||
}
|
||||
// Set the border and box shadow on specific inputs to match
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
.make-grid-columns() {
|
||||
// Common styles for all sizes of grid columns, widths 1-12
|
||||
.col(@index) when (@index = 1) { // initial
|
||||
.col(@index) { // initial
|
||||
@item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}";
|
||||
.col((@index + 1), @item);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@
|
|||
}
|
||||
|
||||
.float-grid-columns(@class) {
|
||||
.col(@index) when (@index = 1) { // initial
|
||||
.col(@index) { // initial
|
||||
@item: ~".col-@{class}-@{index}";
|
||||
.col((@index + 1), @item);
|
||||
}
|
|
@ -8,7 +8,6 @@
|
|||
// Keep images from scaling beyond the width of their parents.
|
||||
.img-responsive(@display: block) {
|
||||
display: @display;
|
||||
width: 100% \9; // Force IE10 and below to size SVG images correctly
|
||||
max-width: 100%; // Part 1: Set a maximum relative to the parent
|
||||
height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.label-variant(@color) {
|
||||
background-color: @color;
|
||||
|
||||
|
||||
&[href] {
|
||||
&:hover,
|
||||
&:focus {
|
|
@ -1,7 +1,7 @@
|
|||
// Responsive utilities
|
||||
|
||||
//
|
||||
// More easily include all the states for responsive-utilities.import.less.
|
||||
// More easily include all the states for responsive-utilities.less.
|
||||
.responsive-visibility() {
|
||||
display: block !important;
|
||||
table& { display: table; }
|