import "./pod-logs.scss"; import React from "react"; import AnsiUp from "ansi_up"; import DOMPurify from "dompurify"; import { t, Trans } from "@lingui/macro"; import { computed, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { _i18n } from "../../i18n"; import { autobind, cssNames } from "../../utils"; import { Icon } from "../icon"; import { Spinner } from "../spinner"; import { IDockTab } from "./dock.store"; import { InfoPanel } from "./info-panel"; import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store"; import { Button } from "../button"; import { PodLogControls } from "./pod-log-controls"; interface Props { className?: string tab: IDockTab } @observer export class PodLogs extends React.Component { @observable ready = false; @observable preloading = false; // Indicator for setting Spinner (loader) at the top of the logs @observable showJumpToBottom = false; private logsElement: HTMLDivElement; private lastLineIsShown = true; // used for proper auto-scroll content after refresh private colorConverter = new AnsiUp(); componentDidMount() { disposeOnUnmount(this, [ reaction(() => this.props.tab.id, async () => { if (podLogsStore.logs.has(this.tabId)) { this.ready = true; return; } await this.load(); }, { fireImmediately: true }), // Check if need to show JumpToBottom if new log amount is less than previous one reaction(() => podLogsStore.logs.get(this.tabId), () => { const { tabId } = this; if (podLogsStore.logs.has(tabId) && podLogsStore.logs.get(tabId).length < logRange) { this.showJumpToBottom = false; } }) ]); } componentDidUpdate() { // scroll logs only when it's already in the end, // otherwise it can interrupt reading by jumping after loading new logs update if (this.logsElement && this.lastLineIsShown) { this.logsElement.scrollTop = this.logsElement.scrollHeight; } } get tabData() { return podLogsStore.getData(this.tabId); } get tabId() { return this.props.tab.id; } @autobind() save(data: Partial) { podLogsStore.setData(this.tabId, { ...this.tabData, ...data }); } load = async () => { this.ready = false; await podLogsStore.load(this.tabId); this.ready = true; } reload = async () => { podLogsStore.clearLogs(this.tabId); this.lastLineIsShown = true; await this.load(); } /** * Function loads more logs (usually after user scrolls to top) and sets proper * scrolling position * @param scrollHeight previous scrollHeight position before adding new lines */ loadMore = async (scrollHeight: number) => { if (podLogsStore.lines < logRange) return; this.preloading = true; await podLogsStore.load(this.tabId).then(() => this.preloading = false); if (this.logsElement.scrollHeight > scrollHeight) { // Set scroll position back to place where preloading started this.logsElement.scrollTop = this.logsElement.scrollHeight - scrollHeight - 48; } } /** * Computed prop which returns logs with or without timestamps added to each line and * does separation between new and old logs * @returns {Array} An array with 2 items - [oldLogs, newLogs] */ @computed get logs() { if (!podLogsStore.logs.has(this.tabId)) return []; const logs = podLogsStore.logs.get(this.tabId); const { getData, removeTimestamps, newLogSince } = podLogsStore; const { showTimestamps } = getData(this.tabId); let oldLogs: string[] = logs; let newLogs: string[] = []; if (newLogSince.has(this.tabId)) { // Finding separator timestamp in logs const index = logs.findIndex(item => item.includes(newLogSince.get(this.tabId))); if (index !== -1) { // Splitting logs to old and new ones oldLogs = logs.slice(0, index); newLogs = logs.slice(index); } } if (!showTimestamps) { return [oldLogs, newLogs].map(logs => logs.map(item => removeTimestamps(item))) } return [oldLogs, newLogs]; } onScroll = (evt: React.UIEvent) => { const logsArea = evt.currentTarget; const toBottomOffset = 100 * 16; // 100 lines * 16px (height of each line) const { scrollHeight, clientHeight, scrollTop } = logsArea; if (scrollTop === 0) { this.loadMore(scrollHeight); } if (scrollHeight - scrollTop > toBottomOffset) { this.showJumpToBottom = true; } else { this.showJumpToBottom = false; } this.lastLineIsShown = clientHeight + scrollTop === scrollHeight; }; renderJumpToBottom() { if (!this.logsElement) return null; return ( ); } renderLogs() { const [oldLogs, newLogs] = this.logs; if (!this.ready) { return ; } if (!oldLogs.length && !newLogs.length) { return (
There are no logs available for container.
); } return ( <> {this.preloading && (
)}
{newLogs.length > 0 && ( <>

)} ); } render() { const { className } = this.props; const controls = ( ) return (
this.logsElement = e}> {this.renderJumpToBottom()} {this.renderLogs()}
); } }