fix: display instance type spec
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user