From 05390743e09515474ba9bf02bfc2f019a4f60dd3 Mon Sep 17 00:00:00 2001 From: jialin Date: Sat, 14 Sep 2024 14:41:03 +0800 Subject: [PATCH] fix: logs view format --- package.json | 3 + pnpm-lock.yaml | 47 +++++ src/components/logs-viewer/index.less | 4 + src/components/logs-viewer/index.tsx | 160 ++++++--------- src/components/logs-viewer/text.ts | 186 ------------------ src/components/logs-viewer/use-size.ts | 13 ++ src/locales/en-US/common.ts | 1 + src/locales/en-US/models.ts | 1 + src/locales/zh-CN/common.ts | 1 + src/locales/zh-CN/models.ts | 1 + .../llmodels/components/advance-config.tsx | 29 ++- .../llmodels/components/view-logs-modal.tsx | 7 +- typings.d.ts | 2 + 13 files changed, 167 insertions(+), 288 deletions(-) delete mode 100644 src/components/logs-viewer/text.ts create mode 100644 src/components/logs-viewer/use-size.ts diff --git a/package.json b/package.json index e24004ff..0d51e3e7 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,11 @@ "@huggingface/hub": "^0.15.1", "@huggingface/tasks": "^0.11.6", "@monaco-editor/react": "^4.6.0", + "@react-hook/resize-observer": "^2.0.2", "@types/lodash": "^4.17.4", "@umijs/max": "^4.2.11", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "ansi-to-html": "^0.7.2", "antd": "^5.18.3", "antd-style": "^3.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67c2b89c..cfb9370b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,12 +23,21 @@ dependencies: '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.50.0)(react-dom@18.2.0)(react@18.2.0) + '@react-hook/resize-observer': + specifier: ^2.0.2 + version: 2.0.2(react@18.2.0) '@types/lodash': specifier: ^4.17.4 version: 4.17.4 '@umijs/max': specifier: ^4.2.11 version: 4.2.11(@babel/core@7.24.9)(@types/react-dom@18.3.0)(@types/react@18.3.1)(dva@2.5.0-beta.2)(prettier@3.2.5)(rc-field-form@1.44.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.5)(webpack@5.93.0) + '@xterm/addon-fit': + specifier: ^0.10.0 + version: 0.10.0(@xterm/xterm@5.5.0) + '@xterm/xterm': + specifier: ^5.5.0 + version: 5.5.0 ansi-to-html: specifier: ^0.7.2 version: 0.7.2 @@ -4452,6 +4461,32 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@react-hook/latest@1.0.3(react@18.2.0): + resolution: {integrity: sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==, tarball: https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz} + peerDependencies: + react: '>=16.8' + dependencies: + react: 18.2.0 + dev: false + + /@react-hook/passive-layout-effect@1.2.1(react@18.2.0): + resolution: {integrity: sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==, tarball: https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz} + peerDependencies: + react: '>=16.8' + dependencies: + react: 18.2.0 + dev: false + + /@react-hook/resize-observer@2.0.2(react@18.2.0): + resolution: {integrity: sha512-tzKKzxNpfE5TWmxuv+5Ae3IF58n0FQgQaWJmcbYkjXTRZATXxClnTprQ2uuYygYTpu1pqbBskpwMpj6jpT1djA==, tarball: https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-2.0.2.tgz} + peerDependencies: + react: '>=18' + dependencies: + '@react-hook/latest': 1.0.3(react@18.2.0) + '@react-hook/passive-layout-effect': 1.2.1(react@18.2.0) + react: 18.2.0 + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==, tarball: https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz} dev: false @@ -6692,6 +6727,18 @@ packages: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 + /@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0): + resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==, tarball: https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz} + peerDependencies: + '@xterm/xterm': ^5.0.0 + dependencies: + '@xterm/xterm': 5.5.0 + dev: false + + /@xterm/xterm@5.5.0: + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==, tarball: https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz} + dev: false + /@xtuc/ieee754@1.2.0: resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==, tarball: https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz} diff --git a/src/components/logs-viewer/index.less b/src/components/logs-viewer/index.less index d54d3d72..3b479918 100644 --- a/src/components/logs-viewer/index.less +++ b/src/components/logs-viewer/index.less @@ -19,4 +19,8 @@ background-color: var(--color-logs-bg); } } + + .xterm .xterm-viewport { + overflow-y: hidden !important; + } } diff --git a/src/components/logs-viewer/index.tsx b/src/components/logs-viewer/index.tsx index f736e0d1..eaad12c0 100644 --- a/src/components/logs-viewer/index.tsx +++ b/src/components/logs-viewer/index.tsx @@ -1,9 +1,12 @@ import useSetChunkRequest from '@/hooks/use-chunk-request'; -import useContainerScroll from '@/hooks/use-container-scorll'; -import Convert from 'ansi-to-html'; +import { FitAddon } from '@xterm/addon-fit'; +import { Terminal } from '@xterm/xterm'; +import '@xterm/xterm/css/xterm.css'; import classNames from 'classnames'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import _ from 'lodash'; +import { memo, useEffect, useRef, useState } from 'react'; import './index.less'; +import useSize from './use-size'; interface LogsViewerProps { height: number; @@ -14,94 +17,23 @@ interface LogsViewerProps { const LogsViewer: React.FC = (props) => { const { height, content, url } = props; const [nowrap, setNowrap] = useState(false); - const [logsContent, setLogsContent] = useState([]); const { setChunkRequest } = useSetChunkRequest(); const chunkRequedtRef = useRef(null); const scroller = useRef(null); - const { updateScrollerPosition, handleContentWheel } = useContainerScroll( - scroller, - { toBottom: true } - ); - - const convert = new Convert({ - newline: true, - escapeXML: true, - stream: false - }); - - useEffect(() => { - updateScrollerPosition(); - }, [logsContent]); - - const ansiEscapeRegex = /(\x1B\[[0-9;]*[A-Za-z])+$/; - - const endsWithAnsiEscapeSequence = useCallback((str: string) => { - return ansiEscapeRegex.test(str); - }, []); - const getCursorUpLines = useCallback((str: string) => { - const matches = str.match(/(?:\x1B\[A)+$/); - - return matches ? matches[0].length / 3 : 0; - }, []); - - const removeDot = useCallback((str: string) => { - return str.replace(/^\(.*?\)/, ''); - }, []); - const replaceAnsiEscapeSequence = useCallback((str: string) => { - const res = str.replace(ansiEscapeRegex, ''); - return removeDot(res); - }, []); - - const handleRControl = useCallback((str: string) => { - if (str.includes('\r')) { - const parts = str.split('\r'); - const lastLine = parts[parts.length - 1]; - return lastLine; - } - return str; - }, []); - - const parseHtmlStr = useCallback((logStr: string) => { - const result: string[] = []; - const lines = logStr.split('\n').filter((line) => line.trim() !== ''); - // const lines = text; - lines.forEach((line: string, index: number) => { - const upCount = getCursorUpLines(line); - console.log('line=========1', { - line, - upCount, - result - }); - if (endsWithAnsiEscapeSequence(line)) { - const newLine = handleRControl(line); - const val = removeDot(newLine); - if (result.length < upCount) { - result.push(''); - } - if (upCount) { - console.log('line=========0', { - line, - upCount, - result - }); - const placeIndex = result.length - upCount; - result[placeIndex] = replaceAnsiEscapeSequence(val); - } else { - result.push(val); - } - } else { - const val = handleRControl(line); - result.push(val); - } - }); - return result.map((item) => { - return convert.toHtml(item); - }); - }, []); + const termRef = useRef(null); + const termInsRef = useRef(null); + const fitAddonRef = useRef(null); + const cacheDatARef = useRef(null); + const size = useSize(scroller); const updateContent = (newVal: string) => { - const list = parseHtmlStr(newVal); - setLogsContent(list); + cacheDatARef.current = newVal; + termRef.current?.reset(); + termRef.current?.write?.(newVal); + }; + + const fitTerm = () => { + fitAddonRef.current.fit(); }; const createChunkConnection = async () => { @@ -116,6 +48,38 @@ const LogsViewer: React.FC = (props) => { handler: updateContent }); }; + const initTerm = () => { + termRef.current?.dispose?.(); + termRef.current = new Terminal({ + lineHeight: 1.2, + fontSize: 12, + fontFamily: + "monospace,Menlo,Courier,'Courier New',Consolas,Monaco, 'Liberation Mono'", + disableStdin: true, + convertEol: true, + theme: { + background: '#1e1e1e' + }, + cursorInactiveStyle: 'none' + // windowOptions: { + // setWinPosition: true, + // setWinSizePixels: true, + // refreshWin: true + // } + }); + fitAddonRef.current = new FitAddon(); + termRef.current.loadAddon(fitAddonRef.current); + termRef.current.open(termInsRef.current); + fitAddonRef.current?.fit(); + }; + + const handleResize = _.throttle(() => { + termRef.current?.clear(); + if (cacheDatARef.current) { + updateContent(cacheDatARef.current); + } + fitTerm(); + }, 100); useEffect(() => { createChunkConnection(); @@ -124,21 +88,23 @@ const LogsViewer: React.FC = (props) => { }; }, [url, props.params]); + useEffect(() => { + if (termInsRef.current) { + initTerm(); + } + }, []); + + useEffect(() => { + handleResize(); + console.log('size======', size); + }, [size]); + return (
-
+
- {logsContent.map((item, index) => { - return ( -
- ); - })} +
diff --git a/src/components/logs-viewer/text.ts b/src/components/logs-viewer/text.ts deleted file mode 100644 index 22ebaa60..00000000 --- a/src/components/logs-viewer/text.ts +++ /dev/null @@ -1,186 +0,0 @@ -export default [ - '2024-09-11T20:09:42+08:00 - gpustack.worker.downloaders - INFO - Downloading model leafspark/Reflection-Llama-3.1-70B-GGUF/Reflection-Llama-3.1-70B.fp16*.gguf', - '\rFetching 4 files: 0%| | 0/4 [00:00 { + setSize(target.current.getBoundingClientRect()); + }, [target]); + + useResizeObserver(target, (entry: any) => setSize(entry?.contentRect)); + return size; +} diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 59d777ea..3b756460 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -46,6 +46,7 @@ export default { 'common.button.disabled': 'Disabled', 'common.button.upgrade': 'Upgrade', 'common.input.holder': 'Please enter', + 'common.validate.value': 'value is required', 'common.button.edit': 'Edit', 'common.button.authorize': 'Role Authorization', 'common.button.confirm': 'Confirm', diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index d7f741c6..55589d5a 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -9,6 +9,7 @@ export default { 'models.form.repoid.desc': 'Only .gguf format is supported', 'models.form.filename': 'File Name', 'models.form.replicas': 'Replicas', + 'models.form.selector': 'Selector', 'models.form.configurations': 'Configurations', 'models.form.s3address': 'S3 Address', 'models.form.partialoffload.tips': diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 327ab596..4c8f30ea 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -109,6 +109,7 @@ export default { 'common.ws.reconnect': '重新连接', 'common.input.key': '键', 'common.input.value': '值', + 'common.validate.value': '值必填', 'common.input.type': '类型', 'common.input.visible': '是否可见', 'common.input.description': '描述', diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index 2f368c9e..06e34d2a 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -9,6 +9,7 @@ export default { 'models.form.repoid.desc': '只支持 .gguf 格式', 'models.form.filename': '文件名', 'models.form.replicas': '副本数', + 'models.form.selector': '选择器', 'models.form.configurations': '配置', 'models.form.s3address': 'S3 地址', 'models.form.partialoffload.tips': diff --git a/src/pages/llmodels/components/advance-config.tsx b/src/pages/llmodels/components/advance-config.tsx index 9cf389e4..734a9fb3 100644 --- a/src/pages/llmodels/components/advance-config.tsx +++ b/src/pages/llmodels/components/advance-config.tsx @@ -11,6 +11,7 @@ import { Tooltip, Typography } from 'antd'; +import _ from 'lodash'; import React, { useCallback, useMemo } from 'react'; import { placementStrategyOptions } from '../config'; import { FormData } from '../config/types'; @@ -129,7 +130,33 @@ const AdvanceConfig: React.FC = (props) => { description={renderSelectTips(placementStrategyTips)} > - name="worker_selector"> + + name="worker_selector" + rules={[ + ({ getFieldValue }) => ({ + validator(rule, value) { + if ( + getFieldValue('scheduleType') === 'auto' && + _.keys(value).length > 0 + ) { + if (_.some(_.keys(value), (k: string) => !value[k])) { + return Promise.reject( + intl.formatMessage( + { + id: 'common.validate.value' + }, + { + name: 'models.form.selector' + } + ) + ); + } + } + return Promise.resolve(); + } + }) + ]} + > = (props) => { title={ {intl.formatMessage({ id: 'common.button.viewlog' })} - + */} } open={open} diff --git a/typings.d.ts b/typings.d.ts index de8cbd19..a663ef54 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -17,5 +17,7 @@ declare module 'react-fittext'; declare module 'lodash'; declare module 'crypto-js'; declare module 'has-ansi'; +declare module 'terminal-kit'; +declare module 'monaco-vim'; declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;