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/09/10 16:19:47 UTC

[apisix-dashboard] branch master updated: Feat: dashboard support route group (#433)

This is an automated email from the ASF dual-hosted git repository.

juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new e2271b3  Feat: dashboard support route group (#433)
e2271b3 is described below

commit e2271b386764cff0b903cae7ee3b16afcd8d1883
Author: liuxiran <be...@126.com>
AuthorDate: Fri Sep 11 00:19:38 2020 +0800

    Feat: dashboard support route group (#433)
    
    * feat: route group UI
    
    * feat(route-ui): add routegroup when create route
    
    * fix: add routegroup to route list
    
    * fix: support add routegroup together with route
    
    * fix: route path define error
    
    * feat: trigger redeploy
    
    * fix: update i18n key
    
    Co-authored-by: 琚致远 <ju...@apache.org>
---
 config/routes.ts                                   | 12 +++
 src/helpers.tsx                                    |  5 ++
 src/locales/en-US/menu.ts                          |  1 +
 src/locales/zh-CN/menu.ts                          |  1 +
 src/pages/Route/Create.tsx                         | 11 ++-
 src/pages/Route/List.tsx                           |  4 +
 src/pages/Route/components/Step1/MetaView.tsx      | 53 ++++++++++++-
 src/pages/Route/constants.ts                       |  2 +
 src/pages/Route/locales/en-US.ts                   |  8 +-
 src/pages/Route/locales/zh-CN.ts                   |  8 +-
 src/pages/Route/service.ts                         | 11 +++
 src/pages/Route/transform.ts                       | 15 +++-
 src/pages/Route/typing.d.ts                        |  4 +
 src/pages/RouteGroup/Create.tsx                    | 88 ++++++++++++++++++++++
 src/pages/{Route => RouteGroup}/List.tsx           | 69 ++++++-----------
 src/pages/RouteGroup/components/Preview.tsx        | 28 +++++++
 .../components/Step1.tsx}                          | 43 ++++++-----
 src/pages/RouteGroup/constants.ts                  | 31 ++++++++
 src/pages/RouteGroup/index.ts                      | 18 +++++
 src/pages/RouteGroup/locales/en-US.ts              | 45 +++++++++++
 src/pages/RouteGroup/locales/zh-CN.ts              | 45 +++++++++++
 src/pages/RouteGroup/service.ts                    | 46 +++++++++++
 src/pages/RouteGroup/typing.d.ts                   | 23 ++++++
 23 files changed, 498 insertions(+), 73 deletions(-)

diff --git a/config/routes.ts b/config/routes.ts
index 6648413..7512519 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -36,6 +36,18 @@ const routes = [
     component: './Route/Create',
   },
   {
+    path: '/routegroup/list',
+    component: './RouteGroup/List',
+  },
+  {
+    path: '/routegroup/create',
+    component: './RouteGroup/Create',
+  },
+  {
+    path: '/routegroup/:gid/edit',
+    component: './RouteGroup/Create',
+  },
+  {
     path: '/ssl/:id/edit',
     component: './SSL/Create',
   },
diff --git a/src/helpers.tsx b/src/helpers.tsx
index 39e97da..4b9151d 100644
--- a/src/helpers.tsx
+++ b/src/helpers.tsx
@@ -35,6 +35,11 @@ export const getMenuData = (): MenuDataItem[] => {
       icon: <IconFont type="iconroute" />,
     },
     {
+      name: 'routegroup',
+      path: '/routegroup/list',
+      icon: <IconFont type="iconroute" />,
+    },
+    {
       name: 'ssl',
       path: '/ssl/list',
       icon: <IconFont type="iconSSLshuzizhengshu" />,
diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts
index 0528b97..8afd19b 100644
--- a/src/locales/en-US/menu.ts
+++ b/src/locales/en-US/menu.ts
@@ -66,6 +66,7 @@ export default {
   'menu.editor.koni': 'Koni Editor',
   'menu.metrics': 'Metrics',
   'menu.routes': 'Route',
+  'menu.routegroup': 'RouteGroup',
   'menu.ssl': 'SSL',
   'menu.upstream': 'Upstream',
   'menu.consumer': 'Consumer',
diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts
index df0fbca..7597a94 100644
--- a/src/locales/zh-CN/menu.ts
+++ b/src/locales/zh-CN/menu.ts
@@ -66,6 +66,7 @@ export default {
   'menu.editor.koni': '拓扑编辑器',
   'menu.metrics': '监控',
   'menu.routes': '路由',
+  'menu.routegroup': '路由分组',
   'menu.ssl': '证书',
   'menu.upstream': '上游',
   'menu.consumer': '用户',
diff --git a/src/pages/Route/Create.tsx b/src/pages/Route/Create.tsx
index 274f5ba..dcc21b1 100644
--- a/src/pages/Route/Create.tsx
+++ b/src/pages/Route/Create.tsx
@@ -29,6 +29,7 @@ import {
   checkUniqueName,
   fetchUpstreamItem,
   checkHostWithSSL,
+  fetchRouteGroupItem,
 } from './service';
 import Step1 from './components/Step1';
 import Step2 from './components/Step2';
@@ -132,7 +133,15 @@ const Page: React.FC<Props> = (props) => {
           data={routeData}
           form={form1}
           onChange={(params: RouteModule.Step1Data) => {
-            setStep1Data({ ...step1Data, ...params });
+            if (params.route_group_id) {
+              fetchRouteGroupItem(params.route_group_id).then((data) => {
+                form1.setFieldsValue({
+                  ...form1.getFieldsValue(),
+                  ...data,
+                });
+              });
+            }
+            setStep1Data({ ...form1.getFieldsValue(), ...step1Data, ...params });
           }}
         />
       );
diff --git a/src/pages/Route/List.tsx b/src/pages/Route/List.tsx
index df2a899..e37d043 100644
--- a/src/pages/Route/List.tsx
+++ b/src/pages/Route/List.tsx
@@ -63,6 +63,10 @@ const Page: React.FC = () => {
       dataIndex: 'description',
     },
     {
+      title: formatMessage({ id: 'route.list.group.name' }),
+      dataIndex: 'route_group_name',
+    },
+    {
       title: formatMessage({ id: 'route.list.edit.time' }),
       dataIndex: 'update_time',
       render: (text) => `${moment.unix(Number(text)).format('YYYY-MM-DD HH:mm:ss')}`,
diff --git a/src/pages/Route/components/Step1/MetaView.tsx b/src/pages/Route/components/Step1/MetaView.tsx
index b04a5ac..afa1b90 100644
--- a/src/pages/Route/components/Step1/MetaView.tsx
+++ b/src/pages/Route/components/Step1/MetaView.tsx
@@ -14,16 +14,33 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import Form from 'antd/es/form';
-import { Input } from 'antd';
+import { Input, Select } from 'antd';
 import { useIntl } from 'umi';
 import { PanelSection } from '@api7-dashboard/ui';
 
+import { fetchRouteGroupList } from '@/pages/Route/service';
+
 interface Props extends RouteModule.Data {}
 
-const MetaView: React.FC<Props> = ({ disabled }) => {
+const MetaView: React.FC<Props> = ({ data, disabled, onChange }) => {
+  const { step1Data } = data;
   const { formatMessage } = useIntl();
+  const routeGroupDisabled = disabled || !!step1Data.route_group_id;
+  const [routeGroups, setRouteGroups] = useState<{ id: string; name: string }[]>();
+  useEffect(() => {
+    // eslint-disable-next-line no-shadow
+    fetchRouteGroupList().then(({ data }) => {
+      setRouteGroups([
+        { name: formatMessage({ id: 'route.meta.api.create.group.name' }), id: null },
+        ...data,
+      ]);
+      if (step1Data.route_group_id) {
+        onChange({ route_group_id: step1Data.route_group_id });
+      }
+    });
+  }, []);
   return (
     <PanelSection title={formatMessage({ id: 'route.meta.name.description' })}>
       <Form.Item
@@ -43,6 +60,36 @@ const MetaView: React.FC<Props> = ({ disabled }) => {
           disabled={disabled}
         />
       </Form.Item>
+      <Form.Item label={formatMessage({ id: 'route.meta.api.group.name' })} name="route_group_id">
+        <Select
+          onChange={(value) => {
+            if (step1Data.route_group_id) {
+              onChange({ route_group_id: value });
+            }
+          }}
+          disabled={disabled}
+        >
+          {(routeGroups || []).map((item) => {
+            return (
+              <Select.Option value={item.id} key={item.id}>
+                {item.name}
+              </Select.Option>
+            );
+          })}
+        </Select>
+      </Form.Item>
+      <Form.Item
+        label={formatMessage({ id: 'route.meta.group.name' })}
+        name="route_group_name"
+        rules={[
+          { required: true, message: formatMessage({ id: 'route.meta.input.api.group.name' }) },
+        ]}
+      >
+        <Input
+          placeholder={formatMessage({ id: 'route.meta.input.api.group.name' })}
+          disabled={routeGroupDisabled}
+        />
+      </Form.Item>
       <Form.Item label={formatMessage({ id: 'route.meta.description' })} name="desc">
         <Input.TextArea
           placeholder={formatMessage({ id: 'route.meta.description.rule' })}
diff --git a/src/pages/Route/constants.ts b/src/pages/Route/constants.ts
index 0620bbd..cc0e05b 100644
--- a/src/pages/Route/constants.ts
+++ b/src/pages/Route/constants.ts
@@ -41,6 +41,8 @@ export const FORM_ITEM_WITHOUT_LABEL = {
 };
 
 export const DEFAULT_STEP_1_DATA: RouteModule.Step1Data = {
+  route_group_id: '',
+  route_group_name: '',
   name: '',
   desc: '',
   priority: 0,
diff --git a/src/pages/Route/locales/en-US.ts b/src/pages/Route/locales/en-US.ts
index 78baa5f..b102e2f 100644
--- a/src/pages/Route/locales/en-US.ts
+++ b/src/pages/Route/locales/en-US.ts
@@ -61,8 +61,13 @@ export default {
     'Maximum length 100, only letters, Numbers, _, and - are supported, and can only begin with letters',
   'rotue.meta.api.rule':
     'Only letters, numbers, _ and - are supported, and can only begin with letters',
-  'route.meta.description': 'Description',
+  'route.meta.api.group.name': 'RouteGroup',
+  'route.meta.group.name': 'GroupName',
+  'route.meta.input.api.group.name': 'Please enter the group name',
+  'route.meta.api.create.group.name': 'Create route group',
+  'route.meta.description': 'APIDescription',
   'route.meta.description.rule': 'Can not more than 200 characters',
+  'route.meta.group.description': 'GroupDescription',
 
   'route.request.config.domain.name': 'Domain Name',
   'route.request.config.domain.or.ip':
@@ -146,6 +151,7 @@ export default {
   'route.list.domain.name': 'Domain Name',
   'route.list.path': 'Path',
   'route.list.description': 'Description',
+  'route.list.group.name': 'RouteGroup',
   'route.list.edit.time': 'Edit Time',
   'route.list.operation': 'Operation',
   'route.list.edit': 'Edit',
diff --git a/src/pages/Route/locales/zh-CN.ts b/src/pages/Route/locales/zh-CN.ts
index 5894354..a3385fd 100644
--- a/src/pages/Route/locales/zh-CN.ts
+++ b/src/pages/Route/locales/zh-CN.ts
@@ -58,8 +58,13 @@ export default {
   'route.meta.input.api.name': '请输入 API 名称',
   'route.meta.api.name.rule': '最大长度100,仅支持字母、数字、- 和 _,且只能以字母开头',
   'rotue.meta.api.rule': '仅支持字母、数字、- 和 _,且只能以字母开头',
-  'route.meta.description': '描述',
+  'route.meta.api.group.name': '路由分组',
+  'route.meta.group.name': '分组名称',
+  'route.meta.input.api.group.name': '请输入路由分组名称',
+  'route.meta.api.create.group.name': '创建路由分组',
+  'route.meta.description': '路由描述',
   'route.meta.description.rule': '不超过 200 个字符',
+  'route.meta.group.description': '分组描述',
 
   'route.request.config.domain.name': '域名',
   'route.request.config.domain.or.ip': '域名或IP,支持泛域名,如:*.test.com',
@@ -141,6 +146,7 @@ export default {
   'route.list.domain.name': '域名',
   'route.list.path': '路径',
   'route.list.description': '描述',
+  'route.list.group.name': '路由分组',
   'route.list.edit.time': '编辑时间',
   'route.list.operation': '操作',
   'route.list.edit': '编辑',
diff --git a/src/pages/Route/service.ts b/src/pages/Route/service.ts
index e9434f5..171d4f5 100644
--- a/src/pages/Route/service.ts
+++ b/src/pages/Route/service.ts
@@ -62,6 +62,17 @@ export const checkUniqueName = (name = '', exclude = '') =>
     ),
   });
 
+export const fetchRouteGroupList = () => request(`/names/routegroups`);
+
+export const fetchRouteGroupItem = (gid: string) => {
+  return request(`/routegroups/${gid}`).then((data) => {
+    return {
+      route_group_name: data.name,
+      route_group_id: data.id,
+    };
+  });
+};
+
 export const fetchUpstreamList = () => request(`/names/upstreams`);
 
 export const fetchUpstreamItem = (sid: string) => {
diff --git a/src/pages/Route/transform.ts b/src/pages/Route/transform.ts
index dae7d91..e3c2b4e 100644
--- a/src/pages/Route/transform.ts
+++ b/src/pages/Route/transform.ts
@@ -155,10 +155,23 @@ export const transformUpstreamNodes = (
 };
 
 export const transformRouteData = (data: RouteModule.Body) => {
-  const { name, desc, methods, uris, protocols, hosts, vars, redirect } = data;
+  const {
+    name,
+    route_group_id,
+    route_group_name,
+    desc,
+    methods,
+    uris,
+    protocols,
+    hosts,
+    vars,
+    redirect,
+  } = data;
 
   const step1Data: Partial<RouteModule.Step1Data> = {
     name,
+    route_group_id,
+    route_group_name,
     desc,
     protocols: protocols.filter((item) => item !== 'websocket'),
     websocket: protocols.includes('websocket'),
diff --git a/src/pages/Route/typing.d.ts b/src/pages/Route/typing.d.ts
index 9fc25dc..409f4cc 100644
--- a/src/pages/Route/typing.d.ts
+++ b/src/pages/Route/typing.d.ts
@@ -51,6 +51,8 @@ declare namespace RouteModule {
     redirectURI?: string;
     redirectCode?: number;
     advancedMatchingRules: MatchingRule[];
+    route_group_id?: string;
+    route_group_name: string;
   };
 
   type Step3Data = {
@@ -113,6 +115,8 @@ declare namespace RouteModule {
   // Request Body or Response Data for API
   type Body = {
     id?: number;
+    route_group_id?: string;
+    route_group_name: string;
     name: string;
     desc: string;
     priority?: number;
diff --git a/src/pages/RouteGroup/Create.tsx b/src/pages/RouteGroup/Create.tsx
new file mode 100644
index 0000000..80213a2
--- /dev/null
+++ b/src/pages/RouteGroup/Create.tsx
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, { useEffect, useState } from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Form, notification, Steps } from 'antd';
+
+import ActionBar from '@/components/ActionBar';
+import { history, useIntl } from 'umi';
+
+import Step1 from './components/Step1';
+import Preview from './components/Preview';
+import { create, fetchOne, update } from './service';
+
+const Page: React.FC = (props) => {
+  const [step, setStep] = useState(1);
+  const [form1] = Form.useForm();
+  const { formatMessage } = useIntl();
+
+  useEffect(() => {
+    const { gid } = (props as any).match.params;
+
+    if (gid) {
+      fetchOne(gid).then((data) => {
+        form1.setFieldsValue(data);
+      });
+    }
+  }, []);
+
+  const onSubmit = () => {
+    const data = { ...form1.getFieldsValue() } as RouteGroupModule.RouteGroupEntity;
+    const { gid } = (props as any).match.params;
+    (gid ? update(gid, data) : create(data)).then(() => {
+      notification.success({
+        message: `${
+          gid
+            ? formatMessage({ id: 'routegroup.create.edit' })
+            : formatMessage({ id: 'routegroup.create.create' })
+        } ${formatMessage({ id: 'routegroup.create.routegroup.successfully' })}`,
+      });
+      history.replace('/routegroup/list');
+    });
+  };
+
+  const onStepChange = (nextStep: number) => {
+    if (step === 1) {
+      form1.validateFields().then(() => {
+        setStep(nextStep);
+      });
+    } else if (nextStep === 3) {
+      onSubmit();
+    } else {
+      setStep(nextStep);
+    }
+  };
+
+  return (
+    <>
+      <PageContainer title={formatMessage({ id: 'routegroup.create.create' })}>
+        <Card bordered={false}>
+          <Steps current={step - 1} style={{ marginBottom: 30 }}>
+            <Steps.Step title={formatMessage({ id: 'routegroup.create.basic.info' })} />
+            <Steps.Step title={formatMessage({ id: 'routegroup.create.preview' })} />
+          </Steps>
+
+          {step === 1 && <Step1 form={form1} />}
+          {step === 2 && <Preview form1={form1} />}
+        </Card>
+      </PageContainer>
+      <ActionBar step={step} lastStep={2} onChange={onStepChange} />
+    </>
+  );
+};
+
+export default Page;
diff --git a/src/pages/Route/List.tsx b/src/pages/RouteGroup/List.tsx
similarity index 59%
copy from src/pages/Route/List.tsx
copy to src/pages/RouteGroup/List.tsx
index df2a899..b2da173 100644
--- a/src/pages/Route/List.tsx
+++ b/src/pages/RouteGroup/List.tsx
@@ -15,86 +15,63 @@
  * limitations under the License.
  */
 import React, { useRef, useState } from 'react';
-import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import { PageContainer } from '@ant-design/pro-layout';
 import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
-import { Button, Popconfirm, notification, Tag, Input } from 'antd';
 import { PlusOutlined } from '@ant-design/icons';
-import moment from 'moment';
+import { Popconfirm, Button, notification, Input } from 'antd';
 import { history, useIntl } from 'umi';
+import moment from 'moment';
 
 import { fetchList, remove } from './service';
 
 const Page: React.FC = () => {
   const ref = useRef<ActionType>();
+
   const [search, setSearch] = useState('');
   const { formatMessage } = useIntl();
 
-  const columns: ProColumns<RouteModule.BaseData>[] = [
+  const columns: ProColumns<RouteGroupModule.RouteGroupEntity>[] = [
     {
-      title: formatMessage({ id: 'route.list.name' }),
+      title: formatMessage({ id: 'routegroup.list.name' }),
       dataIndex: 'name',
     },
     {
-      title: formatMessage({ id: 'route.list.domain.name' }),
-      dataIndex: 'hosts',
-      render: (_, record) =>
-        record.hosts.map((host) => (
-          <Tag key={host} color="geekblue">
-            {host}
-          </Tag>
-        )),
-    },
-    {
-      title: formatMessage({ id: 'route.list.path' }),
-      dataIndex: 'uri',
-      render: (_, record) =>
-        record.uris.map((uri) => (
-          <Tag key={uri} color="geekblue">
-            {uri}
-          </Tag>
-        )),
-    },
-    // {
-    //   title: '优先级',
-    //   dataIndex: 'priority',
-    // },
-    {
-      title: formatMessage({ id: 'route.list.description' }),
+      title: formatMessage({ id: 'routegroup.list.description' }),
       dataIndex: 'description',
     },
     {
-      title: formatMessage({ id: 'route.list.edit.time' }),
+      title: formatMessage({ id: 'routegroup.list.edit.time' }),
       dataIndex: 'update_time',
       render: (text) => `${moment.unix(Number(text)).format('YYYY-MM-DD HH:mm:ss')}`,
     },
     {
-      title: formatMessage({ id: 'route.list.operation' }),
+      title: formatMessage({ id: 'routegroup.list.operation' }),
       valueType: 'option',
       render: (_, record) => (
         <>
           <Button
             type="primary"
-            onClick={() => history.push(`/routes/${record.id}/edit`)}
             style={{ marginRight: 10 }}
+            onClick={() => history.push(`/routegroup/${record.id}/edit`)}
           >
-            {formatMessage({ id: 'route.list.edit' })}
+            {formatMessage({ id: 'routegroup.list.edit' })}
           </Button>
           <Popconfirm
-            title={formatMessage({ id: 'route.list.delete.confrim' })}
+            title={formatMessage({ id: 'routegroup.list.confirm.delete' })}
+            okText={formatMessage({ id: 'routegroup.list.confirm' })}
+            cancelText={formatMessage({ id: 'routegroup.list.cancel' })}
             onConfirm={() => {
               remove(record.id!).then(() => {
                 notification.success({
-                  message: formatMessage({ id: 'route.list.delete.success' }),
+                  message: formatMessage({ id: 'routegroup.list.delete.successfully' }),
                 });
                 /* eslint-disable no-unused-expressions */
                 ref.current?.reload();
               });
             }}
-            okText={formatMessage({ id: 'route.list.confirm' })}
-            cancelText={formatMessage({ id: 'route.list.cancel' })}
           >
             <Button type="primary" danger>
-              {formatMessage({ id: 'route.list.delete' })}
+              {formatMessage({ id: 'routegroup.list.delete' })}
             </Button>
           </Popconfirm>
         </>
@@ -103,29 +80,29 @@ const Page: React.FC = () => {
   ];
 
   return (
-    <PageHeaderWrapper title={formatMessage({ id: 'route.list' })}>
-      <ProTable<RouteModule.BaseData>
+    <PageContainer title={formatMessage({ id: 'routegroup.list' })}>
+      <ProTable<RouteGroupModule.RouteGroupEntity>
         actionRef={ref}
-        rowKey="name"
         columns={columns}
+        rowKey="id"
         search={false}
         request={(params) => fetchList(params, search)}
         toolBarRender={(action) => [
           <Input.Search
-            placeholder={formatMessage({ id: 'route.list.input' })}
+            placeholder={formatMessage({ id: 'routegroup.list.input' })}
             onSearch={(value) => {
               setSearch(value);
               action.setPageInfo({ page: 1 });
               action.reload();
             }}
           />,
-          <Button type="primary" onClick={() => history.push('/routes/create')}>
+          <Button type="primary" onClick={() => history.push('/routegroup/create')}>
             <PlusOutlined />
-            {formatMessage({ id: 'route.list.create' })}
+            {formatMessage({ id: 'routegroup.list.create' })}
           </Button>,
         ]}
       />
-    </PageHeaderWrapper>
+    </PageContainer>
   );
 };
 
diff --git a/src/pages/RouteGroup/components/Preview.tsx b/src/pages/RouteGroup/components/Preview.tsx
new file mode 100644
index 0000000..92d436f
--- /dev/null
+++ b/src/pages/RouteGroup/components/Preview.tsx
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { FormInstance } from 'antd/lib/form';
+
+import Step1 from './Step1';
+
+type Props = {
+  form1: FormInstance;
+};
+
+const Page: React.FC<Props> = ({ form1 }) => <Step1 form={form1} disabled />;
+
+export default Page;
diff --git a/src/pages/Route/components/Step1/MetaView.tsx b/src/pages/RouteGroup/components/Step1.tsx
similarity index 53%
copy from src/pages/Route/components/Step1/MetaView.tsx
copy to src/pages/RouteGroup/components/Step1.tsx
index b04a5ac..59f5dc4 100644
--- a/src/pages/Route/components/Step1/MetaView.tsx
+++ b/src/pages/RouteGroup/components/Step1.tsx
@@ -15,42 +15,45 @@
  * limitations under the License.
  */
 import React from 'react';
-import Form from 'antd/es/form';
-import { Input } from 'antd';
+import { Form, Input } from 'antd';
+import { FormInstance } from 'antd/lib/form';
 import { useIntl } from 'umi';
-import { PanelSection } from '@api7-dashboard/ui';
 
-interface Props extends RouteModule.Data {}
+import { FORM_ITEM_LAYOUT } from '@/pages/Upstream/constants';
 
-const MetaView: React.FC<Props> = ({ disabled }) => {
+type Props = {
+  form: FormInstance;
+  disabled?: boolean;
+};
+
+const initialValues = {
+  name: '',
+  description: '',
+};
+
+const Step1: React.FC<Props> = ({ form, disabled }) => {
   const { formatMessage } = useIntl();
   return (
-    <PanelSection title={formatMessage({ id: 'route.meta.name.description' })}>
+    <Form {...FORM_ITEM_LAYOUT} form={form} initialValues={initialValues}>
       <Form.Item
-        label={formatMessage({ id: 'route.meta.api.name' })}
+        label={formatMessage({ id: 'routegroup.step.name' })}
         name="name"
-        rules={[
-          { required: true, message: formatMessage({ id: 'route.meta.input.api.name' }) },
-          {
-            pattern: new RegExp(/^[a-zA-Z][a-zA-Z0-9_-]{0,100}$/, 'g'),
-            message: formatMessage({ id: 'route.meta.api.name.rule' }),
-          },
-        ]}
-        extra={formatMessage({ id: 'rotue.meta.api.rule' })}
+        rules={[{ required: true }]}
+        extra={formatMessage({ id: 'routegroup.step.name.should.unique' })}
       >
         <Input
-          placeholder={formatMessage({ id: 'route.meta.input.api.name' })}
+          placeholder={formatMessage({ id: 'routegroup.step.input.routegroup.name' })}
           disabled={disabled}
         />
       </Form.Item>
-      <Form.Item label={formatMessage({ id: 'route.meta.description' })} name="desc">
+      <Form.Item label={formatMessage({ id: 'routegroup.step.description' })} name="description">
         <Input.TextArea
-          placeholder={formatMessage({ id: 'route.meta.description.rule' })}
+          placeholder={formatMessage({ id: 'routegroup.step.input.description' })}
           disabled={disabled}
         />
       </Form.Item>
-    </PanelSection>
+    </Form>
   );
 };
 
-export default MetaView;
+export default Step1;
diff --git a/src/pages/RouteGroup/constants.ts b/src/pages/RouteGroup/constants.ts
new file mode 100644
index 0000000..25bb12f
--- /dev/null
+++ b/src/pages/RouteGroup/constants.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const FORM_ITEM_LAYOUT = {
+  labelCol: {
+    span: 6,
+  },
+  wrapperCol: {
+    span: 18,
+  },
+};
+
+export const FORM_ITEM_WITHOUT_LABEL = {
+  wrapperCol: {
+    xs: { span: 24, offset: 0 },
+    sm: { span: 20, offset: 6 },
+  },
+};
diff --git a/src/pages/RouteGroup/index.ts b/src/pages/RouteGroup/index.ts
new file mode 100644
index 0000000..7120a79
--- /dev/null
+++ b/src/pages/RouteGroup/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { default as RouteGroupCN } from './locales/zh-CN';
+export { default as RouteGroupUS } from './locales/en-US';
diff --git a/src/pages/RouteGroup/locales/en-US.ts b/src/pages/RouteGroup/locales/en-US.ts
new file mode 100644
index 0000000..318809d
--- /dev/null
+++ b/src/pages/RouteGroup/locales/en-US.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default {
+  'routegroup.step.create': 'Create',
+  'routegroup.step.name': 'Name',
+  'routegroup.step.name.should.unique': 'Name should be unique',
+  'routegroup.step.input.routegroup.name': 'Please input routegroup name',
+  'routegroup.step.description': 'Description',
+  'routegroup.step.input.description': 'Please input description',
+
+  'routegroup.create.edit': 'Edit',
+  'routegroup.create.create': 'Create',
+  'routegroup.create.routegroup.successfully': 'routegroup successfully',
+  'routegroup.create.basic.info': 'Basic Information',
+  'routegroup.create.preview': 'Preview',
+
+  'routegroup.list.name': 'Name',
+  'routegroup.list.type': 'Type',
+  'routegroup.list.description': 'Description',
+  'routegroup.list.edit.time': 'Edit Time',
+  'routegroup.list.operation': 'Operation',
+  'routegroup.list.edit': 'Edit',
+  'routegroup.list.confirm.delete': 'Are you sure to delete ?',
+  'routegroup.list.confirm': 'Confirm',
+  'routegroup.list.cancel': 'Cancel',
+  'routegroup.list.delete.successfully': 'Delete successfully',
+  'routegroup.list.delete': 'Delete',
+  'routegroup.list': 'routegroup List',
+  'routegroup.list.input': 'Please input',
+  'routegroup.list.create': 'Create',
+};
diff --git a/src/pages/RouteGroup/locales/zh-CN.ts b/src/pages/RouteGroup/locales/zh-CN.ts
new file mode 100644
index 0000000..9797ae4
--- /dev/null
+++ b/src/pages/RouteGroup/locales/zh-CN.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default {
+  'routegroup.step.create': '创建',
+  'routegroup.step.name': '名称',
+  'routegroup.step.name.should.unique': '名称需全局唯一',
+  'routegroup.step.input.routegroup.name': '请输入分组名称',
+  'routegroup.step.description': '描述',
+  'routegroup.step.input.description': '请输入描述',
+
+  'routegroup.create.edit': '编辑',
+  'routegroup.create.create': '创建',
+  'routegroup.create.routegroup.successfully': '分组成功',
+  'routegroup.create.basic.info': '基础信息',
+  'routegroup.create.preview': '预览',
+
+  'routegroup.list.name': '名称',
+  'routegroup.list.type': '类型',
+  'routegroup.list.description': '描述',
+  'routegroup.list.edit.time': '编辑时间',
+  'routegroup.list.operation': '操作',
+  'routegroup.list.edit': '编辑',
+  'routegroup.list.confirm.delete': '确定删除该条记录吗?',
+  'routegroup.list.confirm': '确定',
+  'routegroup.list.cancel': '取消',
+  'routegroup.list.delete.successfully': '删除记录成功',
+  'routegroup.list.delete': '删除',
+  'routegroup.list': '分组列表',
+  'routegroup.list.input': '请输入',
+  'routegroup.list.create': '创建',
+};
diff --git a/src/pages/RouteGroup/service.ts b/src/pages/RouteGroup/service.ts
new file mode 100644
index 0000000..8504e4b
--- /dev/null
+++ b/src/pages/RouteGroup/service.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { request } from 'umi';
+
+export const fetchList = ({ current = 1, pageSize = 10 }, search: string) =>
+  request('/routegroups', {
+    params: {
+      page: current,
+      size: pageSize,
+      search,
+    },
+  }).then(({ data, count }) => ({
+    data,
+    total: count,
+  }));
+
+export const fetchOne = (id: string) =>
+  request<RouteGroupModule.RouteGroupEntity>(`/routegroups/${id}`);
+
+export const create = (data: RouteGroupModule.RouteGroupEntity) =>
+  request('/routegroups', {
+    method: 'POST',
+    data,
+  });
+
+export const update = (id: string, data: RouteGroupModule.RouteGroupEntity) =>
+  request(`/routegroups/${id}`, {
+    method: 'PUT',
+    data,
+  });
+
+export const remove = (id: string) => request(`/routegroups/${id}`, { method: 'DELETE' });
diff --git a/src/pages/RouteGroup/typing.d.ts b/src/pages/RouteGroup/typing.d.ts
new file mode 100644
index 0000000..313272a
--- /dev/null
+++ b/src/pages/RouteGroup/typing.d.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+declare namespace RouteGroupModule {
+  type RouteGroupEntity = {
+    id: string;
+    name: string;
+    description: string;
+  };
+}