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
+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']
};