style: add worker ux
This commit is contained in:
@@ -25,7 +25,7 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({ registrationInfo }) => {
|
||||
return (
|
||||
<HighlightCode
|
||||
theme="dark"
|
||||
code={code.replace(/\\/g, '')}
|
||||
code={code}
|
||||
copyValue={code}
|
||||
lang="bash"
|
||||
></HighlightCode>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BulbOutlined } 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';
|
||||
@@ -9,7 +11,42 @@ import SupportedHardware from './support-hardware';
|
||||
const Title = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
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-container);
|
||||
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: 24px;
|
||||
margin-bottom: 8px;
|
||||
// font-weight: 500;
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
type AddModalProps = {
|
||||
@@ -26,19 +63,57 @@ const AddWorkerStep: React.FC<AddModalProps> = ({
|
||||
registrationInfo
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [currentProvider, setCurrentProvider] = React.useState<string>('cuda');
|
||||
const [workerCommand, setWorkerCommand] = React.useState<Record<string, any>>(
|
||||
{
|
||||
label: 'NVIDIA CUDA',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda'
|
||||
}
|
||||
);
|
||||
|
||||
const handleSelectProvider = (provider: string, item: any) => {
|
||||
setCurrentProvider(provider);
|
||||
setWorkerCommand(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{intl.formatMessage({ id: 'clusters.create.execCommand' })}</Title>
|
||||
<Container>
|
||||
<Title>
|
||||
{intl.formatMessage({ id: 'clusters.create.supportedGpu' })}
|
||||
</Title>
|
||||
<SupportedHardware
|
||||
onSelect={handleSelectProvider}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
{provider === ProviderValueMap.Custom && (
|
||||
<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">
|
||||
{intl.formatMessage({ id: 'clusters.create.addCommand.tips' })}
|
||||
</div>
|
||||
{provider === ProviderValueMap.Kubernetes ? (
|
||||
<RegisterClusterInner registrationInfo={registrationInfo} />
|
||||
) : (
|
||||
<AddWorkerCommand registrationInfo={registrationInfo} />
|
||||
)}
|
||||
<Title style={{ marginTop: 32 }}>
|
||||
{intl.formatMessage({ id: 'clusters.create.supportedGpu' })}
|
||||
</Title>
|
||||
<SupportedHardware />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { fromClusterCreationAtom } from '@/atoms/clusters';
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Link, useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@@ -14,7 +16,7 @@ import { useProviderRegions } from '../hooks/use-provider-regions';
|
||||
type OptionData = {
|
||||
label: string;
|
||||
datacenter: string;
|
||||
value: string;
|
||||
value: string | number;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
@@ -70,14 +72,24 @@ const NotFoundContent: React.FC<{ loading: boolean }> = ({ loading }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const optionRender = (
|
||||
option: Global.BaseOption<
|
||||
number,
|
||||
{
|
||||
data: OptionData;
|
||||
}
|
||||
>
|
||||
): React.ReactNode => {
|
||||
const NotFoundCredentialContent: React.FC = () => {
|
||||
const [, setFromClusterCreation] = useAtom(fromClusterCreationAtom);
|
||||
const intl = useIntl();
|
||||
const handleOnClick = () => {
|
||||
console.log('click add credential');
|
||||
setFromClusterCreation(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<NoContent>
|
||||
<Link to={'/cluster-management/credentials'} onClick={handleOnClick}>
|
||||
{intl.formatMessage({ id: 'clusters.button.addCredential' })}
|
||||
</Link>
|
||||
</NoContent>
|
||||
);
|
||||
};
|
||||
|
||||
const optionRender = (option: any): React.ReactNode => {
|
||||
const { value } = option;
|
||||
const data = option.data!;
|
||||
|
||||
@@ -127,8 +139,8 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
|
||||
}, [credentialID]);
|
||||
|
||||
const labelRender = (props: {
|
||||
label: string;
|
||||
value: string;
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
}): React.ReactNode => {
|
||||
const data = regions.find((item) => item.value === props.value);
|
||||
if (!data) return props.label;
|
||||
@@ -164,6 +176,7 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
|
||||
]}
|
||||
>
|
||||
<SealSelect
|
||||
notFoundContent={<NotFoundCredentialContent />}
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({ id: 'clusters.credential.title' })}
|
||||
required
|
||||
|
||||
@@ -72,7 +72,7 @@ const OptionItem = styled.div.attrs({
|
||||
const DescriptionWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
gap: 4px 8px;
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
@@ -94,34 +94,48 @@ const NotFoundImageContent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const RenderInstanceOption = (option: any) => {
|
||||
const { data, styles } = option;
|
||||
const RenderLabel = (data: {
|
||||
label: React.ReactNode;
|
||||
vendor: string;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const { label, vendor, style } = data;
|
||||
return (
|
||||
<div style={style} className="flex-center gap-8">
|
||||
<IconFont type={_.get(vendorIconMap, vendor, 'icon-gpu1')}></IconFont>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const RenderOption = (option: any) => {
|
||||
const { data = {}, styles } = option;
|
||||
const entries = Object.entries(data?.specInfo || {});
|
||||
return (
|
||||
<CardContainer
|
||||
key={data.value}
|
||||
header={
|
||||
<span>
|
||||
<IconFont
|
||||
type={_.get(vendorIconMap, data.vendor, 'icon-gpu1')}
|
||||
className="m-r-8"
|
||||
></IconFont>
|
||||
{data.description}
|
||||
</span>
|
||||
<RenderLabel
|
||||
label={data.description || data.label}
|
||||
vendor={data.vendor}
|
||||
style={styles?.header}
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<DescriptionWrapper style={{ ...(styles?.description || {}) }}>
|
||||
{Object.entries(data?.specInfo)
|
||||
.filter(([key, value]) => value)
|
||||
.map(([key, value]) => (
|
||||
<OptionItem key={key}>
|
||||
<span className="label">
|
||||
{_.get(instanceTypeFieldMap, key, key)}:
|
||||
</span>
|
||||
<span className="value">{value as string}</span>
|
||||
</OptionItem>
|
||||
))}
|
||||
</DescriptionWrapper>
|
||||
entries.length > 0 && (
|
||||
<DescriptionWrapper style={{ ...styles?.description }}>
|
||||
{Object.entries(data?.specInfo)
|
||||
.filter(([key, value]) => value)
|
||||
.map(([key, value]) => (
|
||||
<OptionItem key={key}>
|
||||
<span className="label">
|
||||
{_.get(instanceTypeFieldMap, key, key)}:
|
||||
</span>
|
||||
<span className="value">{value as string}</span>
|
||||
</OptionItem>
|
||||
))}
|
||||
</DescriptionWrapper>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -186,19 +200,45 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
const imageLabelRender = (data: { label: string; value: string }) => {
|
||||
console.log('imageLabelRender========', data);
|
||||
const imageLabelRender = (data: {
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
}) => {
|
||||
if (action === PageAction.EDIT) {
|
||||
return currentData?.image_name || currentData?.os_image;
|
||||
console.log('imageLabelRender========::', action, currentData, data);
|
||||
// return currentData?.image_name || currentData?.os_image;
|
||||
const vendor = _.split(currentData?.image_name || '', ' ')[0];
|
||||
const iconType = _.get(vendorIconMap, vendor.toLowerCase());
|
||||
return (
|
||||
<div className="flex-center gap-8">
|
||||
{iconType && <IconFont type={iconType}></IconFont>}
|
||||
{currentData?.image_name || currentData?.os_image}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return data.label;
|
||||
const selectImage = osImageList.find((item) => item.value === data.value);
|
||||
console.log('imageLabelRender========::', selectImage);
|
||||
if (selectImage) {
|
||||
return (
|
||||
<RenderLabel label={data.label} vendor={selectImage.vendor || ''} />
|
||||
);
|
||||
}
|
||||
return data.value;
|
||||
};
|
||||
|
||||
const instanceLabelRender = (data: { label: string; value: string }) => {
|
||||
if (action === PageAction.EDIT) {
|
||||
return currentData?.instance_spec?.label || currentData?.instance_type;
|
||||
}
|
||||
return data.label;
|
||||
const instanceLabelRender = (data: {
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
}) => {
|
||||
const currentInstanceSpec =
|
||||
instanceTypeList.find((item) => item.value === data.value) ||
|
||||
instanceSpec;
|
||||
return (
|
||||
<RenderLabel
|
||||
label={data.label || currentInstanceSpec?.label || data.value}
|
||||
vendor={currentInstanceSpec?.vendor || ''}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleOsImageChange = (value: string) => {
|
||||
@@ -329,7 +369,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
required
|
||||
options={instanceTypeList}
|
||||
disabled={action === PageAction.EDIT}
|
||||
optionRender={RenderInstanceOption}
|
||||
optionRender={RenderOption}
|
||||
onChange={handleInstanceTypeChange}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
@@ -383,11 +423,16 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
>
|
||||
<AutoComplete
|
||||
showSearch
|
||||
notFoundContent={<NotFoundImageContent />}
|
||||
onChange={handleOsImageChange}
|
||||
filterOption={filterImageOption}
|
||||
optionRender={RenderInstanceOption}
|
||||
optionRender={(option) =>
|
||||
RenderOption({
|
||||
...option,
|
||||
styles: { header: { marginBlock: 5 } }
|
||||
})
|
||||
}
|
||||
labelRender={imageLabelRender}
|
||||
placeholder={currentData?.image_name}
|
||||
options={osImageList}
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({
|
||||
|
||||
@@ -4,14 +4,20 @@ import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ProviderType } from '../config';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
const Wrapper = styled.div<{ $cols?: number }>`
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 22px;
|
||||
grid-template-columns: repeat(${(props) => props.$cols || 3}, 1fr);
|
||||
gap: 16px;
|
||||
.template-card-wrapper {
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
width: 800px;
|
||||
margin: 0 auto;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`;
|
||||
@@ -22,19 +28,20 @@ const Title = styled.span`
|
||||
justify-content: space-between;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
margin-block: 16px 24px;
|
||||
margin-block: 20px 16px;
|
||||
`;
|
||||
|
||||
interface ProviderCatalogProps {
|
||||
onSelect?: (provider: ProviderType) => void;
|
||||
currentProvider?: ProviderType;
|
||||
onSelect?: (provider: string, item: any) => void;
|
||||
cols?: number;
|
||||
currentProvider?: ProviderType | string;
|
||||
clickable?: boolean;
|
||||
dataList: {
|
||||
label: string;
|
||||
key: string;
|
||||
locale?: boolean;
|
||||
disabled?: boolean;
|
||||
icon: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
description?: string;
|
||||
group?: string;
|
||||
}[];
|
||||
@@ -42,6 +49,7 @@ interface ProviderCatalogProps {
|
||||
|
||||
const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
||||
onSelect,
|
||||
cols = 3,
|
||||
dataList,
|
||||
clickable,
|
||||
currentProvider
|
||||
@@ -66,12 +74,12 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
||||
{groupName !== 'default' && (
|
||||
<Title>{intl.formatMessage({ id: groupName })}</Title>
|
||||
)}
|
||||
<Wrapper>
|
||||
<Wrapper $cols={cols}>
|
||||
{items?.map((action) => (
|
||||
<Card
|
||||
height="auto"
|
||||
height="80px"
|
||||
key={action.key}
|
||||
onClick={() => onSelect?.(action.key as ProviderType)}
|
||||
onClick={() => onSelect?.(action.key as string, action)}
|
||||
active={currentProvider === action.key}
|
||||
disabled={action.disabled}
|
||||
clickable={clickable}
|
||||
@@ -82,7 +90,7 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
||||
}
|
||||
icon={action.icon}
|
||||
>
|
||||
{action.description || 'This is a description'}
|
||||
{action.description}
|
||||
</Card>
|
||||
))}
|
||||
</Wrapper>
|
||||
|
||||
@@ -4,8 +4,8 @@ import hyponPNG from '@/assets/logo/hygon.png';
|
||||
import iluvatarWEBP from '@/assets/logo/Iluvatar.png';
|
||||
import metaxLogo from '@/assets/logo/metax.png';
|
||||
import moorePNG from '@/assets/logo/moore _threads.png';
|
||||
import nvidiaLogo from '@/assets/logo/nvidia.png';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import styled from 'styled-components';
|
||||
import ProviderCatalog from './provider-catalog';
|
||||
|
||||
const ProviderImage = ({ src, showBg }: { src: string; showBg?: boolean }) => {
|
||||
@@ -24,15 +24,19 @@ const supportedHardPlatforms = [
|
||||
{
|
||||
label: 'NVIDIA CUDA',
|
||||
value: 'cuda',
|
||||
key: 'nvidia',
|
||||
description: '',
|
||||
key: 'cuda',
|
||||
locale: false,
|
||||
icon: <ProviderImage src={nvidiaLogo} showBg />
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda',
|
||||
icon: <IconFont type="icon-nvidia2" style={{ fontSize: 32 }} />
|
||||
},
|
||||
{
|
||||
label: 'AMD ROCm',
|
||||
description: '',
|
||||
value: 'rocm',
|
||||
key: 'amd',
|
||||
key: 'rocm',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#amd-rocm',
|
||||
icon: (
|
||||
<IconFont
|
||||
type="icon-amd"
|
||||
@@ -42,30 +46,38 @@ const supportedHardPlatforms = [
|
||||
},
|
||||
{
|
||||
label: 'Ascend CANN',
|
||||
description: '',
|
||||
value: 'npu',
|
||||
key: 'ascend',
|
||||
key: 'npu',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#ascend-cann',
|
||||
icon: <ProviderImage src={ascendLogo} showBg />
|
||||
},
|
||||
{
|
||||
label: 'Hygon DTK',
|
||||
description: '',
|
||||
value: 'dcu',
|
||||
key: 'hygon',
|
||||
key: 'dcu',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#hygon-dtk',
|
||||
icon: <ProviderImage src={hyponPNG} />
|
||||
},
|
||||
{
|
||||
label: 'Moore Threads MUSA',
|
||||
label: 'Moore Threads',
|
||||
description: '',
|
||||
value: 'musa',
|
||||
key: 'musa',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#moore-threads-musa',
|
||||
icon: <ProviderImage src={moorePNG} />
|
||||
},
|
||||
{
|
||||
label: 'Iluvatar Corex',
|
||||
description: '',
|
||||
value: 'corex',
|
||||
key: 'corex',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#iluvatar-corex',
|
||||
icon: <ProviderImage src={iluvatarWEBP} showBg />
|
||||
},
|
||||
{
|
||||
@@ -73,6 +85,7 @@ const supportedHardPlatforms = [
|
||||
value: 'cambricon',
|
||||
key: 'cambricon',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#cambricon-mlu',
|
||||
icon: <ProviderImage src={CambriconPNG} />
|
||||
},
|
||||
{
|
||||
@@ -84,9 +97,50 @@ const supportedHardPlatforms = [
|
||||
}
|
||||
];
|
||||
|
||||
const SupportedHardware = () => {
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
`;
|
||||
|
||||
const renderHeader = (item: any) => {
|
||||
return (
|
||||
<ProviderCatalog dataList={supportedHardPlatforms} clickable={false} />
|
||||
<Header>
|
||||
<span className="icon">{item.icon}</span>
|
||||
<span className="label">{item.label}</span>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
const dataList = supportedHardPlatforms.map((item) => {
|
||||
return {
|
||||
label: renderHeader(item),
|
||||
value: item.value,
|
||||
key: item.key,
|
||||
locale: item.locale
|
||||
};
|
||||
});
|
||||
|
||||
interface SupportedHardwareProps {
|
||||
onSelect?: (provider: string, item: any) => void;
|
||||
currentProvider?: string;
|
||||
}
|
||||
|
||||
const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||
onSelect,
|
||||
currentProvider
|
||||
}) => {
|
||||
return (
|
||||
<ProviderCatalog
|
||||
onSelect={onSelect}
|
||||
currentProvider={currentProvider}
|
||||
dataList={supportedHardPlatforms}
|
||||
clickable={true}
|
||||
cols={4}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user