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 <[email protected]>
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;

Reply via email to