You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@skywalking.apache.org by ha...@apache.org on 2017/12/18 08:04:09 UTC

[incubator-skywalking-ui] branch feature/5.0.0 created (now 381f40b)

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

hanahmily pushed a change to branch feature/5.0.0
in repository https://gitbox.apache.org/repos/asf/incubator-skywalking-ui.git.


      at 381f40b  Finished dashboard demo

This branch includes the following new commits:

     new b22b60d  Init frontend
     new 381f40b  Finished dashboard demo

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.


-- 
To stop receiving notification emails like this one, please contact
['"commits@skywalking.apache.org" <co...@skywalking.apache.org>'].

[incubator-skywalking-ui] 01/02: Init frontend

Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch feature/5.0.0
in repository https://gitbox.apache.org/repos/asf/incubator-skywalking-ui.git

commit b22b60dcb931ad01ee702a4abee19a30636995bd
Author: gaohongtao <ha...@gmail.com>
AuthorDate: Thu Dec 14 09:49:57 2017 +0800

    Init frontend
---
 src/main/frontend/.editorconfig                    |  16 +
 src/main/frontend/.eslintrc                        |  53 +++
 src/main/frontend/.ga                              |   3 +
 src/main/frontend/.gitignore                       |  15 +
 src/main/frontend/.roadhogrc                       |  23 ++
 src/main/frontend/.roadhogrc.mock.js               |  58 ++++
 src/main/frontend/.stylelintrc                     |  25 ++
 src/main/frontend/.travis.yml                      |  33 ++
 src/main/frontend/LICENSE                          | 201 +++++++++++
 src/main/frontend/README.md                        |   1 +
 src/main/frontend/appveyor.yml                     |  22 ++
 src/main/frontend/mock/.gitkeep                    |   0
 src/main/frontend/mock/notices.js                  |  85 +++++
 src/main/frontend/mock/rule.js                     | 127 +++++++
 src/main/frontend/mock/utils.js                    |  45 +++
 src/main/frontend/package.json                     | 107 ++++++
 src/main/frontend/public/index.html                |  15 +
 src/main/frontend/src/common/nav.js                |  26 ++
 .../src/components/StandardFormRow/index.js        |  26 ++
 .../src/components/StandardFormRow/index.less      |  71 ++++
 .../frontend/src/components/StandardTable/index.js | 154 +++++++++
 .../src/components/StandardTable/index.less        |  13 +
 src/main/frontend/src/e2e/home.e2e.js              |   9 +
 src/main/frontend/src/index.js                     |  22 ++
 src/main/frontend/src/index.less                   |  16 +
 src/main/frontend/src/layouts/BasicLayout.js       | 375 +++++++++++++++++++++
 src/main/frontend/src/layouts/BasicLayout.less     | 113 +++++++
 src/main/frontend/src/layouts/BlankLayout.js       |   3 +
 src/main/frontend/src/layouts/PageHeaderLayout.js  |  12 +
 .../frontend/src/layouts/PageHeaderLayout.less     |  11 +
 src/main/frontend/src/models/dashboard.js          |  16 +
 src/main/frontend/src/models/global.js             |  76 +++++
 src/main/frontend/src/models/index.js              |  11 +
 src/main/frontend/src/models/rule.js               |  80 +++++
 src/main/frontend/src/models/user.js               |  66 ++++
 src/main/frontend/src/polyfill.js                  |   7 +
 src/main/frontend/src/router.js                    |  60 ++++
 .../frontend/src/routes/Dashboard/Dashboard.js     |  13 +
 .../frontend/src/routes/Dashboard/Dashboard.less   |  23 ++
 src/main/frontend/src/routes/List/TableList.js     | 326 ++++++++++++++++++
 src/main/frontend/src/routes/List/TableList.less   |  45 +++
 src/main/frontend/src/services/api.js              |  30 ++
 src/main/frontend/src/services/user.js             |   9 +
 src/main/frontend/src/theme.js                     |   5 +
 src/main/frontend/src/utils/request.js             |  56 +++
 src/main/frontend/src/utils/utils.js               |  94 ++++++
 src/main/frontend/src/utils/utils.less             |  50 +++
 src/main/frontend/tests/jasmine.js                 |   1 +
 src/main/frontend/tests/run-tests.js               |  35 ++
 src/main/frontend/tests/setupTests.js              |  13 +
 src/main/frontend/tests/styleMock.js               |   1 +
 51 files changed, 2697 insertions(+)

