fix: cluster worker config issues

This commit is contained in:
jialin
2025-12-18 17:29:07 +08:00
parent e83bff6923
commit 78368e9979
21 changed files with 112 additions and 43 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
const createChunkConnection = async () => { const createChunkConnection = async () => {
chunkRequedtRef.current?.current?.cancel?.(); chunkRequedtRef.current?.current?.cancel?.();
chunkRequedtRef.current = await setChunkRequest({ chunkRequedtRef.current = setChunkRequest({
url, url,
params: { params: {
...props.params, ...props.params,
+14 -11
View File
@@ -3,6 +3,7 @@ import useOverlayScroller from '@/hooks/use-overlay-scroller';
import { Modal, type ModalProps } from 'antd'; import { Modal, type ModalProps } from 'antd';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ScrollerContext } from './use-scroller-context';
const Wrapper = styled.div<{ $maxHeight?: number | string }>` const Wrapper = styled.div<{ $maxHeight?: number | string }>`
max-height: ${({ $maxHeight }) => max-height: ${({ $maxHeight }) =>
@@ -16,7 +17,7 @@ const ScrollerModal = (
) => { ) => {
const scroller = React.useRef<any>(null); const scroller = React.useRef<any>(null);
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const { initialize, destroyInstance } = useOverlayScroller(); const { initialize, destroyInstance, scrollToBottom } = useOverlayScroller();
React.useEffect(() => { React.useEffect(() => {
if (props.open) { if (props.open) {
@@ -68,16 +69,18 @@ const ScrollerModal = (
: {} : {}
}} }}
> >
<Wrapper <ScrollerContext.Provider value={{ scrollToBottom }}>
ref={scroller} <Wrapper
data-overlayscrollbars-initialize ref={scroller}
className="overlay-scroller-wrapper" data-overlayscrollbars-initialize
$maxHeight={props.maxContentHeight || 500} className="overlay-scroller-wrapper"
hidden={false} $maxHeight={props.maxContentHeight || 500}
style={{ paddingInline: 24, paddingBlockEnd: 0 }} hidden={false}
> style={{ paddingInline: 24, paddingBlockEnd: 0 }}
{props.children} >
</Wrapper> {props.children}
</Wrapper>
</ScrollerContext.Provider>
</Modal> </Modal>
); );
}; };
@@ -0,0 +1,11 @@
import { createContext, useContext } from 'react';
interface ScrollerContextProps {
scrollToBottom: () => void;
}
export const ScrollerContext = createContext<ScrollerContextProps>({
scrollToBottom: () => {}
});
export const useScrollerContext = () => useContext(ScrollerContext);
+2 -2
View File
@@ -166,11 +166,11 @@ const useSetChunkRequest = () => {
return axiosToken.current; return axiosToken.current;
}; };
const setChunkRequest = async (config: RequestConfig) => { const setChunkRequest = (config: RequestConfig) => {
requestConfig.current = { ...particalConfig, ...config }; requestConfig.current = { ...particalConfig, ...config };
retryCount.current = totalCount; retryCount.current = totalCount;
clearTimeout(timer.current); clearTimeout(timer.current);
await axiosChunkRequest(requestConfig.current); axiosChunkRequest(requestConfig.current);
return axiosToken; return axiosToken;
}; };
+1 -1
View File
@@ -117,7 +117,7 @@ export default function useTableFetch<T>(
try { try {
const query = _.omit(params || queryParams, ['page', 'perPage']); const query = _.omit(params || queryParams, ['page', 'perPage']);
chunkRequedtRef.current = await setChunkRequest({ chunkRequedtRef.current = setChunkRequest({
url: `${API}?${qs.stringify(_.pickBy(query, (val: any) => !!val))}`, url: `${API}?${qs.stringify(_.pickBy(query, (val: any) => !!val))}`,
handler: updateHandler handler: updateHandler
}); });
+1 -1
View File
@@ -30,7 +30,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
const createWatchChunkRequest = useMemoizedFn(async () => { const createWatchChunkRequest = useMemoizedFn(async () => {
chunkRequestRef.current?.current?.cancel?.(); chunkRequestRef.current?.current?.cancel?.();
try { try {
chunkRequestRef.current = await setChunkRequest({ chunkRequestRef.current = setChunkRequest({
url: `${watchAPI}`, url: `${watchAPI}`,
params: {}, params: {},
handler: updateWatchDataListHandler handler: updateWatchDataListHandler
+3 -1
View File
@@ -100,5 +100,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.dataVolume.tips': 'clusters.addworker.dataVolume.tips':
'Specify a data storage path for GPUStack.', 'Specify a data storage path for GPUStack.',
'clusters.table.ip.internal': 'Internal', 'clusters.table.ip.internal': 'Internal',
'clusters.table.ip.external': 'External' 'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips':
'Specify the server URL accessible from your cloud provider.'
}; };
+5 -2
View File
@@ -100,7 +100,9 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.dataVolume.tips': 'clusters.addworker.dataVolume.tips':
'Specify a data storage path for GPUStack.', 'Specify a data storage path for GPUStack.',
'clusters.table.ip.internal': 'Internal', 'clusters.table.ip.internal': 'Internal',
'clusters.table.ip.external': 'External' 'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips':
'Specify the server URL accessible from your cloud provider.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -185,5 +187,6 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
// 77. 'clusters.addworker.dataVolume': 'GPUStack Data Volume', // 77. 'clusters.addworker.dataVolume': 'GPUStack Data Volume',
// 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.', // 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.',
// 79. 'clusters.table.ip.internal': 'Internal', // 79. 'clusters.table.ip.internal': 'Internal',
// 80. 'clusters.table.ip.external': 'External' // 80. 'clusters.table.ip.external': 'External',
// 81. 'clusters.form.serverUrl.tips': 'Specify the server URL accessible from your cloud provider.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+5 -2
View File
@@ -100,7 +100,9 @@ export default {
'clusters.addworker.dataVolume.tips': 'clusters.addworker.dataVolume.tips':
'Specify a data storage path for GPUStack.', 'Specify a data storage path for GPUStack.',
'clusters.table.ip.internal': 'Internal', 'clusters.table.ip.internal': 'Internal',
'clusters.table.ip.external': 'External' 'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips':
'Specify the server URL accessible from your cloud provider.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -116,5 +118,6 @@ export default {
// 8. 'clusters.addworker.dataVolume': 'GPUStack Data Volume', // 8. 'clusters.addworker.dataVolume': 'GPUStack Data Volume',
// 9. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.', // 9. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.',
// 10. 'clusters.table.ip.internal': 'Internal', // 10. 'clusters.table.ip.internal': 'Internal',
// 11. 'clusters.table.ip.external': 'External' // 11. 'clusters.table.ip.external': 'External',
// 12. 'clusters.form.serverUrl.tips': 'Specify the server URL accessible from your cloud provider.'
// ================================================================ // ================================================================
+2 -1
View File
@@ -96,5 +96,6 @@ export default {
'clusters.addworker.dataVolume': 'GPUStack 数据卷', 'clusters.addworker.dataVolume': 'GPUStack 数据卷',
'clusters.addworker.dataVolume.tips': '为 GPUStack 指定数据存储路径。', 'clusters.addworker.dataVolume.tips': '为 GPUStack 指定数据存储路径。',
'clusters.table.ip.internal': '内', 'clusters.table.ip.internal': '内',
'clusters.table.ip.external': '外' 'clusters.table.ip.external': '外',
'clusters.form.serverUrl.tips': '指定可从您的云服务提供商访问的服务器地址。'
}; };
+4 -2
View File
@@ -18,7 +18,9 @@ const { Text } = Typography;
loader.config({ monaco }); loader.config({ monaco });
const Container = styled.div` const Container = styled.div<{ $minHeight: string | number }>`
min-height: ${({ $minHeight }) =>
typeof $minHeight === 'number' ? `${$minHeight}px` : $minHeight};
position: relative; position: relative;
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius); border-radius: var(--ant-border-radius);
@@ -151,7 +153,7 @@ const YamlEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
}, [value]); }, [value]);
return ( return (
<Container> <Container $minHeight={height}>
<EditorInner <EditorInner
ref={editorRef} ref={editorRef}
header={renderHeader()} header={renderHeader()}
@@ -224,6 +224,7 @@ const ClusterCreate = () => {
return ModuleComponent ? ( return ModuleComponent ? (
<ModuleComponent <ModuleComponent
key={key} key={key}
actionSource={'page'}
stepList={ stepList={
extraData.provider === ProviderValueMap.Docker extraData.provider === ProviderValueMap.Docker
? DockerStepsFromCluster ? DockerStepsFromCluster
@@ -56,6 +56,7 @@ const AddCluster: React.FC<AddModalProps> = ({
closeIcon={true} closeIcon={true}
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
centered={true}
width={680} width={680}
footer={ footer={
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter> <ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
@@ -30,7 +30,7 @@ const Container = styled.div`
* clusterList and onClusterChange are only required when from worker page. * clusterList and onClusterChange are only required when from worker page.
*/ */
type AddWorkerProps = { type AddWorkerProps = {
isModal?: boolean; actionSource?: 'modal' | 'page';
provider: ProviderType; provider: ProviderType;
clusterList?: Global.BaseOption<number, ClusterListItem>[]; clusterList?: Global.BaseOption<number, ClusterListItem>[];
clusterLoading?: boolean; clusterLoading?: boolean;
@@ -51,7 +51,7 @@ type AddWorkerProps = {
*/ */
const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => { const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
const { const {
isModal = false, actionSource,
registrationInfo, registrationInfo,
provider, provider,
clusterList, clusterList,
@@ -81,10 +81,12 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
}, [stepList]); }, [stepList]);
React.useEffect(() => { React.useEffect(() => {
if (!isModal) { // this effect is only triggered when used in cluster create page
console.log('actionSource:', actionSource);
if (actionSource === 'page') {
createModelsChunkRequest(); createModelsChunkRequest();
} }
}, [isModal]); }, [actionSource]);
return ( return (
<AddWorkerContext.Provider <AddWorkerContext.Provider
@@ -141,7 +141,7 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
} }
> >
<AddWorkerStep <AddWorkerStep
isModal={true} actionSource={'modal'}
stepList={stepList} stepList={stepList}
provider={provider} provider={provider}
clusterList={clusterList} clusterList={clusterList}
@@ -193,6 +193,9 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
]} ]}
> >
<SealInput.Input <SealInput.Input
description={intl.formatMessage({
id: 'clusters.form.serverUrl.tips'
})}
label={intl.formatMessage({ id: 'clusters.create.serverUrl' })} label={intl.formatMessage({ id: 'clusters.create.serverUrl' })}
required={true} required={true}
trim={true} trim={true}
@@ -1,3 +1,4 @@
import { useScrollerContext } from '@/components/scroller-modal/use-scroller-context';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealTextArea from '@/components/seal-form/seal-textarea'; import SealTextArea from '@/components/seal-form/seal-textarea';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
@@ -24,21 +25,41 @@ type AddModalProps = {
}; };
const ClusterForm: React.FC<AddModalProps> = forwardRef( const ClusterForm: React.FC<AddModalProps> = forwardRef(
({ action, provider, currentData, credentialList, onFinish }, ref) => { ({ action, provider, currentData, credentialList, onFinish }, ref) => {
const { scrollToBottom } = useScrollerContext();
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl(); const intl = useIntl();
const [activeKey, setActiveKey] = React.useState<string[]>([]); const [activeKey, setActiveKey] = React.useState<string[]>([]);
const advanceConfigRef = React.useRef<any>(null); const advanceConfigRef = React.useRef<any>(null);
const handleOnCollapseChange = (keys: string | string[]) => { const handleOnCollapseChange = async (keys: string | string[]) => {
setActiveKey(Array.isArray(keys) ? keys : [keys]); setActiveKey(Array.isArray(keys) ? keys : [keys]);
if (currentData && advanceConfigRef.current) { if (keys?.includes?.('advanceConfig')) {
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
scrollToBottom();
} }
}; };
const handleOnFinish = (values: FormData) => {
const workerConfig = yaml2Json(advanceConfigRef.current?.getYamlValue());
onFinish({
...values,
worker_config: {
...workerConfig
}
});
};
useEffect(() => { useEffect(() => {
if (currentData) { if (currentData) {
form.setFieldsValue(currentData); form.setFieldsValue(currentData);
}
}, [currentData]);
useEffect(() => {
if (currentData) {
if (advanceConfigRef.current) { if (advanceConfigRef.current) {
const workerConfigYaml = json2Yaml(currentData.worker_config || {}); const workerConfigYaml = json2Yaml(currentData.worker_config || {});
advanceConfigRef.current?.setYamlValue(workerConfigYaml); advanceConfigRef.current?.setYamlValue(workerConfigYaml);
@@ -78,7 +99,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
<Form <Form
name="clusterForm" name="clusterForm"
form={form} form={form}
onFinish={onFinish} onFinish={handleOnFinish}
preserve={false} preserve={false}
scrollToFirstError={true} scrollToFirstError={true}
initialValues={currentData} initialValues={currentData}
@@ -113,7 +134,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
)} )}
<Form.Item<FormData> name="description" rules={[{ required: false }]}> <Form.Item<FormData> name="description" rules={[{ required: false }]}>
<SealTextArea <SealTextArea
autoSize={{ minRows: 4, maxRows: 6 }} autoSize={{ minRows: 2, maxRows: 4 }}
label={intl.formatMessage({ id: 'common.table.description' })} label={intl.formatMessage({ id: 'common.table.description' })}
></SealTextArea> ></SealTextArea>
</Form.Item> </Form.Item>
@@ -25,7 +25,6 @@ export default function useAddWorkerMessage() {
onCreate: (newItems: any) => { onCreate: (newItems: any) => {
if (startWatchRef.current) { if (startWatchRef.current) {
newItemsRef.current = newItemsRef.current.concat(newItems); newItemsRef.current = newItemsRef.current.concat(newItems);
console.log('newItemsRef.current:', newItemsRef.current);
showAddWorkerMessage(); showAddWorkerMessage();
} }
} }
@@ -48,13 +47,13 @@ export default function useAddWorkerMessage() {
chunkRequestRef.current?.current?.cancel?.(); chunkRequestRef.current?.current?.cancel?.();
resetAddedCount(); resetAddedCount();
try { try {
chunkRequestRef.current = await setChunkRequest({ chunkRequestRef.current = setChunkRequest({
url: WORKERS_API, url: WORKERS_API,
handler: updateHandler handler: updateHandler
}); });
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
startWatchRef.current = true; startWatchRef.current = true;
}, 1000); }, 5000);
} catch (error) { } catch (error) {
// ignore // ignore
} }
@@ -1,8 +1,9 @@
import IconFont from '@/components/icon-font';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import YamlEditor from '@/pages/_components/yaml-editor'; import YamlEditor from '@/pages/_components/yaml-editor';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Button, Form } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import { ProviderType, ProviderValueMap } from '../config'; import { ProviderType, ProviderValueMap } from '../config';
import { ClusterFormData as FormData } from '../config/types'; import { ClusterFormData as FormData } from '../config/types';
@@ -24,7 +25,6 @@ const ClusterAdvanceConfig: React.FC<{
return editorRef.current?.getValue(); return editorRef.current?.getValue();
}, },
setYamlValue: (values: any) => { setYamlValue: (values: any) => {
console.log('setYamlValue:', values, editorRef.current);
editorRef.current?.setValue(values); editorRef.current?.setValue(values);
} }
})); }));
@@ -42,6 +42,9 @@ const ClusterAdvanceConfig: React.FC<{
]} ]}
> >
<SealInput.Input <SealInput.Input
description={intl.formatMessage({
id: 'clusters.form.serverUrl.tips'
})}
label={intl.formatMessage({ id: 'clusters.create.serverUrl' })} label={intl.formatMessage({ id: 'clusters.create.serverUrl' })}
required={false} required={false}
trim={true} trim={true}
@@ -62,7 +65,23 @@ const ClusterAdvanceConfig: React.FC<{
</Form.Item> </Form.Item>
<YamlEditor <YamlEditor
ref={editorRef} ref={editorRef}
title={`${intl.formatMessage({ id: 'clusters.create.workerConfig' })} YAML`} title={
<span className="flex-center">
<span>{`${intl.formatMessage({ id: 'clusters.create.workerConfig' })} YAML`}</span>
<Button
size="small"
type="link"
target="_blank"
href="https://docs.gpustack.ai/latest/cli-reference/start/#config-file"
>
{intl.formatMessage({ id: 'playground.audio.enablemic.doc' })}{' '}
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</Button>
</span>
}
value={fileContent} value={fileContent}
height={300} height={300}
onUpload={(content) => { onUpload={(content) => {
@@ -72,11 +72,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
if (!props.id) return; if (!props.id) return;
if (open) { if (open) {
requestRef.current?.current?.cancel?.(); requestRef.current?.current?.cancel?.();
setChunkRequest({ requestRef.current = setChunkRequest({
url: `${MODELS_API}/${props.modelId}/instances`, url: `${MODELS_API}/${props.modelId}/instances`,
handler: updateHandler handler: updateHandler
}).then((res) => {
requestRef.current = res;
}); });
} else { } else {
logsViewerRef.current?.abort(); logsViewerRef.current?.abort();
+1 -1
View File
@@ -201,7 +201,7 @@ const Models: React.FC = () => {
search: search, search: search,
categories: categories categories: categories
}; };
chunkRequedtRef.current = await setChunkRequest({ chunkRequedtRef.current = setChunkRequest({
url: `${MODELS_API}?${qs.stringify(_.pickBy(query, (val: any) => !!val))}`, url: `${MODELS_API}?${qs.stringify(_.pickBy(query, (val: any) => !!val))}`,
handler: updateHandler handler: updateHandler
}); });