From f7edb394f2f6e44e0c3ba3c00be03a35085e4bcd Mon Sep 17 00:00:00 2001 From: Andi Sardina Ramos Date: Mon, 26 Feb 2024 16:02:04 +0200 Subject: [PATCH] chore(webui): Migrate to Quasar 2.x and Vue.js 3.x --- webui/.eslintignore | 6 + webui/{.eslintrc.js => .eslintrc.cjs} | 33 +- webui/{.postcssrc.js => .postcssrc.cjs} | 0 webui/babel.config.cjs | 16 + webui/babel.config.js | 5 - webui/{src/index.template.html => index.html} | 3 +- webui/jsconfig.json | 39 + webui/package.json | 65 +- webui/postcss.config.cjs | 27 + webui/quasar.conf.js | 50 +- webui/src/App.vue | 10 +- webui/src/_directives/resize.js | 20 +- webui/src/_helpers/Errors.js | 2 +- webui/src/_helpers/Helps.js | 17 +- webui/src/_helpers/Mutations.js | 9 +- webui/src/_middleware/Boot.js | 14 +- webui/src/_mixins/GetTableProps.js | 12 +- webui/src/_mixins/Pagination.js | 6 +- webui/src/boot/_globals.js | 4 +- webui/src/boot/_init.js | 8 +- webui/src/boot/api.js | 14 +- webui/src/components/_commons/AvatarState.vue | 30 +- .../src/components/_commons/BooleanState.vue | 32 +- webui/src/components/_commons/Chips.vue | 9 +- webui/src/components/_commons/MainTable.vue | 85 +- webui/src/components/_commons/NavBar.vue | 151 +- webui/src/components/_commons/PageDefault.vue | 2 +- .../components/_commons/PanelHealthCheck.vue | 105 +- .../components/_commons/PanelMiddlewares.vue | 903 +- .../_commons/PanelMirroringServices.vue | 51 +- .../_commons/PanelRouterDetails.vue | 128 +- .../src/components/_commons/PanelServers.vue | 101 +- .../_commons/PanelServiceDetails.vue | 112 +- webui/src/components/_commons/PanelTLS.vue | 97 +- .../_commons/PanelWeightedServices.vue | 49 +- .../src/components/_commons/ProviderIcon.vue | 16 +- webui/src/components/_commons/SidePanel.vue | 23 +- webui/src/components/_commons/SkeletonBox.vue | 6 +- .../_commons/StickyServiceDetails.vue | 75 +- webui/src/components/_commons/TLSState.vue | 15 +- webui/src/components/_commons/ToolBar.vue | 72 +- .../src/components/_commons/ToolBarTable.vue | 48 +- webui/src/components/dashboard/PanelChart.vue | 102 +- webui/src/components/dashboard/PanelEntry.vue | 28 +- .../src/components/dashboard/PanelFeature.vue | 15 +- .../components/dashboard/PanelProvider.vue | 23 +- ...r.variables.styl => quasar.variables.scss} | 18 +- webui/src/layouts/Default.vue | 4 +- webui/src/pages/_commons/Error404.vue | 12 +- webui/src/pages/_commons/MiddlewareDetail.vue | 110 +- webui/src/pages/_commons/RouterDetail.vue | 248 +- webui/src/pages/_commons/ServiceDetail.vue | 191 +- webui/src/pages/dashboard/Index.vue | 316 +- webui/src/pages/http/Middlewares.vue | 54 +- webui/src/pages/http/Routers.vue | 54 +- webui/src/pages/http/Services.vue | 54 +- webui/src/pages/tcp/Middlewares.vue | 54 +- webui/src/pages/tcp/Routers.vue | 54 +- webui/src/pages/tcp/Services.vue | 49 +- webui/src/pages/udp/Routers.vue | 53 +- webui/src/pages/udp/Services.vue | 54 +- webui/src/router/index.js | 28 +- webui/src/router/routes.js | 6 +- webui/src/store/index.js | 12 +- webui/yarn.lock | 8364 ++++------------- 65 files changed, 4374 insertions(+), 7999 deletions(-) rename webui/{.eslintrc.js => .eslintrc.cjs} (62%) rename webui/{.postcssrc.js => .postcssrc.cjs} (100%) create mode 100644 webui/babel.config.cjs delete mode 100644 webui/babel.config.js rename webui/{src/index.template.html => index.html} (94%) create mode 100644 webui/jsconfig.json create mode 100644 webui/postcss.config.cjs rename webui/src/css/{quasar.variables.styl => quasar.variables.scss} (63%) diff --git a/webui/.eslintignore b/webui/.eslintignore index 9b1c8b133..9f81cf845 100644 --- a/webui/.eslintignore +++ b/webui/.eslintignore @@ -1 +1,7 @@ /dist +/src-capacitor +/src-cordova +/.quasar +/node_modules +.eslintrc.cjs +/quasar.config.*.temporary.compiled* diff --git a/webui/.eslintrc.js b/webui/.eslintrc.cjs similarity index 62% rename from webui/.eslintrc.js rename to webui/.eslintrc.cjs index f8fee91f8..c08b11557 100644 --- a/webui/.eslintrc.js +++ b/webui/.eslintrc.cjs @@ -2,21 +2,24 @@ module.exports = { root: true, parserOptions: { - parser: 'babel-eslint', - sourceType: 'module' + parser: '@babel/eslint-parser', + ecmaVersion: 2021, // Allows for the parsing of modern ECMAScript features }, env: { + node: true, browser: true, - mocha: true + mocha: true, + 'vue/setup-compiler-macros': true }, extends: [ // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. - 'plugin:vue/essential', - '@vue/standard', - 'plugin:mocha/recommended' + 'plugin:vue/vue3-essential', + 'plugin:vue/vue3-recommended', + 'plugin:mocha/recommended', + 'standard' ], // required to lint *.vue files @@ -26,10 +29,16 @@ module.exports = { ], globals: { - 'ga': true, // Google Analytics - 'cordova': true, - '__statics': true, - 'process': true + ga: 'readonly', // Google Analytics + cordova: 'readonly', + __statics: 'readonly', + __QUASAR_SSR__: 'readonly', + __QUASAR_SSR_SERVER__: 'readonly', + __QUASAR_SSR_CLIENT__: 'readonly', + __QUASAR_SSR_PWA__: 'readonly', + process: 'readonly', + Capacitor: 'readonly', + chrome: 'readonly' }, // add your custom rules here @@ -39,6 +48,8 @@ module.exports = { // allow paren-less arrow functions 'arrow-parens': 'off', 'one-var': 'off', + 'no-void': 'off', + 'multiline-ternary': 'off', 'import/first': 'off', 'import/named': 'error', @@ -49,6 +60,7 @@ module.exports = { 'import/no-unresolved': 'off', 'import/no-extraneous-dependencies': 'off', 'prefer-promise-reject-errors': 'off', + 'vue/multi-word-component-names': 'off', // allow console.log during development only //'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', @@ -56,3 +68,4 @@ module.exports = { //'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' } } + diff --git a/webui/.postcssrc.js b/webui/.postcssrc.cjs similarity index 100% rename from webui/.postcssrc.js rename to webui/.postcssrc.cjs diff --git a/webui/babel.config.cjs b/webui/babel.config.cjs new file mode 100644 index 000000000..cd63d3933 --- /dev/null +++ b/webui/babel.config.cjs @@ -0,0 +1,16 @@ +/* eslint-disable */ + +module.exports = api => { + return { + presets: [ + [ + '@quasar/babel-preset-app', + api.caller(caller => caller && caller.target === 'node') + ? { targets: { node: 'current' } } + : {} + ] + ] + } +} + + diff --git a/webui/babel.config.js b/webui/babel.config.js deleted file mode 100644 index 9408c6cd4..000000000 --- a/webui/babel.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - presets: [ - '@quasar/babel-preset-app' - ] -} diff --git a/webui/src/index.template.html b/webui/index.html similarity index 94% rename from webui/src/index.template.html rename to webui/index.html index 70fee7d64..6eb6d6091 100644 --- a/webui/src/index.template.html +++ b/webui/index.html @@ -20,7 +20,6 @@ - -
+ diff --git a/webui/jsconfig.json b/webui/jsconfig.json new file mode 100644 index 000000000..456944a5e --- /dev/null +++ b/webui/jsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "src/*": [ + "src/*" + ], + "app/*": [ + "*" + ], + "components/*": [ + "src/components/*" + ], + "layouts/*": [ + "src/layouts/*" + ], + "pages/*": [ + "src/pages/*" + ], + "assets/*": [ + "src/assets/*" + ], + "boot/*": [ + "src/boot/*" + ], + "stores/*": [ + "src/stores/*" + ], + "vue$": [ + "node_modules/vue/dist/vue.runtime.esm-bundler.js" + ] + } + }, + "exclude": [ + "dist", + ".quasar", + "node_modules" + ] +} \ No newline at end of file diff --git a/webui/package.json b/webui/package.json index aa11f65b7..aa8090eae 100644 --- a/webui/package.json +++ b/webui/package.json @@ -16,42 +16,45 @@ "build:nc": "yarn build" }, "dependencies": { - "@quasar/extras": "^1.11.2", - "axios": "^0.21.1", - "bowser": "^2.5.2", - "chart.js": "^2.8.0", - "dot-prop": "^5.2.0", + "@quasar/extras": "^1.16.9", + "axios": "^1.6.7", + "bowser": "^2.11.0", + "chart.js": "^4.4.1", "core-js": "^3.35.1", - "iframe-resizer": "^4.2.11", + "dot-prop": "^8.0.2", + "iframe-resizer": "^4.3.9", "lodash.isequal": "4.5.0", - "moment": "^2.24.0", - "quasar": "^1.22.10", - "query-string": "^6.13.1", + "moment": "^2.30.1", + "quasar": "^2.14.3", + "query-string": "^8.1.0", "vh-check": "^2.0.5", - "vue-chartjs": "^3.4.2", - "vuex-map-fields": "^1.3.4" + "vue": "^3.0.0", + "vue-chartjs": "^5.3.0", + "vue-router": "^4.0.12", + "vuex": "^4.1.0", + "vuex-map-fields": "^1.4.1" }, "devDependencies": { - "@quasar/app": "^2.4.3", - "@vue/eslint-config-standard": "^4.0.0", - "@vue/test-utils": "^1.0.0-beta.29", - "babel-eslint": "^10.0.1", - "chai": "4.2.0", - "eslint": "^5.10.0", - "eslint-loader": "^2.1.1", - "eslint-plugin-prettier": "3.1.1", - "eslint-plugin-mocha": "6.2.1", - "eslint-plugin-vue": "^5.0.0", - "mocha": "^6.2.2", - "mocha-webpack": "^2.0.0-beta.0", - "prettier": "1.19.1" + "@babel/core": "^7.23.9", + "@babel/eslint-parser": "^7.23.10", + "@quasar/app-vite": "^1.4.3", + "@quasar/babel-preset-app": "^2.0.2", + "@vue/test-utils": "^2.4.4", + "autoprefixer": "^10.4.2", + "chai": "5.0.3", + "eslint": "^8.11.0", + "eslint-config-standard": "^17.0.0", + "eslint-plugin-import": "^2.19.1", + "eslint-plugin-mocha": "^10.2.0", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-vue": "^9.0.0", + "mocha": "^10.2.0", + "postcss": "^8.4.14" }, "engines": { - "node": ">= 8.9.0", - "npm": ">= 5.6.0", - "yarn": ">= 1.6.0" - }, - "browserslist": [ - "last 1 version, not dead, ie >= 11" - ] + "node": "^20 || ^18 || ^16", + "npm": ">= 6.13.4", + "yarn": ">= 1.21.1" + } } diff --git a/webui/postcss.config.cjs b/webui/postcss.config.cjs new file mode 100644 index 000000000..94b7b1c85 --- /dev/null +++ b/webui/postcss.config.cjs @@ -0,0 +1,27 @@ +/* eslint-disable */ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + plugins: [ + // https://github.com/postcss/autoprefixer + require('autoprefixer')({ + overrideBrowserslist: [ + 'last 4 Chrome versions', + 'last 4 Firefox versions', + 'last 4 Edge versions', + 'last 4 Safari versions', + 'last 4 Android versions', + 'last 4 ChromeAndroid versions', + 'last 4 FirefoxAndroid versions', + 'last 4 iOS versions' + ] + }) + + // https://github.com/elchininet/postcss-rtlcss + // If you want to support RTL css, then + // 1. yarn/npm install postcss-rtlcss + // 2. optionally set quasar.config.js > framework > lang to an RTL language + // 3. uncomment the following line: + // require('postcss-rtlcss') + ] +} diff --git a/webui/quasar.conf.js b/webui/quasar.conf.js index 1e7bf08a5..29d00f12a 100644 --- a/webui/quasar.conf.js +++ b/webui/quasar.conf.js @@ -1,12 +1,18 @@ // Configuration for your app // https://quasar.dev/quasar-cli/quasar-conf-js -module.exports = function (ctx) { +const { configure } = require('quasar/wrappers') + +module.exports = configure(function (ctx) { return { + eslint: { + warnings: true, + errors: true + }, + // app boot file (/src/boot) // --> boot files are part of "main.js" boot: [ - '_globals', 'api', '_hacks', '_init' @@ -114,6 +120,17 @@ module.exports = function (ctx) { supportIE: false, build: { + viteVuePluginOptions: { + template: { + compilerOptions: { + isCustomElement: (tag) => tag.startsWith('hub-') + } + } + }, + target: { + browser: ['edge88', 'firefox78', 'chrome87', 'safari13.1'], + node: 'node20' + }, publicPath: process.env.APP_PUBLIC_PATH || '', env: process.env.APP_ENV === 'development' ? { // staging: @@ -131,22 +148,7 @@ module.exports = function (ctx) { } }, scopeHoisting: true, - // vueRouterMode: 'history', - // vueCompiler: true, - // gzip: true, - // analyze: true, - // extractCSS: false, - extendWebpack (cfg) { - cfg.module.rules.push({ - enforce: 'pre', - test: /\.(js|vue)$/, - loader: 'eslint-loader', - exclude: /node_modules/, - options: { - formatter: require('eslint').CLIEngine.getFormatter('stylish') - } - }) - } + vueRouterMode: 'hash' // available values: 'hash', 'history' }, devServer: { @@ -166,16 +168,24 @@ module.exports = function (ctx) { animations: [], ssr: { - pwa: false + pwa: false, }, pwa: { + + workboxMode: 'injectManifest', // or 'generateSW' // workboxPluginMode: 'InjectManifest', // workboxOptions: {}, // only for NON InjectManifest workboxOptions: { skipWaiting: true, clientsClaim: true }, + + chainWebpackCustomSW (chain) { + chain.plugin('eslint-webpack-plugin') + .use(ESLintPlugin, [{ extensions: ['js'] }]) + }, + manifest: { // name: 'Traefik', // short_name: 'Traefik', @@ -247,4 +257,4 @@ module.exports = function (ctx) { } } } -} +}) diff --git a/webui/src/App.vue b/webui/src/App.vue index d655741b1..824ed04e6 100644 --- a/webui/src/App.vue +++ b/webui/src/App.vue @@ -13,6 +13,11 @@ export default { computed: { ...mapGetters('core', { coreVersion: 'version' }) }, + watch: { + '$q.dark.isActive' (val) { + localStorage.setItem('traefik-dark', val) + } + }, beforeCreate () { // Set vue instance APP.vue = () => this.$root @@ -21,11 +26,6 @@ export default { console.log('Quasar -> ', this.$q.version) this.$q.dark.set(localStorage.getItem('traefik-dark') === 'true') - }, - watch: { - '$q.dark.isActive' (val) { - localStorage.setItem('traefik-dark', val) - } } } diff --git a/webui/src/_directives/resize.js b/webui/src/_directives/resize.js index 59411eb2f..3a0500a44 100644 --- a/webui/src/_directives/resize.js +++ b/webui/src/_directives/resize.js @@ -1,8 +1,16 @@ -import Vue from 'vue' -import iFrameResize from 'iframe-resizer/js/iframeResizer' +import iframeResize from 'iframe-resizer/js/iframeResizer' -Vue.directive('resize', { - bind: function (el, { value = {} }) { - el.addEventListener('load', () => iFrameResize(value, el)) +const resize = { + mounted (el, binding) { + const options = binding.value || {} + el.addEventListener('load', () => iframeResize(options, el)) + }, + unmounted (el) { + const resizableEl = el + if (resizableEl.iFrameResizer) { + resizableEl.iFrameResizer.removeListeners() + } } -}) +} + +export default resize diff --git a/webui/src/_helpers/Errors.js b/webui/src/_helpers/Errors.js index 749885076..482766f16 100644 --- a/webui/src/_helpers/Errors.js +++ b/webui/src/_helpers/Errors.js @@ -22,7 +22,7 @@ class Errors { static handleResponse (error) { console.log('handleResponse', error, error.response) - let body = error.response.data + const body = error.response.data if (error.response.status === 401) { // TODO - actions... } diff --git a/webui/src/_helpers/Helps.js b/webui/src/_helpers/Helps.js index b8ba03d9e..96aef3e62 100644 --- a/webui/src/_helpers/Helps.js +++ b/webui/src/_helpers/Helps.js @@ -1,4 +1,4 @@ -import { get } from 'dot-prop' +import { getProperty } from 'dot-prop' class Helps { // Getters @@ -11,7 +11,7 @@ class Helps { // ------------------------------------------------------------------------ static get (obj, prop, def = undefined) { - return get(obj, prop, def) + return getProperty(obj, prop, def) } static hasIn (obj, prop) { @@ -39,13 +39,12 @@ class Helps { } static removeEmptyObjects (objects) { - const obj = {} - Object.entries(objects).map(item => { - if (item[1] !== '') { - obj[item[0]] = item[1] - } - }) - return obj + Object.entries(objects) + .filter(item => item[1] !== '') + .reduce((acc, item) => { + acc[item[0]] = item[1] + return acc + }, {}) } // Helps -> Numbers diff --git a/webui/src/_helpers/Mutations.js b/webui/src/_helpers/Mutations.js index 71faeec30..6550fb696 100644 --- a/webui/src/_helpers/Mutations.js +++ b/webui/src/_helpers/Mutations.js @@ -1,8 +1,8 @@ -import { set, get } from 'dot-prop' +import { setProperty, getProperty } from 'dot-prop' export const withPagination = (type, opts = {}) => (state, data) => { const { isSameContext, statePath } = opts - const currentState = get(state, statePath) + const currentState = getProperty(state, statePath) let newState @@ -13,7 +13,7 @@ export const withPagination = (type, opts = {}) => (state, data) => { loading: true } break - case 'success': + case 'success': { const { body, page } = data newState = { ...currentState, @@ -28,6 +28,7 @@ export const withPagination = (type, opts = {}) => (state, data) => { loading: false } break + } case 'failure': newState = { ...currentState, @@ -39,6 +40,6 @@ export const withPagination = (type, opts = {}) => (state, data) => { } if (newState) { - set(state, statePath, newState) + setProperty(state, statePath, newState) } } diff --git a/webui/src/_middleware/Boot.js b/webui/src/_middleware/Boot.js index 8fc5c968b..378794516 100644 --- a/webui/src/_middleware/Boot.js +++ b/webui/src/_middleware/Boot.js @@ -4,6 +4,11 @@ import Helps from '../_helpers/Helps' const Boot = { install (Vue, options) { Vue.mixin({ + filters: { + capFirstLetter (value) { + return Helps.capFirstLetter(value) + } + }, data () { return { } @@ -28,14 +33,9 @@ const Boot = { } } }, - methods: { - }, - filters: { - capFirstLetter (value) { - return Helps.capFirstLetter(value) - } - }, created () { + }, + methods: { } }) } diff --git a/webui/src/_mixins/GetTableProps.js b/webui/src/_mixins/GetTableProps.js index 020b54657..9c0e08824 100644 --- a/webui/src/_mixins/GetTableProps.js +++ b/webui/src/_mixins/GetTableProps.js @@ -1,9 +1,9 @@ -import { get } from 'dot-prop' +import { getProperty } from 'dot-prop' import { QChip } from 'quasar' -import Chips from '../components/_commons/Chips' -import ProviderIcon from '../components/_commons/ProviderIcon' -import AvatarState from '../components/_commons/AvatarState' -import TLSState from '../components/_commons/TLSState' +import Chips from '../components/_commons/Chips.vue' +import ProviderIcon from '../components/_commons/ProviderIcon.vue' +import AvatarState from '../components/_commons/AvatarState.vue' +import TLSState from '../components/_commons/TLSState.vue' const allColumns = [ { @@ -141,7 +141,7 @@ const GetTablePropsMixin = { path: `/${type.replace('-', '/', 'gi')}/${encodeURIComponent(row.name)}` }), columns: allColumns.filter(c => - get(propsByType, `${type}.columns`, []).includes(c.name) + getProperty(propsByType, `${type}.columns`, []).includes(c.name) ) } } diff --git a/webui/src/_mixins/Pagination.js b/webui/src/_mixins/Pagination.js index 4c2091626..a090beac2 100644 --- a/webui/src/_mixins/Pagination.js +++ b/webui/src/_mixins/Pagination.js @@ -1,4 +1,4 @@ -import { get } from 'dot-prop' +import { getProperty } from 'dot-prop' export default function PaginationMixin (opts = {}) { const { pollingIntervalTime, rowsPerPage = 10 } = opts @@ -28,7 +28,7 @@ export default function PaginationMixin (opts = {}) { currentPage = page currentLimit = limit || rowsPerPage - const fetchMethod = get(this, opts.fetchMethod) + const fetchMethod = getProperty(this, opts.fetchMethod) return fetchMethod({ ...params, @@ -41,7 +41,7 @@ export default function PaginationMixin (opts = {}) { }) }, initFetch (params) { - const scrollerRef = get(this.$refs, opts.scrollerRef) + const scrollerRef = getProperty(this.$refs, opts.scrollerRef) if (scrollerRef) { scrollerRef.stop() diff --git a/webui/src/boot/_globals.js b/webui/src/boot/_globals.js index f23935ca9..f099d03cd 100644 --- a/webui/src/boot/_globals.js +++ b/webui/src/boot/_globals.js @@ -1,8 +1,8 @@ import { APP } from '../_helpers/APP' import Boot from '../_middleware/Boot' -export default async ({ app, router, store, Vue }) => { - Vue.use(Boot) +export default async ({ app, router, store }) => { + app.use(Boot) APP.root = app APP.router = router diff --git a/webui/src/boot/_init.js b/webui/src/boot/_init.js index 9c78853ae..5ec3caf4e 100644 --- a/webui/src/boot/_init.js +++ b/webui/src/boot/_init.js @@ -1,10 +1,14 @@ import { APP } from '../_helpers/APP' import errors from '../_helpers/Errors' +import resize from '../_directives/resize' + +export default async ({ app, router }) => { + // Directives + app.directive('resize', resize) -export default async ({ Vue }) => { // Router // ---------------------------------------------- - APP.router.beforeEach(async (to, from, next) => { + router.beforeEach(async (to, from, next) => { // Set APP APP.routeTo = to APP.routeFrom = from diff --git a/webui/src/boot/api.js b/webui/src/boot/api.js index 706330d8c..8d4f5b314 100644 --- a/webui/src/boot/api.js +++ b/webui/src/boot/api.js @@ -1,12 +1,16 @@ +import { boot } from 'quasar/wrappers' import axios from 'axios' - import { APP } from '../_helpers/APP' // Set config defaults when creating the instance -const API = axios.create({ +const api = axios.create({ baseURL: APP.config.apiUrl }) -export default async ({ app, Vue }) => { - Vue.prototype.$api = app.api = APP.api = API -} +export default boot(({ app }) => { + app.config.globalProperties.$axios = axios + app.config.globalProperties.$api = api + APP.api = api +}) + +export { api } diff --git a/webui/src/components/_commons/AvatarState.vue b/webui/src/components/_commons/AvatarState.vue index a0adcd002..e74e73104 100644 --- a/webui/src/components/_commons/AvatarState.vue +++ b/webui/src/components/_commons/AvatarState.vue @@ -1,16 +1,32 @@