Skip to content

Commit

Permalink
Improve reusability of PaginatedSelect. (#20760)
Browse files Browse the repository at this point in the history
* Improve reusability of `PaginatedSelect`.

* Always set loading state when loading data.
  • Loading branch information
linuspahl authored Oct 25, 2024
1 parent 9bec368 commit d7fa24a
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,91 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import { useRef } from 'react';
import { useRef, useState, useEffect, useCallback } from 'react';
import debounce from 'lodash/debounce';

import Select from 'components/common/Select';
import { Spinner } from 'components/common';

type Props = React.ComponentProps<typeof Select>;
const DEFAULT_PAGINATION = { page: 1, perPage: 50, query: '' };

const PaginatedSelect = (props: Props) => {
type Pagination = {
page: number,
perPage: number,
query: string,
}

type PaginatedOptions = {
total: number,
pagination: Pagination,
list: Array<{ label: string, value: unknown }>,
}

type Props = Omit<React.ComponentProps<typeof Select>, 'options'> & {
onLoadOptions: (pagination: Pagination) => Promise<PaginatedOptions>,
}

const PaginatedSelect = ({ onLoadOptions, ...rest }: Props) => {
const selectRef = useRef();
const [paginatedOptions, setPaginatedOptions] = useState<PaginatedOptions | undefined>();
const [isLoading, setIsLoading] = useState(false);

const loadOptions = useCallback((pagination: Pagination, processResponse = (res: PaginatedOptions, _cur: PaginatedOptions) => res) => {
setIsLoading(true);

return onLoadOptions(pagination).then((res) => {
setPaginatedOptions((cur) => processResponse(res, cur));
setIsLoading(false);
});
}, [onLoadOptions]);

const handleSearch = debounce((newValue, actionMeta) => {
if (actionMeta.action === 'input-change') {
return loadOptions({ ...DEFAULT_PAGINATION, query: newValue });
}

if (actionMeta.action === 'menu-close') {
return loadOptions(DEFAULT_PAGINATION);
}

return Promise.resolve();
}, 400);

const handleLoadMore = debounce(() => {
const { pagination, total, list } = paginatedOptions;
const extendList = (res: PaginatedOptions, cur: PaginatedOptions) => ({
...res,
list: [...cur.list, ...res.list],
});

if (isLoading) {
return;
}

if (total > list.length) {
loadOptions({ ...pagination, page: pagination.page + 1, query: '' }, extendList);
}
}, 400);

useEffect(() => {
if (!paginatedOptions) {
onLoadOptions(DEFAULT_PAGINATION).then(setPaginatedOptions);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (!paginatedOptions) {
return <Spinner text="Loading options..." />;
}

return (
<Select ref={selectRef} async {...props} />
<Select {...rest}
ref={selectRef}
options={paginatedOptions.list}
onInputChange={handleSearch}
loadOptions={handleLoadMore}
async
total={paginatedOptions.total} />
);
};

Expand Down
83 changes: 15 additions & 68 deletions graylog2-web-interface/src/components/users/UsersSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,99 +15,46 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import { useEffect, useState, useCallback } from 'react';
import debounce from 'lodash/debounce';
import type { PaginatedUsers } from 'src/stores/users/UsersStore';
import { useCallback } from 'react';

import UsersDomain from 'domainActions/users/UsersDomain';
import { isPermitted } from 'util/PermissionsMixin';
import { Spinner } from 'components/common';
import useCurrentUser from 'hooks/useCurrentUser';

import PaginatedSelect from '../common/Select/PaginatedSelect';

const DEFAULT_PAGINATION = { page: 1, perPage: 50, query: '', total: 0 };

const formatUsers = (users) => users.map((user) => ({ label: `${user.username} (${user.fullName})`, value: user.username }));

type Props = {
value: string,
onChange: (nextValue) => void,
value: string,
onChange: (nextValue) => void,
}

const UsersSelectField = ({ value, onChange }: Props) => {
const currentUser = useCurrentUser();
const [paginatedUsers, setPaginatedUsers] = useState<PaginatedUsers | undefined>();
const [isNextPageLoading, setIsNextPageLoading] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const loadUsersPaginated = useCallback((pagination = DEFAULT_PAGINATION) => {
if (isPermitted(currentUser.permissions, 'users:list')) {
setIsNextPageLoading(true);

return UsersDomain.loadUsersPaginated(pagination).then((newPaginatedUser) => {
setIsNextPageLoading(false);

return newPaginatedUser;
const loadUsers = useCallback((pagination: { page: number, perPage: number, query: string }) => {
if (!isPermitted(currentUser.permissions, 'users:list')) {
return Promise.resolve({
pagination,
total: 0,
list: [],
});
}

return undefined;
return UsersDomain.loadUsersPaginated(pagination).then((results) => ({
total: results.pagination.total,
list: formatUsers(results.list.toArray()),
pagination,
}));
}, [currentUser.permissions]);

const loadUsers = (pagination, query = '') => {
loadUsersPaginated({ ...pagination, page: pagination.page + 1, query }).then((response) => {
setPaginatedUsers((prevUsers) => {
const list = prevUsers.list.concat(response.list);
const newPagination = { ...prevUsers.pagination, ...response.pagination };

return { ...prevUsers, list, pagination: newPagination } as PaginatedUsers;
});
});
};

const loadMoreOptions = debounce(() => {
const { pagination, pagination: { total }, list } = paginatedUsers;

if (total > list.count()) {
loadUsers(pagination);
}
}, 400);

useEffect(() => {
if (!paginatedUsers) {
loadUsersPaginated().then(setPaginatedUsers);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const handleSearch = debounce((newValue, actionMeta) => {
if ((actionMeta.action === 'input-change')) {
setIsSearching(true);

loadUsersPaginated({ ...DEFAULT_PAGINATION, query: newValue }).then((results) => {
setIsSearching(true);
setPaginatedUsers(results);
});
} else if (actionMeta.action === 'menu-close') {
loadUsersPaginated().then(setPaginatedUsers);
}
}, 400);

if (!paginatedUsers) {
return <p><Spinner text="Loading User select..." /></p>;
}

const { list, pagination: { total } } = paginatedUsers;

return (
<PaginatedSelect id="user-select-list"
value={value}
placeholder="Select user(s)..."
options={formatUsers(list.toArray())}
onInputChange={handleSearch}
loadOptions={isNextPageLoading || isSearching ? () => {} : loadMoreOptions}
onLoadOptions={loadUsers}
multi
total={total}
onChange={onChange} />
);
};
Expand Down

0 comments on commit d7fa24a

Please sign in to comment.