fix: logs page by the data after parsing

This commit is contained in:
jialin
2025-01-12 17:04:12 +08:00
parent 04d5b692b8
commit 0de0426f72
12 changed files with 1063 additions and 312 deletions
+3 -3
View File
@@ -16,10 +16,11 @@ interface LogsListProps {
height?: number;
onScroll?: (data: { isTop: boolean; isBottom: boolean }) => void;
diffHeight?: number;
showNum?: boolean;
ref?: any;
}
const LogsList: React.FC<LogsListProps> = forwardRef((props, ref) => {
const { dataList, height, onScroll, diffHeight = 96 } = props;
const { dataList, height, showNum, onScroll, diffHeight = 96 } = props;
const {
initialize,
updateScrollerPosition,
@@ -83,7 +84,6 @@ const LogsList: React.FC<LogsListProps> = forwardRef((props, ref) => {
isBottom: false
});
}
// debounceResetStopScroll();
},
[debounceResetStopScroll, scrollEventElement]
);
@@ -136,7 +136,7 @@ const LogsList: React.FC<LogsListProps> = forwardRef((props, ref) => {
<div className={classNames('content')}>
{_.map(dataList, (item: any, index: number) => {
return (
<div key={item.uid} className="text">
<div key={item.uid} className={classNames('text')}>
{item.content}
</div>
);
@@ -0,0 +1,164 @@
import { controlSeqRegex } from './config';
let uid = 0;
const setId = () => {
uid += 1;
return uid;
};
const removeBrackets = (str: string) => {
return str?.replace?.(/^\(…\)/, '');
};
const isClean = (input: string) => {
let match = controlSeqRegex.exec(input) || [];
const command = match?.[3];
const n = parseInt(match?.[1], 10) || 1;
return command === 'J' && n === 2;
};
const parseAnsi = (input: string, setId: () => number) => {
let cursorRow = 0;
let cursorCol = 0;
let screen = [['']];
let lastIndex = 0;
let rawDataRows = 1;
// handle the \r and \n characters in the text
const handleText = (text: string) => {
let processed = '';
for (let char of text) {
if (char === '\r') {
cursorCol = 0; // move to the beginning of the line
} else if (char === '\n') {
rawDataRows++;
cursorRow++; // move to the next line
cursorCol = 0; // move to the beginning of the line
screen[cursorRow] = screen[cursorRow] || ['']; // create a new line if it does not exist
} else {
// add the character to the screen content array
screen[cursorRow][cursorCol] = char;
cursorCol++;
}
}
return processed;
};
let output = ''; // output text
let match;
// ANSI color map
const colorMap: Record<string, string> = {
'30': 'black',
'31': 'red',
'32': 'green',
'33': 'yellow',
'34': 'blue',
'35': 'magenta',
'36': 'cyan',
'37': 'white'
};
let currentStyle = ''; // current text style
// match ANSI control characters
while ((match = controlSeqRegex.exec(input)) !== null) {
// handle text before the control character
let textBeforeControl = input.slice(lastIndex, match.index);
output += handleText(textBeforeControl); // add the processed text to the output
lastIndex = controlSeqRegex.lastIndex; // update the last index
const n = parseInt(match[1], 10) || 1;
const m = parseInt(match[2], 10) || 1;
const command = match[3];
// handle ANSI control characters
switch (command) {
case 'A': // up
cursorRow = Math.max(0, cursorRow - n);
if (cursorRow === 0) {
// screen = [['']];
// cursorCol = 0;
}
break;
case 'B': // down
cursorRow += n;
break;
case 'C': // right
cursorCol += n;
break;
case 'D': // left
cursorCol = Math.max(0, cursorCol - n);
break;
case 'H': // move the cursor to the specified position (n, m)
cursorRow = Math.max(0, n - 1);
cursorCol = Math.max(0, m - 1);
break;
case 'J': // clear the screen
if (n === 2) {
console.log('clear====');
screen = [['']];
cursorRow = 0;
cursorCol = 0;
}
break;
case 'm':
// if (match[1] === '0') {
// currentStyle = '';
// } else if (colorMap[match[1]]) {
// currentStyle = `color: ${colorMap[match[1]]};`;
// }
break;
}
// check if the row and column are within the screen content array
while (screen.length <= cursorRow) {
screen.push(['']);
}
while (screen[cursorRow].length <= cursorCol) {
screen[cursorRow].push('');
}
}
// handle the remaining text
output += handleText(input.slice(lastIndex));
let result = [];
for (let row = 0; row < screen.length; row++) {
let rowContent = screen[row].join('');
result.push({
content: removeBrackets(rowContent),
uid: setId()
});
}
result.push({
content: output,
uid: setId()
});
return {
data: result,
lines: rawDataRows
};
};
self.onmessage = function (event) {
const { inputStr } = event.data;
const { data: parsedData, lines } = parseAnsi(inputStr, setId);
const result = parsedData.map((item) => ({
content: item.content,
uid: item.uid
}));
self.postMessage({
result,
lines
});
};
self.onerror = function (event) {
console.error('parse logs error===', event);
};
+112 -118
View File
@@ -1,162 +1,156 @@
import { controlSeqRegex } from './config';
let uid = 0;
const setId = () => {
uid += 1;
return uid;
};
const removeBrackets = (str: string) => {
return str?.replace?.(/^\(…\)/, '');
};
const isClean = (input: string) => {
let match = controlSeqRegex.exec(input) || [];
const command = match?.[3];
const n = parseInt(match?.[1], 10) || 1;
return command === 'J' && n === 2;
};
class AnsiParser {
private cursorRow: number = 0;
private cursorCol: number = 0;
private screen: string[][] = [['']];
private rawDataRows: number = 0;
private uid: number = 0;
private isProcessing: boolean = false;
private taskQueue: string[] = [];
const parseAnsi = (input: string, setId: () => number) => {
let cursorRow = 0;
let cursorCol = 0;
let screen = [['']];
let lastIndex = 0;
let rawDataRows = 1;
constructor() {
this.reset();
}
// handle the \r and \n characters in the text
const handleText = (text: string) => {
let processed = '';
for (let char of text) {
public reset() {
this.cursorRow = 0;
this.cursorCol = 0;
this.screen = [['']];
this.rawDataRows = 0;
this.uid = 0;
}
private setId() {
this.uid += 1;
return this.uid;
}
private handleText(text: string) {
for (const char of text) {
if (char === '\r') {
cursorCol = 0; // move to the beginning of the line
this.cursorCol = 0; // move to the beginning of the line
} else if (char === '\n') {
rawDataRows++;
cursorRow++; // move to the next line
cursorCol = 0; // move to the beginning of the line
screen[cursorRow] = screen[cursorRow] || ['']; // create a new line if it does not exist
this.rawDataRows++;
this.cursorRow++;
this.cursorCol = 0; // back to the beginning of the line
if (!this.screen[this.cursorRow]) {
this.screen[this.cursorRow] = [''];
}
} else {
// add the character to the screen content array
screen[cursorRow][cursorCol] = char;
cursorCol++;
const currentLine = this.screen[this.cursorRow];
currentLine[this.cursorCol] = char;
this.cursorCol++;
}
}
return processed;
};
}
let output = ''; // output text
let match;
// ANSI color map
const colorMap: Record<string, string> = {
'30': 'black',
'31': 'red',
'32': 'green',
'33': 'yellow',
'34': 'blue',
'35': 'magenta',
'36': 'cyan',
'37': 'white'
};
let currentStyle = ''; // current text style
// match ANSI control characters
while ((match = controlSeqRegex.exec(input)) !== null) {
// handle text before the control character
let textBeforeControl = input.slice(lastIndex, match.index);
output += handleText(textBeforeControl); // add the processed text to the output
lastIndex = controlSeqRegex.lastIndex; // update the last index
const n = parseInt(match[1], 10) || 1;
const m = parseInt(match[2], 10) || 1;
private handleAnsiSequence(match: RegExpExecArray, isEnd: boolean) {
const n = parseInt(match[1] || '1', 10);
const m = parseInt(match[2] || '1', 10);
const command = match[3];
// handle ANSI control characters
switch (command) {
case 'A': // up
cursorRow = Math.max(0, cursorRow - n);
if (cursorRow === 0) {
// screen = [['']];
// cursorCol = 0;
}
case 'A':
this.cursorRow = Math.max(0, this.cursorRow - n);
break;
case 'B': // down
cursorRow += n;
case 'B':
this.cursorRow += n;
break;
case 'C': // right
cursorCol += n;
case 'C': // move the cursor to the right
this.cursorCol += n;
break;
case 'D': // left
cursorCol = Math.max(0, cursorCol - n);
case 'D': // move the cursor to the left
this.cursorCol = Math.max(0, this.cursorCol - n);
break;
case 'H': // move the cursor to the specified position (n, m)
cursorRow = Math.max(0, n - 1);
cursorCol = Math.max(0, m - 1);
this.cursorRow = Math.max(0, n - 1);
this.cursorCol = Math.max(0, m - 1);
break;
case 'J': // clear the screen
if (n === 2) {
console.log('clear====');
screen = [['']];
cursorRow = 0;
cursorCol = 0;
this.reset();
}
break;
case 'm':
// if (match[1] === '0') {
// currentStyle = '';
// } else if (colorMap[match[1]]) {
// currentStyle = `color: ${colorMap[match[1]]};`;
// }
case 'm': // style: do not handle now
break;
}
// check if the row and column are within the screen content array
while (screen.length <= cursorRow) {
screen.push(['']);
while (this.screen.length <= this.cursorRow && !isEnd) {
this.screen.push(['']);
}
while (screen[cursorRow].length <= cursorCol) {
screen[cursorRow].push('');
while (this.screen[this.cursorRow].length <= this.cursorCol && !isEnd) {
this.screen[this.cursorRow].push('');
}
}
// handle the remaining text
output += handleText(input.slice(lastIndex));
private processInput(input: string) {
let match: RegExpExecArray | null;
let lastIndex = 0;
let result = [];
for (let row = 0; row < screen.length; row++) {
let rowContent = screen[row].join('');
result.push({
content: removeBrackets(rowContent),
uid: setId()
});
while ((match = controlSeqRegex.exec(input)) !== null) {
const textBeforeControl = input.slice(lastIndex, match.index);
this.handleText(textBeforeControl);
lastIndex = controlSeqRegex.lastIndex;
this.handleAnsiSequence(match, lastIndex === input.length - 1);
}
const remainingText = input.slice(lastIndex);
this.handleText(remainingText);
const result = this.screen.map((row) => ({
content: removeBrackets(row.join('')),
uid: this.setId()
}));
return {
data: result,
lines: this.rawDataRows
};
}
result.push({
content: output,
uid: setId()
});
return {
data: result,
lines: rawDataRows
};
};
private async processQueue(): Promise<void> {
if (this.isProcessing) {
return;
}
this.isProcessing = true;
while (this.taskQueue.length > 0) {
const input = this.taskQueue.join('');
this.taskQueue = [];
const result = this.processInput(input);
self.postMessage({
result: result.data,
lines: result.lines
});
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
this.isProcessing = false;
}
public enqueueData(input: string): void {
this.taskQueue.push(input);
this.processQueue();
}
}
const parser = new AnsiParser();
self.onmessage = function (event) {
const { inputStr } = event.data;
const { data: parsedData, lines } = parseAnsi(inputStr, setId);
const result = parsedData.map((item) => ({
content: item.content,
uid: item.uid
}));
self.postMessage({
result,
lines
});
parser.enqueueData(inputStr);
};
self.onerror = function (event) {
@@ -15,6 +15,22 @@
.text {
min-height: 22px;
&.numable {
position: relative;
padding-left: 45px;
.line-num {
display: flex;
align-items: center;
width: 40px;
justify-content: center;
position: absolute;
left: 0;
top: 0;
background-color: rgba(71, 71, 71, 50%);
}
}
}
color: var(--color-logs-text);
@@ -0,0 +1,50 @@
.logs-viewer-wrap-w2 {
.wrap {
padding: 5px 0 5px 10px;
background-color: var(--color-logs-bg);
border-radius: var(--border-radius-mini);
overflow: hidden;
.content {
word-wrap: break-word;
height: 100%;
&.line-break {
word-wrap: break-word;
}
.text {
height: 100%;
}
color: var(--color-logs-text);
font-size: var(--font-size-small);
line-height: 22px;
white-space: pre-wrap;
background-color: var(--color-logs-bg);
}
}
.xterm {
// height: 100% !important;
.xterm-viewport {
overflow-y: auto !important;
&::-webkit-scrollbar {
width: var(--scrollbar-size);
height: var(--scrollbar-size);
}
&::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: var(--color-scrollbar-track);
border-radius: 4px;
}
}
}
}
+50 -24
View File
@@ -1,15 +1,24 @@
import _, { throttle } from 'lodash';
import List from 'rc-virtual-list';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState
} from 'react';
import './styles/logs-list.less';
interface LogsInnerProps {
ref?: any;
data: { content: string; uid: number }[];
onScroll?: (e: any) => void;
onScroll?: (data: { isTop: boolean; isBottom: boolean }) => void;
diffHeight?: number;
}
const LogsInner: React.FC<LogsInnerProps> = (props) => {
const { data, diffHeight = 96 } = props;
const LogsInner: React.FC<LogsInnerProps> = forwardRef((props, ref) => {
const { data, diffHeight = 96, onScroll } = props;
const viewportHeight = window.innerHeight;
const viewHeight = viewportHeight - diffHeight;
const [innerHieght, setInnerHeight] = useState(viewHeight);
@@ -31,13 +40,21 @@ const LogsInner: React.FC<LogsInnerProps> = (props) => {
[data, scroller.current, stopScroll.current]
);
const debounceResetStopScroll = _.debounce(() => {
stopScroll.current = false;
}, 30000);
const updataPositionToTop = useCallback(
throttle(() => {
if (!stopScroll.current && data.length > 0) {
scroller.current?.scrollTo?.({
index: 0,
align: 'bottom'
});
}
}, 200),
[data, scroller.current, stopScroll.current]
);
const updatePositionToTop = useCallback(
_.throttle((isTop: boolean) => {
props.onScroll?.(isTop);
_.throttle((data: { isTop: boolean; isBottom: boolean }) => {
props.onScroll?.(data);
}, 200),
[props.onScroll]
);
@@ -61,20 +78,28 @@ const LogsInner: React.FC<LogsInnerProps> = (props) => {
return virtualList.scrollTop <= 0;
}, []);
const handleOnScroll = useCallback(
(e: any) => {
const isBottom = isScrollBottom(logsWrapper.current);
const isTop = isScrollTop(logsWrapper.current);
if (isBottom) {
stopScroll.current = false;
} else {
stopScroll.current = true;
}
debounceResetStopScroll();
updatePositionToTop(isTop);
const handleOnScroll = useCallback((e: any) => {
const isBottom = isScrollBottom(logsWrapper.current);
const isTop = isScrollTop(logsWrapper.current);
if (isBottom) {
stopScroll.current = false;
} else {
stopScroll.current = true;
}
updatePositionToTop({
isTop,
isBottom
});
}, []);
useImperativeHandle(ref, () => ({
scrollToBottom() {
updataPositionToBottom();
},
[debounceResetStopScroll]
);
scrollToTop() {
updataPositionToTop();
}
}));
useEffect(() => {
updataPositionToBottom();
@@ -92,7 +117,7 @@ const LogsInner: React.FC<LogsInnerProps> = (props) => {
};
}, [diffHeight]);
return (
<div ref={logsWrapper}>
<div ref={logsWrapper} className="logs-wrap" style={{ height: '100%' }}>
<List
ref={scroller}
onScroll={handleOnScroll}
@@ -100,6 +125,7 @@ const LogsInner: React.FC<LogsInnerProps> = (props) => {
itemHeight={22}
height={innerHieght}
itemKey="uid"
className="content"
styles={{
verticalScrollBar: {
width: 'var(--scrollbar-size)'
@@ -118,6 +144,6 @@ const LogsInner: React.FC<LogsInnerProps> = (props) => {
</List>
</div>
);
};
});
export default React.memo(LogsInner);
@@ -0,0 +1,377 @@
import useSetChunkFetch from '@/hooks/use-chunk-fetch';
import { Spin } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import React, {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState
} from 'react';
import { controlSeqRegex, replaceLineRegex } from './config';
import LogsList from './logs-list';
import LogsPagination from './logs-pagination';
import './styles/index.less';
import useLogsPagination from './use-logs-pagination';
interface LogsViewerProps {
height: number;
content?: string;
url: string;
params?: object;
ref?: any;
tail?: number;
enableScorllLoad?: boolean;
diffHeight?: number;
}
const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const { diffHeight, url, tail: defaultTail, enableScorllLoad = true } = props;
const { pageSize, page, setPage, setTotalPage, totalPage } =
useLogsPagination();
const { setChunkFetch } = useSetChunkFetch();
const chunkRequedtRef = useRef<any>(null);
const cacheDataRef = useRef<any>('');
const [logs, setLogs] = useState<any[]>([]);
const logParseWorker = useRef<any>(null);
const tail = useRef<any>(defaultTail);
const [isLoadend, setIsLoadend] = useState(false);
const [loading, setLoading] = useState(false);
const [isAtTop, setIsAtTop] = useState(false);
const [scrollPos, setScrollPos] = useState<any[]>([]);
const logListRef = useRef<any>(null);
const loadMoreDone = useRef(false);
const pageRef = useRef<any>(page);
const totalPageRef = useRef<any>(totalPage);
const isLoadingMoreRef = useRef(false);
const scrollPosRef = useRef<any>({
pos: 'bottom',
page: 1
});
const dataLengthRef = useRef(0);
const lineCountRef = useRef(0);
useImperativeHandle(ref, () => ({
abort() {
chunkRequedtRef.current?.current?.abort?.();
logParseWorker.current?.terminate?.();
}
}));
useEffect(() => {
logParseWorker.current?.terminate?.();
logParseWorker.current = new Worker(
// @ts-ignore
new URL('./parse-worker.ts', import.meta.url),
{
type: 'module'
}
);
logParseWorker.current.onmessage = (event: any) => {
const { result, lines } = event.data;
lineCountRef.current = lines;
setLogs(result);
};
return () => {
if (logParseWorker.current) {
logParseWorker.current.terminate();
}
};
}, []);
const debounceLoading = _.debounce(() => {
setLoading(false);
isLoadingMoreRef.current = false;
}, 200);
const isClean = useCallback((input: string) => {
let match = controlSeqRegex.exec(input) || [];
const command = match?.[3];
const n = parseInt(match?.[1], 10) || 1;
return command === 'J' && n === 2;
}, []);
const getLastPage = (data: string) => {
const list = _.split(data.trim(), '\n');
let result = '';
const totalPage = Math.ceil(list.length / pageSize);
pageRef.current = totalPage;
totalPageRef.current = totalPage;
const lastPageLogs = list.slice(-pageSize).join('\n');
result = lastPageLogs;
setPage(totalPage);
setTotalPage(totalPage);
setScrollPos(['bottom', totalPage]);
scrollPosRef.current = {
pos: 'bottom',
page: totalPage
};
return result;
};
const getCurrentPage = () => {
const list = _.split(cacheDataRef.current.trim(), '\n');
const totalPage = Math.ceil(list.length / pageSize);
let newPage = pageRef.current;
if (newPage < 1) {
newPage = 1;
}
if (isLoadingMoreRef.current) {
setLoading(true);
}
const start = (newPage - 1) * pageSize;
const end = newPage * pageSize;
const currentPage = list.slice(start, end).join('\n');
setPage(newPage);
setTotalPage(totalPage);
if (
pageRef.current === totalPageRef.current &&
scrollPosRef.current.pos === 'bottom'
) {
setScrollPos(['bottom', newPage]);
scrollPosRef.current = {
pos: 'bottom',
page: newPage
};
}
debounceLoading();
pageRef.current = newPage;
logParseWorker.current.postMessage({
inputStr: currentPage
});
};
const getPrePage = useCallback(() => {
const list = _.split(cacheDataRef.current.trim(), '\n');
let newPage = page - 1;
if (newPage < 1) {
newPage = 1;
}
const start = (newPage - 1) * pageSize;
const end = newPage * pageSize;
const prePage = list.slice(start, end).join('\n');
setPage(() => newPage);
setScrollPos(['bottom', newPage]);
scrollPosRef.current = {
pos: 'bottom',
page: newPage
};
pageRef.current = newPage;
logParseWorker.current.postMessage({
inputStr: prePage
});
}, [page, pageSize]);
const getNextPage = useCallback(() => {
const list = _.split(cacheDataRef.current.trim(), '\n');
let newPage = page + 1;
if (newPage > totalPage) {
newPage = totalPage;
}
const start = (newPage - 1) * pageSize;
const end = newPage * pageSize;
const nextPage = list.slice(start, end).join('\n');
setPage(() => newPage);
setScrollPos(['top', newPage]);
scrollPosRef.current = {
pos: 'top',
page: newPage
};
pageRef.current = newPage;
logParseWorker.current.postMessage({
inputStr: nextPage
});
}, [totalPage, page, pageSize]);
const handleonBackend = useCallback(() => {
const list = _.split(cacheDataRef.current.trim(), '\n');
let newPage = totalPage;
const start = (newPage - 1) * pageSize;
const end = newPage * pageSize;
const nextPage = list.slice(start, end).join('\n');
setPage(() => newPage);
setScrollPos(['bottom', newPage]);
scrollPosRef.current = {
pos: 'bottom',
page: newPage
};
pageRef.current = totalPage;
totalPageRef.current = totalPage;
logParseWorker.current.postMessage({
inputStr: nextPage
});
}, [totalPage, page, pageSize]);
const updateContent = (inputStr: string) => {
const data = inputStr.replace(replaceLineRegex, '\n');
if (isClean(data)) {
cacheDataRef.current = data;
} else {
cacheDataRef.current += data;
}
if (
pageRef.current === totalPageRef.current &&
scrollPosRef.current.pos === 'bottom'
) {
logParseWorker.current.postMessage({
inputStr: getLastPage(cacheDataRef.current)
});
} else {
getCurrentPage();
}
};
const createChunkConnection = async () => {
cacheDataRef.current = '';
chunkRequedtRef.current?.current?.abort?.();
chunkRequedtRef.current = setChunkFetch({
url,
params: {
...props.params,
tail: tail.current,
watch: true
},
contentType: 'text',
handler: updateContent
});
};
const handleOnScroll = useCallback(
async (data: { isTop: boolean; isBottom: boolean }) => {
const { isTop, isBottom } = data;
setIsAtTop(isTop);
console.log('scroll========', {
isTop,
isBottom,
loadMoreDone: loadMoreDone.current,
loading: loading,
lineCount: lineCountRef.current,
dataLength: dataLengthRef.current
});
if (isBottom) {
scrollPosRef.current = {
pos: 'bottom',
page: page
};
} else if (isTop) {
scrollPosRef.current = {
pos: 'top',
page: page
};
} else {
scrollPosRef.current = {
pos: 'middle',
page: page
};
}
if (
loading ||
(logs.length > 0 &&
lineCountRef.current < pageSize &&
!loadMoreDone.current) ||
!enableScorllLoad
) {
return;
}
if (isTop && !loadMoreDone.current) {
tail.current = undefined;
createChunkConnection();
loadMoreDone.current = true;
isLoadingMoreRef.current = true;
} else if (isTop && page <= totalPage && page > 1) {
// getPrePage();
} else if (isBottom && page < totalPage) {
// getNextPage();
}
},
[
loading,
logs.length,
pageSize,
enableScorllLoad,
page,
totalPage,
setScrollPos,
createChunkConnection
]
);
const debouncedScroll = useCallback(
_.debounce(() => {
console.log('scrollPos+++++++++++=', scrollPos);
if (scrollPos[0] === 'top' && scrollPosRef.current.pos === 'top') {
logListRef.current?.scrollToTop();
}
if (scrollPos[0] === 'bottom' && scrollPosRef.current.pos === 'bottom') {
logListRef.current?.scrollToBottom();
}
}, 150),
[scrollPos]
);
useEffect(() => {
createChunkConnection();
return () => {
chunkRequedtRef.current?.current?.abort?.();
};
}, [url, props.params]);
useEffect(() => {
debouncedScroll();
}, [scrollPos]);
return (
<div className="logs-viewer-wrap-w2">
<div className="wrap">
<div>
<LogsList
ref={logListRef}
dataList={logs}
diffHeight={diffHeight}
onScroll={handleOnScroll}
></LogsList>
</div>
<Spin
spinning={loading && isAtTop}
className={classNames({
loading: loading && isAtTop
})}
></Spin>
{totalPage > 1 && (
<div className="pg">
<div
className={classNames('pg-inner', {
'at-top': isAtTop
})}
>
<LogsPagination
page={page}
total={totalPage}
pageSize={pageSize}
onNext={getNextPage}
onPrev={getPrePage}
onBackend={handleonBackend}
></LogsPagination>
</div>
</div>
)}
</div>
</div>
);
});
export default memo(LogsViewer);
+85 -156
View File
@@ -11,7 +11,7 @@ import React, {
useRef,
useState
} from 'react';
import { controlSeqRegex, replaceLineRegex } from './config';
import { replaceLineRegex } from './config';
import LogsList from './logs-list';
import LogsPagination from './logs-pagination';
import './styles/index.less';
@@ -36,8 +36,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const cacheDataRef = useRef<any>('');
const [logs, setLogs] = useState<any[]>([]);
const logParseWorker = useRef<any>(null);
const tail = useRef<any>(defaultTail);
const [isLoadend, setIsLoadend] = useState(false);
const tail = useRef<any>(pageSize - 1);
const [loading, setLoading] = useState(false);
const [isAtTop, setIsAtTop] = useState(false);
const [scrollPos, setScrollPos] = useState<any[]>([]);
@@ -46,6 +45,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const pageRef = useRef<any>(page);
const totalPageRef = useRef<any>(totalPage);
const isLoadingMoreRef = useRef(false);
const [currentData, setCurrentData] = useState<any[]>([]);
const scrollPosRef = useRef<any>({
pos: 'bottom',
page: 1
@@ -60,181 +60,70 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
}
}));
useEffect(() => {
logParseWorker.current?.terminate?.();
logParseWorker.current = new Worker(
// @ts-ignore
new URL('./parse-worker.ts', import.meta.url),
{
type: 'module'
}
);
logParseWorker.current.onmessage = (event: any) => {
const { result, lines } = event.data;
lineCountRef.current = lines;
setLogs(result);
};
return () => {
if (logParseWorker.current) {
logParseWorker.current.terminate();
}
};
}, []);
const debounceLoading = _.debounce(() => {
setLoading(false);
isLoadingMoreRef.current = false;
}, 200);
const isClean = useCallback((input: string) => {
let match = controlSeqRegex.exec(input) || [];
const command = match?.[3];
const n = parseInt(match?.[1], 10) || 1;
return command === 'J' && n === 2;
}, []);
const getLastPage = (data: string) => {
const list = _.split(data.trim(), '\n');
let result = '';
const totalPage = Math.ceil(list.length / pageSize);
pageRef.current = totalPage;
totalPageRef.current = totalPage;
const lastPageLogs = list.slice(-pageSize).join('\n');
result = lastPageLogs;
setPage(totalPage);
setTotalPage(totalPage);
setScrollPos(['bottom', totalPage]);
scrollPosRef.current = {
pos: 'bottom',
page: totalPage
};
return result;
};
const getCurrentPage = () => {
const list = _.split(cacheDataRef.current.trim(), '\n');
const totalPage = Math.ceil(list.length / pageSize);
let newPage = pageRef.current;
if (newPage < 1) {
newPage = 1;
const getCurrent = useCallback(() => {
if (pageRef.current < 1) {
pageRef.current = 1;
}
if (isLoadingMoreRef.current) {
setLoading(true);
}
const start = (newPage - 1) * pageSize;
const end = newPage * pageSize;
const currentPage = list.slice(start, end).join('\n');
setPage(newPage);
setTotalPage(totalPage);
if (
pageRef.current === totalPageRef.current &&
scrollPosRef.current.pos === 'bottom'
) {
setScrollPos(['bottom', newPage]);
scrollPosRef.current = {
pos: 'bottom',
page: newPage
};
}
debounceLoading();
pageRef.current = newPage;
logParseWorker.current.postMessage({
inputStr: currentPage
});
};
const start = (pageRef.current - 1) * pageSize;
const end = pageRef.current * pageSize;
const currentLogs = logs.slice(start, end);
setPage(pageRef.current);
setCurrentData(currentLogs);
}, [logs, pageSize]);
const getPrePage = useCallback(() => {
const list = _.split(cacheDataRef.current.trim(), '\n');
let newPage = page - 1;
if (newPage < 1) {
newPage = 1;
}
const start = (newPage - 1) * pageSize;
const end = newPage * pageSize;
const prePage = list.slice(start, end).join('\n');
pageRef.current = pageRef.current - 1;
setPage(() => newPage);
setScrollPos(['bottom', newPage]);
getCurrent();
setScrollPos(['bottom', pageRef.current]);
scrollPosRef.current = {
pos: 'bottom',
page: newPage
page: pageRef.current
};
pageRef.current = newPage;
logParseWorker.current.postMessage({
inputStr: prePage
});
}, [page, pageSize]);
}, [getCurrent]);
const getNextPage = useCallback(() => {
const list = _.split(cacheDataRef.current.trim(), '\n');
let newPage = page + 1;
if (newPage > totalPage) {
newPage = totalPage;
}
const start = (newPage - 1) * pageSize;
const end = newPage * pageSize;
const nextPage = list.slice(start, end).join('\n');
pageRef.current = pageRef.current + 1;
setPage(() => newPage);
setScrollPos(['top', newPage]);
getCurrent();
setScrollPos(['top', pageRef.current]);
scrollPosRef.current = {
pos: 'top',
page: newPage
page: pageRef.current
};
pageRef.current = newPage;
logParseWorker.current.postMessage({
inputStr: nextPage
});
}, [totalPage, page, pageSize]);
}, [getCurrent]);
const handleonBackend = useCallback(() => {
const list = _.split(cacheDataRef.current.trim(), '\n');
let newPage = totalPage;
const start = (newPage - 1) * pageSize;
const end = newPage * pageSize;
const nextPage = list.slice(start, end).join('\n');
setPage(() => newPage);
setScrollPos(['bottom', newPage]);
pageRef.current = totalPageRef.current;
getCurrent();
setPage(pageRef.current);
console.log('pageRef.current', pageRef.current);
setScrollPos(['bottom', pageRef.current]);
scrollPosRef.current = {
pos: 'bottom',
page: newPage
page: pageRef.current
};
pageRef.current = totalPage;
totalPageRef.current = totalPage;
logParseWorker.current.postMessage({
inputStr: nextPage
});
}, [totalPage, page, pageSize]);
}, [getCurrent]);
const updateContent = (inputStr: string) => {
const data = inputStr.replace(replaceLineRegex, '\n');
if (isClean(data)) {
cacheDataRef.current = data;
} else {
cacheDataRef.current += data;
}
if (
pageRef.current === totalPageRef.current &&
scrollPosRef.current.pos === 'bottom'
) {
logParseWorker.current.postMessage({
inputStr: getLastPage(cacheDataRef.current)
});
} else {
getCurrentPage();
cacheDataRef.current = data;
if (isLoadingMoreRef.current) {
setLoading(true);
}
logParseWorker.current.postMessage({
inputStr: data
});
};
const createChunkConnection = async () => {
cacheDataRef.current = '';
chunkRequedtRef.current?.current?.abort?.();
chunkRequedtRef.current = setChunkFetch({
@@ -258,8 +147,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
isBottom,
loadMoreDone: loadMoreDone.current,
loading: loading,
lineCount: lineCountRef.current,
dataLength: dataLengthRef.current
lineCount: lineCountRef.current
});
if (isBottom) {
scrollPosRef.current = {
@@ -280,7 +168,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
if (
loading ||
(logs.length > 0 &&
lineCountRef.current < pageSize &&
lineCountRef.current < pageSize - 1 &&
!loadMoreDone.current) ||
!enableScorllLoad
) {
@@ -312,11 +200,10 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const debouncedScroll = useCallback(
_.debounce(() => {
console.log('scrollPos+++++++++++=', scrollPos);
if (scrollPos[0] === 'top' && scrollPosRef.current.pos === 'top') {
logListRef.current?.scrollToTop();
}
if (scrollPos[0] === 'bottom' && scrollPosRef.current.pos === 'bottom') {
if (scrollPosRef.current.pos === 'bottom') {
logListRef.current?.scrollToBottom();
}
}, 150),
@@ -334,21 +221,63 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
debouncedScroll();
}, [scrollPos]);
useEffect(() => {
logParseWorker.current?.terminate?.();
logParseWorker.current = new Worker(
// @ts-ignore
new URL('./parse-worker.ts', import.meta.url),
{
type: 'module'
}
);
logParseWorker.current.onmessage = (event: any) => {
const { result, lines } = event.data;
lineCountRef.current = lines;
if (pageRef.current < 1) {
pageRef.current = 1;
}
const start = (pageRef.current - 1) * pageSize;
const end = pageRef.current * pageSize;
const currentLogs = result.slice(start, end);
totalPageRef.current = Math.ceil(result.length / pageSize);
console.log(
'lineCountRef.current+++++++++++',
lineCountRef.current,
result.length
);
setLogs(result);
setTotalPage(totalPageRef.current);
setPage(pageRef.current);
setCurrentData(currentLogs);
debounceLoading();
};
return () => {
if (logParseWorker.current) {
logParseWorker.current.terminate();
}
};
}, []);
return (
<div className="logs-viewer-wrap-w2">
<div className="wrap">
<div>
<LogsList
ref={logListRef}
dataList={logs}
dataList={currentData}
diffHeight={diffHeight}
onScroll={handleOnScroll}
></LogsList>
</div>
<Spin
spinning={loading && isAtTop}
spinning={loading}
className={classNames({
loading: loading && isAtTop
loading: loading
})}
></Spin>
{totalPage > 1 && (
+181
View File
@@ -0,0 +1,181 @@
import useSetChunkRequest from '@/hooks/use-chunk-request';
import { FitAddon } from '@xterm/addon-fit';
import { WebglAddon } from '@xterm/addon-webgl';
import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
import classNames from 'classnames';
import _ from 'lodash';
import {
forwardRef,
memo,
useEffect,
useImperativeHandle,
useRef,
useState
} from 'react';
import { replaceLineRegex } from './config';
import './styles/xterm-viewer.less';
import useSize from './use-size';
interface LogsViewerProps {
height: number;
content?: string;
url: string;
ref?: any;
params?: object;
}
const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const { height, content, url } = props;
const { setChunkRequest } = useSetChunkRequest();
const chunkRequedtRef = useRef<any>(null);
const scroller = useRef<any>({});
const termRef = useRef<any>({});
const termwrapRef = useRef<any>({});
const fitAddonRef = useRef<any>({});
const cacheDataRef = useRef<any>(null);
const [logs, setLogs] = useState('');
const [loading, setLoading] = useState(false);
const size = useSize(scroller);
const logParseWorker = useRef<any>(null);
const lineCountRef = useRef(0);
useImperativeHandle(ref, () => ({
abort() {
chunkRequedtRef.current?.current?.abort?.();
logParseWorker.current?.terminate?.();
}
}));
useEffect(() => {
logParseWorker.current?.terminate?.();
logParseWorker.current = new Worker(
// @ts-ignore
new URL('./parse-worker.ts', import.meta.url),
{
type: 'module'
}
);
logParseWorker.current.onmessage = (event: any) => {
const { result, lines } = event.data;
lineCountRef.current = lines;
// setLogs(result.join('\n'));
console.log('res+++++++++++', result);
const data = result.map((item: any) => item.content);
termRef.current?.write?.(data.join('\n'));
};
return () => {
if (logParseWorker.current) {
logParseWorker.current.terminate();
}
};
}, []);
const throttleScroll = _.throttle(() => {
termRef.current?.scrollToBottom?.();
}, 100);
const debounceLoading = _.debounce(() => {
setLoading(false);
}, 200);
const updateContent = (inputStr: string) => {
const data = inputStr.replace(replaceLineRegex, '\n');
cacheDataRef.current = data;
setLoading(true);
logParseWorker.current.postMessage({
inputStr: data
});
debounceLoading();
};
const fitTerm = () => {
fitAddonRef.current?.fit?.();
};
const createChunkConnection = async () => {
chunkRequedtRef.current?.current?.cancel?.();
chunkRequedtRef.current = setChunkRequest({
url,
params: {
...props.params,
watch: true
},
contentType: 'text',
handler: updateContent
});
};
const initTerm = () => {
termRef.current?.dispose?.();
termRef.current = new Terminal({
lineHeight: 1.2,
fontSize: 13,
fontFamily:
"monospace,Menlo,Courier,'Courier New',Consolas,Monaco, 'Liberation Mono'",
disableStdin: true,
convertEol: true,
theme: {
background: '#1e1e1e',
foreground: 'rgba(255,255,255,0.8)'
},
cursorInactiveStyle: 'none',
smoothScrollDuration: 0
});
fitAddonRef.current = new FitAddon();
termRef.current.loadAddon(fitAddonRef.current);
termRef.current.loadAddon(new WebglAddon());
termRef.current.open(termwrapRef.current);
// add event
// termRef.current.onLineFeed((e: any) => {
// if (cacheDataRef.current) {
// throttleScroll();
// }
// });
};
const handleResize = _.throttle(() => {
fitTerm();
}, 100);
useEffect(() => {
createChunkConnection();
return () => {
chunkRequedtRef.current?.current?.cancel?.();
};
}, [url, props.params]);
useEffect(() => {
if (termwrapRef.current) {
initTerm();
}
return () => {
termRef.current?.dispose?.();
};
}, [termwrapRef.current]);
useEffect(() => {
if (size) {
handleResize();
}
}, [size]);
useEffect(() => {
// throttleScroll();
}, [logs]);
return (
<div className="logs-viewer-wrap-w2">
<div className="wrap" style={{ height: height }} ref={scroller}>
<div className={classNames('content')}>
<div className="text" ref={termwrapRef}></div>
</div>
</div>
</div>
);
});
export default memo(LogsViewer);