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 30d98af  chore: improve Route module (#1715)
30d98af is described below

commit 30d98afeb9e3e59d28e9a417fc3f7a799de317f1
Author: 琚致远 <[email protected]>
AuthorDate: Fri Apr 9 20:18:39 2021 +0800

    chore: improve Route module (#1715)
---
 docs/en/latest/I18N_USER_GUIDE.md                  |   7 +-
 web/cypress/fixtures/selector.json                 |   4 +-
 .../create-edit-delete-plugin-template.spec.js     |   1 +
 .../create-plugin-template-with-route.spec.js      |  12 +-
 .../rawDataEditor/test-rawDataEditor.spec.js       |  59 ++-
 ...an-skip-upstream-when-select-service-id.spec.js |   9 +-
 .../create-edit-duplicate-delete-route.spec.js     |  25 +-
 .../create-route-with-proxy-rewrite-plugin.spec.js |   9 +-
 .../route/create-route-with-upstream.spec.js       |   8 +-
 .../integration/route/import_export_route.spec.js  |  20 +-
 web/cypress/integration/route/online-debug.spec.js |  18 +-
 web/cypress/integration/route/search-route.spec.js |   8 +-
 web/src/components/LabelsfDrawer/LabelsDrawer.tsx  |   4 +-
 web/src/locales/en-US/component.ts                 |   3 +-
 web/src/locales/en-US/menu.ts                      |   2 +
 web/src/locales/zh-CN/component.ts                 |   7 +-
 web/src/locales/zh-CN/menu.ts                      |   2 +
 web/src/pages/PluginTemplate/components/Step1.tsx  |   3 +-
 web/src/pages/Route/List.tsx                       | 192 +++++---
 .../Route/components/Step1/MatchingRulesView.tsx   |  35 +-
 web/src/pages/Route/components/Step1/MetaView.tsx  | 291 +++++++++---
 .../pages/Route/components/Step1/ProxyRewrite.tsx  | 205 ++++++---
 .../Route/components/Step1/RequestConfigView.tsx   | 495 +++++++++------------
 web/src/pages/Route/locales/en-US.ts               |  25 +-
 web/src/pages/Route/locales/zh-CN.ts               |  45 +-
 25 files changed, 914 insertions(+), 575 deletions(-)

diff --git a/docs/en/latest/I18N_USER_GUIDE.md 
b/docs/en/latest/I18N_USER_GUIDE.md
index 00f5db3..1bdb051 100644
--- a/docs/en/latest/I18N_USER_GUIDE.md
+++ b/docs/en/latest/I18N_USER_GUIDE.md
@@ -23,13 +23,13 @@ title: i18n User Guide
 
 The Apache APISIX Dashboard uses 
[@umijs/plugin-locale](https://umijs.org/plugins/plugin-locale) to solve the 
i18n issues, in order to make the i18n more clear and reasonable, we would 
recommend to obey the following rules
 
-## Location of locale configuration:
+## Location of locale configuration
 
 - Please put **the global locales** under `src/locales`.
 - Please put **each page's locale file** under `src/pages/$PAGE/locales` 
folder.
 - Please put **the Component's locale file** under 
`src/components/$COMPONENT/locales` folder, and we **MUST** import them manually
 
-## How to name the key for each locale filed:
+## How to name the key for each locale filed
 
 the key can be like this : [basicModule].[moduleName].[elementName].[...desc]
 
@@ -64,7 +64,6 @@ we have already defined many global keys, before you do i18n, 
you can refer to [
 
 ```js
 'page.route.form.itemRulesExtraMessage.parameterName': '仅支持字母和数字,且只能以字母开头',
-'page.route.form.itemLabel.apiName': 'API 名称',
 'page.route.form.itemRulesPatternMessage.apiNameRule': '最大长度100,仅支持字母、数字、- 和 
_,且只能以字母开头',
 ```
 
@@ -101,7 +100,7 @@ we have already defined many global keys, before you do 
i18n, you can refer to [
 **Example:**
 
 ```js
-'page.route.steps.stepTitle.defineApiRequest': '定义 API 请求',
+'page.route.steps.stepTitle.defineApiRequest': '设置路由信息',
 ```
 
 - **Select**
diff --git a/web/cypress/fixtures/selector.json 
b/web/cypress/fixtures/selector.json
index 0236d8e..b903e0f 100644
--- a/web/cypress/fixtures/selector.json
+++ b/web/cypress/fixtures/selector.json
@@ -85,5 +85,7 @@
   "pageTwoActived": ".ant-pagination-item-2.ant-pagination-item-active",
   "selectDropdown": ".ant-select-dropdown",
   "codeMirrorMode": "[data-cy='code-mirror-mode']",
-  "selectJSON":".ant-select-dropdown [label=JSON]"
+  "selectJSON":".ant-select-dropdown [label=JSON]",
+
+  "deleteAlert": ".ant-modal-body"
 }
diff --git 
a/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
 
b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
index f41f54a..5f8186e 100644
--- 
a/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
+++ 
b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
@@ -29,6 +29,7 @@ context('Create Configure and Delete PluginTemplate', () => {
     cy.visit('/');
     cy.contains('Route').click();
     cy.get(this.domSelector.empty).should('be.visible');
+    cy.contains('Advanced').should('be.visible').click();
     cy.contains('Plugin Template Config').should('be.visible').click();
     cy.get(this.domSelector.empty).should('be.visible');
     cy.contains('Create').click();
diff --git 
a/web/cypress/integration/pluginTemplate/create-plugin-template-with-route.spec.js
 
b/web/cypress/integration/pluginTemplate/create-plugin-template-with-route.spec.js
index 0b83369..6b673ba 100644
--- 
a/web/cypress/integration/pluginTemplate/create-plugin-template-with-route.spec.js
+++ 
b/web/cypress/integration/pluginTemplate/create-plugin-template-with-route.spec.js
@@ -28,6 +28,7 @@ context('Create PluginTemplate Binding To Route', () => {
     cy.visit('/');
     cy.contains('Route').click();
     cy.get(this.domSelector.empty).should('be.visible');
+    cy.contains('Advanced').should('be.visible').click();
     cy.contains('Plugin Template Config').should('be.visible').click();
     cy.get(this.domSelector.empty).should('be.visible');
     cy.contains('Create').click();
@@ -37,8 +38,10 @@ context('Create PluginTemplate Binding To Route', () => {
     cy.contains('Submit').click();
     cy.get(this.domSelector.notification).should('contain', 
this.data.createPluginTemplateSuccess);
 
-    cy.visit('routes/list');
+    cy.visit('/routes/list');
     cy.contains('Create').click();
+    cy.get(this.domSelector.empty).should('be.visible');
+    cy.get(this.domSelector.name).click();
     cy.get(this.domSelector.name).type(this.data.routeName);
     cy.contains('Next').click();
     cy.get(this.domSelector.nodes_0_host).type(this.data.ip1);
@@ -98,8 +101,11 @@ context('Create PluginTemplate Binding To Route', () => {
     cy.visit('/routes/list');
     cy.get(this.domSelector.nameSelector).type(this.data.routeName);
     cy.contains('Search').click();
-    cy.contains(this.data.routeName).siblings().contains('Delete').click();
-    cy.contains('button', 'Confirm').click();
+    cy.contains(this.data.routeName).siblings().contains('More').click();
+    cy.contains('Delete').should('be.visible').click();
+    cy.get('.ant-modal-content').should('be.visible').within(() => {
+      cy.contains('OK').click();
+    });
     cy.get(this.domSelector.notification).should('contain', 
this.data.deleteRouteSuccess);
   });
 });
diff --git a/web/cypress/integration/rawDataEditor/test-rawDataEditor.spec.js 
b/web/cypress/integration/rawDataEditor/test-rawDataEditor.spec.js
index 3c80397..31d5dc8 100644
--- a/web/cypress/integration/rawDataEditor/test-rawDataEditor.spec.js
+++ b/web/cypress/integration/rawDataEditor/test-rawDataEditor.spec.js
@@ -35,7 +35,14 @@ context('Test RawDataEditor', () => {
     menuList.forEach(function (item) {
       cy.visit('/');
       cy.contains(item).click();
-      cy.contains('Raw Data Editor').click();
+      cy.get('.anticon-reload').click();
+      if (item === 'Route') {
+        cy.contains('Advanced').should('be.visible').click({ force: true });
+        cy.contains('Raw Data Editor').should('be.visible').click();
+      } else {
+        cy.contains('Raw Data Editor').should('be.visible').click();
+      }
+
       const data = dateset[item];
 
       cy.window().then(({ codemirror }) => {
@@ -52,11 +59,21 @@ context('Test RawDataEditor', () => {
       });
 
       cy.reload();
-      // update with editor
-      cy.contains(item === 'Consumer' ? data.username : data.name)
-        .siblings()
-        .contains('View')
-        .click();
+      if (item === 'Route') {
+        // update with editor
+        cy.contains(item === 'Consumer' ? data.username : data.name)
+          .siblings()
+          .contains('More')
+          .click();
+
+        cy.contains('View').should('be.visible').click({ force: true });
+      } else {
+        // update with editor
+        cy.contains(item === 'Consumer' ? data.username : data.name)
+          .siblings()
+          .contains('View')
+          .click();
+      };
 
       cy.window().then(({ codemirror }) => {
         if (codemirror) {
@@ -75,15 +92,29 @@ context('Test RawDataEditor', () => {
         });
       });
 
-      cy.reload();
-      cy.get(domSelector.tableBody).should('contain', item === 'Consumer' ? 
'newDesc' : 'newName');
+      if (item === 'Route') {
+        cy.reload();
+        cy.get(domSelector.tableBody).should('contain', item === 'Consumer' ? 
'newDesc' : 'newName');
+
+        cy.contains(item === 'Consumer' ? 'newDesc' : 'newName')
+          .siblings()
+          .contains('More')
+          .click();
+
+        cy.contains('Delete').should('be.visible').click();
+        cy.get('.ant-modal-content').should('be.visible').within(() => {
+          cy.contains('OK').click();
+        });
+      } else {
+        cy.reload();
+        cy.get(domSelector.tableBody).should('contain', item === 'Consumer' ? 
'newDesc' : 'newName');
 
-      // delete resource
-      cy.contains(item === 'Consumer' ? 'newDesc' : 'newName')
-        .siblings()
-        .contains('Delete')
-        .click();
-      cy.contains('button', 'Confirm').click();
+        cy.contains(item === 'Consumer' ? 'newDesc' : 'newName')
+          .siblings()
+          .contains('Delete')
+          .click();
+        cy.contains('button', 'Confirm').click();
+      }
 
       cy.get(domSelector.notification).should('contain', 
publicData[`delete${item}Success`]);
       cy.get(domSelector.notificationClose).should('be.visible').click({
diff --git 
a/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
 
b/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
index 9ec4c79..0ca5d28 100644
--- 
a/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
+++ 
b/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
@@ -55,12 +55,14 @@ context('Can select service_id skip upstream in route', () 
=> {
     cy.contains('Create').click();
 
     // The None option doesn't exist when service isn't selected
+    cy.contains('Next').click().click();
     cy.get(this.domSelector.name).type(this.data.routeName);
     cy.contains('Next').click();
     cy.get(this.domSelector.upstreamSelector).click();
     cy.contains('None').should('not.exist');
 
     cy.contains('Previous').click();
+    cy.wait(500);
     cy.contains('None').click();
     cy.contains(this.data.serviceName).click();
     cy.contains('Next').click();
@@ -106,8 +108,11 @@ context('Can select service_id skip upstream in route', () 
=> {
 
     cy.visit('/');
     cy.contains('Route').click();
-    cy.contains(this.data.routeName).siblings().contains('Delete').click();
-    cy.contains('button', 'Confirm').click();
+    cy.contains(this.data.routeName).siblings().contains('More').click();
+    cy.contains('Delete').click();
+    cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => {
+      cy.contains('OK').click();
+    });
     cy.get(this.domSelector.notification).should('contain', 
this.data.deleteRouteSuccess);
 
     cy.visit('/');
diff --git 
a/web/cypress/integration/route/create-edit-duplicate-delete-route.spec.js 
b/web/cypress/integration/route/create-edit-duplicate-delete-route.spec.js
index 048d489..3333832 100644
--- a/web/cypress/integration/route/create-edit-duplicate-delete-route.spec.js
+++ b/web/cypress/integration/route/create-edit-duplicate-delete-route.spec.js
@@ -35,6 +35,7 @@ context('Create and Delete Route', () => {
     cy.contains('Route').click();
     cy.get(this.domSelector.empty).should('be.visible');
     cy.contains('Create').click();
+    cy.contains('Next').click().click();
     cy.get(this.domSelector.name).type(name);
     cy.get(this.domSelector.description).type(this.data.description);
 
@@ -48,7 +49,7 @@ context('Create and Delete Route', () => {
     cy.contains('Advanced Routing Matching Conditions')
       .parent()
       .siblings()
-      .contains('Create')
+      .contains('Add')
       .click();
 
     // create advanced routing matching conditions
@@ -120,7 +121,8 @@ context('Create and Delete Route', () => {
 
     cy.get(this.domSelector.nameSelector).type(name);
     cy.contains('Search').click();
-    cy.contains(name).siblings().contains('View').click();
+    cy.contains(name).siblings().contains('More').click();
+    cy.contains('View').click();
     cy.get(this.domSelector.drawer).should('be.visible');
 
     cy.get(this.domSelector.codemirrorScroll).within(() => {
@@ -137,6 +139,7 @@ context('Create and Delete Route', () => {
     cy.contains('Search').click();
     cy.contains(name).siblings().contains('Configure').click();
 
+    cy.wait(500);
     cy.get(this.domSelector.name).clear().type(newName);
     cy.get(this.domSelector.description).clear().type(this.data.description2);
     cy.contains('Next').click();
@@ -149,7 +152,8 @@ context('Create and Delete Route', () => {
     cy.contains(newName).siblings().should('contain', this.data.description2);
 
     // test view
-    cy.contains(newName).siblings().contains('View').click();
+    cy.contains(newName).siblings().contains('More').click();
+    cy.contains('View').click();
     cy.get(this.domSelector.drawer).should('be.visible');
 
     cy.get(this.domSelector.codemirrorScroll).within(() => {
@@ -165,8 +169,10 @@ context('Create and Delete Route', () => {
 
     cy.get(this.domSelector.nameSelector).type(newName);
     cy.contains('Search').click();
-    cy.contains(newName).siblings().contains('Duplicate').click();
+    cy.contains(newName).siblings().contains('More').click();
+    cy.contains('Duplicate').click();
 
+    cy.wait(500);
     cy.get(this.domSelector.name).clear().type(duplicateNewName);
     cy.get(this.domSelector.description).clear().type(this.data.description2);
     cy.contains('Next').click();
@@ -179,7 +185,8 @@ context('Create and Delete Route', () => {
     cy.contains(duplicateNewName).siblings().should('contain', 
this.data.description2);
 
     // test view
-    cy.contains(duplicateNewName).siblings().contains('View').click();
+    cy.contains(duplicateNewName).siblings().contains('More').click();
+    cy.contains('View').click();
     cy.get(this.domSelector.drawer).should('be.visible');
 
     cy.get(this.domSelector.codemirrorScroll).within(() => {
@@ -195,9 +202,13 @@ context('Create and Delete Route', () => {
     routeNames.forEach(function (routeName) {
       cy.get(domSelector.name).clear().type(routeName);
       cy.contains('Search').click();
-      cy.contains(routeName).siblings().contains('Delete').click();
-      cy.contains('button', 'Confirm').click();
+      cy.contains(routeName).siblings().contains('More').click();
+      cy.contains('Delete').click();
+      cy.get(domSelector.deleteAlert).should('be.visible').within(() => {
+        cy.contains('OK').click();
+      });
       cy.get(domSelector.notification).should('contain', 
data.deleteRouteSuccess);
+      cy.get(domSelector.notificationCloseIcon).click();
     });
   });
 });
diff --git 
a/web/cypress/integration/route/create-route-with-proxy-rewrite-plugin.spec.js 
b/web/cypress/integration/route/create-route-with-proxy-rewrite-plugin.spec.js
index bba3288..b051fce 100644
--- 
a/web/cypress/integration/route/create-route-with-proxy-rewrite-plugin.spec.js
+++ 
b/web/cypress/integration/route/create-route-with-proxy-rewrite-plugin.spec.js
@@ -56,6 +56,7 @@ context('create route with proxy-rewrite plugin', () => {
 
     // show create page
     cy.contains(componentLocaleUS['component.global.create']).click();
+    cy.contains('Next').click().click();
     cy.get(this.domSelector.name).type(this.data.routeName);
 
     // show requestOverride PanelSection
@@ -105,6 +106,7 @@ context('create route with proxy-rewrite plugin', () => {
     cy.get(this.domSelector.nameSelector).type(this.data.routeName);
     cy.contains('Search').click();
     cy.contains(this.data.routeName).siblings().contains('Configure').click();
+    cy.wait(500);
     cy.get(this.domSelector.name).type(this.data.routeName);
 
     
cy.contains(routeLocaleUS['page.route.form.itemLabel.newPath']).should('be.visible');
@@ -132,8 +134,11 @@ context('create route with proxy-rewrite plugin', () => {
     cy.visit('/routes/list');
     cy.get(this.domSelector.nameSelector).type(this.data.routeName);
     cy.contains('Search').click();
-    cy.contains(this.data.routeName).siblings().contains('Delete').click();
-    cy.contains('button', 'Confirm').click();
+    cy.contains(this.data.routeName).siblings().contains('More').click();
+    cy.contains('Delete').click();
+    cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => {
+      cy.contains('OK').click();
+    });
     cy.get(this.domSelector.notification).should('contain', 
this.data.deleteRouteSuccess);
   });
 });
diff --git a/web/cypress/integration/route/create-route-with-upstream.spec.js 
b/web/cypress/integration/route/create-route-with-upstream.spec.js
index 0a6f27a..2c4beb7 100644
--- a/web/cypress/integration/route/create-route-with-upstream.spec.js
+++ b/web/cypress/integration/route/create-route-with-upstream.spec.js
@@ -41,6 +41,7 @@ context('Create Route with Upstream', () => {
     cy.contains('Route').click();
     cy.contains('Create').click();
 
+    cy.contains('Next').click().click();
     cy.get(this.domSelector.name).type(this.data.routeName);
     cy.contains('Next').click();
     // should disable Upstream input boxes after selecting an existing upstream
@@ -106,8 +107,11 @@ context('Create Route with Upstream', () => {
     cy.visit('/routes/list');
     cy.get(this.domSelector.nameSelector).type(this.data.routeName);
     cy.contains('Search').click();
-    cy.contains(this.data.routeName).siblings().contains('Delete').click();
-    cy.contains('button', 'Confirm').click();
+    cy.contains(this.data.routeName).siblings().contains('More').click();
+    cy.contains('Delete').click();
+    cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => {
+      cy.contains('OK').click();
+    });
     cy.get(this.domSelector.notification).should('contain', 
this.data.deleteRouteSuccess);
 
     cy.visit('/');
diff --git a/web/cypress/integration/route/import_export_route.spec.js 
b/web/cypress/integration/route/import_export_route.spec.js
index 791cab7..c327c58 100644
--- a/web/cypress/integration/route/import_export_route.spec.js
+++ b/web/cypress/integration/route/import_export_route.spec.js
@@ -53,6 +53,7 @@ context('import and export routes', () => {
       cy.contains(menuLocaleUS['menu.routes']).click();
       cy.contains(componentLocaleUS['component.global.create']).click();
       // input name, click Next
+      cy.contains('Next').click().click();
       cy.get(this.domSelector.name).type(data[`route_name_${i}`]);
       //FIXME: only GET in methods
       cy.get('#methods').click();
@@ -127,8 +128,11 @@ context('import and export routes', () => {
     cy.get(this.domSelector.refresh).click();
 
     for (let i = 0; i < 2; i += 1) {
-      
cy.contains(data[`route_name_${i}`]).siblings().contains('Delete').click();
-      cy.contains('button', 'Confirm').click();
+      cy.contains(data[`route_name_${i}`]).siblings().contains('More').click();
+      cy.contains('Delete').click();
+      cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => {
+        cy.contains('OK').click();
+      });
       cy.get(this.domSelector.notification).should('contain', 
this.data.deleteRouteSuccess);
       
cy.get(this.domSelector.notificationCloseIcon).click().should('not.exist');
       cy.reload();
@@ -141,7 +145,9 @@ context('import and export routes', () => {
 
     data.uploadRouteFiles.forEach((file) => {
       // click import button
-      cy.contains(routeLocaleUS['page.route.button.importOpenApi']).click();
+      cy.get(this.domSelector.refresh).click();
+      cy.contains('Advanced').click();
+      
cy.contains(routeLocaleUS['page.route.button.importOpenApi']).should('be.visible').click();
       // select file
       cy.get(this.domSelector.fileSelector).attachFile(file);
       // click submit
@@ -159,9 +165,11 @@ context('import and export routes', () => {
         
cy.get(this.domSelector.notificationCloseIcon).click().should('not.exist');
         // delete route just imported
         cy.reload();
-        cy.get(this.domSelector.deleteButton).should('exist').click();
-        cy.contains('button', 
componentLocaleUS['component.global.confirm']).click({ force: true });
-
+        cy.contains('More').click();
+        cy.contains('Delete').should('be.visible').click();
+        cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => 
{
+          cy.contains('OK').click();
+        });
         // show delete successfully notification
         cy.get(this.domSelector.notification).should('contain', 
this.data.deleteRouteSuccess);
         cy.get(this.domSelector.notificationCloseIcon).click();
diff --git a/web/cypress/integration/route/online-debug.spec.js 
b/web/cypress/integration/route/online-debug.spec.js
index 3c32c02..5f80575 100644
--- a/web/cypress/integration/route/online-debug.spec.js
+++ b/web/cypress/integration/route/online-debug.spec.js
@@ -82,6 +82,8 @@ context('Online debug', () => {
     cy.contains(menuLocaleUS['menu.routes']).click();
 
     // show online debug draw
+    cy.get(this.domSelector.refresh).click();
+    cy.contains('Advanced').click();
     cy.contains(routeLocaleUS['page.route.onlineDebug']).click();
     cy.get(domSelector.debugDraw).should('be.visible');
     // input uri with specified special characters
@@ -100,6 +102,8 @@ context('Online debug', () => {
     cy.contains(menuLocaleUS['menu.routes']).click();
 
     // show online debug draw
+    cy.get(this.domSelector.refresh).click();
+    cy.contains('Advanced').click();
     cy.contains(routeLocaleUS['page.route.onlineDebug']).click();
     cy.get(domSelector.debugDraw).should('be.visible');
 
@@ -126,6 +130,8 @@ context('Online debug', () => {
     const currentToken = localStorage.getItem('token');
 
     // show online debug draw
+    cy.get(this.domSelector.refresh).click();
+    cy.contains('Advanced').click();
     cy.contains(routeLocaleUS['page.route.onlineDebug']).click();
     cy.get(domSelector.debugDraw).should('be.visible');
 
@@ -178,6 +184,8 @@ context('Online debug', () => {
     const currentToken = localStorage.getItem('token');
 
     // show online debug draw
+    cy.get(this.domSelector.refresh).click();
+    cy.contains('Advanced').click();
     cy.contains(routeLocaleUS['page.route.onlineDebug']).click();
     cy.get(domSelector.debugDraw).should('be.visible');
     // set debug uri
@@ -215,6 +223,8 @@ context('Online debug', () => {
     const currentToken = localStorage.getItem('token');
 
     // show online debug draw
+    cy.get(this.domSelector.refresh).click();
+    cy.contains('Advanced').click();
     cy.contains(routeLocaleUS['page.route.onlineDebug']).click();
     cy.get(domSelector.debugDraw).should('be.visible');
 
@@ -266,10 +276,14 @@ context('Online debug', () => {
 
     const testRouteNames = [data.routeName, this.routeData.debugPostJson.name];
     for( let routeName in testRouteNames) {
-      
cy.contains(`${testRouteNames[routeName]}`).siblings().contains('Delete').click();
-      cy.contains('button', 'Confirm').click();
+      
cy.contains(`${testRouteNames[routeName]}`).siblings().contains('More').click();
+      cy.contains('Delete').click({ force: true });
+      cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => {
+        cy.contains('OK').click();
+      });
       cy.get(this.domSelector.notification).should('contain', 
this.data.deleteRouteSuccess);
       cy.get(this.domSelector.notificationCloseIcon).click();
+      cy.reload();
     }
   });
 });
diff --git a/web/cypress/integration/route/search-route.spec.js 
b/web/cypress/integration/route/search-route.spec.js
index f2235b6..aed1837 100644
--- a/web/cypress/integration/route/search-route.spec.js
+++ b/web/cypress/integration/route/search-route.spec.js
@@ -43,6 +43,7 @@ context('Create and Search Route', () => {
     cy.contains('Route').click();
     for (let i = 0; i < 3; i += 1) {
       cy.contains('Create').click();
+      cy.contains('Next').click().click();
       cy.get(this.domSelector.name).type(`test${i}`);
       cy.get(this.domSelector.description).type(`desc${i}`);
       cy.get(this.domSelector.hosts_0).type(this.data.host1);
@@ -111,8 +112,11 @@ context('Create and Search Route', () => {
   it('should delete the route', function () {
     cy.visit('/routes/list');
     for (let i = 0; i < 3; i += 1) {
-      cy.contains(`test${i}`).siblings().contains('Delete').click({ timeout });
-      cy.contains('button', 'Confirm').should('be.visible').click({ timeout });
+      cy.contains(`test${i}`).siblings().contains('More').click({ timeout });
+      cy.contains('Delete').should('be.visible').click({ timeout });
+      cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => {
+        cy.contains('OK').click();
+      });
       cy.get(this.domSelector.notification).should('contain', 
this.data.deleteRouteSuccess);
       cy.get(this.domSelector.notificationClose).should('be.visible').click({
         force: true,
diff --git a/web/src/components/LabelsfDrawer/LabelsDrawer.tsx 
b/web/src/components/LabelsfDrawer/LabelsDrawer.tsx
index 33f5880..9f6ff5b 100644
--- a/web/src/components/LabelsfDrawer/LabelsDrawer.tsx
+++ b/web/src/components/LabelsfDrawer/LabelsDrawer.tsx
@@ -113,7 +113,7 @@ const LabelList = (disabled: boolean, labelList: LabelList, 
filterList: string[]
 };
 
 const LabelsDrawer: React.FC<Props> = ({
-  title = 'Label Manager',
+  title = "",
   actionName = '',
   disabled = false,
   dataSource = [],
@@ -135,7 +135,7 @@ const LabelsDrawer: React.FC<Props> = ({
 
   return (
     <Drawer
-      title={title}
+      title={title || formatMessage({ id: "component.label-manager" })}
       placement="right"
       width={512}
       visible
diff --git a/web/src/locales/en-US/component.ts 
b/web/src/locales/en-US/component.ts
index 53e9496..06027ad 100644
--- a/web/src/locales/en-US/component.ts
+++ b/web/src/locales/en-US/component.ts
@@ -67,12 +67,13 @@ export default {
   'component.global.name': 'Name',
   'component.global.updateTime': 'UpdateAt',
   'component.global.form.itemExtraMessage.nameGloballyUnique': 'Name should be 
globally unique',
-  'component.global.input.placeholder.description': 'Can not more than 256 
characters',
+  'component.global.input.placeholder.description': 'Please enter the 
description for this route, max 256 characters',
   // User component
   'component.user.loginByPassword': 'Username & Password',
   'component.user.login': 'Login',
 
   'component.document': 'Document',
+  'component.label-manager': 'Label Manager',
 
   'component.global.noConfigurationRequired': 'No configuration required',
 };
diff --git a/web/src/locales/en-US/menu.ts b/web/src/locales/en-US/menu.ts
index 30ec1e0..e7389b9 100644
--- a/web/src/locales/en-US/menu.ts
+++ b/web/src/locales/en-US/menu.ts
@@ -72,4 +72,6 @@ export default {
   'menu.service': 'Service',
   'menu.setting': 'Settings',
   'menu.serverinfo': 'System Info',
+  'menu.advanced-feature': 'Advanced',
+  'menu.more': 'More'
 };
diff --git a/web/src/locales/zh-CN/component.ts 
b/web/src/locales/zh-CN/component.ts
index 12b29e1..0319cd1 100644
--- a/web/src/locales/zh-CN/component.ts
+++ b/web/src/locales/zh-CN/component.ts
@@ -41,9 +41,9 @@ export default {
   'component.global.edit.plugin': '配置插件',
   'component.global.loading': '加载中',
   'component.global.list': '列表',
-  'component.global.description': '描述',
+  'component.global.description': '描述信息',
   'component.global.labels': '标签',
-  'component.global.version': '版本',
+  'component.global.version': '路由版本',
   'component.global.operation': '操作',
   'component.status.success': '成功',
   'component.status.fail': '失败',
@@ -62,13 +62,14 @@ export default {
   'component.global.steps.stepTitle.pluginConfig': '插件配置',
   'component.global.input.ruleMessage.name': '仅支持字母、数字、- 和 _,且只能以字母开头',
   'component.global.form.itemExtraMessage.nameGloballyUnique': '名称需全局唯一',
-  'component.global.input.placeholder.description': '不超过 256 个字符',
+  'component.global.input.placeholder.description': '请输入路由描述(内容不超过 256 个字符)',
 
   // User component
   'component.user.loginByPassword': '账号密码登录',
   'component.user.login': '登录',
 
   'component.document': '操作手册',
+  'component.label-manager': '标签管理器',
 
   'component.global.noConfigurationRequired': '无需配置',
 };
diff --git a/web/src/locales/zh-CN/menu.ts b/web/src/locales/zh-CN/menu.ts
index 2a5d930..d86b7f6 100644
--- a/web/src/locales/zh-CN/menu.ts
+++ b/web/src/locales/zh-CN/menu.ts
@@ -69,4 +69,6 @@ export default {
   'menu.service': '服务',
   'menu.setting': '系统设置',
   'menu.serverinfo': '系统信息',
+  'menu.advanced-feature': '高级特性',
+  'menu.more': '更多'
 };
diff --git a/web/src/pages/PluginTemplate/components/Step1.tsx 
b/web/src/pages/PluginTemplate/components/Step1.tsx
index dbeea66..6e46b77 100644
--- a/web/src/pages/PluginTemplate/components/Step1.tsx
+++ b/web/src/pages/PluginTemplate/components/Step1.tsx
@@ -42,7 +42,6 @@ const Step1: React.FC<Props> = ({ form, disabled }) => {
 
   const NormalLabelComponent = () => {
     const field = 'custom_normal_labels';
-    const title = 'Label Manager';
     return (
       <React.Fragment>
         <Form.Item label={formatMessage({ id: 'component.global.labels' })} 
name={field}>
@@ -74,7 +73,7 @@ const Step1: React.FC<Props> = ({ form, disabled }) => {
               const labels = form.getFieldValue(field) || [];
               return (
                 <LabelsDrawer
-                  title={title}
+                  title={formatMessage({ id: "component.label-manager" })}
                   actionName={field}
                   dataSource={labels}
                   disabled={disabled || false}
diff --git a/web/src/pages/Route/List.tsx b/web/src/pages/Route/List.tsx
index 1598462..c494478 100644
--- a/web/src/pages/Route/List.tsx
+++ b/web/src/pages/Route/List.tsx
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import type { ReactNode } from 'react';
 import React, { useRef, useEffect, useState } from 'react';
 import { PageHeaderWrapper } from '@ant-design/pro-layout';
 import ProTable from '@ant-design/pro-table';
@@ -30,9 +31,11 @@ import {
   Upload,
   Modal,
   Divider,
+  Menu,
+  Dropdown,
 } from 'antd';
 import { history, useIntl } from 'umi';
-import { PlusOutlined, BugOutlined, ExportOutlined, ImportOutlined } from 
'@ant-design/icons';
+import { PlusOutlined, BugOutlined, ExportOutlined, ImportOutlined, 
DownOutlined } from '@ant-design/icons';
 import { js_beautify } from 'js-beautify';
 import yaml from 'js-yaml';
 import moment from 'moment';
@@ -83,6 +86,7 @@ const Page: React.FC = () => {
   const [id, setId] = useState('');
   const [editorMode, setEditorMode] = useState<'create' | 'update'>('create');
   const [paginationConfig, setPaginationConfig] = useState({ pageSize: 10, 
current: 1 });
+  const [debugDrawVisible, setDebugDrawVisible] = useState(false);
 
   const savePageList = (page = 1, pageSize = 10) => {
     history.replace(`/routes/list?page=${page}&pageSize=${pageSize}`);
@@ -97,7 +101,7 @@ const Page: React.FC = () => {
     setPaginationConfig({ pageSize: Number(pageSize), current: Number(page) });
   }, [window.location.search]);
 
-  const rowSelection = {
+  const rowSelection: any = {
     selectedRowKeys,
     onChange: (currentSelectKeys: string[]) => {
       setSelectedRowKeys(currentSelectKeys);
@@ -177,6 +181,122 @@ const Page: React.FC = () => {
     });
   };
 
+  const ListToolbar = () => {
+    const tools = [
+      {
+        name: formatMessage({ id: 'page.route.pluginTemplateConfig' }),
+        icon: <PlusOutlined />,
+        onClick: () => {
+          history.push('/plugin-template/list')
+        }
+      }, {
+        name: formatMessage({ id: 'component.global.data.editor' }),
+        icon: <PlusOutlined />,
+        onClick: () => {
+          setVisible(true);
+          setEditorMode('create');
+          setRawData({});
+        }
+      }, {
+        name: formatMessage({ id: 'page.route.button.importOpenApi' }),
+        icon: <ImportOutlined />,
+        onClick: () => {
+          setUploadFileList([]);
+          setShowImportModal(true);
+        }
+      }, {
+        name: formatMessage({ id: 'page.route.onlineDebug' }),
+        icon: <BugOutlined />,
+        onClick: () => {
+          setDebugDrawVisible(true)
+        }
+      }
+    ]
+
+    return (
+      <Dropdown overlay={<Menu>
+        {
+          tools.map(item => (
+            <Menu.Item key={item.name} onClick={item.onClick}>
+              {item.icon}
+              {item.name}
+            </Menu.Item>
+          ))
+        }
+      </Menu>}>
+        <Button type="dashed">
+          <DownOutlined /> {formatMessage({ id: "menu.advanced-feature" })}
+        </Button>
+      </Dropdown>
+    )
+  }
+
+  const RecordActionDropdown: React.FC<{ record: any }> = ({ record }) => {
+    const tools: {
+      name: string;
+      onClick: () => void;
+      icon?: ReactNode;
+    }[] = [
+        {
+          name: formatMessage({ id: 'component.global.view' }),
+          onClick: () => {
+            setId(record.id);
+            setRawData(omit(record, DELETE_FIELDS));
+            setVisible(true);
+            setEditorMode('update');
+          }
+        }, {
+          name: formatMessage({ id: 'component.global.duplicate' }),
+          onClick: () => {
+            history.push(`/routes/${record.id}/duplicate`)
+          }
+        }, {
+          name: formatMessage({ id: 'component.global.delete' }),
+          onClick: () => {
+            Modal.confirm({
+              type: "warning",
+              title: formatMessage({ id: 
'component.global.popconfirm.title.delete' }),
+              content: (
+                <>
+                  {formatMessage({ id: 'component.global.name' })} - 
{record.name}<br />
+                  ID - {record.id}
+                </>
+              ),
+              onOk: () => {
+                remove(record.id!).then(() => {
+                  handleTableActionSuccessResponse(
+                    `${formatMessage({ id: 'component.global.delete' })} 
${formatMessage({
+                      id: 'menu.routes',
+                    })} ${formatMessage({ id: 'component.status.success' })}`,
+                  );
+                });
+              }
+            })
+          }
+        }
+      ]
+
+    return (
+      <Dropdown overlay={
+        <Menu>
+          {
+            tools.map(item => (
+              <Menu.Item key={item.name} onClick={item.onClick}>
+                {item.icon && item.icon}
+                {item.name}
+              </Menu.Item>
+            ))
+          }
+        </Menu>
+      }>
+        <Button type="dashed">
+          <DownOutlined />
+          {formatMessage({ id: "menu.more" })}
+        </Button>
+      </Dropdown>
+    )
+  }
+
   const ListFooter: React.FC = () => {
     return (
       <Popconfirm
@@ -208,15 +328,13 @@ const Page: React.FC = () => {
     );
   };
 
-  const [debugDrawVisible, setDebugDrawVisible] = useState(false);
-
   const columns: ProColumns<RouteModule.ResponseBody>[] = [
     {
       title: formatMessage({ id: 'component.global.name' }),
       dataIndex: 'name',
     },
     {
-      title: formatMessage({ id: 'page.route.domainName' }),
+      title: formatMessage({ id: 'page.route.host' }),
       hideInSearch: true,
       render: (_, record) => {
         const list = record.hosts || (record.host && [record.host]) || [];
@@ -409,38 +527,7 @@ const Page: React.FC = () => {
             <Button type="primary" onClick={() => 
history.push(`/routes/${record.id}/edit`)}>
               {formatMessage({ id: 'component.global.edit' })}
             </Button>
-            <Button type="primary" onClick={() => {
-              setId(record.id);
-              setRawData(omit(record, DELETE_FIELDS));
-              setVisible(true);
-              setEditorMode('update');
-            }}>
-              {formatMessage({ id: 'component.global.view' })}
-            </Button>
-            <Button type="primary" onClick={() => 
history.push(`/routes/${record.id}/duplicate`)}>
-              {formatMessage({ id: 'component.global.duplicate' })}
-            </Button>
-            <Popconfirm
-              title={formatMessage({ id: 
'component.global.popconfirm.title.delete' })}
-              onConfirm={() => {
-                remove(record.id!).then(() => {
-                  handleTableActionSuccessResponse(
-                    `${formatMessage({ id: 'component.global.delete' })} 
${formatMessage({
-                      id: 'menu.routes',
-                    })} ${formatMessage({ id: 'component.status.success' })}`,
-                  );
-                });
-              }}
-              okButtonProps={{
-                danger: true,
-              }}
-              okText={formatMessage({ id: 'component.global.confirm' })}
-              cancelText={formatMessage({ id: 'component.global.cancel' })}
-            >
-              <Button type="primary" danger>
-                {formatMessage({ id: 'component.global.delete' })}
-              </Button>
-            </Popconfirm>
+            <RecordActionDropdown record={record} />
           </Space>
         </>
       ),
@@ -448,7 +535,7 @@ const Page: React.FC = () => {
   ];
 
   return (
-    <PageHeaderWrapper title={formatMessage({ id: 'page.route.list' })}>
+    <PageHeaderWrapper title={formatMessage({ id: 'page.route.list' })} 
content={formatMessage({ id: 'page.route.list.description' })}>
       <ProTable<RouteModule.ResponseBody>
         actionRef={ref}
         rowKey="id"
@@ -464,36 +551,11 @@ const Page: React.FC = () => {
           resetText: formatMessage({ id: 'component.global.reset' }),
         }}
         toolBarRender={() => [
-          <Button type="primary" onClick={() => { 
history.push('/plugin-template/list') }}>
-            <PlusOutlined />
-            {formatMessage({ id: 'page.route.pluginTemplateConfig' })}
-          </Button>,
           <Button type="primary" onClick={() => 
history.push(`/routes/create`)}>
             <PlusOutlined />
             {formatMessage({ id: 'component.global.create' })}
           </Button>,
-          <Button type="primary" onClick={() => {
-            setVisible(true);
-            setEditorMode('create');
-            setRawData({});
-          }}>
-            <PlusOutlined />
-            {formatMessage({ id: 'component.global.data.editor' })}
-          </Button>,
-          <Button
-            type="primary"
-            onClick={() => {
-              setUploadFileList([]);
-              setShowImportModal(true);
-            }}
-          >
-            <ImportOutlined />
-            {formatMessage({ id: 'page.route.button.importOpenApi' })}
-          </Button>,
-          <Button type="primary" onClick={() => setDebugDrawVisible(true)}>
-            <BugOutlined />
-            {formatMessage({ id: 'page.route.onlineDebug' })}
-          </Button>,
+          <ListToolbar />
         ]}
         rowSelection={rowSelection}
         footer={() => <ListFooter />}
@@ -529,7 +591,7 @@ const Page: React.FC = () => {
         }}
       >
         <Upload
-          fileList={uploadFileList}
+          fileList={uploadFileList as any}
           beforeUpload={(file) => {
             setUploadFileList([file]);
             return false;
diff --git a/web/src/pages/Route/components/Step1/MatchingRulesView.tsx 
b/web/src/pages/Route/components/Step1/MatchingRulesView.tsx
index 7216bee..1b01077 100644
--- a/web/src/pages/Route/components/Step1/MatchingRulesView.tsx
+++ b/web/src/pages/Route/components/Step1/MatchingRulesView.tsx
@@ -22,7 +22,7 @@ import { PanelSection } from '@api7-dashboard/ui';
 const MatchingRulesView: React.FC<RouteModule.Step1PassProps> = ({
   advancedMatchingRules,
   disabled,
-  onChange = () => {},
+  onChange = () => { },
 }) => {
   const [visible, setVisible] = useState(false);
   const [mode, setMode] = useState<RouteModule.ModalType>('CREATE');
@@ -139,28 +139,27 @@ const MatchingRulesView: 
React.FC<RouteModule.Step1PassProps> = ({
     disabled
       ? {}
       : {
-          title: formatMessage({ id: 'component.global.operation' }),
-          key: 'action',
-          render: (_: any, record: RouteModule.MatchingRule) => (
-            <Space size="middle">
-              <a onClick={() => handleEdit(record)}>
-                {formatMessage({ id: 'component.global.edit' })}
-              </a>
-              <a onClick={() => handleRemove(record.key)}>
-                {formatMessage({ id: 'component.global.delete' })}
-              </a>
-            </Space>
-          ),
-        },
+        title: formatMessage({ id: 'component.global.operation' }),
+        key: 'action',
+        render: (_: any, record: RouteModule.MatchingRule) => (
+          <Space size="middle">
+            <a onClick={() => handleEdit(record)}>
+              {formatMessage({ id: 'component.global.edit' })}
+            </a>
+            <a onClick={() => handleRemove(record.key)}>
+              {formatMessage({ id: 'component.global.delete' })}
+            </a>
+          </Space>
+        ),
+      },
   ].filter((item) => Object.keys(item).length);
 
   const renderModal = () => (
     <Modal
-      title={`${
-        mode === 'EDIT'
+      title={`${mode === 'EDIT'
           ? formatMessage({ id: 'component.global.edit' })
           : formatMessage({ id: 'component.global.create' })
-      } ${formatMessage({ id: 'page.route.rule' })}`}
+        } ${formatMessage({ id: 'page.route.rule' })}`}
       centered
       visible
       onOk={onOk}
@@ -277,7 +276,7 @@ const MatchingRulesView: 
React.FC<RouteModule.Step1PassProps> = ({
             marginBottom: 16,
           }}
         >
-          {formatMessage({ id: 'component.global.create' })}
+          {formatMessage({ id: 'component.global.add' })}
         </Button>
       )}
       <Table key="table" bordered dataSource={advancedMatchingRules} 
columns={columns} />
diff --git a/web/src/pages/Route/components/Step1/MetaView.tsx 
b/web/src/pages/Route/components/Step1/MetaView.tsx
index 8bfb2e1..d691d8f 100644
--- a/web/src/pages/Route/components/Step1/MetaView.tsx
+++ b/web/src/pages/Route/components/Step1/MetaView.tsx
@@ -16,31 +16,31 @@
  */
 import React, { useEffect, useState } from 'react';
 import Form from 'antd/es/form';
-import { Input, Switch, Select, Button, Tag, AutoComplete } from 'antd';
+import { Input, Switch, Select, Button, Tag, AutoComplete, Row, Col } from 
'antd';
 import { useIntl } from 'umi';
 import { PanelSection } from '@api7-dashboard/ui';
 
 import { FORM_ITEM_WITHOUT_LABEL } from '@/pages/Route/constants';
 import LabelsDrawer from '@/components/LabelsfDrawer';
-import { fetchLabelList } from '../../service';
+import { fetchLabelList, fetchServiceList } from '../../service';
 
-const MetaView: React.FC<RouteModule.Step1PassProps> = ({ disabled, form, 
isEdit, onChange }) => {
+const MetaView: React.FC<RouteModule.Step1PassProps> = ({ disabled, form, 
isEdit, onChange = () => { } }) => {
   const { formatMessage } = useIntl();
   const [visible, setVisible] = useState(false);
   const [labelList, setLabelList] = useState<LabelList>({});
+  const [serviceList, setServiceList] = 
useState<ServiceModule.ResponseBody[]>([]);
 
   useEffect(() => {
-    // TODO: use a better state name
     fetchLabelList().then(setLabelList);
+    fetchServiceList().then(({ data }) => setServiceList(data));
   }, []);
 
   const NormalLabelComponent = () => {
     const field = 'custom_normal_labels';
-    const title = 'Label Manager';
 
     return (
       <React.Fragment>
-        <Form.Item label={formatMessage({ id: 'component.global.labels' })} 
name={field}>
+        <Form.Item label={formatMessage({ id: 'component.global.labels' })} 
name={field} tooltip={formatMessage({ id: 
'page.route.configuration.normal-labels.tooltip' })}>
           <Select
             mode="tags"
             style={{ width: '100%' }}
@@ -69,7 +69,7 @@ const MetaView: React.FC<RouteModule.Step1PassProps> = ({ 
disabled, form, isEdit
               const labels = form.getFieldValue(field) || [];
               return (
                 <LabelsDrawer
-                  title={title}
+                  title={formatMessage({ id: "component.label-manager" })}
                   actionName={field}
                   dataSource={labels}
                   disabled={disabled || false}
@@ -88,66 +88,241 @@ const MetaView: React.FC<RouteModule.Step1PassProps> = ({ 
disabled, form, isEdit
 
   const VersionLabelComponent = () => {
     return (
-      <React.Fragment>
-        <Form.Item
-          label={formatMessage({ id: 'component.global.version' })}
-          name="custom_version_label"
-        >
-          <AutoComplete
-            options={(labelList.API_VERSION || []).map((item) => ({ value: 
item }))}
-            disabled={disabled}
-          />
-        </Form.Item>
-      </React.Fragment>
+      <Form.Item
+        label={formatMessage({ id: 'component.global.version' })} 
tooltip={formatMessage({ id: "page.route.configuration.version.tooltip" })}>
+        <Row>
+          <Col span={10}>
+            <Form.Item
+              noStyle
+              name="custom_version_label"
+            >
+              <AutoComplete
+                options={(labelList.API_VERSION || []).map((item) => ({ value: 
item }))}
+                disabled={disabled}
+                placeholder={formatMessage({ id: 
"page.route.configuration.version.placeholder" })}
+              />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form.Item>
     );
   };
 
-  return (
-    <PanelSection title={formatMessage({ id: 
'page.route.panelSection.title.nameDescription' })}>
-      <Form.Item
-        label={formatMessage({ id: 'component.global.name' })}
-        name="name"
-        rules={[
-          {
-            required: true,
-            message: `${formatMessage({ id: 'component.global.pleaseEnter' })} 
${formatMessage({
-              id: 'page.route.form.itemLabel.apiName',
-            })}`,
-          },
-          {
-            pattern: new RegExp(/^[a-zA-Z][a-zA-Z0-9_-]{0,100}$/, 'g'),
-            message: formatMessage({ id: 
'page.route.form.itemRulesPatternMessage.apiNameRule' }),
-          },
-        ]}
-        extra={formatMessage({ id: 
'page.route.form.itemRulesPatternMessage.apiNameRule' })}
-      >
-        <Input
-          placeholder={`${formatMessage({ id: 'component.global.pleaseEnter' 
})} ${formatMessage({
-            id: 'page.route.form.itemLabel.apiName',
-          })}`}
-          disabled={disabled}
-        />
+  const Name: React.FC = () => (
+    <Form.Item label={formatMessage({ id: 'component.global.name' })} 
tooltip={formatMessage({ id: 
'page.route.form.itemRulesPatternMessage.apiNameRule' })}>
+      <Row>
+        <Col span={10}>
+          <Form.Item
+            noStyle
+            name="name"
+            rules={[
+              {
+                required: true,
+                message: formatMessage({ id: 
'page.route.configuration.name.rules.required.description' }),
+              },
+              {
+                pattern: new RegExp(/^[a-zA-Z][a-zA-Z0-9_-]{0,100}$/, 'g'),
+                message: formatMessage({ id: 
'page.route.form.itemRulesPatternMessage.apiNameRule' }),
+              },
+            ]}
+          >
+            <Input
+              placeholder={formatMessage({ id: 
'page.route.configuration.name.placeholder' })}
+              disabled={disabled}
+            />
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form.Item>
+  )
+
+  const Description: React.FC = () => (
+    <Form.Item label={formatMessage({ id: 'component.global.description' })} 
tooltip="路由描述信息">
+      <Row>
+        <Col span={10}>
+          <Form.Item noStyle name="desc">
+            <Input.TextArea
+              placeholder={formatMessage({ id: 
'component.global.input.placeholder.description' })}
+              disabled={disabled}
+              showCount
+              maxLength={256}
+            />
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form.Item>
+  )
+
+  const Publish: React.FC = () => (
+    <Form.Item label={formatMessage({ id: 'page.route.publish' })} 
tooltip={formatMessage({ id: 'page.route.configuration.publish.tooltip' })}>
+      <Row>
+        <Col>
+          <Form.Item
+            noStyle
+            name="status"
+            valuePropName="checked"
+          >
+            <Switch disabled={isEdit} />
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form.Item>
+  )
+
+  const WebSocket: React.FC = () => (
+    <Form.Item label="WebSocket">
+      <Row>
+        <Col>
+          <Form.Item noStyle valuePropName="checked" name="enable_websocket">
+            <Switch disabled={disabled} />
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form.Item>
+  )
+
+  const Redirect: React.FC = () => {
+    const list = [
+      {
+        value: "forceHttps",
+        label: formatMessage({ id: 'page.route.select.option.enableHttps' })
+      }, {
+        value: "customRedirect",
+        label: formatMessage({ id: 'page.route.select.option.configCustom' })
+      }, {
+        value: "disabled",
+        label: formatMessage({ id: 'page.route.select.option.forbidden' })
+      }
+    ]
+
+    return (
+      <Form.Item label={formatMessage({ id: 
'page.route.form.itemLabel.redirect' })} tooltip="redirect 插件">
+        <Row>
+          <Col span={5}>
+            <Form.Item
+              name="redirectOption"
+              noStyle
+            >
+              <Select
+                disabled={disabled}
+                onChange={(parmas) => {
+                  onChange({ action: 'redirectOptionChange', data: parmas });
+                }}
+              >
+                {list.map(item => (
+                  <Select.Option value={item.value} key={item.value}>
+                    {item.label}
+                  </Select.Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Col>
+        </Row>
       </Form.Item>
+    )
+  }
+
+  const CustomRedirect: React.FC = () => (
+    <Form.Item
+      noStyle
+      shouldUpdate={(prev, next) => {
+        if (prev.redirectOption !== next.redirectOption) {
+          onChange({ action: 'redirectOptionChange', data: next.redirectOption 
});
+        }
+        return prev.redirectOption !== next.redirectOption;
+      }}
+    >
+      {() => {
+        if (form.getFieldValue('redirectOption') === 'customRedirect') {
+          return (
+            <Form.Item
+              label={formatMessage({ id: 
'page.route.form.itemLabel.redirectCustom' })}
+              required
+              style={{ marginBottom: 0 }}
+            >
+              <Row gutter={10}>
+                <Col span={5}>
+                  <Form.Item
+                    name="redirectURI"
+                    rules={[
+                      {
+                        required: true,
+                        message: `${formatMessage({
+                          id: 'component.global.pleaseEnter',
+                        })}${formatMessage({
+                          id: 'page.route.form.itemLabel.redirectURI',
+                        })}`,
+                      },
+                    ]}
+                  >
+                    <Input
+                      placeholder={formatMessage({
+                        id: 'page.route.input.placeholder.redirectCustom',
+                      })}
+                      disabled={disabled}
+                    />
+                  </Form.Item>
+                </Col>
+                <Col span={5}>
+                  <Form.Item name="ret_code" rules={[{ required: true }]}>
+                    <Select disabled={disabled}>
+                      <Select.Option value={301}>
+                        {formatMessage({ id: 
'page.route.select.option.redirect301' })}
+                      </Select.Option>
+                      <Select.Option value={302}>
+                        {formatMessage({ id: 
'page.route.select.option.redirect302' })}
+                      </Select.Option>
+                    </Select>
+                  </Form.Item>
+                </Col>
+              </Row>
+            </Form.Item>
+          );
+        }
+        return null;
+      }}
+    </Form.Item>
+  )
+
+  const ServiceSelector: React.FC = () => (
+    <Form.Item label={formatMessage({ id: 'page.route.service' })} 
tooltip="绑定服务(Service)对象,以便复用其中的配置。">
+      <Row>
+        <Col span={5}>
+          <Form.Item noStyle name="service_id">
+            <Select disabled={disabled}>
+              {/* TODO: value === '' means  no service_id select, need to find 
a better way */}
+              <Select.Option value="" 
key={Math.random().toString(36).substring(7)}>
+                {formatMessage({ id: "page.route.service.none" })}
+              </Select.Option>
+              {serviceList.map((item) => {
+                return (
+                  <Select.Option value={item.id} key={item.id}>
+                    {item.name}
+                  </Select.Option>
+                );
+              })}
+            </Select>
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form.Item>
+  )
 
+  return (
+    <PanelSection title={formatMessage({ id: 
'page.route.panelSection.title.nameDescription' })}>
+      <Name />
       <NormalLabelComponent />
       <VersionLabelComponent />
 
-      <Form.Item label={formatMessage({ id: 'component.global.description' })} 
name="desc">
-        <Input.TextArea
-          placeholder={formatMessage({ id: 
'component.global.input.placeholder.description' })}
-          disabled={disabled}
-          showCount
-          maxLength={256}
-        />
-      </Form.Item>
+      <Description />
 
-      <Form.Item
-        label={formatMessage({ id: 'page.route.publish' })}
-        name="status"
-        valuePropName="checked"
-      >
-        <Switch disabled={isEdit} />
-      </Form.Item>
+      <Redirect />
+      <CustomRedirect />
+
+      <ServiceSelector />
+
+      <WebSocket />
+      <Publish />
     </PanelSection>
   );
 };
diff --git a/web/src/pages/Route/components/Step1/ProxyRewrite.tsx 
b/web/src/pages/Route/components/Step1/ProxyRewrite.tsx
index 68d2670..e564711 100644
--- a/web/src/pages/Route/components/Step1/ProxyRewrite.tsx
+++ b/web/src/pages/Route/components/Step1/ProxyRewrite.tsx
@@ -22,13 +22,22 @@ import { useIntl } from 'umi';
 import { PanelSection } from '@api7-dashboard/ui';
 
 import {
-  FORM_ITEM_LAYOUT,
   FORM_ITEM_WITHOUT_LABEL,
   SCHEME_REWRITE,
   URI_REWRITE_TYPE,
   HOST_REWRITE_TYPE
 } from '@/pages/Route/constants';
 
+const removeBtnStyle = {
+  marginLeft: 20,
+  display: 'flex',
+  alignItems: 'center',
+};
+
+/**
+ * https://apisix.apache.org/docs/apisix/plugins/proxy-rewrite
+ * UI for ProxyRewrite plugin
+*/
 const ProxyRewrite: React.FC<RouteModule.Step1PassProps> = ({ form, disabled 
}) => {
   const { formatMessage } = useIntl();
 
@@ -154,88 +163,137 @@ const ProxyRewrite: React.FC<RouteModule.Step1PassProps> 
= ({ form, disabled })
     }
   };
 
-  return (
-    <PanelSection title={formatMessage({ id: 
'page.route.panelSection.title.requestOverride' })}>
+  const SchemeComponent: React.FC = () => {
+    const options = [
+      {
+        value: SCHEME_REWRITE.KEEP,
+        label: formatMessage({ id: 'page.route.radio.staySame' })
+      }, {
+        value: SCHEME_REWRITE.HTTP,
+        label: (SCHEME_REWRITE.HTTP).toLocaleUpperCase()
+      }, {
+        value: SCHEME_REWRITE.HTTPS,
+        label: (SCHEME_REWRITE.HTTPS).toLocaleUpperCase()
+      }
+    ]
+
+    return (
       <Form.Item
         label={formatMessage({ id: 'page.route.form.itemLabel.scheme' })}
         name={['proxyRewrite', 'scheme']}
       >
         <Radio.Group disabled={disabled}>
-          <Radio value={SCHEME_REWRITE.KEEP}>
-            {formatMessage({ id: 'page.route.radio.staySame' })}
-          </Radio>
-          <Radio 
value={SCHEME_REWRITE.HTTP}>{(SCHEME_REWRITE.HTTP).toLocaleUpperCase()}</Radio>
-          <Radio 
value={SCHEME_REWRITE.HTTPS}>{(SCHEME_REWRITE.HTTPS).toLocaleUpperCase()}</Radio>
+          {
+            options.map(item => (
+              <Radio value={item.value} key={item.value}>{item.label}</Radio>
+            ))
+          }
         </Radio.Group>
       </Form.Item>
-      <Form.Item
-        label={formatMessage({ id: 'page.route.form.itemLabel.URIRewriteType' 
})}
-        name='URIRewriteType'
-      >
-        <Radio.Group
-          disabled={disabled}
+    )
+  }
+
+  const URIRewriteType: React.FC = () => {
+    const options = [
+      {
+        value: URI_REWRITE_TYPE.KEEP,
+        label: formatMessage({ id: 'page.route.radio.staySame' })
+      }, {
+        value: URI_REWRITE_TYPE.STATIC,
+        label: formatMessage({ id: 'page.route.radio.static' }),
+        dataCypress: 'uri-static'
+      }, {
+        value: URI_REWRITE_TYPE.REGEXP,
+        label: formatMessage({ id: 'page.route.radio.regex' })
+      }
+    ]
+
+    return (
+      <React.Fragment>
+        <Form.Item
+          label={formatMessage({ id: 
'page.route.form.itemLabel.URIRewriteType' })}
+          name='URIRewriteType'
         >
-          <Radio value={URI_REWRITE_TYPE.KEEP}>
-            {formatMessage({ id: 'page.route.radio.staySame' })}
-          </Radio>
-          <Radio data-cy='uri-static' value={URI_REWRITE_TYPE.STATIC}>
-            {formatMessage({ id: 'page.route.radio.static' })}
-          </Radio>
-          <Radio value={URI_REWRITE_TYPE.REGEXP}>
-            {formatMessage({ id: 'page.route.radio.regex' })}
-          </Radio>
-        </Radio.Group>
-      </Form.Item>
-      <Form.Item shouldUpdate={
-        (prevValues, curValues) => prevValues.URIRewriteType !== 
curValues.URIRewriteType } noStyle>
-        {
-          () => {
-            return getUriRewriteItems()
+          <Radio.Group
+            disabled={disabled}
+          >
+            {
+              options.map(item => (
+                <Radio data-cy={item.dataCypress} value={item.value} 
key={item.value}>
+                  {item.label}
+                </Radio>
+              ))
+            }
+          </Radio.Group>
+        </Form.Item>
+        <Form.Item shouldUpdate={
+          (prevValues, curValues) => prevValues.URIRewriteType !== 
curValues.URIRewriteType} noStyle>
+          {
+            () => {
+              return getUriRewriteItems()
+            }
           }
-        }
-      </Form.Item>
+        </Form.Item>
+      </React.Fragment>
+    )
+  }
 
-      <Form.Item
-        label={formatMessage({ id: 'page.route.form.itemLabel.hostRewriteType' 
})}
-        name='hostRewriteType'
-      >
-        <Radio.Group
-          disabled={disabled}
+  const HostRewriteType: React.FC = () => {
+    const options = [
+      {
+        label: formatMessage({ id: 'page.route.radio.staySame' }),
+        value: HOST_REWRITE_TYPE.KEEP,
+        dataCypress: "host-keep"
+      }, {
+        label: formatMessage({ id: 'page.route.radio.static' }),
+        value: HOST_REWRITE_TYPE.REWRITE,
+        dataCypress: "host-static"
+      }
+    ]
+    return (
+      <React.Fragment>
+        <Form.Item
+          label={formatMessage({ id: 
'page.route.form.itemLabel.hostRewriteType' })}
+          name='hostRewriteType'
         >
-          <Radio data-cy='host-keep' value={HOST_REWRITE_TYPE.KEEP}>
-            {formatMessage({ id: 'page.route.radio.staySame' })}
-          </Radio>
-          <Radio data-cy='host-static' value={HOST_REWRITE_TYPE.REWRITE}>
-            {formatMessage({ id: 'page.route.radio.static' })}
-          </Radio>
-        </Radio.Group>
-      </Form.Item>
-      <Form.Item shouldUpdate={(prevValues, curValues) => 
prevValues.hostRewriteType !== curValues.hostRewriteType} noStyle>
-        {
-          () =>{
-            return getHostRewriteItems();
+          <Radio.Group
+            disabled={disabled}
+          >
+            {
+              options.map(item => (
+                <Radio data-cy={item.dataCypress} value={item.value} 
key={item.value}>
+                  {item.label}
+                </Radio>
+              ))
+            }
+          </Radio.Group>
+        </Form.Item>
+        <Form.Item shouldUpdate={(prevValues, curValues) => 
prevValues.hostRewriteType !== curValues.hostRewriteType} noStyle>
+          {
+            () => {
+              return getHostRewriteItems();
+            }
           }
-        }
-      </Form.Item>
+        </Form.Item>
+      </React.Fragment>
+    )
+  }
 
+  const Headers: React.FC = () => {
+    return (
       <Form.List
         name={['proxyRewrite', 'kvHeaders']}
         initialValue={[{}]}
       >
         {(fields, { add, remove }) => (
           <>
-            {fields.map((field, index) => (
-              <Form.Item
-                {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
-                label={
-                  index === 0
-                    ? formatMessage({ id: 
'page.route.form.itemLabel.headerRewrite' })
-                    : ''
-                }
-                key={field.key}
-              >
-                <Row gutter={24} key={field.name}>
-                  <Col span={11}>
+            <Form.Item
+              label={formatMessage({ id: 
'page.route.form.itemLabel.headerRewrite' })}
+              style={{ marginBottom: 0 }}
+            >
+              {fields.map((field, index) => (
+                <Row gutter={12} key={index} style={{ marginBottom: 10 }}>
+                  <Col span={5}>
                     <Form.Item
                       name={[field.name, 'key']}
                       fieldKey={[field.fieldKey, 'key']}
@@ -249,7 +307,7 @@ const ProxyRewrite: React.FC<RouteModule.Step1PassProps> = 
({ form, disabled })
                       />
                     </Form.Item>
                   </Col>
-                  <Col span={11}>
+                  <Col span={5}>
                     <Form.Item
                       name={[field.name, 'value']}
                       fieldKey={[field.fieldKey, 'value']}
@@ -264,7 +322,7 @@ const ProxyRewrite: React.FC<RouteModule.Step1PassProps> = 
({ form, disabled })
                     </Form.Item>
                   </Col>
                   {!disabled && fields.length > 1 && (
-                    <Col>
+                    <Col style={{ ...removeBtnStyle, marginLeft: -5 }}>
                       <MinusCircleOutlined
                         className="dynamic-delete-button"
                         onClick={() => remove(field.name)}
@@ -272,16 +330,25 @@ const ProxyRewrite: React.FC<RouteModule.Step1PassProps> 
= ({ form, disabled })
                     </Col>
                   )}
                 </Row>
-              </Form.Item>
-            ))}
+              ))}
+            </Form.Item>
             <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
               <Button data-cy='create-new-rewrite-header' type="dashed" 
disabled={disabled} onClick={() => add()} icon={<PlusOutlined />}>
-                {formatMessage({ id: 'component.global.create' })}
+                {formatMessage({ id: 'component.global.add' })}
               </Button>
             </Form.Item>
           </>
         )}
       </Form.List>
+    )
+  }
+
+  return (
+    <PanelSection title={formatMessage({ id: 
'page.route.panelSection.title.requestOverride' })}>
+      <SchemeComponent />
+      <URIRewriteType />
+      <HostRewriteType />
+      <Headers />
     </PanelSection>
   );
 };
diff --git a/web/src/pages/Route/components/Step1/RequestConfigView.tsx 
b/web/src/pages/Route/components/Step1/RequestConfigView.tsx
index 7ad1631..ba03f94 100644
--- a/web/src/pages/Route/components/Step1/RequestConfigView.tsx
+++ b/web/src/pages/Route/components/Step1/RequestConfigView.tsx
@@ -14,78 +14,76 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 import Form from 'antd/es/form';
-import { Button, Input, Select, Row, Col, InputNumber, Switch } from 'antd';
+import { Button, Input, Select, Row, Col, InputNumber } from 'antd';
 import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
 import { useIntl } from 'umi';
 import { PanelSection } from '@api7-dashboard/ui';
 
 import {
   HTTP_METHOD_OPTION_LIST,
-  FORM_ITEM_LAYOUT,
   FORM_ITEM_WITHOUT_LABEL,
 } from '@/pages/Route/constants';
-import { fetchServiceList } from '../../service';
+
+const removeBtnStyle = {
+  marginLeft: 20,
+  display: 'flex',
+  alignItems: 'center',
+};
 
 const RequestConfigView: React.FC<RouteModule.Step1PassProps> = ({
   form,
   disabled,
-  onChange = () => {},
 }) => {
   const { formatMessage } = useIntl();
-  const [serviceList, setServiceList] = 
useState<ServiceModule.ResponseBody[]>([]);
-
-  useEffect(() => {
-    fetchServiceList().then(({ data }) => setServiceList(data));
-  }, []);
 
   const HostList = () => (
     <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 && formatMessage({ id: 
'page.route.domainName' })}
-                key={field.key}
-                extra={
-                  index === 0 && formatMessage({ id: 
'page.route.form.itemExtraMessage.domain' })
-                }
-              >
-                <Form.Item
-                  {...field}
-                  validateTrigger={['onChange', 'onBlur']}
-                  rules={[
-                    {
-                      pattern: new RegExp(/(^\*?[a-zA-Z0-9._-]+$|^\*$)/, 'g'),
-                      message: formatMessage({
-                        id: 'page.route.form.itemRulesPatternMessage.domain',
-                      }),
-                    },
-                  ]}
-                  noStyle
-                >
-                  <Input
-                    placeholder={`${formatMessage({
-                      id: 'component.global.pleaseEnter',
-                    })} ${formatMessage({ id: 'page.route.domainName' })}`}
-                    style={{ width: '60%' }}
-                    disabled={disabled}
-                  />
-                </Form.Item>
-                {!disabled && fields.length > 1 ? (
-                  <MinusCircleOutlined
-                    className="dynamic-delete-button"
-                    style={{ margin: '0 8px' }}
-                    onClick={() => {
-                      remove(field.name);
-                    }}
-                  />
-                ) : null}
-              </Form.Item>
-            ))}
+            <Form.Item
+              label={formatMessage({ id: 'page.route.host' })}
+              tooltip={formatMessage({ id: 
'page.route.form.itemExtraMessage.domain' })}
+              style={{ marginBottom: 0 }}
+            >
+              {fields.map((field, index) => (
+                <Row style={{ marginBottom: 10 }} gutter={16} key={index}>
+                  <Col span={10}>
+                    <Form.Item
+                      {...field}
+                      validateTrigger={['onChange', 'onBlur']}
+                      rules={[
+                        {
+                          // NOTE: 
https://github.com/apache/apisix/blob/master/apisix/schema_def.lua#L40
+                          pattern: new RegExp(/^\\*?[0-9a-zA-Z-._]+$/, 'g'),
+                          message: formatMessage({
+                            id: 
'page.route.form.itemRulesPatternMessage.domain',
+                          }),
+                        },
+                      ]}
+                      noStyle
+                    >
+                      <Input
+                        placeholder={formatMessage({ id: 
'page.route.configuration.host.placeholder' })}
+                        disabled={disabled}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col style={{ ...removeBtnStyle, marginLeft: -10 }}>
+                    {!disabled && fields.length > 1 ? (
+                      <MinusCircleOutlined
+                        className="dynamic-delete-button"
+                        onClick={() => {
+                          remove(field.name);
+                        }}
+                      />
+                    ) : null}
+                  </Col>
+                </Row>
+              ))}
+            </Form.Item>
             {!disabled && (
               <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
                 <Button
@@ -95,7 +93,7 @@ const RequestConfigView: React.FC<RouteModule.Step1PassProps> 
= ({
                     add();
                   }}
                 >
-                  <PlusOutlined /> {formatMessage({ id: 
'component.global.create' })}
+                  <PlusOutlined /> {formatMessage({ id: 'component.global.add' 
})}
                 </Button>
               </Form.Item>
             )}
@@ -110,61 +108,53 @@ const RequestConfigView: 
React.FC<RouteModule.Step1PassProps> = ({
       {(fields, { add, remove }) => {
         return (
           <div>
-            {fields.map((field, index) => (
-              <Form.Item
-                {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
-                label={index === 0 && formatMessage({ id: 'page.route.path' })}
-                required
-                key={field.key}
-                extra={
-                  index === 0 && (
-                    <div>
-                      {formatMessage({ id: 
'page.route.form.itemExtraMessage1.path' })}
-                      <br />
-                      {formatMessage({ id: 
'page.route.form.itemExtraMessage2.path' })}
-                    </div>
-                  )
-                }
-              >
-                <Form.Item
-                  {...field}
-                  validateTrigger={['onChange', 'onBlur']}
-                  rules={[
-                    {
-                      required: true,
-                      whitespace: true,
-                      message: `${formatMessage({
-                        id: 'component.global.pleaseEnter',
-                      })} ${formatMessage({ id: 'page.route.path' })}`,
-                    },
-                    {
-                      pattern: new 
RegExp(/^\/[a-zA-Z0-9\-._~%!$&'()+,;=:@/]*\*?$/, 'g'),
-                      message: formatMessage({
-                        id: 'page.route.form.itemRulesPatternMessage.path',
-                      }),
-                    },
-                  ]}
-                  noStyle
-                >
-                  <Input
-                    placeholder={`${formatMessage({
-                      id: 'component.global.pleaseEnter',
-                    })} ${formatMessage({ id: 'page.route.path' })}`}
-                    style={{ width: '60%' }}
-                    disabled={disabled}
-                  />
-                </Form.Item>
-                {!disabled && fields.length > 1 && (
-                  <MinusCircleOutlined
-                    className="dynamic-delete-button"
-                    style={{ margin: '0 8px' }}
-                    onClick={() => {
-                      remove(field.name);
-                    }}
-                  />
-                )}
-              </Form.Item>
-            ))}
+            <Form.Item
+              label={formatMessage({ id: 'page.route.path' })}
+              required
+              tooltip={
+                formatMessage({ id: 'page.route.form.itemExtraMessage1.path' })
+              }
+              style={{ marginBottom: 0 }}
+            >
+              {fields.map((field, index) => (
+                <Row style={{ marginBottom: 10 }} gutter={16} key={index}>
+                  <Col span={10}>
+                    <Form.Item
+                      {...field}
+                      validateTrigger={['onChange', 'onBlur']}
+                      rules={[
+                        {
+                          required: true,
+                          whitespace: true,
+                          message: formatMessage({ id: 
"page.route.configuration.path.rules.required.description" }),
+                        },
+                        {
+                          pattern: new 
RegExp(/^\/[a-zA-Z0-9\-._~%!$&'()+,;=:@/]*\*?$/, 'g'),
+                          message: formatMessage({
+                            id: 'page.route.form.itemRulesPatternMessage.path',
+                          }),
+                        },
+                      ]}
+                      noStyle
+                    >
+                      <Input
+                        placeholder={formatMessage({ id: 
'page.route.configuration.path.placeholder' })}
+                        disabled={disabled}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col style={{ ...removeBtnStyle, marginLeft: -10 }}>
+                    {!disabled && fields.length > 1 && (
+                      <MinusCircleOutlined
+                        className="dynamic-delete-button"
+                        onClick={() => {
+                          remove(field.name);
+                        }}
+                      />
+                    )}</Col>
+                </Row>
+              ))}
+            </Form.Item>
             {!disabled && (
               <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
                 <Button
@@ -174,7 +164,7 @@ const RequestConfigView: 
React.FC<RouteModule.Step1PassProps> = ({
                     add();
                   }}
                 >
-                  <PlusOutlined /> {formatMessage({ id: 
'component.global.create' })}
+                  <PlusOutlined /> {formatMessage({ id: 'component.global.add' 
})}
                 </Button>
               </Form.Item>
             )}
@@ -189,54 +179,49 @@ const RequestConfigView: 
React.FC<RouteModule.Step1PassProps> = ({
       {(fields, { add, remove }) => {
         return (
           <div>
-            {fields.map((field, index) => (
-              <Form.Item
-                {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
-                label={index === 0 && formatMessage({ id: 
'page.route.remoteAddrs' })}
-                key={field.key}
-                extra={
-                  index === 0 && (
-                    <div>
-                      {formatMessage({ id: 
'page.route.form.itemExtraMessage1.remoteAddrs' })}
-                    </div>
-                  )
-                }
-              >
-                <Form.Item
-                  {...field}
-                  validateTrigger={['onChange', 'onBlur']}
-                  rules={[
-                    {
-                      pattern: new RegExp(
-                        
/^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$|^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}\/[0-9]{1,2}$|^([a-fA-F0-9]{0,4}:){0,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?$|^([a-fA-F0-9]{0,4}:){0,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?\/[0-9]{1,3}$/,
-                        'g',
-                      ),
-                      message: formatMessage({
-                        id: 
'page.route.form.itemRulesPatternMessage.remoteAddrs',
-                      }),
-                    },
-                  ]}
-                  noStyle
-                >
-                  <Input
-                    placeholder={`${formatMessage({
-                      id: 'component.global.pleaseEnter',
-                    })} ${formatMessage({ id: 'page.route.remoteAddrs' })}`}
-                    style={{ width: '60%' }}
-                    disabled={disabled}
-                  />
-                </Form.Item>
-                {!disabled && fields.length > 1 && (
-                  <MinusCircleOutlined
-                    className="dynamic-delete-button"
-                    style={{ margin: '0 8px' }}
-                    onClick={() => {
-                      remove(field.name);
-                    }}
-                  />
-                )}
-              </Form.Item>
-            ))}
+            <Form.Item
+              label={formatMessage({ id: 'page.route.remoteAddrs' })}
+              tooltip={formatMessage({ id: 
'page.route.form.itemExtraMessage1.remoteAddrs' })}
+              style={{ marginBottom: 0 }}
+            >
+              {fields.map((field, index) => (
+                <Row style={{ marginBottom: 10 }} gutter={16} key={index}>
+                  <Col span={10}>
+                    <Form.Item
+                      {...field}
+                      validateTrigger={['onChange', 'onBlur']}
+                      rules={[
+                        {
+                          pattern: new RegExp(
+                            
/^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$|^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}\/[0-9]{1,2}$|^([a-fA-F0-9]{0,4}:){0,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?$|^([a-fA-F0-9]{0,4}:){0,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?\/[0-9]{1,3}$/,
+                            'g',
+                          ),
+                          message: formatMessage({
+                            id: 
'page.route.form.itemRulesPatternMessage.remoteAddrs',
+                          }),
+                        },
+                      ]}
+                      noStyle
+                    >
+                      <Input
+                        placeholder={formatMessage({ id: 
'page.route.configuration.remote_addrs.placeholder' })}
+                        disabled={disabled}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col style={{ ...removeBtnStyle, marginLeft: -10 }}>
+                    {!disabled && fields.length > 1 && (
+                      <MinusCircleOutlined
+                        className="dynamic-delete-button"
+                        onClick={() => {
+                          remove(field.name);
+                        }}
+                      />
+                    )}
+                  </Col>
+                </Row>
+              ))}
+            </Form.Item>
             {!disabled && (
               <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
                 <Button
@@ -246,7 +231,7 @@ const RequestConfigView: 
React.FC<RouteModule.Step1PassProps> = ({
                     add();
                   }}
                 >
-                  <PlusOutlined /> {formatMessage({ id: 
'component.global.create' })}
+                  <PlusOutlined /> {formatMessage({ id: 'component.global.add' 
})}
                 </Button>
               </Form.Item>
             )}
@@ -256,6 +241,65 @@ const RequestConfigView: 
React.FC<RouteModule.Step1PassProps> = ({
     </Form.List>
   );
 
+  const HTTPMethods: React.FC = () => (
+    <Form.Item
+      label={formatMessage({ id: 'page.route.form.itemLabel.httpMethod' })}
+    >
+      <Row>
+        <Col span={10}>
+          <Form.Item
+            name="methods"
+            noStyle
+          >
+            <Select
+              mode="multiple"
+              style={{ width: '100%' }}
+              optionLabelProp="label"
+              disabled={disabled}
+              onChange={(value) => {
+                if ((value as string[]).includes('ALL')) {
+                  form.setFieldsValue({
+                    methods: ['ALL'],
+                  });
+                }
+              }}
+            >
+              {['ALL'].concat(HTTP_METHOD_OPTION_LIST).map((item) => {
+                return (
+                  <Select.Option key={item} value={item}>
+                    {item}
+                  </Select.Option>
+                );
+              })}
+            </Select>
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form.Item>
+  )
+
+  const RoutePriority: React.FC = () => (
+    <Form.Item label={formatMessage({ id: 'page.route.form.itemLabel.priority' 
})}>
+      <Row>
+        <Col span={5}>
+          <Form.Item
+            noStyle
+            name="priority"
+          >
+            <InputNumber
+              placeholder={`Please input ${formatMessage({
+                id: 'page.route.form.itemLabel.priority',
+              })}`}
+              disabled={disabled}
+            />
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form.Item>
+  )
+
+
+
   return (
     <PanelSection
       title={formatMessage({ id: 
'page.route.panelSection.title.requestConfigBasicDefine' })}
@@ -263,141 +307,8 @@ const RequestConfigView: 
React.FC<RouteModule.Step1PassProps> = ({
       <HostList />
       <UriList />
       <RemoteAddrList />
-      <Form.Item
-        label={formatMessage({ id: 'page.route.form.itemLabel.httpMethod' })}
-        name="methods"
-      >
-        <Select
-          mode="multiple"
-          style={{ width: '100%' }}
-          optionLabelProp="label"
-          disabled={disabled}
-          onChange={(value) => {
-            if ((value as string[]).includes('ALL')) {
-              form.setFieldsValue({
-                methods: ['ALL'],
-              });
-            }
-          }}
-        >
-          {['ALL'].concat(HTTP_METHOD_OPTION_LIST).map((item) => {
-            return (
-              <Select.Option key={item} value={item}>
-                {item}
-              </Select.Option>
-            );
-          })}
-        </Select>
-      </Form.Item>
-      <Form.Item
-        label={formatMessage({ id: 'page.route.form.itemLabel.priority' })}
-        name="priority"
-      >
-        <InputNumber
-          placeholder={`Please input ${formatMessage({
-            id: 'page.route.form.itemLabel.priority',
-          })}`}
-          style={{ width: '60%' }}
-          disabled={disabled}
-        />
-      </Form.Item>
-      <Form.Item label="Websocket" valuePropName="checked" 
name="enable_websocket">
-        <Switch disabled={disabled} />
-      </Form.Item>
-      <Form.Item
-        label={formatMessage({ id: 'page.route.form.itemLabel.redirect' })}
-        name="redirectOption"
-      >
-        <Select
-          disabled={disabled}
-          onChange={(parmas) => {
-            onChange({ action: 'redirectOptionChange', data: parmas });
-          }}
-        >
-          <Select.Option value="forceHttps">
-            {formatMessage({ id: 'page.route.select.option.enableHttps' })}
-          </Select.Option>
-          <Select.Option value="customRedirect">
-            {formatMessage({ id: 'page.route.select.option.configCustom' })}
-          </Select.Option>
-          <Select.Option value="disabled">
-            {formatMessage({ id: 'page.route.select.option.forbidden' })}
-          </Select.Option>
-        </Select>
-      </Form.Item>
-      <Form.Item
-        noStyle
-        shouldUpdate={(prev, next) => {
-          if (prev.redirectOption !== next.redirectOption) {
-            onChange({ action: 'redirectOptionChange', data: 
next.redirectOption });
-          }
-          return prev.redirectOption !== next.redirectOption;
-        }}
-      >
-        {() => {
-          if (form.getFieldValue('redirectOption') === 'customRedirect') {
-            return (
-              <Form.Item
-                label={formatMessage({ id: 
'page.route.form.itemLabel.redirectCustom' })}
-                required
-              >
-                <Row gutter={10}>
-                  <Col>
-                    <Form.Item
-                      name="redirectURI"
-                      rules={[
-                        {
-                          required: true,
-                          message: `${formatMessage({
-                            id: 'component.global.pleaseEnter',
-                          })}${formatMessage({
-                            id: 'page.route.form.itemLabel.redirectURI',
-                          })}`,
-                        },
-                      ]}
-                    >
-                      <Input
-                        placeholder={formatMessage({
-                          id: 'page.route.input.placeholder.redirectCustom',
-                        })}
-                        disabled={disabled}
-                      />
-                    </Form.Item>
-                  </Col>
-                  <Col span={10}>
-                    <Form.Item name="ret_code" rules={[{ required: true }]}>
-                      <Select disabled={disabled}>
-                        <Select.Option value={301}>
-                          {formatMessage({ id: 
'page.route.select.option.redirect301' })}
-                        </Select.Option>
-                        <Select.Option value={302}>
-                          {formatMessage({ id: 
'page.route.select.option.redirect302' })}
-                        </Select.Option>
-                      </Select>
-                    </Form.Item>
-                  </Col>
-                </Row>
-              </Form.Item>
-            );
-          }
-          return null;
-        }}
-      </Form.Item>
-      <Form.Item label={formatMessage({ id: 'page.route.service' })} 
name="service_id">
-        <Select disabled={disabled}>
-          {/* TODO: value === '' means  no service_id select, need to find a 
better way */}
-          <Select.Option value="" 
key={Math.random().toString(36).substring(7)}>
-            None
-          </Select.Option>
-          {serviceList.map((item) => {
-            return (
-              <Select.Option value={item.id} key={item.id}>
-                {item.name}
-              </Select.Option>
-            );
-          })}
-        </Select>
-      </Form.Item>
+      <HTTPMethods />
+      <RoutePriority />
     </PanelSection>
   );
 };
diff --git a/web/src/pages/Route/locales/en-US.ts 
b/web/src/pages/Route/locales/en-US.ts
index 7e52826..1a0564c 100644
--- a/web/src/pages/Route/locales/en-US.ts
+++ b/web/src/pages/Route/locales/en-US.ts
@@ -52,7 +52,6 @@ export default {
   'page.route.panelSection.title.advancedMatchRule': 'Advanced Routing 
Matching Conditions',
 
   'page.route.panelSection.title.nameDescription': 'Name And Description',
-  'page.route.form.itemLabel.apiName': 'API Name',
   'page.route.form.itemRulesPatternMessage.apiNameRule':
     'Maximum length 100, only letters, Numbers, _, and - are supported, and 
can only begin with letters',
 
@@ -78,8 +77,7 @@ export default {
   'page.route.form.itemRulesPatternMessage.domain':
     'Only letters, numbers and * are supported. * can only be at the 
beginning, and only single * is supported',
   'page.route.form.itemExtraMessage1.path':
-    '1. Request path, for example: /foo/index.html, supports request path 
prefix /foo/* ;',
-  'page.route.form.itemExtraMessage2.path': '2. /* represents all paths',
+    'HTTP Request path, for example: /foo/index.html, supports request path 
prefix /foo/* ; /* represents all paths',
   'page.route.form.itemRulesPatternMessage.path': 'Begin with / , and * can 
only at the end',
   'page.route.form.itemRulesPatternMessage.remoteAddrs':
     'Please enter a valid IP address, for example: 192.168.1.101, 
192.168.1.0/24, ::1, fe80::1, fe80::1/64',
@@ -124,7 +122,7 @@ export default {
   'page.route.form.itemHelp.status':
     'Whether a route can be used after it is created, the default value is 
false.',
 
-  'page.route.domainName': 'Domain Name',
+  'page.route.host': 'Host',
   'page.route.path': 'Path',
   'page.route.remoteAddrs': 'Remote Addrs',
   'page.route.PanelSection.title.defineRequestParams': 'Define Request 
Parameters',
@@ -149,5 +147,22 @@ export default {
   'page.route.tooltip.pluginOrchWithoutRedirect': 'Plugin orchestration mode 
cannot be used when Redirect in Step 1 is selected to enable HTTPS.',
 
   'page.route.tabs.normalMode': 'Normal mode',
-  'page.route.tabs.orchestration': 'Plugin orchestration'
+  'page.route.tabs.orchestration': 'Plugin orchestration',
+
+  'page.route.list.description': 'Route is the entry point of a request, which 
defines the matching rules between a client request and a service. A route can 
be associated with a service (Service), an upstream (Upstream), a service can 
correspond to a set of routes, and a route can correspond to an upstream object 
(a set of backend service nodes), so each request matching to a route will be 
proxied by the gateway to the route-bound upstream service.',
+
+  'page.route.configuration.name.rules.required.description': 'Please enter 
the route name',
+  'page.route.configuration.name.placeholder': 'Please enter the route name',
+  'page.route.configuration.desc.tooltip': 'Please enter the description of 
the route',
+  'page.route.configuration.publish.tooltip': 'Used to control whether a route 
is published to the gateway immediately after it is created',
+  'page.route.configuration.version.placeholder': 'Please enter the routing 
version number',
+  'page.route.configuration.version.tooltip': 'Version number of the route, 
e.g. V1',
+  'page.route.configuration.normal-labels.tooltip': 'Add custom labels to 
routes that can be used for route grouping.',
+
+  'page.route.configuration.path.rules.required.description': 'Please enter a 
valid HTTP request path',
+  'page.route.configuration.path.placeholder': 'Please enter the HTTP request 
path',
+  'page.route.configuration.remote_addrs.placeholder': 'Please enter the 
client address',
+  'page.route.configuration.host.placeholder': 'Please enter the HTTP request 
hostname',
+
+  'page.route.service.none': 'None',
 };
diff --git a/web/src/pages/Route/locales/zh-CN.ts 
b/web/src/pages/Route/locales/zh-CN.ts
index 7e7f635..ca97132 100644
--- a/web/src/pages/Route/locales/zh-CN.ts
+++ b/web/src/pages/Route/locales/zh-CN.ts
@@ -28,7 +28,7 @@ export default {
   'page.route.regexMatch': '正则匹配',
   'page.route.in': 'IN',
   'page.route.rule': '规则',
-  'page.route.domainName': '域名',
+  'page.route.host': '域名',
   'page.route.path': '路径',
   'page.route.remoteAddrs': '客户端地址',
   'page.route.value': '参数值',
@@ -36,12 +36,12 @@ export default {
   'page.route.status': '状态',
   'page.route.groupName': '分组名称',
   'page.route.offline': '下线',
-  'page.route.publish': '发布',
+  'page.route.publish': '是否发布',
   'page.route.published': '已发布',
   'page.route.unpublished': '未发布',
   'page.route.onlineDebug': '在线调试',
   'page.route.pluginTemplateConfig': '插件模版配置',
-  'page.route.service': '服务',
+  'page.route.service': '绑定服务',
   'page.route.instructions': '说明',
   'page.route.import': '导入',
   'page.route.createRoute': '创建路由',
@@ -61,9 +61,8 @@ export default {
   'page.route.input.placeholder.paramValue': '参数值',
   // form
   'page.route.form.itemRulesRequiredMessage.parameterName': 
'仅支持字母和数字,且只能以字母开头',
-  'page.route.form.itemLabel.apiName': 'API 名称',
   'page.route.form.itemRulesPatternMessage.apiNameRule':
-    '最大长度100,仅支持字母、数字、- 和 _,且只能以字母开头',
+    '路由的名称,最大长度100,仅支持字母、数字、- 和 _,且只能以字母开头',
   'page.route.form.itemLabel.httpMethod': 'HTTP 方法',
   'page.route.form.itemLabel.scheme': '协议',
   'page.route.form.itemLabel.priority': '优先级',
@@ -73,15 +72,14 @@ export default {
   'page.route.form.itemLabel.hostRewriteType': '域名改写',
   'page.route.form.itemLabel.headerRewrite': '请求头改写',
   'page.route.form.itemLabel.redirectURI': '重定向路径',
-  'page.route.form.itemExtraMessage.domain': '域名或IP,支持泛域名,如:*.test.com',
+  'page.route.form.itemExtraMessage.domain': '路由匹配的域名列表。支持泛域名,如:*.test.com',
   'page.route.form.itemRulesPatternMessage.domain':
     '仅支持字母、数字和 * ,且 * 只能是在开头,支持单个 * ',
   'page.route.form.itemExtraMessage1.path':
-    '1. 请求路径,如 /foo/index.html,支持请求路径前缀 /foo/* ;',
-  'page.route.form.itemExtraMessage2.path': '2. /* 代表所有路径',
+    'HTTP 请求路径,如 /foo/index.html,支持请求路径前缀 /foo/*。/* 代表所有路径',
   'page.route.form.itemRulesPatternMessage.path': '以 / 开头,且 * 只能在最后',
   'page.route.form.itemExtraMessage1.remoteAddrs':
-    '客户端 IP,例如:192.168.1.101,192.168.1.0/24,::1,fe80::1,fe80::1/64',
+    '客户端与服务器握手时 IP,即客户端 
IP,例如:192.168.1.101,192.168.1.0/24,::1,fe80::1,fe80::1/64',
   'page.route.form.itemRulesPatternMessage.remoteAddrs':
     '请输入合法的 IP 地址,例如:192.168.1.101,192.168.1.0/24,::1,fe80::1,fe80::1/64',
   'page.route.form.itemLabel.username': '用户名',
@@ -98,15 +96,15 @@ export default {
   'page.route.select.option.inputManually': '手动填写',
 
   // steps
-  'page.route.steps.stepTitle.defineApiRequest': '定义 API 请求',
-  'page.route.steps.stepTitle.defineApiBackendServe': '定义 API 后端服务',
+  'page.route.steps.stepTitle.defineApiRequest': '设置路由信息',
+  'page.route.steps.stepTitle.defineApiBackendServe': '设置上游服务',
 
   // panelSection
-  'page.route.panelSection.title.nameDescription': '名称及其描述',
+  'page.route.panelSection.title.nameDescription': '基本信息',
   'page.route.panelSection.title.httpOverrideRequestHeader': 'HTTP 请求头改写',
   'page.route.panelSection.title.requestOverride': '请求改写',
-  'page.route.panelSection.title.requestConfigBasicDefine': '请求基础定义',
-  'page.route.panelSection.title.advancedMatchRule': '高级路由匹配条件',
+  'page.route.panelSection.title.requestConfigBasicDefine': '匹配条件',
+  'page.route.panelSection.title.advancedMatchRule': '高级匹配条件',
   'page.route.PanelSection.title.defineRequestParams': '请求参数定义',
   'page.route.PanelSection.title.responseResult': '请求响应结果',
 
@@ -148,5 +146,22 @@ export default {
   'page.route.tooltip.pluginOrchWithoutRedirect': '当步骤一中 重定向 选择为 启用 HTTPS 
时,不可使用插件编排模式。',
 
   'page.route.tabs.normalMode': '普通模式',
-  'page.route.tabs.orchestration': '插件编排'
+  'page.route.tabs.orchestration': '插件编排',
+
+  'page.route.list.description': 
'路由(Route)是请求的入口点,它定义了客户端请求与服务之间的匹配规则。路由可以与服务(Service)、上游(Upstream)关联,一个服务可对应一组路由,一个路由可以对应一个上游对象(一组后端服务节点),因此,每个匹配到路由的请求将被网关代理到路由绑定的上游服务中。',
+
+  'page.route.configuration.name.rules.required.description': '请输入路由名称',
+  'page.route.configuration.name.placeholder': '请输入路由名称',
+  'page.route.configuration.desc.tooltip': '路由的描述信息',
+  'page.route.configuration.publish.tooltip': '用于控制路由创建后,是否立即发布到网关',
+  'page.route.configuration.version.placeholder': '请输入路由版本号',
+  'page.route.configuration.version.tooltip': '路由的版本号,如 V1',
+  'page.route.configuration.normal-labels.tooltip': '为路由增加自定义标签,可用于路由分组。',
+
+  'page.route.configuration.path.rules.required.description': '请输入有效的 HTTP 
请求路径',
+  'page.route.configuration.path.placeholder': '请输入 HTTP 请求路径',
+  'page.route.configuration.remote_addrs.placeholder': '请输入客户端地址',
+  'page.route.configuration.host.placeholder': '请输入 HTTP 请求域名',
+
+  'page.route.service.none': '不绑定服务',
 };

Reply via email to