diff --git a/.github/workflows/auto-build-rpm.yml b/.github/workflows/auto-build-rpm.yml new file mode 100644 index 0000000000..7feaeb0480 --- /dev/null +++ b/.github/workflows/auto-build-rpm.yml @@ -0,0 +1,70 @@ +name: Auto Build RPM + +on: + push: + branches: [master, 'release/**'] + paths-ignore: + - 'docs/**' + pull_request: + branches: [master] + paths-ignore: + - 'docs/**' + +jobs: + auto_build_rpm: + name: auto build rpm package + runs-on: ubuntu-latest + + services: + etcd: + image: bitnami/etcd:3.4.0 + ports: + - 2379:2379 + - 2380:2380 + env: + ALLOW_NONE_AUTHENTICATION: yes + ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379 + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Extract branch name + id: branch_env + shell: bash + run: | + echo "##[set-output name=version;]$(echo ${GITHUB_REF##*/})" + + - name: Build rpm package + run: | + export VERSION=${{ steps.branch_env.outputs.version }} + sudo gem install --no-document fpm + git clone https://github.com/api7/apisix-build-tools.git + cd apisix-build-tools + export checkout=master + if [ "$VERSION" != "merge" && "$VERSION" != "master" ];then + export checkout=release/${VERSION} + fi + make package type=rpm app=dashboard version=${VERSION} checkout=${checkout} + + - name: Run centos7 docker and mapping apisix into container + run: | + docker run -itd -v $PWD:/apisix-dashboard --name centos7Instance --net="host" docker.io/centos:7 /bin/bash + + - name: Install rpm package + run: | + export VERSION=${{ steps.branch_env.outputs.version }} + docker exec centos7Instance bash -c "cd apisix-dashboard && yum install -y ./apisix-build-tools/output/apisix-dashboard-${VERSION}-0.x86_64.rpm" + docker logs centos7Instance + # Dependencies are attached with rpm, so revert `make deps` + docker exec centos7Instance bash -c "cd /usr/local/apisix/dashboard/ && nohup ./manager-api &" + + - name: Run test cases + run: | + api/test/shell/manager_smoking.sh -s true + + - name: Publish Artifact + uses: actions/upload-artifact@v2.2.2 + with: + name: "rpm" + path: "./apisix-build-tools/output/apisix-dashboard-${{ steps.branch_env.outputs.version }}-0.x86_64.rpm" diff --git a/.github/workflows/backend-e2e-test.yml b/.github/workflows/backend-e2e-test.yml index 781b44e1d1..a7a3545da7 100644 --- a/.github/workflows/backend-e2e-test.yml +++ b/.github/workflows/backend-e2e-test.yml @@ -45,6 +45,7 @@ jobs: docker-compose up -d sleep 5 docker logs docker_managerapi_1 + docker logs docker_apisix_1 - name: run test working-directory: ./api/test/e2e diff --git a/api/conf/schema.json b/api/conf/schema.json index e69c57aa6e..ff58ff4b78 100644 --- a/api/conf/schema.json +++ b/api/conf/schema.json @@ -10,17 +10,6 @@ "maxLength": 256, "type": "string" }, - "id": { - "anyOf": [{ - "maxLength": 64, - "minLength": 1, - "pattern": "^[a-zA-Z0-9-_.]+$", - "type": "string" - }, { - "minimum": 1, - "type": "integer" - }] - }, "labels": { "description": "key/value pairs to specify attributes", "maxProperties": 16, @@ -147,6 +136,27 @@ "maxLength": 1048576, "minLength": 1, "type": "string" + }, + "create_time": { + "type": "integer" + }, + "desc": { + "maxLength": 256, + "type": "string" + }, + "id": { + "anyOf": [{ + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-_.]+$", + "type": "string" + }, { + "minimum": 1, + "type": "integer" + }] + }, + "update_time": { + "type": "integer" } }, "required": ["content"], @@ -595,10 +605,6 @@ "description": "discovery type", "type": "string" }, - "enable_websocket": { - "description": "enable websocket for request", - "type": "boolean" - }, "hash_on": { "default": "vars", "enum": ["consumer", "cookie", "header", "vars", "vars_combinations"], @@ -664,6 +670,11 @@ "minimum": 1, "type": "integer" }, + "priority": { + "default": 0, + "description": "priority of node", + "type": "integer" + }, "weight": { "description": "weight of node", "minimum": 0, @@ -754,12 +765,6 @@ "uniqueItems": true }, "vars": { - "items": { - "description": "Nginx builtin variable name and value", - "maxItems": 4, - "minItems": 2, - "type": "array" - }, "type": "array" } }, @@ -1025,10 +1030,6 @@ "description": "discovery type", "type": "string" }, - "enable_websocket": { - "description": "enable websocket for request", - "type": "boolean" - }, "hash_on": { "default": "vars", "enum": ["consumer", "cookie", "header", "vars", "vars_combinations"], @@ -1094,6 +1095,11 @@ "minimum": 1, "type": "integer" }, + "priority": { + "default": 0, + "description": "priority of node", + "type": "integer" + }, "weight": { "description": "weight of node", "minimum": 0, @@ -1270,6 +1276,13 @@ }, "stream_route": { "properties": { + "create_time": { + "type": "integer" + }, + "desc": { + "maxLength": 256, + "type": "string" + }, "id": { "anyOf": [{ "maxLength": 64, @@ -1330,6 +1343,9 @@ "description": "server port", "type": "integer" }, + "update_time": { + "type": "integer" + }, "upstream": { "additionalProperties": false, "oneOf": [{ @@ -1535,10 +1551,6 @@ "description": "discovery type", "type": "string" }, - "enable_websocket": { - "description": "enable websocket for request", - "type": "boolean" - }, "hash_on": { "default": "vars", "enum": ["consumer", "cookie", "header", "vars", "vars_combinations"], @@ -1604,6 +1616,11 @@ "minimum": 1, "type": "integer" }, + "priority": { + "default": 0, + "description": "priority of node", + "type": "integer" + }, "weight": { "description": "weight of node", "minimum": 0, @@ -1887,10 +1904,6 @@ "description": "discovery type", "type": "string" }, - "enable_websocket": { - "description": "enable websocket for request", - "type": "boolean" - }, "hash_on": { "default": "vars", "enum": ["consumer", "cookie", "header", "vars", "vars_combinations"], @@ -1956,6 +1969,11 @@ "minimum": 1, "type": "integer" }, + "priority": { + "default": 0, + "description": "priority of node", + "type": "integer" + }, "weight": { "description": "weight of node", "minimum": 0, @@ -2297,54 +2315,59 @@ "priority": 2400, "schema": { "$comment": "this is a mark for our injected plugin schema", - "oneOf": [{ - "properties": { - "blacklist": { - "items": { - "type": "string" + "anyOf": [{ + "required": ["blacklist"] + }, { + "required": ["whitelist"] + }, { + "required": ["allowed_by_methods"] + }], + "properties": { + "allowed_by_methods": { + "items": { + "properties": { + "methods": { + "items": { + "enum": ["CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"], + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "user": { + "type": "string" + } }, - "minItems": 1, - "type": "array" - }, - "rejected_code": { - "default": 403, - "minimum": 200, - "type": "integer" + "type": "object" }, - "type": { - "default": "consumer_name", - "enum": ["consumer_name", "service_id"], - "type": "string" - } + "type": "array" }, - "required": ["blacklist"], - "title": "blacklist" - }, { - "properties": { - "rejected_code": { - "default": 403, - "minimum": 200, - "type": "integer" - }, - "type": { - "default": "consumer_name", - "enum": ["consumer_name", "service_id"], + "blacklist": { + "items": { "type": "string" }, - "whitelist": { - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - } + "minItems": 1, + "type": "array" }, - "required": ["whitelist"], - "title": "whitelist" - }], - "properties": { "disable": { "type": "boolean" + }, + "rejected_code": { + "default": 403, + "minimum": 200, + "type": "integer" + }, + "type": { + "default": "consumer_name", + "enum": ["consumer_name", "service_id"], + "type": "string" + }, + "whitelist": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" } }, "type": "object" @@ -2376,6 +2399,17 @@ "description": "you can use '*' to allow all origins when no credentials,'**' to allow forcefully(it will bring some security risks, be carefully),multiple origin use ',' to split. default: *.", "type": "string" }, + "allow_origins_by_regex": { + "description": "you can use regex to allow specific origins when no credentials,for example use [.*\\.test.com] to allow a.test.com and b.test.com", + "items": { + "maxLength": 4096, + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + }, "disable": { "type": "boolean" }, @@ -2608,6 +2642,9 @@ "type": "integer" }, "vars": { + "items": { + "type": "array" + }, "maxItems": 20, "type": "array" } @@ -2627,6 +2664,9 @@ "type": "integer" }, "vars": { + "items": { + "type": "array" + }, "maxItems": 20, "type": "array" } @@ -3682,6 +3722,9 @@ "maximum": 598, "minimum": 200, "type": "integer" + }, + "vars": { + "type": "array" } }, "type": "object" @@ -3762,6 +3805,9 @@ "default": "http://127.0.0.1:12800", "type": "string" }, + "report_interval": { + "type": "integer" + }, "service_instance_name": { "default": "APISIX Instance Name", "description": "User Service Instance Name", @@ -4028,43 +4074,6 @@ "items": { "properties": { "vars": { - "items": { - "additionalItems": { - "anyOf": [{ - "type": "string" - }, { - "type": "number" - }, { - "type": "boolean" - }, { - "items": { - "anyOf": [{ - "maxLength": 100, - "minLength": 1, - "type": "string" - }, { - "type": "number" - }, { - "type": "boolean" - }] - }, - "type": "array", - "uniqueItems": true - }] - }, - "items": [{ - "maxLength": 100, - "minLength": 1, - "type": "string" - }, { - "maxLength": 2, - "minLength": 1, - "type": "string" - }], - "maxItems": 10, - "minItems": 0, - "type": "array" - }, "type": "array" } }, @@ -4283,10 +4292,6 @@ "description": "discovery type", "type": "string" }, - "enable_websocket": { - "description": "enable websocket for request", - "type": "boolean" - }, "hash_on": { "default": "vars", "enum": ["consumer", "cookie", "header", "vars", "vars_combinations"], @@ -4352,6 +4357,11 @@ "minimum": 1, "type": "integer" }, + "priority": { + "default": 0, + "description": "priority of node", + "type": "integer" + }, "weight": { "description": "weight of node", "minimum": 0, @@ -4578,6 +4588,10 @@ "default": "APISIX", "description": "service name for zipkin reporter", "type": "string" + }, + "span_version": { + "default": 2, + "enum": [1, 2] } }, "required": ["endpoint", "sample_ratio"], diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index c42d31f9db..b81c7f4a6d 100644 --- a/api/internal/core/entity/entity.go +++ b/api/internal/core/entity/entity.go @@ -197,11 +197,12 @@ func (upstream *Upstream) Parse2NameResponse() (*UpstreamNameResponse, error) { // swagger:model Consumer type Consumer struct { - BaseInfo - Username string `json:"username"` - Desc string `json:"desc,omitempty"` - Plugins map[string]interface{} `json:"plugins,omitempty"` - Labels map[string]string `json:"labels,omitempty"` + Username string `json:"username"` + Desc string `json:"desc,omitempty"` + Plugins map[string]interface{} `json:"plugins,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + CreateTime int64 `json:"create_time,omitempty"` + UpdateTime int64 `json:"update_time,omitempty"` } // swagger:model SSL diff --git a/api/internal/core/store/store_mock.go b/api/internal/core/store/store_mock.go index 436ef6ba6c..c6801d69eb 100644 --- a/api/internal/core/store/store_mock.go +++ b/api/internal/core/store/store_mock.go @@ -18,6 +18,7 @@ package store import ( "context" + "sort" "github.com/stretchr/testify/mock" ) @@ -44,6 +45,15 @@ func (m *MockInterface) List(_ context.Context, input ListInput) (*ListOutput, e } else { r0 = ret.Get(0).(*ListOutput) } + + if input.Less == nil { + input.Less = defLessFunc + } + + sort.Slice(r0.Rows, func(i, j int) bool { + return input.Less(r0.Rows[i], r0.Rows[j]) + }) + r1 = ret.Error(1) return r0, r1 diff --git a/api/internal/core/store/validate_test.go b/api/internal/core/store/validate_test.go index 795fab3ec5..7b0b6f37fc 100644 --- a/api/internal/core/store/validate_test.go +++ b/api/internal/core/store/validate_test.go @@ -76,7 +76,6 @@ func TestAPISIXJsonSchemaValidator_Validate(t *testing.T) { consumer := &entity.Consumer{} reqBody := `{ - "id": "jack", "username": "jack", "plugins": { "limit-count": { @@ -93,26 +92,6 @@ func TestAPISIXJsonSchemaValidator_Validate(t *testing.T) { err = validator.Validate(consumer) assert.Nil(t, err) - consumer2 := &entity.Consumer{} - reqBody = `{ - "username": "jack", - "plugins": { - "limit-count": { - "count": 2, - "time_window": 60, - "rejected_code": 503, - "key": "remote_addr" - } - }, - "desc": "test description" - }` - err = json.Unmarshal([]byte(reqBody), consumer2) - assert.Nil(t, err) - - err = validator.Validate(consumer2) - assert.NotNil(t, err) - assert.EqualError(t, err, "schema validate failed: id: Must validate at least one schema (anyOf)\nid: Invalid type. Expected: string, given: null") - //check nil obj err = validator.Validate(nil) assert.NotNil(t, err) @@ -121,17 +100,16 @@ func TestAPISIXJsonSchemaValidator_Validate(t *testing.T) { //plugin schema fail consumer3 := &entity.Consumer{} reqBody = `{ - "id": "jack", - "username": "jack", - "plugins": { - "limit-count": { - "time_window": 60, - "rejected_code": 503, - "key": "remote_addr" - } - }, - "desc": "test description" - }` + "username": "jack", + "plugins": { + "limit-count": { + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + } + }, + "desc": "test description" + }` err = json.Unmarshal([]byte(reqBody), consumer3) assert.Nil(t, err) err = validator.Validate(consumer3) @@ -456,7 +434,6 @@ func TestAPISIXSchemaValidator_Validate(t *testing.T) { // normal config, should pass reqBody := `{ - "id": "jack", "username": "jack", "plugins": { "limit-count": { diff --git a/api/internal/handler/consumer/consumer.go b/api/internal/handler/consumer/consumer.go index 7efb238f97..0b169bbcbc 100644 --- a/api/internal/handler/consumer/consumer.go +++ b/api/internal/handler/consumer/consumer.go @@ -17,21 +17,18 @@ package consumer import ( - "net/http" "reflect" "strings" + "time" "github.com/gin-gonic/gin" "github.com/shiningrush/droplet" - "github.com/shiningrush/droplet/data" "github.com/shiningrush/droplet/wrapper" wgin "github.com/shiningrush/droplet/wrapper/gin" "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/core/store" "github.com/apisix/manager-api/internal/handler" - "github.com/apisix/manager-api/internal/utils" - "github.com/apisix/manager-api/internal/utils/consts" ) type Handler struct { @@ -120,6 +117,17 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) { } return true }, + Less: func(i, j interface{}) bool { + iBase := i.(*entity.Consumer) + jBase := j.(*entity.Consumer) + if iBase.CreateTime != jBase.CreateTime { + return iBase.CreateTime < jBase.CreateTime + } + if iBase.UpdateTime != jBase.UpdateTime { + return iBase.UpdateTime < jBase.UpdateTime + } + return iBase.Username < jBase.Username + }, PageSize: input.PageSize, PageNumber: input.PageNumber, }) @@ -137,16 +145,21 @@ type SetInput struct { func (h *Handler) Set(c droplet.Context) (interface{}, error) { input := c.Input().(*SetInput) - if input.ID != nil && utils.InterfaceToString(input.ID) != input.Username { - return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, - consts.ErrIDUsername - } if input.Username != "" { input.Consumer.Username = input.Username } - input.Consumer.ID = input.Consumer.Username ensurePluginsDefValue(input.Plugins) + // Because the ID of consumer has been removed, + // `BaseInfo` is no longer embedded in consumer's struct, + // So we need to maintain create_time and update_time separately for consumer + savedConsumer, _ := h.consumerStore.Get(c.Context(), input.Consumer.Username) + input.Consumer.CreateTime = time.Now().Unix() + input.Consumer.UpdateTime = time.Now().Unix() + if savedConsumer != nil { + input.Consumer.CreateTime = savedConsumer.(*entity.Consumer).CreateTime + } + ret, err := h.consumerStore.Update(c.Context(), &input.Consumer, true) if err != nil { return handler.SpecCodeResponse(err), err diff --git a/api/internal/handler/consumer/consumer_test.go b/api/internal/handler/consumer/consumer_test.go index 0bc5eab36f..9a76bc98b3 100644 --- a/api/internal/handler/consumer/consumer_test.go +++ b/api/internal/handler/consumer/consumer_test.go @@ -112,8 +112,8 @@ func TestHandler_List(t *testing.T) { }, wantRet: &store.ListOutput{ Rows: []interface{}{ - &entity.Consumer{Username: "testUser"}, &entity.Consumer{Username: "iam-testUser"}, + &entity.Consumer{Username: "testUser"}, &entity.Consumer{Username: "testUser-is-me"}, }, TotalSize: 3, @@ -195,9 +195,6 @@ func TestHandler_Create(t *testing.T) { }, giveCtx: context.WithValue(context.Background(), "test", "value"), giveRet: &entity.Consumer{ - BaseInfo: entity.BaseInfo{ - ID: "name", - }, Username: "name", Plugins: map[string]interface{}{ "jwt-auth": map[string]interface{}{ @@ -207,9 +204,6 @@ func TestHandler_Create(t *testing.T) { }, wantInput: &SetInput{ Consumer: entity.Consumer{ - BaseInfo: entity.BaseInfo{ - ID: "name", - }, Username: "name", Plugins: map[string]interface{}{ "jwt-auth": map[string]interface{}{ @@ -219,9 +213,6 @@ func TestHandler_Create(t *testing.T) { }, }, wantRet: &entity.Consumer{ - BaseInfo: entity.BaseInfo{ - ID: "name", - }, Username: "name", Plugins: map[string]interface{}{ "jwt-auth": map[string]interface{}{ @@ -249,9 +240,6 @@ func TestHandler_Create(t *testing.T) { giveErr: fmt.Errorf("create failed"), wantInput: &SetInput{ Consumer: entity.Consumer{ - BaseInfo: entity.BaseInfo{ - ID: "name", - }, Username: "name", Plugins: map[string]interface{}{ "jwt-auth": map[string]interface{}{ @@ -275,10 +263,12 @@ func TestHandler_Create(t *testing.T) { mStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { methodCalled = true assert.Equal(t, tc.giveCtx, args.Get(0)) - assert.Equal(t, &tc.wantInput.Consumer, args.Get(1)) assert.True(t, args.Bool(2)) }).Return(tc.giveRet, tc.giveErr) + mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { + }).Return(nil, nil) + h := Handler{consumerStore: mStore} ctx := droplet.NewContext() ctx.SetInput(tc.giveInput) @@ -302,6 +292,7 @@ func TestHandler_Update(t *testing.T) { wantInput *entity.Consumer wantRet interface{} wantCalled bool + getRet interface{} }{ { caseDesc: "normal", @@ -317,20 +308,15 @@ func TestHandler_Update(t *testing.T) { }, giveCtx: context.WithValue(context.Background(), "test", "value"), giveRet: &entity.Consumer{ - BaseInfo: entity.BaseInfo{ - ID: "name", - }, Username: "name", Plugins: map[string]interface{}{ "jwt-auth": map[string]interface{}{ "exp": 500, }, }, + CreateTime: 1618648423, }, wantInput: &entity.Consumer{ - BaseInfo: entity.BaseInfo{ - ID: "name", - }, Username: "name", Plugins: map[string]interface{}{ "jwt-auth": map[string]interface{}{ @@ -339,17 +325,20 @@ func TestHandler_Update(t *testing.T) { }, }, wantRet: &entity.Consumer{ - BaseInfo: entity.BaseInfo{ - ID: "name", - }, Username: "name", Plugins: map[string]interface{}{ "jwt-auth": map[string]interface{}{ "exp": 500, }, }, + CreateTime: 1618648423, }, wantCalled: true, + getRet: &entity.Consumer{ + Username: "name", + CreateTime: 1618648423, + UpdateTime: 1618648423, + }, }, { caseDesc: "store update failed", @@ -366,9 +355,6 @@ func TestHandler_Update(t *testing.T) { }, giveErr: fmt.Errorf("create failed"), wantInput: &entity.Consumer{ - BaseInfo: entity.BaseInfo{ - ID: "name", - }, Username: "name", Plugins: map[string]interface{}{ "jwt-auth": map[string]interface{}{ @@ -391,10 +377,12 @@ func TestHandler_Update(t *testing.T) { mStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { methodCalled = true assert.Equal(t, tc.giveCtx, args.Get(0)) - assert.Equal(t, tc.wantInput, args.Get(1)) assert.True(t, args.Bool(2)) }).Return(tc.giveRet, tc.giveErr) + mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { + }).Return(tc.getRet, nil) + h := Handler{consumerStore: mStore} ctx := droplet.NewContext() ctx.SetInput(tc.giveInput) @@ -403,6 +391,10 @@ func TestHandler_Update(t *testing.T) { assert.Equal(t, tc.wantCalled, methodCalled) assert.Equal(t, tc.wantRet, ret) assert.Equal(t, tc.wantErr, err) + if err == nil { + assert.Equal(t, tc.getRet.(*entity.Consumer).CreateTime, ret.(*entity.Consumer).CreateTime) + assert.NotEqual(t, tc.getRet.(*entity.Consumer).UpdateTime, ret.(*entity.Consumer).UpdateTime) + } }) } } diff --git a/api/internal/handler/label/label_test.go b/api/internal/handler/label/label_test.go index 664288d190..d227b95211 100644 --- a/api/internal/handler/label/label_test.go +++ b/api/internal/handler/label/label_test.go @@ -167,10 +167,6 @@ func genUpstream(labels map[string]string) *entity.Upstream { func genConsumer(labels map[string]string) *entity.Consumer { r := entity.Consumer{ - BaseInfo: entity.BaseInfo{ - ID: rand.Int(), - CreateTime: rand.Int63(), - }, Username: "test", Labels: labels, } diff --git a/api/test/docker/apisix_config.yaml b/api/test/docker/apisix_config.yaml index 00fc8782ad..e56a74d16a 100644 --- a/api/test/docker/apisix_config.yaml +++ b/api/test/docker/apisix_config.yaml @@ -22,7 +22,7 @@ etcd: - "http://172.16.238.10:2379" - "http://172.16.238.11:2379" - "http://172.16.238.12:2379" - resync_delay: 0.1 # sync data from etcd quickly for e2e test + resync_delay: 0 # sync data from etcd quickly for e2e test apisix: id: "apisix-server1" diff --git a/api/test/docker/apisix_config2.yaml b/api/test/docker/apisix_config2.yaml index bd788988c9..5d23725d52 100644 --- a/api/test/docker/apisix_config2.yaml +++ b/api/test/docker/apisix_config2.yaml @@ -22,7 +22,7 @@ etcd: - "http://172.16.238.10:2379" - "http://172.16.238.11:2379" - "http://172.16.238.12:2379" - resync_delay: 0.1 # sync data from etcd quickly for e2e test + resync_delay: 0 # sync data from etcd quickly for e2e test apisix: id: "apisix-server2" diff --git a/api/test/docker/docker-compose.yaml b/api/test/docker/docker-compose.yaml index 16d6e62d99..2da09908a2 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.4-alpine + image: apache/apisix:2.5-alpine restart: always volumes: - ./apisix_config.yaml:/usr/local/apisix/conf/config.yaml:ro @@ -148,7 +148,7 @@ services: apisix2: hostname: apisix_server2 - image: apache/apisix:2.4-alpine + image: apache/apisix:2.5-alpine restart: always volumes: - ./apisix_config2.yaml:/usr/local/apisix/conf/config.yaml:ro diff --git a/api/test/e2enew/schema/schema_test.go b/api/test/e2enew/schema/schema_test.go index 4f14d003b3..e856cc278b 100644 --- a/api/test/e2enew/schema/schema_test.go +++ b/api/test/e2enew/schema/schema_test.go @@ -70,7 +70,7 @@ var _ = ginkgo.Describe("Schema Test", func() { Path: "/apisix/admin/schemas/consumer", Headers: map[string]string{"Authorization": base.GetToken()}, ExpectStatus: http.StatusOK, - ExpectBody: `"properties":{"create_time":{"type":"integer"},"desc":{"maxLength":256,"type":"string"},"id":{"anyOf":[{"maxLength":64,"minLength":1,"pattern":"^[a-zA-Z0-9-_.]+$","type":"string"},{"minimum":1,"type":"integer"}]},"labels":{"description":"key/value pairs to specify attributes","maxProperties":16,"patternProperties":{".*":{"description":"value of label","maxLength":64,"minLength":1,"pattern":"^\\S+$","type":"string"}},"type":"object"},"plugins":{"type":"object"},"update_time":{"type":"integer"},"username":{"maxLength":32,"minLength":1,"pattern":"^[a-zA-Z0-9_]+$","type":"string"}}`, + ExpectBody: `"properties":{"create_time":{"type":"integer"},"desc":{"maxLength":256,"type":"string"},"labels":{"description":"key/value pairs to specify attributes","maxProperties":16,"patternProperties":{".*":{"description":"value of label","maxLength":64,"minLength":1,"pattern":"^\\S+$","type":"string"}},"type":"object"},"plugins":{"type":"object"},"update_time":{"type":"integer"},"username":{"maxLength":32,"minLength":1,"pattern":"^[a-zA-Z0-9_]+$","type":"string"}}`, Sleep: base.SleepTime, }), table.Entry("get schema of non-existent resources", base.HttpTestCase{ diff --git a/web/cypress/fixtures/plugin-dataset.json b/web/cypress/fixtures/plugin-dataset.json index b097ea8582..5f3739ad67 100644 --- a/web/cypress/fixtures/plugin-dataset.json +++ b/web/cypress/fixtures/plugin-dataset.json @@ -280,7 +280,7 @@ } }, { - "shouldValid": false, + "shouldValid": true, "data": { "whitelist": ["jack1"], "blacklist": ["jack2"] @@ -1265,7 +1265,7 @@ } }, { - "shouldValid": false, + "shouldValid": true, "data": { "rules": [ { diff --git a/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js b/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js index 3bf211114e..9afa31613c 100644 --- a/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js +++ b/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js @@ -25,7 +25,8 @@ context('Create and Delete Consumer', () => { }); const selector = { - max_age: "#max_age" + max_age: "#max_age", + allow_origins_by_regex: "#allow_origins_by_regex_0" } const data = { @@ -44,7 +45,9 @@ context('Create and Delete Consumer', () => { // config auth plugin cy.contains(this.domSelector.pluginCard, 'key-auth').within(() => { - cy.contains('Enable').click({ force: true }); + cy.contains('Enable').click({ + force: true + }); }); cy.focused(this.domSelector.drawer).should('exist'); cy.get(this.domSelector.disabledSwitcher).click().should('have.class', 'ant-switch-checked'); @@ -69,7 +72,7 @@ context('Create and Delete Consumer', () => { cy.get(this.domSelector.drawer).should('be.visible'); cy.get(selector.max_age).clear(); - // config proxy-mirror form + // config cors form cy.get(this.domSelector.drawer).within(() => { cy.contains('Submit').click({ force: true, @@ -79,6 +82,7 @@ context('Create and Delete Consumer', () => { cy.get(this.domSelector.notificationCloseIcon).click().should('not.exist'); cy.get(selector.max_age).type(data.time); + cy.get(selector.allow_origins_by_regex).type('.*.test.com'); cy.get(this.domSelector.drawer).within(() => { cy.contains('Submit').click({ force: true, diff --git a/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js index cd3a2a4857..255740c233 100644 --- a/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js +++ b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js @@ -41,7 +41,7 @@ context('Create Configure and Delete PluginTemplate', () => { cy.contains('proxy-rewrite').should('not.exist'); cy.contains(this.domSelector.pluginCard, 'basic-auth').within(() => { - cy.contains('Enable').click({ + cy.get('button').click({ force: true, }); }); diff --git a/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js b/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js index d62cd87018..aeb5118bee 100644 --- a/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js +++ b/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js @@ -61,7 +61,7 @@ context('Can select service_id skip upstream in route', () => { cy.get(this.domSelector.name).type(this.data.routeName); cy.contains('Next').click(); cy.get(this.domSelector.upstreamSelector).click(); - cy.contains('None').should('not.exist'); + cy.get('.ant-select-item-option-disabled > .ant-select-item-option-content').contains('None'); cy.contains('Previous').click(); cy.wait(500); @@ -91,9 +91,12 @@ context('Can select service_id skip upstream in route', () => { cy.contains(this.data.routeName).siblings().contains('Configure').click(); cy.get(this.domSelector.serviceSelector).click(); cy.contains('None').click(); + cy.get(this.domSelector.notification).should('contain', 'Please check the configuration of binding service'); + cy.get(this.domSelector.notificationCloseIcon).click(); + cy.contains('Next').click(); - cy.get(this.domSelector.upstream_id).click(); - cy.contains('None').should('not.exist'); + cy.wait(500); + cy.get('[data-cy=upstream_selector]').click(); cy.contains(this.data.upstreamName).click(); cy.contains('Next').click(); cy.contains('Next').click(); @@ -101,13 +104,7 @@ context('Can select service_id skip upstream in route', () => { cy.contains(this.data.submitSuccess); }); - it('should delete upstream, service and route', function () { - cy.visit('/'); - cy.contains('Service').click(); - cy.contains(this.data.serviceName).siblings().contains('Delete').click(); - cy.contains('button', 'Confirm').click(); - cy.get(this.domSelector.notification).should('contain', this.data.deleteServiceSuccess); - + it('should delete route, service and upstream', function () { cy.visit('/'); cy.contains('Route').click(); cy.contains(this.data.routeName).siblings().contains('More').click(); @@ -115,7 +112,13 @@ context('Can select service_id skip upstream in route', () => { cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => { cy.contains('OK').click(); }); + cy.get(this.domSelector.notification).should('contain', this.data.deleteRouteSuccess); + cy.visit('/'); + cy.contains('Service').click(); + cy.contains(this.data.serviceName).siblings().contains('Delete').click(); + cy.contains('button', 'Confirm').click(); + cy.get(this.domSelector.notification).should('contain', this.data.deleteServiceSuccess); cy.visit('/'); cy.contains('Upstream').click(); diff --git a/web/cypress/integration/route/create-route-with-limit-count-plugin-form.spec.js b/web/cypress/integration/route/create-route-with-limit-count-plugin-form.spec.js index f9db28bb56..b1df99e0e9 100644 --- a/web/cypress/integration/route/create-route-with-limit-count-plugin-form.spec.js +++ b/web/cypress/integration/route/create-route-with-limit-count-plugin-form.spec.js @@ -57,7 +57,7 @@ context('Create and delete route with limit-count form', () => { // config limit-count form with local policy cy.contains(this.domSelector.pluginCard, 'limit-count').within(() => { - cy.contains('Enable').click({ + cy.get('button').click({ force: true, }); }); @@ -75,7 +75,7 @@ context('Create and delete route with limit-count form', () => { // config limit-count form with redis policy cy.contains(this.domSelector.pluginCard, 'limit-count').within(() => { - cy.contains('Enable').click({ + cy.get('button').click({ force: true, }); }); @@ -97,7 +97,7 @@ context('Create and delete route with limit-count form', () => { // config limit-count form with redis policy cy.contains(this.domSelector.pluginCard, 'limit-count').within(() => { - cy.contains('Enable').click({ + cy.get('button').click({ force: true, }); }); diff --git a/web/cypress/integration/route/create-route-with-upstream.spec.js b/web/cypress/integration/route/create-route-with-upstream.spec.js index 2a976b8a73..8a64a053d9 100644 --- a/web/cypress/integration/route/create-route-with-upstream.spec.js +++ b/web/cypress/integration/route/create-route-with-upstream.spec.js @@ -54,8 +54,7 @@ context('Create Route with Upstream', () => { cy.get(this.domSelector.input).should('be.disabled'); // should enable Upstream input boxes after selecting Custom mode cy.get(this.domSelector.upstreamSelector).click(); - cy.contains('Custom').click(); - cy.get(this.domSelector.input).should('not.be.disabled'); + cy.contains('.ant-select-item-option-content', 'Custom').click(); cy.get(this.domSelector.nodes_0_host).clear().type(this.data.ip1); cy.get(this.domSelector.nodes_0_port).type(this.data.port); @@ -77,7 +76,9 @@ context('Create Route with Upstream', () => { cy.contains(this.data.routeName).siblings().contains('Configure').click(); cy.get(this.domSelector.name).should('value', this.data.routeName); - cy.contains('Next').click({ force: true }); + cy.contains('Next').click({ + force: true + }); // check if the changes have been saved cy.get(this.domSelector.nodes_0_host).should('value', this.data.ip1); @@ -87,7 +88,7 @@ context('Create Route with Upstream', () => { cy.get(this.domSelector.input).should('be.disabled'); cy.contains(this.data.upstreamName).click(); - cy.contains('Custom').click(); + cy.contains('.ant-select-item-option-content', 'Custom').click(); cy.get(this.domSelector.input).should('not.be.disabled'); cy.get(this.domSelector.nodes_0_host).clear().type(this.data.ip2); @@ -107,7 +108,9 @@ context('Create Route with Upstream', () => { cy.contains(this.data.routeName).siblings().contains('Configure').click(); // ensure it has already changed to edit page cy.get(this.domSelector.name).should('value', this.data.routeName); - cy.contains('Next').click({ force: true }); + cy.contains('Next').click({ + force: true + }); cy.get(this.domSelector.nodes_0_host).should('value', this.data.ip2); }); diff --git a/web/cypress/integration/service/edit-service-with-upstream.spec.js b/web/cypress/integration/service/edit-service-with-upstream.spec.js index 87be509c63..150a9e4a57 100644 --- a/web/cypress/integration/service/edit-service-with-upstream.spec.js +++ b/web/cypress/integration/service/edit-service-with-upstream.spec.js @@ -40,6 +40,7 @@ context('Edit Service with Upstream', () => { it('should create a test service', function () { cy.visit('/'); + cy.get('.ant-empty').should('be.visible'); cy.contains('Service').click(); cy.get(this.domSelector.empty).should('be.visible'); cy.contains('Create').click(); @@ -62,11 +63,14 @@ context('Edit Service with Upstream', () => { cy.contains('Search').click(); cy.contains(this.data.serviceName).siblings().contains('Configure').click(); - cy.get(this.domSelector.nodes_0_host).click({ force: true }).should('value', this.data.ip1); + cy.wait(500); + cy.get(this.domSelector.nodes_0_host).click({ + force: true + }).should('value', this.data.ip1); cy.get(this.domSelector.input).should('be.disabled'); cy.get(this.domSelector.upstreamSelector).click(); - cy.contains('Custom').click(); + cy.contains('.ant-select-item-option-content', 'Custom').click(); cy.get(this.domSelector.nodes_0_host).should('not.be.disabled').clear().type(this.data.ip2); cy.get(this.domSelector.nodes_0_port).type(this.data.port); cy.get(this.domSelector.nodes_0_weight).type(this.data.weight); diff --git a/web/cypress/support/commands.js b/web/cypress/support/commands.js index 590243cb5f..4592efbe30 100644 --- a/web/cypress/support/commands.js +++ b/web/cypress/support/commands.js @@ -59,7 +59,7 @@ Cypress.Commands.add('configurePlugins', (cases) => { cy.contains(name) .parents(domSelector.parents) .within(() => { - cy.contains('Enable').click({ + cy.get('button').click({ force: true, }); }); diff --git a/web/package.json b/web/package.json index f138a1bd00..d0c4d13cf2 100644 --- a/web/package.json +++ b/web/package.json @@ -52,7 +52,6 @@ "@ant-design/icons": "^4.0.0", "@ant-design/pro-layout": "^6.0.0", "@ant-design/pro-table": "2.30.1", - "@api7-dashboard/ui": "^1.0.3", "@mrblenny/react-flow-chart": "^0.0.14", "@rjsf/antd": "2.2.0", "@rjsf/core": "2.2.0", diff --git a/web/src/components/PanelSection/index.tsx b/web/src/components/PanelSection/index.tsx new file mode 100644 index 0000000000..0fdfd10a92 --- /dev/null +++ b/web/src/components/PanelSection/index.tsx @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { CSSProperties } from 'react'; +import { Divider, Tooltip } from 'antd'; +import { QuestionCircleOutlined } from '@ant-design/icons'; + +const PanelSection: React.FC<{ + title: string; + style?: CSSProperties; + id?: string; + tooltip?: string; +}> = ({ title, style, id, children, tooltip }) => { + return ( +
+ + {title} +   + {tooltip && } + +
{children}
+
+ ); +}; + +export default PanelSection; diff --git a/web/src/components/Plugin/PluginDetail.tsx b/web/src/components/Plugin/PluginDetail.tsx index 385f9670eb..72c474b5e4 100644 --- a/web/src/components/Plugin/PluginDetail.tsx +++ b/web/src/components/Plugin/PluginDetail.tsx @@ -285,6 +285,7 @@ const PluginDetail: React.FC = ({ title={formatMessage({ id: 'page.plugin.drawer.popconfirm.title.delete' })} okText={formatMessage({ id: 'component.global.confirm' })} cancelText={formatMessage({ id: 'component.global.cancel' })} + disabled={readonly} onConfirm={() => { onChange({ formData: form.getFieldsValue(), @@ -294,13 +295,14 @@ const PluginDetail: React.FC = ({ }} > {initialData[name] ? ( - ) : null} , ]} title={[ diff --git a/web/src/components/Plugin/UI/api-breaker.tsx b/web/src/components/Plugin/UI/api-breaker.tsx index 2ccf2dde5b..28f851846d 100644 --- a/web/src/components/Plugin/UI/api-breaker.tsx +++ b/web/src/components/Plugin/UI/api-breaker.tsx @@ -67,7 +67,7 @@ const ApiBreaker: React.FC = ({ form }) => { initialValue={300} tooltip={formatMessage({ id: 'component.pluginForm.api-breaker.max_breaker_sec.tooltip' })} > - + diff --git a/web/src/components/Plugin/UI/cors.tsx b/web/src/components/Plugin/UI/cors.tsx index d075fc43a1..4bee0ebb60 100644 --- a/web/src/components/Plugin/UI/cors.tsx +++ b/web/src/components/Plugin/UI/cors.tsx @@ -57,9 +57,8 @@ const Cors: React.FC = ({ form }) => { - + { { - + ) } -const RedisClusterForm: React.FC = () => { +const RedisClusterForm: React.FC = () => { const { formatMessage } = useIntl(); return ( @@ -109,7 +112,7 @@ const RedisClusterForm: React.FC = () => { > - + {(fields, { add, remove }) => { return (
@@ -159,6 +162,21 @@ const RedisClusterForm: React.FC = () => { ); }} + + + + + + ) } @@ -170,7 +188,6 @@ const LimitCount: React.FC = ({ form }) => {
= ({ form }) => { = ({ form }) => { = ({ form }) => { prev.policy !== next.policy} style={{ display: 'none' }}> @@ -221,7 +241,7 @@ const LimitCount: React.FC = ({ form }) => { }} {Boolean(policy === 'redis') && } - {Boolean(policy === 'redis-cluster') && } + {Boolean(policy === 'redis-cluster') && } ); } diff --git a/web/src/components/Plugin/data.tsx b/web/src/components/Plugin/data.tsx index df119c1f24..2c2ea81731 100644 --- a/web/src/components/Plugin/data.tsx +++ b/web/src/components/Plugin/data.tsx @@ -39,7 +39,6 @@ export enum PluginType { traffic = "traffic", serverless = "serverless", observability = "observability", - logging = "logging", other = "other" } @@ -64,7 +63,7 @@ export const PLUGIN_LIST = { type: PluginType.authentication }, "error-log-logger": { - type: PluginType.logging + type: PluginType.observability }, "fault-injection": { type: PluginType.security @@ -79,7 +78,7 @@ export const PLUGIN_LIST = { type: PluginType.other }, "syslog": { - type: PluginType.logging + type: PluginType.observability }, "traffic-split": { type: PluginType.traffic @@ -88,13 +87,13 @@ export const PLUGIN_LIST = { type: PluginType.authentication }, "kafka-logger": { - type: PluginType.logging + type: PluginType.observability }, "limit-conn": { type: PluginType.traffic }, "udp-logger": { - type: PluginType.logging + type: PluginType.observability }, "zipkin": { type: PluginType.observability @@ -104,7 +103,7 @@ export const PLUGIN_LIST = { hidden: true }, "log-rotate": { - type: PluginType.logging, + type: PluginType.observability, hidden: true }, "serverless-pre-function": { @@ -166,16 +165,16 @@ export const PLUGIN_LIST = { type: PluginType.other }, "http-logger": { - type: PluginType.logging + type: PluginType.observability }, "openid-connect": { type: PluginType.authentication }, "sls-logger": { - type: PluginType.logging + type: PluginType.observability }, "tcp-logger": { - type: PluginType.logging + type: PluginType.observability }, "uri-blocker": { type: PluginType.security diff --git a/web/src/components/Plugin/locales/en-US.ts b/web/src/components/Plugin/locales/en-US.ts index 26fa357e80..b1ed0d8cc1 100644 --- a/web/src/components/Plugin/locales/en-US.ts +++ b/web/src/components/Plugin/locales/en-US.ts @@ -21,12 +21,13 @@ export default { 'component.step.select.pluginTemplate.select.option': 'Custom', 'component.plugin.pluginTemplate.tip1': '1. When a route already have plugins field configured, the plugins in the plugin template will be merged into it.', 'component.plugin.pluginTemplate.tip2': '2. The same plugin in the plugin template will override one in the plugins', + 'component.plugin.enable': 'Enable', + 'component.plugin.disable': 'Disable', 'component.plugin.authentication': 'Authentication', 'component.plugin.security': 'Security', 'component.plugin.traffic': 'Traffic Control', 'component.plugin.serverless': 'Serverless', 'component.plugin.observability': 'Tracing & Metrics & Logging', - 'component.plugin.logging': 'Logging', 'component.plugin.other': 'Other', // cors @@ -34,7 +35,7 @@ export default { 'component.pluginForm.cors.allow_origins.extra': 'For example: https://somehost.com:8081', 'component.pluginForm.cors.allow_methods.tooltip': 'Which Method is allowed to enable CORS, such as: GET, POST etc. Multiple method use , to split. When allow_credential is false, you can use * to indicate allow all any method. You also can allow any method forcefully using ** even already enable allow_credential, but it will bring some security risks.', 'component.pluginForm.cors.allow_headers.tooltip': 'Which headers are allowed to set in request when access cross-origin resource. Multiple value use , to split. When allow_credential is false, you can use * to indicate allow all request headers. You also can allow any header forcefully using ** even already enable allow_credential, but it will bring some security risks.', - 'component.pluginForm.cors.expose_headers.tooltip': ' Which headers are allowed to set in response when access cross-origin resource. Multiple value use , to split.', + 'component.pluginForm.cors.expose_headers.tooltip': 'Which headers are allowed to set in response when access cross-origin resource. Multiple value use , to split.', 'component.pluginForm.cors.max_age.tooltip': 'Maximum number of seconds the results can be cached.. Within this time range, the browser will reuse the last check result. -1 means no cache. Please note that the maximum value is depended on browser, please refer to MDN for details.', 'component.pluginForm.cors.allow_credential.tooltip': 'If you set this option to true, you can not use \'*\' for other options.', 'component.pluginForm.cors.allow_origins_by_regex.tooltip': 'Use regex expressions to match which origin is allowed to enable CORS, for example, [".*.test.com"] can use to match all subdomain of test.com.', @@ -53,7 +54,7 @@ export default { // proxy-mirror 'component.pluginForm.proxy-mirror.host.tooltip': 'Specify a mirror service address, e.g. http://127.0.0.1:9797 (address needs to contain schema: http or https, not URI part)', - 'component.pluginForm.proxy-mirror.host.extra': 'e.g. http://127.0.0.1:9797 (address needs to contain schema: http or https, not URI part)', + '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', // limit-conn diff --git a/web/src/components/Plugin/locales/zh-CN.ts b/web/src/components/Plugin/locales/zh-CN.ts index b88f6e80d1..d7438d3ca8 100644 --- a/web/src/components/Plugin/locales/zh-CN.ts +++ b/web/src/components/Plugin/locales/zh-CN.ts @@ -21,12 +21,13 @@ export default { 'component.step.select.pluginTemplate.select.option': '手动配置', 'component.plugin.pluginTemplate.tip1': '1. 若路由已配置插件,则插件模板数据将与已配置的插件数据合并。', 'component.plugin.pluginTemplate.tip2': '2. 插件模板相同的插件会覆盖掉原有的插件。', + 'component.plugin.enable': '启用', + 'component.plugin.disable': '禁用', 'component.plugin.authentication': '身份验证', 'component.plugin.security': '安全防护', 'component.plugin.traffic': '流量控制', 'component.plugin.serverless': '无服务器架构', 'component.plugin.observability': '可观测性', - 'component.plugin.logging': '日志记录', 'component.plugin.other': '其它', // cors @@ -53,7 +54,7 @@ export default { // proxy-mirror 'component.pluginForm.proxy-mirror.host.tooltip': '指定镜像服务地址,例如:http://127.0.0.1:9797(地址中需要包含 schema :http或https,不能包含 URI 部分)', - 'component.pluginForm.proxy-mirror.host.extra': '例如:http://127.0.0.1:9797(地址中需要包含 schema:http或https,不能包含 URI 部分)', + 'component.pluginForm.proxy-mirror.host.extra': '例如:http://127.0.0.1:9797', 'component.pluginForm.proxy-mirror.host.ruletip': '地址中需要包含 schema :http或https,不能包含 URI 部分', // limit-conn diff --git a/web/src/components/RightContent/AvatarDropdown.tsx b/web/src/components/RightContent/AvatarDropdown.tsx index 2304f1cacf..9221a05614 100644 --- a/web/src/components/RightContent/AvatarDropdown.tsx +++ b/web/src/components/RightContent/AvatarDropdown.tsx @@ -34,9 +34,6 @@ export type GlobalHeaderRightProps = { menu?: boolean; }; -/** - * 退出登录,并且将当前的 url 保存 - */ const settings = async () => { history.replace({ pathname: '/settings', diff --git a/web/src/components/Upstream/UpstreamForm.tsx b/web/src/components/Upstream/UpstreamForm.tsx index 42d71443e5..c8a606bbaf 100644 --- a/web/src/components/Upstream/UpstreamForm.tsx +++ b/web/src/components/Upstream/UpstreamForm.tsx @@ -14,13 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Divider, Form, Switch } from 'antd'; +import { Divider, Form, notification, Switch } from 'antd'; import React, { useState, forwardRef, useImperativeHandle, useEffect } from 'react'; import { useIntl } from 'umi'; import type { FormInstance } from 'antd/es/form'; -import { PanelSection } from '@api7-dashboard/ui'; -import { transformRequest } from '@/pages/Upstream/transform'; +import PanelSection from '@/components/PanelSection'; import PassiveCheck from './components/passive-check'; import ActiveCheck from './components/active-check' import Nodes from './components/Nodes' @@ -31,7 +30,7 @@ import UpstreamSelector from './components/UpstreamSelector'; import Retries from './components/Retries'; import PassHost from './components/PassHost'; import TLSComponent from './components/TLS'; -import { transformUpstreamDataFromRequest } from './service'; +import { convertToRequestData } from './service'; type Upstream = { name?: string; @@ -46,14 +45,18 @@ type Props = { // FIXME: use proper typing ref?: any; required?: boolean; + neverReadonly?: boolean }; +/** + * UpstreamForm is used to reuse Upstream Form UI, + * before using this component, we need to execute the following command: + * form.setFieldsValue(convertToFormData(VALUE_FROM_API)) +*/ const UpstreamForm: React.FC = forwardRef( - ({ form, disabled, list = [], showSelector, required = true }, ref) => { + ({ form, disabled = false, list = [], showSelector = false, required = true, neverReadonly = false }, ref) => { const { formatMessage } = useIntl(); - const [readonly, setReadonly] = useState( - Boolean(form.getFieldValue('upstream_id')) || disabled, - ); + const [readonly, setReadonly] = useState(false); const [hiddenForm, setHiddenForm] = useState(false); const timeoutFields = [ @@ -75,41 +78,58 @@ const UpstreamForm: React.FC = forwardRef( ]; useImperativeHandle(ref, () => ({ - getData: () => transformRequest(form.getFieldsValue()), + getData: () => convertToRequestData(form.getFieldsValue()), })); - useEffect(() => { - const formData = transformRequest(form.getFieldsValue()) || {}; - const { upstream_id } = form.getFieldsValue(); + const resetForm = (upstream_id: string) => { + if (upstream_id === undefined) { + return + } + if (!neverReadonly) { + setReadonly(!["Custom", "None"].includes(upstream_id) || disabled); + } + + /** + * upstream_id === None <==> required === false + * No need to bind Upstream object. + * When creating Route and binds with a Service, no need to configure Upstream in Route. + */ if (upstream_id === 'None') { setHiddenForm(true); - if (required) { - requestAnimationFrame(() => { - form.resetFields(); - setHiddenForm(false); - }); - } - } else { - if (upstream_id) { - requestAnimationFrame(() => { - const targetData = list.find((item) => item.id === upstream_id) as UpstreamComponent.ResponseData - if (targetData) { - form.setFieldsValue(transformUpstreamDataFromRequest(targetData)); - } else { - // TODO: 提示 upstream_id 找不到想要的数据 - } - }); - } - if (!required && !Object.keys(formData).length) { - requestAnimationFrame(() => { - form.setFieldsValue({ upstream_id: 'None' }); - setHiddenForm(true); - }); - } + form.resetFields() + form.setFieldsValue({ upstream_id: 'None' }) + return + } + + setHiddenForm(false) + + // NOTE: Use Ant Design's form object to set data automatically + if (upstream_id === "Custom") { + return + } + + // NOTE: Set data from Upstream List (Upstream Selector) + if (list.length === 0) { + return + } + form.resetFields() + const targetData = list.find((item) => item.id === upstream_id) as UpstreamComponent.ResponseData + if (targetData) { + form.setFieldsValue(targetData); } - setReadonly(Boolean(upstream_id) || disabled); - }, [list]); + } + + /** + * upstream_id + * - None: No need to bind Upstream to a resource (e.g Service). + * - Custom: Users could input values on UpstreamForm + * - Upstream ID from API + */ + useEffect(() => { + const upstream_id = form.getFieldValue('upstream_id'); + resetForm(upstream_id) + }, [form.getFieldValue('upstream_id'), list]); const ActiveHealthCheck = () => ( @@ -194,19 +214,26 @@ const UpstreamForm: React.FC = forwardRef( } - + - + prev.custom?.checks?.passive !== next.custom?.checks?.passive} noStyle> { () => { const passive = form.getFieldValue(['custom', 'checks', 'passive']) + const active = form.getFieldValue(['custom', 'checks', 'active']) if (passive) { /* * When enable passive check, we should enable active check, too. * When we use form.setFieldsValue to enable active check, error throws. * We choose to alert users first, and need users to enable active check manually. */ + if (!active) { + notification.warn({ + message: formatMessage({ id: 'component.upstream.other.health-check.invalid' }), + description: formatMessage({ id: 'component.upstream.other.health-check.passive-only' }) + }) + } return } return null @@ -227,32 +254,8 @@ const UpstreamForm: React.FC = forwardRef( list={list} disabled={disabled} required={required} - shouldUpdate={(prev, next) => { - setReadonly(Boolean(next.upstream_id)); - if (prev.upstream_id !== next.upstream_id) { - const id = next.upstream_id; - if (id) { - const targetData = list.find((item) => item.id === id) as UpstreamComponent.ResponseData - if (targetData) { - form.setFieldsValue(transformUpstreamDataFromRequest(targetData)); - } - form.setFieldsValue({ - upstream_id: id, - }); - } - } - return prev.upstream_id !== next.upstream_id; - }} - onChange={(upstream_id) => { - setReadonly(Boolean(upstream_id)); - setHiddenForm(Boolean(upstream_id === 'None')); - const targetData = list.find((item) => item.id === upstream_id) as UpstreamComponent.ResponseData - if (targetData) { - form.setFieldsValue(transformUpstreamDataFromRequest(targetData)); - } - if (upstream_id === '') { - form.resetFields(); - } + onChange={(nextUpstreamId) => { + resetForm(nextUpstreamId); }} /> )} diff --git a/web/src/components/Upstream/components/Nodes.tsx b/web/src/components/Upstream/components/Nodes.tsx index eaaa3a4feb..92bfcda74f 100644 --- a/web/src/components/Upstream/components/Nodes.tsx +++ b/web/src/components/Upstream/components/Nodes.tsx @@ -51,6 +51,9 @@ const Component: React.FC = ({ readonly }) => { /(^([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])(\.(25[0-5]|1\d{2}|2[0-4]\d|[1-9]?\d)){3}$|^(?![0-9.]+$)([a-zA-Z0-9_-]+)(\.[a-zA-Z0-9_-]+){0,}$)/, 'g', ), + message: formatMessage({ + id: 'page.upstream.step.valid.domain.name.or.ip', + }) }, ]} > diff --git a/web/src/components/Upstream/components/PassHost.tsx b/web/src/components/Upstream/components/PassHost.tsx index a2f7e456be..72b582120b 100644 --- a/web/src/components/Upstream/components/PassHost.tsx +++ b/web/src/components/Upstream/components/PassHost.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ import React from 'react' -import { Form, Input, Select } from 'antd' +import { Form, Input, notification, Select } from 'antd' import { useIntl } from 'umi' import type { FormInstance } from 'antd/lib/form' @@ -79,6 +79,14 @@ const Component: React.FC = ({ form, readonly }) => { ); } + + if (form.getFieldValue('pass_host') === 'node' && (form.getFieldValue('nodes') || []).length !== 1) { + notification.warning({ + message: formatMessage({id: 'component.upstream.other.pass_host-with-multiple-nodes.title'}), + description: formatMessage({id: 'component.upstream.other.pass_host-with-multiple-nodes'}) + }) + form.setFieldsValue({pass_host: 'pass'}) + } return null; }} diff --git a/web/src/components/Upstream/components/TLS.tsx b/web/src/components/Upstream/components/TLS.tsx index 2d4feee0a5..bdc01030d7 100644 --- a/web/src/components/Upstream/components/TLS.tsx +++ b/web/src/components/Upstream/components/TLS.tsx @@ -69,7 +69,7 @@ const TLSComponent: React.FC = ({ form, readonly }) => { required rules={[{ required: true, message: "" }, { max: 64 * 1024 }, { min: 128 }]} > - + = ({ form, readonly }) => { required rules={[{ required: true, message: "" }, { max: 64 * 1024 }, { min: 128 }]} > - + ) diff --git a/web/src/components/Upstream/components/UpstreamSelector.tsx b/web/src/components/Upstream/components/UpstreamSelector.tsx index 1f8b2254e9..34f7fec16b 100644 --- a/web/src/components/Upstream/components/UpstreamSelector.tsx +++ b/web/src/components/Upstream/components/UpstreamSelector.tsx @@ -27,18 +27,16 @@ type Props = { list?: Upstream[]; disabled?: boolean; required?: boolean; - shouldUpdate: (prev: any, next: any) => void; onChange: (id: string) => void } -const Component: React.FC = ({ shouldUpdate, onChange, list = [], disabled, required }) => { +const UpstreamSelector: React.FC = ({ onChange, list = [], disabled, required }) => { const { formatMessage } = useIntl() return ( = ({ readonly }) => { const { formatMessage } = useIntl() return ( - - + + = ({ readonly }) => { +const PassiveCheckTypeComponent: React.FC = ({ readonly }) => { const { formatMessage } = useIntl() return ( @@ -66,4 +66,4 @@ const ActiveCheckTypeComponent: React.FC = ({ readonly }) => { ) } -export default ActiveCheckTypeComponent +export default PassiveCheckTypeComponent diff --git a/web/src/components/Upstream/locales/en-US.ts b/web/src/components/Upstream/locales/en-US.ts index 585fb6687d..9f944420e5 100644 --- a/web/src/components/Upstream/locales/en-US.ts +++ b/web/src/components/Upstream/locales/en-US.ts @@ -16,7 +16,9 @@ */ export default { 'component.upstream.fields.tls.client_key': 'Client Key', + 'component.upstream.fields.tls.client_key.required': 'Please enter the client key', 'component.upstream.fields.tls.client_cert': 'Client Cert', + 'component.upstream.fields.tls.client_cert.required': 'Please enter the client cert', 'component.upstream.fields.discovery_type': 'Discovery Type', 'component.upstream.fields.discovery_type.tooltip': 'Discovery Type', @@ -50,7 +52,6 @@ export default { 'component.upstream.fields.checks.active.host.scope': 'Only letters, numbers and . are supported', 'component.upstream.fields.checks.active.port': 'Port', - 'component.upstream.fields.checks.active.port.required': 'Please enter the port', 'component.upstream.fields.checks.active.http_path': 'HTTP Path', 'component.upstream.fields.checks.active.http_path.tooltip': 'The path that should be used when issuing the HTTP GET request to the target. The default value is /.', @@ -88,4 +89,10 @@ export default { 'component.upstream.fields.checks.passive.unhealthy.timeouts': 'Timeouts', 'component.upstream.fields.checks.passive.unhealthy.timeouts.tooltip': 'Number of timeouts in proxied traffic to consider a target unhealthy, as observed by passive health checks.', + + 'component.upstream.other.none': 'None', + 'component.upstream.other.pass_host-with-multiple-nodes.title': 'Please check the target node configuration', + 'component.upstream.other.pass_host-with-multiple-nodes': 'When using a host name or IP in the target node list, make sure there is only one target node', + 'component.upstream.other.health-check.passive-only': 'When passive health check is enabled, active health check needs to be enabled at the same time.', + 'component.upstream.other.health-check.invalid': 'Please check the health check configuration', } diff --git a/web/src/components/Upstream/locales/zh-CN.ts b/web/src/components/Upstream/locales/zh-CN.ts index 62925e6926..809a60a1aa 100644 --- a/web/src/components/Upstream/locales/zh-CN.ts +++ b/web/src/components/Upstream/locales/zh-CN.ts @@ -16,7 +16,9 @@ */ export default { 'component.upstream.fields.tls.client_key': '客户端私钥', + 'component.upstream.fields.tls.client_key.required': '请输入客户端私钥', 'component.upstream.fields.tls.client_cert': '客户端证书', + 'component.upstream.fields.tls.client_cert.required': '请输入客户端证书', 'component.upstream.fields.discovery_type': '服务发现类型', 'component.upstream.fields.discovery_type.tooltip': '服务发现类型', @@ -50,7 +52,6 @@ export default { 'component.upstream.fields.checks.active.host.scope': '仅支持字母、数字和 . ', 'component.upstream.fields.checks.active.port': '端口', - 'component.upstream.fields.checks.active.port.required': '请输入端口', 'component.upstream.fields.checks.active.http_path': '请求路径', 'component.upstream.fields.checks.active.http_path.tooltip': '向目标节点发出 HTTP GET 请求时应使用的路径。', @@ -93,4 +94,10 @@ export default { 'component.upstream.fields.checks.passive.unhealthy.timeouts': '超时时间', 'component.upstream.fields.checks.passive.unhealthy.timeouts.tooltip': '根据被动健康检查的观察,在代理中认为目标不健康的超时次数。', + + 'component.upstream.other.none': '不选择(仅在绑定服务时可用)', + 'component.upstream.other.pass_host-with-multiple-nodes.title': '请检查目标节点配置', + 'component.upstream.other.pass_host-with-multiple-nodes': '当使用目标节点列表中的主机名或者 IP 时,请确认只有一个目标节点', + 'component.upstream.other.health-check.passive-only': '启用被动健康检查时,需要同时启用主动健康检查。', + 'component.upstream.other.health-check.invalid': '请检查健康检查配置', } diff --git a/web/src/components/Upstream/service.ts b/web/src/components/Upstream/service.ts index 2642100dc2..098ed22c87 100644 --- a/web/src/components/Upstream/service.ts +++ b/web/src/components/Upstream/service.ts @@ -14,15 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import cloneDeep from 'lodash/cloneDeep' +import { notification } from 'antd'; +import { isNil, omitBy, omit, pick, cloneDeep } from 'lodash'; +import { formatMessage, request } from 'umi'; /** * Because we have some `custom` field in Upstream Form, like custom.tls/custom.checks.active etc, * we need to transform data that doesn't have `custom` field to data contains `custom` field */ -export const transformUpstreamDataFromRequest = (originData: UpstreamComponent.ResponseData) => { +export const convertToFormData = (originData: UpstreamComponent.ResponseData) => { + if (originData === undefined) { + // NOTE: When binding Service without Upstream configuration (None), originData === undefined + return undefined + } + const data = cloneDeep(originData) data.custom = {} + data.upstream_id = "Custom" if (data.checks) { data.custom.checks = {} @@ -40,5 +48,84 @@ export const transformUpstreamDataFromRequest = (originData: UpstreamComponent.R data.custom.tls = "enable" } + if (data.id) { + data.upstream_id = data.id; + } + return data } + +/** + * Transform Upstream Form data from custom data to API needed data +*/ +export const convertToRequestData = ( + formData: UpstreamModule.RequestBody, +): UpstreamModule.RequestBody | undefined | { upstream_id: string } => { + let data = omitBy(formData, isNil) as UpstreamModule.RequestBody; + data = omit(data, 'custom'); + + const { + type, + hash_on, + key, + k8s_deployment_info, + nodes, + pass_host, + upstream_host, + upstream_id = "Custom", + checks + } = data; + + if (!["Custom", "None"].includes(upstream_id)) { + return { upstream_id }; + } + + data = omit(data, "upstream_id") as any + + if (nodes && k8s_deployment_info) { + return undefined; + } + + if (!nodes && !k8s_deployment_info) { + return undefined; + } + + if (type === 'chash') { + if (!hash_on) { + return undefined; + } + + if (hash_on !== 'consumer' && !key) { + return undefined; + } + } + + if (pass_host === 'rewrite' && !upstream_host) { + return undefined; + } + + if (checks?.passive && !checks.active) { + notification.error({ + message: formatMessage({id: 'component.upstream.other.health-check.invalid'}), + description: formatMessage({id: 'component.upstream.other.health-check.passive-only'}) + }) + return undefined + } + + if (nodes) { + // NOTE: https://github.com/ant-design/ant-design/issues/27396 + data.nodes = data.nodes?.map((item) => { + return pick(item, ['host', 'port', 'weight']); + }); + return data; + } + + return undefined; +}; + +export const fetchUpstreamList = () => { + return request>>('/upstreams').then(({ data }) => ({ + data: data.rows.map(row => convertToFormData(row)), + total: data.total_size, + })); +}; diff --git a/web/src/components/Upstream/typings.d.ts b/web/src/components/Upstream/typings.d.ts index bbe22d3e6e..e7b238290d 100644 --- a/web/src/components/Upstream/typings.d.ts +++ b/web/src/components/Upstream/typings.d.ts @@ -42,7 +42,7 @@ declare namespace UpstreamComponent { } type ResponseData = { - nodes: Node[]; + nodes?: Node[]; retries?: number; timeout?: Timeout; tls?: TLS; @@ -61,6 +61,7 @@ declare namespace UpstreamComponent { desc?: string; service_name?: string; id?: string; + upstream_id?: string; // NOTE: custom field custom?: Record; } diff --git a/web/src/global.less b/web/src/global.less index 3cf4adec56..c8c8668366 100644 --- a/web/src/global.less +++ b/web/src/global.less @@ -66,7 +66,6 @@ ol { } } -// 兼容IE11 @media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) { body .ant-design-pro > .ant-layout { min-height: 100vh; diff --git a/web/src/helpers.tsx b/web/src/helpers.tsx index d1d7d4ec56..9d290b30dc 100644 --- a/web/src/helpers.tsx +++ b/web/src/helpers.tsx @@ -73,9 +73,6 @@ export const getMenuData = (): MenuDataItem[] => { export const isLoginPage = () => window.location.pathname.indexOf('/user/login') !== -1; -/** - * 异常处理程序 - */ export const errorHandler = (error: { response: Response; data: any }): Promise => { const { response } = error; if (error && response && response.status) { diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx index 8c478d1f4d..8da9546169 100644 --- a/web/src/pages/Route/Create.tsx +++ b/web/src/pages/Route/Create.tsx @@ -102,6 +102,7 @@ const Page: React.FC = (props) => { return ( { if (action === 'redirectOptionChange') { diff --git a/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx b/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx index 60dd5de068..b0f1761fca 100644 --- a/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx +++ b/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx @@ -18,11 +18,12 @@ import React, { useEffect, useState, useRef } from 'react'; import { Input, Select, Card, Tabs, Form, Drawer, Spin, notification, Radio } from 'antd'; import { useIntl } from 'umi'; import CodeMirror from '@uiw/react-codemirror'; -import { PanelSection } from '@api7-dashboard/ui'; import queryString from 'query-string'; import Base64 from 'base-64'; import urlRegexSafe from 'url-regex-safe'; +import PanelSection from '@/components/PanelSection'; + import { HTTP_METHOD_OPTION_LIST, DEFAULT_DEBUG_PARAM_FORM_DATA, diff --git a/web/src/pages/Route/components/Step1/MatchingRulesView.tsx b/web/src/pages/Route/components/Step1/MatchingRulesView.tsx index aea9494987..81f4dd2f1b 100644 --- a/web/src/pages/Route/components/Step1/MatchingRulesView.tsx +++ b/web/src/pages/Route/components/Step1/MatchingRulesView.tsx @@ -15,9 +15,10 @@ * limitations under the License. */ import React, { useState } from 'react'; -import { Button, Table, Modal, Form, Select, Input, Space } from 'antd'; +import { Button, Table, Modal, Form, Select, Input, Space, notification } from 'antd'; import { useIntl } from 'umi'; -import { PanelSection } from '@api7-dashboard/ui'; + +import PanelSection from '@/components/PanelSection'; const MatchingRulesView: React.FC = ({ advancedMatchingRules, @@ -36,21 +37,32 @@ const MatchingRulesView: React.FC = ({ const { formatMessage } = useIntl(); const onOk = () => { - modalForm.validateFields().then((value) => { + modalForm.validateFields().then((value: RouteModule.MatchingRule) => { + if (value.operator === "IN") { + try { + JSON.parse(value.value as string) + } catch (error) { + notification.warning({ + message: formatMessage({ id: 'page.route.fields.vars.invalid' }), + description: formatMessage({ id: 'page.route.fields.vars.in.invalid' }) + }) + return + } + } if (mode === 'EDIT') { const key = modalForm.getFieldValue('key'); onChange({ action: 'advancedMatchingRulesChange', data: advancedMatchingRules.map((rule) => { if (rule.key === key) { - return { ...(value as RouteModule.MatchingRule), key }; + return { ...value, key }; } return rule; }), }); } else { const rule = { - ...(value as RouteModule.MatchingRule), + ...value, key: Math.random().toString(36).slice(2), }; onChange({ @@ -280,7 +292,7 @@ const MatchingRulesView: React.FC = ({ }; return ( - + {!disabled && ( } > @@ -96,7 +86,7 @@ const Page: React.FC = (props) => { - {step === 1 && } + {step === 1 && } {step === 2 && } diff --git a/web/src/pages/Upstream/components/Step1.tsx b/web/src/pages/Upstream/components/Step1.tsx index 40f3c31e69..226e71c278 100644 --- a/web/src/pages/Upstream/components/Step1.tsx +++ b/web/src/pages/Upstream/components/Step1.tsx @@ -14,27 +14,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Form, Input } from 'antd'; import type { FormInstance } from 'antd/lib/form'; import { useIntl } from 'umi'; import UpstreamForm from '@/components/Upstream'; -import { fetchList } from '../service'; type Props = { form: FormInstance; disabled?: boolean; upstreamRef?: React.MutableRefObject; + neverReadonly?: boolean; }; -const Step1: React.FC = ({ form, disabled, upstreamRef }) => { +const Step1: React.FC = ({ form, disabled, upstreamRef, neverReadonly }) => { const { formatMessage } = useIntl(); - const [list, setList] = useState([]); - - useEffect(() => { - fetchList({}).then(({ data }) => setList(data)); - }, []); return ( <> @@ -56,7 +51,7 @@ const Step1: React.FC = ({ form, disabled, upstreamRef }) => { /> - + ); }; diff --git a/web/src/pages/Upstream/locales/en-US.ts b/web/src/pages/Upstream/locales/en-US.ts index 6786f17b8b..702104eba4 100644 --- a/web/src/pages/Upstream/locales/en-US.ts +++ b/web/src/pages/Upstream/locales/en-US.ts @@ -19,6 +19,7 @@ export default { 'page.upstream.step.select.upstream.select.option': 'Custom', 'page.upstream.form.item-label.node.domain.or.ip': 'Targets', 'page.upstream.step.input.domain.name.or.ip': 'Please enter domain or IP', + 'page.upstream.step.valid.domain.name.or.ip': 'Please enter valid a domain or IP', 'page.upstream.step.domain.name.or.ip': 'Hostname or IP', 'page.upstream.step.input.port': 'Please enter port number', 'page.upstream.step.port': 'Port', @@ -98,5 +99,7 @@ export default { 'page.upstream.checks.passive.unhealthy.http_statuses.description': 'Which HTTP statuses to consider a success', 'page.upstream.checks.passive.unhealthy.http_failures.description': 'Number of HTTP failures to consider a target unhealthy', 'page.upstream.checks.passive.unhealthy.tcp_failures.description': 'Number of TCP failures to consider a target unhealthy', - 'page.upstream.scheme': 'Scheme' + 'page.upstream.scheme': 'Scheme', + + 'page.upstream.other.configuration.invalid': 'Please check the Upstream configuration' }; diff --git a/web/src/pages/Upstream/locales/zh-CN.ts b/web/src/pages/Upstream/locales/zh-CN.ts index 7a0d648ae9..eb0c19acc9 100644 --- a/web/src/pages/Upstream/locales/zh-CN.ts +++ b/web/src/pages/Upstream/locales/zh-CN.ts @@ -20,6 +20,7 @@ export default { 'page.upstream.form.item-label.node.domain.or.ip': '目标节点', 'page.upstream.step.input.domain.name.or.ip': '请输入域名或 IP', 'page.upstream.step.domain.name.or.ip': '主机名或 IP', + 'page.upstream.step.valid.domain.name.or.ip': '请输入合法的域名或 IP', 'page.upstream.step.input.port': '请输入', 'page.upstream.step.port': '端口', 'page.upstream.step.input.weight': '请输入权重', @@ -33,7 +34,7 @@ export default { 'page.upstream.step.type': '负载均衡算法', 'page.upstream.step.pass-host': 'Host 请求头', 'page.upstream.step.pass-host.pass': '保持与客户端请求一致的主机名', - 'page.upstream.step.pass-host.node': '使用上游节点列表中的主机名或 IP', + 'page.upstream.step.pass-host.node': '使用目标节点列表中的主机名或 IP', 'page.upstream.step.pass-host.rewrite': '自定义 Host 请求头(即将废弃)', 'page.upstream.step.pass-host.upstream_host': '自定义主机名', 'page.upstream.step.connect.timeout': '连接超时', @@ -98,5 +99,7 @@ export default { 'page.upstream.checks.passive.unhealthy.http_statuses.description': '当被动健康检查的探针返回值是 HTTP 状态码列表的某一个值时,代表不健康状态是由代理流量产生的。', 'page.upstream.checks.passive.unhealthy.http_failures.description': '由被动健康检查所观察,代理流量中 HTTP 失败的次数。如果达到此值,则认为上游服务目标节点是不健康的。', 'page.upstream.checks.passive.unhealthy.tcp_failures.description': '被动健康检查所观察到的代理流量中 TCP 失败的次数。如果达到此值,则认为上游服务目标节点是不健康的。', - 'page.upstream.scheme': '协议' + 'page.upstream.scheme': '协议', + + 'page.upstream.other.configuration.invalid': '请检查上游配置' }; diff --git a/web/src/pages/Upstream/service.ts b/web/src/pages/Upstream/service.ts index 3712f34d80..9dee86260e 100644 --- a/web/src/pages/Upstream/service.ts +++ b/web/src/pages/Upstream/service.ts @@ -16,6 +16,8 @@ */ import { request } from 'umi'; +import { convertToFormData } from '@/components/Upstream/service'; + export const fetchList = ({ current = 1, pageSize = 10, ...res }) => { return request>>('/upstreams', { params: { @@ -29,7 +31,7 @@ export const fetchList = ({ current = 1, pageSize = 10, ...res }) => { })); }; -export const fetchOne = (id: string) => request>(`/upstreams/${id}`); +export const fetchOne = (id: string) => request>(`/upstreams/${id}`).then(({data}) => convertToFormData(data)); export const create = (data: UpstreamModule.RequestBody) => request('/upstreams', { diff --git a/web/src/pages/Upstream/transform.ts b/web/src/pages/Upstream/transform.ts deleted file mode 100644 index 0b520f230d..0000000000 --- a/web/src/pages/Upstream/transform.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { pickBy, identity, omit, pick } from 'lodash'; - -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export const transformRequest = ( - formData: UpstreamModule.RequestBody, -): UpstreamModule.RequestBody | undefined | { upstream_id: string } => { - let data = pickBy(formData, identity) as UpstreamModule.RequestBody; - - data = omit(data, 'custom') - - const { - type, - hash_on, - key, - k8s_deployment_info, - nodes, - pass_host, - upstream_host, - upstream_id, - } = data; - - data.checks = pickBy(data.checks || {}, identity); - if (data.checks.active) { - data.checks.active = pickBy( - data.checks.active, - identity, - ) as UpstreamModule.HealthCheck['active']; - } - - if (upstream_id) { - return { upstream_id }; - } - - if (Object.keys(data.checks).length === 0) { - data = omit(data, 'checks'); - } - if (nodes && k8s_deployment_info) { - return undefined; - } - - if (!nodes && !k8s_deployment_info) { - return undefined; - } - - if (type === 'chash') { - if (!hash_on) { - return undefined; - } - - if (hash_on !== 'consumer' && !key) { - return undefined; - } - } - - if (pass_host === 'rewrite' && !upstream_host) { - return undefined; - } - - if (nodes) { - // NOTE: https://github.com/ant-design/ant-design/issues/27396 - data.nodes = data.nodes?.map((item) => { - return pick(item, ['host', 'port', 'weight']); - }); - return data; - } - - return undefined; -}; diff --git a/web/src/typings.d.ts b/web/src/typings.d.ts index f48879e7d5..48d483ed39 100644 --- a/web/src/typings.d.ts +++ b/web/src/typings.d.ts @@ -49,7 +49,6 @@ type Window = { declare let ga: Function; // preview.pro.ant.design only do not use in your production ; -// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined; declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; diff --git a/web/yarn.lock b/web/yarn.lock index 277c30bfc7..b0abedb223 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -185,11 +185,6 @@ lodash "^4.17.15" resize-observer-polyfill "^1.5.0" -"@api7-dashboard/ui@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@api7-dashboard/ui/-/ui-1.0.3.tgz#77011750bebee7bb6f6966ea0596c5576951e3ff" - integrity sha512-WSvcDBPcxmFb5b4nwUHlQ7J1IeA+buHs/if3wawSRU7imoUsuXb5BJ/39JPrOAoBmTjiuBOS6PChNGq4XUKCDg== - "@babel/code-frame@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"