1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fixing logs scrolling state (#1847)

* Passing raw logs to PodLogs child components

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Avoid autoscrolling while user is scrolling

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Removing status panel from log controls

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2020-12-23 11:51:43 +03:00 committed by GitHub
parent 90da642b8a
commit 507c485113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 95 additions and 59 deletions

View File

@ -27,6 +27,7 @@ interface OptionalProps {
showSubmitClose?: boolean;
showInlineInfo?: boolean;
showNotifications?: boolean;
showStatusPanel?: boolean;
}
@observer
@ -38,6 +39,7 @@ export class InfoPanel extends Component<Props> {
showSubmitClose: true,
showInlineInfo: true,
showNotifications: true,
showStatusPanel: true,
};
@observable error = "";
@ -93,7 +95,7 @@ export class InfoPanel extends Component<Props> {
}
render() {
const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose } = this.props;
const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose, showStatusPanel } = this.props;
const { submit, close, submitAndClose, waiting } = this;
const isDisabled = !!(disableSubmit || waiting || error);
@ -102,9 +104,11 @@ export class InfoPanel extends Component<Props> {
<div className="controls">
{controls}
</div>
<div className="info flex gaps align-center">
{showStatusPanel && (
<div className="flex gaps align-center">
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderErrorIcon()}
</div>
)}
{showButtons && (
<>
<Button plain label={<Trans>Cancel</Trans>} onClick={close} />

View File

@ -22,10 +22,9 @@ interface Props extends PodLogSearchProps {
}
export const PodLogControls = observer((props: Props) => {
const { tabData, save, reload, tabId, logs } = props;
const { tabData, save, reload, logs } = props;
const { selectedContainer, showTimestamps, previous } = tabData;
const rawLogs = podLogsStore.logs.get(tabId) || [];
const since = rawLogs.length ? podLogsStore.getTimestamps(rawLogs[0]) : null;
const since = logs.length ? podLogsStore.getTimestamps(logs[0]) : null;
const pod = new Pod(tabData.pod);
const toggleTimestamps = () => {
@ -39,8 +38,9 @@ export const PodLogControls = observer((props: Props) => {
const downloadLogs = () => {
const fileName = selectedContainer ? selectedContainer.name : pod.getName();
const logsToDownload = showTimestamps ? logs : podLogsStore.logsWithoutTimestamps;
saveFileDialog(`${fileName}.log`, logs.join("\n"), "text/plain");
saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
};
const onContainerChange = (option: SelectOption) => {
@ -118,7 +118,10 @@ export const PodLogControls = observer((props: Props) => {
tooltip={_i18n._(t`Save`)}
className="download-icon"
/>
<PodLogSearch {...props} />
<PodLogSearch
{...props}
logs={showTimestamps ? logs : podLogsStore.logsWithoutTimestamps}
/>
</div>
</div>
);

View File

@ -5,7 +5,7 @@ import AnsiUp from "ansi_up";
import DOMPurify from "dompurify";
import debounce from "lodash/debounce";
import { Trans } from "@lingui/macro";
import { action, observable } from "mobx";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Align, ListOnScrollProps } from "react-window";
@ -15,7 +15,7 @@ import { Button } from "../button";
import { Icon } from "../icon";
import { Spinner } from "../spinner";
import { VirtualList } from "../virtual-list";
import { logRange } from "./pod-logs.store";
import { podLogsStore } from "./pod-logs.store";
interface Props {
logs: string[]
@ -47,23 +47,25 @@ export class PodLogList extends React.Component<Props> {
return;
}
if (logs == prevProps.logs || !this.virtualListDiv.current) return;
const newLogsLoaded = prevProps.logs.length < logs.length;
const scrolledToBeginning = this.virtualListDiv.current.scrollTop === 0;
const fewLogsLoaded = logs.length < logRange;
if (this.isLastLineVisible) {
if (this.isLastLineVisible || prevProps.logs.length == 0) {
this.scrollToBottom(); // Scroll down to keep user watching/reading experience
return;
}
if (scrolledToBeginning && newLogsLoaded) {
this.virtualListDiv.current.scrollTop = (logs.length - prevProps.logs.length) * this.lineHeight;
}
const firstLineContents = prevProps.logs[0];
const lineToScroll = this.props.logs.findIndex((value) => value == firstLineContents);
if (fewLogsLoaded) {
this.isJumpButtonVisible = false;
if (lineToScroll !== -1) {
this.scrollToItem(lineToScroll, "start");
}
}
if (!logs.length) {
@ -71,6 +73,20 @@ export class PodLogList extends React.Component<Props> {
}
}
/**
* Returns logs with or without timestamps regarding to showTimestamps prop
*/
@computed
get logs() {
const showTimestamps = podLogsStore.getData(this.props.id).showTimestamps;
if (!showTimestamps) {
return podLogsStore.logsWithoutTimestamps;
}
return this.props.logs;
}
/**
* Checks if JumpToBottom button should be visible and sets its observable
* @param props Scrolling props from virtual list core
@ -115,7 +131,6 @@ export class PodLogList extends React.Component<Props> {
@action
scrollToBottom = () => {
if (!this.virtualListDiv.current) return;
this.isJumpButtonVisible = false;
this.virtualListDiv.current.scrollTop = this.virtualListDiv.current.scrollHeight;
};
@ -123,7 +138,13 @@ export class PodLogList extends React.Component<Props> {
this.virtualListRef.current.scrollToItem(index, align);
};
onScroll = debounce((props: ListOnScrollProps) => {
onScroll = (props: ListOnScrollProps) => {
if (!this.virtualListDiv.current) return;
this.isLastLineVisible = false;
this.onScrollDebounced(props);
};
onScrollDebounced = debounce((props: ListOnScrollProps) => {
if (!this.virtualListDiv.current) return;
this.setButtonVisibility(props);
this.setLastLineVisibility(props);
@ -137,7 +158,7 @@ export class PodLogList extends React.Component<Props> {
*/
getLogRow = (rowIndex: number) => {
const { searchQuery, isActiveOverlay } = searchStore;
const item = this.props.logs[rowIndex];
const item = this.logs[rowIndex];
const contents: React.ReactElement[] = [];
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi));
@ -179,15 +200,15 @@ export class PodLogList extends React.Component<Props> {
};
render() {
const { logs, isLoading } = this.props;
const isInitLoading = isLoading && !logs.length;
const rowHeights = new Array(logs.length).fill(this.lineHeight);
const { isLoading } = this.props;
const isInitLoading = isLoading && !this.logs.length;
const rowHeights = new Array(this.logs.length).fill(this.lineHeight);
if (isInitLoading) {
return <Spinner center/>;
}
if (!logs.length) {
if (!this.logs.length) {
return (
<div className="PodLogList flex box grow align-center justify-center">
<Trans>There are no logs available for container</Trans>
@ -198,7 +219,7 @@ export class PodLogList extends React.Component<Props> {
return (
<div className={cssNames("PodLogList flex", { isLoading })}>
<VirtualList
items={logs}
items={this.logs}
rowHeights={rowHeights}
getRow={this.getLogRow}
onScroll={this.onScroll}

View File

@ -12,10 +12,13 @@ export interface PodLogSearchProps {
onSearch: (query: string) => void
toPrevOverlay: () => void
toNextOverlay: () => void
}
interface Props extends PodLogSearchProps {
logs: string[]
}
export const PodLogSearch = observer((props: PodLogSearchProps) => {
export const PodLogSearch = observer((props: Props) => {
const { logs, onSearch, toPrevOverlay, toNextOverlay } = props;
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore;
const jumpDisabled = !searchQuery || !occurrences.length;

View File

@ -27,11 +27,11 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
private refresher = interval(10, () => {
const id = dockStore.selectedTabId;
if (!this.logs.get(id)) return;
if (!this.podLogs.get(id)) return;
this.loadMore(id);
});
@observable logs = observable.map<TabId, PodLogLine[]>();
@observable podLogs = observable.map<TabId, PodLogLine[]>();
@observable newLogSince = observable.map<TabId, string>(); // Timestamp after which all logs are considered to be new
constructor() {
@ -48,7 +48,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
}
}, { delay: 500 });
reaction(() => this.logs.get(dockStore.selectedTabId), () => {
reaction(() => this.podLogs.get(dockStore.selectedTabId), () => {
this.setNewLogSince(dockStore.selectedTabId);
});
@ -72,7 +72,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
});
this.refresher.start();
this.logs.set(tabId, logs);
this.podLogs.set(tabId, logs);
} catch ({error}) {
const message = [
_i18n._(t`Failed to load logs: ${error.message}`),
@ -80,7 +80,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
];
this.refresher.stop();
this.logs.set(tabId, message);
this.podLogs.set(tabId, message);
}
};
@ -91,14 +91,14 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
* @param tabId
*/
loadMore = async (tabId: TabId) => {
if (!this.logs.get(tabId).length) return;
const oldLogs = this.logs.get(tabId);
if (!this.podLogs.get(tabId).length) return;
const oldLogs = this.podLogs.get(tabId);
const logs = await this.loadLogs(tabId, {
sinceTime: this.getLastSinceTime(tabId)
});
// Add newly received logs to bottom
this.logs.set(tabId, [...oldLogs, ...logs]);
this.podLogs.set(tabId, [...oldLogs, ...logs]);
};
/**
@ -134,7 +134,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
* @param tabId
*/
setNewLogSince(tabId: TabId) {
if (!this.logs.has(tabId) || !this.logs.get(tabId).length || this.newLogSince.has(tabId)) return;
if (!this.podLogs.has(tabId) || !this.podLogs.get(tabId).length || this.newLogSince.has(tabId)) return;
const timestamp = this.getLastSinceTime(tabId);
this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string
@ -147,18 +147,38 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
@computed
get lines() {
const id = dockStore.selectedTabId;
const logs = this.logs.get(id);
const logs = this.podLogs.get(id);
return logs ? logs.length : 0;
}
/**
* Returns logs with timestamps for selected tab
*/
get logs() {
const id = dockStore.selectedTabId;
if (!this.podLogs.has(id)) return [];
return this.podLogs.get(id);
}
/**
* Removes timestamps from each log line and returns changed logs
* @returns Logs without timestamps
*/
get logsWithoutTimestamps() {
return this.logs.map(item => this.removeTimestamps(item));
}
/**
* It gets timestamps from all logs then returns last one + 1 second
* (this allows to avoid getting the last stamp in the selection)
* @param tabId
*/
getLastSinceTime(tabId: TabId) {
const logs = this.logs.get(tabId);
const logs = this.podLogs.get(tabId);
const timestamps = this.getTimestamps(logs[logs.length - 1]);
const stamp = new Date(timestamps ? timestamps[0] : null);
@ -176,7 +196,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
}
clearLogs(tabId: TabId) {
this.logs.delete(tabId);
this.podLogs.delete(tabId);
}
clearData(tabId: TabId) {

View File

@ -1,5 +1,5 @@
import React from "react";
import { computed, observable, reaction } from "mobx";
import { observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { searchStore } from "../../../common/search-store";
@ -79,31 +79,15 @@ export class PodLogs extends React.Component<Props> {
}, 100);
}
/**
* Computed prop which returns logs with or without timestamps added to each line
* @returns {Array} An array log items
*/
@computed
get logs(): string[] {
if (!podLogsStore.logs.has(this.tabId)) return [];
const logs = podLogsStore.logs.get(this.tabId);
const { getData, removeTimestamps } = podLogsStore;
const { showTimestamps } = getData(this.tabId);
if (!showTimestamps) {
return logs.map(item => removeTimestamps(item));
}
return logs;
}
render() {
const logs = podLogsStore.logs;
const controls = (
<PodLogControls
ready={!this.isLoading}
tabId={this.tabId}
tabData={this.tabData}
logs={this.logs}
logs={logs}
save={this.save}
reload={this.reload}
onSearch={this.onSearch}
@ -119,11 +103,12 @@ export class PodLogs extends React.Component<Props> {
controls={controls}
showSubmitClose={false}
showButtons={false}
showStatusPanel={false}
/>
<PodLogList
logs={logs}
id={this.tabId}
isLoading={this.isLoading}
logs={this.logs}
load={this.load}
ref={this.logListElement}
/>