style: add worker ux

This commit is contained in:
jialin
2025-09-16 11:26:17 +08:00
parent f1bb305cbc
commit d9d5ca95df
26 changed files with 436 additions and 119 deletions
+12
View File
@@ -17,6 +17,11 @@ export const regionInstanceTypeListAtom = atom<
{
label: string;
value: string;
description: string;
specInfo: Record<string, any>;
vendor: string;
available: boolean;
regions: string[];
}[]
>([]);
@@ -24,6 +29,11 @@ export const regionOSImageListAtom = atom<
{
label: string;
value: string;
name: string;
description: string;
vendor: string;
specInfo: Record<string, any>;
regions: string[];
}[]
>([]);
@@ -42,3 +52,5 @@ export const allRegionInstanceTypeListAtom = atom<
regions: string[];
}[]
>([]);
export const fromClusterCreationAtom = atom(false);
+2 -2
View File
@@ -23,7 +23,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
legendOptions,
gridOptions,
titleOptions,
showArea
showArea = 0.25
} = props;
const {
grid,
@@ -87,7 +87,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
const data = _.map(seriesData, (item: any) => {
const colors = genColors({
color: item.color,
alpha1: 0.5,
alpha1: 0.25,
alpha2: 0.1
});
return {
+1 -1
View File
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
// import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_5jrvj6qs39.js'
scriptUrl: '//at.alicdn.com/t/c/font_4613488_84hy7h371ty.js'
});
export default IconFont;
@@ -1,8 +1,10 @@
import { AutoComplete, Form, Spin } from 'antd';
import type { AutoCompleteProps } from 'antd/lib';
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import { SealFormItemProps } from './types';
import Wrapper from './wrapper';
import AutoCompleteLabel from './wrapper/auto-complete-label';
import SelectWrapper from './wrapper/select';
const SealAutoComplete: React.FC<
@@ -24,6 +26,7 @@ const SealAutoComplete: React.FC<
...rest
} = props;
const [isFocus, setIsFocus] = useState(false);
const [_isFocus_, _setIsFocus_] = useState(false);
const inputRef = useRef<any>(null);
let status = '';
if (isInFormItems) {
@@ -41,6 +44,7 @@ const SealAutoComplete: React.FC<
if (!props.disabled && !isFocus) {
inputRef.current?.focus?.();
setIsFocus(true);
_setIsFocus_(true);
}
};
@@ -49,17 +53,20 @@ const SealAutoComplete: React.FC<
if (trim) {
value = value?.trim?.();
}
_setIsFocus_(false);
props.onChange?.(value, option);
};
const handleOnFocus = (e: any) => {
setIsFocus(true);
_setIsFocus_(true);
props.onFocus?.(e);
};
const handleOnBlur = (e: any) => {
if (!props.value) {
setIsFocus(false);
_setIsFocus_(false);
}
e.target.value = e.target.value?.trim?.();
props.onBlur?.(e);
@@ -79,6 +86,39 @@ const SealAutoComplete: React.FC<
return addAfter;
};
const renderAutoCompleteLabel = () => {
console.log(
'renderAutoCompleteLabel===',
props.value,
props.showSearch,
_isFocus_,
isFocus
);
if (!props.showSearch || _isFocus_) {
return null;
}
let selectItem: {
label: React.ReactNode;
value: string | number;
} = props.options?.find((item: any) => item.value === props.value) as {
label: React.ReactNode;
value: string | number;
};
if (!selectItem) {
selectItem = { label: props.value, value: props.value };
}
return (
<AutoCompleteLabel
className={classNames({
disabled: props.disabled
})}
>
{props.labelRender ? props.labelRender(selectItem) : selectItem.label}
</AutoCompleteLabel>
);
};
return (
<SelectWrapper style={style}>
<Wrapper
@@ -0,0 +1,28 @@
import styled from 'styled-components';
import { BGCOLOR, INPUT_INNER_PADDING } from '../config';
const AutoCompleteLabel = styled.span`
position: absolute;
left: ${INPUT_INNER_PADDING}px;
height: 20px;
line-height: 20px;
top: 26px;
pointer-events: none;
transition: all 0.2s;
background-color: var(--ant-color-bg-container);
z-index: 10;
&.disabled {
background-color: #f5f5f5;
}
&.isfoucs-has-value {
// display: none;
z-index: -1;
// top: 9px;
// font-size: 12px;
// color: var(--ant-color-text);
// background-color: ${BGCOLOR};
// padding: 0 4px;
}
`;
export default AutoCompleteLabel;
+1 -1
View File
@@ -103,7 +103,7 @@ const SelectWrapper = styled.div`
}
&.ant-select-auto-complete {
.ant-select-selection-search {
inset-inline-start: ${INPUT_INNER_PADDING}px;
padding-inline-start: ${INPUT_INNER_PADDING}px;
}
}
&.ant-select-multiple.ant-cascader .ant-select-selection-search {
+18 -9
View File
@@ -16,7 +16,9 @@ interface CardProps {
onClick?: () => void;
}
const CardWrapper = styled.div`
const CardWrapper = styled.div.attrs({
className: 'template-card-wrapper'
})`
overflow: hidden;
display: flex;
padding: 22px;
@@ -43,37 +45,44 @@ const CardWrapper = styled.div`
cursor: pointer;
}
&.disabled {
cursor: not-allowed;
cursor: default;
opacity: 0.6;
pointer-events: none;
background-color: var(--ant-color-fill-quaternary);
border-style: dashed;
}
`;
const CardContent = styled.div`
const CardContent = styled.div.attrs({
className: 'template-card-content'
})`
width: 100%;
flex: 1;
color: var(--ant-color-text-tertiary);
`;
const Inner = styled.div`
const Inner = styled.div.attrs({
className: 'template-card-inner'
})`
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
justify-content: flex-start;
gap: 8px;
line-height: 1.5;
`;
const Icon = styled.div`
const Icon = styled.div.attrs({
className: 'template-card-icon'
})`
display: flex;
align-items: center;
margin-right: 16px;
font-size: 32px;
`;
const Header = styled.div`
const Header = styled.div.attrs({
className: 'template-card-header'
})`
font-weight: bold;
font-size: var(--font-size-base);
display: flex;
@@ -110,7 +119,7 @@ const Card: React.FC<CardProps> = (props) => {
{icon && <Icon>{icon}</Icon>}
<Inner>
{header && <Header>{header}</Header>}
<CardContent>{children}</CardContent>
{children && <CardContent>{children}</CardContent>}
{footer}
</Inner>
</CardWrapper>
+2 -1
View File
@@ -3,7 +3,8 @@ import { PageActionType, StatusType } from './types';
export const PageAction: Record<string, PageActionType> = {
CREATE: 'create',
UPDATE: 'update',
VIEW: 'view'
VIEW: 'view',
EDIT: 'edit'
};
export const StatusColorMap: Record<
+1 -1
View File
@@ -6,7 +6,7 @@ export interface DropDownItem {
iconfont?: boolean;
}
export type PageActionType = 'create' | 'update' | 'view';
export type PageActionType = 'create' | 'update' | 'view' | 'edit';
export type StatusType =
| 'error'
+5 -1
View File
@@ -37,5 +37,9 @@ export default {
'clusters.create.noInstanceTypes': 'No instance types available',
'clusters.create.noRegions': 'No regions available',
'clusters.workerpool.batchSize.desc':
'Number of workers created simultaneously in the Worker pool'
'Number of workers created simultaneously in the Worker pool',
'clusters.create.addworker.tips':
' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
'clusters.create.addCommand.tips':
' On the Worker that needs to be added, run the following command to join it to the cluster.'
};
+8 -2
View File
@@ -37,7 +37,11 @@ export default {
'clusters.create.noInstanceTypes': 'No instance types available',
'clusters.create.noRegions': 'No regions available',
'clusters.workerpool.batchSize.desc':
'Number of workers created simultaneously in the Worker pool'
'Number of workers created simultaneously in the Worker pool',
'clusters.create.addworker.tips':
' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
'clusters.create.addCommand.tips':
' On the Worker that needs to be added, run the following command to join it to the cluster.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -78,6 +82,8 @@ export default {
// 35. 'clusters.create.noImages': 'No images available',
// 36. 'clusters.create.noInstanceTypes': 'No instance types available',
// 37. 'clusters.create.noRegions': 'No regions available',
// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool'
// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool',
// 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
// 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.'
// ========== End of To-Do List ==========
+7 -1
View File
@@ -37,7 +37,11 @@ export default {
'clusters.create.noInstanceTypes': 'No instance types available',
'clusters.create.noRegions': 'No regions available',
'clusters.workerpool.batchSize.desc':
'Number of workers created simultaneously in the Worker pool'
'Number of workers created simultaneously in the Worker pool',
'clusters.create.addworker.tips':
' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
'clusters.create.addCommand.tips':
' On the Worker that needs to be added, run the following command to join it to the cluster.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -79,5 +83,7 @@ export default {
// 36. 'clusters.create.noInstanceTypes': 'No instance types available',
// 37. 'clusters.create.noRegions': 'No regions available',
// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool'
// 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
// 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.'
// ========== End of To-Do List ==========
+8 -4
View File
@@ -33,8 +33,12 @@ export default {
'clusters.create.execCommand': '执行命令',
'clusters.create.supportedGpu': '支持的 GPU',
'clusters.create.skipfornow': '暂时跳过',
'clusters.create.noImages': '没有可用的镜像',
'clusters.create.noInstanceTypes': '没有可用的实例类型',
'clusters.create.noRegions': '没有可用的区域',
'clusters.workerpool.batchSize.desc': 'Worker池中同时创建的worker数量'
'clusters.create.noImages': '可用的镜像',
'clusters.create.noInstanceTypes': '可用的实例类型',
'clusters.create.noRegions': '可用的区域',
'clusters.workerpool.batchSize.desc': 'Worker 池中同时创建的 worker 数量',
'clusters.create.addworker.tips':
'在执行以下命令之前,请确保已满足 <a href={link} target="_blank">{label}</a> 的先决条件。',
'clusters.create.addCommand.tips':
' 在需要添加的 Worker 上运行以下命令,将其加入到集群中'
};
@@ -187,7 +187,7 @@ const ClusterCreate = () => {
}
};
const handleSelectProvider = (value: ProviderType) => {
const handleSelectProvider = (value: string) => {
if (value === extraData.provider) {
onNext();
return;
@@ -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}
/>
);
};
+8 -2
View File
@@ -1,3 +1,4 @@
import { fromClusterCreationAtom } from '@/atoms/clusters';
import DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools';
@@ -5,9 +6,10 @@ import { PageAction } from '@/config';
import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Empty, Table, message } from 'antd';
import { useAtom } from 'jotai';
import { useState } from 'react';
import {
createCredential,
@@ -52,8 +54,9 @@ const Credentials: React.FC = () => {
deleteAPI: deleteCredential,
contentForDelete: 'menu.clusterManagement.credentials'
});
const [isFromCluster] = useAtom(fromClusterCreationAtom);
const intl = useIntl();
const navigate = useNavigate();
const [openModalStatus, setOpenModalStatus] = useState<{
provider: ProviderType;
open: boolean;
@@ -97,6 +100,9 @@ const Credentials: React.FC = () => {
});
} else {
await createCredential({ data: params });
// if (isFromCluster) {
// navigate(-1);
// }
}
fetchData();
setOpenModalStatus({ ...openModalStatus, open: false });
@@ -1,13 +1,13 @@
import AutoTooltip from '@/components/auto-tooltip';
import DropdownButtons from '@/components/drop-down-buttons';
import { SealColumnProps } from '@/components/seal-table/types';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { ColumnsType } from 'antd/es/table';
import type { SortOrder } from 'antd/es/table/interface';
import dayjs from 'dayjs';
import _ from 'lodash';
import { useMemo } from 'react';
import { RenderInstanceOption } from '../components/pool-form';
import { RenderOption } from '../components/pool-form';
import { NodePoolListItem as ListItem } from '../config/types';
const actionItems = [
@@ -30,7 +30,7 @@ const actionItems = [
const usePoolsColumns = (
handleSelect: (val: string, record: ListItem) => void,
sortOrder?: SortOrder
): ColumnsType<ListItem & { dataIndex: string }> => {
): SealColumnProps[] => {
const intl = useIntl();
return useMemo(() => {
@@ -66,7 +66,7 @@ const usePoolsColumns = (
render: (text: string, record: ListItem) => (
<AutoTooltip
title={
<RenderInstanceOption
<RenderOption
styles={{
description: {
color: 'var(--color-white-quaternary)'
@@ -180,6 +180,7 @@ const usePoolsColumns = (
{
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operations',
dataIndex: 'operations',
span: 3,
style: {
paddingLeft: 36
@@ -67,15 +67,19 @@ const formatSpec = (spec: ParsedSpec): string => {
if (spec.vram) parts.push(`${spec.vram} VRAM`);
if (spec.vcpus) parts.push(`${spec.vcpus} vCPUs`);
if (spec.bootDisk) parts.push(`${spec.bootDisk} Boot disk`);
if (spec.ram) parts.push(`${spec.ram} RAM`);
if (spec.scratchDisk) {
parts.push(`${spec.scratchDisk} Scratch disk`);
}
// if (spec.bootDisk) parts.push(`${spec.bootDisk} Boot disk`);
// if (spec.scratchDisk) {
// parts.push(`${spec.scratchDisk} Scratch disk`);
// }
return parts.join(' / ');
};
const formatLabel = (instanceSpec: any): string => {
return `${_.toUpper(instanceSpec.gpu_info?.model.replace(/_/g, ' '))}`;
};
export const useProviderRegions = () => {
const [regions, setRegions] = useAtom(regionListAtom);
const [, setInstanceTypes] = useAtom(regionInstanceTypeListAtom);
@@ -119,10 +123,12 @@ export const useProviderRegions = () => {
?.filter((sItem: any) => sItem.gpu_info && sItem.available)
.map((item: any) => {
const specInfo = parseSpec(item);
const label = formatLabel(item);
const description = `${label} ${item.gpu_info?.count}X`;
return {
label: formatSpec(specInfo),
label: `${description} - ${formatSpec(specInfo)}`,
value: item.slug,
description: item.description,
description: description,
specInfo: specInfo,
vendor: _.get(_.split(item.gpu_info?.model, '_'), 0),
available: item.available,
@@ -148,10 +154,7 @@ export const useProviderRegions = () => {
name: item.name,
description: item.description,
vendor: _.camelCase(item.distribution),
specInfo: {
size: `${item.size_gigabytes} GiB`,
minDiskSize: `${item.min_disk_size} GiB`
},
specInfo: {},
regions: item.regions || []
};
});
@@ -41,7 +41,7 @@ const BasicForm = forwardRef((props: BasicFormProps, ref) => {
return (
<div>
<PageTools
marginBottom={26}
marginBottom={16}
left={
<Title>
{intl.formatMessage({ id: 'clusters.create.configBasic' })}
+2 -2
View File
@@ -8,6 +8,7 @@ import '../style/gpu-card.less';
const CardWrapper = styled.div`
display: flex;
gap: 10px;
flex-direction: column;
align-items: flex-start;
justify-content: center;
@@ -18,7 +19,6 @@ const CardWrapper = styled.div`
const Header = styled.div`
width: 100%;
margin-bottom: 10px;
`;
const Description = styled.div`
@@ -36,7 +36,7 @@ export const CardContainer: React.FC<{
return (
<CardWrapper>
<Header>{header}</Header>
<Description>{description}</Description>
{description && <Description>{description}</Description>}
</CardWrapper>
);
};
+3 -1
View File
@@ -64,7 +64,9 @@ export const addWorkerGuide: Record<string, any> = {
--ipc=host \\
-v gpustack-data:/var/lib/gpustack \\
${params.image} \\
--server-url ${params.server} --registration-token ${params.token} --worker-ip ${params.workerip}`;
--server-url ${params.server} \\
--registration-token ${params.token} \\
--worker-ip ${params.workerip}`;
}
},
npu: {