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..211e66f936 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;
- }
}
/**
@@ -114,7 +116,7 @@ export class LogList extends React.Component {
return this.props.logs
.map(log => logStore.splitOutTimestamp(log))
- .map(([logTimestamp, log]) => (`${moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`));
+ .map(([logTimestamp, log]) => (`${logTimestamp && moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`));
}
/**
@@ -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/log.store.ts b/src/renderer/components/dock/log.store.ts
index 91682e8c0c..710ed3a690 100644
--- a/src/renderer/components/dock/log.store.ts
+++ b/src/renderer/components/dock/log.store.ts
@@ -183,7 +183,7 @@ export class LogStore {
const extraction = /^(\d+\S+)(.*)/m.exec(logs);
if (!extraction || extraction.length < 3) {
- return ["", ""];
+ return ["", logs];
}
return [extraction[1], extraction[2]];
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 (
+
+ );
+}