You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2020/06/22 08:27:21 UTC

[cloudstack-primate] branch master updated: primate: Add support for UI customisation (#372)

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

rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git


The following commit(s) were added to refs/heads/master by this push:
     new 4398032  primate: Add support for UI customisation (#372)
4398032 is described below

commit 439803245c0082736da9f6e89ccb563862d373b5
Author: Hoang Nguyen <ho...@unitech.vn>
AuthorDate: Mon Jun 22 15:27:14 2020 +0700

    primate: Add support for UI customisation (#372)
    
    - New config.json global config file
    - Customisation: API endpoint, app name, doc link, logo, error and banner images, theme
    - Basic external plugin support to allow users to write UI plugins in any framework, build and import/plug a html file as integration
    
    Signed-off-by: Rohit Yadav <ro...@shapeblue.com>
    Co-authored-by: Rohit Yadav <ro...@shapeblue.com>
---
 docs/customize.md                               |  77 ++++++++++++++++++
 package-lock.json                               | 103 +++++++++++++++++++-----
 package.json                                    |   1 +
 {src => public}/assets/403.png                  | Bin
 {src => public}/assets/404.png                  | Bin
 {src => public}/assets/500.png                  | Bin
 {src => public}/assets/banner.svg               |   0
 {src => public}/assets/error.png                | Bin
 {src => public}/assets/logo.svg                 |   0
 {src => public}/assets/success.png              | Bin
 public/config.json                              |  47 +++++++++++
 public/example.html                             |  12 +++
 src/App.vue                                     |   8 +-
 src/components/header/Logo.vue                  |  10 ++-
 src/components/header/UserMenu.vue              |   8 +-
 src/components/widgets/Breadcrumb.vue           |   6 +-
 src/config/router.js                            |  18 ++++-
 src/config/settings.js                          |   4 -
 src/core/use.js                                 |   1 +
 src/layouts/UserLayout.vue                      |  12 ++-
 src/main.js                                     |  20 +++--
 src/permission.js                               |   4 +-
 src/{App.vue => style/vars.less}                |  46 +++++------
 src/utils/domUtil.js                            |   4 -
 src/utils/request.js                            |   2 -
 src/views/AutogenView.vue                       |   4 +-
 src/views/auth/Login.vue                        |   3 +-
 src/views/compute/DeployVM.vue                  |  37 +--------
 src/views/exception/ExceptionPage.vue           |  29 +++++--
 src/views/image/RegisterOrUploadTemplate.vue    |  26 ++----
 src/{App.vue => views/plugins/IFramePlugin.vue} |  26 ++----
 theme.config.js                                 |  50 ++++++++++++
 vue.config.js                                   |  11 ++-
 33 files changed, 394 insertions(+), 175 deletions(-)

diff --git a/docs/customize.md b/docs/customize.md
new file mode 100644
index 0000000..09a86a1
--- /dev/null
+++ b/docs/customize.md
@@ -0,0 +1,77 @@
+# UI customization
+Use a `public/config.json` (or `dist/config.json` after build) file for customizing theme, logos,...
+
+## Images
+Change the image of the logo, login banner, error page, etc.
+```json
+{
+  "logo": "assets/logo.svg",
+  "banner": "assets/banner.svg",
+  "error": {
+    "404": "assets/404.png",
+    "403": "assets/403.png",
+    "500": "assets/500.png"
+  }
+}
+```
+
+- `logo` changes the logo top-left side image.
+- `banner` changes the login banner image.
+- `error.404` change the image of error Page not found.
+- `error.403` change the image of error Forbidden.
+- `error.500` change the image of error Internal Server Error.
+
+## Theme
+Customize themes like colors, border color, etc.
+```json
+{
+  "theme": {
+    "@primary-color": "#1890ff",
+    "@success-color": "#52c41a",
+    "@processing-color": "#1890ff",
+    "@warning-color": "#faad14",
+    "@error-color": "#f5222d",
+    "@font-size-base": "14px",
+    "@heading-color": "rgba(0, 0, 0, 0.85)",
+    "@text-color": "rgba(0, 0, 0, 0.65)",
+    "@text-color-secondary": "rgba(0, 0, 0, 0.45)",
+    "@disabled-color": "rgba(0, 0, 0, 0.25)",
+    "@border-color-base": "#d9d9d9",
+    "@logo-width": "256px",
+    "@logo-height": "64px",
+    "@banner-width": "700px",
+    "@banner-height": "110px",
+    "@error-width": "256px",
+    "@error-height": "256px"
+  }
+}
+```
+
+- `@primary-color` change the major background color of the page (background button, icon hover, etc).
+- `@success-color` change success state color.
+- `@processing-color` change processing state color. Exp: progress status.
+- `@warning-color` change warning state color.
+- `@error-color` change error state color.
+- `@heading-color` change table header color.
+- `@text-color` change in major text color.
+- `@text-color-secondary` change of secondary text color (breadcrumb icon).
+- `@disabled-color` change disable state color (disabled button, switch, etc).
+- `@border-color-base` change in major border color.
+- `@logo-width` change the width of the logo top-left side.
+- `@logo-height` change the height of the logo top-left side.
+- `@banner-width` changes the width of the login banner.
+- `@banner-height` changes the height of the login banner.
+- `@error-width` changes the width of the error image.
+- `@error-height` changes the height of the error image.
+
+Assorted primary theme colours:
+
+- Blue: #1890FF
+- Red: #F5222D
+- Yellow: #FAAD14
+- Cyan: #13C2C2
+- Green: #52C41A
+- Purple: #722ED1
+
+Also, to add other properties, we can add new properties into `theme.config.js` based on the Ant Design Vue Less variable.
+Refer: https://www.antdv.com/docs/vue/customize-theme/#Ant-Design-Vue-Less-variables
diff --git a/package-lock.json b/package-lock.json
index 7f356ff..9855867 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6767,7 +6767,6 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
       "requires": {
         "color-convert": "^1.9.0"
       }
@@ -6814,6 +6813,44 @@
         "warning": "^4.0.0"
       }
     },
+    "antd-theme-generator": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/antd-theme-generator/-/antd-theme-generator-1.2.4.tgz",
+      "integrity": "sha512-27HCj4NTpbQZGNkz1Ip7RF1p85iSN4izf5rY6rQvytM2shvve9qVLnIwHGdNWvsMPrgOPH5wlu8bauKnh7+6dg==",
+      "requires": {
+        "glob": "^7.1.3",
+        "hash.js": "^1.1.5",
+        "less": "^3.9.0",
+        "less-plugin-npm-import": "^2.1.0",
+        "postcss": "^6.0.21",
+        "strip-css-comments": "^4.1.0"
+      },
+      "dependencies": {
+        "postcss": {
+          "version": "6.0.23",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+          "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+          "requires": {
+            "chalk": "^2.4.1",
+            "source-map": "^0.6.1",
+            "supports-color": "^5.4.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+        }
+      }
+    },
+    "antd-theme-webpack-plugin": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/antd-theme-webpack-plugin/-/antd-theme-webpack-plugin-1.3.6.tgz",
+      "integrity": "sha512-3cxWiblpZYbkZoghQODjcejQhx0hTu8aSOilWH2nZFtdOoa0ZFXT6u60uzghZiwqFuYqgv0ylMfYGElOmTUdiw==",
+      "requires": {
+        "antd-theme-generator": "^1.2.4"
+      }
+    },
     "any-observable": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
