This commit is contained in:
Jeffrey Morgan 2015-02-03 17:45:16 -05:00
Родитель 90ee518905
Коммит e2346cc012
21 изменённых файлов: 432 добавлений и 143 удалений

2
.gitignore поставляемый
Просмотреть файл

@ -12,3 +12,5 @@ resources/boot2docker*
# Cache
cache
resources/settings*

Просмотреть файл

@ -11,7 +11,7 @@ var BrowserWindow = require('browser-window');
var ipc = require('ipc');
var argv = require('minimist')(process.argv);
var saveVMOnQuit = false;
var settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8'));
process.env.NODE_PATH = __dirname + '/../node_modules';
process.env.RESOURCES_PATH = __dirname + '/../resources';
@ -41,10 +41,12 @@ app.on('ready', function() {
show: false
});
var saveVMOnQuit = false;
if (argv.test) {
mainWindow.loadUrl('file://' + __dirname + '/../tests/tests.html');
mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'tests/tests.html')));
} else {
mainWindow.loadUrl('file://' + __dirname + '/../build/index.html');
mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'build/index.html')));
app.on('will-quit', function (e) {
if (saveVMOnQuit) {
exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {});

0
gulp
Просмотреть файл

Просмотреть файл

@ -17,12 +17,15 @@ var packagejson = require('./package.json');
var dependencies = Object.keys(packagejson.dependencies);
var devDependencies = Object.keys(packagejson.devDependencies);
var isBeta = process.argv.indexOf('--beta') !== -1;
var options = {
dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1,
test: process.argv.indexOf('test') !== -1,
integration: process.argv.indexOf('--integration') !== -1,
filename: 'Kitematic.app',
name: 'Kitematic'
beta: isBeta,
filename: isBeta ? 'Kitematic (Beta).app' : 'Kitematic.app',
name: isBeta ? 'Kitematic (Beta)' : 'Kitematic',
icon: isBeta ? 'kitematic-beta.icns' : 'kitematic.icns'
};
gulp.task('js', function () {
@ -83,11 +86,13 @@ gulp.task('dist', function (cb) {
'cp -R ./cache/Atom.app ./dist/osx/<%= filename %>',
'mv ./dist/osx/<%= filename %>/Contents/MacOS/Atom ./dist/osx/<%= filename %>/Contents/MacOS/<%= name %>',
'mkdir -p ./dist/osx/<%= filename %>/Contents/Resources/app',
'mkdir -p ./dist/osx/<%= filename %>/Contents/Resources/app/node_modules',
'cp -R browser dist/osx/<%= filename %>/Contents/Resources/app',
'cp package.json dist/osx/<%= filename %>/Contents/Resources/app/',
'cp settings.json dist/osx/<%= filename %>/Contents/Resources/app/',
'mkdir -p dist/osx/<%= filename %>/Contents/Resources/app/resources',
'cp -v resources/* dist/osx/<%= filename %>/Contents/Resources/app/resources/ || :',
'cp kitematic.icns dist/osx/<%= filename %>/Contents/Resources/atom.icns',
'cp <%= icon %> dist/osx/<%= filename %>/Contents/Resources/atom.icns',
'/usr/libexec/PlistBuddy -c "Set :CFBundleVersion <%= version %>" dist/osx/<%= filename %>/Contents/Info.plist',
'/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist',
'/usr/libexec/PlistBuddy -c "Set :CFBundleName <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist',
@ -95,10 +100,11 @@ gulp.task('dist', function (cb) {
'/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist'
], {
templateData: {
filename: options.filename,
name: options.name,
filename: options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)'),
name: options.name.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)'),
version: packagejson.version,
bundle: 'com.kitematic.app'
bundle: 'com.kitematic.app',
icon: options.icon
}
}));
@ -107,7 +113,7 @@ gulp.task('dist', function (cb) {
'cp -R node_modules/' + d + ' dist/osx/<%= filename %>/Contents/Resources/app/node_modules/'
], {
templateData: {
filename: options.filename
filename: options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)')
}
}));
});
@ -119,7 +125,7 @@ gulp.task('sign', function () {
try {
var signing_identity = fs.readFileSync('./identity', 'utf8').trim();
return gulp.src('').pipe(shell([
'codesign --deep --force --verbose --sign "' + signing_identity + '" ' + options.filename
'codesign --deep --force --verbose --sign "' + signing_identity + '" ' + options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)')
], {
cwd: './dist/osx/'
}));
@ -130,7 +136,7 @@ gulp.task('sign', function () {
gulp.task('zip', function () {
return gulp.src('').pipe(shell([
'ditto -c -k --sequesterRsrc --keepParent ' + options.filename + ' ' + options.name + '-' + packagejson.version + '.zip'
'ditto -c -k --sequesterRsrc --keepParent ' + options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)') + ' ' + options.name.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)') + '-' + packagejson.version + '.zip'
], {
cwd: './dist/osx/'
}));

Двоичные данные
images/boot2docker.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 35 KiB

Двоичные данные
images/boot2docker@2x.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 89 KiB

Двоичные данные
images/virtualbox.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 37 KiB

Двоичные данные
images/virtualbox@2x.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 94 KiB

Двоичные данные
kitematic-beta.icns Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -15,7 +15,8 @@
"test": "gulp test --silent",
"test:integration": "gulp test --silent --integration",
"all-tests": "npm test && npm run integration-tests",
"release": "gulp run release",
"release": "gulp release",
"release:beta": "gulp release --beta",
"preinstall": "./deps"
},
"licenses": [
@ -55,7 +56,8 @@
"request": "^2.51.0",
"request-progress": "0.3.1",
"retina.js": "^1.1.0",
"underscore": "^1.7.0"
"underscore": "^1.7.0",
"rimraf": "^2.2.8"
},
"devDependencies": {
"browserify": "^6.2.0",
@ -80,7 +82,6 @@
"gulp-uglifyjs": "^0.5.0",
"gulp-util": "^3.0.0",
"reactify": "^0.15.2",
"rimraf": "^2.2.8",
"run-sequence": "^1.0.2",
"time-require": "^0.1.2",
"vinyl-source-stream": "^0.1.1",

Просмотреть файл

@ -6,6 +6,8 @@ var exec = require('exec');
var path = require('path');
var assign = require('object-assign');
var remote = require('remote');
var rimraf = require('rimraf');
var fs = require('fs');
var dialog = remote.require('dialog');
var ContainerStore = require('./ContainerStore');
var ContainerUtil = require('./ContainerUtil');
@ -27,7 +29,6 @@ var ContainerDetails = React.createClass({
pendingEnv: {},
ports: {},
defaultPort: null,
volumes: {},
popoverVolumeOpen: false,
popoverViewOpen: false,
};
@ -71,19 +72,20 @@ var ContainerDetails = React.createClass({
this._oldHeight = parent[0].scrollHeight - parent.height();
}
var $viewDropdown = $(this.getDOMNode()).find('.dropdown-view');
var $viewDropdown = $(this.getDOMNode()).find('.dropdown-view > .icon-dropdown');
var $volumeDropdown = $(this.getDOMNode()).find('.dropdown-volume');
var $viewPopover = $(this.getDOMNode()).find('.popover-view');
var $volumePopover = $(this.getDOMNode()).find('.popover-volume');
if ($viewDropdown.offset() && $volumeDropdown.offset()) {
if ($viewDropdown.offset()) {
$viewPopover.offset({
top: $viewDropdown.offset().top + 32,
left: $viewDropdown.offset().left - ($viewPopover.outerWidth() / 2) + 14
top: $viewDropdown.offset().top + 27,
left: $viewDropdown.offset().left - ($viewPopover.outerWidth() / 2) + 5
});
}
if ($volumeDropdown.offset()) {
$volumePopover.offset({
top: $volumeDropdown.offset().top + 32,
top: $volumeDropdown.offset().top + 33,
left: $volumeDropdown.offset().left + $volumeDropdown.outerWidth() - $volumePopover.outerWidth() / 2 - 20
});
}
@ -96,17 +98,16 @@ var ContainerDetails = React.createClass({
this.setState({
progress: ContainerStore.progress(this.getParams().name),
env: ContainerUtil.env(container),
page: this.PAGE_LOGS
});
var ports = ContainerUtil.ports(container);
var webPorts = ['80', '8000', '8080', '3000', '5000', '2368'];
console.log(ports);
this.setState({
ports: ports,
defaultPort: _.find(_.keys(ports), function (port) {
return webPorts.indexOf(port) !== -1;
})
});
console.log(this.state);
this.updateLogs();
},
updateLogs: function (name) {
@ -136,8 +137,6 @@ var ContainerDetails = React.createClass({
},
handleView: function () {
if (this.state.defaultPort) {
console.log(this.state.defaultPort);
console.log(this.state.ports[this.state.defaultPort].url);
exec(['open', this.state.ports[this.state.defaultPort].url], function (err) {
if (err) { throw err; }
});
@ -148,14 +147,48 @@ var ContainerDetails = React.createClass({
if (err) { throw err; }
});
},
handleChangeDefaultPort: function (port) {
this.setState({
defaultPort: port
});
},
handleViewDropdown: function(e) {
this.setState({
popoverViewOpen: !this.state.popoverViewOpen
});
},
handleVolumeDropdown: function(e) {
this.setState({
popoverVolumeOpen: !this.state.popoverVolumeOpen
var self = this;
if (_.keys(this.props.container.Volumes).length) {
exec(['open', path.join(process.env.HOME, 'Kitematic', self.props.container.Name)], function (err) {
if (err) { throw err; }
});
}
},
handleChooseVolumeClick: function (dockerVol) {
var self = this;
dialog.showOpenDialog({properties: ['openDirectory', 'createDirectory']}, function (filenames) {
if (!filenames) {
return;
}
var directory = filenames[0];
if (directory) {
var volumes = _.clone(self.props.container.Volumes);
volumes[dockerVol] = directory;
var binds = _.pairs(volumes).map(function (pair) {
return pair[1] + ':' + pair[0];
});
ContainerStore.updateContainer(self.props.container.Name, {
Binds: binds
}, function (err) {
if (err) console.log(err);
});
}
});
},
handleOpenVolumeClick: function (path) {
exec(['open', path], function (err) {
if (err) { throw err; }
});
},
handleRestart: function () {
@ -165,16 +198,24 @@ var ContainerDetails = React.createClass({
},
handleTerminal: function () {
var container = this.props.container;
var terminal = path.join(process.cwd(), 'resources', 'terminal').replace(/ /g, '\\\\ ');
var cmd = [terminal, boot2docker.command().replace(/ /g, '\\\\ '), 'ssh', '-t', 'sudo', 'docker', 'exec', '-i', '-t', container.Name, 'bash'];
var terminal = path.join(process.cwd(), 'resources', 'terminal');
var cmd = [terminal, boot2docker.command().replace(/ /g, '\\\\\\\\ ').replace(/\(/g, '\\\\\\\\(').replace(/\)/g, '\\\\\\\\)'), 'ssh', '-t', 'sudo', 'docker', 'exec', '-i', '-t', container.Name, 'sh'];
exec(cmd, function (stderr, stdout, code) {
console.log(stderr);
console.log(stdout);
if (code) {
console.log(stderr);
}
});
},
handleSaveContainerName: function () {
if (newName === this.props.container.Name) {
return;
}
var newName = $('#input-container-name').val();
if (fs.existsSync(path.join(process.env.HOME, 'Kitematic', this.props.container.Name))) {
fs.renameSync(path.join(process.env.HOME, 'Kitematic', this.props.container.Name), path.join(process.env.HOME, 'Kitematic', newName));
}
ContainerStore.updateContainer(this.props.container.Name, {
name: newName
}, function (err) {
@ -238,6 +279,12 @@ var ContainerDetails = React.createClass({
message: 'Are you sure you want to delete this container?',
buttons: ['Delete', 'Cancel']
}, function (index) {
var volumePath = path.join(process.env.HOME, 'Kitematic', this.props.container.Name);
if (fs.existsSync(volumePath)) {
rimraf(volumePath, function (err) {
console.log(err);
});
}
if (index === 0) {
ContainerStore.remove(this.props.container.Name, function (err) {
console.error(err);
@ -313,7 +360,7 @@ var ContainerDetails = React.createClass({
btn: true,
'btn-action': true,
'with-icon': true,
disabled: this.props.container.State.Restarting
disabled: this.props.container.State.Downloading || this.props.container.State.Restarting
});
var viewButtonClass = React.addons.classSet({
@ -323,6 +370,17 @@ var ContainerDetails = React.createClass({
disabled: !this.props.container.State.Running || !this.state.defaultPort
});
var kitematicVolumes = _.pairs(this.props.container.Volumes).filter(function (pair) {
return pair[1].indexOf(path.join(process.env.HOME, 'Kitematic')) !== -1;
});
var volumesButtonClass = React.addons.classSet({
btn: true,
'btn-action': true,
'with-icon': true,
disabled: !kitematicVolumes.length
});
var textButtonClasses = React.addons.classSet({
'btn': true,
'btn-action': true,
@ -362,15 +420,19 @@ var ContainerDetails = React.createClass({
disabled: !this.props.container.State.Running
};
var dropdownViewButtonClass = React.addons.classSet(assign({'dropdown-view': true}, dropdownClasses));
var dropdownVolumeButtonClass = React.addons.classSet(assign({'dropdown-volume': true}, dropdownClasses));
var body;
if (this.props.container.State.Downloading) {
body = (
<div className="details-progress">
<ProgressBar now={this.state.progress * 100} label="%(percent)s%" />
</div>
);
if (this.state.progress) {
body = (
<div className="details-progress">
<h3>Downloading</h3>
<ProgressBar now={this.state.progress * 100} label="%(percent)s%"/>
</div>
);
} else {
}
} else {
if (this.state.page === this.PAGE_LOGS) {
body = (
@ -381,14 +443,19 @@ var ContainerDetails = React.createClass({
</div>
);
} else {
var rename = (
<div>
<h3>Container Name</h3>
<div className="container-name">
<input id="input-container-name" type="text" className="line" placeholder="Container Name" defaultValue={this.props.container.Name}></input>
</div>
<a className="btn btn-action" onClick={this.handleSaveContainerName}>Save</a>
</div>
);
body = (
<div className="details-panel">
<div className="settings">
<h3>Container Name</h3>
<div className="container-name">
<input id="input-container-name" type="text" className="line" placeholder="Container Name" defaultValue={this.props.container.Name}></input>
</div>
<a className="btn btn-action" onClick={this.handleSaveContainerName}>Save</a>
{rename}
<h3>Environment Variables</h3>
<div className="env-vars-labels">
<div className="label-key">KEY</div>
@ -419,22 +486,41 @@ var ContainerDetails = React.createClass({
<div key={key} className="table-values">
<span className="value-left">{key}</span><span className="icon icon-arrow-right"></span>
<a className="value-right" onClick={self.handleViewLink.bind(self, val.url)}>{val.display}</a>
<input onChange={self.handleChangeDefaultPort.bind(self, key)} type="radio" checked={self.state.defaultPort === key}/> <label>Default</label>
</div>
);
});
var volumes = _.map(self.props.container.Volumes, function (val, key) {
if (!val || val.indexOf(process.env.HOME) === -1) {
val = 'No Host Folder';
val = <span>No folder<a className="btn btn-primary btn-xs" onClick={self.handleChooseVolumeClick.bind(self, key)}>Choose</a></span>;
} else {
val = <span><a className="value-right" onClick={self.handleOpenVolumeClick.bind(self, val)}>{val.replace(process.env.HOME, '~')}</a> <a className="btn btn-primary btn-xs" onClick={self.handleChooseVolumeClick.bind(self, key)}>Choose</a></span>;
}
return (
<div key={key} className="table-values">
<span className="value-left">{key}</span><span className="icon icon-arrow-right"></span>
<a className="value-right">{val.replace(process.env.HOME, '~')}</a>
{val}
</div>
);
});
var view;
if (this.state.defaultPort) {
view = (
<div className="action btn-group">
<a className={viewButtonClass} onClick={this.handleView}><span className="icon icon-preview-2"></span><span className="content">View</span></a>
<a className={dropdownViewButtonClass} onClick={this.handleViewDropdown}><span className="icon-dropdown icon icon-arrow-37"></span></a>
</div>
);
} else {
view = (
<div className="action">
<a className={dropdownViewButtonClass} onClick={this.handleViewDropdown}><span className="icon icon-preview-2"></span> <span className="content">Ports</span> <span className="icon-dropdown icon icon-arrow-37"></span></a>
</div>
);
}
return (
<div className="details">
<div className="details-header">
@ -442,12 +528,9 @@ var ContainerDetails = React.createClass({
<h1>{this.props.container.Name}</h1>{state}<h2 className="image-label">Image</h2><h2 className="image">{this.props.container.Config.Image}</h2>
</div>
<div className="details-header-actions">
<div className="action btn-group">
<a className={viewButtonClass} onClick={this.handleView}><span className="icon icon-preview-2"></span><span className="content">View</span></a>
<a className={dropdownViewButtonClass} onClick={this.handleViewDropdown}><span className="icon-dropdown icon icon-arrow-37"></span></a>
</div>
{view}
<div className="action">
<a className={dropdownVolumeButtonClass} onClick={this.handleVolumeDropdown}><span className="icon icon-folder-1"></span> <span className="content">Volumes</span> <span className="icon-dropdown icon icon-arrow-37"></span></a>
<a className={volumesButtonClass} onClick={this.handleVolumeDropdown}><span className="icon icon-folder-1"></span> <span className="content">Volumes</span></a>
</div>
<div className="action">
<a className={restartButtonClass} onClick={this.handleRestart}><span className="icon icon-refresh"></span> <span className="content">Restart</span></a>
@ -472,7 +555,7 @@ var ContainerDetails = React.createClass({
<Popover className={popoverVolumeClasses} placement="bottom">
<div className="table volumes">
<div className="table-labels">
<div className="label-left">DOCKER FOLDER</div>
<div className="label-left">DOCKER VOLUME</div>
<div className="label-right">MAC FOLDER</div>
</div>
{volumes}

Просмотреть файл

@ -82,17 +82,16 @@ var ContainerModal = React.createClass({
}
},
handleClick: function (name, event) {
this.props.onRequestHide();
ContainerStore.create(name, 'latest', function (err, containerName) {
if (err) {
throw err;
}
this.props.onRequestHide();
}.bind(this));
},
handleTagClick: function (tag, name, event) {
ContainerStore.create(name, tag, function (err, containerName) {
this.props.onRequestHide();
}.bind(this));
this.props.onRequestHide();
ContainerStore.create(name, tag, function (err, containerName) {});
},
handleDropdownClick: function (name, event) {
this.setState({

Просмотреть файл

@ -20,11 +20,11 @@ var _streams = {};
var _muted = {};
var ContainerStore = assign(EventEmitter.prototype, {
CLIENT_CONTAINER_EVENT: 'client_container',
CLIENT_CONTAINER_EVENT: 'client_container_event',
CLIENT_RECOMMENDED_EVENT: 'client_recommended_event',
SERVER_CONTAINER_EVENT: 'server_container',
SERVER_PROGRESS_EVENT: 'server_progress',
SERVER_LOGS_EVENT: 'server_logs',
SERVER_CONTAINER_EVENT: 'server_container_event',
SERVER_PROGRESS_EVENT: 'server_progress_event',
SERVER_LOGS_EVENT: 'server_logs_event',
_pullScratchImage: function (callback) {
var image = docker.client().getImage('scratch:latest');
image.inspect(function (err, data) {
@ -121,15 +121,24 @@ var ContainerStore = assign(EventEmitter.prototype, {
containerData.Image = containerData.Config.Image;
}
existing.kill(function (err, data) {
if (err) {
console.log(err);
}
existing.remove(function (err, data) {
if (err) {
console.log(err);
}
docker.client().getImage(containerData.Image).inspect(function (err, data) {
if (err) {
callback(err);
return;
}
var binds = [];
if (data.Config.Volumes) {
_.each(data.Config.Volumes, function (value, key) {
binds.push(path.join(process.env.HOME, 'Kitematic', containerData.name, key)+ ':' + key);
});
}
docker.client().createContainer(containerData, function (err, container) {
if (err) {
callback(err, null);
@ -311,11 +320,13 @@ var ContainerStore = assign(EventEmitter.prototype, {
var self = this;
$.ajax({
url: 'https://kitematic.com/recommended.json',
cache: false,
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) {
console.log(data);
var results = data.results;
callback(null, _.find(results, function (r) {
return r.name === repository;
@ -379,38 +390,27 @@ var ContainerStore = assign(EventEmitter.prototype, {
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) {
callback(err);
return;
}
_containers[containerName] = container;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create');
_muted[containerName] = true;
_progress[containerName] = 0;
self._pullImage(repository, tag, function () {
self._createContainer(containerName, {Image: imageName}, function (err, container) {
delete _progress[containerName];
_muted[containerName] = false;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName);
});
}, function (progress) {
_progress[containerName] = progress;
self.emit(self.SERVER_PROGRESS_EVENT, containerName);
});
callback(null, containerName);
});
} else {
// If not then directly create the container
self._createContainer(containerName, {Image: imageName}, function (err, container) {
self.emit(ContainerStore.CLIENT_CONTAINER_EVENT, containerName, 'create');
callback(null, containerName);
});
// Pull image
self._createPlaceholderContainer(imageName, containerName, function (err, container) {
if (err) {
callback(err);
return;
}
_containers[containerName] = container;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create');
_muted[containerName] = true;
_progress[containerName] = 0;
self._pullImage(repository, tag, function () {
self._createContainer(containerName, {Image: imageName}, function (err, container) {
delete _progress[containerName];
_muted[containerName] = false;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName);
});
}, function (progress) {
_progress[containerName] = progress;
self.emit(self.SERVER_PROGRESS_EVENT, containerName);
});
callback(null, containerName);
});
},
updateContainer: function (name, data, callback) {
@ -419,10 +419,10 @@ var ContainerStore = assign(EventEmitter.prototype, {
data.name = data.Name;
}
var fullData = assign(_containers[name], data);
console.log(fullData);
this._createContainer(name, fullData, function (err) {
callback(err);
_muted[name] = false;
this.emit(this.CLIENT_CONTAINER_EVENT, name);
callback(err);
}.bind(this));
},
restart: function (name, callback) {

Просмотреть файл

@ -1,22 +1,26 @@
var module = require('module');
require.main.paths.splice(0, 0, process.env.NODE_PATH);
var remote = require('remote');
var app = remote.require('app');
var ipc = require('ipc');
var React = require('react');
var Router = require('react-router');
var RetinaImage = require('react-retina-image');
var async = require('async');
var fs = require('fs');
var path = require('path');
var docker = require('./Docker');
var router = require('./router');
var boot2docker = require('./boot2docker');
var ContainerStore = require('./ContainerStore');
var SetupStore = require('./ContainerStore');
var Menu = require('./Menu');
var remote = require('remote');
var app = remote.require('app');
var ipc = require('ipc');
var Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute;
var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
var settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8'));
if (process.env.NODE_ENV === 'development') {
var script = document.createElement('script');
@ -24,19 +28,26 @@ if (process.env.NODE_ENV === 'development') {
script.src = 'http://localhost:35729/livereload.js';
var head = document.getElementsByTagName('head')[0];
head.appendChild(script);
} else {
var bugsnag = require('bugsnag-js');
bugsnag.apiKey = settingsjson.bugsnag;
bugsnag.autoNotify = true;
bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production';
bugsnag.notifyReleaseStages = ['production'];
bugsnag.appVersion = app.getVersion();
}
if (!window.location.hash.length || window.location.hash === '#/') {
router.run(function (Handler) {
React.render(<Handler/>, document.body);
});
SetupStore.run(function (err) {
router.transitionTo('setup');
boot2docker.ip(function (err, ip) {
if (err) console.log(err);
docker.setHost(ip);
router.transitionTo('containers');
ContainerStore.init(function (err) {
if (err) console.log(err);
router.run(function (Handler) {
React.render(<Handler/>, document.body);
});
router.transitionTo('containers');
});
});
});

Просмотреть файл

@ -6,24 +6,128 @@ var assign = require('object-assign');
var fs = require('fs');
var path = require('path');
var virtualbox = require('./Virtualbox');
var util = require('./Util');
var SetupStore = require('./SetupStore');
var RetinaImage = require('react-retina-image');
var Setup = React.createClass({
mixins: [ Router.Navigation ],
getInitialState: function () {
return {
message: '',
progress: 0
progress: 0,
name: ''
};
},
componentWillMount: function () {
SetupStore.on(SetupStore.PROGRESS_EVENT, this.update);
SetupStore.on(SetupStore.STEP_EVENT, this.update);
},
componentDidMount: function () {
},
update: function () {
this.setState({
progress: SetupStore.stepProgress(),
step: SetupStore.stepName()
});
},
renderDownloadingVirtualboxStep: function () {
var message = 'Kitematic needs VirtualBox to run containers. VirtualBox is being downloaded from Oracle\'s website.';
return (
<div className="setup">
<div className="image">
<div className="contents">
<RetinaImage img src="virtualbox.png"/>
<div className="detail">
<Radial progress={this.state.progress}/>
</div>
</div>
</div>
<div className="desc">
<div className="content">
<h1>Downloading VirtualBox</h1>
<p>{message}</p>
</div>
</div>
</div>
);
},
renderInstallingVirtualboxStep: function () {
var message = 'VirtualBox is being installed. Administrative privileges are required.';
return (
<div className="setup">
<div className="image">
<div className="contents">
<RetinaImage img src="virtualbox.png"/>
<div className="detail">
<Radial progress="90" spin="true"/>
</div>
</div>
</div>
<div className="desc">
<div className="content">
<h1>Installing VirtualBox</h1>
<p>{message}</p>
</div>
</div>
</div>
);
},
renderInitBoot2DockerStep: function () {
var message = 'Containers run in a virtual machine provided by Boot2Docker. Kitematic is setting up that Linux VM.';
return (
<div className="setup">
<div className="image">
<div className="contents">
<RetinaImage img src="boot2docker.png"/>
<div className="detail">
<Radial progress="90" spin="true"/>
</div>
</div>
</div>
<div className="desc">
<div className="content">
<h1>Setting up the Docker VM</h1>
<p>{message}</p>
</div>
</div>
</div>
);
},
renderStartBoot2DockerStep: function () {
var message = 'Kitematic is starting the Boot2Docker Linux VM.';
return (
<div className="setup">
<div className="image">
<div className="contents">
<RetinaImage img src="boot2docker.png"/>
<div className="detail">
<Radial progress="90" spin="true"/>
</div>
</div>
</div>
<div className="desc">
<div className="content">
<h1>Starting the Docker VM</h1>
<p>{message}</p>
</div>
</div>
</div>
);
},
renderStep: function () {
switch(this.state.step) {
case 'downloading_virtualbox':
return this.renderDownloadingVirtualboxStep();
case 'installing_virtualbox':
return this.renderInstallingVirtualboxStep();
case 'cleanup_kitematic':
return this.renderInitBoot2DockerStep();
case 'init_boot2docker':
return this.renderInitBoot2DockerStep();
case 'start_boot2docker':
return this.renderStartBoot2DockerStep();
default:
return false;
}
},
render: function () {
var radial;
@ -34,6 +138,9 @@ var Setup = React.createClass({
} else {
radial = <Radial spin="true" progress="100"/>;
}
var step = this.renderStep();
if (this.state.error) {
return (
<div className="setup">
@ -42,12 +149,7 @@ var Setup = React.createClass({
</div>
);
} else {
return (
<div className="setup">
{radial}
<p>{this.state.message}</p>
</div>
);
return step;
}
}
});

Просмотреть файл

@ -11,7 +11,7 @@ var packagejson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package
var _currentStep = null;
var _error = null;
var _progress = null;
var _progress = 0;
var SetupStore = assign(EventEmitter.prototype, {
PROGRESS_EVENT: 'setup_progress',
@ -48,26 +48,31 @@ var SetupStore = assign(EventEmitter.prototype, {
}
},
name: 'downloading_virtualbox',
message: 'Downloading Virtualbox',
},
installVirtualboxStep: {
_install: function (callback) {
exec(['hdiutil', 'attach', path.join(setupUtil.supportDir(), 'VirtualBox-4.3.20-96996-OSX.dmg')], function (stderr, stdout, code) {
console.log('attaching');
exec(['hdiutil', 'attach', path.join(setupUtil.supportDir(), packagejson['virtualbox-filename'])], function (stderr, stdout, code) {
if (code) {
callback(stderr);
return;
}
console.log('Attached.');
var iconPath = path.join(setupUtil.resourceDir(), 'kitematic.icns');
setupUtil.isSudo(function (err, isSudo) {
console.log(isSudo);
sudoCmd = isSudo ? ['sudo'] : [path.join(setupUtil.resourceDir(), 'cocoasudo'), '--icon=' + iconPath, '--prompt=Kitematic requires administrative privileges to install VirtualBox and copy itself to the Applications folder.'];
sudoCmd.push.apply(sudoCmd, ['installer', '-pkg', '/Volumes/VirtualBox/VirtualBox.pkg', '-target', '/']);
exec(sudoCmd, function (stderr, stdout, code) {
console.log(stdout);
console.log('Ran installer.');
if (code) {
console.log(stderr);
console.log(stdout);
callback('Could not install virtualbox.');
} else {
exec(['hdiutil', 'detach', '/Volumes/VirtualBox'], function(stderr, stdout, code) {
console.log('detaching');
if (code) {
callback(stderr);
} else {
@ -101,7 +106,6 @@ var SetupStore = assign(EventEmitter.prototype, {
}
},
name: 'installing_virtualbox',
message: 'Installing VirtualBox',
},
cleanupKitematicStep: {
run: function (callback) {
@ -113,7 +117,6 @@ var SetupStore = assign(EventEmitter.prototype, {
});
},
name: 'cleanup_kitematic',
message: 'Cleaning up existing Kitematic install...'
},
initBoot2DockerStep: {
run: function (callback) {
@ -143,7 +146,6 @@ var SetupStore = assign(EventEmitter.prototype, {
});
},
name: 'init_boot2docker',
message: 'Setting up the Docker VM...'
},
startBoot2DockerStep: {
run: function (callback) {
@ -161,31 +163,30 @@ var SetupStore = assign(EventEmitter.prototype, {
});
},
name: 'start_boot2docker',
message: 'Starting the Docker VM...'
},
step: function () {
return _currentStep;
stepName: function () {
return _currentStep.name;
},
progress: function () {
stepProgress: function () {
return _progress;
},
run: function (callback) {
var self = this;
var steps = [this.downloadVirtualboxStep, this.installVirtualboxStep, this.cleanupKitematicStep, this.initBoot2DockerStep, this.startBoot2DockerStep];
async.eachSeries(steps, function (step, callback) {
console.log(step.name);
_currentStep = step;
_progress = null;
_progress = 0;
self.emit(self.STEP_EVENT);
step.run(function (err) {
if (err) {
callback(err);
} else {
self.emit(self.STEP_EVENT);
callback();
}
}, function (progress) {
self.emit(self.PROGRESS_EVENT, progress);
_progress = progress;
self.emit(self.PROGRESS_EVENT, progress);
});
}, function (err) {
if (err) {

Просмотреть файл

@ -2,7 +2,6 @@ var fs = require('fs');
var exec = require('exec');
var path = require('path');
var async = require('async');
var util = require('./Util');
var VirtualBox = {
command: function () {

Просмотреть файл

@ -1,11 +1,11 @@
.popover {
&.popover-view {
min-width: 290px;
min-width: 364px;
}
&.popover-volume {
min-width: 400px;
min-width: 480px;
}
.popover-content {
@ -53,9 +53,15 @@
flex: 0 auto;
}
.value-right {
flex: 1 auto;
position: relative;
flex: 0 auto;
-webkit-user-select: text;
width: 154px;
white-space: nowrap;
a {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.table-new {
@ -92,6 +98,13 @@
}
}
&.ports {
input {
margin-left: 6px;
margin-right: 5px;
}
}
&.volumes {
.label-left {
min-width: 120px;
@ -99,10 +112,22 @@
.value-left {
min-width: 120px;
}
.icon {
.icon-arrow-right {
color: #aaa;
margin: 2px 9px 0;
}
.icon-folder-1 {
position: relative;
top: 4px;
font-size: 16px;
padding-right: 4px;
}
.btn-xs {
padding: 1px 5px;
font-size: 12px;
line-height: 1.5;
height: 22px;
}
}
}
@ -195,15 +220,14 @@
color: inherit;
flex-shrink: 0;
cursor: default;
margin: 0px 3px 0px 8px;
margin: 0px 3px 0px 0;
outline: none;
padding: 4px 5px;
padding: 4px 13px;
&.active {
background: @brand-primary;
li {
border-bottom: none;
border-radius: 40px;
background: @brand-primary;
.name {
color: white;
}
@ -388,6 +412,11 @@
border-bottom: 1px solid transparent;
transition: border-bottom 0.25s;
.action {
a {
-webkit-transition: none;
}
transition: none;
flex: 0 auto;
margin-right: 24px;
}
@ -442,6 +471,7 @@
.details-progress {
margin: 26% auto 0;
text-align: center;
width: 300px;
}

Просмотреть файл

@ -12,16 +12,16 @@
.radial-progress {
&.radial-spinner {
-webkit-animation: rotating 1.2s linear infinite;
-webkit-animation: rotating 2.4s linear infinite;
}
@circle-size: 96px;
@circle-background: transparent;
@inset-size: 92px;
@circle-size: 140px;
@circle-background: #F2F2F2;
@inset-size: 136px;
@inset-color: white;
@transition-length: 1s;
// @percentage-color: #3FD899;
@percentage-font-size: 14px;
@percentage-font-size: 24px;
@percentage-text-width: 57px;
margin: 0 auto;
@ -70,7 +70,7 @@
line-height: 1;
text-align: center;
// color: @percentage-color;
color: @brand-primary;
font-weight: 500;
font-size: @percentage-font-size;
}

Просмотреть файл

@ -1,6 +1,52 @@
.setup {
margin-top: 25%;
text-align: center;
display: flex;
height: 100%;
width: 100%;
flex-direction: row;
-webkit-app-region: drag;
.image {
display: flex;
width: 50%;
height: 100%;
flex: 0 auto;
align-items: center;
justify-content: flex-end;
padding-right: 40px;
.contents {
position: relative;
.detail {
position: absolute;
right: 0;
bottom: 0;
}
}
}
.desc {
display: flex;
width: 50%;
height: 100%;
align-items: center;
padding-left: 40px;
.content {
max-width: 320px;
h1 {
margin-top: -30px;
font-size: 24px;
}
p {
font-size: 13px;
color: @gray-normal;
}
}
}
p {
&.error {

Просмотреть файл

@ -87,6 +87,13 @@ input[type="text"] {
&:disabled,
&[disabled] {
opacity: 0.5;
background: none;
&.active {
background: none;
color: white;
box-shadow: none;
box-shadow: none;
}
}
}