mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Pod logs refactoring (#1516)
* Spreading PodLogs into 2 components Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Removing pod-logs.scss Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Removing unused isScrollHidden param Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Cleaning up logs components Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
parent
ccd38b5cbe
commit
2a96e094bb
5
src/renderer/components/dock/pod-log-controls.scss
Normal file
5
src/renderer/components/dock/pod-log-controls.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.PodLogControls {
|
||||||
|
.Select {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import "./pod-log-controls.scss";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { IPodLogsData, podLogsStore } from "./pod-logs.store";
|
import { IPodLogsData, podLogsStore } from "./pod-logs.store";
|
||||||
@ -21,10 +22,9 @@ interface Props extends PodLogSearchProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PodLogControls = observer((props: Props) => {
|
export const PodLogControls = observer((props: Props) => {
|
||||||
if (!props.ready) return null;
|
|
||||||
const { tabData, save, reload, tabId, logs } = props;
|
const { tabData, save, reload, tabId, logs } = props;
|
||||||
const { selectedContainer, showTimestamps, previous } = tabData;
|
const { selectedContainer, showTimestamps, previous } = tabData;
|
||||||
const rawLogs = podLogsStore.logs.get(tabId);
|
const rawLogs = podLogsStore.logs.get(tabId) || [];
|
||||||
const since = rawLogs.length ? podLogsStore.getTimestamps(rawLogs[0]) : null;
|
const since = rawLogs.length ? podLogsStore.getTimestamps(rawLogs[0]) : null;
|
||||||
const pod = new Pod(tabData.pod);
|
const pod = new Pod(tabData.pod);
|
||||||
|
|
||||||
|
|||||||
78
src/renderer/components/dock/pod-log-list.scss
Normal file
78
src/renderer/components/dock/pod-log-list.scss
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
.PodLogList {
|
||||||
|
--overlay-bg: #8cc474b8;
|
||||||
|
--overlay-active-bg: orange;
|
||||||
|
|
||||||
|
// fix for `this.logsElement.scrollTop = this.logsElement.scrollHeight`
|
||||||
|
// `overflow: overlay` don't allow scroll to the last line
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
color: $textColorAccent;
|
||||||
|
background: $logsBackground;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.VirtualList {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
overflow-x: scroll!important;
|
||||||
|
|
||||||
|
.LogRow {
|
||||||
|
padding: 2px 16px;
|
||||||
|
height: 18px; // Must be equal to lineHeight variable in pod-log-list.tsx
|
||||||
|
font-family: $font-monospace;
|
||||||
|
font-size: smaller;
|
||||||
|
white-space: pre;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $logRowHoverBackground;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
-webkit-font-smoothing: auto; // Better readability on non-retina screens
|
||||||
|
}
|
||||||
|
|
||||||
|
span.overlay {
|
||||||
|
border-radius: 2px;
|
||||||
|
-webkit-font-smoothing: auto;
|
||||||
|
background-color: var(--overlay-bg);
|
||||||
|
|
||||||
|
span {
|
||||||
|
background-color: var(--overlay-bg)!important; // Rewriting inline styles from AnsiUp library
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--overlay-active-bg);
|
||||||
|
|
||||||
|
span {
|
||||||
|
background-color: var(--overlay-active-bg)!important; // Rewriting inline styles from AnsiUp library
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isLoading {
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isScrollHidden {
|
||||||
|
.VirtualList .list {
|
||||||
|
overflow-x: hidden!important; // fixing scroll to bottom issues in PodLogs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.JumpToBottom {
|
||||||
|
position: absolute;
|
||||||
|
right: 30px;
|
||||||
|
padding: $unit / 2 $unit * 1.5;
|
||||||
|
border-radius: $unit * 2;
|
||||||
|
z-index: 2;
|
||||||
|
top: 20px;
|
||||||
|
|
||||||
|
.Icon {
|
||||||
|
--size: $unit * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
224
src/renderer/components/dock/pod-log-list.tsx
Normal file
224
src/renderer/components/dock/pod-log-list.tsx
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import "./pod-log-list.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import AnsiUp from "ansi_up";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
import { Trans } from "@lingui/macro";
|
||||||
|
import { action, observable } from "mobx";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Align, ListOnScrollProps } from "react-window";
|
||||||
|
|
||||||
|
import { searchStore } from "../../../common/search-store";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { Spinner } from "../spinner";
|
||||||
|
import { VirtualList } from "../virtual-list";
|
||||||
|
import { logRange } from "./pod-logs.store";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
logs: string[]
|
||||||
|
isLoading: boolean
|
||||||
|
load: () => void
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorConverter = new AnsiUp();
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class PodLogList extends React.Component<Props> {
|
||||||
|
@observable isJumpButtonVisible = false;
|
||||||
|
@observable isLastLineVisible = true;
|
||||||
|
|
||||||
|
private virtualListDiv = React.createRef<HTMLDivElement>(); // A reference for outer container in VirtualList
|
||||||
|
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
|
||||||
|
private lineHeight = 18; // Height of a log line. Should correlate with styles in pod-log-list.scss
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
const { logs, id } = this.props;
|
||||||
|
if (id != prevProps.id) {
|
||||||
|
this.isLastLineVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (logs == prevProps.logs || !this.virtualListDiv.current) return;
|
||||||
|
const newLogsLoaded = prevProps.logs.length < logs.length;
|
||||||
|
const scrolledToBeginning = this.virtualListDiv.current.scrollTop === 0;
|
||||||
|
const fewLogsLoaded = logs.length < logRange;
|
||||||
|
if (this.isLastLineVisible) {
|
||||||
|
this.scrollToBottom(); // Scroll down to keep user watching/reading experience
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scrolledToBeginning && newLogsLoaded) {
|
||||||
|
this.virtualListDiv.current.scrollTop = (logs.length - prevProps.logs.length) * this.lineHeight;
|
||||||
|
}
|
||||||
|
if (fewLogsLoaded) {
|
||||||
|
this.isJumpButtonVisible = false;
|
||||||
|
}
|
||||||
|
if (!logs.length) {
|
||||||
|
this.isLastLineVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if JumpToBottom button should be visible and sets its observable
|
||||||
|
* @param props Scrolling props from virtual list core
|
||||||
|
*/
|
||||||
|
@action
|
||||||
|
setButtonVisibility = (props: ListOnScrollProps) => {
|
||||||
|
const offset = 100 * this.lineHeight;
|
||||||
|
const { scrollHeight } = this.virtualListDiv.current;
|
||||||
|
const { scrollOffset } = props;
|
||||||
|
if (scrollHeight - scrollOffset < offset) {
|
||||||
|
this.isJumpButtonVisible = false;
|
||||||
|
} else {
|
||||||
|
this.isJumpButtonVisible = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if last log line considered visible to user, setting its observable
|
||||||
|
* @param props Scrolling props from virtual list core
|
||||||
|
*/
|
||||||
|
@action
|
||||||
|
setLastLineVisibility = (props: ListOnScrollProps) => {
|
||||||
|
const { scrollHeight, clientHeight } = this.virtualListDiv.current;
|
||||||
|
const { scrollOffset, scrollDirection } = props;
|
||||||
|
if (scrollDirection == "backward") {
|
||||||
|
this.isLastLineVisible = false;
|
||||||
|
} else {
|
||||||
|
if (clientHeight + scrollOffset === scrollHeight) {
|
||||||
|
this.isLastLineVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user scrolled to top and new logs should be loaded
|
||||||
|
* @param props Scrolling props from virtual list core
|
||||||
|
*/
|
||||||
|
checkLoadIntent = (props: ListOnScrollProps) => {
|
||||||
|
const { scrollOffset } = props;
|
||||||
|
if (scrollOffset === 0) {
|
||||||
|
this.props.load();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
scrollToBottom = () => {
|
||||||
|
if (!this.virtualListDiv.current) return;
|
||||||
|
this.isJumpButtonVisible = false;
|
||||||
|
this.virtualListDiv.current.scrollTop = this.virtualListDiv.current.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollToItem = (index: number, align: Align) => {
|
||||||
|
this.virtualListRef.current.scrollToItem(index, align);
|
||||||
|
};
|
||||||
|
|
||||||
|
onScroll = debounce((props: ListOnScrollProps) => {
|
||||||
|
if (!this.virtualListDiv.current) return;
|
||||||
|
this.setButtonVisibility(props);
|
||||||
|
this.setLastLineVisibility(props);
|
||||||
|
this.checkLoadIntent(props);
|
||||||
|
}, 700); // Increasing performance and giving some time for virtual list to settle down
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function is called by VirtualList for rendering each of the row
|
||||||
|
* @param rowIndex index of the log element in logs array
|
||||||
|
* @returns A react element with a row itself
|
||||||
|
*/
|
||||||
|
getLogRow = (rowIndex: number) => {
|
||||||
|
const { searchQuery, isActiveOverlay } = searchStore;
|
||||||
|
const item = this.props.logs[rowIndex];
|
||||||
|
const contents: React.ReactElement[] = [];
|
||||||
|
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi));
|
||||||
|
if (searchQuery) { // If search is enabled, replace keyword with backgrounded <span>
|
||||||
|
// Case-insensitive search (lowercasing query and keywords in line)
|
||||||
|
const regex = new RegExp(searchStore.escapeRegex(searchQuery), "gi");
|
||||||
|
const matches = item.matchAll(regex);
|
||||||
|
const modified = item.replace(regex, match => match.toLowerCase());
|
||||||
|
// Splitting text line by keyword
|
||||||
|
const pieces = modified.split(searchQuery.toLowerCase());
|
||||||
|
pieces.forEach((piece, index) => {
|
||||||
|
const active = isActiveOverlay(rowIndex, index);
|
||||||
|
const lastItem = index === pieces.length - 1;
|
||||||
|
const overlayValue = matches.next().value;
|
||||||
|
const overlay = !lastItem
|
||||||
|
? <span
|
||||||
|
className={cssNames("overlay", { active })}
|
||||||
|
dangerouslySetInnerHTML={{ __html: ansiToHtml(overlayValue) }}
|
||||||
|
/>
|
||||||
|
: null;
|
||||||
|
contents.push(
|
||||||
|
<React.Fragment key={piece + index}>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: ansiToHtml(piece) }} />
|
||||||
|
{overlay}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={cssNames("LogRow")}>
|
||||||
|
{contents.length > 1 ? contents : (
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: ansiToHtml(item) }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { logs, isLoading } = this.props;
|
||||||
|
const isInitLoading = isLoading && !logs.length;
|
||||||
|
const rowHeights = new Array(logs.length).fill(this.lineHeight);
|
||||||
|
if (isInitLoading) {
|
||||||
|
return <Spinner center/>;
|
||||||
|
}
|
||||||
|
if (!logs.length) {
|
||||||
|
return (
|
||||||
|
<div className="PodLogList flex box grow align-center justify-center">
|
||||||
|
<Trans>There are no logs available for container</Trans>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={cssNames("PodLogList flex", { isLoading })}>
|
||||||
|
<VirtualList
|
||||||
|
items={logs}
|
||||||
|
rowHeights={rowHeights}
|
||||||
|
getRow={this.getLogRow}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
outerRef={this.virtualListDiv}
|
||||||
|
ref={this.virtualListRef}
|
||||||
|
className="box grow"
|
||||||
|
/>
|
||||||
|
{this.isJumpButtonVisible && (
|
||||||
|
<JumpToBottom onClick={this.scrollToBottom} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JumpToBottomProps {
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const JumpToBottom = ({ onClick }: JumpToBottomProps) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
className="JumpToBottom flex gaps"
|
||||||
|
onClick={evt => {
|
||||||
|
evt.currentTarget.blur();
|
||||||
|
onClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans>Jump to bottom</Trans>
|
||||||
|
<Icon material="expand_more" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,86 +0,0 @@
|
|||||||
.PodLogs {
|
|
||||||
--overlay-bg: #8cc474b8;
|
|
||||||
--overlay-active-bg: orange;
|
|
||||||
|
|
||||||
.logs {
|
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
color: $textColorAccent;
|
|
||||||
background: $logsBackground;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
.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;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $logRowHoverBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
-webkit-font-smoothing: auto; // Better readability on non-retina screens
|
|
||||||
}
|
|
||||||
|
|
||||||
span.overlay {
|
|
||||||
border-radius: 2px;
|
|
||||||
-webkit-font-smoothing: auto;
|
|
||||||
background-color: var(--overlay-bg);
|
|
||||||
|
|
||||||
span {
|
|
||||||
background-color: var(--overlay-bg)!important; // Rewriting inline styles from AnsiUp library
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: var(--overlay-active-bg);
|
|
||||||
|
|
||||||
span {
|
|
||||||
background-color: var(--overlay-active-bg)!important; // Rewriting inline styles from AnsiUp library
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jump-to-bottom {
|
|
||||||
position: absolute;
|
|
||||||
right: 30px;
|
|
||||||
padding: $unit / 2 $unit * 1.5;
|
|
||||||
border-radius: $unit * 2;
|
|
||||||
opacity: 0;
|
|
||||||
z-index: 2;
|
|
||||||
top: 20px;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
--size: $unit * 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.PodLogControls {
|
|
||||||
.Select {
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs .VirtualList .list {
|
|
||||||
overflow-x: scroll!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.noscroll {
|
|
||||||
.logs .VirtualList .list {
|
|
||||||
overflow-x: hidden!important; // fixing scroll to bottom issues in PodLogs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,65 +1,30 @@
|
|||||||
import "./pod-logs.scss";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import AnsiUp from 'ansi_up';
|
import { computed, observable, reaction } from "mobx";
|
||||||
import DOMPurify from "dompurify";
|
|
||||||
import { Trans } from "@lingui/macro";
|
|
||||||
import { action, computed, observable, reaction } from "mobx";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { _i18n } from "../../i18n";
|
|
||||||
import { autobind, cssNames } from "../../utils";
|
import { searchStore } from "../../../common/search-store";
|
||||||
import { Icon } from "../icon";
|
import { autobind } from "../../utils";
|
||||||
import { Spinner } from "../spinner";
|
|
||||||
import { IDockTab } from "./dock.store";
|
import { IDockTab } from "./dock.store";
|
||||||
import { InfoPanel } from "./info-panel";
|
import { InfoPanel } from "./info-panel";
|
||||||
import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store";
|
|
||||||
import { Button } from "../button";
|
|
||||||
import { PodLogControls } from "./pod-log-controls";
|
import { PodLogControls } from "./pod-log-controls";
|
||||||
import { VirtualList } from "../virtual-list";
|
import { PodLogList } from "./pod-log-list";
|
||||||
import { searchStore } from "../../../common/search-store";
|
import { IPodLogsData, podLogsStore } from "./pod-logs.store";
|
||||||
import { ListOnScrollProps } from "react-window";
|
|
||||||
|
|
||||||
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 isLoading = true;
|
||||||
@observable preloading = false; // Indicator for setting Spinner (loader) at the top of the logs
|
|
||||||
@observable showJumpToBottom = false;
|
|
||||||
@observable hideHorizontalScroll = true; // Hiding scrollbar allows to scroll logs down to last element
|
|
||||||
|
|
||||||
private logsElement = React.createRef<HTMLDivElement>(); // A reference for outer container in VirtualList
|
private logListElement = React.createRef<PodLogList>(); // A reference for VirtualList component
|
||||||
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
|
|
||||||
private lastLineIsShown = true; // used for proper auto-scroll content after refresh
|
|
||||||
private colorConverter = new AnsiUp();
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this,
|
||||||
reaction(() => this.props.tab.id, async () => {
|
reaction(() => this.props.tab.id, this.reload, { fireImmediately: true })
|
||||||
await this.load();
|
);
|
||||||
this.scrollToBottom();
|
|
||||||
}, { 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.current && this.lastLineIsShown) {
|
|
||||||
this.logsElement.current.scrollTop = this.logsElement.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get tabData() {
|
get tabData() {
|
||||||
@ -76,33 +41,16 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
load = async () => {
|
load = async () => {
|
||||||
this.ready = false;
|
this.isLoading = true;
|
||||||
await podLogsStore.load(this.tabId);
|
await podLogsStore.load(this.tabId);
|
||||||
this.ready = true;
|
this.isLoading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
reload = async () => {
|
reload = async () => {
|
||||||
podLogsStore.clearLogs(this.tabId);
|
podLogsStore.clearLogs(this.tabId);
|
||||||
this.lastLineIsShown = true;
|
|
||||||
await this.load();
|
await this.load();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Function loads more logs (usually after user scrolls to top) and sets proper
|
|
||||||
* scrolling position
|
|
||||||
*/
|
|
||||||
loadMore = async () => {
|
|
||||||
const lines = podLogsStore.lines;
|
|
||||||
if (lines < logRange) return;
|
|
||||||
this.preloading = true;
|
|
||||||
await podLogsStore.load(this.tabId);
|
|
||||||
this.preloading = false;
|
|
||||||
if (podLogsStore.lines > lines) {
|
|
||||||
// Set scroll position back to place where preloading started
|
|
||||||
this.logsElement.current.scrollTop = (podLogsStore.lines - lines) * lineHeight;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function for various actions after search is happened
|
* A function for various actions after search is happened
|
||||||
* @param query {string} A text from search field
|
* @param query {string} A text from search field
|
||||||
@ -118,9 +66,9 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
@autobind()
|
@autobind()
|
||||||
toOverlay() {
|
toOverlay() {
|
||||||
const { activeOverlayLine } = searchStore;
|
const { activeOverlayLine } = searchStore;
|
||||||
if (!this.virtualListRef.current || activeOverlayLine === undefined) return;
|
if (!this.logListElement.current || activeOverlayLine === undefined) return;
|
||||||
// Scroll vertically
|
// Scroll vertically
|
||||||
this.virtualListRef.current.scrollToItem(activeOverlayLine, "center");
|
this.logListElement.current.scrollToItem(activeOverlayLine, "center");
|
||||||
// Scroll horizontally in timeout since virtual list need some time to prepare its contents
|
// Scroll horizontally in timeout since virtual list need some time to prepare its contents
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const overlay = document.querySelector(".PodLogs .list span.active");
|
const overlay = document.querySelector(".PodLogs .list span.active");
|
||||||
@ -145,140 +93,10 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
return logs;
|
return logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll = (props: ListOnScrollProps) => {
|
|
||||||
if (!this.logsElement.current) return;
|
|
||||||
const toBottomOffset = 100 * lineHeight; // 100 lines * 18px (height of each line)
|
|
||||||
const { scrollHeight, clientHeight } = this.logsElement.current;
|
|
||||||
const { scrollDirection, scrollOffset, scrollUpdateWasRequested } = props;
|
|
||||||
if (scrollDirection == "forward") {
|
|
||||||
if (scrollHeight - scrollOffset < toBottomOffset) {
|
|
||||||
this.showJumpToBottom = false;
|
|
||||||
}
|
|
||||||
if (clientHeight + scrollOffset === scrollHeight) {
|
|
||||||
this.lastLineIsShown = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.lastLineIsShown = false;
|
|
||||||
// Trigger loading only if scrolled by user
|
|
||||||
if (scrollOffset === 0 && !scrollUpdateWasRequested) {
|
|
||||||
this.loadMore();
|
|
||||||
}
|
|
||||||
if (scrollHeight - scrollOffset > toBottomOffset) {
|
|
||||||
this.showJumpToBottom = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@action
|
|
||||||
scrollToBottom = () => {
|
|
||||||
if (!this.virtualListRef.current) return;
|
|
||||||
this.hideHorizontalScroll = true;
|
|
||||||
this.virtualListRef.current.scrollToItem(this.logs.length, "end");
|
|
||||||
this.showJumpToBottom = false;
|
|
||||||
// Showing horizontal scrollbar after VirtualList settles down
|
|
||||||
setTimeout(() => this.hideHorizontalScroll = false, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function is called by VirtualList for rendering each of the row
|
|
||||||
* @param rowIndex {Number} index of the log element in logs array
|
|
||||||
* @returns A react element with a row itself
|
|
||||||
*/
|
|
||||||
getLogRow = (rowIndex: number) => {
|
|
||||||
const { searchQuery, isActiveOverlay } = searchStore;
|
|
||||||
const item = this.logs[rowIndex];
|
|
||||||
const contents: React.ReactElement[] = [];
|
|
||||||
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(this.colorConverter.ansi_to_html(ansi));
|
|
||||||
if (searchQuery) { // If search is enabled, replace keyword with backgrounded <span>
|
|
||||||
// Case-insensitive search (lowercasing query and keywords in line)
|
|
||||||
const regex = new RegExp(searchStore.escapeRegex(searchQuery), "gi");
|
|
||||||
const matches = item.matchAll(regex);
|
|
||||||
const modified = item.replace(regex, match => match.toLowerCase());
|
|
||||||
// Splitting text line by keyword
|
|
||||||
const pieces = modified.split(searchQuery.toLowerCase());
|
|
||||||
pieces.forEach((piece, index) => {
|
|
||||||
const active = isActiveOverlay(rowIndex, index);
|
|
||||||
const lastItem = index === pieces.length - 1;
|
|
||||||
const overlayValue = matches.next().value;
|
|
||||||
const overlay = !lastItem ?
|
|
||||||
<span
|
|
||||||
className={cssNames("overlay", { active })}
|
|
||||||
dangerouslySetInnerHTML={{ __html: ansiToHtml(overlayValue) }}
|
|
||||||
/> :
|
|
||||||
null;
|
|
||||||
contents.push(
|
|
||||||
<React.Fragment key={piece + index}>
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: ansiToHtml(piece) }} />
|
|
||||||
{overlay}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={cssNames("LogRow")}>
|
|
||||||
{contents.length > 1 ? contents : (
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: ansiToHtml(item) }} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderJumpToBottom() {
|
|
||||||
if (!this.logsElement) return null;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
className={cssNames("jump-to-bottom flex gaps", {active: this.showJumpToBottom})}
|
|
||||||
onClick={evt => {
|
|
||||||
evt.currentTarget.blur();
|
|
||||||
this.scrollToBottom();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trans>Jump to bottom</Trans>
|
|
||||||
<Icon material="expand_more" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLogs() {
|
|
||||||
// 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 <Spinner center/>;
|
|
||||||
}
|
|
||||||
if (!this.logs.length) {
|
|
||||||
return (
|
|
||||||
<div className="flex box grow align-center justify-center">
|
|
||||||
<Trans>There are no logs available for container.</Trans>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.preloading && (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Spinner center />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<VirtualList
|
|
||||||
items={this.logs}
|
|
||||||
rowHeights={rowHeights}
|
|
||||||
getRow={this.getLogRow}
|
|
||||||
onScroll={this.onScroll}
|
|
||||||
outerRef={this.logsElement}
|
|
||||||
ref={this.virtualListRef}
|
|
||||||
className="box grow"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className } = this.props;
|
|
||||||
const controls = (
|
const controls = (
|
||||||
<PodLogControls
|
<PodLogControls
|
||||||
ready={this.ready}
|
ready={!this.isLoading}
|
||||||
tabId={this.tabId}
|
tabId={this.tabId}
|
||||||
tabData={this.tabData}
|
tabData={this.tabData}
|
||||||
logs={this.logs}
|
logs={this.logs}
|
||||||
@ -290,17 +108,20 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("PodLogs flex column", className, { noscroll: this.hideHorizontalScroll })}>
|
<div className="PodLogs flex column">
|
||||||
<InfoPanel
|
<InfoPanel
|
||||||
tabId={this.props.tab.id}
|
tabId={this.props.tab.id}
|
||||||
controls={controls}
|
controls={controls}
|
||||||
showSubmitClose={false}
|
showSubmitClose={false}
|
||||||
showButtons={false}
|
showButtons={false}
|
||||||
/>
|
/>
|
||||||
<div className="logs flex">
|
<PodLogList
|
||||||
{this.renderJumpToBottom()}
|
id={this.tabId}
|
||||||
{this.renderLogs()}
|
isLoading={this.isLoading}
|
||||||
</div>
|
logs={this.logs}
|
||||||
|
load={this.load}
|
||||||
|
ref={this.logListElement}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user