@@ -9547,7 +9584,6 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
       "requires": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -10256,7 +10292,6 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
       "requires": {
         "color-name": "1.1.3"
       }
@@ -10264,8 +10299,7 @@
     "color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
-      "dev": true
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
     },
     "color-string": {
       "version": "1.5.3",
@@ -12400,7 +12434,6 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
       "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
-      "dev": true,
       "requires": {
         "prr": "~1.0.1"
       }
@@ -14680,8 +14713,7 @@
     "has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
-      "dev": true
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
     },
     "has-symbol-support-x": {
       "version": "1.4.2",
@@ -14765,7 +14797,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
       "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
-      "dev": true,
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-assert": "^1.0.1"
@@ -15253,7 +15284,6 @@
       "version": "0.5.5",
       "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
       "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
-      "dev": true,
       "optional": true
     },
     "import-cwd": {
@@ -15894,6 +15924,11 @@
         "has": "^1.0.3"
       }
     },
+    "is-regexp": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz",
+      "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA=="
+    },
     "is-resolvable": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
@@ -17403,7 +17438,6 @@
       "version": "3.11.1",
       "resolved": "https://registry.npmjs.org/less/-/less-3.11.1.tgz",
       "integrity": "sha512-tlWX341RECuTOvoDIvtFqXsKj072hm3+9ymRBe76/mD6O5ZZecnlAOVDlWAleF2+aohFrxNidXhv2773f6kY7g==",
-      "dev": true,
       "requires": {
         "clone": "^2.1.2",
         "errno": "^0.1.1",
@@ -17420,14 +17454,12 @@
         "clone": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
-          "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
-          "dev": true
+          "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
         },
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
           "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true,
           "optional": true
         }
       }
@@ -17457,6 +17489,30 @@
         }
       }
     },
