feat: add download instance log button

main
jialin 1 year ago
parent 4b73457f7f
commit 8464e0ea3f

@ -17,6 +17,8 @@ class AnsiParser {
private isProcessing: boolean = false;
private taskQueue: string[] = [];
private page: number = 1;
private isComplete: boolean = false;
private chunked: boolean = true; // true: send data in chunks, false: send all data at once
private pageSize: number = 500;
private colorMap = {
'30': 'black',
@ -46,6 +48,14 @@ class AnsiParser {
this.page = page;
}
public setIsCompelete(isComplete: boolean) {
this.isComplete = isComplete;
}
public setChunked(chunked: boolean) {
this.chunked = chunked ?? true;
}
private setId() {
this.uid += 1;
return this.uid;
@ -147,6 +157,13 @@ class AnsiParser {
};
}
private getScreenText() {
const result = this.screen
.map((row) => removeBracketsFromLine(row.join('')))
.join('\n');
return result;
}
private async processQueue(): Promise<void> {
if (this.isProcessing) {
return;
@ -160,7 +177,9 @@ class AnsiParser {
if (input) {
try {
const result = this.processInput(input);
self.postMessage({ result: result.data, lines: result.lines });
if (this.chunked) {
self.postMessage({ result: result.data, lines: result.lines });
}
} catch (error) {
console.error('Error processing input:', error);
}
@ -174,6 +193,9 @@ class AnsiParser {
this.isProcessing = false;
if (this.taskQueue.length > 0) {
this.processQueue();
} else if (this.isComplete && !this.chunked) {
self.postMessage({ result: this.getScreenText() });
this.reset();
}
}
@ -187,8 +209,18 @@ class AnsiParser {
const parser = new AnsiParser();
self.onmessage = function (event) {
const { inputStr, reset, page } = event.data;
const {
inputStr,
reset,
page,
isComplete = false,
chunked = true
} = event.data;
parser.setPage(page);
parser.setIsCompelete(isComplete);
parser.setChunked(chunked);
if (reset) {
parser.reset();
}

@ -2,9 +2,17 @@ import { throttle } from 'lodash';
import qs from 'query-string';
import { useEffect, useRef } from 'react';
export interface HandlerOptions {
isComplete?: boolean | null;
percent?: number;
progress?: number;
contentLength?: number | null;
}
type HandlerFunction = (data: any, options?: HandlerOptions) => any;
interface RequestConfig {
url: string;
handler: (data: any) => any;
handler: HandlerFunction;
beforeReconnect?: () => void;
params?: object;
watch?: boolean;
@ -16,23 +24,47 @@ const useSetChunkFetch = () => {
const requestConfig = useRef<any>({});
const chunkDataRef = useRef<any>([]);
const readTextEventStreamData = async (
reader: ReadableStreamDefaultReader<Uint8Array>,
decoder: TextDecoder,
callback: (data: any) => void,
response: Response,
callback: HandlerFunction,
delay = 200
) => {
class BufferManager {
private buffer: any[] = [];
private contentLength: number | null = null;
private progress: number = 0;
private percent: number = 0;
constructor(private options: { contentLength?: string | null }) {
this.contentLength = options.contentLength
? parseInt(options.contentLength, 10)
: null;
}
private updateProgress(data: any) {
if (this.contentLength) {
this.progress += new TextEncoder().encode(data).length;
this.percent = Math.floor((this.progress / this.contentLength) * 100);
}
}
public add(data: any) {
this.buffer.push(data);
this.updateProgress(data);
}
public flush() {
public flush(done?: boolean) {
if (this.buffer.length > 0) {
const currentBuffer = [...this.buffer];
this.buffer = [];
currentBuffer.forEach((item) => callback(item));
currentBuffer.forEach((item, i) => {
const isComplete = i === currentBuffer.length - 1 && done;
callback(item, {
isComplete,
percent: this.percent,
progress: this.progress,
contentLength: this.contentLength
});
});
}
}
@ -40,7 +72,15 @@ const useSetChunkFetch = () => {
return this.buffer;
}
}
const bufferManager = new BufferManager();
const reader =
response?.body?.getReader() as ReadableStreamDefaultReader<Uint8Array>;
const decoder = new TextDecoder('utf-8');
const contentLength = response.headers.get('content-length');
const bufferManager = new BufferManager({
contentLength: contentLength
});
const throttledCallback = throttle(() => {
bufferManager.flush();
@ -53,7 +93,7 @@ const useSetChunkFetch = () => {
if (done) {
isReading = false;
bufferManager.flush();
bufferManager.flush(done);
break;
}
@ -97,11 +137,7 @@ const useSetChunkFetch = () => {
return;
}
const reader =
response?.body?.getReader() as ReadableStreamDefaultReader<Uint8Array>;
const decoder = new TextDecoder('utf-8');
await readTextEventStreamData(reader, decoder, handler);
await readTextEventStreamData(response, handler);
console.log('chunkDataRef.current===1', chunkDataRef.current);
} catch (error) {

@ -0,0 +1,91 @@
import useSetChunkFetch, { HandlerOptions } from '@/hooks/use-chunk-fetch';
import dayjs from 'dayjs';
import { useEffect, useRef } from 'react';
export default function useDownloadStream() {
const chunkRequedtRef = useRef<any>(null);
const logParseWorker = useRef<any>(null);
const clearScreen = useRef(false);
const filename = useRef('log');
const { setChunkFetch } = useSetChunkFetch();
const downloadFile = (content: string) => {
const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss');
const fileName = `${filename.current}_${timestamp}.txt`;
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const updateContent = (data: string, options?: HandlerOptions) => {
const { isComplete } = options || {};
logParseWorker.current?.postMessage({
inputStr: data,
page: 1,
reset: clearScreen.current,
isComplete: isComplete,
chunked: false
});
clearScreen.current = false;
};
const downloadStream = async (props: {
data?: any;
url: string;
params?: any;
signal?: AbortSignal;
method?: string;
headers?: any;
filename?: string;
}) => {
clearScreen.current = true;
filename.current = props.filename || 'log';
const { params, url } = props;
chunkRequedtRef.current?.current?.abort?.();
chunkRequedtRef.current = setChunkFetch({
url,
params,
watch: false,
contentType: 'text',
handler: updateContent
});
};
useEffect(() => {
logParseWorker.current?.terminate?.();
logParseWorker.current = new Worker(
// @ts-ignore
new URL('@/components/logs-viewer/parse-worker.ts', import.meta.url),
{
type: 'module'
}
);
logParseWorker.current.onmessage = (event: any) => {
const { result } = event.data;
downloadFile(result);
};
return () => {
if (logParseWorker.current) {
logParseWorker.current.terminate();
}
};
}, []);
return {
downloadStream
};
}

@ -227,5 +227,6 @@ export default {
'common.options.all': 'All',
'common.options.none': 'None',
'common.options.auto': 'Auto',
'common.search.empty': 'No matching results found.'
'common.search.empty': 'No matching results found.',
'common.button.downloadLog': 'Download Log'
};

@ -220,5 +220,6 @@ export default {
'common.options.all': '全部',
'common.options.none': '无',
'common.options.auto': '自动',
'common.search.empty': '未找到匹配结果'
'common.search.empty': '未找到匹配结果',
'common.button.downloadLog': '下载日志'
};

@ -10,6 +10,7 @@ import {
} from '@/pages/resources/config/types';
import {
DeleteOutlined,
DownloadOutlined,
HddFilled,
InfoCircleOutlined,
ThunderboltFilled
@ -48,6 +49,18 @@ const childActionList = [
],
icon: <IconFont type="icon-logs" />
},
{
label: 'common.button.downloadLog',
key: 'download',
status: [
InstanceStatusMap.Initializing,
InstanceStatusMap.Running,
InstanceStatusMap.Error,
InstanceStatusMap.Starting,
InstanceStatusMap.Downloading
],
icon: <DownloadOutlined />
},
{
label: 'common.button.delrecreate',
key: 'delete',
@ -60,7 +73,7 @@ const childActionList = [
const setChildActionList = (item: ModelInstanceListItem) => {
return _.filter(childActionList, (action: any) => {
if (action.key === 'viewlog') {
if (action.key === 'viewlog' || action.key === 'download') {
return action.status.includes(item.state);
}
return true;

@ -10,6 +10,7 @@ import { SealColumnProps } from '@/components/seal-table/types';
import { PageAction } from '@/config';
import HotKeys from '@/config/hotkeys';
import useBodyScroll from '@/hooks/use-body-scroll';
import useDownloadStream from '@/hooks/use-download-stream';
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
import useTableRowSelection from '@/hooks/use-table-row-selection';
import useTableSort from '@/hooks/use-table-sort';
@ -181,6 +182,7 @@ const Models: React.FC<ModelsProps> = ({
loadend,
total
}) => {
const { downloadStream } = useDownloadStream();
const { getGPUList, generateFormValues, gpuDeviceList } =
useGenerateFormEditInitialValues();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
@ -440,7 +442,7 @@ const Models: React.FC<ModelsProps> = ({
restoreScrollHeight();
} catch (error) {}
},
[currentData]
[handleSearch]
);
const handleModalCancel = useCallback(() => {
@ -652,6 +654,12 @@ const Models: React.FC<ModelsProps> = ({
if (val === 'viewlog') {
handleViewLogs(row);
}
if (val === 'download') {
downloadStream({
url: `${MODEL_INSTANCE_API}/${row.id}/logs`,
filename: row.name
});
}
},
[handleViewLogs, handleDeleteInstace]
);

@ -314,19 +314,3 @@ export const readLargeStreamData = async (
}
}
};
export const readTextEventStreamData = async (
reader: any,
decoder: TextDecoder,
callback: (data: any) => void
) => {
const { done, value } = await reader.read();
if (done) {
return;
}
let chunk = decoder.decode(value, { stream: true });
callback(chunk);
await readTextEventStreamData(reader, decoder, callback);
};

Loading…
Cancel
Save