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;
+  }
+}