diff --git a/src/renderer/components/+workloads-pods/pod-menu.tsx b/src/renderer/components/+workloads-pods/pod-menu.tsx index 4ec3bb69d0..fecd400449 100644 --- a/src/renderer/components/+workloads-pods/pod-menu.tsx +++ b/src/renderer/components/+workloads-pods/pod-menu.tsx @@ -51,7 +51,6 @@ export class PodMenu extends React.Component { selectedContainer: container, showTimestamps: false, previous: false, - tailLines: 1000 }); } diff --git a/src/renderer/components/dock/pod-logs.store.ts b/src/renderer/components/dock/pod-logs.store.ts index fa1da38c68..8675386273 100644 --- a/src/renderer/components/dock/pod-logs.store.ts +++ b/src/renderer/components/dock/pod-logs.store.ts @@ -1,10 +1,11 @@ -import { autorun, observable } from "mobx"; -import { Pod, IPodContainer, podsApi } from "../../api/endpoints"; +import { autorun, computed, observable } from "mobx"; +import { Pod, IPodContainer, podsApi, IPodLogsQuery } from "../../api/endpoints"; import { autobind, interval } from "../../utils"; import { DockTabStore } from "./dock-tab.store"; import { dockStore, IDockTab, TabKind } from "./dock.store"; import { t } from "@lingui/macro"; import { _i18n } from "../../i18n"; +import { Notifications } from "../notifications"; export interface IPodLogsData { pod: Pod; @@ -12,20 +13,22 @@ export interface IPodLogsData { containers: IPodContainer[] initContainers: IPodContainer[] showTimestamps: boolean - tailLines: number previous: boolean } type TabId = string; +type PodLogs = string; -interface PodLogs { - oldLogs?: string - newLogs?: string -} +// Number for log lines to load +export const logRange = 100; // TODO: Change to 1000 for production @autobind() export class PodLogsStore extends DockTabStore { - private refresher = interval(10, () => this.load(dockStore.selectedTabId)); + private refresher = interval(10, () => { + const id = dockStore.selectedTabId + if (!this.logs.get(id)) return + this.loadMore(id) + }); @observable logs = observable.map(); @@ -43,45 +46,85 @@ export class PodLogsStore extends DockTabStore { }, { delay: 500 }); } + /** + * Function prepares tailLines param for passing to API request + * Each time it increasing it's number, caused to fetch more logs. + * Also, it handles loading errors, rewriting whole logs with error + * messages + * @param tabId + */ load = async (tabId: TabId) => { - if (!this.logs.has(tabId)) { - this.logs.set(tabId, { oldLogs: "", newLogs: "" }) - } - const data = this.getData(tabId); - const { oldLogs, newLogs } = this.logs.get(tabId); - const { selectedContainer, tailLines, previous } = data; - const pod = new Pod(data.pod); - try { - // if logs already loaded, check the latest timestamp for getting updates only from this point - const logsTimestamps = this.getTimestamps(newLogs || oldLogs); - let lastLogDate = new Date(0); - if (logsTimestamps) { - lastLogDate = new Date(logsTimestamps.slice(-1)[0]); - lastLogDate.setSeconds(lastLogDate.getSeconds() + 1); // avoid duplicates from last second - } - const namespace = pod.getNs(); - const name = pod.getName(); - const loadedLogs = await podsApi.getLogs({ namespace, name }, { - sinceTime: lastLogDate.toISOString(), - timestamps: true, // Always setting timestampt to separate old logs from new ones - container: selectedContainer.name, - tailLines, - previous - }); - if (!oldLogs) { - this.logs.set(tabId, { oldLogs: loadedLogs, newLogs }); - } else { - this.logs.set(tabId, { oldLogs, newLogs: loadedLogs }); - } - } catch ({error}) { - this.logs.set(tabId, { - oldLogs: [ + const logs = await this.loadLogs(tabId, { + tailLines: this.lines + logRange + }) + .then(logs => { + if (!this.refresher.isRunning) this.refresher.start(); + return logs; + }) + .catch(({error}) => { + const message = [ _i18n._(t`Failed to load logs: ${error.message}`), _i18n._(t`Reason: ${error.reason} (${error.code})`) - ].join("\n"), - newLogs + ].join("\n"); + this.refresher.stop(); + Notifications.error(message); + return message; }); + this.logs.set(tabId, logs); + } + + /** + * Function is used to refreser/stream-like requests. + * It changes 'sinceTime' param each time allowing to fetch logs + * starting from last line recieved. + * @param tabId + */ + loadMore = async (tabId: TabId) => { + const oldLogs = this.logs.get(tabId); + const timestamps = this.getTimestamps(oldLogs); + let sinceTime = new Date(0); + if (timestamps) { + sinceTime = new Date(timestamps.slice(-1)[0]); + sinceTime.setSeconds(sinceTime.getSeconds() + 1); // avoid duplicates from last second } + const logs = await this.loadLogs(tabId, { + sinceTime: sinceTime.toISOString() + }); + // Add newly received logs to bottom + // TODO: set a new log separator here + this.logs.set(tabId, oldLogs + logs); + } + + /** + * Main logs loading function adds necessary data to payload and makes + * an API request + * @param tabId + * @param params request parameters described in IPodLogsQuery interface + * @returns {Promise} A fetch request promise + */ + loadLogs = async (tabId: TabId, params: Partial) => { + const data = this.getData(tabId); + const { selectedContainer, previous } = data; + const pod = new Pod(data.pod); + const namespace = pod.getNs(); + const name = pod.getName(); + return await podsApi.getLogs({ namespace, name }, { + ...params, + timestamps: true, // Always setting timestampt to separate old logs from new ones + container: selectedContainer.name, + previous + }); + } + + /** + * Converts logs into a string array + * @returns {number} Length of log lines + */ + @computed + get lines() { + const id = dockStore.selectedTabId; + const logs = this.logs.get(id); + return logs ? logs.split("\n").length : 0; } getTimestamps(logs: string) { diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/pod-logs.tsx index 06643bc711..27cca1ecee 100644 --- a/src/renderer/components/dock/pod-logs.tsx +++ b/src/renderer/components/dock/pod-logs.tsx @@ -12,7 +12,7 @@ import { Select, SelectOption } from "../select"; import { Spinner } from "../spinner"; import { IDockTab } from "./dock.store"; import { InfoPanel } from "./info-panel"; -import { IPodLogsData, podLogsStore } from "./pod-logs.store"; +import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store"; interface Props { className?: string @@ -22,16 +22,11 @@ interface Props { @observer export class PodLogs extends React.Component { @observable ready = false; + @observable preloading = false; // Indicator for setting Spinner (loader) at the top of the logs private logsElement: HTMLDivElement; private lastLineIsShown = true; // used for proper auto-scroll content after refresh private colorConverter = new AnsiUp(); - private lineOptions = [ - { label: _i18n._(t`All logs`), value: Number.MAX_SAFE_INTEGER }, - { label: 1000, value: 1000 }, - { label: 10000, value: 10000 }, - { label: 100000, value: 100000 }, - ]; componentDidMount() { disposeOnUnmount(this, @@ -78,22 +73,40 @@ export class PodLogs extends React.Component { 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 + */ + preload = 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 + */ @computed get logs() { if (!podLogsStore.logs.has(this.tabId)) return; - const { oldLogs, newLogs } = podLogsStore.logs.get(this.tabId); + const logs = podLogsStore.logs.get(this.tabId); const { getData, removeTimestamps } = podLogsStore; const { showTimestamps } = getData(this.tabId); - return { - oldLogs: showTimestamps ? oldLogs : removeTimestamps(oldLogs), - newLogs: showTimestamps ? newLogs : removeTimestamps(newLogs) - } + return showTimestamps ? logs : removeTimestamps(logs); } toggleTimestamps = () => { this.save({ showTimestamps: !this.tabData.showTimestamps }); } + /** + * Setting 'previous' param to load API request fetching logs from previous container + */ togglePrevious = () => { this.save({ previous: !this.tabData.previous }); this.reload(); @@ -102,15 +115,16 @@ export class PodLogs extends React.Component { onScroll = (evt: React.UIEvent) => { const logsArea = evt.currentTarget; const { scrollHeight, clientHeight, scrollTop } = logsArea; + if (scrollTop === 0) { + this.preload(scrollHeight); + } this.lastLineIsShown = clientHeight + scrollTop === scrollHeight; }; downloadLogs = () => { - const { oldLogs, newLogs } = this.logs; const { pod, selectedContainer } = this.tabData; const fileName = selectedContainer ? selectedContainer.name : pod.getName(); - const fileContents = oldLogs + newLogs; - downloadFile(fileName + ".log", fileContents, "text/plain"); + downloadFile(fileName + ".log", this.logs, "text/plain"); } onContainerChange = (option: SelectOption) => { @@ -123,11 +137,6 @@ export class PodLogs extends React.Component { this.reload(); } - onTailLineChange = (option: SelectOption) => { - this.save({ tailLines: option.value }) - this.reload(); - } - get containerSelectOptions() { const { containers, initContainers } = this.tabData; return [ @@ -153,8 +162,8 @@ export class PodLogs extends React.Component { renderControls() { if (!this.ready) return null; - const { selectedContainer, showTimestamps, tailLines, previous } = this.tabData; - const timestamps = podLogsStore.getTimestamps(podLogsStore.logs.get(this.tabId).oldLogs); + const { selectedContainer, showTimestamps, previous } = this.tabData; + const timestamps = podLogsStore.getTimestamps(podLogsStore.logs.get(this.tabId)); return (
Container @@ -165,12 +174,6 @@ export class PodLogs extends React.Component { onChange={this.onContainerChange} autoConvertOptions={false} /> - Lines -