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
@@ -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;