style: replace virtual inner with logs list

This commit is contained in:
jialin
2024-11-06 09:29:12 +08:00
parent f684a3b0ae
commit 8bc67b323c
8 changed files with 144 additions and 242 deletions
+111
View File
@@ -0,0 +1,111 @@
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import classNames from 'classnames';
import _, { throttle } from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import './styles/logs-list.less';
interface LogsListProps {
dataList: any[];
height?: number;
onScroll?: (isTop: boolean) => void;
diffHeight?: number;
}
const LogsList: React.FC<LogsListProps> = (props) => {
const { dataList, height, onScroll, diffHeight = 96 } = props;
const {
initialize,
updateScrollerPosition,
generateInstance,
scrollEventElement,
instance,
initialized
} = useOverlayScroller({
theme: 'os-theme-light'
});
const viewportHeight = window.innerHeight;
const viewHeight = viewportHeight - diffHeight;
const [innerHieght, setInnerHeight] = useState(viewHeight);
const scroller = useRef<any>({});
const stopScroll = useRef(false);
const debounceResetStopScroll = _.debounce(() => {
stopScroll.current = false;
}, 30000);
const handleOnWheel = useCallback(
(e: any) => {
const scrollTop = scrollEventElement?.scrollTop;
const scrollHeight = scrollEventElement?.scrollHeight;
const clientHeight = scrollEventElement?.clientHeight;
// is scroll to bottom
stopScroll.current = scrollTop + clientHeight <= scrollHeight;
// is scroll to top
if (scrollTop === 0) {
onScroll?.(true);
} else {
onScroll?.(false);
}
debounceResetStopScroll();
},
[debounceResetStopScroll, scrollEventElement]
);
const debounceUpdateScrollerPosition = _.debounce(() => {
generateInstance();
updateScrollerPosition(0);
}, 200);
useEffect(() => {
const handleResize = throttle(() => {
const viewportHeight = window.innerHeight;
const viewHeight = viewportHeight - diffHeight;
setInnerHeight(viewHeight);
}, 100);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [diffHeight]);
useEffect(() => {
if (scroller.current) {
initialize(scroller.current);
}
}, [scroller.current, initialize]);
useEffect(() => {
if (dataList.length && !stopScroll.current && instance) {
updateScrollerPosition(0);
} else if (dataList.length && !stopScroll.current && scroller.current) {
if (!initialized) {
initialize(scroller.current);
}
if (!instance) {
debounceUpdateScrollerPosition();
} else {
updateScrollerPosition(0);
}
}
}, [dataList, stopScroll.current, instance, scroller.current]);
return (
<div
className="logs-wrap"
style={{ height: innerHieght }}
ref={scroller}
onWheel={handleOnWheel}
>
<div className={classNames('content')}>
{_.map(dataList, (item: any, index: number) => {
return (
<div key={item.uid} className="text">
{item.content}
</div>
);
})}
</div>
</div>
);
};
export default React.memo(LogsList);
@@ -1,95 +0,0 @@
import { controlSeqRegex, replaceLineRegex } from './config';
const useParseAnsi = () => {
const removeBrackets = (str) => str?.replace?.(/^\(…\)/, '');
const getRowContent = (screen, row) => {
let rowContent = [];
let col = 0;
while (screen.has(`${row},${col}`)) {
rowContent.push(screen.get(`${row},${col}`));
col++;
}
return rowContent.join('');
};
const handleText = function* (text, screen, cursor) {
for (let char of text) {
if (char === '\r') {
cursor.col = 0; // Move to the beginning of the line
} else if (char === '\n') {
// Move to the next line
yield getRowContent(screen, cursor.row);
cursor.row++;
cursor.col = 0;
} else {
const position = `${cursor.row},${cursor.col}`;
screen.set(position, char);
cursor.col++;
}
}
};
const parseAnsi = function* (inputStr, setId) {
let cursor = { row: 0, col: 0 };
const screen = new Map();
let lastIndex = 0;
// Replace \r\n with \n
const input = inputStr.replace(replaceLineRegex, '\n');
let match;
while ((match = controlSeqRegex.exec(input)) !== null) {
const textBeforeControl = input.slice(lastIndex, match.index);
yield* handleText(textBeforeControl, screen, cursor);
lastIndex = controlSeqRegex.lastIndex;
const [_, n = '1', m = '1', command] = match;
const parsedN = parseInt(n, 10);
const parsedM = parseInt(m, 10);
switch (command) {
case 'A':
cursor.row = Math.max(0, cursor.row - parsedN);
break;
case 'B':
cursor.row += parsedN;
break;
case 'C':
cursor.col += parsedN;
break;
case 'D':
cursor.col = Math.max(0, cursor.col - parsedN);
break;
case 'H':
cursor.row = Math.max(0, parsedN - 1);
cursor.col = Math.max(0, parsedM - 1);
break;
case 'J':
if (parsedN === 2) {
screen.clear();
cursor.row = 0;
cursor.col = 0;
}
break;
case 'm':
// Skipping color handling in this basic example; can be expanded as needed
break;
}
}
yield* handleText(input.slice(lastIndex), screen, cursor);
// Yield remaining rows
for (let row = 0; row <= cursor.row; row++) {
yield {
content: removeBrackets(getRowContent(screen, row)),
uid: setId()
};
}
};
return { parseAnsi };
};
export default useParseAnsi;
+1 -1
View File
@@ -4,7 +4,7 @@
.pg {
position: absolute;
top: 16px;
right: 16px;
right: 20px;
width: 40px;
height: 130px;
@@ -0,0 +1,26 @@
.logs-wrap {
background-color: var(--color-logs-bg);
border-radius: var(--border-radius-mini);
font-family: monospace, Menlo, Courier, 'Courier New', Consolas, Monaco,
'Liberation Mono' !important;
.content {
word-wrap: break-word;
height: 100%;
padding-right: 2px;
&.line-break {
word-wrap: break-word;
}
.text {
min-height: 22px;
}
color: var(--color-logs-text);
font-size: var(--font-size-small);
line-height: 22px;
white-space: pre-wrap;
background-color: var(--color-logs-bg);
}
}
@@ -1,7 +1,7 @@
import { useState } from 'react';
const useLogsPagination = () => {
const [pageSize, setPageSize] = useState(1000);
const [pageSize, setPageSize] = useState(500);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(1);
@@ -1,139 +0,0 @@
import { useRef, useState } from 'react';
const usePaseByLine = () => {
const [result, setResult] = useState<any[]>([]);
const uidRef = useRef(0);
const cursorRow = useRef(0); // current row
const cursorCol = useRef(0); // current column
// screen.current content array
const screen = useRef([['']]);
// replace carriage return and newline characters in the text
// let input = inputStr.replace(/\r\n/g, '\n')
const removeBrackets = (str: string) => {
return str?.replace?.(/^\(\.*?\)/, '');
};
const setId = () => {
uidRef.current += 1;
return uidRef.current;
};
// handle the \r and \n characters in the text
const handleText = (text: string) => {
for (let char of text) {
if (char === '\r') {
cursorCol.current = 0; // move to the beginning of the line
} else if (char === '\n') {
cursorRow.current += 1; // move to the next line
cursorCol.current = 0; // move to the beginning of the line
screen.current[cursorRow.current] = screen.current[
cursorRow.current
] || ['']; // create a new line if it does not exist
} else {
// add the character to the screen.current content array
screen.current[cursorRow.current][cursorCol.current] = char;
cursorCol.current += 1;
}
}
};
// ANSI
const controlSeqRegex = /\x1b\[(\d*);?(\d*)?([A-DJKHfm])/g;
// ANSI color map
const colorMap: Record<string, string> = {
'30': 'black',
'31': 'red',
'32': 'green',
'33': 'yellow',
'34': 'blue',
'35': 'magenta',
'36': 'cyan',
'37': 'white'
};
const updateResult = () => {
let res = [];
for (let row = 0; row < screen.current.length; row++) {
console.log('screen.current[row]', screen.current[row]);
let rowContent = screen.current[row].join('');
res.push({
content: removeBrackets(rowContent),
uid: setId()
});
}
console.log('res', res);
setResult(res);
};
const handleLine = (line: string) => {
let match;
let lastIndex = 0;
let currentStyle = ''; // current text style
// match ANSI control characters
while ((match = controlSeqRegex.exec(line)) !== null) {
// handle text before the control character
let textBeforeControl = line.slice(lastIndex, match.index);
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.current = Math.max(0, cursorRow.current - n);
break;
case 'B': // down
cursorRow.current += n;
break;
case 'C': // right
cursorCol.current += n;
break;
case 'D': // left
cursorCol.current = Math.max(0, cursorCol.current - n);
break;
case 'H': // move the cursor to the specified position (n, m)
cursorRow.current = Math.max(0, n - 1);
cursorCol.current = Math.max(0, m - 1);
break;
case 'J': // clear the screen.current
if (n === 2) {
screen.current = [['']];
cursorRow.current = 0;
cursorCol.current = 0;
}
break;
case 'm': // color
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.current content array
while (screen.current.length <= cursorRow.current) {
screen.current.push(['']);
}
while (screen.current[cursorRow.current].length <= cursorCol.current) {
screen.current[cursorRow.current].push('');
}
}
handleText(line.slice(lastIndex));
updateResult();
};
return {
handleLine,
result
};
};
export default usePaseByLine;
@@ -2,7 +2,7 @@ import useSetChunkFetch from '@/hooks/use-chunk-fetch';
import { Spin } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import {
import React, {
forwardRef,
memo,
useCallback,
@@ -12,7 +12,7 @@ import {
useState
} from 'react';
import { controlSeqRegex, replaceLineRegex } from './config';
import LogsInner from './logs-inner';
import LogsList from './logs-list';
import LogsPagination from './logs-pagination';
import './styles/index.less';
import useLogsPagination from './use-logs-pagination';
@@ -132,7 +132,6 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const updateContent = useCallback(
(inputStr: string) => {
console.log('data========', inputStr);
const data = inputStr.replace(replaceLineRegex, '\n');
if (isClean(data)) {
cacheDataRef.current = data;
@@ -188,11 +187,11 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
<div className="logs-viewer-wrap-w2">
<div className="wrap">
<div className={classNames('content')}>
<LogsInner
data={logs}
<LogsList
dataList={logs}
diffHeight={diffHeight}
onScroll={handleOnScroll}
></LogsInner>
></LogsList>
</div>
<Spin
spinning={loading && isAtTop}