Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip on endless scrolling pagination #2221

Closed
wants to merge 21 commits into from

Conversation

antgamdia
Copy link
Contributor

@antgamdia antgamdia commented Dec 17, 2020

Description of the change

This (endless) PR adds endless scrolling in the Catalog view as a result of the analysis and discussions of the design document.

Currently, in this PR we perform a server-side filtering for the most expensive calls: 1) filter by text; 2) filter by repository.
Moreover, the available categories are retrieved from the server-side (instead of getting unique names each time on client-side).

I decided to go this way because it results in faster response times from a UX perspective. I mean: to the best of my knowledge, there are no large repos adding meta information such as the categories. The typical use case would be people using bitnami catalog or other hand-crafted repo; in both cases, the amount of charts is still handleable (78 charts, 0.6 seconds)
Assuming this hypothesis, a server-side filtering here would increase the (perceived) response time: click filter, blank page with a spinner, render charts (instead of instantaneously filtering them)

Some notes about the PR

  • The scroll event triggers a bunch of updates -but- due to network issues or other causes, race conditions may occur. A cancelation policy should be implemented, but I'm still not able to figure out a good one; help wanted here.
  • Tests aren't updated yet since I'm waiting to get your first impressions on the overall implementation.
  • Discuss if we really want to move the category filtering to the backend

Current status

This PR is to be split into smaller ones. As I'm creating the new ones, I'm removing the corresponding changes here.

Implementation details

The main changes are in these files:

  • Catalog.tsx
    • It still has the filters for, each time the component is being loaded, applying some filters in client-side for chart
      • Type: either chart or operator
      • Category (list populated by server-side call)
    • The operators part remains unchanged, all operators are being rendered without pagination.
    • The scroll down event is detected using the IntersectionAPI of the browser. This is a widely supported browser API, but could eventually fail in old browsers, but I don't think it''s a concerning issue.
    • The existing fetchCharts function has been replaced with fetchChartsWithPagination. Some attributes like query, page and size have been introduced.
    • This function is called in different points: 1) during the component is mounted or there is a change in the namespace, 2) when the user scrolls down, 3), when the selected repository changes, 4) when there is an error an a manual load button appears,
  • actions/charts.ts
    • Some new actions have been added for managing the paginated fetching process: reachEnd, receiveChartCategories, receiveChartsSearch, requestChartsCategories, requestChartsSearch, requestChartsVersions, resetChartsSearch, resetPaginaton
  • reducers/charts.ts
    • Each of the aforementioned actions has its corresponding reducer function to update the state. It not only takes into account the action, but the status (idleStatus, errorStatus, loadingStatus, finishedStatus). It would be really useful to have a state diagram here, one can get lost very likely...

The remaining changes are less important (adding server URL with query params in url.ts, modifying the search box in SearchFilter.tsx to pass the current value and adding properties in types)

Benefits

Large repositories (>2000) won't take more than 5 seconds in being rendered as each element will be created while the user scrolls down.

Possible drawbacks

Since the scroll event is captured regardless of the current filter, we are triggering the loading even if it is not required.
For instance, in this example, we are retrieving the whole set of charts but displaying only the operators. This also happens in the filtering by category.
This is not that easy, because when rendering the view the first time, it will update the state to be "fetching charts" instead of "finish". Client-side filters do not modify the state.

image

Applicable issues

Additional information

Design discussion at via.vmw.com/2020-kubeapps-ui-proposal.

Current status:

scroll3

@absoludity
Copy link
Contributor

This PR is waiting to be rebased I assume?

@antgamdia
Copy link
Contributor Author

This PR is waiting to be rebased I assume?

You're right. I'll merge upstream changes, resolve conflicts and update the PR description adding more information.
Note, I still have to update the frontend tests.

Copy link
Contributor

@absoludity absoludity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (endless) PR adds endless scro

Haha - yes, it can feel that way sometimse!

query: string,
page: number,
size: number,
nextPage: number,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my own understanding, there's 3 changes here:

  1. repo -> repos - the query now enables charts from multiple repos
  2. addition of query - not yet sure why
  3. Addition of pagination vars - why page and nextPage?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right,
i) repos is because we can select multiple repos.;
ii) query is for searching charts using free text, when ""it doesn't perform any search;
iii) page and size is just for the usual pagination. nextPage is a workaround for attempting to solve the race condition when requesting

