Merge pull request #1020 from FrenchBen/v2-pagination

Added V2 search and pagination
This commit is contained in:
French Ben 2015-10-26 15:12:24 -07:00
commit 6c3784d06b
5 changed files with 154 additions and 28 deletions

View File

@ -7,9 +7,9 @@ class RepositoryActions {
regHubUtil.recommended(); regHubUtil.recommended();
} }
search (query) { search (query, page = 1) {
this.dispatch({}); this.dispatch({query, page});
regHubUtil.search(query); regHubUtil.search(query, page);
} }
repos () { repos () {

View File

@ -23,7 +23,11 @@ module.exports = React.createClass({
username: accountStore.getState().username, username: accountStore.getState().username,
verified: accountStore.getState().verified, verified: accountStore.getState().verified,
accountLoading: accountStore.getState().loading, accountLoading: accountStore.getState().loading,
error: repositoryStore.getState().error error: repositoryStore.getState().error,
currentPage: repositoryStore.getState().currentPage,
totalPage: repositoryStore.getState().totalPage,
previousPage: repositoryStore.getState().previousPage,
nextPage: repositoryStore.getState().nextPage
}; };
}, },
componentDidMount: function () { componentDidMount: function () {
@ -43,7 +47,11 @@ module.exports = React.createClass({
update: function () { update: function () {
this.setState({ this.setState({
loading: repositoryStore.loading(), loading: repositoryStore.loading(),
repos: repositoryStore.all() repos: repositoryStore.all(),
currentPage: repositoryStore.getState().currentPage,
totalPage: repositoryStore.getState().totalPage,
previousPage: repositoryStore.getState().previousPage,
nextPage: repositoryStore.getState().nextPage
}); });
}, },
updateAccount: function () { updateAccount: function () {
@ -53,30 +61,40 @@ module.exports = React.createClass({
accountLoading: accountStore.getState().loading accountLoading: accountStore.getState().loading
}); });
}, },
search: function (query) { search: function (query, page = 1) {
if (_searchPromise) { if (_searchPromise) {
_searchPromise.cancel(); _searchPromise.cancel();
_searchPromise = null; _searchPromise = null;
} }
let previousPage = (page - 1 < 1) ? 1 : page - 1;
let nextPage = (page + 1 > this.state.totalPage) ? this.state.totalPage : page + 1;
this.setState({ this.setState({
query: query, query: query,
loading: true loading: true,
currentPage: page,
previousPage: previousPage,
nextPage: nextPage
}); });
_searchPromise = Promise.delay(200).cancellable().then(() => { _searchPromise = Promise.delay(200).cancellable().then(() => {
metrics.track('Searched for Images'); metrics.track('Searched for Images');
_searchPromise = null; _searchPromise = null;
repositoryActions.search(query); repositoryActions.search(query, page);
}).catch(Promise.CancellationError, () => {}); }).catch(Promise.CancellationError, () => {});
}, },
handleChange: function (e) { handleChange: function (e) {
var query = e.target.value; let query = e.target.value;
if (query === this.state.query) { if (query === this.state.query) {
return; return;
} }
this.search(query); this.search(query);
}, },
handlePage: function (page) {
let query = this.state.query;
this.search(query, page);
},
handleFilter: function (filter) { handleFilter: function (filter) {
// If we're clicking on the filter again - refresh // If we're clicking on the filter again - refresh
@ -111,20 +129,76 @@ module.exports = React.createClass({
}) })
.filter(repo => filter === 'all' || (filter === 'recommended' && repo.is_recommended) || (filter === 'userrepos' && repo.is_user_repo)); .filter(repo => filter === 'all' || (filter === 'recommended' && repo.is_recommended) || (filter === 'userrepos' && repo.is_user_repo));
let results; let results, paginateResults;
let previous = [];
let next = [];
if (this.state.previousPage) {
let previousPage = this.state.currentPage - 7;
if (previousPage < 1) {
previousPage = 1;
}
previous.push((
<li>
<a href="" onClick={this.handlePage.bind(this, 1)} aria-label="First">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
));
for (previousPage; previousPage < this.state.currentPage; previousPage++) {
previous.push((
<li><a href="" onClick={this.handlePage.bind(this, previousPage)}>{previousPage}</a></li>
));
}
}
if (this.state.nextPage) {
let nextPage = this.state.currentPage + 1;
for (nextPage; nextPage < this.state.totalPage; nextPage++) {
next.push((
<li><a href="" onClick={this.handlePage.bind(this, nextPage)}>{nextPage}</a></li>
));
if (nextPage > this.state.currentPage + 7) {
break;
}
}
next.push((
<li>
<a href="" onClick={this.handlePage.bind(this, this.state.totalPage)} aria-label="Last">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
));
}
let current = (
<li className="active">
<span>{this.state.currentPage} <span className="sr-only">(current)</span></span>
</li>
);
paginateResults = next.length || previous.length ? (
<nav>
<ul className="pagination">
{previous}
{current}
{next}
</ul>
</nav>
) : null;
if (this.state.error) { if (this.state.error) {
results = ( results = (
<div className="no-results"> <div className="no-results">
<h2>There was an error contacting Docker Hub.</h2> <h2>There was an error contacting Docker Hub.</h2>
</div> </div>
); );
paginateResults = null;
} else if (filter === 'userrepos' && !accountStore.getState().username) { } else if (filter === 'userrepos' && !accountStore.getState().username) {
results = ( results = (
<div className="no-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> <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}/> <RetinaImage src="connect-art.png" checkIfRetinaImgExists={false}/>
</div> </div>
); );
paginateResults = null;
} else if (filter === 'userrepos' && !accountStore.getState().verified) { } 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; let spinner = this.state.accountLoading ? <div className="spinner la-ball-clip-rotate la-dark"><div></div></div> : null;
results = ( results = (
@ -136,6 +210,7 @@ module.exports = React.createClass({
<RetinaImage src="inspection.png" checkIfRetinaImgExists={false}/> <RetinaImage src="inspection.png" checkIfRetinaImgExists={false}/>
</div> </div>
); );
paginateResults = null;
} else if (this.state.loading) { } else if (this.state.loading) {
results = ( results = (
<div className="no-results"> <div className="no-results">
@ -168,14 +243,20 @@ module.exports = React.createClass({
</div> </div>
) : null; ) : null;
let otherResults = otherItems.length ? ( let otherResults;
<div> if (otherItems.length) {
<h4>Other Repositories</h4> otherResults = (
<div className="result-grid"> <div>
{otherItems} <h4>Other Repositories</h4>
<div className="result-grid">
{otherItems}
</div>
</div> </div>
</div> );
) : null; } else {
otherResults = null;
paginateResults = null;
}
results = ( results = (
<div className="result-grids"> <div className="result-grids">
@ -237,6 +318,9 @@ module.exports = React.createClass({
<div className="results"> <div className="results">
{results} {results}
</div> </div>
<div className="pagination-center">
{paginateResults}
</div>
</div> </div>
</div> </div>
); );

View File

@ -13,6 +13,11 @@ class RepositoryStore {
this.results = []; this.results = [];
this.recommended = []; this.recommended = [];
this.repos = []; this.repos = [];
this.query = null;
this.nextPage = null;
this.previousPage = null;
this.currentPage = 1;
this.totalPage = null;
this.reposLoading = false; this.reposLoading = false;
this.recommendedLoading = false; this.recommendedLoading = false;
this.resultsLoading = false; this.resultsLoading = false;
@ -41,12 +46,18 @@ class RepositoryStore {
} }
} }
search () { search ({query, page}) {
this.setState({error: null, resultsLoading: true}); if (this.query === query) {
let previousPage = (page - 1 < 1) ? 1 : page - 1;
let nextPage = (page + 1 > this.totalPage) ? this.totalPage : page + 1;
this.setState({query: query, error: null, resultsLoading: true, currentPage: page, nextPage: nextPage, previousPage: previousPage});
} else {
this.setState({query: query, error: null, resultsLoading: true, nextPage: null, previousPage: null, currentPage: 1});
}
} }
resultsUpdated ({repos}) { resultsUpdated ({repos, page, previous, next, total}) {
this.setState({results: repos, resultsLoading: false}); this.setState({results: repos, currentPage: page, previousPage: previous, nextPage: next, totalPage: total, resultsLoading: false});
} }
recommended () { recommended () {

View File

@ -8,6 +8,7 @@ import tagServerActions from '../actions/TagServerActions';
let REGHUB2_ENDPOINT = process.env.REGHUB2_ENDPOINT || 'https://hub.docker.com/v2'; let REGHUB2_ENDPOINT = process.env.REGHUB2_ENDPOINT || 'https://hub.docker.com/v2';
let searchReq = null; let searchReq = null;
let PAGING = 24;
module.exports = { module.exports = {
// Normalizes results from search to v2 repository results // Normalizes results from search to v2 repository results
@ -24,7 +25,7 @@ module.exports = {
return obj; return obj;
}, },
search: function (query, page) { search: function (query, page, sorting = null) {
if (searchReq) { if (searchReq) {
searchReq.abort(); searchReq.abort();
searchReq = null; searchReq = null;
@ -33,10 +34,18 @@ module.exports = {
if (!query) { if (!query) {
repositoryServerActions.resultsUpdated({repos: []}); repositoryServerActions.resultsUpdated({repos: []});
} }
/**
* Sort:
* All - no sorting
* ordering: -start_count
* ordering: -pull_count
* is_automated: 1
* is_official: 1
*/
searchReq = request.get({ searchReq = request.get({
url: 'https://registry.hub.docker.com/v1/search?', url: `${REGHUB2_ENDPOINT}/search/repositories/?`,
qs: {q: query, page} qs: {query: query, page: page, page_size: PAGING, sorting}
}, (error, response, body) => { }, (error, response, body) => {
if (error) { if (error) {
repositoryServerActions.error({error}); repositoryServerActions.error({error});
@ -44,10 +53,14 @@ module.exports = {
let data = JSON.parse(body); let data = JSON.parse(body);
let repos = _.map(data.results, result => { let repos = _.map(data.results, result => {
result.name = result.repo_name;
return this.normalize(result); return this.normalize(result);
}); });
let next = data.next;
let previous = data.previous;
let total = Math.floor(data.count / PAGING);
if (response.statusCode === 200) { if (response.statusCode === 200) {
repositoryServerActions.resultsUpdated({repos}); repositoryServerActions.resultsUpdated({repos, page, previous, next, total});
} }
}); });
}, },
@ -99,7 +112,8 @@ module.exports = {
tags: function (repo, callback) { tags: function (repo, callback) {
hubUtil.request({ hubUtil.request({
url: `${REGHUB2_ENDPOINT}/repositories/${repo}/tags` url: `${REGHUB2_ENDPOINT}/repositories/${repo}/tags`,
qs: {page: 1, page_size: 100}
}, (error, response, body) => { }, (error, response, body) => {
if (response.statusCode === 200) { if (response.statusCode === 200) {
let data = JSON.parse(body); let data = JSON.parse(body);
@ -122,7 +136,8 @@ module.exports = {
let namespaces = []; let namespaces = [];
// Get Orgs for user // Get Orgs for user
hubUtil.request({ hubUtil.request({
url: `${REGHUB2_ENDPOINT}/user/orgs/?page_size=50` url: `${REGHUB2_ENDPOINT}/user/orgs/`,
qs: { page_size: 50 }
}, (orgError, orgResponse, orgBody) => { }, (orgError, orgResponse, orgBody) => {
if (orgError) { if (orgError) {
repositoryServerActions.error({orgError}); repositoryServerActions.error({orgError});

View File

@ -36,6 +36,22 @@
.spinner { .spinner {
display: inline-block; display: inline-block;
} }
.pagination-center {
flex: 1 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: #f6f8fb;
.pagination {
margin: 5px 0px;
a:focus {
background-color: #fff;
color: @brand-primary;
}
}
}
.results { .results {
display: flex; display: flex;