diff --git a/src/renderer/components/dock/pod-logs.scss b/src/renderer/components/dock/pod-logs.scss index 2ea1d1bff1..374d24ee8e 100644 --- a/src/renderer/components/dock/pod-logs.scss +++ b/src/renderer/components/dock/pod-logs.scss @@ -6,19 +6,36 @@ // `overflow: overlay` don't allow scroll to the last line overflow: auto; + position: relative; color: $textColorAccent; background: $logsBackground; - line-height: var(--log-line-height); - border-radius: 2px; - padding: $padding * 2; - font-family: $font-monospace; - font-size: smaller; - white-space: pre; flex-grow: 1; - > div { - // Provides font better readability on large screens - -webkit-font-smoothing: subpixel-antialiased; + .find-overlay { + position: absolute; + border-radius: 2px; + background-color: #8cc474; + margin-top: 4px; + opacity: 0.5; + } + + .VirtualList { + height: 100%; + + .list { + .LogRow { + padding: 2px 16px; + height: 18px; // Must be equal to lineHeight variable in pod-logs.scss + font-family: $font-monospace; + font-size: smaller; + white-space: pre; + -webkit-font-smoothing: auto; + + &:hover { + background: #35373a; + } + } + } } } @@ -48,6 +65,8 @@ border-radius: $unit * 2; opacity: 0; transition: opacity 0.2s; + z-index: 2; + top: 20px; &.active { opacity: 1; diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/pod-logs.tsx index d8723187c8..524befa2d1 100644 --- a/src/renderer/components/dock/pod-logs.tsx +++ b/src/renderer/components/dock/pod-logs.tsx @@ -1,8 +1,6 @@ import "./pod-logs.scss"; import React from "react"; -import AnsiUp from "ansi_up"; -import DOMPurify from "dompurify"; -import { t, Trans } from "@lingui/macro"; +import { Trans } from "@lingui/macro"; import { computed, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { _i18n } from "../../i18n"; @@ -14,21 +12,24 @@ import { InfoPanel } from "./info-panel"; import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store"; import { Button } from "../button"; import { PodLogControls } from "./pod-log-controls"; +import { VirtualListRef } from "../virtual-list"; +import debounce from "lodash/debounce"; interface Props { className?: string tab: IDockTab } +const lineHeight = 18; // Height of a log line. Should correlate with styles in pod-logs.scss + @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 logsElement = React.createRef(); // A reference for outer container in VirtualList private lastLineIsShown = true; // used for proper auto-scroll content after refresh - private colorConverter = new AnsiUp(); componentDidMount() { disposeOnUnmount(this, [ @@ -53,8 +54,8 @@ export class PodLogs extends React.Component { 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; + if (this.logsElement.current && this.lastLineIsShown) { + this.logsElement.current.scrollTop = this.logsElement.current.scrollHeight; } } @@ -88,50 +89,39 @@ export class PodLogs extends React.Component { * scrolling position * @param scrollHeight previous scrollHeight position before adding new lines */ - loadMore = async (scrollHeight: number) => { - if (podLogsStore.lines < logRange) return; + loadMore = async () => { + const lines = podLogsStore.lines; + if (lines < logRange) return; this.preloading = true; - await podLogsStore.load(this.tabId).then(() => this.preloading = false); - if (this.logsElement.scrollHeight > scrollHeight) { + await podLogsStore.load(this.tabId); + this.preloading = false; + if (podLogsStore.lines > lines) { // Set scroll position back to place where preloading started - this.logsElement.scrollTop = this.logsElement.scrollHeight - scrollHeight - 48; + this.logsElement.current.scrollTop = (podLogsStore.lines - lines) * lineHeight; } } /** - * 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 prop which returns logs with or without timestamps added to each line + * @returns {Array} An array log items */ @computed - get logs() { + get logs(): string[] { if (!podLogsStore.logs.has(this.tabId)) return []; const logs = podLogsStore.logs.get(this.tabId); - const { getData, removeTimestamps, newLogSince } = podLogsStore; + const { getData, removeTimestamps } = 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 logs.map(item => removeTimestamps(item)); } - return [oldLogs, newLogs]; + return logs; } - onScroll = (evt: React.UIEvent) => { - const logsArea = evt.currentTarget; - const toBottomOffset = 100 * 16; // 100 lines * 16px (height of each line) - const { scrollHeight, clientHeight, scrollTop } = logsArea; + onScroll = debounce(() => { + const toBottomOffset = 100 * lineHeight; // 100 lines * 18px (height of each line) + const { scrollHeight, clientHeight, scrollTop } = this.logsElement.current; if (scrollTop === 0) { - this.loadMore(scrollHeight); + this.loadMore(); } if (scrollHeight - scrollTop > toBottomOffset) { this.showJumpToBottom = true; @@ -139,7 +129,21 @@ export class PodLogs extends React.Component { this.showJumpToBottom = false; } this.lastLineIsShown = clientHeight + scrollTop === scrollHeight; - }; + }, 300); // Debouncing to let virtual list do its internal works + + /** + * A function is called by VirtualList for rendering each of the row + * @param index {Number} index of the log element in logs array + * @returns A react element with a row itself + */ + getLogRow = (index: number) => { + const isSeparator = this.logs[index] === "---newlogs---"; // TODO: Use constant separator + return ( +
+ {this.logs[index]} +
+ ); + } renderJumpToBottom() { if (!this.logsElement) return null; @@ -149,8 +153,8 @@ export class PodLogs extends React.Component { className={cssNames("jump-to-bottom flex gaps", {active: this.showJumpToBottom})} onClick={evt => { evt.currentTarget.blur(); - this.logsElement.scrollTo({ - top: this.logsElement.scrollHeight, + this.logsElement.current.scrollTo({ + top: this.logsElement.current.scrollHeight, behavior: "auto" }); }} @@ -162,11 +166,13 @@ export class PodLogs extends React.Component { } renderLogs() { - const [oldLogs, newLogs] = this.logs; + // Generating equal heights for each row with ability to do multyrow logs in future + // e. g. for wrapping logs feature + const rowHeights = new Array(this.logs.length).fill(lineHeight); if (!this.ready) { return ; } - if (!oldLogs.length && !newLogs.length) { + if (!this.logs.length) { return (
There are no logs available for container. @@ -177,16 +183,16 @@ export class PodLogs extends React.Component { <> {this.preloading && (
- +
)} -
- {newLogs.length > 0 && ( - <> -

-

- - )} + ); } @@ -211,7 +217,7 @@ export class PodLogs extends React.Component { showSubmitClose={false} showButtons={false} /> -
this.logsElement = e}> +
{this.renderJumpToBottom()} {this.renderLogs()}
diff --git a/src/renderer/components/table/table.tsx b/src/renderer/components/table/table.tsx index 6e09748499..c1fce31c80 100644 --- a/src/renderer/components/table/table.tsx +++ b/src/renderer/components/table/table.tsx @@ -159,7 +159,7 @@ export class Table extends React.Component { diff --git a/src/renderer/components/virtual-list/virtual-list.scss b/src/renderer/components/virtual-list/virtual-list.scss index 73d352cacf..4357321c7a 100644 --- a/src/renderer/components/virtual-list/virtual-list.scss +++ b/src/renderer/components/virtual-list/virtual-list.scss @@ -9,6 +9,5 @@ } overflow-y: overlay !important; - overflow-x: hidden !important; } } \ No newline at end of file diff --git a/src/renderer/components/virtual-list/virtual-list.tsx b/src/renderer/components/virtual-list/virtual-list.tsx index d6e77e08ca..8e21f225d5 100644 --- a/src/renderer/components/virtual-list/virtual-list.tsx +++ b/src/renderer/components/virtual-list/virtual-list.tsx @@ -5,7 +5,7 @@ import "./virtual-list.scss"; import React, { Component } from "react"; import { observer } from "mobx-react"; import { ListChildComponentProps, VariableSizeList } from "react-window"; -import { cssNames } from "../../utils"; +import { cssNames, noop } from "../../utils"; import { TableRowProps } from "../table/table-row"; import { ItemObject } from "../../item.store"; import throttle from "lodash/throttle"; @@ -13,15 +13,17 @@ import debounce from "lodash/debounce"; import isEqual from "lodash/isEqual"; import ResizeSensor from "css-element-queries/src/ResizeSensor"; -interface Props { - items: ItemObject[]; +interface Props { + items: T[]; rowHeights: number[]; className?: string; width?: number | string; initialOffset?: number; readyOffset?: number; selectedItemId?: string; - getTableRow?: (uid: string) => React.ReactElement; + getRow?: (uid: string | number) => React.ReactElement; + onScroll?: () => any; + outerRef?: ((instance: unknown) => void) | React.MutableRefObject } interface State { @@ -33,6 +35,7 @@ const defaultProps: Partial = { width: "100%", initialOffset: 1, readyOffset: 10, + onScroll: noop } export class VirtualList extends Component { @@ -73,6 +76,7 @@ export class VirtualList extends Component { getItemSize = (index: number) => this.props.rowHeights[index]; scrollToSelectedItem = debounce(() => { + if (!this.props.selectedItemId) return; const { items, selectedItemId } = this.props; const index = items.findIndex(item => item.getId() == selectedItemId); if (index === -1) return; @@ -80,11 +84,11 @@ export class VirtualList extends Component { }) render() { - const { width, className, items, getTableRow } = this.props; + const { width, className, items, getRow, onScroll, outerRef } = this.props; const { height, overscanCount } = this.state; const rowData: RowData = { items, - getTableRow + getRow }; return (
@@ -97,7 +101,9 @@ export class VirtualList extends Component { itemData={rowData} overscanCount={overscanCount} ref={this.listRef} + outerRef={outerRef} children={Row} + onScroll={() => onScroll()} />
); @@ -106,7 +112,7 @@ export class VirtualList extends Component { interface RowData { items: ItemObject[]; - getTableRow?: (uid: string) => React.ReactElement; + getRow?: (uid: string | number) => React.ReactElement; } interface RowProps extends ListChildComponentProps { @@ -115,11 +121,19 @@ interface RowProps extends ListChildComponentProps { const Row = observer((props: RowProps) => { const { index, style, data } = props; - const { items, getTableRow } = data; - const uid = items[index].getId(); - const row = getTableRow(uid); + const { items, getRow } = data; + const item = items[index]; + const uid = typeof item == "string" ? index : items[index].getId(); + const row = getRow(uid); if (!row) return null; return React.cloneElement(row, { style: Object.assign({}, row.props.style, style) }); -}) \ No newline at end of file +}) + +// A wrapper for passing ref back to parent component. This allows parent +// to control behavior of child component's DOM node. Scrolling event in our case. +// More info about ref forwading: https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-to-dom-components +export const VirtualListRef = React.forwardRef((props: Props, ref) => ( + +)); \ No newline at end of file