const charts = await Chart.fetchCharts(cluster, namespace, repo);
if (charts) {
dispatch(receiveCharts(charts));
if (page === nextPage && !(query === "" && size === 0)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment here would be helpful... it's not immediately clear what this is doing. It looks like: "we only request charts if there's either a query or the size is non-zero" (still not clear what nextpage is for, but I'll find out below).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, totally agree, it isn't clear at all. It has to do with the workaround for skipping useless requests. We should get rid of the nextPage and solve the cancelation of requests in another place.

cluster: string,
namespace: string,
repos: string,
query: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear why we need a separate function here if the previous one includes a query option too? Only the actions look different?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was just a reminiscence of old code. This duplicated function can be safely removed. Thanks for the observation!

try {
const categories = await Chart.fetchChartCategories(cluster, namespace, repos);
if (categories) {
dispatch(receiveChartCategories(categories));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always find it odd that we don't dispatch a receive when we receive zero categories (ie []), but I think it's what other actions do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I just tried to replicate the behavior of other actions in this case

dispatch(receiveReposSecrets(repoSecrets));
} catch (e) {
dispatch(errorRepos(e, "fetch"));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously unhandled error? Nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I faced this unhandled rejection when developing and just solved it here since it is a very minor fix

dispatch(actions.charts.resetChartsSearch());
fetchChartsWithPagination(cluster, namespace, currentRepo, "", 1, size, nextPage); // get the first charts
getCSVs(cluster, namespace);
dispatch(actions.charts.resetPaginaton()); // start from page=1 again
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Won't this mean the next page of results requested will be the first page again? (guessing not, but if you could expand the comment to explain why that'd be helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now I'm not able to recall why the resetPaginaton() was useful. I remember I was getting page=2 results even if I changed the filters, but I'm not able to reproduce it right now. Commenting this line, but not deleting it just in case.

(query: string) => {
setCurrentSearchQuery(trimStart(query));
if (currentSearchQuery.length) {
if (currentSearchQuery.length < 5 || currentSearchQuery.endsWith(" ")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// If the user is still typing a query, throttle requests to a sensible frequency,
// otherwise debounce the requests so we only get a single request for the last event.

though, it's still not clear why we don't just use debounce regardless of whether the last character was a space? If debounce had 400ms and we used it exclusively, we'd just get a request every time the user paused (for more than 400ms). Why do we want the throttled one which will continually request when there are less that 5 letters?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed this post: https://www.peterbe.com/plog/how-to-throttle-and-debounce-an-autocomplete-input-in-react
Previously, I did a PoC of autocompletion in the search box and that's why I followed this twofold approximation.
Perhaps just debouncing may work.

image


const filteredCharts = charts
.filter(
() => filters[filterNames.TYPE].length === 0 || filters[filterNames.TYPE].includes("Charts"),
)
.filter(() => filters[filterNames.OPERATOR_PROVIDER].length === 0)
.filter(c => new RegExp(escapeRegExp(searchFilter), "i").test(c.id))
// .filter(c => new RegExp(escapeRegExp(searchFilter), "i").test(c.id))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guessing you meant to remove this.

() => filters[filterNames.TYPE].length === 0 || filters[filterNames.TYPE].includes("Charts"),
)
.filter(() => filters[filterNames.OPERATOR_PROVIDER].length === 0)
// .filter(c => new RegExp(escapeRegExp(search.query), "i").test(c.id))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here (remove?)

csvs,
cluster,
namespace,
hasFinished,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasFinished isn't really descriptive. I assume this means hasFinishedFetching?

@antgamdia
Copy link
Contributor Author

Thank you for your comments despite it is still a draft PR. I wanted to polish up a couple of things (such as solving some issues when merging and adding more comments) and add some implementation details in the PR comment.
As you have already noticed, there is some odd stuff. It was more complicated than I expected, and, in fact, I wasn't able to effectively solve the issue of canceling requests.
For instance, the nextPage is just an attempt to ensure each page is being requested (despite duplicate requests may and will occur). Nevertheless, I'd like to discuss it a little bit more with you folks.

@antgamdia
Copy link
Contributor Author

antgamdia commented Jan 13, 2021

As per the discussion with @andresmgot, this PR should be split into many smaller PRs, namely:

The main problem is when making multiple requests: we should track each one and determine if it should be accepted (add chunks), ignored (the data shouldn't be added) or retried (if it failed)

@antgamdia
Copy link
Contributor Author

For the record, minor UI bug discovered when provider's name includes a comma. Users won't be able to deselect it, resulting in something like:

image

This was referenced Jan 16, 2021
@antgamdia
Copy link
Contributor Author

With a proper useEffect and reducers usage (and a map to track the requests to avoid dups), now scrolling-triggered requests are working as expected!
Further efforts still needed on handling different changes (selected repos, selected namespace, update text search, change view, refresh page, etc).

Current status:
scroll4

Note: no PR open yet since I want to address other PR's comments (and learn from them :) ) first.

@antgamdia antgamdia mentioned this pull request Jan 20, 2021
6 tasks
@antgamdia
Copy link
Contributor Author

Closing this PR since it has been moved to #2264.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pagination for Catalog page
2 participants