feat: cloud options
This commit is contained in:
@@ -1,19 +1,12 @@
|
||||
import LabelSelector from '@/components/label-selector';
|
||||
import ModalFooter from '@/components/modal-footer';
|
||||
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 useAppUtils from '@/hooks/use-app-utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
NodePoolFormData as FormData,
|
||||
NodePoolListItem as ListItem
|
||||
} from '../config/types';
|
||||
import PoolForm from './pool-form';
|
||||
|
||||
type AddModalProps = {
|
||||
title: string;
|
||||
@@ -24,7 +17,7 @@ type AddModalProps = {
|
||||
onOk: (values: FormData) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
const AddCluster: React.FC<AddModalProps> = ({
|
||||
const AddPool: React.FC<AddModalProps> = ({
|
||||
title,
|
||||
action,
|
||||
open,
|
||||
@@ -33,45 +26,21 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
currentData,
|
||||
onCancel
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const formRef = useRef<any>(null);
|
||||
|
||||
const handleSumit = () => {
|
||||
form.submit();
|
||||
const handleSubmit = () => {
|
||||
formRef.current?.submit?.();
|
||||
};
|
||||
|
||||
const handleOnOk = async (data: FormData) => {
|
||||
const { volumes, ...rest } = data;
|
||||
|
||||
await onOk({
|
||||
...rest,
|
||||
cloud_options: !_.isEmpty(volumes)
|
||||
? {
|
||||
volumes: [
|
||||
{
|
||||
...volumes
|
||||
}
|
||||
]
|
||||
}
|
||||
: {}
|
||||
});
|
||||
const handleOnFinish = async (data: FormData) => {
|
||||
onOk(data);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
formRef.current?.reset?.();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
form.setFieldsValue({
|
||||
...currentData,
|
||||
volumes: currentData.cloud_options?.volumes?.[0] || {}
|
||||
});
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
return (
|
||||
<ScrollerModal
|
||||
title={title}
|
||||
@@ -83,144 +52,18 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
keyboard={false}
|
||||
width={600}
|
||||
footer={
|
||||
<ModalFooter onOk={handleSumit} onCancel={onCancel}></ModalFooter>
|
||||
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleOnOk}
|
||||
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>
|
||||
<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>
|
||||
<PoolForm
|
||||
ref={formRef}
|
||||
action={action}
|
||||
provider={provider}
|
||||
currentData={currentData}
|
||||
onFinish={handleOnFinish}
|
||||
></PoolForm>
|
||||
</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
|
||||
});
|
||||
}
|
||||
setAddPoolStatus({
|
||||
...addPoolStatus,
|
||||
open: false
|
||||
});
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
handleSearch();
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
setAddPoolStatus({
|
||||
...addPoolStatus,
|
||||
open: false
|
||||
});
|
||||
};
|
||||
|
||||
const columns = usePoolsColumns(sortOrder, onSelect);
|
||||
|
||||
Reference in New Issue
Block a user