fix: display instance type spec

This commit is contained in:
jialin
2025-09-16 11:26:17 +08:00
parent deeee70807
commit c467280170
54 changed files with 1058 additions and 204 deletions
@@ -29,7 +29,7 @@ const AddPool: React.FC<AddModalProps> = ({
}) => {
const formRef = useRef<any>(null);
const handleSubmit = () => {
const handleSubmit = async () => {
formRef.current?.submit?.();
};
@@ -1,3 +1,4 @@
import { useIntl } from '@umijs/max';
import React from 'react';
import styled from 'styled-components';
import RegisterClusterInner from './register-cluster-inner';
@@ -18,11 +19,14 @@ type AddModalProps = {
};
};
const AddWorkerStep: React.FC<AddModalProps> = ({ registrationInfo }) => {
const intl = useIntl();
return (
<div>
<Title>Execute Command</Title>
<Title>{intl.formatMessage({ id: 'clusters.create.execCommand' })}</Title>
<RegisterClusterInner registrationInfo={registrationInfo} />
<Title style={{ marginTop: 32 }}>Supported GPUs</Title>
<Title style={{ marginTop: 32 }}>
{intl.formatMessage({ id: 'clusters.create.supportedGpu' })}
</Title>
<SupportedHardware />
</div>
);
@@ -1,12 +1,15 @@
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 { Form } from 'antd';
import _ from 'lodash';
import React from 'react';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { regionList } from '../config';
import { ClusterFormData as FormData } from '../config/types';
import { useProviderRegions } from '../hooks/use-provider-regions';
type OptionData = {
label: string;
@@ -37,11 +40,31 @@ const OptionItem = styled.div`
}
`;
const NoContent = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding-block: 12px;
`;
interface CloudProviderProps {
provider: string; // 'kubernetes' | 'digitalocean';
credentialList: Global.BaseOption<number>[];
action: PageActionType;
credentialID?: number;
}
const NotFoundContent: React.FC<{ loading: boolean }> = ({ loading }) => {
if (loading) {
return (
<NoContent>
<LoadingOutlined />
</NoContent>
);
}
return <NoContent>No regions available</NoContent>;
};
const optionRender = (
option: Global.BaseOption<
number,
@@ -64,27 +87,66 @@ const optionRender = (
);
};
const labelRender = (props: {
label: string;
value: string;
}): React.ReactNode => {
const data = regionList.find((item) => item.value === props.value);
return (
<OptionItem className="flex-center">
<span className="icon">{data?.icon}</span>
<span className="label">{data?.label}</span> <span className="dot"></span>
<span className="datacenter">{data?.datacenter}</span>{' '}
<span className="dot"></span>
<span className="value">{_.toUpper(data?.value)}</span>
</OptionItem>
);
};
const CloudProvider: React.FC<CloudProviderProps> = (props) => {
const { credentialList } = props;
const { credentialList, action, credentialID } = props;
const intl = useIntl();
const {
getRegions,
getOSImages,
updateOSImages,
updateInstanceTypes,
getInstanceTypes,
setLoading,
loading,
regions
} = useProviderRegions();
const { getRuleMessage } = useAppUtils();
const handleCredentialChange = async (value: number) => {
setLoading(true);
await Promise.all([getOSImages(value), getInstanceTypes(value)]);
await getRegions(value);
};
const handleRegionChange = (value: string) => {
updateInstanceTypes(value);
updateOSImages(value);
};
useEffect(() => {
if (credentialID && action !== PageAction.CREATE) {
getRegions(credentialID);
}
}, [credentialID]);
const labelRender = (props: {
label: string;
value: string;
}): React.ReactNode => {
const data = regions.find((item) => item.value === props.value);
if (!data) return props.label;
return (
<OptionItem className="flex-center">
<span className="icon">{data?.icon}</span>
<span className="label">{data?.label}</span>{' '}
<span className="dot"></span>
<span className="datacenter">{data?.datacenter}</span>{' '}
<span className="dot"></span>
<span className="value">{_.toUpper(data?.value)}</span>
</OptionItem>
);
};
const filterRegionOption = (inputValue: string, option: any) => {
return (
option.label.toLowerCase().includes(inputValue.toLowerCase()) ||
option.datacenter.toLowerCase().includes(inputValue.toLowerCase()) ||
option.value.toLowerCase().includes(inputValue.toLowerCase())
);
};
return (
<>
<Form.Item<FormData>
@@ -100,6 +162,7 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
label={intl.formatMessage({ id: 'clusters.credential.title' })}
required
options={credentialList}
onChange={handleCredentialChange}
></SealSelect>
</Form.Item>
<Form.Item<FormData>
@@ -112,11 +175,16 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
]}
>
<SealSelect
showSearch
label={intl.formatMessage({ id: 'clusters.workerpool.region' })}
required
options={regionList}
options={regions}
loading={loading}
filterOption={filterRegionOption}
labelRender={labelRender}
optionRender={optionRender}
onChange={handleRegionChange}
notFoundContent={<NotFoundContent loading={loading} />}
></SealSelect>
</Form.Item>
</>
@@ -78,6 +78,8 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
{provider === ProviderValueMap.DigitalOcean && (
<CloudProvider
provider={provider}
action={action}
credentialID={currentData?.credential_id}
credentialList={credentialList}
></CloudProvider>
)}
@@ -32,18 +32,18 @@ const FooterButtons: React.FC<FooterButtonsProps> = (props) => {
<div className="flex-center gap-16">
{showButtons.previous && (
<Button type="link" icon={<LeftOutlined />} onClick={onPrevious}>
Previous
{intl.formatMessage({ id: 'common.button.prev' })}
</Button>
)}
{showButtons.next && (
<Button type="link" onClick={handleOnNext}>
Next
{intl.formatMessage({ id: 'common.button.next' })}
<RightOutlined />
</Button>
)}
{showButtons.skip && (
<Button type="primary" onClick={handleCancel}>
Skip for now
{intl.formatMessage({ id: 'clusters.create.skipfornow' })}
</Button>
)}
{showButtons.save && (
@@ -0,0 +1,22 @@
import { CardContainer } from '@/pages/llmodels/components/gpu-card';
import React from 'react';
const InstanceTypeOption: React.FC<{
data: any;
header?: React.ReactNode;
info?: React.ReactNode;
}> = ({ data, header, info }) => {
console.log('InstanceTypeOption render', {
data,
header,
info
});
return (
<CardContainer
header={<span>{data.label}</span>}
description={<span>{data.description}</span>}
></CardContainer>
);
};
export default InstanceTypeOption;
@@ -1,19 +1,32 @@
import {
regionInstanceTypeListAtom,
regionOSImageListAtom
} from '@/atoms/clusters';
import CollapsibleContainer, {
CollapsibleContainerProps
} from '@/components/collapse-container';
import IconFont from '@/components/icon-font';
import LabelSelector from '@/components/label-selector';
import SealInputNumber from '@/components/seal-form/input-number';
import SealInput from '@/components/seal-form/seal-input';
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 { CardContainer } from '@/pages/llmodels/components/gpu-card';
import { DeleteOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash';
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useState
} from 'react';
import styled from 'styled-components';
import { ProviderType } from '../config';
import { ProviderType, instanceTypeFieldMap, vendorIconMap } from '../config';
import { NodePoolFormData as FormData } from '../config/types';
import VolumesConfig from './volumes-config';
@@ -22,6 +35,12 @@ const Container = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 0 16px;
.ant-form-item:nth-child(1) {
grid-column: 1 / 3;
}
.ant-form-item:nth-child(2) {
grid-column: 1 / 3;
}
.ant-form-item:nth-child(5) {
grid-column: 1 / 3;
}
@@ -34,6 +53,65 @@ const Container = styled.div`
}
`;
const NoContent = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding-block: 12px;
`;
const OptionItem = styled.div.attrs({
className: 'option-item'
})`
display: flex;
gap: 8px;
align-items: center;
`;
const DescriptionWrapper = styled.div`
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
font-weight: 400;
`;
const NotFoundContent = () => {
return <NoContent>No instance type available</NoContent>;
};
export const RenderInstanceOption = (option: any) => {
const { data, styles } = option;
return (
<CardContainer
key={data.value}
header={
<span>
<IconFont
type={_.get(vendorIconMap, data.vendor, 'icon-gpu1')}
className="m-r-8"
></IconFont>
{data.description}
</span>
}
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>
}
/>
);
};
type AddModalProps = {
ref: any;
name?: string;
@@ -45,6 +123,23 @@ type AddModalProps = {
showDelete?: boolean;
collapseProps?: CollapsibleContainerProps;
};
const InstanceSpecData: React.FC<{ instanceSpec: Record<string, any> }> = ({
instanceSpec
}) => {
return (
<>
{Object.entries(instanceSpec)
.filter(([key, value]) => value)
.map(([key, value]) => (
<Form.Item key={key} name={['instance_spec', key]} hidden>
<SealInput.Input />
</Form.Item>
))}
</>
);
};
const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
const {
action,
@@ -55,22 +150,71 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
currentData,
collapseProps
} = props;
const [instanceTypeList] = useAtom(regionInstanceTypeListAtom);
const [osImageList] = useAtom(regionOSImageListAtom);
const { collapsible, onToggle, ...restCollapseProps } = collapseProps || {};
const [form] = Form.useForm();
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const labels = Form.useWatch('labels', form);
const title = Form.useWatch('name', form);
const [instanceSpec, setInstanceSpec] = useState<Record<string, any>>({});
useEffect(() => {
if (currentData) {
console.log('currentData===========1=', currentData);
form.setFieldsValue({
...currentData
});
setInstanceSpec({
...currentData.instance_spec
});
}
}, [currentData]);
const labelRender = (data: { label: string; value: string }) => {
if (action === PageAction.EDIT) {
return currentData?.image_name || currentData?.os_image;
}
return data.label;
};
const instanceLabelRender = (data: { label: string; value: string }) => {
if (action === PageAction.EDIT) {
return currentData?.instance_spec?.label || currentData?.instance_type;
}
return data.label;
};
const handleOsImageChange = (value: string) => {
form.setFieldsValue({
image_name: osImageList.find((item) => item.value === value)?.label
});
};
const handleInstanceTypeChange = (value: string, option: any) => {
setInstanceSpec({
...option.specInfo,
label: option.label,
vendor: option.vendor,
description: option.description
});
form.setFieldsValue({
instance_spec: {
...option.specInfo,
label: option.label,
vendor: option.vendor,
description: option.description
}
});
};
const filterInstanceOption = (inputValue: string, option: any) => {
return (
option.label.toLowerCase().includes(inputValue.toLowerCase()) ||
option.description.toLowerCase().includes(inputValue.toLowerCase())
);
};
useImperativeHandle(ref, () => ({
resetFields: () => {
form.resetFields();
@@ -149,13 +293,20 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
}
]}
>
<SealInput.Input
<SealSelect
showSearch
filterOption={filterInstanceOption}
labelRender={instanceLabelRender}
notFoundContent={<NotFoundContent />}
label={intl.formatMessage({
id: 'clusters.workerpool.instanceType'
})}
required
options={instanceTypeList}
disabled={action === PageAction.EDIT}
></SealInput.Input>
optionRender={RenderInstanceOption}
onChange={handleInstanceTypeChange}
></SealSelect>
</Form.Item>
<Form.Item<FormData>
name="replicas"
@@ -192,6 +343,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
required
></SealInputNumber>
</Form.Item>
<Form.Item<FormData>
name="os_image"
rules={[
@@ -201,13 +353,18 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
}
]}
>
<SealInput.Input
<SealSelect
showSearch
onChange={handleOsImageChange}
optionRender={RenderInstanceOption}
labelRender={labelRender}
options={osImageList}
disabled={action === PageAction.EDIT}
label={intl.formatMessage({
id: 'clusters.workerpool.osImage'
})}
required
></SealInput.Input>
></SealSelect>
</Form.Item>
<Form.Item<FormData>
@@ -235,12 +392,17 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
]}
>
<LabelSelector
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'resources.table.labels' })}
labels={labels || {}}
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
></LabelSelector>
</Form.Item>
<VolumesConfig></VolumesConfig>
<VolumesConfig disabled={action === PageAction.EDIT}></VolumesConfig>
<Form.Item<FormData> name="image_name" hidden>
<SealInput.Input></SealInput.Input>
</Form.Item>
<InstanceSpecData instanceSpec={instanceSpec} />
</Container>
</Form>
</CollapsibleContainer>
@@ -7,6 +7,7 @@ import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Col, message, Row } from 'antd';
import _ from 'lodash';
import React, { useRef, useState } from 'react';
import { deleteWorkerPool, updateWorkerPool } from '../apis';
import { ProviderType } from '../config';
@@ -78,7 +79,7 @@ const PoolRows: React.FC<PoolRowsProps> = ({
action: PageAction.EDIT,
title: intl.formatMessage(
{ id: 'common.button.edit.item' },
{ name: record.instance_type }
{ name: record.name }
),
provider: provider,
currentData: record,
@@ -125,20 +126,14 @@ const PoolRows: React.FC<PoolRowsProps> = ({
{columns.map((col: Record<string, any>) => {
return (
<Col
key={col.dataIndex as string}
key={col.dataIndex || col.key}
span={col.span}
style={{
paddingInline: 0,
...(col.style || {})
}}
>
{/* {col.render
? col.render(data[col.dataIndex as string], data)
: data[col.dataIndex as string]} */}
<CellContent
{...col}
dataIndex={col.dataIndex}
></CellContent>
<CellContent {..._.omit(col, ['key'])}></CellContent>
</Col>
);
})}
@@ -63,7 +63,9 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
<Container>
{Object.entries(groupList).map(([groupName, items]) => (
<div key={groupName}>
{groupName !== 'default' && <Title>{groupName}</Title>}
{groupName !== 'default' && (
<Title>{intl.formatMessage({ id: groupName })}</Title>
)}
<Wrapper>
{items?.map((action) => (
<Card
@@ -8,7 +8,9 @@ const volumeOptions = fieldConfig.volumes;
const CloudOptions: React.FC<{
ref?: any;
disabled?: boolean;
}> = forwardRef((props, ref) => {
const { disabled } = props;
const intl = useIntl();
const form = Form.useFormInstance();
const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form);
@@ -25,6 +27,7 @@ const CloudOptions: React.FC<{
label={intl.formatMessage({ id: 'clusters.workerpool.volumes' })}
dataList={volumes || []}
properties={volumeOptions.properties || {}}
disabled={disabled}
onChange={(value) => handleOnChange(volumeOptions.name, value)}
/>
</Form.Item>
@@ -73,7 +73,7 @@ const WorkerPools = () => {
action: PageAction.EDIT,
title: intl.formatMessage(
{ id: 'common.button.edit.item' },
{ name: record.instance_type }
{ name: record.name }
),
provider: searchParams.get('provider') as ProviderType,
currentData: record,