You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/06/17 02:56:10 UTC

[incubator-apisix-dashboard] 02/02: feat: merged next

This is an automated email from the ASF dual-hosted git repository.

juzhiyuan pushed a commit to branch feat-master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix-dashboard.git

commit 67bfaff866718ab1a6130eb6e4537582d4a519b6
Author: juzhiyuan <jj...@gmail.com>
AuthorDate: Wed Jun 17 10:55:17 2020 +0800

    feat: merged next
---
 .asf.yaml                                          |    37 +
 .dockerignore                                      |     1 +
 .editorconfig                                      |    16 +
 .eslintignore                                      |     4 +
 .eslintrc.js                                       |     8 +
 .github/workflows/api_ut.yml                       |    19 +
 .gitignore                                         |    43 +
 .prettierignore                                    |    21 +
 .prettierrc.js                                     |     5 +
 .stylelintrc.js                                    |     5 +
 Dockerfile                                         |    19 +
 README-dashboard.md                                |    47 +
 README.md                                          |    10 +
 api/Dockerfile                                     |    49 +
 api/README.md                                      |    22 +
 api/build.sh                                       |    30 +
 api/conf.json                                      |    19 +
 api/conf/conf.go                                   |   104 +
 api/conf/conf.json                                 |    19 +
 api/conf/mysql.go                                  |    46 +
 api/docker-compose.yml                             |     9 +
 api/errno/error.go                                 |    98 +
 api/filter/cors.go                                 |    33 +
 api/filter/logging.go                              |    84 +
 api/filter/recover.go                              |   120 +
 api/filter/request_id.go                           |    42 +
 api/go.mod                                         |    15 +
 api/go.sum                                         |    89 +
 api/log/log.go                                     |   147 +
 api/main.go                                        |    65 +
 api/route/healthz.go                               |    37 +
 api/route/plugin.go                                |    58 +
 api/route/route.go                                 |   242 +
 api/route/ssl.go                                   |   153 +
 api/script/db/schema.sql                           |    31 +
 api/service/base.go                                |    48 +
 api/service/plugin.go                              |    61 +
 api/service/route.go                               |   560 +
 api/service/route_test.go                          |   139 +
 api/service/ssl.go                                 |   300 +
 api/utils/copy.go                                  |    41 +
 api/utils/http.go                                  |   113 +
 compose/README.md                                  |    56 +
 compose/apisix_conf/config.yaml                    |   137 +
 compose/docker-compose.yml                         |   124 +
 compose/grafana_conf/config/grafana.ini            |   756 +
 .../dashboards/apisix_http_prometheus.json         |   956 +
 .../grafana_conf/provisioning/dashboards/all.yaml  |    11 +
 .../grafana_conf/provisioning/datasources/all.yaml |     9 +
 compose/manager_conf/build.sh                      |    20 +
 compose/pics/grafana_1.png                         |   Bin 0 -> 57816 bytes
 compose/pics/grafana_2.png                         |   Bin 0 -> 127932 bytes
 compose/pics/grafana_3.png                         |   Bin 0 -> 103384 bytes
 compose/pics/grafana_4.png                         |   Bin 0 -> 82558 bytes
 compose/pics/grafana_5.png                         |   Bin 0 -> 105366 bytes
 compose/pics/grafana_6.png                         |   Bin 0 -> 179132 bytes
 compose/pics/login.png                             |   Bin 0 -> 84958 bytes
 compose/prometheus_conf/prometheus.yml             |    23 +
 config/config.ts                                   |    48 +
 config/defaultSettings.ts                          |    20 +
 config/proxy.ts                                    |    30 +
 config/routes.ts                                   |    76 +
 docker/nginx.conf                                  |    21 +
 jest.config.js                                     |     9 +
 jsconfig.json                                      |    10 +
 mock/notices.ts                                    |   105 +
 mock/route.ts                                      |     5 +
 mock/user.ts                                       |   154 +
 netlify.toml                                       |    13 +
 package.json                                       |   109 +
 public/favicon.png                                 |   Bin 0 -> 85376 bytes
 public/home_bg.png                                 |   Bin 0 -> 203330 bytes
 public/icons/icon-128x128.png                      |   Bin 0 -> 1329 bytes
 public/icons/icon-192x192.png                      |   Bin 0 -> 1856 bytes
 public/icons/icon-512x512.png                      |   Bin 0 -> 5082 bytes
 public/pro_icon.svg                                |     1 +
 src/access.ts                                      |     6 +
 src/app.tsx                                        |    92 +
 src/assets/logo.svg                                |    21 +
 src/components/Footer/index.tsx                    |    17 +
 src/components/HeaderDropdown/index.less           |    16 +
 src/components/HeaderDropdown/index.tsx            |    19 +
 src/components/NoticeIcon/NoticeList.less          |   103 +
 src/components/NoticeIcon/NoticeList.tsx           |   113 +
 src/components/NoticeIcon/index.less               |    31 +
 src/components/NoticeIcon/index.tsx                |   124 +
 src/components/PageLoading/index.tsx               |     5 +
 src/components/PluginForm/PluginForm.tsx           |   192 +
 src/components/PluginForm/README.md                |     9 +
 src/components/PluginForm/data.ts                  |   100 +
 src/components/PluginForm/index.ts                 |     3 +
 src/components/PluginForm/locales/en-US.ts         |   152 +
 src/components/PluginForm/locales/zh-CN.ts         |   152 +
 src/components/PluginForm/service.ts               |     5 +
 src/components/PluginForm/transformer.ts           |    90 +
 src/components/PluginForm/typing.d.ts              |    64 +
 src/components/PluginModal/index.tsx               |    32 +
 src/components/RightContent/AvatarDropdown.tsx     |    94 +
 src/components/RightContent/index.less             |    82 +
 src/components/RightContent/index.tsx              |    51 +
 src/e2e/__mocks__/antd-pro-merge-less.js           |     1 +
 src/e2e/baseLayout.e2e.js                          |    57 +
 src/global.less                                    |    69 +
 src/global.tsx                                     |    83 +
 src/locales/en-US.ts                               |    23 +
 src/locales/en-US/component.ts                     |    39 +
 src/locales/en-US/globalHeader.ts                  |    17 +
 src/locales/en-US/menu.ts                          |    60 +
 src/locales/en-US/pwa.ts                           |     6 +
 src/locales/en-US/setting.ts                       |     4 +
 src/locales/en-US/settingDrawer.ts                 |    31 +
 src/locales/zh-CN.ts                               |    23 +
 src/locales/zh-CN/component.ts                     |    39 +
 src/locales/zh-CN/globalHeader.ts                  |    17 +
 src/locales/zh-CN/menu.ts                          |    61 +
 src/locales/zh-CN/pwa.ts                           |     6 +
 src/locales/zh-CN/setting.ts                       |    12 +
 src/locales/zh-CN/settingDrawer.ts                 |    31 +
 src/manifest.json                                  |    22 +
 src/pages/404.tsx                                  |    18 +
 src/pages/Metrics/Metrics.tsx                      |    54 +
 src/pages/Routes/Create.less                       |   112 +
 src/pages/Routes/Create.tsx                        |   223 +
 src/pages/Routes/List.tsx                          |    82 +
 .../Routes/components/ActionBar/ActionBar.tsx      |    46 +
 src/pages/Routes/components/ActionBar/index.ts     |     1 +
 .../Routes/components/CreateStep3/CreateStep3.tsx  |    96 +
 .../Routes/components/CreateStep3/PluginCard.tsx   |    26 +
 .../Routes/components/CreateStep3/PluginDrawer.tsx |    73 +
 src/pages/Routes/components/CreateStep3/index.ts   |     1 +
 .../Routes/components/CreateStep4/CreateStep4.tsx  |    35 +
 src/pages/Routes/components/CreateStep4/index.ts   |     1 +
 src/pages/Routes/components/PanelSection/index.tsx |    16 +
 .../Routes/components/ResultView/ResultView.tsx    |    24 +
 src/pages/Routes/components/ResultView/index.ts    |     1 +
 .../Routes/components/Step1/MatchingRulesView.tsx  |   214 +
 src/pages/Routes/components/Step1/MetaView.tsx     |    27 +
 .../Routes/components/Step1/RequestConfigView.tsx  |   201 +
 src/pages/Routes/components/Step1/index.tsx        |    41 +
 .../components/Step2/HttpHeaderRewriteView.tsx     |   166 +
 .../Routes/components/Step2/RequestRewriteView.tsx |   174 +
 src/pages/Routes/components/Step2/index.tsx        |    20 +
 src/pages/Routes/constants.ts                      |    61 +
 src/pages/Routes/service.ts                        |    24 +
 src/pages/Routes/transform.ts                      |   176 +
 src/pages/Routes/typing.d.ts                       |   125 +
 src/pages/Setting/Setting.tsx                      |   102 +
 src/pages/Setting/index.ts                         |     2 +
 src/pages/Setting/service.ts                       |     7 +
 src/pages/Setting/style.less                       |   109 +
 src/pages/Setting/typingd.d.ts                     |     9 +
 src/pages/document.ejs                             |   193 +
 src/pages/ssl/Create.less                          |   101 +
 src/pages/ssl/Create.tsx                           |    65 +
 src/pages/ssl/List.tsx                             |   110 +
 src/pages/ssl/components/CertificateForm/index.tsx |    75 +
 .../ssl/components/CertificateUploader/index.tsx   |    97 +
 src/pages/ssl/components/Step1/index.tsx           |   104 +
 src/pages/ssl/components/Step2/index.tsx           |    32 +
 src/pages/ssl/components/Step3/index.tsx           |    36 +
 src/pages/ssl/service.ts                           |    78 +
 src/pages/ssl/typing.d.ts                          |    18 +
 src/service-worker.js                              |    70 +
 src/services/API.d.ts                              |    24 +
 src/services/login.ts                              |    20 +
 src/services/user.ts                               |    23 +
 src/transforms/global.ts                           |    26 +
 src/typings.d.ts                                   |    40 +
 tests/PuppeteerEnvironment.js                      |    41 +
 tests/beforeTest.js                                |    39 +
 tests/getBrowser.js                                |    45 +
 tests/run-tests.js                                 |    52 +
 tsconfig.json                                      |    26 +
 yarn.lock                                          | 18920 +++++++++++++++++++
 174 files changed, 30378 insertions(+)

