diff --git a/config/config.ts b/config/config.ts index 3d1a6558..8c16aba8 100644 --- a/config/config.ts +++ b/config/config.ts @@ -1,5 +1,6 @@ import { defineConfig } from '@umijs/max'; import keepAlive from './keep-alive'; +import { compressionPluginConfig, monacoPluginConfig } from './plugins'; import proxy from './proxy'; import routes from './routes'; import { getBranchInfo } from './utils'; @@ -29,7 +30,7 @@ export default defineConfig({ defaultSizes: 'parsed' // stat // gzip }, mfsu: { - exclude: ['lodash', 'ml-pca'] + exclude: ['lodash', 'ml-pca', 'monaco-editor'] }, base: process.env.npm_config_base || '/', ...(isProduction @@ -56,21 +57,13 @@ export default defineConfig({ config.output .filename(`js/[name].${t}.js`) .chunkFilename(`js/[name].${t}.chunk.js`); - config - .plugin('compression-webpack-plugin') - .use(CompressionWebpackPlugin, [ - { - filename: '[path][base].gz', - algorithm: 'gzip', - test: /\.(js|css|html|svg)$/, - threshold: 10240, - minRatio: 0.8 - } - ]); + compressionPluginConfig(config); } } : {}), - + chainWebpack(config) { + monacoPluginConfig(config); + }, favicons: ['/static/favicon.png'], jsMinifier: 'terser', cssMinifier: 'cssnano', diff --git a/config/plugins.ts b/config/plugins.ts new file mode 100644 index 00000000..ee8cf363 --- /dev/null +++ b/config/plugins.ts @@ -0,0 +1,36 @@ +import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; +const CompressionWebpackPlugin = require('compression-webpack-plugin'); + +export function monacoPluginConfig(config: any) { + return config + .plugin('monaco-editor-webpack-plugin') + .use(MonacoWebpackPlugin, [ + { + languages: ['yaml'], + customLanguages: [ + { + label: 'yaml', + entry: 'monaco-yaml', + worker: { + id: 'monaco-yaml/yamlWorker', + entry: 'monaco-yaml/yaml.worker' + } + } + ] + } + ]); +} + +export function compressionPluginConfig(config: any) { + return config + .plugin('compression-webpack-plugin') + .use(CompressionWebpackPlugin, [ + { + filename: '[path][base].gz', + algorithm: 'gzip', + test: /\.(js|css|html|svg)$/, + threshold: 10240, + minRatio: 0.8 + } + ]); +} diff --git a/config/routes.ts b/config/routes.ts index d1b89eb1..003df2c5 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -114,6 +114,7 @@ export default [ selectedIcon: 'icon-backend-filled', defaultIcon: 'icon-backend', access: 'canSeeAdmin', + subMenu: ['/models/backends/create'], component: './backends/list' } ] diff --git a/package.json b/package.json index 6f687822..3c7053f4 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "has-ansi": "^5.0.1", "highlight.js": "^11.10.0", "jotai": "^2.8.4", + "js-yaml": "^4.1.0", "jszip": "^3.10.1", "katex": "^0.16.21", "lamejs": "github:zhuker/lamejs", @@ -55,6 +56,9 @@ "marked": "^14.1.0", "ml-dataset-iris": "^1.2.1", "ml-pca": "^4.1.1", + "monaco-editor": "^0.53.0", + "monaco-editor-webpack-plugin": "^7.1.0", + "monaco-yaml": "^5.4.0", "numeral": "^2.0.6", "overlayscrollbars": "^2.10.0", "overlayscrollbars-react": "^0.5.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0be4c58f..673b22f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ dependencies: version: 0.11.13 '@monaco-editor/react': specifier: ^4.6.0 - version: 4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1)(react@18.3.1) + version: 4.7.0(monaco-editor@0.53.0)(react-dom@18.3.1)(react@18.3.1) '@orcid/bibtex-parse-js': specifier: ^0.0.25 version: 0.0.25 @@ -99,6 +99,9 @@ dependencies: jotai: specifier: ^2.8.4 version: 2.13.1(@babel/core@7.28.3)(@types/react@18.3.24)(react@18.3.1) + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 jszip: specifier: ^3.10.1 version: 3.10.1 @@ -126,6 +129,15 @@ dependencies: ml-pca: specifier: ^4.1.1 version: 4.1.1 + monaco-editor: + specifier: ^0.53.0 + version: 0.53.0 + monaco-editor-webpack-plugin: + specifier: ^7.1.0 + version: 7.1.0(monaco-editor@0.53.0)(webpack@5.101.3) + monaco-yaml: + specifier: ^5.4.0 + version: 5.4.0(monaco-editor@0.53.0) numeral: specifier: ^2.0.6 version: 2.0.6 @@ -2699,7 +2711,7 @@ packages: state-local: 1.0.7 dev: false - /@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1)(react@18.3.1): + /@monaco-editor/react@4.7.0(monaco-editor@0.53.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==, tarball: https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz} peerDependencies: monaco-editor: '>= 0.25.0 < 1' @@ -2707,7 +2719,7 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 dependencies: '@monaco-editor/loader': 1.5.0 - monaco-editor: 0.52.2 + monaco-editor: 0.53.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) dev: false @@ -3637,6 +3649,10 @@ packages: /@types/stylis@4.2.7: resolution: {integrity: sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==, tarball: https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz} + /@types/trusted-types@1.0.6: + resolution: {integrity: sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==, tarball: https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz} + dev: false + /@types/trusted-types@2.0.7: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==, tarball: https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz} requiresBuild: true @@ -9332,6 +9348,10 @@ packages: engines: {node: '>=6'} hasBin: true + /jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==, tarball: https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz} + dev: false + /jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==, tarball: https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz} dependencies: @@ -10481,8 +10501,66 @@ packages: /moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==, tarball: https://registry.npmjs.org/moment/-/moment-2.30.1.tgz} - /monaco-editor@0.52.2: - resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==, tarball: https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz} + /monaco-editor-webpack-plugin@7.1.0(monaco-editor@0.53.0)(webpack@5.101.3): + resolution: {integrity: sha512-ZjnGINHN963JQkFqjjcBtn1XBtUATDZBMgNQhDQwd78w2ukRhFXAPNgWuacaQiDZsUr4h1rWv5Mv6eriKuOSzA==, tarball: https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-7.1.0.tgz} + peerDependencies: + monaco-editor: '>= 0.31.0' + webpack: ^4.5.0 || 5.x + dependencies: + loader-utils: 2.0.4 + monaco-editor: 0.53.0 + webpack: 5.101.3 + dev: false + + /monaco-editor@0.53.0: + resolution: {integrity: sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==, tarball: https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz} + dependencies: + '@types/trusted-types': 1.0.6 + dev: false + + /monaco-languageserver-types@0.4.0: + resolution: {integrity: sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==, tarball: https://registry.npmjs.org/monaco-languageserver-types/-/monaco-languageserver-types-0.4.0.tgz} + dependencies: + monaco-types: 0.1.0 + vscode-languageserver-protocol: 3.17.5 + vscode-uri: 3.1.0 + dev: false + + /monaco-marker-data-provider@1.2.4: + resolution: {integrity: sha512-4DsPgsAqpTyUDs3humXRBPUJoihTv+L6v9aupQWD80X2YXaCXUd11mWYeSCYHuPgdUmjFaNWCEOjQ6ewf/QA1Q==, tarball: https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.2.4.tgz} + dependencies: + monaco-types: 0.1.0 + dev: false + + /monaco-types@0.1.0: + resolution: {integrity: sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==, tarball: https://registry.npmjs.org/monaco-types/-/monaco-types-0.1.0.tgz} + dev: false + + /monaco-worker-manager@2.0.1(monaco-editor@0.53.0): + resolution: {integrity: sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==, tarball: https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz} + peerDependencies: + monaco-editor: '>=0.30.0' + dependencies: + monaco-editor: 0.53.0 + dev: false + + /monaco-yaml@5.4.0(monaco-editor@0.53.0): + resolution: {integrity: sha512-tuBVDy1KAPrgO905GHTItu8AaA5bIzF5S4X0JVRAE/D66FpRhkDUk7tKi5bwKMVTTugtpMLsXN4ewh4CgE/FtQ==, tarball: https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-5.4.0.tgz} + peerDependencies: + monaco-editor: '>=0.36' + dependencies: + jsonc-parser: 3.3.1 + monaco-editor: 0.53.0 + monaco-languageserver-types: 0.4.0 + monaco-marker-data-provider: 1.2.4 + monaco-types: 0.1.0 + monaco-worker-manager: 2.0.1(monaco-editor@0.53.0) + path-browserify: 1.0.1 + prettier: 3.6.2 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + yaml: 2.8.1 dev: false /mrmime@2.0.1: @@ -11078,6 +11156,10 @@ packages: resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==, tarball: https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz} dev: false + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==, tarball: https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz} + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, tarball: https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz} engines: {node: '>=8'} @@ -15580,6 +15662,30 @@ packages: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==, tarball: https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz} dev: false + /vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==, tarball: https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz} + engines: {node: '>=14.0.0'} + dev: false + + /vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==, tarball: https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz} + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + dev: false + + /vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==, tarball: https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz} + dev: false + + /vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==, tarball: https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz} + dev: false + + /vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==, tarball: https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz} + dev: false + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==, tarball: https://registry.npmjs.org/walker/-/walker-1.0.8.tgz} dependencies: @@ -15873,7 +15979,6 @@ packages: resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==, tarball: https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz} engines: {node: '>= 14.6'} hasBin: true - dev: true /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==, tarball: https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz} diff --git a/src/assets/logo/lmdeploy.png b/src/assets/logo/lmdeploy.png new file mode 100644 index 00000000..191fbfcc Binary files /dev/null and b/src/assets/logo/lmdeploy.png differ diff --git a/src/components/copy-button/index.tsx b/src/components/copy-button/index.tsx index 48bbd173..bb4dbd78 100644 --- a/src/components/copy-button/index.tsx +++ b/src/components/copy-button/index.tsx @@ -55,9 +55,7 @@ const CopyButton: React.FC = ({ }); clipboardRef.current.on('error', (e: any) => { - message.success( - intl.formatMessage({ id: 'common.copy.fail' }) as string - ); + message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string); e.clearSelection(); }); } diff --git a/src/components/editor-wrap/index.tsx b/src/components/editor-wrap/index.tsx index 6c51ee47..38926123 100644 --- a/src/components/editor-wrap/index.tsx +++ b/src/components/editor-wrap/index.tsx @@ -1,74 +1,66 @@ +import classNames from 'classnames'; import React from 'react'; import styled from 'styled-components'; -import CopyButton from '../copy-button'; -import SegmentLine from '../segment-line'; import './index.less'; -const Wrapper = styled.div<{ $height: number }>` - height: ${(props) => props.$height}px; +const HeaderWrapper = styled.div<{ $height?: number }>` + height: ${(props) => (props.$height ? `${props.$height}px` : 'auto')}; + display: flex; + padding-block: 0; + justify-content: space-between; + align-items: center; +`; + +const Wrapper = styled.div` + border-radius: var(--border-radius-mini); + overflow: hidden; + + &.bordered { + border: 1px solid var(--ant-color-border); + } + &.borderless { + border: none; + } + .code-pre { + margin-bottom: 0; + } + .scrollbar { + .slider { + border-radius: 6px; + } + } `; interface EditorwrapProps { headerHeight?: number; header?: React.ReactNode; children: React.ReactNode; - showHeader?: boolean; - copyText: string; - defaultValue?: string; - langOptions?: Global.BaseOption[]; + variant?: 'bordered' | 'borderless'; styles?: { wrapper?: React.CSSProperties; header?: React.CSSProperties; content?: React.CSSProperties; }; - onChangeLang?: (value: string | number) => void; } const EditorWrap: React.FC = ({ headerHeight = 40, header, children, - copyText, - langOptions = [], - onChangeLang, - defaultValue, - styles = {}, - showHeader = true + variant = 'borderless', + styles = {} }) => { - const handleChangeLang = (value: string | number) => { - onChangeLang?.(value); - }; - const renderHeader = () => { - if (header) { - return
{header}
; - } - if (showHeader) { - return ( - - - - - ); - } - return null; - }; return ( -
- {renderHeader()} -
{children}
-
+ + {header && {header}} +
{children}
+
); }; -export default React.memo(EditorWrap); +export default EditorWrap; diff --git a/src/components/editor/viewer.tsx b/src/components/editor/index.tsx similarity index 54% rename from src/components/editor/viewer.tsx rename to src/components/editor/index.tsx index 20452dc5..680a1dd2 100644 --- a/src/components/editor/viewer.tsx +++ b/src/components/editor/index.tsx @@ -1,30 +1,39 @@ +import { LoadingOutlined } from '@ant-design/icons'; import Editor from '@monaco-editor/react'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef +} from 'react'; import EditorWrap from '../editor-wrap'; - interface ViewerProps { + ref?: any; lang: string; defaultLang?: string; - langOptions?: Global.BaseOption[]; config?: any; value: string; height?: string | number; theme?: string; - showHeader?: boolean; + header?: React.ReactNode; + placeholder?: string; + variant?: 'bordered' | 'borderless'; } -const ViewerEditor: React.FC = (props) => { - const editorRef = useRef(null); +const ViewerEditor: React.FC = forwardRef((props, ref) => { const { lang, value, config, - langOptions, defaultLang, height = 380, - showHeader + theme = 'vs-dark', + header, + variant = 'borderless', + placeholder } = props; - const [langType, setLangType] = useState(defaultLang); + + const editorRef = useRef(null); const handleBeforeMount = (monaco: any) => { monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ @@ -36,12 +45,8 @@ const ViewerEditor: React.FC = (props) => { const handleEditorDidMount = (editor: any, monaco: any) => { editorRef.current = editor; - console.log('loaded====', editor, monaco); }; - const handleOnChangeLang = (value: string) => { - setLangType(value); - }; const formatCode = () => { if (editorRef.current) { setTimeout(() => { @@ -55,35 +60,50 @@ const ViewerEditor: React.FC = (props) => { } }; + useImperativeHandle(ref, () => ({ + format: () => { + formatCode(); + }, + getValue: () => { + return editorRef.current?.getValue?.(); + }, + setValue: (val: string) => { + editorRef.current?.setValue?.(val); + }, + editor: editorRef.current + })); + useEffect(() => { formatCode(); setTimeout(() => { - const lineCount = editorRef.current?.getModel().getLineCount(); // 获取总行数 - editorRef.current?.revealLine(lineCount); // 滚动到最后一行 - }, 100); // 可以调整延时,确保编辑器完全加载 + const lineCount = editorRef.current?.getModel().getLineCount(); + editorRef.current?.revealLine(lineCount); + }, 100); }, [value]); return ( - + } beforeMount={handleBeforeMount} onMount={handleEditorDidMount} /> ); -}; +}); -export default React.memo(ViewerEditor); +export default ViewerEditor; diff --git a/src/components/icon-font/icons.ts b/src/components/icon-font/icons.ts index 04c540d5..823d10f6 100644 --- a/src/components/icon-font/icons.ts +++ b/src/components/icon-font/icons.ts @@ -45,7 +45,11 @@ const icons = { Ascend: React.createElement(IconFont, { type: 'icon-ascend' }), Catalog1: React.createElement(IconFont, { type: 'icon-catalog1' }), AMD: React.createElement(IconFont, { type: 'icon-amd' }), - KubernetesFilled: React.createElement(IconFont, { type: 'icon-k8s-filled' }) + KubernetesFilled: React.createElement(IconFont, { type: 'icon-k8s-filled' }), + EditContent: React.createElement(IconFont, { type: 'icon-edit-content' }), + Yaml: React.createElement(IconFont, { type: 'icon-code_block' }), + Version: React.createElement(IconFont, { type: 'icon-version' }), + Parameter: React.createElement(IconFont, { type: 'icon-parameters' }) }; export default icons; diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index 2e9bc504..9ed10a53 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; // import './iconfont/iconfont.js'; const IconFont = createFromIconfontCN({ - scriptUrl: '//at.alicdn.com/t/c/font_4613488_wt33o3w25g.js' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_c06q7b0n32v.js' }); export default IconFont; diff --git a/src/components/page-tools/index.tsx b/src/components/page-tools/index.tsx index 1c0839c6..89b91170 100644 --- a/src/components/page-tools/index.tsx +++ b/src/components/page-tools/index.tsx @@ -3,6 +3,7 @@ import { DeleteOutlined, DownOutlined, PlusOutlined, + SearchOutlined, SyncOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; @@ -170,6 +171,11 @@ export const FilterBar: React.FC = (props) => { left={ + } placeholder={ inputHolder || intl.formatMessage({ diff --git a/src/components/seal-form/seal-textarea.tsx b/src/components/seal-form/seal-textarea.tsx index 74d8ee66..536eabf0 100644 --- a/src/components/seal-form/seal-textarea.tsx +++ b/src/components/seal-form/seal-textarea.tsx @@ -127,6 +127,7 @@ const SealTextArea: React.FC = ( > ` border-radius: 2px; bottom: 0px; top: unset; + opacity: 1; } } .ant-segmented-item-selected { @@ -55,6 +56,7 @@ const SegmentedQwrapper = styled.div<{ $height: number }>` color: var(--color-white-primary); &::after { height: 2px; + opacity: 1; } } } @@ -68,6 +70,7 @@ const SegmentLine: React.FC = (props) => { className = 'segment-line', ...rest } = props; + return ( = ({ + items +}) => { + return ( + ( + + )} + items={items} + > + ); +}; + +export default CollapsePanel; diff --git a/src/pages/llmodels/components/column-wrapper.tsx b/src/pages/_components/column-wrapper/index.tsx similarity index 51% rename from src/pages/llmodels/components/column-wrapper.tsx rename to src/pages/_components/column-wrapper/index.tsx index 87152fcc..8cef3911 100644 --- a/src/pages/llmodels/components/column-wrapper.tsx +++ b/src/pages/_components/column-wrapper/index.tsx @@ -1,6 +1,6 @@ import useOverlayScroller from '@/hooks/use-overlay-scroller'; import React from 'react'; -import '../style/column-wrapper.less'; +import './style.less'; const ColumnWrapper: React.FC = ({ children, @@ -25,24 +25,25 @@ const ColumnWrapper: React.FC = ({ return ( <> - {footer ? ( -
-
-
- {children} -
+
+
+
+ {children}
- {
{footer}
}
- ) : ( -
-
{children}
-
- )} + {footer &&
{footer}
} +
); }; diff --git a/src/pages/llmodels/style/column-wrapper.less b/src/pages/_components/column-wrapper/style.less similarity index 100% rename from src/pages/llmodels/style/column-wrapper.less rename to src/pages/_components/column-wrapper/style.less diff --git a/src/pages/_components/command-viewer/index.tsx b/src/pages/_components/command-viewer/index.tsx new file mode 100644 index 00000000..0608f205 --- /dev/null +++ b/src/pages/_components/command-viewer/index.tsx @@ -0,0 +1,79 @@ +import CopyButton from '@/components/copy-button'; +import EditorWrap from '@/components/editor-wrap'; +import HighlightCode from '@/components/highlight-code'; +import SegmentLine from '@/components/segment-line'; +import React from 'react'; +import styled from 'styled-components'; + +interface ViewerProps { + code: string; + copyText?: string; + options?: Global.BaseOption[]; + defaultValue?: string; + headerHeight?: number; + lang?: string; + onChange?: (value: string | number) => void; +} + +const Header = styled.div` + width: 100%; + padding-inline: 12px 10px; + display: flex; + align-items: center; + justify-content: space-between; + background-color: var(--color-editor-header-bg); +`; + +const CommandViewer: React.FC = (props) => { + const { + code, + copyText = '', + defaultValue, + options = [], + headerHeight = 40, + lang, + onChange + } = props || {}; + + const [value, setValue] = React.useState(defaultValue || ''); + + const handlOnChange = (value: string | number) => { + setValue(value as string); + onChange?.(value); + }; + + return ( + + + + + + } + > + + + ); +}; + +export default CommandViewer; diff --git a/src/pages/backends/backend-create.tsx b/src/pages/backends/backend-create.tsx new file mode 100644 index 00000000..a8b4ede5 --- /dev/null +++ b/src/pages/backends/backend-create.tsx @@ -0,0 +1,249 @@ +import { PageAction } from '@/config'; +import { PageActionType } from '@/config/types'; +import ClusterSteps from '@/pages/cluster-management/components/cluster-steps'; +import FooterButtons from '@/pages/cluster-management/components/footer-buttons'; +import { PageContainer } from '@ant-design/pro-components'; +import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; +import _ from 'lodash'; +import React, { useMemo, useRef, useState } from 'react'; +import styled from 'styled-components'; +import { createBackend } from './apis'; +import { FormData } from './config/types'; +import { moduleMap, moduleRegistry } from './step-forms/module-registry'; +import useStepList from './step-forms/use-step-list'; + +const Container = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +`; + +const Nav = styled.div` + display: flex; + align-items: center; + height: 72px; + font-weight: 400; + font-size: 20px; + color: var(--ant-color-text-tertiary); + .level-2 { + color: var(--ant-color-text); + font-weight: 600; + } +`; + +const Content = styled.div` + width: 600px; +`; + +const HeaderContainer = styled.div` + position: relative; + display: grid; + grid-template-columns: 1fr; + align-items: center; + padding-inline: var(--layout-content-header-inlinepadding); + .text { + margin-right: 16px; + padding-right: 16px; + border-right: 1px solid var(--ant-color-split); + font-weight: 600; + font-size: 20px; + margin-right: 32px; + } +`; + +const ClusterCreate = () => { + const intl = useIntl(); + const startStep = 0; + const steps = useStepList(); + const [searchParams] = useSearchParams(); + const action = + (searchParams.get('action') as PageActionType) || PageAction.CREATE; + const navigate = useNavigate(); + + const [currentStep, setCurrentStep] = useState(startStep); + + const [extraData, setExtraData] = useState({ + default_version: '' + } as FormData); + const [formValues, setFormValues] = useState>({}); + + const formRefs: Record = { + [moduleMap.BasicForm]: useRef(null), + [moduleMap.ParamertersForm]: useRef(null), + [moduleMap.VersionConfigForm]: useRef(null) + }; + + const handleStepChange = (newStep: number) => { + setCurrentStep(newStep); + console.log('step========', newStep); + }; + + const getFormFieldsValue = () => { + setFormValues((prev) => { + const newFormValues = _.cloneDeep(prev); + for (const [formKey, formRef] of Object.entries(formRefs)) { + const formValues = formRef?.current?.getFieldsValue?.(); + if (formValues) { + newFormValues[formKey] = formValues; + } + } + return newFormValues; + }); + }; + + const onPrevious = () => { + getFormFieldsValue(); + setCurrentStep((prev) => Math.max(prev - 1, 0)); + }; + + const validateForms = async () => { + const step = steps[currentStep]; + const formKeys = step?.showForms || []; + if (formKeys?.length > 0) { + const results = await Promise.allSettled( + formKeys.map((key) => formRefs[key].current?.validateFields()) + ); + + console.log('results========', results); + + const resultsMap = new Map(); + results.forEach((result, index) => { + if (result.status === 'rejected') { + const values = result.reason?.values || {}; + resultsMap.set(formKeys[index], values); + } else if (result.status === 'fulfilled') { + resultsMap.set(formKeys[index], result.value); + } + }); + const newValues = _.merge(formValues, Object.fromEntries(resultsMap)); + + setFormValues(newValues); + + const isValid = results.every((result) => result.status === 'fulfilled'); + if (!isValid) { + return false; + } + return Object.assign({}, ...Object.values(newValues)); + } + return true; + }; + + const onNext = async (callback?: (values: FormData) => void) => { + try { + const result = await validateForms(); + console.log('results========2', result); + if (result) { + await callback?.(result as FormData); + const step = steps[currentStep]; + step.beforeNext?.(); + setCurrentStep((prev) => Math.min(prev + 1, steps.length - 1)); + } + } catch (error) { + console.log('next error:', error); + } + }; + + const resetForm = () => { + setFormValues({}); + for (const [formKey, formRef] of Object.entries(formRefs)) { + formRefs[formKey] = React.createRef(); + } + }; + + const renderForms = () => { + const step = steps[currentStep]; + const formKeys = step?.showForms || []; + if (formKeys.length === 0) { + return null; + } + + return formKeys.map((key) => { + console.log('renderForms key========', key, formValues); + const FormComponent = moduleRegistry[key]; + return FormComponent ? ( + + ) : null; + }); + }; + + const showButtons = useMemo(() => { + const step = steps[currentStep]; + return step?.showButtons ? step?.showButtons?.() : {}; + }, [currentStep, steps]); + + const submit = async (values: FormData) => { + const data = { + ...extraData, + ...(typeof values === 'object' ? values : {}) + }; + const res = await createBackend({ data }); + + return true; + }; + + const handleSubmit = async () => { + onNext(submit); + }; + + const handleCancel = () => { + navigate(-1); + }; + + return ( + + ]} + header={{ + title: false, + style: { + paddingInline: 'var(--layout-content-header-inlinepadding)' + }, + breadcrumb: {} + }} + pageHeaderRender={() => ( + + + + + )} + > + + {renderForms()} + + + ); +}; + +export default ClusterCreate; diff --git a/src/pages/backends/components/add-modal.tsx b/src/pages/backends/components/add-modal.tsx new file mode 100644 index 00000000..4acbc7b6 --- /dev/null +++ b/src/pages/backends/components/add-modal.tsx @@ -0,0 +1,148 @@ +import IconFont from '@/components/icon-font'; +import ModalFooter from '@/components/modal-footer'; +import GSDrawer from '@/components/scroller-modal/gs-drawer'; +import { PageActionType } from '@/config/types'; +import { useIntl } from '@umijs/max'; +import { Segmented, Tabs } from 'antd'; +import React, { useRef, useState } from 'react'; +import styled from 'styled-components'; +import ColumnWrapper from '../../_components/column-wrapper'; +import { FormData, ListItem } from '../config/types'; +import BackendForm from '../forms'; +import ImportYAML from './import-yaml'; + +const ModalFooterStyle = { + padding: '16px 24px', + display: 'flex', + justifyContent: 'flex-end' +}; + +const SegmentedInner = styled(Segmented)` + width: 100%; + border-radius: 0; + .ant-segmented-item { + flex: 1; + } +`; + +const SegmentedHeader = styled.div` + margin-top: 16px; + padding: 0 24px; +`; + +interface AddModalProps { + action: PageActionType; + currentData?: ListItem; // Used when action is EDIT + onClose: () => void; + onSubmit: (values: FormData) => void; + open: boolean; + title?: string; +} + +const AddModal: React.FC = (props) => { + const { action, currentData, onClose, onSubmit, open, title } = props; + const formRef = useRef(null); + const editorRef = useRef(null); + const intl = useIntl(); + const [activeKey, setActiveKey] = useState('form'); + + const onOk = () => { + if (activeKey === 'yaml') { + const content = editorRef.current?.getContent(); + const valid = editorRef.current?.validate(); + if (valid) { + onSubmit({ content: content }); + } + } else { + formRef.current?.submit(); + } + }; + + const onFinish = (values: FormData) => { + onSubmit(values); + }; + + return ( + + + setActiveKey(value as string)} + options={[ + { + label: 'Form', + value: 'form', + icon: + }, + { + label: 'YAML', + value: 'yaml', + icon: + } + ]} + size="middle" + style={{ width: '100%' }} + /> + + + } + > + <>} + activeKey={activeKey} + defaultActiveKey={activeKey} + items={[ + { + key: 'form', + label: 'Form', + children: ( + + ) + }, + { + key: 'yaml', + label: 'YAML', + children: ( + + ) + } + ]} + > + + + ); +}; + +export default AddModal; diff --git a/src/pages/backends/components/backend-card.tsx b/src/pages/backends/components/backend-card.tsx index eaf90753..edf53377 100644 --- a/src/pages/backends/components/backend-card.tsx +++ b/src/pages/backends/components/backend-card.tsx @@ -1,6 +1,7 @@ import DropDownActions from '@/components/drop-down-actions'; import IconFont from '@/components/icon-font'; import Card from '@/components/templates/card'; +import { DockerOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import styled from 'styled-components'; import { backendActions } from '../config'; @@ -22,8 +23,17 @@ const Content = styled.div` display: flex; flex-direction: column; align-items: flex-start; - margin-top: 12x; - gap: 16px; + margin-top: 8px; + gap: 12px; + color: var(--ant-color-text-secondary); + .description { + margin-block: 12px; + color: var(--ant-color-text-tertiary); + } + .text { + margin-left: 4px; + color: var(--ant-color-text-secondary); + } .title { color: var(--ant-color-text); font-weight: 500; @@ -47,12 +57,13 @@ const BackendCard: React.FC = ({ onClick={() => onClick(data)} clickable={true} disabled={false} - height={205} + height={'auto'} ghost header={
{data.backend_name} + {data.backend_name}
= ({ } > -
{data.backend_name}
-
This is a description
-
默认版本:{data.default_version}
+
This is a description
- 镜像:{data.version_configs?.[data.default_version]?.image_name} + Default + version: {data.default_version} +
+
+ Default Image:{' '} + + {data.version_configs?.[data.default_version]?.image_name} +
diff --git a/src/pages/backends/components/import-yaml.tsx b/src/pages/backends/components/import-yaml.tsx new file mode 100644 index 00000000..4da288a7 --- /dev/null +++ b/src/pages/backends/components/import-yaml.tsx @@ -0,0 +1,145 @@ +import { PageAction } from '@/config'; +import { PageActionType } from '@/config/types'; +import useUserSettings from '@/hooks/use-user-settings'; +import { ImportOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, message, Typography, Upload } from 'antd'; +import { RcFile } from 'antd/lib/upload'; +import React, { + forwardRef, + useImperativeHandle, + useRef, + useState +} from 'react'; +import styled from 'styled-components'; +import { yamlTemplate } from '../config'; +import YamlEditor from './yaml-editor'; + +const { Text } = Typography; + +const ErrorText = styled(Text)` + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 4px 6px; + background-color: var(--ant-color-bg-elevated); + border-radius: 0 0 var(--ant-border-radius) var(--ant-border-radius); +`; + +const Header = styled.div` + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + height: 40px; + padding-inline: 10px; + border-bottom: 1px solid var(--ant-color-border); + background-color: var(--ant-color-fill-tertiary); +`; + +const Container = styled.div` + position: relative; + border: 1px solid var(--ant-color-border); + border-radius: var(--ant-border-radius); + .monaco-editor .scroll-decoration { + box-shadow: none; + } +`; + +interface ImportYAMLProps { + ref?: any; + action: PageActionType; + content?: string; + onSubmit?: (content: string) => void; +} + +const ImportYAML: React.FC = forwardRef( + ({ action, content = '' }, ref) => { + const intl = useIntl(); + const { userSettings } = useUserSettings(); + const editorRef = useRef(null); + const [fileContent, setFileContent] = useState( + action === PageAction.CREATE ? yamlTemplate : content + ); + const [error, setError] = useState(''); + + const beforeUpload = (file: RcFile) => { + const isYaml = + file.type === 'application/x-yaml' || + file.type === 'text/yaml' || + file.name.endsWith('.yaml') || + file.name.endsWith('.yml'); + if (!isYaml) { + message.error('You can only upload YAML file!'); + } + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result; + if (typeof content === 'string') { + setFileContent(content); + } else { + message.error('Failed to read file content!'); + } + }; + reader.readAsText(file); + // Prevent upload + return false; + }; + + const validate = () => { + const markers = editorRef.current?.validate(); + if (markers && markers.length > 0) { + setError(markers[0].message); + } + return markers.length === 0; + }; + + const renderHeader = () => { + return ( +
+ YAML Editor + + + +
+ ); + }; + + useImperativeHandle(ref, () => ({ + getContent: () => { + return editorRef.current?.getValue(); + }, + setContent: (val: string) => { + editorRef.current?.setValue?.(val); + }, + validate() { + return validate(); + } + })); + + return ( + + + {error && {error}} + + ); + } +); + +export default ImportYAML; diff --git a/src/pages/backends/components/yaml-editor.tsx b/src/pages/backends/components/yaml-editor.tsx new file mode 100644 index 00000000..198d6f8d --- /dev/null +++ b/src/pages/backends/components/yaml-editor.tsx @@ -0,0 +1,167 @@ +import EditorWrap from '@/components/editor-wrap'; +import { LoadingOutlined } from '@ant-design/icons'; +import Editor, { loader } from '@monaco-editor/react'; +import * as monaco from 'monaco-editor'; +import { configureMonacoYaml } from 'monaco-yaml'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef +} from 'react'; +import schema from '../config/schema.json'; + +loader.config({ monaco }); + +interface ViewerProps { + ref?: any; + lang: string; + defaultLang?: string; + config?: any; + value: string; + height?: string | number; + theme?: string; + header?: React.ReactNode; + placeholder?: string; + variant?: 'bordered' | 'borderless'; +} + +const path = 'inmemory://model/config.yaml'; + +const ViewerEditor: React.FC = forwardRef((props, ref) => { + const { + lang, + value, + config, + defaultLang, + height = 380, + theme = 'vs-dark', + header, + variant = 'borderless', + placeholder + } = props; + + const editorRef = useRef(null); + const monacoRef = useRef(null); + const monacoYamlRef = useRef(null); + + const handleBeforeMount = (monaco: any) => { + const yamlUri = monaco.Uri.parse(path); + + configureMonacoYaml(monaco, { + schemas: [ + { + uri: 'http://example.com/schema-name.json', + fileMatch: [yamlUri.toString()], + schema + } + ] + }); + }; + + const handleEditorDidMount = (editor: any, monaco: any) => { + editorRef.current = editor; + monacoRef.current = monaco; + const uri = monaco.Uri.parse(path); + + // monaco.editor.onDidChangeMarkers((uris: string[]) => { + // if (!uris.some((u) => u.toString() === uri.toString())) return; + + // const model = monaco.editor.getModel(uri); + // if (!model) return; + // console.log('Markers changed:', uris); + // const markers = monaco.editor.getModelMarkers({ resource: uri }); + // const adjusted = markers.map((m: any) => { + // if (m._adjusted) return m; + // return { + // ...m, + // _adjusted: true, + // severity: monaco.MarkerSeverity.ErrorText + // }; + // }); + + // monaco.editor.setModelMarkers( + // monaco.editor.getModel(uri), + // 'yaml', + // adjusted + // ); + // }); + }; + + const formatCode = () => { + if (editorRef.current) { + setTimeout(() => { + editorRef.current + ?.getAction?.('editor.action.formatDocument') + ?.run() + .then(() => { + console.log('format success'); + }); + }, 100); + } + }; + + const getMarkers = () => { + const uri = editorRef.current?.getModel()?.uri; + const markers = monacoRef.current?.editor.getModelMarkers({ + resource: uri + }); + console.log('Validation markers:', uri, markers); + return markers; + }; + + useImperativeHandle(ref, () => ({ + format: () => { + formatCode(); + }, + getValue: () => { + return editorRef.current?.getValue?.(); + }, + setValue: (val: string) => { + editorRef.current?.setValue?.(val); + }, + dispose: () => { + editorRef.current?.dispose?.(); + monacoYamlRef.current?.dispose?.(); + }, + validate() { + return getMarkers(); + }, + editor: editorRef.current + })); + + useEffect(() => { + formatCode(); + }, [value]); + + return ( + + } + beforeMount={handleBeforeMount} + onMount={handleEditorDidMount} + /> + + ); +}); + +export default ViewerEditor; diff --git a/src/pages/backends/config/index.ts b/src/pages/backends/config/index.ts index 43c163a9..e7ab5f4c 100644 --- a/src/pages/backends/config/index.ts +++ b/src/pages/backends/config/index.ts @@ -5,21 +5,21 @@ export const backendActions = [ label: 'Edit Versions', value: 'version', key: 'version', - icon: icons.EditOutlined, + icon: icons.EditContent, locale: false }, { label: 'Edit Parameters', value: 'parameter', key: 'parameter', - icon: icons.EditOutlined, + icon: icons.Parameter, locale: false }, { label: 'Edit by Yaml', value: 'yaml', key: 'edit', - icon: icons.EditOutlined, + icon: icons.Yaml, locale: false }, { @@ -31,3 +31,21 @@ export const backendActions = [ danger: true } ]; + +export const yamlTemplate = ` +name: SGLang +default_version: v0.5.1 +health_check_path: /health +allowed_proxy_uris: + - api/chat + - api/v1 +versions: + v0.0.1: + image: lm/sglang + command: run sglang + v0.0.2: + image: lm/sglang + command: run sglang +default_backend_parameters: + - --host + `; diff --git a/src/pages/backends/config/schema.json b/src/pages/backends/config/schema.json new file mode 100644 index 00000000..a1d94a46 --- /dev/null +++ b/src/pages/backends/config/schema.json @@ -0,0 +1,56 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "backend name", + "severity": "error" + }, + "default_version": { + "type": "string", + "description": "default backend version" + }, + "health_check_path": { + "type": "string", + "description": "health check path" + }, + "allowed_proxy_uris": { + "type": "array", + "items": { + "type": "string" + }, + "description": "allowed proxy URIs" + }, + "versions": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "type": "object", + "properties": { + "image": { + "type": "string", + "description": "image name" + }, + "command": { + "type": "string", + "description": "start command" + } + }, + "required": ["image", "command"], + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "multiple backend versions" + }, + "default_backend_parameters": { + "type": "array", + "items": { + "type": "string" + }, + "description": "default backend parameters" + } + }, + "required": ["name", "default_version", "health_check_path", "versions"], + "additionalProperties": false +} diff --git a/src/pages/backends/config/test.ts b/src/pages/backends/config/test.ts index bef7b5b0..986a5837 100644 --- a/src/pages/backends/config/test.ts +++ b/src/pages/backends/config/test.ts @@ -1,4 +1,4 @@ -import nvidiaLogo from '@/assets/logo/nvidia.png'; +import lmDeployLogo from '@/assets/logo/lmdeploy.png'; import SGLangLogo from '@/assets/logo/sglang.png'; import vLLMLogo from '@/assets/logo/vllm.png'; @@ -37,8 +37,8 @@ export default [ }, { id: 3, - backend_name: 'TensorRT', - icon: nvidiaLogo, + backend_name: 'LMDeploy', + icon: lmDeployLogo, version_configs: { 'v1.0': { image_name: 'example/image:v1.0', @@ -49,6 +49,6 @@ export default [ health_check_path: '/v1/models', default_version: 'v1.0', default_backend_param: ['--param1', '--param2'], - description: 'TensorRT description' + description: 'LMDeploy description' } ]; diff --git a/src/pages/backends/config/types.ts b/src/pages/backends/config/types.ts index 87800c40..6364bbff 100644 --- a/src/pages/backends/config/types.ts +++ b/src/pages/backends/config/types.ts @@ -6,7 +6,7 @@ export interface VersionConfigs { } export interface FormData { - backend_name: string; + backend_name?: string; description?: string; version_configs?: VersionConfigs; compatibility_type?: string; @@ -14,10 +14,11 @@ export interface FormData { default_version?: string; default_backend_param?: string[]; allowed_proxy_uris?: string[]; + content?: string; } export interface ListItem extends FormData { - id: number; - created_at: string; - updated_at: string; + id?: number; + created_at?: string; + updated_at?: string; } diff --git a/src/pages/backends/forms/basic.tsx b/src/pages/backends/forms/basic.tsx new file mode 100644 index 00000000..524dde62 --- /dev/null +++ b/src/pages/backends/forms/basic.tsx @@ -0,0 +1,77 @@ +import ListInput from '@/components/list-input'; +import SealInput from '@/components/seal-form/seal-input'; +import SealSelect from '@/components/seal-form/seal-select'; +import { PageActionType } from '@/config/types'; +import useAppUtils from '@/hooks/use-app-utils'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import React from 'react'; +import { FormData } from '../config/types'; + +type AddModalProps = { + action: PageActionType; +}; +const BasicForm: React.FC = ({ action }) => { + const form = Form.useFormInstance(); + const intl = useIntl(); + const { getRuleMessage } = useAppUtils(); + + return ( + <> + + name="backend_name" + rules={[ + { + required: true, + message: getRuleMessage('input', 'common.table.name') + } + ]} + > + + + + + name="compatibility_type" + rules={[ + { + required: true, + message: getRuleMessage('select', 'template', false) + } + ]} + > + + + + + name="allowed_proxy_uris" + rules={[{ required: false }]} + > + { + form.setFieldValue('allowed_proxy_uris', data); + }} + btnText={'Add Allowed Proxy URI'} + label={'Proxy URI'} + > + + name="description" rules={[{ required: false }]}> + + + + ); +}; + +export default BasicForm; diff --git a/src/pages/backends/forms/index.tsx b/src/pages/backends/forms/index.tsx new file mode 100644 index 00000000..790d75dd --- /dev/null +++ b/src/pages/backends/forms/index.tsx @@ -0,0 +1,73 @@ +import { PageActionType } from '@/config/types'; +import CollapsePanel from '@/pages/_components/collapse-panel'; +import { Form } from 'antd'; +import React, { forwardRef, useEffect, useImperativeHandle } from 'react'; +import { FormData, ListItem } from '../config/types'; +import BasicForm from './basic'; +import ParametersForm from './parameters'; +import VersionsForm from './versions-config'; + +type AddModalProps = { + action: PageActionType; + currentData?: ListItem; // Used when action is EDIT + onFinish: (values: FormData) => void; + ref?: any; +}; +const BackendForm: React.FC = forwardRef( + ({ action, currentData, onFinish }, ref) => { + const [form] = Form.useForm(); + + useEffect(() => { + if (currentData) { + form.setFieldsValue(currentData); + } + }, [currentData]); + + useImperativeHandle(ref, () => ({ + resetFields: () => { + form.resetFields(); + }, + submit: () => { + form.submit(); + }, + setFieldsValue: (values: any) => { + form.setFieldsValue(values); + }, + getFieldsValue: () => { + return form.getFieldsValue(); + }, + validateFields: async () => { + return await form.validateFields(); + } + })); + + return ( +
+ + + }, + { + key: 'parameters', + label: 'Default Parameters', + children: + } + ]} + > +
+ ); + } +); + +export default BackendForm; diff --git a/src/pages/backends/forms/parameters.tsx b/src/pages/backends/forms/parameters.tsx new file mode 100644 index 00000000..c1ccadb7 --- /dev/null +++ b/src/pages/backends/forms/parameters.tsx @@ -0,0 +1,32 @@ +import ListInput from '@/components/list-input'; +import { PageActionType } from '@/config/types'; +import { Form } from 'antd'; +import React from 'react'; +import { FormData } from '../config/types'; + +type AddModalProps = { + action: PageActionType; +}; +const ParametersForm: React.FC = ({ action }) => { + const form = Form.useFormInstance(); + + return ( + <> + + name="default_backend_param" + rules={[{ required: false }]} + > + { + form.setFieldValue('default_backend_param', data); + }} + btnText={'Add Backend Parameter'} + label={'Default Backend Parameters'} + > + + + ); +}; + +export default ParametersForm; diff --git a/src/pages/backends/forms/versions-config.tsx b/src/pages/backends/forms/versions-config.tsx new file mode 100644 index 00000000..57417ce7 --- /dev/null +++ b/src/pages/backends/forms/versions-config.tsx @@ -0,0 +1,139 @@ +import SealInput from '@/components/seal-form/seal-input'; +import { PageActionType } from '@/config/types'; +import { MinusOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Checkbox, Divider, Form } from 'antd'; +import React, { useEffect } from 'react'; +import styled from 'styled-components'; + +const Box = styled.div` + display: grid; + grid-template-columns: 200px 1fr; + gap: 16px; + align-items: center; +`; + +const ItemWrapper = styled.div` + &:hover { + .divider { + border-top: 1px dashed var(--ant-color-primary) !important; + } + } +`; + +const Label = styled.div` + color: var(--ant-color-text-tertiary); + font-size: var(--font-size-base); + margin-bottom: 10px; +`; + +type AddModalProps = { + action: PageActionType; +}; +const VersionsForm: React.FC = ({ action }) => { + const form = Form.useFormInstance(); + const versionList = [ + { + version_no: '', + image_name: '', + run_command: '', + is_default: false + } + ]; + + useEffect(() => { + const versions = form.getFieldValue('version_configs') || []; + if (Array.isArray(versions) && versions.length === 0) { + form.setFieldValue('version_configs', versionList); + } + }, []); + + return ( + + {(fields, { add, remove }) => { + return ( +
+ {fields.map(({ key, name, ...restField }, index) => ( + + + + + + + + + + + + +
+ + + Set as Default Version + + +
+ {fields.length > 1 && ( + + )} +
+
+ + {index === fields.length - 1 && ( + + )} +
+ ))} +
+ ); + }} +
+ ); +}; + +export default VersionsForm; diff --git a/src/pages/backends/list.tsx b/src/pages/backends/list.tsx index 5fe163eb..878473d3 100644 --- a/src/pages/backends/list.tsx +++ b/src/pages/backends/list.tsx @@ -1,21 +1,18 @@ import { FilterBar } from '@/components/page-tools'; +import { PageActionType } from '@/config/types'; import useTableFetch from '@/hooks/use-table-fetch'; -import { - DeleteOutlined, - ImportOutlined, - PlusOutlined -} from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; import { useIntl } from '@umijs/max'; -import { Button, Space } from 'antd'; +import { useState } from 'react'; import { deleteBackend, INFERENCE_BACKEND_API, queryBackendsList } from './apis'; +import AddModal from './components/add-modal'; import BackendCardList from './components/backend-list'; import dataList from './config/test'; -import { ListItem } from './config/types'; +import { FormData, ListItem } from './config/types'; const BackendList = () => { const intl = useIntl(); @@ -40,10 +37,27 @@ const BackendList = () => { watch: false, contentForDelete: 'resources.modelfiles.modelfile' }); + const [openModalStatus, setOpenModalStatus] = useState<{ + open: boolean; + action: PageActionType; + currentData?: Partial; + }>({ open: false, action: 'create' }); - const handleClickDropdown = () => {}; + const handleOnSubmit = (values: FormData) => { + console.log('yaml content:', values); + setOpenModalStatus({ + open: false, + action: 'create', + currentData: undefined + }); + }; - const handleDeleteByBatch = () => {}; + const handleAddBackend = () => { + setOpenModalStatus({ + open: true, + action: 'create' + }); + }; return ( { width={{ input: 300 }} - actionType="dropdown" inputHolder={intl.formatMessage({ id: 'common.filter.name' })} - buttonText={intl.formatMessage({ - id: 'resources.modelfiles.download' - })} - handleDeleteByBatch={handleDeleteByBatch} - handleClickPrimary={handleClickDropdown} + buttonText={'Add Backend'} + handleDeleteByBatch={handleDeleteBatch} + handleClickPrimary={handleAddBackend} handleSearch={handleSearch} handleInputChange={handleNameChange} rowSelection={rowSelection} showSelect={false} - right={ - - - - - - } > { activeId={false} isFirst={!dataSource.loadend} > + setOpenModalStatus({ open: false, action: 'create' })} + onSubmit={handleOnSubmit} + currentData={openModalStatus.currentData} + open={openModalStatus.open} + title={ + openModalStatus.action === 'create' + ? 'Create Backend' + : 'Edit Backend' + } + > ); }; diff --git a/src/pages/backends/step-forms/basic-form.tsx b/src/pages/backends/step-forms/basic-form.tsx new file mode 100644 index 00000000..427d9b21 --- /dev/null +++ b/src/pages/backends/step-forms/basic-form.tsx @@ -0,0 +1,59 @@ +import PageTools from '@/components/page-tools'; +import { PageActionType } from '@/config/types'; +import { useIntl } from '@umijs/max'; +import { forwardRef, useImperativeHandle, useRef } from 'react'; +import styled from 'styled-components'; +import Basic from '../forms/basic'; + +const Title = styled.span` + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 700; + font-size: 16px; + .text { + font-size: 20px; + } +`; +interface BasicFormProps { + action: PageActionType; + currentData?: any; +} + +const BasicForm = forwardRef((props: BasicFormProps, ref) => { + const intl = useIntl(); + const { action, currentData } = props; + const formRef = useRef(null); + + const handleOnFinish = (values: any) => { + console.log(values); + }; + + useImperativeHandle(ref, () => ({ + validateFields: formRef.current?.validateFields, + getFieldsValue: formRef.current?.getFieldsValue, + submit: formRef.current?.submit + })); + + return ( +
+ + {intl.formatMessage({ id: 'clusters.create.configBasic' })} + + } + marginTop={0} + > + +
+ ); +}); + +export default BasicForm; diff --git a/src/pages/backends/step-forms/module-registry.ts b/src/pages/backends/step-forms/module-registry.ts new file mode 100644 index 00000000..0c2dc8c4 --- /dev/null +++ b/src/pages/backends/step-forms/module-registry.ts @@ -0,0 +1,16 @@ +import React from 'react'; +import BasicForm from './basic-form'; +import ParamertersForm from './parameters-form'; +import VersionConfigForm from './version-config-form'; + +export const moduleMap = { + BasicForm: 'BasicForm', + VersionConfigForm: 'VersionConfigForm', + ParamertersForm: 'ParamertersForm' +}; + +export const moduleRegistry: Record> = { + [moduleMap.BasicForm]: BasicForm, + [moduleMap.VersionConfigForm]: VersionConfigForm, + [moduleMap.ParamertersForm]: ParamertersForm +}; diff --git a/src/pages/backends/step-forms/parameters-form.tsx b/src/pages/backends/step-forms/parameters-form.tsx new file mode 100644 index 00000000..75788791 --- /dev/null +++ b/src/pages/backends/step-forms/parameters-form.tsx @@ -0,0 +1,55 @@ +import PageTools from '@/components/page-tools'; +import { PageActionType } from '@/config/types'; +import { useIntl } from '@umijs/max'; +import { forwardRef, useImperativeHandle, useRef } from 'react'; +import styled from 'styled-components'; +import Parameters from '../forms/parameters'; + +const Title = styled.span` + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 700; + font-size: 16px; + .text { + font-size: 20px; + } +`; +interface BasicFormProps { + action: PageActionType; + currentData?: any; +} + +const ParametersForm = forwardRef((props: BasicFormProps, ref) => { + const intl = useIntl(); + const { action, currentData } = props; + const formRef = useRef(null); + + const handleOnFinish = (values: any) => { + console.log(values); + }; + + useImperativeHandle(ref, () => ({ + validateFields: formRef.current?.validateFields, + getFieldsValue: formRef.current?.getFieldsValue, + submit: formRef.current?.submit + })); + + return ( +
+ Parameters Configuration} + marginTop={0} + > + +
+ ); +}); + +export default ParametersForm; diff --git a/src/pages/backends/step-forms/use-step-list.tsx b/src/pages/backends/step-forms/use-step-list.tsx new file mode 100644 index 00000000..5f21cb7b --- /dev/null +++ b/src/pages/backends/step-forms/use-step-list.tsx @@ -0,0 +1,65 @@ +import { useIntl, useNavigate } from '@umijs/max'; +import { useMemoizedFn } from 'ahooks'; +import { useMemo } from 'react'; +import { moduleMap } from './module-registry'; + +export default function useStepList() { + const intl = useIntl(); + const navigate = useNavigate(); + + const handleBack = useMemoizedFn(() => { + navigate(-1); + }); + + return useMemo( + () => [ + { + title: 'Basic Info', + content: '', + showButtons: () => { + return { + previous: false, + next: true, + save: false, + skip: false + }; + }, + defaultShow: true, + showForms: [moduleMap.BasicForm], + showModules: [] + }, + { + title: 'Version Configuration', + content: '', + showButtons: () => { + return { + previous: true, + next: true, + save: false, + skip: false + }; + }, + defaultShow: true, + showForms: [moduleMap.VersionConfigForm], + showModules: [] + }, + { + title: 'Parameters', + content: '', + showButtons: () => { + return { + previous: true, + next: false, + save: true, + skip: false + }; + }, + defaultShow: true, + showForms: [moduleMap.ParamertersForm], + showModules: [], + beforeNext: handleBack + } + ], + [handleBack, intl] + ); +} diff --git a/src/pages/backends/step-forms/version-config-form.tsx b/src/pages/backends/step-forms/version-config-form.tsx new file mode 100644 index 00000000..a033e6e3 --- /dev/null +++ b/src/pages/backends/step-forms/version-config-form.tsx @@ -0,0 +1,227 @@ +import PageTools from '@/components/page-tools'; +import { PageActionType } from '@/config/types'; +import { PlusOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, FormInstance } from 'antd'; +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState +} from 'react'; +import styled from 'styled-components'; +import { FormData } from '../config/types'; +import VersionsConfig from '../forms/versions-config'; + +const PoolContainer = styled.div` + display: flex; + align-items: flex-start; + gap: 16px; +`; + +const PoolFormWrapper = styled.div` + flex: 1; + margin-bottom: 16px; +`; + +const Title = styled.span` + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 700; + font-size: 16px; + .text { + font-size: 20px; + } +`; +interface WorkerPoolsFormProps { + action: PageActionType; + currentData?: any; +} + +const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { + const intl = useIntl(); + const { action, currentData } = props; + const countRef = useRef(0); + const formRefs = useRef | null>>({}); + const [activeKey, setActiveKey] = useState>(new Set([0])); + const [versionConfigs, setVersionConfigs] = useState>( + new Map([ + [ + 0, + { + version_name: '', + image_name: '', + run_command: '' + } + ] + ]) as Map + ); + + const handleOnFinish = (values: any) => {}; + + const updateCount = () => { + countRef.current += 1; + return countRef.current; + }; + + const handleAddPool = () => { + const newId = updateCount(); + setVersionConfigs((prev) => + new Map(prev).set(newId, { + name: `` + } as FormData) + ); + + requestAnimationFrame(() => { + setActiveKey((prev) => new Set([newId])); + window.scrollTo({ + top: document.body.scrollHeight, + behavior: 'smooth' + }); + }); + }; + + const handleRemovePool = (id: number) => { + if (versionConfigs.size === 1) { + return; + } + + setVersionConfigs((prev) => { + const newList = new Map(prev); + newList.delete(id); + return newList; + }); + formRefs.current[id] = null; + }; + + const gatherFormValues = (results: PromiseSettledResult[]) => { + const resultList = results.map((result: PromiseSettledResult) => { + if (result.status === 'fulfilled') { + return result.value; + } + if (result.status === 'rejected') { + return result.reason?.values || {}; + } + return {}; + }); + console.log('gatherFormValues========', resultList); + return resultList.filter((item) => item); + }; + + const validateFields = async () => { + const promises = Array.from(Object.values(formRefs.current)).map((form) => + form?.validateFields() + ); + const results = await Promise.allSettled(promises); + console.log('results========0', promises, results); + if (results.some((result) => result.status === 'rejected')) { + return Promise.reject({ + values: { + worker_pools: gatherFormValues(results) + } + }); + } + return Promise.resolve({ + worker_pools: gatherFormValues(results) + }); + }; + + const setFieldsValue = (data: any) => { + const worker_pools = data.worker_pools || []; + const newWorkerPools = worker_pools.map( + (poolData: FormData, index: number) => [index, poolData] + ); + console.log('newWorkerPools========', newWorkerPools); + setVersionConfigs(new Map(newWorkerPools)); + }; + + const getFieldsValue = () => { + const values = Object.values(formRefs.current).map((form) => + form?.getFieldsValue() + ); + console.log('getFieldsValue========', values); + return { + worker_pools: values + }; + }; + + const handleOnToggle = (open: boolean, key: number) => { + console.log('Active keys changed:', key); + if (open) { + setActiveKey((prev) => new Set([key])); + } else { + setActiveKey((prev) => { + const newSet = new Set(prev); + newSet.delete(key); + return newSet; + }); + } + }; + + useImperativeHandle(ref, () => ({ + submit: () => {}, + validateFields: validateFields, + getFieldsValue: getFieldsValue + })); + + useEffect(() => { + if (currentData) { + console.log('currentData===========1=', currentData); + setFieldsValue(currentData); + } + }, [currentData]); + + return ( +
+ + Backend Versions + + } + right={ + + } + > + + {Array.from(versionConfigs.keys()).map((key, index) => ( + + + { + if (el) { + formRefs.current[key] = el; + } + }} + collapseProps={{ + collapsible: true, + open: activeKey.has(key), + defaultOpen: activeKey.has(key), + onToggle: (open: boolean) => handleOnToggle(open, key) + }} + showDelete={versionConfigs.size > 1} + onFinish={handleOnFinish} + currentData={versionConfigs.get(key)} + onDelete={() => handleRemovePool(key)} + > + + + ))} +
+ ); +}); + +export default WorkerPoolsForm; diff --git a/src/pages/llmodels/catalog.tsx b/src/pages/llmodels/catalog.tsx index 34036b88..49cdfe16 100644 --- a/src/pages/llmodels/catalog.tsx +++ b/src/pages/llmodels/catalog.tsx @@ -5,7 +5,7 @@ import BaseSelect from '@/components/seal-form/base/select'; import { PageAction } from '@/config'; import useBodyScroll from '@/hooks/use-body-scroll'; import { IS_FIRST_LOGIN, writeState } from '@/utils/localstore/index'; -import { SyncOutlined } from '@ant-design/icons'; +import { SearchOutlined, SyncOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; import { useIntl, useNavigate } from '@umijs/max'; import { Button, Input, Pagination, Space, message } from 'antd'; @@ -266,6 +266,11 @@ const Catalog: React.FC = () => { left={ + } placeholder={intl.formatMessage({ id: 'common.filter.name' })} style={{ width: 230 }} size="large" diff --git a/src/pages/llmodels/components/data-form.tsx b/src/pages/llmodels/components/data-form.tsx index 426ace1b..d81cad9f 100644 --- a/src/pages/llmodels/components/data-form.tsx +++ b/src/pages/llmodels/components/data-form.tsx @@ -161,7 +161,6 @@ const DataForm: React.FC = forwardRef((props, ref) => { form={form} onFinish={handleOk} preserve={false} - style={{ padding: '16px 24px' }} clearOnDestroy={true} onValuesChange={handleOnValuesChange} scrollToFirstError={true} diff --git a/src/pages/llmodels/components/deploy-builtin-modal.tsx b/src/pages/llmodels/components/deploy-builtin-modal.tsx index d60a8fb6..5f55886b 100644 --- a/src/pages/llmodels/components/deploy-builtin-modal.tsx +++ b/src/pages/llmodels/components/deploy-builtin-modal.tsx @@ -8,6 +8,7 @@ import { Button } from 'antd'; import _ from 'lodash'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; +import ColumnWrapper from '../../_components/column-wrapper'; import { queryCatalogItemSpec } from '../apis'; import { defaultFormValues, @@ -21,7 +22,6 @@ import { CatalogSpec, FormData, ListItem, SourceType } from '../config/types'; import { generateGPUIds } from '../config/utils'; import { useCheckCompatibility } from '../hooks'; import useFormInitialValues from '../hooks/use-form-initial-values'; -import ColumnWrapper from './column-wrapper'; import CompatibilityAlert from './compatible-alert'; import DataForm from './data-form'; diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index 31ded897..b185a234 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -7,6 +7,7 @@ import { Button } from 'antd'; import _ from 'lodash'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; +import ColumnWrapper from '../../_components/column-wrapper'; import { defaultFormValues, deployFormKeyMap, modelSourceMap } from '../config'; import { backendOptionsMap } from '../config/backend-parameters'; import { BackendOption, FormData, SourceType } from '../config/types'; @@ -17,7 +18,6 @@ import { useSelectModel } from '../hooks'; import useCheckBackend from '../hooks/use-check-backend'; -import ColumnWrapper from './column-wrapper'; import CompatibilityAlert from './compatible-alert'; import DataForm from './data-form'; import GGUFResult from './gguf-result'; diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 4c964db3..034fc77b 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -13,7 +13,7 @@ import useTableRowSelection from '@/hooks/use-table-row-selection'; import useTableSort from '@/hooks/use-table-sort'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; import { handleBatchRequest } from '@/utils'; -import { DownOutlined, SyncOutlined } from '@ant-design/icons'; +import { DownOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; import { useIntl, useNavigate } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; @@ -579,6 +579,11 @@ const Models: React.FC = ({ left={ + } placeholder={intl.formatMessage({ id: 'common.filter.name' })} style={{ width: 160 }} size="large" diff --git a/src/pages/llmodels/components/update-modal.tsx b/src/pages/llmodels/components/update-modal.tsx index bbd926ba..b8dbc604 100644 --- a/src/pages/llmodels/components/update-modal.tsx +++ b/src/pages/llmodels/components/update-modal.tsx @@ -4,6 +4,7 @@ import { useIntl } from '@umijs/max'; import { Button, Modal } from 'antd'; import _ from 'lodash'; import React, { useEffect, useMemo, useRef } from 'react'; +import ColumnWrapper from '../../_components/column-wrapper'; import { deployFormKeyMap, modelSourceMap, @@ -14,7 +15,6 @@ import { backendOptionsMap } from '../config/backend-parameters'; import { BackendOption, FormData } from '../config/types'; import { generateGPUSelector } from '../config/utils'; import { useCheckCompatibility } from '../hooks'; -import ColumnWrapper from './column-wrapper'; import CompatibilityAlert from './compatible-alert'; import DataForm from './data-form'; diff --git a/src/pages/llmodels/download/index.tsx b/src/pages/llmodels/download/index.tsx index 33aa1517..91a2c7be 100644 --- a/src/pages/llmodels/download/index.tsx +++ b/src/pages/llmodels/download/index.tsx @@ -4,7 +4,7 @@ import { ProviderValueMap } from '@/pages/cluster-management/config'; import { useIntl } from '@umijs/max'; import { debounce } from 'lodash'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import ColumnWrapper from '../components/column-wrapper'; +import ColumnWrapper from '../../_components/column-wrapper'; import CompatibilityAlert from '../components/compatible-alert'; import ModelCard from '../components/model-card'; import SearchModel from '../components/search-model'; diff --git a/src/pages/llmodels/download/target-form.tsx b/src/pages/llmodels/download/target-form.tsx index fda2afde..85992761 100644 --- a/src/pages/llmodels/download/target-form.tsx +++ b/src/pages/llmodels/download/target-form.tsx @@ -87,7 +87,6 @@ const TargetForm: React.FC = forwardRef((props, ref) => { form={form} onFinish={handleOk} preserve={false} - style={{ padding: '16px 24px' }} clearOnDestroy={true} initialValues={{ source: source diff --git a/src/pages/playground/components/view-common-code.tsx b/src/pages/playground/components/view-common-code.tsx index 7a325574..17019b4c 100644 --- a/src/pages/playground/components/view-common-code.tsx +++ b/src/pages/playground/components/view-common-code.tsx @@ -1,9 +1,8 @@ -import EditorWrap from '@/components/editor-wrap'; -import HighlightCode from '@/components/highlight-code'; import { BulbOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { Button, Modal } from 'antd'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import CommandViewer from '../../_components/command-viewer'; type ViewModalProps = { title?: string; @@ -32,30 +31,30 @@ const ViewCodeModal: React.FC = (props) => { const { title, open, onCancel, viewCodeContent } = props || {}; const intl = useIntl(); - const [lang, setLang] = useState(langMap.shell); - - const codeValue = useMemo(() => { - if (lang === langMap.shell) { - return viewCodeContent?.curlCode; - } - if (lang === langMap.javascript) { - return viewCodeContent?.nodeJsCode; - } - if (lang === langMap.python) { - return viewCodeContent?.pythonCode; - } - return ''; - }, [lang, viewCodeContent]); + const [codeValue, setCodeValue] = useState(viewCodeContent?.curlCode); const handleOnChangeLang = (value: string | number) => { - setLang(value as string); + if (value === langMap.shell) { + setCodeValue(viewCodeContent?.curlCode); + } + if (value === langMap.javascript) { + setCodeValue(viewCodeContent?.nodeJsCode); + } + if (value === langMap.python) { + setCodeValue(viewCodeContent?.pythonCode); + } }; const handleClose = () => { - setLang(langMap.shell); onCancel(); }; + useEffect(() => { + if (open) { + setCodeValue(viewCodeContent?.curlCode); + } + }, [open]); + return ( <> = (props) => { {intl.formatMessage({ id: 'playground.viewcode.info' })}
- - - + onChange={handleOnChangeLang} + >
@@ -128,4 +114,4 @@ const ViewCodeModal: React.FC = (props) => { ); }; -export default React.memo(ViewCodeModal); +export default ViewCodeModal; diff --git a/src/pages/resources/components/container-install.tsx b/src/pages/resources/components/container-install.tsx index 12d93d75..e822ea47 100644 --- a/src/pages/resources/components/container-install.tsx +++ b/src/pages/resources/components/container-install.tsx @@ -1,7 +1,7 @@ import { GPUStackVersionAtom } from '@/atoms/user'; import { getAtomStorage } from '@/atoms/utils'; -import EditorWrap from '@/components/editor-wrap'; import HighlightCode from '@/components/highlight-code'; +import CommandViewer from '@/pages/_components/command-viewer'; import { useIntl } from '@umijs/max'; import { Radio } from 'antd'; import React from 'react'; @@ -98,27 +98,15 @@ const AddWorker: React.FC = (props) => { >
)} {activeKey === 'npu' ? ( - - - + onChange={handleOnChange} + lang="bash" + code={code.replace(/\\/g, '')} + > ) : (