diff --git a/src/main/frontend/.editorconfig b/src/main/frontend/.editorconfig
new file mode 100755
index 0000000..7e3649a
--- /dev/null
+++ b/src/main/frontend/.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/src/main/frontend/.eslintrc b/src/main/frontend/.eslintrc
new file mode 100755
index 0000000..5209bd9
--- /dev/null
+++ b/src/main/frontend/.eslintrc
@@ -0,0 +1,53 @@
+{
+  "parser": "babel-eslint",
+  "extends": "airbnb",
+  "env": {
+    "browser": true,
+    "node": true,
+    "es6": true,
+    "mocha": true,
+    "jest": true,
+    "jasmine": true
+  },
+  "rules": {
+    "generator-star-spacing": [0],
+    "consistent-return": [0],
+    "react/forbid-prop-types": [0],
+    "react/jsx-filename-extension": [1, { "extensions": [".js"] }],
+    "global-require": [1],
+    "import/prefer-default-export": [0],
+    "react/jsx-no-bind": [0],
+    "react/prop-types": [0],
+    "react/prefer-stateless-function": [0],
+    "no-else-return": [0],
+    "no-restricted-syntax": [0],
+    "import/no-extraneous-dependencies": [0],
+    "no-use-before-define": [0],
+    "jsx-a11y/no-static-element-interactions": [0],
+    "jsx-a11y/no-noninteractive-element-interactions": [0],
+    "jsx-a11y/click-events-have-key-events": [0],
+    "jsx-a11y/anchor-is-valid": [0],
+    "no-nested-ternary": [0],
+    "arrow-body-style": [0],
+    "import/extensions": [0],
+    "no-bitwise": [0],
+    "no-cond-assign": [0],
+    "import/no-unresolved": [0],
+    "comma-dangle": ["error", {
+      "arrays": "always-multiline",
+      "objects": "always-multiline",
+      "imports": "always-multiline",
+      "exports": "always-multiline",
+      "functions": "ignore"
+    }],
+    "object-curly-newline": [0],
+    "function-paren-newline": [0],
+    "no-restricted-globals": [0],
+    "require-yield": [1]
+  },
+  "parserOptions": {
+    "ecmaFeatures": {
+      "experimentalObjectRestSpread": true
+    }
+  }
+}
diff --git a/src/main/frontend/.ga b/src/main/frontend/.ga
new file mode 100644
index 0000000..1932bb5
--- /dev/null
+++ b/src/main/frontend/.ga
@@ -0,0 +1,3 @@
+{
+    "code":"UA-72788897-6"
+} 
diff --git a/src/main/frontend/.gitignore b/src/main/frontend/.gitignore
new file mode 100755
index 0000000..c03ea9a
--- /dev/null
+++ b/src/main/frontend/.gitignore
@@ -0,0 +1,15 @@
+# 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
+
+# production
+/dist
+
+# misc
+.DS_Store
+npm-debug.log*
+
+/coverage
diff --git a/src/main/frontend/.roadhogrc b/src/main/frontend/.roadhogrc
new file mode 100755
index 0000000..e64dacf
--- /dev/null
+++ b/src/main/frontend/.roadhogrc
@@ -0,0 +1,23 @@
+{
+  "entry": "src/index.js",
+  "extraBabelPlugins": [
+    "transform-runtime",
+    "transform-decorators-legacy",
+    "transform-class-properties",
+    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }]
+  ],
+  "env": {
+    "development": {
+      "extraBabelPlugins": [
+        "dva-hmr"
+      ]
+    }
+  },
+  "externals": {
+    "g2": "G2",
+    "g-cloud": "Cloud",
+    "g2-plugin-slider": "G2.Plugin.slider"
+  },
+  "ignoreMomentLocale": true,
+  "theme": "./src/theme.js"
+}
diff --git a/src/main/frontend/.roadhogrc.mock.js b/src/main/frontend/.roadhogrc.mock.js
new file mode 100644
index 0000000..11cfdbc
--- /dev/null
+++ b/src/main/frontend/.roadhogrc.mock.js
@@ -0,0 +1,58 @@
+import mockjs from 'mockjs';
+import { getRule, postRule } from './mock/rule';
+import { imgMap } from './mock/utils';
+import { getNotices } from './mock/notices';
+import { delay } from 'roadhog-api-doc';
+
+// 是否禁用代理
+const noProxy = process.env.NO_PROXY === 'true';
+
+// 代码中会兼容本地 service mock 以及部署站点的静态数据
+const proxy = {
+  // 支持值为 Object 和 Array
+  'GET /api/currentUser': {
+    $desc: "获取当前用户接口",
+    $params: {
+      pageSize: {
+        desc: '分页',
+        exp: 2,
+      },
+    },
+    $body: {
+      name: 'Serati Ma',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/dRFVcIqZOYPcSNrlJsqQ.png',
+      userid: '00000001',
+      notifyCount: 12,
+    },
+  },
+  // 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',
+  }],
+  'GET /api/rule': getRule,
+  'POST /api/rule': {
+    $params: {
+      pageSize: {
+        desc: '分页',
+        exp: 2,
+      },
+    },
+    $body: postRule,
+  },
+  'GET /api/notices': getNotices,
+};
+
+export default noProxy ? {} : delay(proxy, 1000);
diff --git a/src/main/frontend/.stylelintrc b/src/main/frontend/.stylelintrc
new file mode 100644
index 0000000..b4331fb
--- /dev/null
+++ b/src/main/frontend/.stylelintrc
@@ -0,0 +1,25 @@
+{
+  "extends": "stylelint-config-standard",
+  "rules": {
+    "selector-pseudo-class-no-unknown": null,
+    "shorthand-property-no-redundant-values": null,
+    "at-rule-empty-line-before": null,
+    "at-rule-name-space-after": null,
+    "comment-empty-line-before": null,
+    "declaration-bang-space-before": null,
+    "declaration-empty-line-before": null,
+    "function-comma-newline-after": null,
+    "function-name-case": null,
+    "function-parentheses-newline-inside": null,
+    "function-max-empty-lines": null,
+    "function-whitespace-after": null,
+    "number-leading-zero": null,
+    "number-no-trailing-zeros": null,
+    "rule-empty-line-before": null,
+    "selector-combinator-space-after": null,
+    "selector-list-comma-newline-after": null,
+    "selector-pseudo-element-colon-notation": null,
+    "unit-no-unknown": null,
+    "value-list-max-empty-lines": null
+  }
+}
diff --git a/src/main/frontend/.travis.yml b/src/main/frontend/.travis.yml
new file mode 100644
index 0000000..7115c31
--- /dev/null
+++ b/src/main/frontend/.travis.yml
@@ -0,0 +1,33 @@
+language: node_js
+
+node_js:
+  - "8"
+
+env:
+  matrix:
+    - TEST_TYPE=lint
+    - TEST_TYPE=test-all
+    - TEST_TYPE=test-dist
+
+addons:
+  apt:
+    packages:
+      - xvfb
+
+install:
+  - export DISPLAY=':99.0'
+  - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
+  - npm install
+
+script:
+  - |
+    if [ "$TEST_TYPE" = lint ]; then
+      npm run lint
+    elif [ "$TEST_TYPE" = test-all ]; then
+      npm run test:all
+    elif [ "$TEST_TYPE" = test-dist ]; then
+      npm run site
+      mv dist/* ./
+      php -S localhost:8000 &
+      npm test .e2e.js
+    fi
diff --git a/src/main/frontend/LICENSE b/src/main/frontend/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/src/main/frontend/LICENSE
@@ -0,0 +1,201 @@
+                                 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/src/main/frontend/README.md b/src/main/frontend/README.md
new file mode 100644
index 0000000..7a6a4c1
--- /dev/null
+++ b/src/main/frontend/README.md
@@ -0,0 +1 @@
+# SkyWalking UI
diff --git a/src/main/frontend/appveyor.yml b/src/main/frontend/appveyor.yml
new file mode 100644
index 0000000..ee06d65
--- /dev/null
+++ b/src/main/frontend/appveyor.yml
@@ -0,0 +1,22 @@
+# Test against the latest version of this Node.js version
+environment:
+  nodejs_version: "8"
+
+# Install scripts. (runs after repo cloning)
+install:
+  # Get the latest stable version of Node.js or io.js
+  - ps: Install-Product node $env:nodejs_version
+  # install modules
+  - npm install
+  # Output useful info for debugging.
+  - node --version
+  - npm --version
+
+# Post-install test scripts.
+test_script:
+  - npm run lint
+  - npm run test:all
+  - npm run build
+
+# Don't actually build.
+build: off
diff --git a/src/main/frontend/mock/.gitkeep b/src/main/frontend/mock/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/frontend/mock/notices.js b/src/main/frontend/mock/notices.js
new file mode 100644
index 0000000..2b69a25
--- /dev/null
+++ b/src/main/frontend/mock/notices.js
@@ -0,0 +1,85 @@
+export default {
+  getNotices(req, res) {
+    res.json([{
+      id: '000000001',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+      title: '你收到了 14 份新周报',
+      datetime: '2017-08-09',
+      type: '告警',
+    }, {
+      id: '000000002',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
+      title: '你推荐的 曲妮妮 已通过第三轮面试',
+      datetime: '2017-08-08',
+      type: '告警',
+    }, {
+      id: '000000003',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
+      title: '这种模板可以区分多种通知类型',
+      datetime: '2017-08-07',
+      read: true,
+      type: '告警',
+    }, {
+      id: '000000004',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
+      title: '左侧图标用于区分不同的类型',
+      datetime: '2017-08-07',
+      type: '通知',
+    }, {
+      id: '000000005',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+      title: '内容不要超过两行字,超出时自动截断',
+      datetime: '2017-08-07',
+      type: '通知',
+    }, {
+      id: '000000006',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+      title: '曲丽丽 评论了你',
+      description: '描述信息描述信息描述信息',
+      datetime: '2017-08-07',
+      type: '消息',
+    }, {
+      id: '000000007',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+      title: '朱偏右 回复了你',
+      description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+      datetime: '2017-08-07',
+      type: '消息',
+    }, {
+      id: '000000008',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+      title: '标题',
+      description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+      datetime: '2017-08-07',
+      type: '消息',
+    }, {
+      id: '000000009',
+      title: '任务名称',
+      description: '任务需要在 2017-01-12 20:00 前启动',
+      extra: '未开始',
+      status: 'todo',
+      type: '待办',
+    }, {
+      id: '000000010',
+      title: '第三方紧急代码变更',
+      description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+      extra: '马上到期',
+      status: 'urgent',
+      type: '待办',
+    }, {
+      id: '000000011',
+      title: '信息安全考试',
+      description: '指派竹尔于 2017-01-09 前完成更新并发布',
+      extra: '已耗时 8 天',
+      status: 'doing',
+      type: '待办',
+    }, {
+      id: '000000012',
+      title: 'ABCD 版本发布',
+      description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+      extra: '进行中',
+      status: 'processing',
+      type: '待办',
+    }]);
+  },
+};
diff --git a/src/main/frontend/mock/rule.js b/src/main/frontend/mock/rule.js
new file mode 100644
index 0000000..4a1a4da
--- /dev/null
+++ b/src/main/frontend/mock/rule.js
@@ -0,0 +1,127 @@
+import { getUrlParams } from './utils';
+
+// mock tableListDataSource
+let tableListDataSource = [];
+for (let i = 0; i < 46; i += 1) {
+  tableListDataSource.push({
+    key: i,
+    disabled: ((i % 6) === 0),
+    href: 'https://ant.design',
+    avatar: ['https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png'][i % 2],
+    no: `TradeCode ${i}`,
+    title: `一个任务名称 ${i}`,
+    owner: '曲丽丽',
+    description: '这是一段描述',
+    callNo: Math.floor(Math.random() * 1000),
+    status: Math.floor(Math.random() * 10) % 4,
+    updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
+    createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
+    progress: Math.ceil(Math.random() * 100),
+  });
+}
+
+export function getRule(req, res, u) {
+  let url = u;
+  if (!url || Object.prototype.toString.call(url) !== '[object String]') {
+    url = req.url; // eslint-disable-line
+  }
+
+  const params = getUrlParams(url);
+
+  let dataSource = [...tableListDataSource];
+
+  if (params.sorter) {
+    const s = params.sorter.split('_');
+    dataSource = dataSource.sort((prev, next) => {
+      if (s[1] === 'descend') {
+        return next[s[0]] - prev[s[0]];
+      }
+      return prev[s[0]] - next[s[0]];
+    });
+  }
+
+  if (params.status) {
+    const s = params.status.split(',');
+    if (s.length === 1) {
+      dataSource = dataSource.filter(data => parseInt(data.status, 10) === parseInt(s[0], 10));
+    }
+  }
+
+  if (params.no) {
+    dataSource = dataSource.filter(data => data.no.indexOf(params.no) > -1);
+  }
+
+  let pageSize = 10;
+  if (params.pageSize) {
+    pageSize = params.pageSize * 1;
+  }
+
+  const result = {
+    list: dataSource,
+    pagination: {
+      total: dataSource.length,
+      pageSize,
+      current: parseInt(params.currentPage, 10) || 1,
+    },
+  };
+
+  if (res && res.json) {
+    res.json(result);
+  } else {
+    return result;
+  }
+}
+
+export function postRule(req, res, u, b) {
+  let url = u;
+  if (!url || Object.prototype.toString.call(url) !== '[object String]') {
+    url = req.url; // eslint-disable-line
+  }
+
+  const body = (b && b.body) || req.body;
+  const { method, no, description } = body;
+
+  switch (method) {
+    /* eslint no-case-declarations:0 */
+    case 'delete':
+      tableListDataSource = tableListDataSource.filter(item => no.indexOf(item.no) === -1);
+      break;
+    case 'post':
+      const i = Math.ceil(Math.random() * 10000);
+      tableListDataSource.unshift({
+        key: i,
+        href: 'https://ant.design',
+        avatar: ['https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png'][i % 2],
+        no: `TradeCode ${i}`,
+        title: `一个任务名称 ${i}`,
+        owner: '曲丽丽',
+        description,
+        callNo: Math.floor(Math.random() * 1000),
+        status: Math.floor(Math.random() * 10) % 2,
+        updatedAt: new Date(),
+        createdAt: new Date(),
+        progress: Math.ceil(Math.random() * 100),
+      });
+      break;
+    default:
+      break;
+  }
+
+  const result = {
+    list: tableListDataSource,
+    pagination: {
+      total: tableListDataSource.length,
+    },
+  };
+
+  if (res && res.json) {
+    res.json(result);
+  } else {
+    return result;
+  }
+}
+
+export default {
+  getRule,
+  postRule,
+};
diff --git a/src/main/frontend/mock/utils.js b/src/main/frontend/mock/utils.js
new file mode 100644
index 0000000..8438a26
--- /dev/null
+++ b/src/main/frontend/mock/utils.js
@@ -0,0 +1,45 @@
+export const imgMap = {
+  user: 'https://gw.alipayobjects.com/zos/rmsportal/UjusLxePxWGkttaqqmUI.png',
+  a: 'https://gw.alipayobjects.com/zos/rmsportal/ZrkcSjizAKNWwJTwcadT.png',
+  b: 'https://gw.alipayobjects.com/zos/rmsportal/KYlwHMeomKQbhJDRUVvt.png',
+  c: 'https://gw.alipayobjects.com/zos/rmsportal/gabvleTstEvzkbQRfjxu.png',
+  d: 'https://gw.alipayobjects.com/zos/rmsportal/jvpNzacxUYLlNsHTtrAD.png',
+};
+
+// refers: https://www.sitepoint.com/get-url-parameters-with-javascript/
+export function getUrlParams(url) {
+  const d = decodeURIComponent;
+  let queryString = url ? url.split('?')[1] : window.location.search.slice(1);
+  const obj = {};
+  if (queryString) {
+    queryString = queryString.split('#')[0]; // eslint-disable-line
+    const arr = queryString.split('&');
+    for (let i = 0; i < arr.length; i += 1) {
+      const a = arr[i].split('=');
+      let paramNum;
+      const paramName = a[0].replace(/\[\d*\]/, (v) => {
+        paramNum = v.slice(1, -1);
+        return '';
+      });
+      const paramValue = typeof (a[1]) === 'undefined' ? true : a[1];
+      if (obj[paramName]) {
+        if (typeof obj[paramName] === 'string') {
+          obj[paramName] = d([obj[paramName]]);
+        }
+        if (typeof paramNum === 'undefined') {
+          obj[paramName].push(d(paramValue));
+        } else {
+          obj[paramName][paramNum] = d(paramValue);
+        }
+      } else {
+        obj[paramName] = d(paramValue);
+      }
+    }
+  }
+  return obj;
+}
+
+export default {
+  getUrlParams,
+  imgMap,
+};
diff --git a/src/main/frontend/package.json b/src/main/frontend/package.json
new file mode 100755
index 0000000..2deb001
--- /dev/null
+++ b/src/main/frontend/package.json
@@ -0,0 +1,107 @@
+{
+  "name": "skywalking-ui",
+  "version": "5.0.0-alpha",
+  "description": "A web interface of SkyWalking",
+  "private": true,
+  "scripts": {
+    "start": "roadhog server",
+    "start:no-proxy": "cross-env NO_PROXY=true roadhog server",
+    "build": "roadhog build",
+    "site": "roadhog-api-doc static && gh-pages -d dist",
+    "analyze": "roadhog build --analyze",
+    "lint:style": "stylelint \"src/**/*.less\" --syntax less",
+    "lint": "eslint --ext .js src mock tests && npm run lint:style",
+    "lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style",
+    "lint-staged": "lint-staged",
+    "lint-staged:js": "eslint --ext .js",
+    "test": "jest",
+    "test:all": "node ./tests/run-tests.js"
+  },
+  "dependencies": {
+    "ant-design-pro": "^0.2.3-rc.1",
+    "antd": "^3.0.0-beta.1",
+    "babel-runtime": "^6.9.2",
+    "classnames": "^2.2.5",
+    "core-js": "^2.5.1",
+    "dva": "^2.0.3",
+    "lodash": "^4.17.4",
+    "lodash-decorators": "^4.4.1",
+    "lodash.clonedeep": "^4.5.0",
+    "moment": "^2.19.1",
+    "numeral": "^2.0.6",
+    "prop-types": "^15.5.10",
+    "qs": "^6.5.0",
+    "react": "^16.0.0",
+    "react-container-query": "^0.9.1",
+    "react-document-title": "^2.0.3",
+    "react-dom": "^16.0.0",
+    "react-fittext": "^1.0.0"
+  },
+  "devDependencies": {
+    "babel-eslint": "^8.0.1",
+    "babel-jest": "^21.0.0",
+    "babel-plugin-dva-hmr": "^0.3.2",
+    "babel-plugin-import": "^1.2.1",
+    "babel-plugin-transform-class-properties": "^6.24.1",
+    "babel-plugin-transform-decorators-legacy": "^1.3.4",
+    "babel-plugin-transform-runtime": "^6.9.0",
+    "babel-preset-env": "^1.6.1",
+    "babel-preset-react": "^6.24.1",
+    "cross-env": "^5.1.1",
+    "cross-port-killer": "^1.0.1",
+    "enzyme": "^3.1.0",
+    "enzyme-adapter-react-16": "^1.0.2",
+    "eslint": "^4.8.0",
+    "eslint-config-airbnb": "^16.0.0",
+    "eslint-plugin-babel": "^4.0.0",
+    "eslint-plugin-import": "^2.2.0",
+    "eslint-plugin-jsx-a11y": "^6.0.0",
+    "eslint-plugin-markdown": "^1.0.0-beta.6",
+    "eslint-plugin-react": "^7.0.1",
+    "gh-pages": "^1.0.0",
+    "husky": "^0.14.3",
+    "jest": "^21.0.1",
+    "lint-staged": "^4.3.0",
+    "mockjs": "^1.0.1-beta3",
+    "pro-download": "^1.0.0",
+    "react-test-renderer": "^16.0.0",
+    "redbox-react": "^1.3.2",
+    "roadhog": "^1.3.1",
+    "roadhog-api-doc": "^0.2.5",
+    "stylelint": "^8.1.0",
+    "stylelint-config-standard": "^17.0.0"
+  },
+  "optionalDependencies": {
+    "nightmare": "^2.10.0"
+  },
+  "babel": {
+    "presets": [
+      "env",
+      "react"
+    ],
+    "plugins": [
+      "transform-decorators-legacy",
+      "transform-class-properties"
+    ]
+  },
+  "jest": {
+    "setupFiles": [
+      "<rootDir>/tests/setupTests.js"
+    ],
+    "testMatch": [
+      "**/?(*.)(spec|test|e2e).js?(x)"
+    ],
+    "setupTestFrameworkScriptFile": "<rootDir>/tests/jasmine.js",
+    "moduleFileExtensions": [
+      "js",
+      "jsx"
+    ],
+    "moduleNameMapper": {
+      "\\.(css|less)$": "<rootDir>/tests/styleMock.js"
+    }
+  },
+  "lint-staged": {
+    "**/*.{js,jsx}": "lint-staged:js",
+    "**/*.less": "stylelint --syntax less"
+  }
+}
diff --git a/src/main/frontend/public/index.html b/src/main/frontend/public/index.html
new file mode 100755
index 0000000..5d5ec75
--- /dev/null
+++ b/src/main/frontend/public/index.html
@@ -0,0 +1,15 @@
+<!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">
+  <title>Apache SkyWalking</title>
+  <link rel="stylesheet" href="index.css" />
+</head>
+<body>
+  <div id="root"></div>
+  <script src="https://gw.alipayobjects.com/as/g/??datavis/g2/2.3.12/index.js,datavis/g-cloud/1.0.2/index.js,datavis/g2-plugin-slider/1.2.1/slider.js"></script>
+  <script src="index.js"></script>
+</body>
+</html>
diff --git a/src/main/frontend/src/common/nav.js b/src/main/frontend/src/common/nav.js
new file mode 100644
index 0000000..2d7e9dd
--- /dev/null
+++ b/src/main/frontend/src/common/nav.js
@@ -0,0 +1,26 @@
+import dynamic from 'dva/dynamic';
+
+// wrapper of dynamic
+const dynamicWrapper = (app, models, component) => dynamic({
+  app,
+  models: () => models.map(m => import(`../models/${m}.js`)),
+  component,
+});
+
+// nav data
+export const getNavData = app => [
+  {
+    component: dynamicWrapper(app, ['user'], () => import('../layouts/BasicLayout')),
+    layout: 'BasicLayout',
+    name: 'Main', // for breadcrumb
+    path: '/',
+    children: [
+      {
+        name: 'Dashboard',
+        icon: 'dashboard',
+        path: 'dashboard',
+        component: dynamicWrapper(app, ['dashboard'], () => import('../routes/Dashboard/Dashboard')),
+      },
+    ],
+  },
+];
diff --git a/src/main/frontend/src/components/StandardFormRow/index.js b/src/main/frontend/src/components/StandardFormRow/index.js
new file mode 100644
index 0000000..4ed6d9d
--- /dev/null
+++ b/src/main/frontend/src/components/StandardFormRow/index.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export default ({ title, children, last, block, grid, ...rest }) => {
+  const cls = classNames(styles.standardFormRow, {
+    [styles.standardFormRowBlock]: block,
+    [styles.standardFormRowLast]: last,
+    [styles.standardFormRowGrid]: grid,
+  });
+
+  return (
+    <div className={cls} {...rest}>
+      {
+        title && (
+          <div className={styles.label}>
+            <span>{title}</span>
+          </div>
+        )
+      }
+      <div className={styles.content}>
+        {children}
+      </div>
+    </div>
+  );
+};
diff --git a/src/main/frontend/src/components/StandardFormRow/index.less b/src/main/frontend/src/components/StandardFormRow/index.less
new file mode 100644
index 0000000..291d127
--- /dev/null
+++ b/src/main/frontend/src/components/StandardFormRow/index.less
@@ -0,0 +1,71 @@
+@import "~antd/lib/style/themes/default.less";
+
+.standardFormRow {
+  border-bottom: 1px dashed @border-color-split;
+  padding-bottom: 16px;
+  margin-bottom: 16px;
+  display: flex;
+  :global {
+    .ant-form-item {
+      margin-right: 24px;
+    }
+    .ant-form-item-label label {
+      color: @text-color;
+      margin-right: 0;
+    }
+    .ant-form-item-label {
+      padding: 0;
+      line-height: 32px;
+    }
+  }
+  .label {
+    color: @heading-color;
+    font-size: @font-size-base;
+    margin-right: 24px;
+    flex: 0 0 auto;
+    text-align: right;
+    & > span {
+      display: inline-block;
+      height: 32px;
+      line-height: 32px;
+      &:after {
+        content: ':';
+      }
+    }
+  }
+  .content {
+    flex: 1 1 0;
+    :global {
+      .ant-form-item:last-child {
+        margin-right: 0;
+      }
+    }
+  }
+}
+
+.standardFormRowLast {
+  border: none;
+  padding-bottom: 0;
+  margin-bottom: 0;
+}
+
+.standardFormRowBlock {
+  :global {
+    .ant-form-item,
+    div.ant-form-item-control-wrapper {
+      display: block;
+    }
+  }
+}
+
+.standardFormRowGrid {
+  :global {
+    .ant-form-item,
+    div.ant-form-item-control-wrapper {
+      display: block;
+    }
+    .ant-form-item-label {
+      float: left;
+    }
+  }
+}
diff --git a/src/main/frontend/src/components/StandardTable/index.js b/src/main/frontend/src/components/StandardTable/index.js
new file mode 100644
index 0000000..3a4ec8a
--- /dev/null
+++ b/src/main/frontend/src/components/StandardTable/index.js
@@ -0,0 +1,154 @@
+import React, { PureComponent } from 'react';
+import moment from 'moment';
+import { Table, Alert, Badge, Divider } from 'antd';
+import styles from './index.less';
+
+const statusMap = ['default', 'processing', 'success', 'error'];
+class StandardTable extends PureComponent {
+  state = {
+    selectedRowKeys: [],
+    totalCallNo: 0,
+  };
+
+  componentWillReceiveProps(nextProps) {
+    // clean state
+    if (nextProps.selectedRows.length === 0) {
+      this.setState({
+        selectedRowKeys: [],
+        totalCallNo: 0,
+      });
+    }
+  }
+
+  handleRowSelectChange = (selectedRowKeys, selectedRows) => {
+    const totalCallNo = selectedRows.reduce((sum, val) => {
+      return sum + parseFloat(val.callNo, 10);
+    }, 0);
+
+    if (this.props.onSelectRow) {
+      this.props.onSelectRow(selectedRows);
+    }
+
+    this.setState({ selectedRowKeys, totalCallNo });
+  }
+
+  handleTableChange = (pagination, filters, sorter) => {
+    this.props.onChange(pagination, filters, sorter);
+  }
+
+  cleanSelectedKeys = () => {
+    this.handleRowSelectChange([], []);
+  }
+
+  render() {
+    const { selectedRowKeys, totalCallNo } = this.state;
+    const { data: { list, pagination }, loading } = this.props;
+
+    const status = ['关闭', '运行中', '已上线', '异常'];
+
+    const columns = [
+      {
+        title: '规则编号',
+        dataIndex: 'no',
+      },
+      {
+        title: '描述',
+        dataIndex: 'description',
+      },
+      {
+        title: '服务调用次数',
+        dataIndex: 'callNo',
+        sorter: true,
+        render: val => (
+          <div style={{ textAlign: 'center' }}>
+            {val} 万
+          </div>
+        ),
+      },
+      {
+        title: '状态',
+        dataIndex: 'status',
+        filters: [
+          {
+            text: status[0],
+            value: 0,
+          },
+          {
+            text: status[1],
+            value: 1,
+          },
+          {
+            text: status[2],
+            value: 2,
+          },
+          {
+            text: status[3],
+            value: 3,
+          },
+        ],
+        render(val) {
+          return <Badge status={statusMap[val]} text={status[val]} />;
+        },
+      },
+      {
+        title: '更新时间',
+        dataIndex: 'updatedAt',
+        sorter: true,
+        render: val => <span>{moment(val).format('YYYY-MM-DD HH:mm:ss')}</span>,
+      },
+      {
+        title: '操作',
+        render: () => (
+          <div>
+            <a href="">配置</a>
+            <Divider type="vertical" />
+            <a href="">订阅警报</a>
+          </div>
+        ),
+      },
+    ];
+
+    const paginationProps = {
+      showSizeChanger: true,
+      showQuickJumper: true,
+      ...pagination,
+    };
+
+    const rowSelection = {
+      selectedRowKeys,
+      onChange: this.handleRowSelectChange,
+      getCheckboxProps: record => ({
+        disabled: record.disabled,
+      }),
+    };
+
+    return (
+      <div className={styles.standardTable}>
+        <div className={styles.tableAlert}>
+          <Alert
+            message={(
+              <div>
+                已选择 <a style={{ fontWeight: 600 }}>{selectedRowKeys.length}</a> 项&nbsp;&nbsp;
+                服务调用总计 <span style={{ fontWeight: 600 }}>{totalCallNo}</span> 万
+                <a onClick={this.cleanSelectedKeys} style={{ marginLeft: 24 }}>清空</a>
+              </div>
+            )}
+            type="info"
+            showIcon
+          />
+        </div>
+        <Table
+          loading={loading}
+          rowKey={record => record.key}
+          rowSelection={rowSelection}
+          dataSource={list}
+          columns={columns}
+          pagination={paginationProps}
+          onChange={this.handleTableChange}
+        />
+      </div>
+    );
+  }
+}
+
+export default StandardTable;
diff --git a/src/main/frontend/src/components/StandardTable/index.less b/src/main/frontend/src/components/StandardTable/index.less
new file mode 100644
index 0000000..4ced9e7
--- /dev/null
+++ b/src/main/frontend/src/components/StandardTable/index.less
@@ -0,0 +1,13 @@
+@import "~antd/lib/style/themes/default.less";
+
+.standardTable {
+  :global {
+    .ant-table-pagination {
+      margin-top: 24px;
+    }
+  }
+
+  .tableAlert {
+    margin-bottom: 16px;
+  }
+}
diff --git a/src/main/frontend/src/e2e/home.e2e.js b/src/main/frontend/src/e2e/home.e2e.js
new file mode 100644
index 0000000..6de577b
--- /dev/null
+++ b/src/main/frontend/src/e2e/home.e2e.js
@@ -0,0 +1,9 @@
+import Nightmare from 'nightmare';
+
+describe('Homepage', () => {
+  it('it should have logo text', async () => {
+    const page = Nightmare().goto('http://localhost:8000');
+    const text = await page.evaluate(() => document.body.innerHTML).end();
+    expect(text).toContain('<h1>Ant Design Pro</h1>');
+  });
+});
diff --git a/src/main/frontend/src/index.js b/src/main/frontend/src/index.js
new file mode 100644
index 0000000..aad2aaa
--- /dev/null
+++ b/src/main/frontend/src/index.js
@@ -0,0 +1,22 @@
+import dva from 'dva';
+import 'ant-design-pro/dist/ant-design-pro.css';
+// import browserHistory from 'history/createBrowserHistory';
+import './polyfill';
+import './index.less';
+
+// 1. Initialize
+const app = dva({
+  // history: browserHistory(),
+});
+
+// 2. Plugins
+// app.use({});
+
+// 3. Register global model
+app.model(require('./models/global'));
+
+// 4. Router
+app.router(require('./router'));
+
+// 5. Start
+app.start('#root');
diff --git a/src/main/frontend/src/index.less b/src/main/frontend/src/index.less
new file mode 100644
index 0000000..e1a890a
--- /dev/null
+++ b/src/main/frontend/src/index.less
@@ -0,0 +1,16 @@
+
+
+html, body, :global(#root) {
+  height: 100%;
+}
+
+body {
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.globalSpin {
+  width: 100%;
+  margin: 40px 0 !important;
+}
diff --git a/src/main/frontend/src/layouts/BasicLayout.js b/src/main/frontend/src/layouts/BasicLayout.js
new file mode 100644
index 0000000..6eab185
--- /dev/null
+++ b/src/main/frontend/src/layouts/BasicLayout.js
@@ -0,0 +1,375 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import DocumentTitle from 'react-document-title';
+
+import { connect } from 'dva';
+import { Link, Route, Redirect, Switch } from 'dva/router';
+import { Layout, Menu, Icon, Dropdown, Tag, message } from 'antd';
+
+import NoticeIcon from 'ant-design-pro/lib/NoticeIcon';
+import GlobalFooter from 'ant-design-pro/lib/GlobalFooter';
+
+import moment from 'moment';
+import groupBy from 'lodash/groupBy';
+import classNames from 'classnames';
+import { ContainerQuery } from 'react-container-query';
+
+import styles from './BasicLayout.less';
+
+const { Header, Sider, Content } = Layout;
+const { SubMenu } = Menu;
+
+const query = {
+  'screen-xs': {
+    maxWidth: 575,
+  },
+  'screen-sm': {
+    minWidth: 576,
+    maxWidth: 767,
+  },
+  'screen-md': {
+    minWidth: 768,
+    maxWidth: 991,
+  },
+  'screen-lg': {
+    minWidth: 992,
+    maxWidth: 1199,
+  },
+  'screen-xl': {
+    minWidth: 1200,
+  },
+};
+
+class BasicLayout extends React.PureComponent {
+  static childContextTypes = {
+    location: PropTypes.object,
+    breadcrumbNameMap: PropTypes.object,
+  }
+  constructor(props) {
+    super(props);
+    // 把一级 Layout 的 children 作为菜单项
+    this.menus = props.navData.reduce((arr, current) => arr.concat(current.children), []);
+    this.state = {
+      openKeys: this.getDefaultCollapsedSubMenus(props),
+    };
+  }
+  getChildContext() {
+    const { location, navData, getRouteData } = this.props;
+    const routeData = getRouteData('BasicLayout');
+    const firstMenuData = navData.reduce((arr, current) => arr.concat(current.children), []);
+    const menuData = this.getMenuData(firstMenuData, '');
+    const breadcrumbNameMap = {};
+
+    routeData.concat(menuData).forEach((item) => {
+      breadcrumbNameMap[item.path] = item.name;
+    });
+    return { location, breadcrumbNameMap };
+  }
+  componentDidMount() {
+    this.props.dispatch({
+      type: 'user/fetchCurrent',
+    });
+  }
+  componentWillUnmount() {
+    clearTimeout(this.resizeTimeout);
+  }
+  onCollapse = (collapsed) => {
+    this.props.dispatch({
+      type: 'global/changeLayoutCollapsed',
+      payload: collapsed,
+    });
+  }
+  onMenuClick = ({ key }) => {
+    if (key === 'logout') {
+      this.props.dispatch({
+        type: 'login/logout',
+      });
+    }
+  }
+  getMenuData = (data, parentPath) => {
+    let arr = [];
+    data.forEach((item) => {
+      if (item.children) {
+        arr.push({ path: `${parentPath}/${item.path}`, name: item.name });
+        arr = arr.concat(this.getMenuData(item.children, `${parentPath}/${item.path}`));
+      }
+    });
+    return arr;
+  }
+  getDefaultCollapsedSubMenus(props) {
+    const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys(props)];
+    currentMenuSelectedKeys.splice(-1, 1);
+    if (currentMenuSelectedKeys.length === 0) {
+      return ['dashboard'];
+    }
+    return currentMenuSelectedKeys;
+  }
+  getCurrentMenuSelectedKeys(props) {
+    const { location: { pathname } } = props || this.props;
+    const keys = pathname.split('/').slice(1);
+    if (keys.length === 1 && keys[0] === '') {
+      return [this.menus[0].key];
+    }
+    return keys;
+  }
+  getNavMenuItems(menusData, parentPath = '') {
+    if (!menusData) {
+      return [];
+    }
+    return menusData.map((item) => {
+      if (!item.name) {
+        return null;
+      }
+      let itemPath;
+      if (item.path.indexOf('http') === 0) {
+        itemPath = item.path;
+      } else {
+        itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
+      }
+      if (item.children && item.children.some(child => child.name)) {
+        return (
+          <SubMenu
+            title={
+              item.icon ? (
+                <span>
+                  <Icon type={item.icon} />
+                  <span>{item.name}</span>
+                </span>
+              ) : item.name
+            }
+            key={item.key || item.path}
+          >
+            {this.getNavMenuItems(item.children, itemPath)}
+          </SubMenu>
+        );
+      }
+      const icon = item.icon && <Icon type={item.icon} />;
+      return (
+        <Menu.Item key={item.key || item.path}>
+          {
+            /^https?:\/\//.test(itemPath) ? (
+              <a href={itemPath} target={item.target}>
+                {icon}<span>{item.name}</span>
+              </a>
+            ) : (
+              <Link
+                to={itemPath}
+                target={item.target}
+                replace={itemPath === this.props.location.pathname}
+              >
+                {icon}<span>{item.name}</span>
+              </Link>
+            )
+          }
+        </Menu.Item>
+      );
+    });
+  }
+  getPageTitle() {
+    const { location, getRouteData } = this.props;
+    const { pathname } = location;
+    let title = 'SkyWalking';
+    getRouteData('BasicLayout').forEach((item) => {
+      if (item.path === pathname) {
+        title = `${item.name} - SkyWalking`;
+      }
+    });
+    return title;
+  }
+  getNoticeData() {
+    const { notices = [] } = this.props;
+    if (notices.length === 0) {
+      return {};
+    }
+    const newNotices = notices.map((notice) => {
+      const newNotice = { ...notice };
+      if (newNotice.datetime) {
+        newNotice.datetime = moment(notice.datetime).fromNow();
+      }
+      // transform id to item key
+      if (newNotice.id) {
+        newNotice.key = newNotice.id;
+      }
+      if (newNotice.extra && newNotice.status) {
+        const color = ({
+          todo: '',
+          processing: 'blue',
+          urgent: 'red',
+          doing: 'gold',
+        })[newNotice.status];
+        newNotice.extra = <Tag color={color} style={{ marginRight: 0 }}>{newNotice.extra}</Tag>;
+      }
+      return newNotice;
+    });
+    return groupBy(newNotices, 'type');
+  }
+  handleOpenChange = (openKeys) => {
+    const lastOpenKey = openKeys[openKeys.length - 1];
+    const isMainMenu = this.menus.some(
+      item => lastOpenKey && (item.key === lastOpenKey || item.path === lastOpenKey)
+    );
+    this.setState({
+      openKeys: isMainMenu ? [lastOpenKey] : [...openKeys],
+    });
+  }
+  toggle = () => {
+    const { collapsed } = this.props;
+    this.props.dispatch({
+      type: 'global/changeLayoutCollapsed',
+      payload: !collapsed,
+    });
+    this.resizeTimeout = setTimeout(() => {
+      const event = document.createEvent('HTMLEvents');
+      event.initEvent('resize', true, false);
+      window.dispatchEvent(event);
+    }, 600);
+  }
+  handleNoticeClear = (type) => {
+    message.success(`清空了${type}`);
+    this.props.dispatch({
+      type: 'global/clearNotices',
+      payload: type,
+    });
+  }
+  handleNoticeVisibleChange = (visible) => {
+    if (visible) {
+      this.props.dispatch({
+        type: 'global/fetchNotices',
+      });
+    }
+  }
+  render() {
+    const { currentUser, collapsed, fetchingNotices, getRouteData } = this.props;
+
+    const menu = (
+      <Menu selectedKeys={['1']} onClick={this.onMenuClick}>
+        <Menu.Item key="1">Last 15 minutes</Menu.Item>
+        <Menu.Item key="2">Last 1 hour</Menu.Item>
+      </Menu>
+    );
+    const noticeData = this.getNoticeData();
+
+    // Don't show popup menu when it is been collapsed
+    const menuProps = collapsed ? {} : {
+      openKeys: this.state.openKeys,
+    };
+
+    const layout = (
+      <Layout>
+        <Sider
+          trigger={null}
+          collapsible
+          collapsed={collapsed}
+          breakpoint="md"
+          onCollapse={this.onCollapse}
+          width={256}
+          className={styles.sider}
+        >
+          <div className={styles.logo}>
+            <Link to="/">
+              <img src="https://camo.githubusercontent.com/4ac940361b7345156ff71aa21efdb42a449e67d7/68747470733a2f2f736b7977616c6b696e67746573742e6769746875622e696f2f706167652d7265736f75726365732f332e302f736b7977616c6b696e672e706e67" alt="logo" />
+            </Link>
+          </div>
+          <Menu
+            theme="dark"
+            mode="inline"
+            {...menuProps}
+            onOpenChange={this.handleOpenChange}
+            selectedKeys={this.getCurrentMenuSelectedKeys()}
+            style={{ margin: '16px 0', width: '100%' }}
+          >
+            {this.getNavMenuItems(this.menus)}
+          </Menu>
+        </Sider>
+        <Layout>
+          <Header className={styles.header}>
+            <Icon
+              className={styles.trigger}
+              type={collapsed ? 'menu-unfold' : 'menu-fold'}
+              onClick={this.toggle}
+            />
+            <div className={styles.right}>
+              <Dropdown overlay={menu}>
+                <span className={`${styles.action}`}>
+                  Last 15 minutes
+                </span>
+              </Dropdown>
+              <NoticeIcon
+                className={styles.action}
+                count={currentUser.notifyCount}
+                onItemClick={(item, tabProps) => {
+                  console.log(item, tabProps); // eslint-disable-line
+                }}
+                onClear={this.handleNoticeClear}
+                onPopupVisibleChange={this.handleNoticeVisibleChange}
+                loading={fetchingNotices}
+                popupAlign={{ offset: [20, -16] }}
+              >
+                <NoticeIcon.Tab
+                  list={noticeData['告警']}
+                  title="告警"
+                  emptyText="无告警信息"
+                  emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
+                />
+                <NoticeIcon.Tab
+                  list={noticeData['消息']}
+                  title="消息"
+                  emptyText="您已读完所有消息"
+                  emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
+                />
+              </NoticeIcon>
+            </div>
+          </Header>
+          <Content style={{ margin: '24px 24px 0', height: '100%' }}>
+            <Switch>
+              {
+                getRouteData('BasicLayout').map(item =>
+                  (
+                    <Route
+                      exact={item.exact}
+                      key={item.path}
+                      path={item.path}
+                      component={item.component}
+                    />
+                  )
+                )
+              }
+              <Redirect exact from="/" to="/dashboard" />
+            </Switch>
+            <GlobalFooter
+              links={[{
+                title: 'SkyWalking',
+                href: 'http://skywalking.io',
+                blankTarget: true,
+              }, {
+                title: 'GitHub',
+                href: 'https://github.com/apache/incubator-skywalking',
+                blankTarget: true,
+              }]}
+              copyright={
+                <div>
+                  Copyright <Icon type="copyright" /> 2018 SkyWalking
+                </div>
+              }
+            />
+          </Content>
+        </Layout>
+      </Layout>
+    );
+
+    return (
+      <DocumentTitle title={this.getPageTitle()}>
+        <ContainerQuery query={query}>
+          {params => <div className={classNames(params)}>{layout}</div>}
+        </ContainerQuery>
+      </DocumentTitle>
+    );
+  }
+}
+
+export default connect(state => ({
+  currentUser: state.user.currentUser,
+  collapsed: state.global.collapsed,
+  fetchingNotices: state.global.fetchingNotices,
+  notices: state.global.notices,
+}))(BasicLayout);
diff --git a/src/main/frontend/src/layouts/BasicLayout.less b/src/main/frontend/src/layouts/BasicLayout.less
new file mode 100644
index 0000000..6ab937a
--- /dev/null
+++ b/src/main/frontend/src/layouts/BasicLayout.less
@@ -0,0 +1,113 @@
+@import "~antd/lib/style/themes/default.less";
+
+.header {
+  padding: 0 12px 0 0;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
+  position: relative;
+}
+
+.logo {
+  height: 64px;
+  position: relative;
+  line-height: 64px;
+  padding-left: 24px;
+  transition: all .3s;
+  background: #002140;
+  overflow: hidden;
+  img {
+    display: inline-block;
+    vertical-align: middle;
+    height: 32px;
+  }
+  h1 {
+    color: #fff;
+    display: inline-block;
+    vertical-align: middle;
+    font-size: 20px;
+    margin: 0 0 0 12px;
+    font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
+    font-weight: 600;
+  }
+}
+
+:global(.ant-layout-sider-collapsed) .logo {
+  padding-left: 24px;
+  > a {
+    width: 32px;
+  }
+}
+
+.trigger {
+  font-size: 20px;
+  line-height: 64px;
+  cursor: pointer;
+  transition: all .3s;
+  padding: 0 24px;
+  &:hover {
+    background: @primary-1;
+  }
+}
+
+@media screen and (max-width: @screen-xs) {
+  .trigger {
+    display: none;
+  }
+}
+
+.right {
+  float: right;
+  height: 100%;
+  .action {
+    cursor: pointer;
+    padding: 0 12px;
+    display: inline-block;
+    transition: all .3s;
+    height: 100%;
+    > i {
+      font-size: 16px;
+      vertical-align: middle;
+    }
+    &:global(.ant-popover-open),
+    &:hover {
+      background: @primary-1;
+    }
+  }
+  .search {
+    padding: 0;
+    margin: 0 12px;
+    &:hover {
+      background: transparent;
+    }
+  }
+  .account {
+    .avatar {
+      margin: 20px 8px 20px 0;
+      color: @primary-color;
+      background: rgba(255, 255, 255, .85);
+      vertical-align: middle;
+    }
+  }
+}
+
+.menu {
+  :global(.anticon) {
+    margin-right: 8px;
+  }
+  :global(.ant-dropdown-menu-item) {
+    width: 160px;
+  }
+}
+
+:global {
+  .ant-layout {
+    overflow-x: hidden;
+  }
+}
+
+.sider {
+  min-height: 100vh;
+  box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
+  position: relative;
+  z-index: 10;
+}
diff --git a/src/main/frontend/src/layouts/BlankLayout.js b/src/main/frontend/src/layouts/BlankLayout.js
new file mode 100644
index 0000000..505270f
--- /dev/null
+++ b/src/main/frontend/src/layouts/BlankLayout.js
@@ -0,0 +1,3 @@
+import React from 'react';
+
+export default props => <div {...props} />;
diff --git a/src/main/frontend/src/layouts/PageHeaderLayout.js b/src/main/frontend/src/layouts/PageHeaderLayout.js
new file mode 100644
index 0000000..bc4a05c
--- /dev/null
+++ b/src/main/frontend/src/layouts/PageHeaderLayout.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Link } from 'dva/router';
+import PageHeader from 'ant-design-pro/lib/PageHeader';
+import styles from './PageHeaderLayout.less';
+
+export default ({ children, wrapperClassName, top, ...restProps }) => (
+  <div style={{ margin: '-24px -24px 0' }} className={wrapperClassName}>
+    {top}
+    <PageHeader {...restProps} linkElement={Link} />
+    {children ? <div className={styles.content}>{children}</div> : null}
+  </div>
+);
diff --git a/src/main/frontend/src/layouts/PageHeaderLayout.less b/src/main/frontend/src/layouts/PageHeaderLayout.less
new file mode 100644
index 0000000..a0c0a6e
--- /dev/null
+++ b/src/main/frontend/src/layouts/PageHeaderLayout.less
@@ -0,0 +1,11 @@
+@import "~antd/lib/style/themes/default.less";
+
+.content {
+  margin: 24px 24px 0;
+}
+
+@media screen and (max-width: @screen-sm) {
+  .content {
+    margin: 24px 0 0;
+  }
+}
diff --git a/src/main/frontend/src/models/dashboard.js b/src/main/frontend/src/models/dashboard.js
new file mode 100644
index 0000000..8370d25
--- /dev/null
+++ b/src/main/frontend/src/models/dashboard.js
@@ -0,0 +1,16 @@
+// import { xxx } from '../services/xxx';
+export default {
+  namespace: 'dashboar',
+  state: {},
+  effects: {
+    *fetch({ payload }, { call, put }) {
+    },
+  },
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+      };
+    },
+  },
+};
diff --git a/src/main/frontend/src/models/global.js b/src/main/frontend/src/models/global.js
new file mode 100644
index 0000000..91127ed
--- /dev/null
+++ b/src/main/frontend/src/models/global.js
@@ -0,0 +1,76 @@
+import { queryNotices } from '../services/api';
+
+export default {
+  namespace: 'global',
+
+  state: {
+    collapsed: false,
+    notices: [],
+    fetchingNotices: false,
+  },
+
+  effects: {
+    *fetchNotices(_, { call, put }) {
+      yield put({
+        type: 'changeNoticeLoading',
+        payload: true,
+      });
+      const data = yield call(queryNotices);
+      yield put({
+        type: 'saveNotices',
+        payload: data,
+      });
+    },
+    *clearNotices({ payload }, { put, select }) {
+      const count = yield select(state => state.global.notices.length);
+      yield put({
+        type: 'user/changeNotifyCount',
+        payload: count,
+      });
+
+      yield put({
+        type: 'saveClearedNotices',
+        payload,
+      });
+    },
+  },
+
+  reducers: {
+    changeLayoutCollapsed(state, { payload }) {
+      return {
+        ...state,
+        collapsed: payload,
+      };
+    },
+    saveNotices(state, { payload }) {
+      return {
+        ...state,
+        notices: payload,
+        fetchingNotices: false,
+      };
+    },
+    saveClearedNotices(state, { payload }) {
+      return {
+        ...state,
+        notices: state.notices.filter(item => item.type !== payload),
+      };
+    },
+    changeNoticeLoading(state, { payload }) {
+      return {
+        ...state,
+        fetchingNotices: payload,
+      };
+    },
+  },
+
+  subscriptions: {
+    setup({ history }) {
+      // Subscribe history(url) change, trigger `load` action if pathname is `/`
+      return history.listen(({ pathname, search }) => {
+        if (typeof window.ga !== 'undefined') {
+          window.ga('send', 'pageview', pathname + search);
+        }
+      });
+    },
+  },
+};
diff --git a/src/main/frontend/src/models/index.js b/src/main/frontend/src/models/index.js
new file mode 100644
index 0000000..3666614
--- /dev/null
+++ b/src/main/frontend/src/models/index.js
@@ -0,0 +1,11 @@
+// Use require.context to require reducers automatically
+// Ref: https://webpack.github.io/docs/context.html
+const context = require.context('./', false, /\.js$/);
+const keys = context.keys().filter(item => item !== './index.js');
+
+const models = [];
+for (let i = 0; i < keys.length; i += 1) {
+  models.push(context(keys[i]));
+}
+
+export default models;
diff --git a/src/main/frontend/src/models/rule.js b/src/main/frontend/src/models/rule.js
new file mode 100644
index 0000000..8b36ba3
--- /dev/null
+++ b/src/main/frontend/src/models/rule.js
@@ -0,0 +1,80 @@
+import { queryRule, removeRule, addRule } from '../services/api';
+
+export default {
+  namespace: 'rule',
+
+  state: {
+    data: {
+      list: [],
+      pagination: {},
+    },
+    loading: true,
+  },
+
+  effects: {
+    *fetch({ payload }, { call, put }) {
+      yield put({
+        type: 'changeLoading',
+        payload: true,
+      });
+      const response = yield call(queryRule, payload);
+      yield put({
+        type: 'save',
+        payload: response,
+      });
+      yield put({
+        type: 'changeLoading',
+        payload: false,
+      });
+    },
+    *add({ payload, callback }, { call, put }) {
+      yield put({
+        type: 'changeLoading',
+        payload: true,
+      });
+      const response = yield call(addRule, payload);
+      yield put({
+        type: 'save',
+        payload: response,
+      });
+      yield put({
+        type: 'changeLoading',
+        payload: false,
+      });
+
+      if (callback) callback();
+    },
+    *remove({ payload, callback }, { call, put }) {
+      yield put({
+        type: 'changeLoading',
+        payload: true,
+      });
+      const response = yield call(removeRule, payload);
+      yield put({
+        type: 'save',
+        payload: response,
+      });
+      yield put({
+        type: 'changeLoading',
+        payload: false,
+      });
+
+      if (callback) callback();
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        data: action.payload,
+      };
+    },
+    changeLoading(state, action) {
+      return {
+        ...state,
+        loading: action.payload,
+      };
+    },
+  },
+};
diff --git a/src/main/frontend/src/models/user.js b/src/main/frontend/src/models/user.js
new file mode 100644
index 0000000..197e362
--- /dev/null
+++ b/src/main/frontend/src/models/user.js
@@ -0,0 +1,66 @@
+import { query as queryUsers, queryCurrent } from '../services/user';
+
+export default {
+  namespace: 'user',
+
+  state: {
+    list: [],
+    loading: false,
+    currentUser: {},
+  },
+
+  effects: {
+    *fetch(_, { call, put }) {
+      yield put({
+        type: 'changeLoading',
+        payload: true,
+      });
+      const response = yield call(queryUsers);
+      yield put({
+        type: 'save',
+        payload: response,
+      });
+      yield put({
+        type: 'changeLoading',
+        payload: false,
+      });
+    },
+    *fetchCurrent(_, { call, put }) {
+      const response = yield call(queryCurrent);
+      yield put({
+        type: 'saveCurrentUser',
+        payload: response,
+      });
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        list: action.payload,
+      };
+    },
+    changeLoading(state, action) {
+      return {
+        ...state,
+        loading: action.payload,
+      };
+    },
+    saveCurrentUser(state, action) {
+      return {
+        ...state,
+        currentUser: action.payload,
+      };
+    },
+    changeNotifyCount(state, action) {
+      return {
+        ...state,
+        currentUser: {
+          ...state.currentUser,
+          notifyCount: action.payload,
+        },
+      };
+    },
+  },
+};
diff --git a/src/main/frontend/src/polyfill.js b/src/main/frontend/src/polyfill.js
new file mode 100644
index 0000000..a358959
--- /dev/null
+++ b/src/main/frontend/src/polyfill.js
@@ -0,0 +1,7 @@
+import 'core-js/es6/map';
+import 'core-js/es6/set';
+
+global.requestAnimationFrame =
+  global.requestAnimationFrame || function requestAnimationFrame(callback) {
+    setTimeout(callback, 0);
+  };
diff --git a/src/main/frontend/src/router.js b/src/main/frontend/src/router.js
new file mode 100644
index 0000000..1c51d96
--- /dev/null
+++ b/src/main/frontend/src/router.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import { Router, Route, Switch } from 'dva/router';
+import { Spin } from 'antd';
+import dynamic from 'dva/dynamic';
+import cloneDeep from 'lodash/cloneDeep';
+import { getNavData } from './common/nav';
+import { getPlainNode } from './utils/utils';
+
+import styles from './index.less';
+
+dynamic.setDefaultLoadingComponent(() => {
+  return <Spin size="large" className={styles.globalSpin} />;
+});
+
+function getRouteData(navData, path) {
+  if (!navData.some(item => item.layout === path) ||
+    !(navData.filter(item => item.layout === path)[0].children)) {
+    return null;
+  }
+  const route = cloneDeep(navData.filter(item => item.layout === path)[0]);
+  const nodeList = getPlainNode(route.children);
+  return nodeList;
+}
+
+function getLayout(navData, path) {
+  if (!navData.some(item => item.layout === path) ||
+    !(navData.filter(item => item.layout === path)[0].children)) {
+    return null;
+  }
+  const route = navData.filter(item => item.layout === path)[0];
+  return {
+    component: route.component,
+    layout: route.layout,
+    name: route.name,
+    path: route.path,
+  };
+}
+
+function RouterConfig({ history, app }) {
+  const navData = getNavData(app);
+  const BasicLayout = getLayout(navData, 'BasicLayout').component;
+
+  const passProps = {
+    app,
+    navData,
+    getRouteData: (path) => {
+      return getRouteData(navData, path);
+    },
+  };
+
+  return (
+    <Router history={history}>
+      <Switch>
+        <Route path="/" render={props => <BasicLayout {...props} {...passProps} />} />
+      </Switch>
+    </Router>
+  );
+}
+
+export default RouterConfig;
diff --git a/src/main/frontend/src/routes/Dashboard/Dashboard.js b/src/main/frontend/src/routes/Dashboard/Dashboard.js
new file mode 100644
index 0000000..353eca8
--- /dev/null
+++ b/src/main/frontend/src/routes/Dashboard/Dashboard.js
@@ -0,0 +1,13 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+
+@connect(state => ({
+  dashboard: state.dashboard,
+}))
+export default class Dashboard extends PureComponent {
+  render() {
+    return (
+      <div>test</div>
+    );
+  }
+}
diff --git a/src/main/frontend/src/routes/Dashboard/Dashboard.less b/src/main/frontend/src/routes/Dashboard/Dashboard.less
new file mode 100644
index 0000000..e52dd30
--- /dev/null
+++ b/src/main/frontend/src/routes/Dashboard/Dashboard.less
@@ -0,0 +1,23 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.mapChart {
+  padding-top: 24px;
+  height: 457px;
+  text-align: center;
+  img {
+    display: inline-block;
+    max-width: 100%;
+    max-height: 437px;
+  }
+}
+
+.pieCard :global(.pie-stat) {
+  font-size: 24px!important;
+}
+
+@media screen and (max-width: @screen-lg) {
+  .mapChart {
+    height: auto;
+  }
+}
diff --git a/src/main/frontend/src/routes/List/TableList.js b/src/main/frontend/src/routes/List/TableList.js
new file mode 100644
index 0000000..332cd14
--- /dev/null
+++ b/src/main/frontend/src/routes/List/TableList.js
@@ -0,0 +1,326 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+import { Row, Col, Card, Form, Input, Select, Icon, Button, Dropdown, Menu, InputNumber, DatePicker, Modal, message } from 'antd';
+import StandardTable from '../../components/StandardTable';
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+
+import styles from './TableList.less';
+
+const FormItem = Form.Item;
+const { Option } = Select;
+const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+
+@connect(state => ({
+  rule: state.rule,
+}))
+@Form.create()
+export default class TableList extends PureComponent {
+  state = {
+    addInputValue: '',
+    modalVisible: false,
+    expandForm: false,
+    selectedRows: [],
+    formValues: {},
+  };
+
+  componentDidMount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'rule/fetch',
+    });
+  }
+
+  handleStandardTableChange = (pagination, filtersArg, sorter) => {
+    const { dispatch } = this.props;
+    const { formValues } = this.state;
+
+    const filters = Object.keys(filtersArg).reduce((obj, key) => {
+      const newObj = { ...obj };
+      newObj[key] = getValue(filtersArg[key]);
+      return newObj;
+    }, {});
+
+    const params = {
+      currentPage: pagination.current,
+      pageSize: pagination.pageSize,
+      ...formValues,
+      ...filters,
+    };
+    if (sorter.field) {
+      params.sorter = `${sorter.field}_${sorter.order}`;
+    }
+
+    dispatch({
+      type: 'rule/fetch',
+      payload: params,
+    });
+  }
+
+  handleFormReset = () => {
+    const { form, dispatch } = this.props;
+    form.resetFields();
+    dispatch({
+      type: 'rule/fetch',
+      payload: {},
+    });
+  }
+
+  toggleForm = () => {
+    this.setState({
+      expandForm: !this.state.expandForm,
+    });
+  }
+
+  handleMenuClick = (e) => {
+    const { dispatch } = this.props;
+    const { selectedRows } = this.state;
+
+    if (!selectedRows) return;
+
+    switch (e.key) {
+      case 'remove':
+        dispatch({
+          type: 'rule/remove',
+          payload: {
+            no: selectedRows.map(row => row.no).join(','),
+          },
+          callback: () => {
+            this.setState({
+              selectedRows: [],
+            });
+          },
+        });
+        break;
+      default:
+        break;
+    }
+  }
+
+  handleSelectRows = (rows) => {
+    this.setState({
+      selectedRows: rows,
+    });
+  }
+
+  handleSearch = (e) => {
+    e.preventDefault();
+
+    const { dispatch, form } = this.props;
+
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+
+      const values = {
+        ...fieldsValue,
+        updatedAt: fieldsValue.updatedAt && fieldsValue.updatedAt.valueOf(),
+      };
+
+      this.setState({
+        formValues: values,
+      });
+
+      dispatch({
+        type: 'rule/fetch',
+        payload: values,
+      });
+    });
+  }
+
+  handleModalVisible = (flag) => {
+    this.setState({
+      modalVisible: !!flag,
+    });
+  }
+
+  handleAddInput = (e) => {
+    this.setState({
+      addInputValue: e.target.value,
+    });
+  }
+
+  handleAdd = () => {
+    this.props.dispatch({
+      type: 'rule/add',
+      payload: {
+        description: this.state.addInputValue,
+      },
+    });
+
+    message.success('添加成功');
+    this.setState({
+      modalVisible: false,
+    });
+  }
+
+  renderSimpleForm() {
+    const { getFieldDecorator } = this.props.form;
+    return (
+      <Form onSubmit={this.handleSearch} layout="inline">
+        <Row gutter={{ md: 8, lg: 24, xl: 48 }}>
+          <Col md={8} sm={24}>
+            <FormItem label="规则编号">
+              {getFieldDecorator('no')(
+                <Input placeholder="请输入" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={8} sm={24}>
+            <FormItem label="使用状态">
+              {getFieldDecorator('status')(
+                <Select placeholder="请选择" style={{ width: '100%' }}>
+                  <Option value="0">关闭</Option>
+                  <Option value="1">运行中</Option>
+                </Select>
+              )}
+            </FormItem>
+          </Col>
+          <Col md={8} sm={24}>
+            <span className={styles.submitButtons}>
+              <Button type="primary" htmlType="submit">查询</Button>
+              <Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>重置</Button>
+              <a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
+                展开 <Icon type="down" />
+              </a>
+            </span>
+          </Col>
+        </Row>
+      </Form>
+    );
+  }
+
+  renderAdvancedForm() {
+    const { getFieldDecorator } = this.props.form;
+    return (
+      <Form onSubmit={this.handleSearch} layout="inline">
+        <Row gutter={{ md: 8, lg: 24, xl: 48 }}>
+          <Col md={8} sm={24}>
+            <FormItem label="规则编号">
+              {getFieldDecorator('no')(
+                <Input placeholder="请输入" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={8} sm={24}>
+            <FormItem label="使用状态">
+              {getFieldDecorator('status')(
+                <Select placeholder="请选择" style={{ width: '100%' }}>
+                  <Option value="0">关闭</Option>
+                  <Option value="1">运行中</Option>
+                </Select>
+              )}
+            </FormItem>
+          </Col>
+          <Col md={8} sm={24}>
+            <FormItem label="调用次数">
+              {getFieldDecorator('number')(
+                <InputNumber style={{ width: '100%' }} />
+              )}
+            </FormItem>
+          </Col>
+        </Row>
+        <Row gutter={{ md: 8, lg: 24, xl: 48 }}>
+          <Col md={8} sm={24}>
+            <FormItem label="更新日期">
+              {getFieldDecorator('date')(
+                <DatePicker style={{ width: '100%' }} placeholder="请输入更新日期" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={8} sm={24}>
+            <FormItem label="使用状态">
+              {getFieldDecorator('status3')(
+                <Select placeholder="请选择" style={{ width: '100%' }}>
+                  <Option value="0">关闭</Option>
+                  <Option value="1">运行中</Option>
+                </Select>
+              )}
+            </FormItem>
+          </Col>
+          <Col md={8} sm={24}>
+            <FormItem label="使用状态">
+              {getFieldDecorator('status4')(
+                <Select placeholder="请选择" style={{ width: '100%' }}>
+                  <Option value="0">关闭</Option>
+                  <Option value="1">运行中</Option>
+                </Select>
+              )}
+            </FormItem>
+          </Col>
+        </Row>
+        <div style={{ overflow: 'hidden' }}>
+          <span style={{ float: 'right', marginBottom: 24 }}>
+            <Button type="primary" htmlType="submit">查询</Button>
+            <Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>重置</Button>
+            <a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
+              收起 <Icon type="up" />
+            </a>
+          </span>
+        </div>
+      </Form>
+    );
+  }
+
+  renderForm() {
+    return this.state.expandForm ? this.renderAdvancedForm() : this.renderSimpleForm();
+  }
+
+  render() {
+    const { rule: { loading: ruleLoading, data } } = this.props;
+    const { selectedRows, modalVisible, addInputValue } = this.state;
+
+    const menu = (
+      <Menu onClick={this.handleMenuClick} selectedKeys={[]}>
+        <Menu.Item key="remove">删除</Menu.Item>
+        <Menu.Item key="approval">批量审批</Menu.Item>
+      </Menu>
+    );
+
+    return (
+      <PageHeaderLayout title="查询表格">
+        <Card bordered={false}>
+          <div className={styles.tableList}>
+            <div className={styles.tableListForm}>
+              {this.renderForm()}
+            </div>
+            <div className={styles.tableListOperator}>
+              <Button icon="plus" type="primary" onClick={() => this.handleModalVisible(true)}>新建</Button>
+              {
+                selectedRows.length > 0 && (
+                  <span>
+                    <Button>批量操作</Button>
+                    <Dropdown overlay={menu}>
+                      <Button>
+                        更多操作 <Icon type="down" />
+                      </Button>
+                    </Dropdown>
+                  </span>
+                )
+              }
+            </div>
+            <StandardTable
+              selectedRows={selectedRows}
+              loading={ruleLoading}
+              data={data}
+              onSelectRow={this.handleSelectRows}
+              onChange={this.handleStandardTableChange}
+            />
+          </div>
+        </Card>
+        <Modal
+          title="新建规则"
+          visible={modalVisible}
+          onOk={this.handleAdd}
+          onCancel={() => this.handleModalVisible()}
+        >
+          <FormItem
+            labelCol={{ span: 5 }}
+            wrapperCol={{ span: 15 }}
+            label="描述"
+          >
+            <Input placeholder="请输入" onChange={this.handleAddInput} value={addInputValue} />
+          </FormItem>
+        </Modal>
+      </PageHeaderLayout>
+    );
+  }
+}
diff --git a/src/main/frontend/src/routes/List/TableList.less b/src/main/frontend/src/routes/List/TableList.less
new file mode 100644
index 0000000..6c9efa1
--- /dev/null
+++ b/src/main/frontend/src/routes/List/TableList.less
@@ -0,0 +1,45 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.tableList {
+  .tableListOperator {
+    margin-bottom: 16px;
+    button {
+      margin-right: 8px;
+    }
+  }
+}
+
+.tableListForm {
+  :global {
+    .ant-form-item {
+      margin-bottom: 24px;
+      margin-right: 0;
+      display: flex;
+      > .ant-form-item-label {
+        width: auto;
+        line-height: 32px;
+        padding-right: 8px;
+      }
+    }
+    .ant-form-item-control-wrapper {
+      flex: 1;
+    }
+  }
+  .submitButtons {
+    white-space: nowrap;
+    margin-bottom: 24px;
+  }
+}
+
+@media screen and (max-width: @screen-lg) {
+  .tableListForm :global(.ant-form-item) {
+    margin-right: 24px;
+  }
+}
+
+@media screen and (max-width: @screen-md) {
+  .tableListForm :global(.ant-form-item) {
+    margin-right: 8px;
+  }
+}
diff --git a/src/main/frontend/src/services/api.js b/src/main/frontend/src/services/api.js
new file mode 100644
index 0000000..aef7f34
--- /dev/null
+++ b/src/main/frontend/src/services/api.js
@@ -0,0 +1,30 @@
+import { stringify } from 'qs';
+import request from '../utils/request';
+
+export async function queryRule(params) {
+  return request(`/api/rule?${stringify(params)}`);
+}
+
+export async function removeRule(params) {
+  return request('/api/rule', {
+    method: 'POST',
+    body: {
+      ...params,
+      method: 'delete',
+    },
+  });
+}
+
+export async function addRule(params) {
+  return request('/api/rule', {
+    method: 'POST',
+    body: {
+      ...params,
+      method: 'post',
+    },
+  });
+}
+
+export async function queryNotices() {
+  return request('/api/notices');
+}
diff --git a/src/main/frontend/src/services/user.js b/src/main/frontend/src/services/user.js
new file mode 100644
index 0000000..c4defb4
--- /dev/null
+++ b/src/main/frontend/src/services/user.js
@@ -0,0 +1,9 @@
+import request from '../utils/request';
+
+export async function query() {
+  return request('/api/users');
+}
+
+export async function queryCurrent() {
+  return request('/api/currentUser');
+}
diff --git a/src/main/frontend/src/theme.js b/src/main/frontend/src/theme.js
new file mode 100644
index 0000000..9e12511
--- /dev/null
+++ b/src/main/frontend/src/theme.js
@@ -0,0 +1,5 @@
+// https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
+module.exports = {
+  // 'primary-color': '#10e99b',
+  'card-actions-background': '#f5f8fa',
+};
diff --git a/src/main/frontend/src/utils/request.js b/src/main/frontend/src/utils/request.js
new file mode 100644
index 0000000..094f7fc
--- /dev/null
+++ b/src/main/frontend/src/utils/request.js
@@ -0,0 +1,56 @@
+import fetch from 'dva/fetch';
+import { notification } from 'antd';
+
+function checkStatus(response) {
+  if (response.status >= 200 && response.status < 300) {
+    return response;
+  }
+  notification.error({
+    message: `请求错误 ${response.status}: ${response.url}`,
+    description: response.statusText,
+  });
+  const error = new Error(response.statusText);
+  error.response = response;
+  throw error;
+}
+
+/**
+ * Requests a URL, returning a promise.
+ *
+ * @param  {string} url       The URL we want to request
+ * @param  {object} [options] The options we want to pass to "fetch"
+ * @return {object}           An object containing either "data" or "err"
+ */
+export default function request(url, options) {
+  const defaultOptions = {
+    credentials: 'include',
+  };
+  const newOptions = { ...defaultOptions, ...options };
+  if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
+    newOptions.headers = {
+      Accept: 'application/json',
+      'Content-Type': 'application/json; charset=utf-8',
+      ...newOptions.headers,
+    };
+    newOptions.body = JSON.stringify(newOptions.body);
+  }
+
+  return fetch(url, newOptions)
+    .then(checkStatus)
+    .then(response => response.json())
+    .catch((error) => {
+      if (error.code) {
+        notification.error({
+          message: error.name,
+          description: error.message,
+        });
+      }
+      if ('stack' in error && 'message' in error) {
+        notification.error({
+          message: `请求错误: ${url}`,
+          description: error.message,
+        });
+      }
+      return error;
+    });
+}
diff --git a/src/main/frontend/src/utils/utils.js b/src/main/frontend/src/utils/utils.js
new file mode 100644
index 0000000..e0dfd71
--- /dev/null
+++ b/src/main/frontend/src/utils/utils.js
@@ -0,0 +1,94 @@
+import moment from 'moment';
+
+export function fixedZero(val) {
+  return val * 1 < 10 ? `0${val}` : val;
+}
+
+export function getTimeDistance(type) {
+  const now = new Date();
+  const oneDay = 1000 * 60 * 60 * 24;
+
+  if (type === 'today') {
+    now.setHours(0);
+    now.setMinutes(0);
+    now.setSeconds(0);
+    return [moment(now), moment(now.getTime() + (oneDay - 1000))];
+  }
+
+  if (type === 'week') {
+    let day = now.getDay();
+    now.setHours(0);
+    now.setMinutes(0);
+    now.setSeconds(0);
+
+    if (day === 0) {
+      day = 6;
+    } else {
+      day -= 1;
+    }
+
+    const beginTime = now.getTime() - (day * oneDay);
+
+    return [moment(beginTime), moment(beginTime + ((7 * oneDay) - 1000))];
+  }
+
+  if (type === 'month') {
+    const year = now.getFullYear();
+    const month = now.getMonth();
+    const nextDate = moment(now).add(1, 'months');
+    const nextYear = nextDate.year();
+    const nextMonth = nextDate.month();
+
+    return [moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`), moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000)];
+  }
+
+  if (type === 'year') {
+    const year = now.getFullYear();
+
+    return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)];
+  }
+}
+
+export function getPlainNode(nodeList, parentPath = '') {
+  const arr = [];
+  nodeList.forEach((node) => {
+    const item = node;
+    item.path = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
+    item.exact = true;
+    if (item.children && !item.component) {
+      arr.push(...getPlainNode(item.children, item.path));
+    } else {
+      if (item.children && item.component) {
+        item.exact = false;
+      }
+      arr.push(item);
+    }
+  });
+  return arr;
+}
+
+export function digitUppercase(n) {
+  const fraction = ['角', '分'];
+  const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
+  const unit = [
+    ['元', '万', '亿'],
+    ['', '拾', '佰', '仟'],
+  ];
+  let num = Math.abs(n);
+  let s = '';
+  fraction.forEach((item, index) => {
+    s += (digit[Math.floor(num * 10 * (10 ** index)) % 10] + item).replace(/零./, '');
+  });
+  s = s || '整';
+  num = Math.floor(num);
+  for (let i = 0; i < unit[0].length && num > 0; i += 1) {
+    let p = '';
+    for (let j = 0; j < unit[1].length && num > 0; j += 1) {
+      p = digit[num % 10] + unit[1][j] + p;
+      num = Math.floor(num / 10);
+    }
+    s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s;
+  }
+
+  return s.replace(/(零.)*零元/, '元').replace(/(零.)+/g, '零').replace(/^整$/, '零元整');
+}
diff --git a/src/main/frontend/src/utils/utils.less b/src/main/frontend/src/utils/utils.less
new file mode 100644
index 0000000..1ec1efb
--- /dev/null
+++ b/src/main/frontend/src/utils/utils.less
@@ -0,0 +1,50 @@
+.textOverflow() {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  word-break: break-all;
+  white-space: nowrap;
+}
+
+.textOverflowMulti(@line: 3, @bg: #fff) {
+  overflow: hidden;
+  position: relative;
+  line-height: 1.5em;
+  max-height: @line * 1.5em;
+  text-align: justify;
+  margin-right: -1em;
+  padding-right: 1em;
+  &:before {
+    background: @bg;
+    content: '...';
+    padding: 0 1px;
+    position: absolute;
+    right: 14px;
+    bottom: 0;
+  }
+  &:after {
+    background: white;
+    content: '';
+    margin-top: 0.2em;
+    position: absolute;
+    right: 14px;
+    width: 1em;
+    height: 1em;
+  }
+}
+
+// mixins for clearfix
+// ------------------------
+.clearfix() {
+  zoom: 1;
+  &:before,
+  &:after {
+    content: " ";
+    display: table;
+  }
+  &:after {
+    clear: both;
+    visibility: hidden;
+    font-size: 0;
+    height: 0;
+  }
+}
diff --git a/src/main/frontend/tests/jasmine.js b/src/main/frontend/tests/jasmine.js
new file mode 100644
index 0000000..5ff26bf
--- /dev/null
+++ b/src/main/frontend/tests/jasmine.js
@@ -0,0 +1 @@
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
diff --git a/src/main/frontend/tests/run-tests.js b/src/main/frontend/tests/run-tests.js
new file mode 100644
index 0000000..46ef9bb
--- /dev/null
+++ b/src/main/frontend/tests/run-tests.js
@@ -0,0 +1,35 @@
+const { spawn } = require('child_process');
+const { kill } = require('cross-port-killer');
+
+const env = Object.create(process.env);
+env.BROWSER = 'none';
+const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], {
+  env,
+});
+
+startServer.stderr.on('data', (data) => {
+  // eslint-disable-next-line
+  console.log(data);
+});
+
+startServer.on('exit', () => {
+  kill(process.env.PORT || 8000);
+});
+
+// eslint-disable-next-line
+console.log('Starting development server for e2e tests...');
+startServer.stdout.on('data', (data) => {
+  // eslint-disable-next-line
+  console.log(data.toString());
+  if (data.toString().indexOf('The app is running at') >= 0 ||
+      data.toString().indexOf('Compiled with warnings') >= 0) {
+    // eslint-disable-next-line
+    console.log('Development server is started, ready to run tests.');
+    const testCmd = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['test'], {
+      stdio: 'inherit',
+    });
+    testCmd.on('exit', () => {
+      startServer.kill();
+    });
+  }
+});
diff --git a/src/main/frontend/tests/setupTests.js b/src/main/frontend/tests/setupTests.js
new file mode 100644
index 0000000..bb003dd
--- /dev/null
+++ b/src/main/frontend/tests/setupTests.js
@@ -0,0 +1,13 @@
+/* eslint-disable import/first */
+import '../src/polyfill';
+import { jsdom } from 'jsdom';
+import Enzyme from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+Enzyme.configure({ adapter: new Adapter() });
+
+// fixed jsdom miss
+const documentHTML = '<!doctype html><html><body><div id="root"></div></body></html>';
+global.document = jsdom(documentHTML);
+global.window = document.defaultView;
+global.navigator = global.window.navigator;
diff --git a/src/main/frontend/tests/styleMock.js b/src/main/frontend/tests/styleMock.js
new file mode 100644
index 0000000..f053ebf
--- /dev/null
+++ b/src/main/frontend/tests/styleMock.js
@@ -0,0 +1 @@
+module.exports = {};

-- 
To stop receiving notification emails like this one, please contact
"commits@skywalking.apache.org" <co...@skywalking.apache.org>.

[incubator-skywalking-ui] 02/02: Finished dashboard demo

Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch feature/5.0.0
in repository https://gitbox.apache.org/repos/asf/incubator-skywalking-ui.git

commit 381f40bfb9499e32b0aaa0d3e64cdf71df78c148
Author: gaohongtao <ha...@gmail.com>
AuthorDate: Mon Dec 18 16:01:43 2017 +0800

    Finished dashboard demo
---
 src/main/frontend/public/alert.svg                 |   1 +
 src/main/frontend/public/app.svg                   |   1 +
 src/main/frontend/public/database.svg              |   1 +
 src/main/frontend/public/redis.svg                 |   1 +
 src/main/frontend/public/service.svg               |   1 +
 .../frontend/src/components/Charts/Bar/index.d.ts  |  14 ++
 .../frontend/src/components/Charts/Bar/index.js    | 151 ++++++++++++
 .../src/components/Charts/ChartCard/index.d.ts     |  11 +
 .../src/components/Charts/ChartCard/index.js       |  60 +++++
 .../src/components/Charts/ChartCard/index.less     |  74 ++++++
 .../src/components/Charts/Field/index.d.ts         |   7 +
 .../frontend/src/components/Charts/Field/index.js  |  12 +
 .../src/components/Charts/Field/index.less         |  16 ++
 .../src/components/Charts/Gauge/index.d.ts         |  10 +
 .../frontend/src/components/Charts/Gauge/index.js  | 202 ++++++++++++++++
 .../src/components/Charts/MiniArea/index.d.ts      |  29 +++
 .../src/components/Charts/MiniArea/index.js        | 125 ++++++++++
 .../src/components/Charts/MiniBar/index.d.ts       |  11 +
 .../src/components/Charts/MiniBar/index.js         |  87 +++++++
 .../src/components/Charts/MiniProgress/index.d.ts  |  12 +
 .../src/components/Charts/MiniProgress/index.js    |  30 +++
 .../src/components/Charts/MiniProgress/index.less  |  35 +++
 .../frontend/src/components/Charts/Pie/index.d.ts  |  20 ++
 .../frontend/src/components/Charts/Pie/index.js    | 260 +++++++++++++++++++++
 .../frontend/src/components/Charts/Pie/index.less  |  94 ++++++++
 .../src/components/Charts/Radar/index.d.ts         |  14 ++
 .../frontend/src/components/Charts/Radar/index.js  | 189 +++++++++++++++
 .../src/components/Charts/Radar/index.less         |  46 ++++
 .../src/components/Charts/TagCloud/index.d.ts      |  10 +
 .../src/components/Charts/TagCloud/index.js        | 170 ++++++++++++++
 .../src/components/Charts/TagCloud/index.less      |   6 +
 .../src/components/Charts/TimelineChart/index.d.ts |  15 ++
 .../src/components/Charts/TimelineChart/index.js   | 125 ++++++++++
 .../src/components/Charts/TimelineChart/index.less |   3 +
 .../src/components/Charts/WaterWave/index.d.ts     |   9 +
 .../src/components/Charts/WaterWave/index.js       | 200 ++++++++++++++++
 .../src/components/Charts/WaterWave/index.less     |  28 +++
 .../frontend/src/components/Charts/demo/bar.md     |  26 +++
 .../src/components/Charts/demo/chart-card.md       |  65 ++++++
 .../frontend/src/components/Charts/demo/gauge.md   |  18 ++
 .../src/components/Charts/demo/mini-area.md        |  28 +++
 .../src/components/Charts/demo/mini-bar.md         |  28 +++
 .../src/components/Charts/demo/mini-pie.md         |  16 ++
 .../src/components/Charts/demo/mini-progress.md    |  12 +
 .../frontend/src/components/Charts/demo/mix.md     |  83 +++++++
 .../frontend/src/components/Charts/demo/pie.md     |  47 ++++
 .../frontend/src/components/Charts/demo/radar.md   |  64 +++++
 .../src/components/Charts/demo/tag-cloud.md        |  25 ++
 .../src/components/Charts/demo/timeline-chart.md   |  27 +++
 .../src/components/Charts/demo/waterwave.md        |  20 ++
 src/main/frontend/src/components/Charts/equal.js   |  17 ++
 src/main/frontend/src/components/Charts/index.d.ts |  17 ++
 src/main/frontend/src/components/Charts/index.js   |  31 +++
 src/main/frontend/src/components/Charts/index.less |  19 ++
 src/main/frontend/src/components/Charts/index.md   | 132 +++++++++++
 .../frontend/src/components/Trend/demo/basic.md    |  17 ++
 src/main/frontend/src/components/Trend/index.d.ts  |   8 +
 src/main/frontend/src/components/Trend/index.js    |  22 ++
 src/main/frontend/src/components/Trend/index.less  |  30 +++
 src/main/frontend/src/components/Trend/index.md    |  21 ++
 src/main/frontend/src/layouts/BasicLayout.js       |   3 -
 .../frontend/src/routes/Dashboard/Dashboard.js     | 235 ++++++++++++++++++-
 .../frontend/src/routes/Dashboard/Dashboard.less   |   5 +
 63 files changed, 3092 insertions(+), 4 deletions(-)

diff --git a/src/main/frontend/public/alert.svg b/src/main/frontend/public/alert.svg
new file mode 100644
index 0000000..fc9270b
--- /dev/null
+++ b/src/main/frontend/public/alert.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M611.702 44.963l263.203 149.544c60.979 35.06 98.35 99.414 98.154 168.892v299.056c0.037 69.658-37.691 134.044-98.987 168.902l-262.37 149.36c-61.333 34.828-136.864 34.828-198.176 0l-263.61-149.55c-61.285-34.859-99.008-99.246-98 [...]
\ No newline at end of file
diff --git a/src/main/frontend/public/app.svg b/src/main/frontend/public/app.svg
new file mode 100644
index 0000000..4116c21
--- /dev/null
+++ b/src/main/frontend/public/app.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M357.885 49.289h285.276c173.87 0 316.906 143.086 316.906 317.854v285.276c0 173.831-143.086 316.925-316.906 316.925h-285.276c-174.749 0-317.854-143.086-317.854-316.925v-285.276c0-174.749 143.086-317.854 317.854-317.854z" fill= [...]
\ No newline at end of file
diff --git a/src/main/frontend/public/database.svg b/src/main/frontend/public/database.svg
new file mode 100644
index 0000000..7fc1526
--- /dev/null
+++ b/src/main/frontend/public/database.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M896.483256 191.467753v64.081481c0 70.584592-172.214247 128.160915-384.484791 128.160915-212.267475 0-384.483768-57.577347-384.483768-128.160915v-64.081481c0-70.583568 172.216293-128.160915 384.483768-128.160915 212.270545 0  [...]
\ No newline at end of file
diff --git a/src/main/frontend/public/redis.svg b/src/main/frontend/public/redis.svg
new file mode 100644
index 0000000..e634317
--- /dev/null
+++ b/src/main/frontend/public/redis.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M583.66976 285.16864l-229.83168 23.296a2.19136 2.19136 0 0 0-1.84832 1.78176 2.14016 2.14016 0 0 0 1.28 2.27328l179.12832 73.07776c0.28672 0.07168 0.49664 0.14336 0.77824 0.14336 0.77312 0 1.4848-0.42496 1.83808-1.13664l50.65 [...]
\ No newline at end of file
diff --git a/src/main/frontend/public/service.svg b/src/main/frontend/public/service.svg
new file mode 100644
index 0000000..8d2daf7
--- /dev/null
+++ b/src/main/frontend/public/service.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M151.3048309167421 870.0138201689984c199.20612239587413 199.20612239587422 522.1842157706417 199.20612239587433 721.3924594868594-0.002121320343348998 199.2068295026554-199.20682950265532 199.20682950265544-522.1849228774227  [...]
\ No newline at end of file
diff --git a/src/main/frontend/src/components/Charts/Bar/index.d.ts b/src/main/frontend/src/components/Charts/Bar/index.d.ts
new file mode 100644
index 0000000..fd2d05d
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Bar/index.d.ts
@@ -0,0 +1,14 @@
+import * as React from "react";
+export interface BarProps {
+  title: React.ReactNode;
+  color?: string;
+  margin?: [number, number, number, number];
+  height: number;
+  data: Array<{
+    x: string;
+    y: number;
+  }>;
+  autoLabel?: boolean;
+}
+
+export default class Bar extends React.Component<BarProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Bar/index.js b/src/main/frontend/src/components/Charts/Bar/index.js
new file mode 100644
index 0000000..ef834d1
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Bar/index.js
@@ -0,0 +1,151 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import equal from '../equal';
+import styles from '../index.less';
+
+class Bar extends PureComponent {
+  state = {
+    autoHideXLabels: false,
+  }
+
+  componentDidMount() {
+    this.renderChart(this.props.data);
+
+    window.addEventListener('resize', this.resize);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.resize);
+    if (this.chart) {
+      this.chart.destroy();
+    }
+    this.resize.cancel();
+  }
+
+  @Bind()
+  @Debounce(200)
+  resize() {
+    if (!this.node) {
+      return;
+    }
+    const canvasWidth = this.node.parentNode.clientWidth;
+    const { data = [], autoLabel = true } = this.props;
+    if (!autoLabel) {
+      return;
+    }
+    const minWidth = data.length * 30;
+    const { autoHideXLabels } = this.state;
+
+    if (canvasWidth <= minWidth) {
+      if (!autoHideXLabels) {
+        this.setState({
+          autoHideXLabels: true,
+        });
+        this.renderChart(data);
+      }
+    } else if (autoHideXLabels) {
+      this.setState({
+        autoHideXLabels: false,
+      });
+      this.renderChart(data);
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  renderChart(data) {
+    const { autoHideXLabels } = this.state;
+    const {
+      height = 0,
+      fit = true,
+      color = 'rgba(24, 144, 255, 0.85)',
+      margin = [32, 0, (autoHideXLabels ? 8 : 32), 40],
+    } = this.props;
+
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const { Frame } = G2;
+    const frame = new Frame(data);
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height: height - 22,
+      legend: null,
+      plotCfg: {
+        margin,
+      },
+    });
+
+    if (autoHideXLabels) {
+      chart.axis('x', {
+        title: false,
+        tickLine: false,
+        labels: false,
+      });
+    } else {
+      chart.axis('x', {
+        title: false,
+      });
+    }
+    chart.axis('y', {
+      title: false,
+      line: false,
+      tickLine: false,
+    });
+
+    chart.source(frame, {
+      x: {
+        type: 'cat',
+      },
+      y: {
+        min: 0,
+      },
+    });
+
+    chart.tooltip({
+      title: null,
+      crosshairs: false,
+      map: {
+        name: 'x',
+      },
+    });
+    chart.interval().position('x*y').color(color).style({
+      fillOpacity: 1,
+    });
+    chart.render();
+
+    this.chart = chart;
+  }
+
+  render() {
+    const { height, title } = this.props;
+
+    return (
+      <div className={styles.chart} style={{ height }}>
+        <div>
+          { title && <h4>{title}</h4>}
+          <div ref={this.handleRef} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default Bar;
diff --git a/src/main/frontend/src/components/Charts/ChartCard/index.d.ts b/src/main/frontend/src/components/Charts/ChartCard/index.d.ts
new file mode 100644
index 0000000..21d2be3
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/ChartCard/index.d.ts
@@ -0,0 +1,11 @@
+import * as React from "react";
+export interface ChartCardProps {
+  title: React.ReactNode;
+  action?: React.ReactNode;
+  total?: React.ReactNode | number;
+  footer?: React.ReactNode;
+  contentHeight?: number;
+  avatar?: React.ReactNode;
+}
+
+export default class ChartCard extends React.Component<ChartCardProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/ChartCard/index.js b/src/main/frontend/src/components/Charts/ChartCard/index.js
new file mode 100644
index 0000000..a472682
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/ChartCard/index.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import { Card, Spin } from 'antd';
+import classNames from 'classnames';
+
+import styles from './index.less';
+
+const ChartCard = ({
+  loading = false, contentHeight, title, avatar, action, total, footer, children, ...rest
+}) => {
+  const content = (
+    <div className={styles.chartCard}>
+      <div
+        className={classNames(styles.chartTop, { [styles.chartTopMargin]: (!children && !footer) })}
+      >
+        <div className={styles.avatar}>
+          {
+            avatar
+          }
+        </div>
+        <div className={styles.metaWrap}>
+          <div className={styles.meta}>
+            <span className={styles.title}>{title}</span>
+            <span className={styles.action}>{action}</span>
+          </div>
+          {
+            // eslint-disable-next-line
+            (total !== undefined) && (<div className={styles.total} dangerouslySetInnerHTML={{ __html: total }} />)
+          }
+        </div>
+      </div>
+      {
+        children && (
+          <div className={styles.content} style={{ height: contentHeight || 'auto' }}>
+            <div className={contentHeight && styles.contentFixed}>
+              {children}
+            </div>
+          </div>
+        )
+      }
+      {
+        footer && (
+          <div className={classNames(styles.footer, { [styles.footerMargin]: !children })}>
+            {footer}
+          </div>
+        )
+      }
+    </div>
+  );
+
+  return (
+    <Card
+      bodyStyle={{ padding: '20px 24px 8px 24px' }}
+      {...rest}
+    >
+      {<Spin spinning={loading}>{content}</Spin>}
+    </Card>
+  );
+};
+
+export default ChartCard;
diff --git a/src/main/frontend/src/components/Charts/ChartCard/index.less b/src/main/frontend/src/components/Charts/ChartCard/index.less
new file mode 100644
index 0000000..bdc573b
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/ChartCard/index.less
@@ -0,0 +1,74 @@
+@import "~antd/lib/style/themes/default.less";
+
+.chartCard {
+  position: relative;
+  .chartTop {
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+  }
+  .chartTopMargin {
+    margin-bottom: 12px;
+  }
+  .chartTopHasMargin {
+    margin-bottom: 20px;
+  }
+  .metaWrap {
+    float: left;
+  }
+  .avatar {
+    position: relative;
+    top: 4px;
+    float: left;
+    margin-right: 20px;
+    img {
+      border-radius: 100%;
+    }
+  }
+  .meta {
+    color: @text-color-secondary;
+    font-size: @font-size-base;
+    line-height: 22px;
+    height: 22px;
+  }
+  .action {
+    cursor: pointer;
+    position: absolute;
+    top: 0;
+    right: 0;
+  }
+  .total {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    word-break: break-all;
+    white-space: nowrap;
+    color: @heading-color;
+    margin-top: 4px;
+    margin-bottom: 0;
+    font-size: 30px;
+    line-height: 38px;
+    height: 38px;
+  }
+  .content {
+    margin-bottom: 12px;
+    position: relative;
+    width: 100%;
+  }
+  .contentFixed {
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    width: 100%;
+  }
+  .footer {
+    border-top: 1px solid @border-color-split;
+    padding-top: 9px;
+    margin-top: 8px;
+    & > * {
+      position: relative;
+    }
+  }
+  .footerMargin {
+    margin-top: 20px;
+  }
+}
diff --git a/src/main/frontend/src/components/Charts/Field/index.d.ts b/src/main/frontend/src/components/Charts/Field/index.d.ts
new file mode 100644
index 0000000..7fa1328
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Field/index.d.ts
@@ -0,0 +1,7 @@
+import * as React from "react";
+export interface FieldProps {
+  label: React.ReactNode;
+  value: React.ReactNode;
+}
+
+export default class Field extends React.Component<FieldProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Field/index.js b/src/main/frontend/src/components/Charts/Field/index.js
new file mode 100644
index 0000000..0f9ace2
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Field/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+
+import styles from './index.less';
+
+const Field = ({ label, value, ...rest }) => (
+  <div className={styles.field} {...rest}>
+    <span>{label}</span>
+    <span>{value}</span>
+  </div>
+);
+
+export default Field;
diff --git a/src/main/frontend/src/components/Charts/Field/index.less b/src/main/frontend/src/components/Charts/Field/index.less
new file mode 100644
index 0000000..2848f9d
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Field/index.less
@@ -0,0 +1,16 @@
+@import "~antd/lib/style/themes/default.less";
+
+.field {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin: 0;
+  span {
+    font-size: @font-size-base;
+    line-height: 22px;
+  }
+  span:last-child {
+    margin-left: 8px;
+    color: @heading-color;
+  }
+}
diff --git a/src/main/frontend/src/components/Charts/Gauge/index.d.ts b/src/main/frontend/src/components/Charts/Gauge/index.d.ts
new file mode 100644
index 0000000..7f196ae
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Gauge/index.d.ts
@@ -0,0 +1,10 @@
+import * as React from "react";
+export interface GaugeProps {
+  title: React.ReactNode;
+  color?: string;
+  height: number;
+  bgColor?: number;
+  percent: number;
+}
+
+export default class Gauge extends React.Component<GaugeProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Gauge/index.js b/src/main/frontend/src/components/Charts/Gauge/index.js
new file mode 100644
index 0000000..cba4202
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Gauge/index.js
@@ -0,0 +1,202 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import equal from '../equal';
+
+const { Shape } = G2;
+
+const primaryColor = '#2F9CFF';
+const backgroundColor = '#F0F2F5';
+
+/* eslint no-underscore-dangle: 0 */
+class Gauge extends PureComponent {
+  componentDidMount() {
+    setTimeout(() => {
+      this.renderChart();
+    }, 10);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      setTimeout(() => {
+        this.renderChart(nextProps);
+      }, 10);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  initChart(nextProps) {
+    const { title, color = primaryColor } = nextProps || this.props;
+
+    Shape.registShape('point', 'dashBoard', {
+      drawShape(cfg, group) {
+        const originPoint = cfg.points[0];
+        const point = this.parsePoint({ x: originPoint.x, y: 0.4 });
+
+        const center = this.parsePoint({
+          x: 0,
+          y: 0,
+        });
+
+        const shape = group.addShape('polygon', {
+          attrs: {
+            points: [
+              [center.x, center.y],
+              [point.x + 8, point.y],
+              [point.x + 8, point.y - 2],
+              [center.x, center.y - 2],
+            ],
+            radius: 2,
+            lineWidth: 2,
+            arrow: false,
+            fill: color,
+          },
+        });
+
+        group.addShape('Marker', {
+          attrs: {
+            symbol: 'circle',
+            lineWidth: 2,
+            fill: color,
+            radius: 8,
+            x: center.x,
+            y: center.y,
+          },
+        });
+        group.addShape('Marker', {
+          attrs: {
+            symbol: 'circle',
+            lineWidth: 2,
+            fill: '#fff',
+            radius: 5,
+            x: center.x,
+            y: center.y,
+          },
+        });
+
+        const { origin } = cfg;
+        group.addShape('text', {
+          attrs: {
+            x: center.x,
+            y: center.y + 80,
+            text: `${origin._origin.value}%`,
+            textAlign: 'center',
+            fontSize: 24,
+            fill: 'rgba(0, 0, 0, 0.85)',
+          },
+        });
+        group.addShape('text', {
+          attrs: {
+            x: center.x,
+            y: center.y + 45,
+            text: title,
+            textAlign: 'center',
+            fontSize: 14,
+            fill: 'rgba(0, 0, 0, 0.43)',
+          },
+        });
+
+        return shape;
+      },
+    });
+  }
+
+  renderChart(nextProps) {
+    const {
+      height, color = primaryColor, bgColor = backgroundColor, title, percent, format,
+    } = nextProps || this.props;
+    const data = [{ name: title, value: percent }];
+
+    if (this.chart) {
+      this.chart.clear();
+    }
+    if (this.node) {
+      this.node.innerHTML = '';
+    }
+
+    this.initChart(nextProps);
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: true,
+      height,
+      animate: false,
+      plotCfg: {
+        margin: [10, 10, 30, 10],
+      },
+    });
+
+    chart.source(data);
+
+    chart.tooltip(false);
+
+    chart.coord('gauge', {
+      startAngle: -1.2 * Math.PI,
+      endAngle: 0.20 * Math.PI,
+    });
+    chart.col('value', {
+      type: 'linear',
+      nice: true,
+      min: 0,
+      max: 100,
+      tickCount: 6,
+    });
+    chart.axis('value', {
+      subTick: false,
+      tickLine: {
+        stroke: color,
+        lineWidth: 2,
+        value: -14,
+      },
+      labelOffset: -12,
+      formatter: format,
+    });
+    chart.point().position('value').shape('dashBoard');
+    draw(data);
+
+    /* eslint no-shadow: 0 */
+    function draw(data) {
+      const val = data[0].value;
+      const lineWidth = 12;
+      chart.guide().clear();
+
+      chart.guide().arc(() => {
+        return [0, 0.95];
+      }, () => {
+        return [val, 0.95];
+      }, {
+        stroke: color,
+        lineWidth,
+      });
+
+      chart.guide().arc(() => {
+        return [val, 0.95];
+      }, (arg) => {
+        return [arg.max, 0.95];
+      }, {
+        stroke: bgColor,
+        lineWidth,
+      });
+
+      chart.changeData(data);
+    }
+
+    this.chart = chart;
+  }
+
+  render() {
+    return (
+      <div ref={this.handleRef} />
+    );
+  }
+}
+
+export default Gauge;
diff --git a/src/main/frontend/src/components/Charts/MiniArea/index.d.ts b/src/main/frontend/src/components/Charts/MiniArea/index.d.ts
new file mode 100644
index 0000000..d2f67e4
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniArea/index.d.ts
@@ -0,0 +1,29 @@
+import * as React from "react";
+
+// g2已经更新到3.0
+// 不带的写了
+
+export interface Axis {
+  title: any;
+  line: any;
+  gridAlign: any;
+  labels: any;
+  tickLine: any;
+  grid: any;
+}
+
+export interface MiniAreaProps {
+  color?: string;
+  height: number;
+  borderColor?: string;
+  line?: boolean;
+  animate?: boolean;
+  xAxis?: Axis;
+  yAxis?: Axis;
+  data: Array<{
+    x: number;
+    y: number;
+  }>;
+}
+
+export default class MiniArea extends React.Component<MiniAreaProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/MiniArea/index.js b/src/main/frontend/src/components/Charts/MiniArea/index.js
new file mode 100644
index 0000000..65f0969
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniArea/index.js
@@ -0,0 +1,125 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import equal from '../equal';
+import styles from '../index.less';
+
+class MiniArea extends PureComponent {
+  static defaultProps = {
+    borderColor: '#1890FF',
+    color: 'rgba(24, 144, 255, 0.2)',
+  };
+
+  componentDidMount() {
+    this.renderChart(this.props.data);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  renderChart(data) {
+    const {
+      height = 0, fit = true, color, borderWidth = 2, line, xAxis, yAxis, animate = true,
+    } = this.props;
+    const borderColor = this.props.borderColor || color;
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height: height + 54,
+      animate,
+      plotCfg: {
+        margin: [36, 5, 30, 5],
+      },
+      legend: null,
+    });
+
+    if (!xAxis && !yAxis) {
+      chart.axis(false);
+    }
+
+    if (xAxis) {
+      chart.axis('x', xAxis);
+    } else {
+      chart.axis('x', false);
+    }
+
+    if (yAxis) {
+      chart.axis('y', yAxis);
+    } else {
+      chart.axis('y', false);
+    }
+
+    const dataConfig = {
+      x: {
+        type: 'cat',
+        range: [0, 1],
+        ...xAxis,
+      },
+      y: {
+        min: 0,
+        ...yAxis,
+      },
+    };
+
+    chart.tooltip({
+      title: null,
+      crosshairs: false,
+      map: {
+        title: null,
+        name: 'x',
+        value: 'y',
+      },
+    });
+
+    const view = chart.createView();
+    view.source(data, dataConfig);
+
+    view.area().position('x*y').color(color).shape('smooth')
+      .style({ fillOpacity: 1 });
+
+    if (line) {
+      const view2 = chart.createView();
+      view2.source(data, dataConfig);
+      view2.line().position('x*y').color(borderColor).size(borderWidth)
+        .shape('smooth');
+      view2.tooltip(false);
+    }
+    chart.render();
+
+    this.chart = chart;
+  }
+
+  render() {
+    const { height } = this.props;
+
+    return (
+      <div className={styles.miniChart} style={{ height }}>
+        <div className={styles.chartContent}>
+          <div ref={this.handleRef} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default MiniArea;
diff --git a/src/main/frontend/src/components/Charts/MiniBar/index.d.ts b/src/main/frontend/src/components/Charts/MiniBar/index.d.ts
new file mode 100644
index 0000000..09bd761
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniBar/index.d.ts
@@ -0,0 +1,11 @@
+import * as React from "react";
+export interface MiniBarProps {
+  color?: string;
+  height: number;
+  data: Array<{
+    x: number;
+    y: number;
+  }>;
+}
+
+export default class MiniBar extends React.Component<MiniBarProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/MiniBar/index.js b/src/main/frontend/src/components/Charts/MiniBar/index.js
new file mode 100644
index 0000000..991571b
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniBar/index.js
@@ -0,0 +1,87 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import equal from '../equal';
+import styles from '../index.less';
+
+class MiniBar extends PureComponent {
+  componentDidMount() {
+    this.renderChart(this.props.data);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  renderChart(data) {
+    const { height = 0, fit = true, color = '#1890FF' } = this.props;
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const { Frame } = G2;
+    const frame = new Frame(data);
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height: height + 54,
+      plotCfg: {
+        margin: [36, 5, 30, 5],
+      },
+      legend: null,
+    });
+
+    chart.axis(false);
+
+    chart.source(frame, {
+      x: {
+        type: 'cat',
+      },
+      y: {
+        min: 0,
+      },
+    });
+
+    chart.tooltip({
+      title: null,
+      crosshairs: false,
+      map: {
+        name: 'x',
+      },
+    });
+    chart.interval().position('x*y').color(color);
+    chart.render();
+
+    this.chart = chart;
+  }
+
+  render() {
+    const { height } = this.props;
+
+    return (
+      <div className={styles.miniChart} style={{ height }}>
+        <div className={styles.chartContent}>
+          <div ref={this.handleRef} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default MiniBar;
diff --git a/src/main/frontend/src/components/Charts/MiniProgress/index.d.ts b/src/main/frontend/src/components/Charts/MiniProgress/index.d.ts
new file mode 100644
index 0000000..a80b935
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniProgress/index.d.ts
@@ -0,0 +1,12 @@
+import * as React from "react";
+export interface MiniProgressProps {
+  target: number;
+  color?: string;
+  strokeWidth?: number;
+  percent?: number;
+}
+
+export default class MiniProgress extends React.Component<
+  MiniProgressProps,
+  any
+> {}
diff --git a/src/main/frontend/src/components/Charts/MiniProgress/index.js b/src/main/frontend/src/components/Charts/MiniProgress/index.js
new file mode 100644
index 0000000..08fe9b5
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniProgress/index.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { Tooltip } from 'antd';
+
+import styles from './index.less';
+
+const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => (
+  <div className={styles.miniProgress}>
+    <Tooltip title={`目标值: ${target}%`}>
+      <div
+        className={styles.target}
+        style={{ left: (target ? `${target}%` : null) }}
+      >
+        <span style={{ backgroundColor: (color || null) }} />
+        <span style={{ backgroundColor: (color || null) }} />
+      </div>
+    </Tooltip>
+    <div className={styles.progressWrap}>
+      <div
+        className={styles.progress}
+        style={{
+          backgroundColor: (color || null),
+          width: (percent ? `${percent}%` : null),
+          height: (strokeWidth || null),
+        }}
+      />
+    </div>
+  </div>
+);
+
+export default MiniProgress;
diff --git a/src/main/frontend/src/components/Charts/MiniProgress/index.less b/src/main/frontend/src/components/Charts/MiniProgress/index.less
new file mode 100644
index 0000000..06823be
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniProgress/index.less
@@ -0,0 +1,35 @@
+@import "~antd/lib/style/themes/default.less";
+
+.miniProgress {
+  padding: 5px 0;
+  position: relative;
+  width: 100%;
+  .progressWrap {
+    background-color: @background-color-base;
+    position: relative;
+  }
+  .progress {
+    transition: all .4s cubic-bezier(.08, .82, .17, 1) 0s;
+    border-radius: 1px 0 0 1px;
+    background-color: @primary-color;
+    width: 0;
+    height: 100%;
+  }
+  .target {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    span {
+      border-radius: 100px;
+      position: absolute;
+      top: 0;
+      left: 0;
+      height: 4px;
+      width: 2px;
+    }
+    span:last-child {
+      top: auto;
+      bottom: 0;
+    }
+  }
+}
diff --git a/src/main/frontend/src/components/Charts/Pie/index.d.ts b/src/main/frontend/src/components/Charts/Pie/index.d.ts
new file mode 100644
index 0000000..44a465d
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Pie/index.d.ts
@@ -0,0 +1,20 @@
+import * as React from "react";
+export interface PieProps {
+  animate?: boolean;
+  color?: string;
+  height: number;
+  hasLegend?: boolean;
+  margin?: [number, number, number, number];
+  percent?: number;
+  data?: Array<{
+    x: string;
+    y: number;
+  }>;
+  total?: string;
+  title?: React.ReactNode;
+  tooltip?: boolean;
+  valueFormat?: (value: string) => string;
+  subTitle?: React.ReactNode;
+}
+
+export default class Pie extends React.Component<PieProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Pie/index.js b/src/main/frontend/src/components/Charts/Pie/index.js
new file mode 100644
index 0000000..74cd229
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Pie/index.js
@@ -0,0 +1,260 @@
+import React, { Component } from 'react';
+import G2 from 'g2';
+import { Divider } from 'antd';
+import classNames from 'classnames';
+import ReactFitText from 'react-fittext';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import equal from '../equal';
+import styles from './index.less';
+
+/* eslint react/no-danger:0 */
+class Pie extends Component {
+  state = {
+    legendData: [],
+    legendBlock: true,
+  };
+
+  componentDidMount() {
+    this.renderChart();
+    this.resize();
+    window.addEventListener('resize', this.resize);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.resize);
+    if (this.chart) {
+      this.chart.destroy();
+    }
+    this.resize.cancel();
+  }
+
+  @Bind()
+  @Debounce(300)
+  resize() {
+    const { hasLegend } = this.props;
+    if (!hasLegend || !this.root) {
+      window.removeEventListener('resize', this.resize);
+      return;
+    }
+    if (this.root.parentNode.clientWidth <= 380) {
+      if (!this.state.legendBlock) {
+        this.setState({
+          legendBlock: true,
+        }, () => {
+          this.renderChart();
+        });
+      }
+    } else if (this.state.legendBlock) {
+      this.setState({
+        legendBlock: false,
+      }, () => {
+        this.renderChart();
+      });
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  handleRoot = (n) => {
+    this.root = n;
+  }
+
+  handleLegendClick = (item, i) => {
+    const newItem = item;
+    newItem.checked = !newItem.checked;
+
+    const { legendData } = this.state;
+    legendData[i] = newItem;
+
+    if (this.chart) {
+      const filterItem = legendData.filter(l => l.checked).map(l => l.x);
+      this.chart.filter('x', filterItem);
+      this.chart.repaint();
+    }
+
+    this.setState({
+      legendData,
+    });
+  }
+
+  renderChart(d) {
+    let data = d || this.props.data;
+
+    const {
+      height = 0,
+      hasLegend,
+      fit = true,
+      margin = [12, 0, 12, 0], percent, color,
+      inner = 0.75,
+      animate = true,
+      colors,
+      lineWidth = 0,
+    } = this.props;
+
+    const defaultColors = colors;
+
+    let selected = this.props.selected || true;
+    let tooltip = this.props.tooltips || true;
+
+    let formatColor;
+    if (percent) {
+      selected = false;
+      tooltip = false;
+      formatColor = (value) => {
+        if (value === '占比') {
+          return color || 'rgba(24, 144, 255, 0.85)';
+        } else {
+          return '#F0F2F5';
+        }
+      };
+
+      /* eslint no-param-reassign: */
+      data = [
+        {
+          x: '占比',
+          y: parseFloat(percent),
+        },
+        {
+          x: '反比',
+          y: 100 - parseFloat(percent),
+        },
+      ];
+    }
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const { Stat } = G2;
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height,
+      plotCfg: {
+        margin,
+      },
+      animate,
+    });
+
+    if (!tooltip) {
+      chart.tooltip(false);
+    } else {
+      chart.tooltip({
+        title: null,
+      });
+    }
+
+    chart.axis(false);
+    chart.legend(false);
+
+    chart.source(data, {
+      x: {
+        type: 'cat',
+        range: [0, 1],
+      },
+      y: {
+        min: 0,
+      },
+    });
+
+    chart.coord('theta', {
+      inner,
+    });
+
+    chart
+      .intervalStack()
+      .position(Stat.summary.percent('y'))
+      .style({ lineWidth, stroke: '#fff' })
+      .color('x', percent ? formatColor : defaultColors)
+      .selected(selected);
+
+    chart.render();
+
+    this.chart = chart;
+
+    let legendData = [];
+    if (hasLegend) {
+      const geom = chart.getGeoms()[0]; // 获取所有的图形
+      const items = geom.getData(); // 获取图形对应的数据
+      legendData = items.map((item) => {
+        /* eslint no-underscore-dangle:0 */
+        const origin = item._origin;
+        origin.color = item.color;
+        origin.checked = true;
+        return origin;
+      });
+    }
+
+    this.setState({
+      legendData,
+    });
+  }
+
+  render() {
+    const { valueFormat, subTitle, total, hasLegend, className, style } = this.props;
+    const { legendData, legendBlock } = this.state;
+    const pieClassName = classNames(styles.pie, className, {
+      [styles.hasLegend]: !!hasLegend,
+      [styles.legendBlock]: legendBlock,
+    });
+
+    return (
+      <div ref={this.handleRoot} className={pieClassName} style={style}>
+        <ReactFitText maxFontSize={25}>
+          <div className={styles.chart}>
+            <div ref={this.handleRef} style={{ fontSize: 0 }} />
+            {
+              (subTitle || total) && (
+                <div className={styles.total}>
+                  {subTitle && <h4 className="pie-sub-title">{subTitle}</h4>}
+                  {
+                    // eslint-disable-next-line
+                    total && <div className="pie-stat" dangerouslySetInnerHTML={{ __html: total }} />
+                  }
+                </div>
+              )
+            }
+          </div>
+        </ReactFitText>
+
+        {
+          hasLegend && (
+            <ul className={styles.legend}>
+              {
+                legendData.map((item, i) => (
+                  <li key={item.x} onClick={() => this.handleLegendClick(item, i)}>
+                    <span className={styles.dot} style={{ backgroundColor: !item.checked ? '#aaa' : item.color }} />
+                    <span className={styles.legendTitle}>{item.x}</span>
+                    <Divider type="vertical" />
+                    <span className={styles.percent}>{`${(item['..percent'] * 100).toFixed(2)}%`}</span>
+                    <span
+                      className={styles.value}
+                      dangerouslySetInnerHTML={{
+                        __html: valueFormat ? valueFormat(item.y) : item.y,
+                      }}
+                    />
+                  </li>
+                ))
+              }
+            </ul>
+          )
+        }
+      </div>
+    );
+  }
+}
+
+export default Pie;
diff --git a/src/main/frontend/src/components/Charts/Pie/index.less b/src/main/frontend/src/components/Charts/Pie/index.less
new file mode 100644
index 0000000..9478739
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Pie/index.less
@@ -0,0 +1,94 @@
+@import "~antd/lib/style/themes/default.less";
+
+.pie {
+  position: relative;
+  .chart {
+    position: relative;
+  }
+  &.hasLegend .chart {
+    width: ~"calc(100% - 240px)";
+  }
+  .legend {
+    position: absolute;
+    right: 0;
+    min-width: 200px;
+    top: 50%;
+    transform: translateY(-50%);
+    margin: 0 20px;
+    list-style: none;
+    padding: 0;
+    li {
+      cursor: pointer;
+      margin-bottom: 16px;
+      height: 22px;
+      line-height: 22px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+  }
+  .dot {
+    border-radius: 8px;
+    display: inline-block;
+    margin-right: 8px;
+    position: relative;
+    top: -1px;
+    height: 8px;
+    width: 8px;
+  }
+  .line {
+    background-color: @border-color-split;
+    display: inline-block;
+    margin-right: 8px;
+    width: 1px;
+    height: 16px;
+  }
+  .legendTitle {
+    color: @text-color;
+  }
+  .percent {
+    color: @text-color-secondary;
+  }
+  .value {
+    position: absolute;
+    right: 0;
+  }
+  .title {
+    margin-bottom: 8px;
+  }
+  .total {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    text-align: center;
+    height: 62px;
+    transform: translate(-50%, -50%);
+    & > h4 {
+      color: @text-color-secondary;
+      font-size: 14px;
+      line-height: 22px;
+      height: 22px;
+      margin-bottom: 8px;
+      font-weight: normal;
+    }
+    & > p {
+      color: @heading-color;
+      display: block;
+      font-size: 1.2em;
+      height: 32px;
+      line-height: 32px;
+      white-space: nowrap;
+    }
+  }
+}
+
+.legendBlock {
+  &.hasLegend .chart {
+    width: 100%;
+    margin: 0 0 32px 0;
+  }
+  .legend {
+    position: relative;
+    transform: none;
+  }
+}
diff --git a/src/main/frontend/src/components/Charts/Radar/index.d.ts b/src/main/frontend/src/components/Charts/Radar/index.d.ts
new file mode 100644
index 0000000..fa85978
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Radar/index.d.ts
@@ -0,0 +1,14 @@
+import * as React from "react";
+export interface RadarProps {
+  title?: React.ReactNode;
+  height: number;
+  margin?: [number, number, number, number];
+  hasLegend?: boolean;
+  data: Array<{
+    name: string;
+    label: string;
+    value: string;
+  }>;
+}
+
+export default class Radar extends React.Component<RadarProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Radar/index.js b/src/main/frontend/src/components/Charts/Radar/index.js
new file mode 100644
index 0000000..8f338bc
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Radar/index.js
@@ -0,0 +1,189 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import { Row, Col } from 'antd';
+import equal from '../equal';
+import styles from './index.less';
+
+/* eslint react/no-danger:0 */
+class Radar extends PureComponent {
+  state = {
+    legendData: [],
+  }
+
+  componentDidMount() {
+    this.renderChart(this.props.data);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  handleLegendClick = (item, i) => {
+    const newItem = item;
+    newItem.checked = !newItem.checked;
+
+    const { legendData } = this.state;
+    legendData[i] = newItem;
+
+    if (this.chart) {
+      const filterItem = legendData.filter(l => l.checked).map(l => l.name);
+      this.chart.filter('name', filterItem);
+      this.chart.repaint();
+    }
+
+    this.setState({
+      legendData,
+    });
+  }
+
+  renderChart(data) {
+    const { height = 0,
+      hasLegend = true,
+      fit = true,
+      tickCount = 4,
+      margin = [24, 30, 16, 30] } = this.props;
+
+    const colors = [
+      '#1890FF', '#FACC14', '#2FC25B', '#8543E0', '#F04864', '#13C2C2', '#fa8c16', '#a0d911',
+    ];
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height: height - (hasLegend ? 80 : 22),
+      plotCfg: {
+        margin,
+      },
+    });
+
+    this.chart = chart;
+
+    chart.source(data, {
+      value: {
+        min: 0,
+        tickCount,
+      },
+    });
+
+    chart.coord('polar');
+    chart.legend(false);
+
+    chart.axis('label', {
+      line: null,
+      labelOffset: 8,
+      labels: {
+        label: {
+          fill: 'rgba(0, 0, 0, .65)',
+        },
+      },
+      grid: {
+        line: {
+          stroke: '#e9e9e9',
+          lineWidth: 1,
+          lineDash: [0, 0],
+        },
+      },
+    });
+
+    chart.axis('value', {
+      grid: {
+        type: 'polygon',
+        line: {
+          stroke: '#e9e9e9',
+          lineWidth: 1,
+          lineDash: [0, 0],
+        },
+      },
+      labels: {
+        label: {
+          fill: 'rgba(0, 0, 0, .65)',
+        },
+      },
+    });
+
+    chart.line().position('label*value').color('name', colors);
+    chart.point().position('label*value').color('name', colors).shape('circle')
+      .size(3);
+
+    chart.render();
+
+    if (hasLegend) {
+      const geom = chart.getGeoms()[0]; // 获取所有的图形
+      const items = geom.getData(); // 获取图形对应的数据
+      const legendData = items.map((item) => {
+        /* eslint no-underscore-dangle:0 */
+        const origin = item._origin;
+        const result = {
+          name: origin[0].name,
+          color: item.color,
+          checked: true,
+          value: origin.reduce((p, n) => p + n.value, 0),
+        };
+
+        return result;
+      });
+
+      this.setState({
+        legendData,
+      });
+    }
+  }
+
+  render() {
+    const { height, title, hasLegend } = this.props;
+    const { legendData } = this.state;
+
+    return (
+      <div className={styles.radar} style={{ height }}>
+        <div>
+          {title && <h4>{title}</h4>}
+          <div ref={this.handleRef} />
+          {
+            hasLegend && (
+              <Row className={styles.legend}>
+                {
+                  legendData.map((item, i) => (
+                    <Col
+                      span={(24 / legendData.length)}
+                      key={item.name}
+                      onClick={() => this.handleLegendClick(item, i)}
+                    >
+                      <div className={styles.legendItem}>
+                        <p>
+                          <span className={styles.dot} style={{ backgroundColor: !item.checked ? '#aaa' : item.color }} />
+                          <span>{item.name}</span>
+                        </p>
+                        <h6>{item.value}</h6>
+                      </div>
+                    </Col>
+                  ))
+                }
+              </Row>
+            )
+          }
+        </div>
+      </div>
+    );
+  }
+}
+
+export default Radar;
diff --git a/src/main/frontend/src/components/Charts/Radar/index.less b/src/main/frontend/src/components/Charts/Radar/index.less
new file mode 100644
index 0000000..378db9c
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Radar/index.less
@@ -0,0 +1,46 @@
+@import "~antd/lib/style/themes/default.less";
+
+.radar {
+  .legend {
+    margin-top: 16px;
+    .legendItem {
+      position: relative;
+      text-align: center;
+      cursor: pointer;
+      color: @text-color-secondary;
+      line-height: 22px;
+      p {
+        margin: 0;
+      }
+      h6 {
+        color: @heading-color;
+        padding-left: 16px;
+        font-size: 24px;
+        line-height: 32px;
+        margin-top: 4px;
+        margin-bottom: 0;
+      }
+      &:after {
+        background-color: @border-color-split;
+        position: absolute;
+        top: 8px;
+        right: 0;
+        height: 40px;
+        width: 1px;
+        content: '';
+      }
+    }
+    > :last-child .legendItem:after {
+      display: none;
+    }
+    .dot {
+      border-radius: 6px;
+      display: inline-block;
+      margin-right: 6px;
+      position: relative;
+      top: -1px;
+      height: 6px;
+      width: 6px;
+    }
+  }
+}
diff --git a/src/main/frontend/src/components/Charts/TagCloud/index.d.ts b/src/main/frontend/src/components/Charts/TagCloud/index.d.ts
new file mode 100644
index 0000000..e783213
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TagCloud/index.d.ts
@@ -0,0 +1,10 @@
+import * as React from "react";
+export interface TagCloudProps {
+  data: Array<{
+    name: string;
+    value: number;
+  }>;
+  height: number;
+}
+
+export default class TagCloud extends React.Component<TagCloudProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/TagCloud/index.js b/src/main/frontend/src/components/Charts/TagCloud/index.js
new file mode 100644
index 0000000..d3f0d70
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TagCloud/index.js
@@ -0,0 +1,170 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import G2 from 'g2';
+import Cloud from 'g-cloud';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import styles from './index.less';
+
+/* eslint no-underscore-dangle: 0 */
+/* eslint no-param-reassign: 0 */
+
+const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png';
+
+class TagCloud extends PureComponent {
+  componentDidMount() {
+    this.initTagCloud();
+    this.renderChart();
+
+    window.addEventListener('resize', this.resize);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (this.props.data !== nextProps.data) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.resize);
+    this.renderChart.cancel();
+  }
+
+  resize = () => {
+    this.renderChart();
+  }
+
+  initTagCloud = () => {
+    const { Util, Shape } = G2;
+
+    function getTextAttrs(cfg) {
+      const textAttrs = Util.mix(true, {}, {
+        fillOpacity: cfg.opacity,
+        fontSize: cfg.size,
+        rotate: cfg.origin._origin.rotate,
+        // rotate: cfg.origin._origin.rotate,
+        text: cfg.origin._origin.text,
+        textAlign: 'center',
+        fill: cfg.color,
+        textBaseline: 'Alphabetic',
+      }, cfg.style);
+      return textAttrs;
+    }
+
+    // 给point注册一个词云的shape
+    Shape.registShape('point', 'cloud', {
+      drawShape(cfg, container) {
+        cfg.points = this.parsePoints(cfg.points);
+        const attrs = getTextAttrs(cfg);
+        const shape = container.addShape('text', {
+          attrs: Util.mix(attrs, {
+            x: cfg.points[0].x,
+            y: cfg.points[0].y,
+          }),
+        });
+        return shape;
+      },
+    });
+  }
+
+  saveRootRef = (node) => {
+    this.root = node;
+  }
+
+  saveNodeRef = (node) => {
+    this.node = node;
+  }
+
+  @Bind()
+  @Debounce(500)
+  renderChart(newData) {
+    const data = newData || this.props.data;
+    if (!data || data.length < 1) {
+      return;
+    }
+
+    const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C'];
+
+    const height = this.props.height * 4;
+    let width = 0;
+    if (this.root) {
+      width = this.root.offsetWidth * 4;
+    }
+
+    data.sort((a, b) => b.value - a.value);
+
+    const max = data[0].value;
+    const min = data[data.length - 1].value;
+
+    // 构造一个词云布局对象
+    const layout = new Cloud({
+      words: data,
+      width,
+      height,
+
+      rotate: () => 0,
+
+      // 设定文字大小配置函数(默认为12-24px的随机大小)
+      size: words => (((words.value - min) / (max - min)) * 50) + 30,
+
+      // 设定文字内容
+      text: words => words.name,
+    });
+
+    layout.image(imgUrl, (imageCloud) => {
+      // clean
+      if (this.node) {
+        this.node.innerHTML = '';
+      }
+
+      // 执行词云布局函数,并在回调函数中调用G2对结果进行绘制
+      imageCloud.exec((texts) => {
+        const chart = new G2.Chart({
+          container: this.node,
+          width,
+          height,
+          plotCfg: {
+            margin: 0,
+          },
+        });
+
+        chart.legend(false);
+        chart.axis(false);
+        chart.tooltip(false);
+
+        chart.source(texts);
+
+        // 将词云坐标系调整为G2的坐标系
+        chart.coord().reflect();
+
+        chart
+          .point()
+          .position('x*y')
+          .color('text', colors)
+          .size('size', size => size)
+          .shape('cloud')
+          .style({
+            fontStyle: texts[0].style,
+            fontFamily: texts[0].font,
+            fontWeight: texts[0].weight,
+          });
+
+        chart.render();
+      });
+    });
+  }
+
+  render() {
+    return (
+      <div
+        className={classNames(styles.tagCloud, this.props.className)}
+        ref={this.saveRootRef}
+        style={{ width: '100%' }}
+      >
+        <div ref={this.saveNodeRef} style={{ height: this.props.height }} />
+      </div>
+    );
+  }
+}
+
+export default TagCloud;
diff --git a/src/main/frontend/src/components/Charts/TagCloud/index.less b/src/main/frontend/src/components/Charts/TagCloud/index.less
new file mode 100644
index 0000000..96b1006
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TagCloud/index.less
@@ -0,0 +1,6 @@
+.tagCloud {
+  canvas {
+    transform: scale(0.25);
+    transform-origin: 0 0;
+  }
+}
diff --git a/src/main/frontend/src/components/Charts/TimelineChart/index.d.ts b/src/main/frontend/src/components/Charts/TimelineChart/index.d.ts
new file mode 100644
index 0000000..5ea76a1
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TimelineChart/index.d.ts
@@ -0,0 +1,15 @@
+import * as React from "react";
+export interface TimelineChartProps {
+  data: Array<{
+    x: string;
+    y1: string;
+    y2: string;
+  }>;
+  titleMap: { y1: string; y2: string };
+  height?: number;
+}
+
+export default class TimelineChart extends React.Component<
+  TimelineChartProps,
+  any
+> {}
diff --git a/src/main/frontend/src/components/Charts/TimelineChart/index.js b/src/main/frontend/src/components/Charts/TimelineChart/index.js
new file mode 100644
index 0000000..65048b8
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TimelineChart/index.js
@@ -0,0 +1,125 @@
+import React, { Component } from 'react';
+import G2 from 'g2';
+import Slider from 'g2-plugin-slider';
+import styles from './index.less';
+
+class TimelineChart extends Component {
+  componentDidMount() {
+    this.renderChart(this.props.data);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.data !== this.props.data) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+    if (this.slider) {
+      this.slider.destroy();
+    }
+  }
+
+  sliderId = `timeline-chart-slider-${Math.random() * 1000}`
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  renderChart(data) {
+    const { height = 400, margin = [60, 20, 40, 40], titleMap, borderWidth = 2 } = this.props;
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    if (this.sliderId) {
+      document.getElementById(this.sliderId).innerHTML = '';
+    }
+    this.node.innerHTML = '';
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: true,
+      height,
+      plotCfg: {
+        margin,
+      },
+    });
+
+    chart.axis('x', {
+      title: false,
+    });
+    chart.axis('y1', {
+      title: false,
+    });
+    chart.axis('y2', false);
+
+    chart.legend({
+      mode: false,
+      position: 'top',
+    });
+
+    let max;
+    if (data[0] && data[0].y1 && data[0].y2) {
+      max = Math.max(data.sort((a, b) => b.y1 - a.y1)[0].y1,
+        data.sort((a, b) => b.y2 - a.y2)[0].y2);
+    }
+
+    chart.source(data, {
+      x: {
+        type: 'timeCat',
+        tickCount: 16,
+        mask: 'HH:MM',
+        range: [0, 1],
+      },
+      y1: {
+        alias: titleMap.y1,
+        max,
+        min: 0,
+      },
+      y2: {
+        alias: titleMap.y2,
+        max,
+        min: 0,
+      },
+    });
+
+    chart.line().position('x*y1').color('#1890FF').size(borderWidth);
+    chart.line().position('x*y2').color('#2FC25B').size(borderWidth);
+
+    this.chart = chart;
+
+    /* eslint new-cap:0 */
+    const slider = new Slider({
+      domId: this.sliderId,
+      height: 26,
+      xDim: 'x',
+      yDim: 'y1',
+      charts: [chart],
+    });
+    slider.render();
+
+    this.slider = slider;
+  }
+
+  render() {
+    const { height, title } = this.props;
+
+    return (
+      <div className={styles.timelineChart} style={{ height }}>
+        <div>
+          { title && <h4>{title}</h4>}
+          <div ref={this.handleRef} />
+          <div id={this.sliderId} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default TimelineChart;
diff --git a/src/main/frontend/src/components/Charts/TimelineChart/index.less b/src/main/frontend/src/components/Charts/TimelineChart/index.less
new file mode 100644
index 0000000..1751975
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TimelineChart/index.less
@@ -0,0 +1,3 @@
+.timelineChart {
+  background: #fff;
+}
diff --git a/src/main/frontend/src/components/Charts/WaterWave/index.d.ts b/src/main/frontend/src/components/Charts/WaterWave/index.d.ts
new file mode 100644
index 0000000..0fefbea
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/WaterWave/index.d.ts
@@ -0,0 +1,9 @@
+import * as React from "react";
+export interface WaterWaveProps {
+  title: React.ReactNode;
+  color?: string;
+  height: number;
+  percent: number;
+}
+
+export default class WaterWave extends React.Component<WaterWaveProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/WaterWave/index.js b/src/main/frontend/src/components/Charts/WaterWave/index.js
new file mode 100644
index 0000000..a9eece6
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/WaterWave/index.js
@@ -0,0 +1,200 @@
+import React, { PureComponent } from 'react';
+import styles from './index.less';
+
+/* eslint no-return-assign: 0 */
+// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
+
+class WaterWave extends PureComponent {
+  static defaultProps = {
+    height: 160,
+  }
+  state = {
+    radio: 1,
+  }
+
+  componentDidMount() {
+    this.renderChart();
+    this.resize();
+
+    window.addEventListener('resize', this.resize);
+  }
+
+  componentWillUnmount() {
+    cancelAnimationFrame(this.timer);
+    if (this.node) {
+      this.node.innerHTML = '';
+    }
+    window.removeEventListener('resize', this.resize);
+  }
+
+  resize = () => {
+    const { height } = this.props;
+    const { offsetWidth } = this.root.parentNode;
+    this.setState({
+      radio: offsetWidth < height ? offsetWidth / height : 1,
+    });
+  }
+
+  renderChart() {
+    const { percent, color = '#1890FF' } = this.props;
+    const data = percent / 100;
+    const self = this;
+
+    if (!this.node || !data) {
+      return;
+    }
+
+    const canvas = this.node;
+    const ctx = canvas.getContext('2d');
+
+    const canvasWidth = canvas.width;
+    const canvasHeight = canvas.height;
+    const radius = canvasWidth / 2;
+    const lineWidth = 2;
+    const cR = radius - (lineWidth);
+
+    ctx.beginPath();
+    ctx.lineWidth = lineWidth * 2;
+
+    const axisLength = canvasWidth - (lineWidth);
+    const unit = axisLength / 8;
+    const range = 0.2; // 振幅
+    let currRange = range;
+    const xOffset = lineWidth;
+    let sp = 0; // 周期偏移量
+    let currData = 0;
+    const waveupsp = 0.005; // 水波上涨速度
+
+    let arcStack = [];
+    const bR = radius - (lineWidth);
+    const circleOffset = -(Math.PI / 2);
+    let circleLock = true;
+
+    for (let i = circleOffset; i < circleOffset + (2 * Math.PI); i += 1 / (8 * Math.PI)) {
+      arcStack.push([
+        radius + (bR * Math.cos(i)),
+        radius + (bR * Math.sin(i)),
+      ]);
+    }
+
+    const cStartPoint = arcStack.shift();
+    ctx.strokeStyle = color;
+    ctx.moveTo(cStartPoint[0], cStartPoint[1]);
+
+    function drawSin() {
+      ctx.beginPath();
+      ctx.save();
+
+      const sinStack = [];
+      for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) {
+        const x = sp + ((xOffset + i) / unit);
+        const y = Math.sin(x) * currRange;
+        const dx = i;
+        const dy = ((2 * cR * (1 - currData)) + (radius - cR)) - (unit * y);
+
+        ctx.lineTo(dx, dy);
+        sinStack.push([dx, dy]);
+      }
+
+      const startPoint = sinStack.shift();
+
+      ctx.lineTo(xOffset + axisLength, canvasHeight);
+      ctx.lineTo(xOffset, canvasHeight);
+      ctx.lineTo(startPoint[0], startPoint[1]);
+
+      const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
+      gradient.addColorStop(0, '#ffffff');
+      gradient.addColorStop(1, '#1890FF');
+      ctx.fillStyle = gradient;
+      ctx.fill();
+      ctx.restore();
+    }
+
+    function render() {
+      ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+      if (circleLock) {
+        if (arcStack.length) {
+          const temp = arcStack.shift();
+          ctx.lineTo(temp[0], temp[1]);
+          ctx.stroke();
+        } else {
+          circleLock = false;
+          ctx.lineTo(cStartPoint[0], cStartPoint[1]);
+          ctx.stroke();
+          arcStack = null;
+
+          ctx.globalCompositeOperation = 'destination-over';
+          ctx.beginPath();
+          ctx.lineWidth = lineWidth;
+          ctx.arc(radius, radius, bR, 0, 2 * Math.PI, 1);
+
+          ctx.beginPath();
+          ctx.save();
+          ctx.arc(radius, radius, radius - (3 * lineWidth), 0, 2 * Math.PI, 1);
+
+          ctx.restore();
+          ctx.clip();
+          ctx.fillStyle = '#1890FF';
+        }
+      } else {
+        if (data >= 0.85) {
+          if (currRange > range / 4) {
+            const t = range * 0.01;
+            currRange -= t;
+          }
+        } else if (data <= 0.1) {
+          if (currRange < range * 1.5) {
+            const t = range * 0.01;
+            currRange += t;
+          }
+        } else {
+          if (currRange <= range) {
+            const t = range * 0.01;
+            currRange += t;
+          }
+          if (currRange >= range) {
+            const t = range * 0.01;
+            currRange -= t;
+          }
+        }
+        if ((data - currData) > 0) {
+          currData += waveupsp;
+        }
+        if ((data - currData) < 0) {
+          currData -= waveupsp;
+        }
+
+        sp += 0.07;
+        drawSin();
+      }
+      self.timer = requestAnimationFrame(render);
+    }
+
+    render();
+  }
+
+  render() {
+    const { radio } = this.state;
+    const { percent, title, height } = this.props;
+    return (
+      <div className={styles.waterWave} ref={n => (this.root = n)} style={{ transform: `scale(${radio})` }}>
+        <div style={{ width: height, height, overflow: 'hidden' }}>
+          <canvas
+            className={styles.waterWaveCanvasWrapper}
+            ref={n => (this.node = n)}
+            width={height * 2}
+            height={height * 2}
+          />
+        </div>
+        <div className={styles.text} style={{ width: height }}>
+          {
+            title && <span>{title}</span>
+          }
+          <h4>{percent}%</h4>
+        </div>
+      </div>
+    );
+  }
+}
+
+export default WaterWave;
diff --git a/src/main/frontend/src/components/Charts/WaterWave/index.less b/src/main/frontend/src/components/Charts/WaterWave/index.less
new file mode 100644
index 0000000..d185ca3
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/WaterWave/index.less
@@ -0,0 +1,28 @@
+@import "~antd/lib/style/themes/default.less";
+
+.waterWave {
+  display: inline-block;
+  position: relative;
+  transform-origin: left;
+  .text {
+    position: absolute;
+    left: 0;
+    top: 32px;
+    text-align: center;
+    width: 100%;
+    span {
+      color: @text-color-secondary;
+      font-size: 14px;
+      line-height: 22px;
+    }
+    h4 {
+      color: @heading-color;
+      line-height: 32px;
+      font-size: 24px;
+    }
+  }
+  .waterWaveCanvasWrapper {
+    transform: scale(.5);
+    transform-origin: 0 0;
+  }
+}
diff --git a/src/main/frontend/src/components/Charts/demo/bar.md b/src/main/frontend/src/components/Charts/demo/bar.md
new file mode 100644
index 0000000..955f44e
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/bar.md
@@ -0,0 +1,26 @@
+---
+order: 4
+title: 柱状图
+---
+
+通过设置 `x`,`y` 属性,可以快速的构建出一个漂亮的柱状图,各种纬度的关系则是通过自定义的数据展现。
+
+````jsx
+import { Bar } from 'ant-design-pro/lib/Charts';
+
+const salesData = [];
+for (let i = 0; i < 12; i += 1) {
+  salesData.push({
+    x: `${i + 1}月`,
+    y: Math.floor(Math.random() * 1000) + 200,
+  });
+}
+
+ReactDOM.render(
+  <Bar
+    height={200}
+    title="销售额趋势"
+    data={salesData}
+  />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/chart-card.md b/src/main/frontend/src/components/Charts/demo/chart-card.md
new file mode 100644
index 0000000..5120479
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/chart-card.md
@@ -0,0 +1,65 @@
+---
+order: 1
+title: 图表卡片
+---
+
+用于展示图表的卡片容器,可以方便的配合其它图表套件展示丰富信息。
+
+````jsx
+import { ChartCard, yuan, Field } from 'ant-design-pro/lib/Charts';
+import Trend from 'ant-design-pro/lib/Trend';
+import { Row, Col, Icon, Tooltip } from 'antd';
+import numeral from 'numeral';
+
+ReactDOM.render(
+  <Row>
+    <Col span={24}>
+      <ChartCard
+        title="销售额"
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total={yuan(126560)}
+        footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />}
+        contentHeight={46}
+      >
+        <span>
+          周同比
+          <Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
+        </span>
+        <span style={{ marginLeft: 16 }}>
+          日环比
+          <Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
+        </span>
+      </ChartCard>
+    </Col>
+    <Col span={24} style={{ marginTop: 24 }}>
+      <ChartCard
+        title="移动指标"
+        avatar={
+          <img
+            style={{ width: 56, height: 56 }}
+            src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png"
+            alt="indicator"
+          />
+        }
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total={yuan(126560)}
+        footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />}
+      />
+    </Col>
+    <Col span={24} style={{ marginTop: 24 }}>
+      <ChartCard
+        title="移动指标"
+        avatar={(
+          <img
+            alt="indicator"
+            style={{ width: 56, height: 56 }}
+            src="https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png"
+          />
+        )}
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total={yuan(126560)}
+      />
+    </Col>
+  </Row>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/gauge.md b/src/main/frontend/src/components/Charts/demo/gauge.md
new file mode 100644
index 0000000..f53465d
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/gauge.md
@@ -0,0 +1,18 @@
+---
+order: 7
+title: 仪表盘 
+---
+
+仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。
+
+````jsx
+import { Gauge } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+  <Gauge
+    title="核销率"
+    height={164}
+    percent={87}
+  />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/mini-area.md b/src/main/frontend/src/components/Charts/demo/mini-area.md
new file mode 100644
index 0000000..2b9bfb4
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mini-area.md
@@ -0,0 +1,28 @@
+---
+order: 2
+col: 2
+title: 迷你区域图
+---
+
+````jsx
+import { MiniArea } from 'ant-design-pro/lib/Charts';
+import moment from 'moment';
+
+const visitData = [];
+const beginDay = new Date().getTime();
+for (let i = 0; i < 20; i += 1) {
+  visitData.push({
+    x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+    y: Math.floor(Math.random() * 100) + 10,
+  });
+}
+
+ReactDOM.render(
+  <MiniArea
+    line
+    color="#cceafe"
+    height={45}
+    data={visitData}
+  />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/mini-bar.md b/src/main/frontend/src/components/Charts/demo/mini-bar.md
new file mode 100644
index 0000000..fef301b
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mini-bar.md
@@ -0,0 +1,28 @@
+---
+order: 2
+col: 2
+title: 迷你柱状图
+---
+
+迷你柱状图更适合展示简单的区间数据,简洁的表现方式可以很好的减少大数据量的视觉展现压力。
+
+````jsx
+import { MiniBar } from 'ant-design-pro/lib/Charts';
+import moment from 'moment';
+
+const visitData = [];
+const beginDay = new Date().getTime();
+for (let i = 0; i < 20; i += 1) {
+  visitData.push({
+    x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+    y: Math.floor(Math.random() * 100) + 10,
+  });
+}
+
+ReactDOM.render(
+  <MiniBar
+    height={45}
+    data={visitData}
+  />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/mini-pie.md b/src/main/frontend/src/components/Charts/demo/mini-pie.md
new file mode 100644
index 0000000..9b1abf0
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mini-pie.md
@@ -0,0 +1,16 @@
+---
+order: 6
+title: 迷你饼状图
+---
+
+通过简化 `Pie` 属性的设置,可以快速的实现极简的饼状图,可配合 `ChartCard` 组合展
+现更多业务场景。
+
+```jsx
+import { Pie } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+  <Pie percent={28} subTitle="中式快餐" total="28%" height={140} />,
+  mountNode
+);
+```
diff --git a/src/main/frontend/src/components/Charts/demo/mini-progress.md b/src/main/frontend/src/components/Charts/demo/mini-progress.md
new file mode 100644
index 0000000..6308a8f
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mini-progress.md
@@ -0,0 +1,12 @@
+---
+order: 3
+title: 迷你进度条
+---
+
+````jsx
+import { MiniProgress } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+  <MiniProgress percent={78} strokeWidth={8} target={80} />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/mix.md b/src/main/frontend/src/components/Charts/demo/mix.md
new file mode 100644
index 0000000..0c158e5
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mix.md
@@ -0,0 +1,83 @@
+---
+order: 0
+title: 图表套件组合展示
+---
+
+利用 Ant Design Pro 提供的图表套件,可以灵活组合符合设计规范的图表来满足复杂的业务需求。
+
+````jsx
+import { ChartCard, Field, MiniArea, MiniBar, MiniProgress } from 'ant-design-pro/lib/Charts';
+import Trend from 'ant-design-pro/lib/Trend';
+import NumberInfo from 'ant-design-pro/lib/NumberInfo';
+import { Row, Col, Icon, Tooltip } from 'antd';
+import numeral from 'numeral';
+import moment from 'moment';
+
+const visitData = [];
+const beginDay = new Date().getTime();
+for (let i = 0; i < 20; i += 1) {
+  visitData.push({
+    x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+    y: Math.floor(Math.random() * 100) + 10,
+  });
+}
+
+ReactDOM.render(
+  <Row>
+    <Col span={24}>
+      <ChartCard
+        title="搜索用户数量"
+        contentHeight={134}
+      >
+        <NumberInfo
+          subTitle={<span>本周访问</span>}
+          total={numeral(12321).format('0,0')}
+          status="up"
+          subTotal={17.1}
+        />
+        <MiniArea
+          line
+          height={45}
+          data={visitData}
+        />
+      </ChartCard>
+    </Col>
+    <Col span={24} style={{ marginTop: 24 }}>
+      <ChartCard
+        title="访问量"
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total={numeral(8846).format('0,0')}
+        footer={<Field label="日访问量" value={numeral(1234).format('0,0')} />}
+        contentHeight={46}
+      >
+        <MiniBar
+          height={46}
+          data={visitData}
+        />
+      </ChartCard>
+    </Col>
+    <Col span={24} style={{ marginTop: 24 }}>
+      <ChartCard
+        title="线上购物转化率"
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total="78%"
+        footer={
+          <div>
+            <span>
+              周同比
+              <Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
+            </span>
+            <span style={{ marginLeft: 16 }}>
+              日环比
+              <Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
+            </span>
+          </div>
+        }
+        contentHeight={46}
+      >
+        <MiniProgress percent={78} strokeWidth={8} target={80} />
+      </ChartCard>
+    </Col>
+  </Row>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/pie.md b/src/main/frontend/src/components/Charts/demo/pie.md
new file mode 100644
index 0000000..2929f2a
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/pie.md
@@ -0,0 +1,47 @@
+---
+order: 5
+title: 饼状图
+---
+
+````jsx
+import { Pie, yuan } from 'ant-design-pro/lib/Charts';
+
+const salesPieData = [
+  {
+    x: '家用电器',
+    y: 4544,
+  },
+  {
+    x: '食用酒水',
+    y: 3321,
+  },
+  {
+    x: '个护健康',
+    y: 3113,
+  },
+  {
+    x: '服饰箱包',
+    y: 2341,
+  },
+  {
+    x: '母婴产品',
+    y: 1231,
+  },
+  {
+    x: '其他',
+    y: 1231,
+  },
+];
+
+ReactDOM.render(
+  <Pie
+    hasLegend
+    title="销售额"
+    subTitle="销售额"
+    total={yuan(salesPieData.reduce((pre, now) => now.y + pre, 0))}
+    data={salesPieData}
+    valueFormat={val => yuan(val)}
+    height={294}
+  />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/radar.md b/src/main/frontend/src/components/Charts/demo/radar.md
new file mode 100644
index 0000000..584344a
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/radar.md
@@ -0,0 +1,64 @@
+---
+order: 7
+title: 雷达图
+---
+
+````jsx
+import { Radar, ChartCard } from 'ant-design-pro/lib/Charts';
+
+const radarOriginData = [
+  {
+    name: '个人',
+    ref: 10,
+    koubei: 8,
+    output: 4,
+    contribute: 5,
+    hot: 7,
+  },
+  {
+    name: '团队',
+    ref: 3,
+    koubei: 9,
+    output: 6,
+    contribute: 3,
+    hot: 1,
+  },
+  {
+    name: '部门',
+    ref: 4,
+    koubei: 1,
+    output: 6,
+    contribute: 5,
+    hot: 7,
+  },
+];
+const radarData = [];
+const radarTitleMap = {
+  ref: '引用',
+  koubei: '口碑',
+  output: '产量',
+  contribute: '贡献',
+  hot: '热度',
+};
+radarOriginData.forEach((item) => {
+  Object.keys(item).forEach((key) => {
+    if (key !== 'name') {
+      radarData.push({
+        name: item.name,
+        label: radarTitleMap[key],
+        value: item[key],
+      });
+    }
+  });
+});
+
+ReactDOM.render(
+  <ChartCard title="数据比例">
+    <Radar
+      hasLegend
+      height={286}
+      data={radarData}
+    />
+  </ChartCard>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/tag-cloud.md b/src/main/frontend/src/components/Charts/demo/tag-cloud.md
new file mode 100644
index 0000000..c66f6fe
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/tag-cloud.md
@@ -0,0 +1,25 @@
+---
+order: 9
+title: 标签云
+---
+
+标签云是一套相关的标签以及与此相应的权重展示方式,一般典型的标签云有 30 至 150 个标签,而权重影响使用的字体大小或其他视觉效果。
+
+````jsx
+import { TagCloud } from 'ant-design-pro/lib/Charts';
+
+const tags = [];
+for (let i = 0; i < 50; i += 1) {
+  tags.push({
+    name: `TagClout-Title-${i}`,
+    value: Math.floor((Math.random() * 50)) + 20,
+  });
+}
+
+ReactDOM.render(
+  <TagCloud
+    data={tags}
+    height={200}
+  />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/timeline-chart.md b/src/main/frontend/src/components/Charts/demo/timeline-chart.md
new file mode 100644
index 0000000..60773b5
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/timeline-chart.md
@@ -0,0 +1,27 @@
+---
+order: 9
+title: 带有时间轴的图表
+---
+
+使用 `TimelineChart` 组件可以实现带有时间轴的柱状图展现,而其中的 `x` 属性,则是时间值的指向,默认最多支持同时展现两个指标,分别是 `y1` 和 `y2`。
+
+````jsx
+import { TimelineChart } from 'ant-design-pro/lib/Charts';
+
+const chartData = [];
+for (let i = 0; i < 20; i += 1) {
+  chartData.push({
+    x: (new Date().getTime()) + (1000 * 60 * 30 * i),
+    y1: Math.floor(Math.random() * 100) + 1000,
+    y2: Math.floor(Math.random() * 100) + 10,
+  });
+}
+
+ReactDOM.render(
+  <TimelineChart
+    height={200}
+    data={chartData}
+    titleMap={{ y1: '客流量', y2: '支付笔数' }}
+  />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/waterwave.md b/src/main/frontend/src/components/Charts/demo/waterwave.md
new file mode 100644
index 0000000..74d290f
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/waterwave.md
@@ -0,0 +1,20 @@
+---
+order: 8
+title: 水波图 
+---
+
+水波图是一种比例的展示方式,可以更直观的展示关键值的占比。
+
+````jsx
+import { WaterWave } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+  <div style={{ textAlign: 'center' }}>
+    <WaterWave
+      height={161}
+      title="补贴资金剩余"
+      percent={34}
+    />
+  </div>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/equal.js b/src/main/frontend/src/components/Charts/equal.js
new file mode 100644
index 0000000..ff3a4c7
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/equal.js
@@ -0,0 +1,17 @@
+/* eslint eqeqeq: 0 */
+
+function equal(old, target) {
+  let r = true;
+  for (const prop in old) {
+    if (typeof old[prop] === 'function' && typeof target[prop] === 'function') {
+      if (old[prop].toString() != target[prop].toString()) {
+        r = false;
+      }
+    } else if (old[prop] != target[prop]) {
+      r = false;
+    }
+  }
+  return r;
+}
+
+export default equal;
diff --git a/src/main/frontend/src/components/Charts/index.d.ts b/src/main/frontend/src/components/Charts/index.d.ts
new file mode 100644
index 0000000..e47b947
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/index.d.ts
@@ -0,0 +1,17 @@
+export { default as numeral } from "numeral";
+export { default as ChartCard } from "./ChartCard";
+export { default as Bar } from "./Bar";
+export { default as Pie } from "./Pie";
+export { default as Radar } from "./Radar";
+export { default as Gauge } from "./Gauge";
+export { default as MiniArea } from "./MiniArea";
+export { default as MiniBar } from "./MiniBar";
+export { default as MiniProgress } from "./MiniProgress";
+export { default as Field } from "./Field";
+export { default as WaterWave } from "./WaterWave";
+export { default as TagCloud } from "./TagCloud";
+export { default as TimelineChart } from "./TimelineChart";
+
+declare const yuan: (value: number | string) => string;
+
+export { yuan };
diff --git a/src/main/frontend/src/components/Charts/index.js b/src/main/frontend/src/components/Charts/index.js
new file mode 100644
index 0000000..cea9949
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/index.js
@@ -0,0 +1,31 @@
+import numeral from 'numeral';
+import ChartCard from './ChartCard';
+import Bar from './Bar';
+import Pie from './Pie';
+import Radar from './Radar';
+import Gauge from './Gauge';
+import MiniArea from './MiniArea';
+import MiniBar from './MiniBar';
+import MiniProgress from './MiniProgress';
+import Field from './Field';
+import WaterWave from './WaterWave';
+import TagCloud from './TagCloud';
+import TimelineChart from './TimelineChart';
+
+const yuan = val => `&yen; ${numeral(val).format('0,0')}`;
+
+export default {
+  yuan,
+  Bar,
+  Pie,
+  Gauge,
+  Radar,
+  MiniBar,
+  MiniArea,
+  MiniProgress,
+  ChartCard,
+  Field,
+  WaterWave,
+  TagCloud,
+  TimelineChart,
+};
diff --git a/src/main/frontend/src/components/Charts/index.less b/src/main/frontend/src/components/Charts/index.less
new file mode 100644
index 0000000..52f97c4
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/index.less
@@ -0,0 +1,19 @@
+.miniChart {
+  position: relative;
+  width: 100%;
+  .chartContent {
+    position: absolute;
+    bottom: -34px;
+    width: 100%;
+    & > div {
+      margin: 0 -5px;
+      overflow: hidden;
+    }
+  }
+  .chartLoading {
+    position: absolute;
+    top: 16px;
+    left: 50%;
+    margin-left: -7px;
+  }
+}
diff --git a/src/main/frontend/src/components/Charts/index.md b/src/main/frontend/src/components/Charts/index.md
new file mode 100644
index 0000000..218f4eb
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/index.md
@@ -0,0 +1,132 @@
+---
+title: 
+  en-US: Charts
+  zh-CN: Charts
+subtitle: 图表
+order: 2
+cols: 2
+---
+
+Ant Design Pro 提供的业务中常用的图表类型,都是基于 [G2](https://antv.alipay.com/g2/doc/index.html) 按照 Ant Design 图表规范封装,需要注意的是 Ant Design Pro 的图表组件以套件形式提供,可以任意组合实现复杂的业务需求。
+
+因为结合了 Ant Design 的标准设计,本着极简的设计思想以及开箱即用的理念,简化了大量 API 配置,所以如果需要灵活定制图表,可以参考 Ant Design Pro 图表实现,自行基于 [G2](https://antv.alipay.com/g2/doc/index.html) 封装图表组件使用。
+
+## API
+
+### ChartCard
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 卡片标题 | ReactNode\|string | - |
+| action | 卡片操作 | ReactNode | - |
+| total | 数据总量 | ReactNode \| number | - |
+| footer | 卡片底部 | ReactNode | - |
+| contentHeight | 内容区域高度 | number | - |
+| avatar | 右侧图标 | React.ReactNode | - |
+### MiniBar
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| color | 图表颜色 | string | `#1890FF` |
+| height | 图表高度 | number | - |
+| data | 数据 | array<{x, y}> | - |
+
+### MiniArea
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| color | 图表颜色 | string | `rgba(24, 144, 255, 0.2)` |
+| borderColor | 图表边颜色 | string | `#1890FF` |
+| height | 图表高度 | number | - |
+| line | 是否显示描边 | boolean | false |
+| animate | 是否显示动画 | boolean | true |
+| xAxis | [x 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
+| yAxis | [y 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
+| data | 数据 | array<{x, y}> | - |
+
+### MiniProgress
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| target | 目标比例 | number | - |
+| color | 进度条颜色 | string | - |
+| strokeWidth | 进度条高度 | number | - |
+| percent | 进度比例 | number | - |
+
+### Bar
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
+| margin | 图表内部间距 | array | \[32, 0, 32, 40\] |
+| height | 图表高度 | number | - |
+| data | 数据 | array<{x, y}> | - |
+| autoLabel | 在宽度不足时,自动隐藏 x 轴的 label | boolean | `true` |
+
+### Pie
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| animate | 是否显示动画 | boolean | true |
+| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
+| height | 图表高度 | number | - |
+| hasLegend | 是否显示 legend | boolean | `false` |
+| margin | 图表内部间距 | array | \[24, 0, 24, 0\] |
+| percent | 占比 | number | - |
+| tooltip | 是否显示 tooltip | boolean | true |
+| valueFormat | 显示值的格式化函数 | function | - |
+| title | 图表标题 | ReactNode|string | - |
+| subTitle | 图表子标题 | ReactNode|string | - |
+| total | 图标中央的总数 | string | - |
+
+### Radar
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| height | 图表高度 | number | - |
+| hasLegend | 是否显示 legend | boolean | `false` |
+| margin | 图表内部间距 | array | \[24, 30, 16, 30\] |
+| data | 图标数据 | array<{name,label,value}> | - |
+
+### Gauge
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| height | 图表高度 | number | - |
+| color | 图表颜色 | string | `#2F9CFF` |
+| bgColor | 图表背景颜色 | string | `#F0F2F5` |
+| percent | 进度比例 | number | - |
+
+### WaterWave
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| height | 图表高度 | number | - |
+| color | 图表颜色 | string | `#1890FF` |
+| percent | 进度比例 | number | - |
+
+### TagCloud
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| data | 标题 | Array<name, value\> | - |
+| height | 高度值 | number | - |
+
+### TimelineChart
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| data | 标题 | Array<x, y1, y2\> | - |
+| titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - |
+| height | 高度值 | number | 400 |
+
+### Field
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| label | 标题 | ReactNode\|string | - |
+| value | 值 | ReactNode\|string | - |
diff --git a/src/main/frontend/src/components/Trend/demo/basic.md b/src/main/frontend/src/components/Trend/demo/basic.md
new file mode 100644
index 0000000..82afcda
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/demo/basic.md
@@ -0,0 +1,17 @@
+---
+order: 0
+title: 演示
+---
+
+在数值背后添加一个小图标来标识涨跌情况。
+
+````jsx
+import Trend from 'ant-design-pro/lib/Trend';
+
+ReactDOM.render(
+  <div>
+    <Trend flag="up">12%</Trend>
+    <Trend flag="down" style={{ marginLeft: 8 }}>11%</Trend>
+  </div>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Trend/index.d.ts b/src/main/frontend/src/components/Trend/index.d.ts
new file mode 100644
index 0000000..698a49d
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/index.d.ts
@@ -0,0 +1,8 @@
+import * as React from "react";
+
+export interface TrendProps {
+  colorful?: boolean;
+  flag: "up" | "down";
+}
+
+export default class Trend extends React.Component<TrendProps, any> {}
diff --git a/src/main/frontend/src/components/Trend/index.js b/src/main/frontend/src/components/Trend/index.js
new file mode 100644
index 0000000..2cbaad4
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/index.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Icon } from 'antd';
+import classNames from 'classnames';
+import styles from './index.less';
+
+const Trend = ({ colorful = true, flag, children, className, ...rest }) => {
+  const classString = classNames(styles.trendItem, {
+    [styles.trendItemGrey]: !colorful,
+  }, className);
+  return (
+    <div
+      {...rest}
+      className={classString}
+      title={typeof children === 'string' ? children : ''}
+    >
+      <span className={styles.value}>{children}</span>
+      {flag && <span className={styles[flag]}><Icon type={`caret-${flag}`} /></span>}
+    </div>
+  );
+};
+
+export default Trend;
diff --git a/src/main/frontend/src/components/Trend/index.less b/src/main/frontend/src/components/Trend/index.less
new file mode 100644
index 0000000..48695c9
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/index.less
@@ -0,0 +1,30 @@
+@import "~antd/lib/style/themes/default.less";
+
+.trendItem {
+  display: inline-block;
+  font-size: @font-size-base;
+  line-height: 22px;
+
+  .up,
+  .down {
+    margin-left: 4px;
+    position: relative;
+    top: 1px;
+    i {
+      font-size: 12px;
+      transform: scale(0.83);
+    }
+  }
+  .up {
+    color: @red-6;
+  }
+  .down {
+    color: @green-6;
+    top: -1px;
+  }
+
+  &.trendItemGrey .up,
+  &.trendItemGrey .down {
+    color: @text-color;
+  }
+}
diff --git a/src/main/frontend/src/components/Trend/index.md b/src/main/frontend/src/components/Trend/index.md
new file mode 100644
index 0000000..683ed61
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/index.md
@@ -0,0 +1,21 @@
+---
+title: 
+  en-US: Trend
+  zh-CN: Trend
+subtitle: 趋势标记
+cols: 1
+order: 14
+---
+
+趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。
+
+## API
+
+```html
+<Trend flag="up">50%</Trend>
+```
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| colorful | 是否彩色标记 | Boolean | true |
+| flag | 上升下降标识:`up|down` | string | - |
diff --git a/src/main/frontend/src/layouts/BasicLayout.js b/src/main/frontend/src/layouts/BasicLayout.js
index 6eab185..457b2cb 100644
--- a/src/main/frontend/src/layouts/BasicLayout.js
+++ b/src/main/frontend/src/layouts/BasicLayout.js
@@ -66,9 +66,6 @@ class BasicLayout extends React.PureComponent {
     return { location, breadcrumbNameMap };
   }
   componentDidMount() {
-    this.props.dispatch({
-      type: 'user/fetchCurrent',
-    });
   }
   componentWillUnmount() {
     clearTimeout(this.resizeTimeout);
diff --git a/src/main/frontend/src/routes/Dashboard/Dashboard.js b/src/main/frontend/src/routes/Dashboard/Dashboard.js
index 353eca8..9a5dbae 100644
--- a/src/main/frontend/src/routes/Dashboard/Dashboard.js
+++ b/src/main/frontend/src/routes/Dashboard/Dashboard.js
@@ -1,13 +1,246 @@
 import React, { PureComponent } from 'react';
 import { connect } from 'dva';
+import { Row, Col, Icon, Tooltip, Card, Table } from 'antd';
+import moment from 'moment';
+import numeral from 'numeral';
+import {
+  ChartCard, Pie, MiniArea, MiniBar, MiniProgress, Field,
+} from '../../components/Charts';
+import Trend from '../../components/Trend';
+
+import styles from './Dashboard.less';
 
 @connect(state => ({
   dashboard: state.dashboard,
 }))
 export default class Dashboard extends PureComponent {
   render() {
+    const visitData = [];
+    const beginDay = new Date().getTime();
+
+    const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
+    for (let i = 0; i < fakeY.length; i += 1) {
+      visitData.push({
+        x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+        y: fakeY[i],
+      });
+    }
+    const databasePieData = [
+      {
+        x: 'MySQL',
+        y: 10,
+      },
+      {
+        x: 'Oracle',
+        y: 7,
+      },
+      {
+        x: 'SQLServer',
+        y: 3,
+      },
+    ];
+    const tableColumns = [{
+      title: 'Time',
+      dataIndex: 'time',
+      key: 'time',
+    }, {
+      title: 'Name',
+      dataIndex: 'name',
+      key: 'name',
+    }, {
+      title: 'Duration',
+      dataIndex: 'duration',
+      key: 'duration',
+    }];
+
+    const slowServiceData = [{
+      key: '1',
+      name: 'ServiceA',
+      time: '2017/12/11 19:22:32',
+      duration: '5000ms',
+    }, {
+      key: '1',
+      name: 'ServiceA',
+      time: '2017/12/11 19:22:32',
+      duration: '5000ms',
+    }, {
+      key: '1',
+      name: 'ServiceA',
+      time: '2017/12/11 19:22:32',
+      duration: '5000ms',
+    }, {
+      key: '1',
+      name: 'ServiceA',
+      time: '2017/12/11 19:22:32',
+      duration: '5000ms',
+    }, {
+      key: '1',
+      name: 'ServiceA',
+      time: '2017/12/11 19:22:32',
+      duration: '5000ms',
+    }];
+
+    const applicationThroughputColumns = [{
+      title: 'Name',
+      dataIndex: 'name',
+      key: 'name',
+    }, {
+      title: 'Tps',
+      dataIndex: 'tps',
+      key: 'tps',
+    }];
+
+    const applicationThroughputData = [{
+      key: '1',
+      name: 'App1',
+      tps: '500',
+    }, {
+      key: '1',
+      name: 'App1',
+      tps: '500',
+    }, {
+      key: '1',
+      name: 'App1',
+      tps: '500',
+    }, {
+      key: '1',
+      name: 'App1',
+      tps: '500',
+    }, {
+      key: '1',
+      name: 'App1',
+      tps: '500',
+    }];
+
+    const topColResponsiveProps = {
+      xs: 24,
+      sm: 12,
+      md: 12,
+      lg: 6,
+      xl: 6,
+      style: { marginBottom: 24 },
+    };
+    const middleColResponsiveProps = {
+      xs: 24,
+      sm: 24,
+      md: 24,
+      lg: 8,
+      xl: 8,
+      style: { marginBottom: 24, marginTop: 24 },
+    };
     return (
-      <div>test</div>
+      <div>
+        <Row gutter={24}>
+          <Col {...topColResponsiveProps}>
+            <ChartCard
+              title="Total Application"
+              avatar={<img style={{ width: 56, height: 56 }} src="app.svg" alt="app" />}
+              action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+              total={25}
+            />
+          </Col>
+          <Col {...topColResponsiveProps}>
+            <ChartCard
+              title="Total Service"
+              avatar={<img style={{ width: 56, height: 56 }} src="service.svg" alt="service" />}
+              action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+              total={525}
+            />
+          </Col>
+          <Col {...topColResponsiveProps}>
+            <ChartCard
+              title="Total Database"
+              avatar={<img style={{ width: 56, height: 56 }} src="database.svg" alt="database" />}
+              action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+              total={18}
+            />
+          </Col>
+          <Col {...topColResponsiveProps}>
+            <ChartCard
+              title="Total Cache"
+              avatar={<img style={{ width: 56, height: 56 }} src="redis.svg" alt="redis" />}
+              action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+              total={5}
+            />
+          </Col>
+        </Row>
+        <Card
+          bordered={false}
+          bodyStyle={{ padding: 0 }}
+        >
+          <div Style="height: 400px">Topoloy</div>
+        </Card>
+        <Row gutter={24}>
+          <Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: 24 }}>
+            <ChartCard
+              title="Avg Application Alert"
+              avatar={<img style={{ width: 56, height: 56 }} src="alert.svg" alt="app" />}
+              action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+              total="5%"
+              footer={<div><Field label="Max" value="10%" /> <Field label="Min" value="2%" /></div>}
+            >
+              <MiniArea
+                color="#D87093"
+                borderColor="#B22222"
+                line="true"
+                height={196}
+                data={visitData}
+                yAxis={{
+                  formatter(val) {
+                      return `${val} %`;
+                  },
+                }}
+              />
+            </ChartCard>
+          </Col>
+        </Row>
+        <Row gutter={24}>
+          <Col {...middleColResponsiveProps}>
+            <Card
+              bordered={false}
+              bodyStyle={{ padding: 0 }}
+            >
+              <Pie
+                hasLegend
+                title="Database"
+                subTitle="Total"
+                total={databasePieData.reduce((pre, now) => now.y + pre, 0)}
+                data={databasePieData}
+                height={300}
+                lineWidth={4}
+              />
+            </Card>
+          </Col>
+          <Col {...middleColResponsiveProps}>
+            <Card
+              title="Slow Service"
+              bordered={false}
+              bodyStyle={{ padding: 0 }}
+            >
+              <Table
+                columns={tableColumns}
+                dataSource={slowServiceData}
+                pagination={{
+                  style: { marginBottom: 0 },
+                  pageSize: 5,
+                }}
+              />
+            </Card>
+          </Col>
+          <Col {...middleColResponsiveProps}>
+            <Card
+              title="Application Throughput"
+              bordered={false}
+              bodyStyle={{ padding: 0 }}
+            >
+              <Table
+                columns={applicationThroughputColumns}
+                dataSource={applicationThroughputData}
+              />
+            </Card>
+          </Col>
+        </Row>
+      </div>
     );
   }
 }
diff --git a/src/main/frontend/src/routes/Dashboard/Dashboard.less b/src/main/frontend/src/routes/Dashboard/Dashboard.less
index e52dd30..8e13f67 100644
--- a/src/main/frontend/src/routes/Dashboard/Dashboard.less
+++ b/src/main/frontend/src/routes/Dashboard/Dashboard.less
@@ -21,3 +21,8 @@
     height: auto;
   }
 }
+
+.trendText {
+  margin-left: 8px;
+  color: @heading-color;
+}

-- 
To stop receiving notification emails like this one, please contact
"commits@skywalking.apache.org" <co...@skywalking.apache.org>.