зеркало из https://github.com/docker/kitematic.git
WIP beta
This commit is contained in:
Родитель
90ee518905
Коммит
e2346cc012
|
@ -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) {});
|
||||
|
|
24
gulpfile.js
24
gulpfile.js
|
@ -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/'
|
||||
}));
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 35 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 89 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 37 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 94 KiB |
Двоичный файл не отображается.
|
@ -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) {
|
||||
|
|
31
src/Main.js
31
src/Main.js
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче