Files
gpustack-ui/src/components/logs-viewer/xterm-viewer.tsx
T
2025-02-17 13:49:55 +08:00

180 lines
4.3 KiB
TypeScript

import useSetChunkRequest from '@/hooks/use-chunk-request';
import { FitAddon } from '@xterm/addon-fit';
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.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);