From b4e6aacb84a1e439e9f5e80c02ff4985c7710e0c Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Tue, 6 Jul 2021 14:10:39 +0300 Subject: [PATCH] Fixing scroll to bottom in pod logs Signed-off-by: Alex Andreev --- .../dock/__test__/to-bottom.test.tsx | 53 +++++++++++++ src/renderer/components/dock/log-list.scss | 13 ---- src/renderer/components/dock/log-list.tsx | 78 +++++++------------ src/renderer/components/dock/to-bottom.tsx | 38 +++++++++ 4 files changed, 120 insertions(+), 62 deletions(-) create mode 100644 src/renderer/components/dock/__test__/to-bottom.test.tsx create mode 100644 src/renderer/components/dock/to-bottom.tsx diff --git a/src/renderer/components/dock/__test__/to-bottom.test.tsx b/src/renderer/components/dock/__test__/to-bottom.test.tsx new file mode 100644 index 0000000000..c19a7b19e3 --- /dev/null +++ b/src/renderer/components/dock/__test__/to-bottom.test.tsx @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { fireEvent, render } from "@testing-library/react"; +import { ToBottom } from "../to-bottom"; +import { noop } from "../../../utils"; + +describe("", () => { + it("renders w/o errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("has 'To bottom' label", () => { + const { getByText } = render(); + + expect(getByText("To bottom")).toBeInTheDocument(); + }); + + it("has a arrow down icon", () => { + const { getByText } = render(); + + expect(getByText("expand_more")).toBeInTheDocument(); + }); + + it("fires an onclick event", () => { + const callback = jest.fn(); + const { getByText } = render(); + + fireEvent.click(getByText("To bottom")); + expect(callback).toBeCalled(); + }); +}); diff --git a/src/renderer/components/dock/log-list.scss b/src/renderer/components/dock/log-list.scss index edae55ad61..3bf5bfd833 100644 --- a/src/renderer/components/dock/log-list.scss +++ b/src/renderer/components/dock/log-list.scss @@ -84,17 +84,4 @@ overflow-x: hidden!important; // fixing scroll to bottom issues in PodLogs } } - - .JumpToBottom { - position: absolute; - right: 30px; - padding: 4px 9px; - border-radius: 20px; - z-index: 2; - top: 20px; - - .Icon { - --size: calc(var(--unit) * 2); - } - } } diff --git a/src/renderer/components/dock/log-list.tsx b/src/renderer/components/dock/log-list.tsx index 9437a854e2..5cefdd5508 100644 --- a/src/renderer/components/dock/log-list.tsx +++ b/src/renderer/components/dock/log-list.tsx @@ -25,20 +25,19 @@ import React from "react"; import AnsiUp from "ansi_up"; import DOMPurify from "dompurify"; import debounce from "lodash/debounce"; -import { action, computed, observable, makeObservable } from "mobx"; -import { observer } from "mobx-react"; +import { action, computed, observable, makeObservable, reaction } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; import moment from "moment-timezone"; import type { Align, ListOnScrollProps } from "react-window"; import { SearchStore, searchStore } from "../../../common/search-store"; import { UserStore } from "../../../common/user-store"; -import { cssNames } from "../../utils"; -import { Button } from "../button"; -import { Icon } from "../icon"; +import { boundMethod, cssNames } from "../../utils"; import { Spinner } from "../spinner"; import { VirtualList } from "../virtual-list"; import { logStore } from "./log.store"; import { logTabStore } from "./log-tab.store"; +import { ToBottom } from "./to-bottom"; interface Props { logs: string[] @@ -64,41 +63,44 @@ export class LogList extends React.Component { } componentDidMount() { - this.scrollToBottom(); + disposeOnUnmount(this, [ + reaction(() => this.props.logs, this.onLogsInitialLoad), + reaction(() => this.props.logs, this.onLogsUpdate), + reaction(() => this.props.logs, this.onUserScrolledUp) + ]); } - componentDidUpdate(prevProps: Props) { - const { logs, id } = this.props; - - if (id != prevProps.id) { + @boundMethod + onLogsInitialLoad(logs: string[], prevLogs: string[]) { + if (!prevLogs.length && logs.length) { this.isLastLineVisible = true; - - return; } + } - if (logs == prevProps.logs || !this.virtualListDiv.current) return; + @boundMethod + onLogsUpdate() { + if (this.isLastLineVisible) { + setTimeout(() => { + this.scrollToBottom(); + }, 500); // Giving some time to VirtualList to prepare its outerRef (this.virtualListDiv) element + } + } - const newLogsLoaded = prevProps.logs.length < logs.length; + @boundMethod + onUserScrolledUp(logs: string[], prevLogs: string[]) { + if (!this.virtualListDiv.current) return; + + const newLogsAdded = prevLogs.length < logs.length; const scrolledToBeginning = this.virtualListDiv.current.scrollTop === 0; - if (this.isLastLineVisible || prevProps.logs.length == 0) { - this.scrollToBottom(); // Scroll down to keep user watching/reading experience - - return; - } - - if (scrolledToBeginning && newLogsLoaded) { - const firstLineContents = prevProps.logs[0]; + if (newLogsAdded && scrolledToBeginning) { + const firstLineContents = prevLogs[0]; const lineToScroll = this.props.logs.findIndex((value) => value == firstLineContents); if (lineToScroll !== -1) { this.scrollToItem(lineToScroll, "start"); } } - - if (!logs.length) { - this.isLastLineVisible = false; - } } /** @@ -158,7 +160,6 @@ export class LogList extends React.Component { } }; - @action scrollToBottom = () => { if (!this.virtualListDiv.current) return; this.virtualListDiv.current.scrollTop = this.virtualListDiv.current.scrollHeight; @@ -169,7 +170,6 @@ export class LogList extends React.Component { }; onScroll = (props: ListOnScrollProps) => { - if (!this.virtualListDiv.current) return; this.isLastLineVisible = false; this.onScrollDebounced(props); }; @@ -264,29 +264,9 @@ export class LogList extends React.Component { className="box grow" /> {this.isJumpButtonVisible && ( - + )} ); } } - -interface JumpToBottomProps { - onClick: () => void -} - -const JumpToBottom = ({ onClick }: JumpToBottomProps) => { - return ( - - ); -}; diff --git a/src/renderer/components/dock/to-bottom.tsx b/src/renderer/components/dock/to-bottom.tsx new file mode 100644 index 0000000000..f95ff6bebc --- /dev/null +++ b/src/renderer/components/dock/to-bottom.tsx @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import React from "react"; +import { Icon } from "../icon"; + +export function ToBottom({ onClick }: { onClick: () => void }) { + return ( + + ); +}