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 (
-
- );
-});
+ 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: {