diff --git a/frontend/src/App/Login/LoginByGithubCallback/index.tsx b/frontend/src/App/Login/LoginByGithubCallback/index.tsx index af88aa72f1..45be311b6b 100644 --- a/frontend/src/App/Login/LoginByGithubCallback/index.tsx +++ b/frontend/src/App/Login/LoginByGithubCallback/index.tsx @@ -41,7 +41,7 @@ export const LoginByGithubCallback: React.FC = () => { .then(async ({ creds: { token } }) => { dispatch(setAuthData({ token })); if (process.env.UI_VERSION === 'sky') { - const result = await getProjects().unwrap(); + const result = await getProjects({}).unwrap(); if (result?.length === 0) { navigate(ROUTES.PROJECT.ADD); return; diff --git a/frontend/src/hooks/useCheckingForFleetsInProjectsOfMember.ts b/frontend/src/hooks/useCheckingForFleetsInProjectsOfMember.ts index d91b78a3b1..b330358b46 100644 --- a/frontend/src/hooks/useCheckingForFleetsInProjectsOfMember.ts +++ b/frontend/src/hooks/useCheckingForFleetsInProjectsOfMember.ts @@ -5,9 +5,12 @@ import { useGetOnlyNoFleetsProjectsQuery, useGetProjectsQuery } from 'services/p type Args = { projectNames?: IProject['project_name'][] }; export const useCheckingForFleetsInProjects = ({ projectNames }: Args) => { - const { data: projectsData } = useGetProjectsQuery(undefined, { - skip: !!projectNames?.length, - }); + const { data: projectsData } = useGetProjectsQuery( + {}, + { + skip: !!projectNames?.length, + }, + ); const { data: noFleetsProjectsData } = useGetOnlyNoFleetsProjectsQuery(); @@ -16,8 +19,8 @@ export const useCheckingForFleetsInProjects = ({ projectNames }: Args) => { return projectNames; } - if (projectsData) { - return projectsData.map((project) => project.project_name); + if (projectsData?.data) { + return projectsData.data.map((project) => project.project_name); } return []; diff --git a/frontend/src/hooks/useInfiniteScroll.ts b/frontend/src/hooks/useInfiniteScroll.ts index 727586ab00..b11115d63f 100644 --- a/frontend/src/hooks/useInfiniteScroll.ts +++ b/frontend/src/hooks/useInfiniteScroll.ts @@ -9,10 +9,14 @@ const SCROLL_POSITION_GAP = 400; type InfinityListArgs = Partial>; type ListResponse = DataItem[]; +type ResponseWithDataProp = { data: ListResponse; total_count: number }; + +type LazyQueryResponse = ResponseWithDataProp | ListResponse; type UseInfinityParams = { - useLazyQuery: UseLazyQuery, any>>; + useLazyQuery: UseLazyQuery, any>>; args: { limit?: number } & Args; + getResponseItems?: (listItem: DataItem) => Partial; getPaginationParams: (listItem: DataItem) => Partial; skip?: boolean; // options?: UseQueryStateOptions, Record>; @@ -26,22 +30,31 @@ export const useInfiniteScroll = ({ skip, }: UseInfinityParams) => { const [data, setData] = useState>([]); + const [totalCount, setTotalCount] = useState(); const scrollElement = useRef(document.documentElement); const isLoadingRef = useRef(false); + const isDisabledMoreRef = useRef(false); const lastRequestParams = useRef(undefined); - const [disabledMore, setDisabledMore] = useState(false); const { limit, ...argsProp } = args; const lastArgsProps = useRef>(null); const [getItems, { isLoading, isFetching }] = useLazyQuery({ ...args } as Args); const getDataRequest = (params: Args) => { - lastRequestParams.current = params; + if (isEqual(params, lastRequestParams.current)) { + return Promise.reject(); + } - return getItems({ + const request = getItems({ limit, ...params, } as Args).unwrap(); + + request.then(() => { + lastRequestParams.current = { ...params }; + }); + + return request; }; const getEmptyList = () => { @@ -49,9 +62,18 @@ export const useInfiniteScroll = ({ setData([]); - getDataRequest(argsProp as Args).then((result) => { - setDisabledMore(false); - setData(result as ListResponse); + getDataRequest(argsProp as Args).then((result: LazyQueryResponse) => { + // setDisabledMore(false); + isDisabledMoreRef.current = false; + + if ('data' in result) { + setData(result.data as ListResponse); + setTotalCount(result.total_count); + } else { + setData(result as ListResponse); + setTotalCount(); + } + isLoadingRef.current = false; }); }; @@ -64,7 +86,7 @@ export const useInfiniteScroll = ({ }, [argsProp, lastArgsProps, skip]); const getMore = async () => { - if (isLoadingRef.current || disabledMore || skip) { + if (isLoadingRef.current || isDisabledMoreRef.current || skip) { return; } @@ -76,10 +98,20 @@ export const useInfiniteScroll = ({ ...getPaginationParams(data[data.length - 1]), } as Args); - if (result.length > 0) { - setData((prev) => [...prev, ...result]); + let listResponse: ListResponse; + + if ('data' in result) { + listResponse = result.data; + setTotalCount(result.total_count); + } else { + listResponse = result; + setTotalCount(); + } + + if (listResponse.length > 0) { + setData((prev) => [...prev, ...listResponse]); } else { - setDisabledMore(true); + isDisabledMoreRef.current = true; } } catch (e) { console.log(e); @@ -87,7 +119,7 @@ export const useInfiniteScroll = ({ setTimeout(() => { isLoadingRef.current = false; - }, 10); + }, 50); }; useLayoutEffect(() => { @@ -101,7 +133,7 @@ export const useInfiniteScroll = ({ }, [data]); const onScroll = useCallback(() => { - if (disabledMore || isLoadingRef.current) { + if (isDisabledMoreRef.current || isLoadingRef.current) { return; } @@ -112,7 +144,7 @@ export const useInfiniteScroll = ({ if (scrollPositionFromBottom < SCROLL_POSITION_GAP) { getMore().catch(console.log); } - }, [disabledMore, getMore]); + }, [getMore]); useEffect(() => { document.addEventListener('scroll', onScroll); @@ -126,6 +158,7 @@ export const useInfiniteScroll = ({ return { data, + totalCount, isLoading: isLoading || (data.length === 0 && isFetching), isLoadingMore, refreshList: getEmptyList, diff --git a/frontend/src/hooks/useProjectFilter.ts b/frontend/src/hooks/useProjectFilter.ts index b4573b1f8e..58e573d31d 100644 --- a/frontend/src/hooks/useProjectFilter.ts +++ b/frontend/src/hooks/useProjectFilter.ts @@ -16,20 +16,20 @@ export const useProjectFilter = ({ localStorePrefix }: Args) => { null, ); - const { data: projectsData } = useGetProjectsQuery(); + const { data: projectsData } = useGetProjectsQuery({}); const projectOptions = useMemo(() => { - if (!projectsData?.length) return []; + if (!projectsData?.data?.length) return []; - return projectsData.map((project) => ({ label: project.project_name, value: project.project_name })); + return projectsData.data.map((project) => ({ label: project.project_name, value: project.project_name })); }, [projectsData]); useEffect(() => { - if (!projectsData || !selectedProject) { + if (!projectsData?.data || !selectedProject) { return; } - const hasSelectedProject = projectsData.some(({ project_name }) => selectedProject?.value === project_name); + const hasSelectedProject = projectsData.data.some(({ project_name }) => selectedProject?.value === project_name); if (!hasSelectedProject) { setSelectedProject(null); diff --git a/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts b/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts index d3a465fcb5..543eb07a7e 100644 --- a/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts +++ b/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts @@ -44,7 +44,7 @@ export const useTutorials = () => { } = useAppSelector(selectTutorialPanel); const { data: userBillingData } = useGetUserBillingInfoQuery({ username: useName ?? '' }, { skip: !useName }); - const { data: projectData } = useGetProjectsQuery(); + const { data: projectData } = useGetProjectsQuery({}); const { data: runsData } = useGetRunsQuery({ limit: 1, }); @@ -54,14 +54,14 @@ export const useTutorials = () => { useEffect(() => { if ( userBillingData && - projectData && + projectData?.data && runsData && !completeIsChecked.current && location.pathname !== ROUTES.PROJECT.ADD ) { const billingCompleted = userBillingData.balance > 0; const configureCLICompleted = runsData.length > 0; - const createProjectCompleted = projectData.length > 0; + const createProjectCompleted = projectData.data.length > 0; let tempHideStartUp = hideStartUp; @@ -88,7 +88,7 @@ export const useTutorials = () => { }, [userBillingData, runsData, projectData, location.pathname]); useEffect(() => { - if (projectData && projectData.length > 0 && !createProjectCompleted) { + if (projectData?.data && projectData.data.length > 0 && !createProjectCompleted) { dispatch( updateTutorialPanelState({ createProjectCompleted: true, @@ -114,8 +114,8 @@ export const useTutorials = () => { }, []); const startConfigCliTutorial = useCallback(() => { - if (projectData?.length) { - navigate(ROUTES.PROJECT.DETAILS.SETTINGS.FORMAT(projectData[0].project_name)); + if (projectData?.data?.length) { + navigate(ROUTES.PROJECT.DETAILS.SETTINGS.FORMAT(projectData.data[0].project_name)); } }, [projectData]); diff --git a/frontend/src/pages/Events/List/hooks/useFilters.ts b/frontend/src/pages/Events/List/hooks/useFilters.ts index a3d510718f..4b7d38d085 100644 --- a/frontend/src/pages/Events/List/hooks/useFilters.ts +++ b/frontend/src/pages/Events/List/hooks/useFilters.ts @@ -69,8 +69,8 @@ const targetTypes = [ export const useFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); - const { data: projectsData } = useGetProjectsQuery(); - const { data: usersData } = useGetUserListQuery(); + const { data: projectsData } = useGetProjectsQuery({}); + const { data: usersData } = useGetUserListQuery({}); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => requestParamsToTokens({ searchParams, filterKeys }), @@ -84,7 +84,7 @@ export const useFilters = () => { const filteringOptions = useMemo(() => { const options: PropertyFilterProps.FilteringOption[] = []; - projectsData?.forEach(({ project_name }) => { + projectsData?.data?.forEach(({ project_name }) => { options.push({ propertyKey: filterKeys.TARGET_PROJECTS, value: project_name, @@ -96,7 +96,7 @@ export const useFilters = () => { }); }); - usersData?.forEach(({ username }) => { + usersData?.data?.forEach(({ username }) => { options.push({ propertyKey: filterKeys.TARGET_USERS, value: username, @@ -229,14 +229,16 @@ export const useFilters = () => { ...(params[filterKeys.TARGET_PROJECTS] && Array.isArray(params[filterKeys.TARGET_PROJECTS]) ? { [filterKeys.TARGET_PROJECTS]: params[filterKeys.TARGET_PROJECTS]?.map( - (name: string) => projectsData?.find(({ project_name }) => project_name === name)?.['project_id'], + (name: string) => + projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id'], ), } : {}), ...(params[filterKeys.WITHIN_PROJECTS] && Array.isArray(params[filterKeys.WITHIN_PROJECTS]) ? { [filterKeys.WITHIN_PROJECTS]: params[filterKeys.WITHIN_PROJECTS]?.map( - (name: string) => projectsData?.find(({ project_name }) => project_name === name)?.['project_id'], + (name: string) => + projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id'], ), } : {}), @@ -244,7 +246,7 @@ export const useFilters = () => { ...(params[filterKeys.TARGET_USERS] && Array.isArray(params[filterKeys.TARGET_USERS]) ? { [filterKeys.TARGET_USERS]: params[filterKeys.TARGET_USERS]?.map( - (name: string) => usersData?.find(({ username }) => username === name)?.['id'], + (name: string) => usersData?.data?.find(({ username }) => username === name)?.['id'], ), } : {}), @@ -252,7 +254,7 @@ export const useFilters = () => { ...(params[filterKeys.ACTORS] && Array.isArray(params[filterKeys.ACTORS]) ? { [filterKeys.ACTORS]: params[filterKeys.ACTORS]?.map( - (name: string) => usersData?.find(({ username }) => username === name)?.['id'], + (name: string) => usersData?.data?.find(({ username }) => username === name)?.['id'], ), } : {}), diff --git a/frontend/src/pages/Models/List/hooks.tsx b/frontend/src/pages/Models/List/hooks.tsx index 912875b394..461bf28a3b 100644 --- a/frontend/src/pages/Models/List/hooks.tsx +++ b/frontend/src/pages/Models/List/hooks.tsx @@ -129,7 +129,7 @@ const filterKeys: Record = { export const useFilters = (localStorePrefix = 'models-list-page') => { const [searchParams, setSearchParams] = useSearchParams(); const { projectOptions } = useProjectFilter({ localStorePrefix }); - const { data: usersData } = useGetUserListQuery(); + const { data: usersData } = useGetUserListQuery({}); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => requestParamsToTokens({ searchParams, filterKeys }), @@ -151,7 +151,7 @@ export const useFilters = (localStorePrefix = 'models-list-page') => { }); }); - usersData?.forEach(({ username }) => { + usersData?.data?.forEach(({ username }) => { options.push({ propertyKey: filterKeys.USER_NAME, value: username, diff --git a/frontend/src/pages/Project/List/index.tsx b/frontend/src/pages/Project/List/index.tsx index af4afcf5dc..eb896df91d 100644 --- a/frontend/src/pages/Project/List/index.tsx +++ b/frontend/src/pages/Project/List/index.tsx @@ -1,44 +1,36 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { get as _get } from 'lodash'; - -import { - Button, - ButtonWithConfirmation, - Header, - ListEmptyMessage, - Pagination, - SpaceBetween, - Table, - TextFilter, -} from 'components'; - -import { useBreadcrumbs, useCollection } from 'hooks'; + +import { Button, ButtonWithConfirmation, Header, ListEmptyMessage, Loader, SpaceBetween, Table, TextFilter } from 'components'; + +import { DEFAULT_TABLE_PAGE_SIZE } from 'consts'; +import { useBreadcrumbs, useCollection, useInfiniteScroll } from 'hooks'; import { ROUTES } from 'routes'; -import { useGetProjectsQuery } from 'services/project'; +import { useLazyGetProjectsQuery } from 'services/project'; import { useCheckAvailableProjectPermission } from '../hooks/useCheckAvailableProjectPermission'; import { useDeleteProject } from '../hooks/useDeleteProject'; import { useColumnsDefinitions } from './hooks'; -const SEARCHABLE_COLUMNS = ['project_name', 'owner.username']; - export const ProjectList: React.FC = () => { const { t } = useTranslation(); - const { isLoading, isFetching, data, refetch } = useGetProjectsQuery(); const { isAvailableDeletingPermission, isAvailableProjectManaging } = useCheckAvailableProjectPermission(); const { deleteProject, deleteProjects, isDeleting } = useDeleteProject(); + const [filteringText, setFilteringText] = useState(''); + const [namePattern, setNamePattern] = useState(''); const navigate = useNavigate(); - const sortedData = useMemo(() => { - if (!data) return []; + const { data, isLoading, refreshList, isLoadingMore, totalCount } = useInfiniteScroll({ + useLazyQuery: useLazyGetProjectsQuery, + args: { name_pattern: namePattern, limit: DEFAULT_TABLE_PAGE_SIZE }, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return [...data].sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - }, [data]); + getPaginationParams: (lastProject) => ({ + prev_created_at: lastProject.created_at, + prev_id: lastProject.project_id, + }), + }); useBreadcrumbs([ { @@ -51,7 +43,24 @@ export const ProjectList: React.FC = () => { navigate(ROUTES.PROJECT.ADD); }; + const onClearFilter = () => { + setNamePattern(''); + setFilteringText(''); + }; + const renderEmptyMessage = (): React.ReactNode => { + if (isLoading) { + return null; + } + + if (filteringText) { + return ( + + + + ); + } + return ( {isAvailableProjectManaging && } @@ -59,28 +68,10 @@ export const ProjectList: React.FC = () => { ); }; - const renderNoMatchMessage = (onClearFilter: () => void): React.ReactNode => { - return ( - - - - ); - }; - - const { items, actions, filteredItemsCount, collectionProps, filterProps, paginationProps } = useCollection(sortedData, { + const { items, collectionProps } = useCollection(data, { filtering: { empty: renderEmptyMessage(), - noMatch: renderNoMatchMessage(() => actions.setFiltering('')), - - filteringFunction: (projectItem: IProject, filteringText) => { - const filteringTextLowerCase = filteringText.toLowerCase(); - - return SEARCHABLE_COLUMNS.map((key) => _get(projectItem, key)).some( - (value) => typeof value === 'string' && value.trim().toLowerCase().indexOf(filteringTextLowerCase) > -1, - ); - }, }, - pagination: { pageSize: 20 }, selection: {}, }); @@ -104,9 +95,9 @@ export const ProjectList: React.FC = () => { }); const renderCounter = () => { - if (!data?.length) return ''; + if (typeof totalCount !== 'number') return ''; - return `(${data.length})`; + return `(${totalCount})`; }; return ( @@ -116,7 +107,7 @@ export const ProjectList: React.FC = () => { variant="full-page" columnDefinitions={columns} items={items} - loading={isLoading || isFetching} + loading={isLoading} loadingText={t('common.loading')} selectionType={isAvailableProjectManaging ? 'multi' : undefined} stickyHeader={true} @@ -141,9 +132,9 @@ export const ProjectList: React.FC = () => { + + ); + } + return ( @@ -91,22 +76,10 @@ export const UserList: React.FC = () => { ); }; - const renderNoMatchMessage = (onClearFilter: () => void): React.ReactNode => { - return ( - - - - ); - }; - - const { items, actions, filteredItemsCount, collectionProps, filterProps, paginationProps } = useCollection(sortedData, { + const { items, actions, collectionProps } = useCollection(data, { filtering: { empty: renderEmptyMessage(), - noMatch: renderNoMatchMessage(() => actions.setFiltering('')), - filteringFunction: (user, filteringText) => - includeSubString(user.username, filteringText) || includeSubString(user.email ?? '', filteringText), }, - pagination: { pageSize: 20 }, selection: {}, }); @@ -145,13 +118,9 @@ export const UserList: React.FC = () => { }, [collectionProps.selectedItems]); const renderCounter = () => { - const { selectedItems } = collectionProps; - - if (!data?.length) return ''; - - if (selectedItems?.length) return `(${selectedItems?.length}/${data?.length ?? 0})`; + if (typeof totalCount !== 'number') return ''; - return `(${data.length})`; + return `(${totalCount})`; }; return ( @@ -160,9 +129,11 @@ export const UserList: React.FC = () => { {...collectionProps} variant="full-page" isItemDisabled={getIsTableItemDisabled} - columnDefinitions={COLUMN_DEFINITIONS} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + columnDefinitions={columns} items={items} - loading={isLoading || isFetching} + loading={isLoading} loadingText={t('common.loading')} selectionType="multi" stickyHeader={true} @@ -186,9 +157,9 @@ export const UserList: React.FC = () => {