Merge remote-tracking branch 'origin/sean-polish'

Conflicts:
src/Radial.react.js
src/Setup.react.js
styles/radial.less
This commit is contained in:
Jeffrey Morgan 2015-02-13 10:44:00 -08:00
Родитель e8f87e1ebe
Коммит c338678094
24 изменённых файлов: 1482 добавлений и 365 удалений

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

@ -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 &amp; Ports</h4>
<div className="widget">
<p>You can access this container from the outside using the following IP &amp; 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;
}
}
}