You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/06/02 13:07:27 UTC
[incubator-apisix-dashboard] branch next updated: feat: add form
validation (#234)
This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch next
in repository https://gitbox.apache.org/repos/asf/incubator-apisix-dashboard.git
The following commit(s) were added to refs/heads/next by this push:
new 0e5bbe4 feat: add form validation (#234)
0e5bbe4 is described below
commit 0e5bbe4d0578748a7437f198a04325e55be7f248
Author: litesun <31...@users.noreply.github.com>
AuthorDate: Tue Jun 2 21:07:18 2020 +0800
feat: add form validation (#234)
* feat: limit upload file
* feat: intercept default upload api request
* feat: limit upload file type
* fix: show file when parse SSL file fail
* feat: add search feature
* feat: format code
* fix: remove list item not work
* feat: remove relatedRouting
* feat: add routes step1 page
* feat: update route
* feat: format code
* feat: update
* feat: update
* feat: add checkbox rule
* feat: handle Modal close event
* feat: format code
* feat: add page skip
* feat: format code
* merge
* feat: clean code
* fix: step4 error
* feat: add edit modal
* feat: add form validation
---
src/pages/Routes/Create.tsx | 36 +++-
src/pages/Routes/components/Step1/MetaView.tsx | 1 +
.../Routes/components/Step1/RequestConfigView.tsx | 236 +++++++++++----------
src/pages/Routes/components/Step1/index.tsx | 3 +-
.../Routes/components/Step2/RequestRewriteView.tsx | 172 +++++++++------
src/pages/Routes/components/Step2/index.tsx | 5 +-
src/pages/Routes/constants.ts | 10 +-
src/pages/Routes/typing.d.ts | 2 +
8 files changed, 276 insertions(+), 189 deletions(-)
diff --git a/src/pages/Routes/Create.tsx b/src/pages/Routes/Create.tsx
index bd07bda..0772606 100644
--- a/src/pages/Routes/Create.tsx
+++ b/src/pages/Routes/Create.tsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { Card, Steps } from 'antd';
+import { Card, Steps, Form } from 'antd';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import Step1 from './components/Step1';
@@ -17,7 +17,10 @@ const Create: React.FC = () => {
const [step2Data, setStep2Data] = useState(DEFAULT_STEP_2_DATA);
const [step3Data, setStep3Data] = useState(DEFAULT_STEP_3_DATA);
- const [step, setStep] = useState(0);
+ const [form1] = Form.useForm();
+ const [form2] = Form.useForm();
+
+ const [step, setStep] = useState(1);
const [stepHeader] = useState(['定义 API 请求', '定义 API 后端服务', '插件配置', '预览']);
const data = {
@@ -31,6 +34,7 @@ const Create: React.FC = () => {
return (
<Step1
data={data}
+ form={form1}
onChange={(_data: RouteModule.Step1Data) => {
setStep1Data(_data);
}}
@@ -42,6 +46,7 @@ const Create: React.FC = () => {
return (
<Step2
data={data}
+ form={form2}
onChange={(params: RouteModule.Step2Data) => setStep2Data({ ...step2Data, ...params })}
/>
);
@@ -58,6 +63,30 @@ const Create: React.FC = () => {
return null;
};
+ const onStepChange = (nextStep: number) => {
+ const nextStepAction = () => {
+ setStep(nextStep);
+ window.scrollTo({ top: 0 });
+ };
+ if (nextStep > step && nextStep < 3) {
+ // Form Validation
+ if (step === 0) {
+ form1.validateFields().then((value) => {
+ setStep1Data({ ...step1Data, ...value });
+ nextStepAction();
+ });
+ return;
+ }
+ if (step === 1) {
+ form2.validateFields().then((value) => {
+ setStep1Data({ ...step1Data, ...value });
+ nextStepAction();
+ });
+ return;
+ }
+ }
+ nextStepAction();
+ };
return (
<>
<PageHeaderWrapper>
@@ -73,8 +102,7 @@ const Create: React.FC = () => {
<ActionBar
step={step}
onChange={(nextStep) => {
- setStep(nextStep);
- window.scrollTo({ top: 0 });
+ onStepChange(nextStep);
}}
/>
</>
diff --git a/src/pages/Routes/components/Step1/MetaView.tsx b/src/pages/Routes/components/Step1/MetaView.tsx
index 69e64c7..3c1679d 100644
--- a/src/pages/Routes/components/Step1/MetaView.tsx
+++ b/src/pages/Routes/components/Step1/MetaView.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import Form from 'antd/es/form';
import { Input } from 'antd';
+// import { FORM_ITEM_LAYOUT } from '@/pages/Routes/constants';
import PanelSection from '../PanelSection';
interface Props extends RouteModule.Data {}
diff --git a/src/pages/Routes/components/Step1/RequestConfigView.tsx b/src/pages/Routes/components/Step1/RequestConfigView.tsx
index be46a99..f1417fe 100644
--- a/src/pages/Routes/components/Step1/RequestConfigView.tsx
+++ b/src/pages/Routes/components/Step1/RequestConfigView.tsx
@@ -1,149 +1,161 @@
import React, { useState } from 'react';
import Form from 'antd/es/form';
-import { Row, Checkbox, Button, Col, Input, Space } from 'antd';
+import { Checkbox, Button, Input } from 'antd';
import { CheckboxValueType } from 'antd/lib/checkbox/Group';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
-import { HTTP_METHOD_OPTION_LIST } from '@/pages/Routes/constants';
+import {
+ HTTP_METHOD_OPTION_LIST,
+ FORM_ITEM_LAYOUT,
+ FORM_ITEM_WITHOUT_LABEL,
+} from '@/pages/Routes/constants';
+import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
import PanelSection from '../PanelSection';
interface Props extends RouteModule.Data {}
const RequestConfigView: React.FC<Props> = ({ data, disabled, onChange }) => {
- const { paths, hosts, protocols } = data.step1Data;
- const [httpMethodList, setHttpMethodList] = useState({
- checkedList: HTTP_METHOD_OPTION_LIST,
- indeterminate: false,
- checkAll: true,
- });
+ const { protocols } = data.step1Data;
+
+ // TODO: checkedList Validation
+ const [checkedList, setCheckedList] = useState(HTTP_METHOD_OPTION_LIST);
+ const [indeterminate, setIndeterminate] = useState(false);
+ const [checkAll, setCheckAll] = useState(true);
const onProtocolChange = (e: CheckboxValueType[]) => {
if (!e.includes('HTTP') && !e.includes('HTTPS')) return;
onChange({ ...data.step1Data, protocols: e });
};
- const onMethodsChange = (checkedList: CheckboxValueType[]) => {
- setHttpMethodList({
- checkedList: checkedList as RouteModule.HttpMethod[],
- indeterminate: !!checkedList.length && checkedList.length < HTTP_METHOD_OPTION_LIST.length,
- checkAll: checkedList.length === HTTP_METHOD_OPTION_LIST.length,
- });
+
+ const onMethodsChange = (methods: CheckboxValueType[]) => {
+ setCheckedList(methods as RouteModule.HttpMethod[]);
+ setIndeterminate(!!methods.length && methods.length < HTTP_METHOD_OPTION_LIST.length);
+ setCheckAll(methods.length === HTTP_METHOD_OPTION_LIST.length);
};
+
const onCheckAllChange = (e: CheckboxChangeEvent) => {
- setHttpMethodList({
- checkedList: e.target.checked ? HTTP_METHOD_OPTION_LIST : [],
- indeterminate: false,
- checkAll: e.target.checked,
- });
+ setCheckedList(e.target.checked ? HTTP_METHOD_OPTION_LIST : []);
+ setIndeterminate(false);
+ setCheckAll(e.target.checked);
};
- const renderHosts = () =>
- hosts.map((item, index) => (
- <Row key={`${item + index}`} style={{ marginBottom: '10px' }} gutter={[16, 16]}>
- <Col span={16}>
- <Input placeholder="域名" disabled={disabled} />
- </Col>
- <Col span={4}>
- <Space>
- {hosts.length > 1 && !disabled && (
- <Button
- type="primary"
- danger
- onClick={() => {
- onChange({
- ...data.step1Data,
- hosts: hosts.filter((_, _index) => _index !== index),
- });
- }}
+ const renderHosts = () => (
+ <Form.List name="hosts">
+ {(fields, { add, remove }) => {
+ return (
+ <div>
+ {fields.map((field, index) => (
+ <Form.Item
+ {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
+ label={index === 0 ? '域名' : ''}
+ required
+ key={field.key}
>
- 删除
- </Button>
- )}
- </Space>
- </Col>
- </Row>
- ));
+ <Form.Item
+ {...field}
+ validateTrigger={['onChange', 'onBlur']}
+ rules={[
+ {
+ required: true,
+ whitespace: true,
+ message: '请输入域名',
+ },
+ ]}
+ noStyle
+ >
+ <Input placeholder="请输入域名" style={{ width: '60%' }} />
+ </Form.Item>
+ {fields.length > 1 ? (
+ <MinusCircleOutlined
+ className="dynamic-delete-button"
+ style={{ margin: '0 8px' }}
+ onClick={() => {
+ remove(field.name);
+ }}
+ />
+ ) : null}
+ </Form.Item>
+ ))}
+ <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
+ {!disabled && (
+ <Button
+ type="dashed"
+ onClick={() => {
+ add();
+ }}
+ >
+ <PlusOutlined /> 增加
+ </Button>
+ )}
+ </Form.Item>
+ </div>
+ );
+ }}
+ </Form.List>
+ );
- const renderPaths = () =>
- paths.map((item, index) => (
- <Row key={`${item + index}`} style={{ marginBottom: '10px' }} gutter={[16, 16]}>
- <Col span={16}>
- <Input placeholder="请输入请求路径" disabled={disabled} />
- </Col>
- {!disabled && (
- <Col span={4}>
- <Space>
- <Button
- type="primary"
- danger
- onClick={() => {
- onChange({
- ...data.step1Data,
- paths: paths.filter((_, _index) => _index !== index),
- });
- }}
- >
- 删除
- </Button>
- </Space>
- </Col>
- )}
- </Row>
- ));
+ const renderPaths = () => (
+ <Form.List name="paths">
+ {(fields, { add, remove }) => {
+ return (
+ <div>
+ {fields.map((field) => (
+ <Form.Item required key={field.key}>
+ <Form.Item {...field} validateTrigger={['onChange', 'onBlur']} noStyle>
+ <Input placeholder="请输入路径" style={{ width: '60%' }} />
+ </Form.Item>
+ <MinusCircleOutlined
+ className="dynamic-delete-button"
+ style={{ margin: '0 8px' }}
+ onClick={() => {
+ remove(field.name);
+ }}
+ />
+ </Form.Item>
+ ))}
+ <Form.Item>
+ {!disabled && (
+ <Button
+ type="dashed"
+ onClick={() => {
+ add();
+ }}
+ >
+ <PlusOutlined /> 增加
+ </Button>
+ )}
+ </Form.Item>
+ </div>
+ );
+ }}
+ </Form.List>
+ );
return (
<PanelSection title="请求基础定义">
- <Form.Item label="协议" name="protocol" rules={[{ required: true, message: '请勾选协议' }]}>
- <Row>
- <Checkbox.Group
- disabled={disabled}
- options={['HTTP', 'HTTPS', 'WebSocket']}
- defaultValue={protocols}
- value={protocols}
- onChange={onProtocolChange}
- />
- </Row>
- </Form.Item>
- {/* TODO: name */}
- <Form.Item label="域名" rules={[{ required: true, message: '请输入域名' }]}>
- {renderHosts()}
- {!disabled && (
- <Button
- type="primary"
- onClick={() => onChange({ ...data.step1Data, hosts: hosts.concat('') })}
- >
- 增加
- </Button>
- )}
- </Form.Item>
- {/* TODO: name */}
- <Form.Item label="路径">
- {renderPaths()}
- {!disabled && (
- <Button
- onClick={() => onChange({ ...data.step1Data, paths: paths.concat(['']) })}
- type="primary"
- >
- 增加
- </Button>
- )}
+ <Form.Item label="协议" name="protocols" rules={[{ required: true, message: '请勾选协议' }]}>
+ <Checkbox.Group
+ disabled={disabled}
+ options={['HTTP', 'HTTPS', 'WebSocket']}
+ value={protocols}
+ onChange={onProtocolChange}
+ />
</Form.Item>
- <Form.Item
- label="HTTP 方法"
- name="httpMethods"
- rules={[{ required: true, message: '请选择 HTTP 方法' }]}
- >
+ {renderHosts()}
+ <Form.Item label="路径">{renderPaths()}</Form.Item>
+ <Form.Item label="HTTP 方法" name="httpMethods">
<Checkbox
- indeterminate={httpMethodList.indeterminate}
+ indeterminate={indeterminate}
onChange={onCheckAllChange}
- checked={httpMethodList.checkAll}
+ checked={checkAll}
disabled={disabled}
>
ANY
</Checkbox>
<Checkbox.Group
options={HTTP_METHOD_OPTION_LIST}
- value={httpMethodList.checkedList}
+ value={checkedList}
onChange={onMethodsChange}
disabled={disabled}
/>
diff --git a/src/pages/Routes/components/Step1/index.tsx b/src/pages/Routes/components/Step1/index.tsx
index 78b817b..2fcf38c 100644
--- a/src/pages/Routes/components/Step1/index.tsx
+++ b/src/pages/Routes/components/Step1/index.tsx
@@ -9,8 +9,7 @@ import RequestConfigView from './RequestConfigView';
import MatchingRulesView from './MatchingRulesView';
const Step1: React.FC<RouteModule.Data> = (props) => {
- const { data } = props;
- const [form] = Form.useForm();
+ const { data, form } = props;
return (
<>
diff --git a/src/pages/Routes/components/Step2/RequestRewriteView.tsx b/src/pages/Routes/components/Step2/RequestRewriteView.tsx
index a6a3f46..a403769 100644
--- a/src/pages/Routes/components/Step2/RequestRewriteView.tsx
+++ b/src/pages/Routes/components/Step2/RequestRewriteView.tsx
@@ -3,7 +3,8 @@ import Form, { FormInstance } from 'antd/es/form';
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
import { Input, Row, Col, InputNumber, Button } from 'antd';
-import { FORM_ITEM_LAYOUT } from '@/pages/Routes/constants';
+import { FORM_ITEM_LAYOUT, FORM_ITEM_WITHOUT_LABEL } from '@/pages/Routes/constants';
+import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
import PanelSection from '../PanelSection';
import styles from '../../Create.less';
@@ -13,40 +14,82 @@ interface Props extends RouteModule.Data {
const RequestRewriteView: React.FC<Props> = ({ data, form, disabled, onChange }) => {
const { step2Data } = data;
- const { backendAddressList, backendProtocol } = step2Data;
const onProtocolChange = (e: RadioChangeEvent) => {
onChange({ backendProtocol: e.target.value });
};
- const renderBackendAddress = () =>
- backendAddressList.map((item, index) => (
- <Row key={`${item.host + index}`} style={{ marginBottom: '10px' }} gutter={16}>
- <Col span={9}>
- <Input placeholder="域名" disabled={disabled} />
- </Col>
- <Col span={4}>
- <InputNumber placeholder="端口号" disabled={disabled} min={1} max={65535} />
- </Col>
- <Col span={4} offset={1}>
- <InputNumber placeholder="权重" disabled={disabled} min={0} max={100} />
- </Col>
- <Col span={4} offset={1}>
- {backendAddressList.length > 1 && !disabled && (
- <Button
- type="primary"
- danger
- onClick={() => {
- onChange({
- backendAddressList: backendAddressList.filter((_, _index) => _index !== index),
- });
- }}
- >
- 删除
- </Button>
- )}
- </Col>
- </Row>
- ));
+ const renderBackendAddress = () => (
+ <Form.List name="backendAddressList">
+ {(fields, { add, remove }) => {
+ return (
+ <div>
+ {fields.map((field, index) => {
+ return (
+ <Form.Item
+ required
+ key={field.key}
+ {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
+ label={index === 0 ? '域名/IP' : ''}
+ >
+ <Row style={{ marginBottom: '10px' }} gutter={16}>
+ <Col span={9}>
+ <Form.Item
+ style={{ marginBottom: 0 }}
+ name={[field.name, 'host']}
+ rules={[{ required: true, message: '请输入域名/IP' }]}
+ >
+ <Input placeholder="域名/IP" disabled={disabled} />
+ </Form.Item>
+ </Col>
+ <Col span={4}>
+ <Form.Item
+ style={{ marginBottom: 0 }}
+ name={[field.name, 'port']}
+ rules={[{ required: true, message: '请输入端口' }]}
+ >
+ <InputNumber placeholder="端口号" disabled={disabled} min={1} max={65535} />
+ </Form.Item>
+ </Col>
+ <Col span={4} offset={1}>
+ <Form.Item
+ style={{ marginBottom: 0 }}
+ name={[field.name, 'weight']}
+ rules={[{ required: true, message: '请输入权重' }]}
+ >
+ <InputNumber placeholder="权重" disabled={disabled} min={0} max={100} />
+ </Form.Item>
+ </Col>
+ <Col>
+ {fields.length > 1 ? (
+ <MinusCircleOutlined
+ style={{ margin: '0 8px' }}
+ onClick={() => {
+ remove(field.name);
+ }}
+ />
+ ) : null}
+ </Col>
+ </Row>
+ </Form.Item>
+ );
+ })}
+ <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
+ {!disabled && (
+ <Button
+ type="dashed"
+ onClick={() => {
+ add();
+ }}
+ >
+ <PlusOutlined /> 增加
+ </Button>
+ )}
+ </Form.Item>
+ </div>
+ );
+ }}
+ </Form.List>
+ );
return (
<PanelSection title="请求改写">
@@ -57,47 +100,44 @@ const RequestRewriteView: React.FC<Props> = ({ data, form, disabled, onChange })
className={styles.stepForm}
initialValues={step2Data}
>
- <Form.Item label="协议" name="protocol" rules={[{ required: true, message: '请勾选协议' }]}>
- <Row>
- <Radio.Group
- onChange={onProtocolChange}
- name="backendProtocol"
- value={backendProtocol}
- disabled={disabled}
- >
- <Radio value="originalRequest">原始请求</Radio>
- <Radio value="HTTP">HTTP</Radio>
- <Radio value="HTTPS">HTTPS</Radio>
- </Radio.Group>
- </Row>
- </Form.Item>
- <Form.Item label="请求地址" rules={[{ required: true, message: '请输入后端地址' }]}>
- {renderBackendAddress()}
- {!disabled && (
- <Button
- type="primary"
- onClick={() => {
- onChange({
- backendAddressList: backendAddressList.concat({ host: '', port: 0, weight: 0 }),
- });
- }}
- >
- 增加
- </Button>
- )}
+ <Form.Item
+ label="协议"
+ name="backendProtocol"
+ rules={[{ required: true, message: '请勾选协议' }]}
+ >
+ <Radio.Group onChange={onProtocolChange} name="backendProtocol" disabled={disabled}>
+ <Radio value="originalRequest">原始请求</Radio>
+ <Radio value="HTTP">HTTP</Radio>
+ <Radio value="HTTPS">HTTPS</Radio>
+ </Radio.Group>
</Form.Item>
- <Form.Item label="请求路径">
- <Row>
- <Input disabled={disabled} />
- </Row>
+ {renderBackendAddress()}
+ <Form.Item
+ label="请求路径"
+ name="backendAddressPath"
+ rules={[{ required: true, message: '请输入请求路径' }]}
+ >
+ <Input disabled={disabled} />
</Form.Item>
- <Form.Item label="连接超时">
+ <Form.Item
+ label="连接超时"
+ name={['timeout', 'connect']}
+ rules={[{ required: true, message: '请输入连接超时' }]}
+ >
<InputNumber disabled={disabled} defaultValue={30000} /> ms
</Form.Item>
- <Form.Item label="发送超时">
+ <Form.Item
+ label="发送超时"
+ name={['timeout', 'send']}
+ rules={[{ required: true, message: '请输入发送超时' }]}
+ >
<InputNumber disabled={disabled} defaultValue={30000} /> ms
</Form.Item>
- <Form.Item label="接收超时">
+ <Form.Item
+ label="接收超时"
+ name={['timeout', 'read']}
+ rules={[{ required: true, message: '请输入接收超时' }]}
+ >
<InputNumber disabled={disabled} defaultValue={30000} /> ms
</Form.Item>
</Form>
diff --git a/src/pages/Routes/components/Step2/index.tsx b/src/pages/Routes/components/Step2/index.tsx
index e3c3dcc..ff4d918 100644
--- a/src/pages/Routes/components/Step2/index.tsx
+++ b/src/pages/Routes/components/Step2/index.tsx
@@ -1,15 +1,12 @@
import React from 'react';
-import { Form } from 'antd';
import RequestRewriteView from './RequestRewriteView';
import HttpHeaderRewriteView from './HttpHeaderRewriteView';
const Step2: React.FC<RouteModule.Data> = (props) => {
- const [form] = Form.useForm();
-
return (
<>
- <RequestRewriteView form={form} {...props} />
+ <RequestRewriteView form={props.form} {...props} />
<HttpHeaderRewriteView {...props} />
</>
);
diff --git a/src/pages/Routes/constants.ts b/src/pages/Routes/constants.ts
index 5bc45a0..c2473a8 100644
--- a/src/pages/Routes/constants.ts
+++ b/src/pages/Routes/constants.ts
@@ -9,10 +9,17 @@ export const FORM_ITEM_LAYOUT = {
},
};
+export const FORM_ITEM_WITHOUT_LABEL = {
+ wrapperCol: {
+ xs: { span: 24, offset: 0 },
+ sm: { span: 20, offset: 6 },
+ },
+};
+
export const DEFAULT_STEP_1_DATA: RouteModule.Step1Data = {
name: '',
protocols: ['HTTP', 'HTTPS'],
- hosts: [],
+ hosts: [''],
paths: [],
httpMethods: [],
advancedMatchingRules: [],
@@ -22,6 +29,7 @@ export const DEFAULT_STEP_2_DATA: RouteModule.Step2Data = {
backendProtocol: 'originalRequest',
backendAddressList: [{ host: '', port: 0, weight: 0 }],
upstream_header: [],
+ backendAddressPath: '',
timeout: {
connect: 30000,
send: 30000,
diff --git a/src/pages/Routes/typing.d.ts b/src/pages/Routes/typing.d.ts
index 6a340d7..f766abf 100644
--- a/src/pages/Routes/typing.d.ts
+++ b/src/pages/Routes/typing.d.ts
@@ -33,6 +33,7 @@ declare namespace RouteModule {
step3Data: Step3Data;
};
onChange(data: T): void;
+ form?: any;
}
type backendAddressItemProps = {
@@ -51,6 +52,7 @@ declare namespace RouteModule {
type Step2Data = {
backendProtocol: 'HTTP' | 'HTTPS' | 'originalRequest';
backendAddressList: backendAddressItemProps[];
+ backendAddressPath: string;
upstream_header: UpstreamHeader[];
timeout: {
connect: number;