refactor: add worker

This commit is contained in:
jialin
2025-11-15 22:37:44 +08:00
parent 864cf76c07
commit 0f3f31ef45
39 changed files with 1746 additions and 606 deletions
+20 -10
View File
@@ -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<HTMLDivElement>(null);
const [height, setHeight] = useState(isOpen ? 'auto' : '0px');
const renderIcon = () => {
if (showExpandIcon) {
return (
<IconFont
rotate={isOpen ? 180 : 0}
type="icon-down"
style={{
cursor: disabled ? 'not-allowed' : 'pointer',
fontSize: 12
}}
/>
);
}
return null;
};
const renderTitle = () => {
if (!collapsible) {
return null;
@@ -129,16 +147,7 @@ export default function CollapsibleContainer({
<div className={styles.title} onClick={toggle}>
<div className={styles.left}>
<div className={styles.expandIcon}>
{showExpandIcon && (
<IconFont
rotate={isOpen ? 180 : 0}
type="icon-down"
style={{
cursor: disabled ? 'not-allowed' : 'pointer',
fontSize: 12
}}
/>
)}
{iconPosition === 'left' && renderIcon()}
{title && <div>{title}</div>}
</div>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
@@ -146,6 +155,7 @@ export default function CollapsibleContainer({
<div className={styles.right}>
{right && <span>{right}</span>}
{deleteBtn && <span className="del-btn">{deleteBtn}</span>}
{iconPosition === 'right' && renderIcon()}
</div>
</div>
);
+2 -1
View File
@@ -34,6 +34,7 @@ const SealSlider: React.FC<SealSwitchProps> = (props) => {
defaultValue,
checkStatus,
labelWidth,
size = 'small',
...rest
} = props;
@@ -57,7 +58,7 @@ const SealSlider: React.FC<SealSwitchProps> = (props) => {
<LabelInfo label={label} description={description}></LabelInfo>
<Switch
{...rest}
size="small"
size={size}
defaultValue={defaultValue}
style={{ marginBottom: 0, marginTop: 16, marginInline: 0 }}
value={value}
+31 -12
View File
@@ -45,20 +45,39 @@ export default {
'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.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.',
'cluster.provider.comingsoon': 'Coming soon',
'clusters.addworker.nvidiaNotes-01':
'If the worker has multiple outbound IP addresses, enter <span class="bold-text">WORKER_IP</span> to ensure that the node uses the desired IP. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
'clusters.addworker.nvidiaNotes-02':
'If the model directory already exists on the worker, please add the <span class="bold-text">--volume</span> directive to mount it.',
'clusters.addworker.hygonNotes':
'If the <span class="bold-text">/opt/hyhal</span> directory does not exist, please create a symbolic link pointing to the Hygon installed path: <span class="bold-text">/opt/hyhal</span>. Same as <span class="bold-text">/opt/dtk</span> directory.',
'clusters.addworker.corexNotes':
'If the <span class="bold-text">/lib/modules</span> directory does not exist, please create a symbolic link pointing to the Iluvatar installed path: <span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same as <span class="bold-text">/usr/local/corex</span> directory.',
'clusters.addworker.metaxNotes':
'If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, please create a symbolic link pointing to the MetaX installed path: <span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Same as <span class="bold-text">/opt/maca</span> directory.',
'clusters.addworker.cambriconNotes':
'If the <span class="bold-text">/usr/local/neuware</span> directory does not exist, please create a symbolic link pointing to the Cambricon installed path: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.',
'If a model directory already exists on the worker, you can specify the path to mount it.',
'clusters.addworker.hygonNotes': `If the <span class="bold-text">/opt/hyhal</span> directory does not exist, create a symbolic link to the Hygon installation path.
Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.corexNotes': `If the <span class="bold-text">/lib/modules</span> directory does not exist, create a symbolic link to the Iluvatar installation path:
<span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same applies to the <span class="bold-text">/usr/local/corex</span> directory.`,
'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, create a symbolic link to the MetaX installation path:
<span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Same applies to the <span class="bold-text">/opt/maca</span> directory.`,
'clusters.addworker.cambriconNotes': `If the <span class="bold-text">/usr/local/neuware</span> directory does not exist, create a symbolic link to the Cambricon installation path:
<span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.`,
'clusters.addworker.hygonNotes-02':
'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.'
'If device detection fails, try removing <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
'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 /)'
};
+3 -1
View File
@@ -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.'
};
+56 -20
View File
@@ -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 <span class="bold-text">WORKER_IP</span> to ensure that the node uses the desired IP. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
'clusters.addworker.nvidiaNotes-02':
'If the model directory already exists on the worker, please add the <span class="bold-text">--volume</span> directive to mount it.',
'clusters.addworker.hygonNotes':
'If the <span class="bold-text">/opt/hyhal</span> directory does not exist, please create a symbolic link pointing to the Hygon installed path: <span class="bold-text">/opt/hyhal</span>. Same as <span class="bold-text">/opt/dtk</span> directory.',
'clusters.addworker.corexNotes':
'If the <span class="bold-text">/lib/modules</span> directory does not exist, please create a symbolic link pointing to the Iluvatar installed path: <span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same as <span class="bold-text">/usr/local/corex</span> directory.',
'clusters.addworker.metaxNotes':
'If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, please create a symbolic link pointing to the MetaX installed path: <span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Same as <span class="bold-text">/opt/maca</span> directory.',
'clusters.addworker.cambriconNotes':
'If the <span class="bold-text">/usr/local/neuware</span> directory does not exist, please create a symbolic link pointing to the Cambricon installed path: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.',
'If a model directory already exists on the worker, you can specify the path to mount it.',
'clusters.addworker.hygonNotes': `If the <span class="bold-text">/opt/hyhal</span> directory does not exist, create a symbolic link to the Hygon installation path.
Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.corexNotes': `If the <span class="bold-text">/lib/modules</span> directory does not exist, create a symbolic link to the Iluvatar installation path:
<span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same applies to the <span class="bold-text">/usr/local/corex</span> directory.`,
'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, create a symbolic link to the MetaX installation path:
<span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Same applies to the <span class="bold-text">/opt/maca</span> directory.`,
'clusters.addworker.cambriconNotes': `If the <span class="bold-text">/usr/local/neuware</span> directory does not exist, create a symbolic link to the Cambricon installation path:
<span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.`,
'clusters.addworker.hygonNotes-02':
'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.'
'If device detection fails, try removing <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
'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 <a href={link} target="_blank">prerequisites</a> 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 <span class="bold-text">WORKER_IP</span> to ensure that the node uses the desired IP. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
// 45. 'clusters.addworker.nvidiaNotes-02': 'If the model directory already exists on the worker, please add the <span class="bold-text">--volume</span> directive to mount it.',
// 46. 'clusters.addworker.hygonNotes': 'If the <span class="bold-text">/opt/hyhal</span> directory does not exist, please create a symbolic link pointing to the Hygon installed path: <span class="bold-text">/opt/hyhal</span>. Same as <span class="bold-text">/opt/dtk</span> directory.',
// 47. 'clusters.addworker.corexNotes': 'If the <span class="bold-text">/lib/modules</span> directory does not exist, please create a symbolic link pointing to the Iluvatar installed path: <span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same as <span class
// 48. 'clusters.addworker.metaxNotes': 'If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, please create a symbolic link pointing to the MetaX installed path: <span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Same as <span class="bold-text">/opt/maca</span> directory.',
// 49. 'clusters.addworker.cambriconNotes': 'If the <span class="bold-text">/usr/local/neuware</span> directory does not exist, please create a symbolic link pointing to the Cambricon installed path: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.'
// 50. 'clusters.addworker.hygonNotes-02': 'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.'
// 44. 'clusters.addworker.nvidiaNotes-01': 'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
// 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 <span class="bold-text">/opt/hyhal</span> directory does not exist, create a symbolic link to the Hygon installation path. Same applies to the <span class="bold-text">/opt/dtk</span> directory.',
// 47. 'clusters.addworker.corexNotes': 'If the <span class="bold-text">/lib/modules</span> directory does not exist, create a symbolic link to the Iluvatar installation path: <span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same applies to the <span class="bold-text">/usr/local/corex</span> directory.',
// 48. 'clusters.addworker.metaxNotes': 'If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, create a symbolic link to the MetaX installation path: <span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Same applies to the <span class="bold-text">/opt/maca</span> directory.',
// 49. 'clusters.addworker.cambriconNotes': 'If the <span class="bold-text">/usr/local/neuware</span> directory does not exist, create a symbolic link to the Cambricon installation path: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.'
// 50. 'clusters.addworker.hygonNotes-02': 'If device detection fails, try removing <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
// 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 ==========
+5 -2
View File
@@ -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 ==========
+42 -4
View File
@@ -48,9 +48,9 @@ export default {
'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.',
'cluster.provider.comingsoon': 'Скоро будет',
'clusters.addworker.nvidiaNotes-01':
'Если рабочий узел имеет несколько исходящих IP-адресов, укажите <span class="bold-text">WORKER_IP</span>, чтобы гарантировать использование узлом нужного IP. Перепроверьте с помощью команды <span class="bold-text">hostname -I | xargs -n1</span>.',
'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
'clusters.addworker.nvidiaNotes-02':
'Если директория с моделью уже существует на рабочем узле, добавьте директиву <span class="bold-text">--volume</span> для её монтирования.',
'If a model directory already exists on the worker, you can specify the path to mount it.',
'clusters.addworker.hygonNotes':
'Если директория <span class="bold-text">/opt/hyhal</span> не существует, создайте символическую ссылку на путь установки Hygon: <span class="bold-text">/opt/hyhal</span>. Аналогично для директории <span class="bold-text">/opt/dtk</span>.',
'clusters.addworker.corexNotes':
@@ -60,9 +60,47 @@ export default {
'clusters.addworker.cambriconNotes':
'Если директория <span class="bold-text">/usr/local/neuware</span> не существует, создайте символическую ссылку на путь установки Cambricon: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.',
'clusters.addworker.hygonNotes-02':
'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.'
'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
'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 <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.'
// 1. 'clusters.addworker.hygonNotes-02': 'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
// 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 <span class="bold-text">hostname -I | xargs -n1</span>.',
// 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 ==========
+5 -2
View File
@@ -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 ==========
+23 -4
View File
@@ -40,10 +40,10 @@ export default {
'clusters.create.addworker.tips':
'在执行以下命令之前,请确保已满足 {label} 的<a href={link} target="_blank">先决条件</a>。',
'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 地址,请填写 <span class="bold-text">WORKER_IP</span>,以确保使用指定的 IP。可通过命令 <span class="bold-text">hostname -I | xargs -n1</span> 进行确认。',
@@ -58,5 +58,24 @@ export default {
'clusters.addworker.cambriconNotes':
'如果 <span class="bold-text">/usr/local/neuware</span> 目录不存在,请创建指向 Cambricon 安装路径的符号链接:<span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>。',
'clusters.addworker.hygonNotes-02':
'如果未能检测到设备,请尝试移除 <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>。'
'如果未能检测到设备,请尝试移除 <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>。',
'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(路径需以 / 开头)'
};
+2 -1
View File
@@ -101,5 +101,6 @@ export default {
'resources.worker.maintenance.remark': '备注',
'resources.worker.maintenance.remark.rules': '请输入维护备注',
'resources.worker.maintenance.tips':
'启用维护后,节点将停止调度新的模型实例部署任务,正在运行的实例不会受到影响。'
'启用维护后,节点将停止调度新的模型实例部署任务,正在运行的实例不会受到影响。',
'resources.worker.noCluster.tips': '当前无可用集群,请先创建集群再添加节点。'
};
+28 -13
View File
@@ -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 ? (
<ModuleComponent
key={key}
stepList={
extraData.provider === ProviderValueMap.Docker
? DockerStepsFromCluster
: K8sStepsFromCluter
}
registrationInfo={registrationInfo}
provider={extraData.provider}
/>
@@ -270,19 +283,21 @@ const ClusterCreate = () => {
}}
>
<ClusterSteps steps={steps} currentStep={currentStep}></ClusterSteps>
{currentStep === startStep && (
<ProviderCatalog
dataList={providerList}
height={70}
onSelect={handleSelectProvider}
clickable={true}
current={extraData.provider}
/>
)}
{renderModules()}
<Container>
<Content>{renderForms()}</Content>
</Container>
<div style={{ width: 800, margin: '0 auto' }}>
{currentStep === startStep && (
<ProviderCatalog
dataList={providerList}
height={70}
onSelect={handleSelectProvider}
clickable={true}
current={extraData.provider}
/>
)}
{renderModules()}
<Container>
<Content>{renderForms()}</Content>
</Container>
</div>
</PageContainerInner>
);
};
+12 -136
View File
@@ -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}
></AddCluster>
<AddWorker
open={openAddWorker.open}
provider={openAddWorker.provider}
onCancel={() =>
setOpenAddWorker({
open: false,
provider: null,
registrationInfo: {
token: '',
image: '',
server_url: '',
cluster_id: 0
}
})
}
registrationInfo={openAddWorker.registrationInfo}
></AddWorker>
<RegisterCluster
title={intl.formatMessage({ id: 'clusters.button.register' })}
open={registerClusterStatus.open}
provider={registerClusterStatus.provider}
registrationInfo={registerClusterStatus.registrationInfo}
onCancel={() => {
setRegisterClusterStatus({
open: false,
provider: null,
registrationInfo: {
token: '',
image: '',
server_url: '',
cluster_id: 0
}
});
}}
></RegisterCluster>
<AddPool
provider={addPoolStatus.provider}
open={addPoolStatus.open}
@@ -475,6 +350,7 @@ const Clusters: React.FC = () => {
onOk={handleSubmitWorkerPool}
></AddPool>
<DeleteModal ref={modalRef}></DeleteModal>
{AddWorkerModal}
</>
);
};
@@ -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<ViewModalProps> = ({
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 (
<HighlightCode
@@ -1,183 +0,0 @@
import AlertInfoBlock from '@/components/alert-info/block';
import {
AddWorkerDockerNotes,
GPUDriverMap
} from '@/pages/resources/config/gpu-driver';
import { BulbOutlined, ExclamationCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Alert } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { ProviderType, ProviderValueMap } from '../config';
import AddWorkerCommand from './add-worker-command';
import CheckEnvCommand from './check-env-command';
import RegisterClusterInner from './register-cluster-inner';
import SupportedGPUs from './support-gpus';
const StyledTag = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 8px;
`;
const NotesWrapper = styled.ol`
display: flex;
flex-direction: column;
gap: 8px;
font-weight: 400;
margin: 0 !important;
padding: 0 !important;
line-height: 1.25;
counter-reset: main 2 sub 0;
li {
counter-increment: sub;
margin-left: 20px !important;
&::marker {
content: counters(main, '.') '.' counter(sub) ' ';
}
}
`;
const Line = styled.div`
position: relative;
margin: 24px 0;
width: 100%;
border-top: 1px solid var(--ant-color-border);
&::before {
content: '';
position: absolute;
top: -9px;
left: 24px;
width: 16px;
height: 16px;
transform: rotate(45deg);
background-color: var(--ant-color-bg-elevated);
border-top: 1px solid var(--ant-color-border);
border-left: 1px solid var(--ant-color-border);
}
`;
const Container = styled.div`
width: 800px;
margin: 0 auto;
.command-info {
margin-top: 16px;
margin-bottom: 8px;
}
`;
const Content = styled.div`
margin-top: 24px;
margin-bottom: 16px;
`;
type AddModalProps = {
provider: ProviderType;
registrationInfo: {
token: string;
image: string;
server_url: string;
cluster_id: number;
};
};
const AddWorkerStep: React.FC<AddModalProps> = ({
provider,
registrationInfo
}) => {
const intl = useIntl();
const [currentGPU, setCurrentGPU] = React.useState<string>(
GPUDriverMap.NVIDIA
);
const [workerCommand, setWorkerCommand] = React.useState<Record<string, any>>(
{
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 (
<Container>
<SupportedGPUs
onSelect={handleSelectProvider}
current={currentGPU}
clickable={true}
/>
<Content>
<Line></Line>
<Alert
type="info"
showIcon
icon={<BulbOutlined />}
message={
<span
dangerouslySetInnerHTML={{
__html: intl.formatMessage(
{ id: 'clusters.create.addworker.tips' },
{ label: workerCommand.label, link: workerCommand.link }
)
}}
></span>
}
></Alert>
</Content>
<div className="command-info">
<StyledTag>1.</StyledTag>
{intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
</div>
<CheckEnvCommand provider={provider} currentGPU={currentGPU} />
{provider === ProviderValueMap.Kubernetes ? (
<>
<div className="command-info">
<StyledTag>2.</StyledTag>
{intl.formatMessage({ id: 'clusters.create.register.tips' })}
</div>
<RegisterClusterInner registrationInfo={registrationInfo} />
</>
) : (
<>
<div className="command-info">
<StyledTag>2.</StyledTag>
{intl.formatMessage({ id: 'clusters.create.addCommand.tips' })}
</div>
<AlertInfoBlock
maxHeight={200}
style={{ marginBottom: 8 }}
type="warning"
icon={<ExclamationCircleFilled />}
message={
workerCommand.notes?.length > 0 ? (
<NotesWrapper>
{workerCommand.notes.map((note: string, index: number) => (
<li
key={index}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: note })
}}
></li>
))}
</NotesWrapper>
) : null
}
></AlertInfoBlock>
<AddWorkerCommand
registrationInfo={registrationInfo}
currentGPU={currentGPU}
/>
</>
)}
</Container>
);
};
export default AddWorkerStep;
@@ -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<ViewModalProps> = (props) => {
const { open, onCancel, registrationInfo, provider } = props || {};
const intl = useIntl();
return (
<ScrollerModal
title={intl.formatMessage({ id: 'resources.button.create' })}
open={open}
centered={true}
onCancel={onCancel}
destroyOnHidden={true}
closeIcon={true}
maskClosable={false}
keyboard={false}
width={860}
style={{}}
maxContentHeight={'max(calc(100vh - 200px), 600px)'}
footer={null}
>
<AddWorkerStep registrationInfo={registrationInfo} provider={provider} />
</ScrollerModal>
);
};
export default AddWorker;
@@ -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<number, ClusterListItem>[];
provider: ProviderType;
stepList: string[];
onClusterChange?: (value: number, row?: any) => void;
collapseKey: Set<string>;
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<string, any>;
}
export const AddWorkerContext = createContext<AddWorkerContextProps | null>({
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;
};
@@ -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<number, ClusterListItem>[];
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<AddWorkerProps> = (props) => {
const {
registrationInfo,
provider,
clusterList,
stepList = [],
onClusterChange
} = props || {};
const intl = useIntl();
const { update, summary, register } = useSummaryStatus();
const [collapseKey, setCollapseKey] = React.useState<Set<string>>(
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 (
<AddWorkerContext.Provider
value={{
clusterList,
provider,
stepList: stepList,
collapseKey,
onToggle,
onClusterChange: handleOnClusterChange,
registrationInfo,
summary,
registerField: register,
updateField: update
}}
>
<Container>
{stepList.includes(StepNamesMap.SelectCluster) && (
<SelectCluster></SelectCluster>
)}
{stepList.includes(StepNamesMap.SelectCluster) &&
!clusterList?.length && (
<AlertInfoBlock
maxHeight={200}
style={{ marginBottom: 8 }}
type="warning"
icon={<ExclamationCircleFilled />}
message={intl.formatMessage({
id: 'resources.worker.noCluster.tips'
})}
></AlertInfoBlock>
)}
{/* 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)) && (
<>
<SelectVendor></SelectVendor>
<CheckEnvironment></CheckEnvironment>
{provider === ProviderValueMap.Kubernetes && (
<K8sRunCommand></K8sRunCommand>
)}
{provider === ProviderValueMap.Docker && (
<>
<SpecifyArguments></SpecifyArguments>
<DockerRunCommand></DockerRunCommand>
</>
)}
</>
)}
</Container>
</AddWorkerContext.Provider>
);
};
export default AddWorkerSteps;
@@ -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 (
<StepCollapse
name={StepNamesMap.CheckEnv}
title={
<Title>
{stepIndex}.{' '}
{intl.formatMessage({ id: 'clusters.addworker.checkEnv' })}
</Title>
}
>
<Alert
type="info"
showIcon
icon={<BulbOutlined />}
style={{
marginBottom: 8
}}
message={
<span
dangerouslySetInnerHTML={{
__html: intl.formatMessage(
{ id: 'clusters.create.addworker.tips' },
{ label: workerCommand.label, link: workerCommand.link }
)
}}
></span>
}
></Alert>
<Tips style={{ marginBottom: 8, color: 'var(--ant-color-text)' }}>
{intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
</Tips>
<CheckEnvCommand provider={provider} currentGPU={currentGPU} />
</StepCollapse>
);
};
export default CheckEnvironment;
@@ -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;
@@ -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;
`;
@@ -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 (
<StepCollapse
name={StepNamesMap.RunCommand}
title={
<Title>
{stepIndex}.{' '}
{intl.formatMessage({ id: 'clusters.addworker.runCommand' })}
</Title>
}
>
<SummaryData></SummaryData>
<VendorNotes></VendorNotes>
<Tips
style={{
marginBottom: 8,
color: 'var(--ant-color-text)'
}}
>
{intl.formatMessage({
id: 'clusters.create.addCommand.tips'
})}
</Tips>
<AddWorkerCommand
registrationInfo={registrationInfo}
workerIP={workerIPConfig.enable ? workerIPConfig.ip : ''}
modelDir={modelDirConfig.enable ? modelDirConfig.path : ''}
currentGPU={currentGPU}
/>
</StepCollapse>
);
};
export default DockerRunCommand;
@@ -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<number, ClusterListItem>[];
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<AddWorkerProps> = (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 (
<ScrollerModal
title={title}
open={open}
centered={true}
onCancel={onCancel}
destroyOnHidden={true}
closeIcon={true}
maskClosable={false}
keyboard={false}
width={860}
style={{}}
maxContentHeight={'max(calc(100vh - 200px), 600px)'}
footer={null}
>
<AddWorkerStep
stepList={stepList}
provider={provider}
clusterList={clusterList}
onClusterChange={handleOnClusterChange}
registrationInfo={registrationInfo}
></AddWorkerStep>
</ScrollerModal>
);
};
export default AddWorker;
@@ -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 (
<StepCollapse
name={StepNamesMap.RunCommand}
title={
<Title>
{stepIndex}.{' '}
{intl.formatMessage({ id: 'clusters.addworker.runCommand' })}
</Title>
}
>
<Tips
style={{
marginBottom: 8,
color: 'var(--ant-color-text)'
}}
>
{intl.formatMessage({
id: 'clusters.create.addCommand.tips'
})}
</Tips>
<RegisterClusterInner registrationInfo={registrationInfo} />
</StepCollapse>
);
};
export default K8sRunCommand;
@@ -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 (
<SwitchWrapper>
<div className="button">
<span style={{ color: 'var(--ant-color-text)', fontWeight: 500 }}>
Network Interface
</span>
<Switch
checked={networkInterface.enable}
onChange={(checked) =>
setNetworkInterface({
...networkInterface,
enable: checked
})
}
></Switch>
</div>
<Tips>
Enter the NIC name to use for distributed inference (e.g., mlx5_0).
</Tips>
{networkInterface.enable && (
<>
<Input
style={{ width: '100%' }}
placeholder="Enter network interface"
onChange={(e) =>
setNetworkInterface({
...networkInterface,
name: e.target.value
})
}
/>
</>
)}
</SwitchWrapper>
);
};
export default NetworkConfig;
@@ -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 (
<StepCollapse
name={StepNamesMap.SelectCluster}
title={
<div>
<Title>
{stepIndex}.{' '}
{intl.formatMessage({ id: 'clusters.addworker.selectCluster' })}
</Title>
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
{intl.formatMessage({
id: 'clusters.addworker.selectCluster.tips'
})}
</span>
</div>
}
>
<BaseSelect
defaultValue={registrationInfo.cluster_id}
options={clusterList}
value={clusterId}
onChange={onClusterChange}
style={{ width: '100%' }}
/>
</StepCollapse>
);
};
export default SelectCluster;
@@ -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<string>(
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 (
<StepCollapse
name={StepNamesMap.SelectGPU}
title={
<Title>
{stepIndex}.{' '}
{intl.formatMessage({ id: 'clusters.addworker.selectGPU' })}
</Title>
}
>
<SupportedGPUs
onSelect={handleSelectProvider}
current={currentGPU}
clickable={true}
/>
</StepCollapse>
);
};
export default SelectVendor;
@@ -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 (
<StepCollapse
beforeNext={beforeNext}
name={StepNamesMap.SpecifyArgs}
title={
<Title>
{stepIndex}.{' '}
{intl.formatMessage({ id: 'clusters.addworker.specifyArgs' })}
</Title>
}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
marginBottom: 8
}}
>
{/* worker IP config */}
<SwitchWrapper>
<div className="button">
<span style={{ color: 'var(--ant-color-text)', fontWeight: 500 }}>
{workerIPConfig.enable
? intl.formatMessage({
id: 'clusters.addworker.specifyWorkerIP'
})
: intl.formatMessage({
id: 'clusters.addworker.detectWorkerIP'
})}
</span>
<Switch
checked={workerIPConfig.enable}
onChange={(checked) =>
setWorkerIPConfig({
...workerIPConfig,
enable: checked,
required: false
})
}
></Switch>
</div>
{workerIPConfig.enable && (
<>
<Input
style={{ width: '100%' }}
placeholder={intl.formatMessage({
id: 'clusters.addworker.enterWorkerIP'
})}
value={workerIPConfig.ip}
onChange={(e) =>
setWorkerIPConfig({
...workerIPConfig,
ip: e.target.value
})
}
/>
{workerIPConfig.required && !workerIPConfig.ip && (
<Tips
style={{
color: 'var(--ant-color-error)'
}}
>
{intl.formatMessage({
id: 'clusters.addworker.enterWorkerIP.error'
})}
</Tips>
)}
</>
)}
{!workerIPConfig.enable && (
<AlertInfoBlock
maxHeight={200}
contentStyle={{
paddingLeft: 0
}}
style={{ marginBottom: 8 }}
type="warning"
icon={<ExclamationCircleFilled />}
message={
<NotesWrapper>
<li
style={{
marginLeft: '0 !important',
listStyleType: 'none'
}}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: 'clusters.addworker.nvidiaNotes-01'
})
}}
></li>
</NotesWrapper>
}
></AlertInfoBlock>
)}
</SwitchWrapper>
{/* model directory config */}
<SwitchWrapper>
<div className="button">
<span style={{ color: 'var(--ant-color-text)', fontWeight: 500 }}>
{/* optional */}
<span>
{intl.formatMessage({ id: 'clusters.addworker.extraVolume' })}
</span>
</span>
<Switch
checked={modelDirConfig.enable}
onChange={(checked) =>
setModelDirConfig({ ...modelDirConfig, enable: checked })
}
></Switch>
</div>
<Tips
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: 'clusters.addworker.nvidiaNotes-02'
})
}}
></Tips>
{modelDirConfig.enable && (
<Input
style={{ width: '100%' }}
value={modelDirConfig.path}
placeholder={intl.formatMessage({
id: 'clusters.addworker.extraVolume.holder'
})}
onChange={(e) =>
setModelDirConfig({
...modelDirConfig,
path: e.target.value
})
}
/>
)}
</SwitchWrapper>
</div>
</StepCollapse>
);
};
export default SpecifyArguments;
@@ -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<boolean> | 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<StepItemProps> = ({
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 (
<Box
className={
collapseKey?.has(name) ? 'step-collapse-open' : 'step-collapse'
}
>
<CollapsibleContainer
collapsible={true}
open={collapseKey?.has(name)}
iconPosition="right"
styles={{
body: collapseKey?.has(name) ? { padding: 16 } : {},
content: { paddingTop: 0 },
header: {
backgroundColor: 'unset'
}
}}
title={title}
onToggle={(open) => onToggle?.(open, name || '')}
{...rest}
>
{children}
{!isLastStep && (
<ButtonWrapper>
<Button type="primary" onClick={handleOnNext}>
{intl.formatMessage({ id: 'common.button.next' })}
</Button>
</ButtonWrapper>
)}
</CollapsibleContainer>
</Box>
);
};
export default StepCollapse;
@@ -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 (
<ConfigWrapper>
<Title>
{intl.formatMessage({ id: 'clusters.addworker.configSummary' })}
</Title>
<div className="config-content">
{stepList.includes(StepNamesMap.SelectCluster) && (
<div className="item">
<span className="label">
{intl.formatMessage({ id: 'clusters.title' })}:
</span>
<span className="value">
{clusterName}
{clusterName ? (
<CheckCircleOutlined
style={{
color: 'var(--ant-color-success)',
marginLeft: 4
}}
/>
) : (
<WarningOutlined
style={{
color: 'var(--ant-color-warning)',
marginLeft: 4
}}
/>
)}
</span>
</div>
)}
<div className="item">
<span className="label">
{intl.formatMessage({ id: 'clusters.addworker.gpuVendor' })}:
</span>
<span className="value">
{/* for checked style */}
{workerCommand.label}
<CheckCircleOutlined
style={{
color: 'var(--ant-color-success)',
marginLeft: 4
}}
/>
</span>
</div>
<div className="item">
<span className="label">
{intl.formatMessage({ id: 'clusters.addworker.workerIP' })}:
</span>
<span className="value">
{/* 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 && (
<WarningOutlined
style={{
color: 'var(--ant-color-warning)',
marginLeft: 4
}}
/>
)}
{(!workerIPConfig.enable || workerIPConfig.ip) && (
<CheckCircleOutlined
style={{
color: 'var(--ant-color-success)',
marginLeft: 4
}}
/>
)}
</span>
</div>
<div className="item">
<span className="label">
{intl.formatMessage({ id: 'clusters.addworker.extraVolume' })}:
</span>
<span className="value">
{modelDirConfig.enable && modelDirConfig.path
? modelDirConfig.path
: ''}
{(!modelDirConfig.path || !modelDirConfig.enable) && (
<StopOutlined
style={{
color: 'var(--ant-color-text-tertiary)'
}}
/>
)}
{modelDirConfig.enable && modelDirConfig.path && (
<CheckCircleOutlined
style={{
color: 'var(--ant-color-success)',
marginLeft: 4
}}
/>
)}
</span>
</div>
</div>
</ConfigWrapper>
);
};
export default SummaryData;
@@ -0,0 +1,38 @@
import { useCallback, useRef, useState } from 'react';
import { SummaryDataKey, SummaryDataMap } from './config';
const useSummaryStatus = () => {
const summaryRef = useRef<
Map<SummaryDataKey, SummaryDataMap[SummaryDataKey]>
>(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;
@@ -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 (
<AlertInfoBlock
maxHeight={200}
title={`Notes for ${workerCommand.label} Device`}
style={{ marginBottom: 8 }}
type="warning"
contentStyle={{
paddingLeft: 16
}}
icon={<ExclamationCircleFilled />}
message={
workerCommand.notes?.length > 0 ? (
<NotesWrapper>
{workerCommand.notes.map((note: string, index: number) => (
<li
key={index}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: note })
}}
></li>
))}
</NotesWrapper>
) : null
}
></AlertInfoBlock>
);
};
export default VendorNotes;
@@ -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;
@@ -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<AddModalProps> = ({
title,
open,
provider,
registrationInfo,
onCancel
}) => {
const handleCancel = () => {
onCancel();
};
return (
<ScrollerModal
title={title}
open={open}
onCancel={handleCancel}
destroyOnHidden={true}
closeIcon={true}
maskClosable={false}
keyboard={false}
width={860}
footer={false}
>
<AddWorkerStep registrationInfo={registrationInfo} provider={provider} />
</ScrollerModal>
);
};
export default AddCluster;
@@ -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<number, ClusterListItem>[];
}) => {
const intl = useIntl();
const { clusterList } = props || {};
const [stepList, setStepList] = useState<StepName[]>([]);
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 = (
<AddWorker
title={openAddWorker.title}
stepList={stepList || []}
open={openAddWorker.open}
provider={openAddWorker.provider}
clusterList={clusterDataList}
cluster_id={openAddWorker.cluster_id}
onClusterChange={handleClusterChange}
onCancel={() =>
setOpenAddWorker({
open: false,
provider: null,
title: '',
cluster_id: 0
})
}
></AddWorker>
);
return {
handleAddWorker,
AddWorkerModal,
setStepList
};
};
export default useAddWorker;
@@ -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';
+11 -6
View File
@@ -25,6 +25,10 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
onClick(data);
}, [data, onClick]);
const handleOnError = (e: any) => {
e.target.src = fallbackImg;
};
const description = useMemo(() => {
return (
<Typography.Paragraph
@@ -59,12 +63,13 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
>
<div className="content">
<div className="title">
<div
className="img"
style={{
backgroundImage: `url(${data.icon}),url(${fallbackImg})`
}}
></div>
<div className="img">
<img
src={data.icon || fallbackImg}
alt=""
onError={handleOnError}
/>
</div>
<AutoTooltip ghost>{data.name}</AutoTooltip>
</div>
</div>
@@ -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;
+31 -12
View File
@@ -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<number>[];
list: Global.BaseOption<number, ClusterListItem>[];
data: Record<number, string>;
}>({
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' })}
>
<Button type="primary" onClick={handleAddWorker}>
<Button type="primary" onClick={handleOnAddWorker}>
{intl.formatMessage({ id: 'noresult.workers.button.add' })}
</Button>
</NoResult>
@@ -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}
</PageBox>
</>
);
+68 -96
View File
@@ -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<string, string[]> = {
[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']
};