diff --git a/config/routes.ts b/config/routes.ts index 6950525e..3fe45971 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -168,7 +168,7 @@ export default [ }, { name: 'clusterDetail', - path: '/cluster-management/cluster/detail', + path: '/cluster-management/clusters/detail', key: 'clusterDetail', icon: 'icon-cluster2-outline', selectedIcon: 'icon-cluster2-filled', @@ -176,6 +176,16 @@ export default [ hideInMenu: true, component: './cluster-management/cluster-detail' }, + { + name: 'clusterCreate', + path: '/cluster-management/clusters/create', + key: 'clusterCreate', + icon: 'icon-cluster2-outline', + selectedIcon: 'icon-cluster2-filled', + defaultIcon: 'icon-cluster2-outline', + hideInMenu: true, + component: './cluster-management/cluster-create' + }, { name: 'credentials', path: '/cluster-management/credentials', diff --git a/src/assets/logo/Iluvatar.png b/src/assets/logo/Iluvatar.png new file mode 100644 index 00000000..9c18d5ed Binary files /dev/null and b/src/assets/logo/Iluvatar.png differ diff --git a/src/assets/logo/ascend.png b/src/assets/logo/ascend.png new file mode 100644 index 00000000..0470674d Binary files /dev/null and b/src/assets/logo/ascend.png differ diff --git a/src/assets/logo/cambricon.png b/src/assets/logo/cambricon.png new file mode 100644 index 00000000..56588e74 Binary files /dev/null and b/src/assets/logo/cambricon.png differ diff --git a/src/assets/logo/hygon.png b/src/assets/logo/hygon.png new file mode 100644 index 00000000..562f3e4e Binary files /dev/null and b/src/assets/logo/hygon.png differ diff --git a/src/assets/logo/metax.png b/src/assets/logo/metax.png new file mode 100644 index 00000000..cdc5961a Binary files /dev/null and b/src/assets/logo/metax.png differ diff --git a/src/assets/logo/moore _threads.png b/src/assets/logo/moore _threads.png new file mode 100644 index 00000000..7c8cff0a Binary files /dev/null and b/src/assets/logo/moore _threads.png differ diff --git a/src/assets/logo/nvidia.png b/src/assets/logo/nvidia.png new file mode 100644 index 00000000..51890d8f Binary files /dev/null and b/src/assets/logo/nvidia.png differ diff --git a/src/components/dynamic-form/components/list-map.tsx b/src/components/dynamic-form/components/list-map.tsx index 2961e49f..744da97d 100644 --- a/src/components/dynamic-form/components/list-map.tsx +++ b/src/components/dynamic-form/components/list-map.tsx @@ -5,20 +5,6 @@ import React, { useEffect, useMemo } from 'react'; import styled from 'styled-components'; import FormWidget from './form-widget'; -interface ListMapProps { - dataList: any[]; - label?: React.ReactNode; - btnText?: string; - properties: Record; - onChange?: (data: any) => void; -} - -interface ListItemProps { - schemaList: any[]; - data: Record; - onChange?: (data: any) => void; -} - const RowWrapper = styled.div` display: flex; align-items: center; @@ -32,6 +18,20 @@ const WidgetBox = styled.div` gap: 8px; width: 100%; `; +interface ListMapProps { + minItems?: number; + dataList: any[]; + label?: React.ReactNode; + btnText?: string; + properties: Record; + onChange?: (data: any) => void; +} + +interface ListItemProps { + schemaList: any[]; + data: Record; + onChange?: (data: any) => void; +} const ListItem: React.FC = ({ schemaList, data, onChange }) => { const handleValueChange = (name: string, target: any) => { @@ -65,6 +65,7 @@ const ListMap: React.FC = ({ label, btnText, properties = {}, + minItems = 0, onChange }) => { const [items, setItems] = React.useState(dataList || []); @@ -99,11 +100,15 @@ const ListMap: React.FC = ({ }; useEffect(() => { - if (!dataList.length) { + if (!dataList.length && minItems > 0) { handleOnAdd(); } }, []); + useEffect(() => { + setItems(dataList); + }, [dataList]); + return ( {items.map((item, index) => ( diff --git a/src/components/icon-font/icons.ts b/src/components/icon-font/icons.ts index 9ce0a092..3d3e9a18 100644 --- a/src/components/icon-font/icons.ts +++ b/src/components/icon-font/icons.ts @@ -35,7 +35,13 @@ const icons = { Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' }), Docker: React.createElement(IconFont, { type: 'icon-docker' }), DigitalOcean: React.createElement(IconFont, { type: 'icon-digitalocean' }), - DetailInfo: React.createElement(IconFont, { type: 'icon-detail-info' }) + DetailInfo: React.createElement(IconFont, { type: 'icon-detail-info' }), + HuaweiCloud: React.createElement(IconFont, { type: 'icon-huaweicloud' }), + AliCloud: React.createElement(IconFont, { type: 'icon-alicloud' }), + TencentCloud: React.createElement(IconFont, { type: 'icon-tencentcloud' }), + Nvidia: React.createElement(IconFont, { type: 'icon-nvidia' }), + Ascend: React.createElement(IconFont, { type: 'icon-ascend' }), + AMD: React.createElement(IconFont, { type: 'icon-amd' }) }; export default icons; diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index c1218d42..1ff30215 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; // import './iconfont/iconfont.js'; const IconFont = createFromIconfontCN({ - scriptUrl: '//at.alicdn.com/t/c/font_4613488_wy0l8ooa03c.js' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_l0y0uhurkz8.js' }); export default IconFont; diff --git a/src/components/status-tag/index.less b/src/components/status-tag/index.less index 27ba5924..3b20bf52 100644 --- a/src/components/status-tag/index.less +++ b/src/components/status-tag/index.less @@ -9,6 +9,7 @@ width: max-content; height: 24px; font-size: var(--font-size-small); + font-weight: 400; overflow: hidden; .txt { diff --git a/src/components/templates/card.tsx b/src/components/templates/card.tsx index 4c1311b4..3739dce4 100644 --- a/src/components/templates/card.tsx +++ b/src/components/templates/card.tsx @@ -10,20 +10,23 @@ interface CardProps { ghost?: boolean; header?: React.ReactNode; footer?: React.ReactNode; + icon?: React.ReactNode; + active?: boolean; + disabled?: boolean; onClick?: () => void; } const CardWrapper = styled.div` overflow: hidden; display: flex; - flex-direction: column; + padding: 22px; justify-content: flex-start; - align-items: flex-start; + align-items: center; border: 1px solid var(--ant-color-border); border-radius: var(--border-radius-base); cursor: default; width: 100%; - &:hover { + &.clickable:hover:not(.disabled) { background-color: var(--ant-color-fill-tertiary); transition: background-color 0.2s ease; } @@ -35,15 +38,47 @@ const CardWrapper = styled.div` &.active { background-color: var(--ant-color-fill-tertiary); } - &.clickable { + + &.clickable:not(.disabled) { cursor: pointer; } + &.disabled { + cursor: not-allowed; + opacity: 0.6; + pointer-events: none; + background-color: var(--ant-color-fill-quaternary); + } `; const CardContent = styled.div` - padding: 16px 20px; width: 100%; flex: 1; + color: var(--ant-color-text-tertiary); +`; + +const Inner = styled.div` + display: flex; + width: 100%; + height: 100%; + flex-direction: column; + justify-content: flex-start; + gap: 8px; + line-height: 1.5; +`; + +const Icon = styled.div` + display: flex; + align-items: center; + margin-right: 16px; + font-size: 46px; +`; + +const Header = styled.div` + font-weight: bold; + font-size: 16px; + display: flex; + align-items: center; + justify-content: space-between; `; const Card: React.FC = (props) => { @@ -55,6 +90,9 @@ const Card: React.FC = (props) => { ghost = false, header, footer, + icon, + active, + disabled, onClick } = props; @@ -62,14 +100,19 @@ const Card: React.FC = (props) => { - {header} - {children} - {footer} + {icon && {icon}} + + {header &&
{header}
} + {children} + {footer} +
); }; diff --git a/src/global.less b/src/global.less index f06bb503..fe9b0504 100644 --- a/src/global.less +++ b/src/global.less @@ -63,8 +63,9 @@ html { --ant-color-success: #54cc98; --ant-color-error-bg: #fff2f0; --ant-color-error: #ff4d4f; - --ant-box-shadow-secondary: 0 6px 16px 0 rgba(0, 0, 0, 8%), - 0 3px 6px -4px rgba(0, 0, 0, 12%), 0 9px 28px 8px rgba(0, 0, 0, 5%); + --ant-box-shadow-secondary: + 0 6px 16px 0 rgba(0, 0, 0, 8%), 0 3px 6px -4px rgba(0, 0, 0, 12%), + 0 9px 28px 8px rgba(0, 0, 0, 5%); --color-chart-red: rgba(255, 77, 79, 80%); --color-chart-green: rgb(84, 204, 152, 80%); --color-chart-glod: rgba(250, 173, 20, 80%); @@ -823,3 +824,11 @@ body { .ant-progress-line { font-size: var(--font-size-small); } + +.ant-pro-page-container-affix .ant-affix .ant-pro-page-container-warp { + box-shadow: none; +} + +.ant-page-header .ant-page-header-heading-extra { + margin-block: 0; +} diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index bf51a283..f5d81895 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -23,5 +23,7 @@ export default { 'clusters.workerpool.volumes': 'Volumes', 'clusters.workerpool.format': 'Format', 'clusters.workerpool.size': 'Size (GiB)', - 'clusters.workerpool.title': 'Worker Pools' + 'clusters.workerpool.title': 'Worker Pools', + 'clusters.workerpool.cloudOptions': 'Add Cloud Options', + 'clusters.workerpool.volumes.add': 'Add Volume' }; diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 806d798b..a4b920f0 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -259,5 +259,6 @@ export default { 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.', 'common.button.edit.item': 'Edit {name}', - 'common.button.terminal': 'Terminal' + 'common.button.terminal': 'Terminal', + 'common.button.addItem': 'Add Item' }; diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index 29fc6432..35410b0d 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -30,5 +30,6 @@ export default { 'menu.clusterManagement': 'Cluster Management', 'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.credentials': 'Credentials', - 'menu.clusterManagement.clusterDetail': 'Cluster Detail' + '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 2a0c3ffd..e80127de 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -23,7 +23,9 @@ export default { 'clusters.workerpool.volumes': 'Volumes', 'clusters.workerpool.format': 'Format', 'clusters.workerpool.size': 'Size (GiB)', - 'clusters.workerpool.title': 'Worker Pools' + 'clusters.workerpool.title': 'Worker Pools', + 'clusters.workerpool.cloudOptions': 'Add Cloud Options', + 'clusters.workerpool.volumes.add': 'Add Volume' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -51,5 +53,8 @@ export default { // 22. 'clusters.workerpool.format': 'Format', // 23. 'clusters.workerpool.size': 'Size (GiB)', // 24. 'clusters.credential.title': 'Credential', -// 25. 'clusters.workerpool.title': 'Worker Pools' +// 25. 'clusters.workerpool.title': 'Worker Pools', +// 26. 'clusters.workerpool.cloudOptions': 'Add Cloud Options', +// 27. 'clusters.workerpool.volumes.add': 'Add Volume' + // ========== End of To-Do List ========== diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index 2d393a1e..a147842f 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -259,7 +259,8 @@ export default { 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.', 'common.button.edit.item': 'Edit {name}', - 'common.button.terminal': 'Terminal' + 'common.button.terminal': 'Terminal', + 'common.button.addItem': 'Add Item' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -284,5 +285,6 @@ export default { // 19. 'common.login.password': 'Log in with Password', // 20. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.' // 21. 'common.button.edit.item': 'Edit {name}', -// 22. 'common.button.terminal': 'Terminal' +// 22. 'common.button.terminal': 'Terminal', +// 23. 'common.button.addItem': 'Add Item' // ========== End of To-Do List ========== diff --git a/src/locales/ja-JP/menu.ts b/src/locales/ja-JP/menu.ts index 4d60dd06..11d9b9be 100644 --- a/src/locales/ja-JP/menu.ts +++ b/src/locales/ja-JP/menu.ts @@ -30,7 +30,8 @@ export default { 'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.credentials': 'Credentials', 'menu.models.userModels': 'My Models', - 'menu.clusterManagement.clusterDetail': 'Cluster Detail' + 'menu.clusterManagement.clusterDetail': 'Cluster Detail', + 'menu.clusterManagement.clusterCreate': 'Create Cluster' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -45,5 +46,6 @@ export default { // 9. 'menu.clusterManagement.clusters': 'Clusters', // 10. 'menu.clusterManagement.credentials': 'Credentials', // 11. 'menu.models.userModels': 'My Models' -// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail' +// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail', +// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index bd31ceb8..9aaccdf8 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -23,7 +23,9 @@ export default { 'clusters.workerpool.volumes': 'Volumes', 'clusters.workerpool.format': 'Format', 'clusters.workerpool.size': 'Size (GiB)', - 'clusters.workerpool.title': 'Worker Pools' + 'clusters.workerpool.title': 'Worker Pools', + 'clusters.workerpool.cloudOptions': 'Add Cloud Options', + 'clusters.workerpool.volumes.add': 'Add Volume' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -51,5 +53,7 @@ export default { // 22. 'clusters.workerpool.volumes': 'Volumes', // 23. 'clusters.workerpool.format': 'Format', // 24. 'clusters.workerpool.size': 'Size (GiB)', -// 25. 'clusters.workerpool.title': 'Worker Pools' +// 25. 'clusters.workerpool.title': 'Worker Pools', +// 26. 'clusters.workerpool.cloudOptions': 'Add Cloud Options', +// 27. 'clusters.workerpool.volumes.add': 'Add Volume' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index c5221686..17f6c7eb 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -258,7 +258,8 @@ export default { 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.', 'common.button.edit.item': 'Edit {name}', - 'common.button.terminal': 'Terminal' + 'common.button.terminal': 'Terminal', + 'common.button.addItem': 'Add Item' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -266,5 +267,6 @@ export default { // 2. 'common.login.password': 'Log in with Password' // 3. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.' // 4. 'common.button.edit.item': 'Edit {name}', -// 5. 'common.button.terminal': 'Terminal' +// 5. 'common.button.terminal': 'Terminal', +// 6. 'common.button.addItem': 'Add Item' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/menu.ts b/src/locales/ru-RU/menu.ts index 7fa4ea38..eb34faf7 100644 --- a/src/locales/ru-RU/menu.ts +++ b/src/locales/ru-RU/menu.ts @@ -30,7 +30,8 @@ export default { 'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.credentials': 'Credentials', 'menu.models.userModels': 'My Models', - 'menu.clusterManagement.clusterDetail': 'Cluster Detail' + 'menu.clusterManagement.clusterDetail': 'Cluster Detail', + 'menu.clusterManagement.clusterCreate': 'Create Cluster' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -45,5 +46,6 @@ export default { // 9. 'menu.clusterManagement.clusters': 'Clusters', // 10. 'menu.clusterManagement.credentials': 'Credentials', // 11. 'menu.models.userModels': 'My Models', -// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail' +// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail', +// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index 761ff23e..8a7e4098 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -23,5 +23,7 @@ export default { 'clusters.workerpool.volumes': '存储卷', 'clusters.workerpool.format': '文件系统格式', 'clusters.workerpool.size': '容量(GiB)', - 'clusters.workerpool.title': '节点池' + 'clusters.workerpool.title': '节点池', + 'clusters.workerpool.cloudOptions': '添加云配置', + 'clusters.workerpool.volumes.add': '添加存储卷' }; diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index f1007109..a4b81860 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -251,5 +251,6 @@ export default { 'common.external.login': '使用 {type} 登录', 'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。', 'common.button.edit.item': '编辑 {name}', - 'common.button.terminal': '终端' + 'common.button.terminal': '终端', + 'common.button.addItem': '添加项' }; diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index cbd60852..e2dac2b6 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -30,5 +30,6 @@ export default { 'menu.clusterManagement': '集群管理', 'menu.clusterManagement.clusters': '集群', 'menu.clusterManagement.credentials': '凭证', - 'menu.clusterManagement.clusterDetail': '集群详情' + 'menu.clusterManagement.clusterDetail': '集群详情', + 'menu.clusterManagement.clusterCreate': '创建集群' }; diff --git a/src/pages/cluster-management/cluster-create.tsx b/src/pages/cluster-management/cluster-create.tsx new file mode 100644 index 00000000..4e7d9a35 --- /dev/null +++ b/src/pages/cluster-management/cluster-create.tsx @@ -0,0 +1,293 @@ +import { PageAction } from '@/config'; +import { PageActionType } from '@/config/types'; +import { PageContainer } from '@ant-design/pro-components'; +import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; +import _ from 'lodash'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import styled from 'styled-components'; +import { queryClusterList } from './apis'; +import ClusterSteps from './components/cluster-steps'; +import FooterButtons from './components/footer-buttons'; +import ProviderCatalog from './components/provider-catalog'; +import { providerList, ProviderType } from './config'; +import { ClusterFormData } from './config/types'; +import { moduleMap, moduleRegistry } from './step-forms/module-registry'; +import useStepList from './step-forms/use-step-list'; + +const Container = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +`; + +const Content = styled.div` + width: 600px; +`; + +const HeaderContainer = styled.div` + position: relative; + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + padding-inline: var(--layout-content-header-inlinepadding); + border-bottom: 1px solid var(--ant-color-split); + min-height: 72px; + .text { + margin-right: 16px; + padding-right: 16px; + border-right: 1px solid var(--ant-color-split); + font-weight: 600; + font-size: 20px; + margin-right: 32px; + } +`; + +const ClusterCreate = () => { + const intl = useIntl(); + const stepList = useStepList(); + const [searchParams] = useSearchParams(); + const action = + (searchParams.get('action') as PageActionType) || PageAction.CREATE; + const navigate = useNavigate(); + const [credentialList, setCredentialList] = useState< + Global.BaseOption[] + >([]); + const [currentStep, setCurrentStep] = useState(0); + const [registrationInfo, setRegistrationInfo] = useState<{ + token: string; + image: string; + server_url: string; + cluster_id: number; + }>({ + token: '', + image: '', + server_url: '', + cluster_id: 0 + }); + const [extraData, setExtraData] = useState({ + provider: null + } as ClusterFormData); + const [formValues, setFormValues] = useState>({}); + + const formRefs: Record = { + [moduleMap.BasicForm]: useRef(null), + [moduleMap.WorkerPoolForm]: useRef(null) + }; + + const steps = useMemo(() => { + if (!extraData.provider) { + return stepList.filter((step) => step.defaultShow); + } + + return stepList + .filter((step) => + step.providers?.length + ? step.providers.includes(extraData.provider as never) + : true + ) + .map((step, index) => ({ + ...step, + disabled: action === PageAction.CREATE + })); + }, [action, extraData.provider, stepList]); + + const handleStepChange = (newStep: number) => { + setCurrentStep(newStep); + console.log('step========', newStep); + }; + const customizer = (objValue: any, srcValue: any) => { + if (Array.isArray(objValue)) { + return srcValue; + } + }; + const getFormFieldsValue = () => { + setFormValues((prev) => { + const newFormValues = _.cloneDeep(prev); + for (const [formKey, formRef] of Object.entries(formRefs)) { + const formValues = formRef?.current?.getFieldsValue?.(); + if (formValues) { + newFormValues[formKey] = formValues; + } + } + return newFormValues; + }); + }; + + const onPrevious = () => { + getFormFieldsValue(); + setCurrentStep((prev) => Math.max(prev - 1, 0)); + }; + + const validateForms = async () => { + const step = steps[currentStep]; + const formKeys = step?.showForms || []; + if (formKeys?.length > 0) { + const results = await Promise.allSettled( + formKeys.map((key) => formRefs[key].current?.validateFields()) + ); + + console.log('results========', results); + + const resultsMap = new Map(); + results.forEach((result, index) => { + if (result.status === 'rejected') { + const values = result.reason?.values || {}; + resultsMap.set(formKeys[index], values); + } else if (result.status === 'fulfilled') { + resultsMap.set(formKeys[index], result.value); + } + }); + const newValues = _.merge(formValues, Object.fromEntries(resultsMap)); + + setFormValues(newValues); + + const isValid = results.every((result) => result.status === 'fulfilled'); + if (!isValid) { + return false; + } + return Object.assign({}, ...Object.values(newValues)); + } + return true; + }; + + const onNext = async (callback?: (values: ClusterFormData) => void) => { + try { + const result = await validateForms(); + console.log('results========2', result); + if (result) { + await callback?.(result as ClusterFormData); + const step = steps[currentStep]; + step.beforeNext?.(); + setCurrentStep((prev) => Math.min(prev + 1, steps.length - 1)); + } + } catch (error) { + console.log('next error:', error); + } + }; + + const handleSelectProvider = (value: ProviderType) => { + setExtraData({ + provider: value + } as ClusterFormData); + onNext(); + }; + + useEffect(() => { + const fetchData = async () => { + const res = await queryClusterList({ page: 1, perPage: 100 }); + const list = res.items?.map((item) => ({ + label: item.name, + value: item.id + })); + setCredentialList(list); + }; + fetchData(); + }, []); + + const renderForms = () => { + const step = steps[currentStep]; + const formKeys = step?.showForms || []; + if (formKeys.length === 0) { + return null; + } + + return formKeys.map((key) => { + console.log('renderForms key========', key, formValues); + const FormComponent = moduleRegistry[key]; + return FormComponent ? ( + + ) : null; + }); + }; + + const renderModules = () => { + const step = steps[currentStep]; + const moduleKeys = step?.showModules || []; + if (moduleKeys.length === 0) { + return null; + } + return moduleKeys.map((key) => { + const ModuleComponent = moduleRegistry[key]; + return ModuleComponent ? ( + + ) : null; + }); + }; + + const showButtons = useMemo(() => { + const step = steps[currentStep]; + return step?.showButtons ? step?.showButtons(extraData.provider) : {}; + }, [currentStep, steps, extraData.provider]); + + const submit = async (values: ClusterFormData) => { + const data = { + ...extraData, + ...(typeof values === 'object' ? values : {}) + }; + console.log('submit values====:', data); + return true; + }; + + const handleSubmit = async () => { + onNext(submit); + }; + + const handleCancel = () => { + navigate(-1); + }; + + return ( + + ]} + pageHeaderRender={() => ( + + + Create Cluster + + + + )} + > + {currentStep === 0 && ( + + )} + {renderModules()} + + {renderForms()} + + + ); +}; + +export default ClusterCreate; diff --git a/src/pages/cluster-management/cluster-detail.tsx b/src/pages/cluster-management/cluster-detail.tsx index 3d80d860..b1d3ce36 100644 --- a/src/pages/cluster-management/cluster-detail.tsx +++ b/src/pages/cluster-management/cluster-detail.tsx @@ -1,8 +1,6 @@ import { PageContainer } from '@ant-design/pro-components'; import { useSearchParams } from '@umijs/max'; import ClusterMetrics from './components/cluster-metrics'; -import WorkerPools from './components/worker-pools'; -import { ProviderValueMap } from './config'; const ClusterDetailModal = () => { const [searchParams] = useSearchParams(); @@ -25,7 +23,7 @@ const ClusterDetailModal = () => { extra={[]} > - {provider === ProviderValueMap.DigitalOcean && } + {/* {provider === ProviderValueMap.DigitalOcean && } */} ); }; diff --git a/src/pages/cluster-management/clusters.tsx b/src/pages/cluster-management/clusters.tsx index 706dd3a2..b50ff9ef 100644 --- a/src/pages/cluster-management/clusters.tsx +++ b/src/pages/cluster-management/clusters.tsx @@ -21,7 +21,7 @@ import { import AddCluster from './components/add-cluster'; import AddPool from './components/add-pool'; import RegisterCluster from './components/register-cluster'; -import { ProviderLabelMap, ProviderValueMap, addActions } from './config'; +import { ProviderLabelMap, ProviderValueMap } from './config'; import { ClusterFormData as FormData, ClusterListItem as ListItem, @@ -146,7 +146,8 @@ const Credentials: React.FC = () => { }; const handleClickDropdown = (item: any) => { - handleAddCluster(item.key); + navigate(`/cluster-management/clusters/create?action=${PageAction.CREATE}`); + // handleAddCluster(item.key); }; const handleModalOk = async (data: FormData) => { @@ -298,8 +299,6 @@ const Credentials: React.FC = () => { marginTop={30} width={{ input: 300 }} buttonText={intl.formatMessage({ id: 'clusters.button.add' })} - actionType="dropdown" - actionItems={addActions} rowSelection={rowSelection} handleInputChange={handleNameChange} handleSearch={handleSearch} diff --git a/src/pages/cluster-management/components/add-cluster.tsx b/src/pages/cluster-management/components/add-cluster.tsx index 183d0de2..29b3e59d 100644 --- a/src/pages/cluster-management/components/add-cluster.tsx +++ b/src/pages/cluster-management/components/add-cluster.tsx @@ -1,19 +1,12 @@ import ModalFooter from '@/components/modal-footer'; import ScrollerModal from '@/components/scroller-modal/index'; -import SealInput from '@/components/seal-form/seal-input'; import { PageActionType } from '@/config/types'; -import ContainerInstall from '@/pages/resources/components/container-install'; -import { CheckCircleFilled } from '@ant-design/icons'; -import { useIntl } from '@umijs/max'; -import { Form } from 'antd'; -import React, { useEffect, useMemo } from 'react'; -import { ProviderValueMap } from '../config'; +import React, { useRef } from 'react'; import { ClusterFormData as FormData, ClusterListItem as ListItem } from '../config/types'; -import CloudProvider from './cloud-provider-form'; -import RegisterClusterInner from './register-cluster-inner'; +import ClusterForm from './cluster-form'; type AddModalProps = { title: string; @@ -35,15 +28,10 @@ const AddCluster: React.FC = ({ onOk, onCancel }) => { - const [form] = Form.useForm(); - const intl = useIntl(); - const [submissionStatus, setSubmissionStatus] = React.useState<{ - success: boolean; - data: ListItem; - }>({ success: false, data: {} as ListItem }); + const form = useRef(null); const handleSubmit = () => { - form.submit(); + form.current?.submit(); }; const handleOk = async (data: FormData) => { @@ -54,63 +42,13 @@ const AddCluster: React.FC = ({ }; const handleCancel = () => { - form.resetFields(); + form.current?.resetFields(); onCancel(); }; - const renderAddWorkerContent = () => { - if (provider === ProviderValueMap.Custom) { - return ; - } - if (provider === ProviderValueMap.Kubernetes) { - return ; - } - return null; - }; - - const modalTitle = useMemo(() => { - if (submissionStatus.success) { - return ( -
- - Cluster added! Now you can register a worker. -
- ); - } - return title; - }, [submissionStatus.success, title]); - - useEffect(() => { - if (currentData) { - form.setFieldsValue(currentData); - } - }, [currentData]); - - const renderFooter = () => { - if (submissionStatus.success) { - return ( - - ); - } - return ; - }; - return ( = ({ maskClosable={false} keyboard={false} width={680} - footer={renderFooter()} + footer={ + + } > - {submissionStatus.success ? ( - renderAddWorkerContent() - ) : ( -
- - name="name" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, - { - name: intl.formatMessage({ id: 'common.table.name' }) - } - ) - } - ]} - > - - - {provider === ProviderValueMap.DigitalOcean && ( - - )} - name="description" rules={[{ required: false }]}> - - - - )} +
); }; diff --git a/src/pages/cluster-management/components/add-worker-step.tsx b/src/pages/cluster-management/components/add-worker-step.tsx new file mode 100644 index 00000000..f2273306 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker-step.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import styled from 'styled-components'; +import RegisterClusterInner from './register-cluster-inner'; +import SupportedHardware from './support-hardware'; + +const Title = styled.div` + font-size: 16px; + font-weight: 600; + margin-bottom: 24px; +`; + +type AddModalProps = { + registrationInfo: { + token: string; + image: string; + server_url: string; + cluster_id: number; + }; +}; +const AddWorkerStep: React.FC = ({ registrationInfo }) => { + return ( +
+ Supported Hardware Platforms + + Execute Command + +
+ ); +}; + +export default AddWorkerStep; diff --git a/src/pages/cluster-management/components/cloud-options.tsx b/src/pages/cluster-management/components/cloud-options.tsx index 4fe4fa26..92bfab70 100644 --- a/src/pages/cluster-management/components/cloud-options.tsx +++ b/src/pages/cluster-management/components/cloud-options.tsx @@ -2,6 +2,7 @@ import DropDownActions from '@/components/drop-down-actions'; import ListMap from '@/components/dynamic-form/components/list-map'; import { FieldSchema } from '@/components/dynamic-form/config/types'; import { PlusOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { Button, Form } from 'antd'; import _ from 'lodash'; @@ -36,7 +37,7 @@ const ButtonWrapper = styled.span` const CloudOptions: React.FC<{ ref?: any; }> = forwardRef((props, ref) => { - // form instance + const intl = useIntl(); const form = Form.useFormInstance(); const [selectedOptions, setSelectedOptions] = React.useState>( new Set() @@ -102,7 +103,9 @@ const CloudOptions: React.FC<{ @@ -113,7 +116,7 @@ const CloudOptions: React.FC<{ name={['cloud_options', field.name as string]} > []; + onFinish: (values: FormData) => void; + ref?: any; +}; +const ClusterForm: React.FC = forwardRef( + ({ action, provider, currentData, credentialList, onFinish }, ref) => { + const [form] = Form.useForm(); + const intl = useIntl(); + + useEffect(() => { + if (currentData) { + form.setFieldsValue(currentData); + } + }, [currentData]); + + useImperativeHandle(ref, () => ({ + resetFields: () => { + form.resetFields(); + }, + submit: () => { + form.submit(); + }, + setFieldsValue: (values: any) => { + form.setFieldsValue(values); + }, + getFieldsValue: () => { + return form.getFieldsValue(); + }, + validateFields: async () => { + return await form.validateFields(); + } + })); + + return ( +
+ + name="name" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: intl.formatMessage({ id: 'common.table.name' }) + } + ) + } + ]} + > + + + {provider === ProviderValueMap.DigitalOcean && ( + + )} + name="description" rules={[{ required: false }]}> + + + + ); + } +); + +export default ClusterForm; diff --git a/src/pages/cluster-management/components/cluster-steps.tsx b/src/pages/cluster-management/components/cluster-steps.tsx new file mode 100644 index 00000000..6b0ac765 --- /dev/null +++ b/src/pages/cluster-management/components/cluster-steps.tsx @@ -0,0 +1,42 @@ +import { Steps } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; + +const { Step } = Steps; + +const Wrapper = styled.div` + padding-block: 20px; + background-color: var(--ant-color-bg-container); + .ant-steps-item-description { + max-width: 300px !important; + color: var(--ant-color-text-description) !important; + } + .ant-steps-item-content > .ant-steps-item-title { + font-weight: 600; + } +`; + +const ClusterSteps: React.FC<{ + currentStep: number; + onChange: (step: number) => void; + steps: any[]; +}> = (props) => { + const { steps, currentStep, onChange } = props; + + return ( + + + {steps.map((step) => ( + + ))} + + + ); +}; + +export default ClusterSteps; diff --git a/src/pages/cluster-management/components/footer-buttons.tsx b/src/pages/cluster-management/components/footer-buttons.tsx new file mode 100644 index 00000000..814a1b0d --- /dev/null +++ b/src/pages/cluster-management/components/footer-buttons.tsx @@ -0,0 +1,63 @@ +import { LeftOutlined, RightOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button } from 'antd'; +import styled from 'styled-components'; + +const Title = styled.span` + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 700; + font-size: 16px; + .text { + font-size: 20px; + } +`; +interface FooterButtonsProps { + onPrevious: () => void; + onNext: () => void; + handleCancel: () => void; + handleSubmit: () => void; + showButtons: Record; +} + +const FooterButtons: React.FC = (props) => { + const { onPrevious, onNext, handleCancel, handleSubmit, showButtons } = props; + const intl = useIntl(); + const handleOnNext = () => { + onNext(); + }; + return ( + + <div className="flex-center gap-16"> + {showButtons.previous && ( + <Button type="link" icon={<LeftOutlined />} onClick={onPrevious}> + Previous + </Button> + )} + {showButtons.next && ( + <Button type="link" onClick={handleOnNext}> + Next + <RightOutlined /> + </Button> + )} + {showButtons.skip && ( + <Button type="primary" onClick={handleCancel}> + Skip for now + </Button> + )} + {showButtons.save && ( + <Button + type="primary" + onClick={handleSubmit} + style={{ minWidth: 88 }} + > + {intl.formatMessage({ id: 'common.button.save' })} + </Button> + )} + </div> + + ); +}; + +export default FooterButtons; diff --git a/src/pages/cluster-management/components/pool-form.tsx b/src/pages/cluster-management/components/pool-form.tsx index 948d61ce..d0d04f70 100644 --- a/src/pages/cluster-management/components/pool-form.tsx +++ b/src/pages/cluster-management/components/pool-form.tsx @@ -13,21 +13,20 @@ import React, { useImperativeHandle, useRef } from 'react'; -import { - NodePoolFormData as FormData, - NodePoolListItem as ListItem -} from '../config/types'; -import CloudOptions from './cloud-options'; +import { ProviderType } from '../config'; +import { NodePoolFormData as FormData } from '../config/types'; +import VolumesConfig from './volumes-config'; type AddModalProps = { ref: any; + name?: string; action: PageActionType; - provider: string; // 'kubernetes' | 'custom' | 'digitalocean'; - currentData?: ListItem | null; + provider: ProviderType; // 'kubernetes' | 'custom' | 'digitalocean'; + currentData?: FormData | null; onFinish: (values: FormData) => void; }; const PoolForm: React.FC = forwardRef( - ({ action, onFinish, currentData }, ref) => { + ({ action, name = 'workerPoolForm', onFinish, currentData }, ref) => { const cloudOptionsRef = useRef(null); const [form] = Form.useForm(); const intl = useIntl(); @@ -35,21 +34,26 @@ const PoolForm: React.FC = forwardRef( useEffect(() => { if (currentData) { + console.log('currentData===========1=', currentData); form.setFieldsValue({ - ...currentData, - volumes: currentData.cloud_options?.volumes?.[0] || {} + ...currentData }); - cloudOptionsRef.current?.initFieldList(); } }, [currentData]); useImperativeHandle(ref, () => ({ - reset: () => { + resetFields: () => { form.resetFields(); }, submit: () => { form.submit(); }, + setFieldsValue: (values: any) => { + form.setFieldsValue(values); + }, + getFieldsValue: () => { + return form.getFieldsValue(); + }, validateFields: async () => { return await form.validateFields(); } @@ -57,9 +61,11 @@ const PoolForm: React.FC = forwardRef( return (
= forwardRef( btnText={intl.formatMessage({ id: 'common.button.addLabel' })} > - +
); } diff --git a/src/pages/cluster-management/components/provider-catalog.tsx b/src/pages/cluster-management/components/provider-catalog.tsx new file mode 100644 index 00000000..7635dc9d --- /dev/null +++ b/src/pages/cluster-management/components/provider-catalog.tsx @@ -0,0 +1,57 @@ +import Card from '@/components/templates/card'; +import { useIntl } from '@umijs/max'; +import React from 'react'; +import styled from 'styled-components'; +import { ProviderType } from '../config'; + +const Wrapper = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 22px; +`; +interface ProviderCatalogProps { + onSelect?: (provider: ProviderType) => void; + currentProvider?: ProviderType; + clickable?: boolean; + dataList: { + label: string; + key: string; + locale?: boolean; + disabled?: boolean; + icon: React.ReactNode; + description?: string; + }[]; +} + +const ProviderCatalog: React.FC = ({ + onSelect, + dataList, + clickable, + currentProvider +}) => { + const intl = useIntl(); + return ( + + {dataList?.map((action) => ( + onSelect?.(action.key as ProviderType)} + active={currentProvider === action.key} + disabled={action.disabled} + clickable={clickable} + header={ + action.locale + ? intl.formatMessage({ id: action.label }) + : action.label + } + icon={action.icon} + > + {action.description || 'This is a description'} + + ))} + + ); +}; + +export default ProviderCatalog; diff --git a/src/pages/cluster-management/components/support-hardware.tsx b/src/pages/cluster-management/components/support-hardware.tsx new file mode 100644 index 00000000..e3ae70a9 --- /dev/null +++ b/src/pages/cluster-management/components/support-hardware.tsx @@ -0,0 +1,94 @@ +import ascendLogo from '@/assets/logo/ascend.png'; +import CambriconPNG from '@/assets/logo/cambricon.png'; +import hyponPNG from '@/assets/logo/hygon.png'; +import iluvatarWEBP from '@/assets/logo/Iluvatar.png'; +import metaxLogo from '@/assets/logo/metax.png'; +import moorePNG from '@/assets/logo/moore _threads.png'; +import nvidiaLogo from '@/assets/logo/nvidia.png'; +import IconFont from '@/components/icon-font'; +import ProviderCatalog from './provider-catalog'; + +const ProviderImage = ({ src, showBg }: { src: string; showBg?: boolean }) => { + return ( + + ); +}; + +const supportedHardPlatforms = [ + { + label: 'NVIDIA CUDA', + value: 'cuda', + key: 'nvidia', + locale: false, + icon: + }, + { + label: 'AMD ROCm', + value: 'rocm', + key: 'amd', + locale: false, + icon: ( + + ) + }, + { + label: 'Ascend CANN', + value: 'npu', + key: 'ascend', + locale: false, + icon: + }, + { + label: 'Hygon DTK', + value: 'dcu', + key: 'hygon', + locale: false, + icon: + }, + { + label: 'Moore Threads MUSA', + value: 'musa', + key: 'musa', + locale: false, + icon: + }, + { + label: 'Iluvatar Corex', + value: 'corex', + key: 'corex', + locale: false, + icon: + }, + { + label: 'Cambricon', + value: 'cambricon', + key: 'cambricon', + locale: false, + icon: + }, + { + label: 'Metax', + value: 'metax', + key: 'metax', + locale: false, + icon: + } +]; + +const SupportedHardware = () => { + return ( + + ); +}; + +export default SupportedHardware; diff --git a/src/pages/cluster-management/components/volumes-config.tsx b/src/pages/cluster-management/components/volumes-config.tsx new file mode 100644 index 00000000..6c2238cb --- /dev/null +++ b/src/pages/cluster-management/components/volumes-config.tsx @@ -0,0 +1,34 @@ +import ListMap from '@/components/dynamic-form/components/list-map'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import React, { forwardRef } from 'react'; +import { fieldConfig } from '../config/cloud-options-config'; + +const volumeOptions = fieldConfig.volumes; + +const CloudOptions: React.FC<{ + ref?: any; +}> = forwardRef((props, ref) => { + const intl = useIntl(); + const form = Form.useFormInstance(); + const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form); + + const handleOnChange = (name: string, value: any) => { + console.log('handleOnChange========', name, value); + form.setFieldValue(['cloud_options', name], value); + }; + + return ( + + handleOnChange(volumeOptions.name, value)} + /> + + ); +}); + +export default CloudOptions; diff --git a/src/pages/cluster-management/config/cloud-options-config.ts b/src/pages/cluster-management/config/cloud-options-config.ts index 4e47c340..471a33fa 100644 --- a/src/pages/cluster-management/config/cloud-options-config.ts +++ b/src/pages/cluster-management/config/cloud-options-config.ts @@ -41,7 +41,7 @@ export const fieldConfig: Record = { type: 'string', title: 'Format', widget: 'Select', - enum: ['ext4', 'xfs', 'btrfs'], + enum: ['ext4', 'xfs'], style: { width: 150 } } } diff --git a/src/pages/cluster-management/config/index.ts b/src/pages/cluster-management/config/index.ts index 71d09e9e..2a907e69 100644 --- a/src/pages/cluster-management/config/index.ts +++ b/src/pages/cluster-management/config/index.ts @@ -1,6 +1,7 @@ import icons from '@/components/icon-font/icons'; import { StatusMaps } from '@/config'; import { StatusType } from '@/config/types'; +import React from 'react'; export const ClusterStatusValueMap = { Provisioning: 0, @@ -23,12 +24,17 @@ export const ClusterStatus: Record = { export const ProviderValueMap = { Kubernetes: 'Kubernetes', DigitalOcean: 'DigitalOcean', - Custom: 'Custom' + Custom: 'Custom', + HuaweiCloud: 'HuaweiCloud', + AliCloud: 'AliCloud', + TencentCloud: 'TencentCloud' }; +export type ProviderType = keyof typeof ProviderValueMap | null | undefined; + export const ProviderLabelMap = { [ProviderValueMap.Kubernetes]: 'Kubernetes', - [ProviderValueMap.DigitalOcean]: 'Digital Ocean', + [ProviderValueMap.DigitalOcean]: 'DigitalOcean', [ProviderValueMap.Custom]: 'Custom' }; @@ -41,27 +47,62 @@ export const generateRegisterCommand = (params: { --header 'Authorization: Bearer ${params.registrationToken}' | kubectl apply -f -`; }; -export const addActions = [ +export const providerList = [ { label: 'clusters.provider.custom', locale: true, value: ProviderValueMap.Custom, key: ProviderValueMap.Custom, - icon: icons.Docker + icon: React.cloneElement(icons.Docker, { + style: { color: 'var(--ant-color-primary)' } + }) }, { label: 'Kubernetes', locale: false, value: ProviderValueMap.Kubernetes, key: ProviderValueMap.Kubernetes, - icon: icons.KubernetesOutlined + icon: React.cloneElement(icons.KubernetesOutlined, { + style: { color: 'var(--ant-color-primary)' } + }) }, { - label: 'Digital Ocean', + label: 'DigitalOcean', locale: false, value: ProviderValueMap.DigitalOcean, key: ProviderValueMap.DigitalOcean, - icon: icons.DigitalOcean + icon: React.cloneElement(icons.DigitalOcean, { + style: { + color: 'var(--ant-color-primary)' + } + }) + }, + { + label: 'Huawei Cloud', + locale: false, + disabled: true, + value: ProviderValueMap.HuaweiCloud, + key: ProviderValueMap.HuaweiCloud, + icon: icons.HuaweiCloud, + description: 'Currently Not Supported' + }, + { + label: 'Ali Cloud', + locale: false, + disabled: true, + value: ProviderValueMap.AliCloud, + key: ProviderValueMap.AliCloud, + icon: icons.AliCloud, + description: 'Currently Not Supported' + }, + { + label: 'Tencent Cloud', + locale: false, + disabled: true, + value: ProviderValueMap.TencentCloud, + key: ProviderValueMap.TencentCloud, + icon: icons.TencentCloud, + description: 'Currently Not Supported' } ]; @@ -101,13 +142,13 @@ export const clusterActionList = [ locale: true, icon: icons.KubernetesOutlined }, - { - key: 'addPool', - label: 'clusters.button.addNodePool', - provider: ProviderValueMap.DigitalOcean, - locale: true, - icon: icons.Catalog - }, + // { + // key: 'addPool', + // label: 'clusters.button.addNodePool', + // provider: ProviderValueMap.DigitalOcean, + // locale: true, + // icon: icons.Catalog + // }, { key: 'delete', label: 'common.button.delete', diff --git a/src/pages/cluster-management/config/types.ts b/src/pages/cluster-management/config/types.ts index 0b5c7c0e..0d377c2b 100644 --- a/src/pages/cluster-management/config/types.ts +++ b/src/pages/cluster-management/config/types.ts @@ -1,3 +1,5 @@ +import { ProviderType } from '.'; + export interface CredentialFormData { name: string; provider: string; @@ -20,15 +22,6 @@ export interface CredentialListItem { updated_at: string; } -export interface ClusterFormData { - name: string; - description: string; - provider: string; - credential_id: number; - zone: string; - region: string; -} - export interface NodePoolListItem { id: number; instance_type: string; @@ -47,7 +40,6 @@ export interface NodePoolFormData { batch_size: number; labels: Record; cloud_options: Record; - volumes?: Record; } export interface ClusterListItem { @@ -67,3 +59,13 @@ export interface ClusterListItem { state_message: string; worker_pools: NodePoolListItem[]; } + +export interface ClusterFormData { + name: string; + description: string; + provider: ProviderType; + credential_id: number; + zone: string; + region: string; + worker_pools?: NodePoolFormData[]; +} diff --git a/src/pages/cluster-management/hooks/use-cluster-columns.tsx b/src/pages/cluster-management/hooks/use-cluster-columns.tsx index a66880b7..f9ded4fc 100644 --- a/src/pages/cluster-management/hooks/use-cluster-columns.tsx +++ b/src/pages/cluster-management/hooks/use-cluster-columns.tsx @@ -36,7 +36,7 @@ const useClusterColumns = ( render: (text: string, record: ClusterListItem) => ( {text} diff --git a/src/pages/cluster-management/step-forms/basic-form.tsx b/src/pages/cluster-management/step-forms/basic-form.tsx new file mode 100644 index 00000000..142b0f8d --- /dev/null +++ b/src/pages/cluster-management/step-forms/basic-form.tsx @@ -0,0 +1,58 @@ +import PageTools from '@/components/page-tools'; +import { PageActionType } from '@/config/types'; +import { forwardRef, useImperativeHandle, useRef } from 'react'; +import styled from 'styled-components'; +import ClusterForm from '../components/cluster-form'; +import { ProviderType } from '../config'; + +const Title = styled.span` + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 700; + font-size: 16px; + .text { + font-size: 20px; + } +`; +interface BasicFormProps { + provider: ProviderType; + action: PageActionType; + credentialList: Global.BaseOption[]; + currentData?: any; +} + +const BasicForm = forwardRef((props: BasicFormProps, ref) => { + const { provider, credentialList, action, currentData } = props; + const formRef = useRef(null); + + const handleOnFinish = (values: any) => { + console.log(values); + }; + + useImperativeHandle(ref, () => ({ + validateFields: formRef.current?.validateFields, + getFieldsValue: formRef.current?.getFieldsValue, + submit: formRef.current?.submit + })); + + return ( +
+ Basic Configuration} + marginTop={0} + > + +
+ ); +}); + +export default BasicForm; diff --git a/src/pages/cluster-management/step-forms/module-registry.ts b/src/pages/cluster-management/step-forms/module-registry.ts new file mode 100644 index 00000000..6279d361 --- /dev/null +++ b/src/pages/cluster-management/step-forms/module-registry.ts @@ -0,0 +1,19 @@ +import React from 'react'; +import AddWorkerStep from '../components/add-worker-step'; +import ProviderCatalog from '../components/provider-catalog'; +import BasicForm from './basic-form'; +import WorkerPoolForm from './worker-pools-form'; + +export const moduleMap = { + BasicForm: 'BasicForm', + WorkerPoolForm: 'WorkerPoolForm', + ProviderCatalog: 'ProviderCatalog', + AddWorkerStep: 'AddWorkerStep' +}; + +export const moduleRegistry: Record> = { + [moduleMap.BasicForm]: BasicForm, + [moduleMap.WorkerPoolForm]: WorkerPoolForm, + [moduleMap.ProviderCatalog]: ProviderCatalog, + [moduleMap.AddWorkerStep]: AddWorkerStep +}; diff --git a/src/pages/cluster-management/step-forms/use-step-list.tsx b/src/pages/cluster-management/step-forms/use-step-list.tsx new file mode 100644 index 00000000..10a57bc3 --- /dev/null +++ b/src/pages/cluster-management/step-forms/use-step-list.tsx @@ -0,0 +1,100 @@ +import { useNavigate } from '@umijs/max'; +import { useMemoizedFn } from 'ahooks'; +import { useMemo } from 'react'; +import { ProviderType, ProviderValueMap } from '../config'; +import { moduleMap } from './module-registry'; + +export default function useStepList() { + const navigate = useNavigate(); + + const handleBack = useMemoizedFn(() => { + navigate(-1); + }); + + return useMemo( + () => [ + { + title: 'Select Cloud Provider', + content: 'Choose the cloud provider for your cluster.', + showButtons: (provider?: ProviderType) => { + return { + previous: false, + next: true, + save: false, + skip: false + }; + }, + defaultShow: true, + showForms: [], + showModules: [moduleMap.ProviderCatalog], + providers: [] + }, + { + title: 'Configure Cluster Settings', + content: 'Set up the basic configuration for your cluster.', + showButtons: (provider?: ProviderType) => { + return { + previous: true, + next: provider === ProviderValueMap.DigitalOcean, + save: provider !== ProviderValueMap.DigitalOcean, + skip: false + }; + }, + defaultShow: true, + showForms: [moduleMap.BasicForm], + showModules: [], + providers: [] + }, + { + title: 'Add Worker Pools', + content: 'Define the worker pools for your cluster.', + showButtons: (provider?: ProviderType) => { + return { + previous: true, + next: false, + save: true, + skip: false + }; + }, + defaultShow: false, + showForms: [moduleMap.WorkerPoolForm], + showModules: [], + providers: [ProviderValueMap.DigitalOcean], + beforeNext: handleBack + }, + { + title: 'Add Worker', + content: 'Add a worker node to your cluster.', + showButtons: (provider?: ProviderType) => { + return { + previous: false, + next: false, + save: false, + skip: true + }; + }, + defaultShow: false, + showForms: [], + showModules: [moduleMap.AddWorkerStep], + providers: [ProviderValueMap.Custom] + }, + { + title: 'Register Cluster', + content: 'Register your cluster with the chosen provider.', + showButtons: (provider?: ProviderType) => { + return { + previous: false, + next: false, + save: false, + skip: true + }; + }, + defaultShow: false, + showForms: [], + showModules: [moduleMap.AddWorkerStep], + providers: [ProviderValueMap.Kubernetes] + } + ], + [handleBack] + ); +} diff --git a/src/pages/cluster-management/step-forms/worker-pools-form.tsx b/src/pages/cluster-management/step-forms/worker-pools-form.tsx new file mode 100644 index 00000000..f1808cc2 --- /dev/null +++ b/src/pages/cluster-management/step-forms/worker-pools-form.tsx @@ -0,0 +1,218 @@ +import PageTools from '@/components/page-tools'; +import { PageActionType } from '@/config/types'; +import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Divider, FormInstance } from 'antd'; +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState +} from 'react'; +import styled from 'styled-components'; +import WorkerPoolForm from '../components/pool-form'; +import { ProviderType } from '../config'; +import { NodePoolFormData } from '../config/types'; + +const PoolContainer = styled.div` + display: flex; + align-items: flex-start; + gap: 16px; +`; + +const PoolFormWrapper = styled.div` + flex: 1; +`; + +const Title = styled.span` + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 700; + font-size: 16px; + .text { + font-size: 20px; + } +`; +interface WorkerPoolsFormProps { + provider: ProviderType; + action: PageActionType; + currentData?: any; +} + +const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { + const { provider, action, currentData } = props; + const countRef = useRef(1); + const formRefs = useRef | null>>({}); + const [workerPoolList, setWorkerPoolList] = useState< + Map + >( + new Map([ + [ + 1, + { + instance_type: 'Pool-1' + } + ] + ]) as Map + ); + + const handleOnFinish = (values: any) => {}; + + const updateCount = () => { + countRef.current += 1; + return countRef.current; + }; + + const handleAddPool = () => { + const newId = updateCount(); + setWorkerPoolList((prev) => + new Map(prev).set(newId, { + instance_type: `Pool-${newId}` + } as NodePoolFormData) + ); + }; + + const handleRemovePool = (id: number) => { + if (workerPoolList.size === 1) { + return; + } + + setWorkerPoolList((prev) => { + const newList = new Map(prev); + newList.delete(id); + return newList; + }); + formRefs.current[id] = null; + }; + + const gatherFormValues = (results: PromiseSettledResult[]) => { + const resultList = results.map((result: PromiseSettledResult) => { + if (result.status === 'fulfilled') { + return result.value; + } + if (result.status === 'rejected') { + return result.reason?.values || {}; + } + return {}; + }); + console.log('gatherFormValues========', resultList); + return resultList; + }; + + const validateFields = async () => { + const promises = Array.from(Object.values(formRefs.current)).map((form) => + form?.validateFields() + ); + const results = await Promise.allSettled(promises); + console.log('results========0', promises, results); + if (results.some((result) => result.status === 'rejected')) { + return Promise.reject({ + values: { + worker_pools: gatherFormValues(results) + } + }); + } + return Promise.resolve({ + worker_pools: gatherFormValues(results) + }); + }; + + const setFieldsValue = (data: any) => { + const worker_pools = data.worker_pools || []; + const newWorkerPools = worker_pools.map( + (poolData: NodePoolFormData, index: number) => [index, poolData] + ); + console.log('newWorkerPools========', newWorkerPools); + setWorkerPoolList(new Map(newWorkerPools)); + }; + + const getFieldsValue = () => { + const values = Object.values(formRefs.current).map((form) => + form?.getFieldsValue() + ); + console.log('getFieldsValue========', values); + return { + worker_pools: values + }; + }; + + useImperativeHandle(ref, () => ({ + submit: () => {}, + validateFields: validateFields, + getFieldsValue: getFieldsValue + })); + + useEffect(() => { + if (currentData) { + console.log('currentData===========1=', currentData); + setFieldsValue(currentData); + } + }, [currentData]); + + return ( +
+ + Worker Pools Configuration + + } + right={ + + } + > + {Array.from(workerPoolList.keys()).map((key, index) => ( +
+ + + { + if (el) { + formRefs.current[key] = el; + } + }} + onFinish={handleOnFinish} + provider={provider} + currentData={workerPoolList.get(key)} + > + + + {index !== Array.from(workerPoolList.keys()).length - 1 && ( + + + + )} +
+ ))} +
+ +
+
+ ); +}); + +export default WorkerPoolsForm; diff --git a/src/pages/llmodels/components/model-item.tsx b/src/pages/llmodels/components/model-item.tsx index 9a6bcc10..dafdd4ad 100644 --- a/src/pages/llmodels/components/model-item.tsx +++ b/src/pages/llmodels/components/model-item.tsx @@ -21,24 +21,6 @@ const ModelItemContent = styled.div` height: 100%; width: 100%; cursor: default; - .title { - margin-bottom: 16px; - display: flex; - align-items: center; - justify-content: space-between; - padding-bottom: 8px; - .anticon { - font-size: 16px; - color: var(--ant-color-text-secondary); - } - .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; @@ -83,6 +65,25 @@ const ModelItemContent = styled.div` } `; +const Header = styled.div` + margin-bottom: 8px; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + .anticon { + font-size: 16px; + color: var(--ant-color-text-secondary); + } + .text { + display: flex; + align-items: center; + font-size: var(--font-size-middle); + font-weight: 600; + color: var(--ant-color-text); + } +`; + const sourceIconMap = { [modelSourceMap.local_path_value]: 'icon-hard-disk', [modelSourceMap.huggingface_value]: 'icon-huggingface', @@ -96,9 +97,12 @@ const ModelItem: React.FC<{ const { model, onClick } = props; return ( - onClick(model)} clickable={false} ghost> - -
+ onClick(model)} + clickable={false} + ghost + header={ +
-
+ + } + > +