Create
diff --git a/src/components/NewContainerSearch.react.js b/src/components/NewContainerSearch.react.js
index ab81643b28..c44953b1cc 100644
--- a/src/components/NewContainerSearch.react.js
+++ b/src/components/NewContainerSearch.react.js
@@ -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 (
-
- );
- });
+ 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 = (
+
+
There was an error contacting Docker Hub.
+
+ );
+ } else if (filter === 'userrepos' && !accountStore.getState().username) {
+ results = (
+
+
Log In or Sign Up to access your Docker Hub repositories.
+
+
+ );
+ } else if (filter === 'userrepos' && !accountStore.getState().verified) {
+ let spinner = this.state.accountLoading ?
: null;
+ results = (
+
+
Please verify your Docker Hub account email address
+
+ {spinner}
+
+
+
+ );
+ } else if (this.state.loading) {
+ results = (
+
+ );
+ } else if (repos.length) {
+ let recommendedItems = repos.filter(repo => repo.is_recommended).map(image =>
);
+ let otherItems = repos.filter(repo => !repo.is_recommended && !repo.is_user_repo).map(image =>
);
+
+ let recommendedResults = recommendedItems.length ? (
+
+
Recommended
+
+ {recommendedItems}
+
+
+ ) : null;
+
+ let userRepoItems = repos.filter(repo => repo.is_user_repo).map(image =>
);
+ let userRepoResults = userRepoItems.length ? (
+
+
My Repositories
+
+ {userRepoItems}
+
+
+ ) : null;
+
+ let otherResults = otherItems.length ? (
+
+
Other Repositories
+
+ {otherItems}
+
+
+ ) : null;
results = (
-
- {items}
+
+ {userRepoResults}
+ {recommendedResults}
+ {otherResults}
);
} else {
- if (this.state.results.length === 0 && this.state.query === '') {
- results = (
-
- );
- } else {
- results = (
-
-
Cannot find a matching image.
-
- );
- }
+ results = (
+
+
Cannot find a matching image.
+
+ );
}
let loadingClasses = classNames({
@@ -114,16 +196,12 @@ module.exports = React.createClass({
'search-icon': true
});
- let allTabClasses = classNames({
- 'results-filter':
- });
-
return (
- Select a Docker image to create a new container.
+ Select a Docker image to create a container.
@@ -136,9 +214,9 @@ module.exports = React.createClass({
FILTER BY
- All
- Recommended
- My Repositories
+ All
+ Recommended
+ My Repositories
{results}
diff --git a/src/routes.js b/src/routes.js
index d946cdbbda..8fcd2e3c8e 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -45,7 +45,7 @@ var routes = (
-
+
diff --git a/src/stores/AccountStore.js b/src/stores/AccountStore.js
index fc2ce6c89d..7fd1e01044 100644
--- a/src/stores/AccountStore.js
+++ b/src/stores/AccountStore.js
@@ -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}) {
diff --git a/src/stores/RepositoryStore.js b/src/stores/RepositoryStore.js
index 4263b02671..dcaec5e1ba 100644
--- a/src/stores/RepositoryStore.js
+++ b/src/stores/RepositoryStore.js
@@ -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;
}
}
diff --git a/src/stores/TagStore.js b/src/stores/TagStore.js
new file mode 100644
index 0000000000..9293bb16c6
--- /dev/null
+++ b/src/stores/TagStore.js
@@ -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);
diff --git a/src/utils/DockerUtil.js b/src/utils/DockerUtil.js
index ccc6cb8644..66a1499471 100644
--- a/src/utils/DockerUtil.js
+++ b/src/utils/DockerUtil.js
@@ -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();
});
});
});
diff --git a/src/utils/HubUtil.js b/src/utils/HubUtil.js
index 99092da4f9..b803a7cccf 100644
--- a/src/utils/HubUtil.js
+++ b/src/utils/HubUtil.js
@@ -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 = {};
diff --git a/src/utils/RegHubUtil.js b/src/utils/RegHubUtil.js
index 72aacf99dc..662f090fd2 100644
--- a/src/utils/RegHubUtil.js
+++ b/src/utils/RegHubUtil.js
@@ -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});
});
});
}
diff --git a/src/utils/RegistryUtil.js b/src/utils/RegistryUtil.js
deleted file mode 100644
index 8b4352d9f5..0000000000
--- a/src/utils/RegistryUtil.js
+++ /dev/null
@@ -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;
diff --git a/styles/new-container.less b/styles/new-container.less
index e9f7407858..eac8d36df1 100644
--- a/styles/new-container.less
+++ b/styles/new-container.less
@@ -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;
+ }
}
}
}