diff --git a/graylog2-web-interface/src/components/common/Select/PaginatedSelect.tsx b/graylog2-web-interface/src/components/common/Select/PaginatedSelect.tsx index aea633478207..94372c7c28b0 100644 --- a/graylog2-web-interface/src/components/common/Select/PaginatedSelect.tsx +++ b/graylog2-web-interface/src/components/common/Select/PaginatedSelect.tsx @@ -15,17 +15,91 @@ * . */ 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; +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, 'options'> & { + onLoadOptions: (pagination: Pagination) => Promise, +} + +const PaginatedSelect = ({ onLoadOptions, ...rest }: Props) => { const selectRef = useRef(); + const [paginatedOptions, setPaginatedOptions] = useState(); + 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 ; + } return ( - ); }; diff --git a/graylog2-web-interface/src/components/users/UsersSelectField.tsx b/graylog2-web-interface/src/components/users/UsersSelectField.tsx index 89b23bbb898e..8472ccb908c7 100644 --- a/graylog2-web-interface/src/components/users/UsersSelectField.tsx +++ b/graylog2-web-interface/src/components/users/UsersSelectField.tsx @@ -15,99 +15,46 @@ * . */ 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(); - 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

; - } - - const { list, pagination: { total } } = paginatedUsers; - return ( {} : loadMoreOptions} + onLoadOptions={loadUsers} multi - total={total} onChange={onChange} /> ); };