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
+4
View File
@@ -257,6 +257,10 @@
color: var(--ant-color-text-tertiary); color: var(--ant-color-text-tertiary);
} }
.text-quaternary {
color: var(--ant-color-text-quaternary);
}
.tips-desc-list { .tips-desc-list {
list-style-type: decimal; list-style-type: decimal;
padding-left: 16px; padding-left: 16px;
+40
View File
@@ -2,3 +2,43 @@ import { atom } from 'jotai';
// models expand keys: create, update , delete, // models expand keys: create, update , delete,
export const expandKeysAtom = atom<string[]>([]); export const expandKeysAtom = atom<string[]>([]);
export const regionListAtom = atom<
{
datacenter: string;
label: string;
value: string;
icon: string;
sizes: string[];
}[]
>([]);
export const regionInstanceTypeListAtom = atom<
{
label: string;
value: string;
}[]
>([]);
export const regionOSImageListAtom = atom<
{
label: string;
value: string;
}[]
>([]);
export const allRegionOSImageListAtom = atom<
{
label: string;
value: string;
regions: string[];
}[]
>([]);
export const allRegionInstanceTypeListAtom = atom<
{
label: string;
value: string;
regions: string[];
}[]
>([]);
+2 -2
View File
@@ -173,8 +173,8 @@ export default function CollapsibleContainer({
ref={contentRef} ref={contentRef}
style={{ style={{
maxHeight: height, maxHeight: height,
overflow: 'hidden', overflow: 'hidden'
transition: collapsible ? 'max-height 0.2s ease' : 'none' // transition: collapsible ? 'max-height 0.2s ease' : 'none'
}} }}
> >
<div style={{ paddingTop: 8 }}>{children}</div> <div style={{ paddingTop: 8 }}>{children}</div>
+1 -1
View File
@@ -76,7 +76,7 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
disabled={disabled} disabled={disabled}
trigger={trigger} trigger={trigger}
type="primary" type="primary"
dropdownRender={(menus: any) => { popupRender={(menus: any) => {
return ( return (
<DropdownWrapper> <DropdownWrapper>
{_.map(_.tail(items), (item: any) => { {_.map(_.tail(items), (item: any) => {
@@ -18,6 +18,7 @@ const FormWidget: React.FC<
min, min,
max, max,
checked, checked,
readOnly: disabled,
onChange onChange
}) => { }) => {
const Component = ComponentsMap[widget]; const Component = ComponentsMap[widget];
@@ -29,7 +30,15 @@ const FormWidget: React.FC<
return Component ? ( return Component ? (
<Component <Component
{...{ label, required, placeholder, description, min, max }} {...{
label,
required,
placeholder,
description,
min,
max
}}
disabled={disabled}
options={options || optionList} options={options || optionList}
value={value} value={value}
checked={checked} checked={checked}
@@ -24,16 +24,23 @@ interface ListMapProps {
label?: React.ReactNode; label?: React.ReactNode;
btnText?: string; btnText?: string;
properties: Record<string, any>; properties: Record<string, any>;
disabled?: boolean;
onChange?: (data: any) => void; onChange?: (data: any) => void;
} }
interface ListItemProps { interface ListItemProps {
schemaList: any[]; schemaList: any[];
data: Record<string, any>; data: Record<string, any>;
disabled?: boolean;
onChange?: (data: any) => void; onChange?: (data: any) => void;
} }
const ListItem: React.FC<ListItemProps> = ({ schemaList, data, onChange }) => { const ListItem: React.FC<ListItemProps> = ({
schemaList,
data,
onChange,
disabled
}) => {
const handleValueChange = (name: string, target: any) => { const handleValueChange = (name: string, target: any) => {
if (target?.target?.type === 'checkbox') { if (target?.target?.type === 'checkbox') {
const checked = target.target?.checked; const checked = target.target?.checked;
@@ -50,6 +57,7 @@ const ListItem: React.FC<ListItemProps> = ({ schemaList, data, onChange }) => {
<FormWidget <FormWidget
widget={schema.type} widget={schema.type}
{...schema} {...schema}
disabled={disabled}
key={schema.name} key={schema.name}
value={data?.[schema.name]} value={data?.[schema.name]}
checked={data?.[schema.name]} checked={data?.[schema.name]}
@@ -66,6 +74,7 @@ const ListMap: React.FC<ListMapProps> = ({
btnText, btnText,
properties = {}, properties = {},
minItems = 0, minItems = 0,
disabled,
onChange onChange
}) => { }) => {
const [items, setItems] = React.useState(dataList || []); const [items, setItems] = React.useState(dataList || []);
@@ -110,28 +119,36 @@ const ListMap: React.FC<ListMapProps> = ({
}, [dataList]); }, [dataList]);
return ( return (
<Wrapper label={label} btnText={btnText} onAdd={handleOnAdd}> <Wrapper
label={label}
btnText={btnText}
onAdd={handleOnAdd}
disabled={disabled}
>
{items.map((item, index) => ( {items.map((item, index) => (
<RowWrapper key={index}> <RowWrapper key={index}>
<WidgetBox> <WidgetBox>
<ListItem <ListItem
schemaList={schemaList} schemaList={schemaList}
data={item} data={item}
disabled={disabled}
onChange={(value) => handleItemChange(index, value)} onChange={(value) => handleItemChange(index, value)}
/> />
</WidgetBox> </WidgetBox>
<Button {!disabled && (
size="small" <Button
type="default" size="small"
shape="circle" type="default"
style={{ shape="circle"
width: 24, style={{
marginLeft: 10, width: 24,
flex: 'none' marginLeft: 10,
}} flex: 'none'
icon={<MinusOutlined />} }}
onClick={() => handleDelete(index)} icon={<MinusOutlined />}
/> onClick={() => handleDelete(index)}
/>
)}
</RowWrapper> </RowWrapper>
))} ))}
</Wrapper> </Wrapper>
@@ -23,6 +23,7 @@ export interface FormWidgetProps {
title?: string; title?: string;
required?: boolean; required?: boolean;
placeholder?: string; placeholder?: string;
readOnly?: boolean;
options?: { label: string; value: string | number }[]; options?: { label: string; value: string | number }[];
description?: string; description?: string;
enum?: (string | number)[]; enum?: (string | number)[];
+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_l0y0uhurkz8.js' scriptUrl: '//at.alicdn.com/t/c/font_4613488_5jrvj6qs39.js'
}); });
export default IconFont; export default IconFont;
+4 -1
View File
@@ -8,6 +8,7 @@ interface LabelSelectorProps {
label?: string; label?: string;
btnText?: string; btnText?: string;
description?: React.ReactNode; description?: React.ReactNode;
disabled?: boolean;
onChange?: (labels: Record<string, any>) => void; onChange?: (labels: Record<string, any>) => void;
onBlur?: (e: any, type: string, index: number) => void; onBlur?: (e: any, type: string, index: number) => void;
onDelete?: (index: number) => void; onDelete?: (index: number) => void;
@@ -18,6 +19,7 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
onChange, onChange,
onBlur, onBlur,
onDelete, onDelete,
disabled,
label, label,
btnText, btnText,
description description
@@ -81,6 +83,7 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
return ( return (
<Inner <Inner
disabled={disabled}
label={label} label={label}
btnText={btnText} btnText={btnText}
description={ description={
@@ -97,4 +100,4 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
); );
}; };
export default React.memo(LabelSelector); export default LabelSelector;
+4
View File
@@ -14,6 +14,7 @@ interface LabelSelectorProps {
onBlur?: (e: any, type: string, index: number) => void; onBlur?: (e: any, type: string, index: number) => void;
onDelete?: (index: number) => void; onDelete?: (index: number) => void;
description?: React.ReactNode; description?: React.ReactNode;
disabled?: boolean;
} }
const Inner: React.FC<LabelSelectorProps> = ({ const Inner: React.FC<LabelSelectorProps> = ({
@@ -24,6 +25,7 @@ const Inner: React.FC<LabelSelectorProps> = ({
onPaste, onPaste,
onBlur, onBlur,
onDelete, onDelete,
disabled,
label, label,
btnText, btnText,
description description
@@ -75,12 +77,14 @@ const Inner: React.FC<LabelSelectorProps> = ({
label={label} label={label}
description={description} description={description}
onAdd={handleAddLabel} onAdd={handleAddLabel}
disabled={disabled}
btnText={btnText} btnText={btnText}
> >
<> <>
{labelList?.map((item: any, index: number) => { {labelList?.map((item: any, index: number) => {
return ( return (
<LabelItem <LabelItem
disabled={disabled}
key={index} key={index}
label={item} label={item}
seperator=":" seperator=":"
+16 -10
View File
@@ -15,8 +15,9 @@ interface LabelItemProps {
keyAddon?: React.ReactNode; keyAddon?: React.ReactNode;
valueAddon?: React.ReactNode; valueAddon?: React.ReactNode;
seperator?: string; seperator?: string;
onDelete?: () => void;
labelList: { key: string; value: string }[]; labelList: { key: string; value: string }[];
disabled?: boolean;
onDelete?: () => void;
onChange?: (params: { key: string; value: string }) => void; onChange?: (params: { key: string; value: string }) => void;
onPaste?: (e: any) => void; onPaste?: (e: any) => void;
onBlur?: (e: any, type: string) => void; onBlur?: (e: any, type: string) => void;
@@ -27,6 +28,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
seperator, seperator,
keyAddon, keyAddon,
valueAddon, valueAddon,
disabled,
onChange, onChange,
onDelete, onDelete,
onPaste, onPaste,
@@ -82,6 +84,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
title={intl.formatMessage({ id: 'resources.table.key.tips' })} title={intl.formatMessage({ id: 'resources.table.key.tips' })}
> >
<SealInput.Input <SealInput.Input
disabled={disabled}
checkStatus="success" checkStatus="success"
label={intl.formatMessage({ id: 'common.input.key' })} label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key} value={label.key}
@@ -96,6 +99,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
<div className="label-value"> <div className="label-value">
{valueAddon ?? ( {valueAddon ?? (
<SealInput.Input <SealInput.Input
disabled={disabled}
checkStatus={label.value ? 'success' : ''} checkStatus={label.value ? 'success' : ''}
label={intl.formatMessage({ id: 'common.input.value' })} label={intl.formatMessage({ id: 'common.input.value' })}
value={label.value} value={label.value}
@@ -104,15 +108,17 @@ const LabelItem: React.FC<LabelItemProps> = ({
></SealInput.Input> ></SealInput.Input>
)} )}
</div> </div>
<Button {!disabled && (
size="small" <Button
className="btn" size="small"
type="default" className="btn"
shape="circle" type="default"
onClick={onDelete} shape="circle"
> onClick={onDelete}
<MinusOutlined /> >
</Button> <MinusOutlined />
</Button>
)}
</div> </div>
); );
}; };
+15 -11
View File
@@ -11,6 +11,7 @@ interface WrapperProps {
labelExtra?: React.ReactNode; labelExtra?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
btnText?: string; btnText?: string;
disabled?: boolean;
onAdd?: () => void; onAdd?: () => void;
button?: React.ReactNode; button?: React.ReactNode;
} }
@@ -44,6 +45,7 @@ const Wrapper: React.FC<WrapperProps> = ({
labelExtra, labelExtra,
onAdd, onAdd,
btnText, btnText,
disabled,
button button
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@@ -59,17 +61,19 @@ const Wrapper: React.FC<WrapperProps> = ({
</span> </span>
)} )}
{children} {children}
<ButtonWrapper> {!disabled && (
{button || ( <ButtonWrapper>
<Button variant="filled" color="default" block onClick={onAdd}> {button || (
<PlusOutlined className="font-size-14" /> <Button variant="filled" color="default" block onClick={onAdd}>
{btnText || <PlusOutlined className="font-size-14" />
intl.formatMessage({ {btnText ||
id: 'common.button.addSelector' intl.formatMessage({
})} id: 'common.button.addSelector'
</Button> })}
)} </Button>
</ButtonWrapper> )}
</ButtonWrapper>
)}
</Container> </Container>
); );
}; };
+1 -1
View File
@@ -185,7 +185,7 @@ const SealCascader: React.FC<
onBlur={handleOnBlur} onBlur={handleOnBlur}
onChange={handleChange} onChange={handleChange}
notFoundContent={null} notFoundContent={null}
onDropdownVisibleChange={handleDropdownVisibleChange} onOpenChange={handleDropdownVisibleChange}
></Cascader> ></Cascader>
</Wrapper> </Wrapper>
</SelectWrapper> </SelectWrapper>
+2 -1
View File
@@ -18,6 +18,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
options, options,
allowNull, allowNull,
isInFormItems = true, isInFormItems = true,
notFoundContent = null,
...rest ...rest
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
@@ -102,7 +103,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
onFocus={handleOnFocus} onFocus={handleOnFocus}
onBlur={handleOnBlur} onBlur={handleOnBlur}
onChange={handleChange} onChange={handleChange}
notFoundContent={null} notFoundContent={notFoundContent}
> >
{children} {children}
</Select> </Select>
+2 -2
View File
@@ -241,7 +241,7 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
options={optionsList} options={optionsList}
maxTagCount={0} maxTagCount={0}
defaultActiveFirstOption={false} defaultActiveFirstOption={false}
dropdownRender={dropdownRender} popupRender={dropdownRender}
optionRender={optionRender} optionRender={optionRender}
menuItemSelectedIcon={false} menuItemSelectedIcon={false}
onChange={handleOnChange} onChange={handleOnChange}
@@ -250,7 +250,7 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
onFocus={handleOnFocus} onFocus={handleOnFocus}
onSearch={handleOnSearch} onSearch={handleOnSearch}
filterOption={filterOption} filterOption={filterOption}
onDropdownVisibleChange={handleOnOpenChange} onOpenChange={handleOnOpenChange}
></Select> ></Select>
</div> </div>
); );
@@ -156,6 +156,7 @@ export const Label = styled.div.attrs<{
// inner // inner
const Inner = styled.div` const Inner = styled.div`
width: 100%; width: 100%;
display: flex;
`; `;
const Extra = styled.div` const Extra = styled.div`
@@ -79,6 +79,7 @@ const InputWrapper = styled.div`
} }
.ant-input-number { .ant-input-number {
flex: 1;
position: static; position: static;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -87,6 +88,7 @@ const InputWrapper = styled.div`
padding: 0; padding: 0;
height: ${INPUTHEIGHT}px !important; height: ${INPUTHEIGHT}px !important;
background-color: ${BGCOLOR}; background-color: ${BGCOLOR};
flex: 1;
&:hover .ant-input-number-handler-wrap, &:hover .ant-input-number-handler-wrap,
&-focused .ant-input-number-handler-wrap { &-focused .ant-input-number-handler-wrap {
+2 -2
View File
@@ -98,8 +98,8 @@ const SelectWrapper = styled.div`
height: 54px; height: 54px;
.ant-select-selection-search { .ant-select-selection-search {
top: 20px !important; // top: 20px !important;
inset-inline-start: ${INPUT_INNER_PADDING}px; // inset-inline-start: ${INPUT_INNER_PADDING}px;
} }
&.ant-select-multiple.ant-cascader .ant-select-selection-search { &.ant-select-multiple.ant-cascader .ant-select-selection-search {
top: 0 !important; top: 0 !important;
+1
View File
@@ -90,6 +90,7 @@ export default function useTableFetch<T>(
const updateHandler = (list: any) => { const updateHandler = (list: any) => {
_.each(list, (data: any) => { _.each(list, (data: any) => {
console.log('list================:', list);
updateChunkedList(data); updateChunkedList(data);
}); });
}; };
+8 -1
View File
@@ -25,5 +25,12 @@ export default {
'clusters.workerpool.size': 'Size (GiB)', 'clusters.workerpool.size': 'Size (GiB)',
'clusters.workerpool.title': 'Worker Pools', 'clusters.workerpool.title': 'Worker Pools',
'clusters.workerpool.cloudOptions': 'Add Cloud Options', 'clusters.workerpool.cloudOptions': 'Add Cloud Options',
'clusters.workerpool.volumes.add': 'Add Volume' 'clusters.workerpool.volumes.add': 'Add Volume',
'clusters.create.provider.self': 'Self-Hosted',
'clusters.create.provider.cloud': 'Cloud Provider',
'clusters.create.selectProvider': 'Select Provider',
'clusters.create.configBasic': 'Basic Configuration',
'clusters.create.execCommand': 'Execute Command',
'clusters.create.supportedGpu': 'Supported GPUs',
'clusters.create.skipfornow': 'Skip for now'
}; };
+15 -1
View File
@@ -25,7 +25,14 @@ export default {
'clusters.workerpool.size': 'Size (GiB)', 'clusters.workerpool.size': 'Size (GiB)',
'clusters.workerpool.title': 'Worker Pools', 'clusters.workerpool.title': 'Worker Pools',
'clusters.workerpool.cloudOptions': 'Add Cloud Options', 'clusters.workerpool.cloudOptions': 'Add Cloud Options',
'clusters.workerpool.volumes.add': 'Add Volume' 'clusters.workerpool.volumes.add': 'Add Volume',
'clusters.create.provider.self': 'Self-Hosted',
'clusters.create.provider.cloud': 'Cloud Provider',
'clusters.create.selectProvider': 'Select Provider',
'clusters.create.configBasic': 'Basic Configuration',
'clusters.create.execCommand': 'Execute Command',
'clusters.create.supportedGpu': 'Supported GPUs',
'clusters.create.skipfornow': 'Skip for now'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -56,5 +63,12 @@ export default {
// 25. 'clusters.workerpool.title': 'Worker Pools', // 25. 'clusters.workerpool.title': 'Worker Pools',
// 26. 'clusters.workerpool.cloudOptions': 'Add Cloud Options', // 26. 'clusters.workerpool.cloudOptions': 'Add Cloud Options',
// 27. 'clusters.workerpool.volumes.add': 'Add Volume' // 27. 'clusters.workerpool.volumes.add': 'Add Volume'
// 28. 'clusters.create.provider.self': 'Self-Hosted',
// 29. 'clusters.create.provider.cloud': 'Cloud Provider',
// 30. 'clusters.create.selectProvider': 'Select Provider',
// 31. 'clusters.create.configBasic': 'Basic Configuration',
// 32. 'clusters.create.execCommand': 'Execute Command',
// 33. 'clusters.create.supportedGpu': 'Supported GPUs',
// 34. 'clusters.create.skipfornow': 'Skip for now'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+17 -2
View File
@@ -25,7 +25,14 @@ export default {
'clusters.workerpool.size': 'Size (GiB)', 'clusters.workerpool.size': 'Size (GiB)',
'clusters.workerpool.title': 'Worker Pools', 'clusters.workerpool.title': 'Worker Pools',
'clusters.workerpool.cloudOptions': 'Add Cloud Options', 'clusters.workerpool.cloudOptions': 'Add Cloud Options',
'clusters.workerpool.volumes.add': 'Add Volume' 'clusters.workerpool.volumes.add': 'Add Volume',
'clusters.create.provider.self': 'Self-Hosted',
'clusters.create.provider.cloud': 'Cloud Provider',
'clusters.create.selectProvider': 'Select Provider',
'clusters.create.configBasic': 'Basic Configuration',
'clusters.create.execCommand': 'Execute Command',
'clusters.create.supportedGpu': 'Supported GPUs',
'clusters.create.skipfornow': 'Skip for now'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -55,5 +62,13 @@ export default {
// 24. 'clusters.workerpool.size': 'Size (GiB)', // 24. 'clusters.workerpool.size': 'Size (GiB)',
// 25. 'clusters.workerpool.title': 'Worker Pools', // 25. 'clusters.workerpool.title': 'Worker Pools',
// 26. 'clusters.workerpool.cloudOptions': 'Add Cloud Options', // 26. 'clusters.workerpool.cloudOptions': 'Add Cloud Options',
// 27. 'clusters.workerpool.volumes.add': 'Add Volume' // 27. 'clusters.workerpool.volumes.add': 'Add Volume',
// 28. 'clusters.create.provider.self': 'Self-Hosted',
// 29. 'clusters.create.provider.cloud': 'Cloud Provider',
// 30. 'clusters.create.selectProvider': 'Select Provider',
// 31. 'clusters.create.configBasic': 'Basic Configuration',
// 32. 'clusters.create.execCommand': 'Execute Command',
// 33. 'clusters.create.supportedGpu': 'Supported GPUs',
// 34. 'clusters.create.skipfornow': 'Skip for now'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+10 -3
View File
@@ -10,7 +10,7 @@ export default {
'clusters.edit.cluster': '编辑 {cluster}', 'clusters.edit.cluster': '编辑 {cluster}',
'clusters.provider.custom': '自定义', 'clusters.provider.custom': '自定义',
'clusters.button.register': '注册集群', 'clusters.button.register': '注册集群',
'clusters.button.addNodePool': '添加节点池', 'clusters.button.addNodePool': '添加 Worker Pool',
'clusters.button.add.credential': '添加 {provider} 凭证', 'clusters.button.add.credential': '添加 {provider} 凭证',
'clusters.credential.title': '凭证', 'clusters.credential.title': '凭证',
'clusters.credential.token': '访问令牌', 'clusters.credential.token': '访问令牌',
@@ -23,7 +23,14 @@ export default {
'clusters.workerpool.volumes': '存储卷', 'clusters.workerpool.volumes': '存储卷',
'clusters.workerpool.format': '文件系统格式', 'clusters.workerpool.format': '文件系统格式',
'clusters.workerpool.size': '容量(GiB', 'clusters.workerpool.size': '容量(GiB',
'clusters.workerpool.title': '节点池', 'clusters.workerpool.title': 'Worker Pools',
'clusters.workerpool.cloudOptions': '添加云配置', 'clusters.workerpool.cloudOptions': '添加云配置',
'clusters.workerpool.volumes.add': '添加存储卷' 'clusters.workerpool.volumes.add': '添加存储卷',
'clusters.create.provider.self': '自建环境',
'clusters.create.provider.cloud': '云环境',
'clusters.create.selectProvider': '选择环境',
'clusters.create.configBasic': '基本配置',
'clusters.create.execCommand': '执行命令',
'clusters.create.supportedGpu': '支持的 GPU',
'clusters.create.skipfornow': '暂时跳过'
}; };
@@ -17,8 +17,57 @@ export const WORKER_POOLS_API = '/worker-pools';
export const CLUSTER_TOKEN = 'registration-token'; export const CLUSTER_TOKEN = 'registration-token';
export const PROVIDER_PROXY_API = '/provider-proxy';
// ============= DigitalOcean start =====================
export const REGIONS_API = '/v2/regions';
export const INSTANCE_TYPE = '/v2/sizes';
export const OS_IMAGE = '/v2/images';
// ============= DigitalOcean end =======================
// ===================== Credentials ===================== // ===================== Credentials =====================
export async function queryDigitalOceanRegions(params: { id: number }) {
return request(
`${CREDENTIALS_API}/${params.id}${PROVIDER_PROXY_API}${REGIONS_API}`,
{
method: 'GET',
params: {
per_page: 200
}
}
);
}
export async function queryDigitalOceanInstanceTypes(params: { id: number }) {
return request(
`${CREDENTIALS_API}/${params.id}${PROVIDER_PROXY_API}${INSTANCE_TYPE}`,
{
method: 'GET',
params: {
per_page: 200
}
}
);
}
export async function queryDigitalOceanOSImages(params: { id: number }) {
return request(
`${CREDENTIALS_API}/${params.id}${PROVIDER_PROXY_API}${OS_IMAGE}`,
{
method: 'GET',
params: {
per_page: 200,
type: 'distribution'
}
}
);
}
export async function queryCredentialList(params: Global.SearchParams) { export async function queryCredentialList(params: Global.SearchParams) {
return request<Global.PageResponse<CredentialListItem>>( return request<Global.PageResponse<CredentialListItem>>(
`${CREDENTIALS_API}`, `${CREDENTIALS_API}`,
+22 -27
View File
@@ -9,7 +9,8 @@ import { createCluster, queryClusterToken, queryCredentialList } from './apis';
import ClusterSteps from './components/cluster-steps'; import ClusterSteps from './components/cluster-steps';
import FooterButtons from './components/footer-buttons'; import FooterButtons from './components/footer-buttons';
import ProviderCatalog from './components/provider-catalog'; import ProviderCatalog from './components/provider-catalog';
import { providerList, ProviderType, ProviderValueMap } from './config'; import { ProviderType, ProviderValueMap } from './config';
import providerList from './config/providers';
import { ClusterFormData } from './config/types'; import { ClusterFormData } from './config/types';
import { moduleMap, moduleRegistry } from './step-forms/module-registry'; import { moduleMap, moduleRegistry } from './step-forms/module-registry';
import useStepList from './step-forms/use-step-list'; import useStepList from './step-forms/use-step-list';
@@ -63,7 +64,7 @@ const ClusterCreate = () => {
(searchParams.get('action') as PageActionType) || PageAction.CREATE; (searchParams.get('action') as PageActionType) || PageAction.CREATE;
const navigate = useNavigate(); const navigate = useNavigate();
const [credentialList, setCredentialList] = useState< const [credentialList, setCredentialList] = useState<
Global.BaseOption<number>[] Global.BaseOption<number, { provider: ProviderType }>[]
>([]); >([]);
const [currentStep, setCurrentStep] = useState<number>(startStep); const [currentStep, setCurrentStep] = useState<number>(startStep);
const [registrationInfo, setRegistrationInfo] = useState<{ const [registrationInfo, setRegistrationInfo] = useState<{
@@ -87,6 +88,11 @@ const ClusterCreate = () => {
[moduleMap.WorkerPoolForm]: useRef<any>(null) [moduleMap.WorkerPoolForm]: useRef<any>(null)
}; };
const availableCredentials = useMemo(
() => credentialList.filter((item) => item.provider === extraData.provider),
[credentialList, extraData.provider]
);
const steps = useMemo(() => { const steps = useMemo(() => {
if (!extraData.provider) { if (!extraData.provider) {
return stepList.filter((step) => step.defaultShow); return stepList.filter((step) => step.defaultShow);
@@ -182,6 +188,10 @@ const ClusterCreate = () => {
}; };
const handleSelectProvider = (value: ProviderType) => { const handleSelectProvider = (value: ProviderType) => {
if (value === extraData.provider) {
onNext();
return;
}
setExtraData({ setExtraData({
provider: value provider: value
} as ClusterFormData); } as ClusterFormData);
@@ -194,7 +204,8 @@ const ClusterCreate = () => {
const res = await queryCredentialList({ page: 1, perPage: 100 }); const res = await queryCredentialList({ page: 1, perPage: 100 });
const list = res.items?.map((item) => ({ const list = res.items?.map((item) => ({
label: item.name, label: item.name,
value: item.id value: item.id,
provider: item.provider
})); }));
setCredentialList(list); setCredentialList(list);
}; };
@@ -217,7 +228,7 @@ const ClusterCreate = () => {
ref={formRefs[key]} ref={formRefs[key]}
action={action} action={action}
provider={extraData.provider} provider={extraData.provider}
credentialList={credentialList} credentialList={availableCredentials}
currentData={formValues[key]} currentData={formValues[key]}
/> />
) : null; ) : null;
@@ -279,27 +290,7 @@ const ClusterCreate = () => {
/> />
]} ]}
header={{ header={{
title: ( title: false,
<div>
<Nav>
<span className="level-1">Cluster</span>
<span
style={{
marginInline: 20,
color: 'var(--ant-color-split)'
}}
>
/
</span>
<span className="level-2">create</span>
</Nav>
<ClusterSteps
steps={steps}
currentStep={currentStep}
onChange={handleStepChange}
></ClusterSteps>
</div>
),
style: { style: {
paddingInline: 'var(--layout-content-header-inlinepadding)' paddingInline: 'var(--layout-content-header-inlinepadding)'
}, },
@@ -308,7 +299,9 @@ const ClusterCreate = () => {
pageHeaderRender={() => ( pageHeaderRender={() => (
<HeaderContainer> <HeaderContainer>
<Nav> <Nav>
<span className="level-1">Cluster</span> <span className="level-1">
{intl.formatMessage({ id: 'clusters.title' })}
</span>
<span <span
style={{ style={{
marginInline: 20, marginInline: 20,
@@ -317,7 +310,9 @@ const ClusterCreate = () => {
> >
/ /
</span> </span>
<span className="level-2">create</span> <span className="level-2">
{intl.formatMessage({ id: 'common.button.create' })}
</span>
</Nav> </Nav>
<ClusterSteps <ClusterSteps
steps={steps} steps={steps}
+4 -1
View File
@@ -307,7 +307,10 @@ const Credentials: React.FC = () => {
); );
const setDisableExpand = (row: ClusterListItem) => { const setDisableExpand = (row: ClusterListItem) => {
return row.provider !== ProviderValueMap.DigitalOcean; return (
row.provider !== ProviderValueMap.DigitalOcean ||
!allWorkerPoolList.some((item) => item.cluster_id === row.id)
);
}; };
useEffect(() => { useEffect(() => {
@@ -29,7 +29,7 @@ const AddPool: React.FC<AddModalProps> = ({
}) => { }) => {
const formRef = useRef<any>(null); const formRef = useRef<any>(null);
const handleSubmit = () => { const handleSubmit = async () => {
formRef.current?.submit?.(); formRef.current?.submit?.();
}; };
@@ -1,3 +1,4 @@
import { useIntl } from '@umijs/max';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import RegisterClusterInner from './register-cluster-inner'; import RegisterClusterInner from './register-cluster-inner';
@@ -18,11 +19,14 @@ type AddModalProps = {
}; };
}; };
const AddWorkerStep: React.FC<AddModalProps> = ({ registrationInfo }) => { const AddWorkerStep: React.FC<AddModalProps> = ({ registrationInfo }) => {
const intl = useIntl();
return ( return (
<div> <div>
<Title>Execute Command</Title> <Title>{intl.formatMessage({ id: 'clusters.create.execCommand' })}</Title>
<RegisterClusterInner registrationInfo={registrationInfo} /> <RegisterClusterInner registrationInfo={registrationInfo} />
<Title style={{ marginTop: 32 }}>Supported GPUs</Title> <Title style={{ marginTop: 32 }}>
{intl.formatMessage({ id: 'clusters.create.supportedGpu' })}
</Title>
<SupportedHardware /> <SupportedHardware />
</div> </div>
); );
@@ -1,12 +1,15 @@
import SealSelect from '@/components/seal-form/seal-select'; 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 useAppUtils from '@/hooks/use-app-utils';
import { LoadingOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React, { useEffect } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { regionList } from '../config';
import { ClusterFormData as FormData } from '../config/types'; import { ClusterFormData as FormData } from '../config/types';
import { useProviderRegions } from '../hooks/use-provider-regions';
type OptionData = { type OptionData = {
label: string; 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 { interface CloudProviderProps {
provider: string; // 'kubernetes' | 'digitalocean'; provider: string; // 'kubernetes' | 'digitalocean';
credentialList: Global.BaseOption<number>[]; 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 = ( const optionRender = (
option: Global.BaseOption< option: Global.BaseOption<
number, 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 CloudProvider: React.FC<CloudProviderProps> = (props) => {
const { credentialList } = props; const { credentialList, action, credentialID } = props;
const intl = useIntl(); const intl = useIntl();
const {
getRegions,
getOSImages,
updateOSImages,
updateInstanceTypes,
getInstanceTypes,
setLoading,
loading,
regions
} = useProviderRegions();
const { getRuleMessage } = useAppUtils(); 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 ( return (
<> <>
<Form.Item<FormData> <Form.Item<FormData>
@@ -100,6 +162,7 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
label={intl.formatMessage({ id: 'clusters.credential.title' })} label={intl.formatMessage({ id: 'clusters.credential.title' })}
required required
options={credentialList} options={credentialList}
onChange={handleCredentialChange}
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
<Form.Item<FormData> <Form.Item<FormData>
@@ -112,11 +175,16 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
]} ]}
> >
<SealSelect <SealSelect
showSearch
label={intl.formatMessage({ id: 'clusters.workerpool.region' })} label={intl.formatMessage({ id: 'clusters.workerpool.region' })}
required required
options={regionList} options={regions}
loading={loading}
filterOption={filterRegionOption}
labelRender={labelRender} labelRender={labelRender}
optionRender={optionRender} optionRender={optionRender}
onChange={handleRegionChange}
notFoundContent={<NotFoundContent loading={loading} />}
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
</> </>
@@ -78,6 +78,8 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
{provider === ProviderValueMap.DigitalOcean && ( {provider === ProviderValueMap.DigitalOcean && (
<CloudProvider <CloudProvider
provider={provider} provider={provider}
action={action}
credentialID={currentData?.credential_id}
credentialList={credentialList} credentialList={credentialList}
></CloudProvider> ></CloudProvider>
)} )}
@@ -32,18 +32,18 @@ const FooterButtons: React.FC<FooterButtonsProps> = (props) => {
<div className="flex-center gap-16"> <div className="flex-center gap-16">
{showButtons.previous && ( {showButtons.previous && (
<Button type="link" icon={<LeftOutlined />} onClick={onPrevious}> <Button type="link" icon={<LeftOutlined />} onClick={onPrevious}>
Previous {intl.formatMessage({ id: 'common.button.prev' })}
</Button> </Button>
)} )}
{showButtons.next && ( {showButtons.next && (
<Button type="link" onClick={handleOnNext}> <Button type="link" onClick={handleOnNext}>
Next {intl.formatMessage({ id: 'common.button.next' })}
<RightOutlined /> <RightOutlined />
</Button> </Button>
)} )}
{showButtons.skip && ( {showButtons.skip && (
<Button type="primary" onClick={handleCancel}> <Button type="primary" onClick={handleCancel}>
Skip for now {intl.formatMessage({ id: 'clusters.create.skipfornow' })}
</Button> </Button>
)} )}
{showButtons.save && ( {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, { import CollapsibleContainer, {
CollapsibleContainerProps CollapsibleContainerProps
} from '@/components/collapse-container'; } from '@/components/collapse-container';
import IconFont from '@/components/icon-font';
import LabelSelector from '@/components/label-selector'; import LabelSelector from '@/components/label-selector';
import SealInputNumber from '@/components/seal-form/input-number'; import SealInputNumber from '@/components/seal-form/input-number';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
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 { CardContainer } from '@/pages/llmodels/components/gpu-card';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd'; import { Button, Form } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import React, { forwardRef, useEffect, useImperativeHandle } from 'react'; import React, {
forwardRef,
useEffect,
useImperativeHandle,
useState
} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ProviderType } from '../config'; import { ProviderType, instanceTypeFieldMap, vendorIconMap } from '../config';
import { NodePoolFormData as FormData } from '../config/types'; import { NodePoolFormData as FormData } from '../config/types';
import VolumesConfig from './volumes-config'; import VolumesConfig from './volumes-config';
@@ -22,6 +35,12 @@ const Container = styled.div`
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-gap: 0 16px; 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) { .ant-form-item:nth-child(5) {
grid-column: 1 / 3; 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 = { type AddModalProps = {
ref: any; ref: any;
name?: string; name?: string;
@@ -45,6 +123,23 @@ type AddModalProps = {
showDelete?: boolean; showDelete?: boolean;
collapseProps?: CollapsibleContainerProps; 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 PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
const { const {
action, action,
@@ -55,22 +150,71 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
currentData, currentData,
collapseProps collapseProps
} = props; } = props;
const [instanceTypeList] = useAtom(regionInstanceTypeListAtom);
const [osImageList] = useAtom(regionOSImageListAtom);
const { collapsible, onToggle, ...restCollapseProps } = collapseProps || {}; const { collapsible, onToggle, ...restCollapseProps } = collapseProps || {};
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl(); const intl = useIntl();
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
const labels = Form.useWatch('labels', form); const labels = Form.useWatch('labels', form);
const title = Form.useWatch('name', form); const title = Form.useWatch('name', form);
const [instanceSpec, setInstanceSpec] = useState<Record<string, any>>({});
useEffect(() => { useEffect(() => {
if (currentData) { if (currentData) {
console.log('currentData===========1=', currentData);
form.setFieldsValue({ form.setFieldsValue({
...currentData ...currentData
}); });
setInstanceSpec({
...currentData.instance_spec
});
} }
}, [currentData]); }, [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, () => ({ useImperativeHandle(ref, () => ({
resetFields: () => { resetFields: () => {
form.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({ label={intl.formatMessage({
id: 'clusters.workerpool.instanceType' id: 'clusters.workerpool.instanceType'
})} })}
required required
options={instanceTypeList}
disabled={action === PageAction.EDIT} disabled={action === PageAction.EDIT}
></SealInput.Input> optionRender={RenderInstanceOption}
onChange={handleInstanceTypeChange}
></SealSelect>
</Form.Item> </Form.Item>
<Form.Item<FormData> <Form.Item<FormData>
name="replicas" name="replicas"
@@ -192,6 +343,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
required required
></SealInputNumber> ></SealInputNumber>
</Form.Item> </Form.Item>
<Form.Item<FormData> <Form.Item<FormData>
name="os_image" name="os_image"
rules={[ 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} disabled={action === PageAction.EDIT}
label={intl.formatMessage({ label={intl.formatMessage({
id: 'clusters.workerpool.osImage' id: 'clusters.workerpool.osImage'
})} })}
required required
></SealInput.Input> ></SealSelect>
</Form.Item> </Form.Item>
<Form.Item<FormData> <Form.Item<FormData>
@@ -235,12 +392,17 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
]} ]}
> >
<LabelSelector <LabelSelector
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'resources.table.labels' })} label={intl.formatMessage({ id: 'resources.table.labels' })}
labels={labels || {}} labels={labels || {}}
btnText={intl.formatMessage({ id: 'common.button.addLabel' })} btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
></LabelSelector> ></LabelSelector>
</Form.Item> </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> </Container>
</Form> </Form>
</CollapsibleContainer> </CollapsibleContainer>
@@ -7,6 +7,7 @@ import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Col, message, Row } from 'antd'; import { Col, message, Row } from 'antd';
import _ from 'lodash';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { deleteWorkerPool, updateWorkerPool } from '../apis'; import { deleteWorkerPool, updateWorkerPool } from '../apis';
import { ProviderType } from '../config'; import { ProviderType } from '../config';
@@ -78,7 +79,7 @@ const PoolRows: React.FC<PoolRowsProps> = ({
action: PageAction.EDIT, action: PageAction.EDIT,
title: intl.formatMessage( title: intl.formatMessage(
{ id: 'common.button.edit.item' }, { id: 'common.button.edit.item' },
{ name: record.instance_type } { name: record.name }
), ),
provider: provider, provider: provider,
currentData: record, currentData: record,
@@ -125,20 +126,14 @@ const PoolRows: React.FC<PoolRowsProps> = ({
{columns.map((col: Record<string, any>) => { {columns.map((col: Record<string, any>) => {
return ( return (
<Col <Col
key={col.dataIndex as string} key={col.dataIndex || col.key}
span={col.span} span={col.span}
style={{ style={{
paddingInline: 0, paddingInline: 0,
...(col.style || {}) ...(col.style || {})
}} }}
> >
{/* {col.render <CellContent {..._.omit(col, ['key'])}></CellContent>
? col.render(data[col.dataIndex as string], data)
: data[col.dataIndex as string]} */}
<CellContent
{...col}
dataIndex={col.dataIndex}
></CellContent>
</Col> </Col>
); );
})} })}
@@ -63,7 +63,9 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
<Container> <Container>
{Object.entries(groupList).map(([groupName, items]) => ( {Object.entries(groupList).map(([groupName, items]) => (
<div key={groupName}> <div key={groupName}>
{groupName !== 'default' && <Title>{groupName}</Title>} {groupName !== 'default' && (
<Title>{intl.formatMessage({ id: groupName })}</Title>
)}
<Wrapper> <Wrapper>
{items?.map((action) => ( {items?.map((action) => (
<Card <Card
@@ -8,7 +8,9 @@ const volumeOptions = fieldConfig.volumes;
const CloudOptions: React.FC<{ const CloudOptions: React.FC<{
ref?: any; ref?: any;
disabled?: boolean;
}> = forwardRef((props, ref) => { }> = forwardRef((props, ref) => {
const { disabled } = props;
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form); const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form);
@@ -25,6 +27,7 @@ const CloudOptions: React.FC<{
label={intl.formatMessage({ id: 'clusters.workerpool.volumes' })} label={intl.formatMessage({ id: 'clusters.workerpool.volumes' })}
dataList={volumes || []} dataList={volumes || []}
properties={volumeOptions.properties || {}} properties={volumeOptions.properties || {}}
disabled={disabled}
onChange={(value) => handleOnChange(volumeOptions.name, value)} onChange={(value) => handleOnChange(volumeOptions.name, value)}
/> />
</Form.Item> </Form.Item>
@@ -73,7 +73,7 @@ const WorkerPools = () => {
action: PageAction.EDIT, action: PageAction.EDIT,
title: intl.formatMessage( title: intl.formatMessage(
{ id: 'common.button.edit.item' }, { id: 'common.button.edit.item' },
{ name: record.instance_type } { name: record.name }
), ),
provider: searchParams.get('provider') as ProviderType, provider: searchParams.get('provider') as ProviderType,
currentData: record, currentData: record,
@@ -47,6 +47,27 @@ export const generateRegisterCommand = (params: {
--header 'Authorization: Bearer ${params.registrationToken}' | kubectl apply -f -`; --header 'Authorization: Bearer ${params.registrationToken}' | kubectl apply -f -`;
}; };
export const instanceTypeFieldMap = {
vram: 'VRAM',
vcpus: 'vCPUs',
ram: 'RAM',
bootDisk: 'Boot Disk',
scratchDisk: 'Scratch Disk',
minDiskSize: 'Min Disk Size',
size: 'Size'
};
export const vendorIconMap = {
amd: 'icon-amd',
nvidia: 'icon-nvidia1',
rockyLinux: 'icon-rocky-linux',
almaLinux: 'icon-alma-linux',
ubuntu: 'icon-ubuntu',
centOs: 'icon-centos',
debian: 'icon-debian',
fedora: 'icon-fedora'
};
export const providerList = [ export const providerList = [
{ {
label: 'clusters.provider.custom', label: 'clusters.provider.custom',
@@ -0,0 +1,68 @@
import icons from '@/components/icon-font/icons';
import React from 'react';
import { ProviderValueMap } from '.';
export default [
{
label: 'clusters.provider.custom',
locale: true,
value: ProviderValueMap.Custom,
key: ProviderValueMap.Custom,
icon: React.cloneElement(icons.Docker, {
style: { color: 'var(--ant-color-primary)' }
}),
group: 'clusters.create.provider.self'
},
{
label: 'Kubernetes',
locale: false,
value: ProviderValueMap.Kubernetes,
key: ProviderValueMap.Kubernetes,
icon: React.cloneElement(icons.KubernetesOutlined, {
style: { color: 'var(--ant-color-primary)' }
}),
group: 'clusters.create.provider.self'
},
{
label: 'DigitalOcean',
locale: false,
value: ProviderValueMap.DigitalOcean,
key: ProviderValueMap.DigitalOcean,
icon: React.cloneElement(icons.DigitalOcean, {
style: {
color: 'var(--ant-color-primary)'
}
}),
group: 'clusters.create.provider.cloud'
},
{
label: 'Huawei Cloud',
locale: false,
disabled: true,
value: ProviderValueMap.HuaweiCloud,
key: ProviderValueMap.HuaweiCloud,
icon: icons.HuaweiCloud,
description: 'Comming soon',
group: 'clusters.create.provider.cloud'
},
{
label: 'Ali Cloud',
locale: false,
disabled: true,
value: ProviderValueMap.AliCloud,
key: ProviderValueMap.AliCloud,
icon: icons.AliCloud,
description: 'Comming soon',
group: 'clusters.create.provider.cloud'
},
{
label: 'Tencent Cloud',
locale: false,
disabled: true,
value: ProviderValueMap.TencentCloud,
key: ProviderValueMap.TencentCloud,
icon: icons.TencentCloud,
description: 'Comming soon',
group: 'clusters.create.provider.cloud'
}
];
@@ -0,0 +1,17 @@
export const RegionIcons: Record<string, string> = {
nyc1: '🇺🇸',
nyc2: '🇺🇸',
nyc3: '🇺🇸',
tor1: '🇨🇦',
sfo1: '🇺🇸',
sfo2: '🇺🇸',
sfo3: '🇺🇸',
atl1: '🇺🇸',
sgp1: '🇸🇬',
blr1: '🇮🇳',
lon1: '🇬🇧',
ams2: '🇳🇱',
ams3: '🇳🇱',
fra1: '🇩🇪',
syd1: '🇦🇺'
};
+3 -1
View File
@@ -14,7 +14,7 @@ export type ClusterStatusType = 0 | 1 | 3;
export interface CredentialListItem { export interface CredentialListItem {
id: number; id: number;
name: string; name: string;
provider: string; provider: ProviderType;
access_key: string; access_key: string;
secret_key: string; secret_key: string;
description?: string; description?: string;
@@ -26,10 +26,12 @@ export interface NodePoolFormData {
name: string; name: string;
instance_type: string; instance_type: string;
os_image: string; os_image: string;
image_name: string;
replicas: number; replicas: number;
batch_size: number; batch_size: number;
labels: Record<string, string>; labels: Record<string, string>;
cloud_options: Record<string, any>; cloud_options: Record<string, any>;
instance_spec: Record<string, any>;
} }
export interface NodePoolListItem extends NodePoolFormData { export interface NodePoolListItem extends NodePoolFormData {
@@ -25,7 +25,7 @@ const setActionsItems = (row: ClusterListItem) => {
const useClusterColumns = ( const useClusterColumns = (
handleSelect: (val: string, record: ClusterListItem) => void handleSelect: (val: string, record: ClusterListItem) => void
): ColumnsType<ClusterListItem> => { ): ColumnsType<ClusterListItem & { dataIndex: string; span: number }> => {
const intl = useIntl(); const intl = useIntl();
return useMemo(() => { return useMemo(() => {
@@ -47,13 +47,35 @@ const useClusterColumns = (
{ {
title: intl.formatMessage({ id: 'clusters.table.provider' }), title: intl.formatMessage({ id: 'clusters.table.provider' }),
dataIndex: 'provider', dataIndex: 'provider',
span: 3, span: 4,
render: (value: string) => ( render: (value: string) => (
<AutoTooltip ghost minWidth={20}> <AutoTooltip ghost minWidth={20}>
{ProviderLabelMap[value]} {ProviderLabelMap[value]}
</AutoTooltip> </AutoTooltip>
) )
}, },
{
title: 'GPUs',
dataIndex: 'gpus',
span: 2,
render: (value: number) => <span>{value}</span>
},
{
title: intl.formatMessage({ id: 'clusters.table.deployments' }),
dataIndex: 'models',
span: 2,
render: (value: number) => <span>{value}</span>
},
{
title: 'Workers',
dataIndex: 'workers',
span: 3,
render: (value: number, record: ClusterListItem) => (
<span>
{record.ready_workers} / {record.workers}
</span>
)
},
{ {
title: intl.formatMessage({ id: 'common.table.status' }), title: intl.formatMessage({ id: 'common.table.status' }),
dataIndex: 'state', dataIndex: 'state',
@@ -67,28 +89,6 @@ const useClusterColumns = (
/> />
) )
}, },
{
title: 'Workers',
dataIndex: 'workers',
span: 3,
render: (value: number, record: ClusterListItem) => (
<span>
{record.ready_workers} / {record.workers}
</span>
)
},
{
title: 'GPUs',
dataIndex: 'gpus',
span: 3,
render: (value: number) => <span>{value}</span>
},
{
title: intl.formatMessage({ id: 'clusters.table.deployments' }),
dataIndex: 'models',
span: 2,
render: (value: number) => <span>{value}</span>
},
{ {
title: intl.formatMessage({ id: 'common.table.createTime' }), title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: 'created_at', dataIndex: 'created_at',
@@ -5,7 +5,9 @@ import { useIntl } from '@umijs/max';
import { ColumnsType } from 'antd/es/table'; 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 { useMemo } from 'react'; import { useMemo } from 'react';
import { RenderInstanceOption } from '../components/pool-form';
import { NodePoolListItem as ListItem } from '../config/types'; import { NodePoolListItem as ListItem } from '../config/types';
const actionItems = [ const actionItems = [
@@ -24,10 +26,11 @@ const actionItems = [
} }
} }
]; ];
const usePoolsColumns = ( const usePoolsColumns = (
handleSelect: (val: string, record: ListItem) => void, handleSelect: (val: string, record: ListItem) => void,
sortOrder?: SortOrder sortOrder?: SortOrder
): ColumnsType<ListItem> => { ): ColumnsType<ListItem & { dataIndex: string }> => {
const intl = useIntl(); const intl = useIntl();
return useMemo(() => { return useMemo(() => {
@@ -39,7 +42,7 @@ const usePoolsColumns = (
ellipsis: { ellipsis: {
showTitle: false showTitle: false
}, },
span: 4, span: 3,
style: { style: {
paddingInline: 'var(--ant-table-cell-padding-inline)' paddingInline: 'var(--ant-table-cell-padding-inline)'
}, },
@@ -56,29 +59,64 @@ const usePoolsColumns = (
ellipsis: { ellipsis: {
showTitle: false showTitle: false
}, },
span: 3, span: 4,
style: { style: {
paddingLeft: 12 paddingLeft: 62
}, },
render: (text: string) => ( render: (text: string, record: ListItem) => (
<AutoTooltip title={text} ghost minWidth={20}> <AutoTooltip
{text} title={
<RenderInstanceOption
styles={{
description: {
color: 'var(--color-white-quaternary)'
}
}}
data={{
vendor: record.instance_spec.vendor,
description: record.instance_spec.description,
specInfo: _.omit(record.instance_spec, [
'label',
'vendor',
'description'
])
}}
/>
}
showTitle
ghost
minWidth={20}
>
{record.instance_spec.description || record.instance_spec.label}
</AutoTooltip> </AutoTooltip>
) )
}, },
{ {
title: intl.formatMessage({ id: 'clusters.workerpool.osImage' }), title: intl.formatMessage({ id: 'clusters.workerpool.osImage' }),
dataIndex: 'os_image', dataIndex: 'image_name',
key: 'os_image', key: 'image_name',
span: 3, span: 4,
ellipsis: { ellipsis: {
showTitle: false showTitle: false
}, },
style: { style: {
paddingLeft: 16 paddingLeft: 56
}, },
render: (text: string) => ( render: (text: string) => (
<AutoTooltip title={text} ghost minWidth={20}> <AutoTooltip
title={
<span className="flex-column">
<span className="text-tertiary">
{intl.formatMessage({ id: 'clusters.workerpool.osImage' })}
:{' '}
</span>
{text}
</span>
}
showTitle
ghost
minWidth={20}
>
{text} {text}
</AutoTooltip> </AutoTooltip>
) )
@@ -86,11 +124,10 @@ const usePoolsColumns = (
{ {
title: 'Workers', title: 'Workers',
dataIndex: 'replicas', dataIndex: 'replicas',
span: 3, span: 6,
key: 'replicas', key: 'replicas',
style: { style: {
// textAlign: 'center' paddingLeft: 50
paddingLeft: 4
}, },
editable: { editable: {
valueType: 'number', valueType: 'number',
@@ -102,12 +139,13 @@ const usePoolsColumns = (
</span> </span>
) )
}, },
{
title: intl.formatMessage({ id: 'clusters.workerpool.batchSize' }), // {
dataIndex: 'batch_size', // title: intl.formatMessage({ id: 'clusters.workerpool.batchSize' }),
key: 'batch_size', // dataIndex: 'batch_size',
span: 4 // key: 'batch_size',
}, // span: 4
// },
// { // {
// title: intl.formatMessage({ id: 'resources.table.labels' }), // title: intl.formatMessage({ id: 'resources.table.labels' }),
// dataIndex: 'labels', // dataIndex: 'labels',
@@ -0,0 +1,183 @@
import {
allRegionInstanceTypeListAtom,
allRegionOSImageListAtom,
regionInstanceTypeListAtom,
regionListAtom,
regionOSImageListAtom
} from '@/atoms/clusters';
import { convertFileSizeByUnit } from '@/utils';
import { useAtom } from 'jotai';
import _ from 'lodash';
import { useState } from 'react';
import {
queryDigitalOceanInstanceTypes,
queryDigitalOceanOSImages,
queryDigitalOceanRegions
} from '../apis';
import { RegionIcons } from '../config/region-icons';
const parseCityDatacenter = (
input: string
): { label: string; datacenter: string } => {
const parts = input.trim().split(' ');
const number = parts.pop();
const city = parts.join(' ');
return {
label: city,
datacenter: `Datacenter ${number}`
};
};
type ParsedSpec = {
vram: string | number;
vcpus: number;
ram: string | number;
bootDisk: string | number;
scratchDisk: string | number;
};
export const parseSpec = (obj: any): ParsedSpec => {
const gpuInfo = obj.gpu_info;
const diskInfo = obj.disk_info;
return {
vram: convertFileSizeByUnit({
sizeInBytes: gpuInfo?.vram?.amount || 0,
defaultUnit: 'GiB'
}),
vcpus: obj.vcpus || 0,
ram: convertFileSizeByUnit({
sizeInBytes: obj.memory || 0,
defaultUnit: 'MiB'
}),
bootDisk: convertFileSizeByUnit({
sizeInBytes:
diskInfo?.find((d: any) => d.type === 'local')?.size.amount || 0,
defaultUnit: 'GiB'
}),
scratchDisk: convertFileSizeByUnit({
sizeInBytes:
diskInfo?.find((d: any) => d.type === 'scratch')?.size.amount || 0,
defaultUnit: 'GiB'
})
};
};
const formatSpec = (spec: ParsedSpec): string => {
const parts: 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`);
}
return parts.join(' / ');
};
export const useProviderRegions = () => {
const [regions, setRegions] = useAtom(regionListAtom);
const [, setInstanceTypes] = useAtom(regionInstanceTypeListAtom);
const [, setOSImageList] = useAtom(regionOSImageListAtom);
const [allOSImageList, setAllOSImageList] = useAtom(allRegionOSImageListAtom);
const [allInstanceTypes, setAllInstanceTypes] = useAtom(
allRegionInstanceTypeListAtom
);
const [loading, setLoading] = useState(false);
const getRegions = async (credential: number) => {
try {
setLoading(true);
const res = await queryDigitalOceanRegions({ id: credential });
const list = res?.regions
?.filter?.(
(sItem: any) =>
sItem.sizes.some((size: string) => size.includes('gpu')) &&
sItem.available
)
.map((item: any) => {
return {
...parseCityDatacenter(item.name),
value: item.slug,
icon: RegionIcons[item.slug],
sizes: item.sizes || []
};
});
setRegions(list);
} catch (error) {
setRegions([]);
} finally {
setLoading(false);
}
};
const getInstanceTypes = async (credential: number) => {
try {
const res = await queryDigitalOceanInstanceTypes({ id: credential });
const list = res?.sizes
?.filter((sItem: any) => sItem.gpu_info && sItem.available)
.map((item: any) => {
const specInfo = parseSpec(item);
return {
label: formatSpec(specInfo),
value: item.slug,
description: item.description,
specInfo: specInfo,
vendor: _.get(_.split(item.gpu_info?.model, '_'), 0),
available: item.available,
regions: item.regions || []
};
});
setAllInstanceTypes(list);
} catch (error) {
setAllInstanceTypes([]);
}
};
const getOSImages = async (credential: number) => {
try {
const res = await queryDigitalOceanOSImages({ id: credential });
const list = res.images
?.filter((sItem: any) => sItem.status === 'available')
.map((item: any) => {
return {
label: item.description,
value: item.slug,
name: item.name,
description: item.description,
vendor: _.camelCase(item.distribution),
specInfo: {
size: `${item.size_gigabytes} GiB`,
minDiskSize: `${item.min_disk_size} GiB`
},
regions: item.regions || []
};
});
setAllOSImageList(list);
} catch (error) {}
};
const updateInstanceTypes = (region: string) => {
const sizes = allInstanceTypes.filter((item) =>
item.regions.includes(region)
);
setInstanceTypes(sizes);
};
const updateOSImages = (region: string) => {
const list = allOSImageList.filter((item) => item.regions.includes(region));
setOSImageList(list);
};
return {
regions,
loading,
setLoading,
getRegions,
getInstanceTypes,
getOSImages,
updateOSImages,
updateInstanceTypes
};
};
@@ -1,5 +1,6 @@
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
import { forwardRef, useImperativeHandle, useRef } from 'react'; import { forwardRef, useImperativeHandle, useRef } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import ClusterForm from '../components/cluster-form'; import ClusterForm from '../components/cluster-form';
@@ -23,6 +24,7 @@ interface BasicFormProps {
} }
const BasicForm = forwardRef((props: BasicFormProps, ref) => { const BasicForm = forwardRef((props: BasicFormProps, ref) => {
const intl = useIntl();
const { provider, credentialList, action, currentData } = props; const { provider, credentialList, action, currentData } = props;
const formRef = useRef<any>(null); const formRef = useRef<any>(null);
@@ -40,7 +42,11 @@ const BasicForm = forwardRef((props: BasicFormProps, ref) => {
<div> <div>
<PageTools <PageTools
marginBottom={26} marginBottom={26}
left={<Title>Basic Configuration</Title>} left={
<Title>
{intl.formatMessage({ id: 'clusters.create.configBasic' })}
</Title>
}
marginTop={0} marginTop={0}
></PageTools> ></PageTools>
<ClusterForm <ClusterForm
@@ -1,10 +1,11 @@
import { useNavigate } from '@umijs/max'; import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ProviderType, ProviderValueMap } from '../config'; import { ProviderType, ProviderValueMap } from '../config';
import { moduleMap } from './module-registry'; import { moduleMap } from './module-registry';
export default function useStepList() { export default function useStepList() {
const intl = useIntl();
const navigate = useNavigate(); const navigate = useNavigate();
const handleBack = useMemoizedFn(() => { const handleBack = useMemoizedFn(() => {
@@ -14,7 +15,7 @@ export default function useStepList() {
return useMemo( return useMemo(
() => [ () => [
{ {
title: 'Select Cloud Provider', title: intl.formatMessage({ id: 'clusters.create.selectProvider' }),
content: '', content: '',
showButtons: (provider?: ProviderType) => { showButtons: (provider?: ProviderType) => {
return { return {
@@ -30,7 +31,7 @@ export default function useStepList() {
providers: [] providers: []
}, },
{ {
title: 'Configure Cluster Settings', title: intl.formatMessage({ id: 'clusters.create.configBasic' }),
content: '', content: '',
showButtons: (provider?: ProviderType) => { showButtons: (provider?: ProviderType) => {
return { return {
@@ -46,7 +47,7 @@ export default function useStepList() {
providers: [] providers: []
}, },
{ {
title: 'Add Worker Pools', title: intl.formatMessage({ id: 'clusters.button.addNodePool' }),
content: '', content: '',
showButtons: (provider?: ProviderType) => { showButtons: (provider?: ProviderType) => {
return { return {
@@ -63,7 +64,7 @@ export default function useStepList() {
beforeNext: handleBack beforeNext: handleBack
}, },
{ {
title: 'Add Worker', title: intl.formatMessage({ id: 'resources.button.create' }),
content: '', content: '',
showButtons: (provider?: ProviderType) => { showButtons: (provider?: ProviderType) => {
return { return {
@@ -79,7 +80,7 @@ export default function useStepList() {
providers: [ProviderValueMap.Custom] providers: [ProviderValueMap.Custom]
}, },
{ {
title: 'Register Cluster', title: intl.formatMessage({ id: 'clusters.button.register' }),
content: '', content: '',
showButtons: (provider?: ProviderType) => { showButtons: (provider?: ProviderType) => {
return { return {
@@ -95,6 +96,6 @@ export default function useStepList() {
providers: [ProviderValueMap.Kubernetes] providers: [ProviderValueMap.Kubernetes]
} }
], ],
[handleBack] [handleBack, intl]
); );
} }
@@ -1,6 +1,7 @@
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, FormInstance } from 'antd'; import { Button, FormInstance } from 'antd';
import { import {
forwardRef, forwardRef,
@@ -42,6 +43,7 @@ interface WorkerPoolsFormProps {
} }
const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
const intl = useIntl();
const { provider, action, currentData } = props; const { provider, action, currentData } = props;
const countRef = useRef(0); const countRef = useRef(0);
const formRefs = useRef<Record<number, FormInstance<any> | null>>({}); const formRefs = useRef<Record<number, FormInstance<any> | null>>({});
@@ -73,10 +75,13 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
name: `Pool-${newId + 1}` name: `Pool-${newId + 1}`
} as NodePoolFormData) } as NodePoolFormData)
); );
setActiveKey((prev) => new Set([newId]));
requestAnimationFrame(() => { requestAnimationFrame(() => {
window.scrollTo(0, document.body.scrollHeight); setActiveKey((prev) => new Set([newId]));
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
}); });
}; };
@@ -95,6 +100,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
const gatherFormValues = (results: PromiseSettledResult<any>[]) => { const gatherFormValues = (results: PromiseSettledResult<any>[]) => {
const resultList = results.map((result: PromiseSettledResult<any>) => { const resultList = results.map((result: PromiseSettledResult<any>) => {
console.log('gatherFormValues========', results);
if (result.status === 'fulfilled') { if (result.status === 'fulfilled') {
return result.value; return result.value;
} }
@@ -103,7 +109,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
} }
return {}; return {};
}); });
console.log('gatherFormValues========', resultList);
return resultList; return resultList;
}; };
@@ -177,7 +183,9 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
marginTop={0} marginTop={0}
left={ left={
<span className="flex-center gap-16"> <span className="flex-center gap-16">
<Title>Worker Pools</Title> <Title>
{intl.formatMessage({ id: 'clusters.workerpool.title' })}
</Title>
</span> </span>
} }
right={ right={
@@ -187,7 +195,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
color="default" color="default"
icon={<PlusOutlined />} icon={<PlusOutlined />}
> >
Add Pool {intl.formatMessage({ id: 'clusters.button.addNodePool' })}
</Button> </Button>
} }
></PageTools> ></PageTools>
+49 -12
View File
@@ -3,8 +3,44 @@ import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import '../style/gpu-card.less'; import '../style/gpu-card.less';
const CardWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: 100%;
border-bottom: 1px solid var(--ant-color-split);
padding-bottom: 10px;
`;
const Header = styled.div`
width: 100%;
margin-bottom: 10px;
`;
const Description = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
color: var(--ant-color-text-tertiary);
`;
export const CardContainer: React.FC<{
header?: React.ReactNode;
description?: React.ReactNode;
}> = ({ header, description }) => {
return (
<CardWrapper>
<Header>{header}</Header>
<Description>{description}</Description>
</CardWrapper>
);
};
const GPUCard: React.FC<{ const GPUCard: React.FC<{
data: any; data: any;
header?: React.ReactNode; header?: React.ReactNode;
@@ -12,17 +48,17 @@ const GPUCard: React.FC<{
}> = ({ data, header, info }) => { }> = ({ data, header, info }) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
<div className="gpu-card"> <CardContainer
<div className="header" style={{ width: '100%' }}> header={
{header ?? ( header || (
<AutoTooltip ghost> <AutoTooltip ghost>
<span className="font-700">[{data.index}] </span> <span className="font-700">[{data.index}] </span>
{data.label} {data.label}
</AutoTooltip> </AutoTooltip>
)} )
</div> }
<div className="info"> description={
{info ?? ( info || (
<> <>
<span> <span>
{intl.formatMessage({ id: 'resources.table.vram' })}( {intl.formatMessage({ id: 'resources.table.vram' })}(
@@ -37,7 +73,8 @@ const GPUCard: React.FC<{
</span> </span>
<span> <span>
<span> <span>
{intl.formatMessage({ id: 'resources.table.gpuutilization' })}:{' '} {intl.formatMessage({ id: 'resources.table.gpuutilization' })}
:{' '}
</span> </span>
{data?.memory?.used {data?.memory?.used
? _.round(data?.memory?.utilization_rate || 0, 2) ? _.round(data?.memory?.utilization_rate || 0, 2)
@@ -45,10 +82,10 @@ const GPUCard: React.FC<{
% %
</span> </span>
</> </>
)} )
</div> }
</div> ></CardContainer>
); );
}; };
export default React.memo(GPUCard); export default GPUCard;
@@ -147,7 +147,11 @@ const Performance: React.FC = () => {
multiple={ multiple={
form.getFieldValue('backend') !== backendOptionsMap.voxBox form.getFieldValue('backend') !== backendOptionsMap.voxBox
} }
popupClassName="cascader-popup-wrapper gpu-selector" classNames={{
popup: {
root: 'cascader-popup-wrapper gpu-selector'
}
}}
maxTagCount={1} maxTagCount={1}
label={intl.formatMessage({ id: 'models.form.gpuselector' })} label={intl.formatMessage({ id: 'models.form.gpuselector' })}
options={gpuOptions} options={gpuOptions}
@@ -45,7 +45,7 @@ const useModelsColumns = ({
) )
}, },
{ {
title: 'Cluster', title: intl.formatMessage({ id: 'clusters.title' }),
dataIndex: 'cluster', dataIndex: 'cluster',
key: 'cluster', key: 'cluster',
span: 3, span: 3,
+6 -2
View File
@@ -6,7 +6,9 @@ export const WorkerStatusMap = {
unreachable: 'unreachable', unreachable: 'unreachable',
provisioning: 'provisioning', provisioning: 'provisioning',
deleting: 'deleting', deleting: 'deleting',
error: 'error' error: 'error',
pending: 'pending',
provisioned: 'provisioned'
}; };
export const WorkerStatusMapValue = { export const WorkerStatusMapValue = {
@@ -24,7 +26,9 @@ export const status: any = {
[WorkerStatusMap.unreachable]: StatusMaps.error, [WorkerStatusMap.unreachable]: StatusMaps.error,
[WorkerStatusMap.provisioning]: StatusMaps.transitioning, [WorkerStatusMap.provisioning]: StatusMaps.transitioning,
[WorkerStatusMap.deleting]: StatusMaps.transitioning, [WorkerStatusMap.deleting]: StatusMaps.transitioning,
[WorkerStatusMap.error]: StatusMaps.error [WorkerStatusMap.error]: StatusMaps.error,
[WorkerStatusMap.pending]: StatusMaps.transitioning,
[WorkerStatusMap.provisioned]: StatusMaps.transitioning
}; };
export const addWorkerGuide: Record<string, any> = { export const addWorkerGuide: Record<string, any> = {
+1 -1
View File
@@ -67,7 +67,7 @@ export interface ListItem {
cluster_id: number; cluster_id: number;
state_message: string; state_message: string;
ssh_key_id: string; ssh_key_id: string;
progress: string; provision_progress: string;
status: { status: {
cpu: { cpu: {
total: number; total: number;
@@ -228,7 +228,7 @@ const useWorkerColumns = ({
render: (_, record) => ( render: (_, record) => (
<StatusTag <StatusTag
maxTooltipWidth={400} maxTooltipWidth={400}
suffix={record.progress} suffix={record.provision_progress}
statusValue={{ statusValue={{
status: status[record.state] as any, status: status[record.state] as any,
text: WorkerStatusMapValue[record.state], text: WorkerStatusMapValue[record.state],
+24
View File
@@ -44,6 +44,30 @@ export const convertFileSize = (
return `${_.round(size, precs[unitIndex])} ${units[unitIndex]}`; return `${_.round(size, precs[unitIndex])} ${units[unitIndex]}`;
}; };
export const convertFileSizeByUnit = (params: {
sizeInBytes: number;
defaultUnit?: 'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB';
allowEmpty?: boolean;
}): string | number => {
const { sizeInBytes, allowEmpty = false, defaultUnit = 'B' } = params;
if (!sizeInBytes) return allowEmpty ? '' : 0;
const fmt = 1024;
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
const precs = [0, 1, 1, 2, 2]; // precision for each unit
let size = sizeInBytes;
let unitIndex = units.indexOf(defaultUnit);
while (size >= fmt && unitIndex < units.length - 1) {
size /= fmt;
unitIndex++;
}
return `${_.round(size, precs[unitIndex])} ${units[unitIndex]}`;
};
export const platformCall = () => { export const platformCall = () => {
const platform = navigator.userAgent; const platform = navigator.userAgent;
const isMac = () => { const isMac = () => {