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

Moving logs to virtual list

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2020-10-20 11:49:08 +03:00
parent 073b8a43f2
commit edf59c9efd
5 changed files with 109 additions and 71 deletions

View File

@ -6,19 +6,36 @@
// `overflow: overlay` don't allow scroll to the last line // `overflow: overlay` don't allow scroll to the last line
overflow: auto; overflow: auto;
position: relative;
color: $textColorAccent; color: $textColorAccent;
background: $logsBackground; 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; flex-grow: 1;
> div { .find-overlay {
// Provides font better readability on large screens position: absolute;
-webkit-font-smoothing: subpixel-antialiased; 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; border-radius: $unit * 2;
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
z-index: 2;
top: 20px;
&.active { &.active {
opacity: 1; opacity: 1;

View File

@ -1,8 +1,6 @@
import "./pod-logs.scss"; import "./pod-logs.scss";
import React from "react"; import React from "react";
import AnsiUp from "ansi_up"; import { Trans } from "@lingui/macro";
import DOMPurify from "dompurify";
import { t, Trans } from "@lingui/macro";
import { computed, observable, reaction } from "mobx"; import { computed, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
@ -14,21 +12,24 @@ import { InfoPanel } from "./info-panel";
import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store"; import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store";
import { Button } from "../button"; import { Button } from "../button";
import { PodLogControls } from "./pod-log-controls"; import { PodLogControls } from "./pod-log-controls";
import { VirtualListRef } from "../virtual-list";
import debounce from "lodash/debounce";
interface Props { interface Props {
className?: string className?: string
tab: IDockTab tab: IDockTab
} }
const lineHeight = 18; // Height of a log line. Should correlate with styles in pod-logs.scss
@observer @observer
export class PodLogs extends React.Component<Props> { export class PodLogs extends React.Component<Props> {
@observable ready = false; @observable ready = false;
@observable preloading = false; // Indicator for setting Spinner (loader) at the top of the logs @observable preloading = false; // Indicator for setting Spinner (loader) at the top of the logs
@observable showJumpToBottom = false; @observable showJumpToBottom = false;
private logsElement: HTMLDivElement; private logsElement = React.createRef<HTMLDivElement>(); // A reference for outer container in VirtualList
private lastLineIsShown = true; // used for proper auto-scroll content after refresh private lastLineIsShown = true; // used for proper auto-scroll content after refresh
private colorConverter = new AnsiUp();
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
@ -53,8 +54,8 @@ export class PodLogs extends React.Component<Props> {
componentDidUpdate() { componentDidUpdate() {
// scroll logs only when it's already in the end, // scroll logs only when it's already in the end,
// otherwise it can interrupt reading by jumping after loading new logs update // otherwise it can interrupt reading by jumping after loading new logs update
if (this.logsElement && this.lastLineIsShown) { if (this.logsElement.current && this.lastLineIsShown) {
this.logsElement.scrollTop = this.logsElement.scrollHeight; this.logsElement.current.scrollTop = this.logsElement.current.scrollHeight;
} }
} }
@ -88,50 +89,39 @@ export class PodLogs extends React.Component<Props> {
* scrolling position * scrolling position
* @param scrollHeight previous scrollHeight position before adding new lines * @param scrollHeight previous scrollHeight position before adding new lines
*/ */
loadMore = async (scrollHeight: number) => { loadMore = async () => {
if (podLogsStore.lines < logRange) return; const lines = podLogsStore.lines;
if (lines < logRange) return;
this.preloading = true; this.preloading = true;
await podLogsStore.load(this.tabId).then(() => this.preloading = false); await podLogsStore.load(this.tabId);
if (this.logsElement.scrollHeight > scrollHeight) { this.preloading = false;
if (podLogsStore.lines > lines) {
// Set scroll position back to place where preloading started // 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 * Computed prop which returns logs with or without timestamps added to each line
* does separation between new and old logs * @returns {Array} An array log items
* @returns {Array} An array with 2 items - [oldLogs, newLogs]
*/ */
@computed @computed
get logs() { get logs(): string[] {
if (!podLogsStore.logs.has(this.tabId)) return []; if (!podLogsStore.logs.has(this.tabId)) return [];
const logs = podLogsStore.logs.get(this.tabId); const logs = podLogsStore.logs.get(this.tabId);
const { getData, removeTimestamps, newLogSince } = podLogsStore; const { getData, removeTimestamps } = podLogsStore;
const { showTimestamps } = getData(this.tabId); 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) { 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<HTMLDivElement>) => { onScroll = debounce(() => {
const logsArea = evt.currentTarget; const toBottomOffset = 100 * lineHeight; // 100 lines * 18px (height of each line)
const toBottomOffset = 100 * 16; // 100 lines * 16px (height of each line) const { scrollHeight, clientHeight, scrollTop } = this.logsElement.current;
const { scrollHeight, clientHeight, scrollTop } = logsArea;
if (scrollTop === 0) { if (scrollTop === 0) {
this.loadMore(scrollHeight); this.loadMore();
} }
if (scrollHeight - scrollTop > toBottomOffset) { if (scrollHeight - scrollTop > toBottomOffset) {
this.showJumpToBottom = true; this.showJumpToBottom = true;
@ -139,7 +129,21 @@ export class PodLogs extends React.Component<Props> {
this.showJumpToBottom = false; this.showJumpToBottom = false;
} }
this.lastLineIsShown = clientHeight + scrollTop === scrollHeight; 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 (
<div className={cssNames("LogRow", { separator: isSeparator })}>
{this.logs[index]}
</div>
);
}
renderJumpToBottom() { renderJumpToBottom() {
if (!this.logsElement) return null; if (!this.logsElement) return null;
@ -149,8 +153,8 @@ export class PodLogs extends React.Component<Props> {
className={cssNames("jump-to-bottom flex gaps", {active: this.showJumpToBottom})} className={cssNames("jump-to-bottom flex gaps", {active: this.showJumpToBottom})}
onClick={evt => { onClick={evt => {
evt.currentTarget.blur(); evt.currentTarget.blur();
this.logsElement.scrollTo({ this.logsElement.current.scrollTo({
top: this.logsElement.scrollHeight, top: this.logsElement.current.scrollHeight,
behavior: "auto" behavior: "auto"
}); });
}} }}
@ -162,11 +166,13 @@ export class PodLogs extends React.Component<Props> {
} }
renderLogs() { 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) { if (!this.ready) {
return <Spinner center/>; return <Spinner center/>;
} }
if (!oldLogs.length && !newLogs.length) { if (!this.logs.length) {
return ( return (
<div className="flex align-center justify-center"> <div className="flex align-center justify-center">
<Trans>There are no logs available for container.</Trans> <Trans>There are no logs available for container.</Trans>
@ -177,16 +183,16 @@ export class PodLogs extends React.Component<Props> {
<> <>
{this.preloading && ( {this.preloading && (
<div className="flex justify-center"> <div className="flex justify-center">
<Spinner /> <Spinner center />
</div> </div>
)} )}
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(oldLogs.join("\n"))) }} /> <VirtualListRef
{newLogs.length > 0 && ( items={this.logs}
<> rowHeights={rowHeights}
<p className="new-logs-sep" title={_i18n._(t`New logs since opening logs tab`)}/> getRow={this.getLogRow}
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(newLogs.join("\n"))) }} /> onScroll={this.onScroll}
</> ref={this.logsElement}
)} />
</> </>
); );
} }
@ -211,7 +217,7 @@ export class PodLogs extends React.Component<Props> {
showSubmitClose={false} showSubmitClose={false}
showButtons={false} showButtons={false}
/> />
<div className="logs" onScroll={this.onScroll} ref={e => this.logsElement = e}> <div className="logs">
{this.renderJumpToBottom()} {this.renderJumpToBottom()}
{this.renderLogs()} {this.renderLogs()}
</div> </div>

View File

@ -159,7 +159,7 @@ export class Table extends React.Component<TableProps> {
<VirtualList <VirtualList
items={sortedItems} items={sortedItems}
rowHeights={rowHeights} rowHeights={rowHeights}
getTableRow={getTableRow} getRow={getTableRow}
selectedItemId={selectedItemId} selectedItemId={selectedItemId}
className={className} className={className}
/> />

View File

@ -9,6 +9,5 @@
} }
overflow-y: overlay !important; overflow-y: overlay !important;
overflow-x: hidden !important;
} }
} }

View File

@ -5,7 +5,7 @@ import "./virtual-list.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ListChildComponentProps, VariableSizeList } from "react-window"; import { ListChildComponentProps, VariableSizeList } from "react-window";
import { cssNames } from "../../utils"; import { cssNames, noop } from "../../utils";
import { TableRowProps } from "../table/table-row"; import { TableRowProps } from "../table/table-row";
import { ItemObject } from "../../item.store"; import { ItemObject } from "../../item.store";
import throttle from "lodash/throttle"; import throttle from "lodash/throttle";
@ -13,15 +13,17 @@ import debounce from "lodash/debounce";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import ResizeSensor from "css-element-queries/src/ResizeSensor"; import ResizeSensor from "css-element-queries/src/ResizeSensor";
interface Props { interface Props<T extends ItemObject = any> {
items: ItemObject[]; items: T[];
rowHeights: number[]; rowHeights: number[];
className?: string; className?: string;
width?: number | string; width?: number | string;
initialOffset?: number; initialOffset?: number;
readyOffset?: number; readyOffset?: number;
selectedItemId?: string; selectedItemId?: string;
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>; getRow?: (uid: string | number) => React.ReactElement<any>;
onScroll?: () => any;
outerRef?: ((instance: unknown) => void) | React.MutableRefObject<unknown>
} }
interface State { interface State {
@ -33,6 +35,7 @@ const defaultProps: Partial<Props> = {
width: "100%", width: "100%",
initialOffset: 1, initialOffset: 1,
readyOffset: 10, readyOffset: 10,
onScroll: noop
} }
export class VirtualList extends Component<Props, State> { export class VirtualList extends Component<Props, State> {
@ -73,6 +76,7 @@ export class VirtualList extends Component<Props, State> {
getItemSize = (index: number) => this.props.rowHeights[index]; getItemSize = (index: number) => this.props.rowHeights[index];
scrollToSelectedItem = debounce(() => { scrollToSelectedItem = debounce(() => {
if (!this.props.selectedItemId) return;
const { items, selectedItemId } = this.props; const { items, selectedItemId } = this.props;
const index = items.findIndex(item => item.getId() == selectedItemId); const index = items.findIndex(item => item.getId() == selectedItemId);
if (index === -1) return; if (index === -1) return;
@ -80,11 +84,11 @@ export class VirtualList extends Component<Props, State> {
}) })
render() { render() {
const { width, className, items, getTableRow } = this.props; const { width, className, items, getRow, onScroll, outerRef } = this.props;
const { height, overscanCount } = this.state; const { height, overscanCount } = this.state;
const rowData: RowData = { const rowData: RowData = {
items, items,
getTableRow getRow
}; };
return ( return (
<div className={cssNames("VirtualList", className)} ref={this.parentRef}> <div className={cssNames("VirtualList", className)} ref={this.parentRef}>
@ -97,7 +101,9 @@ export class VirtualList extends Component<Props, State> {
itemData={rowData} itemData={rowData}
overscanCount={overscanCount} overscanCount={overscanCount}
ref={this.listRef} ref={this.listRef}
outerRef={outerRef}
children={Row} children={Row}
onScroll={() => onScroll()}
/> />
</div> </div>
); );
@ -106,7 +112,7 @@ export class VirtualList extends Component<Props, State> {
interface RowData { interface RowData {
items: ItemObject[]; items: ItemObject[];
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>; getRow?: (uid: string | number) => React.ReactElement<TableRowProps>;
} }
interface RowProps extends ListChildComponentProps { interface RowProps extends ListChildComponentProps {
@ -115,11 +121,19 @@ interface RowProps extends ListChildComponentProps {
const Row = observer((props: RowProps) => { const Row = observer((props: RowProps) => {
const { index, style, data } = props; const { index, style, data } = props;
const { items, getTableRow } = data; const { items, getRow } = data;
const uid = items[index].getId(); const item = items[index];
const row = getTableRow(uid); const uid = typeof item == "string" ? index : items[index].getId();
const row = getRow(uid);
if (!row) return null; if (!row) return null;
return React.cloneElement(row, { return React.cloneElement(row, {
style: Object.assign({}, row.props.style, style) style: Object.assign({}, row.props.style, style)
}); });
}) })
// 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) => (
<VirtualList outerRef={ref} {...props} />
));