fix: cluster value setting, volumes setting

This commit is contained in:
jialin
2025-09-19 20:00:25 +08:00
parent b640963f65
commit f542ebecb4
27 changed files with 677 additions and 109 deletions
+1 -2
View File
@@ -172,9 +172,8 @@ export default function CollapsibleContainer({
<div <div
ref={contentRef} ref={contentRef}
style={{ style={{
maxHeight: height, height: height,
overflow: 'hidden' overflow: 'hidden'
// transition: collapsible ? 'max-height 0.2s ease' : 'none'
}} }}
> >
<div style={{ paddingTop: 8 }}>{children}</div> <div style={{ paddingTop: 8 }}>{children}</div>
@@ -4,6 +4,7 @@ import { FormWidgetProps } from '../config/types';
const FormWidget: React.FC< const FormWidget: React.FC<
FormWidgetProps & { FormWidgetProps & {
onChange?: (data: any) => void; onChange?: (data: any) => void;
disabled?: boolean;
} }
> = ({ > = ({
widget, widget,
@@ -17,8 +18,10 @@ const FormWidget: React.FC<
value, value,
min, min,
max, max,
status,
checked, checked,
readOnly: disabled, isInFormItems,
disabled,
onChange onChange
}) => { }) => {
const Component = ComponentsMap[widget]; const Component = ComponentsMap[widget];
@@ -38,6 +41,8 @@ const FormWidget: React.FC<
min, min,
max max
}} }}
status={status}
isInFormItems={isInFormItems}
disabled={disabled} disabled={disabled}
options={options || optionList} options={options || optionList}
value={value} value={value}
@@ -3,6 +3,7 @@ import { MinusOutlined } from '@ant-design/icons';
import { Button } from 'antd'; import { Button } from 'antd';
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { statusType } from '../config/types';
import FormWidget from './form-widget'; import FormWidget from './form-widget';
const RowWrapper = styled.div` const RowWrapper = styled.div`
@@ -23,8 +24,12 @@ interface ListMapProps {
dataList: any[]; dataList: any[];
label?: React.ReactNode; label?: React.ReactNode;
btnText?: string; btnText?: string;
requiredFields?: string[];
validateStatusList?: Record<string, statusType>[];
properties: Record<string, any>; properties: Record<string, any>;
disabled?: boolean; disabled?: boolean;
onAdd?: (data: any[]) => void;
onDelete?: (deletedItem: any, data: any[]) => void;
onChange?: (data: any) => void; onChange?: (data: any) => void;
} }
@@ -32,6 +37,7 @@ interface ListItemProps {
schemaList: any[]; schemaList: any[];
data: Record<string, any>; data: Record<string, any>;
disabled?: boolean; disabled?: boolean;
validateStatus?: Record<string, statusType>;
onChange?: (data: any) => void; onChange?: (data: any) => void;
} }
@@ -39,6 +45,7 @@ const ListItem: React.FC<ListItemProps> = ({
schemaList, schemaList,
data, data,
onChange, onChange,
validateStatus,
disabled disabled
}) => { }) => {
const handleValueChange = (name: string, target: any) => { const handleValueChange = (name: string, target: any) => {
@@ -50,17 +57,18 @@ const ListItem: React.FC<ListItemProps> = ({
onChange?.({ [name]: value }); onChange?.({ [name]: value });
} }
}; };
return ( return (
<> <>
{schemaList.map((schema: any) => ( {schemaList.map((schema: any) => (
<FormWidget <FormWidget
status={validateStatus?.[schema.name]}
widget={schema.type} widget={schema.type}
{...schema} {...schema}
disabled={disabled} disabled={disabled || schema.readOnly}
key={schema.name} key={schema.name}
value={data?.[schema.name]} value={data?.[schema.name]}
checked={data?.[schema.name]} checked={data?.[schema.name]}
isInFormItems={false}
onChange={(target) => handleValueChange(schema.name, target)} onChange={(target) => handleValueChange(schema.name, target)}
/> />
))} ))}
@@ -73,8 +81,12 @@ const ListMap: React.FC<ListMapProps> = ({
label, label,
btnText, btnText,
properties = {}, properties = {},
requiredFields = [],
minItems = 0, minItems = 0,
validateStatusList = [],
disabled, disabled,
onAdd,
onDelete,
onChange onChange
}) => { }) => {
const [items, setItems] = React.useState(dataList || []); const [items, setItems] = React.useState(dataList || []);
@@ -82,23 +94,27 @@ const ListMap: React.FC<ListMapProps> = ({
const schemaList = useMemo(() => { const schemaList = useMemo(() => {
const list = Object.entries(properties).map(([key, value]) => ({ const list = Object.entries(properties).map(([key, value]) => ({
...value, ...value,
required: requiredFields.includes(key),
name: key name: key
})); }));
return list; return list;
}, [properties]); }, [properties, requiredFields]);
const handleOnAdd = () => { const handleOnAdd = () => {
const keys = Object.keys(properties); const keys = Object.keys(properties);
setItems([ const newItems = [
...items, ...items,
{ ...keys.reduce((acc, key) => ({ ...acc, [key]: '' }), {}) } { ...keys.reduce((acc, key) => ({ ...acc, [key]: '' }), {}) }
]); ];
setItems(newItems);
onAdd?.(newItems);
}; };
const handleDelete = (index: number) => { const handleDelete = (index: number) => {
const deleteItem = items[index];
const newItems = items.filter((_, i) => i !== index); const newItems = items.filter((_, i) => i !== index);
setItems(newItems); setItems(newItems);
onChange?.(newItems); onDelete?.(deleteItem, newItems);
}; };
const handleItemChange = (index: number, data: { [key: string]: any }) => { const handleItemChange = (index: number, data: { [key: string]: any }) => {
@@ -131,6 +147,7 @@ const ListMap: React.FC<ListMapProps> = ({
<ListItem <ListItem
schemaList={schemaList} schemaList={schemaList}
data={item} data={item}
validateStatus={validateStatusList?.[index]}
disabled={disabled} disabled={disabled}
onChange={(value) => handleItemChange(index, value)} onChange={(value) => handleItemChange(index, value)}
/> />
@@ -15,9 +15,13 @@ export interface FieldSchema {
widget?: string; widget?: string;
min?: number; min?: number;
style?: React.CSSProperties; style?: React.CSSProperties;
required?: string[];
} }
export type statusType = 'error' | 'warning' | '' | undefined;
export interface FormWidgetProps { export interface FormWidgetProps {
status?: statusType;
isInFormItems?: boolean;
widget: 'Input' | 'Select' | 'Checkbox' | 'InputNumber'; widget: 'Input' | 'Select' | 'Checkbox' | 'InputNumber';
name: string; name: string;
title?: string; title?: string;
@@ -0,0 +1,61 @@
import { useRef } from 'react';
import { statusType } from '../config/types';
export default function useValidateFields(params: {
requiredFields?: string[];
setValidateStatusList: (statusList: { [key: string]: statusType }[]) => void;
}) {
const { requiredFields, setValidateStatusList } = params;
const validationEnabled = useRef(false);
const isEmptyValue = (value: any, key: string) => {
return !value;
};
const validateRule = (value: any, key: string) => {
return true;
};
const listMapValidator = async (_: any, valueList: any) => {
if (!validationEnabled.current) {
return Promise.resolve();
}
const fields = new Set<string>();
const statusList: { [key: string]: statusType }[] = [];
(valueList || []).forEach((item: any, index: number) => {
const status: { [key: string]: statusType } = {};
Object.entries(item || {}).forEach(([key, value]) => {
if (isEmptyValue(value, key)) {
fields.add(key);
if (requiredFields?.includes(key)) {
status[key] = 'error';
} else {
status[key] = '';
}
} else if (validateRule(value, key)) {
status[key] = '';
}
});
statusList.push(status);
});
setValidateStatusList(statusList);
if (fields.size > 0) {
return Promise.reject(`${Array.from(fields).join(', ')} is required`);
}
return Promise.resolve();
};
const toggleValidation = (enabled: boolean) => {
validationEnabled.current = enabled;
};
return {
listMapValidator,
toggleValidation
};
}
+35 -33
View File
@@ -3,41 +3,43 @@ import type { SelectProps } from 'antd';
import { Select } from 'antd'; import { Select } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
const BaseSelect: React.FC<SelectProps> = forwardRef((props, ref) => { const BaseSelect: React.FC<SelectProps & { ref?: any }> = forwardRef(
const [isFocus, setIsFocus] = React.useState(false); (props, ref) => {
const inputRef = React.useRef<any>(null); const [isFocus, setIsFocus] = React.useState(false);
const inputRef = React.useRef<any>(null);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
...(inputRef.current || ({} as any)) ...(inputRef.current || ({} as any))
})); }));
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => { const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
setIsFocus(true); setIsFocus(true);
props.onFocus?.(e); props.onFocus?.(e);
}; };
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => { const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setIsFocus(false); setIsFocus(false);
props.onBlur?.(e); props.onBlur?.(e);
}; };
const renderSuffixIcon = () => { const renderSuffixIcon = () => {
if (props.suffixIcon) { if (props.suffixIcon) {
return props.suffixIcon; return props.suffixIcon;
} }
if (!props.showSearch) { if (!props.showSearch) {
return <IconFont type="icon-down"></IconFont>; return <IconFont type="icon-down"></IconFont>;
} }
return !isFocus ? <IconFont type="icon-down"></IconFont> : undefined; return !isFocus ? <IconFont type="icon-down"></IconFont> : undefined;
}; };
return ( return (
<Select <Select
{...props} {...props}
ref={inputRef} ref={inputRef}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
suffixIcon={renderSuffixIcon()} suffixIcon={renderSuffixIcon()}
/> />
); );
}); }
);
export default BaseSelect; export default BaseSelect;
@@ -23,6 +23,8 @@ const SealInputNumber: React.FC<InputNumberProps & SealFormItemProps> = (
if (isInFormItems) { if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.(); const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || ''; status = statusData?.status || '';
} else {
status = props.status || '';
} }
useEffect(() => { useEffect(() => {
+2
View File
@@ -31,6 +31,8 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
if (isInFormItems) { if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.(); const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || ''; status = statusData?.status || '';
} else {
status = props.status || '';
} }
useEffect(() => { useEffect(() => {
+3
View File
@@ -25,12 +25,15 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
const intl = useIntl(); const intl = useIntl();
const [isFocus, setIsFocus] = useState(false); const [isFocus, setIsFocus] = useState(false);
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
let status = ''; let status = '';
// the status can be controlled by Form.Item // the status can be controlled by Form.Item
if (isInFormItems) { if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.(); const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || ''; status = statusData?.status || '';
} else {
status = props.status || '';
} }
const _options = useMemo(() => { const _options = useMemo(() => {
+3 -1
View File
@@ -41,5 +41,7 @@ export default {
'clusters.create.addworker.tips': 'clusters.create.addworker.tips':
' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.', ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
'clusters.create.addCommand.tips': 'clusters.create.addCommand.tips':
' On the Worker that needs to be added, run the following command to join it to the cluster.' ' On the Worker that needs to be added, run the following command to join it to the cluster.',
'cluster.create.checkEnv.tips':
'Use the following command to check if the environment is ready'
}; };
+1 -1
View File
@@ -29,7 +29,7 @@ export default {
'menu.404': '404', 'menu.404': '404',
'menu.clusterManagement': 'Cluster Management', 'menu.clusterManagement': 'Cluster Management',
'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.clusters': 'Clusters',
'menu.clusterManagement.credentials': 'Credentials', 'menu.clusterManagement.credentials': 'Cloud Credentials',
'menu.clusterManagement.clusterDetail': 'Cluster Detail', 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
'menu.clusterManagement.clusterCreate': 'Create Cluster' 'menu.clusterManagement.clusterCreate': 'Create Cluster'
}; };
+5 -2
View File
@@ -41,7 +41,9 @@ export default {
'clusters.create.addworker.tips': 'clusters.create.addworker.tips':
' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.', ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
'clusters.create.addCommand.tips': 'clusters.create.addCommand.tips':
' On the Worker that needs to be added, run the following command to join it to the cluster.' ' On the Worker that needs to be added, run the following command to join it to the cluster.',
'cluster.create.checkEnv.tips':
'Use the following command to check if the environment is ready'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -84,6 +86,7 @@ export default {
// 37. 'clusters.create.noRegions': 'No regions available', // 37. 'clusters.create.noRegions': 'No regions available',
// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool', // 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool',
// 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.', // 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
// 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.' // 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.',
// 41. 'cluster.create.checkEnv.tips': 'Use the following command to check if the environment is ready'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+2 -2
View File
@@ -28,7 +28,7 @@ export default {
'menu.accessControl.users': 'Users', 'menu.accessControl.users': 'Users',
'menu.clusterManagement': 'Cluster Management', 'menu.clusterManagement': 'Cluster Management',
'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.clusters': 'Clusters',
'menu.clusterManagement.credentials': 'Credentials', 'menu.clusterManagement.credentials': 'Cloud Credentials',
'menu.models.userModels': 'My Models', 'menu.models.userModels': 'My Models',
'menu.clusterManagement.clusterDetail': 'Cluster Detail', 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
'menu.clusterManagement.clusterCreate': 'Create Cluster' 'menu.clusterManagement.clusterCreate': 'Create Cluster'
@@ -44,7 +44,7 @@ export default {
// 7. 'menu.accessControl.users': 'Users', // 7. 'menu.accessControl.users': 'Users',
// 8. 'menu.clusterManagement': 'Cluster Management', // 8. 'menu.clusterManagement': 'Cluster Management',
// 9. 'menu.clusterManagement.clusters': 'Clusters', // 9. 'menu.clusterManagement.clusters': 'Clusters',
// 10. 'menu.clusterManagement.credentials': 'Credentials', // 10. 'menu.clusterManagement.credentials': 'Cloud Credentials',
// 11. 'menu.models.userModels': 'My Models' // 11. 'menu.models.userModels': 'My Models'
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail', // 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster' // 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster'
+5 -2
View File
@@ -41,7 +41,9 @@ export default {
'clusters.create.addworker.tips': 'clusters.create.addworker.tips':
' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.', ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
'clusters.create.addCommand.tips': 'clusters.create.addCommand.tips':
' On the Worker that needs to be added, run the following command to join it to the cluster.' ' On the Worker that needs to be added, run the following command to join it to the cluster.',
'cluster.create.checkEnv.tips':
'Use the following command to check if the environment is ready'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -84,6 +86,7 @@ export default {
// 37. 'clusters.create.noRegions': 'No regions available', // 37. 'clusters.create.noRegions': 'No regions available',
// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool' // 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool'
// 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.', // 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
// 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.' // 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.',
// 41. 'cluster.create.checkEnv.tips':'Use the following command to check if the environment is ready'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+2 -2
View File
@@ -28,7 +28,7 @@ export default {
'menu.accessControl.users': 'Пользователи', 'menu.accessControl.users': 'Пользователи',
'menu.clusterManagement': 'Cluster Management', 'menu.clusterManagement': 'Cluster Management',
'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.clusters': 'Clusters',
'menu.clusterManagement.credentials': 'Credentials', 'menu.clusterManagement.credentials': 'Cloud Credentials',
'menu.models.userModels': 'My Models', 'menu.models.userModels': 'My Models',
'menu.clusterManagement.clusterDetail': 'Cluster Detail', 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
'menu.clusterManagement.clusterCreate': 'Create Cluster' 'menu.clusterManagement.clusterCreate': 'Create Cluster'
@@ -44,7 +44,7 @@ export default {
// 7. 'menu.accessControl.users': 'Users', // 7. 'menu.accessControl.users': 'Users',
// 8. 'menu.clusterManagement': 'Cluster Management', // 8. 'menu.clusterManagement': 'Cluster Management',
// 9. 'menu.clusterManagement.clusters': 'Clusters', // 9. 'menu.clusterManagement.clusters': 'Clusters',
// 10. 'menu.clusterManagement.credentials': 'Credentials', // 10. 'menu.clusterManagement.credentials': 'Cloud Credentials',
// 11. 'menu.models.userModels': 'My Models', // 11. 'menu.models.userModels': 'My Models',
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail', // 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster' // 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster'
+2 -1
View File
@@ -40,5 +40,6 @@ export default {
'clusters.create.addworker.tips': 'clusters.create.addworker.tips':
'在执行以下命令之前,请确保已满足 <a href={link} target="_blank">{label}</a> 的先决条件。', '在执行以下命令之前,请确保已满足 <a href={link} target="_blank">{label}</a> 的先决条件。',
'clusters.create.addCommand.tips': 'clusters.create.addCommand.tips':
' 在需要添加的 Worker 上运行以下命令,将其加入到集群中' ' 在需要添加的 Worker 上运行以下命令,将其加入到集群中',
'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当'
}; };
+1 -1
View File
@@ -29,7 +29,7 @@ export default {
'menu.accessControl.users': '用户', 'menu.accessControl.users': '用户',
'menu.clusterManagement': '集群管理', 'menu.clusterManagement': '集群管理',
'menu.clusterManagement.clusters': '集群', 'menu.clusterManagement.clusters': '集群',
'menu.clusterManagement.credentials': '凭证', 'menu.clusterManagement.credentials': '凭证',
'menu.clusterManagement.clusterDetail': '集群详情', 'menu.clusterManagement.clusterDetail': '集群详情',
'menu.clusterManagement.clusterCreate': '创建集群' 'menu.clusterManagement.clusterCreate': '创建集群'
}; };
@@ -5,6 +5,7 @@ import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ProviderType, ProviderValueMap } from '../config'; import { ProviderType, ProviderValueMap } from '../config';
import AddWorkerCommand from './add-worker-command'; import AddWorkerCommand from './add-worker-command';
import CheckEnvCommand from './check-env-command';
import RegisterClusterInner from './register-cluster-inner'; import RegisterClusterInner from './register-cluster-inner';
import SupportedGPUs from './support-gpus'; import SupportedGPUs from './support-gpus';
@@ -62,8 +63,8 @@ const AddWorkerStep: React.FC<AddModalProps> = ({
registrationInfo registrationInfo
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const [currentSelection, setCurrentSelection] = const [currentGPU, setCurrentGPU] = React.useState<string>('cuda');
React.useState<string>('cuda'); React.useState<string>('cuda');
const [workerCommand, setWorkerCommand] = React.useState<Record<string, any>>( const [workerCommand, setWorkerCommand] = React.useState<Record<string, any>>(
{ {
label: 'NVIDIA', label: 'NVIDIA',
@@ -73,7 +74,7 @@ const AddWorkerStep: React.FC<AddModalProps> = ({
const handleSelectProvider = (value: string, item: any) => { const handleSelectProvider = (value: string, item: any) => {
if (provider !== ProviderValueMap.Docker) return; if (provider !== ProviderValueMap.Docker) return;
setCurrentSelection(value); setCurrentGPU(value);
setWorkerCommand(item); setWorkerCommand(item);
}; };
@@ -84,31 +85,35 @@ const AddWorkerStep: React.FC<AddModalProps> = ({
</Title> </Title>
<SupportedGPUs <SupportedGPUs
onSelect={handleSelectProvider} onSelect={handleSelectProvider}
current={provider === ProviderValueMap.Docker ? currentSelection : ''} current={currentGPU}
clickable={provider === ProviderValueMap.Docker} clickable={true}
/> />
{provider === ProviderValueMap.Docker && (
<Content> <Content>
<Line></Line> <Line></Line>
<Alert <Alert
type="info" type="info"
showIcon showIcon
icon={<BulbOutlined />} icon={<BulbOutlined />}
message={ message={
<span <span
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: intl.formatMessage( __html: intl.formatMessage(
{ id: 'clusters.create.addworker.tips' }, { id: 'clusters.create.addworker.tips' },
{ label: workerCommand.label, link: workerCommand.link } { label: workerCommand.label, link: workerCommand.link }
) )
}} }}
></span> ></span>
} }
></Alert> ></Alert>
</Content> </Content>
)}
<div className="command-info"> <div className="command-info">
{intl.formatMessage({ id: 'clusters.create.addCommand.tips' })} 1. {intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
</div>
<CheckEnvCommand provider={provider} currentGPU={currentGPU} />
<div className="command-info">
2. {intl.formatMessage({ id: 'clusters.create.addCommand.tips' })}
</div> </div>
{provider === ProviderValueMap.Kubernetes ? ( {provider === ProviderValueMap.Kubernetes ? (
<RegisterClusterInner registrationInfo={registrationInfo} /> <RegisterClusterInner registrationInfo={registrationInfo} />
@@ -0,0 +1,30 @@
import HighlightCode from '@/components/highlight-code';
import { addWorkerGuide } from '@/pages/resources/config';
import React from 'react';
import { ProviderType } from '../config';
type ViewModalProps = {
provider: ProviderType;
currentGPU: string;
};
const AddWorkerCommand: React.FC<ViewModalProps> = ({
provider = '',
currentGPU
}) => {
const code = React.useMemo(() => {
const command = addWorkerGuide['cuda'];
return command.checkEnvCommand[provider || ''];
}, [provider]);
return (
<HighlightCode
theme="dark"
code={code}
copyValue={code}
lang="bash"
></HighlightCode>
);
};
export default AddWorkerCommand;
@@ -0,0 +1,390 @@
import DropdownButtons from '@/components/drop-down-buttons';
import GaugeChart from '@/components/echarts/gauge';
import IconFont from '@/components/icon-font';
import StatusTag from '@/components/status-tag';
import ThemeTag from '@/components/tags-wrapper/theme-tag';
import Card from '@/components/templates/card';
import { PageAction } from '@/config';
import { Card as ACard, Col, Collapse, Row } from 'antd';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { queryClusterToken } from '../apis';
import {
ClusterStatus,
ClusterStatusLabelMap,
ProviderLabelMap,
ProviderValueMap,
clusterActionList
} from '../config';
import { ClusterListItem as ListItem, NodePoolListItem } from '../config/types';
import AddPool from './add-pool';
import RegisterCluster from './register-cluster';
import WorkerPools from './worker-pools';
const Content = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
.chart-wrapper {
flex: 1;
}
`;
const CardWrapper = styled(ACard)`
text-align: center;
box-shadow: none !important;
flex: 1;
.ant-card {
box-shadow: none;
}
.ant-card-body {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 6px 10px;
}
.label {
font-weight: 400;
margin-bottom: 12px;
font-size: var(--font-size-middle);
color: var(--ant-color-text-secondary);
}
.value {
font-weight: 600;
color: var(--ant-color-text);
font-size: var(--font-size-large);
}
`;
const CardBox = styled.div`
display: flex;
gap: 16px;
flex: 1;
`;
const Inner = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
cursor: default;
.title {
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
.text {
display: flex;
align-items: center;
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
}
}
.content {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
}
`;
const CollapseWrapper = styled(Collapse)`
width: 100%;
border: none;
border-radius: 0;
border-top: 1px solid var(--ant-color-split);
background-color: var(--ant-color-fill-quaternary);
.ant-collapse-content {
border-top: 1px solid var(--ant-color-split);
}
.ant-collapse-header {
font-size: var(--font-size-middle);
padding-block: 10px !important;
.ant-collapse-expand-icon {
margin-left: 0 !important;
}
}
table .ant-table-thead tr {
background-color: transparent !important;
th {
border-bottom: 1px solid var(--ant-color-split) !important;
font-weight: 500 !important;
}
}
`;
const SubTitle = styled.div`
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
margin-block: 24px 16px;
`;
interface CardProps {
data: ListItem;
onSelect?: (key: string, row: ListItem) => void;
}
const gaugeConfig = {
radius: '100%',
progress: {
show: true,
roundCap: false,
width: 8
},
axisLine: {
roundCap: false,
lineStyle: {
width: 8,
color: [
[0.5, 'rgba(84, 204, 152, 80%)'],
[0.8, 'rgba(250, 173, 20, 80%)'],
[1, 'rgba(255, 77, 79, 80%)']
]
}
}
};
const CardItem: React.FC<CardProps> = (props) => {
const chartHeight = 160;
const { data, onSelect } = props;
const [show, setShow] = React.useState(false);
const [addPoolStatus, setAddPoolStatus] = React.useState({
open: false,
action: PageAction.CREATE,
title: '',
provider: ProviderValueMap.DigitalOcean
});
const [registerClusterStatus, setRegisterClusterStatus] = React.useState<{
open: boolean;
registrationInfo: {
token: string;
image: string;
server_url: string;
cluster_id: number;
};
}>({
open: false,
registrationInfo: {
token: '',
image: '',
server_url: '',
cluster_id: 0
}
});
const handleRegisterCluster = async () => {
try {
const info = await queryClusterToken({ id: data.id });
setRegisterClusterStatus({
open: true,
registrationInfo: {
...info,
cluster_id: data.id
}
});
} catch (error) {}
};
// cluster action handler
const handleOnSelect = (key: string) => {
if (key === 'addPool') {
setAddPoolStatus({
open: true,
action: PageAction.CREATE,
title: 'Add Worker Pool',
provider: data.provider
});
return;
}
if (key === 'register_cluster') {
handleRegisterCluster();
return;
}
onSelect?.(key, data);
};
// pool action handler
const handleOnAction = (action: string, record: NodePoolListItem) => {
if (action === 'edit') {
setAddPoolStatus({
open: true,
action: PageAction.CREATE,
title: 'Edit Worker Pool',
provider: data.provider
});
}
};
const actions = useMemo(() => {
return clusterActionList.filter((item) => {
if (item.provider) {
return item.provider === data.provider;
}
return true;
});
}, [data.provider]);
return (
<Card
height={'auto'}
clickable={false}
ghost
footerHolder={
<CollapseWrapper
onChange={() => setShow(!show)}
expandIconPosition="end"
expandIcon={({ isActive }) => (
<IconFont type="icon-down" rotate={isActive ? 0 : -90} />
)}
items={[
{
key: '1',
label: 'More Information',
children: (
<>
<div className="chart-wrapper">
<Row gutter={16} style={{ width: '100%' }}>
<Col span={6}>
<GaugeChart
title="GPU Utilization"
value={85}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="CPU Utilization"
value={50}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="RAM Utilization"
value={70}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="VRAM Utilization"
value={60}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
</Row>
</div>
{data.provider === ProviderValueMap.DigitalOcean && (
<>
<SubTitle>Worker Pools</SubTitle>
<WorkerPools
provider={data.provider}
workerPools={data.worker_pools}
height={show ? 'auto' : 0}
onAction={handleOnAction}
/>
</>
)}
</>
)
}
]}
></CollapseWrapper>
}
>
<Inner>
<div className="title">
<span className="flex-center gap-8">
<span className="text">{data.name}</span>
<ThemeTag color="purple">
{ProviderLabelMap[data.provider]}
</ThemeTag>
<StatusTag
statusValue={{
status: ClusterStatus[data.state],
text: ClusterStatusLabelMap[data.state]
}}
/>
</span>
<span>
<DropdownButtons
items={actions}
onSelect={handleOnSelect}
></DropdownButtons>
</span>
</div>
<Content>
<CardBox>
<CardWrapper bordered={false}>
<div className="label">Workers</div>
<div className="value">
{data.ready_workers} / {data.workers}
</div>
</CardWrapper>
<CardWrapper bordered={false}>
<div className="label">GPUs</div>
<div className="value">{data.gpus}</div>
</CardWrapper>
<CardWrapper bordered={false}>
<div className="label">Deployments</div>
<div className="value">{data.models}</div>
</CardWrapper>
</CardBox>
</Content>
</Inner>
<AddPool
provider={addPoolStatus.provider}
open={addPoolStatus.open}
action={addPoolStatus.action}
title={addPoolStatus.title}
onCancel={() => {
setAddPoolStatus({
open: false,
action: PageAction.CREATE,
title: '',
provider: 'digitalocean'
});
}}
onOk={() => {
setAddPoolStatus({
open: false,
action: addPoolStatus.action,
title: '',
provider: 'digitalocean'
});
}}
></AddPool>
<RegisterCluster
title="Register Cluster"
open={registerClusterStatus.open}
registrationInfo={registerClusterStatus.registrationInfo}
onCancel={() => {
setRegisterClusterStatus({
open: false,
registrationInfo: {
token: '',
image: '',
server_url: '',
cluster_id: 0
}
});
}}
></RegisterCluster>
</Card>
);
};
export default CardItem;
@@ -205,8 +205,6 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
value: string | number; value: string | number;
}) => { }) => {
if (action === PageAction.EDIT) { if (action === PageAction.EDIT) {
console.log('imageLabelRender========::', action, currentData, data);
// return currentData?.image_name || currentData?.os_image;
const vendor = _.split(currentData?.image_name || '', ' ')[0]; const vendor = _.split(currentData?.image_name || '', ' ')[0];
const iconType = _.get(vendorIconMap, vendor.toLowerCase()); const iconType = _.get(vendorIconMap, vendor.toLowerCase());
return ( return (
@@ -217,7 +215,6 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
); );
} }
const selectImage = osImageList.find((item) => item.value === data.value); const selectImage = osImageList.find((item) => item.value === data.value);
console.log('imageLabelRender========::', selectImage);
if (selectImage) { if (selectImage) {
return ( return (
<RenderLabel label={data.label} vendor={selectImage.vendor || ''} /> <RenderLabel label={data.label} vendor={selectImage.vendor || ''} />
@@ -272,14 +269,6 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
); );
}; };
const filterImageOption = (inputValue: string, option: any) => {
return (
option.label.toLowerCase().includes(inputValue.toLowerCase()) ||
option.value.toLowerCase().includes(inputValue.toLowerCase()) ||
option.description.toLowerCase().includes(inputValue.toLowerCase())
);
};
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
resetFields: () => { resetFields: () => {
form.resetFields(); form.resetFields();
@@ -1,7 +1,9 @@
import ListMap from '@/components/dynamic-form/components/list-map'; import ListMap from '@/components/dynamic-form/components/list-map';
import { statusType } from '@/components/dynamic-form/config/types';
import useValidateFields from '@/components/dynamic-form/hooks/use-validate-fields';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import React, { forwardRef } from 'react'; import React, { forwardRef, useState } from 'react';
import { fieldConfig } from '../config/cloud-options-config'; import { fieldConfig } from '../config/cloud-options-config';
const volumeOptions = fieldConfig.volumes; const volumeOptions = fieldConfig.volumes;
@@ -14,21 +16,56 @@ const CloudOptions: React.FC<{
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form); const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form);
const [validateStatusList, setValidateStatusList] = useState<
{ [key: string]: statusType }[]
>([]);
const handleOnChange = (name: string, value: any) => { const updateValidateStatusList = (list: { [key: string]: statusType }[]) => {
console.log('handleOnChange========', name, value); setValidateStatusList(list);
form.setFieldValue(['cloud_options', name], value);
}; };
const { listMapValidator, toggleValidation } = useValidateFields({
requiredFields: volumeOptions.required,
setValidateStatusList: updateValidateStatusList
});
const handleOnChange = (value: any) => {
toggleValidation(true);
form.setFieldValue(['cloud_options', volumeOptions.name], value);
};
const handleOnAdd = (data: any[]) => {
toggleValidation(false);
form.setFieldValue(['cloud_options', volumeOptions.name], data);
toggleValidation(true);
};
const handleOnDelete = (deletedItem: any, data: any[]) => {
toggleValidation(false);
form.setFieldValue(['cloud_options', volumeOptions.name], data);
toggleValidation(true);
};
console.log('disabled=====', disabled);
return ( return (
<Form.Item name={['cloud_options', volumeOptions.name as string]}> <Form.Item
name={['cloud_options', volumeOptions.name as string]}
rules={[
{
validator: listMapValidator
}
]}
>
<ListMap <ListMap
validateStatusList={validateStatusList}
btnText={intl.formatMessage({ id: 'clusters.workerpool.volumes.add' })} btnText={intl.formatMessage({ id: 'clusters.workerpool.volumes.add' })}
label={intl.formatMessage({ id: 'clusters.workerpool.volumes' })} label={intl.formatMessage({ id: 'clusters.workerpool.volumes' })}
dataList={volumes || []} dataList={volumes || []}
properties={volumeOptions.properties || {}} properties={volumeOptions.properties || {}}
requiredFields={volumeOptions.required || []}
disabled={disabled} disabled={disabled}
onChange={(value) => handleOnChange(volumeOptions.name, value)} onAdd={handleOnAdd}
onDelete={handleOnDelete}
onChange={handleOnChange}
/> />
</Form.Item> </Form.Item>
); );
@@ -21,6 +21,7 @@ export const fieldConfig: Record<string, FieldSchema> = {
type: 'array', type: 'array',
title: 'Volumes', title: 'Volumes',
name: 'volumes', name: 'volumes',
required: ['name', 'size_gb', 'format'],
properties: { properties: {
name: { name: {
name: 'name', name: 'name',
@@ -351,6 +351,9 @@ const AddModal: FC<AddModalProps> = (props) => {
}, [onCancel]); }, [onCancel]);
const initClusterId = () => { const initClusterId = () => {
if (initialValues?.cluster_id) {
return initialValues.cluster_id;
}
const cluster_id = const cluster_id =
clusterList?.find((item) => item.provider === ProviderValueMap.Docker) clusterList?.find((item) => item.provider === ProviderValueMap.Docker)
?.value || clusterList?.[0]?.value; ?.value || clusterList?.[0]?.value;
@@ -361,14 +364,12 @@ const AddModal: FC<AddModalProps> = (props) => {
const handleOnOpen = () => { const handleOnOpen = () => {
if (props.deploymentType === 'modelFiles') { if (props.deploymentType === 'modelFiles') {
form.current?.form?.setFieldsValue({ form.current?.form?.setFieldsValue({
...props.initialValues, ...props.initialValues
cluster_id: initClusterId()
}); });
handleOnValuesChange?.({ handleOnValuesChange?.({
changedValues: {}, changedValues: {},
allValues: { allValues: {
...props.initialValues, ...props.initialValues
cluster_id: initClusterId()
}, },
source: source source: source
}); });
@@ -422,7 +423,7 @@ const AddModal: FC<AddModalProps> = (props) => {
message: [] message: []
}); });
}; };
}, [open, clusterList]); }, [open, clusterList, initialValues?.cluster_id]);
return ( return (
<GSDrawer <GSDrawer
@@ -115,7 +115,7 @@ export const useGenerateWorkerOptions = () => {
const [workersList, setWorkersList] = useState< const [workersList, setWorkersList] = useState<
Global.BaseOption< Global.BaseOption<
number, number,
{ state: string; labels: Record<string, string> } { state: string; labels: Record<string, string>; cluster_id: number }
>[] >[]
>([]); >([]);
@@ -129,7 +129,11 @@ export const useGenerateWorkerOptions = () => {
value: cluster.id, value: cluster.id,
parent: true, parent: true,
children: workerList children: workerList
.filter((worker) => worker.cluster_id === cluster.id) .filter(
(worker) =>
worker.cluster_id === cluster.id &&
worker.state === WorkerStatusMap.ready
)
.map((worker) => ({ .map((worker) => ({
disabled: WorkerStatusMap.ready !== worker.state, disabled: WorkerStatusMap.ready !== worker.state,
state: worker.state, state: worker.state,
@@ -166,6 +170,7 @@ export const useGenerateWorkerOptions = () => {
generateCascaderWorkerOptions(workerList, clusterList); generateCascaderWorkerOptions(workerList, clusterList);
setWorkersList( setWorkersList(
workerList.map((item) => ({ workerList.map((item) => ({
cluster_id: item.cluster_id,
state: item.state, state: item.state,
label: item.name, label: item.name,
value: item.id value: item.id
@@ -128,6 +128,9 @@ const ModelFiles = () => {
?.labels?.['worker-name']; ?.labels?.['worker-name'];
return { return {
cluster_id: workersList?.find(
(worker) => worker.value === record.worker_id
)?.cluster_id,
source: modelSourceMap.local_path_value, source: modelSourceMap.local_path_value,
local_path: record.resolved_paths?.[0], local_path: record.resolved_paths?.[0],
worker_selector: targetWorker worker_selector: targetWorker
+8 -5
View File
@@ -1,4 +1,5 @@
import { StatusMaps } from '@/config'; import { StatusMaps } from '@/config';
import { ProviderValueMap } from '@/pages/cluster-management/config';
export const WorkerStatusMap = { export const WorkerStatusMap = {
ready: 'ready', ready: 'ready',
@@ -57,16 +58,18 @@ export const addWorkerGuide: Record<string, any> = {
image: string; image: string;
workerip: string; workerip: string;
}) { }) {
return `docker run -d --name gpustack \\ return `docker run -d --name gpustack-worker \\
--restart=unless-stopped \\ --restart=unless-stopped \\
--gpus all \\ --privileged \\
--network=host \\ --net=host \\
--ipc=host \\
-v gpustack-data:/var/lib/gpustack \\ -v gpustack-data:/var/lib/gpustack \\
${params.image} \\
--server-url ${params.server} \\ --server-url ${params.server} \\
--registration-token ${params.token} \\ --registration-token ${params.token} \\
--worker-ip ${params.workerip}`; --worker-ip ${params.workerip}`;
},
checkEnvCommand: {
[ProviderValueMap.Docker]: `nvidia-smi >/dev/null 2>&1 && echo "NVIDIA driver OK" || (echo "NVIDIA driver issue"; exit 1) && docker info 2>/dev/null | grep -q "Default Runtime: nvidia" && echo "NVIDIA Container Toolkit OK" || (echo "NVIDIA Container Toolkit not configured"; exit 1)`,
[ProviderValueMap.Kubernetes]: 'k8s env check command'
} }
}, },
npu: { npu: {