From d416b90bae7151f3f8a8c5e1a76aaadcf54d86e5 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Tue, 27 Jan 2026 14:00:29 +0300 Subject: [PATCH 1/4] Switch UI to pagination-based projects and users API #3490 --- .../App/Login/LoginByGithubCallback/index.tsx | 2 +- .../useCheckingForFleetsInProjectsOfMember.ts | 9 +- frontend/src/hooks/useProjectFilter.ts | 2 +- .../layouts/AppLayout/TutorialPanel/hooks.ts | 2 +- .../src/pages/Events/List/hooks/useFilters.ts | 4 +- frontend/src/pages/Models/List/hooks.tsx | 2 +- frontend/src/pages/Project/List/index.tsx | 101 ++++++-------- .../Members/UsersAutosuggest/index.tsx | 2 +- frontend/src/pages/Project/Members/index.tsx | 2 +- .../hooks/useGetRunSpecFromYaml.ts | 1 + .../pages/Runs/CreateDevEnvironment/index.tsx | 1 + .../src/pages/Runs/List/hooks/useFilters.ts | 2 +- .../src/pages/User/Details/Projects/index.tsx | 2 +- frontend/src/pages/User/List/hooks.tsx | 39 ++++++ frontend/src/pages/User/List/index.tsx | 129 +++++++----------- frontend/src/services/project.ts | 7 +- frontend/src/services/user.ts | 6 +- frontend/src/types/project.d.ts | 5 +- frontend/src/types/user.d.ts | 5 + frontend/webpack/dev.js | 2 +- 20 files changed, 164 insertions(+), 161 deletions(-) create mode 100644 frontend/src/pages/User/List/hooks.tsx 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..470b4ab73b 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(); diff --git a/frontend/src/hooks/useProjectFilter.ts b/frontend/src/hooks/useProjectFilter.ts index b4573b1f8e..cf8cb22b6e 100644 --- a/frontend/src/hooks/useProjectFilter.ts +++ b/frontend/src/hooks/useProjectFilter.ts @@ -16,7 +16,7 @@ export const useProjectFilter = ({ localStorePrefix }: Args) => { null, ); - const { data: projectsData } = useGetProjectsQuery(); + const { data: projectsData } = useGetProjectsQuery({}); const projectOptions = useMemo(() => { if (!projectsData?.length) return []; diff --git a/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts b/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts index d3a465fcb5..e5a5aaa973 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, }); diff --git a/frontend/src/pages/Events/List/hooks/useFilters.ts b/frontend/src/pages/Events/List/hooks/useFilters.ts index a3d510718f..8c0895b0a4 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 }), diff --git a/frontend/src/pages/Models/List/hooks.tsx b/frontend/src/pages/Models/List/hooks.tsx index 912875b394..23602955b7 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 }), diff --git a/frontend/src/pages/Project/List/index.tsx b/frontend/src/pages/Project/List/index.tsx index af4afcf5dc..f9fa62e8dd 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 } = 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: {}, }); @@ -103,12 +94,6 @@ export const ProjectList: React.FC = () => { onDeleteClick: isAvailableProjectManaging ? deleteProject : undefined, }); - const renderCounter = () => { - if (!data?.length) return ''; - - return `(${data.length})`; - }; - return ( <> { variant="full-page" columnDefinitions={columns} items={items} - loading={isLoading || isFetching} + loading={isLoading} loadingText={t('common.loading')} selectionType={isAvailableProjectManaging ? 'multi' : undefined} stickyHeader={true} header={
@@ -141,9 +125,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: {}, }); @@ -144,32 +117,23 @@ export const UserList: React.FC = () => { return isDeleting || collectionProps.selectedItems?.length !== 1 || userGlobalRole !== 'admin'; }, [collectionProps.selectedItems]); - const renderCounter = () => { - const { selectedItems } = collectionProps; - - if (!data?.length) return ''; - - if (selectedItems?.length) return `(${selectedItems?.length}/${data?.length ?? 0})`; - - return `(${data.length})`; - }; - return ( <>
{ header={
diff --git a/frontend/src/pages/Project/Members/UsersAutosuggest/index.tsx b/frontend/src/pages/Project/Members/UsersAutosuggest/index.tsx index 01288bc9b3..7cd2dd7541 100644 --- a/frontend/src/pages/Project/Members/UsersAutosuggest/index.tsx +++ b/frontend/src/pages/Project/Members/UsersAutosuggest/index.tsx @@ -19,7 +19,7 @@ export const UserAutosuggest: React.FC = ({ optionsFilter, onSelect: onSe const options: AutosuggestOption[] = useMemo(() => { if (!usersData) return []; - return usersData.map((user) => ({ + return usersData?.data.map((user) => ({ value: user.username, })); }, [usersData]); diff --git a/frontend/src/pages/Project/Members/index.tsx b/frontend/src/pages/Project/Members/index.tsx index 818049e175..d68916c6d8 100644 --- a/frontend/src/pages/Project/Members/index.tsx +++ b/frontend/src/pages/Project/Members/index.tsx @@ -96,7 +96,7 @@ export const ProjectMembers: React.FC = ({ members, loading, onChange, r return; } - const selectedUser = usersData?.find((u) => u.username === username); + const selectedUser = usersData?.data?.find((u) => u.username === username); if (selectedUser) { append({ diff --git a/frontend/src/pages/Runs/List/hooks/useFilters.ts b/frontend/src/pages/Runs/List/hooks/useFilters.ts index db2bdcb530..82f1ca40bd 100644 --- a/frontend/src/pages/Runs/List/hooks/useFilters.ts +++ b/frontend/src/pages/Runs/List/hooks/useFilters.ts @@ -46,7 +46,7 @@ export const useFilters = ({ localStorePrefix }: Args) => { }); }); - usersData?.forEach(({ username }) => { + usersData?.data?.forEach(({ username }) => { options.push({ propertyKey: filterKeys.USER_NAME, value: username, diff --git a/frontend/src/pages/User/Details/Projects/index.tsx b/frontend/src/pages/User/Details/Projects/index.tsx index 71ba122d97..2cb9885ab3 100644 --- a/frontend/src/pages/User/Details/Projects/index.tsx +++ b/frontend/src/pages/User/Details/Projects/index.tsx @@ -41,11 +41,10 @@ export const UserProjectList: React.FC = () => { }; const filteredData = useMemo(() => { - if (!data) return []; + if (!data?.data) return []; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return [...data] + // eslint-disable-next-line no-unsafe-optional-chaining + return [...data?.data] .filter((p) => p.owner.username === paramUserName) .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); }, [data]); diff --git a/frontend/src/pages/User/List/index.tsx b/frontend/src/pages/User/List/index.tsx index 368b8dfe94..66d5e2e46f 100644 --- a/frontend/src/pages/User/List/index.tsx +++ b/frontend/src/pages/User/List/index.tsx @@ -25,7 +25,7 @@ export const UserList: React.FC = () => { const navigate = useNavigate(); const [pushNotification] = useNotifications(); - const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll({ + const { data, isLoading, refreshList, isLoadingMore, totalCount } = useInfiniteScroll({ useLazyQuery: useLazyGetUserListQuery, args: { name_pattern: namePattern, limit: DEFAULT_TABLE_PAGE_SIZE }, @@ -117,6 +117,12 @@ export const UserList: React.FC = () => { return isDeleting || collectionProps.selectedItems?.length !== 1 || userGlobalRole !== 'admin'; }, [collectionProps.selectedItems]); + const renderCounter = () => { + if (typeof totalCount !== 'number') return ''; + + return `(${totalCount})`; + }; + return ( <>
{ header={