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; label: string;
value: 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; label: string;
value: 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[]; regions: string[];
}[] }[]
>([]); >([]);
export const fromClusterCreationAtom = atom(false);
+2 -2
View File
@@ -23,7 +23,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
legendOptions, legendOptions,
gridOptions, gridOptions,
titleOptions, titleOptions,
showArea showArea = 0.25
} = props; } = props;
const { const {
grid, grid,
@@ -87,7 +87,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
const data = _.map(seriesData, (item: any) => { const data = _.map(seriesData, (item: any) => {
const colors = genColors({ const colors = genColors({
color: item.color, color: item.color,
alpha1: 0.5, alpha1: 0.25,
alpha2: 0.1 alpha2: 0.1
}); });
return { return {
+1 -1
View File
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
// import './iconfont/iconfont.js'; // import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({ 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; export default IconFont;
@@ -1,8 +1,10 @@
import { AutoComplete, Form, Spin } from 'antd'; import { AutoComplete, Form, Spin } from 'antd';
import type { AutoCompleteProps } from 'antd/lib'; import type { AutoCompleteProps } from 'antd/lib';
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { SealFormItemProps } from './types'; import { SealFormItemProps } from './types';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
import AutoCompleteLabel from './wrapper/auto-complete-label';
import SelectWrapper from './wrapper/select'; import SelectWrapper from './wrapper/select';
const SealAutoComplete: React.FC< const SealAutoComplete: React.FC<
@@ -24,6 +26,7 @@ const SealAutoComplete: React.FC<
...rest ...rest
} = props; } = props;
const [isFocus, setIsFocus] = useState(false); const [isFocus, setIsFocus] = useState(false);
const [_isFocus_, _setIsFocus_] = useState(false);
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
let status = ''; let status = '';
if (isInFormItems) { if (isInFormItems) {
@@ -41,6 +44,7 @@ const SealAutoComplete: React.FC<
if (!props.disabled && !isFocus) { if (!props.disabled && !isFocus) {
inputRef.current?.focus?.(); inputRef.current?.focus?.();
setIsFocus(true); setIsFocus(true);
_setIsFocus_(true);
} }
}; };
@@ -49,17 +53,20 @@ const SealAutoComplete: React.FC<
if (trim) { if (trim) {
value = value?.trim?.(); value = value?.trim?.();
} }
_setIsFocus_(false);
props.onChange?.(value, option); props.onChange?.(value, option);
}; };
const handleOnFocus = (e: any) => { const handleOnFocus = (e: any) => {
setIsFocus(true); setIsFocus(true);
_setIsFocus_(true);
props.onFocus?.(e); props.onFocus?.(e);
}; };
const handleOnBlur = (e: any) => { const handleOnBlur = (e: any) => {
if (!props.value) { if (!props.value) {
setIsFocus(false); setIsFocus(false);
_setIsFocus_(false);
} }
e.target.value = e.target.value?.trim?.(); e.target.value = e.target.value?.trim?.();
props.onBlur?.(e); props.onBlur?.(e);
@@ -79,6 +86,39 @@ const SealAutoComplete: React.FC<
return addAfter; 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 ( return (
<SelectWrapper style={style}> <SelectWrapper style={style}>
<Wrapper <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-auto-complete {
.ant-select-selection-search { .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 { &.ant-select-multiple.ant-cascader .ant-select-selection-search {
+18 -9
View File
@@ -16,7 +16,9 @@ interface CardProps {
onClick?: () => void; onClick?: () => void;
} }
const CardWrapper = styled.div` const CardWrapper = styled.div.attrs({
className: 'template-card-wrapper'
})`
overflow: hidden; overflow: hidden;
display: flex; display: flex;
padding: 22px; padding: 22px;
@@ -43,37 +45,44 @@ const CardWrapper = styled.div`
cursor: pointer; cursor: pointer;
} }
&.disabled { &.disabled {
cursor: not-allowed; cursor: default;
opacity: 0.6; opacity: 0.6;
pointer-events: none; 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%; width: 100%;
flex: 1; flex: 1;
color: var(--ant-color-text-tertiary); color: var(--ant-color-text-tertiary);
`; `;
const Inner = styled.div` const Inner = styled.div.attrs({
className: 'template-card-inner'
})`
display: flex; display: flex;
width: 100%; width: 100%;
height: 100%;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
gap: 8px; gap: 8px;
line-height: 1.5; line-height: 1.5;
`; `;
const Icon = styled.div` const Icon = styled.div.attrs({
className: 'template-card-icon'
})`
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: 16px; margin-right: 16px;
font-size: 32px; font-size: 32px;
`; `;
const Header = styled.div` const Header = styled.div.attrs({
className: 'template-card-header'
})`
font-weight: bold; font-weight: bold;
font-size: var(--font-size-base); font-size: var(--font-size-base);
display: flex; display: flex;
@@ -110,7 +119,7 @@ const Card: React.FC<CardProps> = (props) => {
{icon && <Icon>{icon}</Icon>} {icon && <Icon>{icon}</Icon>}
<Inner> <Inner>
{header && <Header>{header}</Header>} {header && <Header>{header}</Header>}
<CardContent>{children}</CardContent> {children && <CardContent>{children}</CardContent>}
{footer} {footer}
</Inner> </Inner>
</CardWrapper> </CardWrapper>
+2 -1
View File
@@ -3,7 +3,8 @@ import { PageActionType, StatusType } from './types';
export const PageAction: Record<string, PageActionType> = { export const PageAction: Record<string, PageActionType> = {
CREATE: 'create', CREATE: 'create',
UPDATE: 'update', UPDATE: 'update',
VIEW: 'view' VIEW: 'view',
EDIT: 'edit'
}; };
export const StatusColorMap: Record< export const StatusColorMap: Record<
+1 -1
View File
@@ -6,7 +6,7 @@ export interface DropDownItem {
iconfont?: boolean; iconfont?: boolean;
} }
export type PageActionType = 'create' | 'update' | 'view'; export type PageActionType = 'create' | 'update' | 'view' | 'edit';
export type StatusType = export type StatusType =
| 'error' | 'error'
+5 -1
View File
@@ -37,5 +37,9 @@ export default {
'clusters.create.noInstanceTypes': 'No instance types available', 'clusters.create.noInstanceTypes': 'No instance types available',
'clusters.create.noRegions': 'No regions available', 'clusters.create.noRegions': 'No regions available',
'clusters.workerpool.batchSize.desc': '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.noInstanceTypes': 'No instance types available',
'clusters.create.noRegions': 'No regions available', 'clusters.create.noRegions': 'No regions available',
'clusters.workerpool.batchSize.desc': '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -78,6 +82,8 @@ export default {
// 35. 'clusters.create.noImages': 'No images available', // 35. 'clusters.create.noImages': 'No images available',
// 36. 'clusters.create.noInstanceTypes': 'No instance types available', // 36. 'clusters.create.noInstanceTypes': 'No instance types available',
// 37. 'clusters.create.noRegions': 'No regions 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 ========== // ========== End of To-Do List ==========
+7 -1
View File
@@ -37,7 +37,11 @@ export default {
'clusters.create.noInstanceTypes': 'No instance types available', 'clusters.create.noInstanceTypes': 'No instance types available',
'clusters.create.noRegions': 'No regions available', 'clusters.create.noRegions': 'No regions available',
'clusters.workerpool.batchSize.desc': '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -79,5 +83,7 @@ export default {
// 36. 'clusters.create.noInstanceTypes': 'No instance types available', // 36. 'clusters.create.noInstanceTypes': 'No instance types available',
// 37. 'clusters.create.noRegions': 'No regions 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 ========== // ========== End of To-Do List ==========
+8 -4
View File
@@ -33,8 +33,12 @@ export default {
'clusters.create.execCommand': '执行命令', 'clusters.create.execCommand': '执行命令',
'clusters.create.supportedGpu': '支持的 GPU', 'clusters.create.supportedGpu': '支持的 GPU',
'clusters.create.skipfornow': '暂时跳过', 'clusters.create.skipfornow': '暂时跳过',
'clusters.create.noImages': '没有可用的镜像', 'clusters.create.noImages': '可用的镜像',
'clusters.create.noInstanceTypes': '没有可用的实例类型', 'clusters.create.noInstanceTypes': '可用的实例类型',
'clusters.create.noRegions': '没有可用的区域', 'clusters.create.noRegions': '可用的区域',
'clusters.workerpool.batchSize.desc': 'Worker池中同时创建的worker数量' '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) { if (value === extraData.provider) {
onNext(); onNext();
return; return;
@@ -25,7 +25,7 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({ registrationInfo }) => {
return ( return (
<HighlightCode <HighlightCode
theme="dark" theme="dark"
code={code.replace(/\\/g, '')} code={code}
copyValue={code} copyValue={code}
lang="bash" lang="bash"
></HighlightCode> ></HighlightCode>
@@ -1,4 +1,6 @@
import { BulbOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Alert } from 'antd';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ProviderType, ProviderValueMap } from '../config'; import { ProviderType, ProviderValueMap } from '../config';
@@ -9,7 +11,42 @@ import SupportedHardware from './support-hardware';
const Title = styled.div` const Title = styled.div`
font-size: 16px; font-size: 16px;
font-weight: 600; 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 = { type AddModalProps = {
@@ -26,19 +63,57 @@ const AddWorkerStep: React.FC<AddModalProps> = ({
registrationInfo registrationInfo
}) => { }) => {
const intl = useIntl(); 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 ( return (
<div> <Container>
<Title>{intl.formatMessage({ id: 'clusters.create.execCommand' })}</Title> <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 ? ( {provider === ProviderValueMap.Kubernetes ? (
<RegisterClusterInner registrationInfo={registrationInfo} /> <RegisterClusterInner registrationInfo={registrationInfo} />
) : ( ) : (
<AddWorkerCommand registrationInfo={registrationInfo} /> <AddWorkerCommand registrationInfo={registrationInfo} />
)} )}
<Title style={{ marginTop: 32 }}> </Container>
{intl.formatMessage({ id: 'clusters.create.supportedGpu' })}
</Title>
<SupportedHardware />
</div>
); );
}; };
@@ -1,10 +1,12 @@
import { fromClusterCreationAtom } from '@/atoms/clusters';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useAppUtils from '@/hooks/use-app-utils'; import useAppUtils from '@/hooks/use-app-utils';
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { Link, useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -14,7 +16,7 @@ import { useProviderRegions } from '../hooks/use-provider-regions';
type OptionData = { type OptionData = {
label: string; label: string;
datacenter: string; datacenter: string;
value: string; value: string | number;
icon: string; icon: string;
}; };
@@ -70,14 +72,24 @@ const NotFoundContent: React.FC<{ loading: boolean }> = ({ loading }) => {
); );
}; };
const optionRender = ( const NotFoundCredentialContent: React.FC = () => {
option: Global.BaseOption< const [, setFromClusterCreation] = useAtom(fromClusterCreationAtom);
number, const intl = useIntl();
{ const handleOnClick = () => {
data: OptionData; console.log('click add credential');
} setFromClusterCreation(true);
> };
): React.ReactNode => {
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 { value } = option;
const data = option.data!; const data = option.data!;
@@ -127,8 +139,8 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
}, [credentialID]); }, [credentialID]);
const labelRender = (props: { const labelRender = (props: {
label: string; label: React.ReactNode;
value: string; value: string | number;
}): React.ReactNode => { }): React.ReactNode => {
const data = regions.find((item) => item.value === props.value); const data = regions.find((item) => item.value === props.value);
if (!data) return props.label; if (!data) return props.label;
@@ -164,6 +176,7 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
]} ]}
> >
<SealSelect <SealSelect
notFoundContent={<NotFoundCredentialContent />}
disabled={action === PageAction.EDIT} disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'clusters.credential.title' })} label={intl.formatMessage({ id: 'clusters.credential.title' })}
required required
@@ -72,7 +72,7 @@ const OptionItem = styled.div.attrs({
const DescriptionWrapper = styled.div` const DescriptionWrapper = styled.div`
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
gap: 8px; gap: 4px 8px;
font-weight: 400; font-weight: 400;
`; `;
@@ -94,34 +94,48 @@ const NotFoundImageContent = () => {
); );
}; };
export const RenderInstanceOption = (option: any) => { const RenderLabel = (data: {
const { data, styles } = option; 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 ( return (
<CardContainer <CardContainer
key={data.value} key={data.value}
header={ header={
<span> <RenderLabel
<IconFont label={data.description || data.label}
type={_.get(vendorIconMap, data.vendor, 'icon-gpu1')} vendor={data.vendor}
className="m-r-8" style={styles?.header}
></IconFont> />
{data.description}
</span>
} }
description={ description={
<DescriptionWrapper style={{ ...(styles?.description || {}) }}> entries.length > 0 && (
{Object.entries(data?.specInfo) <DescriptionWrapper style={{ ...styles?.description }}>
.filter(([key, value]) => value) {Object.entries(data?.specInfo)
.map(([key, value]) => ( .filter(([key, value]) => value)
<OptionItem key={key}> .map(([key, value]) => (
<span className="label"> <OptionItem key={key}>
{_.get(instanceTypeFieldMap, key, key)}: <span className="label">
</span> {_.get(instanceTypeFieldMap, key, key)}:
<span className="value">{value as string}</span> </span>
</OptionItem> <span className="value">{value as string}</span>
))} </OptionItem>
</DescriptionWrapper> ))}
</DescriptionWrapper>
)
} }
/> />
); );
@@ -186,19 +200,45 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
} }
}, [currentData]); }, [currentData]);
const imageLabelRender = (data: { label: string; value: string }) => { const imageLabelRender = (data: {
console.log('imageLabelRender========', data); label: React.ReactNode;
value: string | number;
}) => {
if (action === PageAction.EDIT) { 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 }) => { const instanceLabelRender = (data: {
if (action === PageAction.EDIT) { label: React.ReactNode;
return currentData?.instance_spec?.label || currentData?.instance_type; value: string | number;
} }) => {
return data.label; 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) => { const handleOsImageChange = (value: string) => {
@@ -329,7 +369,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
required required
options={instanceTypeList} options={instanceTypeList}
disabled={action === PageAction.EDIT} disabled={action === PageAction.EDIT}
optionRender={RenderInstanceOption} optionRender={RenderOption}
onChange={handleInstanceTypeChange} onChange={handleInstanceTypeChange}
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
@@ -383,11 +423,16 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
> >
<AutoComplete <AutoComplete
showSearch showSearch
notFoundContent={<NotFoundImageContent />}
onChange={handleOsImageChange} onChange={handleOsImageChange}
filterOption={filterImageOption} filterOption={filterImageOption}
optionRender={RenderInstanceOption} optionRender={(option) =>
RenderOption({
...option,
styles: { header: { marginBlock: 5 } }
})
}
labelRender={imageLabelRender} labelRender={imageLabelRender}
placeholder={currentData?.image_name}
options={osImageList} options={osImageList}
disabled={action === PageAction.EDIT} disabled={action === PageAction.EDIT}
label={intl.formatMessage({ label={intl.formatMessage({
@@ -4,14 +4,20 @@ import React, { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ProviderType } from '../config'; import { ProviderType } from '../config';
const Wrapper = styled.div` const Wrapper = styled.div<{ $cols?: number }>`
width: 100%;
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: repeat(${(props) => props.$cols || 3}, 1fr);
gap: 22px; gap: 16px;
.template-card-wrapper {
padding: 16px;
}
`; `;
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
width: 800px;
margin: 0 auto;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
`; `;
@@ -22,19 +28,20 @@ const Title = styled.span`
justify-content: space-between; justify-content: space-between;
font-weight: 700; font-weight: 700;
font-size: 16px; font-size: 16px;
margin-block: 16px 24px; margin-block: 20px 16px;
`; `;
interface ProviderCatalogProps { interface ProviderCatalogProps {
onSelect?: (provider: ProviderType) => void; onSelect?: (provider: string, item: any) => void;
currentProvider?: ProviderType; cols?: number;
currentProvider?: ProviderType | string;
clickable?: boolean; clickable?: boolean;
dataList: { dataList: {
label: string; label: string;
key: string; key: string;
locale?: boolean; locale?: boolean;
disabled?: boolean; disabled?: boolean;
icon: React.ReactNode; icon?: React.ReactNode;
description?: string; description?: string;
group?: string; group?: string;
}[]; }[];
@@ -42,6 +49,7 @@ interface ProviderCatalogProps {
const ProviderCatalog: React.FC<ProviderCatalogProps> = ({ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
onSelect, onSelect,
cols = 3,
dataList, dataList,
clickable, clickable,
currentProvider currentProvider
@@ -66,12 +74,12 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
{groupName !== 'default' && ( {groupName !== 'default' && (
<Title>{intl.formatMessage({ id: groupName })}</Title> <Title>{intl.formatMessage({ id: groupName })}</Title>
)} )}
<Wrapper> <Wrapper $cols={cols}>
{items?.map((action) => ( {items?.map((action) => (
<Card <Card
height="auto" height="80px"
key={action.key} key={action.key}
onClick={() => onSelect?.(action.key as ProviderType)} onClick={() => onSelect?.(action.key as string, action)}
active={currentProvider === action.key} active={currentProvider === action.key}
disabled={action.disabled} disabled={action.disabled}
clickable={clickable} clickable={clickable}
@@ -82,7 +90,7 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
} }
icon={action.icon} icon={action.icon}
> >
{action.description || 'This is a description'} {action.description}
</Card> </Card>
))} ))}
</Wrapper> </Wrapper>
@@ -4,8 +4,8 @@ import hyponPNG from '@/assets/logo/hygon.png';
import iluvatarWEBP from '@/assets/logo/Iluvatar.png'; import iluvatarWEBP from '@/assets/logo/Iluvatar.png';
import metaxLogo from '@/assets/logo/metax.png'; import metaxLogo from '@/assets/logo/metax.png';
import moorePNG from '@/assets/logo/moore _threads.png'; import moorePNG from '@/assets/logo/moore _threads.png';
import nvidiaLogo from '@/assets/logo/nvidia.png';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import styled from 'styled-components';
import ProviderCatalog from './provider-catalog'; import ProviderCatalog from './provider-catalog';
const ProviderImage = ({ src, showBg }: { src: string; showBg?: boolean }) => { const ProviderImage = ({ src, showBg }: { src: string; showBg?: boolean }) => {
@@ -24,15 +24,19 @@ const supportedHardPlatforms = [
{ {
label: 'NVIDIA CUDA', label: 'NVIDIA CUDA',
value: 'cuda', value: 'cuda',
key: 'nvidia', description: '',
key: 'cuda',
locale: false, 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', label: 'AMD ROCm',
description: '',
value: 'rocm', value: 'rocm',
key: 'amd', key: 'rocm',
locale: false, locale: false,
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#amd-rocm',
icon: ( icon: (
<IconFont <IconFont
type="icon-amd" type="icon-amd"
@@ -42,30 +46,38 @@ const supportedHardPlatforms = [
}, },
{ {
label: 'Ascend CANN', label: 'Ascend CANN',
description: '',
value: 'npu', value: 'npu',
key: 'ascend', key: 'npu',
locale: false, locale: false,
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#ascend-cann',
icon: <ProviderImage src={ascendLogo} showBg /> icon: <ProviderImage src={ascendLogo} showBg />
}, },
{ {
label: 'Hygon DTK', label: 'Hygon DTK',
description: '',
value: 'dcu', value: 'dcu',
key: 'hygon', key: 'dcu',
locale: false, locale: false,
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#hygon-dtk',
icon: <ProviderImage src={hyponPNG} /> icon: <ProviderImage src={hyponPNG} />
}, },
{ {
label: 'Moore Threads MUSA', label: 'Moore Threads',
description: '',
value: 'musa', value: 'musa',
key: 'musa', key: 'musa',
locale: false, locale: false,
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#moore-threads-musa',
icon: <ProviderImage src={moorePNG} /> icon: <ProviderImage src={moorePNG} />
}, },
{ {
label: 'Iluvatar Corex', label: 'Iluvatar Corex',
description: '',
value: 'corex', value: 'corex',
key: 'corex', key: 'corex',
locale: false, locale: false,
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#iluvatar-corex',
icon: <ProviderImage src={iluvatarWEBP} showBg /> icon: <ProviderImage src={iluvatarWEBP} showBg />
}, },
{ {
@@ -73,6 +85,7 @@ const supportedHardPlatforms = [
value: 'cambricon', value: 'cambricon',
key: 'cambricon', key: 'cambricon',
locale: false, locale: false,
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#cambricon-mlu',
icon: <ProviderImage src={CambriconPNG} /> 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 ( 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 DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools'; import { FilterBar } from '@/components/page-tools';
@@ -5,9 +6,10 @@ import { PageAction } from '@/config';
import type { PageActionType } from '@/config/types'; import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import { PageContainer } from '@ant-design/pro-components'; import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max'; import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Empty, Table, message } from 'antd'; import { ConfigProvider, Empty, Table, message } from 'antd';
import { useAtom } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';
import { import {
createCredential, createCredential,
@@ -52,8 +54,9 @@ const Credentials: React.FC = () => {
deleteAPI: deleteCredential, deleteAPI: deleteCredential,
contentForDelete: 'menu.clusterManagement.credentials' contentForDelete: 'menu.clusterManagement.credentials'
}); });
const [isFromCluster] = useAtom(fromClusterCreationAtom);
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigate();
const [openModalStatus, setOpenModalStatus] = useState<{ const [openModalStatus, setOpenModalStatus] = useState<{
provider: ProviderType; provider: ProviderType;
open: boolean; open: boolean;
@@ -97,6 +100,9 @@ const Credentials: React.FC = () => {
}); });
} else { } else {
await createCredential({ data: params }); await createCredential({ data: params });
// if (isFromCluster) {
// navigate(-1);
// }
} }
fetchData(); fetchData();
setOpenModalStatus({ ...openModalStatus, open: false }); setOpenModalStatus({ ...openModalStatus, open: false });
@@ -1,13 +1,13 @@
import AutoTooltip from '@/components/auto-tooltip'; import AutoTooltip from '@/components/auto-tooltip';
import DropdownButtons from '@/components/drop-down-buttons'; import DropdownButtons from '@/components/drop-down-buttons';
import { SealColumnProps } from '@/components/seal-table/types';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { ColumnsType } from 'antd/es/table';
import type { SortOrder } from 'antd/es/table/interface'; import type { SortOrder } from 'antd/es/table/interface';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { RenderInstanceOption } from '../components/pool-form'; import { RenderOption } from '../components/pool-form';
import { NodePoolListItem as ListItem } from '../config/types'; import { NodePoolListItem as ListItem } from '../config/types';
const actionItems = [ const actionItems = [
@@ -30,7 +30,7 @@ const actionItems = [
const usePoolsColumns = ( const usePoolsColumns = (
handleSelect: (val: string, record: ListItem) => void, handleSelect: (val: string, record: ListItem) => void,
sortOrder?: SortOrder sortOrder?: SortOrder
): ColumnsType<ListItem & { dataIndex: string }> => { ): SealColumnProps[] => {
const intl = useIntl(); const intl = useIntl();
return useMemo(() => { return useMemo(() => {
@@ -66,7 +66,7 @@ const usePoolsColumns = (
render: (text: string, record: ListItem) => ( render: (text: string, record: ListItem) => (
<AutoTooltip <AutoTooltip
title={ title={
<RenderInstanceOption <RenderOption
styles={{ styles={{
description: { description: {
color: 'var(--color-white-quaternary)' color: 'var(--color-white-quaternary)'
@@ -180,6 +180,7 @@ const usePoolsColumns = (
{ {
title: intl.formatMessage({ id: 'common.table.operation' }), title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operations', key: 'operations',
dataIndex: 'operations',
span: 3, span: 3,
style: { style: {
paddingLeft: 36 paddingLeft: 36
@@ -67,15 +67,19 @@ const formatSpec = (spec: ParsedSpec): string => {
if (spec.vram) parts.push(`${spec.vram} VRAM`); if (spec.vram) parts.push(`${spec.vram} VRAM`);
if (spec.vcpus) parts.push(`${spec.vcpus} vCPUs`); 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.ram) parts.push(`${spec.ram} RAM`);
if (spec.scratchDisk) { // if (spec.bootDisk) parts.push(`${spec.bootDisk} Boot disk`);
parts.push(`${spec.scratchDisk} Scratch disk`); // if (spec.scratchDisk) {
} // parts.push(`${spec.scratchDisk} Scratch disk`);
// }
return parts.join(' / '); return parts.join(' / ');
}; };
const formatLabel = (instanceSpec: any): string => {
return `${_.toUpper(instanceSpec.gpu_info?.model.replace(/_/g, ' '))}`;
};
export const useProviderRegions = () => { export const useProviderRegions = () => {
const [regions, setRegions] = useAtom(regionListAtom); const [regions, setRegions] = useAtom(regionListAtom);
const [, setInstanceTypes] = useAtom(regionInstanceTypeListAtom); const [, setInstanceTypes] = useAtom(regionInstanceTypeListAtom);
@@ -119,10 +123,12 @@ export const useProviderRegions = () => {
?.filter((sItem: any) => sItem.gpu_info && sItem.available) ?.filter((sItem: any) => sItem.gpu_info && sItem.available)
.map((item: any) => { .map((item: any) => {
const specInfo = parseSpec(item); const specInfo = parseSpec(item);
const label = formatLabel(item);
const description = `${label} ${item.gpu_info?.count}X`;
return { return {
label: formatSpec(specInfo), label: `${description} - ${formatSpec(specInfo)}`,
value: item.slug, value: item.slug,
description: item.description, description: description,
specInfo: specInfo, specInfo: specInfo,
vendor: _.get(_.split(item.gpu_info?.model, '_'), 0), vendor: _.get(_.split(item.gpu_info?.model, '_'), 0),
available: item.available, available: item.available,
@@ -148,10 +154,7 @@ export const useProviderRegions = () => {
name: item.name, name: item.name,
description: item.description, description: item.description,
vendor: _.camelCase(item.distribution), vendor: _.camelCase(item.distribution),
specInfo: { specInfo: {},
size: `${item.size_gigabytes} GiB`,
minDiskSize: `${item.min_disk_size} GiB`
},
regions: item.regions || [] regions: item.regions || []
}; };
}); });
@@ -41,7 +41,7 @@ const BasicForm = forwardRef((props: BasicFormProps, ref) => {
return ( return (
<div> <div>
<PageTools <PageTools
marginBottom={26} marginBottom={16}
left={ left={
<Title> <Title>
{intl.formatMessage({ id: 'clusters.create.configBasic' })} {intl.formatMessage({ id: 'clusters.create.configBasic' })}
+2 -2
View File
@@ -8,6 +8,7 @@ import '../style/gpu-card.less';
const CardWrapper = styled.div` const CardWrapper = styled.div`
display: flex; display: flex;
gap: 10px;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
justify-content: center; justify-content: center;
@@ -18,7 +19,6 @@ const CardWrapper = styled.div`
const Header = styled.div` const Header = styled.div`
width: 100%; width: 100%;
margin-bottom: 10px;
`; `;
const Description = styled.div` const Description = styled.div`
@@ -36,7 +36,7 @@ export const CardContainer: React.FC<{
return ( return (
<CardWrapper> <CardWrapper>
<Header>{header}</Header> <Header>{header}</Header>
<Description>{description}</Description> {description && <Description>{description}</Description>}
</CardWrapper> </CardWrapper>
); );
}; };
+3 -1
View File
@@ -64,7 +64,9 @@ export const addWorkerGuide: Record<string, any> = {
--ipc=host \\ --ipc=host \\
-v gpustack-data:/var/lib/gpustack \\ -v gpustack-data:/var/lib/gpustack \\
${params.image} \\ ${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: { npu: {