Refactored out Meteor into React.js. WIP

This commit is contained in:
Jeffrey Morgan 2015-01-21 21:09:12 -05:00
commit db38e51128
288 changed files with 13719 additions and 9461 deletions

11
.gitignore vendored
View File

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

View File

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

View File

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

105
app/ContainerList.react.js Normal file
View File

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

126
app/ContainerModal.react.js Normal file
View File

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

308
app/ContainerStore.js Normal file
View File

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

66
app/Containers.react.js Normal file
View File

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

56
app/Header.react.js Normal file
View File

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

37
app/Radial.react.js Normal file
View File

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

196
app/Setup.react.js Normal file
View File

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

218
app/boot2docker.js Normal file
View File

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

28
app/docker.js Normal file
View File

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

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/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

BIN
app/images/downloading.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
app/images/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
app/images/fullscreen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

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/minimize.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

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/paused.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 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

BIN
app/images/stopped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

11
app/index.html Normal file
View File

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

69
app/main.js Normal file
View File

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

84
app/registry.js Normal file
View File

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

View File

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

50
app/styles/bootstrap/bootstrap.less vendored Executable file
View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,7 @@ kbd {
kbd {
padding: 0;
font-size: 100%;
font-weight: bold;
box-shadow: none;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

47
app/styles/bootstrap/media.less Executable file
View File

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

View File

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

View File

@ -10,6 +10,7 @@
&:hover,
&:focus,
&.focus,
&:active,
&.active,
.open > .dropdown-toggle& {
@ -28,6 +29,7 @@
&,
&:hover,
&:focus,
&.focus,
&:active,
&.active {
background-color: @background;

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
.label-variant(@color) {
background-color: @color;
&[href] {
&:hover,
&:focus {

View File

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

Some files were not shown because too many files have changed in this diff Show More