mirror of
https://github.com/lensapp/lens.git
synced 2024-11-10 10:36:25 +03: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 { observer } from "mobx-react";
|
||||
import { IPodLogsData, podLogsStore } from "./pod-logs.store";
|
||||
@ -21,10 +22,9 @@ interface Props extends PodLogSearchProps {
|
||||
}
|
||||
|
||||
export const PodLogControls = observer((props: Props) => {
|
||||
if (!props.ready) return null;
|
||||
const { tabData, save, reload, tabId, logs } = props;
|
||||
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 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 AnsiUp from 'ansi_up';
|
||||
import DOMPurify from "dompurify";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { action, computed, observable, reaction } from "mobx";
|
||||
import { computed, observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { autobind, cssNames } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { Spinner } from "../spinner";
|
||||
|
||||
import { searchStore } from "../../../common/search-store";
|
||||
import { autobind } from "../../utils";
|
||||
import { IDockTab } from "./dock.store";
|
||||
import { InfoPanel } from "./info-panel";
|
||||
import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store";
|
||||
import { Button } from "../button";
|
||||
import { PodLogControls } from "./pod-log-controls";
|
||||
import { VirtualList } from "../virtual-list";
|
||||
import { searchStore } from "../../../common/search-store";
|
||||
import { ListOnScrollProps } from "react-window";
|
||||
import { PodLogList } from "./pod-log-list";
|
||||
import { IPodLogsData, podLogsStore } from "./pod-logs.store";
|
||||
|
||||
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<Props> {
|
||||
@observable ready = false;
|
||||
@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
|
||||
@observable isLoading = true;
|
||||
|
||||
private logsElement = React.createRef<HTMLDivElement>(); // A reference for outer container in VirtualList
|
||||
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();
|
||||
private logListElement = React.createRef<PodLogList>(); // A reference for VirtualList component
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.tab.id, async () => {
|
||||
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;
|
||||
}
|
||||
disposeOnUnmount(this,
|
||||
reaction(() => this.props.tab.id, this.reload, { fireImmediately: true })
|
||||
);
|
||||
}
|
||||
|
||||
get tabData() {
|
||||
@ -76,33 +41,16 @@ export class PodLogs extends React.Component<Props> {
|
||||
}
|
||||
|
||||
load = async () => {
|
||||
this.ready = false;
|
||||
this.isLoading = true;
|
||||
await podLogsStore.load(this.tabId);
|
||||
this.ready = true;
|
||||
this.isLoading = false;
|
||||
};
|
||||
|
||||
reload = async () => {
|
||||
podLogsStore.clearLogs(this.tabId);
|
||||
this.lastLineIsShown = true;
|
||||
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
|
||||
* @param query {string} A text from search field
|
||||
@ -118,9 +66,9 @@ export class PodLogs extends React.Component<Props> {
|
||||
@autobind()
|
||||
toOverlay() {
|
||||
const { activeOverlayLine } = searchStore;
|
||||
if (!this.virtualListRef.current || activeOverlayLine === undefined) return;
|
||||
if (!this.logListElement.current || activeOverlayLine === undefined) return;
|
||||
// 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
|
||||
setTimeout(() => {
|
||||
const overlay = document.querySelector(".PodLogs .list span.active");
|
||||
@ -145,140 +93,10 @@ export class PodLogs extends React.Component<Props> {
|
||||
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() {
|
||||
const { className } = this.props;
|
||||
const controls = (
|
||||
<PodLogControls
|
||||
ready={this.ready}
|
||||
ready={!this.isLoading}
|
||||
tabId={this.tabId}
|
||||
tabData={this.tabData}
|
||||
logs={this.logs}
|
||||
@ -290,17 +108,20 @@ export class PodLogs extends React.Component<Props> {
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className={cssNames("PodLogs flex column", className, { noscroll: this.hideHorizontalScroll })}>
|
||||
<div className="PodLogs flex column">
|
||||
<InfoPanel
|
||||
tabId={this.props.tab.id}
|
||||
controls={controls}
|
||||
showSubmitClose={false}
|
||||
showButtons={false}
|
||||
/>
|
||||
<div className="logs flex">
|
||||
{this.renderJumpToBottom()}
|
||||
{this.renderLogs()}
|
||||
</div>
|
||||
<PodLogList
|
||||
id={this.tabId}
|
||||
isLoading={this.isLoading}
|
||||
logs={this.logs}
|
||||
load={this.load}
|
||||
ref={this.logListElement}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user