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 <rohit.yadav@shapeblue.com>
Co-authored-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Hoang Nguyen 2020-06-22 15:27:14 +07:00 committed by Rohit Yadav
parent 0c0181511d
commit 50327ae339
33 changed files with 442 additions and 131 deletions

77
ui/docs/customize.md Normal file
View File

@ -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

103
ui/package-lock.json generated
View File

@ -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"
}

View File

@ -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",

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

47
ui/public/config.json vendored Normal file
View File

@ -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"
}
]
}

12
ui/public/example.html vendored Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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']),

View File

@ -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 () {

View File

@ -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 = [

View File

@ -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

View File

@ -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'

View File

@ -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;

View File

@ -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')
})

View File

@ -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' })

46
ui/src/style/vars.less Normal file
View File

@ -0,0 +1,46 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
@import "~ant-design-vue/lib/style/themes/default.less";
@logo-width: 256px;
@logo-height: 64px;
@banner-width: 450px;
@banner-height: 110px;
@error-width: 256px;
@error-height: 256px;
.ant-layout-sider-children .logo-image {
width: @logo-width;
height: @logo-height;
}
.user-layout {
&-logo {
width: @banner-width;
height: @banner-height;
}
}
.exception {
.img {
img {
width: @error-width;
height: @error-height;
}
}
}

View File

@ -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

View File

@ -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
})

View File

@ -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: [],

View File

@ -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)
}

View File

@ -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 = []

View File

@ -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%;
}
}

View File

@ -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)

View File

@ -0,0 +1,30 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<iframe :src="$route.meta.path" width="100%" frameBorder="0" style="height: 90vh">
</iframe>
</div>
</template>
<script>
export default {
name: 'IFramePlugin'
}
</script>

50
ui/theme.config.js Normal file
View File

@ -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

View File

@ -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