diff --git a/client/src/app/components/InfiniteScroller/InfiniteScroller.css b/client/src/app/components/InfiniteScroller/InfiniteScroller.css index cd387bb2f..6d8316199 100644 --- a/client/src/app/components/InfiniteScroller/InfiniteScroller.css +++ b/client/src/app/components/InfiniteScroller/InfiniteScroller.css @@ -4,19 +4,3 @@ font-style: italic; font-weight: bold; } - -.infinite-scroll-visible-zone { - position: fixed; - top: 0; - left: 0; - bottom: 10px; - width: 100%; -} - -.infinite-scroll-hidden-zone { - position: fixed; - bottom: 0; - left: 0; - width: 100%; - height: 10px; -} diff --git a/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx b/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx index ebec58626..972c3b87d 100644 --- a/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx +++ b/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx @@ -1,50 +1,56 @@ -import React, { ReactNode, useEffect } from "react"; +import React, { ReactNode, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useVisibilityTracker } from "./useVisibilityTracker"; import "./InfiniteScroller.css"; export interface InfiniteScrollerProps { children: ReactNode; - className?: string; - fetchMore: () => void; + fetchMore: () => boolean; hasMore: boolean; - isReadyToFetch: boolean; + itemCount: number; } export const InfiniteScroller = ({ children, - className, fetchMore, hasMore, - isReadyToFetch, + itemCount, }: InfiniteScrollerProps) => { const { t } = useTranslation(); - const { - isVisible, - loaderRef: sentinelRef, - visibleZoneRef, - hiddenZoneRef, - } = useVisibilityTracker({ - enable: hasMore, - }); + // track how many items were known at time of triggering the fetch + // parent is expected to display empty state until some items are available + // initializing with zero ensures that the effect will be triggered immediately + const itemCountRef = useRef(0); + const { visible: isSentinelVisible, nodeRef: sentinelRef } = + useVisibilityTracker({ + enable: hasMore, + }); + console.log("infinite props ", hasMore, itemCount, itemCountRef.current); useEffect(() => { - if (isVisible && isReadyToFetch) { - fetchMore(); + console.log( + `infinite [visible= >${isSentinelVisible}<] `, + itemCount, + itemCountRef.current + ); + if ( + isSentinelVisible && + itemCountRef.current !== itemCount && + fetchMore() + ) { + itemCountRef.current = itemCount; + } else if (isSentinelVisible && itemCountRef.current === itemCount) { + // network call may fail which would block fetching + // TODO: implement reset based on hit counter i.e. + // if (hitCounter > maxHits) itemCountRef.current = 0 } - console.log("infinite", isVisible); - }, [isVisible, fetchMore]); + }, [isSentinelVisible, fetchMore, itemCount]); return ( -
-
-
-
-
- +
{children} {hasMore && ( -
+
{t("message.loadingTripleDot")}
)} diff --git a/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx b/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx index dc1ab1579..81e0cd933 100644 --- a/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx +++ b/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx @@ -1,50 +1,40 @@ import { useEffect, useRef, useState } from "react"; -const intersectionCallback = - (stateCallback: () => void) => (entries: IntersectionObserverEntry[]) => { - entries.forEach(({ isIntersecting }) => { - if (isIntersecting) { - stateCallback(); - } - }); - }; - export function useVisibilityTracker({ enable }: { enable: boolean }) { - const loaderRef = useRef(null); - const visibleZoneRef = useRef(null); - const hiddenZoneRef = useRef(null); - const [isVisible, setIsVisible] = useState(false); + const nodeRef = useRef(null); + const [visible, setVisible] = useState(false); + const node = nodeRef.current; useEffect(() => { - if ( - !enable || - !loaderRef.current || - !visibleZoneRef.current || - !hiddenZoneRef.current - ) { + if (!enable || !node) { console.log("useVisibilityTracker - disabled"); return undefined; } - const visibleZoneObserver = new IntersectionObserver( - intersectionCallback(() => setIsVisible(true)), - { root: visibleZoneRef.current, rootMargin: "0px", threshold: 1.0 } - ); - const hiddenZoneObserver = new IntersectionObserver( - intersectionCallback(() => setIsVisible(false)), - { root: hiddenZoneRef.current, rootMargin: "0px", threshold: 1.0 } + // observer with default options - the whole view port used + // using a parent is hard + const observer = new IntersectionObserver( + (entries: IntersectionObserverEntry[]) => + entries.forEach(({ isIntersecting, ...rest }) => { + if (isIntersecting) { + setVisible(true); + console.log("useVisibilityTracker - intersection", rest); + } else { + setVisible(false); + console.log("useVisibilityTracker - out-of-box", rest); + } + }) ); - visibleZoneObserver.observe(loaderRef.current); - hiddenZoneObserver.observe(loaderRef.current); + observer.observe(node); console.log("useVisibilityTracker - observe"); return () => { - visibleZoneObserver.disconnect(); - hiddenZoneObserver.disconnect(); + observer.disconnect(); + setVisible(false); console.log("useVisibilityTracker - disconnect"); }; - }, [enable]); + }, [enable, node]); - return { isVisible, loaderRef, visibleZoneRef, hiddenZoneRef }; + return { visible, nodeRef }; } diff --git a/client/src/app/components/task-manager/TaskManagerDrawer.tsx b/client/src/app/components/task-manager/TaskManagerDrawer.tsx index ef0e61096..f4489fe28 100644 --- a/client/src/app/components/task-manager/TaskManagerDrawer.tsx +++ b/client/src/app/components/task-manager/TaskManagerDrawer.tsx @@ -5,13 +5,6 @@ import { Dropdown, DropdownItem, DropdownList, - EmptyState, - EmptyStateActions, - EmptyStateBody, - EmptyStateFooter, - EmptyStateHeader, - EmptyStateIcon, - EmptyStateVariant, MenuToggle, MenuToggleElement, NotificationDrawer, @@ -22,8 +15,15 @@ import { NotificationDrawerListItemBody, NotificationDrawerListItemHeader, Tooltip, + EmptyState, + EmptyStateHeader, + EmptyStateIcon, + EmptyStateBody, + EmptyStateVariant, + EmptyStateFooter, + EmptyStateActions, } from "@patternfly/react-core"; -import { CubesIcon, EllipsisVIcon } from "@patternfly/react-icons"; +import { EllipsisVIcon, CubesIcon } from "@patternfly/react-icons"; import { css } from "@patternfly/react-styles"; import { Task, TaskState } from "@app/api/models"; @@ -81,6 +81,7 @@ export const TaskManagerDrawer: React.FC = forwardRef( setExpandedItems([]); }; + console.log("tasks", tasks?.length); return ( = forwardRef( {tasks.map((task) => ( @@ -291,8 +292,14 @@ const useTaskManagerData = () => { ); const fetchMore = useCallback(() => { + // forced fetch is not allowed when background fetch or other forced fetch is in progress if (!isFetching && !isFetchingNextPage) { fetchNextPage(); + console.log("fetchMore - started"); + return true; + } else { + console.log("fetchMore - blocked"); + return false; } }, [isFetching, isFetchingNextPage, fetchNextPage]); diff --git a/client/src/app/queries/tasks.ts b/client/src/app/queries/tasks.ts index 036314975..81b70ef4d 100644 --- a/client/src/app/queries/tasks.ts +++ b/client/src/app/queries/tasks.ts @@ -94,6 +94,8 @@ export const useInfiniteServerTasks = ( refetchInterval?: number ) => { return useInfiniteQuery({ + // usually the params are part of the key + // infinite query tracks the actual params for all pages under one key // eslint-disable-next-line @tanstack/query/exhaustive-deps queryKey: [TasksQueryKey], queryFn: async ({ pageParam = initialParams }) =>