From 6968f9fa6b676b2d7c7a18e7f4d90f6c4a2a5852 Mon Sep 17 00:00:00 2001 From: jialin Date: Tue, 16 Dec 2025 18:41:55 +0800 Subject: [PATCH] feat: worker_config --- src/locales/en-US/clusters.ts | 4 +- src/locales/ja-JP/clusters.ts | 8 +- src/locales/ru-RU/clusters.ts | 8 +- src/locales/zh-CN/clusters.ts | 4 +- .../yaml-editor/editor.tsx} | 24 +-- src/pages/_components/yaml-editor/index.tsx | 172 ++++++++++++++++++ src/pages/backends/components/import-yaml.tsx | 115 +++--------- .../components/cluster-form.tsx | 48 ++++- src/pages/cluster-management/config/types.ts | 3 + .../config/worker-config.json | 119 ++++++++++++ .../config/yaml-template.ts | 53 ++++++ .../step-forms/advance-config.tsx | 75 ++++++++ 12 files changed, 512 insertions(+), 121 deletions(-) rename src/pages/{backends/components/yaml-editor.tsx => _components/yaml-editor/editor.tsx} (81%) create mode 100644 src/pages/_components/yaml-editor/index.tsx create mode 100644 src/pages/cluster-management/config/worker-config.json create mode 100644 src/pages/cluster-management/config/yaml-template.ts create mode 100644 src/pages/cluster-management/step-forms/advance-config.tsx diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index 1f43158f..d05dd0eb 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -90,5 +90,7 @@ Same applies to the /opt/dtk directory.`, 'Need to create a new token? Click here.', 'clusters.addworker.amdNotes-01': `If the /opt/rocm directory does not exist, please create a symbolic link pointing to the ROCm installed path: ln -s /path/to/rocm /opt/rocm.`, 'clusters.addworker.message.success': - '{count} workers have been added to the cluster.' + '{count} workers have been added to the cluster.', + 'clusters.create.serverUrl': 'Server URL', + 'clusters.create.workerConfig': 'Worker Configuration' }; diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index 0104b161..48879d60 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -90,7 +90,9 @@ Same applies to the /opt/dtk directory.`, 'Need to create a new token? Click here.', 'clusters.addworker.amdNotes-01': `If the /opt/rocm directory does not exist, please create a symbolic link pointing to the ROCm installed path: ln -s /path/to/rocm /opt/rocm.`, 'clusters.addworker.message.success': - '{count} workers have been added to the cluster.' + '{count} workers have been added to the cluster.', + 'clusters.create.serverUrl': 'Server URL', + 'clusters.create.workerConfig': 'Worker Configuration' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -167,5 +169,7 @@ Same applies to the /opt/dtk directory.`, // 71. 'clusters.addworker.cacheVolume': 'Model Cache Volume Mount', // 72. 'clusters.addworker.cacheVolume.tips': 'If you want to customize the model cache directory, you can specify the path to mount it.', // 73. 'clusters.addworker.cacheVolume.holder': 'e.g. /data/cache (path must start with /)', -// 74. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.' +// 74. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.', +// 75. 'clusters.create.serverUrl': 'Server URL', +// 76. 'clusters.create.workerConfig': 'Worker Configuration' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index 46ff3f38..67fe2153 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -90,7 +90,9 @@ export default { 'Need to create a new token? Click here.', 'clusters.addworker.amdNotes-01': `If the /opt/rocm directory does not exist, please create a symbolic link pointing to the ROCm installed path: ln -s /path/to/rocm /opt/rocm.`, 'clusters.addworker.message.success': - '{count} workers have been added to the cluster.' + '{count} workers have been added to the cluster.', + 'clusters.create.serverUrl': 'Server URL', + 'clusters.create.workerConfig': 'Worker Configuration' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -98,5 +100,7 @@ export default { // 2. 'clusters.button.genToken': 'Need to create a new token? Click here.', // 3. 'clusters.addworker.cacheVolume': 'Model Cache Volume Mount', // 4. 'clusters.addworker.cacheVolume.tips': 'If you want to customize the model cache directory, you can specify the path to mount it.', -// 5. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.' +// 5. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.', +// 6. 'clusters.create.serverUrl': 'Server URL', +// 7. 'clusters.create.workerConfig': 'Worker Configuration' // ================================================================ diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index 5f6d220c..d6fd0c3d 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -88,5 +88,7 @@ export default { '需要创建令牌?点击这里。', 'clusters.addworker.amdNotes-01': '如果 /opt/rocm 目录不存在,请创建一个指向已安装 ROCm 路径的符号链接:ln -s /path/to/rocm /opt/rocm。', - 'clusters.addworker.message.success': '已将 {count} 个工作节点添加到集群中。' + 'clusters.addworker.message.success': '已将 {count} 个工作节点添加到集群中。', + 'clusters.create.serverUrl': '服务器地址', + 'clusters.create.workerConfig': '节点配置' }; diff --git a/src/pages/backends/components/yaml-editor.tsx b/src/pages/_components/yaml-editor/editor.tsx similarity index 81% rename from src/pages/backends/components/yaml-editor.tsx rename to src/pages/_components/yaml-editor/editor.tsx index 3bdcb365..ec9aa68d 100644 --- a/src/pages/backends/components/yaml-editor.tsx +++ b/src/pages/_components/yaml-editor/editor.tsx @@ -1,6 +1,4 @@ import EditorWrap from '@/components/editor-wrap'; -import { PageAction } from '@/config'; -import { PageActionType } from '@/config/types'; import { LoadingOutlined } from '@ant-design/icons'; import Editor, { loader } from '@monaco-editor/react'; import * as monaco from 'monaco-editor'; @@ -11,15 +9,11 @@ import React, { useImperativeHandle, useRef } from 'react'; -import createSchema from '../config/schema/create.json'; -import updateBuiltinSchema from '../config/schema/update-builtin.json'; -import udpateCustomSchema from '../config/schema/update-custom.json'; loader.config({ monaco }); interface ViewerProps { ref?: any; - lang: string; defaultLang?: string; config?: any; value: string; @@ -28,22 +22,19 @@ interface ViewerProps { header?: React.ReactNode; placeholder?: string; variant?: 'bordered' | 'borderless'; - actionStatus: { - action: PageActionType; - isBuiltIn: boolean; - }; + schema?: any; } const path = 'inmemory://model/config.yaml'; -const ViewerEditor: React.FC = forwardRef((props, ref) => { +const EditorInner: React.FC = forwardRef((props, ref) => { const { value, height = 380, theme = 'vs-dark', header, variant = 'borderless', - actionStatus, + schema, placeholder } = props; @@ -59,12 +50,7 @@ const ViewerEditor: React.FC = forwardRef((props, ref) => { { uri: 'http://example.com/schema-name.json', fileMatch: [yamlUri.toString()], - schema: - actionStatus.action === PageAction.CREATE - ? createSchema - : actionStatus.isBuiltIn - ? updateBuiltinSchema - : udpateCustomSchema + schema: schema } ] }); @@ -151,4 +137,4 @@ const ViewerEditor: React.FC = forwardRef((props, ref) => { ); }); -export default ViewerEditor; +export default EditorInner; diff --git a/src/pages/_components/yaml-editor/index.tsx b/src/pages/_components/yaml-editor/index.tsx new file mode 100644 index 00000000..24ff2aba --- /dev/null +++ b/src/pages/_components/yaml-editor/index.tsx @@ -0,0 +1,172 @@ +import useUserSettings from '@/hooks/use-user-settings'; +import { ImportOutlined } from '@ant-design/icons'; +import { loader } from '@monaco-editor/react'; +import { useIntl } from '@umijs/max'; +import { Button, message, Typography, Upload } from 'antd'; +import { RcFile } from 'antd/lib/upload'; +import * as monaco from 'monaco-editor'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef +} from 'react'; +import styled from 'styled-components'; +import EditorInner from './editor'; + +const { Text } = Typography; + +loader.config({ monaco }); + +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; + } +`; + +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; + font-size: 14px; + border-bottom: 1px solid var(--ant-color-border); + background-color: var(--ant-color-fill-tertiary); +`; + +interface ViewerProps { + ref?: any; + title?: React.ReactNode; + defaultLang?: string; + config?: any; + value: string; + height?: string | number; + placeholder?: string; + variant?: 'bordered' | 'borderless'; + validateMessage?: React.ReactNode; + schema?: any; + onUpload?: (content: string) => void; +} + +const YamlEditor: React.FC = forwardRef((props, ref) => { + const { + value, + height = 380, + variant = 'borderless', + schema, + placeholder, + validateMessage, + title, + onUpload + } = props; + + const intl = useIntl(); + const { userSettings } = useUserSettings(); + + const editorRef = useRef(null); + + const setContent = (val: string) => { + editorRef.current?.setValue?.(val); + }; + + 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') { + onUpload?.(content); + setContent(content); + } else { + message.error('Failed to read file content!'); + } + }; + reader.readAsText(file); + // Prevent upload + return false; + }; + + const renderHeader = () => { + return ( +
+ {title || 'YAML'} + + + +
+ ); + }; + + useImperativeHandle(ref, () => ({ + format: () => { + editorRef.current?.format(); + }, + getValue: () => { + return editorRef.current?.getValue?.(); + }, + setValue: (val: string) => { + editorRef.current?.setValue?.(val); + }, + dispose: () => { + editorRef.current?.dispose?.(); + }, + validate() { + return editorRef.current?.validate(); + }, + editor: editorRef.current + })); + + useEffect(() => { + editorRef.current?.format(); + }, [value]); + + return ( + + + {validateMessage && ( + {validateMessage} + )} + + ); +}); + +export default YamlEditor; diff --git a/src/pages/backends/components/import-yaml.tsx b/src/pages/backends/components/import-yaml.tsx index 7e2fd88e..a3cf2276 100644 --- a/src/pages/backends/components/import-yaml.tsx +++ b/src/pages/backends/components/import-yaml.tsx @@ -1,51 +1,17 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; -import useUserSettings from '@/hooks/use-user-settings'; -import { ImportOutlined } from '@ant-design/icons'; +import YamlEditor from '@/pages/_components/yaml-editor'; 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 { yaml2Json, 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; - } -`; +import createSchema from '../config/schema/create.json'; +import updateBuiltinSchema from '../config/schema/update-builtin.json'; +import udpateCustomSchema from '../config/schema/update-custom.json'; interface ImportYAMLProps { ref?: any; @@ -60,7 +26,6 @@ interface ImportYAMLProps { const ImportYAML: React.FC = forwardRef( ({ actionStatus, content = '' }, ref) => { const intl = useIntl(); - const { userSettings } = useUserSettings(); const editorRef = useRef(null); const [fileContent, setFileContent] = useState( actionStatus.action === PageAction.CREATE ? yamlTemplate : content @@ -71,30 +36,6 @@ const ImportYAML: React.FC = forwardRef( editorRef.current?.setValue?.(val); }; - 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); - setContent(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) { @@ -152,25 +93,6 @@ const ImportYAML: React.FC = forwardRef( } }; - const renderHeader = () => { - return ( -
- YAML - - - -
- ); - }; - useImperativeHandle(ref, () => ({ getContent: () => { return getContent(); @@ -182,18 +104,23 @@ const ImportYAML: React.FC = forwardRef( })); return ( - - - {error && {error}} - + { + setError(''); + setContent(content); + }} + schema={ + actionStatus.action === PageAction.CREATE + ? createSchema + : actionStatus.isBuiltIn + ? updateBuiltinSchema + : udpateCustomSchema + } + > ); } ); diff --git a/src/pages/cluster-management/components/cluster-form.tsx b/src/pages/cluster-management/components/cluster-form.tsx index 3e0499b3..22567586 100644 --- a/src/pages/cluster-management/components/cluster-form.tsx +++ b/src/pages/cluster-management/components/cluster-form.tsx @@ -1,6 +1,8 @@ import SealInput from '@/components/seal-form/seal-input'; import SealTextArea from '@/components/seal-form/seal-textarea'; import { PageActionType } from '@/config/types'; +import CollapsePanel from '@/pages/_components/collapse-panel'; +import { json2Yaml, yaml2Json } from '@/pages/backends/config'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import React, { forwardRef, useEffect, useImperativeHandle } from 'react'; @@ -9,6 +11,7 @@ import { ClusterFormData as FormData, ClusterListItem as ListItem } from '../config/types'; +import AdvanceConfig from '../step-forms/advance-config'; import CloudProvider from './cloud-provider-form'; type AddModalProps = { @@ -23,12 +26,27 @@ const ClusterForm: React.FC = forwardRef( ({ action, provider, currentData, credentialList, onFinish }, ref) => { const [form] = Form.useForm(); const intl = useIntl(); + const [activeKey, setActiveKey] = React.useState([]); + const advanceConfigRef = React.useRef(null); + + const handleOnCollapseChange = (keys: string | string[]) => { + setActiveKey(Array.isArray(keys) ? keys : [keys]); + if (currentData && advanceConfigRef.current) { + } + }; useEffect(() => { if (currentData) { form.setFieldsValue(currentData); + + if (advanceConfigRef.current) { + const workerConfigYaml = json2Yaml({ + worker_config: currentData.worker_config || {} + }); + advanceConfigRef.current?.setYamlValue(workerConfigYaml); + } } - }, [currentData]); + }, [currentData, advanceConfigRef.current]); useImperativeHandle(ref, () => ({ resetFields: () => { @@ -44,7 +62,15 @@ const ClusterForm: React.FC = forwardRef( return form.getFieldsValue(); }, validateFields: async () => { - return await form.validateFields(); + const values = await form.validateFields(); + const workerConfig = yaml2Json( + advanceConfigRef.current?.getYamlValue() + ); + + return { + ...values, + ...workerConfig + }; } })); @@ -91,6 +117,24 @@ const ClusterForm: React.FC = forwardRef( label={intl.formatMessage({ id: 'common.table.description' })} > + + ) + } + ]} + > ); } diff --git a/src/pages/cluster-management/config/types.ts b/src/pages/cluster-management/config/types.ts index c8a5ba70..7ca3c971 100644 --- a/src/pages/cluster-management/config/types.ts +++ b/src/pages/cluster-management/config/types.ts @@ -53,6 +53,7 @@ export interface ClusterListItem { name: string; display_name: string; description: string; + worker_config: Record; provider: ProviderType; credential_id: number; zone: string; @@ -74,5 +75,7 @@ export interface ClusterFormData { credential_id: number; zone: string; region: string; + server_url?: string; + worker_config?: Record; worker_pools?: NodePoolFormData[]; } diff --git a/src/pages/cluster-management/config/worker-config.json b/src/pages/cluster-management/config/worker-config.json new file mode 100644 index 00000000..8f81f734 --- /dev/null +++ b/src/pages/cluster-management/config/worker-config.json @@ -0,0 +1,119 @@ +{ + "type": "object", + "properties": { + "worker_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "debug": { + "type": "boolean", + "description": "Enable debug mode" + }, + "cache_dir": { + "type": "string", + "description": "Cache directory path" + }, + "log_dir": { + "type": "string", + "description": "Log directory path" + }, + "bin_dir": { + "type": "string", + "description": "Binary directory path" + }, + "system_default_container_registry": { + "type": "string", + "description": "Default container registry" + }, + "image_name_override": { + "type": "string", + "description": "Override image name" + }, + "image_repo": { + "type": "string", + "description": "Container image repository" + }, + "gateway_mode": { + "type": "string", + "description": "Gateway mode", + "enum": ["worker", "gateway", "disabled"] + }, + "gateway_kubeconfig": { + "type": "string", + "description": "Path to kubeconfig used by gateway" + }, + "gateway_concurrency": { + "type": "integer", + "minimum": 0, + "description": "Gateway concurrency limit" + }, + "service_discovery_name": { + "type": "string", + "description": "Service discovery name" + }, + "namespace": { + "type": "string", + "description": "Kubernetes namespace" + }, + "huggingface_token": { + "type": "string", + "description": "Hugging Face access token" + }, + "disable_worker_metrics": { + "type": "boolean", + "description": "Disable worker metrics collection" + }, + "worker_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Worker service port" + }, + "worker_metrics_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Worker metrics port" + }, + "service_port_range": { + "type": "string", + "pattern": "^\\d+-\\d+$", + "description": "Service port range, e.g. 30000-31000" + }, + "ray_port_range": { + "type": "string", + "pattern": "^\\d+-\\d+$", + "description": "Ray port range, e.g. 20000-20100" + }, + "resources": { + "type": "object", + "description": "Additional resource definitions", + "additionalProperties": { + "type": "object" + } + }, + "pipx_path": { + "type": "string", + "description": "pipx installation path" + }, + "tools_download_base_url": { + "type": "string", + "description": "Base URL for downloading tools" + }, + "enable_hf_transfer": { + "type": "boolean", + "description": "Enable Hugging Face transfer acceleration" + }, + "enable_hf_xet": { + "type": "boolean", + "description": "Enable Hugging Face Xet support" + }, + "proxy_mode": { + "type": "string", + "enum": ["worker", "system", "disabled"], + "description": "Proxy mode" + } + } + } + } +} diff --git a/src/pages/cluster-management/config/yaml-template.ts b/src/pages/cluster-management/config/yaml-template.ts new file mode 100644 index 00000000..766c6f03 --- /dev/null +++ b/src/pages/cluster-management/config/yaml-template.ts @@ -0,0 +1,53 @@ +export default `# This is a template for worker_config. + +worker_config: + debug: true + + # directories + cache_dir: + log_dir: + bin_dir: + + # container & image + system_default_container_registry: + image_repo: gpustack/worker + image_name_override: "" + + # gateway + gateway_mode: + gateway_concurrency: 0 + gateway_kubeconfig: "" + + # service & networking + service_discovery_name: + namespace: default + worker_port: + worker_metrics_port: + service_port_range: + ray_port_range: + + # resources + resources: + gpu: + count: + type: + cpu: + limit: + memory: + limit: + + # huggingface + huggingface_token: "" + enable_hf_transfer: true + enable_hf_xet: true + + # metrics + disable_worker_metrics: false + + # tools & runtime + pipx_path: + tools_download_base_url: + + # proxy + proxy_mode: worker +`; diff --git a/src/pages/cluster-management/step-forms/advance-config.tsx b/src/pages/cluster-management/step-forms/advance-config.tsx new file mode 100644 index 00000000..b7144fbb --- /dev/null +++ b/src/pages/cluster-management/step-forms/advance-config.tsx @@ -0,0 +1,75 @@ +import SealInput from '@/components/seal-form/seal-input'; +import { PageActionType } from '@/config/types'; +import YamlEditor from '@/pages/_components/yaml-editor'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import React, { forwardRef, useImperativeHandle } from 'react'; +import { ClusterFormData as FormData } from '../config/types'; +import schema from '../config/worker-config.json'; +import yamlTemplate from '../config/yaml-template'; + +const ClusterAdvanceConfig: React.FC<{ action: PageActionType; ref?: any }> = + forwardRef(({ action }, ref) => { + const [form] = Form.useForm(); + const intl = useIntl(); + const editorRef = React.useRef(null); + const [fileContent, setFileContent] = React.useState(''); + + useImperativeHandle(ref, () => ({ + getYamlValue: () => { + return editorRef.current?.getValue(); + }, + setYamlValue: (values: any) => { + console.log('setYamlValue:', values, editorRef.current); + editorRef.current?.setValue(values); + } + })); + + return ( + <> + + name="server_url" + rules={[ + { + required: false, + message: '' + } + ]} + > + + + + hidden + name="worker_config" + rules={[ + { + required: false, + message: '' + } + ]} + > + + + { + setFileContent(content); + }} + schema={schema} + placeholder={yamlTemplate} + > + + ); + }); + +export default ClusterAdvanceConfig;