feat: cloud options
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
import ComponentsMap from '@/components/seal-form/config/components';
|
||||||
|
import { SealFormItemProps } from '@/components/seal-form/types';
|
||||||
|
import { Form } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface FieldItemProps extends SealFormItemProps {
|
||||||
|
widget: keyof typeof ComponentsMap;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FieldItem: React.FC<FieldItemProps> = (props) => {
|
||||||
|
const { name, widget, required = [], ...rest } = props;
|
||||||
|
|
||||||
|
const Component = ComponentsMap[widget];
|
||||||
|
|
||||||
|
return <Form.Item name={name}></Form.Item>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FieldItem;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import ComponentsMap from '@/components/seal-form/config/components';
|
||||||
|
import { FormWidgetProps } from '../config/types';
|
||||||
|
|
||||||
|
const FormWidget: React.FC<
|
||||||
|
FormWidgetProps & {
|
||||||
|
onChange?: (data: any) => void;
|
||||||
|
}
|
||||||
|
> = ({
|
||||||
|
widget,
|
||||||
|
title: label,
|
||||||
|
required,
|
||||||
|
placeholder,
|
||||||
|
options,
|
||||||
|
description,
|
||||||
|
enum: enumValues,
|
||||||
|
style,
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
checked,
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
const Component = ComponentsMap[widget];
|
||||||
|
|
||||||
|
const optionList = enumValues?.map((item: string | number) => ({
|
||||||
|
label: item,
|
||||||
|
value: item
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Component ? (
|
||||||
|
<Component
|
||||||
|
{...{ label, required, placeholder, description, min, max }}
|
||||||
|
options={options || optionList}
|
||||||
|
value={value}
|
||||||
|
checked={checked}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormWidget;
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import Wrapper from '@/components/label-selector/wrapper';
|
||||||
|
import { MinusOutlined } from '@ant-design/icons';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React, { useEffect, useMemo } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import FormWidget from './form-widget';
|
||||||
|
|
||||||
|
interface ListMapProps {
|
||||||
|
dataList: any[];
|
||||||
|
label?: React.ReactNode;
|
||||||
|
btnText?: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
onChange?: (data: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListItemProps {
|
||||||
|
schemaList: any[];
|
||||||
|
data: Record<string, any>;
|
||||||
|
onChange?: (data: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RowWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WidgetBox = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ListItem: React.FC<ListItemProps> = ({ schemaList, data, onChange }) => {
|
||||||
|
const handleValueChange = (name: string, target: any) => {
|
||||||
|
if (target?.target?.type === 'checkbox') {
|
||||||
|
const checked = target.target?.checked;
|
||||||
|
onChange?.({ [name]: checked });
|
||||||
|
} else {
|
||||||
|
const value = target?.target ? target.target.value : target;
|
||||||
|
onChange?.({ [name]: value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{schemaList.map((schema: any) => (
|
||||||
|
<FormWidget
|
||||||
|
widget={schema.type}
|
||||||
|
{...schema}
|
||||||
|
key={schema.name}
|
||||||
|
value={data?.[schema.name]}
|
||||||
|
checked={data?.[schema.name]}
|
||||||
|
onChange={(target) => handleValueChange(schema.name, target)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListMap: React.FC<ListMapProps> = ({
|
||||||
|
dataList = [],
|
||||||
|
label,
|
||||||
|
btnText,
|
||||||
|
properties = {},
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
const [items, setItems] = React.useState(dataList || []);
|
||||||
|
|
||||||
|
const schemaList = useMemo(() => {
|
||||||
|
const list = Object.entries(properties).map(([key, value]) => ({
|
||||||
|
...value,
|
||||||
|
name: key
|
||||||
|
}));
|
||||||
|
return list;
|
||||||
|
}, [properties]);
|
||||||
|
|
||||||
|
const handleOnAdd = () => {
|
||||||
|
const keys = Object.keys(properties);
|
||||||
|
setItems([
|
||||||
|
...items,
|
||||||
|
{ ...keys.reduce((acc, key) => ({ ...acc, [key]: '' }), {}) }
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (index: number) => {
|
||||||
|
const newItems = items.filter((_, i) => i !== index);
|
||||||
|
setItems(newItems);
|
||||||
|
onChange?.(newItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemChange = (index: number, data: { [key: string]: any }) => {
|
||||||
|
const newItems = [...items];
|
||||||
|
newItems[index] = { ...newItems[index], ...data };
|
||||||
|
setItems(newItems);
|
||||||
|
onChange?.(newItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dataList.length) {
|
||||||
|
handleOnAdd();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper label={label} btnText={btnText} onAdd={handleOnAdd}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<RowWrapper key={index}>
|
||||||
|
<WidgetBox>
|
||||||
|
<ListItem
|
||||||
|
schemaList={schemaList}
|
||||||
|
data={item}
|
||||||
|
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)}
|
||||||
|
/>
|
||||||
|
</RowWrapper>
|
||||||
|
))}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListMap;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// refer to json schema
|
||||||
|
export interface FieldSchema {
|
||||||
|
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
||||||
|
title?: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
properties?: Record<string, FieldSchema>;
|
||||||
|
default?: any;
|
||||||
|
enum?: string[];
|
||||||
|
minItems?: number;
|
||||||
|
maxItems?: number;
|
||||||
|
items?: FieldSchema[];
|
||||||
|
widget?: string;
|
||||||
|
min?: number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormWidgetProps {
|
||||||
|
widget: 'Input' | 'Select' | 'Checkbox' | 'InputNumber';
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
required?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
options?: { label: string; value: string | number }[];
|
||||||
|
description?: string;
|
||||||
|
enum?: (string | number)[];
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
value?: any;
|
||||||
|
checked?: boolean;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { FieldSchema } from '../config/types';
|
||||||
|
|
||||||
|
interface ParsedField {
|
||||||
|
name: (string | number)[];
|
||||||
|
schema: FieldSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSchema = (
|
||||||
|
schema: Record<string, FieldSchema>,
|
||||||
|
parentName: (string | number)[] = []
|
||||||
|
): ParsedField[] => {
|
||||||
|
const fields: ParsedField[] = [];
|
||||||
|
|
||||||
|
Object.entries(schema).forEach(([key, fieldSchema]) => {
|
||||||
|
const currentName = [...parentName, key];
|
||||||
|
if (fieldSchema.type === 'object' && fieldSchema.properties) {
|
||||||
|
fields.push(...parseSchema(fieldSchema.properties, currentName));
|
||||||
|
} else if (fieldSchema.type === 'array' && fieldSchema.items) {
|
||||||
|
fields.push({ name: currentName, schema: fieldSchema });
|
||||||
|
} else {
|
||||||
|
fields.push({ name: currentName, schema: fieldSchema });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useParsedFields = (schema: Record<string, FieldSchema>) => {
|
||||||
|
return useMemo(() => parseSchema(schema), [schema]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useParsedFields;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Form } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { FieldSchema } from './config/types';
|
||||||
|
|
||||||
|
interface DynamicFormProps {
|
||||||
|
schema: FieldSchema;
|
||||||
|
onSubmit: (values: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit }) => {
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
|
const handleFinish = (values: any) => {
|
||||||
|
onSubmit(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form form={form} onFinish={handleFinish}>
|
||||||
|
{/* Render form fields based on schema */}
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicForm;
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button } from 'antd';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useRef } from 'react';
|
import React from 'react';
|
||||||
import LabelItem from './label-item';
|
import LabelItem from './label-item';
|
||||||
import Wrapper from './wrapper';
|
import Wrapper from './wrapper';
|
||||||
interface LabelSelectorProps {
|
interface LabelSelectorProps {
|
||||||
@@ -31,8 +29,6 @@ const Inner: React.FC<LabelSelectorProps> = ({
|
|||||||
description
|
description
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const boxRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const updateLabels = (list: { key: string; value: string }[]) => {
|
const updateLabels = (list: { key: string; value: string }[]) => {
|
||||||
const newLabels = _.reduce(
|
const newLabels = _.reduce(
|
||||||
@@ -75,7 +71,12 @@ const Inner: React.FC<LabelSelectorProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper label={label} description={description}>
|
<Wrapper
|
||||||
|
label={label}
|
||||||
|
description={description}
|
||||||
|
onAdd={handleAddLabel}
|
||||||
|
btnText={btnText}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
{labelList?.map((item: any, index: number) => {
|
{labelList?.map((item: any, index: number) => {
|
||||||
return (
|
return (
|
||||||
@@ -91,26 +92,9 @@ const Inner: React.FC<LabelSelectorProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="flex justify-center">
|
|
||||||
<Button
|
|
||||||
ref={buttonRef}
|
|
||||||
type="text"
|
|
||||||
block
|
|
||||||
style={{
|
|
||||||
marginTop: 16,
|
|
||||||
backgroundColor: 'var(--ant-color-fill-secondary)'
|
|
||||||
}}
|
|
||||||
onClick={handleAddLabel}
|
|
||||||
>
|
|
||||||
<PlusOutlined className="font-size-14" />{' '}
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: btnText || 'common.button.addSelector'
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(Inner);
|
export default Inner;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
padding: 14px;
|
|
||||||
padding-top: 34px;
|
|
||||||
border: 1px solid var(--ant-color-border);
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
:global {
|
|
||||||
.label {
|
|
||||||
position: absolute;
|
|
||||||
left: 16px;
|
|
||||||
line-height: 1;
|
|
||||||
top: 12px;
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,54 @@
|
|||||||
import LabelInfo from '@/components/seal-form/components/label-info';
|
import LabelInfo from '@/components/seal-form/components/label-info';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styles from './styles/wrapper.less';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const Wrapper: React.FC<{
|
interface WrapperProps {
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
labelExtra?: React.ReactNode;
|
labelExtra?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}> = ({ children, label, description, labelExtra, ...rest }) => {
|
btnText?: string;
|
||||||
|
onAdd?: () => void;
|
||||||
|
button?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
position: relative;
|
||||||
|
padding: 14px;
|
||||||
|
padding-top: 34px;
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
top: 12px;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ButtonWrapper = styled.div`
|
||||||
|
margin-top: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Wrapper: React.FC<WrapperProps> = ({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
labelExtra,
|
||||||
|
onAdd,
|
||||||
|
btnText,
|
||||||
|
button
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<div className={styles['wrapper']}>
|
<Container>
|
||||||
{label && (
|
{label && (
|
||||||
<span className="label">
|
<span className="label">
|
||||||
<LabelInfo
|
<LabelInfo
|
||||||
@@ -19,10 +58,19 @@ const Wrapper: React.FC<{
|
|||||||
></LabelInfo>
|
></LabelInfo>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{React.isValidElement(children)
|
{children}
|
||||||
? React.cloneElement(children, { ...rest })
|
<ButtonWrapper>
|
||||||
: children}
|
{button || (
|
||||||
</div>
|
<Button variant="filled" color="default" block onClick={onAdd}>
|
||||||
|
<PlusOutlined className="font-size-14" />
|
||||||
|
{btnText ||
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'common.button.addSelector'
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ButtonWrapper>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button } from 'antd';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Wrapper from '../label-selector/wrapper';
|
import Wrapper from '../label-selector/wrapper';
|
||||||
@@ -34,7 +32,6 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
|
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
|
||||||
const countRef = React.useRef(0);
|
const countRef = React.useRef(0);
|
||||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const updateCountRef = () => {
|
const updateCountRef = () => {
|
||||||
countRef.current = countRef.current + 1;
|
countRef.current = countRef.current + 1;
|
||||||
@@ -65,9 +62,6 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
|||||||
uid: countRef.current
|
uid: countRef.current
|
||||||
});
|
});
|
||||||
setList(values);
|
setList(values);
|
||||||
// setTimeout(() => {
|
|
||||||
// buttonRef.current?.scrollIntoView?.({ behavior: 'smooth' });
|
|
||||||
// }, 100);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -85,7 +79,13 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
|||||||
}, [dataList]);
|
}, [dataList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper label={label} description={description} labelExtra={labelExtra}>
|
<Wrapper
|
||||||
|
label={label}
|
||||||
|
description={description}
|
||||||
|
labelExtra={labelExtra}
|
||||||
|
onAdd={handleOnAdd}
|
||||||
|
btnText={btnText}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
{_.map(list, (item: any, index: number) => {
|
{_.map(list, (item: any, index: number) => {
|
||||||
return (
|
return (
|
||||||
@@ -100,26 +100,9 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="flex justify-center">
|
|
||||||
<Button
|
|
||||||
ref={buttonRef}
|
|
||||||
variant="filled"
|
|
||||||
color="default"
|
|
||||||
block
|
|
||||||
style={{
|
|
||||||
marginTop: 16
|
|
||||||
}}
|
|
||||||
onClick={handleOnAdd}
|
|
||||||
>
|
|
||||||
<PlusOutlined className="font-size-14" />{' '}
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: btnText
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(ListInput);
|
export default ListInput;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
|
|||||||
handleDeleteByBatch,
|
handleDeleteByBatch,
|
||||||
handleClickPrimary,
|
handleClickPrimary,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
actionItems,
|
actionItems = [],
|
||||||
selectOptions,
|
selectOptions,
|
||||||
showSelect,
|
showSelect,
|
||||||
buttonText,
|
buttonText,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
|||||||
trim = true,
|
trim = true,
|
||||||
loading,
|
loading,
|
||||||
labelExtra,
|
labelExtra,
|
||||||
|
style,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const [isFocus, setIsFocus] = useState(false);
|
const [isFocus, setIsFocus] = useState(false);
|
||||||
@@ -73,7 +74,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputWrapper>
|
<InputWrapper style={style}>
|
||||||
<Wrapper
|
<Wrapper
|
||||||
status={checkStatus || status}
|
status={checkStatus || status}
|
||||||
label={label}
|
label={label}
|
||||||
|
|||||||
@@ -29,5 +29,6 @@ export default {
|
|||||||
'menu.404': '404',
|
'menu.404': '404',
|
||||||
'menu.clusterManagement': 'Cluster Management',
|
'menu.clusterManagement': 'Cluster Management',
|
||||||
'menu.clusterManagement.clusters': 'Clusters',
|
'menu.clusterManagement.clusters': 'Clusters',
|
||||||
'menu.clusterManagement.credentials': 'Credentials'
|
'menu.clusterManagement.credentials': 'Credentials',
|
||||||
|
'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export default {
|
|||||||
'menu.clusterManagement': 'Cluster Management',
|
'menu.clusterManagement': 'Cluster Management',
|
||||||
'menu.clusterManagement.clusters': 'Clusters',
|
'menu.clusterManagement.clusters': 'Clusters',
|
||||||
'menu.clusterManagement.credentials': 'Credentials',
|
'menu.clusterManagement.credentials': 'Credentials',
|
||||||
'menu.models.userModels': 'My Models'
|
'menu.models.userModels': 'My Models',
|
||||||
|
'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
@@ -44,4 +45,5 @@ export default {
|
|||||||
// 9. 'menu.clusterManagement.clusters': 'Clusters',
|
// 9. 'menu.clusterManagement.clusters': 'Clusters',
|
||||||
// 10. 'menu.clusterManagement.credentials': 'Credentials',
|
// 10. 'menu.clusterManagement.credentials': 'Credentials',
|
||||||
// 11. 'menu.models.userModels': 'My Models'
|
// 11. 'menu.models.userModels': 'My Models'
|
||||||
|
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export default {
|
|||||||
'menu.clusterManagement': 'Cluster Management',
|
'menu.clusterManagement': 'Cluster Management',
|
||||||
'menu.clusterManagement.clusters': 'Clusters',
|
'menu.clusterManagement.clusters': 'Clusters',
|
||||||
'menu.clusterManagement.credentials': 'Credentials',
|
'menu.clusterManagement.credentials': 'Credentials',
|
||||||
'menu.models.userModels': 'My Models'
|
'menu.models.userModels': 'My Models',
|
||||||
|
'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
@@ -43,5 +44,6 @@ export default {
|
|||||||
// 8. 'menu.clusterManagement': 'Cluster Management',
|
// 8. 'menu.clusterManagement': 'Cluster Management',
|
||||||
// 9. 'menu.clusterManagement.clusters': 'Clusters',
|
// 9. 'menu.clusterManagement.clusters': 'Clusters',
|
||||||
// 10. 'menu.clusterManagement.credentials': 'Credentials',
|
// 10. 'menu.clusterManagement.credentials': 'Credentials',
|
||||||
// 11. 'menu.models.userModels': 'My Models'
|
// 11. 'menu.models.userModels': 'My Models',
|
||||||
|
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -29,5 +29,6 @@ export default {
|
|||||||
'menu.accessControl.users': '用户',
|
'menu.accessControl.users': '用户',
|
||||||
'menu.clusterManagement': '集群管理',
|
'menu.clusterManagement': '集群管理',
|
||||||
'menu.clusterManagement.clusters': '集群',
|
'menu.clusterManagement.clusters': '集群',
|
||||||
'menu.clusterManagement.credentials': '凭证'
|
'menu.clusterManagement.credentials': '凭证',
|
||||||
|
'menu.clusterManagement.clusterDetail': '集群详情'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -251,14 +251,14 @@ const Credentials: React.FC = () => {
|
|||||||
data: formdata,
|
data: formdata,
|
||||||
clusterId: addPoolStatus.clusterId
|
clusterId: addPoolStatus.clusterId
|
||||||
});
|
});
|
||||||
|
setAddPoolStatus({
|
||||||
|
...addPoolStatus,
|
||||||
|
open: false
|
||||||
|
});
|
||||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// error
|
// error
|
||||||
}
|
}
|
||||||
setAddPoolStatus({
|
|
||||||
...addPoolStatus,
|
|
||||||
open: false
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
import LabelSelector from '@/components/label-selector';
|
|
||||||
import ModalFooter from '@/components/modal-footer';
|
import ModalFooter from '@/components/modal-footer';
|
||||||
import ScrollerModal from '@/components/scroller-modal';
|
import ScrollerModal from '@/components/scroller-modal';
|
||||||
import SealInputNumber from '@/components/seal-form/input-number';
|
|
||||||
import SealInput from '@/components/seal-form/seal-input';
|
|
||||||
import { PageAction } from '@/config';
|
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useAppUtils from '@/hooks/use-app-utils';
|
import React, { useRef } from 'react';
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Form } from 'antd';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import {
|
import {
|
||||||
NodePoolFormData as FormData,
|
NodePoolFormData as FormData,
|
||||||
NodePoolListItem as ListItem
|
NodePoolListItem as ListItem
|
||||||
} from '../config/types';
|
} from '../config/types';
|
||||||
|
import PoolForm from './pool-form';
|
||||||
|
|
||||||
type AddModalProps = {
|
type AddModalProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -24,7 +17,7 @@ type AddModalProps = {
|
|||||||
onOk: (values: FormData) => void;
|
onOk: (values: FormData) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
const AddCluster: React.FC<AddModalProps> = ({
|
const AddPool: React.FC<AddModalProps> = ({
|
||||||
title,
|
title,
|
||||||
action,
|
action,
|
||||||
open,
|
open,
|
||||||
@@ -33,45 +26,21 @@ const AddCluster: React.FC<AddModalProps> = ({
|
|||||||
currentData,
|
currentData,
|
||||||
onCancel
|
onCancel
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const formRef = useRef<any>(null);
|
||||||
const intl = useIntl();
|
|
||||||
const { getRuleMessage } = useAppUtils();
|
|
||||||
|
|
||||||
const handleSumit = () => {
|
const handleSubmit = () => {
|
||||||
form.submit();
|
formRef.current?.submit?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnOk = async (data: FormData) => {
|
const handleOnFinish = async (data: FormData) => {
|
||||||
const { volumes, ...rest } = data;
|
onOk(data);
|
||||||
|
|
||||||
await onOk({
|
|
||||||
...rest,
|
|
||||||
cloud_options: !_.isEmpty(volumes)
|
|
||||||
? {
|
|
||||||
volumes: [
|
|
||||||
{
|
|
||||||
...volumes
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
form.resetFields();
|
formRef.current?.reset?.();
|
||||||
onCancel();
|
onCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentData) {
|
|
||||||
form.setFieldsValue({
|
|
||||||
...currentData,
|
|
||||||
volumes: currentData.cloud_options?.volumes?.[0] || {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [currentData]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollerModal
|
<ScrollerModal
|
||||||
title={title}
|
title={title}
|
||||||
@@ -83,144 +52,18 @@ const AddCluster: React.FC<AddModalProps> = ({
|
|||||||
keyboard={false}
|
keyboard={false}
|
||||||
width={600}
|
width={600}
|
||||||
footer={
|
footer={
|
||||||
<ModalFooter onOk={handleSumit} onCancel={onCancel}></ModalFooter>
|
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Form
|
<PoolForm
|
||||||
form={form}
|
ref={formRef}
|
||||||
onFinish={handleOnOk}
|
action={action}
|
||||||
preserve={false}
|
provider={provider}
|
||||||
initialValues={{
|
currentData={currentData}
|
||||||
replicas: 1,
|
onFinish={handleOnFinish}
|
||||||
batch_size: 1
|
></PoolForm>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="instance_type"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: getRuleMessage(
|
|
||||||
'input',
|
|
||||||
'clusters.workerpool.instanceType'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInput.Input
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: 'clusters.workerpool.instanceType'
|
|
||||||
})}
|
|
||||||
required
|
|
||||||
></SealInput.Input>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="replicas"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: getRuleMessage('input', 'clusters.workerpool.replicas')
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInputNumber
|
|
||||||
label={intl.formatMessage({ id: 'clusters.workerpool.replicas' })}
|
|
||||||
required
|
|
||||||
></SealInputNumber>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="batch_size"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: getRuleMessage('input', 'clusters.workerpool.batchSize')
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInputNumber
|
|
||||||
label={intl.formatMessage({ id: 'clusters.workerpool.batchSize' })}
|
|
||||||
required
|
|
||||||
></SealInputNumber>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="os_image"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: getRuleMessage('input', 'clusters.workerpool.osImage')
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInput.Input
|
|
||||||
disabled={action === PageAction.EDIT}
|
|
||||||
label={intl.formatMessage({ id: 'clusters.workerpool.osImage' })}
|
|
||||||
required
|
|
||||||
></SealInput.Input>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="labels"
|
|
||||||
rules={[
|
|
||||||
({ getFieldValue }) => ({
|
|
||||||
validator(rule, value) {
|
|
||||||
if (_.keys(value).length > 0) {
|
|
||||||
if (_.some(_.keys(value), (k: string) => !value[k])) {
|
|
||||||
return Promise.reject(
|
|
||||||
intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: 'common.validate.value'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'labels'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<LabelSelector
|
|
||||||
label={intl.formatMessage({ id: 'resources.table.labels' })}
|
|
||||||
labels={form.getFieldValue('labels') || {}}
|
|
||||||
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
|
|
||||||
></LabelSelector>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="volumes"
|
|
||||||
rules={[
|
|
||||||
({ getFieldValue }) => ({
|
|
||||||
validator(rule, value) {
|
|
||||||
if (_.keys(value).length > 0) {
|
|
||||||
if (_.some(_.keys(value), (k: string) => !value[k])) {
|
|
||||||
return Promise.reject(
|
|
||||||
intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: 'common.validate.value'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Volumes'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<LabelSelector
|
|
||||||
label="Volumes"
|
|
||||||
labels={form.getFieldValue('volumes') || {}}
|
|
||||||
btnText="Add Option"
|
|
||||||
></LabelSelector>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</ScrollerModal>
|
</ScrollerModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddCluster;
|
export default AddPool;
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import DropDownActions from '@/components/drop-down-actions';
|
||||||
|
import ListMap from '@/components/dynamic-form/components/list-map';
|
||||||
|
import { FieldSchema } from '@/components/dynamic-form/config/types';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { Button, Form } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { CloudOptionItems } from '../config';
|
||||||
|
import { fieldConfig } from '../config/cloud-options-config';
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
// background-color: var(--ant-color-fill-secondary);
|
||||||
|
border-radius: var(--ant-border-radius);
|
||||||
|
margin-bottom: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ButtonWrapper = styled.span`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 8px;
|
||||||
|
&:hover {
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CloudOptions: React.FC<{
|
||||||
|
ref?: any;
|
||||||
|
}> = forwardRef((props, ref) => {
|
||||||
|
// form instance
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
const [selectedOptions, setSelectedOptions] = React.useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [fieldList, setFieldList] = React.useState<FieldSchema[]>([]);
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
return CloudOptionItems.map((item) => ({
|
||||||
|
...item,
|
||||||
|
disabled: selectedOptions.has(item.key)
|
||||||
|
}));
|
||||||
|
}, [selectedOptions]);
|
||||||
|
|
||||||
|
const handleAddOption = useMemoizedFn((item: { key: string }) => {
|
||||||
|
const field = fieldConfig[item.key];
|
||||||
|
setFieldList((prev) => [...prev, { ...field, name: item.key }]);
|
||||||
|
setSelectedOptions((prev) => new Set(prev).add(item.key));
|
||||||
|
});
|
||||||
|
|
||||||
|
const menu = useMemo(() => {
|
||||||
|
return {
|
||||||
|
items: items,
|
||||||
|
onClick: handleAddOption
|
||||||
|
};
|
||||||
|
}, [items, handleAddOption]);
|
||||||
|
|
||||||
|
const handleOnChange = (name: string, value: any) => {
|
||||||
|
form.setFieldValue(['cloud_options', name], value);
|
||||||
|
if (_.isEmpty(value)) {
|
||||||
|
setFieldList((prev) => prev.filter((field) => field.name !== name));
|
||||||
|
setSelectedOptions((prev) => {
|
||||||
|
const newSelected = new Set(prev);
|
||||||
|
newSelected.delete(name);
|
||||||
|
return newSelected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// init field list by form data
|
||||||
|
const initFieldList = () => {
|
||||||
|
const cloudOptions = form.getFieldValue('cloud_options');
|
||||||
|
if (cloudOptions) {
|
||||||
|
const fields = Object.entries(cloudOptions)
|
||||||
|
.filter(([, value]) => {
|
||||||
|
return !_.isEmpty(value);
|
||||||
|
})
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const field = fieldConfig[key];
|
||||||
|
return { ...field, name: key };
|
||||||
|
});
|
||||||
|
setFieldList(fields);
|
||||||
|
setSelectedOptions(new Set(Object.keys(cloudOptions)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
initFieldList
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>
|
||||||
|
<DropDownActions menu={menu}>
|
||||||
|
<Button variant="filled" color="default">
|
||||||
|
<PlusOutlined />
|
||||||
|
<span>Add Cloud Options</span>
|
||||||
|
</Button>
|
||||||
|
</DropDownActions>
|
||||||
|
</Title>
|
||||||
|
{fieldList.length > 0 &&
|
||||||
|
fieldList.map((field) => (
|
||||||
|
<Form.Item
|
||||||
|
key={field.name}
|
||||||
|
name={['cloud_options', field.name as string]}
|
||||||
|
>
|
||||||
|
<ListMap
|
||||||
|
btnText={'Add Item'}
|
||||||
|
label={field.title}
|
||||||
|
dataList={form.getFieldValue(['cloud_options', field.name]) || []}
|
||||||
|
properties={field.properties || {}}
|
||||||
|
onChange={(value) => handleOnChange(field.name, value)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CloudOptions;
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import LabelSelector from '@/components/label-selector';
|
||||||
|
import SealInputNumber from '@/components/seal-form/input-number';
|
||||||
|
import SealInput from '@/components/seal-form/seal-input';
|
||||||
|
import { PageAction } from '@/config';
|
||||||
|
import { PageActionType } from '@/config/types';
|
||||||
|
import useAppUtils from '@/hooks/use-app-utils';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Form } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
NodePoolFormData as FormData,
|
||||||
|
NodePoolListItem as ListItem
|
||||||
|
} from '../config/types';
|
||||||
|
import CloudOptions from './cloud-options';
|
||||||
|
|
||||||
|
type AddModalProps = {
|
||||||
|
ref: any;
|
||||||
|
action: PageActionType;
|
||||||
|
provider: string; // 'kubernetes' | 'custom' | 'digitalocean';
|
||||||
|
currentData?: ListItem | null;
|
||||||
|
onFinish: (values: FormData) => void;
|
||||||
|
};
|
||||||
|
const PoolForm: React.FC<AddModalProps> = forwardRef(
|
||||||
|
({ action, onFinish, currentData }, ref) => {
|
||||||
|
const cloudOptionsRef = useRef<any>(null);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const intl = useIntl();
|
||||||
|
const { getRuleMessage } = useAppUtils();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentData) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
...currentData,
|
||||||
|
volumes: currentData.cloud_options?.volumes?.[0] || {}
|
||||||
|
});
|
||||||
|
cloudOptionsRef.current?.initFieldList();
|
||||||
|
}
|
||||||
|
}, [currentData]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
reset: () => {
|
||||||
|
form.resetFields();
|
||||||
|
},
|
||||||
|
submit: () => {
|
||||||
|
form.submit();
|
||||||
|
},
|
||||||
|
validateFields: async () => {
|
||||||
|
return await form.validateFields();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={onFinish}
|
||||||
|
preserve={false}
|
||||||
|
initialValues={{
|
||||||
|
replicas: 1,
|
||||||
|
batch_size: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="instance_type"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: getRuleMessage(
|
||||||
|
'input',
|
||||||
|
'clusters.workerpool.instanceType'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInput.Input
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.workerpool.instanceType'
|
||||||
|
})}
|
||||||
|
required
|
||||||
|
></SealInput.Input>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="replicas"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: getRuleMessage('input', 'clusters.workerpool.replicas')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInputNumber
|
||||||
|
label={intl.formatMessage({ id: 'clusters.workerpool.replicas' })}
|
||||||
|
required
|
||||||
|
></SealInputNumber>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="batch_size"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: getRuleMessage('input', 'clusters.workerpool.batchSize')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInputNumber
|
||||||
|
label={intl.formatMessage({ id: 'clusters.workerpool.batchSize' })}
|
||||||
|
required
|
||||||
|
></SealInputNumber>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="os_image"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: getRuleMessage('input', 'clusters.workerpool.osImage')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInput.Input
|
||||||
|
disabled={action === PageAction.EDIT}
|
||||||
|
label={intl.formatMessage({ id: 'clusters.workerpool.osImage' })}
|
||||||
|
required
|
||||||
|
></SealInput.Input>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="labels"
|
||||||
|
rules={[
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(rule, value) {
|
||||||
|
if (_.keys(value).length > 0) {
|
||||||
|
if (_.some(_.keys(value), (k: string) => !value[k])) {
|
||||||
|
return Promise.reject(
|
||||||
|
intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'common.validate.value'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'labels'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<LabelSelector
|
||||||
|
label={intl.formatMessage({ id: 'resources.table.labels' })}
|
||||||
|
labels={form.getFieldValue('labels') || {}}
|
||||||
|
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
|
||||||
|
></LabelSelector>
|
||||||
|
</Form.Item>
|
||||||
|
<CloudOptions ref={cloudOptionsRef}></CloudOptions>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PoolForm;
|
||||||
@@ -116,15 +116,15 @@ const WorkerPools = () => {
|
|||||||
id: addPoolStatus.currentData!.id
|
id: addPoolStatus.currentData!.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setAddPoolStatus({
|
||||||
|
...addPoolStatus,
|
||||||
|
open: false
|
||||||
|
});
|
||||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||||
handleSearch();
|
handleSearch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// error
|
// error
|
||||||
}
|
}
|
||||||
setAddPoolStatus({
|
|
||||||
...addPoolStatus,
|
|
||||||
open: false
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = usePoolsColumns(sortOrder, onSelect);
|
const columns = usePoolsColumns(sortOrder, onSelect);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { FieldSchema } from '@/components/dynamic-form/config/types';
|
||||||
|
|
||||||
|
export const fields = {
|
||||||
|
volumes: {
|
||||||
|
type: 'array',
|
||||||
|
minItems: 1,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
size_gb: { type: 'number', unit: 'GB' },
|
||||||
|
format: { type: 'string' }
|
||||||
|
},
|
||||||
|
required: ['name', 'size_gb', 'format']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fieldConfig: Record<string, FieldSchema> = {
|
||||||
|
volumes: {
|
||||||
|
type: 'array',
|
||||||
|
title: 'Volumes',
|
||||||
|
name: 'volumes',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name',
|
||||||
|
widget: 'Input'
|
||||||
|
},
|
||||||
|
size_gb: {
|
||||||
|
name: 'size_gb',
|
||||||
|
type: 'number',
|
||||||
|
title: 'Size (GB)',
|
||||||
|
widget: 'InputNumber',
|
||||||
|
min: 0,
|
||||||
|
style: { width: 120 }
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
name: 'format',
|
||||||
|
type: 'string',
|
||||||
|
title: 'Format',
|
||||||
|
widget: 'Select',
|
||||||
|
enum: ['ext4', 'xfs', 'btrfs'],
|
||||||
|
style: { width: 150 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -155,3 +155,10 @@ export const regionList: {
|
|||||||
{ label: 'Frankfurt', datacenter: 'Datacenter 1', value: 'fra1', icon: '🇩🇪' },
|
{ label: 'Frankfurt', datacenter: 'Datacenter 1', value: 'fra1', icon: '🇩🇪' },
|
||||||
{ label: 'Sydney', datacenter: 'Datacenter 1', value: 'syd1', icon: '🇦🇺' }
|
{ label: 'Sydney', datacenter: 'Datacenter 1', value: 'syd1', icon: '🇦🇺' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const CloudOptionItems = [
|
||||||
|
{
|
||||||
|
label: 'Volumes',
|
||||||
|
key: 'volumes'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
|||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
btnText="common.button.addParams"
|
btnText={intl.formatMessage({ id: 'common.button.addParams' })}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'models.form.backend_parameters'
|
id: 'models.form.backend_parameters'
|
||||||
})}
|
})}
|
||||||
@@ -395,7 +395,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
|||||||
id: 'models.form.env'
|
id: 'models.form.env'
|
||||||
})}
|
})}
|
||||||
labels={EnviromentVars}
|
labels={EnviromentVars}
|
||||||
btnText="common.button.vars"
|
btnText={intl.formatMessage({ id: 'common.button.vars' })}
|
||||||
onBlur={handleEnvSelectorOnBlur}
|
onBlur={handleEnvSelectorOnBlur}
|
||||||
onDelete={handleDeleteEnvSelector}
|
onDelete={handleDeleteEnvSelector}
|
||||||
onChange={handleEnviromentVarsChange}
|
onChange={handleEnviromentVarsChange}
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
|
import { downloadFile } from '@/utils/download-stream';
|
||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
|
import { message } from 'antd';
|
||||||
import { GPUDeviceItem, ListItem, ModelFile } from '../config/types';
|
import { GPUDeviceItem, ListItem, ModelFile } from '../config/types';
|
||||||
|
|
||||||
export const WORKERS_API = '/workers';
|
export const WORKERS_API = '/workers';
|
||||||
export const GPU_DEVICES_API = '/gpu-devices';
|
export const GPU_DEVICES_API = '/gpu-devices';
|
||||||
export const MODEL_FILES_API = '/model-files';
|
export const MODEL_FILES_API = '/model-files';
|
||||||
|
|
||||||
export async function downloadWorkerPrivateKey(id: string | number) {
|
// download stream data and save as a csv file
|
||||||
return request(`${WORKERS_API}/${id}/privatekey`, {
|
export async function downloadWorkerPrivateKey({
|
||||||
method: 'GET'
|
id,
|
||||||
});
|
name
|
||||||
|
}: {
|
||||||
|
id: string | number;
|
||||||
|
name?: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/v1${WORKERS_API}/${id}/privatekey`);
|
||||||
|
if (res.ok) {
|
||||||
|
const blob = await res.blob();
|
||||||
|
downloadFile(blob, `${name}-privatekey.csv`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Download failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function queryWorkersList<T extends Record<string, any>>(
|
export async function queryWorkersList<T extends Record<string, any>>(
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const UpdateLabels: React.FC<ViewModalProps> = (props) => {
|
|||||||
id: 'resources.table.labels'
|
id: 'resources.table.labels'
|
||||||
})}
|
})}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
btnText="common.button.addLabel"
|
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
|
||||||
onChange={handleLabelsChange}
|
onChange={handleLabelsChange}
|
||||||
></LabelSelector>
|
></LabelSelector>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -146,7 +146,10 @@ const Workers: React.FC = () => {
|
|||||||
handleViewDetail(record);
|
handleViewDetail(record);
|
||||||
}
|
}
|
||||||
if (val === 'download_ssh_key') {
|
if (val === 'download_ssh_key') {
|
||||||
downloadWorkerPrivateKey(record.id);
|
downloadWorkerPrivateKey({
|
||||||
|
id: record.id,
|
||||||
|
name: record.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const status: any = {
|
|||||||
[WorkerStatusMap.not_ready]: StatusMaps.error,
|
[WorkerStatusMap.not_ready]: StatusMaps.error,
|
||||||
[WorkerStatusMap.unreachable]: StatusMaps.error,
|
[WorkerStatusMap.unreachable]: StatusMaps.error,
|
||||||
[WorkerStatusMap.provisioning]: StatusMaps.transitioning,
|
[WorkerStatusMap.provisioning]: StatusMaps.transitioning,
|
||||||
[WorkerStatusMap.deleting]: StatusMaps.warning,
|
[WorkerStatusMap.deleting]: StatusMaps.transitioning,
|
||||||
[WorkerStatusMap.error]: StatusMaps.error
|
[WorkerStatusMap.error]: StatusMaps.error
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
|
export const downloadFile = (blob: Blob, filename: string) => {
|
||||||
|
saveAs(blob, filename);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user