Skip to content

Commit

Permalink
Monitor item count
Browse files Browse the repository at this point in the history
Signed-off-by: Radoslaw Szwajkowski <rszwajko@redhat.com>
  • Loading branch information
rszwajko committed Aug 30, 2024
1 parent 16c90ea commit ef89747
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 82 deletions.
16 changes: 0 additions & 16 deletions client/src/app/components/InfiniteScroller/InfiniteScroller.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
56 changes: 31 additions & 25 deletions client/src/app/components/InfiniteScroller/InfiniteScroller.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={className}>
<div style={{ position: "relative" }}>
<div ref={visibleZoneRef} className={"infinite-scroll-visible-zone"} />
<div ref={hiddenZoneRef} className={"infinite-scroll-hidden-zone"} />
</div>

<div>
{children}
{hasMore && (
<div ref={sentinelRef} className={"infinite-scroll-sentinel"}>
<div ref={sentinelRef} className="infinite-scroll-sentinel">
{t("message.loadingTripleDot")}
</div>
)}
Expand Down
54 changes: 22 additions & 32 deletions client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const visibleZoneRef = useRef<HTMLDivElement>(null);
const hiddenZoneRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
const nodeRef = useRef<HTMLDivElement>(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 };
}
25 changes: 16 additions & 9 deletions client/src/app/components/task-manager/TaskManagerDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@ import {
Dropdown,
DropdownItem,
DropdownList,
EmptyState,
EmptyStateActions,
EmptyStateBody,
EmptyStateFooter,
EmptyStateHeader,
EmptyStateIcon,
EmptyStateVariant,
MenuToggle,
MenuToggleElement,
NotificationDrawer,
Expand All @@ -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";
Expand Down Expand Up @@ -81,6 +81,7 @@ export const TaskManagerDrawer: React.FC<TaskManagerDrawerProps> = forwardRef(
setExpandedItems([]);
};

console.log("tasks", tasks?.length);
return (
<NotificationDrawer ref={ref}>
<NotificationDrawerHeader
Expand Down Expand Up @@ -113,7 +114,7 @@ export const TaskManagerDrawer: React.FC<TaskManagerDrawerProps> = forwardRef(
<InfiniteScroller
fetchMore={fetchNextPage}
hasMore={hasNextPage}
isReadyToFetch={isReadyToFetch}
itemCount={tasks?.length ?? 0}
>
<NotificationDrawerList>
{tasks.map((task) => (
Expand Down Expand Up @@ -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]);

Expand Down
2 changes: 2 additions & 0 deletions client/src/app/queries/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) =>
Expand Down

0 comments on commit ef89747

Please sign in to comment.