diff --git a/.asf.yaml b/.asf.yaml
new file mode 100644
index 0000000..fe73d20
--- /dev/null
+++ b/.asf.yaml
@@ -0,0 +1,37 @@
+#
+# 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.
+#
+
+github:
+    description: Dashboard for Apache APISIX
+    homepage: https://apisix.apache.org/
+    labels:
+      - dashboard
+      - api
+      - api-management
+      - apisix
+      - devops
+      - docker
+
+    enabled_merge_buttons:
+      squash:  true
+      merge:   false
+      rebase:  false
+
+    notifications:
+      commits:      notifications@apisix.apache.org
+      issues:       notifications@apisix.apache.org
+      pullrequests: notifications@apisix.apache.org
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+node_modules
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..7e3649a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..16116a2
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,4 @@
+/lambda/
+/scripts
+/config
+.history
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..b882c20
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,8 @@
+module.exports = {
+  extends: [require.resolve('@umijs/fabric/dist/eslint')],
+  globals: {
+    ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
+    page: true,
+    REACT_APP_ENV: true,
+  },
+};
diff --git a/.github/workflows/api_ut.yml b/.github/workflows/api_ut.yml
new file mode 100644
index 0000000..6380353
--- /dev/null
+++ b/.github/workflows/api_ut.yml
@@ -0,0 +1,19 @@
+name: API unit test
+
+on:
+  push:
+    branches: 
+      - master
+      - manager
+  pull_request:
+    branches:
+      - master
+      - next
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: use docker-compose in api
+      run: cd ./api && docker-compose up
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9caa192
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,43 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+**/node_modules
+# roadhog-api-doc ignore
+/src/utils/request-temp.js
+_roadhog-api-doc
+
+# production
+/dist
+/.vscode
+
+# misc
+.DS_Store
+npm-debug.log*
+yarn-error.log
+
+/coverage
+.idea
+package-lock.json
+*bak
+.vscode
+
+# visual studio code
+.history
+*.log
+functions/*
+.temp/**
+
+# umi
+.umi
+.umi-production
+
+# screenshot
+screenshot
+.firebase
+.eslintcache
+
+build
+
+/compose/**/*.log
+/compose/**/nginx.pid
+/compose/etcd_data
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..87715a7
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,21 @@
+**/*.svg
+package.json
+.umi
+.umi-production
+/dist
+.dockerignore
+.DS_Store
+.eslintignore
+*.png
+*.toml
+docker
+.editorconfig
+Dockerfile*
+.gitignore
+.prettierignore
+LICENSE
+.eslintcache
+*.lock
+yarn-error.log
+.history
+CNAME
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 0000000..7b597d7
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,5 @@
+const fabric = require('@umijs/fabric');
+
+module.exports = {
+  ...fabric.prettier,
+};
diff --git a/.stylelintrc.js b/.stylelintrc.js
new file mode 100644
index 0000000..c203078
--- /dev/null
+++ b/.stylelintrc.js
@@ -0,0 +1,5 @@
+const fabric = require('@umijs/fabric');
+
+module.exports = {
+  ...fabric.stylelint,
+};
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..72ac8d3
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,19 @@
+# phase-build
+FROM node:12-alpine as builder
+
+WORKDIR /usr/src/app/
+USER root
+
+COPY package.json /usr/src/app/
+RUN yarn
+
+COPY . /usr/src/app/
+RUN yarn build && rm -rf /usr/src/app/node_modules
+
+# phase-run
+FROM nginx:1.16-alpine
+
+COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=builder /usr/src/app/dist /usr/share/nginx/html/dashboard
+
+EXPOSE 80
diff --git a/README-dashboard.md b/README-dashboard.md
new file mode 100644
index 0000000..74f8748
--- /dev/null
+++ b/README-dashboard.md
@@ -0,0 +1,47 @@
+# READMD for Dashboard
+
+This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
+
+## Environment Prepare
+
+1. Make sure you have `Node.js` installed on your machine.
+2. Install [yarn](https://yarnpkg.com/).
+3. Install `node_modules`:
+
+```bash
+$ yarn
+```
+
+### Start project
+
+```bash
+yarn start:no-mock
+```
+
+### Build project
+
+```bash
+yarn build
+```
+
+### Check code style
+
+```bash
+yarn lint
+```
+
+You can also use script to auto fix some lint error:
+
+```bash
+yarn lint:fix
+```
+
+### Test code
+
+```bash
+yarn test
+```
+
+## More
+
+You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0a82a39
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+# Apache APISIX Dashboard
+
+Dashboard for [Apache APISIX](https://github.com/apache/incubator-apisix-dashboard)
+
+## Deploy with Docker
+Please refer to [Deploy with Docker README](./compose/README.md)
+
+## More
+
+1. More infomation about the frontend Dashboard, please refer to [README for Dashboard](./README-dashboard.md)
diff --git a/api/Dockerfile b/api/Dockerfile
new file mode 100644
index 0000000..85092a1
--- /dev/null
+++ b/api/Dockerfile
@@ -0,0 +1,49 @@
+#
+# 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.
+#
+
+FROM golang:1.13.8 AS build-env
+
+WORKDIR /go/src/github.com/apisix/manager-api
+COPY . .
+RUN mkdir /root/manager-api \
+    && go env -w GOPROXY=https://goproxy.io,direct \
+    && export GOPROXY=https://goproxy.io \
+    && go build -o /root/manager-api/manager-api \
+    && mv /go/src/github.com/apisix/manager-api/build.sh /root/manager-api/ \
+    && mv /go/src/github.com/apisix/manager-api/conf.json /root/manager-api/ \
+    && rm -rf /go/src/github.com/apisix/manager-api \
+    && rm -rf /etc/localtime \
+    && ln -s  /usr/share/zoneinfo/Hongkong /etc/localtime \
+    && dpkg-reconfigure -f noninteractive tzdata
+
+FROM alpine:3.11
+
+RUN mkdir /root/manager-api \
+   && apk update  \
+   && apk add ca-certificates \
+   && update-ca-certificates \
+   && apk add --no-cache libc6-compat \
+   && echo "hosts: files dns" > /etc/nsswitch.conf \
+   && rm -rf /var/cache/apk/*
+
+
+WORKDIR /root/manager-api
+COPY --from=build-env /root/manager-api/* /root/manager-api/
+COPY --from=build-env /usr/share/zoneinfo/Hongkong /etc/localtime
+EXPOSE 8080
+RUN chmod +x ./build.sh
+CMD ["/root/manager-api/build.sh"]
diff --git a/api/README.md b/api/README.md
new file mode 100644
index 0000000..24befa6
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,22 @@
+<!--
+#
+# 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.
+#
+-->
+
+# manager-api
+
+This is a back-end project that the dashboard depends on, implemented through golang.
diff --git a/api/build.sh b/api/build.sh
new file mode 100644
index 0000000..086fe73
--- /dev/null
+++ b/api/build.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+#	
+# 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.	
+#
+
+pwd=`pwd`
+
+sed -i -e "s%#mysqlAddress#%`echo $MYSQL_SERVER_ADDRESS`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlUser#%`echo $MYSQL_USER`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlPWD#%`echo $MYSQL_PASSWORD`%g" ${pwd}/conf.json
+sed -i -e "s%#syslogAddress#%`echo $SYSLOG_HOST`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixBaseUrl#%`echo $APISIX_BASE_URL`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixApiKey#%`echo $APISIX_API_KEY`%g" ${pwd}/conf.json
+
+cd /root/manager-api
+exec ./manager-api
+
diff --git a/api/conf.json b/api/conf.json
new file mode 100644
index 0000000..31cbcce
--- /dev/null
+++ b/api/conf.json
@@ -0,0 +1,19 @@
+{
+  "conf": {
+    "mysql":{
+      "address": "#mysqlAddress#",
+      "user": "#mysqlUser#",
+      "password": "#mysqlPWD#",
+      "maxConns": 50,
+      "maxIdleConns": 25,
+      "maxLifeTime": 10
+    },
+    "syslog": {
+      "host": "#syslogAddress#"
+    },
+    "apisix": {
+      "base_url": "#apisixBaseUrl#",
+      "api_key": "#apisixApiKey#"
+    }
+  }
+}
diff --git a/api/conf/conf.go b/api/conf/conf.go
new file mode 100644
index 0000000..5819789
--- /dev/null
+++ b/api/conf/conf.go
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+package conf
+
+import (
+	"fmt"
+	"github.com/tidwall/gjson"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"runtime"
+)
+
+const ServerPort = 8080
+const PROD = "prod"
+const BETA = "beta"
+const DEV = "dev"
+const LOCAL = "local"
+const confPath = "/root/manager-api/conf.json"
+const RequestId = "requestId"
+
+var (
+	ENV      string
+	basePath string
+	ApiKey   = "edd1c9f034335f136f87ad84b625c8f1"
+	BaseUrl  = "http://127.0.0.1:9080/apisix/admin"
+)
+
+func init() {
+	setEnvironment()
+	initMysql()
+	initApisix()
+}
+
+func setEnvironment() {
+	if env := os.Getenv("ENV"); env == "" {
+		ENV = LOCAL
+	} else {
+		ENV = env
+	}
+	_, basePath, _, _ = runtime.Caller(1)
+}
+
+func configurationPath() string {
+	if ENV == LOCAL {
+		return filepath.Join(filepath.Dir(basePath), "conf.json")
+	} else {
+		return confPath
+	}
+}
+
+type mysqlConfig struct {
+	Address  string
+	User     string
+	Password string
+
+	MaxConns     int
+	MaxIdleConns int
+	MaxLifeTime  int
+}
+
+var MysqlConfig mysqlConfig
+
+func initMysql() {
+	filePath := configurationPath()
+	if configurationContent, err := ioutil.ReadFile(filePath); err != nil {
+		panic(fmt.Sprintf("fail to read configuration: %s", filePath))
+	} else {
+		configuration := gjson.ParseBytes(configurationContent)
+		mysqlConf := configuration.Get("conf.mysql")
+		MysqlConfig.Address = mysqlConf.Get("address").String()
+		MysqlConfig.User = mysqlConf.Get("user").String()
+		MysqlConfig.Password = mysqlConf.Get("password").String()
+		MysqlConfig.MaxConns = int(mysqlConf.Get("maxConns").Int())
+		MysqlConfig.MaxIdleConns = int(mysqlConf.Get("maxIdleConns").Int())
+		MysqlConfig.MaxLifeTime = int(mysqlConf.Get("maxLifeTime").Int())
+	}
+}
+
+func initApisix() {
+	filePath := configurationPath()
+	if configurationContent, err := ioutil.ReadFile(filePath); err != nil {
+		panic(fmt.Sprintf("fail to read configuration: %s", filePath))
+	} else {
+		configuration := gjson.ParseBytes(configurationContent)
+		apisixConf := configuration.Get("conf.apisix")
+		BaseUrl = apisixConf.Get("base_url").String()
+		ApiKey = apisixConf.Get("api_key").String()
+	}
+}
diff --git a/api/conf/conf.json b/api/conf/conf.json
new file mode 100644
index 0000000..95fe86e
--- /dev/null
+++ b/api/conf/conf.json
@@ -0,0 +1,19 @@
+{
+  "conf":{
+    "mysql":{
+      "address": "127.0.0.1:3306",
+      "user": "root",
+      "password": "123456",
+      "maxConns": 50,
+      "maxIdleConns": 25,
+      "maxLifeTime": 10
+    },
+    "syslog":{
+      "host": "localhost"
+    },
+    "apisix":{
+      "base_url": "http://127.0.0.1:9080/apisix/admin",
+      "api_key": "edd1c9f034335f136f87ad84b625c8f1"
+    }
+  }
+}
\ No newline at end of file
diff --git a/api/conf/mysql.go b/api/conf/mysql.go
new file mode 100644
index 0000000..62e0f9e
--- /dev/null
+++ b/api/conf/mysql.go
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package conf
+
+import (
+	"fmt"
+	"github.com/jinzhu/gorm"
+	_ "github.com/jinzhu/gorm/dialects/mysql"
+	"time"
+)
+
+var db *gorm.DB
+
+func DB() *gorm.DB {
+	return db
+}
+
+// InitializeMysql creates mysql's *sqlDB instance
+func InitializeMysql() {
+	dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", MysqlConfig.User,
+		MysqlConfig.Password, MysqlConfig.Address, "manager")
+	if tmp, err := gorm.Open("mysql", dsn); err != nil {
+		panic(fmt.Sprintf("fail to connect to DB: %s for %s", err.Error(), dsn))
+	} else {
+		db = tmp
+		db.LogMode(true)
+		db.DB().SetMaxOpenConns(MysqlConfig.MaxConns)
+		db.DB().SetMaxIdleConns(MysqlConfig.MaxIdleConns)
+		db.DB().SetConnMaxLifetime(time.Duration(MysqlConfig.MaxLifeTime) * time.Minute)
+	}
+
+}
diff --git a/api/docker-compose.yml b/api/docker-compose.yml
new file mode 100644
index 0000000..79760d7
--- /dev/null
+++ b/api/docker-compose.yml
@@ -0,0 +1,9 @@
+version: "3"
+
+services:
+  manager:
+    image: golang:1.13.8
+    volumes:
+      - .:/go/src/github.com/apisix/manager-api
+    working_dir: /go/src/github.com/apisix/manager-api
+    command: go test -v github.com/apisix/manager-api/service
diff --git a/api/errno/error.go b/api/errno/error.go
new file mode 100644
index 0000000..20b7e24
--- /dev/null
+++ b/api/errno/error.go
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+package errno
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+type Message struct {
+	Code string
+	Msg  string
+}
+
+var (
+	//AA 01 api-manager-api
+	SystemSuccess   = Message{"010000", "success"}
+	SystemError     = Message{"010001", "system error"}
+	BadRequestError = Message{Code: "010002", Msg: "Request format error"}
+	NotFoundError   = Message{Code: "010003", Msg: "No resources found"}
+
+	//BB 01 config module
+	ConfEnvError      = Message{"010101", "Environment variable not found: %s"}
+	ConfFilePathError = Message{"010102", "Error loading configuration file: %s"}
+
+	// BB 02 route module
+	RouteRequestError      = Message{"010201", "Route request parameters are abnormal: %s"}
+	ApisixRouteCreateError = Message{"010202", "Failed to create APISIX route: %s"}
+	DBRouteCreateError     = Message{"010203", "Route storage failure: %s"}
+	ApisixRouteUpdateError = Message{"010204", "Update APISIX routing failed: %s"}
+	ApisixRouteDeleteError = Message{"010205", "Failed to remove APISIX route: %s"}
+	DBRouteUpdateError     = Message{"010206", "Route update failed: %s"}
+	DBRouteDeleteError     = Message{"010207", "Route remove failed: %s"}
+
+	// 03 plugin module
+	ApisixPluginListError   = Message{"010301", "List APISIX plugins  failed: %s"}
+	ApisixPluginSchemaError = Message{"010301", "Find APISIX plugin schema failed: %s"}
+)
+
+type ManagerError struct {
+	TraceId string
+	Code    string
+	Msg     string
+	Data    interface{}
+}
+
+// toString
+func (e *ManagerError) Error() string {
+	return e.Msg
+}
+
+func FromMessage(m Message, args ...interface{}) *ManagerError {
+	return &ManagerError{TraceId: "", Code: m.Code, Msg: fmt.Sprintf(m.Msg, args...)}
+}
+
+func (e *ManagerError) Response() map[string]interface{} {
+	return map[string]interface{}{
+		"code": e.Code,
+		"msg":  e.Msg,
+	}
+}
+
+func (e *ManagerError) ItemResponse(data interface{}) map[string]interface{} {
+	return map[string]interface{}{
+		"code": e.Code,
+		"msg":  e.Msg,
+		"data": data,
+	}
+}
+
+func (e *ManagerError) ListResponse(count, list interface{}) map[string]interface{} {
+	return map[string]interface{}{
+		"code":  e.Code,
+		"msg":   e.Msg,
+		"count": count,
+		"list":  list,
+	}
+}
+
+func Success() []byte {
+	w := FromMessage(SystemSuccess).Response()
+	result, _ := json.Marshal(w)
+	return result
+}
diff --git a/api/filter/cors.go b/api/filter/cors.go
new file mode 100644
index 0000000..f2c7ac1
--- /dev/null
+++ b/api/filter/cors.go
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package filter
+
+import "github.com/gin-gonic/gin"
+
+func CORS() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
+		c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
+		c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
+		if c.Request.Method == "OPTIONS" {
+			c.AbortWithStatus(204)
+			return
+		}
+		c.Next()
+	}
+}
diff --git a/api/filter/logging.go b/api/filter/logging.go
new file mode 100644
index 0000000..22f5528
--- /dev/null
+++ b/api/filter/logging.go
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+package filter
+
+import (
+	"bytes"
+	"github.com/apisix/manager-api/errno"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+	"time"
+)
+
+func RequestLogHandler() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		start, host, remoteIP, path, method := time.Now(), c.Request.Host, c.ClientIP(), c.Request.URL.Path, c.Request.Method
+		var val interface{}
+		if method == "GET" {
+			val = c.Request.URL.Query()
+		} else {
+			val, _ = c.GetRawData()
+		}
+		c.Set("requestBody", val)
+		uuid, _ := c.Get("X-Request-Id")
+
+		param, _ := c.Get("requestBody")
+		switch param.(type) {
+		case []byte:
+			param = string(param.([]byte))
+		default:
+		}
+
+		blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
+		c.Writer = blw
+		c.Next()
+		latency := time.Now().Sub(start) / 1000000
+		statusCode := c.Writer.Status()
+		respBody := blw.body.String()
+		if uuid == "" {
+			uuid = c.Writer.Header().Get("X-Request-Id")
+		}
+		var errs []string
+		for _, err := range c.Errors {
+			if e, ok := err.Err.(*errno.ManagerError); ok {
+				errs = append(errs, e.Error())
+			}
+		}
+		logger.WithFields(logrus.Fields{
+			"requestId":  uuid,
+			"latency":    latency,
+			"remoteIp":   remoteIP,
+			"method":     method,
+			"path":       path,
+			"statusCode": statusCode,
+			"host":       host,
+			"params":     param,
+			"respBody":   respBody,
+			"errMsg":     errs,
+		}).Info("")
+	}
+}
+
+type bodyLogWriter struct {
+	gin.ResponseWriter
+	body *bytes.Buffer
+}
+
+func (w bodyLogWriter) Write(b []byte) (int, error) {
+	w.body.Write(b)
+	return w.ResponseWriter.Write(b)
+}
diff --git a/api/filter/recover.go b/api/filter/recover.go
new file mode 100644
index 0000000..433331f
--- /dev/null
+++ b/api/filter/recover.go
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+package filter
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/apisix/manager-api/log"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+	"io/ioutil"
+	"net/http"
+	"runtime"
+	"time"
+)
+
+var (
+	logger    = log.GetLogger()
+	dunno     = []byte("???")
+	centerDot = []byte("·")
+	dot       = []byte(".")
+	slash     = []byte("/")
+)
+
+func RecoverHandler() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		defer func() {
+			if err := recover(); err != nil {
+				uuid := c.Writer.Header().Get("X-Request-Id")
+				logger.WithFields(logrus.Fields{
+					"uuid": uuid,
+				})
+				stack := stack(3)
+				logger.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack)
+				c.AbortWithStatus(http.StatusInternalServerError)
+			}
+		}()
+		c.Next()
+	}
+}
+
+func WrapGo(f func(...interface{}), args ...interface{}) {
+	defer func() {
+		if err := recover(); err != nil {
+			stack := stack(3)
+			logger.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack)
+		}
+	}()
+	f(args...)
+}
+
+func stack(skip int) []byte {
+	buf := new(bytes.Buffer) // the returned data
+	// loaded file.
+	var lines [][]byte
+	var lastFile string
+	for i := skip; ; i++ {
+		pc, file, line, ok := runtime.Caller(i)
+		if !ok {
+			break
+		}
+		// Print this much at least.  If we can't find the source, it won't show.
+		fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
+		if file != lastFile {
+			data, err := ioutil.ReadFile(file)
+			if err != nil {
+				continue
+			}
+			lines = bytes.Split(data, []byte{'\n'})
+			lastFile = file
+		}
+		fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
+	}
+	return buf.Bytes()
+}
+
+// source returns a space-trimmed slice of the n'th line.
+func source(lines [][]byte, n int) []byte {
+	n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
+	if n < 0 || n >= len(lines) {
+		return dunno
+	}
+	return bytes.TrimSpace(lines[n])
+}
+
+// function returns, if possible, the name of the function containing the PC.
+func function(pc uintptr) []byte {
+	fn := runtime.FuncForPC(pc)
+	if fn == nil {
+		return dunno
+	}
+	name := []byte(fn.Name())
+	if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
+		name = name[lastslash+1:]
+	}
+	if period := bytes.Index(name, dot); period >= 0 {
+		name = name[period+1:]
+	}
+	name = bytes.Replace(name, centerDot, dot, -1)
+	return name
+}
+
+func timeFormat(t time.Time) string {
+	var timeString = t.Format("2006/01/02 - 15:04:05")
+	return timeString
+}
diff --git a/api/filter/request_id.go b/api/filter/request_id.go
new file mode 100644
index 0000000..2992869
--- /dev/null
+++ b/api/filter/request_id.go
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package filter
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/satori/go.uuid"
+)
+
+func RequestId() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Check for incoming header, use it if exists
+		requestId := c.Request.Header.Get("X-Request-Id")
+
+		// Create request id with UUID4
+		if requestId == "" {
+			u4 := uuid.NewV4()
+			requestId = u4.String()
+		}
+
+		// Expose it for use in the application
+		c.Set("X-Request-Id", requestId)
+
+		// Set X-Request-Id header
+		c.Writer.Header().Set("X-Request-Id", requestId)
+		c.Next()
+	}
+}
diff --git a/api/go.mod b/api/go.mod
new file mode 100644
index 0000000..45372ec
--- /dev/null
+++ b/api/go.mod
@@ -0,0 +1,15 @@
+module github.com/apisix/manager-api
+
+go 1.13
+
+require (
+	github.com/gin-contrib/pprof v1.3.0
+	github.com/gin-gonic/gin v1.6.3
+	github.com/go-sql-driver/mysql v1.5.0
+	github.com/jinzhu/gorm v1.9.12
+	github.com/satori/go.uuid v1.2.0
+	github.com/sirupsen/logrus v1.6.0
+	github.com/stretchr/testify v1.4.0
+	github.com/tidwall/gjson v1.6.0
+	gopkg.in/resty.v1 v1.12.0
+)
diff --git a/api/go.sum b/api/go.sum
new file mode 100644
index 0000000..9e3e3e3
--- /dev/null
+++ b/api/go.sum
@@ -0,0 +1,89 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
+github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
+github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
+github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
+github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
+github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
+github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/api/log/log.go b/api/log/log.go
new file mode 100644
index 0000000..c1645e8
--- /dev/null
+++ b/api/log/log.go
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+package log
+
+import (
+	"bufio"
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/sirupsen/logrus"
+	"os"
+	"runtime"
+	"strings"
+)
+
+var logEntry *logrus.Entry
+
+func GetLogger() *logrus.Entry {
+	if logEntry == nil {
+		var log = logrus.New()
+		setNull(log)
+		log.SetLevel(logrus.DebugLevel)
+		if conf.ENV != conf.LOCAL {
+			log.SetLevel(logrus.ErrorLevel)
+		}
+		log.SetFormatter(&logrus.JSONFormatter{})
+		logEntry = log.WithFields(logrus.Fields{
+			"app": "manager-api",
+		})
+		if hook, err := createHook(); err == nil {
+			log.AddHook(hook)
+		}
+	}
+	return logEntry
+}
+
+func setNull(log *logrus.Logger) {
+	src, err := os.OpenFile(os.DevNull, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
+	if err != nil {
+		fmt.Println("err", err)
+	}
+	writer := bufio.NewWriter(src)
+	log.SetOutput(writer)
+}
+
+type Hook struct {
+	Formatter func(file, function string, line int) string
+}
+
+func createHook() (*Hook, error) {
+	return &Hook{
+		func(file, function string, line int) string {
+			return fmt.Sprintf("%s:%d", file, line)
+		},
+	}, nil
+}
+
+func (hook *Hook) Fire(entry *logrus.Entry) error {
+	str := hook.Formatter(findCaller(5))
+	en := entry.WithField("line", str)
+	en.Level = entry.Level
+	en.Message = entry.Message
+	en.Time = entry.Time
+	line, err := en.String()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
+		return err
+	}
+	switch en.Level {
+	case logrus.PanicLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.FatalLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.ErrorLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.WarnLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.InfoLevel:
+		fmt.Print(line)
+		return nil
+	case logrus.DebugLevel:
+		fmt.Print(line)
+		return nil
+	default:
+		return nil
+	}
+}
+
+func (hook *Hook) Levels() []logrus.Level {
+	return logrus.AllLevels
+}
+
+func findCaller(skip int) (string, string, int) {
+	var (
+		pc       uintptr
+		file     string
+		function string
+		line     int
+	)
+	for i := 0; i < 10; i++ {
+		pc, file, line = getCaller(skip + i)
+		if !strings.HasPrefix(file, "logrus") {
+			break
+		}
+	}
+	if pc != 0 {
+		frames := runtime.CallersFrames([]uintptr{pc})
+		frame, _ := frames.Next()
+		function = frame.Function
+	}
+	return file, function, line
+}
+
+func getCaller(skip int) (uintptr, string, int) {
+	pc, file, line, ok := runtime.Caller(skip)
+	if !ok {
+		return 0, "", 0
+	}
+	n := 0
+	for i := len(file) - 1; i > 0; i-- {
+		if file[i] == '/' {
+			n += 1
+			if n >= 2 {
+				file = file[i+1:]
+				break
+			}
+		}
+	}
+	return pc, file, line
+}
diff --git a/api/main.go b/api/main.go
new file mode 100644
index 0000000..3d756dd
--- /dev/null
+++ b/api/main.go
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package main
+
+import (
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/filter"
+	"github.com/apisix/manager-api/log"
+	"github.com/apisix/manager-api/route"
+	"github.com/gin-contrib/pprof"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"time"
+)
+
+var logger = log.GetLogger()
+
+func setUpRouter() *gin.Engine {
+	if conf.ENV != conf.LOCAL && conf.ENV != conf.BETA {
+		gin.SetMode(gin.DebugMode)
+	} else {
+		gin.SetMode(gin.ReleaseMode)
+	}
+	r := gin.New()
+
+	r.Use(filter.CORS(), filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
+	route.AppendHealthCheck(r)
+	route.AppendRoute(r)
+	route.AppendSsl(r)
+	route.AppendPlugin(r)
+
+	pprof.Register(r)
+
+	return r
+}
+
+func main() {
+	// init
+	conf.InitializeMysql()
+	// routes
+	r := setUpRouter()
+	addr := fmt.Sprintf(":%d", conf.ServerPort)
+	s := &http.Server{
+		Addr:         addr,
+		Handler:      r,
+		ReadTimeout:  time.Duration(1000) * time.Millisecond,
+		WriteTimeout: time.Duration(5000) * time.Millisecond,
+	}
+	s.ListenAndServe()
+}
diff --git a/api/route/healthz.go b/api/route/healthz.go
new file mode 100644
index 0000000..97dcd04
--- /dev/null
+++ b/api/route/healthz.go
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+package route
+
+import (
+	"github.com/apisix/manager-api/log"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+var logger = log.GetLogger()
+
+func healthzHandler() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Copy()
+		c.String(http.StatusOK, "pong")
+	}
+}
+
+func AppendHealthCheck(r *gin.Engine) *gin.Engine {
+	r.GET("/ping", healthzHandler())
+	return r
+}
diff --git a/api/route/plugin.go b/api/route/plugin.go
new file mode 100644
index 0000000..0032b13
--- /dev/null
+++ b/api/route/plugin.go
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package route
+
+import (
+	"encoding/json"
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/service"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func AppendPlugin(r *gin.Engine) *gin.Engine {
+	r.GET("/apisix/admin/plugins", listPlugin)
+	r.GET("/apisix/admin/schema/plugins/:name", findSchema)
+	return r
+}
+
+func findSchema(c *gin.Context) {
+	name := c.Param("name")
+	request := &service.ApisixPluginRequest{Name: name}
+	if result, err := request.Schema(); err != nil {
+		e := errno.FromMessage(errno.ApisixPluginSchemaError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	} else {
+		resp, _ := json.Marshal(result)
+		c.Data(http.StatusOK, service.ContentType, resp)
+	}
+}
+
+func listPlugin(c *gin.Context) {
+	request := &service.ApisixPluginRequest{}
+	if result, err := request.List(); err != nil {
+		e := errno.FromMessage(errno.ApisixPluginListError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	} else {
+		resp, _ := json.Marshal(result)
+		c.Data(http.StatusOK, service.ContentType, resp)
+	}
+}
diff --git a/api/route/route.go b/api/route/route.go
new file mode 100644
index 0000000..efc95d2
--- /dev/null
+++ b/api/route/route.go
@@ -0,0 +1,242 @@
+/*
+ * 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.
+ */
+package route
+
+import (
+	"encoding/json"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/service"
+	"github.com/gin-gonic/gin"
+	"github.com/satori/go.uuid"
+	"net/http"
+	"strconv"
+)
+
+func AppendRoute(r *gin.Engine) *gin.Engine {
+	r.POST("/apisix/admin/routes", createRoute)
+	r.GET("/apisix/admin/routes/:rid", findRoute)
+	r.GET("/apisix/admin/routes", listRoute)
+	r.PUT("/apisix/admin/routes/:rid", updateRoute)
+	r.DELETE("/apisix/admin/routes/:rid", deleteRoute)
+	return r
+}
+
+func listRoute(c *gin.Context) {
+	db := conf.DB()
+	size, _ := strconv.Atoi(c.Query("size"))
+	page, _ := strconv.Atoi(c.Query("page"))
+	if size == 0 {
+		size = 10
+	}
+	isSearch := true
+	if name, exist := c.GetQuery("name"); exist {
+		db = db.Where("name like ? ", "%"+name+"%")
+		isSearch = false
+	}
+	if description, exist := c.GetQuery("description"); exist {
+		db = db.Where("description like ? ", "%"+description+"%")
+		isSearch = false
+	}
+	if host, exist := c.GetQuery("host"); exist {
+		db = db.Where("hosts like ? ", "%"+host+"%")
+		isSearch = false
+	}
+	if uri, exist := c.GetQuery("uri"); exist {
+		db = db.Where("uris like ? ", "%"+uri+"%")
+		isSearch = false
+	}
+	if ip, exist := c.GetQuery("ip"); exist {
+		db = db.Where("upstream_nodes like ? ", "%"+ip+"%")
+		isSearch = false
+	}
+	// search
+	if isSearch {
+		if search, exist := c.GetQuery("search"); exist {
+			db = db.Where("name like ? ", "%"+search+"%").
+				Or("description like ? ", "%"+search+"%").
+				Or("hosts like ? ", "%"+search+"%").
+				Or("uris like ? ", "%"+search+"%").
+				Or("upstream_nodes like ? ", "%"+search+"%")
+		}
+	}
+	// todo params check
+	// mysql
+	routeList := []service.Route{}
+	var count int
+	if err := db.Order("priority, update_time desc").Table("routes").Offset((page - 1) * size).Limit(size).Find(&routeList).Count(&count).Error; err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	} else {
+		responseList := make([]service.RouteResponse, 0)
+		for _, r := range routeList {
+			response := &service.RouteResponse{}
+			response.Parse(&r)
+			responseList = append(responseList, *response)
+		}
+		result := &service.ListResponse{Count: count, Data: responseList}
+		resp, _ := json.Marshal(result)
+		c.Data(http.StatusOK, service.ContentType, resp)
+	}
+}
+
+func deleteRoute(c *gin.Context) {
+	rid := c.Param("rid")
+	// todo  params check
+	// delete from apisix
+	request := &service.ApisixRouteRequest{}
+	if _, err := request.Delete(rid); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteDeleteError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	} else {
+		// delete from mysql
+		rd := &service.Route{}
+		rd.ID = uuid.FromStringOrNil(rid)
+		if err := conf.DB().Delete(rd).Error; err != nil {
+			e := errno.FromMessage(errno.DBRouteDeleteError, err.Error())
+			logger.Error(e.Msg)
+			c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+			return
+		}
+	}
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
+func updateRoute(c *gin.Context) {
+	rid := c.Param("rid")
+	// todo  params check
+	param, exist := c.Get("requestBody")
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	routeRequest := &service.RouteRequest{}
+	if err := routeRequest.Parse(param); err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	logger.Info(routeRequest.Plugins)
+
+	arr := service.ToApisixRequest(routeRequest)
+	logger.Info(arr)
+	if resp, err := arr.Update(rid); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteUpdateError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	} else {
+		// update mysql
+		if rd, err := service.ToRoute(routeRequest, arr, uuid.FromStringOrNil(rid), resp); err != nil {
+			c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+			return
+		} else {
+			if err := conf.DB().Model(&service.Route{}).Update(rd).Error; err != nil {
+				e := errno.FromMessage(errno.DBRouteUpdateError, err.Error())
+				logger.Error(e.Msg)
+				c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+				return
+			}
+			logger.Info(rd)
+		}
+	}
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
+
+func findRoute(c *gin.Context) {
+	rid := c.Param("rid")
+	// todo  params check
+	// find from apisix
+	request := &service.ApisixRouteRequest{}
+	if response, err := request.FindById(rid); err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	} else {
+		// transfer response to dashboard struct
+		if result, err := response.Parse(); err != nil {
+			e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+			logger.Error(e.Msg)
+			c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+			return
+		} else {
+			// need to find name from mysql temporary
+			route := &service.Route{}
+			if err := conf.DB().Table("routes").Where("id=?", rid).First(&route).Error; err != nil {
+				e := errno.FromMessage(errno.RouteRequestError, err.Error()+" route ID: "+rid)
+				logger.Error(e.Msg)
+				c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+				return
+			}
+			result.Name = route.Name
+			resp, _ := json.Marshal(result)
+			c.Data(http.StatusOK, service.ContentType, resp)
+		}
+	}
+}
+
+func createRoute(c *gin.Context) {
+	u4 := uuid.NewV4()
+	rid := u4.String()
+	// todo params check
+	param, exist := c.Get("requestBody")
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	routeRequest := &service.RouteRequest{}
+	if err := routeRequest.Parse(param); err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	logger.Info(routeRequest.Plugins)
+
+	arr := service.ToApisixRequest(routeRequest)
+	logger.Info(arr)
+	if resp, err := arr.Create(rid); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	} else {
+		// update mysql
+		if rd, err := service.ToRoute(routeRequest, arr, u4, resp); err != nil {
+			c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+			return
+		} else {
+			logger.Info(rd)
+			if err := conf.DB().Create(rd).Error; err != nil {
+				e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+				logger.Error(e.Msg)
+				c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+				return
+			}
+		}
+	}
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
diff --git a/api/route/ssl.go b/api/route/ssl.go
new file mode 100644
index 0000000..2a9d6f7
--- /dev/null
+++ b/api/route/ssl.go
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+package route
+
+import (
+	"net/http"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+	"github.com/satori/go.uuid"
+
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/service"
+)
+
+const contentType = "application/json"
+
+func AppendSsl(r *gin.Engine) *gin.Engine {
+	r.POST("/apisix/admin/check_ssl_cert", sslCheck)
+	r.GET("/apisix/admin/ssls", sslList)
+	r.POST("/apisix/admin/ssls", sslCreate)
+	r.GET("/apisix/admin/ssls/:id", sslItem)
+	r.PUT("/apisix/admin/ssls/:id", sslUpdate)
+	r.DELETE("/apisix/admin/ssls/:id", sslDelete)
+	return r
+}
+
+func sslList(c *gin.Context) {
+	size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
+	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+	// todo params check
+	resp, err := service.SslList(page, size)
+
+	if err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, service.ContentType, resp)
+}
+
+func sslItem(c *gin.Context) {
+	id := c.Param("id")
+
+	// todo params check
+	resp, err := service.SslItem(id)
+
+	if err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, service.ContentType, resp)
+}
+
+func sslCheck(c *gin.Context) {
+	// todo params check
+	param, exist := c.Get("requestBody")
+
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	resp, err := service.SslCheck(param)
+	if err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, contentType, resp)
+}
+
+func sslCreate(c *gin.Context) {
+	// todo params check
+	param, exist := c.Get("requestBody")
+
+	u4 := uuid.NewV4()
+
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	if err := service.SslCreate(param, u4.String()); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, contentType, errno.Success())
+}
+
+func sslUpdate(c *gin.Context) {
+	// todo params check
+	param, exist := c.Get("requestBody")
+
+	id := c.Param("id")
+
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	if err := service.SslUpdate(param, id); err != nil {
+		e := errno.FromMessage(errno.ApisixRouteCreateError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, contentType, errno.Success())
+}
+
+func sslDelete(c *gin.Context) {
+	id := c.Param("id")
+	// todo params check
+	if err := service.SslDelete(id); err != nil {
+		e := errno.FromMessage(errno.RouteRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
diff --git a/api/script/db/schema.sql b/api/script/db/schema.sql
new file mode 100644
index 0000000..69f088c
--- /dev/null
+++ b/api/script/db/schema.sql
@@ -0,0 +1,31 @@
+-- this is a db script for init
+CREATE DATABASE `manager`;
+use `manager`;
+CREATE TABLE `routes` (
+  `id` varchar(64) NOT NULL unique,
+  `name` varchar(200) NOT NULL unique, -- not support yet
+  `description` varchar(200) DEFAULT NULL,
+  `hosts` text,
+  `uris` text,
+  `upstream_nodes` text,
+  `upstream_id` varchar(32) , -- fk
+  `priority` int NOT NULL DEFAULT 0,
+  `state` int NOT NULL DEFAULT 1, -- 1-normal 0-disable
+  `content` text,
+  `content_admin_api` text,
+  `create_time` bigint(20),
+  `update_time` bigint(20),
+
+  PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
+CREATE TABLE `ssls` (
+  `id` char(36) NOT NULL DEFAULT '',
+  `public_key` text NOT NULL,
+  `snis` text NOT NULL,
+  `validity_start` bigint(20) unsigned NOT NULL,
+  `validity_end` bigint(20) unsigned NOT NULL,
+  `status` tinyint(1) unsigned NOT NULL DEFAULT '1',
+  `create_time` bigint(20) unsigned NOT NULL,
+  `update_time` bigint(20) unsigned NOT NULL,
+  PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
\ No newline at end of file
diff --git a/api/service/base.go b/api/service/base.go
new file mode 100644
index 0000000..6ee57b5
--- /dev/null
+++ b/api/service/base.go
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package service
+
+import (
+	"github.com/jinzhu/gorm"
+	"github.com/satori/go.uuid"
+	"time"
+)
+
+// Base contains common columns for all tables.
+type Base struct {
+	ID         uuid.UUID `json:"id",sql:"type:uuid;primary_key;"`
+	CreateTime int64     `json:"create_time"`
+	UpdateTime int64     `json:"update_time"`
+}
+
+// BeforeCreate will set a UUID rather than numeric ID.
+func (base *Base) BeforeCreate(scope *gorm.Scope) error {
+	timestamp := time.Now().Unix()
+	err := scope.SetColumn("UpdateTime", timestamp)
+	err = scope.SetColumn("CreateTime", timestamp)
+	if len(base.ID) == 0 {
+		uuid := uuid.NewV4()
+		err = scope.SetColumn("ID", uuid)
+		return err
+	}
+	return err
+}
+
+func (base *Base) BeforeSave(scope *gorm.Scope) error {
+	err := scope.SetColumn("UpdateTime", time.Now().Unix())
+	return err
+}
diff --git a/api/service/plugin.go b/api/service/plugin.go
new file mode 100644
index 0000000..92cd6c3
--- /dev/null
+++ b/api/service/plugin.go
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package service
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/utils"
+)
+
+type ApisixPluginRequest struct {
+	Name string `json:"name"`
+}
+
+func (apr *ApisixPluginRequest) Schema() (map[string]interface{}, error) {
+	url := fmt.Sprintf("%s/schema/plugins/%s", conf.BaseUrl, apr.Name)
+	if resp, err := utils.Get(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		arresp := make(map[string]interface{})
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return arresp, nil
+		}
+	}
+}
+
+func (apr *ApisixPluginRequest) List() ([]string, error) {
+
+	url := fmt.Sprintf("%s/plugins/list", conf.BaseUrl)
+	if resp, err := utils.Get(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		var arresp []string
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return arresp, nil
+		}
+	}
+}
diff --git a/api/service/route.go b/api/service/route.go
new file mode 100644
index 0000000..dae7278
--- /dev/null
+++ b/api/service/route.go
@@ -0,0 +1,560 @@
+/*
+ * 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.
+ */
+package service
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/log"
+	"github.com/apisix/manager-api/utils"
+	"github.com/satori/go.uuid"
+	"time"
+)
+
+const (
+	ContentType      = "application/json"
+	HTTP             = "http"
+	HTTPS            = "https"
+	SCHEME           = "scheme"
+	WEBSOCKET        = "websocket"
+	REDIRECT         = "redirect"
+	PROXY_REWRIETE   = "proxy-rewrite"
+	UPATHTYPE_STATIC = "static"
+	UPATHTYPE_REGX   = "regx"
+	UPATHTYPE_KEEP   = "keep"
+)
+
+var logger = log.GetLogger()
+
+func (r *RouteRequest) Parse(body interface{}) error {
+	if err := json.Unmarshal(body.([]byte), r); err != nil {
+		r = nil
+		return err
+	} else {
+		if r.Uris == nil || len(r.Uris) < 1 {
+			r.Uris = []string{"/*"}
+		}
+	}
+	return nil
+}
+
+func (arr *ApisixRouteRequest) Parse(r *RouteRequest) {
+	arr.Desc = r.Desc
+	arr.Priority = r.Priority
+	arr.Methods = r.Methods
+	arr.Uris = r.Uris
+	arr.Hosts = r.Hosts
+	arr.Vars = r.Vars
+	arr.Upstream = r.Upstream
+	arr.Plugins = r.Plugins
+}
+
+func (rd *Route) Parse(r *RouteRequest, arr *ApisixRouteRequest) error {
+	//rd.Name = arr.Name
+	rd.Description = arr.Desc
+	// todo transfer
+	rd.Hosts = ""
+	rd.Uris = ""
+	rd.UpstreamNodes = ""
+	rd.UpstreamId = ""
+	if content, err := json.Marshal(r); err != nil {
+		return err
+	} else {
+		rd.Content = string(content)
+	}
+	timestamp := time.Now().Unix()
+	rd.CreateTime = timestamp
+	return nil
+}
+
+func (arr *ApisixRouteRequest) FindById(rid string) (*ApisixRouteResponse, error) {
+	url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+	if resp, err := utils.Get(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		var arresp ApisixRouteResponse
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return &arresp, nil
+		}
+	}
+}
+
+func (arr *ApisixRouteRequest) Update(rid string) (*ApisixRouteResponse, error) {
+	url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+	if b, err := json.Marshal(arr); err != nil {
+		return nil, err
+	} else {
+		if resp, err := utils.Patch(url, b); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			var arresp ApisixRouteResponse
+			if err := json.Unmarshal(resp, &arresp); err != nil {
+				logger.Error(err.Error())
+				return nil, err
+			} else {
+				return &arresp, nil
+			}
+		}
+	}
+}
+
+func (arr *ApisixRouteRequest) Create(rid string) (*ApisixRouteResponse, error) {
+	url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+	if b, err := json.Marshal(arr); err != nil {
+		return nil, err
+	} else {
+		fmt.Println(string(b))
+		if resp, err := utils.Put(url, b); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			var arresp ApisixRouteResponse
+			if err := json.Unmarshal(resp, &arresp); err != nil {
+				logger.Error(err.Error())
+				return nil, err
+			} else {
+				return &arresp, nil
+			}
+		}
+	}
+}
+
+func (arr *ApisixRouteRequest) Delete(rid string) (*ApisixRouteResponse, error) {
+	url := fmt.Sprintf("%s/routes/%s", conf.BaseUrl, rid)
+	if resp, err := utils.Delete(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		var arresp ApisixRouteResponse
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return &arresp, nil
+		}
+	}
+}
+
+type RouteRequest struct {
+	ID               string                 `json:"id,omitempty"`
+	Name             string                 `json:"name"`
+	Desc             string                 `json:"desc,omitempty"`
+	Priority         int64                  `json:"priority,omitempty"`
+	Methods          []string               `json:"methods,omitempty"`
+	Uris             []string               `json:"uris"`
+	Hosts            []string               `json:"hosts,omitempty"`
+	Protocols        []string               `json:"protocols,omitempty"`
+	Redirect         *Redirect              `json:"redirect,omitempty"`
+	Vars             [][]string             `json:"vars,omitempty"`
+	Upstream         *Upstream              `json:"upstream,omitempty"`
+	UpstreamProtocol string                 `json:"upstream_protocol,omitempty"`
+	UpstreamPath     *UpstreamPath          `json:"upstream_path,omitempty"`
+	UpstreamHeader   map[string]string      `json:"upstream_header,omitempty"`
+	Plugins          map[string]interface{} `json:"plugins"`
+}
+
+func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
+	o := r.Node.Value
+
+	//Protocols from vars and upstream
+	protocols := make([]string, 0)
+	if o.Upstream != nil && o.Upstream.EnableWebsocket {
+		protocols = append(protocols, WEBSOCKET)
+	}
+	flag := true
+	for _, t := range o.Vars {
+		if t[0] == SCHEME {
+			flag = false
+			protocols = append(protocols, t[2])
+		}
+	}
+	if flag {
+		protocols = append(protocols, HTTP)
+		protocols = append(protocols, HTTPS)
+	}
+	//Redirect from plugins
+	redirect := &Redirect{}
+	upstreamProtocol := UPATHTYPE_KEEP
+	upstreamHeader := make(map[string]string)
+	upstreamPath := &UpstreamPath{}
+	for k, v := range o.Plugins {
+		if k == REDIRECT {
+			if bytes, err := json.Marshal(v); err != nil {
+				return nil, err
+			} else {
+				if err := json.Unmarshal(bytes, redirect); err != nil {
+					return nil, err
+				}
+			}
+
+		}
+		if k == PROXY_REWRIETE {
+			pr := &ProxyRewrite{}
+			if bytes, err := json.Marshal(v); err != nil {
+				return nil, err
+			} else {
+				if err := json.Unmarshal(bytes, pr); err != nil {
+					return nil, err
+				} else {
+					if pr.Scheme != "" {
+						upstreamProtocol = pr.Scheme
+					}
+					upstreamHeader = pr.Headers
+					if pr.RegexUri == nil || len(pr.RegexUri) < 2 {
+						upstreamPath.UPathType = UPATHTYPE_STATIC
+						upstreamPath.To = pr.Uri
+					} else {
+						upstreamPath.UPathType = UPATHTYPE_REGX
+						upstreamPath.From = pr.RegexUri[0]
+						upstreamPath.To = pr.RegexUri[1]
+					}
+				}
+			}
+		}
+	}
+	//Vars
+	requestVars := make([][]string, 0)
+	for _, t := range o.Vars {
+		if t[0] != SCHEME {
+			requestVars = append(requestVars, t)
+		}
+	}
+	//Plugins
+	requestPlugins := utils.CopyMap(o.Plugins)
+	delete(requestPlugins, REDIRECT)
+
+	// check if upstream is not exist
+	if o.Upstream == nil {
+		upstreamProtocol = ""
+		upstreamHeader = nil
+		upstreamPath = nil
+	}
+	if upstreamPath.UPathType == "" {
+		upstreamPath = nil
+	}
+	result := &RouteRequest{
+		ID:               o.Id,
+		Desc:             o.Desc,
+		Priority:         o.Priority,
+		Methods:          o.Methods,
+		Uris:             o.Uris,
+		Hosts:            o.Hosts,
+		Redirect:         redirect,
+		Upstream:         o.Upstream,
+		UpstreamProtocol: upstreamProtocol,
+		UpstreamPath:     upstreamPath,
+		UpstreamHeader:   upstreamHeader,
+		Protocols:        protocols,
+		Vars:             requestVars,
+		Plugins:          requestPlugins,
+	}
+	return result, nil
+}
+
+type Redirect struct {
+	HttpToHttps bool   `json:"http_to_https,omitempty"`
+	Code        int64  `json:"code,omitempty"`
+	Uri         string `json:"uri,omitempty"`
+}
+
+type ProxyRewrite struct {
+	Uri      string            `json:"uri"`
+	RegexUri []string          `json:"regex_uri"`
+	Scheme   string            `json:"scheme"`
+	Host     string            `json:"host"`
+	Headers  map[string]string `json:"headers"`
+}
+
+func (r ProxyRewrite) MarshalJSON() ([]byte, error) {
+	m := make(map[string]interface{})
+	if r.RegexUri != nil {
+		m["regex_uri"] = r.RegexUri
+	}
+	if r.Uri != "" {
+		m["uri"] = r.Uri
+	}
+	if r.Scheme != UPATHTYPE_KEEP && r.Scheme != "" {
+		m["scheme"] = r.Scheme
+	}
+	if r.Host != "" {
+		m["host"] = r.Host
+	}
+	if r.Headers != nil && len(r.Headers) > 0 {
+		m["headers"] = r.Headers
+	}
+	if result, err := json.Marshal(m); err != nil {
+		return nil, err
+	} else {
+		return result, nil
+	}
+}
+
+func (r Redirect) MarshalJSON() ([]byte, error) {
+	m := make(map[string]interface{})
+	if r.HttpToHttps {
+		m["http_to_https"] = true
+	} else {
+		m["code"] = r.Code
+		m["uri"] = r.Uri
+	}
+	if result, err := json.Marshal(m); err != nil {
+		return nil, err
+	} else {
+		return result, nil
+	}
+}
+
+type Upstream struct {
+	UType           string           `json:"type"`
+	Nodes           map[string]int64 `json:"nodes"`
+	Timeout         UpstreamTimeout  `json:"timeout"`
+	EnableWebsocket bool             `json:"enable_websocket"`
+}
+
+type UpstreamTimeout struct {
+	Connect int64 `json:"connect"`
+	Send    int64 `json:"send"`
+	Read    int64 `json:"read"`
+}
+
+type UpstreamPath struct {
+	UPathType string `json:"type"`
+	From      string `json:"from"`
+	To        string `json:"to"`
+}
+
+type ApisixRouteRequest struct {
+	Desc     string                 `json:"desc,omitempty"`
+	Priority int64                  `json:"priority"`
+	Methods  []string               `json:"methods,omitempty"`
+	Uris     []string               `json:"uris,omitempty"`
+	Hosts    []string               `json:"hosts,omitempty"`
+	Vars     [][]string             `json:"vars,omitempty"`
+	Upstream *Upstream              `json:"upstream,omitempty"`
+	Plugins  map[string]interface{} `json:"plugins,omitempty"`
+	//Name     string                 `json:"name"`
+}
+
+// ApisixRouteResponse is response from apisix admin api
+type ApisixRouteResponse struct {
+	Action string `json:"action"`
+	Node   *Node  `json:"node"`
+}
+
+type Node struct {
+	Value         Value  `json:"value"`
+	ModifiedIndex uint64 `json:"modifiedIndex"`
+}
+
+type Value struct {
+	Id         string                 `json:"id"`
+	Name       string                 `json:"name"`
+	Desc       string                 `json:"desc,omitempty"`
+	Priority   int64                  `json:"priority"`
+	Methods    []string               `json:"methods"`
+	Uris       []string               `json:"uris"`
+	Hosts      []string               `json:"hosts"`
+	Vars       [][]string             `json:"vars"`
+	Upstream   *Upstream              `json:"upstream,omitempty"`
+	UpstreamId string                 `json:"upstream_id,omitempty"`
+	Plugins    map[string]interface{} `json:"plugins"`
+}
+
+type Route struct {
+	Base
+	Name            string `json:"name"`
+	Description     string `json:"description,omitempty"`
+	Hosts           string `json:"hosts"`
+	Uris            string `json:"uris"`
+	UpstreamNodes   string `json:"upstream_nodes"`
+	UpstreamId      string `json:"upstream_id"`
+	Priority        int64  `json:"priority"`
+	Content         string `json:"content"`
+	ContentAdminApi string `json:"content_admin_api"`
+}
+
+type RouteResponse struct {
+	Base
+	Name        string    `json:"name"`
+	Description string    `json:"description,omitempty"`
+	Hosts       []string  `json:"hosts,omitempty"`
+	Uris        []string  `json:"uris,omitempty"`
+	Upstream    *Upstream `json:"upstream,omitempty"`
+	UpstreamId  string    `json:"upstream_id,omitempty"`
+	Priority    int64     `json:"priority"`
+}
+
+type ListResponse struct {
+	Count int         `json:"count"`
+	Data  interface{} `json:"data"`
+}
+
+func (rr *RouteResponse) Parse(r *Route) {
+	rr.Base = r.Base
+	rr.Name = r.Name
+	rr.Description = r.Description
+	rr.UpstreamId = r.UpstreamId
+	rr.Priority = r.Priority
+	// hosts
+	if len(r.Hosts) > 0 {
+		var hosts []string
+		if err := json.Unmarshal([]byte(r.Hosts), &hosts); err == nil {
+			rr.Hosts = hosts
+		} else {
+			logger.Error(err.Error())
+		}
+	}
+
+	// uris
+	if len(r.Uris) > 0 {
+		var uris []string
+		if err := json.Unmarshal([]byte(r.Uris), &uris); err == nil {
+			rr.Uris = uris
+		}
+	}
+
+	// uris
+	var resp ApisixRouteResponse
+	if err := json.Unmarshal([]byte(r.ContentAdminApi), &resp); err == nil {
+		rr.Upstream = resp.Node.Value.Upstream
+	}
+}
+
+// RouteRequest -> ApisixRouteRequest
+func ToApisixRequest(routeRequest *RouteRequest) *ApisixRouteRequest {
+	// redirect -> plugins
+	plugins := utils.CopyMap(routeRequest.Plugins)
+	redirect := routeRequest.Redirect
+	if redirect != nil {
+		plugins["redirect"] = redirect
+	}
+
+	logger.Info(routeRequest.Plugins)
+
+	// scheme https and not http -> vars ['scheme', '==', 'https']
+	pMap := utils.Set2Map(routeRequest.Protocols)
+
+	arr := &ApisixRouteRequest{}
+	arr.Parse(routeRequest)
+
+	// protocols[websokect] -> upstream
+	if pMap[WEBSOCKET] == 1 && arr.Upstream != nil {
+		arr.Upstream.EnableWebsocket = true
+	}
+	vars := utils.CopyStrings(routeRequest.Vars)
+	if pMap[HTTP] != 1 || pMap[HTTPS] != 1 {
+		if pMap[HTTP] == 1 {
+			vars = append(vars, []string{SCHEME, "==", HTTP})
+		}
+		if pMap[HTTPS] == 1 {
+			vars = append(vars, []string{SCHEME, "==", HTTPS})
+		}
+	}
+	if len(vars) > 0 {
+		arr.Vars = vars
+	} else {
+		arr.Vars = nil
+	}
+
+	// upstream protocol
+	if arr.Upstream != nil {
+		pr := &ProxyRewrite{}
+		pr.Scheme = routeRequest.UpstreamProtocol
+		// upstream path
+		proxyPath := routeRequest.UpstreamPath
+		if proxyPath != nil {
+			if proxyPath.UPathType == UPATHTYPE_STATIC {
+				pr.Uri = proxyPath.To
+				pr.RegexUri = nil
+			} else {
+				pr.RegexUri = []string{proxyPath.From, proxyPath.To}
+			}
+		}
+		// upstream headers
+		pr.Headers = routeRequest.UpstreamHeader
+		if proxyPath != nil {
+			plugins[PROXY_REWRIETE] = pr
+		}
+	}
+
+	if plugins != nil && len(plugins) > 0 {
+		arr.Plugins = plugins
+	} else {
+		arr.Plugins = nil
+	}
+	return arr
+}
+
+func ToRoute(routeRequest *RouteRequest,
+	arr *ApisixRouteRequest,
+	u4 uuid.UUID,
+	resp *ApisixRouteResponse) (*Route, *errno.ManagerError) {
+	rd := &Route{}
+	if err := rd.Parse(routeRequest, arr); err != nil {
+		e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+		return nil, e
+	}
+	if rd.Name == "" {
+		rd.Name = routeRequest.Name
+	}
+	rd.ID = u4
+	// content_admin_api
+	if respStr, err := json.Marshal(resp); err != nil {
+		e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+		return nil, e
+	} else {
+		rd.ContentAdminApi = string(respStr)
+	}
+	// hosts
+	hosts := resp.Node.Value.Hosts
+	if hb, err := json.Marshal(hosts); err != nil {
+		e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+		logger.Warn(e.Msg)
+	} else {
+		rd.Hosts = string(hb)
+	}
+	// uris
+	uris := resp.Node.Value.Uris
+	if ub, err := json.Marshal(uris); err != nil {
+		e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+		logger.Warn(e.Msg)
+	} else {
+		rd.Uris = string(ub)
+	}
+	// upstreamNodes
+	if resp.Node.Value.Upstream != nil {
+		nodes := resp.Node.Value.Upstream.Nodes
+		ips := make([]string, 0)
+		for k, _ := range nodes {
+			ips = append(ips, k)
+		}
+		if nb, err := json.Marshal(ips); err != nil {
+			e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
+			logger.Warn(e.Msg)
+		} else {
+			rd.UpstreamNodes = string(nb)
+		}
+	}
+	return rd, nil
+}
diff --git a/api/service/route_test.go b/api/service/route_test.go
new file mode 100644
index 0000000..82652ce
--- /dev/null
+++ b/api/service/route_test.go
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+package service
+
+import (
+	"encoding/json"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestRedirectMarshal(t *testing.T) {
+	a := assert.New(t)
+	r := &Redirect{
+		HttpToHttps: true,
+		Code:        302,
+		Uri:         "/hello",
+	}
+	if result, err := json.Marshal(r); err != nil {
+		t.Error(err.Error())
+	} else {
+		h := make(map[string]interface{})
+		json.Unmarshal(result, &h)
+		a.Equal(1, len(h))
+		a.Equal(nil, h["code"])
+		a.Equal(true, h["http_to_https"])
+	}
+}
+
+func TestToApisixRequest_RediretPlugins(t *testing.T) {
+	rr := &RouteRequest{
+		ID:        "u guess a uuid",
+		Name:      "a special name",
+		Desc:      "any description",
+		Priority:  0,
+		Methods:   []string{"GET"},
+		Uris:      []string{},
+		Hosts:     []string{"www.baidu.com"},
+		Protocols: []string{"http", "https", "websocket"},
+		Redirect:  &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+		Vars:      [][]string{},
+	}
+	ar := ToApisixRequest(rr)
+	a := assert.New(t)
+	a.Equal(1, len(ar.Plugins))
+	a.NotEqual(nil, ar.Plugins["redirect"])
+}
+
+func TestToApisixRequest_Vars(t *testing.T) {
+	rr := &RouteRequest{
+		ID:        "u guess a uuid",
+		Name:      "a special name",
+		Desc:      "any description",
+		Priority:  0,
+		Methods:   []string{"GET"},
+		Uris:      []string{},
+		Hosts:     []string{"www.baidu.com"},
+		Protocols: []string{"http", "https", "websocket"},
+		Redirect:  &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+		Vars:      [][]string{},
+	}
+	ar := ToApisixRequest(rr)
+	a := assert.New(t)
+	b, err := json.Marshal(ar)
+	a.Equal(nil, err)
+
+	m := make(map[string]interface{})
+	err = json.Unmarshal(b, &m)
+	a.Equal(nil, err)
+	t.Log(m["vars"])
+	a.Equal(nil, m["vars"])
+}
+
+func TestToApisixRequest_Upstream(t *testing.T) {
+	nodes := make(map[string]int64)
+	nodes["127.0.0.1:8080"] = 100
+	upstream := &Upstream{
+		UType:   "roundrobin",
+		Nodes:   nodes,
+		Timeout: UpstreamTimeout{15, 15, 15},
+	}
+	rr := &RouteRequest{
+		ID:        "u guess a uuid",
+		Name:      "a special name",
+		Desc:      "any description",
+		Priority:  0,
+		Methods:   []string{"GET"},
+		Uris:      []string{},
+		Hosts:     []string{"www.baidu.com"},
+		Protocols: []string{"http", "https", "websocket"},
+		Redirect:  &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+		Vars:      [][]string{},
+		Upstream:  upstream,
+	}
+	ar := ToApisixRequest(rr)
+	a := assert.New(t)
+	a.Equal("roundrobin", ar.Upstream.UType)
+	a.Equal(true, ar.Upstream.EnableWebsocket)
+}
+
+func TestToApisixRequest_UpstreamUnable(t *testing.T) {
+	nodes := make(map[string]int64)
+	nodes["127.0.0.1:8080"] = 100
+	upstream := &Upstream{
+		UType:   "roundrobin",
+		Nodes:   nodes,
+		Timeout: UpstreamTimeout{15, 15, 15},
+	}
+	rr := &RouteRequest{
+		ID:        "u guess a uuid",
+		Name:      "a special name",
+		Desc:      "any description",
+		Priority:  0,
+		Methods:   []string{"GET"},
+		Uris:      []string{},
+		Hosts:     []string{"www.baidu.com"},
+		Protocols: []string{"http", "https"},
+		Redirect:  &Redirect{HttpToHttps: true, Code: 200, Uri: "/hello"},
+		Vars:      [][]string{},
+		Upstream:  upstream,
+	}
+	ar := ToApisixRequest(rr)
+	a := assert.New(t)
+	a.Equal("roundrobin", ar.Upstream.UType)
+	a.Equal(false, ar.Upstream.EnableWebsocket)
+}
diff --git a/api/service/ssl.go b/api/service/ssl.go
new file mode 100644
index 0000000..839e3b8
--- /dev/null
+++ b/api/service/ssl.go
@@ -0,0 +1,300 @@
+/*
+ * 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.
+ */
+package service
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"encoding/pem"
+	"errors"
+	"fmt"
+
+	"github.com/satori/go.uuid"
+
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/utils"
+)
+
+type Ssl struct {
+	Base
+	ValidityStart uint64 `json:"validity_start"`
+	ValidityEnd   uint64 `json:"validity_end"`
+	Snis          string `json:"snis"`
+	Status        uint64 `json:"status"`
+	PublicKey     string `json:"public_key,omitempty"`
+}
+
+type SslDto struct {
+	Base
+	ValidityStart uint64   `json:"validity_start"`
+	ValidityEnd   uint64   `json:"validity_end"`
+	Snis          []string `json:"snis"`
+	Status        uint64   `json:"status"`
+	PublicKey     string   `json:"public_key,omitempty"`
+}
+
+type SslRequest struct {
+	ID         string   `json:"id,omitempty"`
+	PublicKey  string   `json:"cert"`
+	PrivateKey string   `json:"key"`
+	Snis       []string `json:"snis"`
+}
+
+// ApisixSslResponse is response from apisix admin api
+type ApisixSslResponse struct {
+	Action string   `json:"action"`
+	Node   *SslNode `json:"node"`
+}
+
+type SslNode struct {
+	Value         SslRequest `json:"value"`
+	ModifiedIndex uint64     `json:"modifiedIndex"`
+}
+
+func (req *SslRequest) Parse(body interface{}) {
+	if err := json.Unmarshal(body.([]byte), req); err != nil {
+		req = nil
+		logger.Error(errno.FromMessage(errno.RouteRequestError, err.Error()).Msg)
+	}
+}
+
+func (sslDto *SslDto) Parse(ssl *Ssl) error {
+	sslDto.ID = ssl.ID
+	sslDto.ValidityStart = ssl.ValidityStart
+	sslDto.ValidityEnd = ssl.ValidityEnd
+
+	var snis []string
+	_ = json.Unmarshal([]byte(ssl.Snis), &snis)
+	sslDto.Snis = snis
+
+	sslDto.Status = ssl.Status
+	sslDto.PublicKey = ssl.PublicKey
+	sslDto.CreateTime = ssl.CreateTime
+	sslDto.UpdateTime = ssl.UpdateTime
+
+	return nil
+}
+
+func SslList(page, size int) ([]byte, error) {
+	var count int
+	sslList := []Ssl{}
+	if err := conf.DB().Table("ssls").Offset((page - 1) * size).Limit(size).Find(&sslList).Count(&count).Error; err != nil {
+		return nil, err
+	}
+
+	sslDtoList := []SslDto{}
+
+	for _, ssl := range sslList {
+		sslDto := SslDto{}
+		sslDto.Parse(&ssl)
+
+		sslDtoList = append(sslDtoList, sslDto)
+	}
+
+	data := errno.FromMessage(errno.SystemSuccess).ListResponse(count, sslDtoList)
+
+	return json.Marshal(data)
+}
+
+func SslItem(id string) ([]byte, error) {
+	ssl := &Ssl{}
+	if err := conf.DB().Table("ssls").Where("id = ?", id).First(ssl).Error; err != nil {
+		return nil, err
+	}
+
+	sslDto := &SslDto{}
+	sslDto.Parse(ssl)
+
+	data := errno.FromMessage(errno.SystemSuccess).ItemResponse(sslDto)
+
+	return json.Marshal(data)
+}
+
+func SslCheck(param interface{}) ([]byte, error) {
+	sslReq := &SslRequest{}
+	sslReq.Parse(param)
+
+	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+
+	if err != nil {
+		return nil, err
+	}
+
+	ssl.PublicKey = ""
+
+	sslDto := &SslDto{}
+	sslDto.Parse(ssl)
+
+	data := errno.FromMessage(errno.SystemSuccess).ItemResponse(sslDto)
+
+	return json.Marshal(data)
+}
+
+func SslCreate(param interface{}, id string) error {
+	sslReq := &SslRequest{}
+	sslReq.Parse(param)
+
+	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+
+	if err != nil {
+		return err
+	}
+
+	// first admin api
+	var snis []string
+	_ = json.Unmarshal([]byte(ssl.Snis), &snis)
+	sslReq.Snis = snis
+
+	if _, err := sslReq.PutToApisix(id); err != nil {
+		return err
+	}
+	// then mysql
+	ssl.ID = uuid.FromStringOrNil(id)
+	if err := conf.DB().Create(ssl).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func SslUpdate(param interface{}, id string) error {
+	sslReq := &SslRequest{}
+	sslReq.Parse(param)
+
+	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+
+	if err != nil {
+		return err
+	}
+
+	// first admin api
+	var snis []string
+	_ = json.Unmarshal([]byte(ssl.Snis), &snis)
+	sslReq.Snis = snis
+
+	if _, err := sslReq.PutToApisix(id); err != nil {
+		return err
+	}
+
+	// then mysql
+	ssl.ID = uuid.FromStringOrNil(id)
+	data := Ssl{PublicKey: ssl.PublicKey, Snis: ssl.Snis, ValidityStart: ssl.ValidityStart, ValidityEnd: ssl.ValidityEnd}
+	if err := conf.DB().Model(&ssl).Updates(data).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func SslDelete(id string) error {
+	// delete from apisix
+	request := &SslRequest{}
+	request.ID = id
+	if _, err := request.DeleteFromApisix(); err != nil {
+		return err
+	}
+	// delete from mysql
+	ssl := &Ssl{}
+	ssl.ID = uuid.FromStringOrNil(id)
+	if err := conf.DB().Delete(ssl).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (req *SslRequest) PutToApisix(rid string) (*ApisixSslResponse, error) {
+	url := fmt.Sprintf("%s/ssl/%s", conf.BaseUrl, rid)
+	if data, err := json.Marshal(req); err != nil {
+		return nil, err
+	} else {
+		if resp, err := utils.Put(url, data); err != nil {
+			logger.Error(url)
+			logger.Error(string(data))
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			var arresp ApisixSslResponse
+			if err := json.Unmarshal(resp, &arresp); err != nil {
+				logger.Error(err.Error())
+				return nil, err
+			} else {
+				return &arresp, nil
+			}
+		}
+	}
+}
+
+func (req *SslRequest) DeleteFromApisix() (*ApisixSslResponse, error) {
+	id := req.ID
+	url := fmt.Sprintf("%s/ssl/%s", conf.BaseUrl, id)
+
+	if resp, err := utils.Delete(url); err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	} else {
+		var arresp ApisixSslResponse
+		if err := json.Unmarshal(resp, &arresp); err != nil {
+			logger.Error(err.Error())
+			return nil, err
+		} else {
+			return &arresp, nil
+		}
+	}
+}
+
+func ParseCert(crt, key string) (*Ssl, error) {
+	// print private key
+	certDERBlock, _ := pem.Decode([]byte(crt))
+	if certDERBlock == nil {
+		return nil, errors.New("Certificate resolution failed")
+	}
+	// match
+	_, err := tls.X509KeyPair([]byte(crt), []byte(key))
+	if err != nil {
+		return nil, err
+	}
+
+	x509Cert, err := x509.ParseCertificate(certDERBlock.Bytes)
+
+	if err != nil {
+		return nil, errors.New("Certificate resolution failed")
+	} else {
+		ssl := Ssl{}
+
+		//domain
+		snis := []byte{}
+		if x509Cert.DNSNames == nil || len(x509Cert.DNSNames) < 1 {
+			tmp := []string{}
+			if x509Cert.Subject.CommonName != "" {
+				tmp = []string{x509Cert.Subject.CommonName}
+			}
+			snis, _ = json.Marshal(tmp)
+		} else {
+			snis, _ = json.Marshal(x509Cert.DNSNames)
+		}
+		ssl.Snis = string(snis)
+
+		ssl.ValidityStart = uint64(x509Cert.NotBefore.Unix())
+		ssl.ValidityEnd = uint64(x509Cert.NotAfter.Unix())
+		ssl.PublicKey = crt
+
+		return &ssl, nil
+	}
+}
diff --git a/api/utils/copy.go b/api/utils/copy.go
new file mode 100644
index 0000000..30f228a
--- /dev/null
+++ b/api/utils/copy.go
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package utils
+
+func CopyMap(origin map[string]interface{}) map[string]interface{} {
+	result := make(map[string]interface{})
+	for k, v := range origin {
+		result[k] = v
+	}
+	return result
+}
+
+func CopyStrings(origin [][]string) [][]string {
+	result := make([][]string, 0)
+	for _, s := range origin {
+		result = append(result, s)
+	}
+	return result
+}
+
+func Set2Map(origin []string) map[string]int {
+	result := make(map[string]int)
+	for _, s := range origin {
+		result[s] = 1
+	}
+	return result
+}
diff --git a/api/utils/http.go b/api/utils/http.go
new file mode 100644
index 0000000..119cf0b
--- /dev/null
+++ b/api/utils/http.go
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+package utils
+
+import (
+	"fmt"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/log"
+	"gopkg.in/resty.v1"
+	"net/http"
+	"time"
+)
+
+const timeout = 3000
+
+var logger = log.GetLogger()
+
+func Get(url string) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	resp, err := r.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
+
+func Post(url string, bytes []byte) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	r.SetBody(bytes)
+	resp, err := r.Post(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusCreated {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
+
+func Put(url string, bytes []byte) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	r.SetBody(bytes)
+	resp, err := r.Put(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusCreated {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
+
+func Patch(url string, bytes []byte) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	r.SetBody(bytes)
+	resp, err := r.Patch(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
+
+func Delete(url string) ([]byte, error) {
+	r := resty.New().
+		SetTimeout(time.Duration(timeout)*time.Millisecond).
+		R().
+		SetHeader("content-type", "application/json").
+		SetHeader("X-API-KEY", conf.ApiKey)
+	resp, err := r.Delete(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode() != http.StatusOK {
+		return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode(), resp.Body())
+	}
+	return resp.Body(), nil
+}
diff --git a/compose/README.md b/compose/README.md
new file mode 100644
index 0000000..7d7c7ce
--- /dev/null
+++ b/compose/README.md
@@ -0,0 +1,56 @@
+## Deploy
+
+```sh
+$ cd incubator-apisix-dashboard/compose
+
+$ chmod +x ./manager_conf/build.sh
+
+$ docker-compose -p dashboard up -d
+```
+
+## Usage
+
+### 1. login dashboard
+
+Visit `http://127.0.0.1/dashboard/` in the browser, 
+Enter `http://127.0.0.1:8080/apisix/admin` into the first input box, this is the backend management service address
+
+![login](pics/login.png)
+
+now, click `save`.
+
+### 2. If you want to display the grafana metric dashboard, please fill in the grafana shared link as follows
+
+1.get grafana shared link
+
+Visit `http://127.0.0.1:3000/?search=open&orgId=1`
+
+![login](pics/grafana_1.png)
+
+click `Apache APISIX` dashboard, and you can see the page as follow
+
+![login](pics/grafana_2.png)
+
+click the button `shard dashboard` on the right of `Apache APISIX`
+
+![login](pics/grafana_3.png)
+
+copy the link, and then return to dashboard on the step 1
+
+![login](pics/grafana_4.png)
+
+click metric on the left, and then the config button
+
+Paste shared link
+
+![login](pics/grafana_5.png)
+
+save, and you can see the metrics
+ 
+![login](pics/grafana_6.png)
+ 
+
+
+
+
+
diff --git a/compose/apisix_conf/config.yaml b/compose/apisix_conf/config.yaml
new file mode 100644
index 0000000..bbdc65b
--- /dev/null
+++ b/compose/apisix_conf/config.yaml
@@ -0,0 +1,137 @@
+apisix:
+  node_listen: 9080              # APISIX listening port
+  enable_heartbeat: true
+  enable_admin: true
+  enable_admin_cors: true         # Admin API support CORS response headers.
+  enable_debug: false
+  enable_dev_mode: false          # Sets nginx worker_processes to 1 if set to true
+  enable_reuseport: true          # Enable nginx SO_REUSEPORT switch if set to true.
+  enable_ipv6: true
+  config_center: etcd             # etcd: use etcd to store the config value
+                                  # yaml: fetch the config value from local yaml file `/your_path/conf/apisix.yaml`
+
+  #proxy_protocol:                 # Proxy Protocol configuration
+  #  listen_http_port: 9181        # The port with proxy protocol for http, it differs from node_listen and port_admin.
+                                   # This port can only receive http request with proxy protocol, but node_listen & port_admin
+                                   # can only receive http request. If you enable proxy protocol, you must use this port to
+                                   # receive http request with proxy protocol
+  #  listen_https_port: 9182       # The port with proxy protocol for https
+  #  enable_tcp_pp: true           # Enable the proxy protocol for tcp proxy, it works for stream_proxy.tcp option
+  #  enable_tcp_pp_to_upstream: true # Enables the proxy protocol to the upstream server
+
+  proxy_cache:                     # Proxy Caching configuration
+    cache_ttl: 10s                 # The default caching time if the upstream does not specify the cache time
+    zones:                         # The parameters of a cache
+    - name: disk_cache_one         # The name of the cache, administrator can be specify
+                                   # which cache to use by name in the admin api
+      memory_size: 50m             # The size of shared memory, it's used to store the cache index
+      disk_size: 1G                # The size of disk, it's used to store the cache data
+      disk_path: "/tmp/disk_cache_one" # The path to store the cache data
+      cache_levels: "1:2"           # The hierarchy levels of a cache
+  #  - name: disk_cache_two
+  #    memory_size: 50m
+  #    disk_size: 1G
+  #    disk_path: "/tmp/disk_cache_two"
+  #    cache_levels: "1:2"
+
+#  allow_admin:                  # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow
+#    - 127.0.0.0/24              # If we don't set any IP list, then any IP access is allowed by default.
+#    - 172.17.0.0/24
+  #   - "::/64"
+  # port_admin: 9180              # use a separate port
+
+  # Default token when use API to call for Admin API.
+  # *NOTE*: Highly recommended to modify this value to protect APISIX's Admin API.
+  # Disabling this configuration item means that the Admin API does not
+  # require any authentication.
+  admin_key:
+    -
+      name: "admin"
+      key: edd1c9f034335f136f87ad84b625c8f1
+      role: admin                 # admin: manage all configuration data
+                                  # viewer: only can view configuration data
+    -
+      name: "viewer"
+      key: 4054f7cf07e344346cd3f287985e76a2
+      role: viewer
+  router:
+    http: 'radixtree_uri'         # radixtree_uri: match route by uri(base on radixtree)
+                                  # radixtree_host_uri: match route by host + uri(base on radixtree)
+    ssl: 'radixtree_sni'          # radixtree_sni: match route by SNI(base on radixtree)
+  # stream_proxy:                 # TCP/UDP proxy
+  #   tcp:                        # TCP proxy port list
+  #     - 9100
+  #     - 9101
+  #   udp:                        # UDP proxy port list
+  #     - 9200
+  #     - 9211
+  dns_resolver:                   # default DNS resolver, with disable IPv6 and enable local DNS
+    - 127.0.0.11
+    - 114.114.114.114
+    - 223.5.5.5
+    - 1.1.1.1
+    - 8.8.8.8
+  dns_resolver_valid: 30          # valid time for dns result 30 seconds
+  resolver_timeout: 5             # resolver timeout
+  ssl:
+    enable: true
+    enable_http2: true
+    listen_port: 9443
+    ssl_protocols: "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3"
+    ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES2 [...]
+
+nginx_config:                     # config for render the template to genarate nginx.conf
+  error_log: "logs/error.log"
+  error_log_level: "warn"         # warn,error
+  worker_rlimit_nofile: 20480     # the number of files a worker process can open, should be larger than worker_connections
+  event:
+    worker_connections: 10620
+  http:
+    access_log: "logs/access.log"
+    keepalive_timeout: 60s         # timeout during which a keep-alive client connection will stay open on the server side.
+    client_header_timeout: 60s     # timeout for reading client request header, then 408 (Request Time-out) error is returned to the client
+    client_body_timeout: 60s       # timeout for reading client request body, then 408 (Request Time-out) error is returned to the client
+    send_timeout: 10s              # timeout for transmitting a response to the client.then the connection is closed
+    underscores_in_headers: "on"   # default enables the use of underscores in client request header fields
+    real_ip_header: "X-Real-IP"    # http://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_header
+    real_ip_from:                  # http://nginx.org/en/docs/http/ngx_http_realip_module.html#set_real_ip_from
+      - 127.0.0.1
+      - 'unix:'
+    #lua_shared_dicts:              # add custom shared cache to nginx.conf
+    #  ipc_shared_dict: 100m        # custom shared cache, format: `cache-key: cache-size`
+
+etcd:
+  host:                           # it's possible to define multiple etcd hosts addresses of the same etcd cluster.
+    - "http://192.17.5.10:2379"     # multiple etcd address
+  prefix: "/apisix"               # apisix configurations prefix
+  timeout: 3                      # 3 seconds
+
+plugins:                          # plugin list
+  - example-plugin
+  - limit-req
+  - limit-count
+  - limit-conn
+  - key-auth
+  - basic-auth
+  - prometheus
+  - node-status
+  - jwt-auth
+  - zipkin
+  - ip-restriction
+  - grpc-transcode
+  - serverless-pre-function
+  - serverless-post-function
+  - openid-connect
+  - proxy-rewrite
+  - redirect
+  - response-rewrite
+  - fault-injection
+  - udp-logger
+  - wolf-rbac
+  - proxy-cache
+  - tcp-logger
+  - proxy-mirror
+  - kafka-logger
+  - cors
+stream_plugins:
+  - mqtt-proxy
diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml
new file mode 100644
index 0000000..859e390
--- /dev/null
+++ b/compose/docker-compose.yml
@@ -0,0 +1,124 @@
+version: "3"
+
+services:
+  apisix:
+    image: apache/apisix:latest
+    restart: always
+    volumes:
+      - ./apisix_log:/usr/local/apisix/logs
+      - ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
+    depends_on:
+      - etcd
+    ##network_mode: host
+    ports:
+      - "9080:9080/tcp"
+      - "9443:9443/tcp"
+    networks:
+      apisix-dashboard:
+        ipv4_address: 192.17.5.11
+
+  etcd:
+    image: gcr.io/etcd-development/etcd:v3.3.12
+    command: /usr/local/bin/etcd --advertise-client-urls http://0.0.0.0:2379 --listen-client-urls http://0.0.0.0:2379
+    restart: always
+    volumes:
+      - ./etcd_data:/etcd_data
+    environment:
+      ETCD_DATA_DIR: /etcd_data
+    ports:
+      - "2379:2379/tcp"
+    networks:
+      apisix-dashboard:
+        ipv4_address: 192.17.5.10
+
+  web1:
+    image: ruby:2-alpine
+    command: sh -c "mkdir -p /tmp/www && echo 'web1' > /tmp/www/web1.txt && ruby -run -ehttpd /tmp/www -p8000"
+    restart: always
+    ports:
+      - "9081:8000/tcp"
+    networks:
+      apisix-dashboard:
+        ipv4_address: 192.17.5.12
+
+  web2:
+    image: ruby:2-alpine
+    command: sh -c "mkdir -p /tmp/www && echo 'web2' > /tmp/www/web2.txt && ruby -run -ehttpd /tmp/www -p8000"
+    restart: always
+    ports:
+      - "9082:8000/tcp"
+    networks:
+      apisix-dashboard:
+        ipv4_address: 192.17.5.13
+
+  mysql:
+    image: mysql:latest
+    restart: always
+    ports:
+      - "3309:3306/tcp"
+    environment:
+      - MYSQL_ROOT_PASSWORD=123456
+    volumes:
+      - /tmp/datadir:/var/lib/mysql
+      - /tmp/conf.d:/etc/mysql/conf.d
+      - ../api/script/db:/docker-entrypoint-initdb.d
+    networks:
+      apisix-dashboard:
+        ipv4_address: 192.17.5.14
+  
+  manager:
+    build:
+      context: ./../api
+      dockerfile: Dockerfile
+    restart: always
+    ports:
+      - "8080:8080/tcp"
+    environment:
+      - ENV=prod
+    volumes:
+      - ./manager_conf/build.sh:/root/manager-api/build.sh
+    networks:
+      apisix-dashboard:
+        ipv4_address: 192.17.5.15
+
+  prometheus:
+    image: prom/prometheus
+    hostname: prometheus
+    restart: always
+    volumes:
+      - ./prometheus_conf/prometheus.yml:/etc/prometheus/prometheus.yml
+    ports:
+      - "9090:9090"
+    networks:
+      apisix-dashboard:
+        ipv4_address: 192.17.5.16
+
+  grafana:
+    image: grafana/grafana
+    container_name: grafana
+    hostname: grafana
+    restart: always
+    ports:
+      - "3000:3000"
+    volumes:
+      - "./grafana_conf/provisioning:/etc/grafana/provisioning"
+      - "./grafana_conf/dashboards:/var/lib/grafana/dashboards"
+      - "./grafana_conf/config/grafana.ini:/etc/grafana/grafana.ini"
+    networks:
+      apisix-dashboard:
+        ipv4_address: 192.17.5.17
+
+  dashboard:
+    build:
+      context: ./..
+      dockerfile: Dockerfile
+    restart: always
+    ports:
+      - "80:80/tcp"
+
+networks:
+  apisix-dashboard:
+    driver: bridge
+    ipam:
+      config:
+      - subnet: 192.17.0.0/16
\ No newline at end of file
diff --git a/compose/grafana_conf/config/grafana.ini b/compose/grafana_conf/config/grafana.ini
new file mode 100644
index 0000000..cb6a737
--- /dev/null
+++ b/compose/grafana_conf/config/grafana.ini
@@ -0,0 +1,756 @@
+##################### Grafana Configuration Example #####################
+#
+# Everything has defaults so you only need to uncomment things you want to
+# change
+
+# possible values : production, development
+;app_mode = production
+
+# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
+;instance_name = ${HOSTNAME}
+
+#################################### Paths ####################################
+[paths]
+# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
+;data = /var/lib/grafana
+
+# Temporary files in `data` directory older than given duration will be removed
+;temp_data_lifetime = 24h
+
+# Directory where grafana can store logs
+;logs = /var/log/grafana
+
+# Directory where grafana will automatically scan and look for plugins
+;plugins = /var/lib/grafana/plugins
+
+# folder that contains provisioning config files that grafana will apply on startup and while running.
+;provisioning = conf/provisioning
+
+#################################### Server ####################################
+[server]
+# Protocol (http, https, h2, socket)
+;protocol = http
+
+# The ip address to bind to, empty will bind to all interfaces
+;http_addr =
+
+# The http port  to use
+;http_port = 3000
+
+# The public facing domain name used to access grafana from a browser
+;domain = localhost
+
+# Redirect to correct domain if host header does not match domain
+# Prevents DNS rebinding attacks
+;enforce_domain = false
+
+# The full public facing url you use in browser, used for redirects and emails
+# If you use reverse proxy and sub path specify full url (with sub path)
+;root_url = %(protocol)s://%(domain)s:%(http_port)s/
+
+# Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons.
+;serve_from_sub_path = false
+
+# Log web requests
+;router_logging = false
+
+# the path relative working path
+;static_root_path = public
+
+# enable gzip
+;enable_gzip = false
+
+# https certs & key file
+;cert_file =
+;cert_key =
+
+# Unix socket path
+;socket =
+
+#################################### Database ####################################
+[database]
+# You can configure the database connection by specifying type, host, name, user and password
+# as separate properties or as on string using the url properties.
+
+# Either "mysql", "postgres" or "sqlite3", it's your choice
+;type = sqlite3
+;host = 127.0.0.1:3306
+;name = grafana
+;user = root
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+;password =
+
+# Use either URL or the previous fields to configure the database
+# Example: mysql://user:secret@host:port/database
+;url =
+
+# For "postgres" only, either "disable", "require" or "verify-full"
+;ssl_mode = disable
+
+;ca_cert_path =
+;client_key_path =
+;client_cert_path =
+;server_cert_name =
+
+# For "sqlite3" only, path relative to data_path setting
+;path = grafana.db
+
+# Max idle conn setting default is 2
+;max_idle_conn = 2
+
+# Max conn setting default is 0 (mean not set)
+;max_open_conn =
+
+# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours)
+;conn_max_lifetime = 14400
+
+# Set to true to log the sql calls and execution times.
+;log_queries =
+
+# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
+;cache_mode = private
+
+#################################### Cache server #############################
+[remote_cache]
+# Either "redis", "memcached" or "database" default is "database"
+;type = database
+
+# cache connectionstring options
+# database: will use Grafana primary database.
+# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0,ssl=false`. Only addr is required. ssl may be 'true', 'false', or 'insecure'.
+# memcache: 127.0.0.1:11211
+;connstr =
+
+#################################### Data proxy ###########################
+[dataproxy]
+
+# This enables data proxy logging, default is false
+;logging = false
+
+# How long the data proxy should wait before timing out default is 30 (seconds)
+;timeout = 30
+
+# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false.
+;send_user_header = false
+
+#################################### Analytics ####################################
+[analytics]
+# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
+# No ip addresses are being tracked, only simple counters to track
+# running instances, dashboard and error counts. It is very helpful to us.
+# Change this option to false to disable reporting.
+;reporting_enabled = true
+
+# Set to false to disable all checks to https://grafana.net
+# for new vesions (grafana itself and plugins), check is used
+# in some UI views to notify that grafana or plugin update exists
+# This option does not cause any auto updates, nor send any information
+# only a GET request to http://grafana.com to get latest versions
+;check_for_updates = true
+
+# Google Analytics universal tracking code, only enabled if you specify an id here
+;google_analytics_ua_id =
+
+# Google Tag Manager ID, only enabled if you specify an id here
+;google_tag_manager_id =
+
+#################################### Security ####################################
+[security]
+# disable creation of admin user on first start of grafana
+;disable_initial_admin_creation = false
+
+# default admin user, created on startup
+;admin_user = admin
+
+# default admin password, can be changed before first start of grafana,  or in profile settings
+;admin_password = admin
+
+# used for signing
+;secret_key = SW2YcwTIb9zpOOhoPsMm
+
+# disable gravatar profile images
+;disable_gravatar = false
+
+# data source proxy whitelist (ip_or_domain:port separated by spaces)
+;data_source_proxy_whitelist =
+
+# disable protection against brute force login attempts
+;disable_brute_force_login_protection = false
+
+# set to true if you host Grafana behind HTTPS. default is false.
+;cookie_secure = false
+
+# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict", "none" and "disabled"
+;cookie_samesite = none
+
+# set to true if you want to allow browsers to render Grafana in a <frame>, <iframe>, <embed> or <object>. default is false.
+allow_embedding = true
+
+# Set to true if you want to enable http strict transport security (HSTS) response header.
+# This is only sent when HTTPS is enabled in this configuration.
+# HSTS tells browsers that the site should only be accessed using HTTPS.
+# The default version will change to true in the next minor release, 6.3.
+;strict_transport_security = false
+
+# Sets how long a browser should cache HSTS. Only applied if strict_transport_security is enabled.
+;strict_transport_security_max_age_seconds = 86400
+
+# Set to true if to enable HSTS preloading option. Only applied if strict_transport_security is enabled.
+;strict_transport_security_preload = false
+
+# Set to true if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled.
+;strict_transport_security_subdomains = false
+
+# Set to true to enable the X-Content-Type-Options response header.
+# The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised
+# in the Content-Type headers should not be changed and be followed. The default will change to true in the next minor release, 6.3.
+;x_content_type_options = false
+
+# Set to true to enable the X-XSS-Protection header, which tells browsers to stop pages from loading
+# when they detect reflected cross-site scripting (XSS) attacks. The default will change to true in the next minor release, 6.3.
+;x_xss_protection = false
+
+#################################### Snapshots ###########################
+[snapshots]
+# snapshot sharing options
+;external_enabled = true
+;external_snapshot_url = https://snapshots-origin.raintank.io
+;external_snapshot_name = Publish to snapshot.raintank.io
+
+# Set to true to enable this Grafana instance act as an external snapshot server and allow unauthenticated requests for
+# creating and deleting snapshots.
+;public_mode = false
+
+# remove expired snapshot
+;snapshot_remove_expired = true
+
+#################################### Dashboards History ##################
+[dashboards]
+# Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1
+;versions_to_keep = 20
+
+# Minimum dashboard refresh interval. When set, this will restrict users to set the refresh interval of a dashboard lower than given interval. Per default this is 5 seconds.
+# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
+;min_refresh_interval = 5s
+
+#################################### Users ###############################
+[users]
+# disable user signup / registration
+;allow_sign_up = true
+
+# Allow non admin users to create organizations
+;allow_org_create = true
+
+# Set to true to automatically assign new users to the default organization (id 1)
+;auto_assign_org = true
+
+# Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true)
+;auto_assign_org_id = 1
+
+# Default role new users will be automatically assigned (if disabled above is set to true)
+;auto_assign_org_role = Viewer
+
+# Require email validation before sign up completes
+;verify_email_enabled = false
+
+# Background text for the user field on the login page
+;login_hint = email or username
+;password_hint = password
+
+# Default UI theme ("dark" or "light")
+;default_theme = dark
+
+# External user management, these options affect the organization users view
+;external_manage_link_url =
+;external_manage_link_name =
+;external_manage_info =
+
+# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
+;viewers_can_edit = false
+
+# Editors can administrate dashboard, folders and teams they create
+;editors_can_admin = false
+
+[auth]
+# Login cookie name
+;login_cookie_name = grafana_session
+
+# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days,
+;login_maximum_inactive_lifetime_days = 7
+
+# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days.
+;login_maximum_lifetime_days = 30
+
+# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
+;token_rotation_interval_minutes = 10
+
+# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
+;disable_login_form = false
+
+# Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false
+;disable_signout_menu = false
+
+# URL to redirect the user to after sign out
+;signout_redirect_url =
+
+# Set to true to attempt login with OAuth automatically, skipping the login screen.
+# This setting is ignored if multiple OAuth providers are configured.
+;oauth_auto_login = false
+
+# OAuth state max age cookie duration. Defaults to 60 seconds.
+;oauth_state_cookie_max_age = 60
+
+# limit of api_key seconds to live before expiration
+;api_key_max_seconds_to_live = -1
+
+#################################### Anonymous Auth ######################
+[auth.anonymous]
+# enable anonymous access
+enabled = true
+
+# specify organization name that should be used for unauthenticated users
+;org_name = Main Org.
+
+# specify role for unauthenticated users
+;org_role = Viewer
+
+#################################### Github Auth ##########################
+[auth.github]
+;enabled = false
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = user:email,read:org
+;auth_url = https://github.com/login/oauth/authorize
+;token_url = https://github.com/login/oauth/access_token
+;api_url = https://api.github.com/user
+;allowed_domains =
+;team_ids =
+;allowed_organizations =
+
+#################################### GitLab Auth #########################
+[auth.gitlab]
+;enabled = false
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = api
+;auth_url = https://gitlab.com/oauth/authorize
+;token_url = https://gitlab.com/oauth/token
+;api_url = https://gitlab.com/api/v4
+;allowed_domains =
+;allowed_groups =
+
+#################################### Google Auth ##########################
+[auth.google]
+;enabled = false
+;allow_sign_up = true
+;client_id = some_client_id
+;client_secret = some_client_secret
+;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
+;auth_url = https://accounts.google.com/o/oauth2/auth
+;token_url = https://accounts.google.com/o/oauth2/token
+;api_url = https://www.googleapis.com/oauth2/v1/userinfo
+;allowed_domains =
+;hosted_domain =
+
+#################################### Grafana.com Auth ####################
+[auth.grafana_com]
+;enabled = false
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = user:email
+;allowed_organizations =
+
+#################################### Azure AD OAuth #######################
+[auth.azuread]
+;name = Azure AD
+;enabled = false
+;allow_sign_up = true
+;client_id = some_client_id
+;client_secret = some_client_secret
+;scopes = openid email profile
+;auth_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
+;token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
+;allowed_domains =
+;allowed_groups =
+
+#################################### Okta OAuth #######################
+[auth.okta]
+;name = Okta
+;enabled = false
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = openid profile email groups
+;auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
+;token_url = https://<tenant-id>.okta.com/oauth2/v1/token
+;api_url = https://<tenant-id>.okta.com/oauth2/v1/userinfo
+;allowed_domains =
+;allowed_groups =
+;role_attribute_path =
+
+#################################### Generic OAuth ##########################
+[auth.generic_oauth]
+;enabled = false
+;name = OAuth
+;allow_sign_up = true
+;client_id = some_id
+;client_secret = some_secret
+;scopes = user:email,read:org
+;email_attribute_name = email:primary
+;email_attribute_path =
+;auth_url = https://foo.bar/login/oauth/authorize
+;token_url = https://foo.bar/login/oauth/access_token
+;api_url = https://foo.bar/user
+;allowed_domains =
+;team_ids =
+;allowed_organizations =
+;role_attribute_path =
+;tls_skip_verify_insecure = false
+;tls_client_cert =
+;tls_client_key =
+;tls_client_ca =
+
+#################################### Basic Auth ##########################
+[auth.basic]
+;enabled = true
+
+#################################### Auth Proxy ##########################
+[auth.proxy]
+;enabled = false
+;header_name = X-WEBAUTH-USER
+;header_property = username
+;auto_sign_up = true
+;sync_ttl = 60
+;whitelist = 192.168.1.1, 192.168.2.1
+;headers = Email:X-User-Email, Name:X-User-Name
+# Read the auth proxy docs for details on what the setting below enables
+;enable_login_token = false
+
+#################################### Auth LDAP ##########################
+[auth.ldap]
+;enabled = false
+;config_file = /etc/grafana/ldap.toml
+;allow_sign_up = true
+
+# LDAP backround sync (Enterprise only)
+# At 1 am every day
+;sync_cron = "0 0 1 * * *"
+;active_sync_enabled = true
+
+#################################### SMTP / Emailing ##########################
+[smtp]
+;enabled = false
+;host = localhost:25
+;user =
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+;password =
+;cert_file =
+;key_file =
+;skip_verify = false
+;from_address = admin@grafana.localhost
+;from_name = Grafana
+# EHLO identity in SMTP dialog (defaults to instance_name)
+;ehlo_identity = dashboard.example.com
+
+[emails]
+;welcome_email_on_sign_up = false
+;templates_pattern = emails/*.html
+
+#################################### Logging ##########################
+[log]
+# Either "console", "file", "syslog". Default is console and  file
+# Use space to separate multiple modes, e.g. "console file"
+;mode = console file
+
+# Either "debug", "info", "warn", "error", "critical", default is "info"
+;level = info
+
+# optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug
+;filters =
+
+# For "console" mode only
+[log.console]
+;level =
+
+# log line format, valid options are text, console and json
+;format = console
+
+# For "file" mode only
+[log.file]
+;level =
+
+# log line format, valid options are text, console and json
+;format = text
+
+# This enables automated log rotate(switch of following options), default is true
+;log_rotate = true
+
+# Max line number of single file, default is 1000000
+;max_lines = 1000000
+
+# Max size shift of single file, default is 28 means 1 << 28, 256MB
+;max_size_shift = 28
+
+# Segment log daily, default is true
+;daily_rotate = true
+
+# Expired days of log file(delete after max days), default is 7
+;max_days = 7
+
+[log.syslog]
+;level =
+
+# log line format, valid options are text, console and json
+;format = text
+
+# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used.
+;network =
+;address =
+
+# Syslog facility. user, daemon and local0 through local7 are valid.
+;facility =
+
+# Syslog tag. By default, the process' argv[0] is used.
+;tag =
+
+#################################### Usage Quotas ########################
+[quota]
+; enabled = false
+
+#### set quotas to -1 to make unlimited. ####
+# limit number of users per Org.
+; org_user = 10
+
+# limit number of dashboards per Org.
+; org_dashboard = 100
+
+# limit number of data_sources per Org.
+; org_data_source = 10
+
+# limit number of api_keys per Org.
+; org_api_key = 10
+
+# limit number of orgs a user can create.
+; user_org = 10
+
+# Global limit of users.
+; global_user = -1
+
+# global limit of orgs.
+; global_org = -1
+
+# global limit of dashboards
+; global_dashboard = -1
+
+# global limit of api_keys
+; global_api_key = -1
+
+# global limit on number of logged in users.
+; global_session = -1
+
+#################################### Alerting ############################
+[alerting]
+# Disable alerting engine & UI features
+;enabled = true
+# Makes it possible to turn off alert rule execution but alerting UI is visible
+;execute_alerts = true
+
+# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
+;error_or_timeout = alerting
+
+# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
+;nodata_or_nullvalues = no_data
+
+# Alert notifications can include images, but rendering many images at the same time can overload the server
+# This limit will protect the server from render overloading and make sure notifications are sent out quickly
+;concurrent_render_limit = 5
+
+
+# Default setting for alert calculation timeout. Default value is 30
+;evaluation_timeout_seconds = 30
+
+# Default setting for alert notification timeout. Default value is 30
+;notification_timeout_seconds = 30
+
+# Default setting for max attempts to sending alert notifications. Default value is 3
+;max_attempts = 3
+
+# Makes it possible to enforce a minimal interval between evaluations, to reduce load on the backend
+;min_interval_seconds = 1
+
+#################################### Explore #############################
+[explore]
+# Enable the Explore section
+;enabled = true
+
+#################################### Internal Grafana Metrics ##########################
+# Metrics available at HTTP API Url /metrics
+[metrics]
+# Disable / Enable internal metrics
+;enabled           = true
+# Graphite Publish interval
+;interval_seconds  = 10
+# Disable total stats (stat_totals_*) metrics to be generated
+;disable_total_stats = false
+
+#If both are set, basic auth will be required for the metrics endpoint.
+; basic_auth_username =
+; basic_auth_password =
+
+# Send internal metrics to Graphite
+[metrics.graphite]
+# Enable by setting the address setting (ex localhost:2003)
+;address =
+;prefix = prod.grafana.%(instance_name)s.
+
+#################################### Grafana.com integration  ##########################
+# Url used to import dashboards directly from Grafana.com
+[grafana_com]
+;url = https://grafana.com
+
+#################################### Distributed tracing ############
+[tracing.jaeger]
+# Enable by setting the address sending traces to jaeger (ex localhost:6831)
+;address = localhost:6831
+# Tag that will always be included in when creating new spans. ex (tag1:value1,tag2:value2)
+;always_included_tag = tag1:value1
+# Type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote
+;sampler_type = const
+# jaeger samplerconfig param
+# for "const" sampler, 0 or 1 for always false/true respectively
+# for "probabilistic" sampler, a probability between 0 and 1
+# for "rateLimiting" sampler, the number of spans per second
+# for "remote" sampler, param is the same as for "probabilistic"
+# and indicates the initial sampling rate before the actual one
+# is received from the mothership
+;sampler_param = 1
+# Whether or not to use Zipkin propagation (x-b3- HTTP headers).
+;zipkin_propagation = false
+# Setting this to true disables shared RPC spans.
+# Not disabling is the most common setting when using Zipkin elsewhere in your infrastructure.
+;disable_shared_zipkin_spans = false
+
+#################################### External image storage ##########################
+[external_image_storage]
+# Used for uploading images to public servers so they can be included in slack/email messages.
+# you can choose between (s3, webdav, gcs, azure_blob, local)
+;provider =
+
+[external_image_storage.s3]
+;endpoint =
+;path_style_access =
+;bucket =
+;region =
+;path =
+;access_key =
+;secret_key =
+
+[external_image_storage.webdav]
+;url =
+;public_url =
+;username =
+;password =
+
+[external_image_storage.gcs]
+;key_file =
+;bucket =
+;path =
+
+[external_image_storage.azure_blob]
+;account_name =
+;account_key =
+;container_name =
+
+[external_image_storage.local]
+# does not require any configuration
+
+[rendering]
+# Options to configure a remote HTTP image rendering service, e.g. using https://github.com/grafana/grafana-image-renderer.
+# URL to a remote HTTP image renderer service, e.g. http://localhost:8081/render, will enable Grafana to render panels and dashboards to PNG-images using HTTP requests to an external service.
+;server_url =
+# If the remote HTTP image renderer service runs on a different server than the Grafana server you may have to configure this to a URL where Grafana is reachable, e.g. http://grafana.domain/.
+;callback_url =
+# Concurrent render request limit affects when the /render HTTP endpoint is used. Rendering many images at the same time can overload the server,
+# which this setting can help protect against by only allowing a certain amount of concurrent requests.
+;concurrent_render_request_limit = 30
+
+[panels]
+# If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities.
+;disable_sanitize_html = false
+
+[plugins]
+;enable_alpha = false
+;app_tls_skip_verify_insecure = false
+# Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature.
+;allow_loading_unsigned_plugins =
+
+#################################### Grafana Image Renderer Plugin ##########################
+[plugin.grafana-image-renderer]
+# Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert.
+# See ICU’s metaZones.txt (https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt) for a list of supported
+# timezone IDs. Fallbacks to TZ environment variable if not set.
+;rendering_timezone =
+
+# Instruct headless browser instance to use a default language when not provided by Grafana, e.g. when rendering panel image of alert.
+# Please refer to the HTTP header Accept-Language to understand how to format this value, e.g. 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'.
+;rendering_language =
+
+# Instruct headless browser instance to use a default device scale factor when not provided by Grafana, e.g. when rendering panel image of alert.
+# Default is 1. Using a higher value will produce more detailed images (higher DPI), but will require more disk space to store an image.
+;rendering_viewport_device_scale_factor =
+
+# Instruct headless browser instance whether to ignore HTTPS errors during navigation. Per default HTTPS errors are not ignored. Due to
+# the security risk it's not recommended to ignore HTTPS errors.
+;rendering_ignore_https_errors =
+
+# Instruct headless browser instance whether to capture and log verbose information when rendering an image. Default is false and will
+# only capture and log error messages. When enabled, debug messages are captured and logged as well.
+# For the verbose information to be included in the Grafana server log you have to adjust the rendering log level to debug, configure
+# [log].filter = rendering:debug.
+;rendering_verbose_logging =
+
+# Instruct headless browser instance whether to output its debug and error messages into running process of remote rendering service.
+# Default is false. This can be useful to enable (true) when troubleshooting.
+;rendering_dumpio =
+
+# Additional arguments to pass to the headless browser instance. Default is --no-sandbox. The list of Chromium flags can be found
+# here (https://peter.sh/experiments/chromium-command-line-switches/). Multiple arguments is separated with comma-character.
+;rendering_args =
+
+# You can configure the plugin to use a different browser binary instead of the pre-packaged version of Chromium.
+# Please note that this is not recommended, since you may encounter problems if the installed version of Chrome/Chromium is not
+# compatible with the plugin.
+;rendering_chrome_bin =
+
+# Instruct how headless browser instances are created. Default is 'default' and will create a new browser instance on each request.
+# Mode 'clustered' will make sure that only a maximum of browsers/incognito pages can execute concurrently.
+# Mode 'reusable' will have one browser instance and will create a new incognito page on each request.
+;rendering_mode =
+
+# When rendering_mode = clustered you can instruct how many browsers or incognito pages can execute concurrently. Default is 'browser'
+# and will cluster using browser instances.
+# Mode 'context' will cluster using incognito pages.
+;rendering_clustering_mode =
+# When rendering_mode = clustered you can define maximum number of browser instances/incognito pages that can execute concurrently..
+;rendering_clustering_max_concurrency =
+
+# Limit the maxiumum viewport width, height and device scale factor that can be requested.
+;rendering_viewport_max_width =
+;rendering_viewport_max_height =
+;rendering_viewport_max_device_scale_factor =
+
+# Change the listening host and port of the gRPC server. Default host is 127.0.0.1 and default port is 0 and will automatically assign
+# a port not in use.
+;grpc_host =
+;grpc_port =
+
+[enterprise]
+# Path to a valid Grafana Enterprise license.jwt file
+;license_path =
+
+[feature_toggles]
+# enable features, separated by spaces
+;enable =
diff --git a/compose/grafana_conf/dashboards/apisix_http_prometheus.json b/compose/grafana_conf/dashboards/apisix_http_prometheus.json
new file mode 100644
index 0000000..8dd9f8b
--- /dev/null
+++ b/compose/grafana_conf/dashboards/apisix_http_prometheus.json
@@ -0,0 +1,956 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "apisix",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "limit": 100,
+        "name": "Annotations & Alerts",
+        "showIn": 0,
+        "type": "dashboard"
+      }
+    ]
+  },
+  "description": "MicroService API Gateway Apache APISIX",
+  "editable": true,
+  "gnetId": 11719,
+  "graphTooltip": 0,
+  "id": 10,
+  "iteration": 1591947413854,
+  "links": [],
+  "panels": [
+    {
+      "collapsed": false,
+      "datasource": "apisix",
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 10,
+      "panels": [],
+      "title": "Nginx",
+      "type": "row"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorValue": false,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "apisix",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "format": "none",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 5,
+        "w": 5,
+        "x": 0,
+        "y": 1
+      },
+      "id": 8,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": true,
+        "ymax": null,
+        "ymin": null
+      },
+      "tableColumn": "Total",
+      "targets": [
+        {
+          "expr": "sum(apisix_nginx_http_current_connections{state=\"total\", instance=~\"$instance\"})",
+          "intervalFactor": 2,
+          "legendFormat": "Total",
+          "refId": "A"
+        }
+      ],
+      "thresholds": "",
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Total Connections",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorValue": false,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "apisix",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "format": "none",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 5,
+        "w": 5,
+        "x": 5,
+        "y": 1
+      },
+      "id": 16,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": true,
+        "ymax": null,
+        "ymin": null
+      },
+      "tableColumn": "Accepted",
+      "targets": [
+        {
+          "expr": "sum(apisix_nginx_http_current_connections{state=\"accepted\", instance=~\"$instance\"})",
+          "intervalFactor": 2,
+          "legendFormat": "Accepted",
+          "refId": "A"
+        }
+      ],
+      "thresholds": "",
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Accepted Connections",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorValue": false,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "apisix",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "format": "none",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 5,
+        "w": 5,
+        "x": 10,
+        "y": 1
+      },
+      "id": 11,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": true,
+        "ymax": null,
+        "ymin": null
+      },
+      "tableColumn": "Total",
+      "targets": [
+        {
+          "expr": "sum(apisix_nginx_http_current_connections{state=\"handled\", instance=~\"$instance\"})",
+          "intervalFactor": 2,
+          "legendFormat": "Total",
+          "refId": "A"
+        }
+      ],
+      "thresholds": "",
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Handled Connections",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "apisix",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 6,
+        "w": 15,
+        "x": 0,
+        "y": 6
+      },
+      "hiddenSeries": false,
+      "id": 17,
+      "legend": {
+        "alignAsTable": false,
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "rightSide": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "dataLinks": []
+      },
+      "percentage": false,
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "sum(apisix_nginx_http_current_connections{state=~\"active|reading|writing|waiting\", instance=~\"$instance\"}) by (state)",
+          "intervalFactor": 1,
+          "legendFormat": "{{state}}",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Nginx connection state",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "collapsed": false,
+      "datasource": "apisix",
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 12
+      },
+      "id": 13,
+      "panels": [],
+      "title": "Bandwidth",
+      "type": "row"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "apisix",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 6,
+        "w": 15,
+        "x": 0,
+        "y": 13
+      },
+      "hiddenSeries": false,
+      "id": 6,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": false,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "dataLinks": []
+      },
+      "percentage": false,
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "sum(rate(apisix_bandwidth{instance=~\"$instance\"}[30s])) by (type)",
+          "legendFormat": "{{type}}",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Total Bandwidth",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "apisix",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 6,
+        "w": 7,
+        "x": 0,
+        "y": 19
+      },
+      "hiddenSeries": false,
+      "id": 19,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "dataLinks": []
+      },
+      "percentage": false,
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "sum(rate(apisix_bandwidth{type=\"egress\", service =~\"$service\",route=~\"$route\",instance=~\"$instance\"}[1m])) by (service)",
+          "legendFormat": "service:{{service}}",
+          "refId": "A"
+        },
+        {
+          "expr": "sum(rate(apisix_bandwidth{type=\"egress\", service =~\"$service\",route=~\"$route\",instance=~\"$instance\"}[1m])) by (route)",
+          "legendFormat": "route:{{route}}",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Egress per service/route",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "apisix",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 6,
+        "w": 8,
+        "x": 7,
+        "y": 19
+      },
+      "hiddenSeries": false,
+      "id": 21,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "dataLinks": []
+      },
+      "percentage": false,
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "sum(irate(apisix_bandwidth{type=\"ingress\", service =~\"$service\",route=~\"$route\",instance=~\"$instance\"}[1m])) by (service)",
+          "legendFormat": "service:{{service}}",
+          "refId": "A"
+        },
+        {
+          "expr": "sum(irate(apisix_bandwidth{type=\"ingress\", service =~\"$service\",route=~\"$route\",instance=~\"$instance\"}[1m])) by (route)",
+          "legendFormat": "route:{{route}}",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Ingress per service/route",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "collapsed": false,
+      "datasource": "apisix",
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 25
+      },
+      "id": 15,
+      "panels": [],
+      "title": "HTTP Status",
+      "type": "row"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "cacheTimeout": null,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "apisix",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 3,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 6,
+        "w": 15,
+        "x": 0,
+        "y": 26
+      },
+      "hiddenSeries": false,
+      "id": 2,
+      "interval": "",
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "options": {
+        "dataLinks": []
+      },
+      "percentage": false,
+      "pluginVersion": "6.5.2",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "state",
+          "lines": true
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "sum(rate(apisix_http_status{code=~\"2..\",service=~\"$service\",route=~\"$route\",instance=~\"$instance\"}[30s])) by (status_2xx)",
+          "instant": false,
+          "intervalFactor": 1,
+          "legendFormat": "{{status_2xx}}",
+          "refId": "A"
+        },
+        {
+          "expr": "sum(rate(apisix_http_status{code=~\"3..\",service=~\"$service\",route=~\"$route\",instance=~\"$instance\"}[30s])) by (status_3xx)",
+          "legendFormat": "{{status_3xx}}",
+          "refId": "D"
+        },
+        {
+          "expr": "sum(rate(apisix_http_status{code=~\"4..\",service=~\"$service\",route=~\"$route\",instance=~\"$instance\"}[30s])) by (status_4xx)",
+          "intervalFactor": 1,
+          "legendFormat": "{{status_4xx}}",
+          "refId": "B"
+        },
+        {
+          "expr": "sum(rate(apisix_http_status{code=~\"5..\",service=~\"$service\",route=~\"$route\",instance=~\"$instance\"}[30s])) by (status_5xx)",
+          "legendFormat": "{{status_5xx}}",
+          "refId": "C"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Service HTTP Code",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    }
+  ],
+  "refresh": "5s",
+  "schemaVersion": 25,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": [
+      {
+        "allValue": ".*",
+        "current": {
+          "selected": false,
+          "text": "All",
+          "value": "$__all"
+        },
+        "datasource": "apisix",
+        "definition": "label_values(apisix_http_status,service)",
+        "hide": 0,
+        "includeAll": true,
+        "label": null,
+        "multi": true,
+        "name": "service",
+        "options": [],
+        "query": "label_values(apisix_http_status,service)",
+        "refresh": 1,
+        "regex": "",
+        "skipUrlSync": false,
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": ".*",
+        "current": {
+          "selected": false,
+          "text": "All",
+          "value": "$__all"
+        },
+        "datasource": "apisix",
+        "definition": "label_values(apisix_http_status,route)",
+        "hide": 0,
+        "includeAll": true,
+        "label": null,
+        "multi": true,
+        "name": "route",
+        "options": [],
+        "query": "label_values(apisix_http_status,route)",
+        "refresh": 1,
+        "regex": "",
+        "skipUrlSync": false,
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "allValue": ".*",
+        "current": {
+          "selected": false,
+          "text": "All",
+          "value": "$__all"
+        },
+        "datasource": "apisix",
+        "definition": "label_values(apisix_http_status,instance)",
+        "hide": 0,
+        "includeAll": true,
+        "label": null,
+        "multi": true,
+        "name": "instance",
+        "options": [],
+        "query": "label_values(apisix_http_status,instance)",
+        "refresh": 2,
+        "regex": ".*",
+        "skipUrlSync": false,
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      }
+    ]
+  },
+  "time": {
+    "from": "now-30m",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ]
+  },
+  "timezone": "",
+  "title": "Apache APISIX",
+  "uid": "bLlNuRLWz",
+  "version": 1
+}
\ No newline at end of file
diff --git a/compose/grafana_conf/provisioning/dashboards/all.yaml b/compose/grafana_conf/provisioning/dashboards/all.yaml
new file mode 100644
index 0000000..c58cbc6
--- /dev/null
+++ b/compose/grafana_conf/provisioning/dashboards/all.yaml
@@ -0,0 +1,11 @@
+apiVersion: 1
+
+providers:
+- name: 'default'
+  orgId: 1
+  folder: ''
+  type: file
+  disableDeletion: false
+  editable: false
+  options:
+    path: /var/lib/grafana/dashboards
\ No newline at end of file
diff --git a/compose/grafana_conf/provisioning/datasources/all.yaml b/compose/grafana_conf/provisioning/datasources/all.yaml
new file mode 100644
index 0000000..4245eac
--- /dev/null
+++ b/compose/grafana_conf/provisioning/datasources/all.yaml
@@ -0,0 +1,9 @@
+datasources:
+ - access: 'proxy'
+   editable: true
+   is_default: true
+   name: 'apisix'
+   org_id: 1
+   type: 'prometheus'
+   url: 'http://prometheus:9090'
+   version: 1
\ No newline at end of file
diff --git a/compose/manager_conf/build.sh b/compose/manager_conf/build.sh
new file mode 100755
index 0000000..efedb1c
--- /dev/null
+++ b/compose/manager_conf/build.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+pwd=`pwd`
+
+export MYSQL_SERVER_ADDRESS="192.17.5.14:3306"
+export MYSQL_USER=root
+export MYSQL_PASSWORD=123456
+export SYSLOG_HOST=127.0.0.1
+export APISIX_BASE_URL="http://192.17.5.11:9080/apisix/admin"
+export APISIX_API_KEY="edd1c9f034335f136f87ad84b625c8f1"
+
+sed -i -e "s%#mysqlAddress#%`echo $MYSQL_SERVER_ADDRESS`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlUser#%`echo $MYSQL_USER`%g" ${pwd}/conf.json
+sed -i -e "s%#mysqlPWD#%`echo $MYSQL_PASSWORD`%g" ${pwd}/conf.json
+sed -i -e "s%#syslogAddress#%`echo $SYSLOG_HOST`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixBaseUrl#%`echo $APISIX_BASE_URL`%g" ${pwd}/conf.json
+sed -i -e "s%#apisixApiKey#%`echo $APISIX_API_KEY`%g" ${pwd}/conf.json
+
+cd /root/manager-api
+exec ./manager-api
diff --git a/compose/pics/grafana_1.png b/compose/pics/grafana_1.png
new file mode 100644
index 0000000..631276e
Binary files /dev/null and b/compose/pics/grafana_1.png differ
diff --git a/compose/pics/grafana_2.png b/compose/pics/grafana_2.png
new file mode 100644
index 0000000..03711eb
Binary files /dev/null and b/compose/pics/grafana_2.png differ
diff --git a/compose/pics/grafana_3.png b/compose/pics/grafana_3.png
new file mode 100644
index 0000000..5b2e834
Binary files /dev/null and b/compose/pics/grafana_3.png differ
diff --git a/compose/pics/grafana_4.png b/compose/pics/grafana_4.png
new file mode 100644
index 0000000..237e4dc
Binary files /dev/null and b/compose/pics/grafana_4.png differ
diff --git a/compose/pics/grafana_5.png b/compose/pics/grafana_5.png
new file mode 100644
index 0000000..dd9654d
Binary files /dev/null and b/compose/pics/grafana_5.png differ
diff --git a/compose/pics/grafana_6.png b/compose/pics/grafana_6.png
new file mode 100644
index 0000000..8931a0d
Binary files /dev/null and b/compose/pics/grafana_6.png differ
diff --git a/compose/pics/login.png b/compose/pics/login.png
new file mode 100644
index 0000000..c42e375
Binary files /dev/null and b/compose/pics/login.png differ
diff --git a/compose/prometheus_conf/prometheus.yml b/compose/prometheus_conf/prometheus.yml
new file mode 100644
index 0000000..0804c81
--- /dev/null
+++ b/compose/prometheus_conf/prometheus.yml
@@ -0,0 +1,23 @@
+global:
+  scrape_interval: 15s # By default, scrape targets every 15 seconds.
+
+  # Attach these labels to any time series or alerts when communicating with
+  # external systems (federation, remote storage, Alertmanager).
+  external_labels:
+    monitor: "codelab-monitor"
+
+# A scrape configuration containing exactly one endpoint to scrape:
+# Here it's Prometheus itself.
+scrape_configs:
+  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
+  - job_name: "prometheus"
+
+    # Override the global default and scrape targets from this job every 5 seconds.
+    scrape_interval: 5s
+
+    static_configs:
+      - targets: ["localhost:9090"]
+  - job_name: "apisix"
+    metrics_path: "/apisix/prometheus/metrics"
+    static_configs:
+      - targets: ["192.17.5.11:9080"]
diff --git a/config/config.ts b/config/config.ts
new file mode 100644
index 0000000..b332ab4
--- /dev/null
+++ b/config/config.ts
@@ -0,0 +1,48 @@
+import { defineConfig } from 'umi';
+
+import defaultSettings from './defaultSettings';
+import proxy from './proxy';
+import routes from './routes';
+
+const { REACT_APP_ENV } = process.env;
+
+export default defineConfig({
+  hash: true,
+  antd: {},
+  dva: {
+    hmr: true,
+  },
+  locale: {
+    default: 'zh-CN',
+    antd: true,
+    baseNavigator: true,
+  },
+  dynamicImport: {
+    loading: '@/components/PageLoading/index',
+  },
+  targets: {
+    ie: 11,
+  },
+  routes,
+  layout: {
+    name: 'APISIX Dashboard',
+    locale: true,
+    logo: '/favicon.png',
+  },
+  base: '/dashboard/',
+  publicPath: '/',
+  define: {
+    REACT_APP_ENV: REACT_APP_ENV || false,
+  },
+  // Theme for antd: https://ant.design/docs/react/customize-theme-cn
+  theme: {
+    'primary-color': defaultSettings.primaryColor,
+  },
+  // @ts-ignore
+  title: false,
+  ignoreMomentLocale: true,
+  proxy: proxy[REACT_APP_ENV || 'dev'],
+  manifest: {
+    basePath: '/',
+  },
+});
diff --git a/config/defaultSettings.ts b/config/defaultSettings.ts
new file mode 100644
index 0000000..0245a76
--- /dev/null
+++ b/config/defaultSettings.ts
@@ -0,0 +1,20 @@
+import { Settings as LayoutSettings } from '@ant-design/pro-layout';
+
+export default {
+  navTheme: 'light',
+  primaryColor: '#1890ff',
+  layout: 'mix',
+  contentWidth: 'Fluid',
+  fixedHeader: false,
+  autoHideHeader: false,
+  fixSiderbar: false,
+  colorWeak: false,
+  menu: {
+    locale: true,
+  },
+  title: 'APISIX Dashboard',
+  pwa: false,
+  iconfontUrl: '',
+} as LayoutSettings & {
+  pwa: boolean;
+};
diff --git a/config/proxy.ts b/config/proxy.ts
new file mode 100644
index 0000000..e189b0f
--- /dev/null
+++ b/config/proxy.ts
@@ -0,0 +1,30 @@
+/**
+ * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
+ * The agent cannot take effect in the production environment
+ * so there is no configuration of the production environment
+ * For details, please see
+ * https://pro.ant.design/docs/deploy
+ */
+export default {
+  dev: {
+    '/api/': {
+      target: 'https://apisix.iresty.com/apisix/admin/',
+      changeOrigin: true,
+      pathRewrite: { '^/api': '' },
+    },
+  },
+  test: {
+    '/api/': {
+      target: 'https://preview.pro.ant.design',
+      changeOrigin: true,
+      pathRewrite: { '^': '' },
+    },
+  },
+  pre: {
+    '/api/': {
+      target: 'your pre url',
+      changeOrigin: true,
+      pathRewrite: { '^': '' },
+    },
+  },
+};
diff --git a/config/routes.ts b/config/routes.ts
new file mode 100644
index 0000000..75bee66
--- /dev/null
+++ b/config/routes.ts
@@ -0,0 +1,76 @@
+const routes = [
+  {
+    path: '/',
+    redirect: '/ssl',
+  },
+  {
+    name: 'metrics',
+    path: '/metrics',
+    component: './Metrics/Metrics',
+    icon: 'AreaChartOutlined',
+  },
+  {
+    name: 'setting',
+    path: '/setting',
+    component: './Setting',
+    layout: false,
+    hideInMenu: true,
+  },
+  {
+    name: 'ssl',
+    path: '/ssl',
+    icon: 'BarsOutlined',
+    routes: [
+      {
+        path: '/ssl',
+        redirect: '/ssl/list',
+      },
+      {
+        path: '/ssl/list',
+        name: 'list',
+        component: './ssl/List',
+        hideInMenu: true,
+      },
+      {
+        name: 'create',
+        path: '/ssl/create',
+        component: './ssl/Create',
+        hideInMenu: true,
+      },
+    ],
+  },
+  {
+    name: 'routes',
+    path: '/routes',
+    icon: 'BarsOutlined',
+    routes: [
+      {
+        path: '/routes',
+        redirect: '/routes/list',
+      },
+      {
+        path: '/routes/list',
+        name: 'list',
+        icon: 'BarsOutlined',
+        component: './Routes/List',
+      },
+      {
+        path: '/routes/create',
+        name: 'create',
+        component: './Routes/Create',
+        hideInMenu: true,
+      },
+      {
+        path: '/routes/:rid/edit',
+        name: 'edit',
+        component: './Routes/Create',
+        hideInMenu: true,
+      },
+    ],
+  },
+  {
+    component: './404',
+  },
+];
+
+export default routes;
diff --git a/docker/nginx.conf b/docker/nginx.conf
new file mode 100644
index 0000000..40c19cb
--- /dev/null
+++ b/docker/nginx.conf
@@ -0,0 +1,21 @@
+server {
+    listen 80;
+    # gzip config
+    gzip on;
+    gzip_min_length 1k;
+    gzip_comp_level 9;
+    gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
+    gzip_vary on;
+    gzip_disable "MSIE [1-6]\.";
+
+    root /usr/share/nginx/html;
+    include /etc/nginx/mime.types;
+
+    location / {
+        rewrite ^/  /dashboard$uri redirect;
+    }
+
+    location /dashboard {
+        try_files $uri $uri/ /index.html;
+    }
+}
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..4c4eeaf
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  testURL: 'http://localhost:8000',
+  testEnvironment: './tests/PuppeteerEnvironment',
+  verbose: false,
+  globals: {
+    ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
+    localStorage: null,
+  },
+};
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000..f87334d
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}
diff --git a/mock/notices.ts b/mock/notices.ts
new file mode 100644
index 0000000..b9e3bf2
--- /dev/null
+++ b/mock/notices.ts
@@ -0,0 +1,105 @@
+import { Request, Response } from 'express';
+
+const getNotices = (req: Request, res: Response) => {
+  res.json([
+    {
+      id: '000000001',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+      title: '你收到了 14 份新周报',
+      datetime: '2017-08-09',
+      type: 'notification',
+    },
+    {
+      id: '000000002',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
+      title: '你推荐的 曲妮妮 已通过第三轮面试',
+      datetime: '2017-08-08',
+      type: 'notification',
+    },
+    {
+      id: '000000003',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
+      title: '这种模板可以区分多种通知类型',
+      datetime: '2017-08-07',
+      read: true,
+      type: 'notification',
+    },
+    {
+      id: '000000004',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
+      title: '左侧图标用于区分不同的类型',
+      datetime: '2017-08-07',
+      type: 'notification',
+    },
+    {
+      id: '000000005',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+      title: '内容不要超过两行字,超出时自动截断',
+      datetime: '2017-08-07',
+      type: 'notification',
+    },
+    {
+      id: '000000006',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+      title: '曲丽丽 评论了你',
+      description: '描述信息描述信息描述信息',
+      datetime: '2017-08-07',
+      type: 'message',
+      clickClose: true,
+    },
+    {
+      id: '000000007',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+      title: '朱偏右 回复了你',
+      description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+      datetime: '2017-08-07',
+      type: 'message',
+      clickClose: true,
+    },
+    {
+      id: '000000008',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+      title: '标题',
+      description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+      datetime: '2017-08-07',
+      type: 'message',
+      clickClose: true,
+    },
+    {
+      id: '000000009',
+      title: '任务名称',
+      description: '任务需要在 2017-01-12 20:00 前启动',
+      extra: '未开始',
+      status: 'todo',
+      type: 'event',
+    },
+    {
+      id: '000000010',
+      title: '第三方紧急代码变更',
+      description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+      extra: '马上到期',
+      status: 'urgent',
+      type: 'event',
+    },
+    {
+      id: '000000011',
+      title: '信息安全考试',
+      description: '指派竹尔于 2017-01-09 前完成更新并发布',
+      extra: '已耗时 8 天',
+      status: 'doing',
+      type: 'event',
+    },
+    {
+      id: '000000012',
+      title: 'ABCD 版本发布',
+      description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+      extra: '进行中',
+      status: 'processing',
+      type: 'event',
+    },
+  ]);
+};
+
+export default {
+  'GET /api/notices': getNotices,
+};
diff --git a/mock/route.ts b/mock/route.ts
new file mode 100644
index 0000000..418d10f
--- /dev/null
+++ b/mock/route.ts
@@ -0,0 +1,5 @@
+export default {
+  '/api/auth_routes': {
+    '/form/advanced-form': { authority: ['admin', 'user'] },
+  },
+};
diff --git a/mock/user.ts b/mock/user.ts
new file mode 100644
index 0000000..24fa3f7
--- /dev/null
+++ b/mock/user.ts
@@ -0,0 +1,154 @@
+import { Request, Response } from 'express';
+
+function getFakeCaptcha(req: Request, res: Response) {
+  return res.json('captcha-xxx');
+}
+// 代码中会兼容本地 service mock 以及部署站点的静态数据
+export default {
+  // 支持值为 Object 和 Array
+  'GET /api/currentUser': {
+    name: 'Serati Ma',
+    avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+    userid: '00000001',
+    email: 'antdesign@alipay.com',
+    signature: '海纳百川,有容乃大',
+    title: '交互专家',
+    group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
+    tags: [
+      {
+        key: '0',
+        label: '很有想法的',
+      },
+      {
+        key: '1',
+        label: '专注设计',
+      },
+      {
+        key: '2',
+        label: '辣~',
+      },
+      {
+        key: '3',
+        label: '大长腿',
+      },
+      {
+        key: '4',
+        label: '川妹子',
+      },
+      {
+        key: '5',
+        label: '海纳百川',
+      },
+    ],
+    notifyCount: 12,
+    unreadCount: 11,
+    country: 'China',
+    geographic: {
+      province: {
+        label: '浙江省',
+        key: '330000',
+      },
+      city: {
+        label: '杭州市',
+        key: '330100',
+      },
+    },
+    address: '西湖区工专路 77 号',
+    phone: '0752-268888888',
+  },
+  // GET POST 可省略
+  'GET /api/users': [
+    {
+      key: '1',
+      name: 'John Brown',
+      age: 32,
+      address: 'New York No. 1 Lake Park',
+    },
+    {
+      key: '2',
+      name: 'Jim Green',
+      age: 42,
+      address: 'London No. 1 Lake Park',
+    },
+    {
+      key: '3',
+      name: 'Joe Black',
+      age: 32,
+      address: 'Sidney No. 1 Lake Park',
+    },
+  ],
+  'POST /api/login/account': (req: Request, res: Response) => {
+    const { password, userName, type } = req.body;
+    if (password === 'ant.design' && userName === 'admin') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'admin',
+      });
+      return;
+    }
+    if (password === 'ant.design' && userName === 'user') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'user',
+      });
+      return;
+    }
+    if (type === 'mobile') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'admin',
+      });
+      return;
+    }
+
+    res.send({
+      status: 'error',
+      type,
+      currentAuthority: 'guest',
+    });
+  },
+  'POST /api/register': (req: Request, res: Response) => {
+    res.send({ status: 'ok', currentAuthority: 'user' });
+  },
+  'GET /api/500': (req: Request, res: Response) => {
+    res.status(500).send({
+      timestamp: 1513932555104,
+      status: 500,
+      error: 'error',
+      message: 'error',
+      path: '/base/category/list',
+    });
+  },
+  'GET /api/404': (req: Request, res: Response) => {
+    res.status(404).send({
+      timestamp: 1513932643431,
+      status: 404,
+      error: 'Not Found',
+      message: 'No message available',
+      path: '/base/category/list/2121212',
+    });
+  },
+  'GET /api/403': (req: Request, res: Response) => {
+    res.status(403).send({
+      timestamp: 1513932555104,
+      status: 403,
+      error: 'Unauthorized',
+      message: 'Unauthorized',
+      path: '/base/category/list',
+    });
+  },
+  'GET /api/401': (req: Request, res: Response) => {
+    res.status(401).send({
+      timestamp: 1513932555104,
+      status: 401,
+      error: 'Unauthorized',
+      message: 'Unauthorized',
+      path: '/base/category/list',
+    });
+  },
+
+  'GET  /api/login/captcha': getFakeCaptcha,
+};
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 0000000..89a3866
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,13 @@
+[build]
+  publish = "dist/"
+
+[[redirects]]
+  from = "/api/*"
+  to = "https://apisix.iresty.com/apisix/admin/:splat"
+  status = 200
+  force = true
+
+[[redirects]]
+  from = "/*"
+  to = "/index.html"
+  status = 200 
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2dd0b40
--- /dev/null
+++ b/package.json
@@ -0,0 +1,109 @@
+{
+  "name": "apisix-dashboard",
+  "version": "1.0.2",
+  "private": true,
+  "description": "Dashboard for APISIX",
+  "scripts": {
+    "analyze": "cross-env ANALYZE=1 umi build",
+    "build": "umi build",
+    "dev": "npm run start:dev",
+    "fetch:blocks": "pro fetch-blocks --branch antd@4 && npm run prettier",
+    "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
+    "postinstall": "umi g tmp",
+    "lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier",
+    "lint-staged": "lint-staged",
+    "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
+    "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
+    "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
+    "lint:prettier": "prettier --check \"**/*\" --end-of-line auto",
+    "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
+    "prettier": "prettier -c --write \"**/*\"",
+    "site": "npm run fetch:blocks && npm run build",
+    "start": "umi dev",
+    "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none umi dev",
+    "start:no-mock": "cross-env MOCK=none umi dev",
+    "start:no-ui": "cross-env UMI_UI=none umi dev",
+    "start:pre": "cross-env REACT_APP_ENV=pre umi dev",
+    "start:test": "cross-env REACT_APP_ENV=test MOCK=none umi dev",
+    "pretest": "node ./tests/beforeTest",
+    "test": "umi test",
+    "test:all": "node ./tests/run-tests.js",
+    "test:component": "umi test ./src/components",
+    "tsc": "tsc"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "npm run lint-staged"
+    }
+  },
+  "lint-staged": {
+    "**/*.less": "stylelint --syntax less",
+    "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
+    "**/*.{js,jsx,tsx,ts,less,md,json}": [
+      "prettier --write"
+    ]
+  },
+  "dependencies": {
+    "@ant-design/icons": "^4.2.1",
+    "@ant-design/pro-layout": "6.0.0-2",
+    "@ant-design/pro-table": "^2.3.3",
+    "antd": "^4.3.3",
+    "classnames": "^2.2.6",
+    "lodash": "^4.17.15",
+    "moment": "^2.25.3",
+    "nzh": "^1.0.3",
+    "omit.js": "^1.0.2",
+    "path-to-regexp": "2.4.0",
+    "qs": "^6.9.0",
+    "react": "^16.8.6",
+    "react-dom": "^16.8.6",
+    "umi": "^3.1.0",
+    "umi-request": "^1.3.3",
+    "use-merge-value": "^1.0.1",
+    "uuid": "^7.0.2"
+  },
+  "devDependencies": {
+    "@ant-design/pro-cli": "^2.0.2",
+    "@types/classnames": "^2.2.7",
+    "@types/express": "^4.17.0",
+    "@types/history": "^4.7.2",
+    "@types/jest": "^25.1.0",
+    "@types/lodash": "^4.14.144",
+    "@types/node-forge": "^0.9.3",
+    "@types/qs": "^6.5.3",
+    "@types/react": "^16.9.17",
+    "@types/react-dom": "^16.8.4",
+    "@types/react-helmet": "^5.0.13",
+    "@types/uuid": "^7.0.0",
+    "@umijs/fabric": "^2.0.5",
+    "@umijs/plugin-blocks": "^2.0.5",
+    "@umijs/plugin-esbuild": "^1.0.0-beta.2",
+    "@umijs/preset-ant-design-pro": "^1.2.0",
+    "@umijs/preset-react": "^1.4.24",
+    "@umijs/preset-ui": "^2.1.11",
+    "carlo": "^0.9.46",
+    "cross-env": "^7.0.0",
+    "cross-port-killer": "^1.1.1",
+    "detect-installer": "^1.0.1",
+    "eslint": "^6.8.0",
+    "express": "^4.17.1",
+    "husky": "^4.0.7",
+    "lint-staged": "^10.0.0",
+    "mockjs": "^1.0.1-beta3",
+    "prettier": "^2.0.1",
+    "pro-download": "1.0.1",
+    "puppeteer-core": "^2.1.1",
+    "react-helmet-async": "^1.0.6",
+    "stylelint": "^13.0.0"
+  },
+  "engines": {
+    "node": ">=10.0.0"
+  },
+  "checkFiles": [
+    "src/**/*.js*",
+    "src/**/*.ts*",
+    "src/**/*.less",
+    "config/**/*.js*",
+    "scripts/**/*.js"
+  ]
+}
diff --git a/public/favicon.png b/public/favicon.png
new file mode 100644
index 0000000..381ab08
Binary files /dev/null and b/public/favicon.png differ
diff --git a/public/home_bg.png b/public/home_bg.png
new file mode 100644
index 0000000..7c92a4b
Binary files /dev/null and b/public/home_bg.png differ
diff --git a/public/icons/icon-128x128.png b/public/icons/icon-128x128.png
new file mode 100644
index 0000000..48d0e23
Binary files /dev/null and b/public/icons/icon-128x128.png differ
diff --git a/public/icons/icon-192x192.png b/public/icons/icon-192x192.png
new file mode 100644
index 0000000..938e9b5
Binary files /dev/null and b/public/icons/icon-192x192.png differ
diff --git a/public/icons/icon-512x512.png b/public/icons/icon-512x512.png
new file mode 100644
index 0000000..21fc108
Binary files /dev/null and b/public/icons/icon-512x512.png differ
diff --git a/public/pro_icon.svg b/public/pro_icon.svg
new file mode 100644
index 0000000..2c24ec7
--- /dev/null
+++ b/public/pro_icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="图层_1" width="512" height="512" x="0" y="0" enable-background="new 0 0 512 512" version="1.1" viewBox="0 0 512 512" xml:space="preserve"><path fill-rule="evenodd" d="M259.119,233.588c0-3.644,0.041-7.289-0.008-10.932	c-0.111-8.558-4.697-13.308-13.231-13.486c-6.658-0.139-13.326,0.12-19.98-0.096c-3.292-0.107-4.247,0.995-4.24,4.266	c0.094,44.794,0.101,89.589-0.008,134.383c-0.009,3.492,1.346,4.154,4.407,4.11 [...]
\ No newline at end of file
diff --git a/src/access.ts b/src/access.ts
new file mode 100644
index 0000000..207bc4b
--- /dev/null
+++ b/src/access.ts
@@ -0,0 +1,6 @@
+export default function (initialState: { currentUser?: API.CurrentUser | undefined }) {
+  const { currentUser } = initialState || {};
+  return {
+    canAdmin: currentUser && currentUser.access === 'admin',
+  };
+}
diff --git a/src/app.tsx b/src/app.tsx
new file mode 100644
index 0000000..1a38f9c
--- /dev/null
+++ b/src/app.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { notification } from 'antd';
+import { RequestConfig, history } from 'umi';
+import { BasicLayoutProps, Settings as LayoutSettings } from '@ant-design/pro-layout';
+
+import { getSetting } from '@/pages/Setting';
+import RightContent from '@/components/RightContent';
+import Footer from '@/components/Footer';
+import { queryCurrent } from '@/services/user';
+import defaultSettings from '../config/defaultSettings';
+
+export async function getInitialState(): Promise<{
+  currentUser?: API.CurrentUser;
+  settings?: LayoutSettings;
+}> {
+  // 如果是设置页面,不执行
+  if (history.location.pathname !== '/setting') {
+    try {
+      const currentUser = await queryCurrent();
+      return {
+        currentUser,
+        settings: defaultSettings,
+      };
+    } catch (error) {
+      history.push('/setting');
+    }
+  }
+  return {
+    settings: defaultSettings,
+  };
+}
+
+export const layout = ({
+  initialState,
+}: {
+  initialState: { settings?: LayoutSettings };
+}): BasicLayoutProps => {
+  return {
+    rightContentRender: () => <RightContent />,
+    disableContentMargin: false,
+    footerRender: () => <Footer />,
+    menuHeaderRender: undefined,
+    ...initialState?.settings,
+  };
+};
+
+const codeMessage = {
+  200: '服务器成功返回请求的数据。',
+  201: '新建或修改数据成功。',
+  202: '一个请求已经进入后台排队(异步任务)。',
+  204: '删除数据成功。',
+  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+  401: '用户没有权限(令牌、用户名、密码错误)。',
+  403: '用户得到授权,但是访问是被禁止的。',
+  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+  406: '请求的格式不可得。',
+  410: '请求的资源被永久删除,且不会再得到的。',
+  422: '当创建一个对象时,发生一个验证错误。',
+  500: '服务器发生错误,请检查服务器。',
+  502: '网关错误。',
+  503: '服务不可用,服务器暂时过载或维护。',
+  504: '网关超时。',
+};
+
+/**
+ * 异常处理程序
+ */
+const errorHandler = (error: { response: Response; data: any }): Promise<Response> => {
+  const { response } = error;
+  if (response && response.status) {
+    const errorText =
+      error.data.msg || error.data.message || error.data.error_msg || codeMessage[response.status];
+
+    notification.error({
+      message: `请求错误,错误码: ${error.data.errorCode || response.status}`,
+      description: errorText,
+    });
+  } else if (!response) {
+    notification.error({
+      description: '您的网络发生异常,无法连接服务器',
+      message: '网络异常',
+    });
+  }
+  return Promise.reject(response);
+};
+
+const { baseURL } = getSetting();
+export const request: RequestConfig = {
+  prefix: baseURL,
+  errorHandler,
+  credentials: 'same-origin',
+};
diff --git a/src/assets/logo.svg b/src/assets/logo.svg
new file mode 100644
index 0000000..ef5ec53
--- /dev/null
+++ b/src/assets/logo.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <linearGradient id="id0" gradientUnits="userSpaceOnUse" x1="25119.8" y1="11052.5" x2="21725.7" y2="21551.7" gradientTransform="matrix(0.028095, 0, 0, 0.028095, -492.867096, -144.769821)">
+      <stop offset="0" style="stop-color:#A92F33"/>
+      <stop offset="1" style="stop-color:#E62129"/>
+    </linearGradient>
+    <linearGradient id="id1" gradientUnits="userSpaceOnUse" x1="27026.6" y1="8021.8" x2="30514.6" y2="16218.5" gradientTransform="matrix(0.028095, 0, 0, 0.028095, -492.867096, -144.769821)">
+      <stop offset="0" style="stop-color:#A92F33"/>
+      <stop offset="1" style="stop-color:#E8443F"/>
+    </linearGradient>
+    <linearGradient id="id2" gradientUnits="userSpaceOnUse" x1="23046.1" y1="14340.2" x2="26713.9" y2="9900.07" gradientTransform="matrix(0.028095, 0, 0, 0.028095, -492.867096, -144.769821)">
+      <stop offset="0" style="stop-color:#E62129"/>
+      <stop offset="1" style="stop-color:#E8443F"/>
+    </linearGradient>
+  </defs>
+  <path class="fil1" d="M 156.005 337.641 L 247.284 205.232 L 218.515 160.195 L 134.429 259.986 C 134.429 259.986 134.429 259.986 134.429 259.986 C 134.429 259.986 134.429 259.986 134.429 259.986 C 106.503 293.139 102.007 301.399 78.689 340.281 L 156.033 337.641 Z" style="fill: url(#id0);"/>
+  <path class="fil2" d="M 382.616 340.281 L 419.616 340.141 L 253.325 51.188 L 253.325 51.188 L 333.619 340.281 L 382.644 340.281 Z M 253.297 51.188 L 211.239 120.694 L 219.695 106.732 L 253.297 51.216 Z" style="fill: url(#id1);"/>
+  <polygon class="fil3" points="218.487 160.195 333.59 340.281 253.297 51.188 253.297 51.188 253.297 51.188 219.724 106.703 78.661 340.281" style="fill: url(#id2);"/>
+  <path d="M 116.292 266.849 L 116.292 278.269 L 111.252 278.269 L 111.252 277.099 C 111.119 277.392 110.855 277.659 110.462 277.899 C 110.062 278.146 109.702 278.269 109.382 278.269 L 95.732 278.269 C 95.365 278.269 94.995 278.176 94.622 277.989 C 94.249 277.796 93.945 277.526 93.712 277.179 C 93.479 276.839 93.359 276.452 93.352 276.019 C 93.345 275.192 93.335 274.146 93.322 272.879 C 93.309 271.612 93.302 270.946 93.302 270.879 C 93.302 269.932 93.555 269.189 94.062 268.649 C 94.562 2 [...]
+</svg>
\ No newline at end of file
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
new file mode 100644
index 0000000..0f6648b
--- /dev/null
+++ b/src/components/Footer/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { GithubOutlined } from '@ant-design/icons';
+import { DefaultFooter } from '@ant-design/pro-layout';
+
+export default () => (
+  <DefaultFooter
+    copyright="2020 Apache APISIX"
+    links={[
+      {
+        key: 'GitHub',
+        title: <GithubOutlined />,
+        href: 'https://github.com/apache/incubator-apisix',
+        blankTarget: true,
+      },
+    ]}
+  />
+);
diff --git a/src/components/HeaderDropdown/index.less b/src/components/HeaderDropdown/index.less
new file mode 100644
index 0000000..004b53e
--- /dev/null
+++ b/src/components/HeaderDropdown/index.less
@@ -0,0 +1,16 @@
+@import '~antd/es/style/themes/default.less';
+
+.container > * {
+  background-color: @popover-bg;
+  border-radius: 4px;
+  box-shadow: @shadow-1-down;
+}
+
+@media screen and (max-width: @screen-xs) {
+  .container {
+    width: 100% !important;
+  }
+  .container > * {
+    border-radius: 0 !important;
+  }
+}
diff --git a/src/components/HeaderDropdown/index.tsx b/src/components/HeaderDropdown/index.tsx
new file mode 100644
index 0000000..cc60727
--- /dev/null
+++ b/src/components/HeaderDropdown/index.tsx
@@ -0,0 +1,19 @@
+import { DropDownProps } from 'antd/es/dropdown';
+import { Dropdown } from 'antd';
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+declare type OverlayFunc = () => React.ReactNode;
+
+export interface HeaderDropdownProps extends Omit<DropDownProps, 'overlay'> {
+  overlayClassName?: string;
+  overlay: React.ReactNode | OverlayFunc | any;
+  placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
+}
+
+const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
+  <Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
+);
+
+export default HeaderDropdown;
diff --git a/src/components/NoticeIcon/NoticeList.less b/src/components/NoticeIcon/NoticeList.less
new file mode 100755
index 0000000..1aba610
--- /dev/null
+++ b/src/components/NoticeIcon/NoticeList.less
@@ -0,0 +1,103 @@
+@import '~antd/es/style/themes/default.less';
+
+.list {
+  max-height: 400px;
+  overflow: auto;
+  &::-webkit-scrollbar {
+    display: none;
+  }
+  .item {
+    padding-right: 24px;
+    padding-left: 24px;
+    overflow: hidden;
+    cursor: pointer;
+    transition: all 0.3s;
+
+    .meta {
+      width: 100%;
+    }
+
+    .avatar {
+      margin-top: 4px;
+      background: #fff;
+    }
+    .iconElement {
+      font-size: 32px;
+    }
+
+    &.read {
+      opacity: 0.4;
+    }
+    &:last-child {
+      border-bottom: 0;
+    }
+    &:hover {
+      background: @primary-1;
+    }
+    .title {
+      margin-bottom: 8px;
+      font-weight: normal;
+    }
+    .description {
+      font-size: 12px;
+      line-height: @line-height-base;
+    }
+    .datetime {
+      margin-top: 4px;
+      font-size: 12px;
+      line-height: @line-height-base;
+    }
+    .extra {
+      float: right;
+      margin-top: -1.5px;
+      margin-right: 0;
+      color: @text-color-secondary;
+      font-weight: normal;
+    }
+  }
+  .loadMore {
+    padding: 8px 0;
+    color: @primary-6;
+    text-align: center;
+    cursor: pointer;
+    &.loadedAll {
+      color: rgba(0, 0, 0, 0.25);
+      cursor: unset;
+    }
+  }
+}
+
+.notFound {
+  padding: 73px 0 88px;
+  color: @text-color-secondary;
+  text-align: center;
+  img {
+    display: inline-block;
+    height: 76px;
+    margin-bottom: 16px;
+  }
+}
+
+.bottomBar {
+  height: 46px;
+  color: @text-color;
+  line-height: 46px;
+  text-align: center;
+  border-top: 1px solid @border-color-split;
+  border-radius: 0 0 @border-radius-base @border-radius-base;
+  transition: all 0.3s;
+  div {
+    display: inline-block;
+    width: 50%;
+    cursor: pointer;
+    transition: all 0.3s;
+    user-select: none;
+
+    &:only-child {
+      width: 100%;
+    }
+    &:not(:only-child):last-child {
+      border-left: 1px solid @border-color-split;
+    }
+  }
+}
diff --git a/src/components/NoticeIcon/NoticeList.tsx b/src/components/NoticeIcon/NoticeList.tsx
new file mode 100644
index 0000000..8d00be2
--- /dev/null
+++ b/src/components/NoticeIcon/NoticeList.tsx
@@ -0,0 +1,113 @@
+import { Avatar, List } from 'antd';
+
+import React from 'react';
+import classNames from 'classnames';
+import styles from './NoticeList.less';
+
+export interface NoticeIconTabProps {
+  count?: number;
+  name?: string;
+  showClear?: boolean;
+  showViewMore?: boolean;
+  style?: React.CSSProperties;
+  title: string;
+  tabKey: string;
+  data?: API.NoticeIconData[];
+  onClick?: (item: API.NoticeIconData) => void;
+  onClear?: () => void;
+  emptyText?: string;
+  clearText?: string;
+  viewMoreText?: string;
+  list: API.NoticeIconData[];
+  onViewMore?: (e: any) => void;
+}
+const NoticeList: React.SFC<NoticeIconTabProps> = ({
+  data = [],
+  onClick,
+  onClear,
+  title,
+  onViewMore,
+  emptyText,
+  showClear = true,
+  clearText,
+  viewMoreText,
+  showViewMore = false,
+}) => {
+  if (data.length === 0) {
+    return (
+      <div className={styles.notFound}>
+        <img
+          src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
+          alt="not found"
+        />
+        <div>{emptyText}</div>
+      </div>
+    );
+  }
+  return (
+    <div>
+      <List<NoticeIconData>
+        className={styles.list}
+        dataSource={data}
+        renderItem={(item, i) => {
+          const itemCls = classNames(styles.item, {
+            [styles.read]: item.read,
+          });
+          // eslint-disable-next-line no-nested-ternary
+          const leftIcon = item.avatar ? (
+            typeof item.avatar === 'string' ? (
+              <Avatar className={styles.avatar} src={item.avatar} />
+            ) : (
+              <span className={styles.iconElement}>{item.avatar}</span>
+            )
+          ) : null;
+
+          return (
+            <List.Item
+              className={itemCls}
+              key={item.key || i}
+              onClick={() => onClick && onClick(item)}
+            >
+              <List.Item.Meta
+                className={styles.meta}
+                avatar={leftIcon}
+                title={
+                  <div className={styles.title}>
+                    {item.title}
+                    <div className={styles.extra}>{item.extra}</div>
+                  </div>
+                }
+                description={
+                  <div>
+                    <div className={styles.description}>{item.description}</div>
+                    <div className={styles.datetime}>{item.datetime}</div>
+                  </div>
+                }
+              />
+            </List.Item>
+          );
+        }}
+      />
+      <div className={styles.bottomBar}>
+        {showClear ? (
+          <div onClick={onClear}>
+            {clearText} {title}
+          </div>
+        ) : null}
+        {showViewMore ? (
+          <div
+            onClick={(e) => {
+              if (onViewMore) {
+                onViewMore(e);
+              }
+            }}
+          >
+            {viewMoreText}
+          </div>
+        ) : null}
+      </div>
+    </div>
+  );
+};
+
+export default NoticeList;
diff --git a/src/components/NoticeIcon/index.less b/src/components/NoticeIcon/index.less
new file mode 100644
index 0000000..650ccd2
--- /dev/null
+++ b/src/components/NoticeIcon/index.less
@@ -0,0 +1,31 @@
+@import '~antd/es/style/themes/default.less';
+
+.popover {
+  position: relative;
+  width: 336px;
+}
+
+.noticeButton {
+  display: inline-block;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+.icon {
+  padding: 4px;
+  vertical-align: middle;
+}
+
+.badge {
+  font-size: 16px;
+}
+
+.tabs {
+  :global {
+    .ant-tabs-nav-scroll {
+      text-align: center;
+    }
+    .ant-tabs-bar {
+      margin-bottom: 0;
+    }
+  }
+}
diff --git a/src/components/NoticeIcon/index.tsx b/src/components/NoticeIcon/index.tsx
new file mode 100644
index 0000000..0df4061
--- /dev/null
+++ b/src/components/NoticeIcon/index.tsx
@@ -0,0 +1,124 @@
+import { BellOutlined } from '@ant-design/icons';
+import { Badge, Spin, Tabs } from 'antd';
+import useMergeValue from 'use-merge-value';
+import React from 'react';
+import classNames from 'classnames';
+import NoticeList, { NoticeIconTabProps } from './NoticeList';
+
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+const { TabPane } = Tabs;
+
+export interface NoticeIconProps {
+  count?: number;
+  bell?: React.ReactNode;
+  className?: string;
+  loading?: boolean;
+  onClear?: (tabName: string, tabKey: string) => void;
+  onItemClick?: (item: API.NoticeIconData, tabProps: NoticeIconTabProps) => void;
+  onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
+  onTabChange?: (tabTile: string) => void;
+  style?: React.CSSProperties;
+  onPopupVisibleChange?: (visible: boolean) => void;
+  popupVisible?: boolean;
+  clearText?: string;
+  viewMoreText?: string;
+  clearClose?: boolean;
+  emptyImage?: string;
+  children: React.ReactElement<NoticeIconTabProps>[];
+}
+
+const NoticeIcon: React.FC<NoticeIconProps> & {
+  Tab: typeof NoticeList;
+} = (props) => {
+  const getNotificationBox = (): React.ReactNode => {
+    const {
+      children,
+      loading,
+      onClear,
+      onTabChange,
+      onItemClick,
+      onViewMore,
+      clearText,
+      viewMoreText,
+    } = props;
+    if (!children) {
+      return null;
+    }
+    const panes: React.ReactNode[] = [];
+    React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => {
+      if (!child) {
+        return;
+      }
+      const { list, title, count, tabKey, showClear, showViewMore } = child.props;
+      const len = list && list.length ? list.length : 0;
+      const msgCount = count || count === 0 ? count : len;
+      const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
+      panes.push(
+        <TabPane tab={tabTitle} key={tabKey}>
+          <NoticeList
+            clearText={clearText}
+            viewMoreText={viewMoreText}
+            data={list}
+            onClear={(): void => onClear && onClear(title, tabKey)}
+            onClick={(item): void => onItemClick && onItemClick(item, child.props)}
+            onViewMore={(event): void => onViewMore && onViewMore(child.props, event)}
+            showClear={showClear}
+            showViewMore={showViewMore}
+            title={title}
+            {...child.props}
+          />
+        </TabPane>,
+      );
+    });
+    return (
+      <Spin spinning={loading} delay={300}>
+        <Tabs className={styles.tabs} onChange={onTabChange}>
+          {panes}
+        </Tabs>
+      </Spin>
+    );
+  };
+
+  const { className, count, bell } = props;
+
+  const [visible, setVisible] = useMergeValue<boolean>(false, {
+    value: props.popupVisible,
+    onChange: props.onPopupVisibleChange,
+  });
+  const noticeButtonClass = classNames(className, styles.noticeButton);
+  const notificationBox = getNotificationBox();
+  const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />;
+  const trigger = (
+    <span className={classNames(noticeButtonClass, { opened: visible })}>
+      <Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
+        {NoticeBellIcon}
+      </Badge>
+    </span>
+  );
+  if (!notificationBox) {
+    return trigger;
+  }
+
+  return (
+    <HeaderDropdown
+      placement="bottomRight"
+      overlay={notificationBox}
+      overlayClassName={styles.popover}
+      trigger={['click']}
+      visible={visible}
+      onVisibleChange={setVisible}
+    >
+      {trigger}
+    </HeaderDropdown>
+  );
+};
+
+NoticeIcon.defaultProps = {
+  emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
+};
+
+NoticeIcon.Tab = NoticeList;
+
+export default NoticeIcon;
diff --git a/src/components/PageLoading/index.tsx b/src/components/PageLoading/index.tsx
new file mode 100644
index 0000000..096c58f
--- /dev/null
+++ b/src/components/PageLoading/index.tsx
@@ -0,0 +1,5 @@
+import { PageLoading } from '@ant-design/pro-layout';
+
+// loading components from code split
+// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
+export default PageLoading;
diff --git a/src/components/PluginForm/PluginForm.tsx b/src/components/PluginForm/PluginForm.tsx
new file mode 100644
index 0000000..5b69bec
--- /dev/null
+++ b/src/components/PluginForm/PluginForm.tsx
@@ -0,0 +1,192 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Input, Switch, Select, InputNumber, Button } from 'antd';
+import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
+import { useIntl } from 'umi';
+
+import { fetchPluginSchema } from './service';
+import { transformPropertyToRules } from './transformer';
+
+const formLayout = {
+  labelCol: { span: 10 },
+  wrapperCol: { span: 14 },
+};
+
+interface RenderComponentProps {
+  placeholder?: string;
+  disabled?: boolean;
+}
+
+const renderComponentByProperty = (
+  propertyValue: PluginForm.PluginProperty,
+  restProps?: RenderComponentProps,
+) => {
+  const { type, minimum, maximum } = propertyValue;
+
+  if (type === 'string') {
+    if (propertyValue.enum) {
+      return (
+        <Select disabled={restProps?.disabled}>
+          {propertyValue.enum.map((enumValue) => (
+            <Select.Option value={enumValue} key={enumValue}>
+              {enumValue}
+            </Select.Option>
+          ))}
+        </Select>
+      );
+    }
+    return <Input {...restProps} />;
+  }
+
+  if (type === 'boolean') {
+    return <Switch {...restProps} />;
+  }
+
+  if (type === 'number' || type === 'integer') {
+    return (
+      <InputNumber
+        min={minimum ?? Number.MIN_SAFE_INTEGER}
+        max={maximum ?? Number.MAX_SAFE_INTEGER}
+        {...restProps}
+      />
+    );
+  }
+
+  return <Input {...restProps} />;
+};
+
+interface ArrayComponentProps {
+  disabled?: boolean;
+  schema: PluginForm.PluginSchema;
+  propertyName: string;
+  propertyValue: PluginForm.PluginProperty;
+}
+
+const ArrayComponent: React.FC<ArrayComponentProps> = ({
+  propertyName,
+  propertyValue,
+  schema,
+  disabled,
+  children,
+}) => {
+  const { formatMessage } = useIntl();
+  return (
+    <Form.List key={propertyName} name={propertyName}>
+      {(fields, { add, remove }) => (
+        <>
+          {fields.map((field, index) => (
+            <Form.Item
+              key={field.key}
+              rules={transformPropertyToRules(schema!, propertyName, propertyValue)}
+              label={`${propertyName}-${index + 1}`}
+            >
+              {children}
+              {fields.length > 1 ? (
+                <MinusCircleOutlined onClick={() => remove(field.name)} />
+              ) : (
+                <React.Fragment />
+              )}
+            </Form.Item>
+          ))}
+          {/* BUG: There should also care about minItems */}
+          {fields.length < (propertyValue.maxItems ?? Number.MAX_SAFE_INTEGER) ? (
+            <Form.Item label={propertyName}>
+              <Button type="dashed" onClick={add} disabled={disabled}>
+                <PlusOutlined /> {formatMessage({ id: 'component.global.add' })}
+              </Button>
+            </Form.Item>
+          ) : null}
+        </>
+      )}
+    </Form.List>
+  );
+};
+
+const PluginForm: React.FC<PluginForm.Props> = ({
+  name,
+  form,
+  disabled,
+  initialData = {},
+  onFinish,
+}) => {
+  const { formatMessage } = useIntl();
+  const [schema, setSchema] = useState<PluginForm.PluginSchema>();
+
+  useEffect(() => {
+    if (name) {
+      setSchema(undefined);
+      fetchPluginSchema(name).then((data) => {
+        setSchema(data);
+
+        const propertyDefaultData = {};
+        Object.entries(data.properties || {}).forEach(([propertyName, propertyValue]) => {
+          if (propertyValue.hasOwnProperty('default')) {
+            propertyDefaultData[propertyName] = propertyValue.default;
+          }
+        });
+        form.setFieldsValue(propertyDefaultData);
+
+        requestAnimationFrame(() => {
+          form.setFieldsValue(initialData);
+        });
+      });
+    }
+  }, [name]);
+
+  return (
+    <Form {...formLayout} form={form} onFinish={onFinish} labelAlign="left">
+      {Object.entries(schema?.properties || {}).map(([propertyName, propertyValue]) => {
+        // eslint-disable-next-line arrow-body-style
+        if (propertyValue.type === 'array') {
+          return (
+            <ArrayComponent
+              key={propertyName}
+              disabled={disabled}
+              schema={schema!}
+              propertyName={propertyName}
+              propertyValue={propertyValue}
+            >
+              {renderComponentByProperty({ type: 'string' })}
+            </ArrayComponent>
+          );
+        }
+
+        if (propertyValue.type === 'object') {
+          return (
+            <ArrayComponent
+              key={propertyName}
+              disabled={disabled}
+              schema={schema!}
+              propertyName={propertyName}
+              propertyValue={propertyValue}
+            >
+              {/* TODO: there should not be fixed value, and it should receive custom key */}
+              {renderComponentByProperty({ type: 'string' }, { placeholder: 'Header' })}
+              {renderComponentByProperty({ type: 'string' }, { placeholder: 'Value' })}
+            </ArrayComponent>
+          );
+        }
+
+        return (
+          <Form.Item
+            label={formatMessage({
+              id: `PluginForm.plugin.${name}.property.${propertyName}`,
+              defaultMessage: propertyName,
+            })}
+            extra={formatMessage({
+              id: `PluginForm.plugin.${name}.property.${propertyName}.extra`,
+              defaultMessage: '',
+            })}
+            name={propertyName}
+            key={propertyName}
+            rules={transformPropertyToRules(schema!, propertyName, propertyValue)}
+            valuePropName={propertyValue.type === 'boolean' ? 'checked' : 'value'}
+          >
+            {renderComponentByProperty(propertyValue, { disabled })}
+          </Form.Item>
+        );
+      })}
+    </Form>
+  );
+};
+
+export default PluginForm;
diff --git a/src/components/PluginForm/README.md b/src/components/PluginForm/README.md
new file mode 100644
index 0000000..68d4686
--- /dev/null
+++ b/src/components/PluginForm/README.md
@@ -0,0 +1,9 @@
+# PluginForm
+
+> The PluginForm component aims to build UI according to the plugin schema.
+
+## Usage
+
+1. Modify `list` variable in `data.ts` file.
+2. Modify filds under `/locales` folder to have a good translation.
+3. Import files under `/locales` folder to `/src/locales` to be localization.
diff --git a/src/components/PluginForm/data.ts b/src/components/PluginForm/data.ts
new file mode 100644
index 0000000..d8dd6e3
--- /dev/null
+++ b/src/components/PluginForm/data.ts
@@ -0,0 +1,100 @@
+export const PLUGIN_MAPPER_SOURCE: { [name: string]: PluginForm.PluginMapperItem } = {
+  'limit-req': {
+    category: 'Limit',
+  },
+  'limit-count': {
+    category: 'Limit',
+  },
+  'limit-conn': {
+    category: 'Limit',
+  },
+  'key-auth': {
+    category: 'Security',
+    hidden: true,
+  },
+  'basic-auth': {
+    category: 'Security',
+    hidden: true,
+  },
+  prometheus: {
+    category: 'Metric',
+  },
+  'node-status': {
+    category: 'Other',
+  },
+  'jwt-auth': {
+    category: 'Security',
+    hidden: true,
+  },
+  zipkin: {
+    category: 'Metric',
+  },
+  'ip-restriction': {
+    category: 'Security',
+  },
+  'grpc-transcode': {
+    category: 'Other',
+    hidden: true,
+  },
+  'serverless-pre-function': {
+    category: 'Other',
+  },
+  'serverless-post-function': {
+    category: 'Other',
+  },
+  'openid-connect': {
+    category: 'Security',
+  },
+  'proxy-rewrite': {
+    category: 'Other',
+    hidden: true,
+  },
+  redirect: {
+    category: 'Other',
+    hidden: true,
+  },
+  'response-rewrite': {
+    category: 'Other',
+  },
+  'fault-injection': {
+    category: 'Security',
+  },
+  'udp-logger': {
+    category: 'Log',
+  },
+  'wolf-rbac': {
+    category: 'Other',
+    hidden: true,
+  },
+  'proxy-cache': {
+    category: 'Other',
+  },
+  'tcp-logger': {
+    category: 'Log',
+  },
+  'proxy-mirror': {
+    category: 'Other',
+  },
+  'kafka-logger': {
+    category: 'Log',
+  },
+  cors: {
+    category: 'Security',
+  },
+  heartbeat: {
+    category: 'Other',
+    hidden: true,
+  },
+  'batch-requests': {
+    category: 'Other',
+  },
+  'http-logger': {
+    category: 'Log',
+  },
+  'mqtt-proxy': {
+    category: 'Other',
+  },
+  oauth: {
+    category: 'Security',
+  },
+};
diff --git a/src/components/PluginForm/index.ts b/src/components/PluginForm/index.ts
new file mode 100644
index 0000000..8770cd6
--- /dev/null
+++ b/src/components/PluginForm/index.ts
@@ -0,0 +1,3 @@
+export { default } from './PluginForm';
+export { default as PluginFormZhCN } from './locales/zh-CN';
+export { default as PluginFormEnUS } from './locales/en-US';
diff --git a/src/components/PluginForm/locales/en-US.ts b/src/components/PluginForm/locales/en-US.ts
new file mode 100644
index 0000000..4f19e6d
--- /dev/null
+++ b/src/components/PluginForm/locales/en-US.ts
@@ -0,0 +1,152 @@
+export default {
+  'PluginForm.plugin.limit-conn.desc': '限制并发连接数',
+  'PluginForm.plugin.limit-conn.property.conn': 'conn',
+  'PluginForm.plugin.limit-conn.property.conn.extra': '最大并发连接数',
+  'PluginForm.plugin.limit-conn.property.burst': 'burst',
+  'PluginForm.plugin.limit-conn.property.burst.extra': '并发连接数超过 conn,但是低于 conn + burst 时,请求将被延迟处理',
+  'PluginForm.plugin.limit-conn.property.default_conn_delay': '延迟时间',
+  'PluginForm.plugin.limit-conn.property.default_conn_delay.extra': '被延迟处理的请求,需要等待多少秒',
+  'PluginForm.plugin.limit-conn.property.key': 'key',
+  'PluginForm.plugin.limit-conn.property.key.extra': '用来做限制的依据',
+  'PluginForm.plugin.limit-conn.property.rejected_code': '拒绝状态码',
+  'PluginForm.plugin.limit-conn.property.rejected_code.extra': '当并发连接数超过 conn + burst 的限制时,返回给终端的 HTTP 状态码',
+
+  'PluginForm.plugin.limit-count.desc': '在指定的时间范围内,限制总的请求次数',
+  'PluginForm.plugin.limit-count.property.count': '总请求次数',
+  'PluginForm.plugin.limit-count.property.count.extra': '指定时间窗口内的请求数量阈值',
+  'PluginForm.plugin.limit-count.property.time_window': '时间窗口',
+  'PluginForm.plugin.limit-count.property.time_window.extra':
+    '时间窗口的大小(以秒为单位),超过这个时间,总请求次数就会重置',
+  'PluginForm.plugin.limit-count.property.key': 'key',
+  'PluginForm.plugin.limit-count.property.key.extra': '用来做请求计数的依据',
+  'PluginForm.plugin.limit-count.property.rejected_code': '拒绝状态码',
+  'PluginForm.plugin.limit-count.property.rejected_code.extra':
+    '当请求超过阈值时,返回给终端的 HTTP 状态码',
+  'PluginForm.plugin.limit-count.property.policy': '策略',
+  'PluginForm.plugin.limit-count.property.redis_host': '地址',
+  'PluginForm.plugin.limit-count.property.redis_host.extra': '用于集群限流的 Redis 节点地址',
+  'PluginForm.plugin.limit-count.property.redis_port': '端口',
+  'PluginForm.plugin.limit-count.property.redis_password': '密码',
+  'PluginForm.plugin.limit-count.property.redis_timeout': '超时时间(毫秒)',
+
+  'PluginForm.plugin.limit-req.desc': '限制请求速度的插件,基于漏桶算法',
+  'PluginForm.plugin.limit-req.property.rate': 'rate',
+  'PluginForm.plugin.limit-req.property.rate.extra': '每秒请求速率',
+  'PluginForm.plugin.limit-req.property.burst': 'burst',
+  'PluginForm.plugin.limit-req.property.burst.extra':
+    '每秒请求速率超过 rate,但是低于 rate + burst 时,请求将被延迟处理',
+  'PluginForm.plugin.limit-req.property.key': 'key',
+  'PluginForm.plugin.limit-req.property.key.extra': '用来做请求计数的依据',
+  'PluginForm.plugin.limit-req.property.rejected_code': '拒绝状态码',
+  'PluginForm.plugin.limit-req.property.rejected_code.extra':
+    '速率超过 rate + burst 的限制时,返回给终端的 HTTP 状态码',
+
+  'PluginForm.plugin.cors.desc': 'CORS 插件可以为服务端启用 CORS 的返回头',
+  'PluginForm.plugin.cors.property.allow_origins': '允许跨域访问的 Origin',
+  'PluginForm.plugin.cors.property.allow_origins.extra':
+    '比如 https://somehost.com:8081',
+  'PluginForm.plugin.cors.property.allow_methods': '允许跨域访问的 Method',
+
+  'PluginForm.plugin.fault-injection.desc': '故障注入插件,用来模拟各种后端故障和高延迟',
+  'PluginForm.plugin.fault-injection.property.http_status': 'HTTP 状态码',
+  'PluginForm.plugin.fault-injection.property.body': '响应体',
+  'PluginForm.plugin.fault-injection.property.duration': '延迟时间(秒)',
+
+  'PluginForm.plugin.http-logger.desc': 'http-logger 可以将日志数据请求推送到 HTTP/HTTPS 服务器',
+  'PluginForm.plugin.http-logger.property.uri': '日志服务器地址',
+  'PluginForm.plugin.http-logger.property.uri.extra': '比如 127.0.0.1:80/postendpoint?param=1',
+
+  'PluginForm.plugin.ip-restriction.desc':
+    'ip-restriction 可以把一批 IP 地址列入白名单或黑名单(二选一),时间复杂度是O(1),并支持用 CIDR 来表示 IP 范围',
+  'PluginForm.plugin.ip-restriction.property.whitelist': '白名单',
+  'PluginForm.plugin.ip-restriction.property.blacklist': '黑名单',
+
+  'PluginForm.plugin.kafka-logger.desc': '把接口请求日志以 JSON 的形式推送给外部 Kafka 集群',
+  'PluginForm.plugin.kafka-logger.property.broker_list': 'broker',
+  'PluginForm.plugin.kafka-logger.property.kafka_topic': 'topic',
+
+  'PluginForm.plugin.prometheus.desc': '提供符合 prometheus 数据格式的 metrics 数据',
+
+  'PluginForm.plugin.proxy-cache.desc': '代理缓存插件,缓存后端服务的响应数据',
+  'PluginForm.plugin.proxy-cache.property.cache_zone': '缓存区域名',
+  'PluginForm.plugin.proxy-cache.property.cache_zone.extra': ' 本地目录为 /tmp/${区域名},修改默认区域名必须同时修改 config.yaml',
+  'PluginForm.plugin.proxy-cache.property.cache_key': '缓存 key',
+  'PluginForm.plugin.proxy-cache.property.cache_key.extra': '可以使用 Nginx 变量,例如:$host, $uri',
+  'PluginForm.plugin.proxy-cache.property.cache_bypass': '跳过缓存检索',
+  'PluginForm.plugin.proxy-cache.property.cache_bypass.extra': '这里可以使用 Nginx 变量,当此参数的值不为空或非0时将会跳过缓存的检索',
+  'PluginForm.plugin.proxy-cache.property.cache_method': '缓存 Method',
+  'PluginForm.plugin.proxy-cache.property.cache_http_status': '缓存响应状态码',
+  'PluginForm.plugin.proxy-cache.property.hide_cache_headers': '隐藏缓存头',
+  'PluginForm.plugin.proxy-cache.property.hide_cache_headers.extra': '是否将 Expires 和 Cache-Control 响应头返回给客户端',
+  'PluginForm.plugin.proxy-cache.property.no_cache': '不缓存的数据',
+  'PluginForm.plugin.proxy-cache.property.no_cache.extra': '这里可以使用 Nginx 变量, 当此参数的值不为空或非0时将不会缓存数据',
+
+  'PluginForm.plugin.proxy-mirror.desc': 'proxy mirror 代理镜像插件,提供了镜像客户端请求的能力',
+  'PluginForm.plugin.proxy-mirror.property.host': '镜像服务地址',
+  'PluginForm.plugin.proxy-mirror.property.host.extra': '例如:http://127.0.0.1:9797。地址中需要包含 http 或 https,不能包含 URI 部分',
+
+  'PluginForm.plugin.response-rewrite.desc': '该插件支持修改上游服务返回的 body 和 header 信息',
+  'PluginForm.plugin.response-rewrite.property.status_code': '状态码',
+  'PluginForm.plugin.response-rewrite.property.body': '响应体',
+  'PluginForm.plugin.response-rewrite.property.body_base64': '响应体是否需要 base64 解码',
+  'PluginForm.plugin.response-rewrite.property.headers': 'HTTP 头',
+
+  'PluginForm.plugin.syslog.desc': '对接 syslog 日志服务器',
+  'PluginForm.plugin.syslog.property.host': '日志服务器地址',
+  'PluginForm.plugin.syslog.property.port': '日志服务器端口',
+  'PluginForm.plugin.syslog.property.timeout': '超时时间',
+  'PluginForm.plugin.syslog.property.tls': '开启 SSL',
+  'PluginForm.plugin.syslog.property.flush_limit': '缓存区大小',
+  'PluginForm.plugin.syslog.property.sock_type': '协议类型',
+  'PluginForm.plugin.syslog.property.max_retry_times': '重试次数',
+  'PluginForm.plugin.syslog.property.retry_interval': '重试间隔时间(毫秒)',
+  'PluginForm.plugin.syslog.property.pool_size': '连接池大小',
+
+  'PluginForm.plugin.tcp-logger.desc': '对接 TCP 日志服务器',
+  'PluginForm.plugin.tcp-logger.property.host': '日志服务器地址',
+  'PluginForm.plugin.tcp-logger.property.port': '日志服务器地址',
+  'PluginForm.plugin.tcp-logger.property.timeout': '超时时间',
+  'PluginForm.plugin.tcp-logger.property.tls': '开启 SSL',
+  'PluginForm.plugin.tcp-logger.property.tls_options': 'TLS 选型',
+
+  'PluginForm.plugin.udp-logger.desc': '对接 UDP 日志服务器',
+  'PluginForm.plugin.udp-logger.property.host': '日志服务器地址',
+  'PluginForm.plugin.udp-logger.property.port': '日志服务器地址',
+  'PluginForm.plugin.udp-logger.property.timeout': '超时时间',
+
+  'PluginForm.plugin.zipkin.desc': '对接 zipkin',
+  'PluginForm.plugin.zipkin.property.endpoint': 'endpoint',
+  'PluginForm.plugin.zipkin.property.endpoint.extra': '例如 http://127.0.0.1:9411/api/v2/spans',
+  'PluginForm.plugin.zipkin.property.sample_ratio': '采样率',
+  'PluginForm.plugin.zipkin.property.service_name': '服务名',
+  'PluginForm.plugin.zipkin.property.server_addr': '网关实例 IP',
+  'PluginForm.plugin.zipkin.property.server_addr.extra': '默认值是 Nginx 内置变量 server_addr',
+
+  'PluginForm.plugin.skywalking.desc': '对接 Apache Skywalking',
+  'PluginForm.plugin.skywalking.property.endpoint': 'endpoint',
+  'PluginForm.plugin.skywalking.property.endpoint.extra': '例如 http://127.0.0.1:12800',
+  'PluginForm.plugin.skywalking.property.sample_ratio': '采样率',
+  'PluginForm.plugin.skywalking.property.service_name': '服务名',
+
+  'PluginForm.plugin.serverless-pre-function.desc': '在指定阶段最开始的位置,运行指定的 Lua 函数',
+  'PluginForm.plugin.serverless-pre-function.property.phase': '运行阶段',
+  'PluginForm.plugin.serverless-pre-function.property.functions': '运行的函数集',
+
+  'PluginForm.plugin.serverless-post-function.desc': '在指定阶段最后的位置,运行指定的 Lua 函数',
+  'PluginForm.plugin.serverless-post-function.property.phase': '运行阶段',
+  'PluginForm.plugin.serverless-post-function.property.functions': '运行的函数集',
+
+
+  'PluginForm.plugin.basic-auth.desc': 'basic auth 插件',
+  'PluginForm.plugin.jwt-auth.desc': 'JWT 认证插件',
+  'PluginForm.plugin.key-auth.desc': 'key auth 插件',
+  'PluginForm.plugin.wolf-rbac.desc': '对接 wolf RBAC 服务',
+  'PluginForm.plugin.openid-connect.desc': 'Open ID Connect(OIDC) 插件提供对接外部认证服务的能力',
+
+  'PluginForm.plugin.redirect.desc': '重定向插件',
+  'PluginForm.plugin.proxy-rewrite.desc': 'proxy rewrite 代理改写插件,可以改写客户端请求',
+  'PluginForm.plugin.mqtt-proxy.desc': 'mqtt-proxy 插件可以帮助你根据 MQTT 的 client_id 实现动态负载均衡',
+  'PluginForm.plugin.grpc-transcoding.desc': 'gRPC 转换插件,实现 HTTP(s) -> APISIX -> gRPC server 的转换',
+  'PluginForm.plugin.batch-requests.desc':
+    'batch-requests 插件可以一次接受多个请求并以 http pipeline 的方式在网关发起多个 http 请求,合并结果后再返回客户端,这在客户端需要访问多个接口时可以显著地提升请求性能',
+};
diff --git a/src/components/PluginForm/locales/zh-CN.ts b/src/components/PluginForm/locales/zh-CN.ts
new file mode 100644
index 0000000..4f19e6d
--- /dev/null
+++ b/src/components/PluginForm/locales/zh-CN.ts
@@ -0,0 +1,152 @@
+export default {
+  'PluginForm.plugin.limit-conn.desc': '限制并发连接数',
+  'PluginForm.plugin.limit-conn.property.conn': 'conn',
+  'PluginForm.plugin.limit-conn.property.conn.extra': '最大并发连接数',
+  'PluginForm.plugin.limit-conn.property.burst': 'burst',
+  'PluginForm.plugin.limit-conn.property.burst.extra': '并发连接数超过 conn,但是低于 conn + burst 时,请求将被延迟处理',
+  'PluginForm.plugin.limit-conn.property.default_conn_delay': '延迟时间',
+  'PluginForm.plugin.limit-conn.property.default_conn_delay.extra': '被延迟处理的请求,需要等待多少秒',
+  'PluginForm.plugin.limit-conn.property.key': 'key',
+  'PluginForm.plugin.limit-conn.property.key.extra': '用来做限制的依据',
+  'PluginForm.plugin.limit-conn.property.rejected_code': '拒绝状态码',
+  'PluginForm.plugin.limit-conn.property.rejected_code.extra': '当并发连接数超过 conn + burst 的限制时,返回给终端的 HTTP 状态码',
+
+  'PluginForm.plugin.limit-count.desc': '在指定的时间范围内,限制总的请求次数',
+  'PluginForm.plugin.limit-count.property.count': '总请求次数',
+  'PluginForm.plugin.limit-count.property.count.extra': '指定时间窗口内的请求数量阈值',
+  'PluginForm.plugin.limit-count.property.time_window': '时间窗口',
+  'PluginForm.plugin.limit-count.property.time_window.extra':
+    '时间窗口的大小(以秒为单位),超过这个时间,总请求次数就会重置',
+  'PluginForm.plugin.limit-count.property.key': 'key',
+  'PluginForm.plugin.limit-count.property.key.extra': '用来做请求计数的依据',
+  'PluginForm.plugin.limit-count.property.rejected_code': '拒绝状态码',
+  'PluginForm.plugin.limit-count.property.rejected_code.extra':
+    '当请求超过阈值时,返回给终端的 HTTP 状态码',
+  'PluginForm.plugin.limit-count.property.policy': '策略',
+  'PluginForm.plugin.limit-count.property.redis_host': '地址',
+  'PluginForm.plugin.limit-count.property.redis_host.extra': '用于集群限流的 Redis 节点地址',
+  'PluginForm.plugin.limit-count.property.redis_port': '端口',
+  'PluginForm.plugin.limit-count.property.redis_password': '密码',
+  'PluginForm.plugin.limit-count.property.redis_timeout': '超时时间(毫秒)',
+
+  'PluginForm.plugin.limit-req.desc': '限制请求速度的插件,基于漏桶算法',
+  'PluginForm.plugin.limit-req.property.rate': 'rate',
+  'PluginForm.plugin.limit-req.property.rate.extra': '每秒请求速率',
+  'PluginForm.plugin.limit-req.property.burst': 'burst',
+  'PluginForm.plugin.limit-req.property.burst.extra':
+    '每秒请求速率超过 rate,但是低于 rate + burst 时,请求将被延迟处理',
+  'PluginForm.plugin.limit-req.property.key': 'key',
+  'PluginForm.plugin.limit-req.property.key.extra': '用来做请求计数的依据',
+  'PluginForm.plugin.limit-req.property.rejected_code': '拒绝状态码',
+  'PluginForm.plugin.limit-req.property.rejected_code.extra':
+    '速率超过 rate + burst 的限制时,返回给终端的 HTTP 状态码',
+
+  'PluginForm.plugin.cors.desc': 'CORS 插件可以为服务端启用 CORS 的返回头',
+  'PluginForm.plugin.cors.property.allow_origins': '允许跨域访问的 Origin',
+  'PluginForm.plugin.cors.property.allow_origins.extra':
+    '比如 https://somehost.com:8081',
+  'PluginForm.plugin.cors.property.allow_methods': '允许跨域访问的 Method',
+
+  'PluginForm.plugin.fault-injection.desc': '故障注入插件,用来模拟各种后端故障和高延迟',
+  'PluginForm.plugin.fault-injection.property.http_status': 'HTTP 状态码',
+  'PluginForm.plugin.fault-injection.property.body': '响应体',
+  'PluginForm.plugin.fault-injection.property.duration': '延迟时间(秒)',
+
+  'PluginForm.plugin.http-logger.desc': 'http-logger 可以将日志数据请求推送到 HTTP/HTTPS 服务器',
+  'PluginForm.plugin.http-logger.property.uri': '日志服务器地址',
+  'PluginForm.plugin.http-logger.property.uri.extra': '比如 127.0.0.1:80/postendpoint?param=1',
+
+  'PluginForm.plugin.ip-restriction.desc':
+    'ip-restriction 可以把一批 IP 地址列入白名单或黑名单(二选一),时间复杂度是O(1),并支持用 CIDR 来表示 IP 范围',
+  'PluginForm.plugin.ip-restriction.property.whitelist': '白名单',
+  'PluginForm.plugin.ip-restriction.property.blacklist': '黑名单',
+
+  'PluginForm.plugin.kafka-logger.desc': '把接口请求日志以 JSON 的形式推送给外部 Kafka 集群',
+  'PluginForm.plugin.kafka-logger.property.broker_list': 'broker',
+  'PluginForm.plugin.kafka-logger.property.kafka_topic': 'topic',
+
+  'PluginForm.plugin.prometheus.desc': '提供符合 prometheus 数据格式的 metrics 数据',
+
+  'PluginForm.plugin.proxy-cache.desc': '代理缓存插件,缓存后端服务的响应数据',
+  'PluginForm.plugin.proxy-cache.property.cache_zone': '缓存区域名',
+  'PluginForm.plugin.proxy-cache.property.cache_zone.extra': ' 本地目录为 /tmp/${区域名},修改默认区域名必须同时修改 config.yaml',
+  'PluginForm.plugin.proxy-cache.property.cache_key': '缓存 key',
+  'PluginForm.plugin.proxy-cache.property.cache_key.extra': '可以使用 Nginx 变量,例如:$host, $uri',
+  'PluginForm.plugin.proxy-cache.property.cache_bypass': '跳过缓存检索',
+  'PluginForm.plugin.proxy-cache.property.cache_bypass.extra': '这里可以使用 Nginx 变量,当此参数的值不为空或非0时将会跳过缓存的检索',
+  'PluginForm.plugin.proxy-cache.property.cache_method': '缓存 Method',
+  'PluginForm.plugin.proxy-cache.property.cache_http_status': '缓存响应状态码',
+  'PluginForm.plugin.proxy-cache.property.hide_cache_headers': '隐藏缓存头',
+  'PluginForm.plugin.proxy-cache.property.hide_cache_headers.extra': '是否将 Expires 和 Cache-Control 响应头返回给客户端',
+  'PluginForm.plugin.proxy-cache.property.no_cache': '不缓存的数据',
+  'PluginForm.plugin.proxy-cache.property.no_cache.extra': '这里可以使用 Nginx 变量, 当此参数的值不为空或非0时将不会缓存数据',
+
+  'PluginForm.plugin.proxy-mirror.desc': 'proxy mirror 代理镜像插件,提供了镜像客户端请求的能力',
+  'PluginForm.plugin.proxy-mirror.property.host': '镜像服务地址',
+  'PluginForm.plugin.proxy-mirror.property.host.extra': '例如:http://127.0.0.1:9797。地址中需要包含 http 或 https,不能包含 URI 部分',
+
+  'PluginForm.plugin.response-rewrite.desc': '该插件支持修改上游服务返回的 body 和 header 信息',
+  'PluginForm.plugin.response-rewrite.property.status_code': '状态码',
+  'PluginForm.plugin.response-rewrite.property.body': '响应体',
+  'PluginForm.plugin.response-rewrite.property.body_base64': '响应体是否需要 base64 解码',
+  'PluginForm.plugin.response-rewrite.property.headers': 'HTTP 头',
+
+  'PluginForm.plugin.syslog.desc': '对接 syslog 日志服务器',
+  'PluginForm.plugin.syslog.property.host': '日志服务器地址',
+  'PluginForm.plugin.syslog.property.port': '日志服务器端口',
+  'PluginForm.plugin.syslog.property.timeout': '超时时间',
+  'PluginForm.plugin.syslog.property.tls': '开启 SSL',
+  'PluginForm.plugin.syslog.property.flush_limit': '缓存区大小',
+  'PluginForm.plugin.syslog.property.sock_type': '协议类型',
+  'PluginForm.plugin.syslog.property.max_retry_times': '重试次数',
+  'PluginForm.plugin.syslog.property.retry_interval': '重试间隔时间(毫秒)',
+  'PluginForm.plugin.syslog.property.pool_size': '连接池大小',
+
+  'PluginForm.plugin.tcp-logger.desc': '对接 TCP 日志服务器',
+  'PluginForm.plugin.tcp-logger.property.host': '日志服务器地址',
+  'PluginForm.plugin.tcp-logger.property.port': '日志服务器地址',
+  'PluginForm.plugin.tcp-logger.property.timeout': '超时时间',
+  'PluginForm.plugin.tcp-logger.property.tls': '开启 SSL',
+  'PluginForm.plugin.tcp-logger.property.tls_options': 'TLS 选型',
+
+  'PluginForm.plugin.udp-logger.desc': '对接 UDP 日志服务器',
+  'PluginForm.plugin.udp-logger.property.host': '日志服务器地址',
+  'PluginForm.plugin.udp-logger.property.port': '日志服务器地址',
+  'PluginForm.plugin.udp-logger.property.timeout': '超时时间',
+
+  'PluginForm.plugin.zipkin.desc': '对接 zipkin',
+  'PluginForm.plugin.zipkin.property.endpoint': 'endpoint',
+  'PluginForm.plugin.zipkin.property.endpoint.extra': '例如 http://127.0.0.1:9411/api/v2/spans',
+  'PluginForm.plugin.zipkin.property.sample_ratio': '采样率',
+  'PluginForm.plugin.zipkin.property.service_name': '服务名',
+  'PluginForm.plugin.zipkin.property.server_addr': '网关实例 IP',
+  'PluginForm.plugin.zipkin.property.server_addr.extra': '默认值是 Nginx 内置变量 server_addr',
+
+  'PluginForm.plugin.skywalking.desc': '对接 Apache Skywalking',
+  'PluginForm.plugin.skywalking.property.endpoint': 'endpoint',
+  'PluginForm.plugin.skywalking.property.endpoint.extra': '例如 http://127.0.0.1:12800',
+  'PluginForm.plugin.skywalking.property.sample_ratio': '采样率',
+  'PluginForm.plugin.skywalking.property.service_name': '服务名',
+
+  'PluginForm.plugin.serverless-pre-function.desc': '在指定阶段最开始的位置,运行指定的 Lua 函数',
+  'PluginForm.plugin.serverless-pre-function.property.phase': '运行阶段',
+  'PluginForm.plugin.serverless-pre-function.property.functions': '运行的函数集',
+
+  'PluginForm.plugin.serverless-post-function.desc': '在指定阶段最后的位置,运行指定的 Lua 函数',
+  'PluginForm.plugin.serverless-post-function.property.phase': '运行阶段',
+  'PluginForm.plugin.serverless-post-function.property.functions': '运行的函数集',
+
+
+  'PluginForm.plugin.basic-auth.desc': 'basic auth 插件',
+  'PluginForm.plugin.jwt-auth.desc': 'JWT 认证插件',
+  'PluginForm.plugin.key-auth.desc': 'key auth 插件',
+  'PluginForm.plugin.wolf-rbac.desc': '对接 wolf RBAC 服务',
+  'PluginForm.plugin.openid-connect.desc': 'Open ID Connect(OIDC) 插件提供对接外部认证服务的能力',
+
+  'PluginForm.plugin.redirect.desc': '重定向插件',
+  'PluginForm.plugin.proxy-rewrite.desc': 'proxy rewrite 代理改写插件,可以改写客户端请求',
+  'PluginForm.plugin.mqtt-proxy.desc': 'mqtt-proxy 插件可以帮助你根据 MQTT 的 client_id 实现动态负载均衡',
+  'PluginForm.plugin.grpc-transcoding.desc': 'gRPC 转换插件,实现 HTTP(s) -> APISIX -> gRPC server 的转换',
+  'PluginForm.plugin.batch-requests.desc':
+    'batch-requests 插件可以一次接受多个请求并以 http pipeline 的方式在网关发起多个 http 请求,合并结果后再返回客户端,这在客户端需要访问多个接口时可以显著地提升请求性能',
+};
diff --git a/src/components/PluginForm/service.ts b/src/components/PluginForm/service.ts
new file mode 100644
index 0000000..a688e9c
--- /dev/null
+++ b/src/components/PluginForm/service.ts
@@ -0,0 +1,5 @@
+import { request } from 'umi';
+import { transformSchemaFromAPI } from './transformer';
+
+export const fetchPluginSchema = (name: string): Promise<PluginForm.PluginSchema> =>
+  request(`/schema/plugins/${name}`).then((data) => transformSchemaFromAPI(data, name));
diff --git a/src/components/PluginForm/transformer.ts b/src/components/PluginForm/transformer.ts
new file mode 100644
index 0000000..dddbac5
--- /dev/null
+++ b/src/components/PluginForm/transformer.ts
@@ -0,0 +1,90 @@
+import { v4 as uuidv4 } from 'uuid';
+import { Rule } from 'antd/es/form';
+
+/**
+ * Transform schema data from API for target plugin.
+ */
+export const transformSchemaFromAPI = (
+  schema: PluginForm.PluginSchema,
+  pluginName: string,
+): PluginForm.PluginSchema => {
+  if (pluginName === 'key-auth') {
+    return {
+      ...schema,
+      properties: {
+        key: {
+          ...schema.properties!.key,
+          default: uuidv4(),
+        },
+      },
+    };
+  }
+
+  if (pluginName === 'prometheus' || pluginName === 'node-status' || pluginName === 'heartbeat') {
+    return {
+      ...schema,
+      properties: {
+        enabled: {
+          type: 'boolean',
+          default: false,
+        },
+      },
+    };
+  }
+
+  return schema;
+};
+
+/**
+ * Transform schema data to be compatible with API.
+ */
+// eslint-disable-next-line arrow-body-style
+export const transformSchemaToAPI = (schema: PluginForm.PluginSchema, pluginName: string) => {
+  return { schema, pluginName };
+};
+
+/**
+ * Transform schema's property to rules.
+ */
+export const transformPropertyToRules = (
+  schema: PluginForm.PluginSchema,
+  propertyName: string,
+  propertyValue: PluginForm.PluginProperty,
+): Rule[] => {
+  if (!schema) {
+    return [];
+  }
+
+  const { type, minLength, maxLength, minimum, maximum, pattern } = propertyValue;
+
+  const requiredRule = schema.required?.includes(propertyName) ? [{ required: true }] : [];
+  const typeRule = [{ type }];
+  const enumRule = propertyValue.enum ? [{ type: 'enum', enum: propertyValue.enum }] : [];
+  const rangeRule =
+    type !== 'string' &&
+    type !== 'array' &&
+    (propertyValue.hasOwnProperty('minimum') || propertyValue.hasOwnProperty('maximum'))
+      ? [
+          {
+            min: minimum ?? Number.MIN_SAFE_INTEGER,
+            max: maximum ?? Number.MAX_SAFE_INTEGER,
+          },
+        ]
+      : [];
+  const lengthRule =
+    type === 'string' || type === 'array'
+      ? [{ min: minLength ?? Number.MIN_SAFE_INTEGER, max: maxLength ?? Number.MAX_SAFE_INTEGER }]
+      : [];
+  const customPattern = pattern ? [{ pattern: new RegExp(pattern) }] : [];
+
+  const rules = [
+    ...requiredRule,
+    ...typeRule,
+    ...enumRule,
+    ...rangeRule,
+    ...lengthRule,
+    ...customPattern,
+  ];
+  const flattend = rules.reduce((prev, next) => ({ ...prev, ...next }));
+  return [flattend] as Rule[];
+};
diff --git a/src/components/PluginForm/typing.d.ts b/src/components/PluginForm/typing.d.ts
new file mode 100644
index 0000000..dbc106b
--- /dev/null
+++ b/src/components/PluginForm/typing.d.ts
@@ -0,0 +1,64 @@
+declare namespace PluginForm {
+  interface Props {
+    name?: string;
+    disabled?: boolean;
+    // FormInstance
+    form: any;
+    initialData?: PluginSchema;
+    onFinish(values: any): void;
+  }
+
+  interface PluginSchema {
+    type: 'object';
+    id?: string;
+    required?: string[];
+    additionalProperties?: boolean;
+    minProperties?: number;
+    oneOf?: Array<{
+      required: string[];
+    }>;
+    properties?: {
+      [propertyName: string]: PluginProperty;
+    };
+  }
+
+  interface PluginProperty {
+    type: 'number' | 'string' | 'integer' | 'array' | 'boolean' | 'object';
+    // the same as type
+    default?: any;
+    description?: string;
+    // NOTE: maybe 0.00001
+    minimum?: number;
+    maximum?: number;
+    minLength?: number;
+    maxLength?: number;
+    minItems?: number;
+    maxItems?: number;
+    // e.g "^/.*"
+    pattern?: string;
+    enum?: string[];
+    requried?: string[];
+    minProperties?: number;
+    additionalProperties?: boolean;
+    items?: {
+      type: string;
+      anyOf?: Array<{
+        type?: string;
+        description?: string;
+        enum?: string[];
+        pattern?: string;
+      }>;
+    };
+  }
+
+  type PluginCategory = 'Security' | 'Limit' | 'Log' | 'Metric' | 'Other';
+
+  type PluginMapperItem = {
+    category: PluginCategory;
+    hidden?: boolean;
+  };
+
+  interface PluginProps extends PluginMapperItem {
+    name: string;
+  }
+}
diff --git a/src/components/PluginModal/index.tsx b/src/components/PluginModal/index.tsx
new file mode 100644
index 0000000..9f4882c
--- /dev/null
+++ b/src/components/PluginModal/index.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Modal } from 'antd';
+import { useIntl } from 'umi';
+
+import PluginForm from '@/components/PluginForm';
+import { useForm } from 'antd/es/form/util';
+
+interface Props {
+  visible: boolean;
+  name: string;
+  initialData?: PluginForm.PluginSchema;
+  onFinish(values: any): void;
+}
+
+const PluginModal: React.FC<Props> = (props) => {
+  const { name, visible } = props;
+  const [form] = useForm();
+
+  return (
+    <Modal
+      destroyOnClose
+      visible={visible}
+      title={`${useIntl().formatMessage({ id: 'component.global.edit.plugin' })} ${name}`}
+      okText="确定"
+      cancelText="取消"
+    >
+      <PluginForm form={form} {...props} />
+    </Modal>
+  );
+};
+
+export default PluginModal;
diff --git a/src/components/RightContent/AvatarDropdown.tsx b/src/components/RightContent/AvatarDropdown.tsx
new file mode 100644
index 0000000..567f2ae
--- /dev/null
+++ b/src/components/RightContent/AvatarDropdown.tsx
@@ -0,0 +1,94 @@
+import React, { useCallback } from 'react';
+import { SettingOutlined, UserOutlined, SettingFilled } from '@ant-design/icons';
+import { Avatar, Menu, Spin } from 'antd';
+import { ClickParam } from 'antd/es/menu';
+import { history, useModel } from 'umi';
+
+import { stringify } from 'querystring';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+export interface GlobalHeaderRightProps {
+  menu?: boolean;
+}
+
+/**
+ * 退出登录,并且将当前的 url 保存
+ */
+const settings = async () => {
+  history.replace({
+    pathname: '/setting',
+    search: stringify({
+      redirect: window.location.href,
+    }),
+  });
+};
+
+const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
+  const { initialState, setInitialState } = useModel('@@initialState');
+
+  const onMenuClick = useCallback((event: ClickParam) => {
+    const { key } = event;
+    if (key === 'settings') {
+      setInitialState({ ...initialState, currentUser: undefined });
+      settings();
+      return;
+    }
+    history.push(`/account/${key}`);
+  }, []);
+
+  const loading = (
+    <span className={`${styles.action} ${styles.account}`}>
+      <Spin
+        size="small"
+        style={{
+          marginLeft: 8,
+          marginRight: 8,
+        }}
+      />
+    </span>
+  );
+
+  if (!initialState) {
+    return loading;
+  }
+
+  const { currentUser } = initialState;
+
+  if (!currentUser || !currentUser.name) {
+    return loading;
+  }
+
+  const menuHeaderDropdown = (
+    <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
+      {menu && (
+        <Menu.Item key="center">
+          <UserOutlined />
+          个人中心
+        </Menu.Item>
+      )}
+      {menu && (
+        <Menu.Item key="settings">
+          <SettingOutlined />
+          个人设置
+        </Menu.Item>
+      )}
+      {menu && <Menu.Divider />}
+
+      <Menu.Item key="settings">
+        <SettingFilled />
+        修改设置
+      </Menu.Item>
+    </Menu>
+  );
+  return (
+    <HeaderDropdown overlay={menuHeaderDropdown}>
+      <span className={`${styles.action} ${styles.account}`}>
+        <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
+        <span className={`${styles.name} anticon`}>{currentUser.name}</span>
+      </span>
+    </HeaderDropdown>
+  );
+};
+
+export default AvatarDropdown;
diff --git a/src/components/RightContent/index.less b/src/components/RightContent/index.less
new file mode 100644
index 0000000..ef2549e
--- /dev/null
+++ b/src/components/RightContent/index.less
@@ -0,0 +1,82 @@
+@import '~antd/es/style/themes/default.less';
+
+@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
+
+.menu {
+  :global(.anticon) {
+    margin-right: 8px;
+  }
+  :global(.ant-dropdown-menu-item) {
+    min-width: 160px;
+  }
+}
+
+.right {
+  display: flex;
+  float: right;
+  height: 48px;
+  margin-left: auto;
+  overflow: hidden;
+  .action {
+    display: flex;
+    align-items: center;
+    height: 48px;
+    padding: 0 12px;
+    cursor: pointer;
+    transition: all 0.3s;
+
+    &:hover {
+      background: @pro-header-hover-bg;
+    }
+    &:global(.opened) {
+      background: @pro-header-hover-bg;
+    }
+  }
+  .search {
+    padding: 0 12px;
+    &:hover {
+      background: transparent;
+    }
+  }
+  .account {
+    .avatar {
+      margin-right: 8px;
+      color: @primary-color;
+      vertical-align: top;
+      background: rgba(255, 255, 255, 0.85);
+    }
+  }
+}
+
+.dark {
+  .action {
+    &:hover {
+      background: #252a3d;
+    }
+    &:global(.opened) {
+      background: #252a3d;
+    }
+  }
+}
+
+@media only screen and (max-width: @screen-md) {
+  :global(.ant-divider-vertical) {
+    vertical-align: unset;
+  }
+  .name {
+    display: none;
+  }
+  .right {
+    position: absolute;
+    top: 0;
+    right: 12px;
+    .account {
+      .avatar {
+        margin-right: 0;
+      }
+    }
+    .search {
+      display: none;
+    }
+  }
+}
diff --git a/src/components/RightContent/index.tsx b/src/components/RightContent/index.tsx
new file mode 100644
index 0000000..f54e5b7
--- /dev/null
+++ b/src/components/RightContent/index.tsx
@@ -0,0 +1,51 @@
+import { Tooltip, Tag, Space } from 'antd';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import React from 'react';
+import { useModel, SelectLang } from 'umi';
+import Avatar from './AvatarDropdown';
+import styles from './index.less';
+
+export type SiderTheme = 'light' | 'dark';
+
+const ENVTagColor = {
+  dev: 'orange',
+  test: 'green',
+  pre: '#87d068',
+};
+
+const GlobalHeaderRight: React.FC<{}> = () => {
+  const { initialState } = useModel('@@initialState');
+
+  if (!initialState || !initialState.settings) {
+    return null;
+  }
+
+  const { navTheme, layout } = initialState.settings;
+  let className = styles.right;
+
+  if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
+    className = `${styles.right}  ${styles.dark}`;
+  }
+  return (
+    <Space className={className}>
+      <Tooltip title="Documentation">
+        <span
+          className={styles.action}
+          onClick={() => {
+            window.location.href = 'https://github.com/apache/incubator-apisix';
+          }}
+        >
+          <QuestionCircleOutlined />
+        </span>
+      </Tooltip>
+      <Avatar />
+      {REACT_APP_ENV && (
+        <span>
+          <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
+        </span>
+      )}
+      <SelectLang className={styles.action} />
+    </Space>
+  );
+};
+export default GlobalHeaderRight;
diff --git a/src/e2e/__mocks__/antd-pro-merge-less.js b/src/e2e/__mocks__/antd-pro-merge-less.js
new file mode 100644
index 0000000..f237ddf
--- /dev/null
+++ b/src/e2e/__mocks__/antd-pro-merge-less.js
@@ -0,0 +1 @@
+export default undefined;
diff --git a/src/e2e/baseLayout.e2e.js b/src/e2e/baseLayout.e2e.js
new file mode 100644
index 0000000..f328ada
--- /dev/null
+++ b/src/e2e/baseLayout.e2e.js
@@ -0,0 +1,57 @@
+const { uniq } = require('lodash');
+const RouterConfig = require('../../config/config').default.routes;
+
+const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
+
+function formatter(routes, parentPath = '') {
+  const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
+  let result = [];
+  routes.forEach((item) => {
+    if (item.path) {
+      result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
+    }
+    if (item.routes) {
+      result = result.concat(
+        formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
+      );
+    }
+  });
+  return uniq(result.filter((item) => !!item));
+}
+
+beforeEach(async () => {
+  await page.goto(`${BASE_URL}`);
+  await page.evaluate(() => {
+    localStorage.setItem('antd-pro-authority', '["admin"]');
+  });
+});
+
+describe('Ant Design Pro E2E test', () => {
+  const testPage = (path) => async () => {
+    await page.goto(`${BASE_URL}${path}`);
+    await page.waitForSelector('footer', {
+      timeout: 2000,
+    });
+    const haveFooter = await page.evaluate(
+      () => document.getElementsByTagName('footer').length > 0,
+    );
+    expect(haveFooter).toBeTruthy();
+  };
+
+  const routers = formatter(RouterConfig);
+  routers.forEach((route) => {
+    it(`test pages ${route}`, testPage(route));
+  });
+
+  it('topmenu should have footer', async () => {
+    const params = '?navTheme=light&layout=topmenu';
+    await page.goto(`${BASE_URL}${params}`);
+    await page.waitForSelector('footer', {
+      timeout: 2000,
+    });
+    const haveFooter = await page.evaluate(
+      () => document.getElementsByTagName('footer').length > 0,
+    );
+    expect(haveFooter).toBeTruthy();
+  });
+});
diff --git a/src/global.less b/src/global.less
new file mode 100644
index 0000000..f38a267
--- /dev/null
+++ b/src/global.less
@@ -0,0 +1,69 @@
+@import '~antd/es/style/themes/default.less';
+
+html,
+body,
+#root {
+  height: 100%;
+}
+
+.colorWeak {
+  filter: invert(80%);
+}
+
+.ant-layout {
+  min-height: 100vh;
+}
+
+canvas {
+  display: block;
+}
+
+body {
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+ul,
+ol {
+  list-style: none;
+}
+
+@media (max-width: @screen-xs) {
+  .ant-table {
+    width: 100%;
+    overflow-x: auto;
+    &-thead > tr,
+    &-tbody > tr {
+      > th,
+      > td {
+        white-space: pre;
+        > span {
+          display: block;
+        }
+      }
+    }
+  }
+}
+
+// 兼容IE11
+@media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
+  body .ant-design-pro > .ant-layout {
+    min-height: 100vh;
+  }
+}
+
+.ant-pro-sider-light {
+  z-index: 99;
+}
+
+.ant-checkbox-disabled > .ant-checkbox-inner,
+.ant-radio-disabled > .ant-radio-inner,
+.ant-input-number-disabled,
+.ant-input[disabled] {
+  background-color: #fff !important;
+}
+
+.ant-select-disabled.ant-select-single:not(.ant-select-customize-input) .ant-select-selector {
+  background-color: #fff !important;
+}
diff --git a/src/global.tsx b/src/global.tsx
new file mode 100644
index 0000000..f7620c4
--- /dev/null
+++ b/src/global.tsx
@@ -0,0 +1,83 @@
+import { Button, message, notification } from 'antd';
+
+import React from 'react';
+import { formatMessage } from 'umi';
+import defaultSettings from '../config/defaultSettings';
+
+const { pwa } = defaultSettings;
+// if pwa is true
+if (pwa) {
+  // Notify user if offline now
+  window.addEventListener('sw.offline', () => {
+    message.warning(formatMessage({ id: 'app.pwa.offline' }));
+  });
+
+  // Pop up a prompt on the page asking the user if they want to use the latest version
+  window.addEventListener('sw.updated', (event: Event) => {
+    const e = event as CustomEvent;
+    const reloadSW = async () => {
+      // Check if there is sw whose state is waiting in ServiceWorkerRegistration
+      // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
+      const worker = e.detail && e.detail.waiting;
+      if (!worker) {
+        return true;
+      }
+      // Send skip-waiting event to waiting SW with MessageChannel
+      await new Promise((resolve, reject) => {
+        const channel = new MessageChannel();
+        channel.port1.onmessage = (msgEvent) => {
+          if (msgEvent.data.error) {
+            reject(msgEvent.data.error);
+          } else {
+            resolve(msgEvent.data);
+          }
+        };
+        worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
+      });
+      // Refresh current page to use the updated HTML and other assets after SW has skiped waiting
+      window.location.reload(true);
+      return true;
+    };
+    const key = `open${Date.now()}`;
+    const btn = (
+      <Button
+        type="primary"
+        onClick={() => {
+          notification.close(key);
+          reloadSW();
+        }}
+      >
+        {formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
+      </Button>
+    );
+    notification.open({
+      message: formatMessage({ id: 'app.pwa.serviceworker.updated' }),
+      description: formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
+      btn,
+      key,
+      onClose: async () => {},
+    });
+  });
+} else if ('serviceWorker' in navigator) {
+  // unregister service worker
+  const { serviceWorker } = navigator;
+  if (serviceWorker.getRegistrations) {
+    serviceWorker.getRegistrations().then((sws) => {
+      sws.forEach((sw) => {
+        sw.unregister();
+      });
+    });
+  }
+  serviceWorker.getRegistration().then((sw) => {
+    if (sw) sw.unregister();
+  });
+
+  // remove all caches
+  if (window.caches && window.caches.keys) {
+    caches.keys().then((keys) => {
+      keys.forEach((key) => {
+        caches.delete(key);
+      });
+    });
+  }
+}
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
new file mode 100644
index 0000000..6b136f7
--- /dev/null
+++ b/src/locales/en-US.ts
@@ -0,0 +1,23 @@
+import { PluginFormEnUS } from '@/components/PluginForm';
+
+import component from './en-US/component';
+import globalHeader from './en-US/globalHeader';
+import menu from './en-US/menu';
+import pwa from './en-US/pwa';
+import settingDrawer from './en-US/settingDrawer';
+import settings from './en-US/setting';
+
+export default {
+  'navBar.lang': 'Languages',
+  'layout.user.link.help': 'Help',
+  'layout.user.link.privacy': 'Privacy',
+  'layout.user.link.terms': 'Terms',
+  'app.preview.down.block': 'Download this page to your local project',
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+  ...PluginFormEnUS,
+};
diff --git a/src/locales/en-US/component.ts b/src/locales/en-US/component.ts
new file mode 100644
index 0000000..9a7fc5d
--- /dev/null
+++ b/src/locales/en-US/component.ts
@@ -0,0 +1,39 @@
+export default {
+  'component.tagSelect.expand': 'Expand',
+  'component.tagSelect.collapse': 'Collapse',
+  'component.tagSelect.all': 'All',
+  'component.global.remove': 'Remove',
+  'component.global.cancel': 'Cancel',
+  'component.global.submit': 'Submit',
+  'component.global.create': 'Create',
+  'component.global.add': 'Add',
+  'component.global.save': 'Save',
+  'component.global.edit': 'Edit',
+  'component.global.action': 'Action',
+  'component.global.update': 'Update',
+  'component.global.get': 'Get',
+  'component.global.edit.plugin': 'Edit plugin',
+  'component.global.loading': 'Loading',
+  'component.status.success': 'Successfully',
+  'component.status.fail': 'Failed',
+  // User component
+  'component.user.loginByPassword': 'Username & Password',
+  'component.user.login': 'Login',
+  'component.user.username': 'Username',
+  'component.user.password': 'Password',
+  'component.user.rememberMe': 'Remember Me',
+  'component.user.inputUsername': 'Please input username!',
+  'component.user.inputPassword': 'Please input password!',
+  'component.user.wrongUsernameOrPassword': 'Wrong account or password!',
+  // SSL Module
+  'component.ssl.removeSSLItemModalContent': 'You are going to remove this item!',
+  'component.ssl.removeSSLItemModalTitle': 'SSL Remove Alert',
+  'component.ssl.fetchSSLListSuccess': 'Fetch SSL list successfully',
+  'component.ssl.removeSSLSuccess': 'Remove target SSL successfully',
+  'component.ssl.fieldSNIInvalid': 'Please check SNI',
+  'component.ssl.fieldKeyInvalid': 'Please check Key',
+  'component.ssl.fieldCertInvalid': 'Please check Cert',
+  'component.ssl.invalidKey': 'Invalid Key',
+  'component.ssl.fieldKeyTooShort': 'Key is too short, 128 characters at least.',
+  'component.ssl.fieldCertTooShort': 'Cert is too short, 128 characters at least.',
+};
diff --git a/src/locales/en-US/globalHeader.ts b/src/locales/en-US/globalHeader.ts
new file mode 100644
index 0000000..60b6d4e
--- /dev/null
+++ b/src/locales/en-US/globalHeader.ts
@@ -0,0 +1,17 @@
+export default {
+  'component.globalHeader.search': 'Search',
+  'component.globalHeader.search.example1': 'Search example 1',
+  'component.globalHeader.search.example2': 'Search example 2',
+  'component.globalHeader.search.example3': 'Search example 3',
+  'component.globalHeader.help': 'Help',
+  'component.globalHeader.notification': 'Notification',
+  'component.globalHeader.notification.empty': 'You have viewed all notifications.',
+  'component.globalHeader.message': 'Message',
+  'component.globalHeader.message.empty': 'You have viewed all messsages.',
+  'component.globalHeader.event': 'Event',
+  'component.globalHeader.event.empty': 'You have viewed all events.',
+  'component.noticeIcon.clear': 'Clear',
+  'component.noticeIcon.cleared': 'Cleared',
+  'component.noticeIcon.empty': 'No notifications',
+  'component.noticeIcon.view-more': 'View more',
+};
diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts
new file mode 100644
index 0000000..90b066f
--- /dev/null
+++ b/src/locales/en-US/menu.ts
@@ -0,0 +1,60 @@
+export default {
+  'menu.more-blocks': 'More Blocks',
+  'menu.home': 'Home',
+  'menu.admin': 'Admin',
+  'menu.admin.sub-page': 'Sub-Page',
+  'menu.login': 'Login',
+  'menu.register': 'Register',
+  'menu.register.result': 'Register Result',
+  'menu.dashboard': 'Dashboard',
+  'menu.dashboard.analysis': 'Analysis',
+  'menu.dashboard.monitor': 'Monitor',
+  'menu.dashboard.workplace': 'Workplace',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.form': 'Form',
+  'menu.form.basic-form': 'Basic Form',
+  'menu.form.step-form': 'Step Form',
+  'menu.form.step-form.info': 'Step Form(write transfer information)',
+  'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
+  'menu.form.step-form.result': 'Step Form(finished)',
+  'menu.form.advanced-form': 'Advanced Form',
+  'menu.list': 'List',
+  'menu.list.table-list': 'Search Table',
+  'menu.list.basic-list': 'Basic List',
+  'menu.list.card-list': 'Card List',
+  'menu.list.search-list': 'Search List',
+  'menu.list.search-list.articles': 'Search List(articles)',
+  'menu.list.search-list.projects': 'Search List(projects)',
+  'menu.list.search-list.applications': 'Search List(applications)',
+  'menu.profile': 'Profile',
+  'menu.profile.basic': 'Basic Profile',
+  'menu.profile.advanced': 'Advanced Profile',
+  'menu.result': 'Result',
+  'menu.result.success': 'Success',
+  'menu.result.fail': 'Fail',
+  'menu.exception': 'Exception',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': 'Trigger',
+  'menu.account': 'Account',
+  'menu.account.center': 'Account Center',
+  'menu.account.settings': 'Account Settings',
+  'menu.account.trigger': 'Trigger Error',
+  'menu.account.logout': 'Logout',
+  'menu.editor': 'Graphic Editor',
+  'menu.editor.flow': 'Flow Editor',
+  'menu.editor.mind': 'Mind Editor',
+  'menu.editor.koni': 'Koni Editor',
+  'menu.ssl': 'SSL',
+  'menu.ssl.list': 'SSL List',
+  'menu.ssl.edit': 'Edit',
+  'menu.ssl.create': 'Create',
+  'menu.setting': 'Settings',
+  'menu.routes': 'Route',
+  'menu.routes.list': 'Route List',
+  'menu.routes.create': 'Create a Route',
+  'menu.routes.edit': 'Edit the Route',
+};
diff --git a/src/locales/en-US/pwa.ts b/src/locales/en-US/pwa.ts
new file mode 100644
index 0000000..ed8d199
--- /dev/null
+++ b/src/locales/en-US/pwa.ts
@@ -0,0 +1,6 @@
+export default {
+  'app.pwa.offline': 'You are offline now',
+  'app.pwa.serviceworker.updated': 'New content is available',
+  'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page',
+  'app.pwa.serviceworker.updated.ok': 'Refresh',
+};
diff --git a/src/locales/en-US/setting.ts b/src/locales/en-US/setting.ts
new file mode 100644
index 0000000..b6ce2cf
--- /dev/null
+++ b/src/locales/en-US/setting.ts
@@ -0,0 +1,4 @@
+export default {
+  'app.settings.admin-api': 'Admin API Config',
+  'app.settings.item.baseURL': 'API base URL',
+};
diff --git a/src/locales/en-US/settingDrawer.ts b/src/locales/en-US/settingDrawer.ts
new file mode 100644
index 0000000..a644905
--- /dev/null
+++ b/src/locales/en-US/settingDrawer.ts
@@ -0,0 +1,31 @@
+export default {
+  'app.setting.pagestyle': 'Page style setting',
+  'app.setting.pagestyle.dark': 'Dark style',
+  'app.setting.pagestyle.light': 'Light style',
+  'app.setting.content-width': 'Content Width',
+  'app.setting.content-width.fixed': 'Fixed',
+  'app.setting.content-width.fluid': 'Fluid',
+  'app.setting.themecolor': 'Theme Color',
+  'app.setting.themecolor.dust': 'Dust Red',
+  'app.setting.themecolor.volcano': 'Volcano',
+  'app.setting.themecolor.sunset': 'Sunset Orange',
+  'app.setting.themecolor.cyan': 'Cyan',
+  'app.setting.themecolor.green': 'Polar Green',
+  'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
+  'app.setting.themecolor.geekblue': 'Geek Glue',
+  'app.setting.themecolor.purple': 'Golden Purple',
+  'app.setting.navigationmode': 'Navigation Mode',
+  'app.setting.sidemenu': 'Side Menu Layout',
+  'app.setting.topmenu': 'Top Menu Layout',
+  'app.setting.fixedheader': 'Fixed Header',
+  'app.setting.fixedsidebar': 'Fixed Sidebar',
+  'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
+  'app.setting.hideheader': 'Hidden Header when scrolling',
+  'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
+  'app.setting.othersettings': 'Other Settings',
+  'app.setting.weakmode': 'Weak Mode',
+  'app.setting.copy': 'Copy Setting',
+  'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js',
+  'app.setting.production.hint':
+    'Setting panel shows in development environment only, please manually modify',
+};
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
new file mode 100644
index 0000000..67b4086
--- /dev/null
+++ b/src/locales/zh-CN.ts
@@ -0,0 +1,23 @@
+import { PluginFormZhCN } from '@/components/PluginForm';
+
+import component from './zh-CN/component';
+import globalHeader from './zh-CN/globalHeader';
+import menu from './zh-CN/menu';
+import pwa from './zh-CN/pwa';
+import settingDrawer from './zh-CN/settingDrawer';
+import settings from './zh-CN/setting';
+
+export default {
+  'navBar.lang': '语言',
+  'layout.user.link.help': '帮助',
+  'layout.user.link.privacy': '隐私',
+  'layout.user.link.terms': '条款',
+  'app.preview.down.block': '下载此页面到本地项目',
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+  ...PluginFormZhCN,
+};
diff --git a/src/locales/zh-CN/component.ts b/src/locales/zh-CN/component.ts
new file mode 100644
index 0000000..048190e
--- /dev/null
+++ b/src/locales/zh-CN/component.ts
@@ -0,0 +1,39 @@
+export default {
+  'component.tagSelect.expand': '展开',
+  'component.tagSelect.collapse': '收起',
+  'component.tagSelect.all': '全部',
+  'component.global.remove': '删除',
+  'component.global.cancel': '取消',
+  'component.global.submit': '提交',
+  'component.global.create': '创建',
+  'component.global.add': '新建',
+  'component.global.save': '保存',
+  'component.global.edit': '编辑',
+  'component.global.action': '操作',
+  'component.global.update': '更新',
+  'component.global.get': '获取',
+  'component.global.edit.plugin': '编辑插件',
+  'component.global.loading': '加载中',
+  'component.status.success': '成功',
+  'component.status.fail': '失败',
+  // User component
+  'component.user.loginByPassword': '账号密码登录',
+  'component.user.login': '登录',
+  'component.user.username': '账号',
+  'component.user.password': '密码',
+  'component.user.rememberMe': '自动登录',
+  'component.user.inputUsername': '请输入账号!',
+  'component.user.inputPassword': '请输入密码!',
+  'component.user.wrongUsernameOrPassword': '账号或密码错误!',
+  // SSL Module
+  'component.ssl.removeSSLItemModalContent': '确定要删除该项吗?',
+  'component.ssl.removeSSLItemModalTitle': '删除 SSL',
+  'component.ssl.fetchSSLListSuccess': '获取 SSL 列表成功',
+  'component.ssl.removeSSLSuccess': '删除 SSL 成功',
+  'component.ssl.fieldSNIInvalid': '请检查 SNI 值',
+  'component.ssl.fieldKeyInvalid': '请检查 Key 值',
+  'component.ssl.fieldCertInvalid': '请检查 Cert 值',
+  'component.ssl.invalidKey': '非法的 Key',
+  'component.ssl.fieldKeyTooShort': 'Key 值过短,至少需要128位!',
+  'component.ssl.fieldCertTooShort': 'Cert 值过短,至少需要128位!',
+};
diff --git a/src/locales/zh-CN/globalHeader.ts b/src/locales/zh-CN/globalHeader.ts
new file mode 100644
index 0000000..9fd66a5
--- /dev/null
+++ b/src/locales/zh-CN/globalHeader.ts
@@ -0,0 +1,17 @@
+export default {
+  'component.globalHeader.search': '站内搜索',
+  'component.globalHeader.search.example1': '搜索提示一',
+  'component.globalHeader.search.example2': '搜索提示二',
+  'component.globalHeader.search.example3': '搜索提示三',
+  'component.globalHeader.help': '使用文档',
+  'component.globalHeader.notification': '通知',
+  'component.globalHeader.notification.empty': '你已查看所有通知',
+  'component.globalHeader.message': '消息',
+  'component.globalHeader.message.empty': '您已读完所有消息',
+  'component.globalHeader.event': '待办',
+  'component.globalHeader.event.empty': '你已完成所有待办',
+  'component.noticeIcon.clear': '清空',
+  'component.noticeIcon.cleared': '清空了',
+  'component.noticeIcon.empty': '暂无数据',
+  'component.noticeIcon.view-more': '查看更多',
+};
diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts
new file mode 100644
index 0000000..6821b81
--- /dev/null
+++ b/src/locales/zh-CN/menu.ts
@@ -0,0 +1,61 @@
+export default {
+  'menu.more-blocks': '更多区块',
+  'menu.home': '首页',
+  'menu.admin': '管理页',
+  'menu.admin.sub-page': '二级管理页',
+  'menu.login': '登录',
+  'menu.register': '注册',
+  'menu.register.result': '注册结果',
+  'menu.dashboard': 'Dashboard',
+  'menu.dashboard.analysis': '分析页',
+  'menu.dashboard.monitor': '监控页',
+  'menu.dashboard.workplace': '工作台',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.form': '表单页',
+  'menu.form.basic-form': '基础表单',
+  'menu.form.step-form': '分步表单',
+  'menu.form.step-form.info': '分步表单(填写转账信息)',
+  'menu.form.step-form.confirm': '分步表单(确认转账信息)',
+  'menu.form.step-form.result': '分步表单(完成)',
+  'menu.form.advanced-form': '高级表单',
+  'menu.list': '列表页',
+  'menu.list.table-list': '查询表格',
+  'menu.list.basic-list': '标准列表',
+  'menu.list.card-list': '卡片列表',
+  'menu.list.search-list': '搜索列表',
+  'menu.list.search-list.articles': '搜索列表(文章)',
+  'menu.list.search-list.projects': '搜索列表(项目)',
+  'menu.list.search-list.applications': '搜索列表(应用)',
+  'menu.profile': '详情页',
+  'menu.profile.basic': '基础详情页',
+  'menu.profile.advanced': '高级详情页',
+  'menu.result': '结果页',
+  'menu.result.success': '成功页',
+  'menu.result.fail': '失败页',
+  'menu.exception': '异常页',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': '触发错误',
+  'menu.account': '个人页',
+  'menu.account.center': '个人中心',
+  'menu.account.settings': '个人设置',
+  'menu.account.trigger': '触发报错',
+  'menu.account.logout': '退出登录',
+  'menu.editor': '图形编辑器',
+  'menu.editor.flow': '流程编辑器',
+  'menu.editor.mind': '脑图编辑器',
+  'menu.editor.koni': '拓扑编辑器',
+  'menu.ssl': 'SSL',
+  'menu.ssl.list': 'SSL 列表',
+  'menu.ssl.edit': '编辑',
+  'menu.ssl.create': '创建',
+  'menu.setting': '设置',
+  'menu.metrics': '监控',
+  'menu.routes': '路由',
+  'menu.routes.list': '列表',
+  'menu.routes.create': '创建',
+  'menu.routes.edit': '编辑',
+};
diff --git a/src/locales/zh-CN/pwa.ts b/src/locales/zh-CN/pwa.ts
new file mode 100644
index 0000000..e950484
--- /dev/null
+++ b/src/locales/zh-CN/pwa.ts
@@ -0,0 +1,6 @@
+export default {
+  'app.pwa.offline': '当前处于离线状态',
+  'app.pwa.serviceworker.updated': '有新内容',
+  'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面',
+  'app.pwa.serviceworker.updated.ok': '刷新',
+};
diff --git a/src/locales/zh-CN/setting.ts b/src/locales/zh-CN/setting.ts
new file mode 100644
index 0000000..43f198e
--- /dev/null
+++ b/src/locales/zh-CN/setting.ts
@@ -0,0 +1,12 @@
+export default {
+  'app.settings.admin-api': '管理 API 配置',
+  'app.settings.item.baseURL': 'API 基础地址',
+  'app.settings.item.admin-api-schema': '管理 API 协议',
+  'app.settings.item.admin-api-host': '管理 API 地址',
+  'app.settings.item.admin-api-path': '管理 API 路径',
+  'app.settings.item.admin-api-key': '管理 API 密钥',
+  'app.settings.item.admin-api-grafana': 'Grafana 地址',
+  'app.settings.description.invalid-admin-api-schema': '非法的管理 API 协议',
+  'app.settings.description.invalid-admin-api-host': '非法的管理 API 地址',
+  'app.settings.description.invalid-admin-api-path': '非法的管理 API 路径',
+};
diff --git a/src/locales/zh-CN/settingDrawer.ts b/src/locales/zh-CN/settingDrawer.ts
new file mode 100644
index 0000000..15685a4
--- /dev/null
+++ b/src/locales/zh-CN/settingDrawer.ts
@@ -0,0 +1,31 @@
+export default {
+  'app.setting.pagestyle': '整体风格设置',
+  'app.setting.pagestyle.dark': '暗色菜单风格',
+  'app.setting.pagestyle.light': '亮色菜单风格',
+  'app.setting.content-width': '内容区域宽度',
+  'app.setting.content-width.fixed': '定宽',
+  'app.setting.content-width.fluid': '流式',
+  'app.setting.themecolor': '主题色',
+  'app.setting.themecolor.dust': '薄暮',
+  'app.setting.themecolor.volcano': '火山',
+  'app.setting.themecolor.sunset': '日暮',
+  'app.setting.themecolor.cyan': '明青',
+  'app.setting.themecolor.green': '极光绿',
+  'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
+  'app.setting.themecolor.geekblue': '极客蓝',
+  'app.setting.themecolor.purple': '酱紫',
+  'app.setting.navigationmode': '导航模式',
+  'app.setting.sidemenu': '侧边菜单布局',
+  'app.setting.topmenu': '顶部菜单布局',
+  'app.setting.fixedheader': '固定 Header',
+  'app.setting.fixedsidebar': '固定侧边菜单',
+  'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
+  'app.setting.hideheader': '下滑时隐藏 Header',
+  'app.setting.hideheader.hint': '固定 Header 时可配置',
+  'app.setting.othersettings': '其他设置',
+  'app.setting.weakmode': '色弱模式',
+  'app.setting.copy': '拷贝设置',
+  'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
+  'app.setting.production.hint':
+    '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
+};
diff --git a/src/manifest.json b/src/manifest.json
new file mode 100644
index 0000000..a4f731e
--- /dev/null
+++ b/src/manifest.json
@@ -0,0 +1,22 @@
+{
+  "name": "Apache APISIX Dashboard",
+  "short_name": "Apache APISIX Dashboard",
+  "display": "standalone",
+  "start_url": "./?utm_source=homescreen",
+  "theme_color": "#002140",
+  "background_color": "#001529",
+  "icons": [
+    {
+      "src": "icons/icon-192x192.png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "icons/icon-128x128.png",
+      "sizes": "128x128"
+    },
+    {
+      "src": "icons/icon-512x512.png",
+      "sizes": "512x512"
+    }
+  ]
+}
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
new file mode 100644
index 0000000..5b29934
--- /dev/null
+++ b/src/pages/404.tsx
@@ -0,0 +1,18 @@
+import { Button, Result } from 'antd';
+import React from 'react';
+import { history } from 'umi';
+
+const NoFoundPage: React.FC<{}> = () => (
+  <Result
+    status={404}
+    title="404"
+    subTitle="Sorry, the page you visited does not exist."
+    extra={
+      <Button type="primary" onClick={() => history.push('/')}>
+        Back Home
+      </Button>
+    }
+  />
+);
+
+export default NoFoundPage;
diff --git a/src/pages/Metrics/Metrics.tsx b/src/pages/Metrics/Metrics.tsx
new file mode 100644
index 0000000..7693883
--- /dev/null
+++ b/src/pages/Metrics/Metrics.tsx
@@ -0,0 +1,54 @@
+import React, { useState, useEffect } from 'react';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import { Empty, Button, Card } from 'antd';
+import { history } from 'umi';
+import { stringify } from 'qs';
+import { getSetting } from '@/pages/Setting';
+
+const Metrics: React.FC = () => {
+  const [grafanaURL, setGrafanaURL] = useState<string | undefined>();
+  const [showMetrics, setShowMetrics] = useState(false);
+
+  useEffect(() => {
+    const { grafanaURL: url } = getSetting();
+    setGrafanaURL(url);
+    setShowMetrics(Boolean(url));
+  }, []);
+
+  return (
+    <PageHeaderWrapper>
+      <Card>
+        {!showMetrics && (
+          <Empty
+            image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
+            imageStyle={{
+              height: 60,
+            }}
+            description={<span>您还未配置 Grafana</span>}
+          >
+            <Button
+              type="primary"
+              onClick={() => {
+                history.replace({
+                  pathname: '/setting',
+                  search: stringify({
+                    redirect: window.location.href,
+                  }),
+                });
+              }}
+            >
+              现在配置
+            </Button>
+          </Empty>
+        )}
+        {showMetrics && (
+          <div>
+            <iframe title="metrics" src={grafanaURL} width="100%" height="860" frameBorder="0" />
+          </div>
+        )}
+      </Card>
+    </PageHeaderWrapper>
+  );
+};
+
+export default Metrics;
diff --git a/src/pages/Routes/Create.less b/src/pages/Routes/Create.less
new file mode 100644
index 0000000..8d1eb14
--- /dev/null
+++ b/src/pages/Routes/Create.less
@@ -0,0 +1,112 @@
+@import '~antd/es/style/themes/default.less';
+
+.card {
+  margin-bottom: 24px;
+}
+
+.heading {
+  margin: 0 0 16px 0;
+  font-size: 14px;
+  line-height: 22px;
+}
+
+.steps:global(.ant-steps) {
+  max-width: 750px;
+  margin: 16px auto;
+}
+
+.errorIcon {
+  margin-right: 24px;
+  color: @error-color;
+  cursor: pointer;
+
+  span.anticon {
+    margin-right: 4px;
+  }
+}
+
+.errorPopover {
+  :global {
+    .ant-popover-inner-content {
+      min-width: 256px;
+      max-height: 290px;
+      padding: 0;
+      overflow: auto;
+    }
+  }
+}
+
+.errorListItem {
+  padding: 8px 16px;
+  list-style: none;
+  border-bottom: 1px solid @border-color-split;
+  cursor: pointer;
+  transition: all 0.3s;
+
+  &:hover {
+    background: @item-active-bg;
+  }
+
+  &:last-child {
+    border: 0;
+  }
+
+  .errorIcon {
+    float: left;
+    margin-top: 4px;
+    margin-right: 12px;
+    padding-bottom: 22px;
+    color: @error-color;
+  }
+
+  .errorField {
+    margin-top: 2px;
+    color: @text-color-secondary;
+    font-size: 12px;
+  }
+}
+
+.editable {
+  td {
+    padding-top: 13px !important;
+    padding-bottom: 12.5px !important;
+  }
+}
+
+// custom footer for fixed footer toolbar
+.advancedForm + div {
+  padding-bottom: 64px;
+}
+
+.advancedForm {
+  :global {
+    .ant-form .ant-row:last-child .ant-form-item {
+      margin-bottom: 24px;
+    }
+
+    .ant-table td {
+      transition: none !important;
+    }
+  }
+}
+
+.optional {
+  color: @text-color-secondary;
+  font-style: normal;
+}
+
+.stepForm {
+  max-width: 700px;
+  margin: 40px auto 0;
+}
+
+:global {
+  .ant-card.ant-card-bordered {
+    height: 250px;
+  }
+  .ant-card-actions {
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+  }
+}
diff --git a/src/pages/Routes/Create.tsx b/src/pages/Routes/Create.tsx
new file mode 100644
index 0000000..1015358
--- /dev/null
+++ b/src/pages/Routes/Create.tsx
@@ -0,0 +1,223 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Steps, Form } from 'antd';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+
+import { PLUGIN_MAPPER_SOURCE } from '@/components/PluginForm/data';
+import { createRoute, fetchRoute, updateRoute, fetchPluginList } from './service';
+import Step1 from './components/Step1';
+import Step2 from './components/Step2';
+import CreateStep3 from './components/CreateStep3';
+import CreateStep4 from './components/CreateStep4';
+import {
+  DEFAULT_STEP_1_DATA,
+  DEFAULT_STEP_2_DATA,
+  DEFAULT_STEP_3_DATA,
+  STEP_HEADER_2,
+  STEP_HEADER_4,
+} from './constants';
+import ResultView from './components/ResultView';
+import ActionBar from './components/ActionBar';
+import styles from './Create.less';
+
+const { Step } = Steps;
+
+type Props = {
+  // FIXME
+  route: any;
+  match: any;
+};
+
+const Create: React.FC<Props> = (props) => {
+  const [step1Data, setStep1Data] = useState(DEFAULT_STEP_1_DATA);
+  const [step2Data, setStep2Data] = useState(DEFAULT_STEP_2_DATA);
+  const [step3Data, setStep3Data] = useState(DEFAULT_STEP_3_DATA);
+
+  const [redirect, setRedirect] = useState(false);
+
+  const [form1] = Form.useForm();
+  const [form2] = Form.useForm();
+
+  const [step, setStep] = useState(0);
+  const [stepHeader, setStepHeader] = useState(STEP_HEADER_4);
+
+  const routeData = {
+    step1Data,
+    step2Data,
+    step3Data,
+  };
+
+  const setupPlugin = () => {
+    const PLUGIN_BLOCK_LIST = Object.entries(PLUGIN_MAPPER_SOURCE)
+      .filter(([, value]) => value.hidden)
+      .flat()
+      .filter((item) => typeof item === 'string');
+
+    fetchPluginList().then((data: string[]) => {
+      const names = data.filter((name) => !PLUGIN_BLOCK_LIST.includes(name));
+
+      const enabledNames = Object.keys(step3Data.plugins);
+      const disabledNames = names.filter((name) => !enabledNames.includes(name));
+
+      setStep3Data({
+        plugins: step3Data.plugins,
+        _disabledPluginList: disabledNames.map((name) => ({
+          name,
+          ...PLUGIN_MAPPER_SOURCE[name],
+        })),
+        _enabledPluginList: enabledNames.map((name) => ({
+          name,
+          ...PLUGIN_MAPPER_SOURCE[name],
+        })),
+      });
+    });
+  };
+
+  const setupRoute = (rid: number) =>
+    fetchRoute(rid).then((data) => {
+      form1.setFieldsValue(data.step1Data);
+      setStep1Data(data.step1Data as RouteModule.Step1Data);
+
+      form2.setFieldsValue(data.step2Data);
+      setStep2Data(data.step2Data);
+
+      setStep3Data(data.step3Data);
+    });
+
+  useEffect(() => {
+    if (props.route.name === 'edit') {
+      setupRoute(props.match.params.rid).then(() => setupPlugin());
+    } else {
+      setupPlugin();
+    }
+  }, []);
+
+  useEffect(() => {
+    const { redirectOption } = step1Data;
+
+    if (redirectOption === 'customRedirect') {
+      setRedirect(true);
+      setStepHeader(STEP_HEADER_2);
+      return;
+    }
+    setRedirect(false);
+    setStepHeader(STEP_HEADER_4);
+  }, [step1Data]);
+
+  // FIXME
+  const onReset = () => {
+    setStep1Data(DEFAULT_STEP_1_DATA);
+    setStep2Data(DEFAULT_STEP_2_DATA);
+    setStep3Data(DEFAULT_STEP_3_DATA);
+    form1.resetFields();
+    form2.resetFields();
+    setStep(0);
+  };
+
+  const renderStep = () => {
+    if (step === 0) {
+      return (
+        <Step1
+          data={routeData}
+          form={form1}
+          onChange={(params: RouteModule.Step1Data) => {
+            setStep1Data({ ...step1Data, ...params });
+          }}
+        />
+      );
+    }
+
+    if (step === 1) {
+      if (redirect) {
+        return (
+          <CreateStep4 data={routeData} form1={form1} form2={form2} onChange={() => {}} redirect />
+        );
+      }
+
+      return (
+        <Step2
+          data={routeData}
+          form={form2}
+          onChange={(params: RouteModule.Step2Data) => setStep2Data({ ...step2Data, ...params })}
+        />
+      );
+    }
+
+    if (step === 2) {
+      return <CreateStep3 data={routeData} onChange={setStep3Data} />;
+    }
+
+    if (step === 3) {
+      return <CreateStep4 data={routeData} form1={form1} form2={form2} onChange={() => {}} />;
+    }
+
+    if (step === 4) {
+      return <ResultView onReset={onReset} />;
+    }
+
+    return null;
+  };
+
+  const onStepChange = (nextStep: number) => {
+    const onUpdateOrCreate = () => {
+      if ((props as any).route.name === 'edit') {
+        updateRoute((props as any).match.params.rid, { data: routeData }).then(() => {
+          setStep(4);
+        });
+      } else {
+        createRoute({ data: routeData }).then(() => {
+          setStep(4);
+        });
+      }
+    };
+
+    if (nextStep === 0) {
+      setStep(nextStep);
+    }
+
+    if (nextStep === 1) {
+      form1.validateFields().then((value) => {
+        setStep1Data({ ...step1Data, ...value });
+        setStep(nextStep);
+      });
+      return;
+    }
+
+    if (nextStep === 2) {
+      if (redirect) {
+        onUpdateOrCreate();
+        return;
+      }
+      form2.validateFields().then((value) => {
+        setStep2Data({ ...step2Data, ...value });
+        setStep(nextStep);
+      });
+      return;
+    }
+
+    if (nextStep === 3) {
+      setStep(nextStep);
+    }
+
+    if (nextStep === 4) {
+      onUpdateOrCreate();
+    }
+  };
+
+  return (
+    <>
+      <PageHeaderWrapper>
+        <Card bordered={false}>
+          <Steps current={step} className={styles.steps}>
+            {stepHeader.map((item) => (
+              <Step title={item} key={item} />
+            ))}
+          </Steps>
+          {renderStep()}
+        </Card>
+      </PageHeaderWrapper>
+      <ActionBar step={step} redirect={redirect} onChange={onStepChange} />
+    </>
+  );
+};
+
+export default Create;
diff --git a/src/pages/Routes/List.tsx b/src/pages/Routes/List.tsx
new file mode 100644
index 0000000..9c0e71c
--- /dev/null
+++ b/src/pages/Routes/List.tsx
@@ -0,0 +1,82 @@
+import React, { useRef } from 'react';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
+import { history } from 'umi';
+import { Button, Popconfirm, notification } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+
+import { ListItem } from '@/transforms/global';
+import { fetchRouteList, removeRoute } from './service';
+
+const RouteList: React.FC = () => {
+  const ref = useRef<ActionType>();
+
+  const columns: ProColumns<ListItem<RouteModule.BaseData>>[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '优先级',
+      dataIndex: 'priority',
+    },
+    {
+      title: '描述',
+      dataIndex: 'description',
+    },
+    {
+      title: '更新时间',
+      dataIndex: 'update_time',
+      render: (text) => `${new Date(Number(text) * 1000).toLocaleString()}`,
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      render: (_, record) => (
+        <>
+          <Button
+            type="primary"
+            onClick={() => history.push(`/routes/${record.id}/edit`)}
+            style={{ marginRight: 10 }}
+          >
+            编辑
+          </Button>
+          <Popconfirm
+            title="确定删除该路由吗?"
+            onConfirm={() => {
+              removeRoute(record.id!).then(() => {
+                notification.success({ message: '删除路由成功' });
+                /* eslint-disable no-unused-expressions */
+                ref.current?.reload();
+              });
+            }}
+          >
+            <Button type="primary" danger>
+              删除
+            </Button>
+          </Popconfirm>
+        </>
+      ),
+    },
+  ];
+
+  return (
+    <PageHeaderWrapper>
+      <ProTable<ListItem<RouteModule.BaseData>>
+        actionRef={ref}
+        rowKey="name"
+        request={() => fetchRouteList()}
+        columns={columns}
+        search={false}
+        toolBarRender={() => [
+          <Button type="primary" onClick={() => history.push(`/routes/create`)}>
+            <PlusOutlined />
+            创建
+          </Button>,
+        ]}
+      />
+    </PageHeaderWrapper>
+  );
+};
+
+export default RouteList;
diff --git a/src/pages/Routes/components/ActionBar/ActionBar.tsx b/src/pages/Routes/components/ActionBar/ActionBar.tsx
new file mode 100644
index 0000000..3abd3a2
--- /dev/null
+++ b/src/pages/Routes/components/ActionBar/ActionBar.tsx
@@ -0,0 +1,46 @@
+import React, { CSSProperties } from 'react';
+
+import { Row, Col, Button } from 'antd';
+
+interface Props {
+  step: number;
+  onChange(nextStep: number): void;
+  redirect?: boolean;
+}
+
+const style: CSSProperties = {
+  position: 'fixed',
+  bottom: 0,
+  right: 10,
+  margin: '-24px -24px 0',
+  backgroundColor: '#fff',
+  padding: '6px 36px',
+  borderTop: '1px solid #ebecec',
+  width: '100%',
+};
+
+const ActionBar: React.FC<Props> = ({ step, onChange, redirect }) => {
+  if (step > 3) {
+    return null;
+  }
+
+  return (
+    <div style={style}>
+      <Row gutter={10} justify="end">
+        <Col>
+          <Button type="primary" onClick={() => onChange(step - 1)} disabled={step === 0}>
+            上一步
+          </Button>
+        </Col>
+        <Col>
+          <Button type="primary" onClick={() => onChange(step + 1)}>
+            {!redirect && (step < 3 ? '下一步' : '提交')}
+            {redirect && (step === 0 ? '下一步' : '提交')}
+          </Button>
+        </Col>
+      </Row>
+    </div>
+  );
+};
+
+export default ActionBar;
diff --git a/src/pages/Routes/components/ActionBar/index.ts b/src/pages/Routes/components/ActionBar/index.ts
new file mode 100644
index 0000000..d08aed0
--- /dev/null
+++ b/src/pages/Routes/components/ActionBar/index.ts
@@ -0,0 +1 @@
+export { default } from './ActionBar';
diff --git a/src/pages/Routes/components/CreateStep3/CreateStep3.tsx b/src/pages/Routes/components/CreateStep3/CreateStep3.tsx
new file mode 100644
index 0000000..59e08c2
--- /dev/null
+++ b/src/pages/Routes/components/CreateStep3/CreateStep3.tsx
@@ -0,0 +1,96 @@
+import React, { useState } from 'react';
+import { SettingOutlined, LinkOutlined } from '@ant-design/icons';
+import { omit, merge } from 'lodash';
+
+import { PLUGIN_MAPPER_SOURCE } from '@/components/PluginForm/data';
+import PanelSection from '../PanelSection';
+import PluginDrawer from './PluginDrawer';
+import PluginCard from './PluginCard';
+
+interface Props extends RouteModule.Data {}
+
+const sectionStyle = {
+  display: 'grid',
+  gridTemplateColumns: 'repeat(3, 33.333%)',
+  gridRowGap: 10,
+  gridColumnGap: 10,
+};
+
+const CreateStep3: React.FC<Props> = ({ data, disabled, onChange }) => {
+  const [currentPlugin, setCurrentPlugin] = useState<string | undefined>();
+  const { _disabledPluginList = [], _enabledPluginList = [] } = data.step3Data;
+
+  return (
+    <>
+      <PanelSection title="已启用" style={sectionStyle}>
+        {_enabledPluginList.map(({ name }) => (
+          <PluginCard
+            name={name}
+            actions={[
+              <SettingOutlined onClick={() => setCurrentPlugin(name)} />,
+              <LinkOutlined
+                onClick={() =>
+                  window.open(
+                    `https://github.com/apache/incubator-apisix/blob/master/doc/plugins/${name}.md`,
+                  )
+                }
+              />,
+            ]}
+            key={name}
+          />
+        ))}
+      </PanelSection>
+      {!disabled && (
+        <PanelSection title="未启用" style={sectionStyle}>
+          {_disabledPluginList.map(({ name }) => (
+            <PluginCard
+              name={name}
+              actions={[
+                <SettingOutlined onClick={() => setCurrentPlugin(name)} />,
+                <LinkOutlined
+                  onClick={() =>
+                    window.open(
+                      `https://github.com/apache/incubator-apisix/blob/master/doc/plugins/${name}.md`,
+                    )
+                  }
+                />,
+              ]}
+              key={name}
+            />
+          ))}
+        </PanelSection>
+      )}
+      <PluginDrawer
+        name={currentPlugin}
+        disabled={disabled}
+        initialData={currentPlugin ? data.step3Data.plugins[currentPlugin] : {}}
+        active={Boolean(_enabledPluginList.find((item) => item.name === currentPlugin))}
+        onActive={(name: string) => {
+          onChange({
+            ...data.step3Data,
+            _disabledPluginList: _disabledPluginList.filter((item) => item.name !== name),
+            _enabledPluginList: _enabledPluginList.concat({ name, ...PLUGIN_MAPPER_SOURCE[name] }),
+          });
+        }}
+        onInactive={(name: string) => {
+          onChange({
+            ...omit({ ...data.step3Data }, `plugins.${currentPlugin}`),
+            _disabledPluginList: _disabledPluginList.concat({
+              name,
+              ...PLUGIN_MAPPER_SOURCE[name],
+            }),
+            _enabledPluginList: _enabledPluginList.filter((item) => item.name !== name),
+          });
+          setCurrentPlugin(undefined);
+        }}
+        onClose={() => setCurrentPlugin(undefined)}
+        onFinish={(value) => {
+          onChange(merge(data.step3Data, { plugins: { [currentPlugin as string]: value } }));
+          setCurrentPlugin(undefined);
+        }}
+      />
+    </>
+  );
+};
+
+export default CreateStep3;
diff --git a/src/pages/Routes/components/CreateStep3/PluginCard.tsx b/src/pages/Routes/components/CreateStep3/PluginCard.tsx
new file mode 100644
index 0000000..bb3857a
--- /dev/null
+++ b/src/pages/Routes/components/CreateStep3/PluginCard.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { Card } from 'antd';
+import { CardProps } from 'antd/lib/card';
+import { useIntl } from 'umi';
+
+interface Props extends CardProps {
+  name: string;
+}
+
+const PluginCard: React.FC<Props> = ({ name, actions }) => {
+  const { formatMessage } = useIntl();
+
+  return (
+    <Card actions={actions}>
+      <Card.Meta
+        title={name}
+        description={formatMessage({
+          id: `PluginForm.plugin.${name}.desc`,
+          defaultMessage: 'Please view the documentation.',
+        })}
+      />
+    </Card>
+  );
+};
+
+export default PluginCard;
diff --git a/src/pages/Routes/components/CreateStep3/PluginDrawer.tsx b/src/pages/Routes/components/CreateStep3/PluginDrawer.tsx
new file mode 100644
index 0000000..b26d63f
--- /dev/null
+++ b/src/pages/Routes/components/CreateStep3/PluginDrawer.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { Drawer, Button } from 'antd';
+import { useForm } from 'antd/es/form/util';
+
+import PluginForm from '@/components/PluginForm';
+
+interface Props extends Omit<PluginForm.Props, 'form'> {
+  active?: boolean;
+  disabled?: boolean;
+  onActive(name: string): void;
+  onInactive(name: string): void;
+  onClose(): void;
+}
+
+const PluginDrawer: React.FC<Props> = ({
+  name,
+  active,
+  disabled,
+  onActive,
+  onInactive,
+  onClose,
+  ...rest
+}) => {
+  const [form] = useForm();
+
+  if (!name) {
+    return null;
+  }
+
+  return (
+    <Drawer
+      title={`配置 ${name} 插件`}
+      width={400}
+      visible={Boolean(name)}
+      destroyOnClose
+      onClose={onClose}
+      footer={
+        disabled ? null : (
+          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+            <div>
+              {Boolean(active) && (
+                <Button type="primary" danger onClick={() => onInactive(name)}>
+                  禁用
+                </Button>
+              )}
+              {Boolean(!active) && (
+                <Button type="primary" onClick={() => onActive(name)}>
+                  启用
+                </Button>
+              )}
+            </div>
+            {Boolean(active) && (
+              <div>
+                <Button onClick={onClose}>取消</Button>
+                <Button
+                  type="primary"
+                  style={{ marginRight: 8, marginLeft: 8 }}
+                  onClick={() => form.submit()}
+                >
+                  确认
+                </Button>
+              </div>
+            )}
+          </div>
+        )
+      }
+    >
+      <PluginForm name={name!} form={form} {...rest} disabled={disabled} />
+    </Drawer>
+  );
+};
+
+export default PluginDrawer;
diff --git a/src/pages/Routes/components/CreateStep3/index.ts b/src/pages/Routes/components/CreateStep3/index.ts
new file mode 100644
index 0000000..bb56257
--- /dev/null
+++ b/src/pages/Routes/components/CreateStep3/index.ts
@@ -0,0 +1 @@
+export { default } from './CreateStep3';
diff --git a/src/pages/Routes/components/CreateStep4/CreateStep4.tsx b/src/pages/Routes/components/CreateStep4/CreateStep4.tsx
new file mode 100644
index 0000000..a539b9f
--- /dev/null
+++ b/src/pages/Routes/components/CreateStep4/CreateStep4.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { FormInstance } from 'antd/lib/form';
+
+import Step1 from '../Step1';
+import Step2 from '../Step2';
+import CreateStep3 from '../CreateStep3';
+
+interface Props extends RouteModule.Data {
+  form1: FormInstance;
+  form2: FormInstance;
+  redirect?: boolean;
+}
+
+const style = {
+  marginTop: '40px',
+};
+
+const CreateStep4: React.FC<Props> = ({ form1, form2, redirect, ...rest }) => {
+  return (
+    <>
+      <h2>定义 API 请求</h2>
+      <Step1 {...rest} form={form1} disabled />
+      {!redirect && (
+        <>
+          <h2 style={style}>定义 API 后端服务</h2>
+          <Step2 {...rest} form={form2} disabled />
+          <h2 style={style}>插件配置</h2>
+          <CreateStep3 {...rest} disabled />
+        </>
+      )}
+    </>
+  );
+};
+
+export default CreateStep4;
diff --git a/src/pages/Routes/components/CreateStep4/index.ts b/src/pages/Routes/components/CreateStep4/index.ts
new file mode 100644
index 0000000..b9b39e1
--- /dev/null
+++ b/src/pages/Routes/components/CreateStep4/index.ts
@@ -0,0 +1 @@
+export { default } from './CreateStep4';
diff --git a/src/pages/Routes/components/PanelSection/index.tsx b/src/pages/Routes/components/PanelSection/index.tsx
new file mode 100644
index 0000000..153942c
--- /dev/null
+++ b/src/pages/Routes/components/PanelSection/index.tsx
@@ -0,0 +1,16 @@
+import React, { CSSProperties } from 'react';
+import { Divider } from 'antd';
+
+const PanelSection: React.FC<{
+  title: string;
+  style?: CSSProperties;
+}> = ({ title, style, children }) => {
+  return (
+    <>
+      <Divider orientation="left">{title}</Divider>
+      <div style={style}>{children}</div>
+    </>
+  );
+};
+
+export default PanelSection;
diff --git a/src/pages/Routes/components/ResultView/ResultView.tsx b/src/pages/Routes/components/ResultView/ResultView.tsx
new file mode 100644
index 0000000..f4505b2
--- /dev/null
+++ b/src/pages/Routes/components/ResultView/ResultView.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Result, Button } from 'antd';
+import { history } from 'umi';
+
+type Props = {
+  onReset?(): void;
+};
+
+const ResultView: React.FC<Props> = () => (
+  <Result
+    status="success"
+    title="提交成功"
+    extra={[
+      <Button type="primary" key="goto-list" onClick={() => history.replace('/routes')}>
+        返回路由列表
+      </Button>,
+      <Button key="create-new" onClick={() => history.replace('/routes/create')}>
+        创建新路由
+      </Button>,
+    ]}
+  />
+);
+
+export default ResultView;
diff --git a/src/pages/Routes/components/ResultView/index.ts b/src/pages/Routes/components/ResultView/index.ts
new file mode 100644
index 0000000..94e7334
--- /dev/null
+++ b/src/pages/Routes/components/ResultView/index.ts
@@ -0,0 +1 @@
+export { default } from './ResultView';
diff --git a/src/pages/Routes/components/Step1/MatchingRulesView.tsx b/src/pages/Routes/components/Step1/MatchingRulesView.tsx
new file mode 100644
index 0000000..7e9cc17
--- /dev/null
+++ b/src/pages/Routes/components/Step1/MatchingRulesView.tsx
@@ -0,0 +1,214 @@
+import React, { useState } from 'react';
+import { Button, Table, Modal, Form, Select, Input, Space } from 'antd';
+
+import PanelSection from '../PanelSection';
+
+interface Props extends RouteModule.Data {}
+
+const MatchingRulesView: React.FC<Props> = ({ data, disabled, onChange }) => {
+  const { advancedMatchingRules } = data.step1Data;
+
+  const [visible, setVisible] = useState(false);
+  const [mode, setMode] = useState<RouteModule.ModalType>('CREATE');
+  const [namePlaceholder, setNamePlaceholder] = useState('');
+  const [modalForm] = Form.useForm();
+
+  const { Option } = Select;
+
+  const onOk = () => {
+    modalForm.validateFields().then((value) => {
+      if (mode === 'EDIT') {
+        const key = modalForm.getFieldValue('key');
+        onChange({
+          ...data.step1Data,
+          advancedMatchingRules: advancedMatchingRules.map((rule) => {
+            if (rule.key === key) {
+              return { ...(value as RouteModule.MatchingRule), key };
+            }
+            return rule;
+          }),
+        });
+      } else {
+        const rule = {
+          ...(value as RouteModule.MatchingRule),
+          key: Math.random().toString(36).slice(2),
+        };
+        onChange({ ...data.step1Data, advancedMatchingRules: advancedMatchingRules.concat(rule) });
+      }
+      modalForm.resetFields();
+      setVisible(false);
+    });
+  };
+
+  const handleEdit = (record: RouteModule.MatchingRule) => {
+    setMode('EDIT');
+    setVisible(true);
+    modalForm.setFieldsValue(record);
+  };
+
+  const handleRemove = (key: string) => {
+    onChange({
+      ...data.step1Data,
+      advancedMatchingRules: advancedMatchingRules.filter((item) => item.key !== key),
+    });
+  };
+
+  const columns = [
+    {
+      title: '参数位置',
+      key: 'position',
+      render: (text: RouteModule.MatchingRule) => {
+        let renderText;
+        switch (text.position) {
+          case 'http':
+            renderText = 'HTTP 请求头';
+            break;
+          case 'arg':
+            renderText = '请求参数';
+            break;
+          case 'cookie':
+            renderText = 'Cookie';
+            break;
+          default:
+            renderText = '';
+        }
+        return renderText;
+      },
+    },
+    {
+      title: '参数名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '运算符',
+      key: 'operator',
+      render: (text: RouteModule.MatchingRule) => {
+        let renderText;
+        switch (text.operator) {
+          case '==':
+            renderText = '等于';
+            break;
+          case '~=':
+            renderText = '不等于';
+            break;
+          case '>':
+            renderText = '大于';
+            break;
+          case '<':
+            renderText = '小于';
+            break;
+          case '~~':
+            renderText = '正则匹配';
+            break;
+          default:
+            renderText = '';
+        }
+        return renderText;
+      },
+    },
+    {
+      title: '参数值',
+      dataIndex: 'value',
+      key: 'value',
+    },
+    disabled
+      ? {}
+      : {
+          title: '操作',
+          key: 'action',
+          render: (_: any, record: RouteModule.MatchingRule) => (
+            <Space size="middle">
+              <a onClick={() => handleEdit(record)}>编辑</a>
+              <a onClick={() => handleRemove(record.key)}>删除</a>
+            </Space>
+          ),
+        },
+  ];
+
+  const renderModal = () => (
+    <Modal
+      title={mode === 'EDIT' ? '编辑规则' : '新建规则'}
+      centered
+      visible
+      onOk={onOk}
+      onCancel={() => {
+        setVisible(false);
+        modalForm.resetFields();
+      }}
+      okText="确定"
+      cancelText="取消"
+      destroyOnClose
+    >
+      <Form form={modalForm} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
+        <Form.Item
+          label="参数位置"
+          name="position"
+          rules={[{ required: true, message: '请选择参数位置' }]}
+        >
+          <Select
+            onChange={(value) => {
+              if (value === 'http') {
+                setNamePlaceholder('请求头键名:例如 HOST');
+              } else {
+                setNamePlaceholder('参数名称:例如 id');
+              }
+            }}
+          >
+            <Option value="http">HTTP 请求头</Option>
+            <Option value="arg">请求参数</Option>
+            <Option value="cookie">Cookie</Option>
+          </Select>
+        </Form.Item>
+        <Form.Item
+          label="参数名称"
+          name="name"
+          rules={[{ required: true, message: '请输入参数名称' }]}
+          extra="只支持字母和数字,并且以字母开头"
+        >
+          <Input placeholder={namePlaceholder} />
+        </Form.Item>
+        <Form.Item
+          label="运算符"
+          name="operator"
+          rules={[{ required: true, message: '请选择运算符' }]}
+        >
+          <Select>
+            <Option value="==">等于</Option>
+            <Option value="~=">不等于</Option>
+            <Option value=">">大于</Option>
+            <Option value="<">小于</Option>
+            <Option value="~~">正则匹配</Option>
+          </Select>
+        </Form.Item>
+        <Form.Item label="值" name="value" rules={[{ required: true, message: '请输入参数值' }]}>
+          <Input />
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+
+  return (
+    <PanelSection title="高级路由匹配条件">
+      {!disabled && (
+        <Button
+          onClick={() => {
+            setMode('CREATE');
+            setVisible(true);
+          }}
+          type="primary"
+          style={{
+            marginBottom: 16,
+          }}
+        >
+          新建
+        </Button>
+      )}
+      <Table key="table" bordered dataSource={advancedMatchingRules} columns={columns} />
+      {/* NOTE: tricky way, switch visible on Modal component will ocure error */}
+      {visible ? renderModal() : null}
+    </PanelSection>
+  );
+};
+
+export default MatchingRulesView;
diff --git a/src/pages/Routes/components/Step1/MetaView.tsx b/src/pages/Routes/components/Step1/MetaView.tsx
new file mode 100644
index 0000000..baac193
--- /dev/null
+++ b/src/pages/Routes/components/Step1/MetaView.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import Form from 'antd/es/form';
+import { Input } from 'antd';
+
+import PanelSection from '../PanelSection';
+
+interface Props extends RouteModule.Data {}
+
+const MetaView: React.FC<Props> = ({ disabled }) => {
+  return (
+    <PanelSection title="名称及其描述">
+      <Form.Item
+        label="API 名称"
+        name="name"
+        rules={[{ required: true, message: '请输入 API 名称' }]}
+        extra="支持英文,数字,下划线和减号,且只能以英文开头"
+      >
+        <Input placeholder="请输入 API 名称" disabled={disabled} />
+      </Form.Item>
+      <Form.Item label="描述" name="desc">
+        <Input.TextArea placeholder="不超过 200 个字符" disabled={disabled} />
+      </Form.Item>
+    </PanelSection>
+  );
+};
+
+export default MetaView;
diff --git a/src/pages/Routes/components/Step1/RequestConfigView.tsx b/src/pages/Routes/components/Step1/RequestConfigView.tsx
new file mode 100644
index 0000000..2424a4a
--- /dev/null
+++ b/src/pages/Routes/components/Step1/RequestConfigView.tsx
@@ -0,0 +1,201 @@
+import React from 'react';
+import Form from 'antd/es/form';
+import { Checkbox, Button, Input, Switch, Select, Row, Col } from 'antd';
+import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
+import { CheckboxValueType } from 'antd/lib/checkbox/Group';
+
+import {
+  HTTP_METHOD_OPTION_LIST,
+  FORM_ITEM_LAYOUT,
+  FORM_ITEM_WITHOUT_LABEL,
+} from '@/pages/Routes/constants';
+
+import PanelSection from '../PanelSection';
+
+interface Props extends RouteModule.Data {}
+
+const RequestConfigView: React.FC<Props> = ({ data, disabled, onChange }) => {
+  const { step1Data } = data;
+  const { protocols } = step1Data;
+  const onProtocolChange = (e: CheckboxValueType[]) => {
+    if (!e.includes('http') && !e.includes('https')) return;
+    onChange({ ...data.step1Data, protocols: e });
+  };
+  const renderHosts = () => (
+    <Form.List name="hosts">
+      {(fields, { add, remove }) => {
+        return (
+          <div>
+            {fields.map((field, index) => (
+              <Form.Item
+                {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
+                label={index === 0 ? '域名' : ''}
+                required
+                key={field.key}
+                extra={index === 0 ? '域名或 IP:支持泛域名,如 "*.test.com"' : ''}
+              >
+                <Form.Item
+                  {...field}
+                  validateTrigger={['onChange', 'onBlur']}
+                  rules={[
+                    {
+                      required: true,
+                      whitespace: true,
+                      message: '请输入域名',
+                    },
+                  ]}
+                  noStyle
+                >
+                  <Input placeholder="请输入域名" style={{ width: '60%' }} disabled={disabled} />
+                </Form.Item>
+                {!disabled && fields.length > 1 ? (
+                  <MinusCircleOutlined
+                    className="dynamic-delete-button"
+                    style={{ margin: '0 8px' }}
+                    onClick={() => {
+                      remove(field.name);
+                    }}
+                  />
+                ) : null}
+              </Form.Item>
+            ))}
+            {!disabled && (
+              <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
+                <Button
+                  type="dashed"
+                  onClick={() => {
+                    add();
+                  }}
+                >
+                  <PlusOutlined /> 新建
+                </Button>
+              </Form.Item>
+            )}
+          </div>
+        );
+      }}
... 21592 lines suppressed ...