From 0f3f31ef45f47583cbd6e53f36ed2679ec0639b5 Mon Sep 17 00:00:00 2001 From: jialin Date: Sat, 15 Nov 2025 22:18:42 +0800 Subject: [PATCH] refactor: add worker --- src/components/collapse-container/index.tsx | 30 ++- src/components/seal-form/seal-switch.tsx | 3 +- src/locales/en-US/clusters.ts | 43 +++- src/locales/en-US/resources.ts | 4 +- src/locales/ja-JP/clusters.ts | 76 ++++-- src/locales/ja-JP/resources.ts | 7 +- src/locales/ru-RU/clusters.ts | 46 +++- src/locales/ru-RU/resources.ts | 7 +- src/locales/zh-CN/clusters.ts | 27 ++- src/locales/zh-CN/resources.ts | 3 +- .../cluster-management/cluster-create.tsx | 41 +++- src/pages/cluster-management/clusters.tsx | 148 +----------- .../components/add-worker-command.tsx | 25 +- .../components/add-worker-step.tsx | 183 -------------- .../components/add-worker.tsx | 43 ---- .../add-worker/add-worker-context.ts | 36 +++ .../components/add-worker/add-worker-step.tsx | 132 ++++++++++ .../add-worker/check-environment.tsx | 58 +++++ .../components/add-worker/config.ts | 56 +++++ .../components/add-worker/constainers.tsx | 79 ++++++ .../add-worker/docker-run-command.tsx | 60 +++++ .../components/add-worker/index.tsx | 110 +++++++++ .../components/add-worker/k8s-run-command.tsx | 39 +++ .../components/add-worker/network-config.tsx | 52 ++++ .../components/add-worker/select-cluster.tsx | 69 ++++++ .../components/add-worker/select-vendor.tsx | 72 ++++++ .../add-worker/specify-arguments.tsx | 227 ++++++++++++++++++ .../components/add-worker/step-collapse.tsx | 84 +++++++ .../components/add-worker/summary-data.tsx | 137 +++++++++++ .../add-worker/use-summary-status.ts | 38 +++ .../components/add-worker/vendor-notes.tsx | 45 ++++ .../components/provider-catalog.tsx | 1 - .../components/register-cluster.tsx | 46 ---- .../hooks/use-add-worker.tsx | 96 ++++++++ .../step-forms/module-registry.ts | 2 +- .../llmodels/components/catalog-item.tsx | 17 +- src/pages/llmodels/style/catalog-item.less | 3 - src/pages/resources/components/workers.tsx | 43 +++- src/pages/resources/config/gpu-driver.ts | 164 ++++++------- 39 files changed, 1746 insertions(+), 606 deletions(-) delete mode 100644 src/pages/cluster-management/components/add-worker-step.tsx delete mode 100644 src/pages/cluster-management/components/add-worker.tsx create mode 100644 src/pages/cluster-management/components/add-worker/add-worker-context.ts create mode 100644 src/pages/cluster-management/components/add-worker/add-worker-step.tsx create mode 100644 src/pages/cluster-management/components/add-worker/check-environment.tsx create mode 100644 src/pages/cluster-management/components/add-worker/config.ts create mode 100644 src/pages/cluster-management/components/add-worker/constainers.tsx create mode 100644 src/pages/cluster-management/components/add-worker/docker-run-command.tsx create mode 100644 src/pages/cluster-management/components/add-worker/index.tsx create mode 100644 src/pages/cluster-management/components/add-worker/k8s-run-command.tsx create mode 100644 src/pages/cluster-management/components/add-worker/network-config.tsx create mode 100644 src/pages/cluster-management/components/add-worker/select-cluster.tsx create mode 100644 src/pages/cluster-management/components/add-worker/select-vendor.tsx create mode 100644 src/pages/cluster-management/components/add-worker/specify-arguments.tsx create mode 100644 src/pages/cluster-management/components/add-worker/step-collapse.tsx create mode 100644 src/pages/cluster-management/components/add-worker/summary-data.tsx create mode 100644 src/pages/cluster-management/components/add-worker/use-summary-status.ts create mode 100644 src/pages/cluster-management/components/add-worker/vendor-notes.tsx delete mode 100644 src/pages/cluster-management/components/register-cluster.tsx create mode 100644 src/pages/cluster-management/hooks/use-add-worker.tsx diff --git a/src/components/collapse-container/index.tsx b/src/components/collapse-container/index.tsx index 8255755c..9f3e68f1 100644 --- a/src/components/collapse-container/index.tsx +++ b/src/components/collapse-container/index.tsx @@ -77,6 +77,7 @@ export interface CollapsibleContainerProps { onToggle?: (open: boolean) => void; disabled?: boolean; variant?: 'outlined' | 'borderless' | undefined; + iconPosition?: 'left' | 'right'; className?: string; children?: React.ReactNode; styles?: { @@ -99,6 +100,7 @@ export default function CollapsibleContainer({ variant = 'borderless', className = '', collapsible, + iconPosition = 'left', styles: cardStyles, children }: CollapsibleContainerProps) { @@ -121,6 +123,22 @@ export default function CollapsibleContainer({ const contentRef = useRef(null); const [height, setHeight] = useState(isOpen ? 'auto' : '0px'); + const renderIcon = () => { + if (showExpandIcon) { + return ( + + ); + } + return null; + }; + const renderTitle = () => { if (!collapsible) { return null; @@ -129,16 +147,7 @@ export default function CollapsibleContainer({
- {showExpandIcon && ( - - )} + {iconPosition === 'left' && renderIcon()} {title &&
{title}
}
{subtitle &&
{subtitle}
} @@ -146,6 +155,7 @@ export default function CollapsibleContainer({
{right && {right}} {deleteBtn && {deleteBtn}} + {iconPosition === 'right' && renderIcon()}
); diff --git a/src/components/seal-form/seal-switch.tsx b/src/components/seal-form/seal-switch.tsx index bc303dcc..d3f503d2 100644 --- a/src/components/seal-form/seal-switch.tsx +++ b/src/components/seal-form/seal-switch.tsx @@ -34,6 +34,7 @@ const SealSlider: React.FC = (props) => { defaultValue, checkStatus, labelWidth, + size = 'small', ...rest } = props; @@ -57,7 +58,7 @@ const SealSlider: React.FC = (props) => { WORKER_IP to ensure that the node uses the desired IP. Please double-check with hostname -I | xargs -n1.', + 'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with hostname -I | xargs -n1.', 'clusters.addworker.nvidiaNotes-02': - 'If the model directory already exists on the worker, please add the --volume directive to mount it.', - 'clusters.addworker.hygonNotes': - 'If the /opt/hyhal directory does not exist, please create a symbolic link pointing to the Hygon installed path: /opt/hyhal. Same as /opt/dtk directory.', - 'clusters.addworker.corexNotes': - 'If the /lib/modules directory does not exist, please create a symbolic link pointing to the Iluvatar installed path: ln -s /path/to/corex /lib/modules. Same as /usr/local/corex directory.', - 'clusters.addworker.metaxNotes': - 'If the /opt/mxdriver directory does not exist, please create a symbolic link pointing to the MetaX installed path: ln -s /path/to/metax /opt/mxdriver. Same as /opt/maca directory.', - 'clusters.addworker.cambriconNotes': - 'If the /usr/local/neuware directory does not exist, please create a symbolic link pointing to the Cambricon installed path: ln -s /path/to/neuware /usr/local/neuware.', + 'If a model directory already exists on the worker, you can specify the path to mount it.', + 'clusters.addworker.hygonNotes': `If the /opt/hyhal directory does not exist, create a symbolic link to the Hygon installation path. +Same applies to the /opt/dtk directory.`, + 'clusters.addworker.corexNotes': `If the /lib/modules directory does not exist, create a symbolic link to the Iluvatar installation path: +ln -s /path/to/corex /lib/modules. Same applies to the /usr/local/corex directory.`, + 'clusters.addworker.metaxNotes': `If the /opt/mxdriver directory does not exist, create a symbolic link to the MetaX installation path: +ln -s /path/to/metax /opt/mxdriver. Same applies to the /opt/maca directory.`, + 'clusters.addworker.cambriconNotes': `If the /usr/local/neuware directory does not exist, create a symbolic link to the Cambricon installation path: +ln -s /path/to/neuware /usr/local/neuware.`, 'clusters.addworker.hygonNotes-02': - 'If failed to detect devices, please try to remove --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.' + 'If device detection fails, try removing --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.', + 'clusters.addworker.selectCluster': 'Select Cluster', + 'clusters.addworker.selectCluster.tips': + 'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.', + 'clusters.addworker.selectGPU': 'Select GPU Vendor', + 'clusters.addworker.checkEnv': 'Check Environment', + 'clusters.addworker.specifyArgs': 'Specify Arguments', + 'clusters.addworker.runCommand': 'Run Command', + 'clusters.addworker.specifyWorkerIP': 'Specify Worker IP', + 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP', + 'clusters.addworker.enterWorkerIP': 'Enter worker IP', + 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.', + 'clusters.addworker.extraVolume': 'Additional Volume Mount', + 'clusters.addworker.configSummary': 'Configuration Summary', + 'clusters.addworker.gpuVendor': 'GPU Vendor', + 'clusters.addworker.workerIP': 'Worker IP', + 'clusters.addworker.notSpecified': 'Not Specified', + 'clusters.addworker.autoDetect': 'Auto-detect', + 'clusters.addworker.extraVolume.holder': + 'e.g. /data/models (path must start with /)' }; diff --git a/src/locales/en-US/resources.ts b/src/locales/en-US/resources.ts index 71a7907e..1461de43 100644 --- a/src/locales/en-US/resources.ts +++ b/src/locales/en-US/resources.ts @@ -104,5 +104,7 @@ export default { 'resources.worker.maintenance.remark.rules': 'Please enter maintenance remarks', 'resources.worker.maintenance.tips': - 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.' + 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.', + 'resources.worker.noCluster.tips': + 'No available clusters. Please create a cluster before adding a node.' }; diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index fcb8da9d..09835138 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -43,24 +43,43 @@ export default { 'clusters.create.addCommand.tips': 'On the Worker that needs to be added, run the following command to join it to the cluster.', 'cluster.create.checkEnv.tips': - 'Use the following command to check if the environment is ready', + 'Use the following command to check if the environment is ready.', 'clusters.create.register.tips': ' On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.', 'cluster.provider.comingsoon': 'Coming soon', 'clusters.addworker.nvidiaNotes-01': - 'If the worker has multiple outbound IP addresses, enter WORKER_IP to ensure that the node uses the desired IP. Please double-check with hostname -I | xargs -n1.', + 'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with hostname -I | xargs -n1.', 'clusters.addworker.nvidiaNotes-02': - 'If the model directory already exists on the worker, please add the --volume directive to mount it.', - 'clusters.addworker.hygonNotes': - 'If the /opt/hyhal directory does not exist, please create a symbolic link pointing to the Hygon installed path: /opt/hyhal. Same as /opt/dtk directory.', - 'clusters.addworker.corexNotes': - 'If the /lib/modules directory does not exist, please create a symbolic link pointing to the Iluvatar installed path: ln -s /path/to/corex /lib/modules. Same as /usr/local/corex directory.', - 'clusters.addworker.metaxNotes': - 'If the /opt/mxdriver directory does not exist, please create a symbolic link pointing to the MetaX installed path: ln -s /path/to/metax /opt/mxdriver. Same as /opt/maca directory.', - 'clusters.addworker.cambriconNotes': - 'If the /usr/local/neuware directory does not exist, please create a symbolic link pointing to the Cambricon installed path: ln -s /path/to/neuware /usr/local/neuware.', + 'If a model directory already exists on the worker, you can specify the path to mount it.', + 'clusters.addworker.hygonNotes': `If the /opt/hyhal directory does not exist, create a symbolic link to the Hygon installation path. +Same applies to the /opt/dtk directory.`, + 'clusters.addworker.corexNotes': `If the /lib/modules directory does not exist, create a symbolic link to the Iluvatar installation path: +ln -s /path/to/corex /lib/modules. Same applies to the /usr/local/corex directory.`, + 'clusters.addworker.metaxNotes': `If the /opt/mxdriver directory does not exist, create a symbolic link to the MetaX installation path: +ln -s /path/to/metax /opt/mxdriver. Same applies to the /opt/maca directory.`, + 'clusters.addworker.cambriconNotes': `If the /usr/local/neuware directory does not exist, create a symbolic link to the Cambricon installation path: +ln -s /path/to/neuware /usr/local/neuware.`, 'clusters.addworker.hygonNotes-02': - 'If failed to detect devices, please try to remove --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.' + 'If device detection fails, try removing --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.', + 'clusters.addworker.selectCluster': 'Select Cluster', + 'clusters.addworker.selectCluster.tips': + 'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.', + 'clusters.addworker.selectGPU': 'Select GPU Vendor', + 'clusters.addworker.checkEnv': 'Check Environment', + 'clusters.addworker.specifyArgs': 'Specify Arguments', + 'clusters.addworker.runCommand': 'Run Command', + 'clusters.addworker.specifyWorkerIP': 'Specify Worker IP', + 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP', + 'clusters.addworker.enterWorkerIP': 'Enter worker IP', + 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.', + 'clusters.addworker.extraVolume': 'Additional Volume Mount', + 'clusters.addworker.configSummary': 'Configuration Summary', + 'clusters.addworker.gpuVendor': 'GPU Vendor', + 'clusters.addworker.workerIP': 'Worker IP', + 'clusters.addworker.notSpecified': 'Not Specified', + 'clusters.addworker.autoDetect': 'Auto-detect', + 'clusters.addworker.extraVolume.holder': + 'e.g. /data/models (path must start with /)' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -104,14 +123,31 @@ export default { // 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool', // 39. 'clusters.create.addworker.tips': 'Please make sure the prerequisites for {label} are met before executing the following command.', // 40. 'clusters.create.addCommand.tips': 'On the Worker that needs to be added, run the following command to join it to the cluster.', -// 41. 'cluster.create.checkEnv.tips': 'Use the following command to check if the environment is ready', +// 41. 'cluster.create.checkEnv.tips': 'Use the following command to check if the environment is ready.', // 42. 'clusters.create.register.tips': ' On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.', // 43. 'cluster.provider.comingsoon': 'Coming soon', -// 44. 'clusters.addworker.nvidiaNotes-01': 'If the worker has multiple outbound IP addresses, enter WORKER_IP to ensure that the node uses the desired IP. Please double-check with hostname -I | xargs -n1.', -// 45. 'clusters.addworker.nvidiaNotes-02': 'If the model directory already exists on the worker, please add the --volume directive to mount it.', -// 46. 'clusters.addworker.hygonNotes': 'If the /opt/hyhal directory does not exist, please create a symbolic link pointing to the Hygon installed path: /opt/hyhal. Same as /opt/dtk directory.', -// 47. 'clusters.addworker.corexNotes': 'If the /lib/modules directory does not exist, please create a symbolic link pointing to the Iluvatar installed path: ln -s /path/to/corex /lib/modules. Same as /opt/mxdriver directory does not exist, please create a symbolic link pointing to the MetaX installed path: ln -s /path/to/metax /opt/mxdriver. Same as /opt/maca directory.', -// 49. 'clusters.addworker.cambriconNotes': 'If the /usr/local/neuware directory does not exist, please create a symbolic link pointing to the Cambricon installed path: ln -s /path/to/neuware /usr/local/neuware.' -// 50. 'clusters.addworker.hygonNotes-02': 'If failed to detect devices, please try to remove --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.' +// 44. 'clusters.addworker.nvidiaNotes-01': 'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with hostname -I | xargs -n1.', +// 45. 'clusters.addworker.nvidiaNotes-02': 'If a model directory already exists on the worker, you can specify the path to mount it.', +// 46. 'clusters.addworker.hygonNotes': 'If the /opt/hyhal directory does not exist, create a symbolic link to the Hygon installation path. Same applies to the /opt/dtk directory.', +// 47. 'clusters.addworker.corexNotes': 'If the /lib/modules directory does not exist, create a symbolic link to the Iluvatar installation path: ln -s /path/to/corex /lib/modules. Same applies to the /usr/local/corex directory.', +// 48. 'clusters.addworker.metaxNotes': 'If the /opt/mxdriver directory does not exist, create a symbolic link to the MetaX installation path: ln -s /path/to/metax /opt/mxdriver. Same applies to the /opt/maca directory.', +// 49. 'clusters.addworker.cambriconNotes': 'If the /usr/local/neuware directory does not exist, create a symbolic link to the Cambricon installation path: ln -s /path/to/neuware /usr/local/neuware.' +// 50. 'clusters.addworker.hygonNotes-02': 'If device detection fails, try removing --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.', +// 51. 'clusters.addworker.selectCluster': 'Select Cluster', +// 52. 'clusters.addworker.selectCluster.tips': 'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.', +// 53. 'clusters.addworker.selectGPU': 'Select GPU Vendor', +// 54. 'clusters.addworker.checkEnv': 'Check Environment', +// 55. 'clusters.addworker.specifyArgs': 'Specify Arguments', +// 56. 'clusters.addworker.runCommand': 'Run Command', +// 57. 'clusters.addworker.specifyWorkerIP': 'Specify Worker IP', +// 58. 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP', +// 59. 'clusters.addworker.enterWorkerIP': 'Enter worker IP', +// 60. 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.', +// 61. 'clusters.addworker.extraVolume': 'Additional Volume Mount', +// 62. 'clusters.addworker.configSummary': 'Configuration Summary', +// 63. 'clusters.addworker.gpuVendor': 'GPU Vendor', +// 64. 'clusters.addworker.workerIP': 'Worker IP', +// 65. 'clusters.addworker.notSpecified': 'Not Specified', +// 66. 'clusters.addworker.autoDetect': 'Auto-detect', +// 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)' // ========== End of To-Do List ========== diff --git a/src/locales/ja-JP/resources.ts b/src/locales/ja-JP/resources.ts index 861c08c0..c87ab5e2 100644 --- a/src/locales/ja-JP/resources.ts +++ b/src/locales/ja-JP/resources.ts @@ -105,7 +105,9 @@ export default { 'resources.worker.maintenance.remark.rules': 'Please enter maintenance remarks', 'resources.worker.maintenance.tips': - 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.' + 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.', + 'resources.worker.noCluster.tips': + 'No available clusters. Please create a cluster before adding a node.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -131,5 +133,6 @@ export default { // 20. 'resources.worker.maintenance.disable': 'Exit Maintenance Mode', // 21. 'resources.worker.maintenance.remark': 'Remark', // 22. 'resources.worker.maintenance.remark.rules': 'Please enter maintenance remarks', -// 23. 'resources.worker.maintenance.tips': 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.' +// 23. 'resources.worker.maintenance.tips': 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.', +// 24. 'resources.worker.noCluster.tips': 'No available clusters. Please create a cluster before adding a node.' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index 77a85afd..cc1a8f3b 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -48,9 +48,9 @@ export default { 'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.', 'cluster.provider.comingsoon': 'Скоро будет', 'clusters.addworker.nvidiaNotes-01': - 'Если рабочий узел имеет несколько исходящих IP-адресов, укажите WORKER_IP, чтобы гарантировать использование узлом нужного IP. Перепроверьте с помощью команды hostname -I | xargs -n1.', + 'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with hostname -I | xargs -n1.', 'clusters.addworker.nvidiaNotes-02': - 'Если директория с моделью уже существует на рабочем узле, добавьте директиву --volume для её монтирования.', + 'If a model directory already exists on the worker, you can specify the path to mount it.', 'clusters.addworker.hygonNotes': 'Если директория /opt/hyhal не существует, создайте символическую ссылку на путь установки Hygon: /opt/hyhal. Аналогично для директории /opt/dtk.', 'clusters.addworker.corexNotes': @@ -60,9 +60,47 @@ export default { 'clusters.addworker.cambriconNotes': 'Если директория /usr/local/neuware не существует, создайте символическую ссылку на путь установки Cambricon: ln -s /path/to/neuware /usr/local/neuware.', 'clusters.addworker.hygonNotes-02': - 'If failed to detect devices, please try to remove --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.' + 'If failed to detect devices, please try to remove --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.', + 'clusters.addworker.selectCluster': 'Select Cluster', + 'clusters.addworker.selectCluster.tips': + 'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.', + 'clusters.addworker.selectGPU': 'Select GPU Vendor', + 'clusters.addworker.checkEnv': 'Check Environment', + 'clusters.addworker.specifyArgs': 'Specify Arguments', + 'clusters.addworker.runCommand': 'Run Command', + 'clusters.addworker.specifyWorkerIP': 'Specify Worker IP', + 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP', + 'clusters.addworker.enterWorkerIP': 'Enter worker IP', + 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.', + 'clusters.addworker.extraVolume': 'Additional Volume Mount', + 'clusters.addworker.configSummary': 'Configuration Summary', + 'clusters.addworker.gpuVendor': 'GPU Vendor', + 'clusters.addworker.workerIP': 'Worker IP', + 'clusters.addworker.notSpecified': 'Not Specified', + 'clusters.addworker.autoDetect': 'Auto-detect', + 'clusters.addworker.extraVolume.holder': + 'e.g. /data/models (path must start with /)' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== -// 1. 'clusters.addworker.hygonNotes-02': 'If failed to detect devices, please try to remove --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.' +// 1. 'clusters.addworker.hygonNotes-02': 'If failed to detect devices, please try to remove --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.', +// 2. 'clusters.addworker.selectCluster': 'Select Cluster', +// 3. 'clusters.addworker.selectCluster.tips': 'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.', +// 4. 'clusters.addworker.selectGPU': 'Select GPU Vendor', +// 5. 'clusters.addworker.checkEnv': 'Check Environment', +// 6. 'clusters.addworker.specifyArgs': 'Specify Arguments', +// 7. 'clusters.addworker.runCommand': 'Run Command', +// 8. 'clusters.addworker.specifyWorkerIP': 'Specify Worker IP', +// 9. 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP', +// 10. 'clusters.addworker.enterWorkerIP': 'Enter worker IP', +// 11. 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.', +// 12. 'clusters.addworker.extraVolume': 'Additional Volume Mount', +// 13. 'clusters.addworker.configSummary': 'Configuration Summary', +// 14. 'clusters.addworker.gpuVendor': 'GPU Vendor', +// 15. 'clusters.addworker.workerIP': 'Worker IP', +// 16. 'clusters.addworker.notSpecified': 'Not Specified', +// 17. 'clusters.addworker.nvidiaNotes-01': 'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with hostname -I | xargs -n1.', +// 18. 'clusters.addworker.nvidiaNotes-02': 'If a model directory already exists on the worker, you can specify the path to mount it.', +// 19. 'clusters.addworker.autoDetect': 'Auto-detect', +// 20. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/resources.ts b/src/locales/ru-RU/resources.ts index acfa5ada..253928a7 100644 --- a/src/locales/ru-RU/resources.ts +++ b/src/locales/ru-RU/resources.ts @@ -103,7 +103,9 @@ export default { 'resources.worker.maintenance.remark.rules': 'Please enter maintenance remarks', 'resources.worker.maintenance.tips': - 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.' + 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.', + 'resources.worker.noCluster.tips': + 'No available clusters. Please create a cluster before adding a node.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -112,5 +114,6 @@ export default { // 4. 'resources.worker.maintenance.disable': 'Exit Maintenance Mode', // 5. 'resources.worker.maintenance.remark': 'Remark', // 6. 'resources.worker.maintenance.remark.rules': 'Please enter maintenance remarks', -// 7. 'resources.worker.maintenance.tips': 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.' +// 7. 'resources.worker.maintenance.tips': 'When maintenance mode is enabled, the node will stop scheduling new model deployment tasks. Running instances will not be affected.', +// 8. 'resources.worker.noCluster.tips': 'No available clusters. Please create a cluster before adding a node.' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index 20f3a99b..06294fc0 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -40,10 +40,10 @@ export default { 'clusters.create.addworker.tips': '在执行以下命令之前,请确保已满足 {label} 的先决条件。', 'clusters.create.addCommand.tips': - '在需要添加的节点上运行以下命令,将其加入到集群中', + '在需要添加的节点上运行以下命令,将其加入到集群中。', 'clusters.create.register.tips': - '在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中', - 'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当', + '在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。', + 'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。', 'cluster.provider.comingsoon': '即将推出', 'clusters.addworker.nvidiaNotes-01': '如果节点有多个出站 IP 地址,请填写 WORKER_IP,以确保使用指定的 IP。可通过命令 hostname -I | xargs -n1 进行确认。', @@ -58,5 +58,24 @@ export default { 'clusters.addworker.cambriconNotes': '如果 /usr/local/neuware 目录不存在,请创建指向 Cambricon 安装路径的符号链接:ln -s /path/to/neuware /usr/local/neuware。', 'clusters.addworker.hygonNotes-02': - '如果未能检测到设备,请尝试移除 --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib。' + '如果未能检测到设备,请尝试移除 --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib。', + 'clusters.addworker.selectCluster': '选择集群', + 'clusters.addworker.selectCluster.tips': + '非 Docker 集群请前往集群页面注册集群或管理节点池。', + 'clusters.addworker.selectGPU': '选择 GPU 厂商', + 'clusters.addworker.checkEnv': '检查环境', + 'clusters.addworker.specifyArgs': '指定参数', + 'clusters.addworker.runCommand': '运行指令', + 'clusters.addworker.specifyWorkerIP': '指定 Worker IP', + 'clusters.addworker.detectWorkerIP': '自动检测 Worker IP', + 'clusters.addworker.enterWorkerIP': '输入节点 IP', + 'clusters.addworker.enterWorkerIP.error': '请输入节点 IP', + 'clusters.addworker.extraVolume': '额外卷挂载', + 'clusters.addworker.configSummary': '配置摘要', + 'clusters.addworker.gpuVendor': 'GPU 厂商', + 'clusters.addworker.workerIP': '节点 IP', + 'clusters.addworker.notSpecified': '未指定', + 'clusters.addworker.autoDetect': '自动检测', + 'clusters.addworker.extraVolume.holder': + '例如:/data/models(路径需以 / 开头)' }; diff --git a/src/locales/zh-CN/resources.ts b/src/locales/zh-CN/resources.ts index 515f3be9..0014a4d7 100644 --- a/src/locales/zh-CN/resources.ts +++ b/src/locales/zh-CN/resources.ts @@ -101,5 +101,6 @@ export default { 'resources.worker.maintenance.remark': '备注', 'resources.worker.maintenance.remark.rules': '请输入维护备注', 'resources.worker.maintenance.tips': - '启用维护后,节点将停止调度新的模型实例部署任务,正在运行的实例不会受到影响。' + '启用维护后,节点将停止调度新的模型实例部署任务,正在运行的实例不会受到影响。', + 'resources.worker.noCluster.tips': '当前无可用集群,请先创建集群再添加节点。' }; diff --git a/src/pages/cluster-management/cluster-create.tsx b/src/pages/cluster-management/cluster-create.tsx index 5c819bfe..75d7872b 100644 --- a/src/pages/cluster-management/cluster-create.tsx +++ b/src/pages/cluster-management/cluster-create.tsx @@ -7,6 +7,10 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { PageContainerInner } from '../_components/page-box'; import { createCluster, queryClusterToken, queryCredentialList } from './apis'; +import { + DockerStepsFromCluster, + K8sStepsFromCluter +} from './components/add-worker/config'; import ClusterSteps from './components/cluster-steps'; import FooterButtons from './components/footer-buttons'; import ProviderCatalog from './components/provider-catalog'; @@ -198,6 +202,10 @@ const ClusterCreate = () => { }); }; + /** + * this function is used to render the modules in the current step + * @returns + */ const renderModules = () => { const step = steps[currentStep]; const moduleKeys = step?.showModules || []; @@ -209,6 +217,11 @@ const ClusterCreate = () => { return ModuleComponent ? ( @@ -270,19 +283,21 @@ const ClusterCreate = () => { }} > - {currentStep === startStep && ( - - )} - {renderModules()} - - {renderForms()} - +
+ {currentStep === startStep && ( + + )} + {renderModules()} + + {renderForms()} + +
); }; diff --git a/src/pages/cluster-management/clusters.tsx b/src/pages/cluster-management/clusters.tsx index 033196dc..e92ad1c7 100644 --- a/src/pages/cluster-management/clusters.tsx +++ b/src/pages/cluster-management/clusters.tsx @@ -1,4 +1,4 @@ -import { clusterSessionAtom, expandKeysAtom } from '@/atoms/clusters'; +import { expandKeysAtom } from '@/atoms/clusters'; import DeleteModal from '@/components/delete-modal'; import IconFont from '@/components/icon-font'; import { FilterBar } from '@/components/page-tools'; @@ -9,7 +9,6 @@ import type { PageActionType } from '@/config/types'; import useExpandedRowKeys from '@/hooks/use-expanded-row-keys'; import useTableFetch from '@/hooks/use-table-fetch'; import useWatchList from '@/hooks/use-watch-list'; -import AddWorker from '@/pages/cluster-management/components/add-worker'; import { useIntl, useNavigate } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { Button, message } from 'antd'; @@ -22,7 +21,6 @@ import { createWorkerPool, deleteCluster, queryClusterList, - queryClusterToken, queryCredentialList, queryWorkerPools, updateCluster, @@ -30,19 +28,19 @@ import { } from './apis'; import AddCluster from './components/add-cluster'; import AddPool from './components/add-pool'; -import PoolRows from './components/pool-rows'; -import RegisterCluster from './components/register-cluster'; import { - ClusterStatusValueMap, - ProviderType, - ProviderValueMap -} from './config'; + DockerStepsFromCluster, + K8sStepsFromCluter +} from './components/add-worker/config'; +import PoolRows from './components/pool-rows'; +import { ProviderType, ProviderValueMap } from './config'; import { ClusterListItem, ClusterFormData as FormData, ClusterListItem as ListItem, NodePoolFormData } from './config/types'; +import useAddWorker from './hooks/use-add-worker'; import useClusterColumns from './hooks/use-cluster-columns'; const Clusters: React.FC = () => { @@ -66,50 +64,12 @@ const Clusters: React.FC = () => { }); const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API); const [expandAtom] = useAtom(expandKeysAtom); - const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom); const { handleExpandChange, handleExpandAll, expandedRowKeys } = useExpandedRowKeys(expandAtom); const navigate = useNavigate(); const intl = useIntl(); - const [registerClusterStatus, setRegisterClusterStatus] = useState<{ - open: boolean; - provider: ProviderType; - registrationInfo: { - token: string; - image: string; - server_url: string; - cluster_id: number; - }; - }>({ - open: false, - provider: null, - registrationInfo: { - token: '', - image: '', - server_url: '', - cluster_id: 0 - } - }); + const { handleAddWorker, AddWorkerModal, setStepList } = useAddWorker({}); - const [openAddWorker, setOpenAddWorker] = useState<{ - open: boolean; - provider: ProviderType; - registrationInfo: { - token: string; - image: string; - server_url: string; - cluster_id: number; - }; - }>({ - open: false, - provider: null, - registrationInfo: { - token: '', - image: '', - server_url: '', - cluster_id: 0 - } - }); const [openAddModal, setOpenAddModal] = useState<{ open: boolean; action: PageActionType; @@ -206,36 +166,6 @@ const Clusters: React.FC = () => { }); }; - const handleAddWorker = async (row: ListItem) => { - try { - const data = await queryClusterToken({ id: row.id }); - setOpenAddWorker({ - open: true, - provider: row.provider as ProviderType, - registrationInfo: { - ...data, - cluster_id: row.id - } - }); - } catch (error: any) { - message.error(error.message || 'Failed to fetch cluster token'); - } - }; - - const handleRegisterCluster = async (row: ListItem) => { - try { - const info = await queryClusterToken({ id: row.id }); - setRegisterClusterStatus({ - open: true, - provider: row.provider as ProviderType, - registrationInfo: { - ...info, - cluster_id: row.id - } - }); - } catch (error) {} - }; - const handleSelect = useMemoizedFn((val: any, row: ListItem) => { if (val === 'edit') { handleEditCluster(row); @@ -243,10 +173,12 @@ const Clusters: React.FC = () => { handleDelete({ ...row, name: row.name }); } else if (val === 'add_worker') { handleAddWorker(row); + setStepList(DockerStepsFromCluster); } else if (val === 'addPool') { handleAddPool(row); } else if (val === 'register_cluster') { - handleRegisterCluster(row); + handleAddWorker(row); + setStepList(K8sStepsFromCluter); } }); @@ -308,28 +240,6 @@ const Clusters: React.FC = () => { fetchCredentialList(); }, []); - /** - * add worker from workers page redirect - */ - useEffect(() => { - if ( - clusterSession?.firstAddWorker && - dataSource.loadend && - dataSource.dataList?.length > 0 - ) { - const targetCluster = dataSource.dataList.find( - (cluster) => - cluster.provider === ProviderValueMap.Docker && - cluster.state === ClusterStatusValueMap.Ready - ); - if (targetCluster) { - handleAddWorker(targetCluster); - // reset session - setClusterSession(null); - } - } - }, [clusterSession, dataSource.loadend, dataSource.dataList]); - const renderChildren = ( list: any, options: { parent?: any; [key: string]: any } @@ -421,41 +331,6 @@ const Clusters: React.FC = () => { onCancel={handleModalCancel} onOk={handleModalOk} > - - setOpenAddWorker({ - open: false, - provider: null, - registrationInfo: { - token: '', - image: '', - server_url: '', - cluster_id: 0 - } - }) - } - registrationInfo={openAddWorker.registrationInfo} - > - { - setRegisterClusterStatus({ - open: false, - provider: null, - registrationInfo: { - token: '', - image: '', - server_url: '', - cluster_id: 0 - } - }); - }} - > { onOk={handleSubmitWorkerPool} > + {AddWorkerModal} ); }; diff --git a/src/pages/cluster-management/components/add-worker-command.tsx b/src/pages/cluster-management/components/add-worker-command.tsx index 78594d62..6a4457a8 100644 --- a/src/pages/cluster-management/components/add-worker-command.tsx +++ b/src/pages/cluster-management/components/add-worker-command.tsx @@ -5,6 +5,7 @@ import React from 'react'; type ViewModalProps = { currentGPU?: string; workerIP?: string; + modelDir?: string; registrationInfo: { token: string; image: string; @@ -15,19 +16,25 @@ type ViewModalProps = { const AddWorkerCommand: React.FC = ({ registrationInfo, workerIP, + modelDir, currentGPU }) => { const code = React.useMemo(() => { const commandCode = addWorkerGuide['all']; - return commandCode?.registerWorker({ - gpu: currentGPU || '', - server: registrationInfo.server_url || origin, - tag: '', - workerIP: '${WORKER_IP}', - image: registrationInfo.image, - token: registrationInfo.token || '${token}' - }); - }, [registrationInfo, currentGPU, workerIP]); + return commandCode + ?.registerWorker({ + gpu: currentGPU || '', + server: registrationInfo.server_url || origin, + tag: '', + workerIP: workerIP, + modelDir: modelDir, + image: registrationInfo.image, + token: registrationInfo.token || '${token}' + }) + ?.trim() + .replace(/\\\s+$/gm, '') + .replace(/\\+$/, ''); + }, [registrationInfo, currentGPU, workerIP, modelDir]); return ( = ({ - provider, - registrationInfo -}) => { - const intl = useIntl(); - const [currentGPU, setCurrentGPU] = React.useState( - GPUDriverMap.NVIDIA - ); - const [workerCommand, setWorkerCommand] = React.useState>( - { - label: 'NVIDIA', - link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda', - notes: AddWorkerDockerNotes[GPUDriverMap.NVIDIA] - } - ); - - const handleSelectProvider = (value: string, item: any) => { - console.log('selected gpu driver:', value, item); - setCurrentGPU(value); - setWorkerCommand(item); - }; - - return ( - - - - - - } - message={ - - } - > - - -
- 1. - {intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })} -
- - - {provider === ProviderValueMap.Kubernetes ? ( - <> -
- 2. - {intl.formatMessage({ id: 'clusters.create.register.tips' })} -
- - - ) : ( - <> -
- 2. - {intl.formatMessage({ id: 'clusters.create.addCommand.tips' })} -
- } - message={ - workerCommand.notes?.length > 0 ? ( - - {workerCommand.notes.map((note: string, index: number) => ( -
  • - ))} -
    - ) : null - } - >
    - - - )} -
    - ); -}; - -export default AddWorkerStep; diff --git a/src/pages/cluster-management/components/add-worker.tsx b/src/pages/cluster-management/components/add-worker.tsx deleted file mode 100644 index 61cc4aba..00000000 --- a/src/pages/cluster-management/components/add-worker.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import ScrollerModal from '@/components/scroller-modal'; -import { useIntl } from '@umijs/max'; -import React from 'react'; -import { ProviderType } from '../config'; -import AddWorkerStep from './add-worker-step'; - -type ViewModalProps = { - open: boolean; - provider: ProviderType; - registrationInfo: { - token: string; - image: string; - server_url: string; - cluster_id: number; - }; - onCancel: () => void; -}; - -const AddWorker: React.FC = (props) => { - const { open, onCancel, registrationInfo, provider } = props || {}; - const intl = useIntl(); - - return ( - - - - ); -}; - -export default AddWorker; diff --git a/src/pages/cluster-management/components/add-worker/add-worker-context.ts b/src/pages/cluster-management/components/add-worker/add-worker-context.ts new file mode 100644 index 00000000..5d3d08f0 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/add-worker-context.ts @@ -0,0 +1,36 @@ +import { createContext, useContext } from 'react'; +import { ProviderType } from '../../config'; +import { ClusterListItem } from '../../config/types'; +import { SummaryDataKey } from './config'; + +interface AddWorkerContextProps { + clusterList?: Global.BaseOption[]; + provider: ProviderType; + stepList: string[]; + onClusterChange?: (value: number, row?: any) => void; + collapseKey: Set; + onToggle: (open: boolean, key: string) => void; + registrationInfo: { + token: string; + image: string; + server_url: string; + cluster_id: number; + }; + registerField: (key: SummaryDataKey) => () => void; + updateField: (key: SummaryDataKey, value: any) => void; + summary: Map; +} + +export const AddWorkerContext = createContext({ + summary: new Map() +} as AddWorkerContextProps); + +export const useAddWorkerContext = () => { + const context = useContext(AddWorkerContext); + if (!context) { + throw new Error( + 'useAddWorkerContext must be used within an AddWorkerContext.Provider' + ); + } + return context; +}; diff --git a/src/pages/cluster-management/components/add-worker/add-worker-step.tsx b/src/pages/cluster-management/components/add-worker/add-worker-step.tsx new file mode 100644 index 00000000..431f3971 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/add-worker-step.tsx @@ -0,0 +1,132 @@ +import AlertInfoBlock from '@/components/alert-info/block'; +import { ExclamationCircleFilled } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import React from 'react'; +import styled from 'styled-components'; +import { ProviderType, ProviderValueMap } from '../../config'; +import { ClusterListItem } from '../../config/types'; +import { AddWorkerContext } from './add-worker-context'; +import CheckEnvironment from './check-environment'; +import { StepName, StepNamesMap } from './config'; +import DockerRunCommand from './docker-run-command'; +import K8sRunCommand from './k8s-run-command'; +import SelectCluster from './select-cluster'; +import SelectVendor from './select-vendor'; +import SpecifyArguments from './specify-arguments'; +import useSummaryStatus from './use-summary-status'; + +const Container = styled.div` + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 16px; + .command-info { + margin-bottom: 8px; + } +`; +/** + * clusterList and onClusterChange are only required when from worker page. + */ +type AddWorkerProps = { + provider: ProviderType; + clusterList?: Global.BaseOption[]; + stepList: StepName[]; + onClusterChange?: (value: number, row?: any) => void; + registrationInfo: { + token: string; + image: string; + server_url: string; + cluster_id: number; + }; +}; + +/** + * both add worker and register cluster use this component + * @param props + * @returns + */ +const AddWorkerSteps: React.FC = (props) => { + const { + registrationInfo, + provider, + clusterList, + stepList = [], + onClusterChange + } = props || {}; + const intl = useIntl(); + const { update, summary, register } = useSummaryStatus(); + + const [collapseKey, setCollapseKey] = React.useState>( + new Set([stepList[0]]) + ); + + const onToggle = (open: boolean, key: string) => { + setCollapseKey(open ? new Set([key]) : new Set()); + }; + + const handleOnClusterChange = (value: number, row?: any) => { + onClusterChange?.(value, row); + }; + + React.useEffect(() => { + // reset collapseKey when stepList changes + setCollapseKey(new Set([stepList[0]])); + }, [stepList]); + + return ( + + + {stepList.includes(StepNamesMap.SelectCluster) && ( + + )} + {stepList.includes(StepNamesMap.SelectCluster) && + !clusterList?.length && ( + } + message={intl.formatMessage({ + id: 'resources.worker.noCluster.tips' + })} + > + )} + + {/* render the steps only when there is at least one cluster available or cluster selection is not required */} + {((clusterList && clusterList.length > 0) || + !stepList.includes(StepNamesMap.SelectCluster)) && ( + <> + + + + {provider === ProviderValueMap.Kubernetes && ( + + )} + + {provider === ProviderValueMap.Docker && ( + <> + + + + )} + + )} + + + ); +}; + +export default AddWorkerSteps; diff --git a/src/pages/cluster-management/components/add-worker/check-environment.tsx b/src/pages/cluster-management/components/add-worker/check-environment.tsx new file mode 100644 index 00000000..81158968 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/check-environment.tsx @@ -0,0 +1,58 @@ +import { BulbOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Alert } from 'antd'; +import CheckEnvCommand from '../check-env-command'; +import { useAddWorkerContext } from './add-worker-context'; +import { StepNamesMap } from './config'; +import { Tips, Title } from './constainers'; +import StepCollapse from './step-collapse'; + +const CheckEnvironment = () => { + const { stepList, summary, provider } = useAddWorkerContext(); + const intl = useIntl(); + const currentGPU = summary.get('currentGPU'); + const workerCommand = summary.get('workerCommand') || { + label: '', + link: '', + notes: [] + }; + + const stepIndex = stepList.indexOf(StepNamesMap.CheckEnv) + 1; + + return ( + + {stepIndex}.{' '} + {intl.formatMessage({ id: 'clusters.addworker.checkEnv' })} + + } + > + } + style={{ + marginBottom: 8 + }} + message={ + + } + > + + {intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })} + + + + ); +}; + +export default CheckEnvironment; diff --git a/src/pages/cluster-management/components/add-worker/config.ts b/src/pages/cluster-management/components/add-worker/config.ts new file mode 100644 index 00000000..25b67613 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/config.ts @@ -0,0 +1,56 @@ +export const StepNamesMap = { + SelectCluster: 'SelectCluster', + SelectGPU: 'SelectGPU', + CheckEnv: 'CheckEnv', + SpecifyArgs: 'SpecifyArgs', + RunCommand: 'RunCommand' +} as const; + +export type StepName = keyof typeof StepNamesMap; + +export const DockerStepsFromCluster = [ + StepNamesMap.SelectGPU, + StepNamesMap.CheckEnv, + StepNamesMap.SpecifyArgs, + StepNamesMap.RunCommand +]; + +export const DockerStepsFromWorker = [ + StepNamesMap.SelectCluster, + StepNamesMap.SelectGPU, + StepNamesMap.CheckEnv, + StepNamesMap.SpecifyArgs, + StepNamesMap.RunCommand +]; + +export const K8sStepsFromCluter = [ + StepNamesMap.SelectGPU, + StepNamesMap.CheckEnv, + StepNamesMap.RunCommand +]; + +export interface SummaryDataKeys { + currentGPU: string; + cluster_id: number; + clusterName: string; + workerCommand: { + label: string; + link: string; + notes: string[]; + }; + modelDirConfig: { + enabled: boolean; + path: string; + }; + workerIPConfig: { + enabled: boolean; + ip: string; + required: boolean; + }; +} + +export type SummaryDataMap = { + [key in keyof SummaryDataKeys]: SummaryDataKeys[key]; +}; + +export type SummaryDataKey = keyof SummaryDataKeys; diff --git a/src/pages/cluster-management/components/add-worker/constainers.tsx b/src/pages/cluster-management/components/add-worker/constainers.tsx new file mode 100644 index 00000000..b3cf65f3 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/constainers.tsx @@ -0,0 +1,79 @@ +import styled from 'styled-components'; + +export const Title = styled.div` + font-weight: 500; +`; + +export const ConfigWrapper = styled.div` + padding: 16px; + margin-bottom: 16px; + border: 1px solid var(--ant-color-split); + border-radius: var(--ant-border-radius); + background-color: var(--ant-color-fill-tertiary); + background: var(--ant-color-info-bg); + border: var(--ant-line-width) var(--ant-line-type) + var(--ant-color-info-border); + .config-content { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 8px; + } + .item { + display: flex; + align-items: center; + justify-content: space-between; + .label { + color: var(--ant-color-text); + } + } +`; + +export const SwitchWrapper = styled.div` + display: flex; + flex-direction: column; + border-radius: 4px; + border: 1px solid var(--ant-color-border); + padding: 12px; + gap: 8px; + .tips { + color: var(--ant-color-text-secondary); + } + .button { + display: flex; + align-items: center; + justify-content: space-between; + } +`; + +export const Tips = styled.div` + margin-top: 0px; + color: var(--ant-color-text-secondary); +`; + +export const NotesWrapper = styled.ol` + display: flex; + flex-direction: column; + gap: 8px; + font-weight: 400; + margin: 0 !important; + padding: 0 !important; + line-height: 1.25; + li { + margin-left: 0px !important; + } +`; + +export const Container = styled.div` + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 16px; + .command-info { + margin-bottom: 8px; + } +`; + +export const Content = styled.div` + margin-top: 16px; +`; diff --git a/src/pages/cluster-management/components/add-worker/docker-run-command.tsx b/src/pages/cluster-management/components/add-worker/docker-run-command.tsx new file mode 100644 index 00000000..2cce06cf --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/docker-run-command.tsx @@ -0,0 +1,60 @@ +import { useIntl } from '@umijs/max'; +import AddWorkerCommand from '../add-worker-command'; +import { useAddWorkerContext } from './add-worker-context'; +import { StepNamesMap } from './config'; +import { Tips, Title } from './constainers'; +import StepCollapse from './step-collapse'; +import SummaryData from './summary-data'; +import VendorNotes from './vendor-notes'; + +const DockerRunCommand = () => { + const intl = useIntl(); + const { registrationInfo, stepList, summary, clusterList } = + useAddWorkerContext(); + const workerIPConfig = summary.get('workerIPConfig') || { + enable: false, + ip: '', + required: false + }; + const modelDirConfig = summary.get('modelDirConfig') || { + enable: false, + path: '', + required: false + }; + const currentGPU = summary.get('currentGPU') || ''; + + const stepIndex = stepList.indexOf(StepNamesMap.RunCommand) + 1; + + return ( + + {stepIndex}.{' '} + {intl.formatMessage({ id: 'clusters.addworker.runCommand' })} + + } + > + + + + {intl.formatMessage({ + id: 'clusters.create.addCommand.tips' + })} + + + + ); +}; + +export default DockerRunCommand; diff --git a/src/pages/cluster-management/components/add-worker/index.tsx b/src/pages/cluster-management/components/add-worker/index.tsx new file mode 100644 index 00000000..96a01456 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/index.tsx @@ -0,0 +1,110 @@ +import ScrollerModal from '@/components/scroller-modal'; +import React, { useEffect } from 'react'; +import styled from 'styled-components'; +import { queryClusterToken } from '../../apis'; +import { ProviderType } from '../../config'; +import { ClusterListItem } from '../../config/types'; +import AddWorkerStep from './add-worker-step'; +import { StepName } from './config'; + +const Container = styled.div` + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 16px; + .command-info { + margin-bottom: 8px; + } +`; + +type AddWorkerProps = { + open: boolean; + provider: ProviderType; + title: string; + clusterList?: Global.BaseOption[]; + stepList: StepName[]; + onClusterChange?: (value: number, row?: any) => void; + onCancel: () => void; + cluster_id: number; + registrationInfo?: { + token: string; + image: string; + server_url: string; + cluster_id: number; + }; +}; + +/** + * both add worker and register cluster use this component + * @param props + * @returns + */ +const AddWorker: React.FC = (props) => { + const { + open, + onCancel, + provider, + cluster_id, + title, + clusterList, + stepList = [] + } = props || {}; + const firstLoad = React.useRef(true); + const [registrationInfo, setRegistrationInfo] = React.useState<{ + token: string; + image: string; + server_url: string; + cluster_id: number; + }>({ + token: '', + image: '', + server_url: '', + cluster_id: 0 + }); + + const handleOnClusterChange = async (value: number, row?: any) => { + try { + const data = await queryClusterToken({ id: value }); + firstLoad.current = false; + setRegistrationInfo({ + ...data, + cluster_id: value + }); + } catch (error) { + firstLoad.current = false; + } + }; + + useEffect(() => { + if (open && cluster_id && firstLoad.current) { + handleOnClusterChange(cluster_id); + } + }, [open, cluster_id]); + + return ( + + + + ); +}; + +export default AddWorker; diff --git a/src/pages/cluster-management/components/add-worker/k8s-run-command.tsx b/src/pages/cluster-management/components/add-worker/k8s-run-command.tsx new file mode 100644 index 00000000..b9ba35d7 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/k8s-run-command.tsx @@ -0,0 +1,39 @@ +import { useIntl } from '@umijs/max'; +import RegisterClusterInner from '../register-cluster-inner'; +import { useAddWorkerContext } from './add-worker-context'; +import { StepNamesMap } from './config'; +import { Tips, Title } from './constainers'; +import StepCollapse from './step-collapse'; + +const K8sRunCommand = () => { + const { registrationInfo, stepList } = useAddWorkerContext(); + const intl = useIntl(); + + const stepIndex = stepList.indexOf(StepNamesMap.RunCommand) + 1; + + return ( + + {stepIndex}.{' '} + {intl.formatMessage({ id: 'clusters.addworker.runCommand' })} + + } + > + + {intl.formatMessage({ + id: 'clusters.create.addCommand.tips' + })} + + + + ); +}; + +export default K8sRunCommand; diff --git a/src/pages/cluster-management/components/add-worker/network-config.tsx b/src/pages/cluster-management/components/add-worker/network-config.tsx new file mode 100644 index 00000000..3d33860e --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/network-config.tsx @@ -0,0 +1,52 @@ +import { useIntl } from '@umijs/max'; +import { Input, Switch } from 'antd'; +import React from 'react'; +import { SwitchWrapper, Tips } from './constainers'; + +const NetworkConfig = () => { + const intl = useIntl(); + const [networkInterface, setNetworkInterface] = React.useState<{ + enable: boolean; + name?: string; + }>({ + enable: false, + name: '' + }); + return ( + +
    + + Network Interface + + + setNetworkInterface({ + ...networkInterface, + enable: checked + }) + } + > +
    + + Enter the NIC name to use for distributed inference (e.g., mlx5_0). + + {networkInterface.enable && ( + <> + + setNetworkInterface({ + ...networkInterface, + name: e.target.value + }) + } + /> + + )} +
    + ); +}; + +export default NetworkConfig; diff --git a/src/pages/cluster-management/components/add-worker/select-cluster.tsx b/src/pages/cluster-management/components/add-worker/select-cluster.tsx new file mode 100644 index 00000000..4a7d552b --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/select-cluster.tsx @@ -0,0 +1,69 @@ +import BaseSelect from '@/components/seal-form/base/select'; +import { useIntl } from '@umijs/max'; +import { useEffect } from 'react'; +import { useAddWorkerContext } from './add-worker-context'; +import { StepNamesMap } from './config'; +import { Title } from './constainers'; +import StepCollapse from './step-collapse'; + +const SelectCluster = () => { + const { + clusterList, + registrationInfo, + stepList, + summary, + onClusterChange, + registerField, + updateField + } = useAddWorkerContext(); + const intl = useIntl(); + + const clusterId = summary.get('cluster_id'); + + const stepIndex = stepList.indexOf(StepNamesMap.SelectCluster) + 1; + + useEffect(() => { + const unregister = registerField('cluster_id'); + return () => { + unregister(); + }; + }, []); + + useEffect(() => { + updateField('cluster_id', registrationInfo.cluster_id); + // update cluster name in summary + const selectedCluster = clusterList?.find( + (item) => item.value === registrationInfo.cluster_id + ); + updateField('clusterName', selectedCluster?.label || ''); + }, [registrationInfo.cluster_id]); + + return ( + + + {stepIndex}.{' '} + {intl.formatMessage({ id: 'clusters.addworker.selectCluster' })} + + + {intl.formatMessage({ + id: 'clusters.addworker.selectCluster.tips' + })} + +
    + } + > + + + ); +}; + +export default SelectCluster; diff --git a/src/pages/cluster-management/components/add-worker/select-vendor.tsx b/src/pages/cluster-management/components/add-worker/select-vendor.tsx new file mode 100644 index 00000000..4afcbd27 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/select-vendor.tsx @@ -0,0 +1,72 @@ +import { + AddWorkerDockerNotes, + GPUDriverMap +} from '@/pages/resources/config/gpu-driver'; +import { useIntl } from '@umijs/max'; +import React, { useEffect } from 'react'; +import SupportedGPUs from '../support-gpus'; +import { useAddWorkerContext } from './add-worker-context'; +import { StepNamesMap } from './config'; +import { Title } from './constainers'; +import StepCollapse from './step-collapse'; + +const SelectVendor = () => { + const { stepList, registerField, updateField } = useAddWorkerContext(); + const intl = useIntl(); + + const stepIndex = stepList.indexOf(StepNamesMap.SelectGPU) + 1; + const [currentGPU, setCurrentGPU] = React.useState( + GPUDriverMap.NVIDIA + ); + + const handleSelectProvider = (value: string, item: any) => { + console.log('selected gpu driver:', value, item); + setCurrentGPU(value); + + updateField('currentGPU', value); + updateField('workerCommand', item); + }; + + useEffect(() => { + const unregisterField = registerField('currentGPU'); + return () => { + unregisterField(); + }; + }, []); + + useEffect(() => { + const unregisterField = registerField('workerCommand'); + return () => { + unregisterField(); + }; + }, []); + + useEffect(() => { + updateField('currentGPU', GPUDriverMap.NVIDIA); + updateField('workerCommand', { + label: 'NVIDIA', + link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda', + notes: AddWorkerDockerNotes[GPUDriverMap.NVIDIA] + }); + }, []); + + return ( + + {stepIndex}.{' '} + {intl.formatMessage({ id: 'clusters.addworker.selectGPU' })} + + } + > + + + ); +}; + +export default SelectVendor; diff --git a/src/pages/cluster-management/components/add-worker/specify-arguments.tsx b/src/pages/cluster-management/components/add-worker/specify-arguments.tsx new file mode 100644 index 00000000..c59ba176 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/specify-arguments.tsx @@ -0,0 +1,227 @@ +import AlertInfoBlock from '@/components/alert-info/block'; +import { ExclamationCircleFilled } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Input, Switch } from 'antd'; +import { useEffect } from 'react'; +import { useAddWorkerContext } from './add-worker-context'; +import { StepNamesMap } from './config'; +import { NotesWrapper, SwitchWrapper, Tips, Title } from './constainers'; +import StepCollapse from './step-collapse'; + +const SpecifyArguments = () => { + const intl = useIntl(); + const { stepList, summary, updateField, registerField } = + useAddWorkerContext(); + const stepIndex = stepList.indexOf(StepNamesMap.SpecifyArgs) + 1; + + const workerIPConfig = summary.get('workerIPConfig') || { + enable: false, + ip: '', + required: false + }; + const modelDirConfig = summary.get('modelDirConfig') || { + enable: false, + path: '' + }; + + const setWorkerIPConfig = (config: { + enable: boolean; + ip?: string; + required?: boolean; + }) => { + updateField('workerIPConfig', { + ...workerIPConfig, + ...config + }); + }; + + const setModelDirConfig = (config: { enable: boolean; path?: string }) => { + updateField('modelDirConfig', { + ...modelDirConfig, + ...config + }); + }; + + const beforeNext = async () => { + if (workerIPConfig.enable && !workerIPConfig.ip) { + setWorkerIPConfig({ + ...workerIPConfig, + required: true + }); + + return false; + } + + return true; + }; + + useEffect(() => { + const unregister = registerField('workerIPConfig'); + return () => { + unregister(); + }; + }, []); + + useEffect(() => { + const unregister = registerField('modelDirConfig'); + return () => { + unregister(); + }; + }, []); + + useEffect(() => { + updateField('workerIPConfig', { + enable: true, + ip: '', + required: false + }); + + updateField('modelDirConfig', { + enable: false, + path: '' + }); + }, []); + + return ( + + {stepIndex}.{' '} + {intl.formatMessage({ id: 'clusters.addworker.specifyArgs' })} + + } + > +
    + {/* worker IP config */} + +
    + + {workerIPConfig.enable + ? intl.formatMessage({ + id: 'clusters.addworker.specifyWorkerIP' + }) + : intl.formatMessage({ + id: 'clusters.addworker.detectWorkerIP' + })} + + + setWorkerIPConfig({ + ...workerIPConfig, + enable: checked, + required: false + }) + } + > +
    + {workerIPConfig.enable && ( + <> + + setWorkerIPConfig({ + ...workerIPConfig, + ip: e.target.value + }) + } + /> + {workerIPConfig.required && !workerIPConfig.ip && ( + + {intl.formatMessage({ + id: 'clusters.addworker.enterWorkerIP.error' + })} + + )} + + )} + {!workerIPConfig.enable && ( + } + message={ + +
  • +
    + } + >
    + )} +
    + {/* model directory config */} + +
    + + {/* optional */} + + {intl.formatMessage({ id: 'clusters.addworker.extraVolume' })} + + + + + setModelDirConfig({ ...modelDirConfig, enable: checked }) + } + > +
    + + {modelDirConfig.enable && ( + + setModelDirConfig({ + ...modelDirConfig, + path: e.target.value + }) + } + /> + )} +
    +
    +
    + ); +}; + +export default SpecifyArguments; diff --git a/src/pages/cluster-management/components/add-worker/step-collapse.tsx b/src/pages/cluster-management/components/add-worker/step-collapse.tsx new file mode 100644 index 00000000..720e07f2 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/step-collapse.tsx @@ -0,0 +1,84 @@ +import CollapsibleContainer from '@/components/collapse-container'; +import { useIntl } from '@umijs/max'; +import { Button } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import { useAddWorkerContext } from './add-worker-context'; + +interface StepItemProps { + title: React.ReactNode; + children?: React.ReactNode; + name: string; + beforeNext?: () => Promise | void; +} + +const Box = styled.div` + border: 1px solid var(--ant-color-border); + border-radius: 4px; + &.step-collapse-open { + border-color: var(--ant-color-primary); + } +`; + +const ButtonWrapper = styled.div` + display: flex; + justify-content: center; + margin-top: 16px; + width: 100%; +`; + +const StepCollapse: React.FC = ({ + title, + children, + name = '', + beforeNext = async () => true, + ...rest +}) => { + const intl = useIntl(); + const { collapseKey, onToggle, stepList } = useAddWorkerContext(); + + const handleOnNext = async () => { + const res = await beforeNext?.(); + if (!res) return; + // find the next step and open it + const nextName = stepList[stepList.indexOf(name) + 1]; + onToggle(true, nextName); + }; + + const isLastStep = stepList.indexOf(name) === stepList.length - 1; + + return ( + + onToggle?.(open, name || '')} + {...rest} + > + {children} + {!isLastStep && ( + + + + )} + + + ); +}; + +export default StepCollapse; diff --git a/src/pages/cluster-management/components/add-worker/summary-data.tsx b/src/pages/cluster-management/components/add-worker/summary-data.tsx new file mode 100644 index 00000000..cb25b1c3 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/summary-data.tsx @@ -0,0 +1,137 @@ +import { + CheckCircleOutlined, + StopOutlined, + WarningOutlined +} from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import React from 'react'; +import { useAddWorkerContext } from './add-worker-context'; +import { StepNamesMap } from './config'; +import { ConfigWrapper, Title } from './constainers'; + +const SummaryData: React.FC = () => { + const intl = useIntl(); + const { summary, stepList } = useAddWorkerContext(); + const clusterName = summary.get('clusterName') || ''; + const workerCommand = summary.get('workerCommand') || { + label: '', + link: '', + notes: [] + }; + const workerIPConfig = summary.get('workerIPConfig') || { + enable: false, + ip: '' + }; + const modelDirConfig = summary.get('modelDirConfig') || { + enable: false, + path: '' + }; + + return ( + + + {intl.formatMessage({ id: 'clusters.addworker.configSummary' })} + +
    + {stepList.includes(StepNamesMap.SelectCluster) && ( +
    + + {intl.formatMessage({ id: 'clusters.title' })}: + + + {clusterName} + {clusterName ? ( + + ) : ( + + )} + +
    + )} +
    + + {intl.formatMessage({ id: 'clusters.addworker.gpuVendor' })}: + + + {/* for checked style */} + {workerCommand.label} + + +
    +
    + + {intl.formatMessage({ id: 'clusters.addworker.workerIP' })}: + + + {/* for invalidate style */} + {workerIPConfig.enable + ? workerIPConfig.ip + ? workerIPConfig.ip + : intl.formatMessage({ id: 'clusters.addworker.notSpecified' }) + : intl.formatMessage({ id: 'clusters.addworker.autoDetect' })} + + {workerIPConfig.enable && !workerIPConfig.ip && ( + + )} + {(!workerIPConfig.enable || workerIPConfig.ip) && ( + + )} + +
    +
    + + {intl.formatMessage({ id: 'clusters.addworker.extraVolume' })}: + + + {modelDirConfig.enable && modelDirConfig.path + ? modelDirConfig.path + : ''} + {(!modelDirConfig.path || !modelDirConfig.enable) && ( + + )} + + {modelDirConfig.enable && modelDirConfig.path && ( + + )} + +
    +
    +
    + ); +}; + +export default SummaryData; diff --git a/src/pages/cluster-management/components/add-worker/use-summary-status.ts b/src/pages/cluster-management/components/add-worker/use-summary-status.ts new file mode 100644 index 00000000..180f4d62 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/use-summary-status.ts @@ -0,0 +1,38 @@ +import { useCallback, useRef, useState } from 'react'; +import { SummaryDataKey, SummaryDataMap } from './config'; + +const useSummaryStatus = () => { + const summaryRef = useRef< + Map + >(new Map()); + const [, forceRender] = useState({}); + + // Register a key in the summary map, but do not tigger a render + const register = useCallback((key: SummaryDataKey) => { + return () => { + summaryRef.current.delete(key); + forceRender({}); + }; + }, []); + + // call it to update a initial value or change value, it will trigger a render + const update = useCallback( + (key: SummaryDataKey, value: SummaryDataMap[typeof key]) => { + const prev = summaryRef.current.get(key); + + if (prev === value) return; + + summaryRef.current.set(key, value); + forceRender({}); + }, + [] + ); + + return { + summary: summaryRef.current, + register, + update + }; +}; + +export default useSummaryStatus; diff --git a/src/pages/cluster-management/components/add-worker/vendor-notes.tsx b/src/pages/cluster-management/components/add-worker/vendor-notes.tsx new file mode 100644 index 00000000..743c19d9 --- /dev/null +++ b/src/pages/cluster-management/components/add-worker/vendor-notes.tsx @@ -0,0 +1,45 @@ +import AlertInfoBlock from '@/components/alert-info/block'; +import { ExclamationCircleFilled } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { useAddWorkerContext } from './add-worker-context'; +import { NotesWrapper } from './constainers'; + +const VendorNotes = () => { + const intl = useIntl(); + const { summary } = useAddWorkerContext(); + const workerCommand = + summary.get('workerCommand') || + ({ + label: 'N/A', + notes: [] + } as { label: string; notes: string[] }); + + return ( + } + message={ + workerCommand.notes?.length > 0 ? ( + + {workerCommand.notes.map((note: string, index: number) => ( +
  • + ))} +
    + ) : null + } + >
    + ); +}; + +export default VendorNotes; diff --git a/src/pages/cluster-management/components/provider-catalog.tsx b/src/pages/cluster-management/components/provider-catalog.tsx index 1b224b5d..2cb76c96 100644 --- a/src/pages/cluster-management/components/provider-catalog.tsx +++ b/src/pages/cluster-management/components/provider-catalog.tsx @@ -19,7 +19,6 @@ const Wrapper = styled.div<{ $cols?: number }>` const Container = styled.div` display: flex; - width: 800px; margin: 0 auto; flex-direction: column; gap: 16px; diff --git a/src/pages/cluster-management/components/register-cluster.tsx b/src/pages/cluster-management/components/register-cluster.tsx deleted file mode 100644 index 405047bc..00000000 --- a/src/pages/cluster-management/components/register-cluster.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import ScrollerModal from '@/components/scroller-modal/index'; -import React from 'react'; -import { ProviderType } from '../config'; -import AddWorkerStep from './add-worker-step'; - -type AddModalProps = { - title: string; - open: boolean; - provider: ProviderType; - registrationInfo: { - token: string; - image: string; - server_url: string; - cluster_id: number; - }; - onCancel: () => void; -}; -const AddCluster: React.FC = ({ - title, - open, - provider, - registrationInfo, - onCancel -}) => { - const handleCancel = () => { - onCancel(); - }; - - return ( - - - - ); -}; - -export default AddCluster; diff --git a/src/pages/cluster-management/hooks/use-add-worker.tsx b/src/pages/cluster-management/hooks/use-add-worker.tsx new file mode 100644 index 00000000..798ffddc --- /dev/null +++ b/src/pages/cluster-management/hooks/use-add-worker.tsx @@ -0,0 +1,96 @@ +import AddWorker from '@/pages/cluster-management/components/add-worker'; +import { useIntl } from '@umijs/max'; +import { message } from 'antd'; +import { useMemo, useState } from 'react'; +import { StepName } from '../components/add-worker/config'; +import { + ClusterStatusValueMap, + ProviderType, + ProviderValueMap +} from '../config'; +import { ClusterListItem } from '../config/types'; + +const useAddWorker = (props: { + clusterList?: Global.BaseOption[]; +}) => { + const intl = useIntl(); + const { clusterList } = props || {}; + const [stepList, setStepList] = useState([]); + const [openAddWorker, setOpenAddWorker] = useState<{ + open: boolean; + provider: ProviderType; + title: string; + cluster_id: number; + }>({ + open: false, + provider: null, + title: '', + cluster_id: 0 + }); + + const handleAddWorker = async (row: ClusterListItem) => { + try { + // set the title based on provider type + const title = + row.provider === ProviderValueMap.Docker + ? intl.formatMessage({ id: 'resources.button.create' }) + : intl.formatMessage({ id: 'clusters.button.register' }); + + if (!row?.id) return; + + setOpenAddWorker({ + open: true, + title: title, + provider: row.provider as ProviderType, + cluster_id: row.id + }); + } catch (error: any) { + message.error(error?.message || 'Failed to fetch cluster token'); + } + }; + + const handleClusterChange = async (value: number, row: ClusterListItem) => { + handleAddWorker(row); + }; + + const clusterDataList = useMemo(() => { + return clusterList + ?.map((item) => { + return { + ...item, + disabled: + item.state !== ClusterStatusValueMap.Ready || + item.provider !== ProviderValueMap.Docker + }; + }) + .filter((item) => item.state === ClusterStatusValueMap.Ready); + }, [clusterList]); + + const AddWorkerModal = ( + + setOpenAddWorker({ + open: false, + provider: null, + title: '', + cluster_id: 0 + }) + } + > + ); + + return { + handleAddWorker, + AddWorkerModal, + setStepList + }; +}; + +export default useAddWorker; diff --git a/src/pages/cluster-management/step-forms/module-registry.ts b/src/pages/cluster-management/step-forms/module-registry.ts index 6279d361..7657f524 100644 --- a/src/pages/cluster-management/step-forms/module-registry.ts +++ b/src/pages/cluster-management/step-forms/module-registry.ts @@ -1,5 +1,5 @@ import React from 'react'; -import AddWorkerStep from '../components/add-worker-step'; +import AddWorkerStep from '../components/add-worker/add-worker-step'; import ProviderCatalog from '../components/provider-catalog'; import BasicForm from './basic-form'; import WorkerPoolForm from './worker-pools-form'; diff --git a/src/pages/llmodels/components/catalog-item.tsx b/src/pages/llmodels/components/catalog-item.tsx index f0c4017f..c0f9dcd8 100644 --- a/src/pages/llmodels/components/catalog-item.tsx +++ b/src/pages/llmodels/components/catalog-item.tsx @@ -25,6 +25,10 @@ const CatalogItem: React.FC = (props) => { onClick(data); }, [data, onClick]); + const handleOnError = (e: any) => { + e.target.src = fallbackImg; + }; + const description = useMemo(() => { return ( = (props) => { >
    -
    +
    + +
    {data.name}
    diff --git a/src/pages/llmodels/style/catalog-item.less b/src/pages/llmodels/style/catalog-item.less index 1f08cd39..dafbbdbd 100644 --- a/src/pages/llmodels/style/catalog-item.less +++ b/src/pages/llmodels/style/catalog-item.less @@ -19,9 +19,6 @@ height: 32px; border-radius: var(--ant-border-radius); overflow: hidden; - background-size: contain; - background-position: center; - background-repeat: no-repeat; img { height: 32px; diff --git a/src/pages/resources/components/workers.tsx b/src/pages/resources/components/workers.tsx index 20b21c11..357701e2 100644 --- a/src/pages/resources/components/workers.tsx +++ b/src/pages/resources/components/workers.tsx @@ -1,14 +1,19 @@ -import { clusterSessionAtom } from '@/atoms/clusters'; import DeleteModal from '@/components/delete-modal'; import IconFont from '@/components/icon-font'; import { FilterBar } from '@/components/page-tools'; import useTableFetch from '@/hooks/use-table-fetch'; import PageBox from '@/pages/_components/page-box'; import { queryClusterList } from '@/pages/cluster-management/apis'; -import { useIntl, useNavigate } from '@umijs/max'; +import { DockerStepsFromWorker } from '@/pages/cluster-management/components/add-worker/config'; +import { + ClusterStatusValueMap, + ProviderValueMap +} from '@/pages/cluster-management/config'; +import { ClusterListItem } from '@/pages/cluster-management/config/types'; +import useAddWorker from '@/pages/cluster-management/hooks/use-add-worker'; +import { useIntl } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { Button, ConfigProvider, Table, message } from 'antd'; -import { useAtom } from 'jotai'; import React, { useEffect, useState } from 'react'; import NoResult from '../../_components/no-result'; import { @@ -46,11 +51,9 @@ const Workers: React.FC = () => { watch: true, API: WORKERS_API }); - const [, setClusterSession] = useAtom(clusterSessionAtom); const { MaintenanceModal, handleStopMaintenance, setOpenStatus } = - useWorkerMaintenance({ fetchData }); + useWorkerMaintenance({ fetchData: handleSearch }); - const navigate = useNavigate(); const intl = useIntl(); const [updateLabelsData, setUpdateLabelsData] = useState<{ open: boolean; @@ -60,7 +63,7 @@ const Workers: React.FC = () => { data: {} as ListItem }); const [clusterData, setClusterData] = useState<{ - list: Global.BaseOption[]; + list: Global.BaseOption[]; data: Record; }>({ list: [], @@ -73,6 +76,9 @@ const Workers: React.FC = () => { open: false, currentData: null }); + const { handleAddWorker, AddWorkerModal, setStepList } = useAddWorker({ + clusterList: clusterData.list + }); const getClusterList = async () => { try { @@ -89,7 +95,10 @@ const Workers: React.FC = () => { ); const list = res?.items?.map((item: any) => ({ label: item.name, - value: item.id + value: item.id, + id: item.id, + state: item.state, + provider: item.provider })); setClusterData({ list, @@ -170,9 +179,16 @@ const Workers: React.FC = () => { } }); - const handleAddWorker = () => { - setClusterSession({ firstAddWorker: true }); - navigate('/cluster-management/clusters/list'); + const handleOnAddWorker = () => { + let currentData = clusterData.list.find( + (item) => + item.provider === ProviderValueMap.Docker && + item.state === ClusterStatusValueMap.Ready + ); + if (!currentData) { + currentData = clusterData.list[0]; + } + handleAddWorker(currentData as ClusterListItem); }; const renderEmpty = (type?: string) => { @@ -188,7 +204,7 @@ const Workers: React.FC = () => { title={intl.formatMessage({ id: 'noresult.workers.title' })} subTitle={intl.formatMessage({ id: 'noresult.workers.subTitle' })} > - @@ -211,6 +227,7 @@ const Workers: React.FC = () => { useEffect(() => { getClusterList(); + setStepList(DockerStepsFromWorker); }, []); return ( @@ -225,6 +242,7 @@ const Workers: React.FC = () => { handleDeleteByBatch={handleDeleteBatch} handleSearch={handleSearch} handleSelectChange={handleClusterChange} + handleClickPrimary={handleOnAddWorker} handleInputChange={handleNameChange} rowSelection={rowSelection} selectOptions={clusterData.list} @@ -268,6 +286,7 @@ const Workers: React.FC = () => { } /> {MaintenanceModal} + {AddWorkerModal} ); diff --git a/src/pages/resources/config/gpu-driver.ts b/src/pages/resources/config/gpu-driver.ts index 65197f08..9eb08a2e 100644 --- a/src/pages/resources/config/gpu-driver.ts +++ b/src/pages/resources/config/gpu-driver.ts @@ -96,7 +96,7 @@ const generateHygonDockerEnvCommand = (config: { // available for Kubernetes export const generateKubernetesEnvCommand = (gpu: string) => { - const config = GPUsConfigs[gpu]; + const config = GPUsConfigs[gpu] || {}; return `kubectl get runtimeclass ${config.runtime} > /dev/null 2>&1 && echo "${config.label} runtimeclass registered" || (echo "${config.label} runtimeclass issue"; exit 1)`; }; @@ -126,6 +126,20 @@ export const dockerEnvCommandMap = { GPUsConfigs[GPUDriverMap.METAX] ) }; +const setNormalArgs = () => { + return `sudo docker run -d --name gpustack-worker \\ + --restart=unless-stopped \\ + --privileged \\ + --network=host \\ + --volume /var/run/docker.sock:/var/run/docker.sock \\ + --volume gpustack-data:/var/lib/gpustack \\`; +}; + +const setImageArgs = (params: any) => { + return `${params.image} \\ + --server-url ${params.server} \\ + --token ${params.token} \\`; +}; // avaliable for NVIDIA、AMD、MThreads const registerWorker = (params: { @@ -135,18 +149,17 @@ const registerWorker = (params: { image: string; gpu: string; workerIP?: string; + modelDir?: string; }) => { const config = GPUsConfigs[params.gpu]; - return `sudo docker run -d --name gpustack-worker \\ - --restart=unless-stopped \\ - --privileged \\ - --volume /var/run/docker.sock:/var/run/docker.sock \\ - --volume gpustack-data:/var/lib/gpustack \\ + const commonArgs = setNormalArgs(); + const imageArgs = setImageArgs(params); + // remove empty enter lines and trailing backslash + return `${commonArgs} \\ + ${params.modelDir ? `--volume ${params.modelDir}:${params.modelDir} \\` : ''} --runtime ${config.runtime} \\ - ${params.image} \\ - --server-url ${params.server} \\ - --token ${params.token} \\ - ${params.workerIP ? `--advertise-address ${params.workerIP}` : ''}`.trim(); + ${imageArgs} + ${params.workerIP ? `--advertise-address ${params.workerIP} \\` : ''}`; }; // avaliable for Ascend @@ -157,20 +170,17 @@ const registerAscendWorker = (params: { image: string; gpu: string; workerIP?: string; + modelDir?: string; }) => { const config = GPUsConfigs[params.gpu]; - return `sudo docker run -d --name gpustack-worker \\ - --restart=unless-stopped \\ - --privileged \\ - --network=host \\ - --env "ASCEND_VISIBLE_DEVICES=$(npu-smi info -m | tail -n 1 | awk '{print $1}') \\ - --volume /var/run/docker.sock:/var/run/docker.sock \\ - --volume gpustack-data:/var/lib/gpustack \\ + const commonArgs = setNormalArgs(); + const imageArgs = setImageArgs(params); + return `${commonArgs} \\ + --env "ASCEND_VISIBLE_DEVICES=$(sudo ls /dev/davinci* | head -1 | grep -o '[0-9]\\+' || echo "0") \\ --runtime ${config.runtime} \\ - ${params.image} \\ - --server-url ${params.server} \\ - --token ${params.token} \\ - ${params.workerIP ? `--advertise-address ${params.workerIP}` : ''}`.trim(); + ${imageArgs} + ${params.workerIP ? `--advertise-address ${params.workerIP} \\` : ''} + ${params.modelDir ? `--volume ${params.modelDir}:${params.modelDir}` : ''}`; }; const registerHygonWorker = (params: { @@ -180,22 +190,19 @@ const registerHygonWorker = (params: { image: string; gpu: string; workerIP?: string; + modelDir?: string; }) => { const config = GPUsConfigs[params.gpu]; - return `sudo docker run -d --name gpustack-worker \\ - --restart=unless-stopped \\ - --privileged \\ - --network=host \\ - --env ROCM_PATH=/opt/dtk \\ - --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib \\ - --volume /var/run/docker.sock:/var/run/docker.sock \\ - --volume gpustack-data:/var/lib/gpustack \\ + const commonArgs = setNormalArgs(); + const imageArgs = setImageArgs(params); + return `${commonArgs} \\ --volume /opt/hyhal:/opt/hyhal:ro \\ --volume /opt/dtk:/opt/dtk:ro \\ - ${params.image} \\ - --server-url ${params.server} \\ - --token ${params.token} \\ - ${params.workerIP ? `--advertise-address ${params.workerIP}` : ''}`.trim(); + --env ROCM_PATH=/opt/dtk \\ + --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib \\ + ${imageArgs} + ${params.workerIP ? `--advertise-address ${params.workerIP} \\` : ''} + ${params.modelDir ? `--volume ${params.modelDir}:${params.modelDir}` : ''}`; }; const registerIluvatarWorker = (params: { @@ -205,22 +212,19 @@ const registerIluvatarWorker = (params: { image: string; gpu: string; workerIP?: string; + modelDir?: string; }) => { const config = GPUsConfigs[params.gpu]; - return `sudo docker run -d --name gpustack-worker \\ - --restart=unless-stopped \\ - --privileged \\ - --network=host \\ - --volume /var/run/docker.sock:/var/run/docker.sock \\ - --volume gpustack-data:/var/lib/gpustack \\ + const commonArgs = setNormalArgs(); + const imageArgs = setImageArgs(params); + return `${commonArgs} \\ --volume /lib/modules:/lib/modules:ro \\ --volume /usr/local/corex:/usr/local/corex:ro \\ --volume /usr/bin/ixsmi:/usr/bin/ixsmi \\ --runtime ${config.runtime} \\ - ${params.image} \\ - --server-url ${params.server} \\ - --token ${params.token} \\ - ${params.workerIP ? `--advertise-address ${params.workerIP}` : ''}`.trim(); + ${imageArgs} + ${params.workerIP ? `--advertise-address ${params.workerIP} \\` : ''} + ${params.modelDir ? `--volume ${params.modelDir}:${params.modelDir}` : ''}`; }; const registerMetaXWorker = (params: { @@ -230,20 +234,17 @@ const registerMetaXWorker = (params: { image: string; gpu: string; workerIP?: string; + modelDir?: string; }) => { const config = GPUsConfigs[params.gpu]; - return `sudo docker run -d --name gpustack-worker \\ - --restart=unless-stopped \\ - --privileged \\ - --network=host \\ - --volume /var/run/docker.sock:/var/run/docker.sock \\ - --volume gpustack-data:/var/lib/gpustack \\ + const commonArgs = setNormalArgs(); + const imageArgs = setImageArgs(params); + return `${commonArgs} \\ --volume /opt/mxdriver:/opt/mxdriver:ro \\ --volume /opt/maca:/opt/maca:ro \\ - ${params.image} \\ - --server-url ${params.server} \\ - --token ${params.token} \\ - ${params.workerIP ? `--advertise-address ${params.workerIP}` : ''}`.trim(); + ${imageArgs} + ${params.workerIP ? `--advertise-address ${params.workerIP} \\` : ''} + ${params.modelDir ? `--volume ${params.modelDir}:${params.modelDir}` : ''}`; }; const registerCambriconWorker = (params: { @@ -253,20 +254,17 @@ const registerCambriconWorker = (params: { image: string; gpu: string; workerIP?: string; + modelDir?: string; }) => { const config = GPUsConfigs[params.gpu]; - return `sudo docker run -d --name gpustack-worker \\ - --restart=unless-stopped \\ - --privileged \\ - --network=host \\ - --volume /var/run/docker.sock:/var/run/docker.sock \\ - --volume gpustack-data:/var/lib/gpustack \\ + const commonArgs = setNormalArgs(); + const imageArgs = setImageArgs(params); + return `${commonArgs} \\ --volume /usr/local/neuware:/usr/local/neuware:ro \\ --volume /usr/bin/cnmon:/usr/bin/cnmon \\ - ${params.image} \\ - --server-url ${params.server} \\ - --token ${params.token} \\ - ${params.workerIP ? `--advertise-address ${params.workerIP}` : ''}`.trim(); + ${imageArgs} + ${params.workerIP ? `--advertise-address ${params.workerIP} \\` : ''} + ${params.modelDir ? `--volume ${params.modelDir}:${params.modelDir}` : ''}`; }; export const registerAddWokerCommandMap = { @@ -281,41 +279,15 @@ export const registerAddWokerCommandMap = { }; export const AddWorkerDockerNotes: Record = { - [GPUDriverMap.NVIDIA]: [ - 'clusters.addworker.nvidiaNotes-01', - 'clusters.addworker.nvidiaNotes-02' - ], - [GPUDriverMap.AMD]: [ - 'clusters.addworker.nvidiaNotes-01', - 'clusters.addworker.nvidiaNotes-02' - ], - [GPUDriverMap.MOORE_THREADS]: [ - 'clusters.addworker.nvidiaNotes-01', - 'clusters.addworker.nvidiaNotes-02' - ], - [GPUDriverMap.ASCEND]: [ - 'clusters.addworker.nvidiaNotes-01', - 'clusters.addworker.nvidiaNotes-02' - ], + [GPUDriverMap.NVIDIA]: [], + [GPUDriverMap.AMD]: [], + [GPUDriverMap.MOORE_THREADS]: [], + [GPUDriverMap.ASCEND]: [], [GPUDriverMap.HYGON]: [ - 'clusters.addworker.nvidiaNotes-01', - 'clusters.addworker.nvidiaNotes-02', 'clusters.addworker.hygonNotes', 'clusters.addworker.hygonNotes-02' ], - [GPUDriverMap.ILUVATAR]: [ - 'clusters.addworker.nvidiaNotes-01', - 'clusters.addworker.nvidiaNotes-02', - 'clusters.addworker.corexNotes' - ], - [GPUDriverMap.CAMBRICON]: [ - 'clusters.addworker.nvidiaNotes-01', - 'clusters.addworker.nvidiaNotes-02', - 'clusters.addworker.cambriconNotes' - ], - [GPUDriverMap.METAX]: [ - 'clusters.addworker.nvidiaNotes-01', - 'clusters.addworker.nvidiaNotes-02', - 'clusters.addworker.metaxNotes' - ] + [GPUDriverMap.ILUVATAR]: ['clusters.addworker.corexNotes'], + [GPUDriverMap.CAMBRICON]: ['clusters.addworker.cambriconNotes'], + [GPUDriverMap.METAX]: ['clusters.addworker.metaxNotes'] };