Commit f66fc7ed authored by Nathan Friend's avatar Nathan Friend Committed by Natalia Tepluhina

Progressively load releases on the Releases page

parent 6a474c07
......@@ -33,7 +33,34 @@ export default {
},
},
apollo: {
graphqlResponse: {
/**
* The same query as `fullGraphqlResponse`, except that it limits its
* results to a single item. This causes this request to complete much more
* quickly than `fullGraphqlResponse`, which allows the page to show
* meaningful content to the user much earlier.
*/
singleGraphqlResponse: {
query: allReleasesQuery,
// This trick only works when paginating _forward_.
// When paginating backwards, limiting the query to a single item loads
// the _last_ item in the page, which is not useful for our purposes.
skip() {
return !this.includeSingleQuery;
},
variables() {
return {
...this.queryVariables,
first: 1,
};
},
update(data) {
return { data };
},
error() {
this.singleRequestError = true;
},
},
fullGraphqlResponse: {
query: allReleasesQuery,
variables() {
return this.queryVariables;
......@@ -42,7 +69,7 @@ export default {
return { data };
},
error(error) {
this.hasError = true;
this.fullRequestError = true;
createFlash({
message: this.$options.i18n.errorMessage,
......@@ -54,7 +81,8 @@ export default {
},
data() {
return {
hasError: false,
singleRequestError: false,
fullRequestError: false,
cursors: {
before: getParameterByName('before'),
after: getParameterByName('after'),
......@@ -83,42 +111,66 @@ export default {
sort: this.sort,
};
},
isLoading() {
return this.$apollo.queries.graphqlResponse.loading;
/**
* @returns {Boolean} Whether or not to request/include
* the results of the single-item query
*/
includeSingleQuery() {
return Boolean(!this.cursors.before || this.cursors.after);
},
isSingleRequestLoading() {
return this.$apollo.queries.singleGraphqlResponse.loading;
},
isFullRequestLoading() {
return this.$apollo.queries.fullGraphqlResponse.loading;
},
/**
* @returns {Boolean} `true` if the `singleGraphqlResponse`
* query has finished loading without errors
*/
isSingleRequestLoaded() {
return Boolean(!this.isSingleRequestLoading && this.singleGraphqlResponse?.data.project);
},
/**
* @returns {Boolean} `true` if the `fullGraphqlResponse`
* query has finished loading without errors
*/
isFullRequestLoaded() {
return Boolean(!this.isFullRequestLoading && this.fullGraphqlResponse?.data.project);
},
releases() {
if (!this.graphqlResponse || this.hasError) {
return [];
if (this.isFullRequestLoaded) {
return convertAllReleasesGraphQLResponse(this.fullGraphqlResponse).data;
}
if (this.isSingleRequestLoaded && this.includeSingleQuery) {
return convertAllReleasesGraphQLResponse(this.singleGraphqlResponse).data;
}
return convertAllReleasesGraphQLResponse(this.graphqlResponse).data;
return [];
},
pageInfo() {
if (!this.graphqlResponse || this.hasError) {
if (!this.isFullRequestLoaded) {
return {
hasPreviousPage: false,
hasNextPage: false,
};
}
return this.graphqlResponse.data.project.releases.pageInfo;
return this.fullGraphqlResponse.data.project.releases.pageInfo;
},
shouldRenderEmptyState() {
return !this.releases.length && !this.hasError && !this.isLoading;
},
shouldRenderSuccessState() {
return this.releases.length && !this.isLoading && !this.hasError;
return this.isFullRequestLoaded && this.releases.length === 0;
},
shouldRenderLoadingIndicator() {
return this.isLoading && !this.hasError;
},
shouldRenderPagination() {
return (
!this.isLoading &&
!this.hasError &&
(this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage)
(this.isSingleRequestLoading && !this.singleRequestError && !this.isFullRequestLoaded) ||
(this.isFullRequestLoading && !this.fullRequestError)
);
},
shouldRenderPagination() {
return this.isFullRequestLoaded && !this.shouldRenderEmptyState;
},
},
created() {
this.updateQueryParamsFromUrl();
......@@ -130,7 +182,7 @@ export default {
},
methods: {
getReleaseKey(release, index) {
return [release.tagNamerstrs, release.name, index].join('|');
return [release.tagName, release.name, index].join('|');
},
updateQueryParamsFromUrl() {
this.cursors.before = getParameterByName('before');
......@@ -191,18 +243,16 @@ export default {
>
</div>
<release-skeleton-loader v-if="shouldRenderLoadingIndicator" />
<releases-empty-state v-else-if="shouldRenderEmptyState" />
<releases-empty-state v-if="shouldRenderEmptyState" />
<div v-else-if="shouldRenderSuccessState">
<release-block
v-for="(release, index) in releases"
:key="getReleaseKey(release, index)"
:release="release"
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
/>
</div>
<release-skeleton-loader v-if="shouldRenderLoadingIndicator" />
<releases-pagination-apollo-client
v-if="shouldRenderPagination"
......
......@@ -14,7 +14,19 @@ export default () => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
defaultClient: createDefaultClient(
{},
{
// This page attempts to decrease the perceived loading time
// by sending two requests: one request for the first item only (which
// completes relatively quickly), and one for all the items (which is slower).
// By default, Apollo Client batches these requests together, which defeats
// the purpose of making separate requests. So we explicitly
// disable batching on this page.
batchMax: 1,
assumeImmutableResults: true,
},
),
});
return new Vue({
......
......@@ -100,6 +100,17 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
link_type: :image)
end
let_it_be(:another_release) do
create(:release,
project: project,
tag: 'v1.2',
name: 'The second release',
author: admin,
description: 'An okay release :shrug:',
created_at: Time.zone.parse('2019-01-03'),
released_at: Time.zone.parse('2019-01-10'))
end
after(:all) do
remove_repository(project)
end
......
......@@ -3,6 +3,58 @@
exports[`releases/util.js convertAllReleasesGraphQLResponse matches snapshot 1`] = `
Object {
"data": Array [
Object {
"_links": Object {
"closedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.2&scope=all&state=closed",
"closedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=closed",
"editUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.2/edit",
"mergedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=merged",
"openedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.2&scope=all&state=opened",
"openedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=opened",
"self": "http://localhost/releases-namespace/releases-project/-/releases/v1.2",
"selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.2",
},
"assets": Object {
"count": 4,
"links": Array [],
"sources": Array [
Object {
"format": "zip",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.zip",
},
Object {
"format": "tar.gz",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar.gz",
},
Object {
"format": "tar.bz2",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar.bz2",
},
Object {
"format": "tar",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar",
},
],
},
"author": Object {
"avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
"username": "administrator",
"webUrl": "http://localhost/administrator",
},
"commit": Object {
"shortId": "b83d6e39",
"title": "Merge branch 'branch-merged' into 'master'",
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:23\\" dir=\\"auto\\">An okay release <gl-emoji title=\\"shrug\\" data-name=\\"shrug\\" data-unicode-version=\\"9.0\\">🤷</gl-emoji></p>",
"evidences": Array [],
"milestones": Array [],
"name": "The second release",
"releasedAt": "2019-01-10T00:00:00Z",
"tagName": "v1.2",
"tagPath": "/releases-namespace/releases-project/-/tags/v1.2",
"upcomingRelease": true,
},
Object {
"_links": Object {
"closedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.1&scope=all&state=closed",
......@@ -124,7 +176,7 @@ Object {
"endCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTgtMTItMTAgMDA6MDA6MDAuMDAwMDAwMDAwIFVUQyIsImlkIjoiMSJ9",
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTgtMTItMTAgMDA6MDA6MDAuMDAwMDAwMDAwIFVUQyIsImlkIjoiMSJ9",
"startCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTktMDEtMTAgMDA6MDA6MDAuMDAwMDAwMDAwIFVUQyIsImlkIjoiMiJ9",
},
}
`;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment