From 8de272239eb66409ff877f166c0ce6472a14071d Mon Sep 17 00:00:00 2001 From: jialin Date: Sat, 6 Sep 2025 23:58:09 +0800 Subject: [PATCH] refactor: create cluster by steps --- config/routes.ts | 12 +- src/assets/logo/Iluvatar.png | Bin 0 -> 33930 bytes src/assets/logo/ascend.png | Bin 0 -> 5768 bytes src/assets/logo/cambricon.png | Bin 0 -> 3103 bytes src/assets/logo/hygon.png | Bin 0 -> 7173 bytes src/assets/logo/metax.png | Bin 0 -> 7290 bytes src/assets/logo/moore _threads.png | Bin 0 -> 6227 bytes src/assets/logo/nvidia.png | Bin 0 -> 9604 bytes .../dynamic-form/components/list-map.tsx | 35 ++- src/components/icon-font/icons.ts | 8 +- src/components/icon-font/index.tsx | 2 +- src/components/status-tag/index.less | 1 + src/components/templates/card.tsx | 59 +++- src/global.less | 13 +- src/locales/en-US/clusters.ts | 4 +- src/locales/en-US/common.ts | 3 +- src/locales/en-US/menu.ts | 3 +- src/locales/ja-JP/clusters.ts | 9 +- src/locales/ja-JP/common.ts | 6 +- src/locales/ja-JP/menu.ts | 6 +- src/locales/ru-RU/clusters.ts | 8 +- src/locales/ru-RU/common.ts | 6 +- src/locales/ru-RU/menu.ts | 6 +- src/locales/zh-CN/clusters.ts | 4 +- src/locales/zh-CN/common.ts | 3 +- src/locales/zh-CN/menu.ts | 3 +- .../cluster-management/cluster-create.tsx | 293 ++++++++++++++++++ .../cluster-management/cluster-detail.tsx | 4 +- src/pages/cluster-management/clusters.tsx | 7 +- .../components/add-cluster.tsx | 127 +------- .../components/add-worker-step.tsx | 31 ++ .../components/cloud-options.tsx | 9 +- .../components/cluster-form.tsx | 94 ++++++ .../components/cluster-steps.tsx | 42 +++ .../components/footer-buttons.tsx | 63 ++++ .../components/pool-form.tsx | 32 +- .../components/provider-catalog.tsx | 57 ++++ .../components/support-hardware.tsx | 94 ++++++ .../components/volumes-config.tsx | 34 ++ .../config/cloud-options-config.ts | 2 +- src/pages/cluster-management/config/index.ts | 69 ++++- src/pages/cluster-management/config/types.ts | 22 +- .../hooks/use-cluster-columns.tsx | 2 +- .../step-forms/basic-form.tsx | 58 ++++ .../step-forms/module-registry.ts | 19 ++ .../step-forms/use-step-list.tsx | 100 ++++++ .../step-forms/worker-pools-form.tsx | 218 +++++++++++++ src/pages/llmodels/components/model-item.tsx | 51 +-- 48 files changed, 1392 insertions(+), 227 deletions(-) create mode 100644 src/assets/logo/Iluvatar.png create mode 100644 src/assets/logo/ascend.png create mode 100644 src/assets/logo/cambricon.png create mode 100644 src/assets/logo/hygon.png create mode 100644 src/assets/logo/metax.png create mode 100644 src/assets/logo/moore _threads.png create mode 100644 src/assets/logo/nvidia.png create mode 100644 src/pages/cluster-management/cluster-create.tsx create mode 100644 src/pages/cluster-management/components/add-worker-step.tsx create mode 100644 src/pages/cluster-management/components/cluster-form.tsx create mode 100644 src/pages/cluster-management/components/cluster-steps.tsx create mode 100644 src/pages/cluster-management/components/footer-buttons.tsx create mode 100644 src/pages/cluster-management/components/provider-catalog.tsx create mode 100644 src/pages/cluster-management/components/support-hardware.tsx create mode 100644 src/pages/cluster-management/components/volumes-config.tsx create mode 100644 src/pages/cluster-management/step-forms/basic-form.tsx create mode 100644 src/pages/cluster-management/step-forms/module-registry.ts create mode 100644 src/pages/cluster-management/step-forms/use-step-list.tsx create mode 100644 src/pages/cluster-management/step-forms/worker-pools-form.tsx 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 0000000000000000000000000000000000000000..9c18d5ed6a16f28c31bce740c9be9e891ee56f55 GIT binary patch literal 33930 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%0A>pRjP)Od6ed;vLWTQVEo?fLA8sI;yzLe} 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 UHjosfrT_o{07*qoM6N<$g7PVy7ytkO literal 0 HcmV?d00001 diff --git a/src/assets/logo/ascend.png b/src/assets/logo/ascend.png new file mode 100644 index 0000000000000000000000000000000000000000..0470674d2d35f95191c1648213e3d5245c9c6777 GIT binary patch literal 5768 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 Sr9}{skUdh-RxVSt4*5T+3D1uJ literal 0 HcmV?d00001 diff --git a/src/assets/logo/cambricon.png b/src/assets/logo/cambricon.png new file mode 100644 index 0000000000000000000000000000000000000000..56588e74c6180a459d9d72ed176f4c23dc9350d9 GIT binary patch literal 3103 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 literal 0 HcmV?d00001 diff --git a/src/assets/logo/hygon.png b/src/assets/logo/hygon.png new file mode 100644 index 0000000000000000000000000000000000000000..562f3e4e126b14f921448978e46995af4ea849ac GIT binary patch literal 7173 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%0AQ@ljD5gk>-U$Z+ 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 - 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={ +
-
+ + } + > +