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:
parent
073b8a43f2
commit
edf59c9efd
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -9,6 +9,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
overflow-y: overlay !important;
|
overflow-y: overlay !important;
|
||||||
overflow-x: hidden !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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} />
|
||||||
|
));
|
||||||
Loading…
Reference in New Issue
Block a user