fix: add worker, create cluster

This commit is contained in:
jialin
2025-12-17 14:59:07 +08:00
parent 5e48d75ead
commit bd6f4b3f76
14 changed files with 234 additions and 103 deletions
+1
View File
@@ -45,6 +45,7 @@ export function useUpdateChunkedList(options: {
}, 200);
};
const updateChunkedList = (data: ChunkedCollection) => {
console.log('updateChunkedList data:', data);
let collections = data?.collection || [];
if (options?.computedID) {
collections = collections?.map((item: any) => {
+10 -1
View File
@@ -6,7 +6,8 @@ import {
CredentialFormData,
CredentialListItem,
NodePoolFormData,
NodePoolListItem
NodePoolListItem,
SystemConfig
} from '../config/types';
export const CREDENTIALS_API = '/cloud-credentials';
@@ -19,6 +20,8 @@ export const CLUSTER_TOKEN = 'registration-token';
export const PROVIDER_PROXY_API = '/provider-proxy';
export const SYSTEM_CONFIG_API = '/config';
// ============= DigitalOcean start =====================
export const REGIONS_API = '/v2/regions';
@@ -189,3 +192,9 @@ export async function deleteWorkerPool(id: number) {
method: 'DELETE'
});
}
export async function querySystemConfig() {
return request<SystemConfig>(`${SYSTEM_CONFIG_API}`, {
method: 'GET'
});
}
@@ -272,7 +272,7 @@ const ClusterCreate = () => {
const breadcrumbItems = [
{
title: <a>{intl.formatMessage({ id: 'clusters.title' })}</a>,
onClick: () => navigate(-1)
onClick: () => handleCancel()
},
{
title: intl.formatMessage({ id: 'common.button.create' })
@@ -2,6 +2,7 @@ import AlertInfoBlock from '@/components/alert-info/block';
import useAddWorkerMessage from '@/pages/cluster-management/hooks/use-add-worker-message';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Alert } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { ProviderType, ProviderValueMap } from '../../config';
@@ -29,6 +30,7 @@ const Container = styled.div`
* clusterList and onClusterChange are only required when from worker page.
*/
type AddWorkerProps = {
isModal?: boolean;
provider: ProviderType;
clusterList?: Global.BaseOption<number, ClusterListItem>[];
clusterLoading?: boolean;
@@ -49,6 +51,7 @@ type AddWorkerProps = {
*/
const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
const {
isModal = false,
registrationInfo,
provider,
clusterList,
@@ -63,7 +66,7 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
);
const startWatchRef = React.useRef(false);
const { update, summary, register } = useSummaryStatus();
const { contextHolder, createModelsChunkRequest } = useAddWorkerMessage({
const { addedCount, createModelsChunkRequest } = useAddWorkerMessage({
startWatch: startWatchRef
});
@@ -84,8 +87,10 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
}, [stepList]);
React.useEffect(() => {
createModelsChunkRequest();
}, []);
if (!isModal) {
createModelsChunkRequest();
}
}, [isModal]);
return (
<AddWorkerContext.Provider
@@ -139,8 +144,18 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
)}
</>
)}
{addedCount > 0 && (
<Alert
message={intl.formatMessage(
{
id: 'clusters.addworker.message.success'
},
{ count: addedCount }
)}
type="warning"
/>
)}
</Container>
{contextHolder}
</AddWorkerContext.Provider>
);
};
@@ -1,11 +1,22 @@
import ScrollerModal from '@/components/scroller-modal';
import useAddWorkerMessage from '@/pages/cluster-management/hooks/use-add-worker-message';
import { useIntl } from '@umijs/max';
import { Alert, Button } from 'antd';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { queryClusterToken } from '../../apis';
import { ProviderType } from '../../config';
import { ClusterListItem } from '../../config/types';
import AddWorkerStep from './add-worker-step';
import { StepName } from './config';
const Footer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
`;
type AddWorkerProps = {
open: boolean;
provider: ProviderType;
@@ -40,6 +51,11 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
clusterLoading,
stepList = []
} = props || {};
const intl = useIntl();
const startWatchRef = React.useRef(false);
const { addedCount, createModelsChunkRequest } = useAddWorkerMessage({
startWatch: startWatchRef
});
const firstLoad = React.useRef(true);
const [registrationInfo, setRegistrationInfo] = React.useState<{
token: string;
@@ -61,6 +77,7 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
...data,
cluster_id: value
});
startWatchRef.current = true;
} catch (error) {
firstLoad.current = false;
}
@@ -75,6 +92,12 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
};
}, [open, cluster_id]);
useEffect(() => {
if (open) {
createModelsChunkRequest();
}
}, [open]);
return (
<ScrollerModal
title={title}
@@ -88,9 +111,35 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
width={865}
style={{}}
maxContentHeight={'max(calc(100vh - 200px), 600px)'}
footer={null}
footer={
<Footer>
<span>
{addedCount > 0 && (
<Alert
message={intl.formatMessage(
{
id: 'clusters.addworker.message.success'
},
{ count: addedCount }
)}
type="warning"
/>
)}
</span>
<Button
type="primary"
onClick={onCancel}
style={{
minWidth: 88
}}
>
{intl.formatMessage({ id: 'common.button.done' })}
</Button>
</Footer>
}
>
<AddWorkerStep
isModal={true}
stepList={stepList}
provider={provider}
clusterList={clusterList}
@@ -134,7 +134,7 @@ const SummaryData: React.FC = () => {
tips={
workerIPConfig.enable
? workerIPConfig.ip
? workerIPConfig.ip
? ''
: intl.formatMessage({ id: 'clusters.addworker.notSpecified' })
: intl.formatMessage({ id: 'clusters.addworker.autoDetect' })
}
@@ -1,4 +1,5 @@
import { fromClusterCreationAtom } from '@/atoms/clusters';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
@@ -11,6 +12,7 @@ import React, { useEffect } from 'react';
import styled from 'styled-components';
import { ClusterFormData as FormData } from '../config/types';
import { useProviderRegions } from '../hooks/use-provider-regions';
import useSystemConfig from '../services/use-system-config';
const OptionItem = styled.div`
display: flex;
@@ -74,6 +76,8 @@ const optionRender = (option: any): React.ReactNode => {
const CloudProvider: React.FC<CloudProviderProps> = (props) => {
const { credentialList, action, credentialID } = props;
const intl = useIntl();
const { systemConfig } = useSystemConfig();
const form = Form.useFormInstance<FormData>();
const {
getRegions,
@@ -105,6 +109,10 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
}
}, [credentialID]);
useEffect(() => {
form.setFieldValue('server_url', systemConfig?.server_external_url || '');
}, [systemConfig]);
const labelRender = (props: {
label: React.ReactNode;
value: string | number;
@@ -176,6 +184,21 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
})}
></SealSelect>
</Form.Item>
<Form.Item<FormData>
name="server_url"
rules={[
{
required: true,
message: getRuleMessage('input', 'clusters.create.serverUrl')
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'clusters.create.serverUrl' })}
required={true}
trim={true}
></SealInput.Input>
</Form.Item>
</>
);
};
@@ -40,9 +40,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
form.setFieldsValue(currentData);
if (advanceConfigRef.current) {
const workerConfigYaml = json2Yaml({
worker_config: currentData.worker_config || {}
});
const workerConfigYaml = json2Yaml(currentData.worker_config || {});
advanceConfigRef.current?.setYamlValue(workerConfigYaml);
}
}
@@ -69,7 +67,9 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
return {
...values,
...workerConfig
worker_config: {
...workerConfig
}
};
}
}));
@@ -129,6 +129,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
children: (
<AdvanceConfig
action={action}
provider={provider}
ref={advanceConfigRef}
></AdvanceConfig>
)
@@ -79,3 +79,9 @@ export interface ClusterFormData {
worker_config?: Record<string, any>;
worker_pools?: NodePoolFormData[];
}
export interface SystemConfig {
debug: boolean;
server_external_url: string;
system_default_container_registry: string;
}
@@ -36,7 +36,7 @@
"gateway_mode": {
"type": "string",
"description": "Gateway mode",
"enum": ["worker", "gateway", "disabled"]
"enum": ["worker", "gateway", "disabled", "auto"]
},
"gateway_kubeconfig": {
"type": "string",
@@ -88,9 +88,7 @@
"resources": {
"type": "object",
"description": "Additional resource definitions",
"additionalProperties": {
"type": "object"
}
"additionalProperties": {}
},
"pipx_path": {
"type": "string",
@@ -1,53 +1,54 @@
export default `# This is a template for worker_config.
worker_config:
debug: true
# debug: false
# directories
cache_dir:
log_dir:
bin_dir:
# ========= directories ===========
# container & image
system_default_container_registry:
image_repo: gpustack/worker
image_name_override: ""
# cache_dir: "/var/lib/gpustack/cache"
# log_dir: "/var/lib/gpustack/log"
# bin_dir: "/var/lib/gpustack/bin"
# gateway
gateway_mode:
gateway_concurrency: 0
gateway_kubeconfig: ""
# ========= container & image ===========
# service & networking
service_discovery_name:
namespace: default
worker_port:
worker_metrics_port:
service_port_range:
ray_port_range:
# system_default_container_registry: "docker.io"
# image_name_override: "gpustack/gpustack:main"
# image_repo: "gpustack/gpustack"
# resources
resources:
gpu:
count:
type:
cpu:
limit:
memory:
limit:
# ========= gateway ===========
# huggingface
huggingface_token: ""
enable_hf_transfer: true
enable_hf_xet: true
# gateway_mode: "auto"
# gateway_concurrency: 16
# gateway_kubeconfig: "/var/lib/gpustack/higress/kubeconfig"
# metrics
disable_worker_metrics: false
# ========= service & networking ===========
# tools & runtime
pipx_path:
tools_download_base_url:
# service_discovery_name: "worker"
# namespace: "gpustack-system"
# worker_port: 10150
# worker_metrics_port: 10150
# service_port_range: "40000-40063"
# ray_port_range: "41000-41999"
# proxy
proxy_mode: worker
# ========= resources ===========
# resources:
# ========= huggingface ===========
# huggingface_token:
# enable_hf_transfer: false
# enable_hf_xet: false
# ========= metrics ===========
# disable_worker_metrics: false
# ========= tools & runtime ===========
# pipx_path: "/usr/local/bin/pipx"
# tools_download_base_url:
# ========= proxy ===========
# proxy_mode: worker
`;
@@ -25,6 +25,7 @@ export default function useAddWorkerMessage(params: {
onCreate: (newItems: any) => {
if (params.startWatch?.current) {
newItemsRef.current = newItemsRef.current.concat(newItems);
console.log('newItemsRef.current:', newItemsRef.current);
showAddWorkerMessage();
}
}
@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react';
import { querySystemConfig } from '../apis';
import { SystemConfig } from '../config/types';
export default function useSystemConfig() {
const [systemConfig, setSystemConfig] = useState<SystemConfig>(
{} as SystemConfig
);
const fetchSystemConfig = async () => {
try {
const data = await querySystemConfig();
setSystemConfig(data);
} catch (error) {
// handle error if needed
setSystemConfig({} as SystemConfig);
}
};
useEffect(() => {
fetchSystemConfig();
}, []);
return { systemConfig };
}
@@ -4,29 +4,34 @@ import YamlEditor from '@/pages/_components/yaml-editor';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react';
import { ProviderType, ProviderValueMap } from '../config';
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<any>(null);
const [fileContent, setFileContent] = React.useState<string>('');
const ClusterAdvanceConfig: React.FC<{
action: PageActionType;
provider: ProviderType;
ref?: any;
}> = forwardRef(({ action, provider }, ref) => {
const [form] = Form.useForm();
const intl = useIntl();
const editorRef = React.useRef<any>(null);
const [fileContent, setFileContent] = React.useState<string>(yamlTemplate);
useImperativeHandle(ref, () => ({
getYamlValue: () => {
return editorRef.current?.getValue();
},
setYamlValue: (values: any) => {
console.log('setYamlValue:', values, editorRef.current);
editorRef.current?.setValue(values);
}
}));
useImperativeHandle(ref, () => ({
getYamlValue: () => {
return editorRef.current?.getValue();
},
setYamlValue: (values: any) => {
console.log('setYamlValue:', values, editorRef.current);
editorRef.current?.setValue(values);
}
}));
return (
<>
return (
<>
{provider !== ProviderValueMap.DigitalOcean && (
<Form.Item<FormData>
name="server_url"
rules={[
@@ -42,34 +47,31 @@ const ClusterAdvanceConfig: React.FC<{ action: PageActionType; ref?: any }> =
trim={true}
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
hidden
name="worker_config"
rules={[
{
required: false,
message: ''
}
]}
>
<SealInput.TextArea
required={false}
trim={false}
></SealInput.TextArea>
</Form.Item>
<YamlEditor
ref={editorRef}
title={`${intl.formatMessage({ id: 'clusters.create.workerConfig' })} YAML`}
value={fileContent}
height={300}
onUpload={(content) => {
setFileContent(content);
}}
schema={schema}
placeholder={yamlTemplate}
></YamlEditor>
</>
);
});
)}
<Form.Item<FormData>
hidden
name="worker_config"
rules={[
{
required: false,
message: ''
}
]}
>
<SealInput.TextArea required={false} trim={false}></SealInput.TextArea>
</Form.Item>
<YamlEditor
ref={editorRef}
title={`${intl.formatMessage({ id: 'clusters.create.workerConfig' })} YAML`}
value={fileContent}
height={300}
onUpload={(content) => {
setFileContent(content);
}}
schema={schema}
></YamlEditor>
</>
);
});
export default ClusterAdvanceConfig;