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': '不绑定服务',
};