mirror of https://github.com/docker/docs.git
WIP login without refresh token
This commit is contained in:
parent
d80983fb4e
commit
2065063b14
Binary file not shown.
After Width: | Height: | Size: 206 KiB |
Binary file not shown.
After Width: | Height: | Size: 568 KiB |
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
After Width: | Height: | Size: 243 KiB |
Binary file not shown.
After Width: | Height: | Size: 456 B |
Binary file not shown.
After Width: | Height: | Size: 824 B |
|
@ -21,6 +21,11 @@ class AccountActions {
|
|||
this.dispatch({});
|
||||
hub.prompted(true);
|
||||
}
|
||||
|
||||
verify () {
|
||||
this.dispatch({});
|
||||
hub.verify();
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(AccountActions);
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import alt from '../alt';
|
||||
import router from '../router';
|
||||
|
||||
|
||||
class AccountServerActions {
|
||||
constructor () {
|
||||
this.generateActions(
|
||||
'loggedout',
|
||||
'prompted',
|
||||
'errors'
|
||||
'errors',
|
||||
'verified'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,9 @@ class AccountServerActions {
|
|||
}
|
||||
|
||||
signedup ({username}) {
|
||||
router.get().transitionTo('search');
|
||||
if (router.get()) {
|
||||
router.get().goBack();
|
||||
}
|
||||
this.dispatch({username});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import alt from '../alt';
|
||||
import dockerUtil from '../utils/DockerUtil';
|
||||
import hubUtil from '../utils/HubUtil';
|
||||
|
||||
class ContainerActions {
|
||||
start (name) {
|
||||
|
@ -32,7 +33,7 @@ class ContainerActions {
|
|||
}
|
||||
|
||||
run (name, repo, tag) {
|
||||
dockerUtil.run(name, repo, tag);
|
||||
dockerUtil.run(hubUtil.config(), name, repo, tag);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,25 @@
|
|||
import alt from '../alt';
|
||||
import regHubUtil from '../utils/RegHubUtil';
|
||||
import hubUtil from '../utils/HubUtil';
|
||||
|
||||
class RepositoryActions {
|
||||
fetch () {
|
||||
recommended () {
|
||||
this.dispatch({});
|
||||
regHubUtil.recommended();
|
||||
}
|
||||
|
||||
search (query) {
|
||||
this.dispatch({});
|
||||
regHubUtil.search(query);
|
||||
}
|
||||
|
||||
repos () {
|
||||
this.dispatch({});
|
||||
regHubUtil.repos(hubUtil.jwt());
|
||||
}
|
||||
|
||||
tags () {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,10 @@ import alt from '../alt';
|
|||
class RepositoryServerActions {
|
||||
constructor () {
|
||||
this.generateActions(
|
||||
'searched',
|
||||
'fetched',
|
||||
'error'
|
||||
'reposLoading',
|
||||
'resultsUpdated',
|
||||
'recommendedUpdated',
|
||||
'reposUpdated'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import alt from '../alt';
|
||||
import regHubUtil from '../utils/RegHubUtil';
|
||||
import hubUtil from '../utils/HubUtil';
|
||||
|
||||
class TagActions {
|
||||
tags (repo) {
|
||||
this.dispatch({repo});
|
||||
regHubUtil.tags(hubUtil.jwt(), 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);
|
|
@ -18,15 +18,17 @@ 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();
|
||||
repositoryActions.recommended();
|
||||
repositoryActions.repos();
|
||||
|
||||
webUtil.addWindowSizeSaving();
|
||||
webUtil.addLiveReload();
|
||||
webUtil.addBugReporting();
|
||||
webUtil.disableGlobalBackspace();
|
||||
|
||||
hubUtil.init();
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template()));
|
||||
|
||||
metrics.track('Started App');
|
||||
|
|
|
@ -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();
|
||||
|
@ -27,9 +26,9 @@ app.on('open-url', function (event, url) {
|
|||
|
||||
app.on('ready', function () {
|
||||
var mainWindow = new BrowserWindow({
|
||||
width: size.width || 800,
|
||||
width: size.width || 1000,
|
||||
height: size.height || 600,
|
||||
'min-width': 800,
|
||||
'min-width': 1000,
|
||||
'min-height': 600,
|
||||
'standard-window': false,
|
||||
resizable: true,
|
||||
|
|
|
@ -40,13 +40,14 @@ module.exports = React.createClass({
|
|||
|
||||
render: function () {
|
||||
let close = this.state.prompted ?
|
||||
<a className="btn btn-action btn-close" onClick={this.handleClose}>Close</a> :
|
||||
<a className="btn btn-action btn-skip" onClick={this.handleSkip}>Skip For Now</a>;
|
||||
<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}/>
|
||||
|
@ -55,7 +56,6 @@ module.exports = React.createClass({
|
|||
<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>
|
||||
{close}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -36,7 +36,7 @@ var ContainerListNewItem = React.createClass({
|
|||
);
|
||||
}
|
||||
return (
|
||||
<Router.Link to="new">
|
||||
<Router.Link to="search">
|
||||
<li className="new-container-item" onMouseEnter={this.handleItemMouseEnter} onMouseLeave={this.handleItemMouseLeave}>
|
||||
<div className="state state-new"></div>
|
||||
<div className="info">
|
||||
|
|
|
@ -5,7 +5,6 @@ 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 metrics = require('../utils/MetricsUtil');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var shell = require('shell');
|
||||
|
|
|
@ -72,7 +72,12 @@ var Header = React.createClass({
|
|||
},
|
||||
handleUserClick: function (e) {
|
||||
let menu = new Menu();
|
||||
menu.append(new MenuItem({ label: 'Sign Out', click: this.handleLogoutClick.bind(this)}));
|
||||
|
||||
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 () {
|
||||
|
@ -81,6 +86,9 @@ var Header = React.createClass({
|
|||
handleLogoutClick: function () {
|
||||
accountActions.logout();
|
||||
},
|
||||
handleVerifyClick: function () {
|
||||
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;
|
||||
|
@ -108,7 +116,7 @@ var Header = React.createClass({
|
|||
} else if (this.state.username) {
|
||||
username = (
|
||||
<span className="no-drag" onClick={this.handleUserClick}>
|
||||
<RetinaImage src="user.png"/> {this.state.username} <RetinaImage src="userdropdown.png"/>
|
||||
<RetinaImage src="user.png"/> {this.state.username} {this.state.verified ? null : '(Unverified)'} <RetinaImage src="userdropdown.png"/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -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
|
||||
|
@ -33,64 +52,42 @@ var ImageCard = React.createClass({
|
|||
containerActions.run(name, repository, 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/';
|
||||
var repoUri = 'https://registry.hub.docker.com/';
|
||||
if (this.props.image.is_official) {
|
||||
$repoUri = $repoUri + "_/";
|
||||
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 +108,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 +125,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 +146,7 @@ var ImageCard = React.createClass({
|
|||
</div>
|
||||
<div className="card">
|
||||
<div className="badges">
|
||||
{officialBadge}
|
||||
{badge}
|
||||
</div>
|
||||
<div className="name">
|
||||
{name}
|
||||
|
@ -152,17 +155,13 @@ 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>
|
||||
|
|
|
@ -1,35 +1,57 @@
|
|||
var _ = require('underscore');
|
||||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
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,
|
||||
category: 'recommended',
|
||||
recommendedrepos: [],
|
||||
publicrepos: [],
|
||||
userrepos: [],
|
||||
results: [],
|
||||
tab: 'all'
|
||||
loading: repositoryStore.loading(),
|
||||
repos: repositoryStore.all(),
|
||||
username: accountStore.getState().username,
|
||||
verified: accountStore.getState().verified,
|
||||
accountLoading: accountStore.getState().loading,
|
||||
error: repositoryStore.getState().error
|
||||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
// fetch recommended
|
||||
// fetch public repos
|
||||
// if logged in: my repos
|
||||
this.refs.searchInput.getDOMNode().focus();
|
||||
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) {
|
||||
|
@ -37,15 +59,6 @@ module.exports = React.createClass({
|
|||
_searchPromise = null;
|
||||
}
|
||||
|
||||
if (!query.length) {
|
||||
this.setState({
|
||||
query: query,
|
||||
results: _recommended,
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
query: query,
|
||||
loading: true
|
||||
|
@ -54,7 +67,7 @@ module.exports = React.createClass({
|
|||
_searchPromise = Promise.delay(200).cancellable().then(() => {
|
||||
metrics.track('Searched for Images');
|
||||
_searchPromise = null;
|
||||
// TODO: call search action
|
||||
repositoryActions.search(query);
|
||||
}).catch(Promise.CancellationError, () => {});
|
||||
},
|
||||
handleChange: function (e) {
|
||||
|
@ -64,38 +77,107 @@ 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});
|
||||
},
|
||||
handleCheckVerification: function () {
|
||||
accountActions.verify();
|
||||
},
|
||||
render: function () {
|
||||
var data = this.state.recommendedrepos;
|
||||
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.indexOf(this.state.query) !== -1 || repo.namespace.indexOf(this.state.query) !== -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">
|
||||
{userRepoResults}
|
||||
{recommendedResults}
|
||||
{otherResults}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (this.state.results.length === 0 && this.state.query === '') {
|
||||
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 {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<h1>Cannot find a matching image.</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<h2>Cannot find a matching image.</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let loadingClasses = classNames({
|
||||
|
@ -114,16 +196,12 @@ module.exports = React.createClass({
|
|||
'search-icon': true
|
||||
});
|
||||
|
||||
let allTabClasses = classNames({
|
||||
'results-filter':
|
||||
});
|
||||
|
||||
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">
|
||||
|
@ -136,9 +214,9 @@ module.exports = React.createClass({
|
|||
<div className="results">
|
||||
<div className="results-filters">
|
||||
<span className="results-filter results-filter-title">FILTER BY</span>
|
||||
<span className="results-filter results-all tab">All</span>
|
||||
<span className="results-filter results-recommended tab">Recommended</span>
|
||||
<span className="results-filter results-userrepos tab">My Repositories</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>
|
||||
|
|
|
@ -45,7 +45,7 @@ var routes = (
|
|||
</Route>
|
||||
</Route>
|
||||
<Route name="new" path="containers/new">
|
||||
<DefaultRoute name="search" handler={NewContainerSearch}/>
|
||||
<Route name="search" path="containers/new/search" handler={NewContainerSearch}></Route>
|
||||
<Route name="pull" path="containers/new/pull" handler={NewContainerPull}></Route>
|
||||
</Route>
|
||||
<Route name="preferences" path="/preferences" handler={Preferences}/>
|
||||
|
|
|
@ -44,16 +44,20 @@ class AccountStore {
|
|||
});
|
||||
}
|
||||
|
||||
loggedin ({username}) {
|
||||
this.setState({username, errors: {}, loading: false});
|
||||
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});
|
||||
this.setState({verified, loading: false});
|
||||
}
|
||||
|
||||
prompted ({prompted}) {
|
||||
|
|
|
@ -1,30 +1,75 @@
|
|||
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.repos = [];
|
||||
this.bindActions(accountServerActions);
|
||||
this.results = [];
|
||||
this.recommended = [];
|
||||
this.userrepos = [];
|
||||
this.loading = false;
|
||||
this.repos = [];
|
||||
this.reposLoading = false;
|
||||
this.recommendedLoading = false;
|
||||
this.resultsLoading = false;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
fetch () {
|
||||
this.setState({
|
||||
repos: [],
|
||||
error: null,
|
||||
loading: true
|
||||
});
|
||||
}
|
||||
|
||||
fetched ({repos}) {
|
||||
this.setState({repos, loading: false});
|
||||
}
|
||||
|
||||
error ({error}) {
|
||||
this.setState({error, loading: false});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,6 @@ import path from 'path';
|
|||
import dockerode from 'dockerode';
|
||||
import _ from 'underscore';
|
||||
import util from './Util';
|
||||
import registry from '../utils/RegistryUtil';
|
||||
import metrics from '../utils/MetricsUtil';
|
||||
import containerServerActions from '../actions/ContainerServerActions';
|
||||
import Promise from 'bluebird';
|
||||
|
@ -145,7 +144,7 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
run (name, repository, tag) {
|
||||
run (auth, name, repository, tag) {
|
||||
tag = tag || 'latest';
|
||||
let imageName = repository + ':' + tag;
|
||||
|
||||
|
@ -165,7 +164,7 @@ export default {
|
|||
this.placeholders[name] = placeholderData;
|
||||
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
|
||||
|
||||
this.pullImage(repository, tag, error => {
|
||||
this.pullImage(auth, repository, tag, error => {
|
||||
if (error) {
|
||||
containerServerActions.error({name, error});
|
||||
return;
|
||||
|
@ -327,81 +326,66 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
pullImage (repository, tag, callback, progressCallback, blockedCallback) {
|
||||
registry.layers(repository, tag, (err, layerSizes) => {
|
||||
pullImage (auth, repository, tag, callback, progressCallback, blockedCallback) {
|
||||
// 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 || [];
|
||||
|
||||
// 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, {authconfig: {key: auth}}, (err, stream) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
stream.setEncoding('utf8');
|
||||
|
||||
let existingIds = new Set(images.map(function (image) {
|
||||
return image.Id.slice(0, 12);
|
||||
}));
|
||||
let timeout = null;
|
||||
let layerProgress = {};
|
||||
stream.on('data', str => {
|
||||
var data = JSON.parse(str);
|
||||
|
||||
let layersToDownload = layerSizes.filter(function (layerSize) {
|
||||
return !existingIds.has(layerSize.Id);
|
||||
});
|
||||
|
||||
this.client.pull(repository + ':' + tag, (err, stream) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
if (data.error) {
|
||||
return;
|
||||
}
|
||||
stream.setEncoding('utf8');
|
||||
|
||||
let layerProgress = layersToDownload.reduce(function (r, layer) {
|
||||
if (_.findWhere(images, {Id: layer.Id})) {
|
||||
r[layer.Id] = 1;
|
||||
if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) {
|
||||
blockedCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!layerProgress[data.id]) {
|
||||
layerProgress[data.id] = 0;
|
||||
}
|
||||
|
||||
if (data.status === 'Already exists') {
|
||||
layerProgress[data.id] = 1;
|
||||
} else if (data.status === 'Downloading') {
|
||||
let current = data.progressDetail.current;
|
||||
let total = data.progressDetail.total;
|
||||
|
||||
if (total <= 0) {
|
||||
progressCallback(0);
|
||||
return;
|
||||
} else {
|
||||
r[layer.Id] = 0;
|
||||
}
|
||||
return r;
|
||||
}, {});
|
||||
|
||||
let timeout = null;
|
||||
stream.on('data', str => {
|
||||
var data = JSON.parse(str);
|
||||
|
||||
if (data.error) {
|
||||
return;
|
||||
layerProgress[data.id] = current / total;
|
||||
}
|
||||
|
||||
if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) {
|
||||
blockedCallback();
|
||||
return;
|
||||
let sum = _.values(layerProgress).reduce((pv, sv) => pv + sv, 0);
|
||||
let numlayers = _.keys(layerProgress).length;
|
||||
|
||||
var totalProgress = sum / numlayers * 100;
|
||||
|
||||
if (!timeout) {
|
||||
progressCallback(totalProgress);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
if (data.status === 'Already exists') {
|
||||
layerProgress[data.id] = 1;
|
||||
} else if (data.status === 'Downloading') {
|
||||
let current = data.progressDetail.current;
|
||||
let total = data.progressDetail.total;
|
||||
|
||||
if (total <= 0) {
|
||||
progressCallback(0);
|
||||
return;
|
||||
} else {
|
||||
layerProgress[data.id] = current / total;
|
||||
}
|
||||
|
||||
let sum = _.values(layerProgress).reduce((pv, sv) => pv + sv, 0);
|
||||
let numlayers = _.keys(layerProgress).length;
|
||||
|
||||
var totalProgress = sum / numlayers * 100;
|
||||
|
||||
if (!timeout) {
|
||||
progressCallback(totalProgress);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
stream.on('end', function () {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
});
|
||||
stream.on('end', function () {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
var request = require('request');
|
||||
var accountServerActions = require('../actions/AccountServerActions');
|
||||
var regHubUtil = require('./RegHubUtil');
|
||||
|
||||
module.exports = {
|
||||
|
||||
init: function () {
|
||||
accountServerActions.prompted({prompted: localStorage.getItem('auth.prompted')});
|
||||
if (this.jwt()) { // TODO: check for config too
|
||||
let username = localStorage.getItem('auth.username');
|
||||
let verified = localStorage.getItem('auth.verified');
|
||||
let username = localStorage.getItem('auth.username');
|
||||
let verified = localStorage.getItem('auth.verified') === 'true';
|
||||
if (username) { // TODO: check for config too
|
||||
accountServerActions.loggedin({username, verified});
|
||||
}
|
||||
},
|
||||
|
@ -36,19 +36,23 @@ module.exports = {
|
|||
},
|
||||
|
||||
refresh: function () {
|
||||
// TODO: implement me
|
||||
// TODO: implement me and wrap all jwt calls
|
||||
},
|
||||
|
||||
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');
|
||||
accountServerActions.loggedout();
|
||||
},
|
||||
|
||||
// Places a token under ~/.dockercfg and saves a jwt to localstore
|
||||
login: function (username, password) {
|
||||
login: function (username, password, verifying) {
|
||||
request.post('https://hub.docker.com/v2/users/login/', {form: {username, password}}, (err, response, body) => {
|
||||
let data = JSON.parse(body);
|
||||
if (response.statusCode === 200) {
|
||||
|
@ -57,8 +61,15 @@ module.exports = {
|
|||
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});
|
||||
if (verifying) {
|
||||
accountServerActions.verified({username});
|
||||
} else {
|
||||
accountServerActions.loggedin({username, verified: true});
|
||||
}
|
||||
regHubUtil.repos(data.token);
|
||||
} else if (response.statusCode === 401) {
|
||||
if (data && data.detail && data.detail.indexOf('Account not active yet') !== -1) {
|
||||
accountServerActions.loggedin({username, verified: false});
|
||||
|
@ -69,6 +80,17 @@ module.exports = {
|
|||
});
|
||||
},
|
||||
|
||||
verify: function () {
|
||||
let config = this.config();
|
||||
if (!config) {
|
||||
this.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
let [username, password] = new Buffer(config, 'base64').toString().split(/:(.+)?/).slice(0, 2);
|
||||
this.login(username, password, true);
|
||||
},
|
||||
|
||||
// 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/', {
|
||||
|
@ -82,6 +104,9 @@ module.exports = {
|
|||
// TODO: save username to localstorage
|
||||
if (response.statusCode === 204) {
|
||||
accountServerActions.signedup({username, verified: false});
|
||||
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 = {};
|
||||
|
|
|
@ -1,24 +1,44 @@
|
|||
var _ = require('underscore');
|
||||
var request = require('request');
|
||||
var async = require('async');
|
||||
var util = require('../utils/Util');
|
||||
var repositoryServerActions = require('../actions/RepositoryServerActions');
|
||||
var tagServerActions = require('../actions/TagServerActions');
|
||||
|
||||
module.exports = {
|
||||
search: function (query) {
|
||||
// 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 (!query) {
|
||||
return;
|
||||
repositoryServerActions.resultsUpdated({repos: []});
|
||||
}
|
||||
|
||||
request.get({
|
||||
url: 'https://registry.hub.docker.com/v1/search?',
|
||||
qs: {q: query}
|
||||
qs: {q: query, page}
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
// TODO: report search error
|
||||
repositoryServerActions.searchError({error});
|
||||
}
|
||||
|
||||
let data = JSON.parse(body);
|
||||
let repos = _.map(data.results, result => {
|
||||
return this.normalize(result);
|
||||
});
|
||||
if (response.statusCode === 200) {
|
||||
repositoryServerActions.searched({});
|
||||
repositoryServerActions.resultsUpdated({repos});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -26,19 +46,63 @@ module.exports = {
|
|||
recommended: function () {
|
||||
request.get('https://kitematic.com/recommended.json', (error, response, body) => {
|
||||
if (error) {
|
||||
// TODO: report search error
|
||||
repositoryServerActions.recommendedError({error});
|
||||
}
|
||||
|
||||
let data = JSON.parse(body);
|
||||
console.log(data);
|
||||
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 (jwt, repo) {
|
||||
let headers = jwt ? {
|
||||
Authorization: `JWT ${jwt}`
|
||||
} : null;
|
||||
|
||||
request.get({
|
||||
url: `https://registry.hub.docker.com/v2/repositories/${repo}/tags`,
|
||||
headers
|
||||
}, (error, response, body) => {
|
||||
if (response.statusCode === 200) {
|
||||
repositoryServerActions.recommended({});
|
||||
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 (jwt) {
|
||||
if (!jwt) {
|
||||
repositoryServerActions.reposUpdated({repos: []});
|
||||
return;
|
||||
}
|
||||
|
||||
repositoryServerActions.reposLoading({repos: []});
|
||||
|
||||
// TODO: provide jwt
|
||||
request.get({
|
||||
|
@ -48,7 +112,8 @@ module.exports = {
|
|||
}
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
repositoryServerActions.error({error});
|
||||
repositoryServerActions.reposError({error});
|
||||
return;
|
||||
}
|
||||
|
||||
let data = JSON.parse(body);
|
||||
|
@ -61,7 +126,7 @@ module.exports = {
|
|||
}
|
||||
}, (error, response, body) => {
|
||||
if (error) {
|
||||
repositoryServerActions.error({error});
|
||||
repositoryServerActions.reposError({error});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -73,7 +138,12 @@ module.exports = {
|
|||
for (let list of lists) {
|
||||
repos = repos.concat(list);
|
||||
}
|
||||
repositoryServerActions.fetched({repos});
|
||||
|
||||
_.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;
|
|
@ -32,6 +32,11 @@
|
|||
flex: 1 auto;
|
||||
flex-direction: column;
|
||||
padding: 25px 20px 0 25px;
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -39,10 +44,13 @@
|
|||
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;
|
||||
|
||||
.results-filter {
|
||||
text-align: center;
|
||||
|
@ -60,27 +68,38 @@
|
|||
.no-results {
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.verify {
|
||||
margin: 15px 0;
|
||||
position: relative;
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -50px;
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
margin: 0 auto;
|
||||
margin-top: -20%;
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +107,7 @@
|
|||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
flex: 0 auto;
|
||||
flex-shrink: 0;
|
||||
.text {
|
||||
flex: 1 auto;
|
||||
width: 50%;
|
||||
|
@ -134,183 +154,186 @@
|
|||
}
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
margin-top: 10px;
|
||||
.result-grids {
|
||||
|
||||
overflow: auto;
|
||||
.image-item {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue