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:08 UTC

[incubator-apisix-dashboard] branch feat-master created (now 67bfaff)

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

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


      at 67bfaff  feat: merged next

This branch includes the following new commits:

     new 0cc70e3  remove outdated codes
     new 67bfaff  feat: merged next

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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

Posted by ju...@apache.org.
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 ...


[incubator-apisix-dashboard] 01/02: remove outdated codes

Posted by ju...@apache.org.
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 0cc70e3170db2b9e6a6de78e152d56d414967e98
Author: juzhiyuan <jj...@gmail.com>
AuthorDate: Wed Jun 17 10:52:47 2020 +0800

    remove outdated codes
---
 .DS_Store                                         |  Bin 0 -> 6148 bytes
 .browserslistrc                                   |    2 -
 .editorconfig                                     |   35 -
 .env.development                                  |   11 -
 .env.production                                   |    5 -
 .eslintignore                                     |    3 -
 .eslintrc.js                                      |   37 -
 .gitignore                                        |   21 -
 CHANGELOG.md                                      |   36 -
 DISCLAIMER                                        |    5 -
 LICENSE                                           |  206 -
 NOTICE                                            |    5 -
 README.md                                         |  110 -
 babel.config.js                                   |   22 -
 licenses/LICENSE.ALv2                             |  201 -
 licenses/LICENSE.vue-typescript-admin-template    |   21 -
 package.json                                      |   82 -
 postcss.config.js                                 |   22 -
 public/favicon.ico                                |  Bin 9662 -> 0 bytes
 public/img/icons/favicon-16x16.png                |  Bin 9662 -> 0 bytes
 public/img/icons/favicon-32x32.png                |  Bin 9662 -> 0 bytes
 public/index.html                                 |   43 -
 public/robots.txt                                 |    2 -
 src/App.vue                                       |   33 -
 src/api/schema/consumers.ts                       |   50 -
 src/api/schema/plugins.ts                         |   24 -
 src/api/schema/routes.ts                          |   51 -
 src/api/schema/schema.ts                          |   24 -
 src/api/schema/services.ts                        |   51 -
 src/api/schema/ssl.ts                             |   52 -
 src/api/schema/upstream.ts                        |   50 -
 src/api/types.d.ts                                |   78 -
 src/api/users.ts                                  |   38 -
 src/components/Breadcrumb/index.vue               |  125 -
 src/components/Hamburger/index.vue                |   75 -
 src/components/HeaderSearch/index.vue             |  244 -
 src/components/LangSelect/index.vue               |   77 -
 src/components/Pagination/index.vue               |  104 -
 src/components/PluginDialog/index.vue             |  523 --
 src/components/Screenfull/index.vue               |   79 -
 src/components/VarArgs/index.vue                  |  128 -
 src/lang/en.ts                                    |   94 -
 src/lang/index.ts                                 |   67 -
 src/lang/zh.ts                                    |  102 -
 src/layout/components/AppMain.vue                 |   83 -
 src/layout/components/Navbar/index.vue            |  195 -
 src/layout/components/Sidebar/SidebarItem.vue     |  209 -
 src/layout/components/Sidebar/SidebarItemLink.vue |   56 -
 src/layout/components/Sidebar/SidebarLogo.vue     |  123 -
 src/layout/components/Sidebar/index.vue           |  156 -
 src/layout/components/TagsView/ScrollPane.vue     |  113 -
 src/layout/components/TagsView/index.vue          |  359 -
 src/layout/components/index.ts                    |   28 -
 src/layout/index.vue                              |  192 -
 src/layout/mixin/resize.ts                        |   79 -
 src/main.ts                                       |   57 -
 src/permission.ts                                 |  102 -
 src/router/index.ts                               |   82 -
 src/router/modules/schema/consumers.ts            |   56 -
 src/router/modules/schema/routes.ts               |   56 -
 src/router/modules/schema/services.ts             |   56 -
 src/router/modules/schema/ssl.ts                  |   56 -
 src/router/modules/schema/upstream.ts             |   56 -
 src/settings.ts                                   |   48 -
 src/shims.d.ts                                    |   36 -
 src/store/index.ts                                |   37 -
 src/store/modules/app.ts                          |  116 -
 src/store/modules/permission.ts                   |   80 -
 src/store/modules/settings.ts                     |   62 -
 src/store/modules/tags-view.ts                    |  165 -
 src/store/modules/user.ts                         |  169 -
 src/styles/_mixins.scss                           |   31 -
 src/styles/_svgicon.scss                          |   56 -
 src/styles/_transition.scss                       |   73 -
 src/styles/_variables.scss                        |   56 -
 src/styles/_variables.scss.d.ts                   |   33 -
 src/styles/element-variables.scss                 |   47 -
 src/styles/element-variables.scss.d.ts            |   31 -
 src/styles/index.scss                             |  183 -
 src/utils/cookies.ts                              |   44 -
 src/utils/index.ts                                |  112 -
 src/utils/permission.ts                           |   39 -
 src/utils/request.ts                              |   59 -
 src/utils/scroll-to.ts                            |   74 -
 src/utils/validate.ts                             |   39 -
 src/views/login/index.vue                         |  228 -
 src/views/schema/consumers/edit.vue               |  266 -
 src/views/schema/consumers/list.vue               |  222 -
 src/views/schema/routes/edit.vue                  |  549 --
 src/views/schema/routes/list.vue                  |  252 -
 src/views/schema/service/edit.vue                 |  300 -
 src/views/schema/service/list.vue                 |  215 -
 src/views/schema/ssl/edit.vue                     |  191 -
 src/views/schema/ssl/list.vue                     |  206 -
 src/views/schema/upstream/edit.vue                |  434 -
 src/views/schema/upstream/list.vue                |  289 -
 tsconfig.json                                     |   41 -
 vue.config.js                                     |   84 -
 yarn.lock                                         | 9432 ---------------------
 99 files changed, 19351 deletions(-)

diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..5008ddf
Binary files /dev/null and b/.DS_Store differ
diff --git a/.browserslistrc b/.browserslistrc
deleted file mode 100644
index 3a455dd..0000000
--- a/.browserslistrc
+++ /dev/null
@@ -1,2 +0,0 @@
-> 1%
-last 2 versions
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 9b38463..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,35 +0,0 @@
-# http://editorconfig.org
-
-# top-most EditorConfig file
-root = true
-
-# Unix-style newlines with a newline ending every file
-[*]
-charset = utf-8
-end_of_line = lf
-insert_final_newline = true
-trim_trailing_whitespace = true
-
-# Indentation override for js(x), ts(x) and vue files
-[*.{js,jsx,ts,tsx,vue}]
-indent_size = 2
-indent_style = space
-
-# Indentation override for css related files
-[*.{css,styl,scss,less,sass}]
-indent_size = 2
-indent_style = space
-
-# Indentation override for html files
-[*.html]
-indent_size = 2
-indent_style = space
-
-# Trailing space override for markdown file
-[*.md]
-trim_trailing_whitespace = false
-
-# Indentation override for config files
-[*.{json,yml}]
-indent_size = 2
-indent_style = space
\ No newline at end of file
diff --git a/.env.development b/.env.development
deleted file mode 100644
index 1e3d951..0000000
--- a/.env.development
+++ /dev/null
@@ -1,11 +0,0 @@
-# Base api
-VUE_APP_BASE_API = ''
-
-# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
-# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
-# It only does one thing by converting all import() to require().
-# This configuration can significantly increase the speed of hot updates,
-# when you have a large number of pages.
-# Detail:  https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
-
-VUE_CLI_BABEL_TRANSPILE_MODULES = true
\ No newline at end of file
diff --git a/.env.production b/.env.production
deleted file mode 100644
index edd19d0..0000000
--- a/.env.production
+++ /dev/null
@@ -1,5 +0,0 @@
-
-# Base api
-# Remeber to change this to your production server address
-# Here I used my mock server for this project
-VUE_APP_BASE_API = '/apisix/admin/'
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 4bcf40f..0000000
--- a/.eslintignore
+++ /dev/null
@@ -1,3 +0,0 @@
-
-dist/*.js
-tests/unit/coverage
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 4f0b93f..0000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,37 +0,0 @@
-module.exports = {
-    root: true,
-    env: {
-      browser: true,
-      node: true,
-      es6: true
-    },
-    parserOptions: {
-      parser: '@typescript-eslint/parser',
-      sourceType: 'module'
-    },
-    plugins: [
-      'vue'
-    ],
-    rules: {
-      'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
-      'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
-      'space-before-function-paren': [2, 'never'],
-      'vue/array-bracket-spacing': 'error',
-      'vue/arrow-spacing': 'error',
-      'vue/block-spacing': 'error',
-      'vue/brace-style': 'error',
-      'vue/comma-dangle': 'error',
-      'vue/component-name-in-template-casing': 'error',
-      'vue/eqeqeq': 'error',
-      'vue/key-spacing': 'error',
-      'vue/match-component-file-name': 'error',
-      'vue/object-curly-spacing': 'error',
-      "camelcase": 'off'
-    },
-    'extends': [
-      'eslint:recommended',
-      'plugin:vue/recommended',
-      '@vue/standard',
-      '@vue/typescript'
-    ]
-  }
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 2b33ebe..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,21 +0,0 @@
-.DS_Store
-node_modules
-/dist
-
-# local env files
-.env.local
-.env.*.local
-
-# Log files
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# Editor directories and files
-.idea
-.vscode
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw*
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index f2eb346..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,36 +0,0 @@
-
-<!--
-#
-# 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.
-#
--->
-
-# Table of Contents
-
-- [1.0.0](#100)
-
-## 1.0.0
-
-This release is mainly to build some basic panels and resolve License issue.
-
-### Core
-- Dashboard initial. [#1](https://github.com/apache/incubator-apisix-dashboard/pull/1)
-- Resolve licence issues.
-- Remove unused files from the Dashboard boilerplate.
-- Support panel to list, create and modify Route, Consumer, Service, SSL and Upstream.
-- Support custom configuration for Plugin dialog.
-
-[Back to TOC](#table-of-contents)
diff --git a/DISCLAIMER b/DISCLAIMER
deleted file mode 100644
index 70fc20c..0000000
--- a/DISCLAIMER
+++ /dev/null
@@ -1,5 +0,0 @@
-Apache APISIX Dashboard (incubating) is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator PMC.
-Incubation is required of all newly accepted projects until a further review indicates that the infrastructure,
-communications, and decision making process have stabilized in a manner consistent with other successful ASF projects.
-While incubation status is not necessarily a reflection of the completeness or stability of the code,
-it does indicate that the project has yet to be fully endorsed by the ASF.
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 6da9712..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,206 +0,0 @@
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed 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.
-
-=======================================================================
-
-The Apache APISIX project bundles vue-typescript-admin-template under the MIT license.
-For details, see licenses/LICENSE.vue-typescript-admin-template.
diff --git a/NOTICE b/NOTICE
deleted file mode 100644
index f0a4483..0000000
--- a/NOTICE
+++ /dev/null
@@ -1,5 +0,0 @@
-Apache APISIX Dashboard (incubating)
-Copyright 2020 The Apache Software Foundation
-
-This product includes software developed at
-The Apache Software Foundation (http://www.apache.org/).
diff --git a/README.md b/README.md
deleted file mode 100644
index 438999c..0000000
--- a/README.md
+++ /dev/null
@@ -1,110 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-# apisix_dashboard
-
-## Overview
-
-Dashboard for APISIX & based on ElementUI.
-
-## Documentation
-
-[Docs](https://armour.github.io/vue-typescript-admin-docs)
-
-[Vue Config Docs](https://cli.vuejs.org/zh/config/#publicpath)
-
-## Project Structure
-
-```bash
-โ”œโ”€โ”€ public                     # public static assets
-โ”‚   โ”‚โ”€โ”€ img/                   # static image files
-โ”‚   โ”‚โ”€โ”€ favicon.ico            # favicon
-โ”‚   โ”‚โ”€โ”€ index.html             # index.html template
-โ”‚   โ””โ”€โ”€ robots.txt             # robots file
-โ”œโ”€โ”€ src                        # main source code
-โ”‚   โ”œโ”€โ”€ api/                   # api service
-โ”‚   โ”œโ”€โ”€ components/            # global components
-โ”‚   โ”œโ”€โ”€ lang/                  # i18n language
-โ”‚   โ”œโ”€โ”€ layout/                # global layout
-โ”‚   โ”œโ”€โ”€ router/                # router
-โ”‚   โ”œโ”€โ”€ store/                 # store
-โ”‚   โ”œโ”€โ”€ styles/                # global css
-โ”‚   โ”œโ”€โ”€ utils/                 # global utils
-โ”‚   โ”œโ”€โ”€ views/                 # views
-โ”‚   โ”œโ”€โ”€ App.vue                # main app component
-โ”‚   โ”œโ”€โ”€ main.ts                # app entry file
-โ”‚   โ”œโ”€โ”€ permission.ts          # permission authentication
-โ”‚   โ”œโ”€โ”€ settings.ts            # setting file
-โ”‚   โ””โ”€โ”€ shims.d.ts             # type definition shims
-โ”œโ”€โ”€ licenses                   # license files for ALv2 and boilerplate
-โ”œโ”€โ”€ .browserslistrc            # browserslist config file (to support Autoprefixer)
-โ”œโ”€โ”€ .editorconfig              # editor code format consistency config
-โ”œโ”€โ”€ .env.xxx                   # env variable configuration
-โ”œโ”€โ”€ .eslintrc.js               # eslint config
-โ”œโ”€โ”€ .eslintignore              # eslint ignore config
-โ”œโ”€โ”€ .gitignore                 # git ignore config
-โ”œโ”€โ”€ babel.config.js            # babel config
-โ”œโ”€โ”€ LICENSE                    # license file
-โ”œโ”€โ”€ NOTICE                     # notice file
-โ”œโ”€โ”€ package.json               # package.json
-โ”œโ”€โ”€ postcss.config.js          # postcss config
-โ”œโ”€โ”€ README.md                  # some information about APISIX
-โ”œโ”€โ”€ tsconfig.json              # typescript config
-โ”œโ”€โ”€ vue.config.js              # vue-cli config
-โ””โ”€โ”€ yarn.lock                  # keep exact versions of each dependency
-```
-
-## Project setup
-> Make sure Node.js 8.12.0 or higher, and Yarn are installed on your machine: https://yarnpkg.com/en/docs/install
-
-### Install dependencies
-
-```bash
-yarn install
-```
-
-### Compiles and hot-reloads for development
-
-```bash
-yarn run serve
-```
-
-### Compiles and minifies for production
-
-```bash
-yarn run build:prod
-```
-
-### Lints and fixes files
-
-```bash
-yarn run lint
-```
-
-### Customize Vue configuration
-
-See [Configuration Reference](https://cli.vuejs.org/config/).
-
-## Browsers support
-
-Modern browsers and Internet Explorer 10+.
-
-| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/mas [...]
-| --------- | --------- | --------- | --------- |
-| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
diff --git a/babel.config.js b/babel.config.js
deleted file mode 100644
index e7708a0..0000000
--- a/babel.config.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.
- */
-
-module.exports = {
-  presets: [
-    '@vue/app'
-  ]
-}
diff --git a/licenses/LICENSE.ALv2 b/licenses/LICENSE.ALv2
deleted file mode 100644
index 261eeb9..0000000
--- a/licenses/LICENSE.ALv2
+++ /dev/null
@@ -1,201 +0,0 @@
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed 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.
diff --git a/licenses/LICENSE.vue-typescript-admin-template b/licenses/LICENSE.vue-typescript-admin-template
deleted file mode 100644
index 7c3565d..0000000
--- a/licenses/LICENSE.vue-typescript-admin-template
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2018 Chong Guo
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/package.json b/package.json
deleted file mode 100644
index b769c33..0000000
--- a/package.json
+++ /dev/null
@@ -1,82 +0,0 @@
-{
-  "name": "incubator-apisix-dashboard",
-  "version": "0.1.0",
-  "private": true,
-  "scripts": {
-    "serve": "concurrently \"vue-cli-service serve\"",
-    "lint": "vue-cli-service lint",
-    "build:prod": "vue-cli-service build"
-  },
-  "dependencies": {
-    "axios": "^0.19.0",
-    "echarts": "^4.2.1",
-    "element-ui": "^2.9.2",
-    "fuse.js": "^3.4.5",
-    "js-cookie": "^2.2.0",
-    "normalize.css": "^8.0.1",
-    "nprogress": "^0.2.0",
-    "path-to-regexp": "^3.0.0",
-    "screenfull": "^4.2.0",
-    "uuid": "^3.3.2",
-    "vue": "^2.6.10",
-    "vue-class-component": "^7.1.0",
-    "vue-i18n": "^8.11.2",
-    "vue-property-decorator": "^8.2.1",
-    "vue-router": "^3.0.6",
-    "vue-svgicon": "^3.2.6",
-    "vuex": "^3.1.1",
-    "vuex-class": "^0.3.2",
-    "vuex-module-decorators": "^0.9.9"
-  },
-  "devDependencies": {
-    "@types/compression": "^0.0.36",
-    "@types/echarts": "^4.1.9",
-    "@types/js-cookie": "^2.2.2",
-    "@types/nprogress": "^0.2.0",
-    "@types/webpack-env": "^1.13.9",
-    "@vue/cli-plugin-babel": "^3.8.0",
-    "@vue/cli-plugin-eslint": "^3.8.0",
-    "@vue/cli-plugin-typescript": "^3.8.1",
-    "@vue/cli-service": "^3.8.4",
-    "@vue/eslint-config-standard": "^4.0.0",
-    "@vue/eslint-config-typescript": "^4.0.0",
-    "babel-core": "^7.0.0-bridge.0",
-    "babel-eslint": "^10.0.2",
-    "concurrently": "^4.1.0",
-    "eslint": "^5.16.0",
-    "eslint-plugin-vue": "^5.2.2",
-    "fibers": "^4.0.1",
-    "lint-staged": "^8.2.1",
-    "sass": "^1.21.0",
-    "sass-loader": "^7.1.0",
-    "style-resources-loader": "^1.2.1",
-    "typescript": "3.5.2",
-    "vue-cli-plugin-element": "^1.0.1",
-    "vue-cli-plugin-style-resources-loader": "^0.1.3",
-    "vue-template-compiler": "^2.6.10",
-    "webpack": "^4.35.0"
-  },
-  "bugs": {
-    "url": "https://github.com/apache/incubator-apisix-dashboard/issues"
-  },
-  "gitHooks": {
-    "pre-commit": "lint-staged"
-  },
-  "keywords": [
-    "vue",
-    "typescript",
-    "admin",
-    "template",
-    "element-ui"
-  ],
-  "lint-staged": {
-    "*.{js,vue}": [
-      "vue-cli-service lint",
-      "git add"
-    ]
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/apache/incubator-apisix-dashboard.git"
-  }
-}
diff --git a/postcss.config.js b/postcss.config.js
deleted file mode 100644
index a5e9c1d..0000000
--- a/postcss.config.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.
- */
-
-module.exports = {
-  plugins: {
-    autoprefixer: {}
-  }
-}
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index 05cb493..0000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/img/icons/favicon-16x16.png b/public/img/icons/favicon-16x16.png
deleted file mode 100644
index 05cb493..0000000
Binary files a/public/img/icons/favicon-16x16.png and /dev/null differ
diff --git a/public/img/icons/favicon-32x32.png b/public/img/icons/favicon-32x32.png
deleted file mode 100644
index 05cb493..0000000
Binary files a/public/img/icons/favicon-32x32.png and /dev/null differ
diff --git a/public/index.html b/public/index.html
deleted file mode 100644
index c3907ad..0000000
--- a/public/index.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-# 
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-# 
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-# 
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width,initial-scale=1.0">
-    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    <title><%= webpackConfig.name %></title>
-  </head>
-  <body>
-    <noscript>
-      <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
-    </noscript>
-    <div id="app"></div>
-    <!-- built files will be auto injected -->
-  </body>
-</html>
diff --git a/public/robots.txt b/public/robots.txt
deleted file mode 100644
index eb05362..0000000
--- a/public/robots.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-User-agent: *
-Disallow:
diff --git a/src/App.vue b/src/App.vue
deleted file mode 100644
index adf2931..0000000
--- a/src/App.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div id="app">
-    <router-view />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
-
-@Component({
-  name: 'App'
-})
-export default class extends Vue {}
-</script>
diff --git a/src/api/schema/consumers.ts b/src/api/schema/consumers.ts
deleted file mode 100644
index 06b72d8..0000000
--- a/src/api/schema/consumers.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import request from '@/utils/request'
-
-import { IConsumerData } from '../types'
-
-export const defaultConsumerData: IConsumerData = {
-  username: '',
-  plugins: {}
-}
-
-export const updateOrCreateConsumer = (data: IConsumerData) =>
-  request({
-    url: '/consumers',
-    method: 'PUT',
-    data
-  })
-
-export const getList = () =>
-  request({
-    url: '/consumers',
-    method: 'GET'
-  })
-
-export const get = (username: string) =>
-  request({
-    url: `/consumers/${username}`,
-    method: 'GET'
-  })
-
-export const removeConsumer = (username: string) =>
-  request({
-    url: `/consumers/${username}`,
-    method: 'DELETE'
-  })
diff --git a/src/api/schema/plugins.ts b/src/api/schema/plugins.ts
deleted file mode 100644
index bc71e96..0000000
--- a/src/api/schema/plugins.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import request from '@/utils/request'
-
-export const getPluginList = () =>
-  request({
-    url: '/plugins/list',
-    method: 'get'
-  })
diff --git a/src/api/schema/routes.ts b/src/api/schema/routes.ts
deleted file mode 100644
index a871a96..0000000
--- a/src/api/schema/routes.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import request from '@/utils/request'
-import { IRouteData } from '../types'
-
-export const getList = () =>
-  request({
-    url: '/routes',
-    method: 'GET'
-  })
-
-export const updateRouter = (id: string, data: any) =>
-  request({
-    url: `/routes/${id}`,
-    method: 'PUT',
-    data
-  })
-
-export const getRouter = (id: string) =>
-  request({
-    url: `/routes/${id}`,
-    method: 'GET'
-  })
-
-export const removeRouter = (id: string) =>
-  request({
-    url: `/routes/${id}`,
-    method: 'DELETE'
-  })
-
-export const createRouter = (data: any) =>
-  request({
-    url: '/routes',
-    method: 'POST',
-    data
-  })
diff --git a/src/api/schema/schema.ts b/src/api/schema/schema.ts
deleted file mode 100644
index e58a27b..0000000
--- a/src/api/schema/schema.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import request from '@/utils/request'
-
-export const getPluginSchema = (name: string) =>
-  request({
-    url: `/schema/plugins/${name}`,
-    method: 'get'
-  })
diff --git a/src/api/schema/services.ts b/src/api/schema/services.ts
deleted file mode 100644
index d6bb1bb..0000000
--- a/src/api/schema/services.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import request from '@/utils/request'
-import { IServiceData } from '../types'
-
-export const getServiceList = () =>
-  request({
-    url: '/services',
-    method: 'get'
-  })
-
-export const updateService = (id: string, data: any) =>
-  request({
-    url: `/services/${id}`,
-    method: 'PUT',
-    data
-  })
-
-export const getService = (id: string) =>
-  request({
-    url: `/services/${id}`,
-    method: 'GET'
-  })
-
-export const removeService = (id: string) =>
-  request({
-    url: `/services/${id}`,
-    method: 'DELETE'
-  })
-
-export const createService = (data: any) =>
-  request({
-    url: '/services',
-    method: 'POST',
-    data
-  })
diff --git a/src/api/schema/ssl.ts b/src/api/schema/ssl.ts
deleted file mode 100644
index b945c14..0000000
--- a/src/api/schema/ssl.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import request from '@/utils/request'
-
-import { ISSLData } from '../types'
-
-export const getSSLList = () =>
-  request({
-    url: '/ssl',
-    method: 'GET'
-  })
-
-export const updateSSL = (id: string, data: any) =>
-  request({
-    url: `/ssl/${id}`,
-    method: 'PUT',
-    data
-  })
-
-export const getSSL = (id: string) =>
-  request({
-    url: `/ssl/${id}`,
-    method: 'GET'
-  })
-
-export const removeSSL = (id: string) =>
-  request({
-    url: `/ssl/${id}`,
-    method: 'DELETE'
-  })
-
-export const createSSL = (data: any) =>
-  request({
-    url: '/ssl',
-    method: 'POST',
-    data
-  })
diff --git a/src/api/schema/upstream.ts b/src/api/schema/upstream.ts
deleted file mode 100644
index 04c6f03..0000000
--- a/src/api/schema/upstream.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import request from '@/utils/request'
-
-export const updateStream = (id: string, data: any) =>
-  request({
-    url: `/upstreams/${id}`,
-    method: 'PUT',
-    data
-  })
-
-export const getUpstream = (id: string) =>
-  request({
-    url: `/upstreams/${id}`,
-    method: 'GET'
-  })
-
-export const removeUpstream = (id: string) =>
-  request({
-    url: `/upstreams/${id}`,
-    method: 'DELETE'
-  })
-
-export const createUpstream = (data: any) =>
-  request({
-    url: `/upstreams`,
-    method: 'POST',
-    data
-  })
-
-export const getUpstreamList = () =>
-  request({
-    url: `/upstreams`,
-    method: 'GET'
-  })
diff --git a/src/api/types.d.ts b/src/api/types.d.ts
deleted file mode 100644
index c80ec23..0000000
--- a/src/api/types.d.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export type TypeID = number | string
-export type TypePlugin = object
-
-export interface IConsumerData {
-  username: string
-  plugins?: TypePlugin
-}
-
-export interface IRouteData {
-  methods: []
-  plugins: TypePlugin
-  upstream: IUpstreamData
-  uri: string
-  host: string
-  remote_addr: string
-  service_id: TypeID
-  upstream_id: TypeID
-  id: TypeID
-}
-
-export interface IServiceData {
-  id: TypeID
-  plugins: TypePlugin
-  upstream: IUpstreamData
-  upstream_id: TypeID
-}
-
-export interface ISSLData {
-  cert: string
-  key: string
-  sni: string
-}
-
-export enum EnumUpstreamType {
-  chash,
-  roundrobin
-}
-
-export enum EnumUpstreamKey {
-  remote_addr
-}
-export interface IUpstreamData {
-  nodes: object
-  type: EnumUpstreamType
-  key?: EnumUpstreamKey
-  id?: TypeID
-}
-
-export enum EnumAction {
-  get,
-  set,
-  delete,
-  create
-}
-
-export interface IDataWrapper<T> {
-  node: {
-    value: T
-  },
-  action: EnumAction
-}
diff --git a/src/api/users.ts b/src/api/users.ts
deleted file mode 100644
index 591b787..0000000
--- a/src/api/users.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import request from '@/utils/request'
-
-export const getUserInfo = (data: any) =>
-  request({
-    url: '/users/info',
-    method: 'post',
-    data
-  })
-
-export const login = (data: any) =>
-  request({
-    url: '/users/login',
-    method: 'post',
-    data
-  })
-
-export const logout = () =>
-  request({
-    url: '/users/logout',
-    method: 'post'
-  })
diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue
deleted file mode 100644
index 54c04ce..0000000
--- a/src/components/Breadcrumb/index.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <el-breadcrumb
-    class="app-breadcrumb"
-    separator="/"
-  >
-    <transition-group name="breadcrumb">
-      <el-breadcrumb-item
-        v-for="(item, index) in breadcrumbs"
-        :key="item.path"
-      >
-        <span
-          v-if="item.redirect === 'noredirect' || index === breadcrumbs.length-1"
-          class="no-redirect"
-        >{{ $t('route.' + item.meta.title) }}</span>
-        <a
-          v-else
-          @click.prevent="handleLink(item)"
-        >{{ $t('route.' + item.meta.title) }}</a>
-      </el-breadcrumb-item>
-    </transition-group>
-  </el-breadcrumb>
-</template>
-
-<script lang="ts">
-import pathToRegexp from 'path-to-regexp'
-import { Component, Vue, Watch } from 'vue-property-decorator'
-import { RouteRecord, Route } from 'vue-router'
-
-@Component({
-  name: 'Breadcrumb'
-})
-export default class extends Vue {
-  private breadcrumbs: RouteRecord[] = []
-
-  @Watch('$route')
-  private onRouteChange(route: Route) {
-    // if you go to the redirect page, do not update the breadcrumbs
-    if (route.path.startsWith('/redirect/')) {
-      return
-    }
-    this.getBreadcrumb()
-  }
-
-  created() {
-    this.getBreadcrumb()
-  }
-
-  private getBreadcrumb() {
-    let matched = this.$route.matched.filter((item) => item.meta && item.meta.title)
-    const first = matched[0]
-    this.breadcrumbs = matched.filter((item) => {
-      return item.meta && item.meta.title && item.meta.breadcrumb !== false
-    })
-  }
-
-  private isDashboard(route: RouteRecord) {
-    const name = route && route.name
-    if (!name) {
-      return false
-    }
-    return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
-  }
-
-  private pathCompile(path: string) {
-    // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
-    const { params } = this.$route
-    const toPath = pathToRegexp.compile(path)
-    return toPath(params)
-  }
-
-  private handleLink(item: any) {
-    const { redirect, path } = item
-    if (redirect) {
-      this.$router.push(redirect)
-      return
-    }
-    this.$router.push(this.pathCompile(path))
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.el-breadcrumb__inner,
-.el-breadcrumb__inner a {
-  font-weight: 400 !important;
-}
-
-.app-breadcrumb.el-breadcrumb {
-  display: inline-block;
-  font-size: 14px;
-  line-height: 50px;
-  margin-left: 8px;
-
-  .no-redirect {
-    color: #97a8be;
-    cursor: text;
-  }
-}
-</style>
diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue
deleted file mode 100644
index e21e513..0000000
--- a/src/components/Hamburger/index.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div
-    :class="[{'is-active': isActive}]"
-    @click="toggleClick"
-  >
-    <svg
-      viewBox="0 0 100 80"
-      width="20"
-      height="20"
-    >
-      <rect
-        width="100"
-        height="20"
-      />
-      <rect
-        y="30"
-        width="100"
-        height="20"
-      />
-      <rect
-        y="60"
-        width="100"
-        height="20"
-      />
-    </svg>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  name: 'Hamburger'
-})
-export default class extends Vue {
-  @Prop({ default: false }) private isActive!: boolean
-
-  private toggleClick() {
-    this.$emit('toggleClick')
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.svg-icon {
-  vertical-align: middle;
-}
-
-</style>
diff --git a/src/components/HeaderSearch/index.vue b/src/components/HeaderSearch/index.vue
deleted file mode 100644
index c1a2428..0000000
--- a/src/components/HeaderSearch/index.vue
+++ /dev/null
@@ -1,244 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div
-    id="header-search"
-    :class="{'show': show}"
-    class="header-search"
-  >
-    <svg-icon
-      class="search-icon"
-      name="search"
-      @click.stop="click"
-    />
-    <el-select
-      ref="headerSearchSelect"
-      v-model="search"
-      :remote-method="querySearch"
-      filterable
-      default-first-option
-      remote
-      placeholder="Search"
-      class="header-search-select"
-      @change="change"
-    >
-      <el-option
-        v-for="item in options"
-        :key="item.path"
-        :value="item"
-        :label="item.meta.title.join(' > ')"
-      />
-    </el-select>
-  </div>
-</template>
-
-<script lang="ts">
-import path from 'path'
-import Fuse from 'fuse.js' // A lightweight fuzzy-search module
-import { Component, Vue, Watch } from 'vue-property-decorator'
-import { RouteConfig } from 'vue-router'
-import { AppModule } from '@/store/modules/app'
-import { PermissionModule } from '@/store/modules/permission'
-import i18n from '@/lang' // Internationalization
-
-@Component({
-  name: 'HeaderSearch'
-})
-export default class extends Vue {
-  private search = ''
-  private show = false
-  private options: RouteConfig[] = []
-  private searchPool: RouteConfig[] = []
-  private fuse?: Fuse<RouteConfig, Fuse.FuseOptions<RouteConfig>>
-
-  get routes() {
-    return PermissionModule.routes
-  }
-
-  get lang() {
-    return AppModule.language
-  }
-
-  @Watch('lang')
-  private onLangChange() {
-    this.searchPool = this.generateRoutes(this.routes)
-  }
-
-  @Watch('routes')
-  private onRoutesChange() {
-    this.searchPool = this.generateRoutes(this.routes)
-  }
-
-  @Watch('searchPool')
-  private onSearchPoolChange(value: RouteConfig[]) {
-    this.initFuse(value)
-  }
-
-  @Watch('show')
-  private onShowChange(value: boolean) {
-    if (value) {
-      document.body.addEventListener('click', this.close)
-    } else {
-      document.body.removeEventListener('click', this.close)
-    }
-  }
-
-  mounted() {
-    this.searchPool = this.generateRoutes(this.routes)
-  }
-
-  private click() {
-    this.show = !this.show
-    if (this.show) {
-      this.$refs.headerSearchSelect && (this.$refs.headerSearchSelect as HTMLElement).focus()
-    }
-  }
-
-  private close() {
-    this.$refs.headerSearchSelect && (this.$refs.headerSearchSelect as HTMLElement).blur()
-    this.options = []
-    this.show = false
-  }
-
-  private change(route: RouteConfig) {
-    this.$router.push(route.path)
-    this.search = ''
-    this.options = []
-    this.$nextTick(() => {
-      this.show = false
-    })
-  }
-
-  private initFuse(list: RouteConfig[]) {
-    this.fuse = new Fuse(list, {
-      shouldSort: true,
-      threshold: 0.4,
-      location: 0,
-      distance: 100,
-      maxPatternLength: 32,
-      minMatchCharLength: 1,
-      keys: [{
-        name: 'title',
-        weight: 0.7
-      }, {
-        name: 'path',
-        weight: 0.3
-      }]
-    })
-  }
-
-  // Filter out the routes that can be displayed in the sidebar
-  // And generate the internationalized title
-  private generateRoutes(routes: RouteConfig[], basePath = '/', prefixTitle: string[] = []) {
-    let res: RouteConfig[] = []
-
-    for (const router of routes) {
-      // skip hidden router
-      if (router.meta && router.meta.hidden) {
-        continue
-      }
-
-      const data: RouteConfig = {
-        path: path.resolve(basePath, router.path),
-        meta: {
-          title: [...prefixTitle]
-        }
-      }
-
-      if (router.meta && router.meta.title) {
-        // generate internationalized title
-        const i18ntitle = i18n.t(`route.${router.meta.title}`).toString()
-        data.meta.title = [...data.meta.title, i18ntitle]
-        if (router.redirect !== 'noRedirect') {
-          // only push the routes with title
-          // special case: need to exclude parent router without redirect
-          res.push(data)
-        }
-      }
-
-      // recursive child routes
-      if (router.children) {
-        const tempRoutes = this.generateRoutes(router.children, data.path, data.meta.title)
-        if (tempRoutes.length >= 1) {
-          res = [...res, ...tempRoutes]
-        }
-      }
-    }
-    return res
-  }
-
-  private querySearch(query: string) {
-    if (query !== '') {
-      if (this.fuse) {
-        this.options = this.fuse.search(query) as any
-      }
-    } else {
-      this.options = []
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.header-search {
-  font-size: 0 !important;
-
-  .search-icon {
-    cursor: pointer;
-    font-size: 18px;
-    vertical-align: middle;
-  }
-
-  .header-search-select {
-    font-size: 18px;
-    transition: width 0.2s;
-    width: 0;
-    overflow: hidden;
-    background: transparent;
-    border-radius: 0;
-    display: inline-block;
-    vertical-align: middle;
-
-    .el-input__inner {
-      border-radius: 0;
-      border: 0;
-      padding-left: 0;
-      padding-right: 0;
-      box-shadow: none !important;
-      border-bottom: 1px solid #d9d9d9;
-      vertical-align: middle;
-    }
-  }
-
-  &.show {
-    .header-search-select {
-      width: 210px;
-      margin-left: 10px;
-    }
-  }
-}
-</style>
diff --git a/src/components/LangSelect/index.vue b/src/components/LangSelect/index.vue
deleted file mode 100644
index 937813a..0000000
--- a/src/components/LangSelect/index.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <el-dropdown
-    trigger="click"
-    class="international"
-    @command="handleSetLanguage"
-  >
-    <div>
-      <svg-icon
-        name="language"
-        class="international-icon"
-      />
-    </div>
-    <el-dropdown-menu slot="dropdown">
-      <el-dropdown-item
-        :disabled="language==='zh'"
-        command="zh"
-      >
-        ไธญๆ–‡
-      </el-dropdown-item>
-      <el-dropdown-item
-        :disabled="language==='en'"
-        command="en"
-      >
-        English
-      </el-dropdown-item>
-    </el-dropdown-menu>
-  </el-dropdown>
-</template>
-
-<script lang="ts">
-import { Component, Vue, Watch } from 'vue-property-decorator'
-import { AppModule } from '@/store/modules/app'
-
-@Component({
-  name: 'Login'
-})
-export default class extends Vue {
-  get language() {
-    return AppModule.language
-  }
-
-  private handleSetLanguage(lang: string) {
-    this.$i18n.locale = lang
-    AppModule.SetLanguage(lang)
-    this.$message({
-      message: 'Switch Language Success',
-      type: 'success'
-    })
-  }
-}
-</script>
diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue
deleted file mode 100644
index eb31aa1..0000000
--- a/src/components/Pagination/index.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div
-    :class="{'hidden': hidden}"
-    class="pagination-container"
-  >
-    <el-pagination
-      :background="background"
-      :current-page.sync="currentPage"
-      :page-size.sync="pageSize"
-      :layout="layout"
-      :page-sizes="pageSizes"
-      :total="total"
-      v-bind="$attrs"
-      @size-change="handleSizeChange"
-      @current-change="handleCurrentChange"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import { scrollTo } from '@/utils/scroll-to'
-
-@Component({
-  name: 'Pagination'
-})
-export default class extends Vue {
-  @Prop({ required: true }) private total!: number
-  @Prop({ default: 1 }) private page!: number
-  @Prop({ default: 20 }) private limit!: number
-  @Prop({ default: () => [10, 20, 30, 50] }) private pageSizes!: number[]
-  @Prop({ default: 'total, sizes, prev, pager, next, jumper' }) private layout!: string
-  @Prop({ default: true }) private background!: boolean
-  @Prop({ default: true }) private autoScroll!: boolean
-  @Prop({ default: false }) private hidden!: boolean
-
-  get currentPage() {
-    return this.page
-  }
-
-  set currentPage(value) {
-    this.$emit('update:page', value)
-  }
-
-  get pageSize() {
-    return this.limit
-  }
-
-  set pageSize(value) {
-    this.$emit('update:limit', value)
-  }
-
-  handleSizeChange(value: number) {
-    this.$emit('pagination', { page: this.currentPage, limit: value })
-    if (this.autoScroll) {
-      scrollTo(0, 800)
-    }
-  }
-
-  handleCurrentChange(value: number) {
-    this.$emit('pagination', { page: value, limit: this.pageSize })
-    if (this.autoScroll) {
-      scrollTo(0, 800)
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.pagination-container {
-  background: #fff;
-  padding: 32px 16px;
-}
-
-.pagination-container.hidden {
-  display: none;
-}
-</style>
diff --git a/src/components/PluginDialog/index.vue b/src/components/PluginDialog/index.vue
deleted file mode 100644
index d31d4b9..0000000
--- a/src/components/PluginDialog/index.vue
+++ /dev/null
@@ -1,523 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="plugin-dialog">
-    <el-dialog
-      :title="'Plugin ' + name + ' Edit'"
-      :visible.sync="showDialog"
-    >
-      <el-form
-        v-if="schema.oneOf"
-        ref="form"
-        :model="data"
-        :rules="rules"
-        :show-message="false"
-        class="oneof-plugin-wrapper"
-      >
-        <el-form-item
-          label="Option"
-          :rules="{
-            required: true, trigger: 'blur'
-          }"
-        >
-          <el-radio-group
-            v-model="data['radioKey']"
-            @change="handleOneOfChange"
-          >
-            <el-radio
-              v-for="(value, key) in schema.properties"
-              :key="key"
-              :label="key"
-            >
-              {{ key }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-
-        <el-form-item
-          v-for="(value, index) in data.values"
-          :key="index"
-          :label="'Value' + (index + 1)"
-          :rules="{
-            required: true, trigger: 'blur'
-          }"
-        >
-          <el-input v-model="data['values'][index]" />
-          <el-button
-            v-if="data.values.length !== 1"
-            class="remove-value-btn"
-            type="danger"
-            @click.prevent="removeOneOfPropValue(index)"
-          >
-            Remove
-          </el-button>
-        </el-form-item>
-
-        <el-form-item>
-          <el-button
-            :disabled="oneOfPropHasEmptyValue"
-            @click="addValueInput"
-          >
-            {{ $t('button.addValue') }}
-          </el-button>
-        </el-form-item>
-      </el-form>
-
-      <el-form
-        v-if="!schema.oneOf"
-        ref="form"
-        :model="data"
-        :rules="rules"
-        :show-message="false"
-      >
-        <el-form-item
-          v-for="(index, key) in schema.properties"
-          :key="key"
-          :label="key"
-          label-width="160px"
-          :prop="key"
-        >
-          <!-- ๅˆ†ๆƒ…ๅ†ต่ฎจ่ฎบ -->
-          <!-- number property -->
-          <el-input-number
-            v-if="schema.properties[key].type === 'integer' || schema.properties[key].type === 'number'"
-            v-model="data[key]"
-            :min="schema.properties[key].minimum || -99999999999"
-            :max="schema.properties[key].maximum || 99999999999"
-            label="ๆ่ฟฐๆ–‡ๅญ—"
-            @change="onPropertyChange(key, $event)"
-          />
-
-          <!-- enum property -->
-          <el-select
-            v-if="schema.properties[key].hasOwnProperty('enum')"
-            v-model="data[key]"
-            :clearable="true"
-            :placeholder="`Select a ${key}`"
-            @change="onPropertyChange(key, $event)"
-          >
-            <el-option
-              v-for="value in schema.properties[key]['enum']"
-              :key="value"
-              :label="value"
-              :value="value"
-            />
-          </el-select>
-
-          <!-- string property -->
-          <el-input
-            v-if="schema.properties[key].type === 'string' && !schema.properties[key].hasOwnProperty('enum')"
-            v-model="data[key]"
-            :placeholder="key"
-            @input="onPropertyChange(key, $event)"
-          />
-
-          <!-- boolean property -->
-          <el-switch
-            v-if="schema.properties[key].type === 'boolean' && !schema.properties[key].hasOwnProperty('enum')"
-            v-model="data[key]"
-            active-color="#13ce66"
-            inactive-color="#ff4949"
-          />
-
-          <!-- array property -->
-          <div
-            v-if="schema.properties[key].type === 'array'"
-            class="array-input-container"
-          >
-            <el-input
-              v-for="(arrayIndex) in arrayPropertiesLength[key]"
-              :key="arrayIndex"
-              v-model="data[key][arrayIndex]"
-              :placeholder="`${key} [${arrayIndex}]`"
-              @input="isDataChanged = true"
-            />
-
-            <el-button
-              @click="addArrayItem(key)"
-            >
-              {{ $t('button.addValue') }}
-            </el-button>
-          </div>
-
-          <!-- object property -->
-          <div
-            v-if="schema.properties[key].type === 'object'"
-            class="object-input-container"
-          >
-            <div
-              v-for="(objectItem, objectIndex) in objectPropertiesArray[key]"
-              :key="objectIndex"
-              class="object-input-item"
-            >
-              <el-input
-                v-model="objectPropertiesArray[key][objectIndex].key"
-                :placeholder="`${key} [key ${objectIndex}]`"
-                @input="onObjectPropertyChange(key, $event, true)"
-              />
-              <el-input
-                v-model="objectPropertiesArray[key][objectIndex].value"
-                :placeholder="`${key} [value ${objectIndex}]`"
-                @input="onObjectPropertyChange(key, $event, false)"
-              />
-              <el-button
-                @click="deleteObjectItem(key, objectIndex)"
-              >
-                {{ $t('button.delete') }}
-              </el-button>
-            </div>
-            <el-button
-              @click="addObjectItem(key)"
-            >
-              {{ $t('button.addValue') }}
-            </el-button>
-          </div>
-        </el-form-item>
-      </el-form>
-      <span
-        slot="footer"
-        class="dialog-footer"
-      >
-        <el-button
-          @click="onCancel"
-        >
-          {{ $t('button.cancel') }}
-        </el-button>
-        <el-button
-          type="primary"
-          :disabled="!isDataChanged && oneOfPropHasEmptyValue"
-          @click="onSave"
-        >
-          {{ $t('button.confirm') }}
-        </el-button>
-      </span>
-    </el-dialog>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
-
-import { getPluginSchema } from '../../api/schema/schema'
-
-const uuidv1 = require('uuid/v1')
-
-@Component({
-  name: 'PluginDialog'
-})
-export default class extends Vue {
-  @Prop({ default: false }) private show!: boolean
-  @Prop({ default: '' }) private name!: string
-  @Prop({ default: null }) private pluginData!: any
-
-  private schema: any = {
-    properties: {}
-  }
-  private rules: any = {}
-  private data: any = {}
-  private isDataChanged: boolean = false
-  private showDialog: boolean = false
-  private arrayPropertiesLength = {}
-  private objectPropertiesArray = {}
-
-  @Watch('show')
-  private onShowChange(value: boolean) {
-    this.resetPlugin()
-    if (value) {
-      this.getschema(this.name)
-    }
-    this.showDialog = value
-  }
-
-  private resetPlugin() {
-    this.schema = {
-      properties: {}
-    }
-    this.rules = {}
-    this.data = {}
-    this.isDataChanged = false
-  }
-
-  private async getschema(name: string) {
-    const schema = await getPluginSchema(name) as any
-
-    if (!schema.properties) {
-      this.isDataChanged = true
-      return
-    }
-
-    this.schema = Object.assign({}, {
-      ...schema,
-      name: this.name
-    })
-
-    const rules = Object.assign({}, schema.properties)
-
-    for (let pluginName in rules) {
-      const plugin = Object.assign({}, rules[pluginName])
-
-      rules[pluginName] = {
-        trigger: 'blur'
-      }
-
-      if (schema.required) {
-        rules[pluginName].required = schema.required.includes(pluginName)
-      }
-
-      if (plugin.hasOwnProperty('type')) {
-        rules[pluginName]['type'] = plugin['type']
-      }
-
-      if (plugin.hasOwnProperty('minimum')) {
-        rules[pluginName]['min'] = plugin['minimum']
-      }
-
-      if (plugin.hasOwnProperty('maximum')) {
-        rules[pluginName]['max'] = plugin['maximum']
-      }
-
-      if (plugin.hasOwnProperty('enum')) {
-        rules[pluginName]['type'] = 'enum'
-        rules[pluginName]['enum'] = plugin['enum']
-      }
-    }
-
-    this.rules = rules
-
-    // Generate initial data and merge current data
-    let schemaKeys = {}
-    for (let key in schema.properties) {
-      if (schema.properties[key].default) {
-        schemaKeys[key] = schema.properties[key].default
-        continue
-      }
-
-      switch (schema.properties[key].type) {
-        case 'array':
-          schemaKeys[key] = []
-          this.arrayPropertiesLength[key] = [...new Array(this.pluginData[key] ? this.pluginData[key].length : schema.properties[key].minItems).keys()]
-          break
-        case 'object':
-          schemaKeys[key] = {}
-          this.objectPropertiesArray[key] = []
-          if (this.pluginData[key]) {
-            Object.keys(this.pluginData[key]).map(item => {
-              this.objectPropertiesArray[key].push({
-                key: item,
-                value: this.pluginData[key][item]
-              })
-            })
-          }
-          break
-        case 'boolean':
-          schemaKeys[key] = false
-          break
-        default:
-          schemaKeys[key] = ''
-      }
-    }
-
-    if (this.pluginData) {
-      this.data = Object.assign(schemaKeys, this.pluginData)
-    } else {
-      this.data = schemaKeys
-    }
-
-    if (this.name === 'key-auth' && !this.pluginData) {
-      this.data = {
-        key: uuidv1()
-      }
-
-      this.isDataChanged = true
-    }
-
-    // Edit plugin data
-    if (this.name === 'ip-restriction' && this.pluginData) {
-      const key = Object.keys(this.pluginData)[0]
-      this.$nextTick(() => {
-        this.data = {
-          radioKey: key,
-          values: this.pluginData[key]
-        }
-      })
-    }
-
-    // Create new plugin data
-    if (this.schema.oneOf) {
-      this.data = {
-        radioKey: Object.keys(this.schema.properties)[0],
-        values: ['']
-      }
-    }
-  }
-
-  private onCancel() {
-    this.$emit('hidePlugin')
-  }
-
-  private onSave() {
-    (this.$refs.form as any).validate((valid: boolean) => {
-      // ๆ ‡่ฎฐ่ฏฅๆ’ไปถๆ•ฐๆฎๆ˜ฏๅฆ้€š่ฟ‡ๆ ก้ชŒ
-      if (valid) {
-        // Reorganize an array of object properties into objects
-        this.data = Object.assign({}, this.data, this.reorganizeObjectProperty())
-        this.data = this.processOneOfProp(this.data)
-        this.$emit('save', this.name, this.data)
-        this.$message.warning(`${this.$t('message.clickSaveButton')}`)
-      } else {
-        return false
-      }
-    })
-  }
-
-  /**
-   * Add item to array property
-   * @param key
-   */
-  private addArrayItem(key: any) {
-    if (this.arrayPropertiesLength[key].length < this.schema.properties[key].maxItems) {
-      this.arrayPropertiesLength[key].push(this.arrayPropertiesLength[key].length)
-      this.$forceUpdate()
-    } else {
-      this.$message.warning(`${this.$t('message.cannotAddMoreItems')}`)
-    }
-  }
-
-  /**
-   * Add item to object property
-   * @param key
-   */
-  private addObjectItem(key: any) {
-    this.objectPropertiesArray[key].push({
-      key: '',
-      value: ''
-    })
-    this.isDataChanged = true
-    this.$forceUpdate()
-  }
-
-  /**
-   * Delete item form object property
-   * @param key
-   * @param index
-   */
-  private deleteObjectItem(key: any, index: number) {
-    this.objectPropertiesArray[key].splice(index, 1)
-    this.isDataChanged = true
-    this.$forceUpdate()
-  }
-
-  /**
-   * Reorganize an array of object properties into objects
-   */
-  private reorganizeObjectProperty() {
-    let data = {}
-    for (let i in this.objectPropertiesArray) {
-      let objectItem = {}
-      this.objectPropertiesArray[i].map((item: any) => {
-        objectItem[item.key] = item.value
-      })
-      data[i] = objectItem
-    }
-    return data
-  }
-
-  /**
-   * Force rerender on object property content changed
-   */
-  private onObjectPropertyChange() {
-    this.isDataChanged = true
-    this.$forceUpdate()
-  }
-
-  private onPropertyChange(key: any, value: any) {
-    this.data[key] = value
-    this.isDataChanged = true
-  }
-
-  private handleOneOfChange(e: any) {
-    this.data.values = ['']
-  }
-
-  get oneOfPropHasEmptyValue() {
-    return this.data.values ? this.data.values.find((value: string) => value === '') : true
-  }
-
-  private addValueInput() {
-    this.data.values = this.data.values.concat([''])
-  }
-
-  private removeOneOfPropValue(index: number) {
-    this.data.values = this.data.values.filter((item: any, _index: number) => index !== _index)
-  }
-
-  private processOneOfProp(data: any) {
-    if (!this.schema.oneOf) {
-      // remove empty field
-      for (let key in data) {
-        if (data[key] === '') {
-          delete data[key]
-        }
-        if (typeof data[key] === 'object' && Object.keys(data[key]).length === 0) {
-          delete data[key]
-        }
-      }
-      return data
-    }
-
-    return {
-      [this.data.radioKey]: this.data.values
-    }
-  }
-}
-</script>
-
-<style lang="scss">
-.plugin-dialog {
-  .el-form {
-    .el-form-item {
-      .el-form-item__content {
-        .el-input {
-          width: 200px !important;
-        }
-      }
-    }
-  }
-
-  .oneof-plugin-wrapper {
-    .remove-value-btn {
-      margin-left: 10px;
-    }
-  }
-
-  .array-input-container > * {
-    display: flex;
-    margin-top: 5px;
-  }
-
-  .object-input-container > * {
-    display: flex;
-    margin-top: 5px;
-    :not(:first-child){
-      margin-left: 5px;
-    }
-  }
-}
-</style>
diff --git a/src/components/Screenfull/index.vue b/src/components/Screenfull/index.vue
deleted file mode 100644
index 2ad6d56..0000000
--- a/src/components/Screenfull/index.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div id="screenfull">
-    <svg-icon
-      :name="isFullscreen? 'exit-fullscreen': 'fullscreen'"
-      @click="click"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import screenfull from 'screenfull'
-import { Component, Vue } from 'vue-property-decorator'
-
-const sf = screenfull
-
-@Component({
-  name: 'Screenfull'
-})
-export default class extends Vue {
-  private isFullscreen = false
-
-  mounted() {
-    if (sf && sf.enabled) {
-      sf.on('change', this.change)
-    }
-  }
-
-  beforeDestory() {
-    if (sf && sf.enabled) {
-      sf.off('change', this.change)
-    }
-  }
-
-  private change() {
-    if (sf && sf.enabled) {
-      this.isFullscreen = sf.isFullscreen
-    }
-  }
-
-  private click() {
-    if (sf) {
-      if (!sf.enabled) {
-        this.$message({
-          message: 'you browser can not work',
-          type: 'warning'
-        })
-        return false
-      }
-      sf.toggle()
-    }
-  }
-}
-</script>
diff --git a/src/components/VarArgs/index.vue b/src/components/VarArgs/index.vue
deleted file mode 100644
index 1abcdec..0000000
--- a/src/components/VarArgs/index.vue
+++ /dev/null
@@ -1,128 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <el-form>
-    <el-form-item
-      v-for="(item, index) in vars"
-      :key="index"
-      :label="'Var' + (index + 1)"
-      class="var-item"
-    >
-      <el-form-item :prop="'var.' + index + '.ip'">
-        <el-select
-          v-model="item.name"
-          :placeholder="$t('schema.route.inputMultipleValues')"
-          filterable
-          allow-create
-          default-first-option
-          @change="onChange"
-        >
-          <el-option
-            v-for="name in varNames"
-            :key="name"
-            :label="name"
-            :value="name"
-          />
-        </el-select>
-        <el-select
-          v-model="item.operator"
-          placeholder="Operator"
-          @change="onChange"
-        >
-          <el-option
-            v-for="operator in varOperator"
-            :key="operator"
-            :label="operator"
-            :value="operator"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-input
-          v-model="item.value"
-          placeholder=""
-          @input="onChange"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button
-          type="danger"
-          @click.prevent="removeVar(index)"
-        >
-          {{ $t("button.delete") }}
-        </el-button>
-      </el-form-item>
-    </el-form-item>
-    <el-form-item>
-      <el-button @click="addVar">
-        {{ $t("button.add_var") }}
-      </el-button>
-    </el-form-item>
-  </el-form>
-</template>
-
-<script lang="ts">
-import { Component, Vue, Watch, Prop } from 'vue-property-decorator'
-
-@Component({
-  name: 'VarArgs'
-})
-export default class extends Vue {
-  @Prop({ default: () => [] }) private pVars!: any
-  private isFullscreen = false;
-  private get vars() {
-    const _vars = this.pVars.map((arr:Array<any>) => {
-      const [name, operator, value] = arr
-      return { name, operator, value }
-    })
-    return _vars
-  }
-  private varNames = ['remote_addr', 'host', 'uri', 'http_user_agent', 'http_referer', 'http_cookie', 'http_accept_language', 'request_uri', 'query_string', 'remote_port', 'hostname', 'arg_id'];
-
-  private varOperator = ['==', '~=', '>', '<', '~~'];
-
-  private onChange() {
-    const val = this.vars.map((e:any) => Object.values(e))
-    this.$emit('update:pVars', val)
-  }
-
-  private addVar() {
-    (this.vars as any).push({
-      name: null,
-      operator: null,
-      value: null
-    })
-    this.onChange()
-  }
-
-  private removeVar(index:number) {
-    this.$confirm('Do you want to remove the var?', 'Warning', {
-      confirmButtonText: 'Confirm',
-      cancelButtonText: 'Cancel',
-      type: 'warning'
-    })
-      .then(async() => {
-        this.vars.splice(index, 1)
-        this.onChange()
-      })
-      .catch(() => {})
-  }
-}
-</script>
diff --git a/src/lang/en.ts b/src/lang/en.ts
deleted file mode 100644
index 58623f7..0000000
--- a/src/lang/en.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export default {
-  route: {
-    dashboard: 'Dashboard',
-    i18n: 'I18n',
-    profile: 'Profile',
-    SchemaRoutes: 'Routes',
-    SchemaRoutesList: 'Routes',
-    SchemaRoutesEdit: 'Router Edit',
-    SchemaRoutesCreate: 'Router Create',
-    SchemaConsumers: 'Consumers',
-    SchemaConsumersList: 'Consumers',
-    SchemaConsumersEdit: 'Consumer Edit',
-    SchemaConsumersCreate: 'Consumer Create',
-    SchemaServices: 'Services',
-    SchemaServiceList: 'Services',
-    SchemaServiceCreate: 'Service Create',
-    SchemaServiceEdit: 'Service Edit',
-    SchemaSSL: 'SSL',
-    SchemaSSLList: 'SSL',
-    SchemaSSLEdit: 'SSL Edit',
-    SchemaSSLCreate: 'SSL Create',
-    SchemaUpstream: 'Upstream',
-    SchemaUpstreamList: 'Upstream',
-    SchemaUpstreamCreate: 'Upstream Create',
-    SchemaUpstreamEdit: 'Upstream Edit'
-  },
-  table: {
-    add: 'Add',
-    edit: 'Edit',
-    delete: 'Delete',
-    actions: 'Action'
-  },
-  navbar: {
-    logOut: 'Log Out'
-  },
-  login: {
-    title: 'APISIX Login',
-    logIn: 'Login',
-    username: 'Username',
-    password: 'Password'
-  },
-  tagsView: {
-    refresh: 'Refresh',
-    close: 'Close',
-    closeOthers: 'Close Others',
-    closeAll: 'Close All'
-  },
-  button: {
-    save: 'Save',
-    confirm: 'Confirm',
-    cancel: 'Cancel',
-    add_plugin: 'Add Plugin',
-    add_node: 'Add Node',
-    add_var: 'Add Var',
-    delete: 'Delete',
-    addValue: 'Add Value'
-  },
-  schema: {
-    route: {
-      inputMultipleValues: 'Input value then press return button',
-      propertyHostsTip: 'Any values are supported',
-      fileterFunc: 'User-defined filtering function๏ผŒBegins with "function()" and ends with "end".'
-    }
-  },
-  upstream: {
-    websocket: {
-      EnabledTip: 'WebSocket enabled',
-      DisabledTip: 'WebSocket disabled'
-    },
-    keyTip: 'You can edit and input any value here,and press Enter to confirm.',
-    hashOnTip: 'Select a hash on.'
-  },
-  message: {
-    cannotAddMoreItems: 'You cannot add more item!',
-    clickSaveButton: 'Your data will be saved after you click the Save button!'
-  }
-}
diff --git a/src/lang/index.ts b/src/lang/index.ts
deleted file mode 100644
index 2671616..0000000
--- a/src/lang/index.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import Vue from 'vue'
-import VueI18n from 'vue-i18n'
-
-import { getLanguage } from '@/utils/cookies'
-
-// element-ui built-in lang
-import elementEnLocale from 'element-ui/lib/locale/lang/en'
-import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'
-
-// User defined lang
-import enLocale from './en'
-import zhLocale from './zh'
-
-Vue.use(VueI18n)
-
-const messages = {
-  en: {
-    ...enLocale,
-    ...elementEnLocale
-  },
-  zh: {
-    ...zhLocale,
-    ...elementZhLocale
-  }
-}
-
-export const getLocale = () => {
-  const cookieLanguage = getLanguage()
-  if (cookieLanguage) {
-    return cookieLanguage
-  }
-
-  const language = navigator.language.toLowerCase()
-  const locales = Object.keys(messages)
-  for (const locale of locales) {
-    if (language.indexOf(locale) > -1) {
-      return locale
-    }
-  }
-
-  // Default language is english
-  return 'en'
-}
-
-const i18n = new VueI18n({
-  locale: getLocale(),
-  messages
-})
-
-export default i18n
diff --git a/src/lang/zh.ts b/src/lang/zh.ts
deleted file mode 100644
index 5eb56c4..0000000
--- a/src/lang/zh.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export default {
-  route: {
-    dashboard: '้ฆ–้กต',
-    i18n: 'ๅ›ฝ้™…ๅŒ–',
-    profile: 'ไธชไบบไธญๅฟƒ',
-    SchemaRoutes: 'Routes',
-    SchemaRoutesList: 'Routes',
-    SchemaRoutesEdit: '็ผ–่พ‘ Router',
-    SchemaRoutesCreate: 'ๅˆ›ๅปบ Router',
-    SchemaConsumers: 'Consumers',
-    SchemaConsumersList: 'Consumers',
-    SchemaConsumersEdit: '็ผ–่พ‘ Consumer',
-    SchemaConsumersCreate: 'ๅˆ›ๅปบ Consumer',
-    SchemaServices: 'Services',
-    SchemaServiceList: 'Services',
-    SchemaServiceCreate: 'ๅˆ›ๅปบ Service',
-    SchemaServiceEdit: '็ผ–่พ‘ Service',
-    SchemaSSL: 'SSL',
-    SchemaSSLList: 'SSL',
-    SchemaSSLEdit: '็ผ–่พ‘ SSL',
-    SchemaSSLCreate: 'ๅˆ›ๅปบ SSL',
-    SchemaUpstream: 'Upstream',
-    SchemaUpstreamList: 'Upstream',
-    SchemaUpstreamCreate: 'ๅˆ›ๅปบ Upstream',
-    SchemaUpstreamEdit: '็ผ–่พ‘ Upstream'
-  },
-  table: {
-    add: 'ๆทปๅŠ ',
-    edit: '็ผ–่พ‘',
-    delete: 'ๅˆ ้™ค',
-    actions: 'ๆ“ไฝœ'
-  },
-  navbar: {
-    logOut: '้€€ๅ‡บ็™ปๅฝ•'
-  },
-  login: {
-    title: '็ณป็ปŸ็™ปๅฝ•',
-    logIn: '็™ปๅฝ•',
-    username: '่ดฆๅท',
-    password: 'ๅฏ†็ '
-  },
-  tagsView: {
-    refresh: 'ๅˆทๆ–ฐ',
-    close: 'ๅ…ณ้—ญ',
-    closeOthers: 'ๅ…ณ้—ญๅ…ถๅฎƒ',
-    closeAll: 'ๅ…ณ้—ญๆ‰€ๆœ‰'
-  },
-  settings: {
-    title: '็ณป็ปŸๅธƒๅฑ€้…็ฝฎ',
-    theme: 'ไธป้ข˜่‰ฒ',
-    showTagsView: 'ๆ˜พ็คบ Tags-View',
-    showSidebarLogo: 'ๆ˜พ็คบไพง่พนๆ  Logo',
-    fixedHeader: 'ๅ›บๅฎš Header',
-    sidebarTextTheme: 'ไพง่พนๆ ๆ–‡ๅญ—ไธป้ข˜่‰ฒ'
-  },
-  button: {
-    save: 'ไฟๅญ˜',
-    confirm: '็กฎ่ฎค',
-    cancel: 'ๅ–ๆถˆ',
-    add_plugin: 'ๆทปๅŠ  Plugin',
-    add_node: 'ๆทปๅŠ  Node',
-    add_var: 'ๆทปๅŠ  Var',
-    delete: 'ๅˆ ้™ค',
-    addValue: 'ๆทปๅŠ ๅ€ผ'
-  },
-  schema: {
-    route: {
-      inputMultipleValues: '่พ“ๅ…ฅๅ€ผๅŽๅนถๅ›ž่ฝฆ',
-      propertyHostsTip: 'ไปปไฝ•ๅ€ผๅ‡ๅฏ',
-      fileterFunc: '็”จๆˆท่‡ชๅฎšไน‰็š„่ฟ‡ๆปคๅ‡ฝๆ•ฐ๏ผŒไปฅโ€˜function()โ€™ๅผ€ๅคด๏ผŒไปฅโ€˜endโ€™็ป“ๅฐพใ€‚'
-    }
-  },
-  upstream: {
-    websocket: {
-      EnabledTip: 'WebSocket ๆ‰“ๅผ€',
-      DisabledTip: 'WebSocket ๅ…ณ้—ญ'
-    },
-    keyTip: 'ไฝ ๅฏไปฅ็ผ–่พ‘ๅนถ่พ“ๅ…ฅไปปไฝ•ๅ€ผ,่พ“ๅ…ฅๅ€ผๅŽๅ›ž่ฝฆ็กฎ่ฎคใ€‚',
-    hashOnTip: '้€‰ๆ‹ฉ chash ๅ‚ๆ•ฐๆฅๆบใ€‚'
-  },
-  message: {
-    cannotAddMoreItems: 'ไธ่ƒฝๆทปๅŠ ๆ›ดๅคš้กนไบ†๏ผ',
-    clickSaveButton: 'ไฝ ็š„ๆ•ฐๆฎๅฐ†ๅœจ็‚นๅ‡ปไฟๅญ˜ๆŒ‰้’ฎๅŽ่ขซไฟๅญ˜๏ผ'
-  }
-}
diff --git a/src/layout/components/AppMain.vue b/src/layout/components/AppMain.vue
deleted file mode 100644
index e7d12f6..0000000
--- a/src/layout/components/AppMain.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <section class="app-main">
-    <transition
-      name="fade-transform"
-      mode="out-in"
-    >
-      <keep-alive :include="cachedViews">
-        <router-view :key="key" />
-      </keep-alive>
-    </transition>
-  </section>
-</template>
-
-<script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
-import { TagsViewModule } from '@/store/modules/tags-view'
-
-@Component({
-  name: 'AppMain'
-})
-export default class extends Vue {
-  get cachedViews() {
-    return TagsViewModule.cachedViews
-  }
-
-  get key() {
-    return this.$route.path
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.app-main {
-  /* 50= navbar  50  */
-  min-height: calc(100vh - 50px);
-  width: 100%;
-  position: relative;
-  overflow: hidden;
-}
-
-.fixed-header+.app-main {
-  padding-top: 50px;
-  height: 100vh;
-  overflow: auto;
-}
-
-.hasTagsView {
-  .app-main {
-    /* 84 = navbar + tags-view = 50 + 34 */
-    min-height: calc(100vh - 84px);
-  }
-
-  .fixed-header+.app-main {
-    padding-top: 84px;
-  }
-}
-</style>
diff --git a/src/layout/components/Navbar/index.vue b/src/layout/components/Navbar/index.vue
deleted file mode 100644
index 5de0aff..0000000
--- a/src/layout/components/Navbar/index.vue
+++ /dev/null
@@ -1,195 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div class="navbar">
-    <hamburger
-      id="hamburger-container"
-      :is-active="sidebar.opened"
-      class="hamburger-container"
-      @toggleClick="toggleSideBar"
-    />
-    <breadcrumb
-      id="breadcrumb-container"
-      class="breadcrumb-container"
-    />
-    <div class="right-menu">
-      <template v-if="device!=='mobile'">
-        <screenfull class="right-menu-item hover-effect" />
-        <lang-select class="right-menu-item hover-effect" />
-      </template>
-      <el-dropdown
-        class="avatar-container right-menu-item hover-effect"
-        trigger="click"
-      >
-        <div class="avatar-wrapper">
-          <img
-            :src="avatar"
-            class="user-avatar"
-          >
-          <i class="el-icon-caret-bottom" />
-        </div>
-        <el-dropdown-menu slot="dropdown">
-          <el-dropdown-item divided>
-            <span
-              style="display:block;"
-              @click="logout"
-            >{{ $t('navbar.logOut') }}</span>
-          </el-dropdown-item>
-        </el-dropdown-menu>
-      </el-dropdown>
-    </div>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
-import { AppModule } from '@/store/modules/app'
-import { UserModule } from '@/store/modules/user'
-import Breadcrumb from '@/components/Breadcrumb/index.vue'
-import Hamburger from '@/components/Hamburger/index.vue'
-import HeaderSearch from '@/components/HeaderSearch/index.vue'
-import LangSelect from '@/components/LangSelect/index.vue'
-import Screenfull from '@/components/Screenfull/index.vue'
-
-@Component({
-  name: 'Navbar',
-  components: {
-    Breadcrumb,
-    Hamburger,
-    HeaderSearch,
-    LangSelect,
-    Screenfull
-  }
-})
-export default class extends Vue {
-  get sidebar() {
-    return AppModule.sidebar
-  }
-
-  get device() {
-    return AppModule.device.toString()
-  }
-
-  get avatar() {
-    return UserModule.avatar
-  }
-
-  private toggleSideBar() {
-    AppModule.ToggleSideBar(false)
-  }
-
-  private async logout() {
-    await UserModule.LogOut()
-    this.$router.push(`/login?redirect=${this.$route.fullPath}`)
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.navbar {
-  height: 50px;
-  overflow: hidden;
-  position: relative;
-  background: #fff;
-  box-shadow: 0 1px 4px rgba(0,21,41,.08);
-
-  .hamburger-container {
-    line-height: 46px;
-    height: 100%;
-    float: left;
-    padding: 0 15px;
-    cursor: pointer;
-    transition: background .3s;
-    -webkit-tap-highlight-color:transparent;
-
-    &:hover {
-      background: rgba(0, 0, 0, .025)
-    }
-  }
-
-  .breadcrumb-container {
-    float: left;
-  }
-
-  .errLog-container {
-    display: inline-block;
-    vertical-align: top;
-  }
-
-  .right-menu {
-    float: right;
-    height: 100%;
-    line-height: 50px;
-
-    &:focus {
-      outline: none;
-    }
-
-    .right-menu-item {
-      display: inline-block;
-      padding: 0 8px;
-      height: 100%;
-      font-size: 18px;
-      color: #5a5e66;
-      vertical-align: text-bottom;
-
-      &.hover-effect {
-        cursor: pointer;
-        transition: background .3s;
-
-        &:hover {
-          background: rgba(0, 0, 0, .025)
-        }
-      }
-    }
-
-    .avatar-container {
-      margin-right: 30px;
-
-      .avatar-wrapper {
-        margin-top: 5px;
-        position: relative;
-
-        .user-avatar {
-          cursor: pointer;
-          width: 40px;
-          height: 40px;
-          border-radius: 10px;
-        }
-
-        .el-icon-caret-bottom {
-          cursor: pointer;
-          position: absolute;
-          right: -20px;
-          top: 25px;
-          font-size: 12px;
-        }
-      }
-    }
-  }
-}
-</style>
diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue
deleted file mode 100644
index c98fbb0..0000000
--- a/src/layout/components/Sidebar/SidebarItem.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div
-    v-if="!item.meta || !item.meta.hidden"
-    :class="['menu-wrapper', isCollapse ? 'simple-mode' : 'full-mode', {'first-level': isFirstLevel}]"
-  >
-    <template v-if="!alwaysShowRootMenu && theOnlyOneChild && !theOnlyOneChild.children">
-      <sidebar-item-link
-        v-if="theOnlyOneChild.meta"
-        :to="resolvePath(theOnlyOneChild.path)"
-      >
-        <el-menu-item
-          :index="resolvePath(theOnlyOneChild.path)"
-          :class="{'submenu-title-noDropdown': isFirstLevel}"
-        >
-          <svg-icon
-            v-if="theOnlyOneChild.meta.icon"
-            :name="theOnlyOneChild.meta.icon"
-          />
-          <span
-            v-if="theOnlyOneChild.meta.title"
-            slot="title"
-          >{{ $t('route.' + theOnlyOneChild.meta.title) }}</span>
-        </el-menu-item>
-      </sidebar-item-link>
-    </template>
-    <el-submenu
-      v-else
-      :index="resolvePath(item.path)"
-      popper-append-to-body
-    >
-      <template slot="title">
-        <svg-icon
-          v-if="item.meta && item.meta.icon"
-          :name="item.meta.icon"
-        />
-        <span
-          v-if="item.meta && item.meta.title"
-          slot="title"
-        >{{ $t('route.' + item.meta.title) }}</span>
-      </template>
-      <template v-if="item.children">
-        <sidebar-item
-          v-for="child in item.children"
-          :key="child.path"
-          :item="child"
-          :is-collapse="isCollapse"
-          :is-first-level="false"
-          :base-path="resolvePath(child.path)"
-          class="nest-menu"
-        />
-      </template>
-    </el-submenu>
-  </div>
-</template>
-
-<script lang="ts">
-import path from 'path'
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import { Route, RouteConfig } from 'vue-router'
-import { isExternal } from '@/utils/validate'
-import SidebarItemLink from './SidebarItemLink.vue'
-
-@Component({
-  // Set 'name' here to prevent uglifyjs from causing recursive component not work
-  // See https://medium.com/haiiro-io/element-component-name-with-vue-class-component-f3b435656561 for detail
-  name: 'SidebarItem',
-  components: {
-    SidebarItemLink
-  }
-})
-export default class extends Vue {
-  @Prop({ required: true }) private item!: RouteConfig
-  @Prop({ default: false }) private isCollapse!: boolean
-  @Prop({ default: true }) private isFirstLevel!: boolean
-  @Prop({ default: '' }) private basePath!: string
-
-  get alwaysShowRootMenu() {
-    if (this.item.meta && this.item.meta.alwaysShow) {
-      return true
-    }
-    return false
-  }
-
-  get showingChildNumber() {
-    if (this.item.children) {
-      const showingChildren = this.item.children.filter((item) => {
-        if (item.meta && item.meta.hidden) {
-          return false
-        } else {
-          return true
-        }
-      })
-      return showingChildren.length
-    }
-    return 0
-  }
-
-  get theOnlyOneChild() {
-    if (this.showingChildNumber > 1) {
-      return null
-    }
-    if (this.item.children) {
-      for (let child of this.item.children) {
-        if (!child.meta || !child.meta.hidden) {
-          return child
-        }
-      }
-    }
-    // If there is no children, return itself with path removed,
-    // because this.basePath already conatins item's path information
-    return { ...this.item, path: '' }
-  }
-
-  private resolvePath(routePath: string) {
-    if (isExternal(routePath)) {
-      return routePath
-    }
-    if (isExternal(this.basePath)) {
-      return this.basePath
-    }
-    return path.resolve(this.basePath, routePath)
-  }
-}
-</script>
-
-<style lang="scss">
-.el-submenu.is-active > .el-submenu__title {
-  color: $subMenuActiveText !important;
-}
-
-.full-mode {
-  .nest-menu .el-submenu>.el-submenu__title,
-  .el-submenu .el-menu-item {
-    min-width: $sideBarWidth !important;
-    background-color: $subMenuBg !important;
-
-    &:hover {
-      background-color: $subMenuHover !important;
-    }
-  }
-}
-
-.simple-mode {
-  &.first-level {
-    .submenu-title-noDropdown {
-      padding: 0 !important;
-      position: relative;
-
-      .el-tooltip {
-        padding: 0 !important;
-      }
-    }
-
-    .el-submenu {
-      overflow: hidden;
-
-      &>.el-submenu__title {
-        padding: 0px !important;
-
-        .el-submenu__icon-arrow {
-          display: none;
-        }
-
-        &>span {
-          visibility: hidden;
-        }
-      }
-    }
-  }
-}
-</style>
-
-<style lang="scss" scoped>
-.svg-icon {
-  margin-right: 16px;
-}
-
-.simple-mode {
-  .svg-icon {
-    margin-left: 20px;
-  }
-}
-</style>
diff --git a/src/layout/components/Sidebar/SidebarItemLink.vue b/src/layout/components/Sidebar/SidebarItemLink.vue
deleted file mode 100644
index 7c80a86..0000000
--- a/src/layout/components/Sidebar/SidebarItemLink.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <a
-    v-if="isExternal(to)"
-    :href="to"
-    target="_blank"
-    rel="noopener"
-  >
-    <slot />
-  </a>
-  <router-link
-    v-else
-    :to="to"
-  >
-    <slot />
-  </router-link>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import { isExternal } from '@/utils/validate'
-
-@Component({
-  name: 'SidebarItemLink'
-})
-export default class extends Vue {
-  @Prop({ required: true }) private to!: string
-
-  private isExternal = isExternal
-}
-</script>
diff --git a/src/layout/components/Sidebar/SidebarLogo.vue b/src/layout/components/Sidebar/SidebarLogo.vue
deleted file mode 100644
index 4ddf188..0000000
--- a/src/layout/components/Sidebar/SidebarLogo.vue
+++ /dev/null
@@ -1,123 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div
-    class="sidebar-logo-container"
-    :class="{'collapse': collapse}"
-  >
-    <transition name="sidebarLogoFade">
-      <router-link
-        v-if="collapse"
-        key="collapse"
-        class="sidebar-logo-link"
-        to="/"
-      >
-        <img
-          src="favicon.ico"
-          class="sidebar-logo"
-        >
-      </router-link>
-      <router-link
-        v-else
-        key="expand"
-        class="sidebar-logo-link"
-        to="/"
-      >
-        <img
-          src="favicon.ico"
-          class="sidebar-logo"
-        >
-        <h1 class="sidebar-title">
-          {{ title }}
-        </h1>
-      </router-link>
-    </transition>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  name: 'SidebarLogo'
-})
-export default class extends Vue {
-  @Prop({ required: true }) private collapse!: boolean
-
-  private title = 'APISIX'
-}
-</script>
-
-<style lang="scss" scoped>
-.sidebarLogoFade-enter-active {
-  transition: opacity 1.5s;
-}
-
-.sidebarLogoFade-enter,
-.sidebarLogoFade-leave-to {
-  opacity: 0;
-}
-
-.sidebar-logo-container {
-  position: relative;
-  width: 100%;
-  height: 50px;
-  line-height: 50px;
-  background: #304156f5;
-  text-align: center;
-  overflow: hidden;
-
-  & .sidebar-logo-link {
-    height: 100%;
-    width: 100%;
-
-    & .sidebar-logo {
-      width: 32px;
-      height: 32px;
-      vertical-align: middle;
-      margin-right: 12px;
-    }
-
-    & .sidebar-title {
-      display: inline-block;
-      margin: 0;
-      color: #fff;
-      font-weight: 600;
-      line-height: 50px;
-      font-size: 14px;
-      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
-      vertical-align: middle;
-    }
-  }
-
-  &.collapse {
-    .sidebar-logo {
-      margin-right: 0px;
-    }
-  }
-}
-</style>
diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue
deleted file mode 100644
index cfd367e..0000000
--- a/src/layout/components/Sidebar/index.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div :class="{'has-logo': showLogo}">
-    <sidebar-logo
-      v-if="showLogo"
-      :collapse="isCollapse"
-    />
-    <el-scrollbar wrap-class="scrollbar-wrapper">
-      <el-menu
-        :default-active="activeMenu"
-        :collapse="isCollapse"
-        :background-color="variables.menuBg"
-        :text-color="variables.menuText"
-        :active-text-color="menuActiveTextColor"
-        :unique-opened="false"
-        :collapse-transition="false"
-        mode="vertical"
-      >
-        <sidebar-item
-          v-for="route in routes"
-          :key="route.path"
-          :item="route"
-          :base-path="route.path"
-          :is-collapse="isCollapse"
-        />
-      </el-menu>
-    </el-scrollbar>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import { AppModule } from '@/store/modules/app'
-import { PermissionModule } from '@/store/modules/permission'
-import { SettingsModule } from '@/store/modules/settings'
-import SidebarItem from './SidebarItem.vue'
-import SidebarLogo from './SidebarLogo.vue'
-import variables from '@/styles/_variables.scss'
-
-@Component({
-  name: 'SideBar',
-  components: {
-    SidebarItem,
-    SidebarLogo
-  }
-})
-export default class extends Vue {
-  get sidebar() {
-    return AppModule.sidebar
-  }
-
-  get routes() {
-    return PermissionModule.routes
-  }
-
-  get showLogo() {
-    return SettingsModule.showSidebarLogo
-  }
-
-  get menuActiveTextColor() {
-    if (SettingsModule.sidebarTextTheme) {
-      return SettingsModule.theme
-    } else {
-      return variables.menuActiveText
-    }
-  }
-
-  get variables() {
-    return variables
-  }
-
-  get activeMenu() {
-    const route = this.$route
-    const { meta, path } = route
-    // if set path, the sidebar will highlight the path you set
-    if (meta.activeMenu) {
-      return meta.activeMenu
-    }
-    return path
-  }
-
-  get isCollapse() {
-    return !this.sidebar.opened
-  }
-}
-</script>
-
-<style lang="scss">
-.sidebar-container {
-  // reset element-ui css
-  .horizontal-collapse-transition {
-    transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
-  }
-
-  .scrollbar-wrapper {
-    overflow-x: hidden !important;
-  }
-
-  .el-scrollbar__view {
-    height: 100%
-  }
-
-  .el-scrollbar__bar {
-    &.is-vertical {
-      right: 0px;
-    }
-
-    &.is-horizontal {
-      display: none;
-    }
-  }
-}
-</style>
-
-<style lang="scss" scoped>
-.el-scrollbar {
-  height: 100%
-}
-
-.has-logo {
-  .el-scrollbar {
-    height: calc(100% - 50px);
-  }
-}
-
-.el-menu {
-  border: none;
-  height: 100%;
-  width: 100% !important;
-}
-</style>
diff --git a/src/layout/components/TagsView/ScrollPane.vue b/src/layout/components/TagsView/ScrollPane.vue
deleted file mode 100644
index 77c61e1..0000000
--- a/src/layout/components/TagsView/ScrollPane.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <el-scrollbar
-    ref="scrollContainer"
-    :vertical="false"
-    class="scroll-container"
-    @wheel.native.prevent="handleScroll"
-  >
-    <slot />
-  </el-scrollbar>
-</template>
-
-<script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
-
-const tagSpacing = 4
-
-@Component({
-  name: 'ScrollPane'
-})
-export default class extends Vue {
-  private handleScroll(e: MouseWheelEvent) {
-    const eventDelta = (e as any).wheelDelta || -e.deltaY * 40
-    const scrollContainer = this.$refs.scrollContainer as Vue
-    const scrollWrapper = scrollContainer.$refs.wrap as HTMLElement
-    scrollWrapper.scrollLeft = scrollWrapper.scrollLeft + eventDelta / 4
-  }
-
-  public moveToTarget(currentTag: HTMLElement) {
-    const scrollContainer = this.$refs.scrollContainer as Vue
-    const container = scrollContainer.$el as HTMLElement
-    const containerWidth = container.offsetWidth
-    const scrollWrapper = scrollContainer.$refs.wrap as HTMLElement
-    const tagList = this.$parent.$refs.tag as any[]
-
-    let firstTag = null
-    let lastTag = null
-
-    // find first tag and last tag
-    if (tagList.length > 0) {
-      firstTag = tagList[0]
-      lastTag = tagList[tagList.length - 1]
-    }
-
-    if (firstTag === currentTag) {
-      scrollWrapper.scrollLeft = 0
-    } else if (lastTag === currentTag) {
-      scrollWrapper.scrollLeft = scrollWrapper.scrollWidth - containerWidth
-    } else {
-      // find preTag and nextTag
-      const currentIndex = tagList.findIndex(item => item === currentTag)
-      const prevTag = tagList[currentIndex - 1]
-      const nextTag = tagList[currentIndex + 1]
-      // the tag's offsetLeft after of nextTag
-      const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagSpacing
-      // the tag's offsetLeft before of prevTag
-      const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagSpacing
-
-      if (afterNextTagOffsetLeft > scrollWrapper.scrollLeft + containerWidth) {
-        scrollWrapper.scrollLeft = afterNextTagOffsetLeft - containerWidth
-      } else if (beforePrevTagOffsetLeft < scrollWrapper.scrollLeft) {
-        scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss">
-.scroll-container {
-  .el-scrollbar__bar {
-    bottom: 0px;
-  }
-
-  .el-scrollbar__wrap {
-    height: 49px;
-  }
-}
-</style>
-
-<style lang="scss" scoped>
-.scroll-container {
-  white-space: nowrap;
-  position: relative;
-  overflow: hidden;
-  width: 100%;
-}
-</style>
diff --git a/src/layout/components/TagsView/index.vue b/src/layout/components/TagsView/index.vue
deleted file mode 100644
index d87419c..0000000
--- a/src/layout/components/TagsView/index.vue
+++ /dev/null
@@ -1,359 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div
-    id="tags-view-container"
-    class="tags-view-container"
-  >
-    <scroll-pane
-      ref="scrollPane"
-      class="tags-view-wrapper"
-    >
-      <router-link
-        v-for="tag in visitedViews"
-        ref="tag"
-        :key="tag.path"
-        :class="isActive(tag) ? 'active' : ''"
-        :to="{path: tag.path, query: tag.query, fullPath: tag.fullPath}"
-        tag="span"
-        class="tags-view-item"
-        @click.middle.native="closeSelectedTag(tag)"
-        @contextmenu.prevent.native="openMenu(tag, $event)"
-      >
-        {{ $t('route.' + tag.meta.title) }}
-        <span
-          v-if="!tag.meta.affix"
-          class="el-icon-close"
-          @click.prevent.stop="closeSelectedTag(tag)"
-        />
-      </router-link>
-    </scroll-pane>
-    <ul
-      v-show="visible"
-      :style="{left: left+'px', top: top+'px'}"
-      class="contextmenu"
-    >
-      <li @click="refreshSelectedTag(selectedTag)">
-        {{ $t('tagsView.refresh') }}
-      </li>
-      <li
-        v-if="!(selectedTag.meta&&selectedTag.meta.affix)"
-        @click="closeSelectedTag(selectedTag)"
-      >
-        {{
-          $t('tagsView.close') }}
-      </li>
-      <li @click="closeOthersTags">
-        {{ $t('tagsView.closeOthers') }}
-      </li>
-      <li @click="closeAllTags(selectedTag)">
-        {{ $t('tagsView.closeAll') }}
-      </li>
-    </ul>
-  </div>
-</template>
-
-<script lang="ts">
-import path from 'path'
-import { Component, Vue, Watch } from 'vue-property-decorator'
-import VueRouter, { Route, RouteRecord, RouteConfig } from 'vue-router'
-import { PermissionModule } from '@/store/modules/permission'
-import { TagsViewModule, ITagView } from '@/store/modules/tags-view'
-import ScrollPane from './ScrollPane.vue'
-
-@Component({
-  name: 'TagsView',
-  components: {
-    ScrollPane
-  }
-})
-export default class extends Vue {
-  private visible: boolean = false
-  private top: number = 0
-  private left: number = 0
-  private selectedTag: ITagView = {}
-  private affixTags: ITagView[] = []
-
-  get visitedViews() {
-    return TagsViewModule.visitedViews
-  }
-
-  get routes() {
-    return PermissionModule.routes
-  }
-
-  @Watch('$route')
-  private onRouteChange() {
-    this.addTags()
-    this.moveToCurrentTag()
-  }
-
-  @Watch('visible')
-  private onVisibleChange(value: boolean) {
-    if (value) {
-      document.body.addEventListener('click', this.closeMenu)
-    } else {
-      document.body.removeEventListener('click', this.closeMenu)
-    }
-  }
-
-  mounted() {
-    this.initTags()
-    this.addTags()
-  }
-
-  private isActive(route: ITagView) {
-    return route.path === this.$route.path
-  }
-
-  private filterAffixTags(routes: RouteConfig[], basePath = '/') {
-    let tags: ITagView[] = []
-    routes.forEach(route => {
-      if (route.meta && route.meta.affix) {
-        const tagPath = path.resolve(basePath, route.path)
-        tags.push({
-          fullPath: tagPath,
-          path: tagPath,
-          name: route.name,
-          meta: { ...route.meta }
-        })
-      }
-      if (route.children) {
-        const childTags = this.filterAffixTags(route.children, route.path)
-        if (childTags.length >= 1) {
-          tags = [...tags, ...childTags]
-        }
-      }
-    })
-    return tags
-  }
-
-  private initTags() {
-    this.affixTags = this.filterAffixTags(this.routes)
-    for (const tag of this.affixTags) {
-      // Must have tag name
-      if (tag.name) {
-        TagsViewModule.addVisitedView(tag)
-      }
-    }
-  }
-
-  private addTags() {
-    const { name } = this.$route
-    if (name) {
-      TagsViewModule.addView(this.$route)
-    }
-    return false
-  }
-
-  private moveToCurrentTag() {
-    const tags = this.$refs.tag as any[] // TODO: better typescript support for router-link
-    this.$nextTick(() => {
-      for (const tag of tags) {
-        if ((tag.to as ITagView).path === this.$route.path) {
-          (this.$refs.scrollPane as ScrollPane).moveToTarget(tag as any)
-          // When query is different then update
-          if ((tag.to as ITagView).fullPath !== this.$route.fullPath) {
-            TagsViewModule.updateVisitedView(this.$route)
-          }
-          break
-        }
-      }
-    })
-  }
-
-  private refreshSelectedTag(view: ITagView) {
-    TagsViewModule.delCachedView(view)
-    const { fullPath } = view
-    this.$nextTick(() => {
-      this.$router.replace({
-        path: '/redirect' + fullPath
-      })
-    })
-  }
-
-  private closeSelectedTag(view: ITagView) {
-    TagsViewModule.delView(view)
-    if (this.isActive(view)) {
-      this.toLastView(TagsViewModule.visitedViews, view)
-    }
-  }
-
-  private closeOthersTags() {
-    this.$router.push(this.selectedTag)
-    TagsViewModule.delOthersViews(this.selectedTag)
-    this.moveToCurrentTag()
-  }
-
-  private closeAllTags(view: ITagView) {
-    TagsViewModule.delAllViews()
-    if (this.affixTags.some(tag => tag.path === this.$route.path)) {
-      return
-    }
-    this.toLastView(TagsViewModule.visitedViews, view)
-  }
-
-  private toLastView(visitedViews: ITagView[], view: ITagView) {
-    const latestView = visitedViews.slice(-1)[0]
-    if (latestView) {
-      this.$router.push(latestView)
-    } else {
-      // Default redirect to the home page if there is no tags-view, adjust it if you want
-      if (view.name === 'Dashboard') {
-        // to reload home page
-        this.$router.replace({ path: '/redirect' + view.fullPath })
-      } else {
-        this.$router.push('/')
-      }
-    }
-  }
-
-  private openMenu(tag: ITagView, e: MouseEvent) {
-    const menuMinWidth = 105
-    const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
-    const offsetWidth = (this.$el as HTMLElement).offsetWidth // container width
-    const maxLeft = offsetWidth - menuMinWidth // left boundary
-    const left = e.clientX - offsetLeft + 15 // 15: margin right
-    if (left > maxLeft) {
-      this.left = maxLeft
-    } else {
-      this.left = left
-    }
-    this.top = e.clientY
-    this.visible = true
-    this.selectedTag = tag
-  }
-
-  private closeMenu() {
-    this.visible = false
-  }
-}
-</script>
-
-<style lang="scss">
-// Reset element css of el-icon-close
-.tags-view-wrapper {
-  .tags-view-item {
-    .el-icon-close {
-      width: 16px;
-      height: 16px;
-      vertical-align: 2px;
-      border-radius: 50%;
-      text-align: center;
-      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-      transform-origin: 100% 50%;
-
-      &:before {
-        transform: scale(0.6);
-        display: inline-block;
-        vertical-align: -3px;
-      }
-
-      &:hover {
-        background-color: #b4bccc;
-        color: #fff;
-      }
-    }
-  }
-}
-
-</style>
-<style lang="scss" scoped>
-.tags-view-container {
-  height: 34px;
-  width: 100%;
-  background: #fff;
-  border-bottom: 1px solid #d8dce5;
-  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
-
-  .tags-view-wrapper {
-    .tags-view-item {
-      display: inline-block;
-      position: relative;
-      cursor: pointer;
-      height: 26px;
-      line-height: 26px;
-      border: 1px solid #d8dce5;
-      color: #495060;
-      background: #fff;
-      padding: 0 8px;
-      font-size: 12px;
-      margin-left: 5px;
-      margin-top: 4px;
-
-      &:first-of-type {
-        margin-left: 15px;
-      }
-
-      &:last-of-type {
-        margin-right: 15px;
-      }
-
-      &.active {
-        background-color: #42b983;
-        color: #fff;
-        border-color: #42b983;
-
-        &::before {
-          content: '';
-          background: #fff;
-          display: inline-block;
-          width: 8px;
-          height: 8px;
-          border-radius: 50%;
-          position: relative;
-          margin-right: 2px;
-        }
-      }
-    }
-  }
-
-  .contextmenu {
-    margin: 0;
-    background: #fff;
-    z-index: 3000;
-    position: absolute;
-    list-style-type: none;
-    padding: 5px 0;
-    border-radius: 4px;
-    font-size: 12px;
-    font-weight: 400;
-    color: #333;
-    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
-
-    li {
-      margin: 0;
-      padding: 7px 16px;
-      cursor: pointer;
-
-      &:hover {
-        background: #eee;
-      }
-    }
-  }
-}
-</style>
diff --git a/src/layout/components/index.ts b/src/layout/components/index.ts
deleted file mode 100644
index ec44cf9..0000000
--- a/src/layout/components/index.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-export { default as AppMain } from './AppMain.vue'
-export { default as Navbar } from './Navbar/index.vue'
-export { default as Sidebar } from './Sidebar/index.vue'
-export { default as TagsView } from './TagsView/index.vue'
diff --git a/src/layout/index.vue b/src/layout/index.vue
deleted file mode 100644
index 7379e7d..0000000
--- a/src/layout/index.vue
+++ /dev/null
@@ -1,192 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div
-    :class="classObj"
-    class="app-wrapper"
-  >
-    <div
-      v-if="classObj.mobile && sidebar.opened"
-      class="drawer-bg"
-      @click="handleClickOutside"
-    />
-    <sidebar class="sidebar-container" />
-    <div
-      :class="{hasTagsView: showTagsView}"
-      class="main-container"
-    >
-      <div :class="{'fixed-header': fixedHeader}">
-        <navbar />
-        <tags-view v-if="showTagsView" />
-      </div>
-      <app-main />
-    </div>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component } from 'vue-property-decorator'
-import { mixins } from 'vue-class-component'
-import { DeviceType, AppModule } from '@/store/modules/app'
-import { SettingsModule } from '@/store/modules/settings'
-import { AppMain, Navbar, Sidebar, TagsView } from './components'
-import ResizeMixin from './mixin/resize'
-
-@Component({
-  name: 'Layout',
-  components: {
-    AppMain,
-    Navbar,
-    Sidebar,
-    TagsView
-  }
-})
-export default class extends mixins(ResizeMixin) {
-  get classObj() {
-    return {
-      hideSidebar: !this.sidebar.opened,
-      openSidebar: this.sidebar.opened,
-      withoutAnimation: this.sidebar.withoutAnimation,
-      mobile: this.device === DeviceType.Mobile
-    }
-  }
-
-  get showSettings() {
-    return SettingsModule.showSettings
-  }
-
-  get showTagsView() {
-    return SettingsModule.showTagsView
-  }
-
-  get fixedHeader() {
-    return SettingsModule.fixedHeader
-  }
-
-  private handleClickOutside() {
-    AppModule.CloseSideBar(false)
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.app-wrapper {
-  @include clearfix;
-  position: relative;
-  height: 100%;
-  width: 100%;
-}
-
-.drawer-bg {
-  background: #000;
-  opacity: 0.3;
-  width: 100%;
-  top: 0;
-  height: 100%;
-  position: absolute;
-  z-index: 999;
-}
-
-.main-container {
-  min-height: 100%;
-  transition: margin-left .28s;
-  margin-left: $sideBarWidth;
-  position: relative;
-}
-
-.sidebar-container {
-  transition: width 0.28s;
-  width: $sideBarWidth !important;
-  height: 100%;
-  position: fixed;
-  font-size: 0px;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  z-index: 1001;
-  overflow: hidden;
-}
-
-.fixed-header {
-  position: fixed;
-  top: 0;
-  right: 0;
-  z-index: 9;
-  width: calc(100% - #{$sideBarWidth});
-  transition: width 0.28s;
-}
-
-.hideSidebar {
-  .main-container {
-    margin-left: 54px;
-  }
-
-  .sidebar-container {
-    width: 54px !important;
-  }
-
-  .fixed-header {
-    width: calc(100% - 54px)
-  }
-}
-
-/* for mobile response ้€‚้…็งปๅŠจ็ซฏ */
-.mobile {
-  .main-container {
-    margin-left: 0px;
-  }
-
-  .sidebar-container {
-    transition: transform .28s;
-    width: $sideBarWidth !important;
-  }
-
-  &.openSidebar {
-    position: fixed;
-    top: 0;
-  }
-
-  &.hideSidebar {
-    .sidebar-container {
-      pointer-events: none;
-      transition-duration: 0.3s;
-      transform: translate3d(-$sideBarWidth, 0, 0);
-    }
-  }
-
-  .fixed-header {
-    width: 100%;
-  }
-}
-
-.withoutAnimation {
-  .main-container,
-  .sidebar-container {
-    transition: none;
-  }
-}
-</style>
diff --git a/src/layout/mixin/resize.ts b/src/layout/mixin/resize.ts
deleted file mode 100644
index 8935e69..0000000
--- a/src/layout/mixin/resize.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import { Component, Vue, Watch } from 'vue-property-decorator'
-import { AppModule, DeviceType } from '@/store/modules/app'
-
-const WIDTH = 992 // refer to Bootstrap's responsive design
-
-@Component({
-  name: 'ResizeMixin'
-})
-export default class extends Vue {
-  get device() {
-    return AppModule.device
-  }
-
-  get sidebar() {
-    return AppModule.sidebar
-  }
-
-  @Watch('$route')
-  private onRouteChange() {
-    if (this.device === DeviceType.Mobile && this.sidebar.opened) {
-      AppModule.CloseSideBar(false)
-    }
-  }
-
-  beforeMount() {
-    window.addEventListener('resize', this.resizeHandler)
-  }
-
-  mounted() {
-    const isMobile = this.isMobile()
-    if (isMobile) {
-      AppModule.ToggleDevice(DeviceType.Mobile)
-      AppModule.CloseSideBar(true)
-    }
-  }
-
-  beforeDestroy() {
-    window.removeEventListener('resize', this.resizeHandler)
-  }
-
-  private isMobile() {
-    const rect = document.body.getBoundingClientRect()
-    return rect.width - 1 < WIDTH
-  }
-
-  private resizeHandler() {
-    if (!document.hidden) {
-      const isMobile = this.isMobile()
-      AppModule.ToggleDevice(isMobile ? DeviceType.Mobile : DeviceType.Desktop)
-      if (isMobile) {
-        AppModule.CloseSideBar(true)
-      }
-    }
-  }
-}
diff --git a/src/main.ts b/src/main.ts
deleted file mode 100644
index 88e5dec..0000000
--- a/src/main.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import Vue from 'vue'
-
-import 'normalize.css'
-import ElementUI from 'element-ui'
-import SvgIcon from 'vue-svgicon'
-import '@/styles/index.scss'
-import Component from 'vue-class-component'
-
-import App from '@/App.vue'
-import store from '@/store'
-import { AppModule } from '@/store/modules/app'
-import router from '@/router'
-import i18n from '@/lang'
-import '@/permission'
-
-Vue.use(ElementUI, {
-  size: AppModule.size, // Set element-ui default size
-  i18n: (key: string, value: string) => i18n.t(key, value)
-})
-
-Vue.use(SvgIcon, {
-  tagName: 'svg-icon',
-  defaultWidth: '1em',
-  defaultHeight: '1em'
-})
-
-Vue.config.productionTip = false
-
-Component.registerHooks([
-  'beforeRouteEnter',
-  'beforeRouteLeave',
-  'beforeRouteUpdate'
-])
-
-new Vue({
-  router,
-  store,
-  i18n,
-  render: (h) => h(App)
-}).$mount('#app')
diff --git a/src/permission.ts b/src/permission.ts
deleted file mode 100644
index b5194aa..0000000
--- a/src/permission.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
-*/
-
-import router from './router'
-import NProgress from 'nprogress'
-import 'nprogress/nprogress.css'
-import { Message } from 'element-ui'
-import { Route } from 'vue-router'
-import { UserModule } from '@/store/modules/user'
-import { PermissionModule } from '@/store/modules/permission'
-import i18n from '@/lang' // Internationalization
-import settings from './settings'
-
-NProgress.configure({ showSpinner: false })
-
-const whiteList = ['/login', '/auth-redirect']
-
-const getPageTitle = (key: string) => {
-  const hasKey = i18n.te(`route.${key}`)
-  if (hasKey) {
-    const pageName = i18n.t(`route.${key}`)
-    return `${pageName} - ${settings.title}`
-  }
-  return `${settings.title}`
-}
-
-router.beforeEach(async(to: Route, _: Route, next: any) => {
-  // Start progress bar
-  NProgress.start()
-
-  // Determine whether the user has logged in
-  if (UserModule.token) {
-    if (to.path === '/login') {
-      // If is logged in, redirect to the home page
-      next({ path: '/' })
-      NProgress.done()
-    } else {
-      // Check whether the user has obtained his permission roles
-      if (UserModule.roles.length === 0) {
-        try {
-          // Note: roles must be a object array! such as: ['admin'] or ['developer', 'editor']
-          await UserModule.GetUserInfo()
-          const roles = UserModule.roles
-          // Generate accessible routes map based on role
-          PermissionModule.GenerateRoutes(roles)
-          // Dynamically add accessible routes
-          router.addRoutes(PermissionModule.dynamicRoutes)
-          // Hack: ensure addRoutes is complete
-          // Set the replace: true, so the navigation will not leave a history record
-          next({ ...to, replace: true })
-        } catch (err) {
-          // Remove token and redirect to login page
-          UserModule.ResetToken()
-          Message.error(err || 'Has Error')
-          next(`/login?redirect=${to.path}`)
-          NProgress.done()
-        }
-      } else {
-        next()
-      }
-    }
-  } else {
-    // Has no token
-    if (whiteList.indexOf(to.path) !== -1) {
-      // In the free login whitelist, go directly
-      next()
-    } else {
-      // Other pages that do not have permission to access are redirected to the login page.
-      next(`/login?redirect=${to.path}`)
-      NProgress.done()
-    }
-  }
-})
-
-router.afterEach((to: Route) => {
-  // Finish progress bar
-  NProgress.done()
-
-  // set page title
-  document.title = getPageTitle(to.meta.title)
-})
diff --git a/src/router/index.ts b/src/router/index.ts
deleted file mode 100644
index 2ab4a5d..0000000
--- a/src/router/index.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import Vue from 'vue'
-import Router, { RouteConfig } from 'vue-router'
-
-/* Layout */
-import Layout from '@/layout/index.vue'
-
-/* Router modules */
-import SchemaRoutesRouter from './modules/schema/routes'
-import SchemaConsumersRouter from './modules/schema/consumers'
-import SchemaServicesRouter from './modules/schema/services'
-import SchemaSSLRouter from './modules/schema/ssl'
-import SchemaUpstreamRouter from './modules/schema/upstream'
-
-Vue.use(Router)
-
-export const constantRoutes: RouteConfig[] = [
-  {
-    path: '/login',
-    component: () => import('@/views/login/index.vue'),
-    meta: { hidden: true }
-  },
-  {
-    path: '/',
-    component: Layout,
-    redirect: '/schema/routes/list'
-  }
-]
-
-/**
- * asyncRoutes
- * the routes that need to be dynamically loaded based on user roles
-*/
-export const asyncRoutes: RouteConfig[] = [
-  SchemaRoutesRouter,
-  SchemaConsumersRouter,
-  SchemaServicesRouter,
-  SchemaSSLRouter,
-  SchemaUpstreamRouter,
-  {
-    path: '*',
-    redirect: '/404',
-    meta: { hidden: true }
-  }
-]
-
-const createRouter = () => new Router({
-  scrollBehavior: (to, from, savedPosition) => {
-    if (savedPosition) {
-      return savedPosition
-    } else {
-      return { x: 0, y: 0 }
-    }
-  },
-  base: process.env.BASE_URL,
-  routes: constantRoutes
-})
-
-const router = createRouter()
-
-export function resetRouter() {
-  const newRouter = createRouter();
-  (router as any).matcher = (newRouter as any).matcher // reset router
-}
-
-export default router
diff --git a/src/router/modules/schema/consumers.ts b/src/router/modules/schema/consumers.ts
deleted file mode 100644
index e9d53b1..0000000
--- a/src/router/modules/schema/consumers.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { RouteConfig } from 'vue-router'
-import Layout from '@/layout/index.vue'
-
-const tableRoutes: RouteConfig = {
-  path: '/schema/consumers',
-  component: Layout,
-  name: 'SchemaConsumers',
-  meta: {
-    title: 'SchemaConsumers',
-    icon: 'table'
-  },
-  redirect: '/schema/consumers/list',
-  children: [
-    {
-      path: 'list',
-      component: () => import(/* webpackChunkName: "complex-table" */ '@/views/schema/consumers/list.vue'),
-      name: 'SchemaConsumersList',
-      meta: { title: 'SchemaConsumersList' }
-    }, {
-      path: 'edit/:username',
-      component: () => import('@/views/schema/consumers/edit.vue'),
-      name: 'SchemaConsumersEdit',
-      meta: {
-        title: 'SchemaConsumersEdit',
-        hidden: true
-      }
-    }, {
-      path: 'create',
-      component: () => import('@/views/schema/consumers/edit.vue'),
-      name: 'SchemaConsumersCreate',
-      meta: {
-        title: 'SchemaConsumersCreate',
-        hidden: true
-      }
-    }
-  ]
-}
-
-export default tableRoutes
diff --git a/src/router/modules/schema/routes.ts b/src/router/modules/schema/routes.ts
deleted file mode 100644
index ef7a2f2..0000000
--- a/src/router/modules/schema/routes.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { RouteConfig } from 'vue-router'
-import Layout from '@/layout/index.vue'
-
-const tableRoutes: RouteConfig = {
-  path: '/schema/routes',
-  component: Layout,
-  name: 'SchemaRoutes',
-  meta: {
-    title: 'SchemaRoutes',
-    icon: 'table'
-  },
-  redirect: '/schema/routes/list',
-  children: [
-    {
-      path: 'list',
-      component: () => import('@/views/schema/routes/list.vue'),
-      name: 'SchemaRoutesList',
-      meta: { title: 'SchemaRoutesList' }
-    }, {
-      path: 'edit/:id',
-      component: () => import('@/views/schema/routes/edit.vue'),
-      name: 'SchemaRoutesEdit',
-      meta: {
-        title: 'SchemaRoutesEdit',
-        hidden: true
-      }
-    }, {
-      path: 'create',
-      component: () => import('@/views/schema/routes/edit.vue'),
-      name: 'SchemaRoutesCreate',
-      meta: {
-        title: 'SchemaRoutesCreate',
-        hidden: true
-      }
-    }
-  ]
-}
-
-export default tableRoutes
diff --git a/src/router/modules/schema/services.ts b/src/router/modules/schema/services.ts
deleted file mode 100644
index cb35d70..0000000
--- a/src/router/modules/schema/services.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { RouteConfig } from 'vue-router'
-import Layout from '@/layout/index.vue'
-
-const tableRoutes: RouteConfig = {
-  path: '/schema/services',
-  component: Layout,
-  name: 'SchemaServices',
-  meta: {
-    title: 'SchemaServices',
-    icon: 'table'
-  },
-  redirect: '/schema/services/list',
-  children: [
-    {
-      path: 'list',
-      component: () => import('@/views/schema/service/list.vue'),
-      name: 'SchemaServiceList',
-      meta: { title: 'SchemaServiceList' }
-    }, {
-      path: 'edit/:id',
-      component: () => import('@/views/schema/service/edit.vue'),
-      name: 'SchemaServiceEdit',
-      meta: {
-        title: 'SchemaServiceEdit',
-        hidden: true
-      }
-    }, {
-      path: 'create',
-      component: () => import('@/views/schema/service/edit.vue'),
-      name: 'SchemaServiceCreate',
-      meta: {
-        title: 'SchemaServiceCreate',
-        hidden: true
-      }
-    }
-  ]
-}
-
-export default tableRoutes
diff --git a/src/router/modules/schema/ssl.ts b/src/router/modules/schema/ssl.ts
deleted file mode 100644
index 5afb069..0000000
--- a/src/router/modules/schema/ssl.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { RouteConfig } from 'vue-router'
-import Layout from '@/layout/index.vue'
-
-const tableRoutes: RouteConfig = {
-  path: '/schema/ssl',
-  component: Layout,
-  name: 'SchemaSSL',
-  meta: {
-    title: 'SchemaSSL',
-    icon: 'table'
-  },
-  redirect: '/schema/ssl/list',
-  children: [
-    {
-      path: 'list',
-      component: () => import('@/views/schema/ssl/list.vue'),
-      name: 'SchemaSSLList',
-      meta: { title: 'SchemaSSLList' }
-    }, {
-      path: 'edit/:id',
-      component: () => import('@/views/schema/ssl/edit.vue'),
-      name: 'SchemaSSLEdit',
-      meta: {
-        title: 'SchemaSSLEdit',
-        hidden: true
-      }
-    }, {
-      path: 'create',
-      component: () => import('@/views/schema/ssl/edit.vue'),
-      name: 'SchemaSSLCreate',
-      meta: {
-        title: 'SchemaSSLCreate',
-        hidden: true
-      }
-    }
-  ]
-}
-
-export default tableRoutes
diff --git a/src/router/modules/schema/upstream.ts b/src/router/modules/schema/upstream.ts
deleted file mode 100644
index e803687..0000000
--- a/src/router/modules/schema/upstream.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { RouteConfig } from 'vue-router'
-import Layout from '@/layout/index.vue'
-
-const tableUpstream: RouteConfig = {
-  path: '/schema/upstream',
-  component: Layout,
-  name: 'SchemaUpstream',
-  meta: {
-    title: 'SchemaUpstream',
-    icon: 'table'
-  },
-  redirect: '/schema/upstream/list',
-  children: [
-    {
-      path: 'list',
-      component: () => import('@/views/schema/upstream/list.vue'),
-      name: 'SchemaUpstreamList',
-      meta: { title: 'SchemaUpstreamList' }
-    }, {
-      path: 'edit/:id',
-      component: () => import('@/views/schema/upstream/edit.vue'),
-      name: 'SchemaUpstreamEdit',
-      meta: {
-        title: 'SchemaUpstreamEdit',
-        hidden: true
-      }
-    }, {
-      path: 'create',
-      component: () => import('@/views/schema/upstream/edit.vue'),
-      name: 'SchemaUpstreamCreate',
-      meta: {
-        title: 'SchemaUpstreamCreate',
-        hidden: true
-      }
-    }
-  ]
-}
-
-export default tableUpstream
diff --git a/src/settings.ts b/src/settings.ts
deleted file mode 100644
index 91ccd73..0000000
--- a/src/settings.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
-*/
-
-interface ISettings {
-  title: string // Overrides the default title
-  showSettings: boolean // Controls settings panel display
-  showTagsView: boolean // Controls tagsview display
-  showSidebarLogo: boolean // Controls siderbar logo display
-  fixedHeader: boolean // If true, will fix the header component
-  errorLog: string[] // The env to enable the errorlog component, default 'production' only
-  sidebarTextTheme: boolean // If true, will change active text color for sidebar based on theme
-  devServerPort: number // Port number for webpack-dev-server
-}
-
-// You can customize below settings :)
-const settings: ISettings = {
-  title: 'Dashboard for APISIX',
-  showSettings: true,
-  showTagsView: true,
-  fixedHeader: false,
-  showSidebarLogo: true,
-  errorLog: ['production'],
-  sidebarTextTheme: true,
-  devServerPort: 9527
-}
-
-export default settings
diff --git a/src/shims.d.ts b/src/shims.d.ts
deleted file mode 100644
index 2858796..0000000
--- a/src/shims.d.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
-*/
-
-declare module '*.vue' {
-  import Vue from 'vue'
-  export default Vue
-}
-
-declare module 'element-ui/lib/locale/lang/*' {
-  export const elementLocale: any
-}
-
-declare module '*.gif' {
-  export const gif: any
-}
diff --git a/src/store/index.ts b/src/store/index.ts
deleted file mode 100644
index aa389a9..0000000
--- a/src/store/index.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import Vue from 'vue'
-import Vuex from 'vuex'
-import { IAppState } from './modules/app'
-import { IUserState } from './modules/user'
-import { ITagsViewState } from './modules/tags-view'
-import { IPermissionState } from './modules/permission'
-import { ISettingsState } from './modules/settings'
-
-Vue.use(Vuex)
-
-export interface IRootState {
-  app: IAppState
-  user: IUserState
-  tagsView: ITagsViewState
-  permission: IPermissionState
-  settings: ISettingsState
-}
-
-// Declare empty store first, dynamically register all modules later.
-export default new Vuex.Store<IRootState>({})
diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts
deleted file mode 100644
index d002fe7..0000000
--- a/src/store/modules/app.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import { VuexModule, Module, Mutation, Action, getModule } from 'vuex-module-decorators'
-import { getSidebarStatus, getSize, setSidebarStatus, setLanguage, setSize } from '@/utils/cookies'
-import { getLocale } from '@/lang'
-import store from '@/store'
-
-export enum DeviceType {
-  Mobile,
-  Desktop,
-}
-
-export interface IAppState {
-  device: DeviceType
-  sidebar: {
-    opened: boolean
-    withoutAnimation: boolean
-  }
-  language: string
-  size: string
-}
-
-@Module({ dynamic: true, store, name: 'app' })
-class App extends VuexModule implements IAppState {
-  public sidebar = {
-    opened: getSidebarStatus() !== 'closed',
-    withoutAnimation: false
-  }
-  public device = DeviceType.Desktop
-  public language = getLocale()
-  public size = getSize() || 'medium'
-
-  @Mutation
-  private TOGGLE_SIDEBAR(withoutAnimation: boolean) {
-    this.sidebar.opened = !this.sidebar.opened
-    this.sidebar.withoutAnimation = withoutAnimation
-    if (this.sidebar.opened) {
-      setSidebarStatus('opened')
-    } else {
-      setSidebarStatus('closed')
-    }
-  }
-
-  @Mutation
-  private CLOSE_SIDEBAR(withoutAnimation: boolean) {
-    this.sidebar.opened = false
-    this.sidebar.withoutAnimation = withoutAnimation
-    setSidebarStatus('closed')
-  }
-
-  @Mutation
-  private TOGGLE_DEVICE(device: DeviceType) {
-    this.device = device
-  }
-
-  @Mutation
-  private SET_LANGUAGE(language: string) {
-    this.language = language
-    setLanguage(this.language)
-  }
-
-  @Mutation
-  private SET_SIZE(size: string) {
-    this.size = size
-    setSize(this.size)
-  }
-
-  @Action
-  public ToggleSideBar(withoutAnimation: boolean) {
-    this.TOGGLE_SIDEBAR(withoutAnimation)
-  }
-
-  @Action
-  public CloseSideBar(withoutAnimation: boolean) {
-    this.CLOSE_SIDEBAR(withoutAnimation)
-  }
-
-  @Action
-  public ToggleDevice(device: DeviceType) {
-    this.TOGGLE_DEVICE(device)
-  }
-
-  @Action
-  public SetLanguage(language: string) {
-    this.SET_LANGUAGE(language)
-  }
-
-  @Action
-  public SetSize(size: string) {
-    this.SET_SIZE(size)
-  }
-}
-
-export const AppModule = getModule(App)
diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts
deleted file mode 100644
index 9093d28..0000000
--- a/src/store/modules/permission.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import { VuexModule, Module, Mutation, Action, getModule } from 'vuex-module-decorators'
-import { RouteConfig } from 'vue-router'
-import { asyncRoutes, constantRoutes } from '@/router'
-import store from '@/store'
-
-const hasPermission = (roles: string[], route: RouteConfig) => {
-  if (route.meta && route.meta.roles) {
-    return roles.some(role => route.meta.roles.includes(role))
-  } else {
-    return true
-  }
-}
-
-export const filterAsyncRoutes = (routes: RouteConfig[], roles: string[]) => {
-  const res: RouteConfig[] = []
-  routes.forEach(route => {
-    const r = { ...route }
-    if (hasPermission(roles, r)) {
-      if (r.children) {
-        r.children = filterAsyncRoutes(r.children, roles)
-      }
-      res.push(r)
-    }
-  })
-  return res
-}
-
-export interface IPermissionState {
-  routes: RouteConfig[]
-  dynamicRoutes: RouteConfig[]
-}
-
-@Module({ dynamic: true, store, name: 'permission' })
-class Permission extends VuexModule implements IPermissionState {
-  public routes: RouteConfig[] = []
-  public dynamicRoutes: RouteConfig[] = []
-
-  @Mutation
-  private SET_ROUTES(routes: RouteConfig[]) {
-    this.routes = constantRoutes.concat(routes)
-    this.dynamicRoutes = routes
-  }
-
-  @Action
-  public GenerateRoutes(roles: string[]) {
-    let accessedRoutes
-    if (roles.includes('admin')) {
-      accessedRoutes = asyncRoutes
-    } else {
-      accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
-    }
-    this.SET_ROUTES(accessedRoutes)
-  }
-}
-
-export const PermissionModule = getModule(Permission)
diff --git a/src/store/modules/settings.ts b/src/store/modules/settings.ts
deleted file mode 100644
index f3a0be5..0000000
--- a/src/store/modules/settings.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import { VuexModule, Module, Mutation, Action, getModule } from 'vuex-module-decorators'
-import store from '@/store'
-import elementVariables from '@/styles/element-variables.scss'
-import defaultSettings from '@/settings'
-
-export interface ISettingsState {
-  theme: string
-  fixedHeader: boolean
-  showSettings: boolean
-  showTagsView: boolean
-  showSidebarLogo: boolean
-  sidebarTextTheme: boolean
-}
-
-@Module({ dynamic: true, store, name: 'settings' })
-class Settings extends VuexModule implements ISettingsState {
-  public theme = elementVariables.theme
-  public fixedHeader = defaultSettings.fixedHeader
-  public showSettings = defaultSettings.showSettings
-  public showTagsView = defaultSettings.showTagsView
-  public showSidebarLogo = defaultSettings.showSidebarLogo
-  public sidebarTextTheme = defaultSettings.sidebarTextTheme
-
-  @Mutation
-  private CHANGE_SETTING(payload: { key: string, value: any }) {
-    const { key, value } = payload
-    if (this.hasOwnProperty(key)) {
-      (this as any)[key] = value
-    }
-  }
-
-  @Action
-  public ChangeSetting(payload: { key: string, value: any}) {
-    this.CHANGE_SETTING(payload)
-  }
-}
-
-export const SettingsModule = getModule(Settings)
diff --git a/src/store/modules/tags-view.ts b/src/store/modules/tags-view.ts
deleted file mode 100644
index 0d744c3..0000000
--- a/src/store/modules/tags-view.ts
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import { VuexModule, Module, Mutation, Action, getModule } from 'vuex-module-decorators'
-import { Route } from 'vue-router'
-import store from '@/store'
-
-export interface ITagView extends Partial<Route> {
-  title?: string
-}
-
-export interface ITagsViewState {
-  visitedViews: ITagView[]
-  cachedViews: (string | undefined)[]
-}
-
-@Module({ dynamic: true, store, name: 'tagsView' })
-class TagsView extends VuexModule implements ITagsViewState {
-  public visitedViews: ITagView[] = []
-  public cachedViews: (string | undefined)[] = []
-
-  @Mutation
-  private ADD_VISITED_VIEW(view: ITagView) {
-    if (this.visitedViews.some(v => v.path === view.path)) return
-    this.visitedViews.push(
-      Object.assign({}, view, {
-        title: view.meta.title || 'no-name'
-      })
-    )
-  }
-
-  @Mutation
-  private ADD_CACHED_VIEW(view: ITagView) {
-    if (this.cachedViews.includes(view.name)) return
-    if (!view.meta.noCache) {
-      this.cachedViews.push(view.name)
-    }
-  }
-
-  @Mutation
-  private DEL_VISITED_VIEW(view: ITagView) {
-    for (const [i, v] of this.visitedViews.entries()) {
-      if (v.path === view.path) {
-        this.visitedViews.splice(i, 1)
-        break
-      }
-    }
-  }
-
-  @Mutation
-  private DEL_CACHED_VIEW(view: ITagView) {
-    for (const [i, v] of this.cachedViews.entries()) {
-      if (v === view.name) {
-        this.cachedViews.splice(i, 1)
-        break
-      }
-    }
-  }
-
-  @Mutation
-  private DEL_OTHERS_VISITED_VIEWS(view: ITagView) {
-    this.visitedViews = this.visitedViews.filter(v => {
-      return v.meta.affix || v.path === view.path
-    })
-  }
-
-  @Mutation
-  private DEL_OTHERS_CACHED_VIEWS(view: ITagView) {
-    for (const [i, v] of this.cachedViews.entries()) {
-      if (v === view.name) {
-        this.cachedViews = this.cachedViews.slice(i, i + 1)
-        break
-      }
-    }
-  }
-
-  @Mutation
-  private DEL_ALL_VISITED_VIEWS() {
-    // keep affix tags
-    const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
-    this.visitedViews = affixTags
-  }
-
-  @Mutation
-  private DEL_ALL_CACHED_VIEWS() {
-    this.cachedViews = []
-  }
-
-  @Mutation
-  private UPDATE_VISITED_VIEW(view: ITagView) {
-    for (let v of this.visitedViews) {
-      if (v.path === view.path) {
-        v = Object.assign(v, view)
-        break
-      }
-    }
-  }
-
-  @Action
-  public addView(view: ITagView) {
-    this.ADD_VISITED_VIEW(view)
-    this.ADD_CACHED_VIEW(view)
-  }
-
-  @Action
-  public addVisitedView(view: ITagView) {
-    this.ADD_VISITED_VIEW(view)
-  }
-
-  @Action
-  public delView(view: ITagView) {
-    this.DEL_VISITED_VIEW(view)
-    this.DEL_CACHED_VIEW(view)
-  }
-
-  @Action
-  public delCachedView(view: ITagView) {
-    this.DEL_CACHED_VIEW(view)
-  }
-
-  @Action
-  public delOthersViews(view: ITagView) {
-    this.DEL_OTHERS_VISITED_VIEWS(view)
-    this.DEL_OTHERS_CACHED_VIEWS(view)
-  }
-
-  @Action
-  public delAllViews() {
-    this.DEL_ALL_VISITED_VIEWS()
-    this.DEL_ALL_CACHED_VIEWS()
-  }
-
-  @Action
-  public delAllCachedViews() {
-    this.DEL_ALL_CACHED_VIEWS()
-  }
-
-  @Action
-  public updateVisitedView(view: ITagView) {
-    this.UPDATE_VISITED_VIEW(view)
-  }
-}
-
-export const TagsViewModule = getModule(TagsView)
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
deleted file mode 100644
index 95d4785..0000000
--- a/src/store/modules/user.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import { VuexModule, Module, Action, Mutation, getModule } from 'vuex-module-decorators'
-// import { login, logout, getUserInfo } from '@/api/users'
-import { getToken, setToken, removeToken } from '@/utils/cookies'
-import router, { resetRouter } from '@/router'
-import { PermissionModule } from './permission'
-import { TagsViewModule } from './tags-view'
-import store from '@/store'
-
-export interface IUserState {
-  token: string
-  name: string
-  avatar: string
-  introduction: string
-  roles: string[]
-  email: string
-}
-
-@Module({ dynamic: true, store, name: 'user' })
-class User extends VuexModule implements IUserState {
-  public token = getToken() || ''
-  public name = ''
-  public avatar = ''
-  public introduction = ''
-  public roles: string[] = []
-  public email = ''
-
-  @Mutation
-  private SET_TOKEN(token: string) {
-    this.token = token
-  }
-
-  @Mutation
-  private SET_NAME(name: string) {
-    this.name = name
-  }
-
-  @Mutation
-  private SET_AVATAR(avatar: string) {
-    this.avatar = avatar
-  }
-
-  @Mutation
-  private SET_INTRODUCTION(introduction: string) {
-    this.introduction = introduction
-  }
-
-  @Mutation
-  private SET_ROLES(roles: string[]) {
-    this.roles = roles
-  }
-
-  @Mutation
-  private SET_EMAIL(email: string) {
-    this.email = email
-  }
-
-  @Action
-  public async Login(userInfo: { username: string, password: string}) {
-    let { username, password } = userInfo
-    username = username.trim()
-    // TEMP: ๅœจๆญคๅค„็ป•่ฟ‡็™ปๅฝ•
-    // const { data } = await login({ username, password })
-
-    const data = {
-      accessToken: username + '-token'
-    }
-
-    setToken(data.accessToken)
-    this.SET_TOKEN(data.accessToken)
-  }
-
-  @Action
-  public ResetToken() {
-    removeToken()
-    this.SET_TOKEN('')
-    this.SET_ROLES([])
-  }
-
-  @Action
-  public async GetUserInfo() {
-    if (this.token === '') {
-      throw Error('GetUserInfo: token is undefined!')
-    }
-    // TEMP: ๆš‚ๆ—ถ็ป•่ฟ‡็™ปๅฝ•
-    // const { data } = await getUserInfo({ /* Your params here */ })
-    const data = {
-      user: {
-        id: 0,
-        username: 'admin',
-        password: 'any',
-        name: 'Super Admin',
-        avatar: 'https://user-images.githubusercontent.com/2106987/68087873-a0543980-fe94-11e9-8203-62958ce69ed6.png',
-        introduction: 'I am a super administrator',
-        email: 'dev@apache.org',
-        phone: '1234567890',
-        roles: ['admin']
-      }
-    }
-
-    if (!data) {
-      throw Error('Verification failed, please Login again.')
-    }
-    const { roles, name, avatar, introduction, email } = data.user
-    // roles must be a non-empty array
-    if (!roles || roles.length <= 0) {
-      throw Error('GetUserInfo: roles must be a non-null array!')
-    }
-    this.SET_ROLES(roles)
-    this.SET_NAME(name)
-    this.SET_AVATAR(avatar)
-    this.SET_INTRODUCTION(introduction)
-    this.SET_EMAIL(email)
-  }
-
-  @Action
-  public async ChangeRoles(role: string) {
-    // Dynamically modify permissions
-    const token = role + '-token'
-    this.SET_TOKEN(token)
-    setToken(token)
-    await this.GetUserInfo()
-    resetRouter()
-    // Generate dynamic accessible routes based on roles
-    PermissionModule.GenerateRoutes(this.roles)
-    // Add generated routes
-    router.addRoutes(PermissionModule.dynamicRoutes)
-    // Reset visited views and cached views
-    TagsViewModule.delAllViews()
-  }
-
-  @Action
-  public async LogOut() {
-    if (this.token === '') {
-      throw Error('LogOut: token is undefined!')
-    }
-    // TEMP: ๆš‚ๆ—ถ่ทณ่ฟ‡ API ๆณจ้”€
-    // await logout()
-    removeToken()
-    resetRouter()
-    this.SET_TOKEN('')
-    this.SET_ROLES([])
-  }
-}
-
-export const UserModule = getModule(User)
diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss
deleted file mode 100644
index 74340c8..0000000
--- a/src/styles/_mixins.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
-*/
-
-@mixin clearfix {
-  &:after {
-    content: "";
-    display: table;
-    clear: both;
-  }
-}
diff --git a/src/styles/_svgicon.scss b/src/styles/_svgicon.scss
deleted file mode 100644
index 3dd54b2..0000000
--- a/src/styles/_svgicon.scss
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
-*/
-
-/* recommended css code for vue-svgicon */
-.svg-icon {
-    display: inline-block;
-    width: 16px;
-    height: 16px;
-    color: inherit;
-    fill: none;
-    stroke: currentColor;
-    vertical-align: -0.15em;
-}
-
-.svg-fill {
-    fill: currentColor;
-    stroke: none;
-}
-
-.svg-up {
-    /* default */
-    transform: rotate(0deg);
-}
-
-.svg-right {
-    transform: rotate(90deg);
-}
-
-.svg-down {
-    transform: rotate(180deg);
-}
-
-.svg-left {
-    transform: rotate(-90deg);
-}
diff --git a/src/styles/_transition.scss b/src/styles/_transition.scss
deleted file mode 100644
index 30c1d9d..0000000
--- a/src/styles/_transition.scss
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
-*/
-
-/* global transition css for vue.js */
-/* see https://vuejs.org/v2/guide/transitions.html for detail */
-
-/* fade */
-.fade-enter-active,
-.fade-leave-active {
-  transition: opacity 0.28s;
-}
-
-.fade-enter,
-.fade-leave-active {
-  opacity: 0;
-}
-
-/* fade-transform */
-.fade-transform-leave-active,
-.fade-transform-enter-active {
-  transition: all .5s;
-}
-
-.fade-transform-enter {
-  opacity: 0;
-  transform: translateX(-30px);
-}
-
-.fade-transform-leave-to {
-  opacity: 0;
-  transform: translateX(30px);
-}
-
-/* fade */
-.breadcrumb-enter-active,
-.breadcrumb-leave-active {
-  transition: all .5s;
-}
-
-.breadcrumb-enter,
-.breadcrumb-leave-active {
-  opacity: 0;
-  transform: translateX(20px);
-}
-
-.breadcrumb-move {
-  transition: all .5s;
-}
-
-.breadcrumb-leave-active {
-  position: absolute;
-}
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
deleted file mode 100644
index 0cb9cb5..0000000
--- a/src/styles/_variables.scss
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
-*/
-
-/* base color */
-$blue:#324157;
-$light-blue:#3A71A8;
-$red:#C03639;
-$pink: #E65D6E;
-$green: #30B08F;
-$tiffany: #4AB7BD;
-$yellow:#FEC171;
-$panGreen: #30B08F;
-
-/* Sidebar */
-$sideBarWidth: 210px;
-$subMenuBg:#1f2d3d;
-$subMenuHover:#001528;
-$subMenuActiveText:#f4f4f5;
-$menuBg:#304156;
-$menuText:#bfcbd9;
-$menuActiveText:#409EFF; // Also see settings.sidebarTextTheme
-
-/* Login page */
-$lightGray: #eee;
-$darkGray:#889aa4;
-$loginBg: #2d3a4b;
-$loginCursorColor: #fff;
-
-// The :export directive is the magic sauce for webpack
-// https://mattferderer.com/use-sass-variables-in-typescript-and-javascript
-:export {
-  menuBg: $menuBg;
-  menuText: $menuText;
-  menuActiveText: $menuActiveText;
-}
diff --git a/src/styles/_variables.scss.d.ts b/src/styles/_variables.scss.d.ts
deleted file mode 100644
index a51ca70..0000000
--- a/src/styles/_variables.scss.d.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-export interface IScssVariables {
-  menuBg: string
-  menuText: string
-  menuActiveText: string
-}
-
-export const variables: IScssVariables
-
-export default variables
diff --git a/src/styles/element-variables.scss b/src/styles/element-variables.scss
deleted file mode 100644
index 8bb3e01..0000000
--- a/src/styles/element-variables.scss
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
-*/
-
-/* Override Element UI variables */
-$--color-primary: #1890ff;
-$--color-success: #13ce66;
-$--color-warning: #FFBA00;
-$--color-danger: #ff4949;
-$--color-info: #5d5d5d;
-$--button-font-weight: 400;
-$--color-text-regular: #1f2d3d;
-$--border-color-light: #dfe4ed;
-$--border-color-lighter: #e6ebf5;
-$--table-border:1px solid#dfe6ec;
-
-/* icon font path, required */
-$--font-path: '~element-ui/lib/theme-chalk/fonts';
-
-// Apply overrided variables in Element UI
-@import '~element-ui/packages/theme-chalk/src/index';
-
-// The :export directive is the magic sauce for webpack
-// https://mattferderer.com/use-sass-variables-in-typescript-and-javascript
-:export {
-  theme: $--color-primary;
-}
diff --git a/src/styles/element-variables.scss.d.ts b/src/styles/element-variables.scss.d.ts
deleted file mode 100644
index 833945d..0000000
--- a/src/styles/element-variables.scss.d.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-export interface IScssVariables {
-  theme: string
-}
-
-export const variables: IScssVariables
-
-export default variables
diff --git a/src/styles/index.scss b/src/styles/index.scss
deleted file mode 100644
index 75976a2..0000000
--- a/src/styles/index.scss
+++ /dev/null
@@ -1,183 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
-*/
-
-@import './element-variables.scss';
-@import './variables.scss';
-@import './mixins.scss';
-@import './transition.scss';
-@import './svgicon.scss';
-
-body {
-  height: 100%;
-  -moz-osx-font-smoothing: grayscale;
-  -webkit-font-smoothing: antialiased;
-  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
-}
-
-html {
-  height: 100%;
-}
-
-#app {
-  height: 100%;
-}
-
-*,
-*:before,
-*:after {
-  box-sizing: border-box;
-}
-
-a,
-a:focus,
-a:hover {
-  color: inherit;
-  outline: none;
-  text-decoration: none;
-}
-
-div:focus {
-  outline: none;
-}
-
-.clearfix {
-  @include clearfix;
-}
-
-label {
-  font-weight: 700;
-}
-
-.app-container {
-  padding: 20px;
-}
-
-.components-container {
-  margin: 30px 50px;
-  position: relative;
-}
-
-// Refine element ui upload
-.upload-container {
-  .el-upload {
-    width: 100%;
-
-    .el-upload-dragger {
-      width: 100%;
-      height: 200px;
-    }
-  }
-}
-
-aside {
-  background: #eef1f6;
-  color: #2c3e50;
-  padding: 8px 24px;
-  margin-bottom: 20px;
-  border-radius: 2px;
-  display: block;
-  line-height: 32px;
-  font-size: 16px;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-
-  a {
-    color: #337ab7;
-    cursor: pointer;
-
-    &:hover {
-      color: rgb(32, 160, 255);
-    }
-  }
-}
-
-.fixed-width {
-  .el-button--mini {
-    padding: 7px 10px;
-    width: 60px;
-  }
-}
-
-.filter-container {
-  padding-bottom: 10px;
-
-  .filter-item {
-    display: inline-block;
-    vertical-align: middle;
-    margin-bottom: 10px;
-  }
-}
-
-.link-type,
-.link-type:focus {
-  color: #337ab7;
-  cursor: pointer;
-
-  &:hover {
-    color: rgb(32, 160, 255);
-  }
-}
-
-.status-col {
-  .cell {
-    padding: 0 10px;
-    text-align: center;
-  }
-}
-
-.text-center {
-  text-align: center
-}
-
-.sub-navbar {
-  height: 50px;
-  line-height: 50px;
-  position: relative;
-  width: 100%;
-  text-align: right;
-  padding-right: 20px;
-  transition: 600ms ease position;
-  background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
-
-  .subtitle {
-    font-size: 20px;
-    color: #fff;
-  }
-
-  &.draft {
-    background: #d0d0d0;
-  }
-
-  &.deleted {
-    background: #d0d0d0;
-  }
-}
-
-.no-padding {
-  padding: 0px !important;
-}
-
-.el-message-box {
-  vertical-align: baseline !important;
-}
\ No newline at end of file
diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts
deleted file mode 100644
index 2aca434..0000000
--- a/src/utils/cookies.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import Cookies from 'js-cookie'
-
-// App
-const sidebarStatusKey = 'sidebar_status'
-export const getSidebarStatus = () => Cookies.get(sidebarStatusKey)
-export const setSidebarStatus = (sidebarStatus: string) => Cookies.set(sidebarStatusKey, sidebarStatus)
-
-const languageKey = 'language'
-export const getLanguage = () => Cookies.get(languageKey)
-export const setLanguage = (language: string) => Cookies.set(languageKey, language)
-
-const sizeKey = 'size'
-export const getSize = () => Cookies.get(sizeKey)
-export const setSize = (size: string) => Cookies.set(sizeKey, size)
-
-// User
-const tokenKey = 'vue_typescript_admin_access_token'
-export const getToken = () => Cookies.get(tokenKey)
-export const setToken = (token: string) => Cookies.set(tokenKey, token)
-export const removeToken = () => Cookies.remove(tokenKey)
diff --git a/src/utils/index.ts b/src/utils/index.ts
deleted file mode 100644
index 0017073..0000000
--- a/src/utils/index.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-// Parse the time to string
-export const parseTime = (
-  time?: object | string | number,
-  cFormat?: string
-): string | null => {
-  if (time === undefined) {
-    return null
-  }
-  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
-  let date: Date
-  if (typeof time === 'object') {
-    date = time as Date
-  } else {
-    if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
-      time = parseInt(time)
-    }
-    if (typeof time === 'number' && time.toString().length === 10) {
-      time = time * 1000
-    }
-    date = new Date(time)
-  }
-  const formatObj: { [key: string]: number } = {
-    y: date.getFullYear(),
-    m: date.getMonth() + 1,
-    d: date.getDate(),
-    h: date.getHours(),
-    i: date.getMinutes(),
-    s: date.getSeconds(),
-    a: date.getDay()
-  }
-  const timeStr = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
-    let value = formatObj[key]
-    // Note: getDay() returns 0 on Sunday
-    if (key === 'a') {
-      return ['ๆ—ฅ', 'ไธ€', 'ไบŒ', 'ไธ‰', 'ๅ››', 'ไบ”', 'ๅ…ญ'][value]
-    }
-    if (result.length > 0 && value < 10) {
-      return '0' + value
-    }
-    return String(value) || '0'
-  })
-  return timeStr
-}
-
-// Format and filter json data using filterKeys array
-export const formatJson = (filterKeys: any, jsonData: any) =>
-  jsonData.map((data: any) => filterKeys.map((key: string) => {
-    if (key === 'timestamp') {
-      return parseTime(data[key])
-    } else {
-      return data[key]
-    }
-  }))
-
-// Check if an element has a class
-export const hasClass = (ele: HTMLElement, className: string) => {
-  return !!ele.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'))
-}
-
-// Add class to element
-export const addClass = (ele: HTMLElement, className: string) => {
-  if (!hasClass(ele, className)) ele.className += ' ' + className
-}
-
-// Remove class from element
-export const removeClass = (ele: HTMLElement, className: string) => {
-  if (hasClass(ele, className)) {
-    const reg = new RegExp('(\\s|^)' + className + '(\\s|$)')
-    ele.className = ele.className.replace(reg, ' ')
-  }
-}
-
-// Toggle class for the selected element
-export const toggleClass = (ele: HTMLElement, className: string) => {
-  if (!ele || !className) {
-    return
-  }
-  let classString = ele.className
-  const nameIndex = classString.indexOf(className)
-  if (nameIndex === -1) {
-    classString += '' + className
-  } else {
-    classString =
-      classString.substr(0, nameIndex) +
-      classString.substr(nameIndex + className.length)
-  }
-  ele.className = classString
-}
diff --git a/src/utils/permission.ts b/src/utils/permission.ts
deleted file mode 100644
index 5112042..0000000
--- a/src/utils/permission.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import { UserModule } from '@/store/modules/user'
-
-export const checkPermission = (value: string[]): boolean => {
-  if (value && value instanceof Array && value.length > 0) {
-    const roles = UserModule.roles
-    const permissionRoles = value
-    const hasPermission = roles.some(role => {
-      return permissionRoles.includes(role)
-    })
-    return hasPermission
-  } else {
-    console.error(`need roles! Like v-permission="['admin','editor']"`)
-    return false
-  }
-}
diff --git a/src/utils/request.ts b/src/utils/request.ts
deleted file mode 100644
index e730d5f..0000000
--- a/src/utils/request.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import axios from 'axios'
-import { Message } from 'element-ui'
-
-const service = axios.create({
-  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
-  timeout: 5000,
-  headers: {
-    'X-API-KEY': localStorage.getItem('GLOBAL_API_KEY')
-  }
-})
-
-service.interceptors.request.use(
-  (config) => {
-    return config
-  },
-  (error) => {
-    Promise.reject(error)
-  }
-)
-
-service.interceptors.response.use(
-  (response) => {
-    return response.data
-  },
-  (error) => {
-    Message({
-      message: error.response.data.error_msg || error.message,
-      type: 'error',
-      duration: 5 * 1000
-    })
-    return Promise.reject(error)
-  }
-)
-
-export default service
diff --git a/src/utils/scroll-to.ts b/src/utils/scroll-to.ts
deleted file mode 100644
index 082382a..0000000
--- a/src/utils/scroll-to.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
-  t /= d / 2
-  if (t < 1) {
-    return c / 2 * t * t + b
-  }
-  t--
-  return -c / 2 * (t * (t - 2) - 1) + b
-}
-
-// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
-const requestAnimFrame = (function() {
-  return window.requestAnimationFrame || window.webkitRequestAnimationFrame || (window as any).mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
-})()
-
-// Because it's so fucking difficult to detect the scrolling element, just move them all
-const move = (amount: number) => {
-  document.documentElement.scrollTop = amount;
-  (document.body.parentNode as HTMLElement).scrollTop = amount
-  document.body.scrollTop = amount
-}
-
-const position = () => {
-  return document.documentElement.scrollTop || (document.body.parentNode as HTMLElement).scrollTop || document.body.scrollTop
-}
-
-export const scrollTo = (to: number, duration: number, callback?: Function) => {
-  const start = position()
-  const change = to - start
-  const increment = 20
-  let currentTime = 0
-  duration = (typeof (duration) === 'undefined') ? 500 : duration
-  const animateScroll = function() {
-    // increment the time
-    currentTime += increment
-    // find the value with the quadratic in-out easing function
-    const val = easeInOutQuad(currentTime, start, change, duration)
-    // move the document.body
-    move(val)
-    // do the animation unless its over
-    if (currentTime < duration) {
-      requestAnimFrame(animateScroll)
-    } else {
-      if (callback && typeof (callback) === 'function') {
-        // the animation is done so lets callback
-        callback()
-      }
-    }
-  }
-  animateScroll()
-}
diff --git a/src/utils/validate.ts b/src/utils/validate.ts
deleted file mode 100644
index b7a944c..0000000
--- a/src/utils/validate.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2018 Chong Guo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-export const isValidUsername = (str: string) => ['admin', 'editor'].indexOf(str.trim()) >= 0
-
-export const isExternal = (path: string) => /^(https?:|mailto:|tel:)/.test(path)
-
-export const isArray = (arg: any) => {
-  if (typeof Array.isArray === 'undefined') {
-    return Object.prototype.toString.call(arg) === '[object Array]'
-  }
-  return Array.isArray(arg)
-}
-
-export const isValidURL = (url: string) => {
-  const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
-  return reg.test(url)
-}
diff --git a/src/views/login/index.vue b/src/views/login/index.vue
deleted file mode 100644
index 3efe7c9..0000000
--- a/src/views/login/index.vue
+++ /dev/null
@@ -1,228 +0,0 @@
-<!--
-#
-# MIT License
-#
-# Copyright (c) 2018 Chong Guo
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
--->
-
-<template>
-  <div class="login-container">
-    <el-form
-      ref="loginForm"
-      :model="loginForm"
-      class="login-form"
-      autocomplete="on"
-      label-position="left"
-    >
-      <div class="title-container">
-        <h3 class="title">
-          {{ $t('login.title') }}
-        </h3>
-      </div>
-
-      <el-form-item prop="apikey">
-        <span class="svg-container">
-          <svg-icon name="user" />
-        </span>
-        <el-input
-          ref="apikey"
-          v-model="loginForm.apikey"
-          placeholder="Please input API KEY here"
-          name="apikey"
-          type="text"
-          autocomplete="on"
-        />
-      </el-form-item>
-
-      <el-button
-        :loading="loading"
-        type="primary"
-        style="width:100%; margin-bottom:30px;"
-        @click.native.prevent="handleLogin"
-      >
-        {{ $t('login.logIn') }}
-      </el-button>
-    </el-form>
-
-    <el-dialog
-      :title="$t('login.thirdparty')"
-      :visible.sync="showDialog"
-    >
-      {{ $t('login.thirdpartyTips') }}
-      <br>
-      <br>
-      <br>
-      <social-sign />
-    </el-dialog>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Vue, Watch } from 'vue-property-decorator'
-import { Route } from 'vue-router'
-import { Form as ElForm, Input } from 'element-ui'
-import { UserModule } from '@/store/modules/user'
-import LangSelect from '@/components/LangSelect/index.vue'
-
-@Component({
-  name: 'Login',
-  components: {
-    LangSelect
-  }
-})
-export default class extends Vue {
-  private loginForm = {
-    apikey: ''
-  }
-  private loading = false
-  private showDialog = false
-  private redirect?: string
-
-  private async handleLogin() {
-    await UserModule.Login({ username: '', password: '' })
-    localStorage.setItem('GLOBAL_API_KEY', this.loginForm.apikey)
-    window.location.replace('/')
-  }
-}
-</script>
-
-<style lang="scss">
-// References: https://www.zhangxinxu.com/wordpress/2018/01/css-caret-color-first-line/
-@supports (-webkit-mask: none) and (not (cater-color: $loginCursorColor)) {
-  .login-container .el-input {
-    input { color: $loginCursorColor; }
-    input::first-line { color: $lightGray; }
-  }
-}
-
-.login-container {
-  .el-input {
-    display: inline-block;
-    height: 47px;
-    width: 85%;
-
-    input {
-      height: 47px;
-      background: transparent;
-      border: 0px;
-      border-radius: 0px;
-      padding: 12px 5px 12px 15px;
-      color: $lightGray;
-      caret-color: $loginCursorColor;
-      -webkit-appearance: none;
-
-      &:-webkit-autofill {
-        box-shadow: 0 0 0px 1000px $loginBg inset !important;
-        -webkit-text-fill-color: #fff !important;
-      }
-    }
-  }
-
-  .el-form-item {
-    border: 1px solid rgba(255, 255, 255, 0.1);
-    background: rgba(0, 0, 0, 0.1);
-    border-radius: 5px;
-    color: #454545;
-  }
-}
-</style>
-
-<style lang="scss" scoped>
-.login-container {
-  height: 100%;
-  width: 100%;
-  overflow: hidden;
-  background-color: $loginBg;
-
-  .login-form {
-    position: relative;
-    width: 520px;
-    max-width: 100%;
-    padding: 160px 35px 0;
-    margin: 0 auto;
-    overflow: hidden;
-  }
-
-  .tips {
-    font-size: 14px;
-    color: #fff;
-    margin-bottom: 10px;
-
-    span {
-      &:first-of-type {
-        margin-right: 16px;
-      }
-    }
-  }
-
-  .svg-container {
-    padding: 6px 5px 6px 15px;
-    color: $darkGray;
-    vertical-align: middle;
-    width: 30px;
-    display: inline-block;
-  }
-
-  .title-container {
-    position: relative;
-
-    .title {
-      font-size: 26px;
-      color: $lightGray;
-      margin: 0px auto 40px auto;
-      text-align: center;
-      font-weight: bold;
-    }
-
-    .set-language {
-      color: #fff;
-      position: absolute;
-      top: 3px;
-      font-size: 18px;
-      right: 0px;
-      cursor: pointer;
-    }
-  }
-
-  .show-pwd {
-    position: absolute;
-    right: 10px;
-    top: 7px;
-    font-size: 16px;
-    color: $darkGray;
-    cursor: pointer;
-    user-select: none;
-  }
-
-  .thirdparty-button {
-    position: absolute;
-    right: 0;
-    bottom: 6px;
-  }
-
-  @media only screen and (max-width: 470px) {
-    .thirdparty-button {
-      display: none;
-    }
-  }
-}
-</style>
diff --git a/src/views/schema/consumers/edit.vue b/src/views/schema/consumers/edit.vue
deleted file mode 100644
index d59174c..0000000
--- a/src/views/schema/consumers/edit.vue
+++ /dev/null
@@ -1,266 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="consumers-wrapper">
-    <el-form
-      ref="form"
-      :model="form"
-      :rules="rules"
-      label-width="80px"
-      :show-message="false"
-    >
-      <el-form-item
-        label="Desc"
-      >
-        <el-input
-          v-model="form.desc"
-          placeholder="Description"
-        />
-      </el-form-item>
-
-      <el-form-item
-        label="name"
-        prop="username"
-      >
-        <el-input v-model="form.username" />
-      </el-form-item>
-
-      <el-form-item
-        v-for="(index, item) in form.plugins"
-        :key="item"
-        :label="&quot;plugin&quot;"
-        class="plugin-item"
-      >
-        <el-button
-          v-if="item !== 'tempPlugin'"
-          type="info"
-          plain
-          @click="showPlugin(item)"
-        >
-          {{ item }}
-        </el-button>
-
-        <el-button
-          v-if="item !== 'tempPlugin'"
-          type="danger"
-          @click.prevent="removePlugin(item)"
-        >
-          {{ $t('button.delete') }}
-        </el-button>
-
-        <el-select
-          v-if="item === 'tempPlugin'"
-          :value="null"
-          class="plugin-select"
-          placeholder="Select a Plugin"
-          @change="showPlugin"
-        >
-          <el-option
-            v-for="name in filteredPluginList"
-            :key="name"
-            :label="name"
-            :value="name"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button
-          :disabled="!filteredPluginList.length"
-          @click="addPlugin"
-        >
-          {{ $t('button.add_plugin') }}
-        </el-button>
-      </el-form-item>
-
-      <el-form-item>
-        <el-button
-          type="primary"
-          @click="onSubmit"
-        >
-          {{ $t('button.save') }}
-        </el-button>
-        <el-button @click="toPreviousPage">
-          {{ $t('button.cancel') }}
-        </el-button>
-      </el-form-item>
-    </el-form>
-
-    <PluginDialog
-      :show="showPluginDialog"
-      :name="pluginName"
-      :plugin-data="form.plugins[pluginName]"
-      @hidePlugin="showPluginDialog = false"
-      @save="onPluginSave"
-    />
-  </div>
-</template>
-
-<script lang='ts'>
-import { Component, Vue } from 'vue-property-decorator'
-import { Form } from 'element-ui'
-
-import { getList, get, updateOrCreateConsumer } from '../../../api/schema/consumers'
-import { getPluginList } from '@/api/schema/plugins'
-import PluginDialog from '@/components/PluginDialog/index.vue'
-
-import { IConsumerData, IDataWrapper } from '../../../api/types'
-import { TagsViewModule } from '@/store/modules/tags-view'
-
-@Component({
-  name: 'ConsumerEdit',
-  components: {
-    PluginDialog
-  }
-})
-export default class extends Vue {
-  private isEditMode: boolean = false
-
-  private form = {
-    username: '',
-    plugins: {},
-    desc: ''
-  }
-
-  private rules = {
-    username: {
-      required: true
-    }
-  }
-
-  private pluginList = []
-  private pluginName: string = ''
-  private showPluginDialog: boolean = false
-
-  get filteredPluginList() {
-    return this.pluginList.filter(item => !this.form.plugins.hasOwnProperty(item))
-  }
-
-  created() {
-    this.isEditMode = (this.$route as any).name.indexOf('Create') === -1
-
-    if (this.isEditMode) {
-      this.getConsumerData()
-    }
-
-    this.getPluginList()
-  }
-
-  private async getConsumerData() {
-    const username = this.$route.params.username
-    const data = await get(username) as any
-
-    (this.form as any) = {
-      desc: data.node.value.desc,
-      username,
-      plugins: data.node.value.plugins
-    }
-  }
-
-  private async getPluginList() {
-    this.pluginList = await getPluginList() as any
-  }
-
-  private async addPlugin() {
-    if (this.form.plugins.hasOwnProperty('tempPlugin')) return
-
-    this.form.plugins = {
-      ...this.form.plugins,
-      tempPlugin: null
-    }
-  }
-
-  private async onSubmit() {
-    (this.$refs['form'] as any).validate(async(valid: boolean) => {
-      if (valid) {
-        delete this.form.plugins['tempPlugin']
-
-        let data = Object.assign({}, this.form)
-        Object.entries(data).forEach(([key, value]) => {
-          if (typeof data[key] === 'object') {
-            if (key !== 'plugins' && Object.keys(value).length === 0) {
-              delete data[key]
-            }
-          } else {
-            if (value === '') {
-              delete data[key]
-            }
-          }
-        })
-
-        await updateOrCreateConsumer(Object.assign({}, data))
-
-        this.$message.success(`${this.isEditMode ? 'Update the' : 'Create a'} consumer successfully!`)
-
-        if (this.isEditMode) return
-
-        TagsViewModule.delView(this.$route)
-        this.$nextTick(() => {
-          this.$router.push({
-            name: 'SchemaConsumersList'
-          })
-        })
-      } else {
-        return false
-      }
-    })
-  }
-
-  private async showPlugin(name: string) {
-    this.pluginName = name
-    this.showPluginDialog = true
-  }
-
-  private onPluginSave(name: string, data: any) {
-    delete this.form.plugins['tempPlugin']
-    this.showPluginDialog = false
-    this.form.plugins[name] = data
-  }
-
-  private toPreviousPage() {
-    this.$router.go(-1)
-  }
-
-  private removePlugin(name: any) {
-    this.$confirm(`Do you want to remove ${name} plugin?`, 'Warning', {
-      confirmButtonText: 'Confirm',
-      cancelButtonText: 'Cancel',
-      type: 'warning'
-    })
-      .then(async() => {
-        Vue.delete(this.form.plugins, name)
-      }).catch(() => {})
-  }
-}
-</script>
-
-<style lang='scss'>
-.consumers-wrapper {
-  padding: 20px;
-  .el-form {
-    .el-form-item {
-      .el-form-item__content {
-        .el-input {
-          width: 220px;
-        }
-      }
-    }
-  }
-}
-</style>
diff --git a/src/views/schema/consumers/list.vue b/src/views/schema/consumers/list.vue
deleted file mode 100644
index 1ecbaa9..0000000
--- a/src/views/schema/consumers/list.vue
+++ /dev/null
@@ -1,222 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="app-container">
-    <div class="filter-container">
-      <el-button
-        class="filter-item"
-        style="margin-left: 10px;"
-        type="primary"
-        icon="el-icon-edit"
-        @click="handleCreate"
-      >
-        {{ $t('table.add') }}
-      </el-button>
-    </div>
-
-    <el-table
-      :key="tableKey"
-      v-loading="listLoading"
-      :data="tableData"
-      :border="false"
-      fit
-      highlight-current-row
-      style="width: 100%;"
-      @sort-change="sortChange"
-    >
-      <el-table-column
-        v-for="(item, index) of tableKeys"
-        :key="index"
-        :label="item.key"
-        :prop="item.key"
-        :width="item.width"
-        :class-name="item.align === 'left' ? '' : 'status-col'"
-        header-align="center"
-      />
-      <el-table-column
-        :label="$t('table.actions')"
-        align="center"
-        width="230"
-        class-name="fixed-width"
-      >
-        <template slot-scope="{row}">
-          <el-button
-            type="primary"
-            size="mini"
-            @click="handleToEdit(row)"
-          >
-            {{ $t('table.edit') }}
-          </el-button>
-
-          <el-button
-            v-if="row.status!=='deleted'"
-            size="mini"
-            type="danger"
-            @click="handleRemove(row)"
-          >
-            {{ $t('table.delete') }}
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
-import { Form } from 'element-ui'
-
-import Pagination from '../../../components/Pagination/index.vue'
-
-import { getList, removeConsumer } from '../../../api/schema/consumers'
-
-@Component({
-  name: 'ComplexTable',
-  components: {
-    Pagination
-  }
-})
-export default class extends Vue {
-  private tableKey = 0
-  private list: any[] = []
-  private total = 0
-  private listLoading = true
-  private listQuery = {
-    page: 1,
-    limit: 20,
-    importance: undefined,
-    title: undefined,
-    type: undefined,
-    sort: '+id'
-  }
-
-  private tableData: string[] = []
-  private tableKeys: any[] = []
-
-  created() {
-    this.getList()
-  }
-
-  private async getList() {
-    this.listLoading = true
-
-    this.tableKeys = [
-      {
-        key: 'username',
-        width: 300
-      }, {
-        key: 'description',
-        width: 300,
-        align: 'left'
-      }, {
-        key: 'plugins',
-        width: 400
-      }
-    ]
-
-    let { node: { nodes = [] } } = await getList() as any
-    nodes = [...nodes].map((item: any) => {
-      const pluginArr: any = []
-
-      const { value } = item
-      const username = value.username
-      const desc = value.desc
-
-      Object.entries(value.plugins as any).map(([ key, value ]: any) => {
-        pluginArr.push({
-          name: key,
-          key: value.key
-        })
-      })
-
-      return {
-        username,
-        plugins: pluginArr.map((item: any) => item.name).join(', '),
-        pluginArr,
-        description: desc
-      }
-    })
-
-    this.tableData = nodes
-    this.total = nodes.length
-
-    setTimeout(() => {
-      this.listLoading = false
-    }, 0.5 * 1000)
-  }
-
-  private handleFilter() {
-    this.listQuery.page = 1
-    this.getList()
-  }
-
-  private handleRemove(row: any) {
-    this.$confirm(`Do you want to remove consumer ${row.username}?`, 'Warning', {
-      confirmButtonText: 'Confirm',
-      cancelButtonText: 'Cancel',
-      type: 'warning'
-    })
-      .then(async() => {
-        await removeConsumer(row.username)
-        this.getList()
-        this.$message.success(`Remove consumer ${row.username} successfully!`)
-      })
-  }
-
-  private sortChange(data: any) {
-    const { prop, order } = data
-    if (prop === 'id') {
-      this.sortByID(order)
-    }
-  }
-
-  private sortByID(order: string) {
-    if (order === 'ascending') {
-      this.listQuery.sort = '+id'
-    } else {
-      this.listQuery.sort = '-id'
-    }
-    this.handleFilter()
-  }
-
-  private handleCreate() {
-    this.$router.push({
-      name: 'SchemaConsumersCreate'
-    })
-  }
-
-  private handleToEdit(row: any) {
-    this.$router.push({
-      name: 'SchemaConsumersEdit',
-      params: {
-        username: row.username
-      }
-    })
-  }
-}
-</script>
-
-<style lang="scss">
-.app-container {
-  .el-table::before {
-    height: 0 !important;
-  }
-}
-</style>
diff --git a/src/views/schema/routes/edit.vue b/src/views/schema/routes/edit.vue
deleted file mode 100644
index 80c5ded..0000000
--- a/src/views/schema/routes/edit.vue
+++ /dev/null
@@ -1,549 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="container">
-    <el-form
-      ref="form"
-      :model="form"
-      :rules="rules"
-      label-width="80px"
-      :show-message="false"
-    >
-      <el-form-item
-        label="Desc"
-      >
-        <el-input
-          v-model="form.desc"
-          placeholder="Description"
-        />
-      </el-form-item>
-
-      <el-form-item
-        label="URIs"
-        prop="uris"
-      >
-        <el-select
-          v-model="form.uris"
-          allow-create
-          filterable
-          multiple
-          default-first-option
-          :placeholder="$t('schema.route.inputMultipleValues')"
-          @change="filterUriOptions"
-        >
-          <el-option
-            v-for="item in ExistedUris"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item
-        label="Hosts"
-        prop="hosts"
-        placeholder="Hosts"
-      >
-        <el-select
-          v-model="form.hosts"
-          multiple
-          filterable
-          allow-create
-          default-first-option
-          :placeholder="$t('schema.route.inputMultipleValues')"
-        >
-          <el-option
-            v-for="item in ExistedHosts"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
-          />
-        </el-select>
-        <p class="tip">
-          {{ $t('schema.route.propertyHostsTip') }}
-        </p>
-      </el-form-item>
-
-      <el-form-item
-        label="Remote Address"
-        prop="remote_addr"
-      >
-        <el-input
-          v-model="form.remote_addr"
-          placeholder="Remote Address"
-        />
-      </el-form-item>
-
-      <el-form-item
-        label="Methods"
-      >
-        <el-select
-          v-model="form.methods"
-          multiple
-          placeholder="Methods"
-        >
-          <el-option
-            v-for="item in methods"
-            :key="item"
-            :label="item"
-            :value="item"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item
-        label="Upstream"
-        prop="upstream_id"
-      >
-        <el-select
-          v-model="form.upstream_id"
-          placeholder="Upstream"
-          clearable
-          filterable
-        >
-          <el-option
-            v-for="item in upstreamList"
-            :key="item.id"
-            :label="item.desc"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item
-        label="Service"
-        prop="service_id"
-      >
-        <el-select
-          v-model="form.service_id"
-          placeholder="Service"
-          clearable
-          filterable
-        >
-          <el-option
-            v-for="item in serviceList"
-            :key="item.id"
-            :label="item.desc"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item
-        v-for="(index, item) in form.plugins"
-        :key="item"
-        :label="&quot;plugin&quot;"
-        class="plugin-item"
-      >
-        <el-button
-          v-if="item !== 'tempPlugin'"
-          type="info"
-          plain
-          @click="showPlugin(item)"
-        >
-          {{ item }}
-        </el-button>
-
-        <el-button
-          v-if="item !== 'tempPlugin'"
-          type="danger"
-          @click.prevent="removePlugin(item)"
-        >
-          {{ $t('button.delete') }}
-        </el-button>
-
-        <el-select
-          v-if="item === 'tempPlugin'"
-          :value="null"
-          class="plugin-select"
-          placeholder="Select a Plugin"
-          @change="showPlugin"
-        >
-          <el-option
-            v-for="name in filteredPluginList"
-            :key="name"
-            :label="name"
-            :value="name"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item>
-        <el-button
-          :disabled="!filteredPluginList.length"
-          @click="addPlugin"
-        >
-          {{ $t('button.add_plugin') }}
-        </el-button>
-      </el-form-item>
-      <VarArgs
-        :p-vars.sync="form.vars"
-        @onChange="onVarArgsChange"
-      />
-
-      <el-form-item
-        label="filter_func"
-        prop="filter_func"
-      >
-        <el-input
-          v-model="form.filter_func"
-          type="textarea"
-          :autosize="{minRows: 2, maxRows: 4}"
-          :placeholder="$t('schema.route.fileterFunc')"
-        />
-      </el-form-item>
-
-      <el-form-item>
-        <el-button
-          type="primary"
-          @click="onSubmit"
-        >
-          {{ $t('button.save') }}
-        </el-button>
-        <el-button @click="toPreviousPage">
-          {{ $t('button.cancel') }}
-        </el-button>
-      </el-form-item>
-    </el-form>
-    <PluginDialog
-      :show="showPluginDialog"
-      :name="pluginName"
-      :plugin-data="form.plugins[pluginName]"
-      @hidePlugin="showPluginDialog = false"
-      @save="onPluginSave"
-    />
-  </div>
-</template>
-
-<script lang='ts'>
-import { Component, Vue } from 'vue-property-decorator'
-import { Form } from 'element-ui'
-
-import PluginDialog from '@/components/PluginDialog/index.vue'
-import VarArgs from '@/components/VarArgs/index.vue'
-
-import { getRouter, createRouter, updateRouter } from '@/api/schema/routes'
-import { getPluginList } from '@/api/schema/plugins'
-import { getUpstreamList } from '@/api/schema/upstream'
-import { getServiceList } from '@/api/schema/services'
-import { TagsViewModule } from '@/store/modules/tags-view'
-
-import i18n from '@/lang'
-
-@Component({
-  name: 'RouterEdit',
-  components: {
-    PluginDialog,
-    VarArgs
-  }
-})
-
-export default class extends Vue {
-  private form = {
-    uris: [],
-    hosts: [],
-    remote_addr: '',
-    upstream_id: '',
-    service_id: '',
-    methods: [],
-    plugins: {},
-    vars: [],
-    desc: '',
-    filter_func: ''
-  }
-
-  // TODO: can add existed info from route list
-  private ExistedUris = [{}]
-  private ExistedHosts = [{}]
-  private validateFilterFuncRegexp = /^function\(\)[^]*?\bend$/
-
-  private rules = {
-    uris: {
-      required: true
-    },
-    filter_func: [
-      { pattern: this.validateFilterFuncRegexp, trigger: 'blur', message: i18n.t('schema.route.fileterFunc') }
-    ]
-  }
-  private isEditMode: boolean = false
-
-  private methods = [
-    'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'
-  ]
-
-  private pluginList = []
-  private pluginName: string = ''
-  private showPluginDialog: boolean = false
-  private upstreamList = []
-  private serviceList = []
-
-  created() {
-    this.isEditMode = (this.$route as any).name.indexOf('Create') === -1
-
-    if (this.isEditMode) {
-      this.getData()
-    }
-
-    this.getPluginList()
-    this.getUpstreamList()
-    this.getServiceList()
-  }
-
-  get filteredPluginList() {
-    return this.pluginList.filter(item => !this.form.plugins.hasOwnProperty(item))
-  }
-
-  private reset() {
-    this.form = {
-      uris: [],
-      hosts: [],
-      remote_addr: '',
-      upstream_id: '',
-      service_id: '',
-      methods: [],
-      plugins: {},
-      vars: [],
-      desc: '',
-      filter_func: ''
-    }
-  }
-
-  filterDataWithRegex(val: any, regex: any) {
-    if (val.length > 0) {
-      const newArr: string[] = []
-      val.filter(function(item: any) {
-        if (typeof item === 'string') {
-          item = item.replace(/\s+/g, '')
-          if (regex.test(item)) {
-            newArr.push(item)
-          }
-        }
-      })
-
-      newArr.map(function(item: any, index: number) {
-        val[index] = item
-      })
-
-      if (val.length > newArr.length) {
-        val.splice(newArr.length, val.length)
-      }
-    }
-  }
-
-  private filterUriOptions(val: any) {
-    this.filterDataWithRegex(val, new RegExp('^([\\*\\./0-9a-zA-Z-_~@\\?\\!#$\\(\\)]+)$'))
-  }
-
-  private async getData() {
-    const { id } = this.$route.params
-    let {
-      node: {
-        value: {
-          uri = '',
-          uris = [],
-          hosts = [],
-          host = '',
-          remote_addr = '',
-          upstream_id = '',
-          service_id = '',
-          methods = [],
-          plugins = {},
-          vars = [],
-          desc = '',
-          filter_func = ''
-        }
-      }
-    } = await getRouter(id) as any
-
-    if (hosts.length === 0 && host.length > 0) {
-      hosts.push(host)
-    }
-
-    if (uris.length === 0 && uri.length > 0) {
-      uris.push(uri)
-    }
-
-    this.form = {
-      uris,
-      hosts,
-      remote_addr,
-      upstream_id,
-      service_id,
-      methods,
-      plugins,
-      vars,
-      desc,
-      filter_func
-    }
-  }
-
-  private async onSubmit() {
-    (this.$refs.form as any).validate(async(valid: boolean, invalidField: any) => {
-      if (valid) {
-        let data = Object.assign({}, this.form)
-        if (!data.methods.length) {
-          delete data.methods
-        }
-
-        Object.entries(data).forEach(([key, value]) => {
-          if (typeof data[key] === 'object') {
-            if (key !== 'plugins' && Object.keys(value).length === 0) {
-              delete data[key]
-            }
-          } else {
-            if (value === '') {
-              delete data[key]
-            }
-          }
-        })
-
-        delete data['tempPlugin']
-        if (this.isEditMode) {
-          await updateRouter(this.$route.params.id, data)
-        } else {
-          await createRouter(data)
-        }
-
-        this.$message.success(`${this.isEditMode ? 'Update the' : 'Create a'} service successfully!`)
-
-        if (!this.isEditMode) {
-          TagsViewModule.delView(this.$route)
-          this.$nextTick(() => {
-            this.$router.push({
-              name: 'SchemaRoutesList'
-            })
-          })
-        }
-      } else {
-        if (invalidField.filter_func) {
-          this.$message.warning(invalidField.filter_func[0].message)
-        }
-        return false
-      }
-    })
-  }
-
-  private toPreviousPage() {
-    this.$router.go(-1)
-  }
-
-  private async getUpstreamList() {
-    const {
-      node: {
-        nodes = []
-      }
-    } = await getUpstreamList() as any
-
-    this.upstreamList = nodes.map((item: any) => {
-      const id = item.key.match(/\/([0-9]+)/)[1]
-      return {
-        ...item.value,
-        id
-      }
-    })
-  }
-
-  private async getServiceList() {
-    const {
-      node: {
-        nodes = []
-      }
-    } = await getServiceList() as any
-
-    this.serviceList = nodes.map((item: any) => {
-      const id = item.key.match(/\/([0-9]+)/)[1]
-      return {
-        ...item.value,
-        id
-      }
-    })
-  }
-
-  private async getPluginList() {
-    this.pluginList = await getPluginList() as any
-  }
-
-  private async showPlugin(name: string) {
-    this.pluginName = name
-    this.showPluginDialog = true
-  }
-
-  private onPluginSave(name: string, data: any) {
-    delete this.form.plugins['tempPlugin']
-    this.showPluginDialog = false
-    this.form.plugins[name] = data
-  }
-
-  private async addPlugin() {
-    if (this.form.plugins.hasOwnProperty('tempPlugin')) return
-
-    this.form.plugins = {
-      ...this.form.plugins,
-      tempPlugin: null
-    }
-  }
-
-  private removePlugin(name: any) {
-    this.$confirm(`Do you want to remove ${name} plugin?`, 'Warning', {
-      confirmButtonText: 'Confirm',
-      cancelButtonText: 'Cancel',
-      type: 'warning'
-    })
-      .then(async() => {
-        Vue.delete(this.form.plugins, name)
-      }).catch(() => {})
-  }
-
-  private onVarArgsChange(val: any) {
-    this.form.vars = val
-  }
-}
-</script>
-
-<style lang='scss'>
-.container {
-  padding: 20px;
-  .el-form {
-    .el-form-item {
-      .el-form-item__content {
-        .el-input {
-          width: 300px;
-        }
-        .tip {
-          line-height: 24px;
-          font-size: 12px;
-          margin: 0;
-          color: #8e8c8c;
-        }
-      }
-    }
-  }
-  .var-item {
-    .el-form-item {
-      margin-bottom: 10px;
-      display: inline-block;
-      .el-form-item__content {
-        margin-right: 10px;
-      }
-    }
-  }
-}
-</style>
diff --git a/src/views/schema/routes/list.vue b/src/views/schema/routes/list.vue
deleted file mode 100644
index 2014c4d..0000000
--- a/src/views/schema/routes/list.vue
+++ /dev/null
@@ -1,252 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="app-container">
-    <div class="filter-container">
-      <el-button
-        class="filter-item"
-        style="margin-left: 10px;"
-        type="primary"
-        icon="el-icon-edit"
-        @click="handleCreate"
-      >
-        {{ $t('table.add') }}
-      </el-button>
-    </div>
-
-    <el-table
-      :key="tableKey"
-      v-loading="listLoading"
-      :data="tableData"
-      :border="false"
-      fit
-      highlight-current-row
-      style="width: 100%;"
-      :default-sort="{prop: 'id', order: 'descending'}"
-      @sort-change="sortChange"
-    >
-      <el-table-column
-        v-for="(item, index) of tableKeys"
-        :key="index"
-        :label="item.key"
-        :prop="item.key"
-        :width="item.width"
-        :class-name="item.align === 'left' ? '' : 'status-col'"
-        header-align="center"
-      />
-      <el-table-column
-        :label="$t('table.actions')"
-        align="center"
-        width="200"
-        class-name="fixed-width"
-        fixed="right"
-      >
-        <template slot-scope="{row}">
-          <el-button
-            type="primary"
-            size="mini"
-            @click="handleToEdit(row)"
-          >
-            {{ $t('table.edit') }}
-          </el-button>
-
-          <el-button
-            v-if="row.status!=='deleted'"
-            size="mini"
-            type="danger"
-            @click="handleRemove(row)"
-          >
-            {{ $t('table.delete') }}
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
-import { Form } from 'element-ui'
-
-import Pagination from '../../../components/Pagination/index.vue'
-
-import { getList, removeRouter } from '../../../api/schema/routes'
-
-@Component({
-  name: 'RoutesList',
-  components: {
-    Pagination
-  }
-})
-export default class extends Vue {
-  private tableKey = 0
-  private list = []
-  private total = 0
-  private listLoading = true
-  private listQuery = {
-    page: 1,
-    limit: 20,
-    importance: undefined,
-    title: undefined,
-    type: undefined,
-    sort: '+id'
-  }
-
-  private tableData = []
-  private tableKeys: any[] = []
-
-  created() {
-    this.getList()
-  }
-
-  private async getList() {
-    this.listLoading = true
-    this.tableKeys = [
-      {
-        key: 'id',
-        width: 80
-      }, {
-        key: 'description',
-        width: 300,
-        align: 'left'
-      }, {
-        key: 'uri',
-        width: 200
-      }, {
-        key: 'host',
-        width: 200
-      }, {
-        key: 'remote_addr',
-        width: 200
-      }, {
-        key: 'upstream_id',
-        width: 200
-      }, {
-        key: 'service_id',
-        width: 200
-      }, {
-        key: 'methods',
-        width: 200
-      }, {
-        key: 'plugins',
-        width: 400
-      }
-    ]
-
-    let { node: { nodes = [] } } = await getList() as any
-    nodes = [...nodes].map((item: any) => {
-      const id = item.key.match(/\/([a-zA-Z0-9-_]+)$/)[1]
-      const fakeId = id
-
-      let {
-        uri = '',
-        host = '',
-        remote_addr = '',
-        upstream_id = '',
-        service_id = '',
-        methods = [],
-        plugins = {},
-        desc = ''
-      } = item.value
-
-      methods = methods.join(', ')
-
-      if (item.value.uris) {
-        uri = item.value.uris.join(', ')
-      }
-
-      if (item.value.hosts) {
-        host = item.value.hosts.join('\n ')
-      }
-
-      plugins = Object.keys(plugins as any).join(', ')
-
-      return {
-        id: fakeId,
-        realId: id,
-        uri,
-        host,
-        remote_addr,
-        upstream_id: upstream_id && String(upstream_id).replace(/^(0+)/, ''),
-        service_id: service_id && String(service_id).replace(/^(0+)/, ''),
-        methods,
-        plugins,
-        description: desc
-      }
-    })
-
-    this.tableData = nodes
-    this.total = nodes.length
-
-    setTimeout(() => {
-      this.listLoading = false
-    }, 0.5 * 1000)
-  }
-
-  private handleFilter() {
-    this.listQuery.page = 1
-    this.getList()
-  }
-
-  private handleRemove(row: any) {
-    this.$confirm(`Do you want to remove router ${row.id}?`, 'Warning', {
-      confirmButtonText: 'Confirm',
-      cancelButtonText: 'Cancel',
-      type: 'warning'
-    })
-      .then(async() => {
-        await removeRouter(row.realId)
-        this.getList()
-        this.$message.success(`Remove router ${row.id} successfully!`)
-      })
-  }
-
-  private sortChange(data: any) {
-    const { prop, order } = data
-    if (prop === 'id') {
-      this.sortByID(order)
-    }
-  }
-
-  private sortByID(order: string) {
-    if (order === 'ascending') {
-      this.listQuery.sort = '+id'
-    } else {
-      this.listQuery.sort = '-id'
-    }
-    this.handleFilter()
-  }
-
-  private handleCreate() {
-    this.$router.push({
-      name: 'SchemaRoutesCreate'
-    })
-  }
-
-  private handleToEdit(row: any) {
-    this.$router.push({
-      name: 'SchemaRoutesEdit',
-      params: {
-        id: row.realId
-      }
-    })
-  }
-}
-</script>
diff --git a/src/views/schema/service/edit.vue b/src/views/schema/service/edit.vue
deleted file mode 100644
index 9994b0a..0000000
--- a/src/views/schema/service/edit.vue
+++ /dev/null
@@ -1,300 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="container">
-    <el-form
-      ref="form"
-      :model="form"
-      :rules="rules"
-      label-width="80px"
-      :show-message="false"
-    >
-      <el-form-item
-        label="Desc"
-      >
-        <el-input
-          v-model="form.desc"
-          placeholder="Description"
-        />
-      </el-form-item>
-
-      <el-form-item
-        label="Upstream"
-      >
-        <el-select
-          v-model="form.upstream_id"
-          placeholder="Upstream"
-        >
-          <el-option
-            v-for="item in upstreamList"
-            :key="item.id"
-            :label="item.desc"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item
-        v-for="(index, item) in form.plugins"
-        :key="item"
-        :label="&quot;plugin&quot;"
-        class="plugin-item"
-      >
-        <el-button
-          v-if="item !== 'tempPlugin'"
-          type="info"
-          plain
-          @click="showPlugin(item)"
-        >
-          {{ item }}
-        </el-button>
-
-        <el-button
-          v-if="item !== 'tempPlugin'"
-          type="danger"
-          @click.prevent="removePlugin(item)"
-        >
-          {{ $t('button.delete') }}
-        </el-button>
-
-        <el-select
-          v-if="item === 'tempPlugin'"
-          :value="null"
-          class="plugin-select"
-          placeholder="Select a Plugin"
-          @change="showPlugin"
-        >
-          <el-option
-            v-for="name in filteredPluginList"
-            :key="name"
-            :label="name"
-            :value="name"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item>
-        <el-button
-          :disabled="!filteredPluginList.length"
-          @click="addPlugin"
-        >
-          {{ $t('button.add_plugin') }}
-        </el-button>
-      </el-form-item>
-
-      <el-form-item>
-        <el-button
-          type="primary"
-          @click="onSubmit"
-        >
-          {{ $t('button.save') }}
-        </el-button>
-        <el-button @click="toPreviousPage">
-          {{ $t('button.cancel') }}
-        </el-button>
-      </el-form-item>
-    </el-form>
-    <PluginDialog
-      :show="showPluginDialog"
-      :name="pluginName"
-      :plugin-data="form.plugins[pluginName]"
-      @hidePlugin="showPluginDialog = false"
-      @save="onPluginSave"
-    />
-  </div>
-</template>
-
-<script lang='ts'>
-import { Component, Vue } from 'vue-property-decorator'
-import { Form } from 'element-ui'
-
-import PluginDialog from '@/components/PluginDialog/index.vue'
-
-import { getUpstreamList } from '@/api/schema/upstream'
-import { getService, createService, updateService } from '@/api/schema/services'
-import { getPluginList } from '@/api/schema/plugins'
-import { TagsViewModule } from '@/store/modules/tags-view'
-
-@Component({
-  name: 'RouterEdit',
-  components: {
-    PluginDialog
-  }
-})
-export default class extends Vue {
-  private form = {
-    plugins: {},
-    upstream_id: '',
-    desc: ''
-  }
-
-  private rules = {}
-  private isEditMode: boolean = false
-
-  private pluginList = []
-  private pluginName: string = ''
-  private showPluginDialog: boolean = false
-  private upstreamList = []
-
-  created() {
-    this.isEditMode = (this.$route as any).name.indexOf('Create') === -1
-
-    this.getUpstreamList()
-    this.getPluginList()
-
-    if (this.isEditMode) {
-      this.getData()
-    }
-  }
-
-  get filteredPluginList() {
-    return this.pluginList.filter(item => !this.form.plugins.hasOwnProperty(item))
-  }
-
-  private toPreviousPage() {
-    this.$router.go(-1)
-  }
-
-  private async getData() {
-    const { id } = this.$route.params
-    let {
-      node: {
-        value: {
-          plugins = {},
-          upstream_id = '',
-          desc = ''
-        }
-      }
-    } = (await getService(id)) as any
-
-    this.form = {
-      plugins,
-      upstream_id,
-      desc
-    }
-  }
-
-  private async getUpstreamList() {
-    const {
-      node: {
-        nodes = []
-      }
-    } = await getUpstreamList() as any
-
-    this.upstreamList = nodes.map((item: any) => {
-      const id = item.key.match(/\/([0-9]+)/)[1]
-      return {
-        ...item.value,
-        id
-      }
-    })
-  }
-
-  private async getPluginList() {
-    this.pluginList = await getPluginList() as any
-  }
-
-  private async showPlugin(name: string) {
-    this.pluginName = name
-    this.showPluginDialog = true
-  }
-
-  private onPluginSave(name: string, data: any) {
-    delete this.form.plugins['tempPlugin']
-    this.showPluginDialog = false
-    this.form.plugins[name] = data
-  }
-
-  private async addPlugin() {
-    if (this.form.plugins.hasOwnProperty('tempPlugin')) return
-
-    this.form.plugins = {
-      ...this.form.plugins,
-      tempPlugin: null
-    }
-  }
-
-  private async onSubmit() {
-    (this.$refs.form as any).validate(async(valid: boolean) => {
-      console.log('onSubmit', this.form)
-
-      if (valid) {
-        let data = Object.assign({}, this.form)
-        Object.entries(data).forEach(([key, value]) => {
-          if (typeof data[key] === 'object') {
-            if (key !== 'plugins' && Object.keys(value).length === 0) {
-              delete data[key]
-            }
-          } else {
-            if (value === '') {
-              delete data[key]
-            }
-          }
-        })
-
-        if (this.isEditMode) {
-          await updateService(this.$route.params.id, data)
-        } else {
-          await createService(data)
-        }
-
-        this.$message.success(`${this.isEditMode ? 'Update the' : 'Create a'} service successfully!`)
-
-        if (!this.isEditMode) {
-          TagsViewModule.delView(this.$route)
-          this.$nextTick(() => {
-            this.$router.push({
-              name: 'SchemaServiceList'
-            })
-          })
-        }
-      } else {
-        return false
-      }
-    })
-  }
-
-  private removePlugin(name: any) {
-    this.$confirm(`Do you want to remove ${name} plugin?`, 'Warning', {
-      confirmButtonText: 'Confirm',
-      cancelButtonText: 'Cancel',
-      type: 'warning'
-    })
-      .then(async() => {
-        Vue.delete(this.form.plugins, name)
-      }).catch(() => {})
-  }
-}
-</script>
-
-<style lang='scss'>
-.container {
-  padding: 20px;
-  .el-form {
-    .el-form-item {
-      .el-form-item__content {
-        .el-input {
-          width: 220px;
-        }
-      }
-    }
-  }
-}
-</style>
diff --git a/src/views/schema/service/list.vue b/src/views/schema/service/list.vue
deleted file mode 100644
index c4f424c..0000000
--- a/src/views/schema/service/list.vue
+++ /dev/null
@@ -1,215 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="app-container">
-    <div class="filter-container">
-      <el-button
-        class="filter-item"
-        style="margin-left: 10px;"
-        type="primary"
-        icon="el-icon-edit"
-        @click="handleCreate"
-      >
-        {{ $t('table.add') }}
-      </el-button>
-    </div>
-
-    <el-table
-      :key="tableKey"
-      v-loading="listLoading"
-      :data="tableData"
-      :border="false"
-      fit
-      highlight-current-row
-      style="width: 100%;"
-      :default-sort="{prop: 'id', order: 'descending'}"
-      @sort-change="sortChange"
-    >
-      <el-table-column
-        v-for="(item, index) of tableKeys"
-        :key="index"
-        :label="item.key"
-        :prop="item.key"
-        :width="item.width"
-        :class-name="item.align === 'left' ? '' : 'status-col'"
-        header-align="center"
-      />
-      <el-table-column
-        :label="$t('table.actions')"
-        align="center"
-        width="230"
-        class-name="fixed-width"
-      >
-        <template slot-scope="{row}">
-          <el-button
-            type="primary"
-            size="mini"
-            @click="handleToEdit(row)"
-          >
-            {{ $t('table.edit') }}
-          </el-button>
-
-          <el-button
-            v-if="row.status!=='deleted'"
-            size="mini"
-            type="danger"
-            @click="handleRemove(row)"
-          >
-            {{ $t('table.delete') }}
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
-import { Form } from 'element-ui'
-
-import Pagination from '@/components/Pagination/index.vue'
-
-import { getServiceList, removeService } from '@/api/schema/services'
-
-@Component({
-  name: 'ServiceList',
-  components: {
-    Pagination
-  }
-})
-export default class extends Vue {
-  private tableKey = 0
-  private list = []
-  private total = 0
-  private listLoading = true
-  private listQuery = {
-    page: 1,
-    limit: 20,
-    importance: undefined,
-    title: undefined,
-    type: undefined,
-    sort: '+id'
-  }
-
-  private tableData = []
-  private tableKeys: any[] = []
-
-  created() {
-    this.getList()
-  }
-
-  private async getList() {
-    this.listLoading = true
-
-    this.tableKeys = [
-      {
-        key: 'id',
-        width: 100
-      }, {
-        key: 'description',
-        width: 300,
-        align: 'left'
-      }, {
-        key: 'plugins',
-        width: 400
-      }
-    ]
-    let { node: { nodes = [] } } = await getServiceList() as any
-    nodes = [...nodes].map((item: any) => {
-      const id = item.key.match(/\/([a-zA-Z0-9-_]+)$/)[1]
-      const fakeId = id
-      const desc = item.value.desc
-
-      const pluginArr: any[] = []
-      if (item.value.plugins !== undefined) {
-        Object.entries(item.value.plugins as any).map(([ key, value ]: any) => {
-          pluginArr.push({
-            name: key,
-            key: value.key
-          })
-        })
-      }
-
-      return {
-        id: fakeId,
-        realId: id,
-        plugins: pluginArr.map((item: any) => item.name).join(', '),
-        description: desc
-      }
-    })
-
-    this.tableData = nodes
-    this.total = nodes.length
-
-    setTimeout(() => {
-      this.listLoading = false
-    }, 0.5 * 1000)
-  }
-
-  private handleFilter() {
-    this.listQuery.page = 1
-    this.getList()
-  }
-
-  private handleRemove(row: any) {
-    this.$confirm(`Do you want to remove service ${row.id}?`, 'Warning', {
-      confirmButtonText: 'Confirm',
-      cancelButtonText: 'Cancel',
-      type: 'warning'
-    })
-      .then(async() => {
-        await removeService(row.realId)
-        this.getList()
-        this.$message.success(`Remove service ${row.id} successfully!`)
-      })
-  }
-
-  private sortChange(data: any) {
-    const { prop, order } = data
-    if (prop === 'id') {
-      this.sortByID(order)
-    }
-  }
-
-  private sortByID(order: string) {
-    if (order === 'ascending') {
-      this.listQuery.sort = '+id'
-    } else {
-      this.listQuery.sort = '-id'
-    }
-    this.handleFilter()
-  }
-
-  private handleCreate() {
-    this.$router.push({
-      name: 'SchemaServiceCreate'
-    })
-  }
-
-  private handleToEdit(row: any) {
-    this.$router.push({
-      name: 'SchemaServiceEdit',
-      params: {
-        id: row.realId
-      }
-    })
-  }
-}
-</script>
diff --git a/src/views/schema/ssl/edit.vue b/src/views/schema/ssl/edit.vue
deleted file mode 100644
index 09c30bf..0000000
--- a/src/views/schema/ssl/edit.vue
+++ /dev/null
@@ -1,191 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="container">
-    <el-form
-      ref="form"
-      :model="form"
-      :rules="rules"
-      label-width="80px"
-      :show-message="false"
-    >
-      <el-form-item
-        label="SNI"
-        prop="sni"
-      >
-        <el-input
-          v-model="form.sni"
-          placeholder="SNI"
-        />
-      </el-form-item>
-
-      <el-form-item
-        label="CERT"
-        prop="cert"
-      >
-        <el-input
-          v-model="form.cert"
-          placeholder="CERT"
-          type="textarea"
-          :rows="7"
-        />
-      </el-form-item>
-
-      <el-form-item
-        label="KEY"
-        prop="key"
-      >
-        <el-input
-          v-model="form.key"
-          placeholder="KEY"
-          type="textarea"
-          :rows="7"
-        />
-      </el-form-item>
-
-      <el-form-item>
-        <el-button
-          type="primary"
-          @click="onSubmit"
-        >
-          {{ $t('button.save') }}
-        </el-button>
-        <el-button @click="toPreviousPage">
-          {{ $t('button.cancel') }}
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </div>
-</template>
-
-<script lang='ts'>
-import { Component, Vue } from 'vue-property-decorator'
-import { Form } from 'element-ui'
-
-import { TagsViewModule } from '@/store/modules/tags-view'
-import { getSSL, updateSSL, createSSL } from '@/api/schema/ssl'
-
-@Component({
-  name: 'RouterEdit'
-})
-export default class extends Vue {
-  private form = {
-    sni: '',
-    cert: '',
-    key: ''
-  };
-
-  private rules = {
-    sni: {
-      required: true
-    },
-    cert: {
-      required: true
-    },
-    key: {
-      required: true
-    }
-  }
-  private isEditMode: boolean = false;
-
-  created() {
-    this.isEditMode = (this.$route as any).name.indexOf('Create') === -1
-
-    if (this.isEditMode) {
-      this.getData()
-    }
-  }
-
-  private async getData() {
-    const { id } = this.$route.params
-    let {
-      node: {
-        value: {
-          sni = '',
-          cert = '',
-          key = ''
-        }
-      }
-    } = (await getSSL(id)) as any
-
-    this.form = {
-      sni,
-      cert,
-      key
-    }
-  }
-
-  private async onSubmit() {
-    (this.$refs.form as any).validate(async(valid: boolean) => {
-      console.log('onSubmit', this.form)
-
-      if (valid) {
-        let data = Object.assign({}, this.form)
-        Object.entries(data).forEach(([key, value]) => {
-          if (value === '') {
-            delete data[key]
-          }
-        })
-
-        if (this.isEditMode) {
-          await updateSSL(this.$route.params.id, data)
-        } else {
-          await createSSL(data)
-        }
-
-        this.$message.success(
-          `${this.isEditMode ? 'Update the' : 'Create a'} ssl successfully!`
-        )
-
-        if (!this.isEditMode) {
-          TagsViewModule.delView(this.$route)
-          this.$nextTick(() => {
-            this.$router.push({
-              name: 'SchemaSSLList'
-            })
-          })
-        }
-      } else {
-        return false
-      }
-    })
-  }
-
-  private toPreviousPage() {
-    this.$router.go(-1)
-  }
-}
-</script>
-
-<style lang='scss'>
-.container {
-  padding: 20px;
-  .el-form-item {
-    .el-form-item__content {
-      .el-input {
-        width: 300px;
-      }
-      .el-textarea {
-        width: 400px;
-      }
-    }
-  }
-}
-</style>
diff --git a/src/views/schema/ssl/list.vue b/src/views/schema/ssl/list.vue
deleted file mode 100644
index ff20897..0000000
--- a/src/views/schema/ssl/list.vue
+++ /dev/null
@@ -1,206 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="app-container">
-    <div class="filter-container">
-      <el-button
-        class="filter-item"
-        style="margin-left: 10px;"
-        type="primary"
-        icon="el-icon-edit"
-        @click="handleCreate"
-      >
-        {{ $t('table.add') }}
-      </el-button>
-    </div>
-
-    <el-table
-      :key="tableKey"
-      v-loading="listLoading"
-      :data="tableData"
-      :border="false"
-      fit
-      highlight-current-row
-      style="width: 100%;"
-      :default-sort="{prop: 'id', order: 'descending'}"
-      @sort-change="sortChange"
-    >
-      <el-table-column
-        v-for="(item, index) of tableKeys"
-        :key="index"
-        :label="item.key"
-        :prop="item.key"
-        :width="item.width"
-        class-name="status-col"
-      />
-      <el-table-column
-        :label="$t('table.actions')"
-        align="center"
-        width="230"
-        class-name="fixed-width"
-      >
-        <template slot-scope="{row}">
-          <el-button
-            type="primary"
-            size="mini"
-            @click="handleToEdit(row)"
-          >
-            {{ $t('table.edit') }}
-          </el-button>
-
-          <el-button
-            v-if="row.status!=='deleted'"
-            size="mini"
-            type="danger"
-            @click="handleRemove(row)"
-          >
-            {{ $t('table.delete') }}
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
-import { Form } from 'element-ui'
-
-import Pagination from '@/components/Pagination/index.vue'
-
-import { getSSLList, removeSSL } from '@/api/schema/ssl'
-
-@Component({
-  name: 'UpstreamList',
-  components: {
-    Pagination
-  }
-})
-export default class extends Vue {
-  private tableKey = 0
-  private list = []
-  private total = 0
-  private listLoading = true
-  private listQuery = {
-    page: 1,
-    limit: 20,
-    importance: undefined,
-    title: undefined,
-    type: undefined,
-    sort: '+id'
-  }
-
-  private tableData = []
-  private tableKeys: any[] = []
-
-  created() {
-    this.getList()
-  }
-
-  private async getList() {
-    this.listLoading = true
-
-    this.tableKeys = [
-      {
-        key: 'id',
-        width: 100
-      }, {
-        key: 'sni',
-        width: 300
-      }
-    ]
-    let { node: { nodes = [] } } = await getSSLList() as any
-    nodes = [...nodes].map((item: any) => {
-      const id = item.key.match(/\/([a-zA-Z0-9-_]+)$/)[1]
-      const fakeId = id
-
-      return {
-        id: fakeId,
-        realId: id,
-        sni: item.value.sni
-      }
-    })
-
-    this.tableData = nodes
-    this.total = nodes.length
-
-    setTimeout(() => {
-      this.listLoading = false
-    }, 0.5 * 1000)
-  }
-
-  private handleFilter() {
-    this.listQuery.page = 1
-    this.getList()
-  }
-
-  private handleRemove(row: any) {
-    this.$confirm(`Do you want to remove ssl ${row.id}?`, 'Warning', {
-      confirmButtonText: 'Confirm',
-      cancelButtonText: 'Cancel',
-      type: 'warning'
-    })
-      .then(async() => {
-        await removeSSL(row.realId)
-        this.getList()
-        this.$message.success(`Remove ssl ${row.id} successfully!`)
-      })
-  }
-
-  private sortChange(data: any) {
-    const { prop, order } = data
-    if (prop === 'id') {
-      this.sortByID(order)
-    }
-  }
-
-  private sortByID(order: string) {
-    if (order === 'ascending') {
-      this.listQuery.sort = '+id'
-    } else {
-      this.listQuery.sort = '-id'
-    }
-    this.handleFilter()
-  }
-
-  private handleCreate() {
-    this.$router.push({
-      name: 'SchemaSSLCreate'
-    })
-  }
-
-  private handleToEdit(row: any) {
-    this.$router.push({
-      name: 'SchemaSSLEdit',
-      params: {
-        id: row.realId
-      }
-    })
-  }
-}
-</script>
-
-<style lang="scss">
-.app-container {
-  .el-table::before {
-    height: 0 !important;
-  }
-}
-</style>
diff --git a/src/views/schema/upstream/edit.vue b/src/views/schema/upstream/edit.vue
deleted file mode 100644
index 445e42d..0000000
--- a/src/views/schema/upstream/edit.vue
+++ /dev/null
@@ -1,434 +0,0 @@
-<!--
-#
-# 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.
-#
--->
-
-<template>
-  <div class="container">
-    <el-form
-      ref="form"
-      :model="form"
-      :rules="rules"
-      label-width="80px"
-      :show-message="false"
-    >
-      <el-form-item
-        label="Desc"
-      >
-        <el-input
-          v-model="form.desc"
-          placeholder="Description"
-        />
-      </el-form-item>
-
-      <el-form-item
-        label="Type"
-        prop="type"
-      >
-        <el-select
-          v-model="form.type"
-          placeholder="Select a Type"
-          @change="typeSelectorChange"
-        >
-          <el-option
-            v-for="item in types"
-            :key="item"
-            :label="item"
-            :value="item"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item
-        label="Hash On"
-      >
-        <el-select
-          v-model="form.hash_on"
-          filterable
-          default-first-option
-          value-key="form.hash_on"
-          :disabled="form.type !== 'chash'"
-        >
-          <el-option
-            v-for="item in defaultHashOnKeys"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
-          />
-        </el-select>
-        <p class="tip">
-          {{ $t('upstream.hashOnTip') }}
-        </p>
-      </el-form-item>
-
-      <el-form-item
-        label="Key"
-      >
-        <el-select
-          v-model="form.key"
-          placeholder="Input a Key"
-          allow-create
-          filterable
-          default-first-option
-          value-key="form.key"
-          clearable
-          :disabled="form.type !== 'chash'"
-        >
-          <el-option
-            v-for="item in defaultHashKeys"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
-          />
-        </el-select>
-        <p class="tip">
-          {{ $t('upstream.keyTip') }}
-        </p>
-      </el-form-item>
-
-      <el-form-item
-        label="WebSocket"
-      >
-        <el-switch
-          v-model="form.enable_websocket"
-
-          active-color="#13ce66"
-          inactive-color="#ff4949"
-        />
-        <p class="tip">
-          {{ form.enable_websocket ? $t('upstream.websocket.EnabledTip') : $t('upstream.websocket.DisabledTip') }}
-        </p>
-      </el-form-item>
-
-      <el-form-item
-        v-for="(item, index) in form.nodes"
-        :key="index"
-        :label="'Node' + (index + 1)"
-        class="node-item"
-      >
-        <el-form-item
-          :rules="[{required: true, pattern: IPAndURLRegexp, type: 'string'}]"
-          :prop="'nodes.' + index + '.ip'"
-        >
-          <el-input
-            v-model="item.ip"
-            placeholder="IP/HOST"
-          />
-        </el-form-item>
-        <el-form-item
-          :rules="[{required: true}]"
-          :prop="'nodes.' + index + '.port'"
-        >
-          <el-input
-            v-model="item.port"
-            placeholder="Port"
-            type="number"
-          />
-        </el-form-item>
-        <el-form-item
-          :rules="[{required: true}]"
-          :prop="'nodes.' + index + '.weights'"
-        >
-          <el-input
-            v-model="item.weights"
-            placeholder="Weights"
-            type="number"
-          />
-        </el-form-item>
-        <el-form-item>
-          <el-button
-            type="danger"
-            @click.prevent="removeNode(item)"
-          >
-            {{ $t('button.delete') }}
-          </el-button>
-        </el-form-item>
-      </el-form-item>
-
-      <el-form-item>
-        <el-button
-          @click="addNode"
-        >
-          {{ $t('button.add_node') }}
-        </el-button>
-      </el-form-item>
-
-      <el-form-item>
-        <el-button
-          type="primary"
-          @click="onSubmit"
-        >
-          {{ $t('button.save') }}
-        </el-button>
-        <el-button @click="toPreviousPage">
-          {{ $t('button.cancel') }}
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </div>
-</template>
-
-<script lang='ts'>
-import { Component, Vue } from 'vue-property-decorator'
-import { Form } from 'element-ui'
-
-import { getUpstream, createUpstream, updateStream } from '../../../api/schema/upstream'
-import { TagsViewModule } from '@/store/modules/tags-view'
-
-@Component({
-  name: 'RouterEdit'
-})
-export default class extends Vue {
-  private IPAndURLRegexp = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})$|^((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9]*[A-Za-z0-9]))$/
-  private form = {
-    type: null,
-    key: null,
-    nodes: [],
-    desc: '',
-    enable_websocket: null,
-    hash_on: null
-  }
-
-  private rules = {
-    type: {
-      required: true
-    }
-  }
-  private isEditMode: boolean = false
-
-  private types = ['roundrobin', 'chash']
-
-  private defaultHashOnKeys = [
-    {
-      value: 'vars',
-      label: 'vars'
-    },
-    {
-      value: 'header',
-      label: 'header'
-    },
-    {
-      value: 'cookie',
-      label: 'cookie'
-    },
-    {
-      value: 'consumer',
-      label: 'consumer'
-    }
-  ]
-
-  private defaultHashKeys = [
-    {
-      value: 'remote_addr',
-      label: 'remote_addr'
-    },
-    {
-      value: 'host',
-      label: 'host'
-    },
-    {
-      value: 'uri',
-      label: 'uri'
-    },
-    {
-      value: 'server_name',
-      label: 'server_name'
-    },
-    {
... 10053 lines suppressed ...