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
cache cache
resources/settings*

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

@ -11,7 +11,7 @@ var BrowserWindow = require('browser-window');
var ipc = require('ipc'); var ipc = require('ipc');
var argv = require('minimist')(process.argv); 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.NODE_PATH = __dirname + '/../node_modules';
process.env.RESOURCES_PATH = __dirname + '/../resources'; process.env.RESOURCES_PATH = __dirname + '/../resources';
@ -41,10 +41,12 @@ app.on('ready', function() {
show: false show: false
}); });
var saveVMOnQuit = false;
if (argv.test) { if (argv.test) {
mainWindow.loadUrl('file://' + __dirname + '/../tests/tests.html'); mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'tests/tests.html')));
} else { } 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) { app.on('will-quit', function (e) {
if (saveVMOnQuit) { if (saveVMOnQuit) {
exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {}); 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 dependencies = Object.keys(packagejson.dependencies);
var devDependencies = Object.keys(packagejson.devDependencies); var devDependencies = Object.keys(packagejson.devDependencies);
var isBeta = process.argv.indexOf('--beta') !== -1;
var options = { var options = {
dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1, dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1,
test: process.argv.indexOf('test') !== -1, test: process.argv.indexOf('test') !== -1,
integration: process.argv.indexOf('--integration') !== -1, integration: process.argv.indexOf('--integration') !== -1,
filename: 'Kitematic.app', beta: isBeta,
name: 'Kitematic' filename: isBeta ? 'Kitematic (Beta).app' : 'Kitematic.app',
name: isBeta ? 'Kitematic (Beta)' : 'Kitematic',
icon: isBeta ? 'kitematic-beta.icns' : 'kitematic.icns'
}; };
gulp.task('js', function () { gulp.task('js', function () {
@ -83,11 +86,13 @@ gulp.task('dist', function (cb) {
'cp -R ./cache/Atom.app ./dist/osx/<%= filename %>', 'cp -R ./cache/Atom.app ./dist/osx/<%= filename %>',
'mv ./dist/osx/<%= filename %>/Contents/MacOS/Atom ./dist/osx/<%= filename %>/Contents/MacOS/<%= name %>', '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',
'mkdir -p ./dist/osx/<%= filename %>/Contents/Resources/app/node_modules',
'cp -R browser dist/osx/<%= filename %>/Contents/Resources/app', 'cp -R browser dist/osx/<%= filename %>/Contents/Resources/app',
'cp package.json 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', 'mkdir -p dist/osx/<%= filename %>/Contents/Resources/app/resources',
'cp -v resources/* 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 :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 :CFBundleDisplayName <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist',
'/usr/libexec/PlistBuddy -c "Set :CFBundleName <%= 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' '/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist'
], { ], {
templateData: { templateData: {
filename: options.filename, filename: options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)'),
name: options.name, name: options.name.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)'),
version: packagejson.version, 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/' 'cp -R node_modules/' + d + ' dist/osx/<%= filename %>/Contents/Resources/app/node_modules/'
], { ], {
templateData: { templateData: {
filename: options.filename filename: options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)')
} }
})); }));
}); });
@ -119,7 +125,7 @@ gulp.task('sign', function () {
try { try {
var signing_identity = fs.readFileSync('./identity', 'utf8').trim(); var signing_identity = fs.readFileSync('./identity', 'utf8').trim();
return gulp.src('').pipe(shell([ 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/' cwd: './dist/osx/'
})); }));
@ -130,7 +136,7 @@ gulp.task('sign', function () {
gulp.task('zip', function () { gulp.task('zip', function () {
return gulp.src('').pipe(shell([ 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/' 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": "gulp test --silent",
"test:integration": "gulp test --silent --integration", "test:integration": "gulp test --silent --integration",
"all-tests": "npm test && npm run integration-tests", "all-tests": "npm test && npm run integration-tests",
"release": "gulp run release", "release": "gulp release",
"release:beta": "gulp release --beta",
"preinstall": "./deps" "preinstall": "./deps"
}, },
"licenses": [ "licenses": [
@ -55,7 +56,8 @@
"request": "^2.51.0", "request": "^2.51.0",
"request-progress": "0.3.1", "request-progress": "0.3.1",
"retina.js": "^1.1.0", "retina.js": "^1.1.0",
"underscore": "^1.7.0" "underscore": "^1.7.0",
"rimraf": "^2.2.8"
}, },
"devDependencies": { "devDependencies": {
"browserify": "^6.2.0", "browserify": "^6.2.0",
@ -80,7 +82,6 @@
"gulp-uglifyjs": "^0.5.0", "gulp-uglifyjs": "^0.5.0",
"gulp-util": "^3.0.0", "gulp-util": "^3.0.0",
"reactify": "^0.15.2", "reactify": "^0.15.2",
"rimraf": "^2.2.8",
"run-sequence": "^1.0.2", "run-sequence": "^1.0.2",
"time-require": "^0.1.2", "time-require": "^0.1.2",
"vinyl-source-stream": "^0.1.1", "vinyl-source-stream": "^0.1.1",

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

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

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

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

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

@ -20,11 +20,11 @@ var _streams = {};
var _muted = {}; var _muted = {};
var ContainerStore = assign(EventEmitter.prototype, { var ContainerStore = assign(EventEmitter.prototype, {
CLIENT_CONTAINER_EVENT: 'client_container', CLIENT_CONTAINER_EVENT: 'client_container_event',
CLIENT_RECOMMENDED_EVENT: 'client_recommended_event', CLIENT_RECOMMENDED_EVENT: 'client_recommended_event',
SERVER_CONTAINER_EVENT: 'server_container', SERVER_CONTAINER_EVENT: 'server_container_event',
SERVER_PROGRESS_EVENT: 'server_progress', SERVER_PROGRESS_EVENT: 'server_progress_event',
SERVER_LOGS_EVENT: 'server_logs', SERVER_LOGS_EVENT: 'server_logs_event',
_pullScratchImage: function (callback) { _pullScratchImage: function (callback) {
var image = docker.client().getImage('scratch:latest'); var image = docker.client().getImage('scratch:latest');
image.inspect(function (err, data) { image.inspect(function (err, data) {
@ -121,15 +121,24 @@ var ContainerStore = assign(EventEmitter.prototype, {
containerData.Image = containerData.Config.Image; containerData.Image = containerData.Config.Image;
} }
existing.kill(function (err, data) { existing.kill(function (err, data) {
if (err) {
console.log(err);
}
existing.remove(function (err, data) { existing.remove(function (err, data) {
if (err) {
console.log(err);
}
docker.client().getImage(containerData.Image).inspect(function (err, data) { docker.client().getImage(containerData.Image).inspect(function (err, data) {
if (err) {
callback(err);
return;
}
var binds = []; var binds = [];
if (data.Config.Volumes) { if (data.Config.Volumes) {
_.each(data.Config.Volumes, function (value, key) { _.each(data.Config.Volumes, function (value, key) {
binds.push(path.join(process.env.HOME, 'Kitematic', containerData.name, key)+ ':' + key); binds.push(path.join(process.env.HOME, 'Kitematic', containerData.name, key)+ ':' + key);
}); });
} }
docker.client().createContainer(containerData, function (err, container) { docker.client().createContainer(containerData, function (err, container) {
if (err) { if (err) {
callback(err, null); callback(err, null);
@ -311,11 +320,13 @@ var ContainerStore = assign(EventEmitter.prototype, {
var self = this; var self = this;
$.ajax({ $.ajax({
url: 'https://kitematic.com/recommended.json', url: 'https://kitematic.com/recommended.json',
cache: false,
dataType: 'json', dataType: 'json',
success: function (res, status) { success: function (res, status) {
var recommended = res.recommended; var recommended = res.recommended;
async.map(recommended, function (repository, callback) { async.map(recommended, function (repository, callback) {
$.get('https://registry.hub.docker.com/v1/search?q=' + repository, function (data) { $.get('https://registry.hub.docker.com/v1/search?q=' + repository, function (data) {
console.log(data);
var results = data.results; var results = data.results;
callback(null, _.find(results, function (r) { callback(null, _.find(results, function (r) {
return r.name === repository; return r.name === repository;
@ -379,38 +390,27 @@ var ContainerStore = assign(EventEmitter.prototype, {
var imageName = repository + ':' + tag; var imageName = repository + ':' + tag;
var containerName = this._generateName(repository); var containerName = this._generateName(repository);
var image = docker.client().getImage(imageName); var image = docker.client().getImage(imageName);
// Pull image
image.inspect(function (err, data) { self._createPlaceholderContainer(imageName, containerName, function (err, container) {
if (!data) { if (err) {
// Pull image callback(err);
self._createPlaceholderContainer(imageName, containerName, function (err, container) { return;
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);
});
} }
_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) { updateContainer: function (name, data, callback) {
@ -419,10 +419,10 @@ var ContainerStore = assign(EventEmitter.prototype, {
data.name = data.Name; data.name = data.Name;
} }
var fullData = assign(_containers[name], data); var fullData = assign(_containers[name], data);
console.log(fullData);
this._createContainer(name, fullData, function (err) { this._createContainer(name, fullData, function (err) {
callback(err);
_muted[name] = false; _muted[name] = false;
this.emit(this.CLIENT_CONTAINER_EVENT, name);
callback(err);
}.bind(this)); }.bind(this));
}, },
restart: function (name, callback) { 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 React = require('react');
var Router = require('react-router'); var Router = require('react-router');
var RetinaImage = require('react-retina-image'); var RetinaImage = require('react-retina-image');
var async = require('async'); var fs = require('fs');
var path = require('path');
var docker = require('./Docker'); var docker = require('./Docker');
var router = require('./router'); var router = require('./router');
var boot2docker = require('./boot2docker'); var boot2docker = require('./boot2docker');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var SetupStore = require('./ContainerStore'); var SetupStore = require('./ContainerStore');
var Menu = require('./Menu'); var Menu = require('./Menu');
var remote = require('remote');
var app = remote.require('app');
var ipc = require('ipc');
var Route = Router.Route; var Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute; var NotFoundRoute = Router.NotFoundRoute;
var DefaultRoute = Router.DefaultRoute; var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link; var Link = Router.Link;
var RouteHandler = Router.RouteHandler; var RouteHandler = Router.RouteHandler;
var settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8'));
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
var script = document.createElement('script'); var script = document.createElement('script');
@ -24,19 +28,26 @@ if (process.env.NODE_ENV === 'development') {
script.src = 'http://localhost:35729/livereload.js'; script.src = 'http://localhost:35729/livereload.js';
var head = document.getElementsByTagName('head')[0]; var head = document.getElementsByTagName('head')[0];
head.appendChild(script); 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 === '#/') { if (!window.location.hash.length || window.location.hash === '#/') {
router.run(function (Handler) {
React.render(<Handler/>, document.body);
});
SetupStore.run(function (err) { SetupStore.run(function (err) {
router.transitionTo('setup');
boot2docker.ip(function (err, ip) { boot2docker.ip(function (err, ip) {
if (err) console.log(err); if (err) console.log(err);
docker.setHost(ip); docker.setHost(ip);
router.transitionTo('containers');
ContainerStore.init(function (err) { ContainerStore.init(function (err) {
if (err) console.log(err); router.transitionTo('containers');
router.run(function (Handler) {
React.render(<Handler/>, document.body);
});
}); });
}); });
}); });

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

@ -6,24 +6,128 @@ var assign = require('object-assign');
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var virtualbox = require('./Virtualbox'); var virtualbox = require('./Virtualbox');
var util = require('./Util');
var SetupStore = require('./SetupStore'); var SetupStore = require('./SetupStore');
var RetinaImage = require('react-retina-image');
var Setup = React.createClass({ var Setup = React.createClass({
mixins: [ Router.Navigation ], mixins: [ Router.Navigation ],
getInitialState: function () { getInitialState: function () {
return { return {
message: '', progress: 0,
progress: 0 name: ''
}; };
}, },
componentWillMount: function () { componentWillMount: function () {
SetupStore.on(SetupStore.PROGRESS_EVENT, this.update); SetupStore.on(SetupStore.PROGRESS_EVENT, this.update);
SetupStore.on(SetupStore.STEP_EVENT, this.update);
}, },
componentDidMount: function () { componentDidMount: function () {
}, },
update: 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 () { render: function () {
var radial; var radial;
@ -34,6 +138,9 @@ var Setup = React.createClass({
} else { } else {
radial = <Radial spin="true" progress="100"/>; radial = <Radial spin="true" progress="100"/>;
} }
var step = this.renderStep();
if (this.state.error) { if (this.state.error) {
return ( return (
<div className="setup"> <div className="setup">
@ -42,12 +149,7 @@ var Setup = React.createClass({
</div> </div>
); );
} else { } else {
return ( return step;
<div className="setup">
{radial}
<p>{this.state.message}</p>
</div>
);
} }
} }
}); });

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

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

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

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

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

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

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

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

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

@ -1,6 +1,52 @@
.setup { .setup {
margin-top: 25%; display: flex;
text-align: center; 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 { p {
&.error { &.error {

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

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