fix: display instance type spec
This commit is contained in:
@@ -257,6 +257,10 @@
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
|
||||
.text-quaternary {
|
||||
color: var(--ant-color-text-quaternary);
|
||||
}
|
||||
|
||||
.tips-desc-list {
|
||||
list-style-type: decimal;
|
||||
padding-left: 16px;
|
||||
|
||||
@@ -2,3 +2,43 @@ import { atom } from 'jotai';
|
||||
|
||||
// models expand keys: create, update , delete,
|
||||
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[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
@@ -173,8 +173,8 @@ export default function CollapsibleContainer({
|
||||
ref={contentRef}
|
||||
style={{
|
||||
maxHeight: height,
|
||||
overflow: 'hidden',
|
||||
transition: collapsible ? 'max-height 0.2s ease' : 'none'
|
||||
overflow: 'hidden'
|
||||
// transition: collapsible ? 'max-height 0.2s ease' : 'none'
|
||||
}}
|
||||
>
|
||||
<div style={{ paddingTop: 8 }}>{children}</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
|
||||
disabled={disabled}
|
||||
trigger={trigger}
|
||||
type="primary"
|
||||
dropdownRender={(menus: any) => {
|
||||
popupRender={(menus: any) => {
|
||||
return (
|
||||
<DropdownWrapper>
|
||||
{_.map(_.tail(items), (item: any) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ const FormWidget: React.FC<
|
||||
min,
|
||||
max,
|
||||
checked,
|
||||
readOnly: disabled,
|
||||
onChange
|
||||
}) => {
|
||||
const Component = ComponentsMap[widget];
|
||||
@@ -29,7 +30,15 @@ const FormWidget: React.FC<
|
||||
|
||||
return Component ? (
|
||||
<Component
|
||||
{...{ label, required, placeholder, description, min, max }}
|
||||
{...{
|
||||
label,
|
||||
required,
|
||||
placeholder,
|
||||
description,
|
||||
min,
|
||||
max
|
||||
}}
|
||||
disabled={disabled}
|
||||
options={options || optionList}
|
||||
value={value}
|
||||
checked={checked}
|
||||
|
||||
@@ -24,16 +24,23 @@ interface ListMapProps {
|
||||
label?: React.ReactNode;
|
||||
btnText?: string;
|
||||
properties: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
interface ListItemProps {
|
||||
schemaList: any[];
|
||||
data: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
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) => {
|
||||
if (target?.target?.type === 'checkbox') {
|
||||
const checked = target.target?.checked;
|
||||
@@ -50,6 +57,7 @@ const ListItem: React.FC<ListItemProps> = ({ schemaList, data, onChange }) => {
|
||||
<FormWidget
|
||||
widget={schema.type}
|
||||
{...schema}
|
||||
disabled={disabled}
|
||||
key={schema.name}
|
||||
value={data?.[schema.name]}
|
||||
checked={data?.[schema.name]}
|
||||
@@ -66,6 +74,7 @@ const ListMap: React.FC<ListMapProps> = ({
|
||||
btnText,
|
||||
properties = {},
|
||||
minItems = 0,
|
||||
disabled,
|
||||
onChange
|
||||
}) => {
|
||||
const [items, setItems] = React.useState(dataList || []);
|
||||
@@ -110,28 +119,36 @@ const ListMap: React.FC<ListMapProps> = ({
|
||||
}, [dataList]);
|
||||
|
||||
return (
|
||||
<Wrapper label={label} btnText={btnText} onAdd={handleOnAdd}>
|
||||
<Wrapper
|
||||
label={label}
|
||||
btnText={btnText}
|
||||
onAdd={handleOnAdd}
|
||||
disabled={disabled}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<RowWrapper key={index}>
|
||||
<WidgetBox>
|
||||
<ListItem
|
||||
schemaList={schemaList}
|
||||
data={item}
|
||||
disabled={disabled}
|
||||
onChange={(value) => handleItemChange(index, value)}
|
||||
/>
|
||||
</WidgetBox>
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
shape="circle"
|
||||
style={{
|
||||
width: 24,
|
||||
marginLeft: 10,
|
||||
flex: 'none'
|
||||
}}
|
||||
icon={<MinusOutlined />}
|
||||
onClick={() => handleDelete(index)}
|
||||
/>
|
||||
{!disabled && (
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
shape="circle"
|
||||
style={{
|
||||
width: 24,
|
||||
marginLeft: 10,
|
||||
flex: 'none'
|
||||
}}
|
||||
icon={<MinusOutlined />}
|
||||
onClick={() => handleDelete(index)}
|
||||
/>
|
||||
)}
|
||||
</RowWrapper>
|
||||
))}
|
||||
</Wrapper>
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface FormWidgetProps {
|
||||
title?: string;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
readOnly?: boolean;
|
||||
options?: { label: string; value: string | number }[];
|
||||
description?: string;
|
||||
enum?: (string | number)[];
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
|
||||
// import './iconfont/iconfont.js';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_l0y0uhurkz8.js'
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_5jrvj6qs39.js'
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface LabelSelectorProps {
|
||||
label?: string;
|
||||
btnText?: string;
|
||||
description?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
onChange?: (labels: Record<string, any>) => void;
|
||||
onBlur?: (e: any, type: string, index: number) => void;
|
||||
onDelete?: (index: number) => void;
|
||||
@@ -18,6 +19,7 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
|
||||
onChange,
|
||||
onBlur,
|
||||
onDelete,
|
||||
disabled,
|
||||
label,
|
||||
btnText,
|
||||
description
|
||||
@@ -81,6 +83,7 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
|
||||
|
||||
return (
|
||||
<Inner
|
||||
disabled={disabled}
|
||||
label={label}
|
||||
btnText={btnText}
|
||||
description={
|
||||
@@ -97,4 +100,4 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(LabelSelector);
|
||||
export default LabelSelector;
|
||||
|
||||
@@ -14,6 +14,7 @@ interface LabelSelectorProps {
|
||||
onBlur?: (e: any, type: string, index: number) => void;
|
||||
onDelete?: (index: number) => void;
|
||||
description?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Inner: React.FC<LabelSelectorProps> = ({
|
||||
@@ -24,6 +25,7 @@ const Inner: React.FC<LabelSelectorProps> = ({
|
||||
onPaste,
|
||||
onBlur,
|
||||
onDelete,
|
||||
disabled,
|
||||
label,
|
||||
btnText,
|
||||
description
|
||||
@@ -75,12 +77,14 @@ const Inner: React.FC<LabelSelectorProps> = ({
|
||||
label={label}
|
||||
description={description}
|
||||
onAdd={handleAddLabel}
|
||||
disabled={disabled}
|
||||
btnText={btnText}
|
||||
>
|
||||
<>
|
||||
{labelList?.map((item: any, index: number) => {
|
||||
return (
|
||||
<LabelItem
|
||||
disabled={disabled}
|
||||
key={index}
|
||||
label={item}
|
||||
seperator=":"
|
||||
|
||||
@@ -15,8 +15,9 @@ interface LabelItemProps {
|
||||
keyAddon?: React.ReactNode;
|
||||
valueAddon?: React.ReactNode;
|
||||
seperator?: string;
|
||||
onDelete?: () => void;
|
||||
labelList: { key: string; value: string }[];
|
||||
disabled?: boolean;
|
||||
onDelete?: () => void;
|
||||
onChange?: (params: { key: string; value: string }) => void;
|
||||
onPaste?: (e: any) => void;
|
||||
onBlur?: (e: any, type: string) => void;
|
||||
@@ -27,6 +28,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
seperator,
|
||||
keyAddon,
|
||||
valueAddon,
|
||||
disabled,
|
||||
onChange,
|
||||
onDelete,
|
||||
onPaste,
|
||||
@@ -82,6 +84,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
|
||||
>
|
||||
<SealInput.Input
|
||||
disabled={disabled}
|
||||
checkStatus="success"
|
||||
label={intl.formatMessage({ id: 'common.input.key' })}
|
||||
value={label.key}
|
||||
@@ -96,6 +99,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
<div className="label-value">
|
||||
{valueAddon ?? (
|
||||
<SealInput.Input
|
||||
disabled={disabled}
|
||||
checkStatus={label.value ? 'success' : ''}
|
||||
label={intl.formatMessage({ id: 'common.input.value' })}
|
||||
value={label.value}
|
||||
@@ -104,15 +108,17 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
></SealInput.Input>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
className="btn"
|
||||
type="default"
|
||||
shape="circle"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</Button>
|
||||
{!disabled && (
|
||||
<Button
|
||||
size="small"
|
||||
className="btn"
|
||||
type="default"
|
||||
shape="circle"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ interface WrapperProps {
|
||||
labelExtra?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
btnText?: string;
|
||||
disabled?: boolean;
|
||||
onAdd?: () => void;
|
||||
button?: React.ReactNode;
|
||||
}
|
||||
@@ -44,6 +45,7 @@ const Wrapper: React.FC<WrapperProps> = ({
|
||||
labelExtra,
|
||||
onAdd,
|
||||
btnText,
|
||||
disabled,
|
||||
button
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
@@ -59,17 +61,19 @@ const Wrapper: React.FC<WrapperProps> = ({
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
<ButtonWrapper>
|
||||
{button || (
|
||||
<Button variant="filled" color="default" block onClick={onAdd}>
|
||||
<PlusOutlined className="font-size-14" />
|
||||
{btnText ||
|
||||
intl.formatMessage({
|
||||
id: 'common.button.addSelector'
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonWrapper>
|
||||
{!disabled && (
|
||||
<ButtonWrapper>
|
||||
{button || (
|
||||
<Button variant="filled" color="default" block onClick={onAdd}>
|
||||
<PlusOutlined className="font-size-14" />
|
||||
{btnText ||
|
||||
intl.formatMessage({
|
||||
id: 'common.button.addSelector'
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonWrapper>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -185,7 +185,7 @@ const SealCascader: React.FC<
|
||||
onBlur={handleOnBlur}
|
||||
onChange={handleChange}
|
||||
notFoundContent={null}
|
||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||
onOpenChange={handleDropdownVisibleChange}
|
||||
></Cascader>
|
||||
</Wrapper>
|
||||
</SelectWrapper>
|
||||
|
||||
@@ -18,6 +18,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
|
||||
options,
|
||||
allowNull,
|
||||
isInFormItems = true,
|
||||
notFoundContent = null,
|
||||
...rest
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
@@ -102,7 +103,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onChange={handleChange}
|
||||
notFoundContent={null}
|
||||
notFoundContent={notFoundContent}
|
||||
>
|
||||
{children}
|
||||
</Select>
|
||||
|
||||
@@ -241,7 +241,7 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
|
||||
options={optionsList}
|
||||
maxTagCount={0}
|
||||
defaultActiveFirstOption={false}
|
||||
dropdownRender={dropdownRender}
|
||||
popupRender={dropdownRender}
|
||||
optionRender={optionRender}
|
||||
menuItemSelectedIcon={false}
|
||||
onChange={handleOnChange}
|
||||
@@ -250,7 +250,7 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
|
||||
onFocus={handleOnFocus}
|
||||
onSearch={handleOnSearch}
|
||||
filterOption={filterOption}
|
||||
onDropdownVisibleChange={handleOnOpenChange}
|
||||
onOpenChange={handleOnOpenChange}
|
||||
></Select>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -156,6 +156,7 @@ export const Label = styled.div.attrs<{
|
||||
// inner
|
||||
const Inner = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Extra = styled.div`
|
||||
|
||||
@@ -79,6 +79,7 @@ const InputWrapper = styled.div`
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
flex: 1;
|
||||
position: static;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -87,6 +88,7 @@ const InputWrapper = styled.div`
|
||||
padding: 0;
|
||||
height: ${INPUTHEIGHT}px !important;
|
||||
background-color: ${BGCOLOR};
|
||||
flex: 1;
|
||||
|
||||
&:hover .ant-input-number-handler-wrap,
|
||||
&-focused .ant-input-number-handler-wrap {
|
||||
|
||||
@@ -98,8 +98,8 @@ const SelectWrapper = styled.div`
|
||||
height: 54px;
|
||||
|
||||
.ant-select-selection-search {
|
||||
top: 20px !important;
|
||||
inset-inline-start: ${INPUT_INNER_PADDING}px;
|
||||
// top: 20px !important;
|
||||
// inset-inline-start: ${INPUT_INNER_PADDING}px;
|
||||
}
|
||||
&.ant-select-multiple.ant-cascader .ant-select-selection-search {
|
||||
top: 0 !important;
|
||||
|
||||
@@ -90,6 +90,7 @@ export default function useTableFetch<T>(
|
||||
|
||||
const updateHandler = (list: any) => {
|
||||
_.each(list, (data: any) => {
|
||||
console.log('list================:', list);
|
||||
updateChunkedList(data);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,5 +25,12 @@ export default {
|
||||
'clusters.workerpool.size': 'Size (GiB)',
|
||||
'clusters.workerpool.title': 'Worker Pools',
|
||||
'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'
|
||||
};
|
||||
|
||||
@@ -25,7 +25,14 @@ export default {
|
||||
'clusters.workerpool.size': 'Size (GiB)',
|
||||
'clusters.workerpool.title': 'Worker Pools',
|
||||
'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) ==========
|
||||
@@ -56,5 +63,12 @@ export default {
|
||||
// 25. 'clusters.workerpool.title': 'Worker Pools',
|
||||
// 26. 'clusters.workerpool.cloudOptions': 'Add Cloud Options',
|
||||
// 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 ==========
|
||||
|
||||
@@ -25,7 +25,14 @@ export default {
|
||||
'clusters.workerpool.size': 'Size (GiB)',
|
||||
'clusters.workerpool.title': 'Worker Pools',
|
||||
'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) ==========
|
||||
@@ -55,5 +62,13 @@ export default {
|
||||
// 24. 'clusters.workerpool.size': 'Size (GiB)',
|
||||
// 25. 'clusters.workerpool.title': 'Worker Pools',
|
||||
// 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 ==========
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
'clusters.edit.cluster': '编辑 {cluster}',
|
||||
'clusters.provider.custom': '自定义',
|
||||
'clusters.button.register': '注册集群',
|
||||
'clusters.button.addNodePool': '添加节点池',
|
||||
'clusters.button.addNodePool': '添加 Worker Pool',
|
||||
'clusters.button.add.credential': '添加 {provider} 凭证',
|
||||
'clusters.credential.title': '凭证',
|
||||
'clusters.credential.token': '访问令牌',
|
||||
@@ -23,7 +23,14 @@ export default {
|
||||
'clusters.workerpool.volumes': '存储卷',
|
||||
'clusters.workerpool.format': '文件系统格式',
|
||||
'clusters.workerpool.size': '容量(GiB)',
|
||||
'clusters.workerpool.title': '节点池',
|
||||
'clusters.workerpool.title': 'Worker Pools',
|
||||
'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 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 =====================
|
||||
|
||||
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) {
|
||||
return request<Global.PageResponse<CredentialListItem>>(
|
||||
`${CREDENTIALS_API}`,
|
||||
|
||||
@@ -9,7 +9,8 @@ import { createCluster, queryClusterToken, queryCredentialList } from './apis';
|
||||
import ClusterSteps from './components/cluster-steps';
|
||||
import FooterButtons from './components/footer-buttons';
|
||||
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 { moduleMap, moduleRegistry } from './step-forms/module-registry';
|
||||
import useStepList from './step-forms/use-step-list';
|
||||
@@ -63,7 +64,7 @@ const ClusterCreate = () => {
|
||||
(searchParams.get('action') as PageActionType) || PageAction.CREATE;
|
||||
const navigate = useNavigate();
|
||||
const [credentialList, setCredentialList] = useState<
|
||||
Global.BaseOption<number>[]
|
||||
Global.BaseOption<number, { provider: ProviderType }>[]
|
||||
>([]);
|
||||
const [currentStep, setCurrentStep] = useState<number>(startStep);
|
||||
const [registrationInfo, setRegistrationInfo] = useState<{
|
||||
@@ -87,6 +88,11 @@ const ClusterCreate = () => {
|
||||
[moduleMap.WorkerPoolForm]: useRef<any>(null)
|
||||
};
|
||||
|
||||
const availableCredentials = useMemo(
|
||||
() => credentialList.filter((item) => item.provider === extraData.provider),
|
||||
[credentialList, extraData.provider]
|
||||
);
|
||||
|
||||
const steps = useMemo(() => {
|
||||
if (!extraData.provider) {
|
||||
return stepList.filter((step) => step.defaultShow);
|
||||
@@ -182,6 +188,10 @@ const ClusterCreate = () => {
|
||||
};
|
||||
|
||||
const handleSelectProvider = (value: ProviderType) => {
|
||||
if (value === extraData.provider) {
|
||||
onNext();
|
||||
return;
|
||||
}
|
||||
setExtraData({
|
||||
provider: value
|
||||
} as ClusterFormData);
|
||||
@@ -194,7 +204,8 @@ const ClusterCreate = () => {
|
||||
const res = await queryCredentialList({ page: 1, perPage: 100 });
|
||||
const list = res.items?.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
value: item.id,
|
||||
provider: item.provider
|
||||
}));
|
||||
setCredentialList(list);
|
||||
};
|
||||
@@ -217,7 +228,7 @@ const ClusterCreate = () => {
|
||||
ref={formRefs[key]}
|
||||
action={action}
|
||||
provider={extraData.provider}
|
||||
credentialList={credentialList}
|
||||
credentialList={availableCredentials}
|
||||
currentData={formValues[key]}
|
||||
/>
|
||||
) : null;
|
||||
@@ -279,27 +290,7 @@ const ClusterCreate = () => {
|
||||
/>
|
||||
]}
|
||||
header={{
|
||||
title: (
|
||||
<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>
|
||||
),
|
||||
title: false,
|
||||
style: {
|
||||
paddingInline: 'var(--layout-content-header-inlinepadding)'
|
||||
},
|
||||
@@ -308,7 +299,9 @@ const ClusterCreate = () => {
|
||||
pageHeaderRender={() => (
|
||||
<HeaderContainer>
|
||||
<Nav>
|
||||
<span className="level-1">Cluster</span>
|
||||
<span className="level-1">
|
||||
{intl.formatMessage({ id: 'clusters.title' })}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
marginInline: 20,
|
||||
@@ -317,7 +310,9 @@ const ClusterCreate = () => {
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<span className="level-2">create</span>
|
||||
<span className="level-2">
|
||||
{intl.formatMessage({ id: 'common.button.create' })}
|
||||
</span>
|
||||
</Nav>
|
||||
<ClusterSteps
|
||||
steps={steps}
|
||||
|
||||
@@ -307,7 +307,10 @@ const Credentials: React.FC = () => {
|
||||
);
|
||||
|
||||
const setDisableExpand = (row: ClusterListItem) => {
|
||||
return row.provider !== ProviderValueMap.DigitalOcean;
|
||||
return (
|
||||
row.provider !== ProviderValueMap.DigitalOcean ||
|
||||
!allWorkerPoolList.some((item) => item.cluster_id === row.id)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -47,6 +47,27 @@ export const generateRegisterCommand = (params: {
|
||||
--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 = [
|
||||
{
|
||||
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: '🇦🇺'
|
||||
};
|
||||
@@ -14,7 +14,7 @@ export type ClusterStatusType = 0 | 1 | 3;
|
||||
export interface CredentialListItem {
|
||||
id: number;
|
||||
name: string;
|
||||
provider: string;
|
||||
provider: ProviderType;
|
||||
access_key: string;
|
||||
secret_key: string;
|
||||
description?: string;
|
||||
@@ -26,10 +26,12 @@ export interface NodePoolFormData {
|
||||
name: string;
|
||||
instance_type: string;
|
||||
os_image: string;
|
||||
image_name: string;
|
||||
replicas: number;
|
||||
batch_size: number;
|
||||
labels: Record<string, string>;
|
||||
cloud_options: Record<string, any>;
|
||||
instance_spec: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface NodePoolListItem extends NodePoolFormData {
|
||||
|
||||
@@ -25,7 +25,7 @@ const setActionsItems = (row: ClusterListItem) => {
|
||||
|
||||
const useClusterColumns = (
|
||||
handleSelect: (val: string, record: ClusterListItem) => void
|
||||
): ColumnsType<ClusterListItem> => {
|
||||
): ColumnsType<ClusterListItem & { dataIndex: string; span: number }> => {
|
||||
const intl = useIntl();
|
||||
|
||||
return useMemo(() => {
|
||||
@@ -47,13 +47,35 @@ const useClusterColumns = (
|
||||
{
|
||||
title: intl.formatMessage({ id: 'clusters.table.provider' }),
|
||||
dataIndex: 'provider',
|
||||
span: 3,
|
||||
span: 4,
|
||||
render: (value: string) => (
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
{ProviderLabelMap[value]}
|
||||
</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' }),
|
||||
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' }),
|
||||
dataIndex: 'created_at',
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useIntl } from '@umijs/max';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import type { SortOrder } from 'antd/es/table/interface';
|
||||
import dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { RenderInstanceOption } from '../components/pool-form';
|
||||
import { NodePoolListItem as ListItem } from '../config/types';
|
||||
|
||||
const actionItems = [
|
||||
@@ -24,10 +26,11 @@ const actionItems = [
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const usePoolsColumns = (
|
||||
handleSelect: (val: string, record: ListItem) => void,
|
||||
sortOrder?: SortOrder
|
||||
): ColumnsType<ListItem> => {
|
||||
): ColumnsType<ListItem & { dataIndex: string }> => {
|
||||
const intl = useIntl();
|
||||
|
||||
return useMemo(() => {
|
||||
@@ -39,7 +42,7 @@ const usePoolsColumns = (
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
span: 4,
|
||||
span: 3,
|
||||
style: {
|
||||
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
||||
},
|
||||
@@ -56,29 +59,64 @@ const usePoolsColumns = (
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
span: 3,
|
||||
span: 4,
|
||||
style: {
|
||||
paddingLeft: 12
|
||||
paddingLeft: 62
|
||||
},
|
||||
render: (text: string) => (
|
||||
<AutoTooltip title={text} ghost minWidth={20}>
|
||||
{text}
|
||||
render: (text: string, record: ListItem) => (
|
||||
<AutoTooltip
|
||||
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>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'clusters.workerpool.osImage' }),
|
||||
dataIndex: 'os_image',
|
||||
key: 'os_image',
|
||||
span: 3,
|
||||
dataIndex: 'image_name',
|
||||
key: 'image_name',
|
||||
span: 4,
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
style: {
|
||||
paddingLeft: 16
|
||||
paddingLeft: 56
|
||||
},
|
||||
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}
|
||||
</AutoTooltip>
|
||||
)
|
||||
@@ -86,11 +124,10 @@ const usePoolsColumns = (
|
||||
{
|
||||
title: 'Workers',
|
||||
dataIndex: 'replicas',
|
||||
span: 3,
|
||||
span: 6,
|
||||
key: 'replicas',
|
||||
style: {
|
||||
// textAlign: 'center'
|
||||
paddingLeft: 4
|
||||
paddingLeft: 50
|
||||
},
|
||||
editable: {
|
||||
valueType: 'number',
|
||||
@@ -102,12 +139,13 @@ const usePoolsColumns = (
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'clusters.workerpool.batchSize' }),
|
||||
dataIndex: 'batch_size',
|
||||
key: 'batch_size',
|
||||
span: 4
|
||||
},
|
||||
|
||||
// {
|
||||
// title: intl.formatMessage({ id: 'clusters.workerpool.batchSize' }),
|
||||
// dataIndex: 'batch_size',
|
||||
// key: 'batch_size',
|
||||
// span: 4
|
||||
// },
|
||||
// {
|
||||
// title: intl.formatMessage({ id: 'resources.table.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 { PageActionType } from '@/config/types';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ClusterForm from '../components/cluster-form';
|
||||
@@ -23,6 +24,7 @@ interface BasicFormProps {
|
||||
}
|
||||
|
||||
const BasicForm = forwardRef((props: BasicFormProps, ref) => {
|
||||
const intl = useIntl();
|
||||
const { provider, credentialList, action, currentData } = props;
|
||||
const formRef = useRef<any>(null);
|
||||
|
||||
@@ -40,7 +42,11 @@ const BasicForm = forwardRef((props: BasicFormProps, ref) => {
|
||||
<div>
|
||||
<PageTools
|
||||
marginBottom={26}
|
||||
left={<Title>Basic Configuration</Title>}
|
||||
left={
|
||||
<Title>
|
||||
{intl.formatMessage({ id: 'clusters.create.configBasic' })}
|
||||
</Title>
|
||||
}
|
||||
marginTop={0}
|
||||
></PageTools>
|
||||
<ClusterForm
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useNavigate } from '@umijs/max';
|
||||
import { useIntl, useNavigate } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useMemo } from 'react';
|
||||
import { ProviderType, ProviderValueMap } from '../config';
|
||||
import { moduleMap } from './module-registry';
|
||||
|
||||
export default function useStepList() {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleBack = useMemoizedFn(() => {
|
||||
@@ -14,7 +15,7 @@ export default function useStepList() {
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Select Cloud Provider',
|
||||
title: intl.formatMessage({ id: 'clusters.create.selectProvider' }),
|
||||
content: '',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
@@ -30,7 +31,7 @@ export default function useStepList() {
|
||||
providers: []
|
||||
},
|
||||
{
|
||||
title: 'Configure Cluster Settings',
|
||||
title: intl.formatMessage({ id: 'clusters.create.configBasic' }),
|
||||
content: '',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
@@ -46,7 +47,7 @@ export default function useStepList() {
|
||||
providers: []
|
||||
},
|
||||
{
|
||||
title: 'Add Worker Pools',
|
||||
title: intl.formatMessage({ id: 'clusters.button.addNodePool' }),
|
||||
content: '',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
@@ -63,7 +64,7 @@ export default function useStepList() {
|
||||
beforeNext: handleBack
|
||||
},
|
||||
{
|
||||
title: 'Add Worker',
|
||||
title: intl.formatMessage({ id: 'resources.button.create' }),
|
||||
content: '',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
@@ -79,7 +80,7 @@ export default function useStepList() {
|
||||
providers: [ProviderValueMap.Custom]
|
||||
},
|
||||
{
|
||||
title: 'Register Cluster',
|
||||
title: intl.formatMessage({ id: 'clusters.button.register' }),
|
||||
content: '',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
@@ -95,6 +96,6 @@ export default function useStepList() {
|
||||
providers: [ProviderValueMap.Kubernetes]
|
||||
}
|
||||
],
|
||||
[handleBack]
|
||||
[handleBack, intl]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PageTools from '@/components/page-tools';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, FormInstance } from 'antd';
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -42,6 +43,7 @@ interface WorkerPoolsFormProps {
|
||||
}
|
||||
|
||||
const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
const intl = useIntl();
|
||||
const { provider, action, currentData } = props;
|
||||
const countRef = useRef(0);
|
||||
const formRefs = useRef<Record<number, FormInstance<any> | null>>({});
|
||||
@@ -73,10 +75,13 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
name: `Pool-${newId + 1}`
|
||||
} as NodePoolFormData)
|
||||
);
|
||||
setActiveKey((prev) => new Set([newId]));
|
||||
|
||||
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 resultList = results.map((result: PromiseSettledResult<any>) => {
|
||||
console.log('gatherFormValues========', results);
|
||||
if (result.status === 'fulfilled') {
|
||||
return result.value;
|
||||
}
|
||||
@@ -103,7 +109,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
}
|
||||
return {};
|
||||
});
|
||||
console.log('gatherFormValues========', resultList);
|
||||
|
||||
return resultList;
|
||||
};
|
||||
|
||||
@@ -177,7 +183,9 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
marginTop={0}
|
||||
left={
|
||||
<span className="flex-center gap-16">
|
||||
<Title>Worker Pools</Title>
|
||||
<Title>
|
||||
{intl.formatMessage({ id: 'clusters.workerpool.title' })}
|
||||
</Title>
|
||||
</span>
|
||||
}
|
||||
right={
|
||||
@@ -187,7 +195,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
color="default"
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
Add Pool
|
||||
{intl.formatMessage({ id: 'clusters.button.addNodePool' })}
|
||||
</Button>
|
||||
}
|
||||
></PageTools>
|
||||
|
||||
@@ -3,8 +3,44 @@ import { convertFileSize } from '@/utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
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<{
|
||||
data: any;
|
||||
header?: React.ReactNode;
|
||||
@@ -12,17 +48,17 @@ const GPUCard: React.FC<{
|
||||
}> = ({ data, header, info }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="gpu-card">
|
||||
<div className="header" style={{ width: '100%' }}>
|
||||
{header ?? (
|
||||
<CardContainer
|
||||
header={
|
||||
header || (
|
||||
<AutoTooltip ghost>
|
||||
<span className="font-700">[{data.index}] </span>
|
||||
{data.label}
|
||||
</AutoTooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="info">
|
||||
{info ?? (
|
||||
)
|
||||
}
|
||||
description={
|
||||
info || (
|
||||
<>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'resources.table.vram' })}(
|
||||
@@ -37,7 +73,8 @@ const GPUCard: React.FC<{
|
||||
</span>
|
||||
<span>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'resources.table.gpuutilization' })}:{' '}
|
||||
{intl.formatMessage({ id: 'resources.table.gpuutilization' })}
|
||||
:{' '}
|
||||
</span>
|
||||
{data?.memory?.used
|
||||
? _.round(data?.memory?.utilization_rate || 0, 2)
|
||||
@@ -45,10 +82,10 @@ const GPUCard: React.FC<{
|
||||
%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
></CardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(GPUCard);
|
||||
export default GPUCard;
|
||||
|
||||
@@ -147,7 +147,11 @@ const Performance: React.FC = () => {
|
||||
multiple={
|
||||
form.getFieldValue('backend') !== backendOptionsMap.voxBox
|
||||
}
|
||||
popupClassName="cascader-popup-wrapper gpu-selector"
|
||||
classNames={{
|
||||
popup: {
|
||||
root: 'cascader-popup-wrapper gpu-selector'
|
||||
}
|
||||
}}
|
||||
maxTagCount={1}
|
||||
label={intl.formatMessage({ id: 'models.form.gpuselector' })}
|
||||
options={gpuOptions}
|
||||
|
||||
@@ -45,7 +45,7 @@ const useModelsColumns = ({
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Cluster',
|
||||
title: intl.formatMessage({ id: 'clusters.title' }),
|
||||
dataIndex: 'cluster',
|
||||
key: 'cluster',
|
||||
span: 3,
|
||||
|
||||
@@ -6,7 +6,9 @@ export const WorkerStatusMap = {
|
||||
unreachable: 'unreachable',
|
||||
provisioning: 'provisioning',
|
||||
deleting: 'deleting',
|
||||
error: 'error'
|
||||
error: 'error',
|
||||
pending: 'pending',
|
||||
provisioned: 'provisioned'
|
||||
};
|
||||
|
||||
export const WorkerStatusMapValue = {
|
||||
@@ -24,7 +26,9 @@ export const status: any = {
|
||||
[WorkerStatusMap.unreachable]: StatusMaps.error,
|
||||
[WorkerStatusMap.provisioning]: 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> = {
|
||||
|
||||
@@ -67,7 +67,7 @@ export interface ListItem {
|
||||
cluster_id: number;
|
||||
state_message: string;
|
||||
ssh_key_id: string;
|
||||
progress: string;
|
||||
provision_progress: string;
|
||||
status: {
|
||||
cpu: {
|
||||
total: number;
|
||||
|
||||
@@ -228,7 +228,7 @@ const useWorkerColumns = ({
|
||||
render: (_, record) => (
|
||||
<StatusTag
|
||||
maxTooltipWidth={400}
|
||||
suffix={record.progress}
|
||||
suffix={record.provision_progress}
|
||||
statusValue={{
|
||||
status: status[record.state] as any,
|
||||
text: WorkerStatusMapValue[record.state],
|
||||
|
||||
@@ -44,6 +44,30 @@ export const convertFileSize = (
|
||||
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 = () => {
|
||||
const platform = navigator.userAgent;
|
||||
const isMac = () => {
|
||||
|
||||
Reference in New Issue
Block a user