From f542ebecb45a1f095593c39a3f2acf066f1c70fe Mon Sep 17 00:00:00 2001 From: jialin Date: Fri, 19 Sep 2025 19:59:24 +0800 Subject: [PATCH] fix: cluster value setting, volumes setting --- src/components/collapse-container/index.tsx | 3 +- .../dynamic-form/components/form-widget.tsx | 7 +- .../dynamic-form/components/list-map.tsx | 29 +- src/components/dynamic-form/config/types.ts | 4 + .../dynamic-form/hooks/use-validate-fields.ts | 61 +++ src/components/seal-form/base/select.tsx | 68 +-- src/components/seal-form/input-number.tsx | 2 + src/components/seal-form/seal-input.tsx | 2 + src/components/seal-form/seal-select.tsx | 3 + src/locales/en-US/clusters.ts | 4 +- src/locales/en-US/menu.ts | 2 +- src/locales/ja-JP/clusters.ts | 7 +- src/locales/ja-JP/menu.ts | 4 +- src/locales/ru-RU/clusters.ts | 7 +- src/locales/ru-RU/menu.ts | 4 +- src/locales/zh-CN/clusters.ts | 3 +- src/locales/zh-CN/menu.ts | 2 +- .../components/add-worker-step.tsx | 57 +-- .../components/check-env-command.tsx | 30 ++ .../components/cluster-item.tsx | 390 ++++++++++++++++++ .../components/pool-form.tsx | 11 - .../components/volumes-config.tsx | 49 ++- .../config/cloud-options-config.ts | 1 + .../llmodels/components/deploy-modal.tsx | 11 +- .../llmodels/hooks/use-form-initial-values.ts | 9 +- .../resources/components/model-files.tsx | 3 + src/pages/resources/config/index.ts | 13 +- 27 files changed, 677 insertions(+), 109 deletions(-) create mode 100644 src/components/dynamic-form/hooks/use-validate-fields.ts create mode 100644 src/pages/cluster-management/components/check-env-command.tsx create mode 100644 src/pages/cluster-management/components/cluster-item.tsx diff --git a/src/components/collapse-container/index.tsx b/src/components/collapse-container/index.tsx index d9ae15b6..cb6827cd 100644 --- a/src/components/collapse-container/index.tsx +++ b/src/components/collapse-container/index.tsx @@ -172,9 +172,8 @@ export default function CollapsibleContainer({
{children}
diff --git a/src/components/dynamic-form/components/form-widget.tsx b/src/components/dynamic-form/components/form-widget.tsx index f9531899..d741dfa2 100644 --- a/src/components/dynamic-form/components/form-widget.tsx +++ b/src/components/dynamic-form/components/form-widget.tsx @@ -4,6 +4,7 @@ import { FormWidgetProps } from '../config/types'; const FormWidget: React.FC< FormWidgetProps & { onChange?: (data: any) => void; + disabled?: boolean; } > = ({ widget, @@ -17,8 +18,10 @@ const FormWidget: React.FC< value, min, max, + status, checked, - readOnly: disabled, + isInFormItems, + disabled, onChange }) => { const Component = ComponentsMap[widget]; @@ -38,6 +41,8 @@ const FormWidget: React.FC< min, max }} + status={status} + isInFormItems={isInFormItems} disabled={disabled} options={options || optionList} value={value} diff --git a/src/components/dynamic-form/components/list-map.tsx b/src/components/dynamic-form/components/list-map.tsx index 888f2881..93785bd5 100644 --- a/src/components/dynamic-form/components/list-map.tsx +++ b/src/components/dynamic-form/components/list-map.tsx @@ -3,6 +3,7 @@ import { MinusOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import React, { useEffect, useMemo } from 'react'; import styled from 'styled-components'; +import { statusType } from '../config/types'; import FormWidget from './form-widget'; const RowWrapper = styled.div` @@ -23,8 +24,12 @@ interface ListMapProps { dataList: any[]; label?: React.ReactNode; btnText?: string; + requiredFields?: string[]; + validateStatusList?: Record[]; properties: Record; disabled?: boolean; + onAdd?: (data: any[]) => void; + onDelete?: (deletedItem: any, data: any[]) => void; onChange?: (data: any) => void; } @@ -32,6 +37,7 @@ interface ListItemProps { schemaList: any[]; data: Record; disabled?: boolean; + validateStatus?: Record; onChange?: (data: any) => void; } @@ -39,6 +45,7 @@ const ListItem: React.FC = ({ schemaList, data, onChange, + validateStatus, disabled }) => { const handleValueChange = (name: string, target: any) => { @@ -50,17 +57,18 @@ const ListItem: React.FC = ({ onChange?.({ [name]: value }); } }; - return ( <> {schemaList.map((schema: any) => ( handleValueChange(schema.name, target)} /> ))} @@ -73,8 +81,12 @@ const ListMap: React.FC = ({ label, btnText, properties = {}, + requiredFields = [], minItems = 0, + validateStatusList = [], disabled, + onAdd, + onDelete, onChange }) => { const [items, setItems] = React.useState(dataList || []); @@ -82,23 +94,27 @@ const ListMap: React.FC = ({ const schemaList = useMemo(() => { const list = Object.entries(properties).map(([key, value]) => ({ ...value, + required: requiredFields.includes(key), name: key })); return list; - }, [properties]); + }, [properties, requiredFields]); const handleOnAdd = () => { const keys = Object.keys(properties); - setItems([ + const newItems = [ ...items, { ...keys.reduce((acc, key) => ({ ...acc, [key]: '' }), {}) } - ]); + ]; + setItems(newItems); + onAdd?.(newItems); }; const handleDelete = (index: number) => { + const deleteItem = items[index]; const newItems = items.filter((_, i) => i !== index); setItems(newItems); - onChange?.(newItems); + onDelete?.(deleteItem, newItems); }; const handleItemChange = (index: number, data: { [key: string]: any }) => { @@ -131,6 +147,7 @@ const ListMap: React.FC = ({ handleItemChange(index, value)} /> diff --git a/src/components/dynamic-form/config/types.ts b/src/components/dynamic-form/config/types.ts index f1424ef4..3822cd30 100644 --- a/src/components/dynamic-form/config/types.ts +++ b/src/components/dynamic-form/config/types.ts @@ -15,9 +15,13 @@ export interface FieldSchema { widget?: string; min?: number; style?: React.CSSProperties; + required?: string[]; } +export type statusType = 'error' | 'warning' | '' | undefined; export interface FormWidgetProps { + status?: statusType; + isInFormItems?: boolean; widget: 'Input' | 'Select' | 'Checkbox' | 'InputNumber'; name: string; title?: string; diff --git a/src/components/dynamic-form/hooks/use-validate-fields.ts b/src/components/dynamic-form/hooks/use-validate-fields.ts new file mode 100644 index 00000000..e942f400 --- /dev/null +++ b/src/components/dynamic-form/hooks/use-validate-fields.ts @@ -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(); + 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 + }; +} diff --git a/src/components/seal-form/base/select.tsx b/src/components/seal-form/base/select.tsx index 2a42e980..7a78b6c5 100644 --- a/src/components/seal-form/base/select.tsx +++ b/src/components/seal-form/base/select.tsx @@ -3,41 +3,43 @@ import type { SelectProps } from 'antd'; import { Select } from 'antd'; import React, { forwardRef, useImperativeHandle } from 'react'; -const BaseSelect: React.FC = forwardRef((props, ref) => { - const [isFocus, setIsFocus] = React.useState(false); - const inputRef = React.useRef(null); +const BaseSelect: React.FC = forwardRef( + (props, ref) => { + const [isFocus, setIsFocus] = React.useState(false); + const inputRef = React.useRef(null); - useImperativeHandle(ref, () => ({ - ...(inputRef.current || ({} as any)) - })); + useImperativeHandle(ref, () => ({ + ...(inputRef.current || ({} as any)) + })); - const handleFocus = (e: React.FocusEvent) => { - setIsFocus(true); - props.onFocus?.(e); - }; - const handleBlur = (e: React.FocusEvent) => { - setIsFocus(false); - props.onBlur?.(e); - }; - const renderSuffixIcon = () => { - if (props.suffixIcon) { - return props.suffixIcon; - } - if (!props.showSearch) { - return ; - } - return !isFocus ? : undefined; - }; + const handleFocus = (e: React.FocusEvent) => { + setIsFocus(true); + props.onFocus?.(e); + }; + const handleBlur = (e: React.FocusEvent) => { + setIsFocus(false); + props.onBlur?.(e); + }; + const renderSuffixIcon = () => { + if (props.suffixIcon) { + return props.suffixIcon; + } + if (!props.showSearch) { + return ; + } + return !isFocus ? : undefined; + }; - return ( - + ); + } +); export default BaseSelect; diff --git a/src/components/seal-form/input-number.tsx b/src/components/seal-form/input-number.tsx index b7baae20..3aadd8f5 100644 --- a/src/components/seal-form/input-number.tsx +++ b/src/components/seal-form/input-number.tsx @@ -23,6 +23,8 @@ const SealInputNumber: React.FC = ( if (isInFormItems) { const statusData = Form?.Item?.useStatus?.(); status = statusData?.status || ''; + } else { + status = props.status || ''; } useEffect(() => { diff --git a/src/components/seal-form/seal-input.tsx b/src/components/seal-form/seal-input.tsx index 485d8309..9155fd17 100644 --- a/src/components/seal-form/seal-input.tsx +++ b/src/components/seal-form/seal-input.tsx @@ -31,6 +31,8 @@ const SealInput: React.FC = (props) => { if (isInFormItems) { const statusData = Form?.Item?.useStatus?.(); status = statusData?.status || ''; + } else { + status = props.status || ''; } useEffect(() => { diff --git a/src/components/seal-form/seal-select.tsx b/src/components/seal-form/seal-select.tsx index e8d56b95..51137b92 100644 --- a/src/components/seal-form/seal-select.tsx +++ b/src/components/seal-form/seal-select.tsx @@ -25,12 +25,15 @@ const SealSelect: React.FC = (props) => { const intl = useIntl(); const [isFocus, setIsFocus] = useState(false); const inputRef = useRef(null); + let status = ''; // the status can be controlled by Form.Item if (isInFormItems) { const statusData = Form?.Item?.useStatus?.(); status = statusData?.status || ''; + } else { + status = props.status || ''; } const _options = useMemo(() => { diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index 9407b93c..a143d9fe 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -41,5 +41,7 @@ export default { 'clusters.create.addworker.tips': ' Please make sure the prerequisites for {label} are met before executing the following command.', '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' }; diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index 35410b0d..1e97abff 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -29,7 +29,7 @@ export default { 'menu.404': '404', 'menu.clusterManagement': 'Cluster Management', 'menu.clusterManagement.clusters': 'Clusters', - 'menu.clusterManagement.credentials': 'Credentials', + 'menu.clusterManagement.credentials': 'Cloud Credentials', 'menu.clusterManagement.clusterDetail': 'Cluster Detail', 'menu.clusterManagement.clusterCreate': 'Create Cluster' }; diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index 9b447279..5645f2d1 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -41,7 +41,9 @@ export default { 'clusters.create.addworker.tips': ' Please make sure the prerequisites for {label} are met before executing the following command.', '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) ========== @@ -84,6 +86,7 @@ export default { // 37. 'clusters.create.noRegions': 'No regions available', // 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 {label} 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 ========== diff --git a/src/locales/ja-JP/menu.ts b/src/locales/ja-JP/menu.ts index 11d9b9be..7f0c362e 100644 --- a/src/locales/ja-JP/menu.ts +++ b/src/locales/ja-JP/menu.ts @@ -28,7 +28,7 @@ export default { 'menu.accessControl.users': 'Users', 'menu.clusterManagement': 'Cluster Management', 'menu.clusterManagement.clusters': 'Clusters', - 'menu.clusterManagement.credentials': 'Credentials', + 'menu.clusterManagement.credentials': 'Cloud Credentials', 'menu.models.userModels': 'My Models', 'menu.clusterManagement.clusterDetail': 'Cluster Detail', 'menu.clusterManagement.clusterCreate': 'Create Cluster' @@ -44,7 +44,7 @@ export default { // 7. 'menu.accessControl.users': 'Users', // 8. 'menu.clusterManagement': 'Cluster Management', // 9. 'menu.clusterManagement.clusters': 'Clusters', -// 10. 'menu.clusterManagement.credentials': 'Credentials', +// 10. 'menu.clusterManagement.credentials': 'Cloud Credentials', // 11. 'menu.models.userModels': 'My Models' // 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail', // 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster' diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index dbbce08a..8d853e9d 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -41,7 +41,9 @@ export default { 'clusters.create.addworker.tips': ' Please make sure the prerequisites for {label} are met before executing the following command.', '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) ========== @@ -84,6 +86,7 @@ export default { // 37. 'clusters.create.noRegions': 'No regions available', // 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 {label} 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 ========== diff --git a/src/locales/ru-RU/menu.ts b/src/locales/ru-RU/menu.ts index eb34faf7..9c77aa33 100644 --- a/src/locales/ru-RU/menu.ts +++ b/src/locales/ru-RU/menu.ts @@ -28,7 +28,7 @@ export default { 'menu.accessControl.users': 'Пользователи', 'menu.clusterManagement': 'Cluster Management', 'menu.clusterManagement.clusters': 'Clusters', - 'menu.clusterManagement.credentials': 'Credentials', + 'menu.clusterManagement.credentials': 'Cloud Credentials', 'menu.models.userModels': 'My Models', 'menu.clusterManagement.clusterDetail': 'Cluster Detail', 'menu.clusterManagement.clusterCreate': 'Create Cluster' @@ -44,7 +44,7 @@ export default { // 7. 'menu.accessControl.users': 'Users', // 8. 'menu.clusterManagement': 'Cluster Management', // 9. 'menu.clusterManagement.clusters': 'Clusters', -// 10. 'menu.clusterManagement.credentials': 'Credentials', +// 10. 'menu.clusterManagement.credentials': 'Cloud Credentials', // 11. 'menu.models.userModels': 'My Models', // 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail', // 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster' diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index 116f9d97..b32ad6bd 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -40,5 +40,6 @@ export default { 'clusters.create.addworker.tips': '在执行以下命令之前,请确保已满足 {label} 的先决条件。', 'clusters.create.addCommand.tips': - ' 在需要添加的 Worker 上运行以下命令,将其加入到集群中' + ' 在需要添加的 Worker 上运行以下命令,将其加入到集群中', + 'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当' }; diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index e2dac2b6..e08d2728 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -29,7 +29,7 @@ export default { 'menu.accessControl.users': '用户', 'menu.clusterManagement': '集群管理', 'menu.clusterManagement.clusters': '集群', - 'menu.clusterManagement.credentials': '凭证', + 'menu.clusterManagement.credentials': '云凭证', 'menu.clusterManagement.clusterDetail': '集群详情', 'menu.clusterManagement.clusterCreate': '创建集群' }; diff --git a/src/pages/cluster-management/components/add-worker-step.tsx b/src/pages/cluster-management/components/add-worker-step.tsx index 79f1c965..5e90557d 100644 --- a/src/pages/cluster-management/components/add-worker-step.tsx +++ b/src/pages/cluster-management/components/add-worker-step.tsx @@ -5,6 +5,7 @@ import React from 'react'; import styled from 'styled-components'; import { ProviderType, ProviderValueMap } from '../config'; import AddWorkerCommand from './add-worker-command'; +import CheckEnvCommand from './check-env-command'; import RegisterClusterInner from './register-cluster-inner'; import SupportedGPUs from './support-gpus'; @@ -62,8 +63,8 @@ const AddWorkerStep: React.FC = ({ registrationInfo }) => { const intl = useIntl(); - const [currentSelection, setCurrentSelection] = - React.useState('cuda'); + const [currentGPU, setCurrentGPU] = React.useState('cuda'); + React.useState('cuda'); const [workerCommand, setWorkerCommand] = React.useState>( { label: 'NVIDIA', @@ -73,7 +74,7 @@ const AddWorkerStep: React.FC = ({ const handleSelectProvider = (value: string, item: any) => { if (provider !== ProviderValueMap.Docker) return; - setCurrentSelection(value); + setCurrentGPU(value); setWorkerCommand(item); }; @@ -84,31 +85,35 @@ const AddWorkerStep: React.FC = ({ - {provider === ProviderValueMap.Docker && ( - - - } - message={ - - } - > - - )} + + + + } + message={ + + } + > + +
- {intl.formatMessage({ id: 'clusters.create.addCommand.tips' })} + 1. {intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })} +
+ +
+ 2. {intl.formatMessage({ id: 'clusters.create.addCommand.tips' })}
{provider === ProviderValueMap.Kubernetes ? ( diff --git a/src/pages/cluster-management/components/check-env-command.tsx b/src/pages/cluster-management/components/check-env-command.tsx new file mode 100644 index 00000000..18376329 --- /dev/null +++ b/src/pages/cluster-management/components/check-env-command.tsx @@ -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 = ({ + provider = '', + currentGPU +}) => { + const code = React.useMemo(() => { + const command = addWorkerGuide['cuda']; + return command.checkEnvCommand[provider || '']; + }, [provider]); + + return ( + + ); +}; + +export default AddWorkerCommand; diff --git a/src/pages/cluster-management/components/cluster-item.tsx b/src/pages/cluster-management/components/cluster-item.tsx new file mode 100644 index 00000000..b33c6c64 --- /dev/null +++ b/src/pages/cluster-management/components/cluster-item.tsx @@ -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 = (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 ( + setShow(!show)} + expandIconPosition="end" + expandIcon={({ isActive }) => ( + + )} + items={[ + { + key: '1', + label: 'More Information', + children: ( + <> +
+ + + + + + + + + + + + + + +
+ {data.provider === ProviderValueMap.DigitalOcean && ( + <> + Worker Pools + + + )} + + ) + } + ]} + > + } + > + +
+ + {data.name} + + {ProviderLabelMap[data.provider]} + + + + + + +
+ + + +
Workers
+
+ {data.ready_workers} / {data.workers} +
+
+ +
GPUs
+
{data.gpus}
+
+ +
Deployments
+
{data.models}
+
+
+
+
+ { + setAddPoolStatus({ + open: false, + action: PageAction.CREATE, + title: '', + provider: 'digitalocean' + }); + }} + onOk={() => { + setAddPoolStatus({ + open: false, + action: addPoolStatus.action, + title: '', + provider: 'digitalocean' + }); + }} + > + { + setRegisterClusterStatus({ + open: false, + registrationInfo: { + token: '', + image: '', + server_url: '', + cluster_id: 0 + } + }); + }} + > +
+ ); +}; + +export default CardItem; diff --git a/src/pages/cluster-management/components/pool-form.tsx b/src/pages/cluster-management/components/pool-form.tsx index ef5595db..d6b4b29c 100644 --- a/src/pages/cluster-management/components/pool-form.tsx +++ b/src/pages/cluster-management/components/pool-form.tsx @@ -205,8 +205,6 @@ const PoolForm: React.FC = forwardRef((props, ref) => { value: string | number; }) => { 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 iconType = _.get(vendorIconMap, vendor.toLowerCase()); return ( @@ -217,7 +215,6 @@ const PoolForm: React.FC = forwardRef((props, ref) => { ); } const selectImage = osImageList.find((item) => item.value === data.value); - console.log('imageLabelRender========::', selectImage); if (selectImage) { return ( @@ -272,14 +269,6 @@ const PoolForm: React.FC = 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, () => ({ resetFields: () => { form.resetFields(); diff --git a/src/pages/cluster-management/components/volumes-config.tsx b/src/pages/cluster-management/components/volumes-config.tsx index 3f366daa..52fe57d3 100644 --- a/src/pages/cluster-management/components/volumes-config.tsx +++ b/src/pages/cluster-management/components/volumes-config.tsx @@ -1,7 +1,9 @@ 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 { Form } from 'antd'; -import React, { forwardRef } from 'react'; +import React, { forwardRef, useState } from 'react'; import { fieldConfig } from '../config/cloud-options-config'; const volumeOptions = fieldConfig.volumes; @@ -14,21 +16,56 @@ const CloudOptions: React.FC<{ const intl = useIntl(); const form = Form.useFormInstance(); const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form); + const [validateStatusList, setValidateStatusList] = useState< + { [key: string]: statusType }[] + >([]); - const handleOnChange = (name: string, value: any) => { - console.log('handleOnChange========', name, value); - form.setFieldValue(['cloud_options', name], value); + const updateValidateStatusList = (list: { [key: string]: statusType }[]) => { + setValidateStatusList(list); }; + 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 ( - + handleOnChange(volumeOptions.name, value)} + onAdd={handleOnAdd} + onDelete={handleOnDelete} + onChange={handleOnChange} /> ); diff --git a/src/pages/cluster-management/config/cloud-options-config.ts b/src/pages/cluster-management/config/cloud-options-config.ts index 471a33fa..608f17b9 100644 --- a/src/pages/cluster-management/config/cloud-options-config.ts +++ b/src/pages/cluster-management/config/cloud-options-config.ts @@ -21,6 +21,7 @@ export const fieldConfig: Record = { type: 'array', title: 'Volumes', name: 'volumes', + required: ['name', 'size_gb', 'format'], properties: { name: { name: 'name', diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index 3d8ebca8..3c406573 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -351,6 +351,9 @@ const AddModal: FC = (props) => { }, [onCancel]); const initClusterId = () => { + if (initialValues?.cluster_id) { + return initialValues.cluster_id; + } const cluster_id = clusterList?.find((item) => item.provider === ProviderValueMap.Docker) ?.value || clusterList?.[0]?.value; @@ -361,14 +364,12 @@ const AddModal: FC = (props) => { const handleOnOpen = () => { if (props.deploymentType === 'modelFiles') { form.current?.form?.setFieldsValue({ - ...props.initialValues, - cluster_id: initClusterId() + ...props.initialValues }); handleOnValuesChange?.({ changedValues: {}, allValues: { - ...props.initialValues, - cluster_id: initClusterId() + ...props.initialValues }, source: source }); @@ -422,7 +423,7 @@ const AddModal: FC = (props) => { message: [] }); }; - }, [open, clusterList]); + }, [open, clusterList, initialValues?.cluster_id]); return ( { const [workersList, setWorkersList] = useState< Global.BaseOption< number, - { state: string; labels: Record } + { state: string; labels: Record; cluster_id: number } >[] >([]); @@ -129,7 +129,11 @@ export const useGenerateWorkerOptions = () => { value: cluster.id, parent: true, children: workerList - .filter((worker) => worker.cluster_id === cluster.id) + .filter( + (worker) => + worker.cluster_id === cluster.id && + worker.state === WorkerStatusMap.ready + ) .map((worker) => ({ disabled: WorkerStatusMap.ready !== worker.state, state: worker.state, @@ -166,6 +170,7 @@ export const useGenerateWorkerOptions = () => { generateCascaderWorkerOptions(workerList, clusterList); setWorkersList( workerList.map((item) => ({ + cluster_id: item.cluster_id, state: item.state, label: item.name, value: item.id diff --git a/src/pages/resources/components/model-files.tsx b/src/pages/resources/components/model-files.tsx index c3687a95..1cc9cc5c 100644 --- a/src/pages/resources/components/model-files.tsx +++ b/src/pages/resources/components/model-files.tsx @@ -128,6 +128,9 @@ const ModelFiles = () => { ?.labels?.['worker-name']; return { + cluster_id: workersList?.find( + (worker) => worker.value === record.worker_id + )?.cluster_id, source: modelSourceMap.local_path_value, local_path: record.resolved_paths?.[0], worker_selector: targetWorker diff --git a/src/pages/resources/config/index.ts b/src/pages/resources/config/index.ts index 61886550..e5c04dec 100644 --- a/src/pages/resources/config/index.ts +++ b/src/pages/resources/config/index.ts @@ -1,4 +1,5 @@ import { StatusMaps } from '@/config'; +import { ProviderValueMap } from '@/pages/cluster-management/config'; export const WorkerStatusMap = { ready: 'ready', @@ -57,16 +58,18 @@ export const addWorkerGuide: Record = { image: string; workerip: string; }) { - return `docker run -d --name gpustack \\ + return `docker run -d --name gpustack-worker \\ --restart=unless-stopped \\ - --gpus all \\ - --network=host \\ - --ipc=host \\ + --privileged \\ + --net=host \\ -v gpustack-data:/var/lib/gpustack \\ - ${params.image} \\ --server-url ${params.server} \\ --registration-token ${params.token} \\ --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: {