+    "less-plugin-npm-import": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/less-plugin-npm-import/-/less-plugin-npm-import-2.1.0.tgz",
+      "integrity": "sha1-gj5phskzGKmBccqFiEi2vq1Vvz4=",
+      "requires": {
+        "promise": "~7.0.1",
+        "resolve": "~1.1.6"
+      },
+      "dependencies": {
+        "promise": {
+          "version": "7.0.4",
+          "resolved": "https://registry.npmjs.org/promise/-/promise-7.0.4.tgz",
+          "integrity": "sha1-Nj6EpMNsg1a4kP7WLJHOhdAu1Tk=",
+          "requires": {
+            "asap": "~2.0.3"
+          }
+        },
+        "resolve": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+          "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
+        }
+      }
+    },
     "leven": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -18397,8 +18453,7 @@
     "mime": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
-      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
-      "dev": true
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
     },
     "mime-db": {
       "version": "1.40.0",
@@ -18480,8 +18535,7 @@
     "minimalistic-assert": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
-      "dev": true
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
     },
     "minimalistic-crypto-utils": {
       "version": "1.0.1",
@@ -21428,7 +21482,6 @@
       "version": "7.3.1",
       "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
       "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
-      "dev": true,
       "optional": true,
       "requires": {
         "asap": "~2.0.3"
@@ -21477,8 +21530,7 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
-      "dev": true
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
     },
     "ps-list": {
       "version": "4.1.0",
@@ -23989,6 +24041,14 @@
       "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
       "dev": true
     },
+    "strip-css-comments": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/strip-css-comments/-/strip-css-comments-4.1.0.tgz",
+      "integrity": "sha512-azjRwrqk7nK21LU7QuL7DpDyPjvRROQvqPrNyyz6emdzbOh6fsNTvkSvUiThBLzC6+MN90rFu296VbPb/KV+3A==",
+      "requires": {
+        "is-regexp": "^2.1.0"
+      }
+    },
     "strip-dirs": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz",
@@ -24097,7 +24157,6 @@
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
       "requires": {
         "has-flag": "^3.0.0"
       }
diff --git a/package.json b/package.json
index 745e568..5d55c58 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
     "@fortawesome/free-solid-svg-icons": "^5.13.0",
     "@fortawesome/vue-fontawesome": "^0.1.9",
     "ant-design-vue": "~1.6.2",
+    "antd-theme-webpack-plugin": "^1.3.4",
     "axios": "^0.19.2",
     "core-js": "^3.6.5",
     "enquire.js": "^2.1.6",
diff --git a/src/assets/403.png b/public/assets/403.png
similarity index 100%
rename from src/assets/403.png
rename to public/assets/403.png
diff --git a/src/assets/404.png b/public/assets/404.png
similarity index 100%
rename from src/assets/404.png
rename to public/assets/404.png
diff --git a/src/assets/500.png b/public/assets/500.png
similarity index 100%
rename from src/assets/500.png
rename to public/assets/500.png
diff --git a/src/assets/banner.svg b/public/assets/banner.svg
similarity index 100%
rename from src/assets/banner.svg
rename to public/assets/banner.svg
diff --git a/src/assets/error.png b/public/assets/error.png
similarity index 100%
rename from src/assets/error.png
rename to public/assets/error.png
diff --git a/src/assets/logo.svg b/public/assets/logo.svg
similarity index 100%
rename from src/assets/logo.svg
rename to public/assets/logo.svg
diff --git a/src/assets/success.png b/public/assets/success.png
similarity index 100%
rename from src/assets/success.png
rename to public/assets/success.png
diff --git a/public/config.json b/public/config.json
new file mode 100644
index 0000000..f36e77c
--- /dev/null
+++ b/public/config.json
@@ -0,0 +1,47 @@
+{
+  "apiBase": "/client/api",
+  "docBase": "http://docs.cloudstack.apache.org/en/latest",
+  "appTitle": "CloudStack",
+  "logo": "assets/logo.svg",
+  "banner": "assets/banner.svg",
+  "error": {
+    "404": "assets/404.png",
+    "403": "assets/403.png",
+    "500": "assets/500.png"
+  },
+  "theme": {
+    "@primary-color": "#1890ff",
+    "@processing-color": "#1890ff",
+    "@success-color": "#52c41a",
+    "@warning-color": "#faad14",
+    "@error-color": "#f5222d",
+    "@font-size-base": "14px",
+    "@heading-color": "rgba(0, 0, 0, 0.85)",
+    "@text-color": "rgba(0, 0, 0, 0.65)",
+    "@text-color-secondary": "rgba(0, 0, 0, 0.45)",
+    "@disabled-color": "rgba(0, 0, 0, 0.25)",
+    "@border-color-base": "#d9d9d9",
+    "@border-radius-base": "4px",
+    "@box-shadow-base": "0 2px 8px rgba(0, 0, 0, 0.15)",
+    "@logo-width": "256px",
+    "@logo-height": "64px",
+    "@login-banner-width": "700px",
+    "@login-banner-height": "110px",
+    "@error-width": "256px",
+    "@error-height": "256px"
+  },
+  "keyboardOptions": {
+    "us": "label.standard.us.keyboard",
+    "uk": "label.uk.keyboard",
+    "fr": "label.french.azerty.keyboard",
+    "jp": "label.japanese.keyboard",
+    "sc": "label.simplified.chinese.keyboard"
+  },
+  "plugins": [
+    {
+      "name": "ExamplePlugin",
+      "icon": "appstore",
+      "path": "example.html"
+    }
+  ]
+}
diff --git a/public/example.html b/public/example.html
new file mode 100644
index 0000000..594baf1
--- /dev/null
+++ b/public/example.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en-gb">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <title>Example Plugin</title>
+  </head>
+  <body>
+    This is an example iframe plugin, please configure the config.json to remove this in production environment.
+  </body>
+</html>
diff --git a/src/App.vue b/src/App.vue
index f21947b..fa75a8e 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -31,11 +31,13 @@ export default {
   mixins: [AppDeviceEnquire],
   data () {
     return {
-      locale: enUS
+      locale: enUS,
+      configs: {}
     }
   },
-  mounted () {
-
+  created () {
+    window.less.modifyVars(this.$config.theme)
+    console.log('config and theme applied')
   }
 }
 </script>
diff --git a/src/components/header/Logo.vue b/src/components/header/Logo.vue
index 37e1e9c..ba879e4 100644
--- a/src/components/header/Logo.vue
+++ b/src/components/header/Logo.vue
@@ -17,7 +17,14 @@
 
 <template>
   <div class="logo">
-    <img class="logo-image" src="~@/assets/logo.svg"/>
+    <img
+      v-if="$config.logo"
+      :style="{
+        width: $config.theme['@logo-width'],
+        height: $config.theme['@logo-height']
+      }"
+      :src="$config.logo"
+      class="logo-image" />
   </div>
 </template>
 
