feat: yaml editor

This commit is contained in:
jialin
2025-10-13 14:40:26 +08:00
parent e863b84016
commit 54703c67c1
48 changed files with 2062 additions and 246 deletions
+6 -13
View File
@@ -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',
+36
View File
@@ -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
}
]);
}
+1
View File
@@ -114,6 +114,7 @@ export default [
selectedIcon: 'icon-backend-filled',
defaultIcon: 'icon-backend',
access: 'canSeeAdmin',
subMenu: ['/models/backends/create'],
component: './backends/list'
}
]
+4
View File
@@ -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",
+111 -6
View File
@@ -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}
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+1 -3
View File
@@ -55,9 +55,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
});
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();
});
}
+41 -49
View File
@@ -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<string | number>[];
variant?: 'bordered' | 'borderless';
styles?: {
wrapper?: React.CSSProperties;
header?: React.CSSProperties;
content?: React.CSSProperties;
};
onChangeLang?: (value: string | number) => void;
}
const EditorWrap: React.FC<EditorwrapProps> = ({
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 <div className="editor-header">{header}</div>;
}
if (showHeader) {
return (
<Wrapper className="editor-header" $height={headerHeight}>
<SegmentLine
height={headerHeight}
defaultValue={defaultValue}
size="small"
options={langOptions}
onChange={handleChangeLang}
></SegmentLine>
<CopyButton
text={copyText}
size="small"
style={{
color: 'rgba(255,255,255,.7)'
}}
/>
</Wrapper>
);
}
return null;
};
return (
<div className="editor-wrap" style={{ ...styles.wrapper }}>
{renderHeader()}
<div className="editor-content">{children}</div>
</div>
<Wrapper
style={{ ...styles.wrapper }}
className={classNames({
bordered: variant === 'bordered',
borderless: variant === 'borderless'
})}
>
{header && <HeaderWrapper $height={headerHeight}>{header}</HeaderWrapper>}
<div>{children}</div>
</Wrapper>
);
};
export default React.memo(EditorWrap);
export default EditorWrap;
@@ -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<string>[];
config?: any;
value: string;
height?: string | number;
theme?: string;
showHeader?: boolean;
header?: React.ReactNode;
placeholder?: string;
variant?: 'bordered' | 'borderless';
}
const ViewerEditor: React.FC<ViewerProps> = (props) => {
const editorRef = useRef<any>(null);
const ViewerEditor: React.FC<ViewerProps> = 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<any>(null);
const handleBeforeMount = (monaco: any) => {
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
@@ -36,12 +45,8 @@ const ViewerEditor: React.FC<ViewerProps> = (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<ViewerProps> = (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 (
<EditorWrap
copyText={value}
langOptions={langOptions}
defaultValue={lang}
showHeader={showHeader}
onChangeLang={handleOnChangeLang}
>
<EditorWrap header={header} variant={variant}>
<Editor
height={height}
theme="vs-dark"
theme={theme}
className="monaco-editor"
defaultLanguage={defaultLang}
language={langType}
language={lang}
value={value}
options={config}
options={{
minimap: { enabled: false },
scrollbar: {
verticalScrollbarSize: 6,
horizontalScrollbarSize: 6
},
placeholder: placeholder
}}
loading={<LoadingOutlined style={{ fontSize: 24 }}></LoadingOutlined>}
beforeMount={handleBeforeMount}
onMount={handleEditorDidMount}
/>
</EditorWrap>
);
};
});
export default React.memo(ViewerEditor);
export default ViewerEditor;
+5 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+6
View File
@@ -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<FilterBarProps> = (props) => {
left={
<Space>
<Input
prefix={
<SearchOutlined
style={{ color: 'var(--ant-color-text-placeholder)' }}
></SearchOutlined>
}
placeholder={
inputHolder ||
intl.formatMessage({
@@ -127,6 +127,7 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
>
<Input.TextArea
{...rest}
spellCheck={rest.spellCheck ?? false}
autoSize={autoSize}
ref={inputRef}
style={{ minHeight: '80px', ...style }}
+3
View File
@@ -48,6 +48,7 @@ const SegmentedQwrapper = styled.div<{ $height: number }>`
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<SegmentLineProps> = (props) => {
className = 'segment-line',
...rest
} = props;
return (
<SegmentedQwrapper $height={height}>
<Segmented
-7
View File
@@ -588,13 +588,6 @@ body {
}
}
// =============== segment start==============
.ant-segmented .ant-segmented-item::after {
border-radius: var(--border-radius-mini);
}
// =============== segment end ===============
.background {
position: fixed;
top: 0;
@@ -0,0 +1,55 @@
import IconFont from '@/components/icon-font';
import { Collapse, CollapseProps } from 'antd';
import React from 'react';
import styled from 'styled-components';
const CollapseInner = styled(Collapse)`
.ant-collapse-header {
display: flex;
align-items: center;
margin-bottom: 10px !important;
padding-inline: 5px !important;
padding-block: 8px !important;
border-radius: var(--border-radius-base) !important;
font-size: 14px !important;
font-weight: 600 !important;
&:hover {
background-color: var(--ant-color-fill-tertiary) !important;
}
}
.ant-collapse-content-box {
padding-inline: 0 !important;
padding-block: 0 !important;
}
.ant-collapse-header-text {
display: flex;
align-items: center;
height: 24px;
}
`;
const CollapsePanel: React.FC<{ items: CollapseProps['items'] }> = ({
items
}) => {
return (
<CollapseInner
expandIconPosition="start"
bordered={false}
ghost
accordion
destroyOnHidden={false}
expandIcon={({ isActive }) => (
<IconFont
type="icon-down"
rotate={isActive ? 0 : -90}
style={{ fontSize: '14px' }}
></IconFont>
)}
items={items}
></CollapseInner>
);
};
export default CollapsePanel;
@@ -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<any> = ({
children,
@@ -25,24 +25,25 @@ const ColumnWrapper: React.FC<any> = ({
return (
<>
{footer ? (
<div className="column-wrapper-footer">
<div className="column-wrapper" ref={scroller} style={{ maxHeight }}>
<div
style={{
paddingBottom: paddingBottom
}}
>
{children}
</div>
<div
className="column-wrapper-footer"
style={{ height: maxHeight || '100%' }}
>
<div
className="column-wrapper"
ref={scroller}
style={{ padding: '16px 24px' }}
>
<div
style={{
paddingBottom: footer ? paddingBottom : 0
}}
>
{children}
</div>
{<div className="footer">{footer}</div>}
</div>
) : (
<div className="column-wrapper" ref={scroller} style={{ maxHeight }}>
<div>{children}</div>
</div>
)}
{footer && <div className="footer">{footer}</div>}
</div>
</>
);
};
@@ -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<string>[];
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<ViewerProps> = (props) => {
const {
code,
copyText = '',
defaultValue,
options = [],
headerHeight = 40,
lang,
onChange
} = props || {};
const [value, setValue] = React.useState<string>(defaultValue || '');
const handlOnChange = (value: string | number) => {
setValue(value as string);
onChange?.(value);
};
return (
<EditorWrap
header={
<Header>
<SegmentLine
height={headerHeight}
defaultValue={defaultValue}
value={value}
size="small"
options={options}
onChange={handlOnChange}
></SegmentLine>
<CopyButton
text={copyText || code}
size="small"
style={{
color: 'rgba(255,255,255,.7)'
}}
/>
</Header>
}
>
<HighlightCode
height={380}
theme="dark"
code={code}
lang={lang || value}
copyable={false}
></HighlightCode>
</EditorWrap>
);
};
export default CommandViewer;
+249
View File
@@ -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<number>(startStep);
const [extraData, setExtraData] = useState<FormData>({
default_version: ''
} as FormData);
const [formValues, setFormValues] = useState<Record<string, any>>({});
const formRefs: Record<string, any> = {
[moduleMap.BasicForm]: useRef<any>(null),
[moduleMap.ParamertersForm]: useRef<any>(null),
[moduleMap.VersionConfigForm]: useRef<any>(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<string, any>();
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 ? (
<FormComponent
key={key}
ref={formRefs[key]}
action={action}
currentData={formValues[key]}
/>
) : 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 (
<PageContainer
ghost
footer={[
<FooterButtons
key="buttons"
onPrevious={onPrevious}
onNext={onNext}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
showButtons={showButtons}
/>
]}
header={{
title: false,
style: {
paddingInline: 'var(--layout-content-header-inlinepadding)'
},
breadcrumb: {}
}}
pageHeaderRender={() => (
<HeaderContainer>
<Nav>
<span className="level-1">Backends</span>
<span
style={{
marginInline: 20,
color: 'var(--ant-color-split)'
}}
>
/
</span>
<span className="level-2">
{intl.formatMessage({ id: 'common.button.create' })}
</span>
</Nav>
<ClusterSteps
steps={steps}
currentStep={currentStep}
onChange={handleStepChange}
></ClusterSteps>
</HeaderContainer>
)}
>
<Container>
<Content>{renderForms()}</Content>
</Container>
</PageContainer>
);
};
export default ClusterCreate;
+148
View File
@@ -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<AddModalProps> = (props) => {
const { action, currentData, onClose, onSubmit, open, title } = props;
const formRef = useRef<any>(null);
const editorRef = useRef<any>(null);
const intl = useIntl();
const [activeKey, setActiveKey] = useState<string>('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 (
<GSDrawer
title={title}
open={open}
onClose={onClose}
destroyOnHidden={true}
closeIcon={false}
maskClosable={false}
keyboard={false}
styles={{
body: {
padding: '0 0 16px',
overflowX: 'hidden'
},
content: {
borderRadius: '6px 0 0 6px'
}
}}
width={600}
footer={false}
>
<SegmentedHeader>
<SegmentedInner
shape="round"
onChange={(value) => setActiveKey(value as string)}
options={[
{
label: 'Form',
value: 'form',
icon: <IconFont type="icon-edit-content" />
},
{
label: 'YAML',
value: 'yaml',
icon: <IconFont type="icon-code_block" />
}
]}
size="middle"
style={{ width: '100%' }}
/>
</SegmentedHeader>
<ColumnWrapper
style={{ flex: 1 }}
maxHeight={'calc(100vh - 123px)'}
footer={
<ModalFooter
onCancel={onClose}
onOk={onOk}
style={ModalFooterStyle}
></ModalFooter>
}
>
<Tabs
renderTabBar={() => <></>}
activeKey={activeKey}
defaultActiveKey={activeKey}
items={[
{
key: 'form',
label: 'Form',
children: (
<BackendForm
onFinish={onFinish}
action={action}
currentData={currentData}
ref={formRef}
/>
)
},
{
key: 'yaml',
label: 'YAML',
children: (
<ImportYAML action={action} ref={editorRef}></ImportYAML>
)
}
]}
></Tabs>
</ColumnWrapper>
</GSDrawer>
);
};
export default AddModal;
+23 -7
View File
@@ -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<BackendCardProps> = ({
onClick={() => onClick(data)}
clickable={true}
disabled={false}
height={205}
height={'auto'}
ghost
header={
<Header>
<div className="title">
<img src={data.icon} alt={data.backend_name} height={20} />
<span>{data.backend_name}</span>
</div>
<span>
<DropDownActions
@@ -72,11 +83,16 @@ const BackendCard: React.FC<BackendCardProps> = ({
}
>
<Content>
<div className="title">{data.backend_name}</div>
<div>This is a description</div>
<div>{data.default_version}</div>
<div className="description">This is a description</div>
<div>
{data.version_configs?.[data.default_version]?.image_name}
<IconFont type="icon-version" style={{ marginRight: 4 }} /> Default
version: <span className="text">{data.default_version}</span>
</div>
<div>
<DockerOutlined style={{ marginRight: 4 }} /> Default Image:{' '}
<span className="text">
{data.version_configs?.[data.default_version]?.image_name}
</span>
</div>
</Content>
</Card>
@@ -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<ImportYAMLProps> = forwardRef(
({ action, content = '' }, ref) => {
const intl = useIntl();
const { userSettings } = useUserSettings();
const editorRef = useRef<any>(null);
const [fileContent, setFileContent] = useState<string>(
action === PageAction.CREATE ? yamlTemplate : content
);
const [error, setError] = useState<string>('');
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 (
<Header>
<span className="title">YAML Editor</span>
<Upload
name="file"
multiple={false}
beforeUpload={beforeUpload}
showUploadList={false}
accept=".yaml,.yml,text/yaml,application/x-yaml"
>
<Button icon={<ImportOutlined />} type="text" size="small">
Import
</Button>
</Upload>
</Header>
);
};
useImperativeHandle(ref, () => ({
getContent: () => {
return editorRef.current?.getValue();
},
setContent: (val: string) => {
editorRef.current?.setValue?.(val);
},
validate() {
return validate();
}
}));
return (
<Container>
<YamlEditor
ref={editorRef}
lang="yaml"
theme={userSettings?.isDarkTheme ? 'vs-dark' : 'vs-light'}
value={fileContent}
height={'calc(100vh - 255px)'}
header={renderHeader()}
></YamlEditor>
{error && <ErrorText type="danger">{error}</ErrorText>}
</Container>
);
}
);
export default ImportYAML;
@@ -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<ViewerProps> = forwardRef((props, ref) => {
const {
lang,
value,
config,
defaultLang,
height = 380,
theme = 'vs-dark',
header,
variant = 'borderless',
placeholder
} = props;
const editorRef = useRef<any>(null);
const monacoRef = useRef<any>(null);
const monacoYamlRef = useRef<any>(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 (
<EditorWrap header={header} variant={variant}>
<Editor
path={path}
defaultPath={path}
height={height}
theme={theme}
className="monaco-editor"
defaultLanguage={'yaml'}
language={'yaml'}
value={value}
options={{
minimap: { enabled: false },
quickSuggestions: true,
suggestOnTriggerCharacters: true,
fontSize: 14,
scrollbar: {
verticalScrollbarSize: 6,
horizontalScrollbarSize: 6
},
placeholder: placeholder
}}
loading={<LoadingOutlined style={{ fontSize: 24 }}></LoadingOutlined>}
beforeMount={handleBeforeMount}
onMount={handleEditorDidMount}
/>
</EditorWrap>
);
});
export default ViewerEditor;
+21 -3
View File
@@ -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
`;
+56
View File
@@ -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
}
+4 -4
View File
@@ -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'
}
];
+5 -4
View File
@@ -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;
}
+77
View File
@@ -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<AddModalProps> = ({ action }) => {
const form = Form.useFormInstance();
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
return (
<>
<Form.Item<FormData>
name="backend_name"
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'common.table.name' })}
required
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="compatibility_type"
rules={[
{
required: true,
message: getRuleMessage('select', 'template', false)
}
]}
>
<SealSelect
label={'Compatibility Template'}
required
options={[
{ label: 'Template 1', value: 'tensorflow' },
{ label: 'Template 2', value: 'torchserve' }
]}
></SealSelect>
</Form.Item>
<Form.Item<FormData>
name="allowed_proxy_uris"
rules={[{ required: false }]}
>
<ListInput
dataList={form.getFieldValue('allowed_proxy_uris') || []}
onChange={(data: string[]) => {
form.setFieldValue('allowed_proxy_uris', data);
}}
btnText={'Add Allowed Proxy URI'}
label={'Proxy URI'}
></ListInput>
</Form.Item>
<Form.Item<FormData> name="description" rules={[{ required: false }]}>
<SealInput.TextArea
label={intl.formatMessage({ id: 'common.table.description' })}
></SealInput.TextArea>
</Form.Item>
</>
);
};
export default BasicForm;
+73
View File
@@ -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<AddModalProps> = 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 (
<Form
name="basicForm"
form={form}
onFinish={onFinish}
preserve={false}
scrollToFirstError={true}
initialValues={currentData}
>
<BasicForm action={action}></BasicForm>
<CollapsePanel
items={[
{
key: 'versions',
label: 'Versions',
children: <VersionsForm action={action}></VersionsForm>
},
{
key: 'parameters',
label: 'Default Parameters',
children: <ParametersForm action={action}></ParametersForm>
}
]}
></CollapsePanel>
</Form>
);
}
);
export default BackendForm;
+32
View File
@@ -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<AddModalProps> = ({ action }) => {
const form = Form.useFormInstance();
return (
<>
<Form.Item<FormData>
name="default_backend_param"
rules={[{ required: false }]}
>
<ListInput
dataList={form.getFieldValue('default_backend_param') || []}
onChange={(data: string[]) => {
form.setFieldValue('default_backend_param', data);
}}
btnText={'Add Backend Parameter'}
label={'Default Backend Parameters'}
></ListInput>
</Form.Item>
</>
);
};
export default ParametersForm;
@@ -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<AddModalProps> = ({ 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 (
<Form.List name="version_configs">
{(fields, { add, remove }) => {
return (
<div
style={{
border: '1px solid var(--ant-color-border)',
borderRadius: 'var(--border-radius-base)',
padding: '14px'
}}
>
{fields.map(({ key, name, ...restField }, index) => (
<ItemWrapper key={key}>
<Box>
<Form.Item
{...restField}
name={[name, 'version_no']}
rules={[
{ required: true, message: 'Version No. is required' }
]}
>
<SealInput.Input
label="Version No."
required
></SealInput.Input>
</Form.Item>
<Form.Item
{...restField}
name={[name, 'image_name']}
rules={[
{ required: true, message: 'Image Name is required' }
]}
>
<SealInput.Input
label="Image Name"
required
></SealInput.Input>
</Form.Item>
</Box>
<Form.Item name={[name, 'run_command']} {...restField}>
<SealInput.TextArea label="Execution Command"></SealInput.TextArea>
</Form.Item>
<div className="flex justify-between items-center">
<span className="flex-center">
<Form.Item
{...restField}
name={[name, 'is_default']}
valuePropName="checked"
initialValue={false}
noStyle
>
<Checkbox>Set as Default Version</Checkbox>
</Form.Item>
</span>
<div className="flex-center gap-16">
{fields.length > 1 && (
<Button
size="small"
onClick={() => remove(name)}
shape="circle"
>
<MinusOutlined />
</Button>
)}
</div>
</div>
<Divider
className="divider"
style={{ borderTop: '1px dashed var(--ant-color-border)' }}
/>
{index === fields.length - 1 && (
<Button
onClick={() => add()}
block
variant="filled"
color="default"
>
<PlusOutlined /> Add Version
</Button>
)}
</ItemWrapper>
))}
</div>
);
}}
</Form.List>
);
};
export default VersionsForm;
+38 -38
View File
@@ -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<FormData>;
}>({ 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 (
<PageContainer
@@ -63,40 +77,14 @@ const BackendList = () => {
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={
<Space size={20}>
<Button type="primary" icon={<PlusOutlined />}>
Add Backend
</Button>
<Button type="primary" icon={<ImportOutlined />}>
Import backend from yaml
</Button>
<Button
icon={<DeleteOutlined />}
danger
onClick={handleDeleteByBatch}
disabled={!rowSelection?.selectedRowKeys?.length}
>
<span>
{intl?.formatMessage?.({ id: 'common.button.delete' })}
{rowSelection?.selectedRowKeys?.length > 0 && (
<span>({rowSelection?.selectedRowKeys?.length})</span>
)}
</span>
</Button>
</Space>
}
></FilterBar>
<BackendCardList
@@ -105,6 +93,18 @@ const BackendList = () => {
activeId={false}
isFirst={!dataSource.loadend}
></BackendCardList>
<AddModal
action={openModalStatus.action}
onClose={() => setOpenModalStatus({ open: false, action: 'create' })}
onSubmit={handleOnSubmit}
currentData={openModalStatus.currentData}
open={openModalStatus.open}
title={
openModalStatus.action === 'create'
? 'Create Backend'
: 'Edit Backend'
}
></AddModal>
</PageContainer>
);
};
@@ -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<any>(null);
const handleOnFinish = (values: any) => {
console.log(values);
};
useImperativeHandle(ref, () => ({
validateFields: formRef.current?.validateFields,
getFieldsValue: formRef.current?.getFieldsValue,
submit: formRef.current?.submit
}));
return (
<div>
<PageTools
marginBottom={16}
left={
<Title>
{intl.formatMessage({ id: 'clusters.create.configBasic' })}
</Title>
}
marginTop={0}
></PageTools>
<Basic
action={action}
ref={formRef}
onFinish={handleOnFinish}
currentData={currentData}
></Basic>
</div>
);
});
export default BasicForm;
@@ -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<string, React.ComponentType<any>> = {
[moduleMap.BasicForm]: BasicForm,
[moduleMap.VersionConfigForm]: VersionConfigForm,
[moduleMap.ParamertersForm]: ParamertersForm
};
@@ -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<any>(null);
const handleOnFinish = (values: any) => {
console.log(values);
};
useImperativeHandle(ref, () => ({
validateFields: formRef.current?.validateFields,
getFieldsValue: formRef.current?.getFieldsValue,
submit: formRef.current?.submit
}));
return (
<div>
<PageTools
marginBottom={16}
left={<Title>Parameters Configuration</Title>}
marginTop={0}
></PageTools>
<Parameters
action={action}
ref={formRef}
onFinish={handleOnFinish}
currentData={currentData}
></Parameters>
</div>
);
});
export default ParametersForm;
@@ -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]
);
}
@@ -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<Record<number, FormInstance<any> | null>>({});
const [activeKey, setActiveKey] = useState<Set<number>>(new Set([0]));
const [versionConfigs, setVersionConfigs] = useState<Map<number, FormData>>(
new Map([
[
0,
{
version_name: '',
image_name: '',
run_command: ''
}
]
]) as Map<number, FormData>
);
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<any>[]) => {
const resultList = results.map((result: PromiseSettledResult<any>) => {
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 (
<div>
<PageTools
marginBottom={22}
marginTop={0}
left={
<span className="flex-center gap-16">
<Title>Backend Versions</Title>
</span>
}
right={
<Button
onClick={handleAddPool}
variant="filled"
color="default"
icon={<PlusOutlined />}
>
Add Version
</Button>
}
></PageTools>
{Array.from(versionConfigs.keys()).map((key, index) => (
<PoolContainer key={key}>
<PoolFormWrapper>
<VersionsConfig
name={`versionForm_${key}`}
action={action}
ref={(el: any) => {
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)}
></VersionsConfig>
</PoolFormWrapper>
</PoolContainer>
))}
</div>
);
});
export default WorkerPoolsForm;
+6 -1
View File
@@ -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={
<Space>
<Input
prefix={
<SearchOutlined
style={{ color: 'var(--ant-color-text-placeholder)' }}
></SearchOutlined>
}
placeholder={intl.formatMessage({ id: 'common.filter.name' })}
style={{ width: 230 }}
size="large"
@@ -161,7 +161,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
form={form}
onFinish={handleOk}
preserve={false}
style={{ padding: '16px 24px' }}
clearOnDestroy={true}
onValuesChange={handleOnValuesChange}
scrollToFirstError={true}
@@ -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';
@@ -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';
+6 -1
View File
@@ -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<ModelsProps> = ({
left={
<Space>
<Input
prefix={
<SearchOutlined
style={{ color: 'var(--ant-color-text-placeholder)' }}
></SearchOutlined>
}
placeholder={intl.formatMessage({ id: 'common.filter.name' })}
style={{ width: 160 }}
size="large"
@@ -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';
+1 -1
View File
@@ -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';
@@ -87,7 +87,6 @@ const TargetForm: React.FC<TargetFormProps> = forwardRef((props, ref) => {
form={form}
onFinish={handleOk}
preserve={false}
style={{ padding: '16px 24px' }}
clearOnDestroy={true}
initialValues={{
source: source
@@ -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<ViewModalProps> = (props) => {
const { title, open, onCancel, viewCodeContent } = props || {};
const intl = useIntl();
const [lang, setLang] = useState<string>(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<string>(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 (
<>
<Modal
@@ -74,26 +73,13 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
{intl.formatMessage({ id: 'playground.viewcode.info' })}
</div>
<div>
<EditorWrap
<CommandViewer
code={codeValue}
copyText={codeValue}
langOptions={langOptions}
options={langOptions}
defaultValue={langMap.shell}
showHeader={true}
onChangeLang={handleOnChangeLang}
styles={{
wrapper: {
backgroundColor: 'var(--color-editor-dark)'
}
}}
>
<HighlightCode
height={380}
theme="dark"
code={codeValue}
lang={lang}
copyable={false}
></HighlightCode>
</EditorWrap>
onChange={handleOnChangeLang}
></CommandViewer>
<div
style={{ marginTop: 10, display: 'flex', alignItems: 'baseline' }}
>
@@ -128,4 +114,4 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
);
};
export default React.memo(ViewCodeModal);
export default ViewCodeModal;
@@ -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<ViewModalProps> = (props) => {
></div>
)}
{activeKey === 'npu' ? (
<EditorWrap
<CommandViewer
headerHeight={32}
copyText={code}
langOptions={npuOptions}
options={npuOptions}
defaultValue="npu"
showHeader={true}
onChangeLang={handleOnChange}
styles={{
wrapper: {
backgroundColor: 'var(--color-editor-dark)'
}
}}
>
<HighlightCode
theme="dark"
code={code.replace(/\\/g, '')}
copyValue={code}
lang="bash"
copyable={false}
></HighlightCode>
</EditorWrap>
onChange={handleOnChange}
lang="bash"
code={code.replace(/\\/g, '')}
></CommandViewer>
) : (
<HighlightCode
theme="dark"