You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2021/10/03 08:03:29 UTC

[apisix-dashboard] branch master updated: feat: basic support Apache APISIX 2.10 (#2149)

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 321a195  feat: basic support Apache APISIX 2.10 (#2149)
321a195 is described below

commit 321a195b3d3423d57ec1562e5b085bc528ec1b45
Author: bzp2010 <bz...@apache.org>
AuthorDate: Sun Oct 3 03:03:24 2021 -0500

    feat: basic support Apache APISIX 2.10 (#2149)
---
 api/conf/schema.json                               | 322 ++++++++++++++++++++-
 api/internal/core/entity/entity.go                 |   1 +
 api/test/docker/docker-compose.yaml                |   2 +-
 api/test/e2enew/service/service_test.go            | 126 +++++++-
 web/cypress/fixtures/plugin-dataset.json           | 118 +++++++-
 ...delete-plugin.spec.js => plugin-schema.spec.js} |  27 +-
 ...ate-route-with-referer-restriction-form.spec.js |  63 +++-
 .../service/create-edit-delete-service.spec.js     |  12 +
 web/cypress/support/commands.js                    | 157 +++++-----
 web/src/components/Plugin/PluginDetail.tsx         |  21 +-
 web/src/components/Plugin/UI/limit-conn.tsx        |  50 +++-
 web/src/components/Plugin/UI/proxy-mirror.tsx      |  17 +-
 .../components/Plugin/UI/referer-restriction.tsx   | 121 ++++++--
 web/src/components/Plugin/locales/en-US.ts         |  11 +
 web/src/components/Plugin/locales/zh-CN.ts         |  11 +-
 web/src/pages/Service/components/Step1.tsx         |  68 ++++-
 web/src/pages/Service/locales/en-US.ts             |   2 +
 web/src/pages/Service/locales/zh-CN.ts             |   2 +
 web/src/pages/Service/service.ts                   |   5 +-
 .../pages/Service/{typing.d.ts => transform.ts}    |  39 +--
 web/src/pages/Service/typing.d.ts                  |   1 +
 21 files changed, 997 insertions(+), 179 deletions(-)

diff --git a/api/conf/schema.json b/api/conf/schema.json
index 76f1fb8..ba4f438 100644
--- a/api/conf/schema.json
+++ b/api/conf/schema.json
@@ -845,6 +845,15 @@
 					"description": "enable websocket for request",
 					"type": "boolean"
 				},
+				"hosts": {
+					"items": {
+						"pattern": "^\\*?[0-9a-zA-Z-._]+$",
+						"type": "string"
+					},
+					"minItems": 1,
+					"type": "array",
+					"uniqueItems": true
+				},
 				"id": {
 					"anyOf": [{
 						"maxLength": 64,
@@ -2582,6 +2591,7 @@
 				},
 				"type": "object"
 			},
+			"scope": "global",
 			"version": 0.1
 		},
 		"client-control": {
@@ -2682,7 +2692,7 @@
 					},
 					"allow_methods": {
 						"default": "*",
-						"description": "you can use '*' to allow all methods when no credentials and '**','**' to allow forcefully(it will bring some security risks, be carefully),multiple method use ',' to split. default: *.",
+						"description": "you can use '*' to allow all methods when no credentials,'**' to allow forcefully(it will bring some security risks, be carefully),multiple method use ',' to split. default: *.",
 						"type": "string"
 					},
 					"allow_origins": {
@@ -2707,12 +2717,12 @@
 					},
 					"expose_headers": {
 						"default": "*",
-						"description": "you can use '*' to expose all header when no credentials,multiple header use ',' to split. default: *.",
+						"description": "you can use '*' to expose all header when no credentials,'**' to allow forcefully(it will bring some security risks, be carefully),multiple header use ',' to split. default: *.",
 						"type": "string"
 					},
 					"max_age": {
 						"default": 5,
-						"description": "maximum number of seconds the results can be cached.-1 mean no cached,the max value is depend on browser,more detail plz check MDN. default: 5.",
+						"description": "maximum number of seconds the results can be cached.-1 means no cached, the max value is depend on browser,more details plz check MDN. default: 5.",
 						"type": "integer"
 					}
 				},
@@ -2786,6 +2796,16 @@
 		},
 		"error-log-logger": {
 			"metadata_schema": {
+				"oneOf":[
+					{
+						"required":["skywalking"]
+					},
+					{
+						"required":["tcp"]
+					},
+					{
+						"required":["host", "port"]
+					}],
 				"properties": {
 					"batch_max_size": {
 						"default": 1000,
@@ -2798,8 +2818,11 @@
 						"type": "integer"
 					},
 					"host": {
-						"pattern": "^\\*?[0-9a-zA-Z-._]+$",
-						"type": "string"
+						"1":{
+							"pattern":"^\\*?[0-9a-zA-Z-._]+$",
+							"type":"string"
+						},
+						"description":"Deprecated, use `tcp.host` instead."
 					},
 					"inactive_timeout": {
 						"default": 3,
@@ -2826,6 +2849,7 @@
 						"type": "string"
 					},
 					"port": {
+						"description":"Deprecated, use `tcp.port` instead.",
 						"minimum": 0,
 						"type": "integer"
 					},
@@ -2834,6 +2858,43 @@
 						"minimum": 0,
 						"type": "integer"
 					},
+					"skywalking":{
+						"properties":{
+							"endpoint_addr":{
+								"default":"http://127.0.0.1:12900/v3/logs"
+							},
+							"service_instance_name":{
+								"default":"APISIX Service Instance",
+								"type":"string"
+							},
+							"service_name":{
+								"default":"APISIX",
+								"type":"string"
+							}
+						},
+						"type":"object"
+					},
+					"tcp":{
+						"properties":{
+							"host":{
+								"pattern":"^\\*?[0-9a-zA-Z-._]+$",
+								"type":"string"
+							},
+							"port":{
+								"minimum":0,
+								"type":"integer"
+							},
+							"tls":{
+								"default":false,
+								"type":"boolean"
+							},
+							"tls_server_name":{
+								"type":"string"
+							}
+						},
+						"required":["host", "port"],
+						"type":"object"
+					},
 					"timeout": {
 						"default": 3,
 						"minimum": 1,
@@ -2841,13 +2902,14 @@
 					},
 					"tls": {
 						"default": false,
+						"description":"Deprecated, use `tcp.tls` instead.",
 						"type": "boolean"
 					},
 					"tls_server_name": {
+						"description":"Deprecated, use `tcp.tls_server_name` instead.",
 						"type": "string"
 					}
 				},
-				"required": ["host", "port"],
 				"type": "object"
 			},
 			"priority": 1091,
@@ -2860,6 +2922,7 @@
 				},
 				"type": "object"
 			},
+			"scope": "global",
 			"version": 0.1
 		},
 		"example-plugin": {
@@ -3183,6 +3246,11 @@
 						"title": "whether to keep the http request header",
 						"type": "boolean"
 					},
+					"max_req_body":{
+						"default": 524288,
+						"title": "Max request body size",
+						"type": "integer"
+					},
 					"secret_key": {
 						"maxLength": 256,
 						"minLength": 1,
@@ -3195,6 +3263,11 @@
 							"type": "string"
 						},
 						"type": "array"
+					},
+					"validate_request_body": {
+						"default": false,
+						"title": "A boolean value telling the plugin to enable body validation",
+						"type": "boolean"
 					}
 				},
 				"required": ["access_key", "secret_key"],
@@ -3452,6 +3525,15 @@
 						"type": "integer"
 					},
 					"broker_list": {
+						"minProperties": 1,
+						"patternProperties": {
+							".*": {
+								"description": "the port of kafka broker",
+								"maximum": 65535,
+								"minimum": 1,
+								"type": "integer"
+							}
+						},
 						"type": "object"
 					},
 					"buffer_duration": {
@@ -3459,6 +3541,11 @@
 						"minimum": 1,
 						"type": "integer"
 					},
+					"cluster_name": {
+						"default": 1,
+						"minimum": 1,
+						"type": "integer"
+					},
 					"disable": {
 						"type": "boolean"
 					},
@@ -3496,6 +3583,11 @@
 						"enum": ["async", "sync"],
 						"type": "string"
 					},
