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/05/28 08:51:37 UTC
[incubator-apisix-dashboard] branch next updated: feat: add routes
step1 page (#218)
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 4a1c7aa feat: add routes step1 page (#218)
4a1c7aa is described below
commit 4a1c7aa71805f99a5e7576c03f1c99e81b04099d
Author: litesun <31...@users.noreply.github.com>
AuthorDate: Thu May 28 16:51:27 2020 +0800
feat: add routes step1 page (#218)
* 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
---
config/routes.ts | 17 ++
src/locales/zh-CN/menu.ts | 2 +
src/pages/Routes/Create.less | 101 +++++++
src/pages/Routes/Create.tsx | 61 ++++
src/pages/Routes/components/PanelSection/index.tsx | 16 ++
src/pages/Routes/components/Step1/index.tsx | 311 +++++++++++++++++++++
src/pages/Routes/service.ts | 0
src/pages/Routes/typing.d.ts | 25 ++
8 files changed, 533 insertions(+)
diff --git a/config/routes.ts b/config/routes.ts
index d8a6406..79aa5ee 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -44,6 +44,23 @@ const routes = [
],
},
{
+ name: 'routes',
+ path: '/routes',
+ icon: 'BarsOutlined',
+ routes: [
+ {
+ path: '/routes',
+ redirect: '/routes/create',
+ },
+ {
+ path: '/routes/create',
+ name: 'create',
+ component: './Routes/Create',
+ hideInMenu: true,
+ },
+ ],
+ },
+ {
component: './404',
},
];
diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts
index 11ea16f..5a877bb 100644
--- a/src/locales/zh-CN/menu.ts
+++ b/src/locales/zh-CN/menu.ts
@@ -53,4 +53,6 @@ export default {
'menu.ssl.edit': '编辑',
'menu.ssl.create': '创建',
'menu.settings': '设置',
+ 'menu.routes': '路由',
+ 'menu.routes.create': '创建',
};
diff --git a/src/pages/Routes/Create.less b/src/pages/Routes/Create.less
new file mode 100644
index 0000000..b0dca3a
--- /dev/null
+++ b/src/pages/Routes/Create.less
@@ -0,0 +1,101 @@
+@import '~antd/es/style/themes/default.less';
+
+.card {
+ margin-bottom: 24px;
+}
+
+.heading {
+ margin: 0 0 16px 0;
+ font-size: 14px;
+ line-height: 22px;
+}
+
+.steps:global(.ant-steps) {
+ max-width: 750px;
+ margin: 16px auto;
+}
+
+.errorIcon {
+ margin-right: 24px;
+ color: @error-color;
+ cursor: pointer;
+
+ span.anticon {
+ margin-right: 4px;
+ }
+}
+
+.errorPopover {
+ :global {
+ .ant-popover-inner-content {
+ min-width: 256px;
+ max-height: 290px;
+ padding: 0;
+ overflow: auto;
+ }
+ }
+}
+
+.errorListItem {
+ padding: 8px 16px;
+ list-style: none;
+ border-bottom: 1px solid @border-color-split;
+ cursor: pointer;
+ transition: all 0.3s;
+
+ &:hover {
+ background: @item-active-bg;
+ }
+
+ &:last-child {
+ border: 0;
+ }
+
+ .errorIcon {
+ float: left;
+ margin-top: 4px;
+ margin-right: 12px;
+ padding-bottom: 22px;
+ color: @error-color;
+ }
+
+ .errorField {
+ margin-top: 2px;
+ color: @text-color-secondary;
+ font-size: 12px;
+ }
+}
+
+.editable {
+ td {
+ padding-top: 13px !important;
+ padding-bottom: 12.5px !important;
+ }
+}
+
+// custom footer for fixed footer toolbar
+.advancedForm + div {
+ padding-bottom: 64px;
+}
+
+.advancedForm {
+ :global {
+ .ant-form .ant-row:last-child .ant-form-item {
+ margin-bottom: 24px;
+ }
+
+ .ant-table td {
+ transition: none !important;
+ }
+ }
+}
+
+.optional {
+ color: @text-color-secondary;
+ font-style: normal;
+}
+
+.stepForm {
+ max-width: 700px;
+ margin: 40px auto 0;
+}
diff --git a/src/pages/Routes/Create.tsx b/src/pages/Routes/Create.tsx
new file mode 100644
index 0000000..dc5ff9e
--- /dev/null
+++ b/src/pages/Routes/Create.tsx
@@ -0,0 +1,61 @@
+import React, { useState } from 'react';
+import { Card, Steps } from 'antd';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import Step1 from './components/Step1';
+import styles from './Create.less';
+
+const { Step } = Steps;
+
+const Create: React.FC = () => {
+ const [step1Data, setStep1Data] = useState<RoutesModule.Step1DataProps>({
+ name: '',
+ protocol: [],
+ hosts: [''],
+ paths: [],
+ httpMethods: [],
+ advancedMatchingRules: [],
+ });
+
+ const [currentStep] = useState(0);
+ const [stepHeader] = useState(['定义 API 请求', '定义 API 后端服务', '插件配置', '预览']);
+ const data = {
+ step1Data,
+ };
+
+ const handleChange = (step: number, params: RoutesModule.Step1DataProps) => {
+ switch (step) {
+ case 0:
+ setStep1Data({ ...step1Data, ...params });
+ break;
+ default:
+ }
+ };
+
+ const renderStep = () => {
+ return (
+ <>
+ {Boolean(currentStep === 0) && (
+ <Step1
+ data={data}
+ onChange={(params: RoutesModule.Step1DataProps) => handleChange(currentStep, params)}
+ />
+ )}
+ </>
+ );
+ };
+
+ return (
+ <PageHeaderWrapper>
+ <Card bordered={false}>
+ <Steps current={currentStep} className={styles.steps}>
+ {stepHeader.map((item) => (
+ <Step title={item} key={item} />
+ ))}
+ </Steps>
+ {renderStep()}
+ </Card>
+ </PageHeaderWrapper>
+ );
+};
+
+export default Create;
diff --git a/src/pages/Routes/components/PanelSection/index.tsx b/src/pages/Routes/components/PanelSection/index.tsx
new file mode 100644
index 0000000..93c8f04
--- /dev/null
+++ b/src/pages/Routes/components/PanelSection/index.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Divider } from 'antd';
+
+const PanelSection: React.FC<{
+ title: string;
+}> = ({ title, children }) => {
+ return (
+ <>
+ <div>{title}</div>
+ <Divider />
+ {children}
+ </>
+ );
+};
+
+export default PanelSection;
diff --git a/src/pages/Routes/components/Step1/index.tsx b/src/pages/Routes/components/Step1/index.tsx
new file mode 100644
index 0000000..3b42dec
--- /dev/null
+++ b/src/pages/Routes/components/Step1/index.tsx
@@ -0,0 +1,311 @@
+import React, { useState } from 'react';
+import { Form, Button, Input, Checkbox, Row, Col, Table, Space, Modal, Select } from 'antd';
+import styles from '../../Create.less';
+
+import PanelSection from '../PanelSection';
+
+const { TextArea } = Input;
+const { Option } = Select;
+
+const formItemLayout = {
+ labelCol: {
+ span: 6,
+ },
+ wrapperCol: {
+ span: 18,
+ },
+};
+
+const Step1: React.FC<RoutesModule.StepProps> = ({ data, onChange }) => {
+ const { step1Data } = data;
+ const { hosts, paths, advancedMatchingRules } = step1Data;
+ const [form] = Form.useForm();
+ const [modalVisible, setModalVisible] = useState(false);
+ const [editModalData, setEditModalData] = useState<RoutesModule.MatchingRule>({
+ paramsLocation: 'query',
+ paramsName: '',
+ paramsExpresstion: '==',
+ paramsValue: '',
+ key: '',
+ });
+
+ const handleAdd = () => {
+ setModalVisible(true);
+ };
+
+ const handleEdit = (record: RoutesModule.MatchingRule) => {
+ setEditModalData(record);
+ requestAnimationFrame(() => {
+ setModalVisible(true);
+ });
+ };
+
+ const handleRemove = (key: string) => {
+ const filteredAdvancedMatchingRules = advancedMatchingRules.filter((item) => item.key !== key);
+ onChange({ advancedMatchingRules: filteredAdvancedMatchingRules });
+ };
+
+ const columns = [
+ {
+ title: '参数位置',
+ dataIndex: 'paramsLocation',
+ key: 'paramsLocation',
+ },
+ {
+ title: '参数名称',
+ dataIndex: 'paramsName',
+ key: 'paramsName',
+ },
+ {
+ title: '运算符',
+ dataIndex: 'paramsExpresstion',
+ key: 'paramsExpresstion',
+ },
+ {
+ title: '参数值',
+ dataIndex: 'paramsValue',
+ key: 'paramsValue',
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: (_: any, record: RoutesModule.MatchingRule) => (
+ <Space size="middle">
+ <a onClick={() => handleEdit(record)}>编辑</a>
+ <a onClick={() => handleRemove(record.key)}>移除</a>
+ </Space>
+ ),
+ },
+ ];
+
+ const addHost = () => {
+ onChange({
+ hosts: hosts.concat(''),
+ });
+ };
+
+ const renderHosts = () =>
+ hosts.map((item, index) => (
+ <Row key={`${item + index}`} style={{ marginBottom: '10px' }} gutter={[16, 16]}>
+ <Col span={16}>
+ <Input placeholder="HOST" />
+ </Col>
+ <Col span={4}>
+ <Space>
+ {hosts.length > 1 && (
+ <Button
+ type="primary"
+ danger
+ onClick={() => {
+ onChange({ hosts: hosts.filter((_, _index) => _index !== index) });
+ }}
+ >
+ 删除
+ </Button>
+ )}
+ </Space>
+ </Col>
+ </Row>
+ ));
+
+ const renderPaths = () =>
+ paths.map((item, index) => (
+ <Row key={`${item + index}`} style={{ marginBottom: '10px' }} gutter={[16, 16]}>
+ <Col span={16}>
+ <Input placeholder="请输入 Path" />
+ </Col>
+ <Col span={4}>
+ <Space>
+ <Button
+ type="primary"
+ danger
+ onClick={() => {
+ onChange({ paths: paths.filter((_, _index) => _index !== index) });
+ }}
+ >
+ 删除
+ </Button>
+ </Space>
+ </Col>
+ </Row>
+ ));
+
+ const addPath = () => {
+ onChange({
+ paths: paths.concat(['']),
+ });
+ };
+
+ const renderMeta = () => (
+ <>
+ <PanelSection title="名称及其描述">
+ <Form {...formItemLayout} form={form} layout="horizontal" className={styles.stepForm}>
+ <Form.Item
+ label="API 名称"
+ name="name"
+ rules={[{ required: true, message: '请输入 API 名称' }]}
+ >
+ <Input placeholder="请输入 API 名称" />
+ </Form.Item>
+ <Form.Item label="描述" name="desc">
+ <TextArea placeholder="请输入描述" />
+ </Form.Item>
+ </Form>
+ </PanelSection>
+ </>
+ );
+
+ const renderBaseRequestConfig = () => (
+ <>
+ <PanelSection title="请求基础定义">
+ <Form {...formItemLayout} form={form} layout="horizontal" className={styles.stepForm}>
+ <Form.Item
+ label="协议"
+ name="protocol"
+ rules={[{ required: true, message: '请勾选协议' }]}
+ >
+ <Checkbox.Group style={{ width: '100%' }}>
+ <Row>
+ {['HTTP', 'HTTPS', 'WebSocket'].map((item) => (
+ <Col span={6} key={item}>
+ <Checkbox value={item}>{item}</Checkbox>
+ </Col>
+ ))}
+ </Row>
+ </Checkbox.Group>
+ </Form.Item>
+ {/* TODO: name */}
+ <Form.Item label="HOST" rules={[{ required: true, message: '请输入 HOST' }]}>
+ {renderHosts()}
+ <Button
+ type="primary"
+ onClick={() => {
+ addHost();
+ }}
+ >
+ 增加
+ </Button>
+ </Form.Item>
+ {/* TODO: name */}
+ <Form.Item label="PATH">
+ {renderPaths()}
+ <Button onClick={addPath} type="primary">
+ 增加
+ </Button>
+ </Form.Item>
+ <Form.Item
+ label="HTTP Methods"
+ name="httpMethods"
+ rules={[{ required: true, message: '请勾选 HTTP Methods' }]}
+ >
+ <Checkbox.Group style={{ width: '100%' }}>
+ <Row>
+ {['ANY', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'].map((item) => (
+ <Col span={6} key={item}>
+ <Checkbox value={item}>{item}</Checkbox>
+ </Col>
+ ))}
+ </Row>
+ </Checkbox.Group>
+ </Form.Item>
+ </Form>
+ </PanelSection>
+ </>
+ );
+
+ const [modalForm] = Form.useForm();
+ const handleOk = () => {
+ modalForm.validateFields().then((value) => {
+ onChange({
+ advancedMatchingRules: advancedMatchingRules.concat({
+ ...(value as RoutesModule.MatchingRule),
+ key: Math.random().toString(36).slice(2),
+ }),
+ });
+ setModalVisible(false);
+ });
+ };
+
+ const handleCancel = () => {
+ setModalVisible(false);
+ };
+
+ const renderAdvancedMatchingRules = () => (
+ <>
+ <PanelSection title="高级路由匹配条件">
+ <div>
+ <Button
+ onClick={handleAdd}
+ type="primary"
+ style={{
+ marginBottom: 16,
+ }}
+ >
+ 新增
+ </Button>
+ <Table key="table" bordered dataSource={advancedMatchingRules} columns={columns} />
+ </div>
+ </PanelSection>
+ </>
+ );
+
+ return (
+ <>
+ {modalVisible && (
+ <Modal title="新增" centered visible onOk={handleOk} onCancel={handleCancel} destroyOnClose>
+ <Form
+ form={modalForm}
+ labelCol={{ span: 4 }}
+ wrapperCol={{ span: 20 }}
+ initialValues={editModalData}
+ >
+ <Form.Item
+ label="参数位置"
+ name="paramsLocation"
+ rules={[{ required: true, message: '请选择参数位置' }]}
+ >
+ <Select>
+ <Option value="header">header</Option>
+ <Option value="query">query</Option>
+ <Option value="params">params</Option>
+ <Option value="cookie">cookie</Option>
+ </Select>
+ </Form.Item>
+ <Form.Item
+ label="参数名称"
+ name="paramsName"
+ rules={[{ required: true, message: '请输入参数名称' }]}
+ >
+ <Input />
+ </Form.Item>
+ <Form.Item
+ label="运算符"
+ name="paramsExpresstion"
+ rules={[{ required: true, message: '请选择运算符' }]}
+ >
+ <Select>
+ <Option value="==">等于</Option>
+ <Option value="~=">不等于</Option>
+ <Option value=">">大于</Option>
+ <Option value="<">小于</Option>
+ <Option value="~~">正则匹配</Option>
+ </Select>
+ </Form.Item>
+ <Form.Item
+ label="值"
+ name="paramsValue"
+ rules={[{ required: true, message: '请输入参数值' }]}
+ >
+ <Input />
+ </Form.Item>
+ </Form>
+ </Modal>
+ )}
+ {renderMeta()}
+ {renderBaseRequestConfig()}
+ {renderAdvancedMatchingRules()}
+ </>
+ );
+};
+
+export default Step1;
diff --git a/src/pages/Routes/service.ts b/src/pages/Routes/service.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/pages/Routes/typing.d.ts b/src/pages/Routes/typing.d.ts
new file mode 100644
index 0000000..0f26e6b
--- /dev/null
+++ b/src/pages/Routes/typing.d.ts
@@ -0,0 +1,25 @@
+declare namespace RoutesModule {
+ interface MatchingRule {
+ paramsLocation: 'query' | 'params' | 'header' | 'cookie';
+ paramsName: string;
+ paramsExpresstion: '==' | '~=' | '>' | '<' | '~~';
+ paramsValue: string;
+ key: string;
+ }
+
+ interface Step1DataProps {
+ name: string;
+ protocol: [];
+ hosts: string[];
+ paths: string[];
+ httpMethods: [];
+ advancedMatchingRules: MatchingRule[];
+ }
+
+ interface StepProps {
+ data: {
+ step1Data: Step1DataProps;
+ };
+ onChange(data: T): void;
+ }
+}