diff --git a/.eslintignore b/.eslintignore index a3ac40f7a0..97a76b54f1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,6 +12,7 @@ dist-pkg .DS_Store shell/utils/dynamic-importer.js ksconfig.json +nuxt storybook-static/ utils/dynamic-importer.js shell/assets/fonts diff --git a/.github/workflows/scripts/build-dashboard.sh b/.github/workflows/scripts/build-dashboard.sh index 877a68112c..028c52ecb4 100755 --- a/.github/workflows/scripts/build-dashboard.sh +++ b/.github/workflows/scripts/build-dashboard.sh @@ -28,7 +28,7 @@ echo Installing dependencies yarn install:ci echo Building -NUXT_ENV_commit=$GITHUB_SHA NUXT_ENV_version=$GITHUB_REF_NAME OUTPUT_DIR="$ARTIFACT_LOCATION" ROUTER_BASE="$ROUTER_BASE" RANCHER_ENV=$RANCHER_ENV API=$API RESOURCE_BASE=$RESOURCE_BASE EXCLUDES_PKG=$EXCLUDES_PKG yarn run build --spa +COMMIT=$GITHUB_SHA VERSION=$GITHUB_REF_NAME OUTPUT_DIR="$ARTIFACT_LOCATION" ROUTER_BASE="$ROUTER_BASE" RANCHER_ENV=$RANCHER_ENV API=$API RESOURCE_BASE=$RESOURCE_BASE EXCLUDES_PKG=$EXCLUDES_PKG yarn run build --spa echo Creating tar tar -czf $RELEASE_LOCATION.tar.gz -C $ARTIFACT_LOCATION . diff --git a/babel.config.js b/babel.config.js index 3993386e34..3d78f1703f 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1 +1 @@ -module.exports = { env: { test: { presets: [['@babel/env', { targets: { node: 'current' } }]] } } }; +module.exports = require('./shell/babel.config.js'); diff --git a/cypress/e2e/po/components/checkbox-input.po.ts b/cypress/e2e/po/components/checkbox-input.po.ts index aabe924939..d4925491c2 100644 --- a/cypress/e2e/po/components/checkbox-input.po.ts +++ b/cypress/e2e/po/components/checkbox-input.po.ts @@ -6,7 +6,7 @@ export default class CheckboxInputPo extends ComponentPo { return new CheckboxInputPo( self .find('.checkbox-outer-container') - .contains(`${ label } `) + .contains(label) .parent() ); } diff --git a/cypress/e2e/po/components/labeled-input.po.ts b/cypress/e2e/po/components/labeled-input.po.ts index 1a80a83833..b828493a95 100644 --- a/cypress/e2e/po/components/labeled-input.po.ts +++ b/cypress/e2e/po/components/labeled-input.po.ts @@ -6,7 +6,7 @@ export default class LabeledInputPo extends ComponentPo { return new LabeledInputPo( self .find('.labeled-input', { includeShadowDom: true }) - .contains(`${ label } `) + .contains(label) .next() ); } diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 5efc2bbca2..7209693e6a 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,8 +1,13 @@ { - "extends": "../tsconfig.default.json", + "extends": "../shell/tsconfig.default.json", "compilerOptions": { "noEmit": true, - "types": ["cypress"] + "types": [ + "cypress" + ] }, - "include": ["./**/*.ts", "../types/*.ts"] + "include": [ + "./**/*.ts", + "../types/*.ts" + ] } \ No newline at end of file diff --git a/docusaurus/docs/code-base-works/middleware.md b/docusaurus/docs/code-base-works/middleware.md new file mode 100644 index 0000000000..262f399497 --- /dev/null +++ b/docusaurus/docs/code-base-works/middleware.md @@ -0,0 +1,33 @@ +# Middleware + +## Location +The definitions of middleware reside in `shell/middleware`. Middleware added to the object in `shell/nuxt/middleware.js` will be initialized at the start of the app rendering. + + +## Notes +This file was generated by nuxt and will soon be redefined by hand. It's safe to add new middleware to this file. + +## Pattern +Define the middleware in a file that resides within `shell/middleware`. Then add the instantiation to the object that resides in `shell/nuxt/middleware.js`. + +shell/middleware/i18n.js +```js +export default async function({ + isHMR, app, store, route, params, error, redirect +}) { + // If middleware is called from hot module replacement, ignore it + if (isHMR) { + return; + } + + await store.dispatch('i18n/init'); +} +``` + +shell/nuxt/middleware.js +```js +... +middleware['i18n'] = require('../middleware/i18n.js') +middleware['i18n'] = middleware['i18n'].default || middleware['i18n'] +... +``` diff --git a/docusaurus/docs/code-base-works/nuxt-plugins.md b/docusaurus/docs/code-base-works/nuxt-plugins.md new file mode 100644 index 0000000000..7bf4a3e7d9 --- /dev/null +++ b/docusaurus/docs/code-base-works/nuxt-plugins.md @@ -0,0 +1,47 @@ +# Nuxt Plugins + +## Location +The definitions of plugins reside in `shell/plugins`. Plugins added to `shell/nuxt/index.js` will be initialized at the start of the app rendering. + + +## Notes +This file was generated by nuxt and will soon be redefined by hand. It's safe to add new plugins to this file. + +## Pattern +Define the store in a file that resides within `shell/plugins`. Then add the plugins import and execution to `shell/nuxt/index.js`. + +shell/plugins/version.js +```js +/** + * Fetch version metadata from backend /rancherversion API and store it + * + * This metadata does not change for an installation of Rancher + */ + +import { setVersionData } from '@shell/config/version'; + +export default async function({ store }) { + try { + const response = await store.dispatch('rancher/request', { + url: '/rancherversion', + method: 'get', + redirectUnauthorized: false + }); + + setVersionData(response); + } catch (e) { + console.warn('Failed to fetch Rancher version metadata', e); // eslint-disable-line no-console + } +} +``` + +shell/nuxt/index.js +```js +... +import version from '../plugins/version'; +... +if (process.client && typeof version === 'function') { + await version(app.context, inject); +} +... +``` diff --git a/docusaurus/docs/code-base-works/routes.md b/docusaurus/docs/code-base-works/routes.md new file mode 100644 index 0000000000..27dd2a27db --- /dev/null +++ b/docusaurus/docs/code-base-works/routes.md @@ -0,0 +1,22 @@ +# Routes + +## Location + +The core dashboard routes are defined in `shell/nuxt/router.js`. + + +## Notes +This file was generated by nuxt and will soon be redefined by hand. It's safe to add new routes to this file. + +## Pattern +First instantiate a page component at the top of the file. Then define a new route at the bottom of the file by giving the page component a unique path and name +```js +const about = () => interopDefault(import('../pages/about.vue')) +... +{ + path: "/about", + component: about, + name: "about" +} +... +``` \ No newline at end of file diff --git a/docusaurus/docs/code-base-works/stores.md b/docusaurus/docs/code-base-works/stores.md new file mode 100644 index 0000000000..ebda057616 --- /dev/null +++ b/docusaurus/docs/code-base-works/stores.md @@ -0,0 +1,18 @@ +# Stores + +## Location +The definitions of stores reside in `shell/store`. Stores added to `shell/nuxt/store.js` will be initialized at the start of the app rendering. + + +## Notes +This file was generated by nuxt and will soon be redefined by hand. It's safe to add new stores to this file. + +## Pattern +Define the store in a file that resides within `shell/store`. Then add the store to `shell/nuxt/store.js`. + +shell/nuxt/store.js +```js +... +resolveStoreModules(require('../store/i18n.js'), 'i18n.js') +... +``` diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js index ab8aa10fdb..3064df42e5 100644 --- a/docusaurus/sidebars.js +++ b/docusaurus/sidebars.js @@ -55,6 +55,10 @@ const sidebars = { 'code-base-works/helm-chart-apps', 'code-base-works/keyboard-shortcuts', 'code-base-works/kubernetes-resources-data-load', + 'code-base-works/routes', + 'code-base-works/middleware', + 'code-base-works/stores', + 'code-base-works/nuxt-plugins', 'code-base-works/machine-drivers', 'code-base-works/performance', 'code-base-works/sortable-table', diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 383d63c985..0000000000 --- a/jsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "baseUrl": ".", - "module": "commonjs", - "paths": { - "@/*": ["*"] - }, - "sourceMap": true, - "target": "es2017" - }, - - "exclude": ["node_modules", "dist","tmp"] -} diff --git a/package.json b/package.json index a0f2b72f92..e44ee53db4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "pkg/rancher-components" ], "scripts": { - "build-pkg": "./shell/scripts/build-pkg.sh", + "build-pkg": "yarn lint && ./shell/scripts/build-pkg.sh", "publish-pkg": "./shell/scripts/publish-pkg.sh", "serve-pkgs": "./shell/scripts/serve-pkgs", "publish-shell": "./shell/scripts/publish-shell.sh", @@ -24,20 +24,19 @@ "test": "jest --watch", "test:ci": "jest --collectCoverage", "install:ci": "yarn install --frozen-lockfile", - "nuxt": "./node_modules/.bin/nuxt", - "dev": "source ./scripts/version && ./node_modules/.bin/nuxt dev", - "mem-dev": "source ./scripts/version && node --max-old-space-size=8192 ./node_modules/.bin/nuxt dev", + "dev": "source ./scripts/version && NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve", + "mem-dev": "source ./scripts/version && NODE_ENV=dev node --max-old-space-size=8192 ./node_modules/.bin/vue-cli-service serve", "docker-dev": "docker run --rm --name dashboard-dev -p 8005:8005 -e API=$API -v $(pwd):/src -v dashboard_node:/src/node_modules rancher/dashboard:dev", "docker:local:start": "docker run -d --restart=unless-stopped -p 80:80 -p 443:443 -e CATTLE_BOOTSTRAP_PASSWORD=password -e CATTLE_PASSWORD_MIN_LENGTH=3 --name cypress --privileged rancher/rancher:v2.7-head", "docker:local:stop": "docker kill cypress || docker rm cypress || true", - "build": "./node_modules/.bin/nuxt build --devtools", + "build": "yarn lint && ./node_modules/.bin/vue-cli-service build", "build:lib": "cd pkg/rancher-components && yarn build:lib", - "analyze": "./node_modules/.bin/nuxt build --analyze", - "start": "./node_modules/.bin/nuxt start", + "analyze": "./node_modules/.bin/vue-cli-service build --report", + "start": "./node_modules/.bin/vue-cli-service serve", "start:dev": "NODE_ENV=dev yarn start", - "start:prod": "DEV_PORTS=true NODE_ENV=production yarn start", - "generate": "./node_modules/.bin/nuxt generate", - "dev-debug": "node --inspect ./node_modules/.bin/nuxt", + "start:prod": "NODE_OPTIONS=--max_old_space_size=4096 DEV_PORTS=true NODE_ENV=production yarn start", + "generate": "yarn build", + "dev-debug": "node --inspect ./node_modules/.bin/vue-cli-service", "cy:e2e": "cypress open --e2e --browser chrome", "cy:open": "cypress open", "cy:run": "cypress run --browser chrome", @@ -109,12 +108,18 @@ "shell-quote": "1.7.3", "sinon": "8.1.1", "ts-node": "8.10.2", + "ufo": "0.7.11", + "unfetch": "4.2.0", "url-parse": "1.5.10", "v-tooltip": "2.0.3", + "vue-client-only": "2.1.0", "vue-clipboard2": "0.3.1", "vue-codemirror": "4.0.6", "vue-js-modal": "1.3.35", + "vue-meta": "2.4.0", + "vue-no-ssr": "1.1.1", "vue-resize": "0.4.5", + "vue-router": "3.6.5", "vue-select": "3.18.3", "vue-server-renderer": "2.6.14", "vue-shortkey": "3.1.7", @@ -141,14 +146,13 @@ "@nuxtjs/eslint-config-typescript": "6.0.1", "@nuxtjs/eslint-module": "1.2.0", "@nuxtjs/style-resources": "1.2.1", + "@types/copy-webpack-plugin": "^5.0.3", "@types/jest": "27.4.1", "@types/lodash": "4.14.184", "@types/node": "16.4.3", "@types/vue-select": "3.16.0", "@typescript-eslint/eslint-plugin": "4.33.0", "@typescript-eslint/parser": "4.33.0", - "@vue/cli-plugin-babel": "4.5.15", - "@vue/cli-plugin-typescript": "4.5.15", "@vue/cli-service": "4.5.15", "@vue/eslint-config-standard": "5.1.2", "@vue/test-utils": "1.2.1", diff --git a/pkg/harvester/package.json b/pkg/harvester/package.json index 6d9bb38512..3e3271e158 100644 --- a/pkg/harvester/package.json +++ b/pkg/harvester/package.json @@ -10,8 +10,7 @@ "registry": "http://localhost:4873" }, "scripts": { - "dev": "./node_modules/.bin/nuxt dev", - "nuxt": "./node_modules/.bin/nuxt" + "dev": "./node_modules/.bin/vue-cli-service dev" }, "engines": { "node": ">=14" diff --git a/scripts/build-embedded b/scripts/build-embedded index e694758b03..49de2575e0 100755 --- a/scripts/build-embedded +++ b/scripts/build-embedded @@ -21,7 +21,7 @@ DIR=${GIT_TAG:-$COMMIT_BRANCH} OUTPUT_DIR=dist/${DIR}-embedded echo "Building..." -NUXT_ENV_commit=${COMMIT} NUXT_ENV_version=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard' yarn run build +COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard' yarn run build if [ -v EMBED_PKG ]; then echo "Build and embed plugin from: $EMBED_PKG" diff --git a/scripts/build-hosted b/scripts/build-hosted index 0a0dc3e243..e735344db5 100755 --- a/scripts/build-hosted +++ b/scripts/build-hosted @@ -25,4 +25,4 @@ BASE=${BASE:-https://releases.rancher.com/dashboard/${DIR}} echo "Building for ${BASE}..." -NUXT_ENV_commit=${COMMIT} NUXT_ENV_version=${VERSION} OUTPUT_DIR=dist/${DIR} ROUTER_BASE="/dashboard" RESOURCE_BASE="${BASE}" yarn run build --spa +COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=dist/${DIR} ROUTER_BASE="/dashboard" RESOURCE_BASE="${BASE}" yarn run build diff --git a/shell/assets/styles/app.scss b/shell/assets/styles/app.scss index ebfade38dd..135a7f7697 100644 --- a/shell/assets/styles/app.scss +++ b/shell/assets/styles/app.scss @@ -33,4 +33,4 @@ @import "./vendor/vue-select"; @import "./vendor/vue-js-modal"; @import "./vendor/code-mirror"; -@import '@/node_modules/xterm/css/xterm.css'; +@import 'node_modules/xterm/css/xterm.css'; \ No newline at end of file diff --git a/shell/assets/styles/fonts/_fontstack.scss b/shell/assets/styles/fonts/_fontstack.scss index 6bbafa79bf..ed0b94a1b3 100644 --- a/shell/assets/styles/fonts/_fontstack.scss +++ b/shell/assets/styles/fonts/_fontstack.scss @@ -4,8 +4,8 @@ font-style: normal; font-weight: normal; src: local(''), - url('~shell/assets/fonts/poppins/poppins-v15-latin-300.woff2') format('woff2'), /* Super Modern Browsers */ - url('~shell/assets/fonts/poppins/poppins-v15-latin-300.woff') format('woff'), /* Modern Browsers */ + url('~@shell/assets/fonts/poppins/poppins-v15-latin-300.woff2') format('woff2'), /* Super Modern Browsers */ + url('~@shell/assets/fonts/poppins/poppins-v15-latin-300.woff') format('woff'), /* Modern Browsers */ } /* poppins-500 - latin */ @@ -14,8 +14,8 @@ font-style: normal; font-weight: bold; src: local(''), - url('~shell/assets/fonts/poppins/poppins-v15-latin-500.woff2') format('woff2'), /* Super Modern Browsers */ - url('~shell/assets/fonts/poppins/poppins-v15-latin-500.woff') format('woff'), /* Modern Browsers */ + url('~@shell/assets/fonts/poppins/poppins-v15-latin-500.woff2') format('woff2'), /* Super Modern Browsers */ + url('~@shell/assets/fonts/poppins/poppins-v15-latin-500.woff') format('woff'), /* Modern Browsers */ } /* lato-regular - latin */ @@ -24,8 +24,8 @@ font-style: normal; font-weight: normal; src: local(''), - url('~shell/assets/fonts/lato/lato-v17-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ - url('~shell/assets/fonts/lato/lato-v17-latin-regular.woff') format('woff'), /* Modern Browsers */ + url('~@shell/assets/fonts/lato/lato-v17-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('~@shell/assets/fonts/lato/lato-v17-latin-regular.woff') format('woff'), /* Modern Browsers */ } /* lato-700 - latin */ @@ -34,8 +34,8 @@ font-style: normal; font-weight: bold; src: local(''), - url('~shell/assets/fonts/lato/lato-v17-latin-700.woff2') format('woff2'), /* Super Modern Browsers */ - url('~shell/assets/fonts/lato/lato-v17-latin-700.woff') format('woff'), /* Modern Browsers */ + url('~@shell/assets/fonts/lato/lato-v17-latin-700.woff2') format('woff2'), /* Super Modern Browsers */ + url('~@shell/assets/fonts/lato/lato-v17-latin-700.woff') format('woff'), /* Modern Browsers */ } /* roboto-mono-regular - latin */ @@ -44,6 +44,6 @@ font-style: normal; font-weight: normal; src: local(''), - url('~shell/assets/fonts/roboto-mono/roboto-mono-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ - url('~shell/assets/fonts/roboto-mono/roboto-mono-v13-latin-regular.woff') format('woff'), /* Modern Browsers */ -} + url('~@shell/assets/fonts/roboto-mono/roboto-mono-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('~@shell/assets/fonts/roboto-mono/roboto-mono-v13-latin-regular.woff') format('woff'), /* Modern Browsers */ +} \ No newline at end of file diff --git a/shell/assets/styles/vendor/vue-js-modal.scss b/shell/assets/styles/vendor/vue-js-modal.scss index beed3fbcd9..8868b3859d 100644 --- a/shell/assets/styles/vendor/vue-js-modal.scss +++ b/shell/assets/styles/vendor/vue-js-modal.scss @@ -1,4 +1,4 @@ -@import '~/node_modules/vue-js-modal/dist/styles.css'; +@import 'node_modules/vue-js-modal/dist/styles.css'; .v--modal-overlay { background-color: var(--overlay-bg); @@ -10,7 +10,7 @@ } .v--modal { - background-color: var(--modal-bg)!important; + background-color: var(--modal-bg) !important; box-shadow: none; border: 2px solid var(--modal-border); -} +} \ No newline at end of file diff --git a/shell/babel.config.js b/shell/babel.config.js new file mode 100644 index 0000000000..6fb6748ca1 --- /dev/null +++ b/shell/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: [ + [ + '@vue/cli-plugin-babel/preset', + { useBuiltIns: false } + ], + [ + '@babel/preset-env', + { targets: { node: 'current' } } + ] + ], + env: { test: { presets: [['@babel/env', { targets: { node: 'current' } }]] } } +}; diff --git a/shell/creators/app/files/babel.config.js b/shell/creators/app/files/babel.config.js index dcd9db785b..ac41fa8d56 100644 --- a/shell/creators/app/files/babel.config.js +++ b/shell/creators/app/files/babel.config.js @@ -1,18 +1 @@ -module.exports = { - env: { - test: { - plugins: [ - [ - 'module-resolver', - { - root: ['.'], - alias: { - '@': '.', - '~': '.', - }, - }, - ], - ], - }, - }, -}; +module.exports = require('@rancher/shell/babel.config.js'); diff --git a/shell/creators/app/files/nuxt.config.js b/shell/creators/app/files/nuxt.config.js deleted file mode 100644 index 5e978d809a..0000000000 --- a/shell/creators/app/files/nuxt.config.js +++ /dev/null @@ -1,6 +0,0 @@ -import config from '@rancher/shell/nuxt.config'; - -export default config(__dirname, { - excludes: [], - autoImport: [] -}); diff --git a/shell/creators/app/files/vue.config.js b/shell/creators/app/files/vue.config.js new file mode 100644 index 0000000000..5b55ecf216 --- /dev/null +++ b/shell/creators/app/files/vue.config.js @@ -0,0 +1,10 @@ +/** + * This file is here purely to support using the typescript version of the vue config vue.config.ts. + */ +require('ts-node').register({ + project: './tsconfig.json', + compilerOptions: { module: 'commonjs' }, + logError: true +}); + +module.exports = require('./vue.config.ts').default; diff --git a/shell/creators/app/files/vue.config.ts b/shell/creators/app/files/vue.config.ts new file mode 100644 index 0000000000..52938bdc23 --- /dev/null +++ b/shell/creators/app/files/vue.config.ts @@ -0,0 +1,7 @@ +import config from '@rancher/shell/vue.config'; + +export default config(__dirname, { + excludes: [], + // excludes: ['fleet', 'example'] + // autoLoad: ['fleet', 'example'] +}); diff --git a/shell/creators/app/init b/shell/creators/app/init index b4bf3f5280..c52b3d98a8 100755 --- a/shell/creators/app/init +++ b/shell/creators/app/init @@ -4,15 +4,15 @@ const path = require('path'); const fs = require('fs-extra'); const targets = { - dev: './node_modules/.bin/nuxt dev', - nuxt: './node_modules/.bin/nuxt', - build: './node_modules/.bin/nuxt build', + dev: 'NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve', + build: './node_modules/.bin/vue-cli-service build', clean: './node_modules/@rancher/shell/scripts/clean' }; const files = [ 'tsconfig.json', - 'nuxt.config.js', + 'vue.config.js', + 'vue.config.ts', '.eslintignore', '.eslintrc.js', 'babel.config.js', @@ -25,7 +25,7 @@ console.log('Creating Skeleton Application'); const args = process.argv; let appFolder = path.resolve('.'); -if (args.length == 3) { +if (args.length === 3) { const name = args[2]; const folder = path.resolve('.'); diff --git a/shell/nuxt.config.js b/shell/nuxt.config.js deleted file mode 100644 index d52ac0d9e1..0000000000 --- a/shell/nuxt.config.js +++ /dev/null @@ -1,799 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import serveStatic from 'serve-static'; -import webpack from 'webpack'; - -import { STANDARD } from './config/private-label'; -import { generateDynamicTypeImport } from './pkg/auto-import'; -import { directiveSsr as t } from './plugins/i18n'; -import { trimWhitespaceSsr as trimWhitespace } from './plugins/trim-whitespace'; - -const createProxyMiddleware = require('http-proxy-middleware'); - -// Global variables -let api = process.env.API || 'http://localhost:8989'; - -if ( !api.startsWith('http') ) { - api = `https://${ api }`; -} - -// needed for proxies -export const API_PATH = api; - -const dev = (process.env.NODE_ENV !== 'production'); -const devPorts = dev || process.env.DEV_PORTS === 'true'; - -// human readable version used on rancher dashboard about page -const dashboardVersion = process.env.DASHBOARD_VERSION; - -const prime = process.env.PRIME; - -const pl = process.env.PL || STANDARD; -const perfTest = (process.env.PERF_TEST === 'true'); // Enable performance testing when in dev -const instrumentCode = (process.env.TEST_INSTRUMENT === 'true'); // Instrument code for code coverage in e2e tests - -// Allow skipping of eslint check -// 0 = Skip browser and console checks -// 1 = Skip browser check -// 2 = Do not skip any checks -const skipEsLintCheckStr = (process.env.SKIP_ESLINT || ''); -let skipEsLintCheck = parseInt(skipEsLintCheckStr, 10) || 2; - -// =============================================================================================== -// Nuxt configuration -// =============================================================================================== - -// Expose a function that can be used by an app to provide a nuxt configuration for building an application -// This takes the directory of the application as tehfirst argument so that we can derive folder locations -// from it, rather than from the location of this file -export default function(dir, _appConfig) { - // Paths to the shell folder when it is included as a node dependency - let SHELL = 'node_modules/@rancher/shell'; - let SHELL_ABS = path.join(dir, 'node_modules/@rancher/shell'); - let NUXT_SHELL = '~~node_modules/@rancher/shell'; - let COMPONENTS_DIR = path.join(SHELL_ABS, 'rancher-components'); - let typescript = {}; - - if (fs.existsSync(SHELL_ABS)) { - const stat = fs.lstatSync(SHELL_ABS); - - // If @rancher/shell is a symlink, then use the components folder for it - if (stat.isSymbolicLink()) { - const REAL_SHELL_ABS = fs.realpathSync(SHELL_ABS); // In case the shell is being linked via 'yarn link' - - COMPONENTS_DIR = path.join(REAL_SHELL_ABS, '..', 'pkg', 'rancher-components', 'src', 'components'); - - // For now, skip eslint check when being linked via yarn link - pkg folder is linked otherwise - // This will change when we remove nuxt - skipEsLintCheck = true; - } - } - - // If we have a local folder named 'shell' then use that rather than the one in node_modules - // This will be the case in the main dashboard repository. - if (fs.existsSync(path.join(dir, 'shell'))) { - SHELL = './shell'; - SHELL_ABS = path.join(dir, 'shell'); - NUXT_SHELL = '~~/shell'; - COMPONENTS_DIR = path.join(dir, 'pkg', 'rancher-components', 'src', 'components'); - - // Skip eslint check that runs as part of nuxt build in the console - if (skipEsLintCheck > 0) { - typescript = { typeCheck: { eslint: { files: './shell/**/*.{ts,js,vue}' } } }; - } - } - - // Instrument code for tests - const babelPlugins = [ - // TODO: Browser support - // ['@babel/plugin-transform-modules-commonjs'], - ['@babel/plugin-proposal-private-property-in-object', { loose: true }] - ]; - - if (instrumentCode) { - babelPlugins.push('babel-plugin-istanbul'); - - console.warn('Instrumenting code for coverage'); // eslint-disable-line no-console - } - - // =============================================================================================== - // Functions for the UI Plugins - // =============================================================================================== - - const appConfig = _appConfig || {}; - const excludes = appConfig.excludes || []; - const autoLoad = appConfig.autoLoad || []; - - const serverMiddleware = []; - const autoLoadPackages = []; - const watcherIgnores = [ - /.shell/, - /dist-pkg/, - /scripts\/standalone/ - ]; - - autoLoad.forEach((pkg) => { - // Need the version number of each file - const pkgPackageFile = require(path.join(dir, 'pkg', pkg, 'package.json')); - const pkgRef = `${ pkg }-${ pkgPackageFile.version }`; - - autoLoadPackages.push({ - name: `app-autoload-${ pkgRef }`, - content: `/pkg/${ pkgRef }/${ pkgRef }.umd.min.js` - }); - - // Anything auto-loaded should also be excluded - if (!excludes.includes(pkg)) { - excludes.push(pkg); - } - }); - - // Find any UI packages in node_modules - const NM = path.join(dir, 'node_modules'); - const pkg = require(path.join(dir, 'package.json')); - const nmPackages = {}; - - if (pkg && pkg.dependencies) { - Object.keys(pkg.dependencies).forEach((pkg) => { - const f = require(path.join(NM, pkg, 'package.json')); - - // The package.json must have the 'rancher' property to mark it as a UI package - if (f.rancher) { - const id = `${ f.name }-${ f.version }`; - - nmPackages[id] = f.main; - - // Add server middleware to serve up the files for this UI package - serverMiddleware.push({ - path: `/pkg/${ id }`, - handler: serveStatic(path.join(NM, pkg)) - }); - } - }); - } - - serverMiddleware.push({ - path: '/uiplugins-catalog', - handler: (req, res, next) => { - const p = req.url.split('?'); - - try { - const proxy = createProxyMiddleware({ - target: p[1], - pathRewrite: { '^.*': p[0] } - }); - - return proxy(req, res, next); - } catch (e) { - console.error(e); // eslint-disable-line no-console - } - } - }); - - function includePkg(name) { - if (name.startsWith('.') || name === 'node_modules') { - return false; - } - - return !excludes || (excludes && !excludes.includes(name)); - } - - excludes.forEach((e) => { - watcherIgnores.push(new RegExp(`/pkg.${ e }`)); - }); - - // For each package in the pkg folder that is being compiled into the application, - // Add in the code to automatically import the types from that package - // This imports models, edit, detail, list etc - // When built as a UI package, shell/pkg/vue.config.js does the same thing - const autoImportTypes = {}; - const VirtualModulesPlugin = require('webpack-virtual-modules'); - let reqs = ''; - const pkgFolder = path.relative(dir, './pkg'); - - if (fs.existsSync(pkgFolder)) { - const items = fs.readdirSync(path.relative(dir, './pkg')); - - // Ignore hidden folders - items.filter(name => !name.startsWith('.')).forEach((name) => { - const f = require(path.join(dir, 'pkg', name, 'package.json')); - - // Package file must have rancher field to be a plugin - if (includePkg(name) && f.rancher) { - reqs += `$plugin.initPlugin('${ name }', require(\'~/pkg/${ name }\')); `; - - // // Serve the code for the UI package in case its used for dynamic loading (but not if the same package was provided in node_modules) - // if (!nmPackages[name]) { - // const pkgPackageFile = require(path.join(dir, 'pkg', name, 'package.json')); - // const pkgRef = `${ name }-${ pkgPackageFile.version }`; - - // serverMiddleware.push({ path: `/pkg/${ pkgRef }`, handler: serveStatic(`${ dir }/dist-pkg/${ pkgRef }`) }); - // } - autoImportTypes[`@rancher/auto-import/${ name }`] = generateDynamicTypeImport(`@pkg/${ name }`, path.join(dir, `pkg/${ name }`)); - } - }); - } - - Object.keys(nmPackages).forEach((m) => { - reqs += `$plugin.loadAsync('${ m }', '/pkg/${ m }/${ nmPackages[m] }');`; - }); - - // Generate a virtual module '@rancher/dyanmic.js` which imports all of the packages that should be built into the application - // This is imported in 'shell/extensions/extension-loader.js` which ensures the all code for plugins to be included is imported in the application - const virtualModules = new VirtualModulesPlugin({ 'node_modules/@rancher/dynamic.js': `export default function ($plugin) { ${ reqs } };` }); - const autoImport = new webpack.NormalModuleReplacementPlugin(/^@rancher\/auto-import$/, (resource) => { - const ctx = resource.context.split('/'); - const pkg = ctx[ctx.length - 1]; - - resource.request = `@rancher/auto-import/${ pkg }`; - }); - - // @pkg imports must be resolved to the package that it importing them - this allows a package to use @pkg as an alis - // to the root of that particular package - const pkgImport = new webpack.NormalModuleReplacementPlugin(/^@pkg/, (resource) => { - const ctx = resource.context.split('/'); - // Find 'pkg' folder in the contxt - const index = ctx.findIndex(s => s === 'pkg'); - - if (index !== -1 && (index + 1) < ctx.length) { - const pkg = ctx[index + 1]; - let p = path.resolve(dir, 'pkg', pkg, resource.request.substr(5)); - - if (resource.request.startsWith(`@pkg/${ pkg }`)) { - p = path.resolve(dir, 'pkg', resource.request.substr(5)); - } - - resource.request = p; - } - }); - - // Serve up the dist-pkg folder under /pkg - serverMiddleware.push({ path: `/pkg/`, handler: serveStatic(`${ dir }/dist-pkg/`) }); - // Add the standard dashboard server middleware after the middleware added to serve up UI packages - serverMiddleware.push(path.resolve(dir, SHELL, 'server', 'server-middleware')); - - // =============================================================================================== - // Dashboard nuxt configuration - // =============================================================================================== - - require('events').EventEmitter.defaultMaxListeners = 20; - require('dotenv').config(); - - let routerBasePath = '/'; - let resourceBase = ''; - let outputDir = 'dist'; - - if ( typeof process.env.ROUTER_BASE !== 'undefined' ) { - routerBasePath = process.env.ROUTER_BASE; - } - - if ( typeof process.env.RESOURCE_BASE !== 'undefined' ) { - resourceBase = process.env.RESOURCE_BASE; - } - - if ( typeof process.env.OUTPUT_DIR !== 'undefined' ) { - outputDir = process.env.OUTPUT_DIR; - } - - if ( resourceBase && !resourceBase.endsWith('/') ) { - resourceBase += '/'; - } - - console.log(`Build: ${ dev ? 'Development' : 'Production' }`); // eslint-disable-line no-console - - if ( !dev ) { - console.log(`Version: ${ dashboardVersion }`); // eslint-disable-line no-console - } - - if ( resourceBase ) { - console.log(`Resource Base URL: ${ resourceBase }`); // eslint-disable-line no-console - } - - if ( routerBasePath !== '/' ) { - console.log(`Router Base Path: ${ routerBasePath }`); // eslint-disable-line no-console - } - - if ( pl !== STANDARD ) { - console.log(`PL: ${ pl }`); // eslint-disable-line no-console - } - const rancherEnv = process.env.RANCHER_ENV || 'web'; - - console.log(`API: '${ api }'. Env: '${ rancherEnv }'`); // eslint-disable-line no-console - - // Nuxt modules - let nuxtModules = [ - '@nuxtjs/proxy', - '@nuxtjs/axios', - '@nuxtjs/eslint-module', - '@nuxtjs/webpack-profile', - 'cookie-universal-nuxt', - 'portal-vue/nuxt', - path.join(NUXT_SHELL, 'plugins/dashboard-store/rehydrate-all'), - ]; - - // Remove es-lint nuxt module if env var configures this - if (skipEsLintCheck < 2) { - nuxtModules = nuxtModules.filter(s => !s.includes('eslint-module')); - } - - const config = { - dev, - - // Configuration visible to the client, https://nuxtjs.org/api/configuration-env - env: { - dev, - pl, - perfTest, - rancherEnv, - harvesterPkgUrl: process.env.HARVESTER_PKG_URL, - api - }, - - // vars accessible via this.$config https://nuxtjs.org/docs/configuration-glossary/configuration-runtime-config/ - publicRuntimeConfig: { rancherEnv, dashboardVersion }, - - buildDir: dev ? '.nuxt' : '.nuxt-prod', - - buildModules: [ - '@nuxtjs/style-resources', - '@nuxt/typescript-build' - ], - styleResources: { - // only import functions, mixins, or variables, NEVER import full styles https://github.com/nuxt-community/style-resources-module#warning - hoistUseStatements: true, - scss: [ - path.resolve(SHELL_ABS, 'assets/styles/base/_variables.scss'), - path.resolve(SHELL_ABS, 'assets/styles/base/_functions.scss'), - path.resolve(SHELL_ABS, 'assets/styles/base/_mixins.scss'), - ], - }, - - loadingIndicator: path.join(SHELL_ABS, 'static/loading-indicator.html'), - - loading: path.join(SHELL_ABS, 'components/nav/GlobalLoading.vue'), - - // Axios: https://axios.nuxtjs.org/options - axios: { - https: true, - proxy: true, - retry: { retries: 0 }, - // debug: true - }, - - content: { - dir: path.resolve(SHELL_ABS, 'content'), - markdown: { prism: { theme: false } }, - liveEdit: false - }, - - router: { - base: routerBasePath, - middleware: ['i18n'], - prefetchLinks: false - }, - - alias: { - '~shell': SHELL_ABS, - '@shell': SHELL_ABS, - '@pkg': path.join(dir, 'pkg'), - '@components': COMPONENTS_DIR, - }, - - modulesDir: [ - path.resolve(dir), - './node_modules', - SHELL_ABS - ], - - dir: { - assets: path.posix.join(SHELL, 'assets'), - layouts: path.posix.join(SHELL, 'layouts'), - middleware: path.posix.join(SHELL, 'middleware'), - pages: path.posix.join(SHELL, 'pages'), - static: path.posix.join(SHELL, 'static'), - store: path.posix.join(SHELL, 'store'), - }, - - watchers: { webpack: { ignore: watcherIgnores } }, - - build: { - publicPath: resourceBase, - parallel: true, - cache: true, - hardSource: true, - - // Uses the Webpack Build Analyzer to generate a report of the bundle contents - // analyze: { analyzerMode: 'static' }, - - uglify: { - uglifyOptions: { compress: !dev }, - cache: './node_modules/.cache/uglify' - }, - - 'html.minify': { - collapseBooleanAttributes: !dev, - decodeEntities: !dev, - minifyCSS: !dev, - minifyJS: !dev, - processConditionalComments: !dev, - removeEmptyAttributes: !dev, - removeRedundantAttributes: !dev, - trimCustomFragments: !dev, - useShortDoctype: !dev - }, - - // Don't include `[name]` in prod file names - // This flattens out the folder structure (avoids crazy paths like `_nuxt/pages/account/create-key/pages/c/_cluster/_product/_resource/_id/pages/c/_cluster/_product/_resource`) - // and uses nuxt's workaround to address issues with filenames containing `//` (see https://github.com/nuxt/nuxt.js/issues/8274) - filenames: { chunk: ({ isDev }) => isDev ? '[name].js' : '[contenthash].js' }, - // @TODO figure out how to split chunks up better, by product - // optimization: { - // splitChunks: { - // cacheGroups: { - // styles: { - // name: 'styles', - // test: /\.(css|vue)$/, - // chunks: 'all', - // enforce: true - // }, - // } - // } - // }, - - plugins: [ - virtualModules, - autoImport, - new VirtualModulesPlugin(autoImportTypes), - pkgImport, - ], - - extend(config, { isClient, isDev }) { - if ( isDev ) { - config.devtool = 'cheap-module-source-map'; - } else { - config.devtool = 'source-map'; - } - - if ( resourceBase ) { - config.output.publicPath = resourceBase; - } - - // Remove default image handling rules - for ( let i = config.module.rules.length - 1 ; i >= 0 ; i-- ) { - if ( /svg/.test(config.module.rules[i].test) ) { - config.module.rules.splice(i, 1); - } - } - - config.resolve.symlinks = false; - - // Ensure we process files in the @rancher/shell folder - config.module.rules.forEach((r) => { - if ('test.js'.match(r.test)) { - if (r.exclude) { - const orig = r.exclude; - - r.exclude = function(modulePath) { - if (modulePath.indexOf(SHELL_ABS) === 0) { - return false; - } - - return orig(modulePath); - }; - } - } - }); - - // And substitute our own loader for images - config.module.rules.unshift({ - test: /\.(png|jpe?g|gif|svg|webp)$/, - use: [ - { - loader: 'url-loader', - options: { - name: '[path][name].[ext]', - limit: 1, - esModule: false - }, - } - ] - }); - - // Handler for yaml files (used for i18n files, for example) - config.module.rules.unshift({ - test: /\.ya?ml$/i, - loader: 'js-yaml-loader', - options: { name: '[path][name].[ext]' }, - }); - - // Handler for csv files (e.g. ec2 instance data) - config.module.rules.unshift({ - test: /\.csv$/i, - loader: 'csv-loader', - options: { - dynamicTyping: true, - header: true, - skipEmptyLines: true - }, - }); - - // Ensure there is a fallback for browsers that don't support web workers - config.module.rules.unshift({ - test: /web-worker.[a-z-]+.js/i, - loader: 'worker-loader', - options: { inline: 'fallback' }, - }); - - // Prevent warning in log with the md files in the content folder - config.module.rules.push({ - test: /\.md$/, - use: [ - { - loader: 'frontmatter-markdown-loader', - options: { mode: ['body'] } - } - ] - }); - }, - - // extractCSS: true, - cssSourceMap: true, - babel: { - presets({ isServer }) { - return [ - [ - require.resolve('@nuxt/babel-preset-app'), - { - // buildTarget: isServer ? 'server' : 'client', - corejs: { version: 3 }, - targets: isServer ? { node: '12' } : { browsers: ['last 2 versions'] }, - modern: !isServer - } - ], - '@babel/preset-typescript', - ]; - }, - plugins: babelPlugins - } - }, - - render: { - bundleRenderer: { - directives: { - trimWhitespace, - t, - } - } - }, - - // modern: true, -- now part of preset above - - generate: { dir: outputDir }, - - // Global CSS - css: [ - path.resolve(SHELL_ABS, 'assets/styles/app.scss') - ], - - head: { - title: process.env.npm_package_name || '', - meta: [ - { charset: 'utf-8' }, - { name: 'viewport', content: 'width=device-width, initial-scale=1' }, - { - hid: 'description', - name: 'description', - content: process.env.npm_package_description || '' - }, - ...autoLoadPackages, - ], - link: [{ - hid: 'icon', - rel: 'icon', - type: 'image/x-icon', - href: `${ resourceBase || '/' }favicon.png` - }] - }, - - // Nuxt modules - modules: nuxtModules, - - // Vue plugins - plugins: [ - // Extensions - path.relative(dir, path.join(SHELL, 'core/plugins.js')), - path.relative(dir, path.join(SHELL, 'core/plugins-loader.js')), // Load builtin plugins - - // Third-party - path.join(NUXT_SHELL, 'plugins/axios'), - path.join(NUXT_SHELL, 'plugins/tooltip'), - path.join(NUXT_SHELL, 'plugins/vue-clipboard2'), - path.join(NUXT_SHELL, 'plugins/v-select'), - path.join(NUXT_SHELL, 'plugins/directives'), - path.join(NUXT_SHELL, 'plugins/transitions'), - { src: path.join(NUXT_SHELL, 'plugins/vue-js-modal') }, - { src: path.join(NUXT_SHELL, 'plugins/js-yaml'), ssr: false }, - { src: path.join(NUXT_SHELL, 'plugins/resize'), ssr: false }, - { src: path.join(NUXT_SHELL, 'plugins/shortkey'), ssr: false }, - - // First-party - path.join(NUXT_SHELL, 'plugins/i18n'), - path.join(NUXT_SHELL, 'plugins/global-formatters'), - path.join(NUXT_SHELL, 'plugins/trim-whitespace'), - { src: path.join(NUXT_SHELL, 'plugins/extend-router') }, - { src: path.join(NUXT_SHELL, 'plugins/console'), ssr: false }, - { src: path.join(NUXT_SHELL, 'plugins/int-number'), ssr: false }, - { src: path.join(NUXT_SHELL, 'plugins/nuxt-client-init'), ssr: false }, - path.join(NUXT_SHELL, 'plugins/replaceall'), - path.join(NUXT_SHELL, 'plugins/back-button'), - { src: path.join(NUXT_SHELL, 'plugins/plugin'), ssr: false }, // Load dyanmic plugins - { src: path.join(NUXT_SHELL, 'plugins/codemirror-loader'), ssr: false }, - { src: path.join(NUXT_SHELL, 'plugins/formatters'), ssr: false }, // Populate formatters cache for sorted table - { src: path.join(NUXT_SHELL, 'plugins/version'), ssr: false }, // Makes a fetch to the backend to get version metadata - { src: path.join(NUXT_SHELL, 'plugins/steve-create-worker'), ssr: false }, // Add steve web worker creator to the store, to break the import chain - ], - - // Proxy: https://github.com/nuxt-community/proxy-module#options - proxy: { - ...appConfig.proxies, - '/k8s': proxyWsOpts(api), // Straight to a remote cluster (/k8s/clusters//) - '/pp': proxyWsOpts(api), // For (epinio) standalone API - '/api': proxyWsOpts(api), // Management k8s API - '/apis': proxyWsOpts(api), // Management k8s API - '/v1': proxyWsOpts(api), // Management Steve API - '/v3': proxyWsOpts(api), // Rancher API - '/v3-public': proxyOpts(api), // Rancher Unauthed API - '/api-ui': proxyOpts(api), // Browser API UI - '/meta': proxyMetaOpts(api), // Browser API UI - '/v1-*': proxyOpts(api), // SAML, KDM, etc - '/rancherversion': proxyPrimeOpts(api), // Rancher version endpoint - // These are for Ember embedding - '/c/*/edit': proxyOpts('https://127.0.0.1:8000'), // Can't proxy all of /c because that's used by Vue too - '/k/': proxyOpts('https://127.0.0.1:8000'), - '/g/': proxyOpts('https://127.0.0.1:8000'), - '/n/': proxyOpts('https://127.0.0.1:8000'), - '/p/': proxyOpts('https://127.0.0.1:8000'), - '/assets': proxyOpts('https://127.0.0.1:8000'), - '/translations': proxyOpts('https://127.0.0.1:8000'), - '/engines-dist': proxyOpts('https://127.0.0.1:8000'), - }, - - // Nuxt server - server: { - https: (devPorts ? { - key: fs.readFileSync(path.resolve(dir, SHELL, 'server/server.key')), - cert: fs.readFileSync(path.resolve(dir, SHELL, 'server/server.crt')) - } : null), - port: (devPorts ? 8005 : 80), - host: '0.0.0.0', - }, - - // Server middleware - serverMiddleware, - - // Eslint module options - eslint: { - cache: path.join(dir, 'node_modules/.cache/eslint'), - exclude: [ - '.nuxt' - ] - }, - - // Typescript eslint - typescript, - - ssr: false, - }; - - return config; -} - -// =============================================================================================== -// Functions for the request proxying used in dev -// =============================================================================================== - -export function proxyMetaOpts(target) { - return { - target, - followRedirects: true, - secure: !dev, - onProxyReq, - onProxyReqWs, - onError, - onProxyRes, - }; -} - -export function proxyOpts(target) { - return { - target, - secure: !devPorts, - onProxyReq, - onProxyReqWs, - onError, - onProxyRes, - }; -} - -// Intercept the /rancherversion API call wnad modify the 'RancherPrime' value -// if configured to do so by the environment variable PRIME -export function proxyPrimeOpts(target) { - const opts = proxyOpts(target); - - // Don't intercept if the PRIME environment variable is not set - if (!prime?.length) { - return opts; - } - - opts.onProxyRes = (proxyRes, req, res) => { - const _end = res.end; - let body = ''; - - proxyRes.on( 'data', (data) => { - data = data.toString('utf-8'); - body += data; - }); - - res.write = () => {}; - - res.end = () => { - let output = body; - - try { - const out = JSON.parse(body); - - out.RancherPrime = prime; - output = JSON.stringify(out); - } catch (err) {} - - res.setHeader('content-length', output.length ); - res.setHeader('content-type', 'application/json' ); - res.setHeader('transfer-encoding', ''); - res.setHeader('cache-control', 'no-cache'); - res.writeHead(proxyRes.statusCode); - _end.apply(res, [output]); - }; - }; - - return opts; -} - -export function onProxyRes(proxyRes, req, res) { - if (devPorts) { - proxyRes.headers['X-Frame-Options'] = 'ALLOWALL'; - } -} - -export function proxyWsOpts(target) { - return { - ...proxyOpts(target), - ws: true, - changeOrigin: true, - }; -} - -export function onProxyReq(proxyReq, req) { - if (!(proxyReq._currentRequest && proxyReq._currentRequest._headerSent)) { - proxyReq.setHeader('x-api-host', req.headers['host']); - proxyReq.setHeader('x-forwarded-proto', 'https'); - // console.log(proxyReq.getHeaders()); - } -} - -export function onProxyReqWs(proxyReq, req, socket, options, head) { - req.headers.origin = options.target.href; - proxyReq.setHeader('origin', options.target.href); - proxyReq.setHeader('x-api-host', req.headers['host']); - proxyReq.setHeader('x-forwarded-proto', 'https'); - // console.log(proxyReq.getHeaders()); - - socket.on('error', (err) => { - console.error('Proxy WS Error:', err); // eslint-disable-line no-console - }); -} - -export function onError(err, req, res) { - res.statusCode = 598; - console.error('Proxy Error:', err); // eslint-disable-line no-console - res.write(JSON.stringify(err)); -} diff --git a/shell/nuxt/App.js b/shell/nuxt/App.js new file mode 100644 index 0000000000..3e0c784022 --- /dev/null +++ b/shell/nuxt/App.js @@ -0,0 +1,210 @@ +import Vue from 'vue' +import { decode, parsePath, withoutBase, withoutTrailingSlash, normalizeURL } from 'ufo' + +import { getMatchedComponentsInstances, getChildrenComponentInstancesUsingFetch, promisify, globalHandleError, urlJoin, sanitizeComponent } from './utils' +import NuxtError from '../layouts/error.vue' +import NuxtLoading from '../components/nav/GlobalLoading.vue' +import NuxtBuildIndicator from './components/nuxt-build-indicator' + +import '../assets/styles/app.scss' + +import _77180f1e from '../layouts/blank.vue' +import _6f6c098b from '../layouts/default.vue' +import _2d2495d5 from '../layouts/home.vue' +import _77dd5794 from '../layouts/plain.vue' +import _44002f80 from '../layouts/unauthenticated.vue' + +const layouts = { "_blank": sanitizeComponent(_77180f1e),"_default": sanitizeComponent(_6f6c098b),"_home": sanitizeComponent(_2d2495d5),"_plain": sanitizeComponent(_77dd5794),"_unauthenticated": sanitizeComponent(_44002f80) } + +export default { + render (h, props) { + const loadingEl = h('NuxtLoading', { ref: 'loading' }) + + const layoutEl = h(this.layout || 'nuxt') + const templateEl = h('div', { + domProps: { + id: '__layout' + }, + key: this.layoutName + }, [layoutEl]) + + const transitionEl = h('transition', { + props: { + name: 'layout', + mode: 'out-in' + }, + on: { + beforeEnter (el) { + // Ensure to trigger scroll event after calling scrollBehavior + window.$nuxt.$nextTick(() => { + window.$nuxt.$emit('triggerScroll') + }) + } + } + }, [templateEl]) + + return h('div', { + domProps: { + id: '__nuxt' + } + }, [ + loadingEl, + //h(NuxtBuildIndicator), // The build indicator doesn't work as is right now and emits an error in the console so I'm leaving it out for now + transitionEl + ]) + }, + + data: () => ({ + isOnline: true, + + layout: null, + layoutName: '', + + nbFetching: 0 + }), + + beforeCreate () { + Vue.util.defineReactive(this, 'nuxt', this.$options.nuxt) + }, + created () { + // Add this.$nuxt in child instances + this.$root.$options.$nuxt = this + + if (process.client) { + // add to window so we can listen when ready + window.$nuxt = this + + this.refreshOnlineStatus() + // Setup the listeners + window.addEventListener('online', this.refreshOnlineStatus) + window.addEventListener('offline', this.refreshOnlineStatus) + } + // Add $nuxt.error() + this.error = this.nuxt.error + // Add $nuxt.context + this.context = this.$options.context + }, + + async mounted () { + this.$loading = this.$refs.loading + }, + + watch: { + 'nuxt.err': 'errorChanged' + }, + + computed: { + isOffline () { + return !this.isOnline + }, + + isFetching () { + return this.nbFetching > 0 + }, + }, + + methods: { + refreshOnlineStatus () { + if (process.client) { + if (typeof window.navigator.onLine === 'undefined') { + // If the browser doesn't support connection status reports + // assume that we are online because most apps' only react + // when they now that the connection has been interrupted + this.isOnline = true + } else { + this.isOnline = window.navigator.onLine + } + } + }, + + async refresh () { + const pages = getMatchedComponentsInstances(this.$route) + + if (!pages.length) { + return + } + this.$loading.start() + + const promises = pages.map((page) => { + const p = [] + + // Old fetch + if (page.$options.fetch && page.$options.fetch.length) { + p.push(promisify(page.$options.fetch, this.context)) + } + if (page.$fetch) { + p.push(page.$fetch()) + } else { + // Get all component instance to call $fetch + for (const component of getChildrenComponentInstancesUsingFetch(page.$vnode.componentInstance)) { + p.push(component.$fetch()) + } + } + + if (page.$options.asyncData) { + p.push( + promisify(page.$options.asyncData, this.context) + .then((newData) => { + for (const key in newData) { + Vue.set(page.$data, key, newData[key]) + } + }) + ) + } + + return Promise.all(p) + }) + try { + await Promise.all(promises) + } catch (error) { + this.$loading.fail(error) + globalHandleError(error) + this.error(error) + } + this.$loading.finish() + }, + errorChanged () { + if (this.nuxt.err) { + if (this.$loading) { + if (this.$loading.fail) { + this.$loading.fail(this.nuxt.err) + } + if (this.$loading.finish) { + this.$loading.finish() + } + } + + let errorLayout = (NuxtError.options || NuxtError).layout; + + if (typeof errorLayout === 'function') { + errorLayout = errorLayout(this.context) + } + + this.setLayout(errorLayout) + } + }, + + setLayout (layout) { + if(layout && typeof layout !== 'string') { + throw new Error('[nuxt] Avoid using non-string value as layout property.') + } + + if (!layout || !layouts['_' + layout]) { + layout = 'default' + } + this.layoutName = layout + this.layout = layouts['_' + layout] + return this.layout + }, + loadLayout (layout) { + if (!layout || !layouts['_' + layout]) { + layout = 'default' + } + return Promise.resolve(layouts['_' + layout]) + }, + }, + + components: { + NuxtLoading + } +} diff --git a/shell/nuxt/axios.js b/shell/nuxt/axios.js new file mode 100644 index 0000000000..3f13955892 --- /dev/null +++ b/shell/nuxt/axios.js @@ -0,0 +1,186 @@ +import Axios from 'axios' +import defu from 'defu' +import axiosRetry from 'axios-retry' + +// Axios.prototype cannot be modified +const axiosExtra = { + setBaseURL (baseURL) { + this.defaults.baseURL = baseURL + }, + setHeader (name, value, scopes = 'common') { + for (let scope of Array.isArray(scopes) ? scopes : [ scopes ]) { + if (!value) { + delete this.defaults.headers[scope][name]; + return + } + this.defaults.headers[scope][name] = value + } + }, + setToken (token, type, scopes = 'common') { + const value = !token ? null : (type ? type + ' ' : '') + token + this.setHeader('Authorization', value, scopes) + }, + onRequest(fn) { + this.interceptors.request.use(config => fn(config) || config) + }, + onResponse(fn) { + this.interceptors.response.use(response => fn(response) || response) + }, + onRequestError(fn) { + this.interceptors.request.use(undefined, error => fn(error) || Promise.reject(error)) + }, + onResponseError(fn) { + this.interceptors.response.use(undefined, error => fn(error) || Promise.reject(error)) + }, + onError(fn) { + this.onRequestError(fn) + this.onResponseError(fn) + }, + create(options) { + return createAxiosInstance(defu(options, this.defaults)) + } +} + +// Request helpers ($get, $post, ...) +for (let method of ['request', 'delete', 'get', 'head', 'options', 'post', 'put', 'patch']) { + axiosExtra['$' + method] = function () { return this[method].apply(this, arguments).then(res => res && res.data) } +} + +const extendAxiosInstance = axios => { + for (let key in axiosExtra) { + axios[key] = axiosExtra[key].bind(axios) + } +} + +const createAxiosInstance = axiosOptions => { + // Create new axios instance + const axios = Axios.create(axiosOptions) + axios.CancelToken = Axios.CancelToken + axios.isCancel = Axios.isCancel + + // Extend axios proto + extendAxiosInstance(axios) + + // Setup interceptors + + setupProgress(axios) + axiosRetry(axios, {"retries":0}) + + return axios +} + +const setupProgress = (axios) => { + if (process.server) { + return + } + + // A noop loading inteterface for when $nuxt is not yet ready + const noopLoading = { + finish: () => { }, + start: () => { }, + fail: () => { }, + set: () => { } + } + + const $loading = () => { + const $nuxt = typeof window !== 'undefined' && window['$nuxt'] + return ($nuxt && $nuxt.$loading && $nuxt.$loading.set) ? $nuxt.$loading : noopLoading + } + + let currentRequests = 0 + + axios.onRequest(config => { + if (config && config.progress === false) { + return + } + + currentRequests++ + }) + + axios.onResponse(response => { + if (response && response.config && response.config.progress === false) { + return + } + + currentRequests-- + if (currentRequests <= 0) { + currentRequests = 0 + $loading().finish() + } + }) + + axios.onError(error => { + if (error && error.config && error.config.progress === false) { + return + } + + currentRequests-- + + if (Axios.isCancel(error)) { + return + } + + $loading().fail() + $loading().finish() + }) + + const onProgress = e => { + if (!currentRequests) { + return + } + const progress = ((e.loaded * 100) / (e.total * currentRequests)) + $loading().set(Math.min(100, progress)) + } + + axios.defaults.onUploadProgress = onProgress + axios.defaults.onDownloadProgress = onProgress +} + +export default (ctx, inject) => { + // runtimeConfig + const runtimeConfig = ctx.$config && ctx.$config.axios || {} + // baseURL + const baseURL = process.browser + ? (runtimeConfig.browserBaseURL || runtimeConfig.baseURL || '/') + : (runtimeConfig.baseURL || process.env._AXIOS_BASE_URL_ || 'https://localhost:8005/') + + // Create fresh objects for all default header scopes + // Axios creates only one which is shared across SSR requests! + // https://github.com/mzabriskie/axios/blob/master/lib/defaults.js + const headers = { + "common": { + "Accept": "application/json, text/plain, */*" + }, + "delete": {}, + "get": {}, + "head": {}, + "post": {}, + "put": {}, + "patch": {} +} + + const axiosOptions = { + baseURL, + headers + } + + // Proxy SSR request headers headers + if (process.server && ctx.req && ctx.req.headers) { + const reqHeaders = { ...ctx.req.headers } + for (let h of ["accept","host","cf-ray","cf-connecting-ip","content-length","content-md5","content-type"]) { + delete reqHeaders[h] + } + axiosOptions.headers.common = { ...reqHeaders, ...axiosOptions.headers.common } + } + + if (process.server) { + // Don't accept brotli encoding because Node can't parse it + axiosOptions.headers.common['accept-encoding'] = 'gzip, deflate' + } + + const axios = createAxiosInstance(axiosOptions) + + // Inject axios to the context as $axios + ctx.$axios = axios + inject('axios', axios) +} diff --git a/shell/nuxt/client.js b/shell/nuxt/client.js new file mode 100644 index 0000000000..e9efe4583e --- /dev/null +++ b/shell/nuxt/client.js @@ -0,0 +1,817 @@ +import Vue from 'vue' +import fetch from 'unfetch' +import middleware from './middleware.js' +import { + applyAsyncData, + promisify, + middlewareSeries, + sanitizeComponent, + resolveRouteComponents, + getMatchedComponents, + getMatchedComponentsInstances, + flatMapComponents, + setContext, + getLocation, + compile, + getQueryDiff, + globalHandleError, + isSamePath, + urlJoin +} from './utils.js' +import { createApp, NuxtError } from './index.js' +import fetchMixin from './mixins/fetch.client' +import NuxtLink from './components/nuxt-link.client.js' // should be included after ./index.js + +// Fetch mixin +if (!Vue.__nuxt__fetch__mixin__) { + Vue.mixin(fetchMixin) + Vue.__nuxt__fetch__mixin__ = true +} + +// Component: +Vue.component(NuxtLink.name, NuxtLink) +Vue.component('NLink', NuxtLink) + +if (!global.fetch) { global.fetch = fetch } + +// Global shared references +let _lastPaths = [] +let app +let router +let store + +// Try to rehydrate SSR data from window +const NUXT = window.__NUXT__ || {} + +const $config = nuxt.publicRuntimeConfig || {} +if ($config._app) { + __webpack_public_path__ = urlJoin($config._app.cdnURL, $config._app.assetsPath) +} + +Object.assign(Vue.config, {"silent":false,"performance":true}) + +const logs = NUXT.logs || [] + if (logs.length > 0) { + const ssrLogStyle = 'background: #2E495E;border-radius: 0.5em;color: white;font-weight: bold;padding: 2px 0.5em;' + console.group && console.group ('%cNuxt SSR', ssrLogStyle) + logs.forEach(logObj => (console[logObj.type] || console.log)(...logObj.args)) + delete NUXT.logs + console.groupEnd && console.groupEnd() +} + +// Setup global Vue error handler +if (!Vue.config.$nuxt) { + const defaultErrorHandler = Vue.config.errorHandler + Vue.config.errorHandler = async (err, vm, info, ...rest) => { + // Call other handler if exist + let handled = null + if (typeof defaultErrorHandler === 'function') { + handled = defaultErrorHandler(err, vm, info, ...rest) + } + if (handled === true) { + return handled + } + + if (vm && vm.$root) { + const nuxtApp = Object.keys(Vue.config.$nuxt) + .find(nuxtInstance => vm.$root[nuxtInstance]) + + // Show Nuxt Error Page + if (nuxtApp && vm.$root[nuxtApp].error && info !== 'render function') { + const currentApp = vm.$root[nuxtApp] + + // Load error layout + let layout = (NuxtError.options || NuxtError).layout + if (typeof layout === 'function') { + layout = layout(currentApp.context) + } + if (layout) { + await currentApp.loadLayout(layout).catch(() => {}) + } + currentApp.setLayout(layout) + + currentApp.error(err) + } + } + + if (typeof defaultErrorHandler === 'function') { + return handled + } + + // Log to console + if (process.env.NODE_ENV !== 'production') { + console.error(err) + } else { + console.error(err.message || err) + } + } + Vue.config.$nuxt = {} +} +Vue.config.$nuxt.$nuxt = true + +const errorHandler = Vue.config.errorHandler || console.error + +// Create and mount App +createApp(null, nuxt.publicRuntimeConfig).then(mountApp).catch(errorHandler) + +function componentOption (component, key, ...args) { + if (!component || !component.options || !component.options[key]) { + return {} + } + const option = component.options[key] + if (typeof option === 'function') { + return option(...args) + } + return option +} + +function mapTransitions (toComponents, to, from) { + const componentTransitions = (component) => { + const transition = componentOption(component, 'transition', to, from) || {} + return (typeof transition === 'string' ? { name: transition } : transition) + } + + const fromComponents = from ? getMatchedComponents(from) : [] + const maxDepth = Math.max(toComponents.length, fromComponents.length) + + const mergedTransitions = [] + for (let i=0; i typeof toTransitions[key] !== 'undefined' && !key.toLowerCase().includes('leave')) + .forEach((key) => { transitions[key] = toTransitions[key] }) + + mergedTransitions.push(transitions) + } + return mergedTransitions +} + +async function loadAsyncComponents (to, from, next) { + // Check if route changed (this._routeChanged), only if the page is not an error (for validate()) + this._routeChanged = Boolean(app.nuxt.err) || from.name !== to.name + this._paramChanged = !this._routeChanged && from.path !== to.path + this._queryChanged = !this._paramChanged && from.fullPath !== to.fullPath + this._diffQuery = (this._queryChanged ? getQueryDiff(to.query, from.query) : []) + + if ((this._routeChanged || this._paramChanged) && this.$loading.start && !this.$loading.manual) { + this.$loading.start() + } + + try { + if (this._queryChanged) { + const Components = await resolveRouteComponents( + to, + (Component, instance) => ({ Component, instance }) + ) + // Add a marker on each component that it needs to refresh or not + const startLoader = Components.some(({ Component, instance }) => { + const watchQuery = Component.options.watchQuery + if (watchQuery === true) { + return true + } + if (Array.isArray(watchQuery)) { + return watchQuery.some(key => this._diffQuery[key]) + } + if (typeof watchQuery === 'function') { + return watchQuery.apply(instance, [to.query, from.query]) + } + return false + }) + + if (startLoader && this.$loading.start && !this.$loading.manual) { + this.$loading.start() + } + } + // Call next() + next() + } catch (error) { + const err = error || {} + const statusCode = err.statusCode || err.status || (err.response && err.response.status) || 500 + const message = err.message || '' + + // Handle chunk loading errors + // This may be due to a new deployment or a network problem + if (/^Loading( CSS)? chunk (\d)+ failed\./.test(message)) { + window.location.reload(true /* skip cache */) + return // prevent error page blinking for user + } + + this.error({ statusCode, message }) + this.$nuxt.$emit('routeChanged', to, from, err) + next() + } +} + +function applySSRData (Component, ssrData) { + if (NUXT.serverRendered && ssrData) { + applyAsyncData(Component, ssrData) + } + + Component._Ctor = Component + return Component +} + +// Get matched components +function resolveComponents (route) { + return flatMapComponents(route, async (Component, _, match, key, index) => { + // If component is not resolved yet, resolve it + if (typeof Component === 'function' && !Component.options) { + Component = await Component() + } + // Sanitize it and save it + const _Component = applySSRData(sanitizeComponent(Component), NUXT.data ? NUXT.data[index] : null) + match.components[key] = _Component + return _Component + }) +} + +function callMiddleware (Components, context, layout) { + let midd = ["i18n"] + let unknownMiddleware = false + + // If layout is undefined, only call global middleware + if (typeof layout !== 'undefined') { + midd = [] // Exclude global middleware if layout defined (already called before) + layout = sanitizeComponent(layout) + if (layout.options.middleware) { + midd = midd.concat(layout.options.middleware) + } + Components.forEach((Component) => { + if (Component.options.middleware) { + midd = midd.concat(Component.options.middleware) + } + }) + } + + midd = midd.map((name) => { + if (typeof name === 'function') { + return name + } + if (typeof middleware[name] !== 'function') { + unknownMiddleware = true + this.error({ statusCode: 500, message: 'Unknown middleware ' + name }) + } + return middleware[name] + }) + + if (unknownMiddleware) { + return + } + return middlewareSeries(midd, context) +} + +async function render (to, from, next) { + if (this._routeChanged === false && this._paramChanged === false && this._queryChanged === false) { + return next() + } + // Handle first render on SPA mode + let spaFallback = false + if (to === from) { + _lastPaths = [] + spaFallback = true + } else { + const fromMatches = [] + _lastPaths = getMatchedComponents(from, fromMatches).map((Component, i) => { + return compile(from.matched[fromMatches[i]].path)(from.params) + }) + } + + // nextCalled is true when redirected + let nextCalled = false + const _next = (path) => { + if (from.path === path.path && this.$loading.finish) { + this.$loading.finish() + } + + if (from.path !== path.path && this.$loading.pause) { + this.$loading.pause() + } + + if (nextCalled) { + return + } + + nextCalled = true + next(path) + } + + // Update context + await setContext(app, { + route: to, + from, + next: _next.bind(this) + }) + this._dateLastError = app.nuxt.dateErr + this._hadError = Boolean(app.nuxt.err) + + // Get route's matched components + const matches = [] + const Components = getMatchedComponents(to, matches) + + // If no Components matched, generate 404 + if (!Components.length) { + // Default layout + await callMiddleware.call(this, Components, app.context) + if (nextCalled) { + return + } + + // Load layout for error page + const errorLayout = (NuxtError.options || NuxtError).layout + const layout = await this.loadLayout( + typeof errorLayout === 'function' + ? errorLayout.call(NuxtError, app.context) + : errorLayout + ) + + await callMiddleware.call(this, Components, app.context, layout) + if (nextCalled) { + return + } + + // Show error page + app.context.error({ statusCode: 404, message: 'This page could not be found' }) + return next() + } + + // Update ._data and other properties if hot reloaded + Components.forEach((Component) => { + if (Component._Ctor && Component._Ctor.options) { + Component.options.asyncData = Component._Ctor.options.asyncData + Component.options.fetch = Component._Ctor.options.fetch + } + }) + + // Apply transitions + this.setTransitions(mapTransitions(Components, to, from)) + + try { + // Call middleware + await callMiddleware.call(this, Components, app.context) + if (nextCalled) { + return + } + if (app.context._errored) { + return next() + } + + // Set layout + let layout = Components[0].options.layout + if (typeof layout === 'function') { + layout = layout(app.context) + } + layout = await this.loadLayout(layout) + + // Call middleware for layout + await callMiddleware.call(this, Components, app.context, layout) + if (nextCalled) { + return + } + if (app.context._errored) { + return next() + } + + // Call .validate() + let isValid = true + try { + for (const Component of Components) { + if (typeof Component.options.validate !== 'function') { + continue + } + + isValid = await Component.options.validate(app.context) + + if (!isValid) { + break + } + } + } catch (validationError) { + // ...If .validate() threw an error + this.error({ + statusCode: validationError.statusCode || '500', + message: validationError.message + }) + return next() + } + + // ...If .validate() returned false + if (!isValid) { + this.error({ statusCode: 404, message: 'This page could not be found' }) + return next() + } + + let instances + // Call asyncData & fetch hooks on components matched by the route. + await Promise.all(Components.map(async (Component, i) => { + // Check if only children route changed + Component._path = compile(to.matched[matches[i]].path)(to.params) + Component._dataRefresh = false + const childPathChanged = Component._path !== _lastPaths[i] + // Refresh component (call asyncData & fetch) when: + // Route path changed part includes current component + // Or route param changed part includes current component and watchParam is not `false` + // Or route query is changed and watchQuery returns `true` + if (this._routeChanged && childPathChanged) { + Component._dataRefresh = true + } else if (this._paramChanged && childPathChanged) { + const watchParam = Component.options.watchParam + Component._dataRefresh = watchParam !== false + } else if (this._queryChanged) { + const watchQuery = Component.options.watchQuery + if (watchQuery === true) { + Component._dataRefresh = true + } else if (Array.isArray(watchQuery)) { + Component._dataRefresh = watchQuery.some(key => this._diffQuery[key]) + } else if (typeof watchQuery === 'function') { + if (!instances) { + instances = getMatchedComponentsInstances(to) + } + Component._dataRefresh = watchQuery.apply(instances[i], [to.query, from.query]) + } + } + if (!this._hadError && this._isMounted && !Component._dataRefresh) { + return + } + + const promises = [] + + const hasAsyncData = ( + Component.options.asyncData && + typeof Component.options.asyncData === 'function' + ) + + const hasFetch = Boolean(Component.options.fetch) && Component.options.fetch.length + + const loadingIncrease = (hasAsyncData && hasFetch) ? 30 : 45 + + // Call asyncData(context) + if (hasAsyncData) { + const promise = promisify(Component.options.asyncData, app.context) + + promise.then((asyncDataResult) => { + applyAsyncData(Component, asyncDataResult) + + if (this.$loading.increase) { + this.$loading.increase(loadingIncrease) + } + }) + promises.push(promise) + } + + // Check disabled page loading + this.$loading.manual = Component.options.loading === false + + // Call fetch(context) + if (hasFetch) { + let p = Component.options.fetch(app.context) + if (!p || (!(p instanceof Promise) && (typeof p.then !== 'function'))) { + p = Promise.resolve(p) + } + p.then((fetchResult) => { + if (this.$loading.increase) { + this.$loading.increase(loadingIncrease) + } + }) + promises.push(p) + } + + return Promise.all(promises) + })) + + // If not redirected + if (!nextCalled) { + if (this.$loading.finish && !this.$loading.manual) { + this.$loading.finish() + } + + next() + } + } catch (err) { + const error = err || {} + if (error.message === 'ERR_REDIRECT') { + return this.$nuxt.$emit('routeChanged', to, from, error) + } + _lastPaths = [] + + globalHandleError(error) + + // Load error layout + let layout = (NuxtError.options || NuxtError).layout + if (typeof layout === 'function') { + layout = layout(app.context) + } + await this.loadLayout(layout) + + this.error(error) + this.$nuxt.$emit('routeChanged', to, from, error) + next() + } +} + +// Fix components format in matched, it's due to code-splitting of vue-router +function normalizeComponents (to, ___) { + flatMapComponents(to, (Component, _, match, key) => { + if (typeof Component === 'object' && !Component.options) { + // Updated via vue-router resolveAsyncComponents() + Component = Vue.extend(Component) + Component._Ctor = Component + match.components[key] = Component + } + return Component + }) +} + +function setLayoutForNextPage (to) { + // Set layout + let hasError = Boolean(this.$options.nuxt.err) + if (this._hadError && this._dateLastError === this.$options.nuxt.dateErr) { + hasError = false + } + let layout = hasError + ? (NuxtError.options || NuxtError).layout + : to.matched[0].components.default.options.layout + + if (typeof layout === 'function') { + layout = layout(app.context) + } + + this.setLayout(layout) +} + +function checkForErrors (app) { + // Hide error component if no error + if (app._hadError && app._dateLastError === app.$options.nuxt.dateErr) { + app.error() + } +} + +// When navigating on a different route but the same component is used, Vue.js +// Will not update the instance data, so we have to update $data ourselves +function fixPrepatch (to, ___) { + if (this._routeChanged === false && this._paramChanged === false && this._queryChanged === false) { + return + } + + const instances = getMatchedComponentsInstances(to) + const Components = getMatchedComponents(to) + + let triggerScroll = false + + Vue.nextTick(() => { + instances.forEach((instance, i) => { + if (!instance || instance._isDestroyed) { + return + } + + if ( + instance.constructor._dataRefresh && + Components[i] === instance.constructor && + instance.$vnode.data.keepAlive !== true && + typeof instance.constructor.options.data === 'function' + ) { + const newData = instance.constructor.options.data.call(instance) + for (const key in newData) { + Vue.set(instance.$data, key, newData[key]) + } + + triggerScroll = true + } + }) + + if (triggerScroll) { + // Ensure to trigger scroll event after calling scrollBehavior + window.$nuxt.$nextTick(() => { + window.$nuxt.$emit('triggerScroll') + }) + } + + checkForErrors(this) + + // Hot reloading + setTimeout(() => hotReloadAPI(this), 100) + }) +} + +function nuxtReady (_app) { + window.onNuxtReadyCbs.forEach((cb) => { + if (typeof cb === 'function') { + cb(_app) + } + }) + // Special JSDOM + if (typeof window._onNuxtLoaded === 'function') { + window._onNuxtLoaded(_app) + } + // Add router hooks + router.afterEach((to, from) => { + // Wait for fixPrepatch + $data updates + Vue.nextTick(() => _app.$nuxt.$emit('routeChanged', to, from)) + }) +} + +const noopData = () => { return {} } +const noopFetch = () => {} + +// Special hot reload with asyncData(context) +function getNuxtChildComponents ($parent, $components = []) { + $parent.$children.forEach(($child) => { + if ($child.$vnode && $child.$vnode.data.nuxtChild && !$components.find(c =>(c.$options.__file === $child.$options.__file))) { + $components.push($child) + } + if ($child.$children && $child.$children.length) { + getNuxtChildComponents($child, $components) + } + }) + + return $components +} + +function hotReloadAPI(_app) { + if (!module.hot) return + + let $components = getNuxtChildComponents(_app.$nuxt, []) + + $components.forEach(addHotReload.bind(_app)) +} + +function addHotReload ($component, depth) { + if ($component.$vnode.data._hasHotReload) return + $component.$vnode.data._hasHotReload = true + + var _forceUpdate = $component.$forceUpdate.bind($component.$parent) + + $component.$vnode.context.$forceUpdate = async () => { + let Components = getMatchedComponents(router.currentRoute) + let Component = Components[depth] + if (!Component) { + return _forceUpdate() + } + if (typeof Component === 'object' && !Component.options) { + // Updated via vue-router resolveAsyncComponents() + Component = Vue.extend(Component) + Component._Ctor = Component + } + this.error() + let promises = [] + const next = function (path) { + this.$loading.finish && this.$loading.finish() + router.push(path) + } + await setContext(app, { + route: router.currentRoute, + isHMR: true, + next: next.bind(this) + }) + const context = app.context + + if (this.$loading.start && !this.$loading.manual) { + this.$loading.start() + } + + callMiddleware.call(this, Components, context) + .then(() => { + // If layout changed + if (depth !== 0) { + return + } + + let layout = Component.options.layout || 'default' + if (typeof layout === 'function') { + layout = layout(context) + } + if (this.layoutName === layout) { + return + } + let promise = this.loadLayout(layout) + promise.then(() => { + this.setLayout(layout) + Vue.nextTick(() => hotReloadAPI(this)) + }) + return promise + }) + + .then(() => { + return callMiddleware.call(this, Components, context, this.layout) + }) + + .then(() => { + // Call asyncData(context) + let pAsyncData = promisify(Component.options.asyncData || noopData, context) + pAsyncData.then((asyncDataResult) => { + applyAsyncData(Component, asyncDataResult) + this.$loading.increase && this.$loading.increase(30) + }) + promises.push(pAsyncData) + + // Call fetch() + Component.options.fetch = Component.options.fetch || noopFetch + let pFetch = Component.options.fetch.length && Component.options.fetch(context) + if (!pFetch || (!(pFetch instanceof Promise) && (typeof pFetch.then !== 'function'))) { pFetch = Promise.resolve(pFetch) } + pFetch.then(() => this.$loading.increase && this.$loading.increase(30)) + promises.push(pFetch) + + return Promise.all(promises) + }) + .then(() => { + this.$loading.finish && this.$loading.finish() + _forceUpdate() + setTimeout(() => hotReloadAPI(this), 100) + }) + } +} + +async function mountApp (__app) { + // Set global variables + app = __app.app + router = __app.router + store = __app.store + + // Create Vue instance + const _app = new Vue(app) + + // Mounts Vue app to DOM element + const mount = () => { + _app.$mount('#app') + + // Add afterEach router hooks + router.afterEach(normalizeComponents) + + router.afterEach(setLayoutForNextPage.bind(_app)) + + router.afterEach(fixPrepatch.bind(_app)) + + // Listen for first Vue update + Vue.nextTick(() => { + // Call window.{{globals.readyCallback}} callbacks + nuxtReady(_app) + + // Enable hot reloading + hotReloadAPI(_app) + }) + } + + // Resolve route components + const Components = await Promise.all(resolveComponents(app.context.route)) + + // Enable transitions + _app.setTransitions = _app.$options.nuxt.setTransitions.bind(_app) + if (Components.length) { + _app.setTransitions(mapTransitions(Components, router.currentRoute)) + _lastPaths = router.currentRoute.matched.map(route => compile(route.path)(router.currentRoute.params)) + } + + // Initialize error handler + _app.$loading = {} // To avoid error while _app.$nuxt does not exist + if (NUXT.error) { + _app.error(NUXT.error) + } + + // Add beforeEach router hooks + router.beforeEach(loadAsyncComponents.bind(_app)) + router.beforeEach(render.bind(_app)) + + // Fix in static: remove trailing slash to force hydration + // Full static, if server-rendered: hydrate, to allow custom redirect to generated page + + // Fix in static: remove trailing slash to force hydration + if (NUXT.serverRendered && isSamePath(NUXT.routePath, _app.context.route.path)) { + return mount() + } + + // First render on client-side + const clientFirstMount = () => { + normalizeComponents(router.currentRoute, router.currentRoute) + setLayoutForNextPage.call(_app, router.currentRoute) + checkForErrors(_app) + // Don't call fixPrepatch.call(_app, router.currentRoute, router.currentRoute) since it's first render + mount() + } + + // fix: force next tick to avoid having same timestamp when an error happen on spa fallback + await new Promise(resolve => setTimeout(resolve, 0)) + render.call(_app, router.currentRoute, router.currentRoute, (path) => { + // If not redirected + if (!path) { + clientFirstMount() + return + } + + // Add a one-time afterEach hook to + // mount the app wait for redirect and route gets resolved + const unregisterHook = router.afterEach((to, from) => { + unregisterHook() + clientFirstMount() + }) + + // Push the path and let route to be resolved + router.push(path, undefined, (err) => { + if (err) { + errorHandler(err) + } + }) + }) +} diff --git a/shell/nuxt/components/nuxt-build-indicator.vue b/shell/nuxt/components/nuxt-build-indicator.vue new file mode 100644 index 0000000000..913f5448d3 --- /dev/null +++ b/shell/nuxt/components/nuxt-build-indicator.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/shell/nuxt/components/nuxt-child.js b/shell/nuxt/components/nuxt-child.js new file mode 100644 index 0000000000..af27a4d349 --- /dev/null +++ b/shell/nuxt/components/nuxt-child.js @@ -0,0 +1,122 @@ + +export default { + name: 'NuxtChild', + functional: true, + props: { + nuxtChildKey: { + type: String, + default: '' + }, + keepAlive: Boolean, + keepAliveProps: { + type: Object, + default: undefined + } + }, + render (_, { parent, data, props }) { + const h = parent.$createElement + + data.nuxtChild = true + const _parent = parent + const transitions = parent.$nuxt.nuxt.transitions + const defaultTransition = parent.$nuxt.nuxt.defaultTransition + + let depth = 0 + while (parent) { + if (parent.$vnode && parent.$vnode.data.nuxtChild) { + depth++ + } + parent = parent.$parent + } + data.nuxtChildDepth = depth + const transition = transitions[depth] || defaultTransition + const transitionProps = {} + transitionsKeys.forEach((key) => { + if (typeof transition[key] !== 'undefined') { + transitionProps[key] = transition[key] + } + }) + + const listeners = {} + listenersKeys.forEach((key) => { + if (typeof transition[key] === 'function') { + listeners[key] = transition[key].bind(_parent) + } + }) + if (process.client) { + // Add triggerScroll event on beforeEnter (fix #1376) + const beforeEnter = listeners.beforeEnter + listeners.beforeEnter = (el) => { + // Ensure to trigger scroll event after calling scrollBehavior + window.$nuxt.$nextTick(() => { + window.$nuxt.$emit('triggerScroll') + }) + if (beforeEnter) { + return beforeEnter.call(_parent, el) + } + } + } + + // make sure that leave is called asynchronous (fix #5703) + if (transition.css === false) { + const leave = listeners.leave + + // only add leave listener when user didnt provide one + // or when it misses the done argument + if (!leave || leave.length < 2) { + listeners.leave = (el, done) => { + if (leave) { + leave.call(_parent, el) + } + + _parent.$nextTick(done) + } + } + } + + let routerView = h('routerView', data) + + if (props.keepAlive) { + routerView = h('keep-alive', { props: props.keepAliveProps }, [routerView]) + } + + return h('transition', { + props: transitionProps, + on: listeners + }, [routerView]) + } +} + +const transitionsKeys = [ + 'name', + 'mode', + 'appear', + 'css', + 'type', + 'duration', + 'enterClass', + 'leaveClass', + 'appearClass', + 'enterActiveClass', + 'enterActiveClass', + 'leaveActiveClass', + 'appearActiveClass', + 'enterToClass', + 'leaveToClass', + 'appearToClass' +] + +const listenersKeys = [ + 'beforeEnter', + 'enter', + 'afterEnter', + 'enterCancelled', + 'beforeLeave', + 'leave', + 'afterLeave', + 'leaveCancelled', + 'beforeAppear', + 'appear', + 'afterAppear', + 'appearCancelled' +] diff --git a/shell/nuxt/components/nuxt-error.vue b/shell/nuxt/components/nuxt-error.vue new file mode 100644 index 0000000000..e71f7d0168 --- /dev/null +++ b/shell/nuxt/components/nuxt-error.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/shell/nuxt/components/nuxt-link.client.js b/shell/nuxt/components/nuxt-link.client.js new file mode 100644 index 0000000000..13e5dd0eb9 --- /dev/null +++ b/shell/nuxt/components/nuxt-link.client.js @@ -0,0 +1,98 @@ +import Vue from 'vue' + +const requestIdleCallback = window.requestIdleCallback || + function (cb) { + const start = Date.now() + return setTimeout(function () { + cb({ + didTimeout: false, + timeRemaining: () => Math.max(0, 50 - (Date.now() - start)) + }) + }, 1) + } + +const cancelIdleCallback = window.cancelIdleCallback || function (id) { + clearTimeout(id) +} + +const observer = window.IntersectionObserver && new window.IntersectionObserver((entries) => { + entries.forEach(({ intersectionRatio, target: link }) => { + if (intersectionRatio <= 0 || !link.__prefetch) { + return + } + link.__prefetch() + }) +}) + +export default { + name: 'NuxtLink', + extends: Vue.component('RouterLink'), + props: { + prefetch: { + type: Boolean, + default: false + }, + noPrefetch: { + type: Boolean, + default: false + } + }, + mounted () { + if (this.prefetch && !this.noPrefetch) { + this.handleId = requestIdleCallback(this.observe, { timeout: 2e3 }) + } + }, + beforeDestroy () { + cancelIdleCallback(this.handleId) + + if (this.__observed) { + observer.unobserve(this.$el) + delete this.$el.__prefetch + } + }, + methods: { + observe () { + // If no IntersectionObserver, avoid prefetching + if (!observer) { + return + } + // Add to observer + if (this.shouldPrefetch()) { + this.$el.__prefetch = this.prefetchLink.bind(this) + observer.observe(this.$el) + this.__observed = true + } + }, + shouldPrefetch () { + return this.getPrefetchComponents().length > 0 + }, + canPrefetch () { + const conn = navigator.connection + const hasBadConnection = this.$nuxt.isOffline || (conn && ((conn.effectiveType || '').includes('2g') || conn.saveData)) + + return !hasBadConnection + }, + getPrefetchComponents () { + const ref = this.$router.resolve(this.to, this.$route, this.append) + const Components = ref.resolved.matched.map(r => r.components.default) + + return Components.filter(Component => typeof Component === 'function' && !Component.options && !Component.__prefetched) + }, + prefetchLink () { + if (!this.canPrefetch()) { + return + } + // Stop observing this link (in case of internet connection changes) + observer.unobserve(this.$el) + const Components = this.getPrefetchComponents() + + for (const Component of Components) { + const componentOrPromise = Component() + if (componentOrPromise instanceof Promise) { + componentOrPromise.catch(() => {}) + } + Component.__prefetched = true + } + } + } +} diff --git a/shell/nuxt/components/nuxt-link.server.js b/shell/nuxt/components/nuxt-link.server.js new file mode 100644 index 0000000000..b54df35119 --- /dev/null +++ b/shell/nuxt/components/nuxt-link.server.js @@ -0,0 +1,16 @@ +import Vue from 'vue' + +export default { + name: 'NuxtLink', + extends: Vue.component('RouterLink'), + props: { + prefetch: { + type: Boolean, + default: false + }, + noPrefetch: { + type: Boolean, + default: false + } + } +} diff --git a/shell/nuxt/components/nuxt-loading.vue b/shell/nuxt/components/nuxt-loading.vue new file mode 100644 index 0000000000..45b913085c --- /dev/null +++ b/shell/nuxt/components/nuxt-loading.vue @@ -0,0 +1,154 @@ + diff --git a/shell/nuxt/components/nuxt.js b/shell/nuxt/components/nuxt.js new file mode 100644 index 0000000000..8f4210e1f4 --- /dev/null +++ b/shell/nuxt/components/nuxt.js @@ -0,0 +1,101 @@ +import Vue from 'vue' +import { compile } from '../utils' + +import NuxtError from '../../layouts/error.vue' + +import NuxtChild from './nuxt-child' + +export default { + name: 'Nuxt', + components: { + NuxtChild, + NuxtError + }, + props: { + nuxtChildKey: { + type: String, + default: undefined + }, + keepAlive: Boolean, + keepAliveProps: { + type: Object, + default: undefined + }, + name: { + type: String, + default: 'default' + } + }, + errorCaptured (error) { + // if we receive and error while showing the NuxtError component + // capture the error and force an immediate update so we re-render + // without the NuxtError component + if (this.displayingNuxtError) { + this.errorFromNuxtError = error + this.$forceUpdate() + } + }, + computed: { + routerViewKey () { + // If nuxtChildKey prop is given or current route has children + if (typeof this.nuxtChildKey !== 'undefined' || this.$route.matched.length > 1) { + return this.nuxtChildKey || compile(this.$route.matched[0].path)(this.$route.params) + } + + const [matchedRoute] = this.$route.matched + + if (!matchedRoute) { + return this.$route.path + } + + const Component = matchedRoute.components.default + + if (Component && Component.options) { + const { options } = Component + + if (options.key) { + return (typeof options.key === 'function' ? options.key(this.$route) : options.key) + } + } + + const strict = /\/$/.test(matchedRoute.path) + return strict ? this.$route.path : this.$route.path.replace(/\/$/, '') + } + }, + beforeCreate () { + Vue.util.defineReactive(this, 'nuxt', this.$root.$options.nuxt) + }, + render (h) { + // if there is no error + if (!this.nuxt.err) { + // Directly return nuxt child + return h('NuxtChild', { + key: this.routerViewKey, + props: this.$props + }) + } + + // if an error occurred within NuxtError show a simple + // error message instead to prevent looping + if (this.errorFromNuxtError) { + this.$nextTick(() => (this.errorFromNuxtError = false)) + + return h('div', {}, [ + h('h2', 'An error occurred while showing the error page'), + h('p', 'Unfortunately an error occurred and while showing the error page another error occurred'), + h('p', `Error details: ${this.errorFromNuxtError.toString()}`), + h('nuxt-link', { props: { to: '/' } }, 'Go back to home') + ]) + } + + // track if we are showing the NuxtError component + this.displayingNuxtError = true + this.$nextTick(() => (this.displayingNuxtError = false)) + + return h(NuxtError, { + props: { + error: this.nuxt.err + } + }) + } +} diff --git a/shell/nuxt/cookie-universal-nuxt.js b/shell/nuxt/cookie-universal-nuxt.js new file mode 100644 index 0000000000..b5abdafcdc --- /dev/null +++ b/shell/nuxt/cookie-universal-nuxt.js @@ -0,0 +1,9 @@ +import cookieUniversal from 'cookie-universal' + +export default ({ req, res }, inject) => { + const options = { + "alias": "cookies", + "parseJSON": true +} + inject(options.alias, cookieUniversal(req, res, options.parseJSON)) +} diff --git a/shell/nuxt/empty.js b/shell/nuxt/empty.js new file mode 100644 index 0000000000..a3ac0d8486 --- /dev/null +++ b/shell/nuxt/empty.js @@ -0,0 +1 @@ +// This file is intentionally left empty for noop aliases diff --git a/shell/nuxt/index.js b/shell/nuxt/index.js new file mode 100644 index 0000000000..a3e543c5b0 --- /dev/null +++ b/shell/nuxt/index.js @@ -0,0 +1,364 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import Meta from 'vue-meta'; +import ClientOnly from 'vue-client-only'; +import NoSsr from 'vue-no-ssr'; +import { createRouter } from './router.js'; +import NuxtChild from './components/nuxt-child.js'; +import NuxtError from '../layouts/error.vue'; +import Nuxt from './components/nuxt.js'; +import App from './App.js'; +import { setContext, getLocation, getRouteData, normalizeError } from './utils'; +import { createStore } from './store.js'; + +/* Plugins */ + +import './portal-vue.js'; +import cookieUniversalNuxt from './cookie-universal-nuxt.js'; +import axios from './axios.js'; +import plugins from '../core/plugins.js'; +import pluginsLoader from '../core/plugins-loader.js'; +import axiosShell from '../plugins/axios'; +import '../plugins/tooltip'; +import '../plugins/vue-clipboard2'; +import '../plugins/v-select'; +import '../plugins/directives'; +import '../plugins/transitions'; +import '../plugins/vue-js-modal'; +import '../plugins/js-yaml'; +import '../plugins/resize'; +import '../plugins/shortkey'; +import '../plugins/i18n'; +import '../plugins/global-formatters'; +import '../plugins/trim-whitespace'; +import '../plugins/extend-router'; + +import consolePlugin from '../plugins/console'; +import intNumber from '../plugins/int-number'; +import nuxtClientInit from '../plugins/nuxt-client-init'; +import replaceAll from '../plugins/replaceall'; +import backButton from '../plugins/back-button'; +import plugin from '../plugins/plugin'; +import codeMirror from '../plugins/codemirror-loader'; +import '../plugins/formatters'; +import version from '../plugins/version'; +import steveCreateWorker from '../plugins/steve-create-worker'; + +// Component: +Vue.component(ClientOnly.name, ClientOnly); + +// TODO: Remove in Nuxt 3: +Vue.component(NoSsr.name, { + ...NoSsr, + render(h, ctx) { + if (process.client && !NoSsr._warned) { + NoSsr._warned = true; + + console.warn(' has been deprecated and will be removed in Nuxt 3, please use instead'); + } + + return NoSsr.render(h, ctx); + } +}); + +// Component: +Vue.component(NuxtChild.name, NuxtChild); +Vue.component('NChild', NuxtChild); + +// Component NuxtLink is imported in server.js or client.js + +// Component: +Vue.component(Nuxt.name, Nuxt); + +Object.defineProperty(Vue.prototype, '$nuxt', { + get() { + const globalNuxt = this.$root.$options.$nuxt; + + if (process.client && !globalNuxt && typeof window !== 'undefined') { + return window.$nuxt; + } + + return globalNuxt; + }, + configurable: true +}); + +Vue.use(Meta, { + keyName: 'head', attribute: 'data-n-head', ssrAttribute: 'data-n-head-ssr', tagIDKeyName: 'hid' +}); + +const defaultTransition = { + name: 'page', mode: 'out-in', appear: true, appearClass: 'appear', appearActiveClass: 'appear-active', appearToClass: 'appear-to' +}; + +const originalRegisterModule = Vuex.Store.prototype.registerModule; + +function registerModule(path, rawModule, options = {}) { + const preserveState = process.client && ( + Array.isArray(path) ? !!path.reduce((namespacedState, path) => namespacedState && namespacedState[path], this.state) : path in this.state + ); + + return originalRegisterModule.call(this, path, rawModule, { preserveState, ...options }); +} + +async function createApp(ssrContext, config = {}) { + const router = await createRouter(ssrContext, config); + + const store = createStore(ssrContext); + + // Add this.$router into store actions/mutations + store.$router = router; + + // Create Root instance + + // here we inject the router and store to all child components, + // making them available everywhere as `this.$router` and `this.$store`. + const app = { + head: {"title":"dashboard","meta":[{"charset":"utf-8"},{"name":"viewport","content":"width=device-width, initial-scale=1"},{"hid":"description","name":"description","content":"Rancher Dashboard"}],"link":[{"hid":"icon","rel":"icon","type":"image\u002Fx-icon","href":"\u002Ffavicon.png"}],"style":[],"script":[]}, + + store, + router, + nuxt: { + defaultTransition, + transitions: [defaultTransition], + setTransitions(transitions) { + if (!Array.isArray(transitions)) { + transitions = [transitions]; + } + transitions = transitions.map((transition) => { + if (!transition) { + transition = defaultTransition; + } else if (typeof transition === 'string') { + transition = Object.assign({}, defaultTransition, { name: transition }); + } else { + transition = Object.assign({}, defaultTransition, transition); + } + + return transition; + }); + this.$options.nuxt.transitions = transitions; + + return transitions; + }, + + err: null, + dateErr: null, + error(err) { + err = err || null; + app.context._errored = Boolean(err); + err = err ? normalizeError(err) : null; + let nuxt = app.nuxt; // to work with @vue/composition-api, see https://github.com/nuxt/nuxt.js/issues/6517#issuecomment-573280207 + + if (this) { + nuxt = this.nuxt || this.$options.nuxt; + } + nuxt.dateErr = Date.now(); + nuxt.err = err; + // Used in src/server.js + if (ssrContext) { + ssrContext.nuxt.error = err; + } + + return err; + } + }, + ...App + }; + + // Make app available into store via this.app + store.app = app; + + const next = ssrContext ? ssrContext.next : location => app.router.push(location); + // Resolve route + let route; + + if (ssrContext) { + route = router.resolve(ssrContext.url).route; + } else { + const path = getLocation(router.options.base, router.options.mode); + + route = router.resolve(path).route; + } + + // Set context to app.context + await setContext(app, { + store, + route, + next, + error: app.nuxt.error.bind(app), + payload: ssrContext ? ssrContext.payload : undefined, + req: ssrContext ? ssrContext.req : undefined, + res: ssrContext ? ssrContext.res : undefined, + beforeRenderFns: ssrContext ? ssrContext.beforeRenderFns : undefined, + ssrContext + }); + + function inject(key, value) { + if (!key) { + throw new Error('inject(key, value) has no key provided'); + } + if (value === undefined) { + throw new Error(`inject('${ key }', value) has no value provided`); + } + + key = `$${ key }`; + // Add into app + app[key] = value; + // Add into context + if (!app.context[key]) { + app.context[key] = value; + } + + // Add into store + store[key] = app[key]; + + // Check if plugin not already installed + const installKey = `__nuxt_${ key }_installed__`; + + if (Vue[installKey]) { + return; + } + Vue[installKey] = true; + // Call Vue.use() to install the plugin into vm + Vue.use(() => { + if (!Object.prototype.hasOwnProperty.call(Vue.prototype, key)) { + Object.defineProperty(Vue.prototype, key, { + get() { + return this.$root.$options[key]; + } + }); + } + }); + } + + // Inject runtime config as $config + inject('config', config); + + if (process.client) { + // Replace store state before plugins execution + if (window.__NUXT__ && window.__NUXT__.state) { + store.replaceState(window.__NUXT__.state); + } + } + + // Add enablePreview(previewData = {}) in context for plugins + if (process.static && process.client) { + app.context.enablePreview = function(previewData = {}) { + app.previewData = Object.assign({}, previewData); + inject('preview', previewData); + }; + } + // Plugin execution + + // if (typeof nuxt_plugin_portalvue_6babae27 === 'function') { + // await nuxt_plugin_portalvue_6babae27(app.context, inject); + // } + + if (typeof cookieUniversalNuxt === 'function') { + await cookieUniversalNuxt(app.context, inject); + } + + if (typeof axios === 'function') { + await axios(app.context, inject); + } + + if (typeof plugins === 'function') { + await plugins(app.context, inject); + } + + if (typeof pluginsLoader === 'function') { + await pluginsLoader(app.context, inject); + } + + if (typeof axiosShell === 'function') { + await axiosShell(app.context, inject); + } + + if (process.client && typeof consolePlugin === 'function') { + await consolePlugin(app.context, inject); + } + + if (process.client && typeof intNumber === 'function') { + await intNumber(app.context, inject); + } + + if (process.client && typeof nuxtClientInit === 'function') { + await nuxtClientInit(app.context, inject); + } + + if (typeof replaceAll === 'function') { + await replaceAll(app.context, inject); + } + + if (typeof backButton === 'function') { + await backButton(app.context, inject); + } + + if (process.client && typeof plugin === 'function') { + await plugin(app.context, inject); + } + + if (process.client && typeof codeMirror === 'function') { + await codeMirror(app.context, inject); + } + + if (process.client && typeof version === 'function') { + await version(app.context, inject); + } + + if (process.client && typeof steveCreateWorker === 'function') { + await steveCreateWorker(app.context, inject); + } + + // if (process.client && typeof formatters === 'function') { + // await formatters(app.context, inject); + // } + + // Lock enablePreview in context + if (process.static && process.client) { + app.context.enablePreview = function() { + console.warn('You cannot call enablePreview() outside a plugin.'); + }; + } + + // Wait for async component to be resolved first + await new Promise((resolve, reject) => { + // Ignore 404s rather than blindly replacing URL in browser + if (process.client) { + const { route } = router.resolve(app.context.route.fullPath); + + if (!route.matched.length) { + return resolve(); + } + } + router.replace(app.context.route.fullPath, resolve, (err) => { + // https://github.com/vuejs/vue-router/blob/v3.4.3/src/util/errors.js + if (!err._isRouter) { + return reject(err); + } + if (err.type !== 2 /* NavigationFailureType.redirected */) { + return resolve(); + } + + // navigated to a different route in router guard + const unregister = router.afterEach(async(to, from) => { + if (process.server && ssrContext && ssrContext.url) { + ssrContext.url = to.fullPath; + } + app.context.route = await getRouteData(to); + app.context.params = to.params || {}; + app.context.query = to.query || {}; + unregister(); + resolve(); + }); + }); + }); + + return { + store, + app, + router + }; +} + +export { createApp, NuxtError }; diff --git a/shell/nuxt/jsonp.js b/shell/nuxt/jsonp.js new file mode 100644 index 0000000000..702adf2189 --- /dev/null +++ b/shell/nuxt/jsonp.js @@ -0,0 +1,82 @@ +const chunks = {} // chunkId => exports +const chunksInstalling = {} // chunkId => Promise +const failedChunks = {} + +function importChunk(chunkId, src) { + // Already installed + if (chunks[chunkId]) { + return Promise.resolve(chunks[chunkId]) + } + + // Failed loading + if (failedChunks[chunkId]) { + return Promise.reject(failedChunks[chunkId]) + } + + // Installing + if (chunksInstalling[chunkId]) { + return chunksInstalling[chunkId] + } + + // Set a promise in chunk cache + let resolve, reject + const promise = chunksInstalling[chunkId] = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + + // Clear chunk data from cache + delete chunks[chunkId] + + // Start chunk loading + const script = document.createElement('script') + script.charset = 'utf-8' + script.timeout = 120 + script.src = src + let timeout + + // Create error before stack unwound to get useful stacktrace later + const error = new Error() + + // Complete handlers + const onScriptComplete = script.onerror = script.onload = (event) => { + // Cleanups + clearTimeout(timeout) + delete chunksInstalling[chunkId] + + // Avoid mem leaks in IE + script.onerror = script.onload = null + + // Verify chunk is loaded + if (chunks[chunkId]) { + return resolve(chunks[chunkId]) + } + + // Something bad happened + const errorType = event && (event.type === 'load' ? 'missing' : event.type) + const realSrc = event && event.target && event.target.src + error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')' + error.name = 'ChunkLoadError' + error.type = errorType + error.request = realSrc + failedChunks[chunkId] = error + reject(error) + } + + // Timeout + timeout = setTimeout(() => { + onScriptComplete({ type: 'timeout', target: script }) + }, 120000) + + // Append script + document.head.appendChild(script) + + // Return promise + return promise +} + +export function installJsonp() { + window.__NUXT_JSONP__ = function (chunkId, exports) { chunks[chunkId] = exports } + window.__NUXT_JSONP_CACHE__ = chunks + window.__NUXT_IMPORT__ = importChunk +} diff --git a/shell/nuxt/loading.html b/shell/nuxt/loading.html new file mode 100644 index 0000000000..5eb6a06120 --- /dev/null +++ b/shell/nuxt/loading.html @@ -0,0 +1,39 @@ + +
+ +
diff --git a/shell/nuxt/middleware.js b/shell/nuxt/middleware.js new file mode 100644 index 0000000000..4da667797f --- /dev/null +++ b/shell/nuxt/middleware.js @@ -0,0 +1,12 @@ +const middleware = {} + +middleware['authenticated'] = require('../middleware/authenticated.js') +middleware['authenticated'] = middleware['authenticated'].default || middleware['authenticated'] + +middleware['i18n'] = require('../middleware/i18n.js') +middleware['i18n'] = middleware['i18n'].default || middleware['i18n'] + +middleware['unauthenticated'] = require('../middleware/unauthenticated.js') +middleware['unauthenticated'] = middleware['unauthenticated'].default || middleware['unauthenticated'] + +export default middleware diff --git a/shell/nuxt/mixins/fetch.client.js b/shell/nuxt/mixins/fetch.client.js new file mode 100644 index 0000000000..017e559e76 --- /dev/null +++ b/shell/nuxt/mixins/fetch.client.js @@ -0,0 +1,90 @@ +import Vue from 'vue' +import { hasFetch, normalizeError, addLifecycleHook, createGetCounter } from '../utils' + +const isSsrHydration = (vm) => vm.$vnode && vm.$vnode.elm && vm.$vnode.elm.dataset && vm.$vnode.elm.dataset.fetchKey +const nuxtState = window.__NUXT__ + +export default { + beforeCreate () { + if (!hasFetch(this)) { + return + } + + this._fetchDelay = typeof this.$options.fetchDelay === 'number' ? this.$options.fetchDelay : 200 + + Vue.util.defineReactive(this, '$fetchState', { + pending: false, + error: null, + timestamp: Date.now() + }) + + this.$fetch = $fetch.bind(this) + addLifecycleHook(this, 'created', created) + addLifecycleHook(this, 'beforeMount', beforeMount) + } +} + +function beforeMount() { + if (!this._hydrated) { + return this.$fetch() + } +} + +function created() { + if (!isSsrHydration(this)) { + return + } + + // Hydrate component + this._hydrated = true + this._fetchKey = this.$vnode.elm.dataset.fetchKey + const data = nuxtState.fetch[this._fetchKey] + + // If fetch error + if (data && data._error) { + this.$fetchState.error = data._error + return + } + + // Merge data + for (const key in data) { + Vue.set(this.$data, key, data[key]) + } +} + +function $fetch() { + if (!this._fetchPromise) { + this._fetchPromise = $_fetch.call(this) + .then(() => { delete this._fetchPromise }) + } + return this._fetchPromise +} + +async function $_fetch() { + this.$nuxt.nbFetching++ + this.$fetchState.pending = true + this.$fetchState.error = null + this._hydrated = false + let error = null + const startTime = Date.now() + + try { + await this.$options.fetch.call(this) + } catch (err) { + if (process.dev) { + console.error('Error in fetch():', err) + } + error = normalizeError(err) + } + + const delayLeft = this._fetchDelay - (Date.now() - startTime) + if (delayLeft > 0) { + await new Promise(resolve => setTimeout(resolve, delayLeft)) + } + + this.$fetchState.error = error + this.$fetchState.pending = false + this.$fetchState.timestamp = Date.now() + + this.$nextTick(() => this.$nuxt.nbFetching--) +} diff --git a/shell/nuxt/mixins/fetch.server.js b/shell/nuxt/mixins/fetch.server.js new file mode 100644 index 0000000000..24a00686f1 --- /dev/null +++ b/shell/nuxt/mixins/fetch.server.js @@ -0,0 +1,69 @@ +import Vue from 'vue' +import { hasFetch, normalizeError, addLifecycleHook, purifyData, createGetCounter } from '../utils' + +async function serverPrefetch() { + if (!this._fetchOnServer) { + return + } + + // Call and await on $fetch + try { + await this.$options.fetch.call(this) + } catch (err) { + if (process.dev) { + console.error('Error in fetch():', err) + } + this.$fetchState.error = normalizeError(err) + } + this.$fetchState.pending = false + + // Define an ssrKey for hydration + this._fetchKey = this._fetchKey || this.$ssrContext.fetchCounters['']++ + + // Add data-fetch-key on parent element of Component + const attrs = this.$vnode.data.attrs = this.$vnode.data.attrs || {} + attrs['data-fetch-key'] = this._fetchKey + + // Add to ssrContext for window.__NUXT__.fetch + + if (this.$ssrContext.nuxt.fetch[this._fetchKey] !== undefined) { + console.warn(`Duplicate fetch key detected (${this._fetchKey}). This may lead to unexpected results.`) + } + + this.$ssrContext.nuxt.fetch[this._fetchKey] = + this.$fetchState.error ? { _error: this.$fetchState.error } : purifyData(this._data) +} + +export default { + created() { + if (!hasFetch(this)) { + return + } + + if (typeof this.$options.fetchOnServer === 'function') { + this._fetchOnServer = this.$options.fetchOnServer.call(this) !== false + } else { + this._fetchOnServer = this.$options.fetchOnServer !== false + } + + const defaultKey = this.$options._scopeId || this.$options.name || '' + const getCounter = createGetCounter(this.$ssrContext.fetchCounters, defaultKey) + + if (typeof this.$options.fetchKey === 'function') { + this._fetchKey = this.$options.fetchKey.call(this, getCounter) + } else { + const key = 'string' === typeof this.$options.fetchKey ? this.$options.fetchKey : defaultKey + this._fetchKey = key ? key + ':' + getCounter(key) : String(getCounter(key)) + } + + // Added for remove vue undefined warning while ssr + this.$fetch = () => {} // issue #8043 + Vue.util.defineReactive(this, '$fetchState', { + pending: true, + error: null, + timestamp: Date.now() + }) + + addLifecycleHook(this, 'serverPrefetch', serverPrefetch) + } +} diff --git a/shell/nuxt/portal-vue.js b/shell/nuxt/portal-vue.js new file mode 100644 index 0000000000..439021bc7d --- /dev/null +++ b/shell/nuxt/portal-vue.js @@ -0,0 +1,4 @@ +import Vue from 'vue' +import PortalVue from 'portal-vue' + +Vue.use(PortalVue) diff --git a/shell/nuxt/router.js b/shell/nuxt/router.js new file mode 100644 index 0000000000..a1b88e8bc6 --- /dev/null +++ b/shell/nuxt/router.js @@ -0,0 +1,503 @@ +import Vue from 'vue' +import Router from 'vue-router' +import { normalizeURL, decode } from 'ufo' +import { interopDefault } from './utils' +import scrollBehavior from './router.scrollBehavior.js' + +const _b1075e64 = () => interopDefault(import('../pages/about.vue' /* webpackChunkName: "pages/about" */)) +const _99a1a59e = () => interopDefault(import('../pages/account/index.vue' /* webpackChunkName: "pages/account/index" */)) +const _10f92ab2 = () => interopDefault(import('../pages/c/index.vue' /* webpackChunkName: "pages/c/index" */)) +const _0dd7368b = () => interopDefault(import('../pages/clusters/index.vue' /* webpackChunkName: "pages/clusters/index" */)) +const _a0a6a994 = () => interopDefault(import('../pages/diagnostic.vue' /* webpackChunkName: "pages/diagnostic" */)) +const _6249d7c9 = () => interopDefault(import('../pages/fail-whale.vue' /* webpackChunkName: "pages/fail-whale" */)) +const _8d3a64a4 = () => interopDefault(import('../pages/home.vue' /* webpackChunkName: "pages/home" */)) +const _aa26f31e = () => interopDefault(import('../pages/prefs.vue' /* webpackChunkName: "pages/prefs" */)) +const _11f3a39f = () => interopDefault(import('../pages/safeMode.vue' /* webpackChunkName: "pages/safeMode" */)) +const _35190053 = () => interopDefault(import('../pages/support/index.vue' /* webpackChunkName: "pages/support/index" */)) +const _da172982 = () => interopDefault(import('../pages/account/create-key.vue' /* webpackChunkName: "pages/account/create-key" */)) +const _35d0be51 = () => interopDefault(import('../pages/auth/login.vue' /* webpackChunkName: "pages/auth/login" */)) +const _5d4fd75c = () => interopDefault(import('../pages/auth/logout.vue' /* webpackChunkName: "pages/auth/logout" */)) +const _0da94136 = () => interopDefault(import('../pages/auth/setup.vue' /* webpackChunkName: "pages/auth/setup" */)) +const _9d01107e = () => interopDefault(import('../pages/auth/verify.vue' /* webpackChunkName: "pages/auth/verify" */)) +const _52164cec = () => interopDefault(import('../pages/docs/toc.js' /* webpackChunkName: "pages/docs/toc" */)) +const _06776753 = () => interopDefault(import('../pages/rio/mesh.vue' /* webpackChunkName: "pages/rio/mesh" */)) +const _2992430e = () => interopDefault(import('../pages/c/_cluster/index.vue' /* webpackChunkName: "pages/c/_cluster/index" */)) +const _71a3608e = () => interopDefault(import('../pages/docs/_doc.vue' /* webpackChunkName: "pages/docs/_doc" */)) +const _5efe405e = () => interopDefault(import('../pages/c/_cluster/apps/index.vue' /* webpackChunkName: "pages/c/_cluster/apps/index" */)) +const _7eff6fd8 = () => interopDefault(import('../pages/c/_cluster/auth/index.vue' /* webpackChunkName: "pages/c/_cluster/auth/index" */)) +const _20a6f76e = () => interopDefault(import('../pages/c/_cluster/backup/index.vue' /* webpackChunkName: "pages/c/_cluster/backup/index" */)) +const _ece6af92 = () => interopDefault(import('../pages/c/_cluster/cis/index.vue' /* webpackChunkName: "pages/c/_cluster/cis/index" */)) +const _89e6b70e = () => interopDefault(import('../pages/c/_cluster/ecm/index.vue' /* webpackChunkName: "pages/c/_cluster/ecm/index" */)) +const _0561a16b = () => interopDefault(import('../pages/c/_cluster/explorer/index.vue' /* webpackChunkName: "pages/c/_cluster/explorer/index" */)) +const _9f4d6090 = () => interopDefault(import('../pages/c/_cluster/fleet/index.vue' /* webpackChunkName: "pages/c/_cluster/fleet/index" */)) +const _0fa0d22e = () => interopDefault(import('../pages/c/_cluster/gatekeeper/index.vue' /* webpackChunkName: "pages/c/_cluster/gatekeeper/index" */)) +const _1af88b1a = () => interopDefault(import('../pages/c/_cluster/istio/index.vue' /* webpackChunkName: "pages/c/_cluster/istio/index" */)) +const _3facd8b5 = () => interopDefault(import('../pages/c/_cluster/legacy/index.vue' /* webpackChunkName: "pages/c/_cluster/legacy/index" */)) +const _24bc84c9 = () => interopDefault(import('../pages/c/_cluster/logging/index.vue' /* webpackChunkName: "pages/c/_cluster/logging/index" */)) +const _22d2372b = () => interopDefault(import('../pages/c/_cluster/longhorn/index.vue' /* webpackChunkName: "pages/c/_cluster/longhorn/index" */)) +const _e66f80d2 = () => interopDefault(import('../pages/c/_cluster/manager/index.vue' /* webpackChunkName: "pages/c/_cluster/manager/index" */)) +const _02abbf34 = () => interopDefault(import('../pages/c/_cluster/mcapps/index.vue' /* webpackChunkName: "pages/c/_cluster/mcapps/index" */)) +const _e1577418 = () => interopDefault(import('../pages/c/_cluster/monitoring/index.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/index" */)) +const _4954338b = () => interopDefault(import('../pages/c/_cluster/neuvector/index.vue' /* webpackChunkName: "pages/c/_cluster/neuvector/index" */)) +const _86270f62 = () => interopDefault(import('../pages/c/_cluster/settings/index.vue' /* webpackChunkName: "pages/c/_cluster/settings/index" */)) +const _0afff7f6 = () => interopDefault(import('../pages/c/_cluster/uiplugins/index.vue' /* webpackChunkName: "pages/c/_cluster/uiplugins/index" */)) +const _3cd56cbc = () => interopDefault(import('../pages/c/_cluster/apps/charts/index.vue' /* webpackChunkName: "pages/c/_cluster/apps/charts/index" */)) +const _11b0721a = () => interopDefault(import('../pages/c/_cluster/auth/config/index.vue' /* webpackChunkName: "pages/c/_cluster/auth/config/index" */)) +const _25aea87c = () => interopDefault(import('../pages/c/_cluster/auth/roles/index.vue' /* webpackChunkName: "pages/c/_cluster/auth/roles/index" */)) +const _74341dba = () => interopDefault(import('../pages/c/_cluster/explorer/ConfigBadge.vue' /* webpackChunkName: "pages/c/_cluster/explorer/ConfigBadge" */)) +const _14bdf46e = () => interopDefault(import('../pages/c/_cluster/explorer/EventsTable.vue' /* webpackChunkName: "pages/c/_cluster/explorer/EventsTable" */)) +const _a75fe116 = () => interopDefault(import('../pages/c/_cluster/explorer/explorer-utils.js' /* webpackChunkName: "pages/c/_cluster/explorer/explorer-utils" */)) +const _01865512 = () => interopDefault(import('../pages/c/_cluster/explorer/tools/index.vue' /* webpackChunkName: "pages/c/_cluster/explorer/tools/index" */)) +const _9c418d0e = () => interopDefault(import('../pages/c/_cluster/fleet/GitRepoGraphConfig.js' /* webpackChunkName: "pages/c/_cluster/fleet/GitRepoGraphConfig" */)) +const _f1812060 = () => interopDefault(import('../pages/c/_cluster/gatekeeper/constraints/index.vue' /* webpackChunkName: "pages/c/_cluster/gatekeeper/constraints/index" */)) +const _b539bb82 = () => interopDefault(import('../pages/c/_cluster/legacy/project/index.vue' /* webpackChunkName: "pages/c/_cluster/legacy/project/index" */)) +const _44fb97b4 = () => interopDefault(import('../pages/c/_cluster/manager/cloudCredential/index.vue' /* webpackChunkName: "pages/c/_cluster/manager/cloudCredential/index" */)) +const _17ce10e4 = () => interopDefault(import('../pages/c/_cluster/monitoring/alertmanagerconfig/index.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/alertmanagerconfig/index" */)) +const _57f0357f = () => interopDefault(import('../pages/c/_cluster/monitoring/monitor/index.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/monitor/index" */)) +const _acf430f8 = () => interopDefault(import('../pages/c/_cluster/monitoring/route-receiver/index.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/route-receiver/index" */)) +const _9c1862f8 = () => interopDefault(import('../pages/c/_cluster/settings/banners.vue' /* webpackChunkName: "pages/c/_cluster/settings/banners" */)) +const _83bd8ef8 = () => interopDefault(import('../pages/c/_cluster/settings/brand.vue' /* webpackChunkName: "pages/c/_cluster/settings/brand" */)) +const _6ace98ec = () => interopDefault(import('../pages/c/_cluster/settings/DefaultLinksEditor.vue' /* webpackChunkName: "shell/pages/c/_cluster/settings/DefaultLinksEditor" */)) +const _e56e5894 = () => interopDefault(import('../pages/c/_cluster/settings/links.vue' /* webpackChunkName: "pages/c/_cluster/settings/links" */)) +const _0ff4c0ed = () => interopDefault(import('../pages/c/_cluster/settings/performance.vue' /* webpackChunkName: "pages/c/_cluster/settings/performance" */)) +const _978f0576 = () => interopDefault(import('../pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue' /* webpackChunkName: "pages/c/_cluster/uiplugins/DeveloperInstallDialog" */)) +const _256d9147 = () => interopDefault(import('../pages/c/_cluster/uiplugins/InstallDialog.vue' /* webpackChunkName: "pages/c/_cluster/uiplugins/InstallDialog" */)) +const _f6d8b8f2 = () => interopDefault(import('../pages/c/_cluster/uiplugins/PluginInfoPanel.vue' /* webpackChunkName: "pages/c/_cluster/uiplugins/PluginInfoPanel" */)) +const _33e16f6c = () => interopDefault(import('../pages/c/_cluster/uiplugins/RemoveUIPlugins.vue' /* webpackChunkName: "pages/c/_cluster/uiplugins/RemoveUIPlugins" */)) +const _d7c9a08a = () => interopDefault(import('../pages/c/_cluster/uiplugins/SetupUIPlugins.vue' /* webpackChunkName: "pages/c/_cluster/uiplugins/SetupUIPlugins" */)) +const _266ebcce = () => interopDefault(import('../pages/c/_cluster/uiplugins/UninstallDialog.vue' /* webpackChunkName: "pages/c/_cluster/uiplugins/UninstallDialog" */)) +const _69909470 = () => interopDefault(import('../pages/c/_cluster/apps/charts/chart.vue' /* webpackChunkName: "pages/c/_cluster/apps/charts/chart" */)) +const _685cdef6 = () => interopDefault(import('../pages/c/_cluster/apps/charts/install.vue' /* webpackChunkName: "pages/c/_cluster/apps/charts/install" */)) +const _5e92cb4c = () => interopDefault(import('../pages/c/_cluster/auth/group.principal/assign-edit.vue' /* webpackChunkName: "pages/c/_cluster/auth/group.principal/assign-edit" */)) +const _97480d04 = () => interopDefault(import('../pages/c/_cluster/legacy/project/pipelines.vue' /* webpackChunkName: "pages/c/_cluster/legacy/project/pipelines" */)) +const _fba8acec = () => interopDefault(import('../pages/c/_cluster/manager/cloudCredential/create.vue' /* webpackChunkName: "pages/c/_cluster/manager/cloudCredential/create" */)) +const _646a75c2 = () => interopDefault(import('../pages/c/_cluster/monitoring/monitor/create.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/monitor/create" */)) +const _a229588c = () => interopDefault(import('../pages/c/_cluster/monitoring/route-receiver/create.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/route-receiver/create" */)) +const _24eaabc8 = () => interopDefault(import('../pages/c/_cluster/explorer/tools/pages/_page.vue' /* webpackChunkName: "pages/c/_cluster/explorer/tools/pages/_page" */)) +const _77d0ea1b = () => interopDefault(import('../pages/c/_cluster/auth/config/_id.vue' /* webpackChunkName: "pages/c/_cluster/auth/config/_id" */)) +const _58626ef4 = () => interopDefault(import('../pages/c/_cluster/legacy/pages/_page.vue' /* webpackChunkName: "pages/c/_cluster/legacy/pages/_page" */)) +const _0e2466db = () => interopDefault(import('../pages/c/_cluster/legacy/project/_page.vue' /* webpackChunkName: "pages/c/_cluster/legacy/project/_page" */)) +const _5b9811c8 = () => interopDefault(import('../pages/c/_cluster/manager/cloudCredential/_id.vue' /* webpackChunkName: "pages/c/_cluster/manager/cloudCredential/_id" */)) +const _0f85fde8 = () => interopDefault(import('../pages/c/_cluster/manager/pages/_page.vue' /* webpackChunkName: "pages/c/_cluster/manager/pages/_page" */)) +const _107ed876 = () => interopDefault(import('../pages/c/_cluster/mcapps/pages/_page.vue' /* webpackChunkName: "pages/c/_cluster/mcapps/pages/_page" */)) +const _f12c4b3c = () => interopDefault(import('../pages/c/_cluster/monitoring/alertmanagerconfig/_alertmanagerconfigid/index.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/alertmanagerconfig/_alertmanagerconfigid/index" */)) +const _25affaec = () => interopDefault(import('../pages/c/_cluster/monitoring/route-receiver/_id.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/route-receiver/_id" */)) +const _0e9569c4 = () => interopDefault(import('../pages/c/_cluster/auth/roles/_resource/create.vue' /* webpackChunkName: "pages/c/_cluster/auth/roles/_resource/create" */)) +const _0d1a9be2 = () => interopDefault(import('../pages/c/_cluster/monitoring/alertmanagerconfig/_alertmanagerconfigid/receiver.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/alertmanagerconfig/_alertmanagerconfigid/receiver" */)) +const _1643de08 = () => interopDefault(import('../pages/c/_cluster/auth/roles/_resource/_id.vue' /* webpackChunkName: "pages/c/_cluster/auth/roles/_resource/_id" */)) +const _30293caa = () => interopDefault(import('../pages/c/_cluster/monitoring/monitor/_namespace/_id.vue' /* webpackChunkName: "pages/c/_cluster/monitoring/monitor/_namespace/_id" */)) +const _3dcc28a0 = () => interopDefault(import('../pages/c/_cluster/navlinks/_group.vue' /* webpackChunkName: "pages/c/_cluster/navlinks/_group" */)) +const _327638c8 = () => interopDefault(import('../pages/c/_cluster/_product/index.vue' /* webpackChunkName: "pages/c/_cluster/_product/index" */)) +const _3feb57b4 = () => interopDefault(import('../pages/c/_cluster/_product/members/index.vue' /* webpackChunkName: "pages/c/_cluster/_product/members/index" */)) +const _2e2d89c4 = () => interopDefault(import('../pages/c/_cluster/_product/namespaces.vue' /* webpackChunkName: "pages/c/_cluster/_product/namespaces" */)) +const _76d90818 = () => interopDefault(import('../pages/c/_cluster/_product/projectsnamespaces.vue' /* webpackChunkName: "pages/c/_cluster/_product/projectsnamespaces" */)) +const _587122fa = () => interopDefault(import('../pages/c/_cluster/_product/_resource/index.vue' /* webpackChunkName: "pages/c/_cluster/_product/_resource/index" */)) +const _4530f1f8 = () => interopDefault(import('../pages/c/_cluster/_product/_resource/create.vue' /* webpackChunkName: "pages/c/_cluster/_product/_resource/create" */)) +const _ac00c83c = () => interopDefault(import('../pages/c/_cluster/_product/_resource/_id.vue' /* webpackChunkName: "pages/c/_cluster/_product/_resource/_id" */)) +const _1cac498f = () => interopDefault(import('../pages/c/_cluster/_product/_resource/_namespace/_id.vue' /* webpackChunkName: "pages/c/_cluster/_product/_resource/_namespace/_id" */)) +const _7197a8da = () => interopDefault(import('../pages/index.vue' /* webpackChunkName: "pages/index" */)) + +const emptyFn = () => {} + +Vue.use(Router) + +export const routerOptions = { + mode: 'history', + base: '/', + linkActiveClass: 'nuxt-link-active', + linkExactActiveClass: 'nuxt-link-exact-active', + scrollBehavior, + + routes: [{ + path: "/about", + component: _b1075e64, + name: "about" + }, { + path: "/account", + component: _99a1a59e, + name: "account" + }, { + path: "/c", + component: _10f92ab2, + name: "c" + }, { + path: "/clusters", + component: _0dd7368b, + name: "clusters" + }, { + path: "/diagnostic", + component: _a0a6a994, + name: "diagnostic" + }, { + path: "/fail-whale", + component: _6249d7c9, + name: "fail-whale" + }, { + path: "/home", + component: _8d3a64a4, + name: "home" + }, { + path: "/prefs", + component: _aa26f31e, + name: "prefs" + }, { + path: "/safeMode", + component: _11f3a39f, + name: "safeMode" + }, { + path: "/support", + component: _35190053, + name: "support" + }, { + path: "/account/create-key", + component: _da172982, + name: "account-create-key" + }, { + path: "/auth/login", + component: _35d0be51, + name: "auth-login" + }, { + path: "/auth/logout", + component: _5d4fd75c, + name: "auth-logout" + }, { + path: "/auth/setup", + component: _0da94136, + name: "auth-setup" + }, { + path: "/auth/verify", + component: _9d01107e, + name: "auth-verify" + }, { + path: "/docs/toc", + component: _52164cec, + name: "docs-toc" + }, { + path: "/rio/mesh", + component: _06776753, + name: "rio-mesh" + }, { + path: "/c/:cluster", + component: _2992430e, + name: "c-cluster" + }, { + path: "/docs/:doc?", + component: _71a3608e, + name: "docs-doc" + }, { + path: "/c/:cluster/apps", + component: _5efe405e, + name: "c-cluster-apps" + }, { + path: "/c/:cluster/auth", + component: _7eff6fd8, + name: "c-cluster-auth" + }, { + path: "/c/:cluster/backup", + component: _20a6f76e, + name: "c-cluster-backup" + }, { + path: "/c/:cluster/cis", + component: _ece6af92, + name: "c-cluster-cis" + }, { + path: "/c/:cluster/ecm", + component: _89e6b70e, + name: "c-cluster-ecm" + }, { + path: "/c/:cluster/explorer", + component: _0561a16b, + name: "c-cluster-explorer" + }, { + path: "/c/:cluster/fleet", + component: _9f4d6090, + name: "c-cluster-fleet" + }, { + path: "/c/:cluster/gatekeeper", + component: _0fa0d22e, + name: "c-cluster-gatekeeper" + }, { + path: "/c/:cluster/istio", + component: _1af88b1a, + name: "c-cluster-istio" + }, { + path: "/c/:cluster/legacy", + component: _3facd8b5, + name: "c-cluster-legacy" + }, { + path: "/c/:cluster/logging", + component: _24bc84c9, + name: "c-cluster-logging" + }, { + path: "/c/:cluster/longhorn", + component: _22d2372b, + name: "c-cluster-longhorn" + }, { + path: "/c/:cluster/manager", + component: _e66f80d2, + name: "c-cluster-manager" + }, { + path: "/c/:cluster/mcapps", + component: _02abbf34, + name: "c-cluster-mcapps" + }, { + path: "/c/:cluster/monitoring", + component: _e1577418, + name: "c-cluster-monitoring" + }, { + path: "/c/:cluster/neuvector", + component: _4954338b, + name: "c-cluster-neuvector" + }, { + path: "/c/:cluster/settings", + component: _86270f62, + name: "c-cluster-settings" + }, { + path: "/c/:cluster/uiplugins", + component: _0afff7f6, + name: "c-cluster-uiplugins" + }, { + path: "/c/:cluster/apps/charts", + component: _3cd56cbc, + name: "c-cluster-apps-charts" + }, { + path: "/c/:cluster/auth/config", + component: _11b0721a, + name: "c-cluster-auth-config" + }, { + path: "/c/:cluster/auth/roles", + component: _25aea87c, + name: "c-cluster-auth-roles" + }, { + path: "/c/:cluster/explorer/ConfigBadge", + component: _74341dba, + name: "c-cluster-explorer-ConfigBadge" + }, { + path: "/c/:cluster/explorer/EventsTable", + component: _14bdf46e, + name: "c-cluster-explorer-EventsTable" + }, { + path: "/c/:cluster/explorer/explorer-utils", + component: _a75fe116, + name: "c-cluster-explorer-explorer-utils" + }, { + path: "/c/:cluster/explorer/tools", + component: _01865512, + name: "c-cluster-explorer-tools" + }, { + path: "/c/:cluster/fleet/GitRepoGraphConfig", + component: _9c418d0e, + name: "c-cluster-fleet-GitRepoGraphConfig" + }, { + path: "/c/:cluster/gatekeeper/constraints", + component: _f1812060, + name: "c-cluster-gatekeeper-constraints" + }, { + path: "/c/:cluster/legacy/project", + component: _b539bb82, + name: "c-cluster-legacy-project" + }, { + path: "/c/:cluster/manager/cloudCredential", + component: _44fb97b4, + name: "c-cluster-manager-cloudCredential" + }, { + path: "/c/:cluster/monitoring/alertmanagerconfig", + component: _17ce10e4, + name: "c-cluster-monitoring-alertmanagerconfig" + }, { + path: "/c/:cluster/monitoring/monitor", + component: _57f0357f, + name: "c-cluster-monitoring-monitor" + }, { + path: "/c/:cluster/monitoring/route-receiver", + component: _acf430f8, + name: "c-cluster-monitoring-route-receiver" + }, { + path: "/c/:cluster/settings/banners", + component: _9c1862f8, + name: "c-cluster-settings-banners" + }, { + path: "/c/:cluster/settings/brand", + component: _83bd8ef8, + name: "c-cluster-settings-brand" + }, { + path: "/c/:cluster/settings/DefaultLinksEditor", + component: _6ace98ec, + name: "c-cluster-settings-DefaultLinksEditor" + }, { + path: "/c/:cluster/settings/links", + component: _e56e5894, + name: "c-cluster-settings-links" + }, { + path: "/c/:cluster/settings/performance", + component: _0ff4c0ed, + name: "c-cluster-settings-performance" + }, { + path: "/c/:cluster/uiplugins/DeveloperInstallDialog", + component: _978f0576, + name: "c-cluster-uiplugins-DeveloperInstallDialog" + }, { + path: "/c/:cluster/uiplugins/InstallDialog", + component: _256d9147, + name: "c-cluster-uiplugins-InstallDialog" + }, { + path: "/c/:cluster/uiplugins/PluginInfoPanel", + component: _f6d8b8f2, + name: "c-cluster-uiplugins-PluginInfoPanel" + }, { + path: "/c/:cluster/uiplugins/RemoveUIPlugins", + component: _33e16f6c, + name: "c-cluster-uiplugins-RemoveUIPlugins" + }, { + path: "/c/:cluster/uiplugins/SetupUIPlugins", + component: _d7c9a08a, + name: "c-cluster-uiplugins-SetupUIPlugins" + }, { + path: "/c/:cluster/uiplugins/UninstallDialog", + component: _266ebcce, + name: "c-cluster-uiplugins-UninstallDialog" + }, { + path: "/c/:cluster/apps/charts/chart", + component: _69909470, + name: "c-cluster-apps-charts-chart" + }, { + path: "/c/:cluster/apps/charts/install", + component: _685cdef6, + name: "c-cluster-apps-charts-install" + }, { + path: "/c/:cluster/auth/group.principal/assign-edit", + component: _5e92cb4c, + name: "c-cluster-auth-group.principal-assign-edit" + }, { + path: "/c/:cluster/legacy/project/pipelines", + component: _97480d04, + name: "c-cluster-legacy-project-pipelines" + }, { + path: "/c/:cluster/manager/cloudCredential/create", + component: _fba8acec, + name: "c-cluster-manager-cloudCredential-create" + }, { + path: "/c/:cluster/monitoring/monitor/create", + component: _646a75c2, + name: "c-cluster-monitoring-monitor-create" + }, { + path: "/c/:cluster/monitoring/route-receiver/create", + component: _a229588c, + name: "c-cluster-monitoring-route-receiver-create" + }, { + path: "/c/:cluster/explorer/tools/pages/:page?", + component: _24eaabc8, + name: "c-cluster-explorer-tools-pages-page" + }, { + path: "/c/:cluster/auth/config/:id", + component: _77d0ea1b, + name: "c-cluster-auth-config-id" + }, { + path: "/c/:cluster/legacy/pages/:page?", + component: _58626ef4, + name: "c-cluster-legacy-pages-page" + }, { + path: "/c/:cluster/legacy/project/:page", + component: _0e2466db, + name: "c-cluster-legacy-project-page" + }, { + path: "/c/:cluster/manager/cloudCredential/:id", + component: _5b9811c8, + name: "c-cluster-manager-cloudCredential-id" + }, { + path: "/c/:cluster/manager/pages/:page?", + component: _0f85fde8, + name: "c-cluster-manager-pages-page" + }, { + path: "/c/:cluster/mcapps/pages/:page?", + component: _107ed876, + name: "c-cluster-mcapps-pages-page" + }, { + path: "/c/:cluster/monitoring/alertmanagerconfig/:alertmanagerconfigid", + component: _f12c4b3c, + name: "c-cluster-monitoring-alertmanagerconfig-alertmanagerconfigid" + }, { + path: "/c/:cluster/monitoring/route-receiver/:id?", + component: _25affaec, + name: "c-cluster-monitoring-route-receiver-id" + }, { + path: "/c/:cluster/auth/roles/:resource/create", + component: _0e9569c4, + name: "c-cluster-auth-roles-resource-create" + }, { + path: "/c/:cluster/monitoring/alertmanagerconfig/:alertmanagerconfigid/receiver", + component: _0d1a9be2, + name: "c-cluster-monitoring-alertmanagerconfig-alertmanagerconfigid-receiver" + }, { + path: "/c/:cluster/auth/roles/:resource/:id?", + component: _1643de08, + name: "c-cluster-auth-roles-resource-id" + }, { + path: "/c/:cluster/monitoring/monitor/:namespace/:id?", + component: _30293caa, + name: "c-cluster-monitoring-monitor-namespace-id" + }, { + path: "/c/:cluster/navlinks/:group?", + component: _3dcc28a0, + name: "c-cluster-navlinks-group" + }, { + path: "/c/:cluster/:product", + component: _327638c8, + name: "c-cluster-product" + }, { + path: "/c/:cluster/:product/members", + component: _3feb57b4, + name: "c-cluster-product-members" + }, { + path: "/c/:cluster/:product/namespaces", + component: _2e2d89c4, + name: "c-cluster-product-namespaces" + }, { + path: "/c/:cluster/:product/projectsnamespaces", + component: _76d90818, + name: "c-cluster-product-projectsnamespaces" + }, { + path: "/c/:cluster/:product/:resource", + component: _587122fa, + name: "c-cluster-product-resource" + }, { + path: "/c/:cluster/:product/:resource/create", + component: _4530f1f8, + name: "c-cluster-product-resource-create" + }, { + path: "/c/:cluster/:product/:resource/:id", + component: _ac00c83c, + name: "c-cluster-product-resource-id" + }, { + path: "/c/:cluster/:product/:resource/:namespace/:id?", + component: _1cac498f, + name: "c-cluster-product-resource-namespace-id" + }, { + path: "/", + component: _7197a8da, + name: "index" + }], + + fallback: false +} + +export function createRouter (ssrContext, config) { + const base = (config._app && config._app.basePath) || routerOptions.base + const router = new Router({ ...routerOptions, base }) + + // TODO: remove in Nuxt 3 + const originalPush = router.push + router.push = function push (location, onComplete = emptyFn, onAbort) { + return originalPush.call(this, location, onComplete, onAbort) + } + + const resolve = router.resolve.bind(router) + router.resolve = (to, current, append) => { + if (typeof to === 'string') { + to = normalizeURL(to) + } + return resolve(to, current, append) + } + + return router +} diff --git a/shell/nuxt/router.scrollBehavior.js b/shell/nuxt/router.scrollBehavior.js new file mode 100644 index 0000000000..4d42dcc7a1 --- /dev/null +++ b/shell/nuxt/router.scrollBehavior.js @@ -0,0 +1,76 @@ +import { getMatchedComponents, setScrollRestoration } from './utils' + +if (process.client) { + if ('scrollRestoration' in window.history) { + setScrollRestoration('manual') + + // reset scrollRestoration to auto when leaving page, allowing page reload + // and back-navigation from other pages to use the browser to restore the + // scrolling position. + window.addEventListener('beforeunload', () => { + setScrollRestoration('auto') + }) + + // Setting scrollRestoration to manual again when returning to this page. + window.addEventListener('load', () => { + setScrollRestoration('manual') + }) + } +} + +function shouldScrollToTop(route) { + const Pages = getMatchedComponents(route) + if (Pages.length === 1) { + const { options = {} } = Pages[0] + return options.scrollToTop !== false + } + return Pages.some(({ options }) => options && options.scrollToTop) +} + +export default function (to, from, savedPosition) { + // If the returned position is falsy or an empty object, will retain current scroll position + let position = false + const isRouteChanged = to !== from + + // savedPosition is only available for popstate navigations (back button) + if (savedPosition) { + position = savedPosition + } else if (isRouteChanged && shouldScrollToTop(to)) { + position = { x: 0, y: 0 } + } + + const nuxt = window.$nuxt + + if ( + // Initial load (vuejs/vue-router#3199) + !isRouteChanged || + // Route hash changes + (to.path === from.path && to.hash !== from.hash) + ) { + nuxt.$nextTick(() => nuxt.$emit('triggerScroll')) + } + + return new Promise((resolve) => { + // wait for the out transition to complete (if necessary) + nuxt.$once('triggerScroll', () => { + // coords will be used if no selector is provided, + // or if the selector didn't match any element. + if (to.hash) { + let hash = to.hash + // CSS.escape() is not supported with IE and Edge. + if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') { + hash = '#' + window.CSS.escape(hash.substr(1)) + } + try { + if (document.querySelector(hash)) { + // scroll to anchor by returning the selector + position = { selector: hash } + } + } catch (e) { + console.warn('Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).') + } + } + resolve(position) + }) + }) +} diff --git a/shell/nuxt/server.js b/shell/nuxt/server.js new file mode 100644 index 0000000000..58813dec07 --- /dev/null +++ b/shell/nuxt/server.js @@ -0,0 +1,312 @@ +import Vue from 'vue' +import { joinURL, normalizeURL, withQuery } from 'ufo' +import fetch from 'node-fetch' +import middleware from './middleware.js' +import { + applyAsyncData, + middlewareSeries, + sanitizeComponent, + getMatchedComponents, + promisify +} from './utils.js' +import fetchMixin from './mixins/fetch.server' +import { createApp, NuxtError } from './index.js' +import NuxtLink from './components/nuxt-link.server.js' // should be included after ./index.js + +// Update serverPrefetch strategy +Vue.config.optionMergeStrategies.serverPrefetch = Vue.config.optionMergeStrategies.created + +// Fetch mixin +if (!Vue.__nuxt__fetch__mixin__) { + Vue.mixin(fetchMixin) + Vue.__nuxt__fetch__mixin__ = true +} + +if (!Vue.__original_use__) { + Vue.__original_use__ = Vue.use + Vue.__install_times__ = 0 + Vue.use = function (plugin, ...args) { + plugin.__nuxt_external_installed__ = Vue._installedPlugins.includes(plugin) + return Vue.__original_use__(plugin, ...args) + } +} +if (Vue.__install_times__ === 2) { + Vue.__install_times__ = 0 + Vue._installedPlugins = Vue._installedPlugins.filter(plugin => { + return plugin.__nuxt_external_installed__ === true + }) +} +Vue.__install_times__++ + +// Component: +Vue.component(NuxtLink.name, NuxtLink) +Vue.component('NLink', NuxtLink) + +if (!global.fetch) { global.fetch = fetch } + +const noopApp = () => new Vue({ render: h => h('div', { domProps: { id: '__nuxt' } }) }) + +const createNext = ssrContext => (opts) => { + // If static target, render on client-side + ssrContext.redirected = opts + if (ssrContext.target === 'static' || !ssrContext.res) { + ssrContext.nuxt.serverRendered = false + return + } + let fullPath = withQuery(opts.path, opts.query) + const $config = ssrContext.runtimeConfig || {} + const routerBase = ($config._app && $config._app.basePath) || '/' + if (!fullPath.startsWith('http') && (routerBase !== '/' && !fullPath.startsWith(routerBase))) { + fullPath = joinURL(routerBase, fullPath) + } + // Avoid loop redirect + if (decodeURI(fullPath) === decodeURI(ssrContext.url)) { + ssrContext.redirected = false + return + } + ssrContext.res.writeHead(opts.status, { + Location: normalizeURL(fullPath) + }) + ssrContext.res.end() +} + +// This exported function will be called by `bundleRenderer`. +// This is where we perform data-prefetching to determine the +// state of our application before actually rendering it. +// Since data fetching is async, this function is expected to +// return a Promise that resolves to the app instance. +export default async (ssrContext) => { + // Create ssrContext.next for simulate next() of beforeEach() when wanted to redirect + ssrContext.redirected = false + ssrContext.next = createNext(ssrContext) + // Used for beforeNuxtRender({ Components, nuxtState }) + ssrContext.beforeRenderFns = [] + // Nuxt object (window.{{globals.context}}, defaults to window.__NUXT__) + ssrContext.nuxt = { layout: 'default', data: [], fetch: {}, error: null, state: null, serverRendered: true, routePath: '' } + + ssrContext.fetchCounters = {} + + // Remove query from url is static target + + // Public runtime config + ssrContext.nuxt.config = ssrContext.runtimeConfig.public + if (ssrContext.nuxt.config._app) { + __webpack_public_path__ = joinURL(ssrContext.nuxt.config._app.cdnURL, ssrContext.nuxt.config._app.assetsPath) + } + // Create the app definition and the instance (created for each request) + const { app, router, store } = await createApp(ssrContext, ssrContext.runtimeConfig.private) + const _app = new Vue(app) + // Add ssr route path to nuxt context so we can account for page navigation between ssr and csr + ssrContext.nuxt.routePath = app.context.route.path + + // Add meta infos (used in renderer.js) + ssrContext.meta = _app.$meta() + + // Keep asyncData for each matched component in ssrContext (used in app/utils.js via this.$ssrContext) + ssrContext.asyncData = {} + + const beforeRender = async () => { + // Call beforeNuxtRender() methods + await Promise.all(ssrContext.beforeRenderFns.map(fn => promisify(fn, { Components, nuxtState: ssrContext.nuxt }))) + + ssrContext.rendered = () => { + // Add the state from the vuex store + ssrContext.nuxt.state = store.state + } + } + + const renderErrorPage = async () => { + // Don't server-render the page in static target + if (ssrContext.target === 'static') { + ssrContext.nuxt.serverRendered = false + } + + // Load layout for error page + const layout = (NuxtError.options || NuxtError).layout + const errLayout = typeof layout === 'function' ? layout.call(NuxtError, app.context) : layout + ssrContext.nuxt.layout = errLayout || 'default' + await _app.loadLayout(errLayout) + _app.setLayout(errLayout) + + await beforeRender() + return _app + } + const render404Page = () => { + app.context.error({ statusCode: 404, path: ssrContext.url, message: 'This page could not be found' }) + return renderErrorPage() + } + + const s = Date.now() + + // Components are already resolved by setContext -> getRouteData (app/utils.js) + const Components = getMatchedComponents(app.context.route) + + /* + ** Dispatch store nuxtServerInit + */ + if (store._actions && store._actions.nuxtServerInit) { + try { + await store.dispatch('nuxtServerInit', app.context) + } catch (err) { + console.debug('Error occurred when calling nuxtServerInit: ', err.message) + throw err + } + } + // ...If there is a redirect or an error, stop the process + if (ssrContext.redirected) { + return noopApp() + } + if (ssrContext.nuxt.error) { + return renderErrorPage() + } + + /* + ** Call global middleware (nuxt.config.js) + */ + let midd = ["i18n"] + midd = midd.map((name) => { + if (typeof name === 'function') { + return name + } + if (typeof middleware[name] !== 'function') { + app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name }) + } + return middleware[name] + }) + await middlewareSeries(midd, app.context) + // ...If there is a redirect or an error, stop the process + if (ssrContext.redirected) { + return noopApp() + } + if (ssrContext.nuxt.error) { + return renderErrorPage() + } + + /* + ** Set layout + */ + let layout = Components.length ? Components[0].options.layout : NuxtError.layout + if (typeof layout === 'function') { + layout = layout(app.context) + } + await _app.loadLayout(layout) + if (ssrContext.nuxt.error) { + return renderErrorPage() + } + layout = _app.setLayout(layout) + ssrContext.nuxt.layout = _app.layoutName + + /* + ** Call middleware (layout + pages) + */ + midd = [] + + layout = sanitizeComponent(layout) + if (layout.options.middleware) { + midd = midd.concat(layout.options.middleware) + } + + Components.forEach((Component) => { + if (Component.options.middleware) { + midd = midd.concat(Component.options.middleware) + } + }) + midd = midd.map((name) => { + if (typeof name === 'function') { + return name + } + if (typeof middleware[name] !== 'function') { + app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name }) + } + return middleware[name] + }) + await middlewareSeries(midd, app.context) + // ...If there is a redirect or an error, stop the process + if (ssrContext.redirected) { + return noopApp() + } + if (ssrContext.nuxt.error) { + return renderErrorPage() + } + + /* + ** Call .validate() + */ + let isValid = true + try { + for (const Component of Components) { + if (typeof Component.options.validate !== 'function') { + continue + } + + isValid = await Component.options.validate(app.context) + + if (!isValid) { + break + } + } + } catch (validationError) { + // ...If .validate() threw an error + app.context.error({ + statusCode: validationError.statusCode || '500', + message: validationError.message + }) + return renderErrorPage() + } + + // ...If .validate() returned false + if (!isValid) { + // Render a 404 error page + return render404Page() + } + + // If no Components found, returns 404 + if (!Components.length) { + return render404Page() + } + + // Call asyncData & fetch hooks on components matched by the route. + const asyncDatas = await Promise.all(Components.map((Component) => { + const promises = [] + + // Call asyncData(context) + if (Component.options.asyncData && typeof Component.options.asyncData === 'function') { + const promise = promisify(Component.options.asyncData, app.context) + promise.then((asyncDataResult) => { + ssrContext.asyncData[Component.cid] = asyncDataResult + applyAsyncData(Component) + return asyncDataResult + }) + promises.push(promise) + } else { + promises.push(null) + } + + // Call fetch(context) + if (Component.options.fetch && Component.options.fetch.length) { + promises.push(Component.options.fetch(app.context)) + } else { + promises.push(null) + } + + return Promise.all(promises) + })) + + if (process.env.DEBUG && asyncDatas.length) console.debug('Data fetching ' + ssrContext.url + ': ' + (Date.now() - s) + 'ms') + + // datas are the first row of each + ssrContext.nuxt.data = asyncDatas.map(r => r[0] || {}) + + // ...If there is a redirect or an error, stop the process + if (ssrContext.redirected) { + return noopApp() + } + if (ssrContext.nuxt.error) { + return renderErrorPage() + } + + // Call beforeNuxtRender methods & add store state + await beforeRender() + + return _app +} diff --git a/shell/nuxt/store.js b/shell/nuxt/store.js new file mode 100644 index 0000000000..e429f9bb79 --- /dev/null +++ b/shell/nuxt/store.js @@ -0,0 +1,178 @@ +import Vue from 'vue' +import Vuex from 'vuex' + +Vue.use(Vuex) + +const VUEX_PROPERTIES = ['state', 'getters', 'actions', 'mutations'] + +let store = {}; + +(function updateModules () { + store = normalizeRoot(require('../store/index.js'), 'store/index.js') + + // If store is an exported method = classic mode (deprecated) + + if (typeof store === 'function') { + return console.warn('Classic mode for store/ is deprecated and will be removed in Nuxt 3.') + } + + // Enforce store modules + store.modules = store.modules || {} + + resolveStoreModules(require('../store/action-menu.js'), 'action-menu.js') + resolveStoreModules(require('../store/auth.js'), 'auth.js') + resolveStoreModules(require('../store/aws.js'), 'aws.js') + resolveStoreModules(require('../store/catalog.js'), 'catalog.js') + resolveStoreModules(require('../store/digitalocean.js'), 'digitalocean.js') + resolveStoreModules(require('../store/features.js'), 'features.js') + resolveStoreModules(require('../store/github.js'), 'github.js') + resolveStoreModules(require('../store/growl.js'), 'growl.js') + resolveStoreModules(require('../store/i18n.js'), 'i18n.js') + resolveStoreModules(require('../store/linode.js'), 'linode.js') + resolveStoreModules(require('../store/plugins.js'), 'plugins.js') + resolveStoreModules(require('../store/pnap.js'), 'pnap.js') + resolveStoreModules(require('../store/prefs.js'), 'prefs.js') + resolveStoreModules(require('../store/resource-fetch.js'), 'resource-fetch.js') + resolveStoreModules(require('../store/type-map.js'), 'type-map.js') + resolveStoreModules(require('../store/uiplugins.ts'), 'uiplugins.ts') + resolveStoreModules(require('../store/wm.js'), 'wm.js') + + // If the environment supports hot reloading... + + if (process.client && module.hot) { + // Whenever any Vuex module is updated... + module.hot.accept([ + '../store/action-menu.js', + '../store/auth.js', + '../store/aws.js', + '../store/catalog.js', + '../store/digitalocean.js', + '../store/features.js', + '../store/github.js', + '../store/growl.js', + '../store/i18n.js', + '../store/index.js', + '../store/linode.js', + '../store/plugins.js', + '../store/pnap.js', + '../store/prefs.js', + '../store/resource-fetch.js', + '../store/type-map.js', + '../store/uiplugins.ts', + '../store/wm.js', + ], () => { + // Update `root.modules` with the latest definitions. + updateModules() + // Trigger a hot update in the store. + window.$nuxt.$store.hotUpdate(store) + }) + } +})() + +// createStore +export const createStore = store instanceof Function ? store : () => { + return new Vuex.Store(Object.assign({ + strict: (process.env.NODE_ENV !== 'production') + }, store)) +} + +function normalizeRoot (moduleData, filePath) { + moduleData = moduleData.default || moduleData + + if (moduleData.commit) { + throw new Error(`[nuxt] ${filePath} should export a method that returns a Vuex instance.`) + } + + if (typeof moduleData !== 'function') { + // Avoid TypeError: setting a property that has only a getter when overwriting top level keys + moduleData = Object.assign({}, moduleData) + } + return normalizeModule(moduleData, filePath) +} + +function normalizeModule (moduleData, filePath) { + if (moduleData.state && typeof moduleData.state !== 'function') { + console.warn(`'state' should be a method that returns an object in ${filePath}`) + + const state = Object.assign({}, moduleData.state) + // Avoid TypeError: setting a property that has only a getter when overwriting top level keys + moduleData = Object.assign({}, moduleData, { state: () => state }) + } + return moduleData +} + +function resolveStoreModules (moduleData, filename) { + moduleData = moduleData.default || moduleData + // Remove store src + extension (./foo/index.js -> foo/index) + const namespace = filename.replace(/\.(js|mjs|ts)$/, '') + const namespaces = namespace.split('/') + let moduleName = namespaces[namespaces.length - 1] + const filePath = `store/${filename}` + + moduleData = moduleName === 'state' + ? normalizeState(moduleData, filePath) + : normalizeModule(moduleData, filePath) + + // If src is a known Vuex property + if (VUEX_PROPERTIES.includes(moduleName)) { + const property = moduleName + const propertyStoreModule = getStoreModule(store, namespaces, { isProperty: true }) + + // Replace state since it's a function + mergeProperty(propertyStoreModule, moduleData, property) + return + } + + // If file is foo/index.js, it should be saved as foo + const isIndexModule = (moduleName === 'index') + if (isIndexModule) { + namespaces.pop() + moduleName = namespaces[namespaces.length - 1] + } + + const storeModule = getStoreModule(store, namespaces) + + for (const property of VUEX_PROPERTIES) { + mergeProperty(storeModule, moduleData[property], property) + } + + if (moduleData.namespaced === false) { + delete storeModule.namespaced + } +} + +function normalizeState (moduleData, filePath) { + if (typeof moduleData !== 'function') { + console.warn(`${filePath} should export a method that returns an object`) + const state = Object.assign({}, moduleData) + return () => state + } + return normalizeModule(moduleData, filePath) +} + +function getStoreModule (storeModule, namespaces, { isProperty = false } = {}) { + // If ./mutations.js + if (!namespaces.length || (isProperty && namespaces.length === 1)) { + return storeModule + } + + const namespace = namespaces.shift() + + storeModule.modules[namespace] = storeModule.modules[namespace] || {} + storeModule.modules[namespace].namespaced = true + storeModule.modules[namespace].modules = storeModule.modules[namespace].modules || {} + + return getStoreModule(storeModule.modules[namespace], namespaces, { isProperty }) +} + +function mergeProperty (storeModule, moduleData, property) { + if (!moduleData) { + return + } + + if (property === 'state') { + storeModule.state = moduleData || storeModule.state + } else { + storeModule[property] = Object.assign({}, storeModule[property], moduleData) + } +} diff --git a/shell/nuxt/utils.js b/shell/nuxt/utils.js new file mode 100644 index 0000000000..ed82f4d496 --- /dev/null +++ b/shell/nuxt/utils.js @@ -0,0 +1,630 @@ +import Vue from 'vue' +import { isSamePath as _isSamePath, joinURL, normalizeURL, withQuery, withoutTrailingSlash } from 'ufo' + +// window.{{globals.loadedCallback}} hook +// Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading) +if (process.client) { + window.onNuxtReadyCbs = [] + window.onNuxtReady = (cb) => { + window.onNuxtReadyCbs.push(cb) + } +} + +export function createGetCounter (counterObject, defaultKey = '') { + return function getCounter (id = defaultKey) { + if (counterObject[id] === undefined) { + counterObject[id] = 0 + } + return counterObject[id]++ + } +} + +export function empty () {} + +export function globalHandleError (error) { + if (Vue.config.errorHandler) { + Vue.config.errorHandler(error) + } +} + +export function interopDefault (promise) { + return promise.then(m => m.default || m) +} + +export function hasFetch(vm) { + return vm.$options && typeof vm.$options.fetch === 'function' && !vm.$options.fetch.length +} +export function purifyData(data) { + if (process.env.NODE_ENV === 'production') { + return data + } + + return Object.entries(data).filter( + ([key, value]) => { + const valid = !(value instanceof Function) && !(value instanceof Promise) + if (!valid) { + console.warn(`${key} is not able to be stringified. This will break in a production environment.`) + } + return valid + } + ).reduce((obj, [key, value]) => { + obj[key] = value + return obj + }, {}) +} +export function getChildrenComponentInstancesUsingFetch(vm, instances = []) { + const children = vm.$children || [] + for (const child of children) { + if (child.$fetch) { + instances.push(child) + continue; // Don't get the children since it will reload the template + } + if (child.$children) { + getChildrenComponentInstancesUsingFetch(child, instances) + } + } + return instances +} + +export function applyAsyncData (Component, asyncData) { + if ( + // For SSR, we once all this function without second param to just apply asyncData + // Prevent doing this for each SSR request + !asyncData && Component.options.__hasNuxtData + ) { + return + } + + const ComponentData = Component.options._originDataFn || Component.options.data || function () { return {} } + Component.options._originDataFn = ComponentData + + Component.options.data = function () { + const data = ComponentData.call(this, this) + if (this.$ssrContext) { + asyncData = this.$ssrContext.asyncData[Component.cid] + } + return { ...data, ...asyncData } + } + + Component.options.__hasNuxtData = true + + if (Component._Ctor && Component._Ctor.options) { + Component._Ctor.options.data = Component.options.data + } +} + +export function sanitizeComponent (Component) { + // If Component already sanitized + if (Component.options && Component._Ctor === Component) { + return Component + } + if (!Component.options) { + Component = Vue.extend(Component) // fix issue #6 + Component._Ctor = Component + } else { + Component._Ctor = Component + Component.extendOptions = Component.options + } + // If no component name defined, set file path as name, (also fixes #5703) + if (!Component.options.name && Component.options.__file) { + Component.options.name = Component.options.__file + } + return Component +} + +export function getMatchedComponents (route, matches = false, prop = 'components') { + return Array.prototype.concat.apply([], route.matched.map((m, index) => { + return Object.keys(m[prop]).map((key) => { + matches && matches.push(index) + return m[prop][key] + }) + })) +} + +export function getMatchedComponentsInstances (route, matches = false) { + return getMatchedComponents(route, matches, 'instances') +} + +export function flatMapComponents (route, fn) { + return Array.prototype.concat.apply([], route.matched.map((m, index) => { + return Object.keys(m.components).reduce((promises, key) => { + if (m.components[key]) { + promises.push(fn(m.components[key], m.instances[key], m, key, index)) + } else { + delete m.components[key] + } + return promises + }, []) + })) +} + +export function resolveRouteComponents (route, fn) { + return Promise.all( + flatMapComponents(route, async (Component, instance, match, key) => { + // If component is a function, resolve it + if (typeof Component === 'function' && !Component.options) { + try { + Component = await Component() + } catch (error) { + // Handle webpack chunk loading errors + // This may be due to a new deployment or a network problem + if ( + error && + error.name === 'ChunkLoadError' && + typeof window !== 'undefined' && + window.sessionStorage + ) { + const timeNow = Date.now() + const previousReloadTime = parseInt(window.sessionStorage.getItem('nuxt-reload')) + + // check for previous reload time not to reload infinitely + if (!previousReloadTime || previousReloadTime + 60000 < timeNow) { + window.sessionStorage.setItem('nuxt-reload', timeNow) + window.location.reload(true /* skip cache */) + } + } + + throw error + } + } + match.components[key] = Component = sanitizeComponent(Component) + return typeof fn === 'function' ? fn(Component, instance, match, key) : Component + }) + ) +} + +export async function getRouteData (route) { + if (!route) { + return + } + // Make sure the components are resolved (code-splitting) + await resolveRouteComponents(route) + // Send back a copy of route with meta based on Component definition + return { + ...route, + meta: getMatchedComponents(route).map((Component, index) => { + return { ...Component.options.meta, ...(route.matched[index] || {}).meta } + }) + } +} + +export async function setContext (app, context) { + // If context not defined, create it + if (!app.context) { + app.context = { + isStatic: process.static, + isDev: true, + isHMR: false, + app, + store: app.store, + payload: context.payload, + error: context.error, + base: app.router.options.base, + env: {"commit":"head","version":"0.1.2","dev":true,"pl":1,"perfTest":false,"rancherEnv":"web","api":"http://localhost:8989"} + } + // Only set once + + if (context.req) { + app.context.req = context.req + } + if (context.res) { + app.context.res = context.res + } + + if (context.ssrContext) { + app.context.ssrContext = context.ssrContext + } + app.context.redirect = (status, path, query) => { + if (!status) { + return + } + app.context._redirected = true + // if only 1 or 2 arguments: redirect('/') or redirect('/', { foo: 'bar' }) + let pathType = typeof path + if (typeof status !== 'number' && (pathType === 'undefined' || pathType === 'object')) { + query = path || {} + path = status + pathType = typeof path + status = 302 + } + if (pathType === 'object') { + path = app.router.resolve(path).route.fullPath + } + // "/absolute/route", "./relative/route" or "../relative/route" + if (/(^[.]{1,2}\/)|(^\/(?!\/))/.test(path)) { + app.context.next({ + path, + query, + status + }) + } else { + path = withQuery(path, query) + if (process.server) { + app.context.next({ + path, + status + }) + } + if (process.client) { + // https://developer.mozilla.org/en-US/docs/Web/API/Location/replace + window.location.replace(path) + + // Throw a redirect error + throw new Error('ERR_REDIRECT') + } + } + } + if (process.server) { + app.context.beforeNuxtRender = fn => context.beforeRenderFns.push(fn) + } + if (process.client) { + app.context.nuxtState = window.__NUXT__ + } + } + + // Dynamic keys + const [currentRouteData, fromRouteData] = await Promise.all([ + getRouteData(context.route), + getRouteData(context.from) + ]) + + if (context.route) { + app.context.route = currentRouteData + } + + if (context.from) { + app.context.from = fromRouteData + } + + app.context.next = context.next + app.context._redirected = false + app.context._errored = false + app.context.isHMR = Boolean(context.isHMR) + app.context.params = app.context.route.params || {} + app.context.query = app.context.route.query || {} +} + +export function middlewareSeries (promises, appContext) { + if (!promises.length || appContext._redirected || appContext._errored) { + return Promise.resolve() + } + return promisify(promises[0], appContext) + .then(() => { + return middlewareSeries(promises.slice(1), appContext) + }) +} + +export function promisify (fn, context) { + let promise + if (fn.length === 2) { + console.warn('Callback-based asyncData, fetch or middleware calls are deprecated. ' + + 'Please switch to promises or async/await syntax') + + // fn(context, callback) + promise = new Promise((resolve) => { + fn(context, function (err, data) { + if (err) { + context.error(err) + } + data = data || {} + resolve(data) + }) + }) + } else { + promise = fn(context) + } + + if (promise && promise instanceof Promise && typeof promise.then === 'function') { + return promise + } + return Promise.resolve(promise) +} + +// Imported from vue-router +export function getLocation (base, mode) { + if (mode === 'hash') { + return window.location.hash.replace(/^#\//, '') + } + + base = decodeURI(base).slice(0, -1) // consideration is base is normalized with trailing slash + let path = decodeURI(window.location.pathname) + + if (base && path.startsWith(base)) { + path = path.slice(base.length) + } + + const fullPath = (path || '/') + window.location.search + window.location.hash + + return normalizeURL(fullPath) +} + +// Imported from path-to-regexp + +/** + * Compile a string to a template function for the path. + * + * @param {string} str + * @param {Object=} options + * @return {!function(Object=, Object=)} + */ +export function compile (str, options) { + return tokensToFunction(parse(str, options), options) +} + +export function getQueryDiff (toQuery, fromQuery) { + const diff = {} + const queries = { ...toQuery, ...fromQuery } + for (const k in queries) { + if (String(toQuery[k]) !== String(fromQuery[k])) { + diff[k] = true + } + } + return diff +} + +export function normalizeError (err) { + let message + if (!(err.message || typeof err === 'string')) { + try { + message = JSON.stringify(err, null, 2) + } catch (e) { + message = `[${err.constructor.name}]` + } + } else { + message = err.message || err + } + return { + ...err, + message, + statusCode: (err.statusCode || err.status || (err.response && err.response.status) || 500) + } +} + +/** + * The main path matching regexp utility. + * + * @type {RegExp} + */ +const PATH_REGEXP = new RegExp([ + // Match escaped characters that would otherwise appear in future matches. + // This allows the user to escape special characters that won't transform. + '(\\\\.)', + // Match Express-style parameters and un-named parameters with a prefix + // and optional suffixes. Matches appear as: + // + // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined] + // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined] + // "/*" => ["/", undefined, undefined, undefined, undefined, "*"] + '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))' +].join('|'), 'g') + +/** + * Parse a string for the raw tokens. + * + * @param {string} str + * @param {Object=} options + * @return {!Array} + */ +function parse (str, options) { + const tokens = [] + let key = 0 + let index = 0 + let path = '' + const defaultDelimiter = (options && options.delimiter) || '/' + let res + + while ((res = PATH_REGEXP.exec(str)) != null) { + const m = res[0] + const escaped = res[1] + const offset = res.index + path += str.slice(index, offset) + index = offset + m.length + + // Ignore already escaped sequences. + if (escaped) { + path += escaped[1] + continue + } + + const next = str[index] + const prefix = res[2] + const name = res[3] + const capture = res[4] + const group = res[5] + const modifier = res[6] + const asterisk = res[7] + + // Push the current path onto the tokens. + if (path) { + tokens.push(path) + path = '' + } + + const partial = prefix != null && next != null && next !== prefix + const repeat = modifier === '+' || modifier === '*' + const optional = modifier === '?' || modifier === '*' + const delimiter = res[2] || defaultDelimiter + const pattern = capture || group + + tokens.push({ + name: name || key++, + prefix: prefix || '', + delimiter, + optional, + repeat, + partial, + asterisk: Boolean(asterisk), + pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?') + }) + } + + // Match any characters still remaining. + if (index < str.length) { + path += str.substr(index) + } + + // If the path exists, push it onto the end. + if (path) { + tokens.push(path) + } + + return tokens +} + +/** + * Prettier encoding of URI path segments. + * + * @param {string} + * @return {string} + */ +function encodeURIComponentPretty (str, slashAllowed) { + const re = slashAllowed ? /[?#]/g : /[/?#]/g + return encodeURI(str).replace(re, (c) => { + return '%' + c.charCodeAt(0).toString(16).toUpperCase() + }) +} + +/** + * Encode the asterisk parameter. Similar to `pretty`, but allows slashes. + * + * @param {string} + * @return {string} + */ +function encodeAsterisk (str) { + return encodeURIComponentPretty(str, true) +} + +/** + * Escape a regular expression string. + * + * @param {string} str + * @return {string} + */ +function escapeString (str) { + return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1') +} + +/** + * Escape the capturing group by escaping special characters and meaning. + * + * @param {string} group + * @return {string} + */ +function escapeGroup (group) { + return group.replace(/([=!:$/()])/g, '\\$1') +} + +/** + * Expose a method for transforming tokens into the path function. + */ +function tokensToFunction (tokens, options) { + // Compile all the tokens into regexps. + const matches = new Array(tokens.length) + + // Compile all the patterns before compilation. + for (let i = 0; i < tokens.length; i++) { + if (typeof tokens[i] === 'object') { + matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$', flags(options)) + } + } + + return function (obj, opts) { + let path = '' + const data = obj || {} + const options = opts || {} + const encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i] + + if (typeof token === 'string') { + path += token + + continue + } + + const value = data[token.name || 'pathMatch'] + let segment + + if (value == null) { + if (token.optional) { + // Prepend partial segment prefixes. + if (token.partial) { + path += token.prefix + } + + continue + } else { + throw new TypeError('Expected "' + token.name + '" to be defined') + } + } + + if (Array.isArray(value)) { + if (!token.repeat) { + throw new TypeError('Expected "' + token.name + '" to not repeat, but received `' + JSON.stringify(value) + '`') + } + + if (value.length === 0) { + if (token.optional) { + continue + } else { + throw new TypeError('Expected "' + token.name + '" to not be empty') + } + } + + for (let j = 0; j < value.length; j++) { + segment = encode(value[j]) + + if (!matches[i].test(segment)) { + throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received `' + JSON.stringify(segment) + '`') + } + + path += (j === 0 ? token.prefix : token.delimiter) + segment + } + + continue + } + + segment = token.asterisk ? encodeAsterisk(value) : encode(value) + + if (!matches[i].test(segment)) { + throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"') + } + + path += token.prefix + segment + } + + return path + } +} + +/** + * Get the flags for a regexp from the options. + * + * @param {Object} options + * @return {string} + */ +function flags (options) { + return options && options.sensitive ? '' : 'i' +} + +export function addLifecycleHook(vm, hook, fn) { + if (!vm.$options[hook]) { + vm.$options[hook] = [] + } + if (!vm.$options[hook].includes(fn)) { + vm.$options[hook].push(fn) + } +} + +export const urlJoin = joinURL + +export const stripTrailingSlash = withoutTrailingSlash + +export const isSamePath = _isSamePath + +export function setScrollRestoration (newVal) { + try { + window.history.scrollRestoration = newVal; + } catch(e) {} +} diff --git a/shell/nuxt/views/app.template.html b/shell/nuxt/views/app.template.html new file mode 100644 index 0000000000..3427d3ea23 --- /dev/null +++ b/shell/nuxt/views/app.template.html @@ -0,0 +1,9 @@ + + + + {{ HEAD }} + + + {{ APP }} + + diff --git a/shell/nuxt/views/error.html b/shell/nuxt/views/error.html new file mode 100644 index 0000000000..082a41fcb0 --- /dev/null +++ b/shell/nuxt/views/error.html @@ -0,0 +1,23 @@ + + + +Server error + + + + + +
+
+ +
Server error
+
{{ message }}
+
+ +
+ + diff --git a/shell/package.json b/shell/package.json index 0d3714093e..492023e35f 100644 --- a/shell/package.json +++ b/shell/package.json @@ -16,15 +16,11 @@ "clean": "./scripts/clean", "lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .ts,.js,.vue .", "test": "./node_modules/.bin/nyc ava --serial --verbose", - "nuxt": "./node_modules/.bin/nuxt", - "dev": "./node_modules/.bin/nuxt dev", - "mem-dev": "node --max-old-space-size=8192 ./node_modules/.bin/nuxt dev", + "dev": "./node_modules/.bin/vue-cli-service dev", "docker-dev": "docker run --rm --name dashboard-dev -p 8005:8005 -e API=$API -v $(pwd):/src -v dashboard_node:/src/node_modules rancher/dashboard:dev", - "build": "./node_modules/.bin/nuxt build --devtools", - "analyze": "./node_modules/.bin/nuxt build --analyze", - "start": "./node_modules/.bin/nuxt start", - "generate": "./node_modules/.bin/nuxt generate", - "dev-debug": "node --inspect ./node_modules/.bin/nuxt", + "build": "./node_modules/.bin/vue-cli-service build", + "analyze": "./node_modules/.bin/vue-cli-service build --report", + "start": "./node_modules/.bin/vue-cli-service start", "cy:run": "cypress run", "cy:open": "cypress open", "e2e:pre": "NODE_ENV=dev yarn build", @@ -41,7 +37,6 @@ "@innologica/vue-dropdown-menu": "0.1.3", "@novnc/novnc": "1.2.0", "@nuxt/types": "2.14.6", - "@nuxt/typescript-build": "2.1.0", "@nuxtjs/axios": "5.12.0", "@nuxtjs/eslint-config-typescript": "6.0.1", "@nuxtjs/eslint-module": "1.2.0", @@ -52,8 +47,6 @@ "@types/node": "16.4.3", "@typescript-eslint/eslint-plugin": "4.33.0", "@typescript-eslint/parser": "4.33.0", - "@vue/cli-plugin-babel": "4.5.15", - "@vue/cli-plugin-typescript": "4.5.15", "@vue/cli-service": "4.5.15", "@vue/test-utils": "1.2.1", "@vue/vue2-jest": "27.0.0", @@ -157,5 +150,8 @@ ".js", ".vue" ] + }, + "devDependencies": { + "@vue/cli": "4.5.15" } } diff --git a/shell/pages/auth/setup.vue b/shell/pages/auth/setup.vue index 3d5a2ff821..2430e8436b 100644 --- a/shell/pages/auth/setup.vue +++ b/shell/pages/auth/setup.vue @@ -495,7 +495,7 @@ export default { } .landscape { - background-image: url('~shell/assets/images/pl/login-landscape.svg'); + background-image: url('~@shell/assets/images/pl/login-landscape.svg'); background-repeat: no-repeat; background-size: cover; background-position: center center; diff --git a/shell/plugins/steve/subscribe.js b/shell/plugins/steve/subscribe.js index 370a1702f2..68facb68bd 100644 --- a/shell/plugins/steve/subscribe.js +++ b/shell/plugins/steve/subscribe.js @@ -354,7 +354,7 @@ const sharedActions = { msg.selector = selector; } - const worker = this.$workers[getters.storeName] || {}; + const worker = this.$workers?.[getters.storeName] || {}; if (worker.mode === 'advanced') { if ( force ) { diff --git a/shell/public/index.html b/shell/public/index.html new file mode 100644 index 0000000000..ad24155de4 --- /dev/null +++ b/shell/public/index.html @@ -0,0 +1,65 @@ + + + + + + + + + Rancher + + + +
+ + +
+ +
+
+ + + diff --git a/shell/scripts/build-pkg.sh b/shell/scripts/build-pkg.sh index eed1254029..9911feb77d 100755 --- a/shell/scripts/build-pkg.sh +++ b/shell/scripts/build-pkg.sh @@ -40,6 +40,7 @@ if [ -d "${BASE_DIR}/pkg/${1}" ]; then echo " Package name: ${NAME}" echo " Package version: ${VERSION}" echo " Output formats: ${FORMATS}" + echo " Output directory: ${PKG_DIST}" rm -rf ${PKG_DIST} mkdir -p ${PKG_DIST} diff --git a/tsconfig.default.json b/shell/tsconfig.default.json similarity index 87% rename from tsconfig.default.json rename to shell/tsconfig.default.json index ed687227b1..d6b824e472 100644 --- a/tsconfig.default.json +++ b/shell/tsconfig.default.json @@ -17,16 +17,16 @@ "rootDir": ".", "paths": { "~/*": [ - "./*" + "../*" ], "@/*": [ - "./*" + "../*" ], "@shell/*": [ - "./shell/*" + "../shell/*" ], "@pkg/*": [ - "./shell/pkg/*" + "../shell/pkg/*" ] }, "typeRoots": [ diff --git a/shell/tsconfig.json b/shell/tsconfig.json new file mode 100644 index 0000000000..06e39f0fd2 --- /dev/null +++ b/shell/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.default.json", + "compilerOptions": { + "types": [ + "@types/node", + "@types/jest", + "@nuxt/types" + ] + }, + "exclude": [ + "node_modules", + ".nuxt", + "dist", + "dist-pkg", + "cypress", + "shell/creators", + "shell/scripts", + "cypress", + "./cypress.config.ts", + "docusaurus", + "script/standalone", + "**/*.spec.ts" + ] +} \ No newline at end of file diff --git a/shell/vue.config.ts b/shell/vue.config.ts new file mode 100644 index 0000000000..8b97f0bb38 --- /dev/null +++ b/shell/vue.config.ts @@ -0,0 +1,669 @@ +import fs from 'fs'; +import path from 'path'; +import serveStatic from 'serve-static'; +import webpack from 'webpack'; +import { STANDARD } from './config/private-label'; +import { generateDynamicTypeImport } from './pkg/auto-import'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import { createProxyMiddleware } from 'http-proxy-middleware'; + +const dev = (process.env.NODE_ENV !== 'production'); +const devPorts = dev || process.env.DEV_PORTS === 'true'; + +// human readable version used on rancher dashboard about page +const dashboardVersion = process.env.DASHBOARD_VERSION; + +const prime = process.env.PRIME; + +const pl = process.env.PL || STANDARD; +const commit = process.env.COMMIT || 'head'; +const perfTest = (process.env.PERF_TEST === 'true'); // Enable performance testing when in dev +const instrumentCode = (process.env.TEST_INSTRUMENT === 'true'); // Instrument code for code coverage in e2e tests + +let api = process.env.API || 'http://localhost:8989'; + +if ( !api.startsWith('http') ) { + api = `https://${ api }`; +} +// =============================================================================================== +// Nuxt configuration +// =============================================================================================== + +// Expose a function that can be used by an app to provide a nuxt configuration for building an application +// This takes the directory of the application as tehfirst argument so that we can derive folder locations +// from it, rather than from the location of this file +export default function(dir: any, _appConfig: any) { + // Paths to the shell folder when it is included as a node dependency + let SHELL = 'node_modules/@rancher/shell'; + let SHELL_ABS = path.join(dir, 'node_modules/@rancher/shell'); + let COMPONENTS_DIR = path.join(SHELL_ABS, 'rancher-components'); + + if (fs.existsSync(SHELL_ABS)) { + const stat = fs.lstatSync(SHELL_ABS); + + // If @rancher/shell is a symlink, then use the components folder for it + if (stat.isSymbolicLink()) { + const REAL_SHELL_ABS = fs.realpathSync(SHELL_ABS); // In case the shell is being linked via 'yarn link' + + COMPONENTS_DIR = path.join(REAL_SHELL_ABS, '..', 'pkg', 'rancher-components', 'src', 'components'); + } + } + + // If we have a local folder named 'shell' then use that rather than the one in node_modules + // This will be the case in the main dashboard repository. + if (fs.existsSync(path.join(dir, 'shell'))) { + SHELL = './shell'; + SHELL_ABS = path.join(dir, 'shell'); + COMPONENTS_DIR = path.join(dir, 'pkg', 'rancher-components', 'src', 'components'); + } + + const babelPlugins: string | (string | object)[] = [ + // TODO: Browser support + // ['@babel/plugin-transform-modules-commonjs'], + ['@babel/plugin-proposal-private-property-in-object', { loose: true }] + ]; + + if (instrumentCode) { + babelPlugins.push('babel-plugin-istanbul'); + + console.warn('Instrumenting code for coverage'); // eslint-disable-line no-console + } + + // =============================================================================================== + // Functions for the UI Pluginas + // =============================================================================================== + + const appConfig = _appConfig || {}; + const excludes = appConfig.excludes || []; + const autoLoad = appConfig.autoLoad || []; + + const serverMiddleware = []; + const autoLoadPackages = []; + const watcherIgnores = [ + /.shell/, + /dist-pkg/, + /scripts\/standalone/ + ]; + + autoLoad.forEach((pkg: any) => { + // Need the version number of each file + const pkgPackageFile = require(path.join(dir, 'pkg', pkg, 'package.json')); + const pkgRef = `${ pkg }-${ pkgPackageFile.version }`; + + autoLoadPackages.push({ + name: `app-autoload-${ pkgRef }`, + content: `/pkg/${ pkgRef }/${ pkgRef }.umd.min.js` + }); + + // Anything auto-loaded should also be excluded + if (!excludes.includes(pkg)) { + excludes.push(pkg); + } + }); + + // Find any UI packages in node_modules + const NM = path.join(dir, 'node_modules'); + const pkg = require(path.join(dir, 'package.json')); + const nmPackages: any = {}; + + if (pkg && pkg.dependencies) { + Object.keys(pkg.dependencies).forEach((pkg) => { + const f = require(path.join(NM, pkg, 'package.json')); + + // The package.json must have the 'rancher' property to mark it as a UI package + if (f.rancher) { + const id = `${ f.name }-${ f.version }`; + + nmPackages[id] = f.main; + + // Add server middleware to serve up the files for this UI package + serverMiddleware.push({ + path: `/pkg/${ id }`, + handler: serveStatic(path.join(NM, pkg)) + }); + } + }); + } + + serverMiddleware.push({ + path: '/uiplugins-catalog', + handler: (req: any, res: any, next: any) => { + const p = req.url.split('?'); + + try { + const proxy = createProxyMiddleware({ + target: p[1], + pathRewrite: { '^.*': p[0] } + }); + + return proxy(req, res, next); + } catch (e) { + console.error(e); // eslint-disable-line no-console + } + } + }); + + function includePkg(name: any) { + if (name.startsWith('.') || name === 'node_modules') { + return false; + } + + return !excludes || (excludes && !excludes.includes(name)); + } + + excludes.forEach((e: any) => { + watcherIgnores.push(new RegExp(`/pkg.${ e }`)); + }); + + // For each package in the pkg folder that is being compiled into the application, + // Add in the code to automatically import the types from that package + // This imports models, edit, detail, list etc + // When built as a UI package, shell/pkg/vue.config.js does the same thing + const autoImportTypes: any = {}; + const VirtualModulesPlugin = require('webpack-virtual-modules'); + let reqs = ''; + const pkgFolder = path.relative(dir, './pkg'); + + if (fs.existsSync(pkgFolder)) { + const items = fs.readdirSync(path.relative(dir, './pkg')); + + // Ignore hidden folders + items.filter(name => !name.startsWith('.')).forEach((name) => { + const f = require(path.join(dir, 'pkg', name, 'package.json')); + + // Package file must have rancher field to be a plugin + if (includePkg(name) && f.rancher) { + reqs += `$plugin.initPlugin('${ name }', require(\'~/pkg/${ name }\')); `; + } + + // // Serve the code for the UI package in case its used for dynamic loading (but not if the same package was provided in node_modules) + // if (!nmPackages[name]) { + // const pkgPackageFile = require(path.join(dir, 'pkg', name, 'package.json')); + // const pkgRef = `${ name }-${ pkgPackageFile.version }`; + + // serverMiddleware.push({ path: `/pkg/${ pkgRef }`, handler: serveStatic(`${ dir }/dist-pkg/${ pkgRef }`) }); + // } + autoImportTypes[`node_modules/@rancher/auto-import/${ name }`] = generateDynamicTypeImport(`@pkg/${ name }`, path.join(dir, `pkg/${ name }`)); + }); + } + + Object.keys(nmPackages).forEach((m) => { + reqs += `$plugin.loadAsync('${ m }', '/pkg/${ m }/${ nmPackages[m] }');`; + }); + + // Generate a virtual module '@rancher/dyanmic.js` which imports all of the packages that should be built into the application + // This is imported in 'shell/extensions/extension-loader.js` which ensures the all code for plugins to be included is imported in the application + const virtualModules = new VirtualModulesPlugin({ 'node_modules/@rancher/dynamic.js': `export default function ($plugin) { ${ reqs } };` }); + const autoImport = new webpack.NormalModuleReplacementPlugin(/^@rancher\/auto-import$/, (resource: any) => { + const ctx = resource.context.split('/'); + const pkg = ctx[ctx.length - 1]; + + resource.request = `@rancher/auto-import/${ pkg }`; + }); + + // @pkg imports must be resolved to the package that it importing them - this allows a package to use @pkg as an alis + // to the root of that particular package + const pkgImport = new webpack.NormalModuleReplacementPlugin(/^@pkg/, (resource: any) => { + const ctx = resource.context.split('/'); + // Find 'pkg' folder in the contxt + const index = ctx.findIndex((s: any) => s === 'pkg'); + + if (index !== -1 && (index + 1) < ctx.length) { + const pkg = ctx[index + 1]; + let p = path.resolve(dir, 'pkg', pkg, resource.request.substr(5)); + + if (resource.request.startsWith(`@pkg/${ pkg }`)) { + p = path.resolve(dir, 'pkg', resource.request.substr(5)); + } + + resource.request = p; + } + }); + + // Serve up the dist-pkg folder under /pkg + serverMiddleware.push({ path: `/pkg/`, handler: serveStatic(`${ dir }/dist-pkg/`) }); + // Endpoint to download and unpack a tgz from the local verdaccio rgistry (dev) + serverMiddleware.push(path.resolve(dir, SHELL, 'server', 'verdaccio-middleware')); + // Add the standard dashboard server middleware after the middleware added to serve up UI packages + serverMiddleware.push(path.resolve(dir, SHELL, 'server', 'server-middleware')); + + // =============================================================================================== + // Dashboard nuxt configuration + // =============================================================================================== + + require('events').EventEmitter.defaultMaxListeners = 20; + require('dotenv').config(); + + let routerBasePath = '/'; + let resourceBase = ''; + let outputDir = 'dist'; + + if ( typeof process.env.ROUTER_BASE !== 'undefined' ) { + routerBasePath = process.env.ROUTER_BASE; + } + + if ( typeof process.env.RESOURCE_BASE !== 'undefined' ) { + resourceBase = process.env.RESOURCE_BASE; + } + + if ( typeof process.env.OUTPUT_DIR !== 'undefined' ) { + outputDir = process.env.OUTPUT_DIR; + } + + if ( resourceBase && !resourceBase.endsWith('/') ) { + resourceBase += '/'; + } + + console.log(`Build: ${ dev ? 'Development' : 'Production' }`); // eslint-disable-line no-console + + if ( !dev ) { + console.log(`Version: ${ dashboardVersion }`); // eslint-disable-line no-console + } + + if ( !dev ) { + console.log(`Version: ${ dashboardVersion }`); // eslint-disable-line no-console + } + + if ( resourceBase ) { + console.log(`Resource Base URL: ${ resourceBase }`); // eslint-disable-line no-console + } + + if ( routerBasePath !== '/' ) { + console.log(`Router Base Path: ${ routerBasePath }`); // eslint-disable-line no-console + } + + if ( pl !== STANDARD ) { + console.log(`PL: ${ pl }`); // eslint-disable-line no-console + } + const rancherEnv = process.env.RANCHER_ENV || 'web'; + + console.log(`API: '${ api }'. Env: '${ rancherEnv }'`); // eslint-disable-line no-console + const proxy = { + ...appConfig.proxies, + '/k8s': proxyWsOpts(api), // Straight to a remote cluster (/k8s/clusters//) + '/pp': proxyWsOpts(api), // For (epinio) standalone API + '/api': proxyWsOpts(api), // Management k8s API + '/apis': proxyWsOpts(api), // Management k8s API + '/v1': proxyWsOpts(api), // Management Steve API + '/v3': proxyWsOpts(api), // Rancher API + '/v3-public': proxyOpts(api), // Rancher Unauthed API + '/api-ui': proxyOpts(api), // Browser API UI + '/meta': proxyMetaOpts(api), // Browser API UI + '/v1-*': proxyOpts(api), // SAML, KDM, etc + '/rancherversion': proxyPrimeOpts(api), // Rancher version endpoint + // These are for Ember embedding + '/c/*/edit': proxyOpts('https://127.0.0.1:8000'), // Can't proxy all of /c because that's used by Vue too + '/k/': proxyOpts('https://127.0.0.1:8000'), + '/g/': proxyOpts('https://127.0.0.1:8000'), + '/n/': proxyOpts('https://127.0.0.1:8000'), + '/p/': proxyOpts('https://127.0.0.1:8000'), + '/assets': proxyOpts('https://127.0.0.1:8000'), + '/translations': proxyOpts('https://127.0.0.1:8000'), + '/engines-dist': proxyOpts('https://127.0.0.1:8000'), + }; + + const config = { + // Vue server + devServer: { + https: (devPorts ? { + key: fs.readFileSync(path.resolve(__dirname, 'server/server.key')), + cert: fs.readFileSync(path.resolve(__dirname, 'server/server.crt')) + } : null), + port: (devPorts ? 8005 : 80), + host: '0.0.0.0', + public: `https://0.0.0.0:${ devPorts ? 8005 : 80 }`, + before(app: any, server: any) { + const proxies: any = {}; + + Object.keys(proxy).forEach((p) => { + const px = createProxyMiddleware({ + ...proxy[p], + ws: false // We will handle the web socket upgrade + }); + + proxies[p] = px; + app.use(p, px); + }); + + server.websocketProxies.push({ + upgrade(req: any, socket: any, head:any) { + if (req.url.startsWith('/v1')) { + return proxies['/v1'].upgrade(req, socket, head); + } else if (req.url.startsWith('/v3')) { + return proxies['/v3'].upgrade(req, socket, head); + } else if (req.url.startsWith('/k8s/')) { + return proxies['/k8s'].upgrade(req, socket, head); + } else { + console.log(`Unknown Web socket upgrade request for ${ req.url }`); // eslint-disable-line no-console + } + } + }); + }, + }, + + css: { + loaderOptions: { + sass: { + // This is effectively added to the beginning of each style that's imported or included in a vue file. We may want to look into including these in app.scss + additionalData: ` + @use 'sass:math'; + @import "~shell/assets/styles/base/_variables.scss"; + @import "~shell/assets/styles/base/_functions.scss"; + @import "~shell/assets/styles/base/_mixins.scss"; + ` + } + } + }, + + outputDir, + + pages: { + index: { + entry: path.join(SHELL_ABS, '/nuxt/client.js'), + template: path.join(SHELL_ABS, '/public/index.html') + } + }, + + configureWebpack(config: any) { + config.resolve.alias['~'] = dir; + config.resolve.alias['@'] = dir; + config.resolve.alias['~assets'] = path.join(__dirname, 'assets'); + config.resolve.alias['~shell'] = SHELL_ABS; + config.resolve.alias['@shell'] = SHELL_ABS; + config.resolve.alias['@pkg'] = path.join(dir, 'pkg'); + config.resolve.alias['./node_modules'] = path.join(dir, 'node_modules'); + config.resolve.alias['@components'] = COMPONENTS_DIR; + config.resolve.modules.push(__dirname); + config.plugins.push(virtualModules); + config.plugins.push(autoImport); + config.plugins.push(new VirtualModulesPlugin(autoImportTypes)); + config.plugins.push(pkgImport); + // DefinePlugin does string replacement within our code. We may want to consider replacing it with something else. In code we'll see something like + // process.env.commit even though process and env aren't even defined objects. This could cause people to be mislead. + config.plugins.push(new webpack.DefinePlugin({ + 'process.client': JSON.stringify(true), + 'process.env.commit': JSON.stringify(commit), + 'process.env.version': JSON.stringify(dashboardVersion), + 'process.env.dev': JSON.stringify(dev), + 'process.env.pl': JSON.stringify(pl), + 'process.env.perfTest': JSON.stringify(perfTest), + 'process.env.rancherEnv': JSON.stringify(rancherEnv), + 'process.env.harvesterPkgUrl': JSON.stringify(process.env.HARVESTER_PKG_URL), + 'process.env.api': JSON.stringify(api), + + // This is a replacement of the nuxt publicRuntimeConfig + 'nuxt.publicRuntimeConfig': JSON.stringify({ + rancherEnv, + dashboardVersion + }), + })); + + // The static assets need to be in the built public folder in order to get served (primarily the favicon for now) + config.plugins.push(new CopyWebpackPlugin([{ from: path.join(SHELL_ABS, 'static'), to: 'public' }])); + + config.resolve.extensions.push(...['.tsx', '.ts', '.js', '.vue', '.scss']); + config.watchOptions = config.watchOptions || {}; + config.watchOptions.ignored = watcherIgnores; + + if (dev) { + config.devtool = 'cheap-module-source-map'; + } else { + config.devtool = 'source-map'; + } + + if (resourceBase) { + config.output.publicPath = resourceBase; + } + + config.resolve.symlinks = false; + + // Ensure we process files in the @rancher/shell folder + config.module.rules.forEach((r: any) => { + if ('test.js'.match(r.test)) { + if (r.exclude) { + const orig = r.exclude; + + r.exclude = function(modulePath: string) { + if (modulePath.indexOf(SHELL_ABS) === 0) { + return false; + } + + return orig(modulePath); + }; + } + } + }); + + // Instrument code for tests + const babelPlugins: (string | ([] | Object)[])[] = [ + // TODO: Browser support + // ['@babel/plugin-transform-modules-commonjs'], + ['@babel/plugin-proposal-private-property-in-object', { loose: true }], + ['@babel/plugin-proposal-class-properties', { loose: true }] + ]; + + if (instrumentCode) { + babelPlugins.push('babel-plugin-istanbul'); + + console.warn('Instrumenting code for coverage'); // eslint-disable-line no-console + } + + const loaders = [ + // Ensure there is a fallback for browsers that don't support web workers + { + test: /web-worker.[a-z-]+.js/i, + loader: 'worker-loader', + options: { inline: 'fallback' }, + }, + // Handler for csv files (e.g. ec2 instance data) + { + test: /\.csv$/i, + loader: 'csv-loader', + options: { + dynamicTyping: true, + header: true, + skipEmptyLines: true + }, + }, + // Handler for yaml files (used for i18n files, for example) + { + test: /\.ya?ml$/i, + loader: 'js-yaml-loader', + options: { name: '[path][name].[ext]' }, + }, + { + test: /\.m?[tj]sx?$/, + // This excludes no modules except for node_modules/@rancher/... so that plugins can properly compile + // when referencing @rancher/shell + exclude: /node_modules\/(?!(@rancher)\/).*/, + use: [ + { + loader: 'cache-loader', + options: { + cacheDirectory: 'node_modules/.cache/babel-loader', + cacheIdentifier: 'e93f32da' + } + }, + { + loader: 'babel-loader', + options: { + presets: [ + [ + require.resolve('@nuxt/babel-preset-app'), + { + corejs: { version: 3 }, + targets: { browsers: ['last 2 versions'] }, + modern: true + } + ], + '@babel/preset-typescript', + ], + plugins: babelPlugins + } + } + ] + }, + { + test: /\.tsx?$/, + use: [ + { + loader: 'cache-loader', + options: { + cacheDirectory: 'node_modules/.cache/ts-loader', + cacheIdentifier: '3596741e' + } + }, + { + loader: 'ts-loader', + options: { + transpileOnly: true, + happyPackMode: false, + appendTsxSuffixTo: [ + '\\.vue$' + ], + configFile: path.join(SHELL_ABS, 'tsconfig.json') + } + } + ] + }, + // Prevent warning in log with the md files in the content folder + { + test: /\.md$/, + use: [ + { + loader: 'url-loader', + options: { + name: '[path][name].[ext]', + limit: 1, + esModule: false + }, + } + ] + }, + // Prevent warning in log with the md files in the content folder + { + test: /\.md$/, + use: [ + { + loader: 'frontmatter-markdown-loader', + options: { mode: ['body'] } + } + ] + } + ]; + + config.module.rules.push(...loaders); + }, + }; + + return config; +} + +// =============================================================================================== +// Functions for the request proxying used in dev +// =============================================================================================== + +function proxyMetaOpts(target: any) { + return { + target, + followRedirects: true, + secure: !dev, + onProxyReq, + onProxyReqWs, + onError, + onProxyRes, + }; +} + +function proxyOpts(target: any) { + return { + target, + secure: !devPorts, + onProxyReq, + onProxyReqWs, + onError, + onProxyRes + }; +} + +// Intercept the /rancherversion API call wnad modify the 'RancherPrime' value +// if configured to do so by the environment variable PRIME +function proxyPrimeOpts(target: any) { + const opts = proxyOpts(target); + + // Don't intercept if the PRIME environment variable is not set + if (!prime?.length) { + return opts; + } + + opts.onProxyRes = (proxyRes, req, res) => { + const _end = res.end; + let body = ''; + + proxyRes.on( 'data', (data: any) => { + data = data.toString('utf-8'); + body += data; + }); + + res.write = () => {}; + + res.end = () => { + let output = body; + + try { + const out = JSON.parse(body); + + out.RancherPrime = prime; + output = JSON.stringify(out); + } catch (err) {} + + res.setHeader('content-length', output.length ); + res.setHeader('content-type', 'application/json' ); + res.setHeader('transfer-encoding', ''); + res.setHeader('cache-control', 'no-cache'); + res.writeHead(proxyRes.statusCode); + _end.apply(res, [output]); + }; + }; + + return opts; +} + +function onProxyRes(proxyRes: any, req: any, res: any) { + if (devPorts) { + proxyRes.headers['X-Frame-Options'] = 'ALLOWALL'; + } +} + +function proxyWsOpts(target: any) { + return { + ...proxyOpts(target), + ws: true, + changeOrigin: true, + }; +} + +function onProxyReq(proxyReq: any, req: any) { + if (!(proxyReq._currentRequest && proxyReq._currentRequest._headerSent)) { + proxyReq.setHeader('x-api-host', req.headers['host']); + proxyReq.setHeader('x-forwarded-proto', 'https'); + } +} + +function onProxyReqWs(proxyReq: any, req: any, socket: any, options: any, head: any) { + req.headers.origin = options.target.href; + proxyReq.setHeader('origin', options.target.href); + proxyReq.setHeader('x-api-host', req.headers['host']); + proxyReq.setHeader('x-forwarded-proto', 'https'); + // console.log(proxyReq.getHeaders()); + + socket.on('error', (err: any) => { + console.error('Proxy WS Error:', err); // eslint-disable-line no-console + }); +} + +function onError(err: any, req: any, res: any) { + res.statusCode = 598; + console.error('Proxy Error:', err); // eslint-disable-line no-console + res.write(JSON.stringify(err)); +} diff --git a/tsconfig.json b/tsconfig.json index 8d8afb82d3..55f153d625 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,3 @@ { - "extends": "./tsconfig.default.json", - "compilerOptions": { - "types": ["@types/node", "@types/jest", "@nuxt/types"] - }, - "exclude": [ - "node_modules", - ".nuxt", - "dist", - "dist-pkg", - "cypress", - "shell/creators", - "shell/scripts", - "cypress", - "./cypress.config.ts", - "docusaurus", - "script/standalone", - "**/*.spec.ts" - ] -} + "extends": "./shell/tsconfig.json", +} \ No newline at end of file diff --git a/vue.config.js b/vue.config.js new file mode 100644 index 0000000000..fbdca9fd47 --- /dev/null +++ b/vue.config.js @@ -0,0 +1,7 @@ +require('ts-node').register({ + project: './tsconfig.json', + compilerOptions: { module: 'commonjs' }, + logError: true +}); + +module.exports = require('./vue.config.ts').default; diff --git a/nuxt.config.js b/vue.config.ts similarity index 91% rename from nuxt.config.js rename to vue.config.ts index 3ffecfca92..2238ac618e 100644 --- a/nuxt.config.js +++ b/vue.config.ts @@ -1,4 +1,5 @@ -import config from './shell/nuxt.config'; + +import config from './shell/vue.config'; // Excludes the following plugins if there's no .env file. let defaultExcludes = 'epinio, rancher-components, harvester'; diff --git a/yarn.lock b/yarn.lock index d335c6b8ec..c2edaaebe7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1001,7 +1001,12 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.14.0", "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8", "@babel/compat-data@^7.19.3": +"@babel/compat-data@^7.14.0", "@babel/compat-data@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.4.tgz#95c86de137bf0317f3a570e1b6e996b427299747" + integrity sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw== + +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8", "@babel/compat-data@^7.19.3": version "7.19.3" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.3.tgz#707b939793f867f5a73b2666e6d9a3396eb03151" integrity sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw== @@ -1217,6 +1222,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" @@ -1373,6 +1383,17 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.18.8" +"@babel/plugin-proposal-object-rest-spread@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.19.4.tgz#a8fc86e8180ff57290c91a75d83fe658189b642d" + integrity sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q== + dependencies: + "@babel/compat-data" "^7.19.4" + "@babel/helper-compilation-targets" "^7.19.3" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.18.8" + "@babel/plugin-proposal-optional-catch-binding@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" @@ -1605,6 +1626,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" +"@babel/plugin-transform-block-scoping@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.19.4.tgz#315d70f68ce64426db379a3d830e7ac30be02e9b" + integrity sha512-934S2VLLlt2hRJwPf4MczaOr4hYF0z+VKPwqTNxyKX7NthTiPfhuKFWQZHXRM0vh/wo/VyXB3s4bZUNA08l+tQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/plugin-transform-classes@^7.19.0": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.19.0.tgz#0e61ec257fba409c41372175e7c1e606dc79bb20" @@ -1634,6 +1662,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" +"@babel/plugin-transform-destructuring@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.19.4.tgz#46890722687b9b89e1369ad0bd8dc6c5a3b4319d" + integrity sha512-t0j0Hgidqf0aM86dF8U+vXYReUgJnlv4bZLsyoPnwZNrGY+7/38o8YjaELrvHeVfTZao15kjR0PVv0nju2iduA== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" @@ -1849,7 +1884,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.14.1", "@babel/preset-env@^7.4.4": +"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.4.4": version "7.19.3" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.19.3.tgz#52cd19abaecb3f176a4ff9cc5e15b7bf06bec754" integrity sha512-ziye1OTc9dGFOAXSWKUqQblYHNlBOaDl8wzqf2iKXJAltYiR3hKHUKmkt+S9PppW7RQpq4fFCrwwpIDj/f5P4w== @@ -1930,6 +1965,87 @@ core-js-compat "^3.25.1" semver "^6.3.0" +"@babel/preset-env@^7.14.1": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.19.4.tgz#4c91ce2e1f994f717efb4237891c3ad2d808c94b" + integrity sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg== + dependencies: + "@babel/compat-data" "^7.19.4" + "@babel/helper-compilation-targets" "^7.19.3" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-async-generator-functions" "^7.19.1" + "@babel/plugin-proposal-class-properties" "^7.18.6" + "@babel/plugin-proposal-class-static-block" "^7.18.6" + "@babel/plugin-proposal-dynamic-import" "^7.18.6" + "@babel/plugin-proposal-export-namespace-from" "^7.18.9" + "@babel/plugin-proposal-json-strings" "^7.18.6" + "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" + "@babel/plugin-proposal-numeric-separator" "^7.18.6" + "@babel/plugin-proposal-object-rest-spread" "^7.19.4" + "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-private-methods" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object" "^7.18.6" + "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.18.6" + "@babel/plugin-transform-async-to-generator" "^7.18.6" + "@babel/plugin-transform-block-scoped-functions" "^7.18.6" + "@babel/plugin-transform-block-scoping" "^7.19.4" + "@babel/plugin-transform-classes" "^7.19.0" + "@babel/plugin-transform-computed-properties" "^7.18.9" + "@babel/plugin-transform-destructuring" "^7.19.4" + "@babel/plugin-transform-dotall-regex" "^7.18.6" + "@babel/plugin-transform-duplicate-keys" "^7.18.9" + "@babel/plugin-transform-exponentiation-operator" "^7.18.6" + "@babel/plugin-transform-for-of" "^7.18.8" + "@babel/plugin-transform-function-name" "^7.18.9" + "@babel/plugin-transform-literals" "^7.18.9" + "@babel/plugin-transform-member-expression-literals" "^7.18.6" + "@babel/plugin-transform-modules-amd" "^7.18.6" + "@babel/plugin-transform-modules-commonjs" "^7.18.6" + "@babel/plugin-transform-modules-systemjs" "^7.19.0" + "@babel/plugin-transform-modules-umd" "^7.18.6" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" + "@babel/plugin-transform-new-target" "^7.18.6" + "@babel/plugin-transform-object-super" "^7.18.6" + "@babel/plugin-transform-parameters" "^7.18.8" + "@babel/plugin-transform-property-literals" "^7.18.6" + "@babel/plugin-transform-regenerator" "^7.18.6" + "@babel/plugin-transform-reserved-words" "^7.18.6" + "@babel/plugin-transform-shorthand-properties" "^7.18.6" + "@babel/plugin-transform-spread" "^7.19.0" + "@babel/plugin-transform-sticky-regex" "^7.18.6" + "@babel/plugin-transform-template-literals" "^7.18.9" + "@babel/plugin-transform-typeof-symbol" "^7.18.9" + "@babel/plugin-transform-unicode-escapes" "^7.18.10" + "@babel/plugin-transform-unicode-regex" "^7.18.6" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.19.4" + babel-plugin-polyfill-corejs2 "^0.3.3" + babel-plugin-polyfill-corejs3 "^0.6.0" + babel-plugin-polyfill-regenerator "^0.4.1" + core-js-compat "^3.25.1" + semver "^6.3.0" + "@babel/preset-modules@^0.1.5": version "0.1.5" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" @@ -1950,13 +2066,20 @@ "@babel/helper-validator-option" "^7.16.7" "@babel/plugin-transform-typescript" "^7.16.7" -"@babel/runtime@^7.11.0", "@babel/runtime@^7.14.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.11.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.14.0": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78" + integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.3.3": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" @@ -1991,6 +2114,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.4.tgz#0dd5c91c573a202d600490a35b33246fed8a41c7" + integrity sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -3122,6 +3254,15 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== +"@types/copy-webpack-plugin@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/copy-webpack-plugin/-/copy-webpack-plugin-5.0.3.tgz#9c7ea433ea0abcd0ad1b65a6d11a92c4ad0c691c" + integrity sha512-+Iv2lqU4lvAQdXMtQYjT2VuhPcWP+UWdQXfTiz2yfypo2XzEv8WggQcFfK481x1iav3W6FV0bzXBXnlq7+ukhg== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + "@types/webpack" "^4" + "@types/eslint-scope@^3.7.3": version "3.7.4" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" @@ -3870,6 +4011,15 @@ postcss "^8.4.14" source-map "^0.6.1" +"@vue/compiler-sfc@2.7.11": + version "2.7.11" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.11.tgz#3b8a60b4145f615a5da023492ee34cc3bb2b545b" + integrity sha512-Cf8zvrZWjROgd8yPL8Tc+O3q/Y8ZGM0Y+8blrAvj1RQsVouzUY0oHcx8BA7Hybhb90JRnzeApFrlQGZRUdYpRw== + dependencies: + "@babel/parser" "^7.18.4" + postcss "^8.4.14" + source-map "^0.6.1" + "@vue/component-compiler-utils@^2.3.1": version "2.6.0" resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.6.0.tgz#aa46d2a6f7647440b0b8932434d22f12371e543b" @@ -5487,11 +5637,16 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001228, caniuse-lite@^1.0.30001400: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001400: version "1.0.30001412" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz#30f67d55a865da43e0aeec003f073ea8764d5d7c" integrity sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA== +caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001228: + version "1.0.30001418" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz#5f459215192a024c99e3e3a53aac310fc7cf24e6" + integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg== + case-sensitive-paths-webpack-plugin@^2.3.0: version "2.4.0" resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" @@ -5617,7 +5772,12 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -ci-info@^3.1.1, ci-info@^3.2.0: +ci-info@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.5.0.tgz#bfac2a29263de4c829d806b1ab478e35091e171f" + integrity sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw== + +ci-info@^3.2.0: version "3.4.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.4.0.tgz#b28484fd436cbc267900364f096c9dc185efb251" integrity sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug== @@ -6142,7 +6302,14 @@ copy-webpack-plugin@^5.1.1: serialize-javascript "^4.0.0" webpack-log "^2.0.0" -core-js-compat@^3.12.1, core-js-compat@^3.25.1, core-js-compat@^3.6.5: +core-js-compat@^3.12.1: + version "3.25.5" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.5.tgz#0016e8158c904f7b059486639e6e82116eafa7d9" + integrity sha512-ovcyhs2DEBUIE0MGEKHP4olCUW/XYte3Vroyxuh38rD1wAO4dHohsovUC4eAOuzFxE6b+RXvBU3UZ9o0YhUTkA== + dependencies: + browserslist "^4.21.4" + +core-js-compat@^3.25.1, core-js-compat@^3.6.5: version "3.25.3" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.3.tgz#d6a442a03f4eade4555d4e640e6a06151dd95d38" integrity sha512-xVtYpJQ5grszDHEUU9O7XbjjcZ0ccX3LgQsyqSvTnjX97ZqEgn9F5srmrwwwMtbKzDllyFPL+O+2OFMl1lU4TQ== @@ -16267,7 +16434,7 @@ terser@^4.1.2, terser@^4.3.9, terser@^4.6.3: source-map "~0.6.1" source-map-support "~0.5.12" -terser@^5.10.0, terser@^5.14.1, terser@^5.3.4: +terser@^5.10.0, terser@^5.14.1: version "5.15.0" resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.0.tgz#e16967894eeba6e1091509ec83f0c60e179f2425" integrity sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA== @@ -16277,6 +16444,16 @@ terser@^5.10.0, terser@^5.14.1, terser@^5.3.4: commander "^2.20.0" source-map-support "~0.5.20" +terser@^5.3.4: + version "5.15.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.1.tgz#8561af6e0fd6d839669c73b92bdd5777d870ed6c" + integrity sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -16711,7 +16888,7 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== -ufo@^0.7.4: +ufo@0.7.11, ufo@^0.7.4: version "0.7.11" resolved "https://registry.yarnpkg.com/ufo/-/ufo-0.7.11.tgz#17defad497981290383c5d26357773431fdbadcb" integrity sha512-IT3q0lPvtkqQ8toHQN/BkOi4VIqoqheqM1FnkNWT9y0G8B3xJhwnoKBu5OHx8zHDOvveQzfKuFowJ0VSARiIDg== @@ -16725,9 +16902,9 @@ uglify-js@3.4.x: source-map "~0.6.1" uglify-js@^3.5.1: - version "3.17.2" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.2.tgz#f55f668b9a64b213977ae688703b6bbb7ca861c6" - integrity sha512-bbxglRjsGQMchfvXZNusUcYgiB9Hx2K4AHYXQy2DITZ9Rd+JzhX7+hoocE5Winr7z2oHvPsekkBwXtigvxevXg== + version "3.17.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.3.tgz#f0feedf019c4510f164099e8d7e72ff2d7304377" + integrity sha512-JmMFDME3iufZnBpyKL+uS78LRiC+mK55zWfM5f/pWBJfpOttXAqYfdDGRukYhJuyRinvPVAtUhvy7rlDybNtFg== un-eval@^1.2.0: version "1.2.0" @@ -16749,7 +16926,7 @@ undefsafe@^2.0.2: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -unfetch@^4.2.0: +unfetch@4.2.0, unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== @@ -17068,7 +17245,7 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -vue-client-only@^2.0.0: +vue-client-only@2.1.0, vue-client-only@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/vue-client-only/-/vue-client-only-2.1.0.tgz#1a67a47b8ecacfa86d75830173fffee3bf8a4ee3" integrity sha512-vKl1skEKn8EK9f8P2ZzhRnuaRHLHrlt1sbRmazlvsx6EiC3A8oWF8YCBrMJzoN+W3OnElwIGbVjsx6/xelY1AA== @@ -17131,14 +17308,14 @@ vue-loader@^15.9.2, vue-loader@^15.9.7: vue-hot-reload-api "^2.3.0" vue-style-loader "^4.1.0" -vue-meta@^2.4.0: +vue-meta@2.4.0, vue-meta@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/vue-meta/-/vue-meta-2.4.0.tgz#a419fb4b4135ce965dab32ec641d1989c2ee4845" integrity sha512-XEeZUmlVeODclAjCNpWDnjgw+t3WA6gdzs6ENoIAgwO1J1d5p1tezDhtteLUFwcaQaTtayRrsx7GL6oXp/m2Jw== dependencies: deepmerge "^4.2.2" -vue-no-ssr@^1.1.1: +vue-no-ssr@1.1.1, vue-no-ssr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz#875f3be6fb0ae41568a837f3ac1a80eaa137b998" integrity sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g== @@ -17148,7 +17325,7 @@ vue-resize@0.4.5, vue-resize@^0.4.5: resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea" integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg== -vue-router@^3.5.1: +vue-router@3.6.5, vue-router@^3.5.1: version "3.6.5" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.6.5.tgz#95847d52b9a7e3f1361cb605c8e6441f202afad8" integrity sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ== @@ -17173,9 +17350,9 @@ vue-server-renderer@2.6.14: source-map "0.5.6" vue-server-renderer@^2.6.12: - version "2.7.10" - resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.7.10.tgz#e73241c879fcc81de91882ceff135a40f756377c" - integrity sha512-hvlnyTZmDmnI7IpQE5YwIwexPi6yJq8eeNTUgLycPX3uhuEobygAQklHoeVREvwNKcET/MnVOtjF4c7t7mw6CQ== + version "2.7.11" + resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.7.11.tgz#b27ff9114b3c5a73a64e379a2b195394d202d3f6" + integrity sha512-KQe79LrrgvJ2c5PqnhjKta45B64jyOWGOK34VIiujKzsYAN5ynBNYr1E0NaqcobIr/drCnA9S6LZg/rBprfm4w== dependencies: chalk "^4.1.2" hash-sum "^2.0.0" @@ -17211,9 +17388,9 @@ vue-template-compiler@2.6.14: he "^1.1.0" vue-template-compiler@^2.6.12, vue-template-compiler@^2.6.14: - version "2.7.10" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.10.tgz#9e20f35b2fdccacacf732dd7dedb49bf65f4556b" - integrity sha512-QO+8R9YRq1Gudm8ZMdo/lImZLJVUIAM8c07Vp84ojdDAf8HmPJc7XB556PcXV218k2AkKznsRz6xB5uOjAC4EQ== + version "2.7.11" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.11.tgz#e18e8e0dcb2647e8d6ff2ede8a3a92e677ff8dea" + integrity sha512-17QnXkFiBLOH3gGCA3nWAWpmdlTjOWLyP/2eg5ptgY1OvDBuIDGOW9FZ7ZSKmF1UFyf56mLR3/E1SlCTml1LWQ== dependencies: de-indent "^1.0.2" he "^1.2.0" @@ -17238,7 +17415,7 @@ vue@2.6.14: resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235" integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ== -vue@^2.0.0, vue@^2.6.10, vue@^2.6.12: +vue@^2.0.0, vue@^2.6.10: version "2.7.10" resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.10.tgz#ae516cc6c88e1c424754468844218fdd5e280f40" integrity sha512-HmFC70qarSHPXcKtW8U8fgIkF6JGvjEmDiVInTkKZP0gIlEPhlVlcJJLkdGIDiNkIeA2zJPQTWJUI4iWe+AVfg== @@ -17246,6 +17423,14 @@ vue@^2.0.0, vue@^2.6.10, vue@^2.6.12: "@vue/compiler-sfc" "2.7.10" csstype "^3.1.0" +vue@^2.6.12: + version "2.7.11" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.11.tgz#e051d54a131e7094c3ea3a86b2c6ecf25f8d0002" + integrity sha512-VPAW5QelT7Tx6UoSw/cwx/jDROOKAK1y/Q0o7HkmVJ1WAypE7w1+UoFa+KsGxy1aYdHPU1oODB3vR6XwSfVhDg== + dependencies: + "@vue/compiler-sfc" "2.7.11" + csstype "^3.1.0" + vuedraggable@2.24.3: version "2.24.3" resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.24.3.tgz#43c93849b746a24ce503e123d5b259c701ba0d19"