@@ -59,7 +66,6 @@ export default {
 }
 
 .logo-image {
-  width: 256px;
   display: inline-block;
   vertical-align: middle;
 }
diff --git a/src/components/header/UserMenu.vue b/src/components/header/UserMenu.vue
index b697f29..b30110a 100644
--- a/src/components/header/UserMenu.vue
+++ b/src/components/header/UserMenu.vue
@@ -33,7 +33,7 @@
           </router-link>
         </a-menu-item>
         <a-menu-item class="user-menu-item" key="1" disabled>
-          <a :href="docBase" target="_blank">
+          <a :href="$config.docBase" target="_blank">
             <a-icon class="user-menu-item-icon" type="question-circle-o"></a-icon>
             <span class="user-menu-item-name">{{ $t('label.help') }}</span>
           </a>
@@ -51,7 +51,6 @@
 </template>
 
 <script>
-import config from '@/config/settings'
 import HeaderNotice from './HeaderNotice'
 import TranslationMenu from './TranslationMenu'
 import { mapActions, mapGetters } from 'vuex'
@@ -62,11 +61,6 @@ export default {
     TranslationMenu,
     HeaderNotice
   },
-  data () {
-    return {
-      docBase: config.docBase
-    }
-  },
   methods: {
     ...mapActions(['Logout']),
     ...mapGetters(['nickname', 'avatar']),
diff --git a/src/components/widgets/Breadcrumb.vue b/src/components/widgets/Breadcrumb.vue
index c13c0ea..053bc3c 100644
--- a/src/components/widgets/Breadcrumb.vue
+++ b/src/components/widgets/Breadcrumb.vue
@@ -39,7 +39,7 @@
           <a
             v-if="item.meta.docHelp"
             style="margin-right: 12px"
-            :href="docBase + '/' + $route.meta.docHelp"
+            :href="$config.docBase + '/' + $route.meta.docHelp"
             target="_blank">
             <a-icon type="question-circle-o"></a-icon>
           </a>
@@ -52,7 +52,6 @@
 </template>
 
 <script>
-import config from '@/config/settings'
 
 export default {
   name: 'Breadcrumb',
@@ -67,8 +66,7 @@ export default {
   data () {
     return {
       name: '',
-      breadList: [],
-      docBase: config.docBase
+      breadList: []
     }
   },
   created () {
diff --git a/src/config/router.js b/src/config/router.js
index 672764e..cad2aee 100644
--- a/src/config/router.js
+++ b/src/config/router.js
@@ -18,6 +18,8 @@
 // eslint-disable-next-line
 import { UserLayout, BasicLayout, RouteView, BlankLayout, PageView } from '@/layouts'
 import AutogenView from '@/views/AutogenView.vue'
+import IFramePlugin from '@/views/plugins/IFramePlugin.vue'
+import Vue from 'vue'
 
 import compute from '@/config/section/compute'
 import storage from '@/config/section/storage'
@@ -167,7 +169,7 @@ function generateRouterMap (section) {
 }
 
 export function asyncRouterMap () {
-  return [{
+  const routerMap = [{
     path: '/',
     name: 'index',
     component: BasicLayout,
@@ -255,6 +257,20 @@ export function asyncRouterMap () {
   {
     path: '*', redirect: '/exception/404', hidden: true
   }]
+
+  const plugins = Vue.prototype.$config.plugins
+  if (plugins && plugins.length > 0) {
+    plugins.map(plugin => {
+      routerMap[0].children.push({
+        path: '/plugins/' + plugin.name,
+        name: plugin.name,
+        component: IFramePlugin,
+        meta: { title: plugin.name, icon: plugin.icon, path: plugin.path }
+      })
+    })
+  }
+
+  return routerMap
 }
 
 export const constantRouterMap = [
diff --git a/src/config/settings.js b/src/config/settings.js
index 95c3225..6e599e7 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -25,10 +25,6 @@ export default {
   autoHideHeader: false, //  auto hide header
   invertedMode: true,
   multiTab: false, // enable to have tab/route history stuff
-  // CloudStack options
-  apiBase: '/client/api',
-  docBase: 'http://docs.cloudstack.apache.org/en/latest',
-  appTitle: 'CloudStack',
   // vue-ls options
   storageOptions: {
     namespace: 'primate__', // key prefix
diff --git a/src/core/use.js b/src/core/use.js
index 014d06e..89c4f2e 100644
--- a/src/core/use.js
+++ b/src/core/use.js
@@ -24,6 +24,7 @@ import Antd from 'ant-design-vue'
 import Viser from 'viser-vue'
 import VueCropper from 'vue-cropper'
 import 'ant-design-vue/dist/antd.less'
+import '@/style/vars.less'
 
 // ext library
 import VueClipboard from 'vue-clipboard2'
diff --git a/src/layouts/UserLayout.vue b/src/layouts/UserLayout.vue
index 61ce2f3..f5bb5a4 100644
--- a/src/layouts/UserLayout.vue
+++ b/src/layouts/UserLayout.vue
@@ -19,7 +19,15 @@
   <div id="userLayout" :class="['user-layout', device]">
     <div class="user-layout-container">
       <div class="user-layout-header">
-        <img src="~@/assets/banner.svg" class="user-layout-logo" alt="logo">
+        <img
+          v-if="$config.banner"
+          :style="{
+            width: $config.theme['@banner-width'],
+            height: $config.theme['@banner-height']
+          }"
+          :src="$config.banner"
+          class="user-layout-logo"
+          alt="logo">
       </div>
       <route-view></route-view>
     </div>
@@ -65,8 +73,6 @@ export default {
   }
 
   &-logo {
-    width: 95%;
-    max-width: 450px;
     border-style: none;
     margin: 0 auto 2rem;
     display: block;
diff --git a/src/main.js b/src/main.js
index 267a0c4..11ee548 100644
--- a/src/main.js
+++ b/src/main.js
@@ -20,7 +20,6 @@ import App from './App.vue'
 import router from './router'
 import store from './store'
 import i18n from './locales'
-import { VueAxios } from './utils/request'
 
 import bootstrap from './core/bootstrap'
 import './core/use'
@@ -28,16 +27,21 @@ import './core/ext'
 import './permission' // permission control
 import './utils/filter' // global filter
 import { pollJobPlugin, notifierPlugin } from './utils/plugins'
+import { VueAxios } from './utils/request'
 
 Vue.config.productionTip = false
 Vue.use(VueAxios, router)
 Vue.use(pollJobPlugin)
 Vue.use(notifierPlugin)
 
-new Vue({
-  router,
-  store,
-  i18n,
-  created: bootstrap,
-  render: h => h(App)
-}).$mount('#app')
+fetch('config.json').then(response => response.json()).then(config => {
+  Vue.prototype.$config = config
+  Vue.axios.defaults.baseURL = config.apiBase
+  new Vue({
+    router,
+    store,
+    i18n,
+    created: bootstrap,
+    render: h => h(App)
+  }).$mount('#app')
+})
diff --git a/src/permission.js b/src/permission.js
index 4d37e0d..65fcb2b 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -25,7 +25,7 @@ import NProgress from 'nprogress' // progress bar
 import 'nprogress/nprogress.css' // progress bar style
 import message from 'ant-design-vue/es/message'
 import notification from 'ant-design-vue/es/notification'
-import { setDocumentTitle, domTitle } from '@/utils/domUtil'
+import { setDocumentTitle } from '@/utils/domUtil'
 import { ACCESS_TOKEN, APIS } from '@/store/mutation-types'
 
 NProgress.configure({ showSpinner: false }) // NProgress Configuration
@@ -36,7 +36,7 @@ router.beforeEach((to, from, next) => {
   // start progress bar
   NProgress.start()
   if (to.meta && typeof to.meta.title !== 'undefined') {
-    const title = i18n.t(to.meta.title) + ' - ' + domTitle
+    const title = i18n.t(to.meta.title) + ' - ' + Vue.prototype.$config.appTitle
     setDocumentTitle(title)
   }
   const validLogin = Vue.ls.get(ACCESS_TOKEN) || Cookies.get('userid') || Cookies.get('userid', { path: '/client' })
diff --git a/src/App.vue b/src/style/vars.less
similarity index 64%
copy from src/App.vue
copy to src/style/vars.less
index f21947b..3d6845f 100644
--- a/src/App.vue
+++ b/src/style/vars.less
@@ -15,32 +15,32 @@
 // specific language governing permissions and limitations
 // under the License.
 
-<template>
-  <a-config-provider :locale="locale">
-    <div id="app">
-      <router-view/>
-    </div>
-  </a-config-provider>
-</template>
+@import "~ant-design-vue/lib/style/themes/default.less";
 
-<script>
-import enUS from 'ant-design-vue/lib/locale-provider/en_US'
-import { AppDeviceEnquire } from '@/utils/mixin'
+@logo-width: 256px;
+@logo-height: 64px;
+@banner-width: 450px;
+@banner-height: 110px;
+@error-width: 256px;
+@error-height: 256px;
 
-export default {
-  mixins: [AppDeviceEnquire],
-  data () {
-    return {
-      locale: enUS
-    }
-  },
-  mounted () {
+.ant-layout-sider-children .logo-image {
+  width: @logo-width;
+  height: @logo-height;
+}
 
+.user-layout {
+  &-logo {
+    width: @banner-width;
+    height: @banner-height;
   }
 }
-</script>
-<style>
-#app {
-  height: 100%;
+
+.exception {
+  .img {
+    img {
+      width: @error-width;
+      height: @error-height;
+    }
+  }
 }
-</style>
diff --git a/src/utils/domUtil.js b/src/utils/domUtil.js
index 50ab68c..898bc59 100644
--- a/src/utils/domUtil.js
+++ b/src/utils/domUtil.js
@@ -15,8 +15,6 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import config from '@/config/settings'
-
 export const setDocumentTitle = function (title) {
   document.title = title
   const ua = navigator.userAgent
@@ -34,5 +32,3 @@ export const setDocumentTitle = function (title) {
     document.body.appendChild(i)
   }
 }
-
-export const domTitle = config.appTitle
diff --git a/src/utils/request.js b/src/utils/request.js
index 1131a6e..9dd0217 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -17,7 +17,6 @@
 
 import Vue from 'vue'
 import axios from 'axios'
-import config from '@/config/settings'
 import router from '@/router'
 import store from '@/store'
 import { VueAxios } from './axios'
@@ -25,7 +24,6 @@ import notification from 'ant-design-vue/es/notification'
 import { CURRENT_PROJECT } from '@/store/mutation-types'
 
 const service = axios.create({
-  baseURL: config.apiBase,
   timeout: 600000
 })
 
diff --git a/src/views/AutogenView.vue b/src/views/AutogenView.vue
index 9f87310..029f92b 100644
--- a/src/views/AutogenView.vue
+++ b/src/views/AutogenView.vue
@@ -118,7 +118,7 @@
           <a
             v-if="currentAction.docHelp || $route.meta.docHelp"
             style="margin-left: 5px"
-            :href="docBase + '/' + (currentAction.docHelp || $route.meta.docHelp)"
+            :href="$config.docBase + '/' + (currentAction.docHelp || $route.meta.docHelp)"
             target="_blank">
             <a-icon type="question-circle-o"></a-icon>
           </a>
@@ -304,7 +304,6 @@
 import { api } from '@/api'
 import { mixinDevice } from '@/utils/mixin.js'
 import { genericCompare } from '@/utils/sort.js'
-import config from '@/config/settings'
 import store from '@/store'
 
 import Breadcrumb from '@/components/widgets/Breadcrumb'
@@ -336,7 +335,6 @@ export default {
   data () {
     return {
       apiName: '',
-      docBase: config.docBase,
       loading: false,
       actionLoading: false,
       columns: [],
diff --git a/src/views/auth/Login.vue b/src/views/auth/Login.vue
index 0434f33..1421fc2 100644
--- a/src/views/auth/Login.vue
+++ b/src/views/auth/Login.vue
@@ -111,7 +111,6 @@
 <script>
 import { api } from '@/api'
 import { mapActions } from 'vuex'
-import config from '@/config/settings'
 import TranslationMenu from '@/components/header/TranslationMenu'
 
 export default {
@@ -195,7 +194,7 @@ export default {
               })
           } else if (customActiveKey === 'saml') {
             state.loginBtn = false
-            var samlUrl = config.apiBase + '?command=samlSso'
+            var samlUrl = this.$config.apiBase + '?command=samlSso'
             if (values.idp) {
               samlUrl += ('&idpid=' + values.idp)
             }
diff --git a/src/views/compute/DeployVM.vue b/src/views/compute/DeployVM.vue
index 2446516..ce7084b 100644
--- a/src/views/compute/DeployVM.vue
+++ b/src/views/compute/DeployVM.vue
@@ -683,10 +683,11 @@ export default {
       return options
     },
     keyboardSelectOptions () {
-      return this.options.keyboards.map((keyboard) => {
+      const keyboardOpts = this.$config.keyboardOptions || {}
+      return Object.keys(keyboardOpts).map((keyboard) => {
         return {
-          label: this.$t(keyboard.description),
-          value: keyboard.id
+          label: this.$t(keyboardOpts[keyboard]),
+          value: keyboard
         }
       })
     }
@@ -805,7 +806,6 @@ export default {
         })
       }
 
-      this.fetchKeyboard()
       this.fetchBootTypes()
       this.fetchBootModes()
       Vue.nextTick().then(() => {
@@ -826,35 +826,6 @@ export default {
       })
       await this.fetchAllTemplates()
     },
-    fetchKeyboard () {
-      const keyboardType = []
-      keyboardType.push({
-        id: '',
-        description: ''
-      })
-      keyboardType.push({
-        id: 'us',
-        description: 'label.standard.us.keyboard'
-      })
-      keyboardType.push({
-        id: 'uk',
-        description: 'label.uk.keyboard'
-      })
-      keyboardType.push({
-        id: 'fr',
-        description: 'label.french.azerty.keyboard'
-      })
-      keyboardType.push({
-        id: 'jp',
-        description: 'label.japanese.keyboard'
-      })
-      keyboardType.push({
-        id: 'sc',
-        description: 'label.simplified.chinese.keyboard'
-      })
-
-      this.$set(this.options, 'keyboards', keyboardType)
-    },
     fetchBootTypes () {
       const bootTypes = []
 
diff --git a/src/views/exception/ExceptionPage.vue b/src/views/exception/ExceptionPage.vue
index be591b0..841d78e 100644
--- a/src/views/exception/ExceptionPage.vue
+++ b/src/views/exception/ExceptionPage.vue
@@ -18,13 +18,31 @@
 <template>
   <div class="exception">
     <div class="img" v-if="type == '403'">
-      <img src="@/assets/403.png"/>
+      <img
+        v-if="$config.error['403']"
+        :src="$config.error['403']"
+        :style="{
+          width: $config.theme['@error-width'],
+          height: $config.theme['@error-height']
+        }"/>
     </div>
     <div class="img" v-if="type == '404'">
-      <img src="@/assets/404.png"/>
+      <img
+        v-if="$config.error['404']"
+        :src="$config.error['404']"
+        :style="{
+          width: $config.theme['@error-width'],
+          height: $config.theme['@error-height']
+        }"/>
     </div>
     <div class="img" v-if="type == '500'">
-      <img src="@/assets/500.png"/>
+      <img
+        v-if="$config.error['500']"
+        :src="$config.error['500']"
+        :style="{
+          width: $config.theme['@error-width'],
+          height: $config.theme['@error-height']
+        }"/>
     </div>
     <div class="content">
       <h1>{{ config[type].title }}</h1>
@@ -71,10 +89,6 @@ export default {
       display: inline-block;
       padding-right: 52px;
       zoom: 1;
-      img {
-        height: 256px;
-        max-width: 256px;
-      }
     }
     .content {
       display: inline-block;
@@ -102,7 +116,6 @@ export default {
         padding-right: unset;
 
         img {
-          height: 40%;
           max-width: 80%;
         }
       }
diff --git a/src/views/image/RegisterOrUploadTemplate.vue b/src/views/image/RegisterOrUploadTemplate.vue
index 45ad66f..a4f90c9 100644
--- a/src/views/image/RegisterOrUploadTemplate.vue
+++ b/src/views/image/RegisterOrUploadTemplate.vue
@@ -653,29 +653,17 @@ export default {
     },
     fetchKeyboardType () {
       const keyboardType = []
+      const keyboardOpts = this.$config.keyboardOptions || {}
       keyboardType.push({
         id: '',
         description: ''
       })
-      keyboardType.push({
-        id: 'us',
-        description: 'label.standard.us.keyboard'
-      })
-      keyboardType.push({
-        id: 'uk',
-        description: 'label.uk.keyboard'
-      })
-      keyboardType.push({
-        id: 'fr',
-        description: 'label.french.azerty.keyboard'
-      })
-      keyboardType.push({
-        id: 'jp',
-        description: 'label.japanese.keyboard'
-      })
-      keyboardType.push({
-        id: 'sc',
-        description: 'label.simplified.chinese.keyboard'
+
+      Object.keys(keyboardOpts).forEach(keyboard => {
+        keyboardType.push({
+          id: keyboard,
+          description: this.$t(keyboardOpts[keyboard])
+        })
       })
 
       this.$set(this.keyboardType, 'opts', keyboardType)
diff --git a/src/App.vue b/src/views/plugins/IFramePlugin.vue
similarity index 70%
copy from src/App.vue
copy to src/views/plugins/IFramePlugin.vue
index f21947b..3e07ba7 100644
--- a/src/App.vue
+++ b/src/views/plugins/IFramePlugin.vue
@@ -16,31 +16,15 @@
 // under the License.
 
 <template>
-  <a-config-provider :locale="locale">
-    <div id="app">
-      <router-view/>
-    </div>
-  </a-config-provider>
+  <div>
+    <iframe :src="$route.meta.path" width="100%" frameBorder="0" style="height: 90vh">
+    </iframe>
+  </div>
 </template>
 
 <script>
-import enUS from 'ant-design-vue/lib/locale-provider/en_US'
-import { AppDeviceEnquire } from '@/utils/mixin'
 
 export default {
-  mixins: [AppDeviceEnquire],
-  data () {
-    return {
-      locale: enUS
-    }
-  },
-  mounted () {
-
-  }
+  name: 'IFramePlugin'
 }
 </script>
-<style>
-#app {
-  height: 100%;
-}
-</style>
diff --git a/theme.config.js b/theme.config.js
new file mode 100644
index 0000000..2462674
--- /dev/null
+++ b/theme.config.js
@@ -0,0 +1,50 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+const path = require('path')
+const AntDesignThemePlugin = require('antd-theme-webpack-plugin')
+
+function resolve (dir) {
+  return path.join(__dirname, dir)
+}
+
+const options = {
+  stylesDir: resolve('./src/style'),
+  antDir: resolve('./node_modules/ant-design-vue'),
+  varFile: resolve('./src/style/vars.less'),
+  themeVariables: [
+    '@primary-color',
+    '@success-color',
+    '@warning-color',
+    '@processing-color',
+    '@error-color',
+    '@heading-color',
+    '@text-color',
+    '@text-color-secondary',
+    '@disabled-color',
+    '@border-color-base',
+    '@border-radius-base',
+    '@box-shadow-base'
+  ],
+  indexFileName: 'index.html',
+  publicPath: '.',
+  generateOnce: false
+}
+
+const createThemeColorReplacerPlugin = () => new AntDesignThemePlugin(options)
+
+module.exports = createThemeColorReplacerPlugin
diff --git a/vue.config.js b/vue.config.js
index 4421a02..e92841a 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -20,13 +20,14 @@ const webpack = require('webpack')
 const fs = require('fs')
 const packageJson = fs.readFileSync('./package.json')
 const version = JSON.parse(packageJson).version || 'master'
+const createThemeColorReplacerPlugin = require('./theme.config')
 
 function resolve (dir) {
   return path.join(__dirname, dir)
 }
 
 // vue.config.js
-module.exports = {
+const vueConfig = {
   publicPath: './',
   /*
     Vue-cli3:
@@ -56,6 +57,7 @@ module.exports = {
 
   chainWebpack: (config) => {
     config.resolve.alias
+      .set('@public', resolve('public'))
       .set('@$', resolve('src'))
       .set('@api', resolve('src/api'))
       .set('@assets', resolve('src/assets'))
@@ -103,11 +105,8 @@ module.exports = {
     loaderOptions: {
       less: {
         modifyVars: {
-          // Refer:
           // https://ant.design/docs/spec/colors
           // https://vue.ant.design/docs/vue/customize-theme/
-          'primary-color': '#1890ff',
-          'link-color': '#1890ff'
         },
         javascriptEnabled: true
       }
@@ -149,3 +148,7 @@ module.exports = {
     }
   }
 }
+
+vueConfig.configureWebpack.plugins.push(createThemeColorReplacerPlugin())
+
+module.exports = vueConfig