+					"required_acks": {
+						"default": 1,
+						"enum": [-1, 0, 1],
+						"type": "integer"
+					},
 					"retry_delay": {
 						"default": 1,
 						"minimum": 0,
@@ -3548,6 +3640,10 @@
 			"schema": {
 				"$comment": "this is a mark for our injected plugin schema",
 				"properties": {
+					"allow_degradation": {
+						"default": false,
+						"type": "boolean"
+					},
 					"burst": {
 						"minimum": 0,
 						"type": "integer"
@@ -3564,12 +3660,28 @@
 						"type": "boolean"
 					},
 					"key": {
-						"enum": ["remote_addr", "server_addr"],
+						"enum": [
+							"consumer_name",
+							"http_x_forwarded_for",
+							"http_x_real_ip",
+							"remote_addr",
+							"server_addr"
+						],
 						"type": "string"
 					},
 					"only_use_default_delay": {
 						"default": false,
 						"type": "boolean"
+					},
+					"rejected_code": {
+						"default": 503,
+						"maximum": 599,
+						"minimum": 200,
+						"type": "integer"
+					},
+					"rejected_msg": {
+						"minLength": 1,
+						"type": "string"
 					}
 				},
 				"required": ["burst", "conn", "default_conn_delay", "key"],
@@ -3744,7 +3856,6 @@
 			"priority": 100,
 			"schema": {
 				"$comment": "this is a mark for our injected plugin schema",
-				"additionalProperties": false,
 				"properties": {
 					"disable": {
 						"type": "boolean"
@@ -3752,6 +3863,7 @@
 				},
 				"type": "object"
 			},
+			"scope": "global",
 			"version": 0.1
 		},
 		"mqtt-proxy": {
@@ -3797,7 +3909,6 @@
 			"priority": 1000,
 			"schema": {
 				"$comment": "this is a mark for our injected plugin schema",
-				"additionalProperties": false,
 				"properties": {
 					"disable": {
 						"type": "boolean"
@@ -3805,6 +3916,7 @@
 				},
 				"type": "object"
 			},
+			"scope": "global",
 			"version": 0.1
 		},
 		"openid-connect": {
@@ -3994,6 +4106,12 @@
 					"host": {
 						"pattern": "^http(s)?:\\/\\/[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:[0-9]{1,5})?$",
 						"type": "string"
+					},
+					"sample_ratio": {
+						"default": 1,
+						"maximum": 1,
+						"minimum": 0.00001,
+						"type": "number"
 					}
 				},
 				"required": ["host"],
@@ -4143,7 +4261,20 @@
 			"priority": 2990,
 			"schema": {
 				"$comment": "this is a mark for our injected plugin schema",
+				"oneOf": [{
+						"required": ["whitelist"]
+					},{
+						"required": ["blacklist"]
+					}],
 				"properties": {
+					"blacklist": {
+						"items": {
+							"pattern": "^\\*?[0-9a-zA-Z-._]+$",
+							"type": "string"
+						},
+						"minItems": 1,
+						"type": "array"
+					},
 					"bypass_missing": {
 						"default": false,
 						"type": "boolean"
@@ -4151,6 +4282,12 @@
 					"disable": {
 						"type": "boolean"
 					},
+					"message": {
+						"default": "Your referer host is not allowed",
+						"maxLength": 1024,
+						"minLength": 1,
+						"type": "string"
+					},
 					"whitelist": {
 						"items": {
 							"pattern": "^\\*?[0-9a-zA-Z-._]+$",
@@ -4160,7 +4297,6 @@
 						"type": "array"
 					}
 				},
-				"required": ["whitelist"],
 				"type": "object"
 			},
 			"version": 0.1
@@ -4270,6 +4406,7 @@
 				},
 				"type": "object"
 			},
+			"scope": "global",
 			"version": 0.1
 		},
 		"serverless-post-function": {
@@ -5124,6 +5261,10 @@
 						"type": "array",
 						"uniqueItems": true
 					},
+					"case_insensitive": {
+						"default": false,
+						"type": "boolean"
+					},
 					"disable": {
 						"type": "boolean"
 					},
@@ -5204,5 +5345,166 @@
 			},
 			"version": 0.1
 		}
+	},
+	"stream_plugins": {
+		"ip-restriction": {
+			"priority": 3000,
+			"schema": {
+				"$comment": "this is a mark for our injected plugin schema",
+				"oneOf": [{
+						"required": ["whitelist"]
+					}, {
+						"required": ["blacklist"]
+					}],
+				"properties": {
+					"blacklist": {
+						"items": {
+							"anyOf": [
+								{
+									"format": "ipv4",
+									"title": "IPv4",
+									"type": "string"
+								},
+								{
+									"pattern": "^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([12]?[0-9]|3[0-2])$",
+									"title": "IPv4/CIDR",
+									"type": "string"
+								},
+								{
+									"format": "ipv6",
+									"title": "IPv6",
+									"type": "string"
+								},
+								{
+									"pattern": "^([a-fA-F0-9]{0,4}:){1,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?/[0-9]{1,3}$",
+									"title": "IPv6/CIDR",
+									"type": "string"
+								}
+							]
+						},
+						"minItems": 1,
+						"type": "array"
+					},
+					"disable": {
+						"type": "boolean"
+					},
+					"message": {
+						"default": "Your IP address is not allowed",
+						"maxLength": 1024,
+						"minLength": 1,
+						"type": "string"
+					},
+					"whitelist": {
+						"items": {
+							"anyOf": [
+								{
+									"format": "ipv4",
+									"title": "IPv4",
+									"type": "string"
+								},
+								{
+									"pattern": "^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([12]?[0-9]|3[0-2])$",
+									"title": "IPv4/CIDR",
+									"type": "string"
+								},
+								{
+									"format": "ipv6",
+									"title": "IPv6",
+									"type": "string"
+								},
+								{
+									"pattern": "^([a-fA-F0-9]{0,4}:){1,8}(:[a-fA-F0-9]{0,4}){0,8}([a-fA-F0-9]{0,4})?/[0-9]{1,3}$",
+									"title": "IPv6/CIDR",
+									"type": "string"
+								}
+							]
+						},
+						"minItems": 1,
+						"type": "array"
+					}
+				},
+				"type": "object"
+			},
+			"version": 0.1
+		},
+		"limit-conn": {
+			"priority": 1003,
+			"schema": {
+				"$comment": "this is a mark for our injected plugin schema",
+				"properties": {
+					"burst": {
+						"minimum": 0,
+						"type": "integer"
+					},
+					"conn": {
+						"exclusiveMinimum": 0,
+						"type": "integer"
+					},
+					"default_conn_delay": {
+						"exclusiveMinimum": 0,
+						"type": "number"
+					},
+					"disable": {
+						"type": "boolean"
+					},
+					"key": {
+						"enum": ["remote_addr", "server_addr"],
+						"type": "string"
+					},
+					"only_use_default_delay": {
+						"default": false,
+						"type": "boolean"
+					}
+				},
+				"required": ["burst", "conn", "default_conn_delay", "key"],
+				"type": "object"
+			},
+			"version": 0.1
+		},
+		"mqtt-proxy": {
+			"priority": 1000,
+			"schema": {
+				"$comment": "this is a mark for our injected plugin schema",
+				"properties": {
+					"disable": {
+						"type": "boolean"
+					},
+					"protocol_level": {
+						"type": "integer"
+					},
+					"protocol_name": {
+						"type": "string"
+					},
+					"upstream": {
+						"oneOf": [{
+								"required": [
+									"host",
+									"port"
+								]
+							}, {
+								"required": [
+									"ip",
+									"port"
+								]
+							}],
+						"properties": {
+							"host": {
+								"type": "string"
+							},
+							"ip": {
+								"type": "string"
+							},
+							"port": {
+								"type": "number"
+							}
+						},
+						"type": "object"
+					}
+				},
+				"required": ["protocol_level", "protocol_name", "upstream"],
+				"type": "object"
+			},
+			"version": 0.1
+		}
 	}
 }
diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go
index 1d17c9a..5564ec5 100644
--- a/api/internal/core/entity/entity.go
+++ b/api/internal/core/entity/entity.go
@@ -254,6 +254,7 @@ type Service struct {
 	Script          string                 `json:"script,omitempty"`
 	Labels          map[string]string      `json:"labels,omitempty"`
 	EnableWebsocket bool                   `json:"enable_websocket,omitempty"`
+	Hosts           []string               `json:"hosts,omitempty"`
 }
 
 type Script struct {
diff --git a/api/test/docker/docker-compose.yaml b/api/test/docker/docker-compose.yaml
index 072f312..4782d0e 100644
--- a/api/test/docker/docker-compose.yaml
+++ b/api/test/docker/docker-compose.yaml
@@ -127,7 +127,7 @@ services:
 
   apisix:
     hostname: apisix_server1
-    image: apache/apisix:2.9-alpine
+    image: apache/apisix:2.10.0-alpine
     restart: always
     volumes:
       - ./apisix_config.yaml:/usr/local/apisix/conf/config.yaml:ro
diff --git a/api/test/e2enew/service/service_test.go b/api/test/e2enew/service/service_test.go
index f2c3cb3..a0d0665 100644
--- a/api/test/e2enew/service/service_test.go
+++ b/api/test/e2enew/service/service_test.go
@@ -24,8 +24,9 @@ import (
 	"github.com/onsi/ginkgo"
 	"github.com/onsi/gomega"
 
-	"github.com/apisix/manager-api/test/e2enew/base"
 	"github.com/onsi/ginkgo/extensions/table"
+
+	"github.com/apisix/manager-api/test/e2enew/base"
 )
 
 var _ = ginkgo.Describe("create service without plugin", func() {
@@ -614,3 +615,126 @@ var _ = ginkgo.Describe("test service delete", func() {
 			ExpectStatus: http.StatusNotFound,
 		}))
 })
+
+var _ = ginkgo.Describe("test service with hosts", func() {
+	var createServiceBody = map[string]interface{}{
+		"name": "testservice",
+		"upstream": map[string]interface{}{
+			"type": "roundrobin",
+			"nodes": []map[string]interface{}{
+				{
+					"host":   base.UpstreamIp,
+					"port":   1980,
+					"weight": 1,
+				},
+			},
+		},
+		"hosts": []string{
+			"test.com",
+			"test1.com",
+		},
+	}
+	_createServiceBody, err := json.Marshal(createServiceBody)
+	gomega.Expect(err).To(gomega.BeNil())
+
+	var createRouteBody = map[string]interface{}{
+		"id":   "r1",
+		"name": "route1",
+		"uri":  "/hello",
+		"upstream": map[string]interface{}{
+			"type": "roundrobin",
+			"nodes": map[string]interface{}{
+				base.UpstreamIp + ":1980": 1,
+			},
+		},
+		"service_id": "s1",
+	}
+	_createRouteBody, err := json.Marshal(createRouteBody)
+	gomega.Expect(err).To(gomega.BeNil())
+
+	table.DescribeTable("test service with hosts",
+		func(tc func() base.HttpTestCase) {
+			base.RunTestCase(tc())
+		},
+		table.Entry("create service with hosts params", func() base.HttpTestCase {
+			return base.HttpTestCase{
+				Desc:         "create service with hosts params",
+				Object:       base.ManagerApiExpect(),
+				Method:       http.MethodPut,
+				Path:         "/apisix/admin/services/s1",
+				Headers:      map[string]string{"Authorization": base.GetToken()},
+				Body:         string(_createServiceBody),
+				ExpectStatus: http.StatusOK,
+			}
+		}),
+		table.Entry("create route use service s1", func() base.HttpTestCase {
+			return base.HttpTestCase{
+				Desc:         "create route use service s1",
+				Object:       base.ManagerApiExpect(),
+				Method:       http.MethodPut,
+				Path:         "/apisix/admin/routes/r1",
+				Body:         string(_createRouteBody),
+				Headers:      map[string]string{"Authorization": base.GetToken()},
+				ExpectStatus: http.StatusOK,
+			}
+		}),
+		table.Entry("hit route by test.com", func() base.HttpTestCase {
+			return base.HttpTestCase{
+				Object: base.APISIXExpect(),
+				Method: http.MethodGet,
+				Path:   "/hello",
+				Headers: map[string]string{
+					"Host": "test.com",
+				},
+				ExpectStatus: http.StatusOK,
+				ExpectBody:   "hello world",
+				Sleep:        base.SleepTime,
+			}
+		}),
+		table.Entry("hit route by test1.com", func() base.HttpTestCase {
+			return base.HttpTestCase{
+				Object: base.APISIXExpect(),
+				Method: http.MethodGet,
+				Path:   "/hello",
+				Headers: map[string]string{
+					"Host": "test1.com",
+				},
+				ExpectStatus: http.StatusOK,
+				ExpectBody:   "hello world",
+				Sleep:        base.SleepTime,
+			}
+		}),
+		table.Entry("hit route by test2.com", func() base.HttpTestCase {
+			return base.HttpTestCase{
+				Object: base.APISIXExpect(),
+				Method: http.MethodGet,
+				Path:   "/hello",
+				Headers: map[string]string{
+					"Host": "test2.com",
+				},
+				ExpectStatus: http.StatusNotFound,
+				Sleep:        base.SleepTime,
+			}
+		}),
+		table.Entry("delete route", func() base.HttpTestCase {
+			return base.HttpTestCase{
+				Desc:         "delete route first",
+				Object:       base.ManagerApiExpect(),
+				Method:       http.MethodDelete,
+				Path:         "/apisix/admin/routes/r1",
+				Headers:      map[string]string{"Authorization": base.GetToken()},
+				ExpectStatus: http.StatusOK,
+			}
+		}),
+		table.Entry("delete service", func() base.HttpTestCase {
+			return base.HttpTestCase{
+				Desc:         "delete service success",
+				Object:       base.ManagerApiExpect(),
+				Method:       http.MethodDelete,
+				Path:         "/apisix/admin/services/s1",
+				Headers:      map[string]string{"Authorization": base.GetToken()},
+				ExpectStatus: http.StatusOK,
+			}
+		}),
+	)
+})
diff --git a/web/cypress/fixtures/plugin-dataset.json b/web/cypress/fixtures/plugin-dataset.json
index 0ae63b9..4bbae88 100644
--- a/web/cypress/fixtures/plugin-dataset.json
+++ b/web/cypress/fixtures/plugin-dataset.json
@@ -463,7 +463,6 @@
         "conn": 1,
         "burst": 0,
         "default_conn_delay": 0.1,
-        "rejected_code": 503,
         "key": "remote_addr"
       }
     },
@@ -472,16 +471,15 @@
       "data": {
         "conn": 1,
         "default_conn_delay": 0.1,
-        "rejected_code": 503,
         "key": "remote_addr"
       }
     },
     {
       "shouldValid": false,
       "data": {
-        "burst": 0,
+        "conn": 1,
+        "burst": -1,
         "default_conn_delay": 0.1,
-        "rejected_code": 503,
         "key": "remote_addr"
       }
     },
@@ -489,24 +487,31 @@
       "shouldValid": false,
       "data": {
         "conn": -1,
-        "burst": 0,
+        "burst": 1,
         "default_conn_delay": 0.1,
-        "rejected_code": 503,
         "key": "remote_addr"
       }
     },
     {
-      "shouldValid": true,
+      "shouldValid": false,
       "data": {
         "conn": 100,
         "burst": 50,
-        "default_conn_delay": 0.1,
-        "rejected_code": 503,
+        "default_conn_delay": -1,
         "key": "server_addr"
       }
     },
     {
-      "shouldValid": false,
+      "shouldValid": true,
+      "data": {
+        "conn": 5,
+        "burst": 1,
+        "default_conn_delay": 0.1,
+        "key": "consumer_name"
+      }
+    },
+    {
+      "shouldValid": true,
       "data": {
         "conn": 5,
         "burst": 1,
@@ -516,7 +521,7 @@
       }
     },
     {
-      "shouldValid": false,
+      "shouldValid": true,
       "data": {
         "conn": 5,
         "burst": 1,
@@ -528,11 +533,61 @@
     {
       "shouldValid": true,
       "data": {
-        "conn": 2,
+        "conn": 5,
         "burst": 1,
         "default_conn_delay": 0.1,
         "key": "remote_addr"
       }
+    },
+    {
+      "shouldValid": true,
+      "data": {
+        "conn": 5,
+        "burst": 1,
+        "default_conn_delay": 0.1,
+        "key": "server_addr"
+      }
+    },
+    {
+      "shouldValid": true,
+      "data": {
+        "conn": 5,
+        "burst": 1,
+        "default_conn_delay": 0.1,
+        "key": "server_addr",
+        "rejected_code": 503,
+        "rejected_msg": "test"
+      }
+    },
+    {
+      "shouldValid": false,
+      "data": {
+        "conn": 5,
+        "burst": 1,
+        "default_conn_delay": 0.1,
+        "key": "server_addr",
+        "rejected_code": 600
+      }
+    },
+    {
+      "shouldValid": false,
+      "data": {
+        "conn": 5,
+        "burst": 1,
+        "default_conn_delay": 0.1,
+        "key": "server_addr",
+        "rejected_msg": ""
+      }
+    },
+    {
+      "shouldValid": false,
+      "data": {
+        "conn": 5,
+        "burst": 1,
+        "default_conn_delay": 0.1,
+        "key": "server_addr",
+        "allow_degradation": 1
+      }
     }
   ],
   "limit-count": [
@@ -798,21 +853,35 @@
   ],
   "proxy-mirror": [
     {
+      "shouldValid": true,
+      "data": {
+        "host": "http://127.0.0.1"
+      }
+    },
+    {
       "shouldValid": false,
       "data": {
         "host": "127.0.0.1:1999"
       }
     },
     {
+      "shouldValid": false,
+      "data": {
+        "host": "http://127.0.0.1:1999/invalid_uri"
+      }
+    },
+    {
       "shouldValid": true,
       "data": {
-        "host": "http://127.0.0.1"
+        "host": "http://127.0.0.1",
+        "sample_ratio": 0.1
       }
     },
     {
       "shouldValid": false,
       "data": {
-        "host": "http://127.0.0.1:1999/invalid_uri"
+        "host": "http://127.0.0.1",
+        "sample_ratio": 2
       }
     }
   ],
@@ -951,6 +1020,20 @@
         "bypass_missing": true,
         "whitelist": ["*.xx.com", "yy.com"]
       }
