1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/renderer/components/dock/logs/log-list.tsx
Alex Andreev 968621f7c6 Fix scroll position when more logs loaded
Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
2022-09-02 13:46:44 +03:00

175 lines
5.1 KiB
TypeScript

import { useVirtualizer } from '@tanstack/react-virtual';
import AnsiUp from 'ansi_up';
import DOMPurify from 'dompurify';
import { observer } from 'mobx-react';
import React, { useEffect, useRef } from 'react';
import { SearchStore } from '../../../search-store/search-store';
import { cssNames } from '../../../utils';
import type { LogTabViewModel } from './logs-view-model';
export interface LogListProps {
model: LogTabViewModel;
}
export const LogList = observer(({ model }: LogListProps) => {
const [toBottomVisible, setToBottomVisible] = React.useState(false);
const [lastLineVisible, setLastLineVisible] = React.useState(true);
const { visibleLogs } = model;
const parentRef = useRef<HTMLDivElement>(null)
const rowVirtualizer = useVirtualizer({
count: visibleLogs.get().length,
getScrollElement: () => parentRef.current,
estimateSize: () => 38,
overscan: 5,
scrollPaddingEnd: 0,
scrollPaddingStart: 0,
});
const onScroll = (event: React.UIEvent<HTMLDivElement>) => {
if (!parentRef.current) return;
setToBottomVisibility();
setLastLineVisibility();
onScrollToTop();
}
// TODO: Move to its own hook
const setToBottomVisibility = () => {
const { scrollTop, scrollHeight } = parentRef.current as HTMLDivElement;
// console.log("scrolling", scrollHeight, scrollTop, rowVirtualizer.getTotalSize())
if (scrollHeight - scrollTop > 4000) {
setToBottomVisible(true);
} else {
setToBottomVisible(false);
}
}
const setLastLineVisibility = () => {
const { scrollTop, scrollHeight } = parentRef.current as HTMLDivElement;
if (scrollHeight - scrollTop < 4000) {
setLastLineVisible(true);
} else {
setLastLineVisible(false);
}
}
/**
* Check if user scrolled to top and new logs should be loaded
*/
const onScrollToTop = async () => {
const { scrollTop } = parentRef.current as HTMLDivElement;
if (scrollTop === 0) {
const logs = model.logs.get();
const firstLog = logs[0];
await model.loadLogs();
const scrollToIndex = model.logs.get().findIndex(log => log === firstLog);
rowVirtualizer.scrollToIndex(scrollToIndex, { align: 'start', smoothScroll: false });
}
};
useEffect(() => {
setTimeout(() => {
// Initial scroll to bottom
rowVirtualizer.scrollToIndex(visibleLogs.get().length - 1, { align: 'end', smoothScroll: false });
}, 200)
}, [])
return (
<div
ref={parentRef}
style={{
flexGrow: 1,
overflow: 'auto', // Make it scroll!
}}
onScroll={onScroll}
>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.index}
ref={virtualRow.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div>
<LogRow rowIndex={virtualRow.index} model={model} />
</div>
</div>
))}
<div style={{
width: "100%",
height: "1px",
background: "red",
position: "absolute",
bottom: 0,
}}></div>
</div>
</div>
)
});
const colorConverter = new AnsiUp();
function LogRow({ rowIndex, model }: { rowIndex: number; model: LogTabViewModel }) {
const { searchQuery, isActiveOverlay } = model.searchStore;
const log = model.visibleLogs.get()[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 = log.matchAll(regex);
const modified = log.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(log) }} />
)}
{/* For preserving copy-paste experience and keeping line breaks */}
<br />
</div>
);
}