diff --git a/src/renderer/components/dock/logs/log-list.module.scss b/src/renderer/components/dock/logs/log-list.module.scss index 459262ba41..a72072f40a 100644 --- a/src/renderer/components/dock/logs/log-list.module.scss +++ b/src/renderer/components/dock/logs/log-list.module.scss @@ -29,6 +29,14 @@ } } +.firstLine { + width: 100%; + height: 1px; + top: 0; + background-color: var(--logsBackground); + position: absolute; +} + .lastLine { width: 100%; height: 1px; diff --git a/src/renderer/components/dock/logs/log-list.tsx b/src/renderer/components/dock/logs/log-list.tsx index 3bf75b7371..d170da5c07 100644 --- a/src/renderer/components/dock/logs/log-list.tsx +++ b/src/renderer/components/dock/logs/log-list.tsx @@ -5,18 +5,19 @@ import styles from "./log-list.module.scss"; -import throttle from "lodash/throttle"; import { useVirtualizer } from '@tanstack/react-virtual'; import { observer } from 'mobx-react'; -import React, { useEffect, useRef } from 'react'; -import type { LogTabViewModel } from './logs-view-model'; -import { LogRow } from "./log-row"; +import React, { useRef } from 'react'; import { cssNames } from "../../../utils"; -import { v4 as getRandomId } from "uuid"; -import { useJumpToBottomButton } from "./use-scroll-to-bottom"; -import { useInitialScrollToBottom } from "./use-initial-scroll-to-bottom"; +import { LogRow } from "./log-row"; +import type { LogTabViewModel } from './logs-view-model'; import { ToBottom } from "./to-bottom"; -import useIntersectionObserver from "../../../hooks/useIntersectionObserver"; +import { useInitialScrollToBottom } from "./use-initial-scroll-to-bottom"; +import { useOnScrollTop } from "./use-on-scroll-top"; +import { useRefreshListOnDataChange } from "./use-refresh-list-on-data-change"; +import { useScrollOnSearch } from "./use-scroll-on-search"; +import { useJumpToBottomButton } from "./use-scroll-to-bottom"; +import { useStickToBottomOnLogsLoad } from "./use-stick-to-bottom-on-logs-load"; export interface LogListProps { model: LogTabViewModel; @@ -25,10 +26,9 @@ export interface LogListProps { export const LogList = observer(({ model }: LogListProps) => { const { visibleLogs } = model; const parentRef = useRef(null); - const lastLineRef = useRef(null); - const [rowKeySuffix, setRowKeySuffix] = React.useState(getRandomId()); + const topLineRef = useRef(null); + const bottomLineRef = useRef(null); const [toBottomVisible, setButtonVisibility] = useJumpToBottomButton(parentRef.current); - const entry = useIntersectionObserver(lastLineRef.current, {}); const rowVirtualizer = useVirtualizer({ count: visibleLogs.get().length, @@ -45,50 +45,21 @@ export const LogList = observer(({ model }: LogListProps) => { scrollTo(visibleLogs.get().length - 1); } - const onScroll = throttle(() => { + const onScroll = () => { if (!parentRef.current) return; setButtonVisibility(); - onScrollToTop(); - }, 1_000, { trailing: true, leading: true }); - - /** - * Loads new logs if user scrolled to the top - */ - const onScrollToTop = async () => { - const { scrollTop } = parentRef.current as HTMLDivElement; - - if (scrollTop === 0) { - const logs = model.logs.get(); - const firstLog = logs[0]; - - await model.loadLogs(); - - const scrollToIndex = model.logs.get().findIndex(log => log === firstLog); - - scrollTo(scrollToIndex); - } }; useInitialScrollToBottom(model, scrollToBottom); - useEffect(() => { - // rowVirtualizer.scrollToIndex(visibleLogs.get().length - 1, { align: 'end', smoothScroll: false }); - // Refresh list - setRowKeySuffix(getRandomId()); - }, [model.logTabData.get()]); + const uniqRowKey = useRefreshListOnDataChange(model.logTabData.get()); - useEffect(() => { - if (!model.searchStore.occurrences.length) return; + useScrollOnSearch(model.searchStore, scrollTo); - scrollTo(model.searchStore.occurrences[model.searchStore.activeOverlayIndex]); - }, [model.searchStore.searchQuery, model.searchStore.activeOverlayIndex]) + useStickToBottomOnLogsLoad({ bottomLineRef, model, scrollToBottom }); - useEffect(() => { - if (entry?.isIntersecting) { - scrollToBottom(); - } - }, [model.visibleLogs.get().length]); + useOnScrollTop({ topLineRef, model, scrollTo }); return (
{ }} className={styles.virtualizer} > +
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
{
))} -
+
{toBottomVisible && ( diff --git a/src/renderer/components/dock/logs/use-on-scroll-top.ts b/src/renderer/components/dock/logs/use-on-scroll-top.ts new file mode 100644 index 0000000000..1452b0791a --- /dev/null +++ b/src/renderer/components/dock/logs/use-on-scroll-top.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { RefObject } from "react"; +import { useEffect } from "react"; +import useIntersectionObserver from "../../../hooks/useIntersectionObserver"; +import type { LogTabViewModel } from "./logs-view-model"; + +interface UseStickToBottomProps { + topLineRef: RefObject; + model: LogTabViewModel; + scrollTo: (index: number) => void; +} + +export function useOnScrollTop({ topLineRef, model, scrollTo }: UseStickToBottomProps) { + const topLineEntry = useIntersectionObserver(topLineRef.current, {}); + + function getPreviouslyFirstLogIndex(firstLog: string) { + return model.logs.get().findIndex(log => log === firstLog); + } + + async function onScrolledTop() { + const firstLog = model.logs.get()[0]; + const scrollIndex = () => getPreviouslyFirstLogIndex(firstLog); + + await model.loadLogs(); + scrollTo(scrollIndex()); + } + + useEffect(() => { + if (topLineEntry?.isIntersecting) { + onScrolledTop(); + } + }, [topLineEntry?.isIntersecting]); +} diff --git a/src/renderer/components/dock/logs/use-refresh-list-on-data-change.ts b/src/renderer/components/dock/logs/use-refresh-list-on-data-change.ts new file mode 100644 index 0000000000..983b83a323 --- /dev/null +++ b/src/renderer/components/dock/logs/use-refresh-list-on-data-change.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { useEffect, useState } from "react"; +import type { LogTabData } from "./tab-store"; +import { v4 as getRandomId } from "uuid"; + +export function useRefreshListOnDataChange(data: LogTabData | undefined) { + const [rowKeySuffix, setRowKeySuffix] = useState(getRandomId()); + + useEffect(() => { + // Refresh virtualizer list rows by changing their keys + setRowKeySuffix(getRandomId()); + }, [data]); + + return rowKeySuffix; +} diff --git a/src/renderer/components/dock/logs/use-scroll-on-search.ts b/src/renderer/components/dock/logs/use-scroll-on-search.ts new file mode 100644 index 0000000000..8d6d0ad9ff --- /dev/null +++ b/src/renderer/components/dock/logs/use-scroll-on-search.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { useEffect } from "react"; +import type { SearchStore } from "../../../search-store/search-store"; + +export function useScrollOnSearch(store: SearchStore, scrollTo: (index: number) => void) { + const { occurrences, searchQuery, activeOverlayIndex } = store; + + useEffect(() => { + if (!occurrences.length) return; + + scrollTo(occurrences[activeOverlayIndex]); + }, [searchQuery, activeOverlayIndex]); +} diff --git a/src/renderer/components/dock/logs/use-stick-to-bottom-on-logs-load.ts b/src/renderer/components/dock/logs/use-stick-to-bottom-on-logs-load.ts new file mode 100644 index 0000000000..3f2628f064 --- /dev/null +++ b/src/renderer/components/dock/logs/use-stick-to-bottom-on-logs-load.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { RefObject } from "react"; +import { useEffect } from "react"; +import useIntersectionObserver from "../../../hooks/useIntersectionObserver"; +import type { LogTabViewModel } from "./logs-view-model"; + +interface UseStickToBottomProps { + bottomLineRef: RefObject; + model: LogTabViewModel; + scrollToBottom: () => void; +} + +export function useStickToBottomOnLogsLoad({ bottomLineRef, model, scrollToBottom }: UseStickToBottomProps) { + const bottomLineEntry = useIntersectionObserver(bottomLineRef.current, {}); + + useEffect(() => { + if (bottomLineEntry?.isIntersecting) { + scrollToBottom(); + } + }, [model.visibleLogs.get().length]); +}