+    },
+    {
+      "shouldValid": false,
+      "data": {
+        "whitelist": ["*.xx.com", "yy.com"],
+        "message": ""
+      }
+    },
+    {
+      "shouldValid": false,
+      "data": {
+        "whitelist": ["*.xx.com", "yy.com"],
+        "blacklist": ["*.xx.com", "yy.com"]
+      }
     }
   ],
   "request-id": [
@@ -1327,6 +1410,13 @@
       "data": {
         "block_rules": ["aa"]
       }
+    },
+    {
+      "shouldValid": true,
+      "data": {
+        "block_rules": ["aa"],
+        "case_insensitive": true
+      }
     }
   ],
   "zipkin": [
diff --git a/web/cypress/integration/plugin/create-edit-delete-plugin.spec.js b/web/cypress/integration/plugin/plugin-schema.spec.js
similarity index 77%
rename from web/cypress/integration/plugin/create-edit-delete-plugin.spec.js
rename to web/cypress/integration/plugin/plugin-schema.spec.js
index 6f5f433..73d6419 100644
--- a/web/cypress/integration/plugin/create-edit-delete-plugin.spec.js
+++ b/web/cypress/integration/plugin/plugin-schema.spec.js
@@ -16,8 +16,17 @@
  */
 /* eslint-disable */
 
-context('Enable and Delete Plugin List', () => {
+describe('Plugin Schema Test', () => {
   const timeout = 5000;
+  const cases = require('../../fixtures/plugin-dataset.json');
+  const pluginList = [];
+  const casesList = [];
+
+  // prepare plugin cases
+  let keys = Object.keys(cases);
+  let values = Object.values(cases);
+  pluginList.push(...keys);
+  casesList.push(...values);
 
   beforeEach(() => {
     cy.login();
@@ -30,10 +39,20 @@ context('Enable and Delete Plugin List', () => {
     cy.visit('/');
     cy.contains('Plugin').click();
     cy.contains('Enable').click();
+  });
+
+  pluginList.forEach((plugin, pluginIndex) => {
+    const cases = casesList[pluginIndex];
 
-    cy.fixture('plugin-dataset.json').as('cases');
-    cy.get('@cases').then((cases) => {
-      cy.configurePlugins(cases);
+    if (cases.length <= 0) {
+      it(`${plugin} plugin no cases`);
+      return;
+    }
+
+    cases.forEach((c, caseIndex) => {
+      it(`${plugin} plugin #${caseIndex + 1} case`, function () {
+        cy.configurePlugin({ name: plugin, cases: c });
+      });
     });
   });
 
diff --git a/web/cypress/integration/route/create-route-with-referer-restriction-form.spec.js b/web/cypress/integration/route/create-route-with-referer-restriction-form.spec.js
index f15ff2b..4ae865d 100644
--- a/web/cypress/integration/route/create-route-with-referer-restriction-form.spec.js
+++ b/web/cypress/integration/route/create-route-with-referer-restriction-form.spec.js
@@ -31,10 +31,13 @@ context('Create and delete route with referer-restriction form', () => {
     notification: '.ant-notification-notice-message',
     notificationCloseIcon: '.ant-notification-close-icon',
     deleteAlert: '.ant-modal-body',
-    whitlist: '#whitelist_0',
+    whitelist: '#whitelist_0',
+    whitelist_1: '#whitelist_1',
+    blacklist: '#blacklist_0',
+    blacklist_1: '#blacklist_1',
     alert: '.ant-form-item-explain-error [role=alert]',
-    newAdd: '.ant-btn-dashed',
-    whitlist_1: '#whitelist_1',
+    newAddWhitelist: '[data-cy=addWhitelist]',
+    newAddBlacklist: '[data-cy=addBlacklist]',
     passSwitcher: '#bypass_missing',
   };
 
@@ -86,27 +89,26 @@ context('Create and delete route with referer-restriction form', () => {
       });
 
     // config referer-restriction form without whitelist
-    cy.get(selector.whitlist).click();
-    cy.get(selector.alert).contains('Please Enter whitelist');
+    cy.get(selector.whitelist).click();
     cy.get(selector.drawer).within(() => {
       cy.contains('Submit').click({
         force: true,
       });
     });
     cy.get(selector.notification).should('contain', 'Invalid plugin data');
-    cy.get(selector.notificationCloseIcon).click();
+    cy.get(selector.notificationCloseIcon).click({ multiple: true });
 
     // config referer-restriction form with whitelist
-    cy.get(selector.whitlist).type(data.wrongIp);
-    cy.get(selector.whitlist).closest('div').next().children('span').should('not.exist');
+    cy.get(selector.whitelist).type(data.wrongIp);
+    cy.get(selector.whitelist).closest('div').next().children('span').should('exist');
     cy.get(selector.alert).should('exist');
-    cy.get(selector.whitlist).clear().type(data.correctIp);
+    cy.get(selector.whitelist).clear().type(data.correctIp);
     cy.get(selector.alert).should('not.exist');
 
-    cy.get(selector.newAdd).click();
-    cy.get(selector.whitlist).closest('div').next().children('span').should('exist');
-    cy.get(selector.whitlist_1).closest('div').next().children('span').should('exist');
-    cy.get(selector.whitlist_1).type(data.correctIp);
+    cy.get(selector.newAddWhitelist).click();
+    cy.get(selector.whitelist).closest('div').next().children('span').should('exist');
+    cy.get(selector.whitelist_1).closest('div').next().children('span').should('exist');
+    cy.get(selector.whitelist_1).type(data.correctIp);
     cy.get(selector.alert).should('not.exist');
 
     cy.get(selector.disabledSwitcher).click();
@@ -117,6 +119,41 @@ context('Create and delete route with referer-restriction form', () => {
     });
     cy.get(selector.drawer).should('not.exist');
 
+    // reopen plugin drawer for blacklist test
+    cy.contains('referer-restriction')
+      .parents(selector.pluginCardBordered)
+      .within(() => {
+        cy.get('button').click({
+          force: true,
+        });
+      });
+    cy.get(selector.drawer)
+      .should('be.visible')
+      .within(() => {
+        cy.get(selector.disabledSwitcher).click();
+        cy.get(selector.disabledSwitcher).should('have.class', data.activeClass);
+        cy.get(selector.passSwitcher).should('not.have.class', data.activeClass);
+      });
+    cy.get(selector.blacklist).type(data.correctIp);
+    cy.get(selector.newAddBlacklist).click();
+    cy.get(selector.blacklist_1).type(data.correctIp);
+    cy.get(selector.drawer).within(() => {
+      cy.contains('Submit').click({
+        force: true,
+      });
+    });
+    cy.get(selector.notification).should('contain', 'Invalid plugin data');
+    cy.get(selector.notificationCloseIcon).click({ multiple: true });
+    cy.get(selector.whitelist).closest('div').next().children('span').click();
+    cy.get(selector.whitelist).closest('div').next().children('span').click();
+    cy.get(selector.drawer).within(() => {
+      cy.contains('Submit').click({
+        force: true,
+      });
+    });
+    cy.get(selector.drawer).should('not.exist');
+
+    // create route
     cy.contains('button', 'Next').click();
     cy.contains('button', 'Submit').click();
     cy.contains(data.submitSuccess);
diff --git a/web/cypress/integration/service/create-edit-delete-service.spec.js b/web/cypress/integration/service/create-edit-delete-service.spec.js
index a34c3f7..26d2301 100644
--- a/web/cypress/integration/service/create-edit-delete-service.spec.js
+++ b/web/cypress/integration/service/create-edit-delete-service.spec.js
@@ -22,6 +22,9 @@ context('Create and Delete Service ', () => {
   const selector = {
     name: '#name',
     description: '#desc',
+    hosts_0: '#hosts_0',
+    hosts_1: '#hosts_1',
+    hosts_2: '#hosts_2',
     nodes_0_host: '#submitNodes_0_host',
     nodes_0_port: '#submitNodes_0_port',
     nodes_0_weight: '#submitNodes_0_weight',
@@ -64,6 +67,15 @@ context('Create and Delete Service ', () => {
 
     cy.get(selector.name).type(data.serviceName);
     cy.get(selector.description).type(data.description);
+
+    // add hosts
+    cy.get(selector.hosts_0).type('host0.com');
+    cy.get('[data-cy=addHost]').click();
+    cy.get(selector.hosts_1).type('host1.com');
+    cy.get('[data-cy=addHost]').click();
+    cy.get(selector.hosts_2).type('host2.com');
+
+    // add node
     cy.get(selector.nodes_0_host).click();
     cy.get(selector.nodes_0_host).type(data.ip1);
     cy.get(selector.nodes_0_port).clear().type(data.port0);
diff --git a/web/cypress/support/commands.js b/web/cypress/support/commands.js
index 72d18b3..990288a 100644
--- a/web/cypress/support/commands.js
+++ b/web/cypress/support/commands.js
@@ -33,7 +33,7 @@ Cypress.Commands.add('login', () => {
   });
 });
 
-Cypress.Commands.add('configurePlugins', (cases) => {
+Cypress.Commands.add('configurePlugin', ({ name, cases }) => {
   const timeout = 300;
   const domSelector = {
     name: '[data-cy-plugin-name]',
@@ -46,91 +46,100 @@ Cypress.Commands.add('configurePlugins', (cases) => {
     monacoMode: '[data-cy="monaco-mode"]',
     selectJSON: '.ant-select-dropdown [label=JSON]',
     monacoViewZones: '.view-zones',
+    notification: '.ant-notification-notice-message',
   };
 
-  cy.get(domSelector.name, { timeout }).then(function (cards) {
-    [...cards].forEach((card) => {
-      const name = card.innerText;
-      const pluginCases = cases[name] || [];
-      // eslint-disable-next-line consistent-return
-      pluginCases.forEach(({ shouldValid, data, type = '' }) => {
-        if (type === 'consumer') {
-          return true;
-        }
+  const shouldValid = cases.shouldValid;
+  const data = cases.data;
+  const type = cases.type;
 
-        cy.contains(name)
-          .parents(domSelector.parents)
-          .within(() => {
-            cy.get('button').click({
-              force: true,
-            });
-          });
+  if (type === 'consumer') {
+    cy.log('consumer schema case, skipping');
+    return;
+  }
 
-        // NOTE: wait for the Drawer to appear on the DOM
-        cy.focused(domSelector.drawer).should('exist');
-
-        cy.get(domSelector.monacoMode)
-          .invoke('text')
-          .then((text) => {
-            if (text === 'Form') {
-              cy.wait(5000);
-              cy.get(domSelector.monacoMode).should('be.visible');
-              cy.get(domSelector.monacoMode).click();
-              cy.get(domSelector.selectDropdown).should('be.visible');
-              cy.get(domSelector.selectJSON).click();
-            }
-          });
+  cy.get(domSelector.name, { timeout }).then(function (cards) {
+    let needCheck = false;
+    [...cards].forEach((item) => {
+      if (name === item.innerText) needCheck = true;
+    });
 
-        cy.get(domSelector.drawer, { timeout }).within(() => {
-          cy.get(domSelector.switch).click({
-            force: true,
-          });
-        });
+    if (!needCheck) {
+      cy.log('non global plugin, skipping');
+      return;
+    }
 
-        cy.get(domSelector.monacoMode)
-          .invoke('text')
-          .then((text) => {
-            if (text === 'Form') {
-              // FIXME: https://github.com/cypress-io/cypress/issues/7306
-              cy.wait(5000);
-              cy.get(domSelector.monacoMode).should('be.visible');
-              cy.get(domSelector.monacoMode).click();
-              cy.get(domSelector.selectDropdown).should('be.visible');
-              cy.get(domSelector.selectJSON).click();
-            }
-          });
-        // edit monaco
-        cy.get(domSelector.monacoViewZones).should('exist').click({ force: true });
-        cy.window().then((window) => {
-          window.monacoEditor.setValue(JSON.stringify(data));
-
-          cy.get(domSelector.drawer, { timeout }).within(() => {
-            cy.contains('Submit').click({
-              force: true,
-            });
-            cy.get(domSelector.drawer).should('not.exist');
-          });
+    cy.contains(name)
+      .parents(domSelector.parents)
+      .within(() => {
+        cy.get('button').click({
+          force: true,
         });
+      });
 
-        if (shouldValid === true) {
-          cy.get(domSelector.drawer).should('not.exist');
-        } else if (shouldValid === false) {
-          cy.get(this.domSelector.notification).should('contain', 'Invalid plugin data');
+    // NOTE: wait for the Drawer to appear on the DOM
+    cy.focused(domSelector.drawer).should('exist');
 
-          cy.get(domSelector.close).should('be.visible').click({
-            force: true,
-            multiple: true,
-          });
+    cy.get(domSelector.monacoMode)
+      .invoke('text')
+      .then((text) => {
+        if (text === 'Form') {
+          cy.wait(1000);
+          cy.get(domSelector.monacoMode).should('be.visible');
+          cy.get(domSelector.monacoMode).click();
+          cy.get(domSelector.selectDropdown).should('be.visible');
+          cy.get(domSelector.selectJSON).click();
+        }
+      });
+
+    cy.get(domSelector.drawer, { timeout }).within(() => {
+      cy.get(domSelector.switch).click({
+        force: true,
+      });
+    });
 
-          cy.get(domSelector.drawer, { timeout })
-            .invoke('show')
-            .within(() => {
-              cy.contains('Cancel').click({
-                force: true,
-              });
-            });
+    cy.get(domSelector.monacoMode)
+      .invoke('text')
+      .then((text) => {
+        if (text === 'Form') {
+          // FIXME: https://github.com/cypress-io/cypress/issues/7306
+          cy.wait(1000);
+          cy.get(domSelector.monacoMode).should('be.visible');
+          cy.get(domSelector.monacoMode).click();
+          cy.get(domSelector.selectDropdown).should('be.visible');
+          cy.get(domSelector.selectJSON).click();
         }
       });
+    // edit monaco
+    cy.get(domSelector.monacoViewZones).should('exist').click({ force: true });
+    cy.window().then((window) => {
+      window.monacoEditor.setValue(JSON.stringify(data));
+
+      cy.get(domSelector.drawer, { timeout }).within(() => {
+        cy.contains('Submit').click({
+          force: true,
+        });
+        cy.get(domSelector.drawer).should('not.exist');
+      });
     });
+
+    if (shouldValid === true) {
+      cy.get(domSelector.drawer).should('not.exist');
+    } else if (shouldValid === false) {
+      cy.get(domSelector.notification).should('contain', 'Invalid plugin data');
+
+      cy.get(domSelector.close).should('be.visible').click({
+        force: true,
+        multiple: true,
+      });
+
+      cy.get(domSelector.drawer, { timeout })
+        .invoke('show')
+        .within(() => {
+          cy.contains('Cancel').click({
+            force: true,
+          });
+        });
+    }
   });
 });
diff --git a/web/src/components/Plugin/PluginDetail.tsx b/web/src/components/Plugin/PluginDetail.tsx
index 935dea6..14fd54a 100644
--- a/web/src/components/Plugin/PluginDetail.tsx
+++ b/web/src/components/Plugin/PluginDetail.tsx
@@ -124,8 +124,9 @@ const PluginDetail: React.FC<Props> = ({
   }
 
   const getUIFormData = () => {
+    const formData = UIForm.getFieldsValue();
+
     if (name === 'cors') {
-      const formData = UIForm.getFieldsValue();
       const newMethods = formData.allow_methods.join(',');
       const compactAllowRegex = compact(formData.allow_origins_by_regex);
       // Note: default allow_origins_by_regex setted for UI is [''], but this is not allowed, omit it.
@@ -135,7 +136,23 @@ const PluginDetail: React.FC<Props> = ({
 
       return { ...formData, allow_methods: newMethods };
     }
-    return UIForm.getFieldsValue();
+
+    if (name === 'referer-restriction') {
+      if ('whitelist' in formData) {
+        formData.whitelist = formData.whitelist.filter((item: string) => !!item);
+        if (formData.whitelist <= 0) {
+          delete formData.whitelist;
+        }
+      }
+      if ('blacklist' in formData) {
+        formData.blacklist = formData.blacklist.filter((item: string) => !!item);
+        if (formData.blacklist <= 0) {
+          delete formData.blacklist;
+        }
+      }
+    }
+
+    return formData;
   };
 
   const setUIFormData = (formData: any) => {
diff --git a/web/src/components/Plugin/UI/limit-conn.tsx b/web/src/components/Plugin/UI/limit-conn.tsx
index 9928e6b..3a81795 100644
--- a/web/src/components/Plugin/UI/limit-conn.tsx
+++ b/web/src/components/Plugin/UI/limit-conn.tsx
@@ -16,7 +16,7 @@
  */
 import React from 'react';
 import type { FormInstance } from 'antd/es/form';
-import { Form, InputNumber, Select, Switch } from 'antd';
+import { Form, Input, InputNumber, Select, Switch } from 'antd';
 import { useIntl } from 'umi';
 
 type Props = {
@@ -27,7 +27,7 @@ type Props = {
 
 const FORM_ITEM_LAYOUT = {
   labelCol: {
-    span: 6,
+    span: 8,
   },
   wrapperCol: {
     span: 8,
@@ -36,10 +36,14 @@ const FORM_ITEM_LAYOUT = {
 
 const LimitConn: React.FC<Props> = ({ form, schema }) => {
   const { formatMessage } = useIntl();
-  const propertires = schema?.properties;
+  const properties = schema?.properties;
   const onlyUseDefaultDelay = form.getFieldValue('only_use_default_delay')
     ? form.getFieldValue('only_use_default_delay')
     : false;
+  const allowDegradation = form.getFieldValue('allow_degradation')
+    ? form.getFieldValue('allow_degradation')
+    : false;
+
   return (
     <Form form={form} {...FORM_ITEM_LAYOUT}>
       <Form.Item
@@ -48,7 +52,7 @@ const LimitConn: React.FC<Props> = ({ form, schema }) => {
         name="conn"
         tooltip={formatMessage({ id: 'component.pluginForm.limit-conn.conn.tooltip' })}
       >
-        <InputNumber min={propertires.conn.exclusiveMinimum} required />
+        <InputNumber min={properties.conn.exclusiveMinimum} required />
       </Form.Item>
       <Form.Item
         label="burst"
@@ -56,7 +60,7 @@ const LimitConn: React.FC<Props> = ({ form, schema }) => {
         name="burst"
         tooltip={formatMessage({ id: 'component.pluginForm.limit-conn.burst.tooltip' })}
       >
-        <InputNumber min={propertires.burst.minimum} required />
+        <InputNumber min={properties.burst.minimum} required />
       </Form.Item>
       <Form.Item
         label="default_conn_delay"
@@ -66,20 +70,18 @@ const LimitConn: React.FC<Props> = ({ form, schema }) => {
           id: 'component.pluginForm.limit-conn.default_conn_delay.tooltip',
         })}
       >
-        <InputNumber step={0.001} min={propertires.default_conn_delay.exclusiveMinimum} required />
+        <InputNumber step={0.001} min={properties.default_conn_delay.exclusiveMinimum} required />
       </Form.Item>
-
       <Form.Item
         label="only_use_default_delay"
         name="only_use_default_delay"
-        initialValue={propertires.only_use_default_delay.default}
+        initialValue={properties.only_use_default_delay.default}
         tooltip={formatMessage({
           id: 'component.pluginForm.limit-conn.only_use_default_delay.tooltip',
         })}
       >
         <Switch defaultChecked={onlyUseDefaultDelay} />
       </Form.Item>
-
       <Form.Item
         label="key"
         required
@@ -87,7 +89,7 @@ const LimitConn: React.FC<Props> = ({ form, schema }) => {
         tooltip={formatMessage({ id: 'component.pluginForm.limit-conn.key.tooltip' })}
       >
         <Select>
-          {propertires.key.enum.map((item: string) => {
+          {properties.key.enum.map((item: string) => {
             return (
               <Select.Option value={item} key={item}>
                 {item}
@@ -96,6 +98,34 @@ const LimitConn: React.FC<Props> = ({ form, schema }) => {
           })}
         </Select>
       </Form.Item>
+      <Form.Item
+        label="rejected_code"
+        name="rejected_code"
+        tooltip={formatMessage({ id: 'component.pluginForm.limit-conn.rejected_code.tooltip' })}
+        initialValue={properties.rejected_code.default}
+      >
+        <InputNumber
+          max={properties.rejected_code.maximum}
+          min={properties.rejected_code.minimum}
+        />
+      </Form.Item>
+      <Form.Item
+        label="rejected_msg"
+        name="rejected_msg"
+        tooltip={formatMessage({ id: 'component.pluginForm.limit-conn.rejected_msg.tooltip' })}
+      >
+        <Input min={1} />
+      </Form.Item>
+      <Form.Item
+        label="allow_degradation"
+        name="allow_degradation"
+        initialValue={properties.allow_degradation.default}
+        tooltip={formatMessage({
+          id: 'component.pluginForm.limit-conn.only_use_default_delay.tooltip',
+        })}
+      >
+        <Switch defaultChecked={allowDegradation} />
+      </Form.Item>
     </Form>
   );
 };
diff --git a/web/src/components/Plugin/UI/proxy-mirror.tsx b/web/src/components/Plugin/UI/proxy-mirror.tsx
index a1285bd..5b19cab 100644
--- a/web/src/components/Plugin/UI/proxy-mirror.tsx
+++ b/web/src/components/Plugin/UI/proxy-mirror.tsx
@@ -16,7 +16,7 @@
  */
 import React from 'react';
 import type { FormInstance } from 'antd/es/form';
-import { Form, Input } from 'antd';
+import { Form, Input, InputNumber } from 'antd';
 import { useIntl } from 'umi';
 
 type Props = {
@@ -53,6 +53,21 @@ const ProxyMirror: React.FC<Props> = ({ form, schema }) => {
       >
         <Input />
       </Form.Item>
+      <Form.Item
+        label="sample_ratio"
+        name="sample_ratio"
+        tooltip={formatMessage({
+          id: 'component.pluginForm.proxy-mirror.sample_ratio.tooltip',
+        })}
+        required
+      >
+        <InputNumber
+          step={0.00001}
+          min={properties.sample_ratio.minimum}
+          max={properties.sample_ratio.maximum}
+          required
+        />
+      </Form.Item>
     </Form>
   );
 };
diff --git a/web/src/components/Plugin/UI/referer-restriction.tsx b/web/src/components/Plugin/UI/referer-restriction.tsx
index ded1728..a32e9c8 100644
--- a/web/src/components/Plugin/UI/referer-restriction.tsx
+++ b/web/src/components/Plugin/UI/referer-restriction.tsx
@@ -48,17 +48,20 @@ const removeBtnStyle = {
 };
 
 const RefererRestriction: React.FC<Props> = ({ form, schema }) => {
-  const { formatMessage } = useIntl()
-  const properties = schema?.properties
-  const allowListMinLength = properties.whitelist.minItems
-  const whiteInit = Array(allowListMinLength).join('.').split('.')
+  const { formatMessage } = useIntl();
+  const properties = schema?.properties;
+  const allowWhitelistMinLength = properties.whitelist.minItems;
+  const allowBlacklistMinLength = properties.blacklist.minItems;
+  const whiteInit = Array(allowWhitelistMinLength).join('.').split('.');
+  const blackInit = Array(allowBlacklistMinLength).join('.').split('.');
+
   return (
     <Form
       form={form}
       {...FORM_ITEM_LAYOUT}
-      initialValues={{ whitelist: whiteInit }}
+      initialValues={{ whitelist: whiteInit, blacklist: blackInit }}
     >
-      <Form.List name="whitelist">
+      <Form.List name="whitelist" initialValue={[]}>
         {(fields, { add, remove }) => {
           return (
             <div>
@@ -70,9 +73,83 @@ const RefererRestriction: React.FC<Props> = ({ form, schema }) => {
                 tooltip={formatMessage({
                   id: 'component.pluginForm.referer-restriction.whitelist.tooltip',
                 })}
-                required
                 style={{ marginBottom: 0 }}
               >
+                {fields.length === 0 && (
+                  <span style={{ ...removeBtnStyle, marginLeft: 0 }}>
+                    {formatMessage({
+                      id: 'component.pluginForm.referer-restriction.listEmpty.tooltip',
+                    })}
+                  </span>
+                )}
+                {fields.map((field, index) => (
+                  <Row style={{ marginBottom: 10 }} gutter={16} key={index}>
+                    <Col span={10}>
+                      <Form.Item
+                        {...field}
+                        validateTrigger={['onChange', 'onBlur', 'onClick']}
+                        noStyle
+                        rules={[
+                          {
+                            message: formatMessage({
+                              id: 'page.route.form.itemRulesPatternMessage.domain',
+                            }),
+                            pattern: new RegExp(`${properties.whitelist.items.pattern}`, 'g'),
+                          },
+                        ]}
+                      >
+                        <Input />
+                      </Form.Item>
+                    </Col>
+                    <Col style={{ ...removeBtnStyle, marginLeft: -10 }}>
+                      {fields.length > 0 && (
+                        <MinusCircleOutlined
+                          className="dynamic-delete-button"
+                          onClick={() => {
+                            remove(field.name);
+                          }}
+                        />
+                      )}
+                    </Col>
+                  </Row>
+                ))}
+              </Form.Item>
+              <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
+                <Button
+                  type="dashed"
+                  data-cy="addWhitelist"
+                  onClick={() => {
+                    add();
+                  }}
+                >
+                  <PlusOutlined /> {formatMessage({ id: 'component.global.add' })}
+                </Button>
+              </Form.Item>
+            </div>
+          );
+        }}
+      </Form.List>
+      <Form.List name="blacklist" initialValue={[]}>
+        {(fields, { add, remove }) => {
+          return (
+            <div>
+              <Form.Item
+                extra={formatMessage({
+                  id: 'component.pluginForm.referer-restriction.blacklist.tooltip',
+                })}
+                label="blacklist"
+                tooltip={formatMessage({
+                  id: 'component.pluginForm.referer-restriction.blacklist.tooltip',
+                })}
+                style={{ marginBottom: 0 }}
+              >
+                {fields.length === 0 && (
+                  <span style={{ ...removeBtnStyle, marginLeft: 0 }}>
+                    {formatMessage({
+                      id: 'component.pluginForm.referer-restriction.listEmpty.tooltip',
+                    })}
+                  </span>
+                )}
                 {fields.map((field, index) => (
                   <Row style={{ marginBottom: 10 }} gutter={16} key={index}>
                     <Col span={10}>
@@ -80,29 +157,27 @@ const RefererRestriction: React.FC<Props> = ({ form, schema }) => {
                         {...field}
                         validateTrigger={['onChange', 'onBlur', 'onClick']}
                         noStyle
-                        required
-                        rules={[{
-                          message: formatMessage({
-                            id: 'page.route.form.itemRulesPatternMessage.domain',
-                          }),
-                          pattern: new RegExp(`${properties.whitelist.items.pattern}`, 'g')
-                        }, {
-                          required: true,
-                          message: `${formatMessage({ id: 'component.global.pleaseEnter' })} whitelist`
-                        }]}
+                        rules={[
+                          {
+                            message: formatMessage({
+                              id: 'page.route.form.itemRulesPatternMessage.domain',
+                            }),
+                            pattern: new RegExp(`${properties.blacklist.items.pattern}`, 'g'),
+                          },
+                        ]}
                       >
                         <Input />
                       </Form.Item>
                     </Col>
                     <Col style={{ ...removeBtnStyle, marginLeft: -10 }}>
-                      {fields.length > allowListMinLength &&
+                      {fields.length > 0 && (
                         <MinusCircleOutlined
                           className="dynamic-delete-button"
                           onClick={() => {
                             remove(field.name);
                           }}
                         />
-                      }
+                      )}
                     </Col>
                   </Row>
                 ))}
@@ -110,6 +185,7 @@ const RefererRestriction: React.FC<Props> = ({ form, schema }) => {
               <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
                 <Button
                   type="dashed"
+                  data-cy="addBlacklist"
                   onClick={() => {
                     add();
                   }}
@@ -134,6 +210,13 @@ const RefererRestriction: React.FC<Props> = ({ form, schema }) => {
       >
         <Switch defaultChecked={properties.bypass_missing.default} />
       </Form.Item>
+      <Form.Item
+        label="message"
+        name="message"
+        tooltip={formatMessage({ id: 'component.pluginForm.referer-restriction.message.tooltip' })}
+      >
+        <Input min={1} max={1024} placeholder={properties.message.default} />
+      </Form.Item>
     </Form>
   );
 };
diff --git a/web/src/components/Plugin/locales/en-US.ts b/web/src/components/Plugin/locales/en-US.ts
index 39bdb31..ec28027 100644
--- a/web/src/components/Plugin/locales/en-US.ts
+++ b/web/src/components/Plugin/locales/en-US.ts
@@ -52,8 +52,13 @@ export default {
   // referer-restriction
   'component.pluginForm.referer-restriction.whitelist.tooltip':
     'List of hostname to whitelist. The hostname can be started with * as a wildcard.',
+  'component.pluginForm.referer-restriction.blacklist.tooltip':
+    'List of hostname to blacklist. The hostname can be started with * as a wildcard.',
+  'component.pluginForm.referer-restriction.listEmpty.tooltip': 'List empty',
   'component.pluginForm.referer-restriction.bypass_missing.tooltip':
     'Whether to bypass the check when the Referer header is missing or malformed.',
+  'component.pluginForm.referer-restriction.message.tooltip':
+    'Message returned in case access is not allowed.',
 
   // api-breaker
   'component.pluginForm.api-breaker.break_response_code.tooltip':
@@ -73,6 +78,8 @@ export default {
   'component.pluginForm.proxy-mirror.host.extra': 'e.g. http://127.0.0.1:9797',
   'component.pluginForm.proxy-mirror.host.ruletip':
     'address needs to contain schema: http or https, not URI part',
+  'component.pluginForm.proxy-mirror.sample_ratio.tooltip':
+    'the sample ratio that requests will be mirrored.',
 
   // limit-conn
   'component.pluginForm.limit-conn.conn.tooltip':
@@ -85,8 +92,12 @@ export default {
     'to limit the concurrency level. For example, one can use the host name (or server zone) as the key so that we limit concurrency per host name. Otherwise, we can also use the client address as the key so that we can avoid a single client from flooding our service with too many parallel connections or requests. Now accept those as key: "remote_addr"(client\'s IP), "server_addr"(server\'s IP), "X-Forwarded-For/X-Real-IP" in request header, "consumer_name"(consumer\'s username).',
   'component.pluginForm.limit-conn.rejected_code.tooltip':
     'returned when the request exceeds conn + burst will be rejected.',
+  'component.pluginForm.limit-conn.rejected_msg.tooltip':
+    'the response body returned when the request exceeds conn + burst will be rejected.',
   'component.pluginForm.limit-conn.only_use_default_delay.tooltip':
     'enable the strict mode of the latency seconds. If you set this option to true, it will run strictly according to the latency seconds you set without additional calculation logic.',
+  'component.pluginForm.limit-conn.allow_degradation.tooltip':
+    'Whether to enable plugin degradation when the limit-conn function is temporarily unavailable. Allow requests to continue when the value is set to true, default false.',
 
   // limit-req
   'component.pluginForm.limit-req.rate.tooltip':
diff --git a/web/src/components/Plugin/locales/zh-CN.ts b/web/src/components/Plugin/locales/zh-CN.ts
index 324ae28..04cacbd 100644
--- a/web/src/components/Plugin/locales/zh-CN.ts
+++ b/web/src/components/Plugin/locales/zh-CN.ts
@@ -50,9 +50,13 @@ export default {
 
   // referer-restriction
   'component.pluginForm.referer-restriction.whitelist.tooltip':
-    "域名列表。域名开头可以用'*'作为通配符。",
+    "白名单域名列表。域名开头可以用'*'作为通配符。",
+  'component.pluginForm.referer-restriction.blacklist.tooltip':
+    "黑名单域名列表。域名开头可以用'*'作为通配符。",
+  'component.pluginForm.referer-restriction.listEmpty.tooltip': '列表为空',
   'component.pluginForm.referer-restriction.bypass_missing.tooltip':
     '当 Referer 不存在或格式有误时,是否绕过检查。',
+  'component.pluginForm.referer-restriction.message.tooltip': '在未允许访问的情况下返回的信息。',
 
   // api-breaker
   'component.pluginForm.api-breaker.break_response_code.tooltip': '不健康返回错误码。',
@@ -69,6 +73,7 @@ export default {
   'component.pluginForm.proxy-mirror.host.extra': '例如:http://127.0.0.1:9797',
   'component.pluginForm.proxy-mirror.host.ruletip':
     '地址中需要包含 schema :http或https,不能包含 URI 部分',
+  'component.pluginForm.proxy-mirror.sample_ratio.tooltip': '镜像请求采样率',
 
   // limit-conn
   'component.pluginForm.limit-conn.conn.tooltip':
@@ -80,8 +85,12 @@ export default {
     '用户指定的限制并发级别的关键字,可以是客户端 IP 或服务端 IP。例如,可以使用主机名(或服务器区域)作为关键字,以便限制每个主机名的并发性。 否则,我们也可以使用客户端地址作为关键字,这样我们就可以避免单个客户端用太多的并行连接或请求淹没我们的服务。当前接受的 key 有:"remote_addr"(客户端 IP 地址), "server_addr"(服务端 IP 地址), 请求头中的"X-Forwarded-For" 或 "X-Real-IP", "consumer_name"(consumer 的 username)。',
   'component.pluginForm.limit-conn.rejected_code.tooltip':
     '当请求超过 conn + burst 这个阈值时,返回的 HTTP 状态码。',
+  'component.pluginForm.limit-conn.rejected_msg.tooltip':
+    '当请求超过 conn + burst 这个阈值时,返回的响应体。',
   'component.pluginForm.limit-conn.only_use_default_delay.tooltip':
     '延迟时间的严格模式。 如果设置为true的话,将会严格按照设置的时间来进行延迟',
+  'component.pluginForm.limit-conn.allow_degradation.tooltip':
+    '当插件功能临时不可用时是否允许请求继续。当值设置为 true 时则自动允许请求继续,默认值是 false。',
 
   // limit-req
   'component.pluginForm.limit-req.rate.tooltip':
diff --git a/web/src/pages/Service/components/Step1.tsx b/web/src/pages/Service/components/Step1.tsx
index 7a9bedd..18b43f4 100644
--- a/web/src/pages/Service/components/Step1.tsx
+++ b/web/src/pages/Service/components/Step1.tsx
@@ -15,11 +15,13 @@
  * limitations under the License.
  */
 import React, { useEffect, useState } from 'react';
-import { Form, Input } from 'antd';
+import { Button, Col, Form, Input, Row } from 'antd';
 import { useIntl } from 'umi';
 
 import UpstreamForm from '@/components/Upstream';
 import { fetchUpstreamList } from '@/components/Upstream/service';
+import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
+import { FORM_ITEM_WITHOUT_LABEL } from '@/pages/Route/constants';
 
 const FORM_LAYOUT = {
   labelCol: {
@@ -67,6 +69,70 @@ const Step1: React.FC<ServiceModule.Step1PassProps> = ({
             placeholder={formatMessage({ id: 'component.global.description.required' })}
           />
         </Form.Item>
+        <Form.List name="hosts" initialValue={[undefined]}>
+          {(fields, { add, remove }) => {
+            return (
+              <div>
+                <Form.Item
+                  label={formatMessage({ id: 'page.service.fields.hosts' })}
+                  tooltip={formatMessage({ id: 'page.route.form.itemExtraMessage.domain' })}
+                  style={{ marginBottom: 0 }}
+                  wrapperCol={{ span: 24 }}
+                >
+                  {fields.map((field, index) => (
+                    <Row style={{ marginBottom: 10 }} key={index}>
+                      <Col span={9}>
+                        <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.service.fields.hosts.placeholder',
+                            })}
+                            disabled={disabled}
+                          />
+                        </Form.Item>
+                      </Col>
+                      <Col style={{ marginLeft: 10, display: 'flex', alignItems: 'center' }}>
+                        {!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
+                      data-cy="addHost"
+                      onClick={() => {
+                        add();
+                      }}
+                    >
+                      <PlusOutlined /> {formatMessage({ id: 'component.global.add' })}
+                    </Button>
+                  </Form.Item>
+                )}
+              </div>
+            );
+          }}
+        </Form.List>
       </Form>
       <UpstreamForm
         ref={upstreamRef}
diff --git a/web/src/pages/Service/locales/en-US.ts b/web/src/pages/Service/locales/en-US.ts
index 10bbe5d..975cd28 100644
--- a/web/src/pages/Service/locales/en-US.ts
+++ b/web/src/pages/Service/locales/en-US.ts
@@ -22,6 +22,8 @@ export default {
   'page.service.description':
     'A service consists of a combination of public plugin configuration and upstream target information in a route. Services are associated with Routes and Upstreams, and a service can correspond to a set of upstream nodes and can be bound by multiple routes.',
   'page.service.fields.name.required': 'Please enter service name',
+  'page.service.fields.hosts': 'Hosts',
+  'page.service.fields.hosts.placeholder': 'Please enter service hosts',
   'page.service.create': 'Create Service',
   'page.service.configure': 'Configure Service',
 };
diff --git a/web/src/pages/Service/locales/zh-CN.ts b/web/src/pages/Service/locales/zh-CN.ts
index c18e904..c492494 100644
--- a/web/src/pages/Service/locales/zh-CN.ts
+++ b/web/src/pages/Service/locales/zh-CN.ts
@@ -22,6 +22,8 @@ export default {
   'page.service.description':
     '服务由路由中公共的插件配置、上游目标信息组合而成。服务与路由、上游关联,一个服务可对应一组上游节点、可被多条路由绑定。',
   'page.service.fields.name.required': '请输入服务名称',
+  'page.service.fields.hosts': '域名',
+  'page.service.fields.hosts.placeholder': '请输入服务域名',
   'page.service.create': '创建服务',
   'page.service.configure': '配置服务',
 };
diff --git a/web/src/pages/Service/service.ts b/web/src/pages/Service/service.ts
index 659c3ef..d01f542 100644
--- a/web/src/pages/Service/service.ts
+++ b/web/src/pages/Service/service.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import { request } from 'umi';
+import { transformData } from './transform';
 
 export const fetchList = ({ current = 1, pageSize = 10, ...res }) =>
   request('/services', {
@@ -31,13 +32,13 @@ export const fetchList = ({ current = 1, pageSize = 10, ...res }) =>
 export const create = (data: ServiceModule.Entity) =>
   request('/services', {
     method: 'POST',
-    data,
+    data: transformData(data),
   });
 
 export const update = (serviceId: string, data: ServiceModule.Entity) =>
   request(`/services/${serviceId}`, {
     method: 'PUT',
-    data,
+    data: transformData(data),
   });
 
 export const remove = (serviceId: string) =>
diff --git a/web/src/pages/Service/typing.d.ts b/web/src/pages/Service/transform.ts
similarity index 58%
copy from web/src/pages/Service/typing.d.ts
copy to web/src/pages/Service/transform.ts
index 67aed68..64b1346 100644
--- a/web/src/pages/Service/typing.d.ts
+++ b/web/src/pages/Service/transform.ts
@@ -14,31 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace ServiceModule {
-  type Entity = {
-    name: string;
-    desc: string;
-    upstream: any;
-    upstream_id: string;
-    labels: string;
-    enable_websocket: boolean;
-    plugins: Record<string, any>;
-  };
+export const transformData = (data: ServiceModule.Entity): ServiceModule.Entity => {
+  const newData = data;
 
-  type ResponseBody = {
-    id: string;
-    plugins: Record<string, any>;
-    upstream_id: string;
-    upstream: Record<string, any>;
-    name: string;
-    desc: string;
-    enable_websocket: boolean;
-  };
+  if (newData.hosts) {
+    newData.hosts = newData.hosts.filter((item) => {
+      return !!item;
+    });
 
-  type Step1PassProps = {
-    form: FormInstance;
-    upstreamForm: FormInstance;
-    disabled?: boolean;
-    upstreamRef: any;
-  };
-}
+    if (newData.hosts.length <= 0) {
+      delete newData.hosts;
+    }
+  }
+
+  return newData;
+};
diff --git a/web/src/pages/Service/typing.d.ts b/web/src/pages/Service/typing.d.ts
index 67aed68..7bea45d 100644
--- a/web/src/pages/Service/typing.d.ts
+++ b/web/src/pages/Service/typing.d.ts
@@ -18,6 +18,7 @@ declare namespace ServiceModule {
   type Entity = {
     name: string;
     desc: string;
+    hosts?: string[];
     upstream: any;
     upstream_id: string;
     labels: string;