зеркало из https://github.com/docker/kitematic.git
Merge remote-tracking branch 'origin/sean-polish'
Conflicts: src/Radial.react.js src/Setup.react.js styles/radial.less
This commit is contained in:
Родитель
e8f87e1ebe
Коммит
c338678094
|
@ -16,7 +16,7 @@
|
|||
"release": "gulp release",
|
||||
"release:beta": "gulp release --beta",
|
||||
"preinstall": "./deps",
|
||||
"lint": "jsxhint src/**/* && jsxhint browser/**/*"
|
||||
"lint": "jsxhint src && jsxhint browser"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
|
|
|
@ -3,7 +3,10 @@ var React = require('react/addons');
|
|||
var ContainerDetailsHeader = React.createClass({
|
||||
render: function () {
|
||||
var state;
|
||||
if (this.props.container.State.Running) {
|
||||
if (!this.props.container) {
|
||||
return false;
|
||||
}
|
||||
if (this.props.container.State.Running && !this.props.container.State.Paused && !this.props.container.State.Restarting) {
|
||||
state = <span className="status running">RUNNING</span>;
|
||||
} else if (this.props.container.State.Restarting) {
|
||||
state = <span className="status restarting">RESTARTING</span>;
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
var exec = require('exec');
|
||||
var path = require('path');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var ContainerUtil = require('./ContainerUtil');
|
||||
var boot2docker = require('./Boot2Docker');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var Router = require('react-router');
|
||||
|
||||
var ContainerDetailsSubheader = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
defaultPort: null
|
||||
};
|
||||
},
|
||||
componentWillReceiveProps: function () {
|
||||
this.init();
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.init();
|
||||
},
|
||||
init: function () {
|
||||
this.setState({
|
||||
currentRoute: _.last(this.getRoutes()).name
|
||||
});
|
||||
var container = ContainerStore.container(this.getParams().name);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
var ports = ContainerUtil.ports(container);
|
||||
var webPorts = ['80', '8000', '8080', '3000', '5000', '2368'];
|
||||
this.setState({
|
||||
ports: ports,
|
||||
defaultPort: _.find(_.keys(ports), function (port) {
|
||||
return webPorts.indexOf(port) !== -1;
|
||||
})
|
||||
});
|
||||
},
|
||||
disableRun: function () {
|
||||
if (!this.props.container) {
|
||||
return false;
|
||||
}
|
||||
return (!this.props.container.State.Running || !this.state.defaultPort);
|
||||
},
|
||||
disableRestart: function () {
|
||||
if (!this.props.container) {
|
||||
return false;
|
||||
}
|
||||
return (this.props.container.State.Downloading || this.props.container.State.Restarting);
|
||||
},
|
||||
disableTerminal: function () {
|
||||
if (!this.props.container) {
|
||||
return false;
|
||||
}
|
||||
return (!this.props.container.State.Running);
|
||||
},
|
||||
disableTab: function () {
|
||||
if (!this.props.container) {
|
||||
return false;
|
||||
}
|
||||
return (this.props.container.State.Downloading);
|
||||
},
|
||||
showHome: function () {
|
||||
if (!this.disableTab()) {
|
||||
this.transitionTo('containerHome', {name: this.getParams().name});
|
||||
}
|
||||
},
|
||||
showLogs: function () {
|
||||
if (!this.disableTab()) {
|
||||
this.transitionTo('containerLogs', {name: this.getParams().name});
|
||||
}
|
||||
},
|
||||
showSettings: function () {
|
||||
if (!this.disableTab()) {
|
||||
this.transitionTo('containerSettings', {name: this.getParams().name});
|
||||
}
|
||||
},
|
||||
handleRun: function () {
|
||||
if (this.state.defaultPort && !this.disableRun()) {
|
||||
exec(['open', this.state.ports[this.state.defaultPort].url], function (err) {
|
||||
if (err) { throw err; }
|
||||
});
|
||||
}
|
||||
},
|
||||
handleRestart: function () {
|
||||
if (!this.disableRestart()) {
|
||||
ContainerStore.restart(this.props.container.Name, function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
handleTerminal: function () {
|
||||
if (!this.disableTerminal()) {
|
||||
var container = this.props.container;
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
handleItemMouseEnterRun: function () {
|
||||
var $action = $(this.getDOMNode()).find('.action .run');
|
||||
$action.css("visibility", "visible");
|
||||
},
|
||||
handleItemMouseLeaveRun: function () {
|
||||
var $action = $(this.getDOMNode()).find('.action .run');
|
||||
$action.css("visibility", "hidden");
|
||||
},
|
||||
handleItemMouseEnterRestart: function () {
|
||||
var $action = $(this.getDOMNode()).find('.action .restart');
|
||||
$action.css("visibility", "visible");
|
||||
},
|
||||
handleItemMouseLeaveRestart: function () {
|
||||
var $action = $(this.getDOMNode()).find('.action .restart');
|
||||
$action.css("visibility", "hidden");
|
||||
},
|
||||
handleItemMouseEnterTerminal: function () {
|
||||
var $action = $(this.getDOMNode()).find('.action .terminal');
|
||||
$action.css("visibility", "visible");
|
||||
},
|
||||
handleItemMouseLeaveTerminal: function () {
|
||||
var $action = $(this.getDOMNode()).find('.action .terminal');
|
||||
$action.css("visibility", "hidden");
|
||||
},
|
||||
render: function () {
|
||||
var runActionClass = React.addons.classSet({
|
||||
action: true,
|
||||
disabled: this.disableRun()
|
||||
});
|
||||
var restartActionClass = React.addons.classSet({
|
||||
action: true,
|
||||
disabled: this.disableRestart()
|
||||
});
|
||||
var terminalActionClass = React.addons.classSet({
|
||||
action: true,
|
||||
disabled: this.disableTerminal()
|
||||
});
|
||||
var tabHomeClasses = React.addons.classSet({
|
||||
'tab': true,
|
||||
'active': this.state.currentRoute === 'containerHome',
|
||||
disabled: this.disableTab()
|
||||
});
|
||||
var tabLogsClasses = React.addons.classSet({
|
||||
'tab': true,
|
||||
'active': this.state.currentRoute === 'containerLogs',
|
||||
disabled: this.disableTab()
|
||||
});
|
||||
var tabSettingsClasses = React.addons.classSet({
|
||||
'tab': true,
|
||||
'active': this.state.currentRoute && (this.state.currentRoute.indexOf('containerSettings') >= 0),
|
||||
disabled: this.disableTab()
|
||||
});
|
||||
return (
|
||||
<div className="details-subheader">
|
||||
<div className="details-header-actions">
|
||||
<div className={runActionClass} onMouseEnter={this.handleItemMouseEnterRun} onMouseLeave={this.handleItemMouseLeaveRun}>
|
||||
<span className="action-icon" onClick={this.handleRun}><RetinaImage src="button-run.png"/></span>
|
||||
<span className="btn-label run">Run</span>
|
||||
</div>
|
||||
<div className={restartActionClass} onMouseEnter={this.handleItemMouseEnterRestart} onMouseLeave={this.handleItemMouseLeaveRestart}>
|
||||
<span className="action-icon" onClick={this.handleRestart}><RetinaImage src="button-restart.png"/></span>
|
||||
<span className="btn-label restart">Restart</span>
|
||||
</div>
|
||||
<div className={terminalActionClass} onMouseEnter={this.handleItemMouseEnterTerminal} onMouseLeave={this.handleItemMouseLeaveTerminal}>
|
||||
<span className="action-icon" onClick={this.handleTerminal}><RetinaImage src="button-terminal.png"/></span>
|
||||
<span className="btn-label terminal">Terminal</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="details-subheader-tabs">
|
||||
<span className={tabHomeClasses} onClick={this.showHome}>Home</span>
|
||||
<span className={tabLogsClasses} onClick={this.showLogs}>Logs</span>
|
||||
<span className={tabSettingsClasses} onClick={this.showSettings}>Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerDetailsSubheader;
|
|
@ -18,7 +18,7 @@ var Radial = require('./Radial.react');
|
|||
|
||||
var _oldHeight = 0;
|
||||
|
||||
var ContainerDetails = React.createClass({
|
||||
var ContainerDetailsbak = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
PAGE_HOME: 'home',
|
||||
PAGE_LOGS: 'logs',
|
||||
|
@ -94,15 +94,32 @@ var ContainerDetails = React.createClass({
|
|||
});
|
||||
}
|
||||
},
|
||||
disableRun: function () {
|
||||
return (!this.props.container.State.Running || !this.state.defaultPort);
|
||||
},
|
||||
disableRestart: function () {
|
||||
return (this.props.container.State.Downloading || this.props.container.State.Restarting);
|
||||
},
|
||||
disableTerminal: function () {
|
||||
return (!this.props.container.State.Running);
|
||||
},
|
||||
disableTab: function () {
|
||||
return (this.props.container.State.Downloading);
|
||||
},
|
||||
showHome: function () {
|
||||
this.setState({
|
||||
page: this.PAGE_HOME
|
||||
});
|
||||
if (!this.disableTab()) {
|
||||
/*this.setState({
|
||||
page: this.PAGE_HOME
|
||||
});*/
|
||||
this.transitionTo('containerHome', {name: this.getParams().name});
|
||||
}
|
||||
},
|
||||
showLogs: function () {
|
||||
this.setState({
|
||||
page: this.PAGE_LOGS
|
||||
});
|
||||
if (!this.disableTab()) {
|
||||
this.setState({
|
||||
page: this.PAGE_LOGS
|
||||
});
|
||||
}
|
||||
},
|
||||
showPorts: function () {
|
||||
this.setState({
|
||||
|
@ -115,20 +132,40 @@ var ContainerDetails = React.createClass({
|
|||
});
|
||||
},
|
||||
showSettings: function () {
|
||||
this.setState({
|
||||
page: this.PAGE_SETTINGS
|
||||
});
|
||||
if (!this.disableTab()) {
|
||||
this.setState({
|
||||
page: this.PAGE_SETTINGS
|
||||
});
|
||||
}
|
||||
},
|
||||
handleView: function () {
|
||||
console.log('CLICKED');
|
||||
console.log(this.state.ports);
|
||||
console.log(this.state.defaultPort);
|
||||
if (this.state.defaultPort) {
|
||||
handleRun: function () {
|
||||
if (this.state.defaultPort && !this.disableRun()) {
|
||||
exec(['open', this.state.ports[this.state.defaultPort].url], function (err) {
|
||||
if (err) { throw err; }
|
||||
});
|
||||
}
|
||||
},
|
||||
handleRestart: function () {
|
||||
if (!this.disableRestart()) {
|
||||
ContainerStore.restart(this.props.container.Name, function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
handleTerminal: function () {
|
||||
if (!this.disableTerminal()) {
|
||||
var container = this.props.container;
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
handleViewLink: function (url) {
|
||||
exec(['open', url], function (err) {
|
||||
if (err) { throw err; }
|
||||
|
@ -171,23 +208,6 @@ var ContainerDetails = React.createClass({
|
|||
if (err) { throw err; }
|
||||
});
|
||||
},
|
||||
handleRestart: function () {
|
||||
ContainerStore.restart(this.props.container.Name, function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
},
|
||||
handleTerminal: function () {
|
||||
var container = this.props.container;
|
||||
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 () {
|
||||
var newName = $('#input-container-name').val();
|
||||
if (newName === this.props.container.Name) {
|
||||
|
@ -515,19 +535,19 @@ var ContainerDetails = React.createClass({
|
|||
var tabHomeClasses = React.addons.classSet({
|
||||
'tab': true,
|
||||
'active': this.state.page === this.PAGE_HOME,
|
||||
disabled: this.props.container.State.Downloading
|
||||
disabled: this.disableTab()
|
||||
});
|
||||
|
||||
var tabLogsClasses = React.addons.classSet({
|
||||
'tab': true,
|
||||
'active': this.state.page === this.PAGE_LOGS,
|
||||
disabled: this.props.container.State.Downloading
|
||||
disabled: this.disableTab()
|
||||
});
|
||||
|
||||
var tabSettingsClasses = React.addons.classSet({
|
||||
'tab': true,
|
||||
'active': this.state.page === this.PAGE_SETTINGS,
|
||||
disabled: this.props.container.State.Downloading
|
||||
disabled: this.disableTab()
|
||||
});
|
||||
|
||||
/*var ports = _.map(_.pairs(self.state.ports), function (pair, index, list) {
|
||||
|
@ -572,20 +592,35 @@ var ContainerDetails = React.createClass({
|
|||
);
|
||||
}*/
|
||||
|
||||
var runActionClass = React.addons.classSet({
|
||||
action: true,
|
||||
disabled: this.disableRun()
|
||||
});
|
||||
|
||||
var restartActionClass = React.addons.classSet({
|
||||
action: true,
|
||||
disabled: this.disableRestart()
|
||||
});
|
||||
|
||||
var terminalActionClass = React.addons.classSet({
|
||||
action: true,
|
||||
disabled: this.disableTerminal()
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="details">
|
||||
<ContainerDetailsHeader container={this.props.container} />
|
||||
<div className="details-subheader">
|
||||
<div className="details-header-actions">
|
||||
<div className="action" onMouseEnter={this.handleItemMouseEnterRun} onMouseLeave={this.handleItemMouseLeaveRun}>
|
||||
<span className="action-icon" onClick={this.handleView}><RetinaImage src="button-run.png"/></span>
|
||||
<div className={runActionClass} onMouseEnter={this.handleItemMouseEnterRun} onMouseLeave={this.handleItemMouseLeaveRun}>
|
||||
<span className="action-icon" onClick={this.handleRun}><RetinaImage src="button-run.png"/></span>
|
||||
<span className="btn-label run">Run</span>
|
||||
</div>
|
||||
<div className="action" onMouseEnter={this.handleItemMouseEnterRestart} onMouseLeave={this.handleItemMouseLeaveRestart}>
|
||||
<div className={restartActionClass} onMouseEnter={this.handleItemMouseEnterRestart} onMouseLeave={this.handleItemMouseLeaveRestart}>
|
||||
<span className="action-icon" onClick={this.handleRestart}><RetinaImage src="button-restart.png"/></span>
|
||||
<span className="btn-label restart">Restart</span>
|
||||
</div>
|
||||
<div className="action" onMouseEnter={this.handleItemMouseEnterTerminal} onMouseLeave={this.handleItemMouseLeaveTerminal}>
|
||||
<div className={terminalActionClass} onMouseEnter={this.handleItemMouseEnterTerminal} onMouseLeave={this.handleItemMouseLeaveTerminal}>
|
||||
<span className="action-icon" onClick={this.handleTerminal}><RetinaImage src="button-terminal.png"/></span>
|
||||
<span className="btn-label terminal">Terminal</span>
|
||||
</div>
|
||||
|
@ -602,4 +637,4 @@ var ContainerDetails = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerDetails;
|
||||
module.exports = ContainerDetailsbak;
|
|
@ -1,105 +1,130 @@
|
|||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var path = require('path');
|
||||
var exec = require('exec');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var Router = require('react-router');
|
||||
var Radial = require('./Radial.react');
|
||||
var ContainerHomePreview = require('./ContainerHomePreview.react');
|
||||
var ContainerHomeLogs = require('./ContainerHomeLogs.react');
|
||||
var ContainerHomeFolders = require('./ContainerHomeFolders.react');
|
||||
var ContainerUtil = require('./ContainerUtil');
|
||||
|
||||
var resizeWindow = function () {
|
||||
$('.left .wrapper').height(window.innerHeight - 240);
|
||||
$('.right .wrapper').height(window.innerHeight / 2 - 100);
|
||||
};
|
||||
|
||||
var ContainerHome = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
ports: {},
|
||||
defaultPort: null
|
||||
};
|
||||
},
|
||||
handleResize: function () {
|
||||
$('.web-preview').height(window.innerHeight - 240);
|
||||
$('.mini-logs').height(window.innerHeight / 2 - 100);
|
||||
$('.folders').height(window.innerHeight / 2 - 150);
|
||||
resizeWindow();
|
||||
},
|
||||
componentWillReceiveProps: function () {
|
||||
this.init();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
$('.web-preview').height(window.innerHeight - 240);
|
||||
$('.mini-logs').height(window.innerHeight / 2 - 100);
|
||||
$('.folders').height(window.innerHeight / 2 - 150);
|
||||
this.init();
|
||||
ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
|
||||
resizeWindow();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
componentDidUpdate: function () {
|
||||
// Scroll logs to bottom
|
||||
$('.web-preview').height(window.innerHeight - 240);
|
||||
$('.mini-logs').height(window.innerHeight / 2 - 100);
|
||||
$('.folders').height(window.innerHeight / 2 - 150);
|
||||
var parent = $('.mini-logs');
|
||||
if (parent.length) {
|
||||
if (parent.scrollTop() >= this._oldHeight) {
|
||||
parent.stop();
|
||||
parent.scrollTop(parent[0].scrollHeight - parent.height());
|
||||
}
|
||||
this._oldHeight = parent[0].scrollHeight - parent.height();
|
||||
}
|
||||
resizeWindow();
|
||||
},
|
||||
handleClickFolder: function (path) {
|
||||
exec(['open', path], function (err) {
|
||||
if (err) { throw err; }
|
||||
init: function () {
|
||||
var container = ContainerStore.container(this.getParams().name);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
var ports = ContainerUtil.ports(container);
|
||||
var webPorts = ['80', '8000', '8080', '3000', '5000', '2368'];
|
||||
this.setState({
|
||||
ports: ports,
|
||||
defaultPort: _.find(_.keys(ports), function (port) {
|
||||
return webPorts.indexOf(port) !== -1;
|
||||
}),
|
||||
progress: ContainerStore.progress(this.getParams().name)
|
||||
});
|
||||
},
|
||||
handleClickPreview: function () {
|
||||
if (this.props.defaultPort) {
|
||||
exec(['open', this.props.ports[this.props.defaultPort].url], function (err) {
|
||||
if (err) { throw err; }
|
||||
updateProgress: function (name) {
|
||||
if (name === this.getParams().name) {
|
||||
this.setState({
|
||||
progress: ContainerStore.progress(name)
|
||||
});
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
var preview;
|
||||
if (this.props.defaultPort) {
|
||||
preview = (
|
||||
<div className="web-preview">
|
||||
<h4>Web Preview</h4>
|
||||
<div className="widget">
|
||||
<iframe sandbox="allow-same-origin allow-scripts" src={this.props.ports[this.props.defaultPort].url} scrolling="no"></iframe>
|
||||
<div className="iframe-overlay" onClick={this.handleClickPreview}><span className="icon icon-upload-2"></span><div className="text">Open in Browser</div></div>
|
||||
var body;
|
||||
if (this.props.container && this.props.container.State.Downloading) {
|
||||
if (this.state.progress) {
|
||||
body = (
|
||||
<div className="details-progress">
|
||||
<h2>Downloading Image</h2>
|
||||
<Radial progress={Math.min(Math.round(this.state.progress * 100), 99)} thick={true} gray={true}/>
|
||||
</div>
|
||||
<div className="subtext">Not showing correctly?</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
console.log(this.props.container.Volumes);
|
||||
var self = this;
|
||||
var folders = _.map(self.props.container.Volumes, function (val, key) {
|
||||
var firstFolder = key.split(path.sep)[1];
|
||||
if (!val || val.indexOf(process.env.HOME) === -1) {
|
||||
return;
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={key} className="folder" onClick={self.handleClickFolder.bind(self, val)}>
|
||||
<RetinaImage src="folder.png" />
|
||||
<div className="text">{firstFolder}</div>
|
||||
body = (
|
||||
<div className="details-progress">
|
||||
<h2>Connecting to Docker Hub</h2>
|
||||
<Radial spin="true" progress="90" thick={true} transparent={true}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div className="details-panel home">
|
||||
<div className="content">
|
||||
<div className="left">
|
||||
{preview}
|
||||
</div>
|
||||
<div className="right">
|
||||
<div className="mini-logs">
|
||||
<h4>Logs</h4>
|
||||
<div className="widget">
|
||||
{this.props.logs}
|
||||
<div className="mini-logs-overlay"><span className="icon icon-scale-spread-1"></span><div className="text">View Logs</div></div>
|
||||
} else {
|
||||
if (this.state.defaultPort) {
|
||||
body = (
|
||||
<div className="details-panel home">
|
||||
<div className="content">
|
||||
<div className="left">
|
||||
<ContainerHomePreview />
|
||||
</div>
|
||||
<div className="right">
|
||||
<ContainerHomeLogs />
|
||||
<ContainerHomeFolders container={this.props.container} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="folders">
|
||||
<h4>Edit Files</h4>
|
||||
<div className="widget">
|
||||
{folders}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
var right;
|
||||
if (_.keys(this.state.ports) > 0) {
|
||||
right = (
|
||||
<div className="right">
|
||||
<ContainerHomePreview />
|
||||
<ContainerHomeFolders container={this.props.container} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
right = (
|
||||
<div className="right">
|
||||
<ContainerHomeFolders container={this.props.container} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
body = (
|
||||
<div className="details-panel home">
|
||||
<div className="content">
|
||||
<div className="left">
|
||||
<ContainerHomeLogs />
|
||||
</div>
|
||||
<div className="subtext">Change Folders</div>
|
||||
{right}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
return body;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
var _ = require('underscore');
|
||||
var React = require('react/addons');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var path = require('path');
|
||||
var exec = require('exec');
|
||||
var Router = require('react-router');
|
||||
|
||||
var ContainerHomeFolder = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
handleClickFolder: function (path) {
|
||||
exec(['open', path], function (err) {
|
||||
if (err) { throw err; }
|
||||
});
|
||||
},
|
||||
handleClickChangeFolders: function () {
|
||||
this.transitionTo('containerSettingsVolumes', {name: this.getParams().name});
|
||||
},
|
||||
render: function () {
|
||||
var folders;
|
||||
if (this.props.container) {
|
||||
var self = this;
|
||||
folders = _.map(self.props.container.Volumes, function (val, key) {
|
||||
var firstFolder = key.split(path.sep)[1];
|
||||
if (!val || val.indexOf(process.env.HOME) === -1) {
|
||||
return;
|
||||
} else {
|
||||
return (
|
||||
<div key={key} className="folder" onClick={self.handleClickFolder.bind(self, val)}>
|
||||
<RetinaImage src="folder.png" />
|
||||
<div className="text">{firstFolder}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div className="folders wrapper">
|
||||
<h4>Edit Files</h4>
|
||||
<div className="widget">
|
||||
{folders}
|
||||
</div>
|
||||
<div className="subtext" onClick={this.handleClickChangeFolders}>Change Folders</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerHomeFolder;
|
|
@ -0,0 +1,64 @@
|
|||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var Router = require('react-router');
|
||||
|
||||
var ContainerHomeLogs = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
logs: []
|
||||
};
|
||||
},
|
||||
componentWillReceiveProps: function () {
|
||||
this.init();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.init();
|
||||
ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
|
||||
},
|
||||
componentDidUpdate: function () {
|
||||
// Scroll logs to bottom
|
||||
var parent = $('.mini-logs');
|
||||
if (parent.length) {
|
||||
if (parent.scrollTop() >= this._oldHeight) {
|
||||
parent.stop();
|
||||
parent.scrollTop(parent[0].scrollHeight - parent.height());
|
||||
}
|
||||
this._oldHeight = parent[0].scrollHeight - parent.height();
|
||||
}
|
||||
},
|
||||
init: function () {
|
||||
this.updateLogs();
|
||||
},
|
||||
updateLogs: function (name) {
|
||||
if (name && name !== this.getParams().name) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
logs: ContainerStore.logs(this.getParams().name)
|
||||
});
|
||||
},
|
||||
handleClickLogs: function () {
|
||||
this.transitionTo('containerLogs', {name: this.getParams().name});
|
||||
},
|
||||
render: function () {
|
||||
var logs = this.state.logs.map(function (l, i) {
|
||||
return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>;
|
||||
});
|
||||
return (
|
||||
<div className="mini-logs wrapper">
|
||||
<h4>Logs</h4>
|
||||
<div className="widget">
|
||||
{logs}
|
||||
<div className="mini-logs-overlay" onClick={this.handleClickLogs}><span className="icon icon-scale-spread-1"></span><div className="text">View Logs</div></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerHomeLogs;
|
|
@ -0,0 +1,97 @@
|
|||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
var exec = require('exec');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var ContainerUtil = require('./ContainerUtil');
|
||||
var Router = require('react-router');
|
||||
|
||||
var ContainerHomePreview = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
ports: {},
|
||||
defaultPort: null
|
||||
};
|
||||
},
|
||||
componentWillReceiveProps: function () {
|
||||
this.init();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.init();
|
||||
this.timer = setInterval(this.tick, 2000);
|
||||
},
|
||||
tick: function () {
|
||||
if (document.getElementById('web-preview-frame')) {
|
||||
var frameContent = document.getElementById('web-preview-frame').contentDocument;
|
||||
var $body = $(frameContent.body);
|
||||
if ($body.is(':empty')) {
|
||||
document.getElementById('web-preview-frame').contentDocument.location.reload(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
clearInterval(this.timer);
|
||||
},
|
||||
init: function () {
|
||||
var container = ContainerStore.container(this.getParams().name);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
var ports = ContainerUtil.ports(container);
|
||||
var webPorts = ['80', '8000', '8080', '3000', '5000', '2368'];
|
||||
this.setState({
|
||||
ports: ports,
|
||||
defaultPort: _.find(_.keys(ports), function (port) {
|
||||
return webPorts.indexOf(port) !== -1;
|
||||
})
|
||||
});
|
||||
},
|
||||
handleClickPreview: function () {
|
||||
if (this.state.defaultPort) {
|
||||
exec(['open', this.state.ports[this.state.defaultPort].url], function (err) {
|
||||
if (err) { throw err; }
|
||||
});
|
||||
}
|
||||
},
|
||||
handleClickNotShowingCorrectly: function () {
|
||||
this.transitionTo('containerSettingsPorts', {name: this.getParams().name});
|
||||
},
|
||||
render: function () {
|
||||
var preview;
|
||||
if (this.state.defaultPort) {
|
||||
preview = (
|
||||
<div className="web-preview wrapper">
|
||||
<h4>Web Preview</h4>
|
||||
<div className="widget">
|
||||
<iframe id="web-preview-frame" name="disable-x-frame-options" sandbox="allow-same-origin allow-scripts" src={this.state.ports[this.state.defaultPort].url} scrolling="no"></iframe>
|
||||
<div className="iframe-overlay" onClick={this.handleClickPreview}><span className="icon icon-upload-2"></span><div className="text">Open in Browser</div></div>
|
||||
</div>
|
||||
<div className="subtext" onClick={this.handleClickNotShowingCorrectly}>Not showing correctly?</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
var ports = _.map(_.pairs(this.state.ports), function (pair) {
|
||||
var key = pair[0];
|
||||
var val = pair[1];
|
||||
return (
|
||||
<div key={key} className="ip-port">
|
||||
{val.display}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
preview = (
|
||||
<div className="web-preview wrapper">
|
||||
<h4>IP & Ports</h4>
|
||||
<div className="widget">
|
||||
<p>You can access this container from the outside using the following IP & Port(s):</p>
|
||||
{ports}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerHomePreview;
|
|
@ -66,7 +66,7 @@ var ContainerListItem = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<Router.Link data-container={name} to="container" params={{name: container.Name}}>
|
||||
<Router.Link data-container={name} to="containerDetail" params={{name: container.Name}}>
|
||||
<li onMouseEnter={self.handleItemMouseEnter} onMouseLeave={self.handleItemMouseLeave}>
|
||||
{state}
|
||||
<div className="info">
|
||||
|
|
|
@ -3,6 +3,7 @@ var React = require('react/addons');
|
|||
var Router = require('react-router');
|
||||
|
||||
var ContainerListNewItem = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
handleItemMouseEnter: function () {
|
||||
var $action = $(this.getDOMNode()).find('.action');
|
||||
$action.show();
|
||||
|
@ -13,6 +14,7 @@ var ContainerListNewItem = React.createClass({
|
|||
},
|
||||
handleDelete: function () {
|
||||
$(this.getDOMNode()).fadeOut();
|
||||
this.transitionTo('containers');
|
||||
},
|
||||
render: function () {
|
||||
var self = this;
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var Router = require('react-router');
|
||||
|
||||
var ContainerLogs = React.createClass({
|
||||
mixins: [Router.State],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
logs: []
|
||||
};
|
||||
},
|
||||
componentWillReceiveProps: function () {
|
||||
this.init();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.init();
|
||||
ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
|
||||
},
|
||||
componentDidUpdate: function () {
|
||||
// Scroll logs to bottom
|
||||
var parent = $('.details-logs');
|
||||
if (parent.length) {
|
||||
if (parent.scrollTop() >= this._oldHeight) {
|
||||
parent.stop();
|
||||
parent.scrollTop(parent[0].scrollHeight - parent.height());
|
||||
}
|
||||
this._oldHeight = parent[0].scrollHeight - parent.height();
|
||||
}
|
||||
},
|
||||
init: function () {
|
||||
this.updateLogs();
|
||||
},
|
||||
updateLogs: function (name) {
|
||||
if (name && name !== this.getParams().name) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
logs: ContainerStore.logs(this.getParams().name)
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var logs = this.state.logs.map(function (l, i) {
|
||||
return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>;
|
||||
});
|
||||
return (
|
||||
<div className="details-panel details-logs logs">
|
||||
{logs}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerLogs;
|
|
@ -0,0 +1,53 @@
|
|||
var _ = require('underscore');
|
||||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
|
||||
var ContainerSettings = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
componentWillReceiveProps: function () {
|
||||
this.init();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.init();
|
||||
},
|
||||
init: function () {
|
||||
var currentRoute = _.last(this.getRoutes()).name;
|
||||
if (currentRoute === 'containerSettings') {
|
||||
this.transitionTo('containerSettingsGeneral', {name: this.getParams().name});
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
var container = this.props.container;
|
||||
if (!container) {
|
||||
return (<div></div>);
|
||||
}
|
||||
return (
|
||||
<div className="details-panel">
|
||||
<div className="settings">
|
||||
<div className="settings-menu">
|
||||
<ul>
|
||||
<Router.Link to="containerSettingsGeneral" params={{name: container.Name}}>
|
||||
<li>
|
||||
General
|
||||
</li>
|
||||
</Router.Link>
|
||||
<Router.Link to="containerSettingsPorts" params={{name: container.Name}}>
|
||||
<li>
|
||||
Ports
|
||||
</li>
|
||||
</Router.Link>
|
||||
<Router.Link to="containerSettingsVolumes" params={{name: container.Name}}>
|
||||
<li>
|
||||
Volumes
|
||||
</li>
|
||||
</Router.Link>
|
||||
</ul>
|
||||
</div>
|
||||
<Router.RouteHandler container={container}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerSettings;
|
|
@ -0,0 +1,234 @@
|
|||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var path = require('path');
|
||||
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');
|
||||
|
||||
var containerNameSlugify = function (text) {
|
||||
text = text.replace(/^\s+|\s+$/g, ''); // Trim
|
||||
text = text.toLowerCase();
|
||||
// Remove Accents
|
||||
var from = "àáäâèéëêìíïîòóöôùúüûñç·/,:;";
|
||||
var to = "aaaaeeeeiiiioooouuuunc-----";
|
||||
for (var i=0, l=from.length ; i<l ; i++) {
|
||||
text = text.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
|
||||
}
|
||||
text = text.replace(/[^a-z0-9 -_]/g, '') // Remove invalid chars
|
||||
.replace(/\s+/g, '-') // Collapse whitespace and replace by -
|
||||
.replace(/-+/g, '-') // Collapse dashes
|
||||
.replace(/_+/g, '_'); // Collapse underscores
|
||||
return text;
|
||||
};
|
||||
|
||||
var ContainerSettingsGeneral = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
slugName: null,
|
||||
env: {},
|
||||
pendingEnv: {}
|
||||
};
|
||||
},
|
||||
componentWillReceiveProps: function () {
|
||||
this.init();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.init();
|
||||
},
|
||||
init: function () {
|
||||
var container = ContainerStore.container(this.getParams().name);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
env: ContainerUtil.env(container),
|
||||
});
|
||||
},
|
||||
handleNameChange: function (e) {
|
||||
var newName = e.target.value;
|
||||
if (newName === this.state.slugName) {
|
||||
return;
|
||||
}
|
||||
if (!newName.length) {
|
||||
this.setState({
|
||||
slugName: null
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
slugName: containerNameSlugify(newName)
|
||||
});
|
||||
}
|
||||
},
|
||||
handleNameOnKeyUp: function (e) {
|
||||
if (e.keyCode === 13 && this.state.slugName) {
|
||||
this.handleSaveContainerName();
|
||||
}
|
||||
},
|
||||
handleSaveContainerName: function () {
|
||||
var newName = this.state.slugName;
|
||||
if (newName === this.props.container.Name) {
|
||||
return;
|
||||
}
|
||||
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));
|
||||
}
|
||||
this.setState({
|
||||
slugName: null
|
||||
});
|
||||
ContainerStore.updateContainer(this.props.container.Name, {
|
||||
name: newName
|
||||
}, function (err) {
|
||||
this.transitionTo('containerSettingsGeneral', {name: newName});
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
handleSaveEnvVar: function () {
|
||||
var $rows = $('.env-vars .keyval-row');
|
||||
var envVarList = [];
|
||||
$rows.each(function () {
|
||||
var key = $(this).find('.key').val();
|
||||
var val = $(this).find('.val').val();
|
||||
if (!key.length || !val.length) {
|
||||
return;
|
||||
}
|
||||
envVarList.push(key + '=' + val);
|
||||
});
|
||||
var self = this;
|
||||
ContainerStore.updateContainer(self.props.container.Name, {
|
||||
Env: envVarList
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
self.setState({
|
||||
pendingEnv: {}
|
||||
});
|
||||
$('#new-env-key').val('');
|
||||
$('#new-env-val').val('');
|
||||
}
|
||||
});
|
||||
},
|
||||
handleAddPendingEnvVar: function () {
|
||||
var newKey = $('#new-env-key').val();
|
||||
var newVal = $('#new-env-val').val();
|
||||
var newEnv = {};
|
||||
newEnv[newKey] = newVal;
|
||||
this.setState({
|
||||
pendingEnv: _.extend(this.state.pendingEnv, newEnv)
|
||||
});
|
||||
$('#new-env-key').val('');
|
||||
$('#new-env-val').val('');
|
||||
},
|
||||
handleRemoveEnvVar: function (key) {
|
||||
var newEnv = _.omit(this.state.env, key);
|
||||
this.setState({
|
||||
env: newEnv
|
||||
});
|
||||
},
|
||||
handleRemovePendingEnvVar: function (key) {
|
||||
var newEnv = _.omit(this.state.pendingEnv, key);
|
||||
this.setState({
|
||||
pendingEnv: newEnv
|
||||
});
|
||||
},
|
||||
handleDeleteContainer: function () {
|
||||
dialog.showMessageBox({
|
||||
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);
|
||||
});
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
render: function () {
|
||||
if (!this.props.container) {
|
||||
return (<div></div>);
|
||||
}
|
||||
var willBeRenamedAs;
|
||||
var btnSaveName = (
|
||||
<a className="btn btn-action" onClick={this.handleSaveContainerName} disabled="disabled">Save</a>
|
||||
);
|
||||
if (this.state.slugName) {
|
||||
willBeRenamedAs = (
|
||||
<p>Will be renamed as: <strong>{this.state.slugName}</strong></p>
|
||||
);
|
||||
btnSaveName = (
|
||||
<a className="btn btn-action" onClick={this.handleSaveContainerName}>Save</a>
|
||||
);
|
||||
}
|
||||
var rename = (
|
||||
<div className="settings-section">
|
||||
<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} onChange={this.handleNameChange} onKeyUp={this.handleNameOnKeyUp}></input>
|
||||
{willBeRenamedAs}
|
||||
</div>
|
||||
{btnSaveName}
|
||||
</div>
|
||||
);
|
||||
var self = this;
|
||||
var envVars = _.map(this.state.env, function (val, key) {
|
||||
return (
|
||||
<div key={key} className="keyval-row">
|
||||
<input type="text" className="key line" defaultValue={key}></input>
|
||||
<input type="text" className="val line" defaultValue={val}></input>
|
||||
<a onClick={self.handleRemoveEnvVar.bind(self, key)} className="only-icon btn btn-action small"><span className="icon icon-cross"></span></a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
var pendingEnvVars = _.map(this.state.pendingEnv, function (val, key) {
|
||||
return (
|
||||
<div key={key} className="keyval-row">
|
||||
<input type="text" className="key line" defaultValue={key}></input>
|
||||
<input type="text" className="val line" defaultValue={val}></input>
|
||||
<a onClick={self.handleRemovePendingEnvVar.bind(self, key)} className="only-icon btn btn-action small"><span className="icon icon-arrow-undo"></span></a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="settings-panel">
|
||||
{rename}
|
||||
<div className="settings-section">
|
||||
<h3>Environment Variables</h3>
|
||||
<div className="env-vars-labels">
|
||||
<div className="label-key">KEY</div>
|
||||
<div className="label-val">VALUE</div>
|
||||
</div>
|
||||
<div className="env-vars">
|
||||
{envVars}
|
||||
{pendingEnvVars}
|
||||
<div className="keyval-row">
|
||||
<input id="new-env-key" type="text" className="key line"></input>
|
||||
<input id="new-env-val" type="text" className="val line"></input>
|
||||
<a onClick={this.handleAddPendingEnvVar} className="only-icon btn btn-positive small"><span className="icon icon-add-1"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<a className="btn btn-action" onClick={this.handleSaveEnvVar}>Save</a>
|
||||
</div>
|
||||
<div className="settings-section">
|
||||
<h3>Delete Container</h3>
|
||||
<a className="btn btn-action" onClick={this.handleDeleteContainer}>Delete Container</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerSettingsGeneral;
|
|
@ -0,0 +1,85 @@
|
|||
var _ = require('underscore');
|
||||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var exec = require('exec');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
var ContainerUtil = require('./ContainerUtil');
|
||||
|
||||
var ContainerSettingsPorts = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
ports: {},
|
||||
defaultPort: null
|
||||
};
|
||||
},
|
||||
componentWillReceiveProps: function () {
|
||||
this.init();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.init();
|
||||
},
|
||||
init: function () {
|
||||
var container = ContainerStore.container(this.getParams().name);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
var ports = ContainerUtil.ports(container);
|
||||
var webPorts = ['80', '8000', '8080', '3000', '5000', '2368'];
|
||||
this.setState({
|
||||
ports: ports,
|
||||
defaultPort: _.find(_.keys(ports), function (port) {
|
||||
return webPorts.indexOf(port) !== -1;
|
||||
})
|
||||
});
|
||||
},
|
||||
handleViewLink: function (url) {
|
||||
exec(['open', url], function (err) {
|
||||
if (err) { throw err; }
|
||||
});
|
||||
},
|
||||
handleChangeDefaultPort: function (port, e) {
|
||||
if (e.target.checked) {
|
||||
this.setState({
|
||||
defaultPort: null
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
defaultPort: port
|
||||
});
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
if (!this.props.container) {
|
||||
return (<div></div>);
|
||||
}
|
||||
var self = this;
|
||||
var ports = _.map(_.pairs(self.state.ports), function (pair) {
|
||||
var key = pair[0];
|
||||
var val = pair[1];
|
||||
return (
|
||||
<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="checkbox" checked={self.state.defaultPort === key}/> <label>Web Preview</label>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="settings-panel">
|
||||
<div className="settings-section">
|
||||
<h3>Configure Ports</h3>
|
||||
<div className="table ports">
|
||||
<div className="table-labels">
|
||||
<div className="label-left">DOCKER PORT</div>
|
||||
<div className="label-right">MAC PORT</div>
|
||||
</div>
|
||||
{ports}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerSettingsPorts;
|
|
@ -0,0 +1,82 @@
|
|||
var _ = require('underscore');
|
||||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var remote = require('remote');
|
||||
var exec = require('exec');
|
||||
var dialog = remote.require('dialog');
|
||||
var ContainerStore = require('./ContainerStore');
|
||||
|
||||
var ContainerSettingsVolumes = React.createClass({
|
||||
mixins: [Router.State, Router.Navigation],
|
||||
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; }
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
if (!this.props.container) {
|
||||
return (<div></div>);
|
||||
}
|
||||
var self = this;
|
||||
var volumes = _.map(self.props.container.Volumes, function (val, key) {
|
||||
if (!val || val.indexOf(process.env.HOME) === -1) {
|
||||
val = (
|
||||
<span>
|
||||
<a className="value-right">No Folder</a>
|
||||
<a className="btn btn-action small" onClick={self.handleChooseVolumeClick.bind(self, key)}>Change</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-action small" onClick={self.handleChooseVolumeClick.bind(self, key)}>Change</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={key} className="table-values">
|
||||
<span className="value-left">{key}</span><span className="icon icon-arrow-right"></span>
|
||||
{val}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="settings-panel">
|
||||
<div className="settings-section">
|
||||
<h3>Configure Volumes</h3>
|
||||
<div className="table volumes">
|
||||
<div className="table-labels">
|
||||
<div className="label-left">DOCKER FOLDER</div>
|
||||
<div className="label-right">MAC FOLDER</div>
|
||||
</div>
|
||||
{volumes}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContainerSettingsVolumes;
|
|
@ -22,7 +22,7 @@ var ContainerUtil = {
|
|||
if (value && value.length) {
|
||||
var port = value[0].HostPort;
|
||||
localUrl = 'http://' + ip + ':' + port;
|
||||
localUrlDisplay = ip + ': ' + port;
|
||||
localUrlDisplay = ip + ':' + port;
|
||||
}
|
||||
res[dockerPort] = {
|
||||
url: localUrl,
|
||||
|
|
|
@ -20,7 +20,7 @@ var Containers = React.createClass({
|
|||
ContainerStore.on(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient);
|
||||
|
||||
if (this.state.sorted.length) {
|
||||
this.transitionTo('container', {name: this.state.sorted[0].Name});
|
||||
this.transitionTo('containerHome', {name: this.state.sorted[0].Name});
|
||||
}
|
||||
},
|
||||
componentDidUnmount: function () {
|
||||
|
@ -34,7 +34,7 @@ var Containers = React.createClass({
|
|||
});
|
||||
if (status === 'destroy') {
|
||||
if (this.state.sorted.length) {
|
||||
this.transitionTo('container', {name: this.state.sorted[0].Name});
|
||||
this.transitionTo('containerHome', {name: this.state.sorted[0].Name});
|
||||
} else {
|
||||
this.transitionTo('containers');
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ var Containers = React.createClass({
|
|||
sorted: ContainerStore.sorted()
|
||||
});
|
||||
if (status === 'create') {
|
||||
this.transitionTo('container', {name: name});
|
||||
this.transitionTo('containerHome', {name: name});
|
||||
}
|
||||
},
|
||||
handleScroll: function (e) {
|
||||
|
|
|
@ -11,7 +11,7 @@ var NewContainer = React.createClass({
|
|||
getInitialState: function () {
|
||||
return {
|
||||
query: '',
|
||||
results: ContainerStore.recommended(),
|
||||
results: [],
|
||||
loading: false,
|
||||
tags: {},
|
||||
active: null,
|
||||
|
@ -24,6 +24,7 @@ var NewContainer = React.createClass({
|
|||
});
|
||||
this.refs.searchInput.getDOMNode().focus();
|
||||
ContainerStore.on(ContainerStore.CLIENT_RECOMMENDED_EVENT, this.update);
|
||||
this.update();
|
||||
},
|
||||
update: function () {
|
||||
if (!this.state.query.length) {
|
||||
|
@ -105,8 +106,10 @@ var NewContainer = React.createClass({
|
|||
render: function () {
|
||||
var self = this;
|
||||
var title = this.state.query ? 'Results' : 'Recommended';
|
||||
var data = this.state.results.slice(0, 6);
|
||||
|
||||
var data = [];
|
||||
if (this.state.results) {
|
||||
data = this.state.results.slice(0, 6);
|
||||
}
|
||||
var results;
|
||||
if (data.length) {
|
||||
var items = data.map(function (r) {
|
||||
|
@ -173,11 +176,22 @@ var NewContainer = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} else {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<Radial spin="true" progress={90}/>
|
||||
</div>
|
||||
);
|
||||
if (this.state.results.length === 0 && this.state.query === '') {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<div className="loader">
|
||||
<h2>Loading Images</h2>
|
||||
<Radial spin="true" progress={90} thick={true} transparent={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<h1>Cannot find a matching image.</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
var loadingClasses = React.addons.classSet({
|
||||
hidden: !this.state.loading,
|
||||
|
@ -198,7 +212,7 @@ var NewContainer = React.createClass({
|
|||
</div>
|
||||
<div className="search">
|
||||
<div className="search-bar">
|
||||
<input type="search" ref="searchInput" className="form-control" placeholder="Find an existing image" onChange={this.handleChange}/>
|
||||
<input type="search" ref="searchInput" className="form-control" placeholder="Find an image from Docker Hub" onChange={this.handleChange}/>
|
||||
<div className={magnifierClasses}></div>
|
||||
<RetinaImage className={loadingClasses} src="loading.png"/>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,8 @@ var Radial = React.createClass({
|
|||
'radial-spinner': this.props.spin,
|
||||
'radial-negative': this.props.error,
|
||||
'radial-thick': this.props.thick || false,
|
||||
'radial-gray': this.props.gray || false
|
||||
'radial-gray': this.props.gray || false,
|
||||
'radial-transparent': this.props.transparent || false
|
||||
});
|
||||
return (
|
||||
<div className={classes} data-progress={this.props.progress}>
|
||||
|
|
|
@ -2,6 +2,12 @@ var React = require('react/addons');
|
|||
var Setup = require('./Setup.react');
|
||||
var Containers = require('./Containers.react');
|
||||
var ContainerDetails = require('./ContainerDetails.react');
|
||||
var ContainerHome = require('./ContainerHome.react');
|
||||
var ContainerLogs = require('./ContainerLogs.react');
|
||||
var ContainerSettings = require('./ContainerSettings.react');
|
||||
var ContainerSettingsGeneral = require('./ContainerSettingsGeneral.react');
|
||||
var ContainerSettingsPorts = require('./ContainerSettingsPorts.react');
|
||||
var ContainerSettingsVolumes = require('./ContainerSettingsVolumes.react');
|
||||
var Preferences = require('./Preferences.react');
|
||||
var NewContainer = require('./NewContainer.react');
|
||||
var Router = require('react-router');
|
||||
|
@ -21,7 +27,15 @@ var App = React.createClass({
|
|||
var routes = (
|
||||
<Route name="app" path="/" handler={App}>
|
||||
<Route name="containers" handler={Containers}>
|
||||
<Route name="container" path="/containers/:name" handler={ContainerDetails}/>
|
||||
<Route name="containerDetail" path="/containers/:name" handler={ContainerDetails}>
|
||||
<Route name="containerHome" path="/containers/:name/home" handler={ContainerHome} />
|
||||
<Route name="containerLogs" path="/containers/:name/logs" handler={ContainerLogs}/>
|
||||
<Route name="containerSettings" path="/containers/:name/settings" handler={ContainerSettings}>
|
||||
<Route name="containerSettingsGeneral" path="/containers/:name/settings/general" handler={ContainerSettingsGeneral}/>
|
||||
<Route name="containerSettingsPorts" path="/containers/:name/settings/ports" handler={ContainerSettingsPorts}/>
|
||||
<Route name="containerSettingsVolumes" path="/containers/:name/settings/volumes" handler={ContainerSettingsVolumes}/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route name="preferences" path="/preferences" handler={Preferences}/>
|
||||
<DefaultRoute name="new" handler={NewContainer}/>
|
||||
</Route>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
.image-item {
|
||||
display: flex;
|
||||
width: 320px;
|
||||
height: 170px;
|
||||
height: 166px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @gray-lightest;
|
||||
background-color: white;
|
||||
|
@ -50,6 +50,10 @@
|
|||
font-size: 18px;
|
||||
color: @gray-darkest;
|
||||
margin-bottom: 5px;
|
||||
width: 190px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
img {
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
|
@ -59,7 +63,7 @@
|
|||
.description {
|
||||
font-size: 12px;
|
||||
color: @gray-normal;
|
||||
height: 70px;
|
||||
height: 65px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
|
@ -136,6 +140,21 @@
|
|||
flex: 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.loader {
|
||||
margin: 0 auto;
|
||||
margin-top: -20%;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
color: @gray-lightest;
|
||||
font-size: 24px;
|
||||
margin: 0 auto;
|
||||
margin-top: -20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.new-container-header {
|
||||
|
@ -253,7 +272,7 @@
|
|||
color: @brand-action;
|
||||
transition: all 0.25s;
|
||||
&:hover {
|
||||
color: darken(@brand-action, 10%);
|
||||
color: darken(@brand-action, 15%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -306,6 +325,8 @@
|
|||
.btn-delete {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
.state-new {
|
||||
.at2x('container-white.png', 20px, 20px);
|
||||
|
@ -353,6 +374,7 @@
|
|||
margin-left: 16px;
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 140px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
|
@ -364,6 +386,7 @@
|
|||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 140px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -379,6 +402,8 @@
|
|||
.btn-delete {
|
||||
font-size: 24px;
|
||||
color: @gray-lighter;
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -521,6 +546,9 @@
|
|||
margin-top: -12px;
|
||||
.action {
|
||||
display: inline-block;
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.action-icon {
|
||||
color: @gray-normal;
|
||||
font-size: 30px;
|
||||
|
@ -564,6 +592,9 @@
|
|||
color: white;
|
||||
background-image: linear-gradient(-180deg, #24B8EB 4%, #24A3EB 100%);
|
||||
}
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -657,129 +688,155 @@
|
|||
.left {
|
||||
width: 60%;
|
||||
flex-direction: column;
|
||||
.web-preview {
|
||||
margin-right: 30px;
|
||||
.subtext {
|
||||
text-align: right;
|
||||
color: @gray-lightest;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.widget {
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @gray-lightest;
|
||||
position: relative;
|
||||
iframe {
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
/*width: 100%;
|
||||
height: 100%;*/
|
||||
position: relative;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
.iframe-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
color: transparent;
|
||||
transition: all 0.25s;
|
||||
.icon {
|
||||
margin-top: 40%;
|
||||
display: block;
|
||||
font-size: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
.text {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: @gray-darkest;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
margin-right: 30px;
|
||||
}
|
||||
.right {
|
||||
width: 40%;
|
||||
flex-direction: column;
|
||||
.mini-logs {
|
||||
margin-bottom: 50px;
|
||||
.widget {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @gray-lightest;
|
||||
background-color: @gray-darkest;
|
||||
color: @gray-lightest;
|
||||
height: 100%;
|
||||
}
|
||||
.web-preview {
|
||||
margin-bottom: 50px;
|
||||
.subtext {
|
||||
text-align: right;
|
||||
color: @gray-lightest;
|
||||
margin-top: 2px;
|
||||
transition: all 0.25s;
|
||||
&:hover {
|
||||
color: darken(@gray-lightest, 10%);
|
||||
}
|
||||
}
|
||||
.widget {
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @gray-lightest;
|
||||
position: relative;
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: @gray-normal;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.ip-port {
|
||||
padding: 20px;
|
||||
padding-top: 5px;
|
||||
color: @gray-darkest;
|
||||
font-family: Menlo;
|
||||
font-size: 8px;
|
||||
white-space: pre-wrap;
|
||||
p {
|
||||
margin-bottom: 0px;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
iframe {
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
.iframe-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
color: transparent;
|
||||
transition: all 0.25s;
|
||||
.icon {
|
||||
margin-top: 40%;
|
||||
display: block;
|
||||
font-size: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
.mini-logs-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
color: transparent;
|
||||
transition: all 0.25s;
|
||||
.icon {
|
||||
margin-top: 25%;
|
||||
display: block;
|
||||
font-size: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
.text {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: @gray-darkest;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.text {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: @gray-darkest;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
.folders {
|
||||
.subtext {
|
||||
text-align: right;
|
||||
color: @gray-lightest;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.mini-logs {
|
||||
margin-bottom: 50px;
|
||||
.widget {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @gray-lightest;
|
||||
background-color: @gray-darkest;
|
||||
color: @gray-lightest;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
font-family: Menlo;
|
||||
font-size: 7px;
|
||||
white-space: pre;
|
||||
p {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.widget {
|
||||
padding: 20px 10px;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @gray-lightest;
|
||||
display: flex;
|
||||
.folder {
|
||||
width: 100px;
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.text {
|
||||
text-align: center;
|
||||
}
|
||||
.mini-logs-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
color: transparent;
|
||||
transition: all 0.25s;
|
||||
.icon {
|
||||
margin-top: 25%;
|
||||
display: block;
|
||||
font-size: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
.text {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: @gray-darkest;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.folders {
|
||||
.subtext {
|
||||
text-align: right;
|
||||
color: @gray-lightest;
|
||||
margin-top: 2px;
|
||||
transition: all 0.25s;
|
||||
&:hover {
|
||||
color: darken(@gray-lightest, 10%);
|
||||
}
|
||||
}
|
||||
.widget {
|
||||
padding: 10px 5px;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @gray-lightest;
|
||||
display: flex;
|
||||
.folder {
|
||||
width: 110px;
|
||||
padding: 5px;
|
||||
&:hover {
|
||||
background-color: #F9F9F9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.text {
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -799,108 +856,62 @@
|
|||
}
|
||||
}
|
||||
.settings {
|
||||
padding: 18px 38px;
|
||||
.settings-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
.ports {
|
||||
padding: 18px 38px;
|
||||
}
|
||||
.volumes {
|
||||
padding: 18px 38px;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
.icon-arrow-right {
|
||||
color: #aaa;
|
||||
margin: 2px 9px 0;
|
||||
flex: 0 auto;
|
||||
min-width: 13px;
|
||||
}
|
||||
.btn {
|
||||
min-width: 22px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.table-labels {
|
||||
margin-top: 20px;
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
color: @gray-lightest;
|
||||
.label-left {
|
||||
flex: 0 auto;
|
||||
min-width: 80px;
|
||||
margin-right: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
.label-right {
|
||||
flex: 1 auto;
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
.table-values {
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 8px 0;
|
||||
.value-left {
|
||||
text-align: right;
|
||||
min-width: 80px;
|
||||
flex: 0 auto;
|
||||
}
|
||||
.value-right {
|
||||
flex: 1 auto;
|
||||
-webkit-user-select: text;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
.table-new {
|
||||
margin-top: 10px;
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
input {
|
||||
display: flex;
|
||||
flex: 1 auto;
|
||||
flex-direction: row;
|
||||
.settings-menu {
|
||||
min-width: 160px;
|
||||
ul {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
input.new-left {
|
||||
flex: 0 auto;
|
||||
text-align: right;
|
||||
min-width: 80px;
|
||||
max-width: 80px;
|
||||
}
|
||||
.new-right-wrapper {
|
||||
position: relative;
|
||||
padding-top: 14px;
|
||||
|
||||
display: flex;
|
||||
flex: 1 auto;
|
||||
.new-right-placeholder {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 0;
|
||||
font-weight: 400;
|
||||
flex-direction: column;
|
||||
|
||||
a {
|
||||
min-width: 160px;
|
||||
margin-left: 12px;
|
||||
color: @gray-normal;
|
||||
flex-shrink: 0;
|
||||
cursor: default;
|
||||
outline: none;
|
||||
margin-bottom: 10px;
|
||||
&.active {
|
||||
li {
|
||||
color: white;
|
||||
border-radius: 40px;
|
||||
background-image: linear-gradient(-180deg, #24B8EB 4%, #24A3EB 100%);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
li {
|
||||
cursor: default;
|
||||
border-radius: 40px;
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
input.new-right {
|
||||
flex: 1 auto;
|
||||
height: 24px;
|
||||
position :relative;
|
||||
padding-left: 107px;
|
||||
li {
|
||||
vertical-align: middle;
|
||||
padding: 5px 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.volumes {
|
||||
.label-left {
|
||||
min-width: 120px;
|
||||
}
|
||||
.value-left {
|
||||
min-width: 120px;
|
||||
}
|
||||
.icon {
|
||||
color: #aaa;
|
||||
margin: 2px 9px 0;
|
||||
.settings-panel {
|
||||
padding-left: 40px;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
.settings-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -909,7 +920,16 @@
|
|||
.container-name {
|
||||
margin-bottom: 20px;
|
||||
input {
|
||||
width: 20%;
|
||||
width: 40%;
|
||||
}
|
||||
p {
|
||||
font-weight: 300;
|
||||
margin-top: 5px;
|
||||
color: @gray-lighter;
|
||||
font-size: 12px;
|
||||
strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -923,11 +943,11 @@
|
|||
.label-key {
|
||||
display: inline-block;
|
||||
margin-right: 30px;
|
||||
width: 20%;
|
||||
width: 30%;
|
||||
}
|
||||
.label-val {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
.env-vars {
|
||||
|
@ -938,10 +958,109 @@
|
|||
input {
|
||||
margin-right: 30px;
|
||||
&.key {
|
||||
width: 20%;
|
||||
width: 30%;
|
||||
}
|
||||
&.val {
|
||||
width: 40%;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
.icon-arrow-right {
|
||||
color: #BBB;
|
||||
font-size: 20px;
|
||||
margin: 0px 10px;
|
||||
flex: 0 auto;
|
||||
min-width: 13px;
|
||||
}
|
||||
&.ports {
|
||||
.table-labels {
|
||||
margin-top: 20px;
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
color: @gray-lightest;
|
||||
.label-left {
|
||||
flex: 0 auto;
|
||||
min-width: 85px;
|
||||
margin-right: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
.label-right {
|
||||
flex: 1 auto;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
.table-values {
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 8px 0;
|
||||
.value-left {
|
||||
text-align: right;
|
||||
min-width: 85px;
|
||||
flex: 0 auto;
|
||||
padding: 0px;
|
||||
}
|
||||
.value-right {
|
||||
flex: 1 auto;
|
||||
-webkit-user-select: text;
|
||||
max-width: 170px;
|
||||
padding: 0px;
|
||||
}
|
||||
label {
|
||||
margin-left: 8px;
|
||||
margin-top: 1px;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
}
|
||||
input[type="checked"] {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
&.volumes {
|
||||
.table-labels {
|
||||
margin-top: 20px;
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
color: @gray-lightest;
|
||||
.label-left {
|
||||
flex: 0 auto;
|
||||
margin-right: 30px;
|
||||
width: 30%;
|
||||
}
|
||||
.label-right {
|
||||
flex: 1 auto;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
.table-values {
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 8px 0;
|
||||
.value-left {
|
||||
width: 30%;
|
||||
flex: 0 auto;
|
||||
padding: 0px;
|
||||
}
|
||||
.value-right {
|
||||
flex: 1 auto;
|
||||
-webkit-user-select: text;
|
||||
width: 60%;
|
||||
padding: 0px;
|
||||
}
|
||||
.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
position: absolute;
|
||||
min-width: 100%;
|
||||
flex: 0;
|
||||
min-height: 30px;
|
||||
min-height: 40px;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
// border-bottom: 1px solid #efefef;
|
||||
|
|
|
@ -90,24 +90,11 @@
|
|||
.inset {
|
||||
width: @inset-size;
|
||||
height: @inset-size;
|
||||
position: absolute;
|
||||
margin-left: (@circle-size - @inset-size) / 2.0;
|
||||
margin-top: (@circle-size - @inset-size) / 2.0;
|
||||
|
||||
background-color: @inset-color;
|
||||
border-radius: 100%;
|
||||
.percentage {
|
||||
width: @percentage-text-width;
|
||||
position: absolute;
|
||||
top: (@inset-size - @percentage-font-size) / 2.0;
|
||||
left: (@inset-size - @percentage-text-width) / 2.0;
|
||||
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
|
||||
color: @brand-primary;
|
||||
font-weight: 500;
|
||||
font-size: @percentage-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -116,6 +103,14 @@
|
|||
background: #EEE;
|
||||
}
|
||||
|
||||
&.radial-transparent {
|
||||
@inset-color: #F9F9F9;
|
||||
background: #F9F9F9;
|
||||
.inset {
|
||||
background-color: @inset-color;
|
||||
}
|
||||
}
|
||||
|
||||
@i: 0;
|
||||
@increment: 180deg / 100;
|
||||
.loop (@i) when (@i <= 100) {
|
||||
|
|
|
@ -68,8 +68,8 @@ input[type="text"] {
|
|||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: darken(@btn-color, 10%);
|
||||
color: darken(@btn-color, 10%);
|
||||
border-color: darken(@btn-color, 15%);
|
||||
color: darken(@btn-color, 15%);
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
|
@ -77,8 +77,8 @@ input[type="text"] {
|
|||
|
||||
&:active {
|
||||
background-color: lighten(@btn-color, 45%);
|
||||
border-color: darken(@btn-color, 10%);
|
||||
color: darken(@btn-color, 10%);
|
||||
border-color: darken(@btn-color, 15%);
|
||||
color: darken(@btn-color, 15%);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
@ -131,11 +131,13 @@ input[type="text"] {
|
|||
box-shadow: none;
|
||||
font-weight: 400;
|
||||
text-shadow: none;
|
||||
padding: 4px 14px 4px 14px;
|
||||
height: 28px;
|
||||
padding: 5px 14px 5px 14px;
|
||||
height: 30px;
|
||||
cursor: default;
|
||||
|
||||
&.small {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px 3px 8px;
|
||||
height: 22px;
|
||||
.icon {
|
||||
font-size: 10px;
|
||||
|
@ -179,7 +181,7 @@ input[type="text"] {
|
|||
padding: 6px 7px 6px 7px;
|
||||
&.small {
|
||||
width: 22px;
|
||||
padding: 2px 5px 3px 5px;
|
||||
padding: 4px 5px 4px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче