After Width: | Height: | Size: 206 KiB |
After Width: | Height: | Size: 568 KiB |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 856 B |
Before Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 671 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 243 KiB |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 667 B |
Before Width: | Height: | Size: 976 B After Width: | Height: | Size: 624 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 456 B |
After Width: | Height: | Size: 824 B |
Before Width: | Height: | Size: 732 B |
Before Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 931 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 349 B |
After Width: | Height: | Size: 681 B |
After Width: | Height: | Size: 711 B |
After Width: | Height: | Size: 1.2 KiB |
|
@ -66,7 +66,7 @@
|
|||
"classnames": "^1.2.0",
|
||||
"coveralls": "^2.11.2",
|
||||
"deep-extend": "^0.4.0",
|
||||
"dockerode": "^2.1.1",
|
||||
"dockerode": "^2.1.4",
|
||||
"exec": "0.2.0",
|
||||
"install": "^0.1.8",
|
||||
"jquery": "^2.1.3",
|
||||
|
@ -83,7 +83,8 @@
|
|||
"request": "^2.55.0",
|
||||
"request-progress": "^0.3.1",
|
||||
"rimraf": "^2.3.2",
|
||||
"underscore": "^1.8.3"
|
||||
"underscore": "^1.8.3",
|
||||
"validator": "^3.39.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel": "^5.1.10",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import alt from '../alt';
|
||||
import hub from '../utils/HubUtil';
|
||||
|
||||
class AccountActions {
|
||||
login (username, password) {
|
||||
this.dispatch({});
|
||||
hub.login(username, password);
|
||||
}
|
||||
|
||||
signup (username, password, email, subscribe) {
|
||||
this.dispatch({});
|
||||
hub.signup(username, password, email, subscribe);
|
||||
}
|
||||
|
||||
logout () {
|
||||
this.dispatch({});
|
||||
hub.logout();
|
||||
}
|
||||
|
||||
skip () {
|
||||
this.dispatch({});
|
||||
hub.setPrompted(true);
|
||||
}
|
||||
|
||||
verify () {
|
||||
this.dispatch({});
|
||||
hub.verify();
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(AccountActions);
|
|
@ -0,0 +1,16 @@
|
|||
import alt from '../alt';
|
||||
|
||||
class AccountServerActions {
|
||||
constructor () {
|
||||
this.generateActions(
|
||||
'signedup',
|
||||
'loggedin',
|
||||
'loggedout',
|
||||
'prompted',
|
||||
'errors',
|
||||
'verified'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(AccountServerActions);
|
|
@ -0,0 +1,21 @@
|
|||
import alt from '../alt';
|
||||
import regHubUtil from '../utils/RegHubUtil';
|
||||
|
||||
class RepositoryActions {
|
||||
recommended () {
|
||||
this.dispatch({});
|
||||
regHubUtil.recommended();
|
||||
}
|
||||
|
||||
search (query) {
|
||||
this.dispatch({});
|
||||
regHubUtil.search(query);
|
||||
}
|
||||
|
||||
repos () {
|
||||
this.dispatch({});
|
||||
regHubUtil.repos();
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(RepositoryActions);
|
|
@ -0,0 +1,14 @@
|
|||
import alt from '../alt';
|
||||
|
||||
class RepositoryServerActions {
|
||||
constructor () {
|
||||
this.generateActions(
|
||||
'reposLoading',
|
||||
'resultsUpdated',
|
||||
'recommendedUpdated',
|
||||
'reposUpdated'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(RepositoryServerActions);
|
|
@ -0,0 +1,11 @@
|
|||
import alt from '../alt';
|
||||
import regHubUtil from '../utils/RegHubUtil';
|
||||
|
||||
class TagActions {
|
||||
tags (repo) {
|
||||
this.dispatch({repo});
|
||||
regHubUtil.tags(repo);
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(TagActions);
|
|
@ -0,0 +1,11 @@
|
|||
import alt from '../alt';
|
||||
|
||||
class TagServerActions {
|
||||
constructor () {
|
||||
this.generateActions(
|
||||
'tagsUpdated'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(TagServerActions);
|
28
src/app.js
|
@ -9,10 +9,24 @@ var metrics = require('./utils/MetricsUtil');
|
|||
var router = require('./router');
|
||||
var template = require('./menutemplate');
|
||||
var webUtil = require('./utils/WebUtil');
|
||||
var hubUtil = require('./utils/HubUtil');
|
||||
var urlUtil = require ('./utils/URLUtil');
|
||||
var app = remote.require('app');
|
||||
var request = require('request');
|
||||
var docker = require('./utils/DockerUtil');
|
||||
var hub = require('./utils/HubUtil');
|
||||
var Router = require('react-router');
|
||||
var routes = require('./routes');
|
||||
var routerContainer = require('./router');
|
||||
var repositoryActions = require('./actions/RepositoryActions');
|
||||
|
||||
hubUtil.init();
|
||||
|
||||
if (hubUtil.loggedin()) {
|
||||
repositoryActions.repos();
|
||||
}
|
||||
|
||||
repositoryActions.recommended();
|
||||
|
||||
webUtil.addWindowSizeSaving();
|
||||
webUtil.addLiveReload();
|
||||
|
@ -27,12 +41,20 @@ setInterval(function () {
|
|||
metrics.track('app heartbeat');
|
||||
}, 14400000);
|
||||
|
||||
var router = Router.create({
|
||||
routes: routes
|
||||
});
|
||||
router.run(Handler => React.render(<Handler/>, document.body));
|
||||
routerContainer.set(router);
|
||||
|
||||
SetupStore.setup().then(() => {
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template()));
|
||||
docker.init();
|
||||
router.transitionTo('search');
|
||||
if (!hub.prompted() && !hub.loggedin()) {
|
||||
router.transitionTo('login');
|
||||
} else {
|
||||
router.transitionTo('search');
|
||||
}
|
||||
}).catch(err => {
|
||||
metrics.track('Setup Failed', {
|
||||
step: 'catch',
|
||||
|
@ -63,3 +85,7 @@ ipc.on('application:open-url', opts => {
|
|||
urlUtil.openUrl(opts.url, flags, app.getVersion());
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
router: router
|
||||
};
|
||||
|
|
|
@ -18,7 +18,6 @@ try {
|
|||
settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8'));
|
||||
} catch (err) {}
|
||||
|
||||
|
||||
var openURL = null;
|
||||
app.on('open-url', function (event, url) {
|
||||
event.preventDefault();
|
||||
|
@ -28,13 +27,13 @@ app.on('open-url', function (event, url) {
|
|||
app.on('ready', function () {
|
||||
var mainWindow = new BrowserWindow({
|
||||
width: size.width || 1000,
|
||||
height: size.height || 700,
|
||||
height: size.height || 780,
|
||||
'min-width': 1000,
|
||||
'min-height': 700,
|
||||
'min-height': 600,
|
||||
'standard-window': false,
|
||||
resizable: true,
|
||||
frame: false,
|
||||
show: true,
|
||||
show: false,
|
||||
});
|
||||
|
||||
mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'build/index.html')));
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var Header = require('./Header.react');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
var accountStore = require('../stores/AccountStore');
|
||||
var accountActions = require('../actions/AccountActions');
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [Router.Navigation],
|
||||
|
||||
getInitialState: function () {
|
||||
return accountStore.getState();
|
||||
},
|
||||
|
||||
componentDidMount: function () {
|
||||
document.addEventListener('keyup', this.handleDocumentKeyUp, false);
|
||||
accountStore.listen(this.update);
|
||||
},
|
||||
|
||||
componentWillUnmount: function () {
|
||||
document.removeEventListener('keyup', this.handleDocumentKeyUp, false);
|
||||
accountStore.unlisten(this.update);
|
||||
},
|
||||
|
||||
componentWillUpdate: function (nextProps, nextState) {
|
||||
if (!this.state.username && nextState.username) {
|
||||
if (nextState.prompted) {
|
||||
this.goBack();
|
||||
} else {
|
||||
this.transitionTo('search');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleSkip: function () {
|
||||
accountActions.skip();
|
||||
this.transitionTo('search');
|
||||
metrics.track('Skipped Login');
|
||||
},
|
||||
|
||||
handleClose: function () {
|
||||
this.goBack();
|
||||
metrics.track('Closed Login');
|
||||
},
|
||||
|
||||
update: function () {
|
||||
this.setState(accountStore.getState());
|
||||
},
|
||||
|
||||
render: function () {
|
||||
let close = this.state.prompted ?
|
||||
<a className="btn btn-action btn-close" disabled={this.state.loading} onClick={this.handleClose}>Close</a> :
|
||||
<a className="btn btn-action btn-skip" disabled={this.state.loading} onClick={this.handleSkip}>Skip For Now</a>;
|
||||
|
||||
return (
|
||||
<div className="setup">
|
||||
<Header hideLogin={true}/>
|
||||
<div className="setup-content">
|
||||
{close}
|
||||
<div className="form-section">
|
||||
<RetinaImage src={'connect-to-hub.png'} checkIfRetinaImgExists={false}/>
|
||||
<Router.RouteHandler errors={this.state.errors} loading={this.state.loading} {...this.props}/>
|
||||
</div>
|
||||
<div className="desc">
|
||||
<div className="content">
|
||||
<h1>Connect to Docker Hub</h1>
|
||||
<p>Pull and run private Docker Hub images by connecting your Docker Hub account to Kitematic.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
var _ = require('underscore');
|
||||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var validator = require('validator');
|
||||
var accountActions = require('../actions/AccountActions');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
var shell = require('shell');
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [Router.Navigation, React.addons.LinkedStateMixin],
|
||||
|
||||
getInitialState: function () {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
errors: {}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function () {
|
||||
React.findDOMNode(this.refs.usernameInput).focus();
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
this.setState({errors: nextProps.errors});
|
||||
},
|
||||
|
||||
validate: function () {
|
||||
let errors = {};
|
||||
if (!validator.isLowercase(this.state.username) || !validator.isAlphanumeric(this.state.username) || !validator.isLength(this.state.username, 4, 30)) {
|
||||
errors.username = 'Must be 4-30 lower case letters or numbers';
|
||||
}
|
||||
|
||||
if (!validator.isLength(this.state.password, 5)) {
|
||||
errors.password = 'Must be at least 5 characters long';
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
handleBlur: function () {
|
||||
this.setState({errors: _.omit(this.validate(), (val, key) => !this.state[key].length)});
|
||||
},
|
||||
|
||||
handleLogin: function () {
|
||||
let errors = this.validate();
|
||||
this.setState({errors});
|
||||
|
||||
if (_.isEmpty(errors)) {
|
||||
accountActions.login(this.state.username, this.state.password);
|
||||
metrics.track('Clicked Log In');
|
||||
}
|
||||
},
|
||||
|
||||
handleClickSignup: function () {
|
||||
if (!this.props.loading) {
|
||||
this.replaceWith('signup');
|
||||
metrics.track('Switched to Sign Up');
|
||||
}
|
||||
},
|
||||
|
||||
handleClickForgotPassword: function () {
|
||||
shell.openExternal('https://hub.docker.com/account/forgot-password/');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
let loading = this.props.loading ? <div className="spinner la-ball-clip-rotate la-dark"><div></div></div> : null;
|
||||
return (
|
||||
<form className="form-connect">
|
||||
<input ref="usernameInput"maxLength="30" name="username" placeholder="username" type="text" disabled={this.props.loading} valueLink={this.linkState('username')} onBlur={this.handleBlur}/>
|
||||
<p className="error-message">{this.state.errors.username}</p>
|
||||
<input ref="passwordInput" name="password" placeholder="password" type="password" disabled={this.props.loading} valueLink={this.linkState('password')} onBlur={this.handleBlur}/>
|
||||
<p className="error-message">{this.state.errors.password}</p>
|
||||
<a className="link" onClick={this.handleClickForgotPassword}>Forgot your password?</a>
|
||||
<p className="error-message">{this.state.errors.detail}</p>
|
||||
<div className="submit">
|
||||
{loading}
|
||||
<button className="btn btn-action" disabled={this.props.loading} onClick={this.handleLogin} type="submit">Log In</button>
|
||||
</div>
|
||||
<br/>
|
||||
<div className="extra">Don't have an account yet? <a disabled={this.state.loading} onClick={this.handleClickSignup}>Sign Up</a></div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
var _ = require('underscore');
|
||||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var validator = require('validator');
|
||||
var accountActions = require('../actions/AccountActions');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [Router.Navigation, React.addons.LinkedStateMixin],
|
||||
|
||||
getInitialState: function () {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
subscribe: true,
|
||||
errors: {}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function () {
|
||||
React.findDOMNode(this.refs.usernameInput).focus();
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
this.setState({errors: nextProps.errors});
|
||||
},
|
||||
|
||||
validate: function () {
|
||||
let errors = {};
|
||||
if (!validator.isLowercase(this.state.username) || !validator.isAlphanumeric(this.state.username) || !validator.isLength(this.state.username, 4, 30)) {
|
||||
errors.username = 'Must be 4-30 lower case letters or numbers';
|
||||
}
|
||||
|
||||
if (!validator.isLength(this.state.password, 5)) {
|
||||
errors.password = 'Must be at least 5 characters long';
|
||||
}
|
||||
|
||||
if (!validator.isEmail(this.state.email)) {
|
||||
errors.email = 'Must be a valid email address';
|
||||
}
|
||||
return errors;
|
||||
},
|
||||
|
||||
handleBlur: function () {
|
||||
this.setState({errors: _.omit(this.validate(), (val, key) => !this.state[key].length)});
|
||||
},
|
||||
|
||||
handleSignUp: function () {
|
||||
let errors = this.validate();
|
||||
this.setState({errors});
|
||||
|
||||
if (_.isEmpty(errors)) {
|
||||
accountActions.signup(this.state.username, this.state.password, this.state.email, this.state.subscribe);
|
||||
metrics.track('Clicked Signed Up');
|
||||
}
|
||||
},
|
||||
|
||||
handleClickLogin: function () {
|
||||
if (!this.props.loading) {
|
||||
this.replaceWith('login');
|
||||
metrics.track('Switched to Log In');
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
let loading = this.props.loading ? <div className="spinner la-ball-clip-rotate la-dark"><div></div></div> : null;
|
||||
return (
|
||||
<form className="form-connect" onSubmit={this.handleSignUp}>
|
||||
<input ref="usernameInput" maxLength="30" name="username" placeholder="Username" type="text" disabled={this.props.loading} valueLink={this.linkState('username')} onBlur={this.handleBlur}/>
|
||||
<p className="error-message">{this.state.errors.username}</p>
|
||||
<input ref="emailInput" name="email" placeholder="Email" type="text" valueLink={this.linkState('email')} disabled={this.props.loading} onBlur={this.handleBlur}/>
|
||||
<p className="error-message">{this.state.errors.email}</p>
|
||||
<input ref="passwordInput" name="password" placeholder="Password" type="password" valueLink={this.linkState('password')} disabled={this.props.loading} onBlur={this.handleBlur}/>
|
||||
<p className="error-message">{this.state.errors.password}</p>
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" disabled={this.props.loading} checkedLink={this.linkState('subscribe')}/> Subscribe to the Docker newsletter.
|
||||
</label>
|
||||
</div>
|
||||
<p className="error-message">{this.state.errors.detail}</p>
|
||||
<div className="submit">
|
||||
{loading}
|
||||
<button className="btn btn-action" disabled={this.props.loading} type="submit">Sign Up</button>
|
||||
</div>
|
||||
<br/>
|
||||
<div className="extra">Already have an account? <a disabled={this.state.loading} onClick={this.handleClickLogin}>Log In</a></div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -51,13 +51,11 @@ var ContainerHome = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} else if (this.props.container && this.props.container.State.Downloading) {
|
||||
if (this.props.container.Progress !== undefined) {
|
||||
|
||||
if (this.props.container.Progress) {
|
||||
let values = [];
|
||||
let sum = 0.0;
|
||||
|
||||
for (let i = 0; i < this.props.container.Progress.amount; i++) {
|
||||
|
||||
values.push(Math.round(this.props.container.Progress.progress[i].value));
|
||||
sum += this.props.container.Progress.progress[i].value;
|
||||
}
|
||||
|
@ -67,9 +65,9 @@ var ContainerHome = React.createClass({
|
|||
body = (
|
||||
<div className="details-progress">
|
||||
<h2>Downloading Image</h2>
|
||||
<h2>{Math.round(sum*100)/100}%</h2>
|
||||
<h2>{(Math.round(sum*100)/100).toFixed(2)}%</h2>
|
||||
<div className="container-progress-wrapper">
|
||||
<ContainerProgress pBar1={values[0]} pBar2={values[1]} pBar3={values[2]} pBar4={values[3]} />
|
||||
<ContainerProgress pBar1={values[0]} pBar2={values[1]} pBar3={values[2]} pBar4={values[3]}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -64,7 +64,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
render: function () {
|
||||
var logs = this.state.logs.map(function (l, i) {
|
||||
return <span key={i} dangerouslySetInnerHTML={{__html: l}}></span>;
|
||||
return <span key={i} dangerouslySetInnerHTML={{__html: l}}></span>;
|
||||
});
|
||||
if (logs.length === 0) {
|
||||
logs = "No logs for this container.";
|
||||
|
|
|
@ -5,10 +5,7 @@ var Router = require('react-router');
|
|||
var containerStore = require('../stores/ContainerStore');
|
||||
var ContainerList = require('./ContainerList.react');
|
||||
var Header = require('./Header.react');
|
||||
var ipc = require('ipc');
|
||||
var remote = require('remote');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
var autoUpdater = remote.require('auto-updater');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var shell = require('shell');
|
||||
var machine = require('../utils/DockerMachineUtil');
|
||||
|
@ -21,31 +18,21 @@ var Containers = React.createClass({
|
|||
getInitialState: function () {
|
||||
return {
|
||||
sidebarOffset: 0,
|
||||
containers: {},
|
||||
sorted: [],
|
||||
updateAvailable: false,
|
||||
currentButtonLabel: ''
|
||||
containers: containerStore.getState().containers,
|
||||
sorted: this.sorted(containerStore.getState().containers)
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function () {
|
||||
containerStore.listen(this.update);
|
||||
|
||||
ipc.on('application:update-available', () => {
|
||||
this.setState({
|
||||
updateAvailable: true
|
||||
});
|
||||
});
|
||||
autoUpdater.checkForUpdates();
|
||||
},
|
||||
|
||||
componentDidUnmount: function () {
|
||||
containerStore.unlisten(this.update);
|
||||
},
|
||||
|
||||
update: function () {
|
||||
let containers = containerStore.getState().containers;
|
||||
let sorted = _.values(containers).sort(function (a, b) {
|
||||
sorted: function (containers) {
|
||||
return _.values(containers).sort(function (a, b) {
|
||||
if (a.State.Downloading && !b.State.Downloading) {
|
||||
return -1;
|
||||
} else if (!a.State.Downloading && b.State.Downloading) {
|
||||
|
@ -60,6 +47,11 @@ var Containers = React.createClass({
|
|||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
update: function () {
|
||||
let containers = containerStore.getState().containers;
|
||||
let sorted = this.sorted(containerStore.getState().containers);
|
||||
|
||||
let name = this.context.router.getCurrentParams().name;
|
||||
if (containerStore.getState().pending) {
|
||||
|
@ -97,11 +89,6 @@ var Containers = React.createClass({
|
|||
metrics.track('Pressed New Container');
|
||||
},
|
||||
|
||||
handleAutoUpdateClick: function () {
|
||||
metrics.track('Restarted to Update');
|
||||
ipc.send('application:quit-install');
|
||||
},
|
||||
|
||||
handleClickPreferences: function () {
|
||||
metrics.track('Opened Preferences', {
|
||||
from: 'app'
|
||||
|
@ -164,12 +151,6 @@ var Containers = React.createClass({
|
|||
if (this.state.sidebarOffset) {
|
||||
sidebarHeaderClass += ' sep';
|
||||
}
|
||||
var updateWidget;
|
||||
if (this.state.updateAvailable) {
|
||||
updateWidget = (
|
||||
<a className="btn btn-action small" onClick={this.handleAutoUpdateClick}>New Update</a>
|
||||
);
|
||||
}
|
||||
|
||||
var container = this.context.router.getCurrentParams().name ? this.state.containers[this.context.router.getCurrentParams().name] : {};
|
||||
return (
|
||||
|
@ -187,14 +168,11 @@ var Containers = React.createClass({
|
|||
</section>
|
||||
<section className="sidebar-containers" onScroll={this.handleScroll}>
|
||||
<ContainerList containers={this.state.sorted} newContainer={this.state.newContainer} />
|
||||
<div className="sidebar-buttons">
|
||||
<div className="btn-label">{this.state.currentButtonLabel}</div>
|
||||
<span className="btn-sidebar" onClick={this.handleClickDockerTerminal} onMouseEnter={this.handleMouseEnterDockerTerminal} onMouseLeave={this.handleMouseLeaveDockerTerminal}><RetinaImage src="docker-terminal.png"/></span>
|
||||
<span className="btn-sidebar" onClick={this.handleClickReportIssue} onMouseEnter={this.handleMouseEnterReportIssue} onMouseLeave={this.handleMouseLeaveReportIssue}><RetinaImage src="report-issue.png"/></span>
|
||||
<span className="btn-sidebar" onClick={this.handleClickPreferences} onMouseEnter={this.handleMouseEnterPreferences} onMouseLeave={this.handleMouseLeavePreferences}><RetinaImage src="preferences.png"/></span>
|
||||
{updateWidget}
|
||||
</div>
|
||||
<div className="sidebar-buttons-padding"></div>
|
||||
</section>
|
||||
<section className="sidebar-buttons">
|
||||
<span className="btn-sidebar btn-terminal" onClick={this.handleClickDockerTerminal} onMouseEnter={this.handleMouseEnterDockerTerminal} onMouseLeave={this.handleMouseLeaveDockerTerminal}><RetinaImage src="whaleicon.png"/> <span className="text">DOCKER CLI</span></span>
|
||||
<span className="btn-sidebar btn-feedback" onClick={this.handleClickReportIssue} onMouseEnter={this.handleMouseEnterDockerTerminal} onMouseLeave={this.handleMouseLeaveDockerTerminal}><RetinaImage src="feedback.png"/></span>
|
||||
<span className="btn-sidebar" onClick={this.handleClickPreferences} onMouseEnter={this.handleMouseEnterDockerTerminal} onMouseLeave={this.handleMouseLeaveDockerTerminal}><RetinaImage src="preferences.png"/></span>
|
||||
</section>
|
||||
</div>
|
||||
<Router.RouteHandler pending={this.state.pending} containers={this.state.containers} container={container}/>
|
||||
|
|
|
@ -1,18 +1,49 @@
|
|||
var React = require('react/addons');
|
||||
var remote = require('remote');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var remote = require('remote');
|
||||
var ipc = require('ipc');
|
||||
var autoUpdater = remote.require('auto-updater');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
var Menu = remote.require('menu');
|
||||
var MenuItem = remote.require('menu-item');
|
||||
var accountStore = require('../stores/AccountStore');
|
||||
var accountActions = require('../actions/AccountActions');
|
||||
var Router = require('react-router');
|
||||
var classNames = require('classNames');
|
||||
|
||||
var Header = React.createClass({
|
||||
mixins: [Router.Navigation],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
fullscreen: false
|
||||
fullscreen: false,
|
||||
updateAvailable: false,
|
||||
username: accountStore.getState().username,
|
||||
verified: accountStore.getState().verified
|
||||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
document.addEventListener('keyup', this.handleDocumentKeyUp, false);
|
||||
|
||||
accountStore.listen(this.update);
|
||||
|
||||
ipc.on('application:update-available', () => {
|
||||
this.setState({
|
||||
updateAvailable: true
|
||||
});
|
||||
});
|
||||
autoUpdater.checkForUpdates();
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
document.removeEventListener('keyup', this.handleDocumentKeyUp, false);
|
||||
accountStore.unlisten(this.update);
|
||||
},
|
||||
update: function () {
|
||||
let accountState = accountStore.getState();
|
||||
this.setState({
|
||||
username: accountState.username,
|
||||
verified: accountState.verified
|
||||
});
|
||||
},
|
||||
handleDocumentKeyUp: function (e) {
|
||||
if (e.keyCode === 27 && remote.getCurrentWindow().isFullScreen()) {
|
||||
|
@ -35,30 +66,89 @@ var Header = React.createClass({
|
|||
handleFullscreenHover: function () {
|
||||
this.update();
|
||||
},
|
||||
handleAutoUpdateClick: function () {
|
||||
metrics.track('Restarted to Update');
|
||||
ipc.send('application:quit-install');
|
||||
},
|
||||
handleUserClick: function (e) {
|
||||
let menu = new Menu();
|
||||
|
||||
if (!this.state.verified) {
|
||||
menu.append(new MenuItem({ label: 'I\'ve Verified My Email Address', click: this.handleVerifyClick}));
|
||||
}
|
||||
|
||||
menu.append(new MenuItem({ label: 'Sign Out', click: this.handleLogoutClick}));
|
||||
menu.popup(remote.getCurrentWindow(), e.currentTarget.offsetLeft, e.currentTarget.offsetTop + e.currentTarget.clientHeight + 10);
|
||||
},
|
||||
handleLoginClick: function () {
|
||||
this.transitionTo('login');
|
||||
metrics.track('Opened Log In Screen');
|
||||
},
|
||||
handleLogoutClick: function () {
|
||||
metrics.track('Logged Out');
|
||||
accountActions.logout();
|
||||
},
|
||||
handleVerifyClick: function () {
|
||||
metrics.track('Verified Account', {
|
||||
from: 'header'
|
||||
});
|
||||
accountActions.verify();
|
||||
},
|
||||
render: function () {
|
||||
let updateWidget = this.state.updateAvailable ? <a className="btn btn-action small no-drag" onClick={this.handleAutoUpdateClick}>UPDATE NOW</a> : null;
|
||||
let buttons;
|
||||
if (this.state.fullscreen) {
|
||||
return (
|
||||
<div className="header no-drag">
|
||||
<div className="buttons">
|
||||
<div className="button button-close disabled"></div>
|
||||
<div className="button button-minimize disabled"></div>
|
||||
<div className="button button-fullscreenclose enabled" onClick={this.handleFullscreen}></div>
|
||||
</div>
|
||||
<RetinaImage className="logo" src="logo.png"/>
|
||||
buttons = (
|
||||
<div className="buttons">
|
||||
<div className="button button-close disabled"></div>
|
||||
<div className="button button-minimize disabled"></div>
|
||||
<div className="button button-fullscreenclose enabled" onClick={this.handleFullscreen}></div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="header">
|
||||
<div className="buttons">
|
||||
<div className="button button-close enabled" onClick={this.handleClose}></div>
|
||||
<div className="button button-minimize enabled" onClick={this.handleMinimize}></div>
|
||||
<div className="button button-fullscreen enabled" onClick={this.handleFullscreen}></div>
|
||||
</div>
|
||||
<RetinaImage className="logo" src="logo.png"/>
|
||||
buttons = (
|
||||
<div className="buttons">
|
||||
<div className="button button-close enabled" onClick={this.handleClose}></div>
|
||||
<div className="button button-minimize enabled" onClick={this.handleMinimize}></div>
|
||||
<div className="button button-fullscreen enabled" onClick={this.handleFullscreen}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let username;
|
||||
if (this.props.hideLogin) {
|
||||
username = null;
|
||||
} else if (this.state.username) {
|
||||
username = (
|
||||
<span className="no-drag" onClick={this.handleUserClick}>
|
||||
<RetinaImage src="user.png"/> {this.state.username} {this.state.verified ? null : '(Unverified)'} <RetinaImage src="userdropdown.png"/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
username = (
|
||||
<span className="no-drag" onClick={this.handleLoginClick}>
|
||||
<RetinaImage src="user.png"/> Log In
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let headerClasses = classNames({
|
||||
bordered: !this.props.hideLogin,
|
||||
header: true,
|
||||
'no-drag': true
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={headerClasses}>
|
||||
{buttons}
|
||||
<div className="updates">
|
||||
{updateWidget}
|
||||
</div>
|
||||
<div className="login">
|
||||
{username}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var shell = require('shell');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
var OverlayTrigger = require('react-bootstrap').OverlayTrigger;
|
||||
var Tooltip = require('react-bootstrap').Tooltip;
|
||||
var util = require('../utils/Util');
|
||||
var containerActions = require('../actions/ContainerActions');
|
||||
var containerStore = require('../stores/ContainerStore');
|
||||
var tagStore = require('../stores/TagStore');
|
||||
var tagActions = require('../actions/TagActions');
|
||||
|
||||
var ImageCard = React.createClass({
|
||||
mixins: [Router.Navigation],
|
||||
|
@ -17,6 +19,23 @@ var ImageCard = React.createClass({
|
|||
chosenTag: 'latest'
|
||||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
tagStore.listen(this.update);
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
tagStore.unlisten(this.update);
|
||||
},
|
||||
update: function () {
|
||||
let repo = this.props.image.namespace + '/' + this.props.image.name;
|
||||
let state = tagStore.getState();
|
||||
if (this.state.tags.length && !state.tags[repo]) {
|
||||
$(this.getDOMNode()).find('.tag-overlay').fadeOut(300);
|
||||
}
|
||||
this.setState({
|
||||
loading: tagStore.getState().loading[repo] || false,
|
||||
tags: tagStore.getState().tags[repo] || []
|
||||
});
|
||||
},
|
||||
handleTagClick: function (tag) {
|
||||
this.setState({
|
||||
chosenTag: tag
|
||||
|
@ -25,72 +44,55 @@ var ImageCard = React.createClass({
|
|||
$tagOverlay.fadeOut(300);
|
||||
metrics.track('Selected Image Tag');
|
||||
},
|
||||
handleClick: function (repository) {
|
||||
handleClick: function () {
|
||||
metrics.track('Created Container', {
|
||||
from: 'search'
|
||||
from: 'search',
|
||||
private: this.props.image.is_private,
|
||||
official: this.props.image.namespace === 'library',
|
||||
userowned: this.props.image.is_user_repo,
|
||||
recommended: this.props.image.is_recommended
|
||||
});
|
||||
let name = containerStore.generateName(repository);
|
||||
containerActions.run(name, repository, this.state.chosenTag);
|
||||
let name = containerStore.generateName(this.props.image.name);
|
||||
let repo = this.props.image.namespace === 'library' ? this.props.image.name : this.props.image.namespace + '/' + this.props.image.name;
|
||||
containerActions.run(name, repo, this.state.chosenTag);
|
||||
this.transitionTo('containerHome', {name});
|
||||
},
|
||||
handleTagOverlayClick: function (name) {
|
||||
var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');
|
||||
handleTagOverlayClick: function () {
|
||||
let $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');
|
||||
$tagOverlay.fadeIn(300);
|
||||
$.get('https://registry.hub.docker.com/v1/repositories/' + name + '/tags', result => {
|
||||
this.setState({
|
||||
tags: result
|
||||
});
|
||||
});
|
||||
tagActions.tags(this.props.image.namespace + '/' + this.props.image.name);
|
||||
},
|
||||
handleCloseTagOverlay: function () {
|
||||
var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');
|
||||
$tagOverlay.fadeOut(300);
|
||||
},
|
||||
handleRepoClick: function () {
|
||||
var $repoUri = 'https://registry.hub.docker.com/';
|
||||
if (this.props.image.is_official) {
|
||||
$repoUri = $repoUri + "_/";
|
||||
var repoUri = 'https://registry.hub.docker.com/';
|
||||
if (this.props.image.namespace === 'library') {
|
||||
repoUri = repoUri + '_/' + this.props.image.name;
|
||||
} else {
|
||||
$repoUri = $repoUri + "u/";
|
||||
repoUri = repoUri + 'u/' + this.props.image.namespace + '/' + this.props.image.name;
|
||||
}
|
||||
util.exec(['open', $repoUri + this.props.image.name]);
|
||||
},
|
||||
componentDidMount: function() {
|
||||
$.get('https://registry.hub.docker.com/v1/repositories/' + this.props.image.name + '/tags', result => {
|
||||
this.setState({
|
||||
tags: result,
|
||||
chosenTag: result[0].name
|
||||
});
|
||||
});
|
||||
shell.openExternal(repoUri);
|
||||
},
|
||||
render: function () {
|
||||
var self = this;
|
||||
var name;
|
||||
var imageNameTokens = this.props.image.name.split('/');
|
||||
var namespace;
|
||||
var repo;
|
||||
if (imageNameTokens.length > 1) {
|
||||
namespace = imageNameTokens[0];
|
||||
repo = imageNameTokens[1];
|
||||
} else {
|
||||
namespace = "official";
|
||||
repo = imageNameTokens[0];
|
||||
}
|
||||
if (this.props.image.is_official) {
|
||||
let name;
|
||||
if (this.props.image.namespace === 'library') {
|
||||
name = (
|
||||
<div>
|
||||
<div className="namespace official">{namespace}</div>
|
||||
<div className="namespace official">official</div>
|
||||
<OverlayTrigger placement="bottom" overlay={<Tooltip>View on Docker Hub</Tooltip>}>
|
||||
<span className="repo" onClick={this.handleRepoClick}>{repo}</span>
|
||||
<span className="repo" onClick={this.handleRepoClick}>{this.props.image.name}</span>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
name = (
|
||||
<div>
|
||||
<div className="namespace">{namespace}</div>
|
||||
<div className="namespace">{this.props.image.namespace}</div>
|
||||
<OverlayTrigger placement="bottom" overlay={<Tooltip>View on Docker Hub</Tooltip>}>
|
||||
<span className="repo" onClick={this.handleRepoClick}>{repo}</span>
|
||||
<span className="repo" onClick={this.handleRepoClick}>{this.props.image.name}</span>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
|
@ -111,12 +113,16 @@ var ImageCard = React.createClass({
|
|||
imgsrc = 'http://kitematic.com/recommended/kitematic_html.png';
|
||||
}
|
||||
var tags;
|
||||
if (self.state.tags.length > 0) {
|
||||
if (self.state.loading) {
|
||||
tags = <RetinaImage className="tags-loading" src="loading-white.png"/>;
|
||||
} else if (self.state.tags.length === 0) {
|
||||
tags = <span>No Tags</span>;
|
||||
} else {
|
||||
var tagDisplay = self.state.tags.map(function (t) {
|
||||
if (t.name === self.state.chosenTag) {
|
||||
return <div className="tag active" key={t.name} onClick={self.handleTagClick.bind(self, t.name)}>{t.name}</div>;
|
||||
if (t === self.state.chosenTag) {
|
||||
return <div className="tag active" key={t} onClick={self.handleTagClick.bind(self, t)}>{t}</div>;
|
||||
} else {
|
||||
return <div className="tag" key={t.name} onClick={self.handleTagClick.bind(self, t.name)}>{t.name}</div>;
|
||||
return <div className="tag" key={t} onClick={self.handleTagClick.bind(self, t)}>{t}</div>;
|
||||
}
|
||||
});
|
||||
tags = (
|
||||
|
@ -124,13 +130,15 @@ var ImageCard = React.createClass({
|
|||
{tagDisplay}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
tags = <RetinaImage className="tags-loading" src="loading-white.png"/>;
|
||||
}
|
||||
var officialBadge;
|
||||
if (this.props.image.is_official) {
|
||||
officialBadge = (
|
||||
<RetinaImage src="official.png" />
|
||||
var badge = null;
|
||||
if (this.props.image.namespace === 'library') {
|
||||
badge = (
|
||||
<RetinaImage src="official.png"/>
|
||||
);
|
||||
} else if (this.props.image.is_private) {
|
||||
badge = (
|
||||
<RetinaImage src="private.png"/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@ -143,7 +151,7 @@ var ImageCard = React.createClass({
|
|||
</div>
|
||||
<div className="card">
|
||||
<div className="badges">
|
||||
{officialBadge}
|
||||
{badge}
|
||||
</div>
|
||||
<div className="name">
|
||||
{name}
|
||||
|
@ -152,20 +160,16 @@ var ImageCard = React.createClass({
|
|||
{description}
|
||||
</div>
|
||||
<div className="actions">
|
||||
<OverlayTrigger placement="bottom" overlay={<Tooltip>Favorites</Tooltip>}>
|
||||
<div className="stars">
|
||||
<span className="icon icon-star-9"></span>
|
||||
<span className="text">{this.props.image.star_count}</span>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
<div className="stars">
|
||||
<span className="icon icon-star-9"></span>
|
||||
<span className="text">{this.props.image.star_count}</span>
|
||||
</div>
|
||||
<div className="tags">
|
||||
<span className="icon icon-bookmark-2"></span>
|
||||
<OverlayTrigger placement="bottom" overlay={<Tooltip>Change Tag</Tooltip>}>
|
||||
<span className="text" onClick={self.handleTagOverlayClick.bind(self, this.props.image.name)} data-name={this.props.image.name}>{this.state.chosenTag}</span>
|
||||
</OverlayTrigger>
|
||||
<span className="text" onClick={self.handleTagOverlayClick.bind(self, this.props.image.name)} data-name={this.props.image.name}>{this.state.chosenTag}</span>
|
||||
</div>
|
||||
<div className="action">
|
||||
<a className="btn btn-action" onClick={self.handleClick.bind(self, this.props.image.name)}>Create</a>
|
||||
<a className="btn btn-action" onClick={self.handleClick}>Create</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var React = require('react');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var Radial = require('./Radial.react');
|
||||
var ImageCard = require('./ImageCard.react');
|
||||
var Promise = require('bluebird');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
var classNames = require('classnames');
|
||||
|
||||
var _recommended = [];
|
||||
var _searchPromise = null;
|
||||
|
||||
var NewContainer = React.createClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
query: '',
|
||||
loading: false,
|
||||
results: _recommended
|
||||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.refs.searchInput.getDOMNode().focus();
|
||||
this.recommended();
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
if (_searchPromise) {
|
||||
_searchPromise.cancel();
|
||||
}
|
||||
},
|
||||
search: function (query) {
|
||||
if (_searchPromise) {
|
||||
_searchPromise.cancel();
|
||||
_searchPromise = null;
|
||||
}
|
||||
|
||||
if (!query.length) {
|
||||
this.setState({
|
||||
query: query,
|
||||
results: _recommended,
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
query: query,
|
||||
loading: true
|
||||
});
|
||||
|
||||
_searchPromise = Promise.delay(200).then(() => Promise.resolve($.get('https://registry.hub.docker.com/v1/search?q=' + query))).cancellable().then(data => {
|
||||
metrics.track('Searched for Images');
|
||||
this.setState({
|
||||
results: data.results,
|
||||
query: query,
|
||||
loading: false
|
||||
});
|
||||
_searchPromise = null;
|
||||
}).catch(Promise.CancellationError, () => {
|
||||
});
|
||||
},
|
||||
recommended: function () {
|
||||
if (_recommended.length) {
|
||||
return;
|
||||
}
|
||||
Promise.resolve($.ajax({
|
||||
url: 'https://kitematic.com/recommended.json',
|
||||
cache: false,
|
||||
dataType: 'json',
|
||||
})).then(res => res.repos).map(repo => {
|
||||
var query = repo.repo;
|
||||
var vals = query.split('/');
|
||||
if (vals.length === 1) {
|
||||
query = 'library/' + vals[0];
|
||||
}
|
||||
return $.get('https://registry.hub.docker.com/v1/repositories_info/' + query).then(data => {
|
||||
var res = _.extend(data, repo);
|
||||
res.description = data.short_description;
|
||||
res.is_official = data.namespace === 'library';
|
||||
res.name = data.repo;
|
||||
res.star_count = data.stars;
|
||||
return res;
|
||||
});
|
||||
}).then(results => {
|
||||
_recommended = results.filter(r => !!r);
|
||||
if (!this.state.query.length && this.isMounted()) {
|
||||
this.setState({
|
||||
results: _recommended
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
},
|
||||
handleChange: function (e) {
|
||||
var query = e.target.value;
|
||||
if (query === this.state.query) {
|
||||
return;
|
||||
}
|
||||
this.search(query);
|
||||
},
|
||||
render: function () {
|
||||
var title = this.state.query ? 'Results' : 'Recommended';
|
||||
var data = this.state.results;
|
||||
var results;
|
||||
if (data.length) {
|
||||
var items = data.map(function (image) {
|
||||
return (
|
||||
<ImageCard key={image.name} image={image} />
|
||||
);
|
||||
});
|
||||
|
||||
results = (
|
||||
<div className="result-grid">
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
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 = classNames({
|
||||
hidden: !this.state.loading,
|
||||
loading: true
|
||||
});
|
||||
var magnifierClasses = classNames({
|
||||
hidden: this.state.loading,
|
||||
icon: true,
|
||||
'icon-magnifier': true,
|
||||
'search-icon': true
|
||||
});
|
||||
return (
|
||||
<div className="details">
|
||||
<div className="new-container">
|
||||
<div className="new-container-header">
|
||||
<div className="text">
|
||||
Select a Docker image to create a new container.
|
||||
</div>
|
||||
<div className="search">
|
||||
<div className="search-bar">
|
||||
<input type="search" ref="searchInput" className="form-control" placeholder="Search Docker Hub for an image" onChange={this.handleChange}/>
|
||||
<div className={magnifierClasses}></div>
|
||||
<RetinaImage className={loadingClasses} src="loading.png"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="results">
|
||||
<h4>{title}</h4>
|
||||
{results}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = NewContainer;
|
|
@ -1,32 +1,57 @@
|
|||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var Radial = require('./Radial.react');
|
||||
var ImageCard = require('./ImageCard.react');
|
||||
var Promise = require('bluebird');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
var classNames = require('classnames');
|
||||
var repositoryActions = require('../actions/RepositoryActions');
|
||||
var repositoryStore = require('../stores/RepositoryStore');
|
||||
var accountStore = require('../stores/AccountStore');
|
||||
var accountActions = require('../actions/AccountActions');
|
||||
|
||||
var _recommended = [];
|
||||
var _searchPromise = null;
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [Router.Navigation, Router.State],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
query: '',
|
||||
loading: false,
|
||||
results: _recommended
|
||||
loading: repositoryStore.loading(),
|
||||
repos: repositoryStore.all(),
|
||||
username: accountStore.getState().username,
|
||||
verified: accountStore.getState().verified,
|
||||
accountLoading: accountStore.getState().loading,
|
||||
error: repositoryStore.getState().error
|
||||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.refs.searchInput.getDOMNode().focus();
|
||||
this.recommended();
|
||||
repositoryStore.listen(this.update);
|
||||
accountStore.listen(this.updateAccount);
|
||||
repositoryActions.search();
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
if (_searchPromise) {
|
||||
_searchPromise.cancel();
|
||||
}
|
||||
|
||||
repositoryStore.unlisten(this.update);
|
||||
accountStore.unlisten(this.updateAccount);
|
||||
},
|
||||
update: function () {
|
||||
this.setState({
|
||||
loading: repositoryStore.loading(),
|
||||
repos: repositoryStore.all()
|
||||
});
|
||||
},
|
||||
updateAccount: function () {
|
||||
this.setState({
|
||||
username: accountStore.getState().username,
|
||||
verified: accountStore.getState().verified,
|
||||
accountLoading: accountStore.getState().loading
|
||||
});
|
||||
},
|
||||
search: function (query) {
|
||||
if (_searchPromise) {
|
||||
|
@ -34,63 +59,16 @@ module.exports = React.createClass({
|
|||
_searchPromise = null;
|
||||
}
|
||||
|
||||
if (!query.length) {
|
||||
this.setState({
|
||||
query: query,
|
||||
results: _recommended,
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
query: query,
|
||||
loading: true
|
||||
});
|
||||
|
||||
_searchPromise = Promise.delay(200).cancellable().then(() => Promise.resolve($.get('https://registry.hub.docker.com/v1/search?q=' + query))).then(data => {
|
||||
_searchPromise = Promise.delay(200).cancellable().then(() => {
|
||||
metrics.track('Searched for Images');
|
||||
this.setState({
|
||||
results: data.results,
|
||||
query: query,
|
||||
loading: false
|
||||
});
|
||||
_searchPromise = null;
|
||||
}).catch(Promise.CancellationError, () => {
|
||||
});
|
||||
},
|
||||
recommended: function () {
|
||||
if (_recommended.length) {
|
||||
return;
|
||||
}
|
||||
Promise.resolve($.ajax({
|
||||
url: 'https://kitematic.com/recommended.json',
|
||||
cache: false,
|
||||
dataType: 'json',
|
||||
})).then(res => res.repos).map(repo => {
|
||||
var query = repo.repo;
|
||||
var vals = query.split('/');
|
||||
if (vals.length === 1) {
|
||||
query = 'library/' + vals[0];
|
||||
}
|
||||
return $.get('https://registry.hub.docker.com/v1/repositories_info/' + query).then(data => {
|
||||
var res = _.extend(data, repo);
|
||||
res.description = data.short_description;
|
||||
res.is_official = data.namespace === 'library';
|
||||
res.name = data.repo;
|
||||
res.star_count = data.stars;
|
||||
return res;
|
||||
});
|
||||
}).then(results => {
|
||||
_recommended = results.filter(r => !!r);
|
||||
if (!this.state.query.length && this.isMounted()) {
|
||||
this.setState({
|
||||
results: _recommended
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
repositoryActions.search(query);
|
||||
}).catch(Promise.CancellationError, () => {});
|
||||
},
|
||||
handleChange: function (e) {
|
||||
var query = e.target.value;
|
||||
|
@ -99,67 +77,162 @@ module.exports = React.createClass({
|
|||
}
|
||||
this.search(query);
|
||||
},
|
||||
handleFilter: function (filter) {
|
||||
|
||||
// If we're clicking on the filter again - refresh
|
||||
if (filter === 'userrepos' && this.getQuery().filter === 'userrepos') {
|
||||
repositoryActions.repos();
|
||||
}
|
||||
|
||||
if (filter === 'recommended' && this.getQuery().filter === 'recommended') {
|
||||
repositoryActions.recommended();
|
||||
}
|
||||
|
||||
this.transitionTo('search', {}, {filter: filter});
|
||||
|
||||
metrics.track('Filtered Results', {
|
||||
filter: filter
|
||||
});
|
||||
},
|
||||
handleCheckVerification: function () {
|
||||
accountActions.verify();
|
||||
metrics.track('Verified Account', {
|
||||
from: 'search'
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var title = this.state.query ? 'Results' : 'Recommended';
|
||||
var data = this.state.results;
|
||||
var results;
|
||||
if (data.length) {
|
||||
var items = data.map(function (image) {
|
||||
return (
|
||||
<ImageCard key={image.name} image={image} />
|
||||
);
|
||||
});
|
||||
let filter = this.getQuery().filter || 'all';
|
||||
let repos = _.values(this.state.repos)
|
||||
.filter(repo => repo.name.toLowerCase().indexOf(this.state.query.toLowerCase()) !== -1 || repo.namespace.toLowerCase().indexOf(this.state.query.toLowerCase()) !== -1)
|
||||
.filter(repo => filter === 'all' || (filter === 'recommended' && repo.is_recommended) || (filter === 'userrepos' && repo.is_user_repo));
|
||||
|
||||
let results;
|
||||
if (this.state.error) {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<h2>There was an error contacting Docker Hub.</h2>
|
||||
</div>
|
||||
);
|
||||
} else if (filter === 'userrepos' && !accountStore.getState().username) {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<h2><Router.Link to="login">Log In</Router.Link> or <Router.Link to="signup">Sign Up</Router.Link> to access your Docker Hub repositories.</h2>
|
||||
<RetinaImage src="connect-art.png" checkIfRetinaImgExists={false}/>
|
||||
</div>
|
||||
);
|
||||
} else if (filter === 'userrepos' && !accountStore.getState().verified) {
|
||||
let spinner = this.state.accountLoading ? <div className="spinner la-ball-clip-rotate la-dark"><div></div></div> : null;
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<h2>Please verify your Docker Hub account email address</h2>
|
||||
<div className="verify">
|
||||
<button className="btn btn-primary btn-lg" onClick={this.handleCheckVerification}>{'I\'ve Verified my Email Address'}</button> {spinner}
|
||||
</div>
|
||||
<RetinaImage src="inspection.png" checkIfRetinaImgExists={false}/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.loading) {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<div className="loader">
|
||||
<h2>Loading Images</h2>
|
||||
<div className="spinner la-ball-clip-rotate la-dark la-lg"><div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (repos.length) {
|
||||
let recommendedItems = repos.filter(repo => repo.is_recommended).map(image => <ImageCard key={image.namespace + '/' + image.name} image={image} />);
|
||||
let otherItems = repos.filter(repo => !repo.is_recommended && !repo.is_user_repo).map(image => <ImageCard key={image.namespace + '/' + image.name} image={image} />);
|
||||
|
||||
let recommendedResults = recommendedItems.length ? (
|
||||
<div>
|
||||
<h4>Recommended</h4>
|
||||
<div className="result-grid">
|
||||
{recommendedItems}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
let userRepoItems = repos.filter(repo => repo.is_user_repo).map(image => <ImageCard key={image.namespace + '/' + image.name} image={image} />);
|
||||
let userRepoResults = userRepoItems.length ? (
|
||||
<div>
|
||||
<h4>My Repositories</h4>
|
||||
<div className="result-grid">
|
||||
{userRepoItems}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
let otherResults = otherItems.length ? (
|
||||
<div>
|
||||
<h4>Other Repositories</h4>
|
||||
<div className="result-grid">
|
||||
{otherItems}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
results = (
|
||||
<div className="result-grid">
|
||||
{items}
|
||||
<div className="result-grids">
|
||||
{recommendedResults}
|
||||
{userRepoResults}
|
||||
{otherResults}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (this.state.results.length === 0 && this.state.query === '') {
|
||||
if (this.state.query.length) {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<div className="loader">
|
||||
<h2>Loading Images</h2>
|
||||
<Radial spin="true" progress={90} thick={true} transparent={true} />
|
||||
</div>
|
||||
<h2>Cannot find a matching image.</h2>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<h1>Cannot find a matching image.</h1>
|
||||
<h2>No Images</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
var loadingClasses = classNames({
|
||||
|
||||
let loadingClasses = classNames({
|
||||
hidden: !this.state.loading,
|
||||
loading: true
|
||||
spinner: true,
|
||||
loading: true,
|
||||
'la-ball-clip-rotate': true,
|
||||
'la-dark': true,
|
||||
'la-sm': true
|
||||
});
|
||||
var magnifierClasses = classNames({
|
||||
|
||||
let magnifierClasses = classNames({
|
||||
hidden: this.state.loading,
|
||||
icon: true,
|
||||
'icon-magnifier': true,
|
||||
'search-icon': true
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="details">
|
||||
<div className="new-container">
|
||||
<div className="new-container-header">
|
||||
<div className="text">
|
||||
Select a Docker image to create a new container.
|
||||
Select a Docker image to create a container.
|
||||
</div>
|
||||
<div className="search">
|
||||
<div className="search-bar">
|
||||
<input type="search" ref="searchInput" className="form-control" placeholder="Search Docker Hub for an image" onChange={this.handleChange}/>
|
||||
<div className={magnifierClasses}></div>
|
||||
<RetinaImage className={loadingClasses} src="loading.png"/>
|
||||
<div className={loadingClasses}><div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="results">
|
||||
<h4>{title}</h4>
|
||||
<div className="results-filters">
|
||||
<span className="results-filter results-filter-title">FILTER BY</span>
|
||||
<span className={`results-filter results-all tab ${filter === 'all' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'all')}>All</span>
|
||||
<span className={`results-filter results-recommended tab ${filter === 'recommended' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'recommended')}>Recommended</span>
|
||||
<span className={`results-filter results-userrepos tab ${filter === 'userrepos' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'userrepos')}>My Repositories</span>
|
||||
</div>
|
||||
{results}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -76,15 +76,17 @@ var Setup = React.createClass({
|
|||
renderStep: function () {
|
||||
return (
|
||||
<div className="setup">
|
||||
<Header />
|
||||
<div className="image">
|
||||
{this.renderContents()}
|
||||
</div>
|
||||
<div className="desc">
|
||||
<div className="content">
|
||||
<h4>Step {SetupStore.number()} out of {SetupStore.stepCount()}</h4>
|
||||
<h1>{SetupStore.step().title}</h1>
|
||||
<p>{SetupStore.step().message}</p>
|
||||
<Header hideLogin={true}/>
|
||||
<div className="setup-content">
|
||||
<div className="image">
|
||||
{this.renderContents()}
|
||||
</div>
|
||||
<div className="desc">
|
||||
<div className="content">
|
||||
<h4>Step {SetupStore.number()} out of {SetupStore.stepCount()}</h4>
|
||||
<h1>{SetupStore.step().title}</h1>
|
||||
<p>{SetupStore.step().message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -93,17 +95,19 @@ var Setup = React.createClass({
|
|||
renderCancelled: function () {
|
||||
return (
|
||||
<div className="setup">
|
||||
<Header />
|
||||
<div className="image">
|
||||
{this.renderContents()}
|
||||
</div>
|
||||
<div className="desc">
|
||||
<div className="content">
|
||||
<h4>Setup Cancelled</h4>
|
||||
<h1>Couldn't Install Requirements</h1>
|
||||
<p>Kitematic didn't receive the administrative privileges required to install or upgrade VirtualBox & Docker.</p>
|
||||
<p>Please click retry. If VirtualBox is not installed, you can download & install it manually from the <a onClick={this.handleOpenWebsite}>official Oracle website</a>.</p>
|
||||
<p><button className="btn btn-action" onClick={this.handleCancelRetry}>Retry</button></p>
|
||||
<Header hideLogin={true}/>
|
||||
<div className="setup-content">
|
||||
<div className="image">
|
||||
{this.renderContents()}
|
||||
</div>
|
||||
<div className="desc">
|
||||
<div className="content">
|
||||
<h4>Setup Cancelled</h4>
|
||||
<h1>Couldn't Install Requirements</h1>
|
||||
<p>Kitematic didn't receive the administrative privileges required to install or upgrade VirtualBox & Docker.</p>
|
||||
<p>Please click retry. If VirtualBox is not installed, you can download & install it manually from the <a onClick={this.handleOpenWebsite}>official Oracle website</a>.</p>
|
||||
<p><button className="btn btn-action" onClick={this.handleCancelRetry}>Retry</button></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -112,24 +116,26 @@ var Setup = React.createClass({
|
|||
renderError: function () {
|
||||
return (
|
||||
<div className="setup">
|
||||
<Header />
|
||||
<div className="image">
|
||||
<div className="contents">
|
||||
<RetinaImage src="install-error.png" checkIfRetinaImgExists={false}/>
|
||||
<div className="detail">
|
||||
<Header hideLogin={true}/>
|
||||
<div className="setup-content">
|
||||
<div className="image">
|
||||
<div className="contents">
|
||||
<RetinaImage src="install-error.png" checkIfRetinaImgExists={false}/>
|
||||
<div className="detail">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="desc">
|
||||
<div className="content">
|
||||
<h4>Setup Error</h4>
|
||||
<h1>We're Sorry!</h1>
|
||||
<p>There seems to have been an unexpected error with Kitematic:</p>
|
||||
<p className="error">{this.state.error.message || this.state.error}</p>
|
||||
<p className="setup-actions">
|
||||
<button className="btn btn-action" onClick={this.handleErrorRetry}>Retry Setup</button>
|
||||
<button className="btn btn-action" onClick={this.handleErrorRemoveRetry}>Delete VM and Retry Setup</button>
|
||||
</p>
|
||||
<div className="desc">
|
||||
<div className="content">
|
||||
<h4>Setup Error</h4>
|
||||
<h1>We're Sorry!</h1>
|
||||
<p>There seems to have been an unexpected error with Kitematic:</p>
|
||||
<p className="error">{this.state.error.message || this.state.error}</p>
|
||||
<p className="setup-actions">
|
||||
<button className="btn btn-action" onClick={this.handleErrorRetry}>Retry Setup</button>
|
||||
<button className="btn btn-action" onClick={this.handleErrorRemoveRetry}>Delete VM and Retry Setup</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
var Router = require('react-router');
|
||||
var routes = require('./routes');
|
||||
module.exports = {
|
||||
router: null,
|
||||
|
||||
var router = Router.create({
|
||||
routes: routes
|
||||
});
|
||||
get: function () {
|
||||
return this.router;
|
||||
},
|
||||
|
||||
module.exports = router;
|
||||
set: function (router) {
|
||||
this.router = router;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
var React = require('react/addons');
|
||||
var Setup = require('./components/Setup.react');
|
||||
var Account = require('./components/Account.react');
|
||||
var AccountSignup = require('./components/AccountSignup.react');
|
||||
var AccountLogin = require('./components/AccountLogin.react');
|
||||
var Containers = require('./components/Containers.react');
|
||||
var ContainerDetails = require('./components/ContainerDetails.react');
|
||||
var ContainerHome = require('./components/ContainerHome.react');
|
||||
|
@ -16,7 +19,6 @@ var Router = require('react-router');
|
|||
var Route = Router.Route;
|
||||
var DefaultRoute = Router.DefaultRoute;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
var Redirect = Router.Redirect;
|
||||
|
||||
var App = React.createClass({
|
||||
render: function () {
|
||||
|
@ -28,6 +30,10 @@ var App = React.createClass({
|
|||
|
||||
var routes = (
|
||||
<Route name="app" path="/" handler={App}>
|
||||
<Route name="account" path="/account" handler={Account}>
|
||||
<Route name="signup" path="/account/signup" handler={AccountSignup}/>
|
||||
<Route name="login" path="/account/login" handler={AccountLogin}/>
|
||||
</Route>
|
||||
<Route name="containers" handler={Containers}>
|
||||
<Route name="container" path="containers/details/:name" handler={ContainerDetails}>
|
||||
<DefaultRoute name="containerHome" handler={ContainerHome} />
|
||||
|
@ -45,7 +51,6 @@ var routes = (
|
|||
<Route name="preferences" path="/preferences" handler={Preferences}/>
|
||||
</Route>
|
||||
<DefaultRoute name="setup" handler={Setup}/>
|
||||
<Redirect from="containers/details/:name" to="containerHome"/>
|
||||
</Route>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import alt from '../alt';
|
||||
import accountServerActions from '../actions/AccountServerActions';
|
||||
import accountActions from '../actions/AccountActions';
|
||||
|
||||
class AccountStore {
|
||||
constructor () {
|
||||
this.bindActions(accountServerActions);
|
||||
this.bindActions(accountActions);
|
||||
|
||||
this.prompted = false;
|
||||
this.loading = false;
|
||||
this.errors = {};
|
||||
|
||||
this.verified = false;
|
||||
this.username = null;
|
||||
}
|
||||
|
||||
skip () {
|
||||
this.setState({
|
||||
prompted: true
|
||||
});
|
||||
}
|
||||
|
||||
login () {
|
||||
this.setState({
|
||||
loading: true,
|
||||
errors: {}
|
||||
});
|
||||
}
|
||||
|
||||
logout () {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errors: {},
|
||||
username: null,
|
||||
verified: false
|
||||
});
|
||||
}
|
||||
|
||||
signup () {
|
||||
this.setState({
|
||||
loading: true,
|
||||
errors: {}
|
||||
});
|
||||
}
|
||||
|
||||
loggedin ({username, verified}) {
|
||||
this.setState({username, verified, errors: {}, loading: false});
|
||||
}
|
||||
|
||||
signedup ({username}) {
|
||||
this.setState({username, errors: {}, loading: false});
|
||||
}
|
||||
|
||||
verify () {
|
||||
this.setState({loading: true});
|
||||
}
|
||||
|
||||
verified ({verified}) {
|
||||
this.setState({verified, loading: false});
|
||||
}
|
||||
|
||||
prompted ({prompted}) {
|
||||
this.setState({prompted});
|
||||
}
|
||||
|
||||
errors ({errors}) {
|
||||
this.setState({errors, loading: false});
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createStore(AccountStore);
|
|
@ -0,0 +1,76 @@
|
|||
import _ from 'underscore';
|
||||
import alt from '../alt';
|
||||
import repositoryServerActions from '../actions/RepositoryServerActions';
|
||||
import repositoryActions from '../actions/RepositoryActions';
|
||||
import accountServerActions from '../actions/AccountServerActions';
|
||||
import accountStore from './AccountStore';
|
||||
|
||||
class RepositoryStore {
|
||||
constructor () {
|
||||
this.bindActions(repositoryActions);
|
||||
this.bindActions(repositoryServerActions);
|
||||
this.bindActions(accountServerActions);
|
||||
this.results = [];
|
||||
this.recommended = [];
|
||||
this.repos = [];
|
||||
this.reposLoading = false;
|
||||
this.recommendedLoading = false;
|
||||
this.resultsLoading = false;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
error ({error}) {
|
||||
this.setState({error: error, reposLoading: false, recommendedLoading: false, resultsLoading: false});
|
||||
}
|
||||
|
||||
repos () {
|
||||
this.setState({reposError: null, reposLoading: true});
|
||||
}
|
||||
|
||||
reposLoading () {
|
||||
this.setState({reposLoading: true});
|
||||
}
|
||||
|
||||
reposUpdated ({repos}) {
|
||||
let accountState = accountStore.getState();
|
||||
|
||||
if (accountState.username && accountState.verified) {
|
||||
this.setState({repos, reposLoading: false});
|
||||
} else {
|
||||
this.setState({repos: [], reposLoading: false});
|
||||
}
|
||||
}
|
||||
|
||||
search () {
|
||||
this.setState({error: null, resultsLoading: true});
|
||||
}
|
||||
|
||||
resultsUpdated ({repos}) {
|
||||
this.setState({results: repos, resultsLoading: false});
|
||||
}
|
||||
|
||||
recommended () {
|
||||
this.setState({error: null, recommendedLoading: true});
|
||||
}
|
||||
|
||||
recommendedUpdated ({repos}) {
|
||||
this.setState({recommended: repos, recommendedLoading: false});
|
||||
}
|
||||
|
||||
loggedout () {
|
||||
this.setState({repos: []});
|
||||
}
|
||||
|
||||
static all () {
|
||||
let state = this.getState();
|
||||
let all = state.recommended.concat(state.repos).concat(state.results);
|
||||
return _.uniq(all, false, repo => repo.namespace + '/' + repo.name);
|
||||
}
|
||||
|
||||
static loading () {
|
||||
let state = this.getState();
|
||||
return state.recommendedLoading || state.resultsLoading || state.reposLoading;
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createStore(RepositoryStore);
|
|
@ -59,7 +59,7 @@ var _steps = [{
|
|||
message: 'To run Docker containers on your computer, Kitematic is starting a Linux virtual machine. This may take a minute...',
|
||||
totalPercent: 60,
|
||||
percent: 0,
|
||||
seconds: 58,
|
||||
seconds: 72,
|
||||
run: Promise.coroutine(function* (progressCallback) {
|
||||
setupUtil.simulateProgress(this.seconds, progressCallback);
|
||||
var exists = yield machine.exists();
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import alt from '../alt';
|
||||
import tagActions from '../actions/TagActions';
|
||||
import tagServerActions from '../actions/TagServerActions';
|
||||
import accountServerActions from '../actions/AccountServerActions';
|
||||
|
||||
class TagStore {
|
||||
constructor () {
|
||||
this.bindActions(tagActions);
|
||||
this.bindActions(tagServerActions);
|
||||
this.bindActions(accountServerActions);
|
||||
|
||||
// maps 'namespace/name' => [list of tags]
|
||||
this.tags = {};
|
||||
|
||||
// maps 'namespace/name' => true / false
|
||||
this.loading = {};
|
||||
}
|
||||
|
||||
tags ({repo}) {
|
||||
this.loading[repo] = true;
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
tagsUpdated ({repo, tags}) {
|
||||
this.tags[repo] = tags;
|
||||
this.loading[repo] = false;
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
remove ({repo}) {
|
||||
delete this.tags[repo];
|
||||
delete this.loading[repo];
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
loggedout () {
|
||||
this.loading = {};
|
||||
this.tags = {};
|
||||
this.emitChange();
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createStore(TagStore);
|
|
@ -4,7 +4,7 @@ import path from 'path';
|
|||
import dockerode from 'dockerode';
|
||||
import _ from 'underscore';
|
||||
import util from './Util';
|
||||
import registry from '../utils/RegistryUtil';
|
||||
import hubUtil from './HubUtil';
|
||||
import metrics from '../utils/MetricsUtil';
|
||||
import containerServerActions from '../actions/ContainerServerActions';
|
||||
import Promise from 'bluebird';
|
||||
|
@ -317,7 +317,6 @@ export default {
|
|||
stream.setEncoding('utf8');
|
||||
stream.on('data', json => {
|
||||
let data = JSON.parse(json);
|
||||
// console.log(data);
|
||||
|
||||
if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete') {
|
||||
return;
|
||||
|
@ -335,160 +334,117 @@ export default {
|
|||
},
|
||||
|
||||
pullImage (repository, tag, callback, progressCallback, blockedCallback) {
|
||||
registry.layers(repository, tag, (err, layerSizes) => {
|
||||
let opts = {}, config = hubUtil.config();
|
||||
if (!hubUtil.config()) {
|
||||
opts = {};
|
||||
} else {
|
||||
let [username, password] = hubUtil.creds(config);
|
||||
opts = {
|
||||
authconfig: {
|
||||
username,
|
||||
password,
|
||||
auth: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Support v2 registry API
|
||||
// TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
|
||||
// Use the per-layer pull progress % to update the total progress.
|
||||
this.client.listImages({all: 1}, (err, images) => {
|
||||
images = images || [];
|
||||
this.client.pull(repository + ':' + tag, opts, (err, stream) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let existingIds = new Set(images.map(function (image) {
|
||||
return image.Id.slice(0, 12);
|
||||
}));
|
||||
stream.setEncoding('utf8');
|
||||
|
||||
let layersToDownload = layerSizes.filter(function (layerSize) {
|
||||
return !existingIds.has(layerSize.Id);
|
||||
});
|
||||
// scheduled to inform about progression at given interval
|
||||
let tick = null;
|
||||
let layerProgress = {};
|
||||
|
||||
console.log("existingIds:" + existingIds.size)
|
||||
console.log("layersToDownload:" + layersToDownload.length)
|
||||
// Split the loading in a few columns for more feedback
|
||||
let columns = {};
|
||||
columns.amount = 4; // arbitrary
|
||||
columns.toFill = 0; // the current column index, waiting for layer IDs to be displayed
|
||||
|
||||
this.client.pull(repository + ':' + tag, (err, stream) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
stream.setEncoding('utf8');
|
||||
// data is associated with one layer only (can be identified with id)
|
||||
stream.on('data', str => {
|
||||
var data = JSON.parse(str);
|
||||
|
||||
// layerProgress contains progression infos for all layers
|
||||
let layerProgress = layersToDownload.reduce(function (r, layer) {
|
||||
if (_.findWhere(images, {Id: layer.Id})) {
|
||||
// If the layer is already here, we set current and total to 1
|
||||
r[layer.Id] = {current:1, total:1, column:-1};
|
||||
} else {
|
||||
// At this point, the total layer size is unknown
|
||||
// so we set total to -1 to avoid displaying it
|
||||
r[layer.Id] = {current:0, total:-1, column:-1};
|
||||
}
|
||||
return r;
|
||||
}, {});
|
||||
if (data.error) {
|
||||
callback(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) {
|
||||
blockedCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let layersToLoad = _.keys(layerProgress).length;
|
||||
|
||||
console.log("nbLayers:" + layersToLoad);
|
||||
|
||||
// Split the loading in a few columns for more feedback
|
||||
let columns = {};
|
||||
columns.amount = 4; // arbitrary
|
||||
columns.progress = []; // layerIDs, nbLayers, maxLayers, progress value
|
||||
columns.toFill = 0; // the current column index, waiting for layer IDs to be displayed
|
||||
|
||||
for (let i = 0; i < columns.amount; i++)
|
||||
{
|
||||
let layerAmount = Math.ceil(layersToLoad / (columns.amount - i));
|
||||
layersToLoad -= layerAmount;
|
||||
columns.progress[i] = { layerIDs:[], nbLayers:0 , maxLayers:layerAmount , value:0.0 };
|
||||
}
|
||||
|
||||
|
||||
|
||||
// scheduled to inform about progression at given interval
|
||||
let tick = null;
|
||||
|
||||
|
||||
// data is associated with one layer only (can be identified with id)
|
||||
stream.on('data', str => {
|
||||
var data = JSON.parse(str);
|
||||
|
||||
if (data.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) {
|
||||
blockedCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'Already exists') {
|
||||
|
||||
console.log("Already exists.");
|
||||
|
||||
//layerProgress[data.id].current = 1;
|
||||
//layerProgress[data.id].total = 1;
|
||||
|
||||
} else if (data.status === 'Downloading') {
|
||||
|
||||
// aduermael: How total could be <= 0 ?
|
||||
|
||||
// if (data.progressDetail.total <= 0) {
|
||||
// progressCallback(0);
|
||||
// return;
|
||||
// } else {
|
||||
|
||||
layerProgress[data.id].current = data.progressDetail.current;
|
||||
layerProgress[data.id].total = data.progressDetail.total;
|
||||
|
||||
// Assign to a column if not done yet
|
||||
if (layerProgress[data.id].column == -1)
|
||||
{
|
||||
// test if we can still add layers to that column
|
||||
if (columns.progress[columns.toFill].nbLayers == columns.progress[columns.toFill].maxLayers) columns.toFill++;
|
||||
|
||||
layerProgress[data.id].column = columns.toFill;
|
||||
columns.progress[columns.toFill].layerIDs.push(data.id);
|
||||
columns.progress[columns.toFill].nbLayers++;
|
||||
|
||||
if (data.status === 'Pulling fs layer') {
|
||||
layerProgress[data.id] = {
|
||||
current: 0,
|
||||
total: 1
|
||||
};
|
||||
} else if (data.status === 'Downloading') {
|
||||
if (!columns.progress) {
|
||||
columns.progress = []; // layerIDs, nbLayers, maxLayers, progress value
|
||||
let layersToLoad = _.keys(layerProgress).length;
|
||||
let layersPerColumn = Math.floor(layersToLoad / columns.amount);
|
||||
let leftOverLayers = layersToLoad % columns.amount;
|
||||
for (let i = 0; i < columns.amount; i++) {
|
||||
let layerAmount = layersPerColumn;
|
||||
if (i < leftOverLayers) {
|
||||
layerAmount += 1;
|
||||
}
|
||||
columns.progress[i] = {layerIDs: [], nbLayers:0 , maxLayers: layerAmount, value: 0.0};
|
||||
}
|
||||
}
|
||||
|
||||
//}
|
||||
layerProgress[data.id].current = data.progressDetail.current;
|
||||
layerProgress[data.id].total = data.progressDetail.total;
|
||||
|
||||
if (!tick) {
|
||||
tick = setInterval( function(){
|
||||
// console.log(JSON.stringify(layerProgress))
|
||||
// Assign to a column if not done yet
|
||||
if (!layerProgress[data.id].column) {
|
||||
// test if we can still add layers to that column
|
||||
if (columns.progress[columns.toFill].nbLayers === columns.progress[columns.toFill].maxLayers && columns.toFill < columns.amount - 1) {
|
||||
columns.toFill++;
|
||||
}
|
||||
|
||||
// update values
|
||||
for (let i = 0; i < columns.amount; i++)
|
||||
{
|
||||
columns.progress[i].value = 0.0;
|
||||
layerProgress[data.id].column = columns.toFill;
|
||||
columns.progress[columns.toFill].layerIDs.push(data.id);
|
||||
columns.progress[columns.toFill].nbLayers++;
|
||||
}
|
||||
|
||||
// Start only if the column has accurate values for all layers
|
||||
if (columns.progress[i].nbLayers == columns.progress[i].maxLayers)
|
||||
{
|
||||
let layer;
|
||||
let totalSum = 0;
|
||||
let currentSum = 0;
|
||||
if (!tick) {
|
||||
tick = setTimeout(() => {
|
||||
clearInterval(tick);
|
||||
tick = null;
|
||||
for (let i = 0; i < columns.amount; i++) {
|
||||
columns.progress[i].value = 0.0;
|
||||
if (columns.progress[i].nbLayers > 0) {
|
||||
let layer;
|
||||
let totalSum = 0;
|
||||
let currentSum = 0;
|
||||
|
||||
for (let j = 0; j < columns.progress[i].nbLayers; j++)
|
||||
{
|
||||
layer = layerProgress[columns.progress[i].layerIDs[j]];
|
||||
totalSum += layer.total;
|
||||
currentSum += layer.current;
|
||||
}
|
||||
|
||||
if (totalSum > 0) columns.progress[i].value = 100.0 * currentSum / totalSum;
|
||||
else columns.progress[i].value = 0.0;
|
||||
}
|
||||
for (let j = 0; j < columns.progress[i].nbLayers; j++) {
|
||||
layer = layerProgress[columns.progress[i].layerIDs[j]];
|
||||
totalSum += layer.total;
|
||||
currentSum += layer.current;
|
||||
}
|
||||
|
||||
progressCallback(columns);
|
||||
|
||||
},33);
|
||||
if (totalSum > 0) {
|
||||
columns.progress[i].value = Math.min(100.0 * currentSum / totalSum, 100);
|
||||
} else {
|
||||
columns.progress[i].value = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
|
||||
clearInterval(tick);
|
||||
callback();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
progressCallback(columns);
|
||||
}, 16);
|
||||
}
|
||||
}
|
||||
});
|
||||
stream.on('end', function () {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
var _ = require('underscore');
|
||||
var request = require('request');
|
||||
var accountServerActions = require('../actions/AccountServerActions');
|
||||
|
||||
module.exports = {
|
||||
init: function () {
|
||||
accountServerActions.prompted({prompted: localStorage.getItem('auth.prompted')});
|
||||
let username = localStorage.getItem('auth.username');
|
||||
let verified = localStorage.getItem('auth.verified') === 'true';
|
||||
if (username) {
|
||||
accountServerActions.loggedin({username, verified});
|
||||
}
|
||||
},
|
||||
|
||||
username: function () {
|
||||
return localStorage.getItem('auth.username') || null;
|
||||
},
|
||||
|
||||
// Returns the base64 encoded index token or null if no token exists
|
||||
config: function () {
|
||||
let config = localStorage.getItem('auth.config');
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
// Retrives the current jwt hub token or null if no token exists
|
||||
jwt: function () {
|
||||
let jwt = localStorage.getItem('auth.jwt');
|
||||
if (!jwt) {
|
||||
return null;
|
||||
}
|
||||
return jwt;
|
||||
},
|
||||
|
||||
prompted: function () {
|
||||
return localStorage.getItem('auth.prompted');
|
||||
},
|
||||
|
||||
setPrompted: function (prompted) {
|
||||
localStorage.setItem('auth.prompted', true);
|
||||
accountServerActions.prompted({prompted});
|
||||
},
|
||||
|
||||
request: function (req, callback) {
|
||||
let jwt = this.jwt();
|
||||
|
||||
if (jwt) {
|
||||
_.extend(req, {
|
||||
headers: {
|
||||
Authorization: `JWT ${jwt}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// First attempt with existing JWT
|
||||
request(req, (error, response, body) => {
|
||||
let data = JSON.parse(body);
|
||||
|
||||
// If the JWT has expired, then log in again to get a new JWT
|
||||
if (data && data.detail === 'Signature has expired.') {
|
||||
let config = this.config();
|
||||
if (!this.config()) {
|
||||
this.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
let [username, password] = this.creds(config);
|
||||
this.auth(username, password, (error, response, body) => {
|
||||
let data = JSON.parse(body);
|
||||
if (response.statusCode === 200 && data && data.token) {
|
||||
localStorage.setItem('auth.jwt', data.token);
|
||||
} else {
|
||||
this.logout();
|
||||
}
|
||||
|
||||
this.request(req, callback);
|
||||
});
|
||||
} else {
|
||||
callback(error, response, body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loggedin: function () {
|
||||
return this.jwt() && this.config();
|
||||
},
|
||||
|
||||
logout: function () {
|
||||
accountServerActions.loggedout();
|
||||
localStorage.removeItem('auth.jwt');
|
||||
localStorage.removeItem('auth.username');
|
||||
localStorage.removeItem('auth.verified');
|
||||
localStorage.removeItem('auth.config');
|
||||
},
|
||||
|
||||
login: function (username, password) {
|
||||
this.auth(username, password, (error, response, body) => {
|
||||
if (error) {
|
||||
accountServerActions.errors({errors: {detail: error.message}});
|
||||
return;
|
||||
}
|
||||
|
||||
let data = JSON.parse(body);
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
if (data.token) {
|
||||
localStorage.setItem('auth.jwt', data.token);
|
||||
localStorage.setItem('auth.username', username);
|
||||
localStorage.setItem('auth.verified', true);
|
||||
localStorage.setItem('auth.config', new Buffer(username + ':' + password).toString('base64'));
|
||||
accountServerActions.loggedin({username, verified: true});
|
||||
accountServerActions.prompted({prompted: true});
|
||||
require('./RegHubUtil').repos();
|
||||
} else {
|
||||
accountServerActions.errors({errors: {detail: 'Did not receive login token.'}});
|
||||
}
|
||||
} else if (response.statusCode === 401) {
|
||||
if (data && data.detail && data.detail.indexOf('Account not active yet') !== -1) {
|
||||
accountServerActions.loggedin({username, verified: false});
|
||||
accountServerActions.prompted({prompted: true});
|
||||
localStorage.setItem('auth.username', username);
|
||||
localStorage.setItem('auth.verified', false);
|
||||
localStorage.setItem('auth.config', new Buffer(username + ':' + password).toString('base64'));
|
||||
} else {
|
||||
accountServerActions.errors({errors: data});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
auth: function (username, password, callback) {
|
||||
request.post('https://hub.docker.com/v2/users/login/', {form: {username, password}}, (error, response, body) => {
|
||||
callback(error, response, body);
|
||||
});
|
||||
},
|
||||
|
||||
verify: function () {
|
||||
let config = this.config();
|
||||
if (!config) {
|
||||
this.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
let [username, password] = this.creds(config);
|
||||
this.login(username, password);
|
||||
},
|
||||
|
||||
creds: function (config) {
|
||||
return new Buffer(config, 'base64').toString().split(/:(.+)?/).slice(0, 2);
|
||||
},
|
||||
|
||||
// Signs up and places a token under ~/.dockercfg and saves a jwt to localstore
|
||||
signup: function (username, password, email, subscribe) {
|
||||
request.post('https://hub.docker.com/v2/users/signup/', {
|
||||
form: {
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
subscribe
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
// TODO: save username to localstorage
|
||||
if (response.statusCode === 204) {
|
||||
accountServerActions.signedup({username, verified: false});
|
||||
accountServerActions.prompted({prompted: true});
|
||||
localStorage.setItem('auth.username', username);
|
||||
localStorage.setItem('auth.verified', false);
|
||||
localStorage.setItem('auth.config', new Buffer(username + ':' + password).toString('base64'));
|
||||
} else {
|
||||
let data = JSON.parse(body);
|
||||
let errors = {};
|
||||
for (let key in data) {
|
||||
errors[key] = data[key][0];
|
||||
}
|
||||
accountServerActions.errors({errors});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,142 @@
|
|||
var _ = require('underscore');
|
||||
var request = require('request');
|
||||
var async = require('async');
|
||||
var util = require('../utils/Util');
|
||||
var hubUtil = require('../utils/HubUtil');
|
||||
var repositoryServerActions = require('../actions/RepositoryServerActions');
|
||||
var tagServerActions = require('../actions/TagServerActions');
|
||||
|
||||
let searchReq = null;
|
||||
|
||||
module.exports = {
|
||||
// Normalizes results from search to v2 repository results
|
||||
normalize: function (repo) {
|
||||
let obj = _.clone(repo);
|
||||
if (obj.is_official) {
|
||||
obj.namespace = 'library';
|
||||
} else {
|
||||
let [namespace, name] = repo.name.split('/');
|
||||
obj.namespace = namespace;
|
||||
obj.name = name;
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
search: function (query, page) {
|
||||
if (searchReq) {
|
||||
searchReq.abort();
|
||||
searchReq = null;
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
repositoryServerActions.resultsUpdated({repos: []});
|
||||
}
|
||||
|
||||
searchReq = request.get({
|
||||
url: 'https://registry.hub.docker.com/v1/search?',
|
||||
qs: {q: query, page}
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
repositoryServerActions.searchError({error});
|
||||
}
|
||||
|
||||
let data = JSON.parse(body);
|
||||
let repos = _.map(data.results, result => {
|
||||
return this.normalize(result);
|
||||
});
|
||||
if (response.statusCode === 200) {
|
||||
repositoryServerActions.resultsUpdated({repos});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
recommended: function () {
|
||||
request.get('https://kitematic.com/recommended.json', (error, response, body) => {
|
||||
if (error) {
|
||||
repositoryServerActions.recommendedError({error});
|
||||
}
|
||||
|
||||
let data = JSON.parse(body);
|
||||
let repos = data.repos;
|
||||
async.map(repos, (repo, cb) => {
|
||||
let name = repo.repo;
|
||||
if (util.isOfficialRepo(name)) {
|
||||
name = 'library/' + name;
|
||||
}
|
||||
|
||||
request.get({
|
||||
url: `https://registry.hub.docker.com/v2/repositories/${name}`,
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
repositoryServerActions.error({error});
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
let data = JSON.parse(body);
|
||||
data.is_recommended = true;
|
||||
_.extend(data, repo);
|
||||
cb(null, data);
|
||||
}
|
||||
});
|
||||
}, (error, repos) => {
|
||||
repositoryServerActions.recommendedUpdated({repos});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
tags: function (repo) {
|
||||
hubUtil.request({
|
||||
url: `https://registry.hub.docker.com/v2/repositories/${repo}/tags`
|
||||
}, (error, response, body) => {
|
||||
if (response.statusCode === 200) {
|
||||
let data = JSON.parse(body);
|
||||
tagServerActions.tagsUpdated({repo, tags: data.tags});
|
||||
} else if (response.statusCude === 401) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Returns the base64 encoded index token or null if no token exists
|
||||
repos: function () {
|
||||
repositoryServerActions.reposLoading({repos: []});
|
||||
|
||||
hubUtil.request({
|
||||
url: 'https://registry.hub.docker.com/v2/namespaces/',
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
repositoryServerActions.reposError({error});
|
||||
return;
|
||||
}
|
||||
|
||||
let data = JSON.parse(body);
|
||||
let namespaces = data.namespaces;
|
||||
async.map(namespaces, (namespace, cb) => {
|
||||
hubUtil.request({
|
||||
url: `https://registry.hub.docker.com/v2/repositories/${namespace}`
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
repositoryServerActions.reposError({error});
|
||||
return;
|
||||
}
|
||||
|
||||
let data = JSON.parse(body);
|
||||
cb(null, data.results);
|
||||
});
|
||||
}, (error, lists) => {
|
||||
let repos = [];
|
||||
for (let list of lists) {
|
||||
repos = repos.concat(list);
|
||||
}
|
||||
|
||||
_.each(repos, repo => {
|
||||
repo.is_user_repo = true;
|
||||
});
|
||||
|
||||
repositoryServerActions.reposUpdated({repos});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,84 +0,0 @@
|
|||
var async = require('async');
|
||||
var $ = require('jquery');
|
||||
|
||||
var Registry = {
|
||||
token: function(repository, callback) {
|
||||
$.ajax({
|
||||
url: 'https://registry.hub.docker.com/v1/repositories/' + repository + '/images',
|
||||
headers: {
|
||||
'X-Docker-Token': true,
|
||||
},
|
||||
success: function (res, status, xhr) {
|
||||
callback(null, xhr.getResponseHeader('X-Docker-Token'));
|
||||
},
|
||||
error: function (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
ancestry: function (imageId, token, callback) {
|
||||
$.ajax({
|
||||
url: 'https://registry-1.docker.io/v1/images/' + imageId + '/ancestry',
|
||||
headers: {
|
||||
Authorization: 'Token ' + token
|
||||
},
|
||||
success: function (layers) {
|
||||
callback(null, layers);
|
||||
},
|
||||
error: function (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
imageId: function (repository, tag, token, callback) {
|
||||
$.ajax({
|
||||
url: 'https://registry-1.docker.io/v1/repositories/' + repository + '/tags/' + tag,
|
||||
headers: {
|
||||
Authorization: 'Token ' + token
|
||||
},
|
||||
success: function (res) {
|
||||
callback(null, res);
|
||||
},
|
||||
error: function (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Returns an array [{Id: <12 character image ID, size: size of layer in bytes}]
|
||||
layers: function (repository, tag, callback) {
|
||||
var self = this;
|
||||
this.token(repository, function (err, token) {
|
||||
self.imageId(repository, tag, token, function (err, imageId) {
|
||||
self.ancestry(imageId, token, function (err, layers) {
|
||||
async.map(layers, function (layer, callback) {
|
||||
$.ajax({
|
||||
url: 'https://registry-1.docker.io/v1/images/' + layer + '/json',
|
||||
headers: {
|
||||
Authorization: 'Token ' + token
|
||||
},
|
||||
success: function (res, status, xhr) {
|
||||
var size = xhr.getResponseHeader('X-Docker-Size');
|
||||
callback(null, {
|
||||
Id: layer.slice(0, 12),
|
||||
size: parseInt(size, 10)
|
||||
});
|
||||
},
|
||||
error: function (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
callback('Could not sum' + err);
|
||||
return;
|
||||
}
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Registry;
|
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
.subtext {
|
||||
text-align: right;
|
||||
color: @gray-lightest;
|
||||
color: @gray-lighter;
|
||||
margin-top: 2px;
|
||||
transition: all 0.25s;
|
||||
&:hover {
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
.container-progress-wrapper {
|
||||
margin-left: 37%;
|
||||
}
|
||||
|
||||
.container-progress {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
|
|
@ -1,25 +1,56 @@
|
|||
.header {
|
||||
position: absolute;
|
||||
min-width: 100%;
|
||||
min-height: 40px;
|
||||
min-height: 50px;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
&.no-drag {
|
||||
|
||||
&.bordered {
|
||||
border-bottom: 1px solid #E7E7E7;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
|
||||
.no-drag {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
.logo {
|
||||
position: relative;
|
||||
float: right;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
.updates {
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-right: 20px;
|
||||
|
||||
img {
|
||||
margin: 0 14px;
|
||||
height: 16px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
flex: 0 auto;
|
||||
display: flex;
|
||||
color: #88919C;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-right: 13px;
|
||||
|
||||
&:active {
|
||||
img, span {
|
||||
-webkit-filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 10px;
|
||||
left: 15px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
margin-left: 14px;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:hover {
|
||||
.button-minimize.enabled {
|
||||
.at2x('minimize.png', 10px, 10px);
|
||||
|
@ -35,6 +66,7 @@
|
|||
}
|
||||
}
|
||||
.button {
|
||||
flex: 0 auto;
|
||||
.traffic-light();
|
||||
&.button-close {
|
||||
background-color: @traffic-light-red;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* Sidebar */
|
||||
|
||||
.sidebar {
|
||||
padding-top: 28px;
|
||||
padding-top: 10px;
|
||||
background-color: white;
|
||||
margin: 0;
|
||||
border-right: 1px solid @color-divider;
|
||||
|
@ -9,6 +9,7 @@
|
|||
flex-direction: column;
|
||||
min-width: 260px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
.sidebar-header {
|
||||
flex: 0 auto;
|
||||
min-width: 240px;
|
||||
|
@ -59,12 +60,15 @@
|
|||
}
|
||||
.sidebar-containers {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
max-width: 260px;
|
||||
ul {
|
||||
flex: 1 auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
|
@ -240,13 +244,13 @@
|
|||
/* Sidebar Buttons */
|
||||
|
||||
.sidebar-buttons {
|
||||
border-top: 1px solid #F0F4F8;
|
||||
min-height: 48px;
|
||||
flex: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: white;
|
||||
width: 259px;
|
||||
opacity: 0.9;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
padding: 15px;
|
||||
padding-top: 5px;
|
||||
z-index: 10000;
|
||||
.btn-label {
|
||||
color: @gray-lighter;
|
||||
|
@ -254,8 +258,35 @@
|
|||
height: 18px;
|
||||
}
|
||||
.btn-sidebar {
|
||||
display: inline-block;
|
||||
margin-right: 7px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: @brand-primary;
|
||||
|
||||
flex: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 48px;
|
||||
|
||||
&:active {
|
||||
img, .text {
|
||||
-webkit-filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-terminal {
|
||||
flex: 1 auto;
|
||||
border-right: 1px solid #F0F4F8;
|
||||
img {
|
||||
margin: 0 10px 0 0;
|
||||
height: 21px;
|
||||
width: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-feedback {
|
||||
border-right: 1px solid #F0F4F8;
|
||||
}
|
||||
.btn {
|
||||
position: relative;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
@import "container-home.less";
|
||||
@import "container-logs.less";
|
||||
@import "container-settings.less";
|
||||
@import "spinner.less";
|
||||
@import "animation.less";
|
||||
@import "container-progress.less";
|
||||
|
||||
|
@ -24,6 +25,7 @@ html, body {
|
|||
overflow: hidden;
|
||||
background: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
font-family: @font-regular;
|
||||
cursor: default;
|
||||
img {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
background: white;
|
||||
margin-right: 9px;
|
||||
margin-right: 8px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border: 1px solid @traffic-light-gray-border;
|
||||
|
|
|
@ -31,58 +31,103 @@
|
|||
display: flex;
|
||||
flex: 1 auto;
|
||||
flex-direction: column;
|
||||
padding: 35px 20px 32px 25px;
|
||||
padding: 25px 0 0;
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 auto;
|
||||
color: @gray-normal;
|
||||
|
||||
.results-filters {
|
||||
flex: 0 auto;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
margin: 0 20px;
|
||||
|
||||
.results-filter {
|
||||
text-align: center;
|
||||
margin: 0 10px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.results-filter-title {
|
||||
color: @gray-lighter;
|
||||
font-weight: 500;
|
||||
padding-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 380px;
|
||||
}
|
||||
|
||||
.verify {
|
||||
margin: 15px 0;
|
||||
position: relative;
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -50px;
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 0 auto;
|
||||
margin-top: -20%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
margin-top: -10%;
|
||||
h2 {
|
||||
color: @gray-normal;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
color: @gray-lightest;
|
||||
color: @gray-lighter;
|
||||
font-size: 24px;
|
||||
margin: 0 auto;
|
||||
margin-top: -20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.new-container-header {
|
||||
margin-bottom: 18px;
|
||||
margin: 0 20px 8px;
|
||||
display: flex;
|
||||
flex: 0 auto;
|
||||
flex-shrink: 0;
|
||||
.text {
|
||||
flex: 1 auto;
|
||||
width: 50%;
|
||||
font-size: 14px;
|
||||
color: @gray-lighter;
|
||||
color: @gray-normal;
|
||||
}
|
||||
.search {
|
||||
flex: 1 auto;
|
||||
margin-right: 30px;
|
||||
.search-bar {
|
||||
top: -7px;
|
||||
position: relative;
|
||||
.loading {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 7px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
-webkit-animation-name: spin;
|
||||
-webkit-animation-duration: 1.8s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-timing-function: linear;
|
||||
left: 10px;
|
||||
}
|
||||
.search-icon {
|
||||
font-size: 18px;
|
||||
|
@ -105,7 +150,7 @@
|
|||
border-color: @brand-primary;
|
||||
}
|
||||
&::-webkit-input-placeholder {
|
||||
color: @gray-lightest;
|
||||
color: @gray-lighter;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
@ -114,183 +159,187 @@
|
|||
}
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
margin-top: 10px;
|
||||
.result-grids {
|
||||
|
||||
overflow: auto;
|
||||
.image-item {
|
||||
margin: 0 0 0 20px;
|
||||
.result-grid {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 320px;
|
||||
height: 166px;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
.tag-overlay {
|
||||
z-index: 999;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
border-radius: 4px;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
margin-top: 10px;
|
||||
.image-item {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 320px;
|
||||
height: 166px;
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
padding: 10px;
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
flex-flow: row wrap;
|
||||
height: 140px;
|
||||
overflow: auto;
|
||||
.tag {
|
||||
display: inline-block;
|
||||
flex: 0 auto;
|
||||
margin-right: 2px;
|
||||
padding: 3px 5px;
|
||||
&.active {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
border-radius: 20px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tags-loading {
|
||||
position: relative;
|
||||
left: 42%;
|
||||
top: 20%;
|
||||
text-align: center;
|
||||
margin: 14px auto;
|
||||
-webkit-animation-name: spin;
|
||||
-webkit-animation-duration: 1.8s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-timing-function: linear;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
flex: 1 auto;
|
||||
min-width: 90px;
|
||||
background-color: @brand-action;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
box-shadow: inset 0px 0px 0px 1px rgba(0,0,0,0.2);
|
||||
img {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
.card {
|
||||
padding: 10px 20px 10px 20px;
|
||||
position: relative;
|
||||
border: 1px solid @gray-lighter;
|
||||
border-left: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
.badges {
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
.tag-overlay {
|
||||
z-index: 999;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
border-radius: 4px;
|
||||
width: 320px;
|
||||
height: 166px;
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 8px;
|
||||
}
|
||||
.name {
|
||||
font-size: 18px;
|
||||
color: @gray-darkest;
|
||||
margin-bottom: 0px;
|
||||
position: relative;
|
||||
width: 190px;
|
||||
.namespace {
|
||||
font-size: 11px;
|
||||
color: @gray-lighter;
|
||||
margin-bottom: -3px;
|
||||
&.official {
|
||||
color: @brand-action;
|
||||
}
|
||||
}
|
||||
.repo {
|
||||
display: inline-block;
|
||||
max-width: 190px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: @gray-normal;
|
||||
height: 50px;
|
||||
width: 190px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
display: -webkit-box;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.actions {
|
||||
width: 190px;
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
.stars {
|
||||
height: 15px;
|
||||
font-size: 10px;
|
||||
color: @gray-darker;
|
||||
border-right: 1px solid @gray-lightest;
|
||||
padding-right: 10px;
|
||||
.icon {
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
margin-right: 3px;
|
||||
top: -1px;
|
||||
color: @gray-darkest;
|
||||
}
|
||||
.text {
|
||||
position: relative;
|
||||
top: -6px;
|
||||
}
|
||||
}
|
||||
.tags {
|
||||
height: 15px;
|
||||
font-size: 10px;
|
||||
color: @gray-darker;
|
||||
padding-left: 10px;
|
||||
.icon {
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
padding: 10px;
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
flex-flow: row wrap;
|
||||
height: 140px;
|
||||
overflow: auto;
|
||||
.tag {
|
||||
display: inline-block;
|
||||
flex: 0 auto;
|
||||
margin-right: 2px;
|
||||
top: 2px;
|
||||
color: @gray-darkest;
|
||||
}
|
||||
.text {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
padding: 3px 5px;
|
||||
text-decoration: underline;
|
||||
&.active {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
border-radius: 20px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: @brand-action;
|
||||
color: white;
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action {
|
||||
flex: 1 auto;
|
||||
.btn {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
float: right;
|
||||
top: -7px;
|
||||
.tags-loading {
|
||||
position: relative;
|
||||
left: 42%;
|
||||
top: 20%;
|
||||
text-align: center;
|
||||
margin: 14px auto;
|
||||
-webkit-animation-name: spin;
|
||||
-webkit-animation-duration: 1.8s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-timing-function: linear;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
flex: 1 auto;
|
||||
min-width: 90px;
|
||||
background-color: @brand-action;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
box-shadow: inset 0px 0px 0px 1px rgba(0,0,0,0.2);
|
||||
img {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
.card {
|
||||
padding: 10px 20px 10px 20px;
|
||||
position: relative;
|
||||
border: 1px solid @gray-lighter;
|
||||
border-left: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
.badges {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 8px;
|
||||
}
|
||||
.name {
|
||||
font-size: 18px;
|
||||
color: @gray-darkest;
|
||||
margin-bottom: 0px;
|
||||
position: relative;
|
||||
width: 190px;
|
||||
.namespace {
|
||||
font-size: 11px;
|
||||
color: @gray-lighter;
|
||||
margin-bottom: -3px;
|
||||
&.official {
|
||||
color: @brand-action;
|
||||
}
|
||||
}
|
||||
.repo {
|
||||
display: inline-block;
|
||||
max-width: 190px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
flex-direaction: row;
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: @gray-normal;
|
||||
height: 50px;
|
||||
width: 190px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
display: -webkit-box;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.actions {
|
||||
width: 190px;
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
.stars {
|
||||
height: 15px;
|
||||
font-size: 10px;
|
||||
color: @gray-darker;
|
||||
border-right: 1px solid @gray-lightest;
|
||||
padding-right: 10px;
|
||||
.icon {
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
margin-right: 3px;
|
||||
top: -1px;
|
||||
color: @gray-darkest;
|
||||
}
|
||||
.text {
|
||||
position: relative;
|
||||
top: -6px;
|
||||
}
|
||||
}
|
||||
.tags {
|
||||
height: 15px;
|
||||
font-size: 10px;
|
||||
color: @gray-darker;
|
||||
padding-left: 10px;
|
||||
.icon {
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
margin-right: 2px;
|
||||
top: 2px;
|
||||
color: @gray-darkest;
|
||||
}
|
||||
.text {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
padding: 3px 5px;
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
background-color: @brand-action;
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action {
|
||||
flex: 1 auto;
|
||||
.btn {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
float: right;
|
||||
top: -7px;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
flex-direaction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,38 +65,15 @@
|
|||
text-align: right;
|
||||
margin-right: 3px;
|
||||
margin-top: 3px;
|
||||
.tab {
|
||||
margin-left: 16px;
|
||||
padding: 6px 10px;
|
||||
font-weight: 400;
|
||||
display: inline-block;
|
||||
&:hover {
|
||||
border-radius: 40px;
|
||||
background-color: darken(@color-background, 2%);
|
||||
}
|
||||
&.active {
|
||||
border-radius: 40px;
|
||||
color: white;
|
||||
.brand-gradient();
|
||||
}
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
border-radius: 40px;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.details-header {
|
||||
flex: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 31px 24px 18px 24px;
|
||||
padding: 18px 24px 24px 24px;
|
||||
position: relative;
|
||||
background-color: white;
|
||||
height: 75px;
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
|
@ -124,9 +101,33 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
margin-left: 16px;
|
||||
padding: 6px 10px;
|
||||
font-weight: 400;
|
||||
display: inline-block;
|
||||
&.active {
|
||||
border-radius: 40px;
|
||||
color: white;
|
||||
.brand-gradient();
|
||||
}
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
border-radius: 40px;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details-progress {
|
||||
margin: 20% auto 0;
|
||||
width: 400px;
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin-top: -70px;
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
|
|
@ -2,80 +2,220 @@
|
|||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
//-webkit-app-region: drag;
|
||||
|
||||
.image {
|
||||
.setup-content {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
flex: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 40px;
|
||||
padding-left: 80px;
|
||||
flex-direction: row;
|
||||
|
||||
img {
|
||||
width: 399px;
|
||||
height: 340px;
|
||||
flex: 1 auto;
|
||||
|
||||
.image {
|
||||
display: flex;
|
||||
flex: 1 auto;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 40px;
|
||||
|
||||
img {
|
||||
width: 399px;
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
.contents {
|
||||
position: relative;
|
||||
.detail {
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contents {
|
||||
position: relative;
|
||||
.detail {
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
bottom: 0;
|
||||
.form-section {
|
||||
display: flex;
|
||||
flex: 1 auto;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-right: 60px;
|
||||
padding-left: 80px;
|
||||
flex-direction: column;
|
||||
|
||||
img {
|
||||
width: 323px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
form {
|
||||
margin-top: 40px;
|
||||
text-align: right;
|
||||
input[type="text"], input[type="password"] {
|
||||
display: block;
|
||||
border: 0;
|
||||
border-bottom: 1px solid @gray-lightest;
|
||||
color: @gray-normal;
|
||||
font-weight: 300;
|
||||
padding: 10px 5px;
|
||||
transition: all 0.25s;
|
||||
font-size: 18px;
|
||||
width: 340px;
|
||||
&.error {
|
||||
border-bottom: 1px solid @brand-negative;
|
||||
&:focus {
|
||||
border-bottom: 1px solid @brand-negative;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-bottom: 1px solid @brand-action;
|
||||
}
|
||||
&::-webkit-input-placeholder {
|
||||
color: @gray-lighter;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
align-items: center;
|
||||
padding-left: 40px;
|
||||
div.checkbox {
|
||||
text-align: left;
|
||||
color: @gray-normal;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 320px;
|
||||
div.submit {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
color: @gray-darkest;
|
||||
font-size: 24px;
|
||||
}
|
||||
h4 {
|
||||
color: @gray-lightest;
|
||||
font-size: 13px;
|
||||
margin-top: -30px;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: @gray-normal;
|
||||
&.error {
|
||||
.spinner {
|
||||
margin-right: 10px;
|
||||
flex: 0 auto;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
flex: 0 auto;
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
padding: 10px 20px;
|
||||
color: white;
|
||||
background-color: @brand-action;
|
||||
border: 0;
|
||||
&:hover {
|
||||
background-color: darken(@brand-action, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px solid #D7DFEA;
|
||||
}
|
||||
|
||||
.extra {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: @gray-normal;
|
||||
margin-top: 16px;
|
||||
.btn {
|
||||
margin-left: 6px;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: @brand-primary;
|
||||
}
|
||||
}
|
||||
.link {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
top: -15px;
|
||||
left: 5px;
|
||||
}
|
||||
.error-message {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
min-height: 17px;
|
||||
color: @brand-negative;
|
||||
}
|
||||
div.error {
|
||||
font-size: 13px;
|
||||
color: @gray-normal;
|
||||
color: @brand-negative;
|
||||
background-color: lighten(@brand-negative, 32%);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
.setup-actions {
|
||||
button {
|
||||
margin-right: 12px;
|
||||
width: 340px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
p {
|
||||
&.error {
|
||||
color: @brand-danger;
|
||||
word-wrap: break-word;
|
||||
.btn-skip {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
margin-top: 20px;
|
||||
|
||||
.btn-close {
|
||||
-webkit-app-region: no-drag;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
padding-left: 40px;
|
||||
|
||||
.content {
|
||||
max-width: 320px;
|
||||
|
||||
h1 {
|
||||
color: @gray-darkest;
|
||||
font-size: 24px;
|
||||
}
|
||||
h4 {
|
||||
color: @gray-lightest;
|
||||
font-size: 13px;
|
||||
margin-top: -30px;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: @gray-normal;
|
||||
&.error {
|
||||
color: @brand-negative;
|
||||
background-color: lighten(@brand-negative, 32%);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
.setup-actions {
|
||||
button {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
p {
|
||||
&.error {
|
||||
color: @brand-danger;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Daniel Cardoso
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
@import "variables.less";
|
||||
|
||||
.la-ball-clip-rotate {
|
||||
display: block;
|
||||
}
|
||||
.la-ball-clip-rotate > div {
|
||||
box-sizing: content-box;
|
||||
color: #fff;
|
||||
background: #fff;
|
||||
border-color: #fff;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
.la-ball-clip-rotate:after {
|
||||
display: table;
|
||||
clear: both;
|
||||
line-height: 0;
|
||||
content: "";
|
||||
}
|
||||
.la-ball-clip-rotate.la-dark > div {
|
||||
color: @brand-primary;
|
||||
background: @brand-primary;
|
||||
border-color: @brand-primary;
|
||||
}
|
||||
/*
|
||||
* Animation
|
||||
*/
|
||||
@-webkit-keyframes ball-clip-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.la-ball-clip-rotate {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.la-ball-clip-rotate > div {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0;
|
||||
background: transparent !important;
|
||||
border-style: solid;
|
||||
border-width: 1.8px;
|
||||
border-bottom-color: transparent !important;
|
||||
border-radius: 100%;
|
||||
-webkit-animation: ball-clip-rotate 0.9s linear infinite;
|
||||
}
|
||||
.la-ball-clip-rotate.la-sm {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.la-ball-clip-rotate.la-sm > div {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
border-width: 1px;
|
||||
}
|
||||
.la-ball-clip-rotate.la-lg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.la-ball-clip-rotate.la-lg > div {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
margin: 0;
|
||||
border-width: 2px;
|
||||
}
|
||||
.la-ball-clip-rotate.la-2x {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.la-ball-clip-rotate.la-2x > div {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 0;
|
||||
border-width: 2px;
|
||||
}
|