mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(xo-server): set XO6 the default page (#9212)
This commit is contained in:
@@ -4,14 +4,17 @@ import isEqual from 'lodash/isEqual.js'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { parseDuration } from '@vates/parse-duration'
|
||||
import { watch } from 'app-conf'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const { warn } = createLogger('xo:mixins:config')
|
||||
const { warn, info } = createLogger('xo:mixins:config')
|
||||
|
||||
// if path is undefined, an empty string or an empty array, returns the root value
|
||||
const niceGet = (value, path) => (path === undefined || path.length === 0 ? value : get(value, path))
|
||||
|
||||
export default class Config {
|
||||
constructor(app, { appDir, appName, config }) {
|
||||
this._app = app
|
||||
this._appDir = appDir
|
||||
this._config = config
|
||||
const watchers = (this._watchers = new Set())
|
||||
|
||||
@@ -90,4 +93,43 @@ export default class Config {
|
||||
watchDuration(path, cb) {
|
||||
return this.watch(path, cb, parseDuration)
|
||||
}
|
||||
|
||||
async getGuiRoutes() {
|
||||
const mounts = this.getOptional('http.mounts') ?? {}
|
||||
|
||||
let channel = 'latest'
|
||||
try {
|
||||
channel = await this._app.getCurrentChannel?.()
|
||||
} catch (error) {
|
||||
info('Unable to get current channel, fallback to latest', error)
|
||||
}
|
||||
|
||||
const guiRoutes = {}
|
||||
guiRoutes.xo5 = { url: '/v5', path: `${resolve(this._appDir, '..')}/xo-web/dist` }
|
||||
guiRoutes.xo6 = { url: '/v6', path: `${resolve(this._appDir, '../..')}/@xen-orchestra/web/dist` }
|
||||
|
||||
if (channel === 'stable') {
|
||||
guiRoutes.default = { ...guiRoutes.xo5, url: '/' }
|
||||
} else {
|
||||
guiRoutes.default = { ...guiRoutes.xo6, url: '/' }
|
||||
}
|
||||
|
||||
for (let [url, path] of Object.entries(mounts)) {
|
||||
url = url.replace(/(.+)\/$/, '$1')
|
||||
const conflictRoute = Object.entries(guiRoutes).find(([_, route]) => route.url === url)
|
||||
|
||||
if (conflictRoute) {
|
||||
const [key] = conflictRoute
|
||||
guiRoutes[key] = { url, path }
|
||||
} else {
|
||||
const key = `xo${url
|
||||
.split('/')
|
||||
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join('')}`
|
||||
guiRoutes[key] = { url, path }
|
||||
}
|
||||
}
|
||||
|
||||
return guiRoutes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,24 @@ export type XoApp = {
|
||||
config: {
|
||||
getOptional(path: string): Record<string, string> | undefined
|
||||
getOptionalDuration(path: string): number | undefined
|
||||
getGuiRoutes(): Promise<{
|
||||
default: {
|
||||
url: string
|
||||
path: string
|
||||
}
|
||||
xo5: {
|
||||
url: string
|
||||
path: string
|
||||
}
|
||||
xo6: {
|
||||
url: string
|
||||
path: string
|
||||
}
|
||||
[key: string]: {
|
||||
url: string
|
||||
path: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
objects: EventEmitter & {
|
||||
|
||||
@@ -12,16 +12,19 @@ import { badRequestResp, unauthorizedResp } from '../open-api/common/response.co
|
||||
import { xoaDashboard } from '../open-api/oa-examples/xoa.oa-example.mjs'
|
||||
import { XoaService } from './xoa.service.mjs'
|
||||
import { NDJSON_CONTENT_TYPE } from '../helpers/utils.helper.mjs'
|
||||
import { RestApi } from '../rest-api/rest-api.mjs'
|
||||
|
||||
@Route('')
|
||||
@Security('*')
|
||||
@Tags('xoa')
|
||||
@provide(XoaController)
|
||||
export class XoaController extends Controller {
|
||||
#restApi: RestApi
|
||||
#xoaService: XoaService
|
||||
|
||||
constructor(@inject(XoaService) xoaService: XoaService) {
|
||||
constructor(@inject(RestApi) restApi: RestApi, @inject(XoaService) xoaService: XoaService) {
|
||||
super()
|
||||
this.#restApi = restApi
|
||||
this.#xoaService = xoaService
|
||||
}
|
||||
|
||||
@@ -60,7 +63,12 @@ export class XoaController extends Controller {
|
||||
@Security('none')
|
||||
@Example(guiRoutes)
|
||||
@Get('gui-routes')
|
||||
getGuiRoutes(): XoGuiRoutes {
|
||||
return this.#xoaService.getGuiRoutes()
|
||||
async getGuiRoutes(): Promise<XoGuiRoutes> {
|
||||
const { xo5, xo6 } = await this.#restApi.xoApp.config.getGuiRoutes()
|
||||
|
||||
return {
|
||||
xo5: xo5.url,
|
||||
xo6: xo6.url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { parse } from 'xo-remote-parser'
|
||||
import { Writable } from 'node:stream'
|
||||
|
||||
import { type AsyncCacheEntry, getFromAsyncCache } from '../helpers/cache.helper.mjs'
|
||||
import { DashboardBackupRepositoriesSizeInfo, DashboardBackupsInfo, XoaDashboard, XoGuiRoutes } from './xoa.type.mjs'
|
||||
import { DashboardBackupRepositoriesSizeInfo, DashboardBackupsInfo, XoaDashboard } from './xoa.type.mjs'
|
||||
import { isReplicaVm, isSrWritableOrIso, promiseWriteInStream, vmContainsNoBakTag } from '../helpers/utils.helper.mjs'
|
||||
import type { MaybePromise } from '../helpers/helper.type.mjs'
|
||||
import { RestApi } from '../rest-api/rest-api.mjs'
|
||||
@@ -606,24 +606,4 @@ export class XoaService {
|
||||
vmsStatus,
|
||||
}
|
||||
}
|
||||
|
||||
getGuiRoutes(): XoGuiRoutes {
|
||||
const mounts = this.#restApi.xoApp.config.getOptional('http.mounts') ?? {}
|
||||
|
||||
let xo5Mount: string | undefined
|
||||
let xo6Mount: string | undefined
|
||||
|
||||
for (const [key, value] of Object.entries(mounts)) {
|
||||
if (value.includes('xo-web/dist')) {
|
||||
xo5Mount = key
|
||||
} else if (value.includes('@xen-orchestra/web/dist')) {
|
||||
xo6Mount = key
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
xo5: xo5Mount,
|
||||
xo6: xo6Mount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,6 @@ export type PingResponse = {
|
||||
}
|
||||
|
||||
export type XoGuiRoutes = {
|
||||
xo5: string | undefined
|
||||
xo6: string | undefined
|
||||
xo5?: string
|
||||
xo6?: string
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
- [Backups/s3] Update filesystem handling to use DeleteObjectsCommand in order to improve performance (PR [#9281](https://github.com/vatesfr/xen-orchestra/pull/9281))
|
||||
- [REST API] Add link to the openAPI JSON directly in the swagger description (PR [#9285](https://github.com/vatesfr/xen-orchestra/pull/9285))
|
||||
- [XO] XO6 is now the default page (PR [#9212](https://github.com/vatesfr/xen-orchestra/pull/9212))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "TURBO_TELEMETRY_DISABLED=1 turbo run build --filter xo-server --filter xo-server-'*' --filter xo-web",
|
||||
"build": "TURBO_TELEMETRY_DISABLED=1 turbo run build --filter xo-server --filter xo-server-'*' --filter xo-web --filter @xen-orchestra/web",
|
||||
"build:xo-lite": "turbo run build --filter @xen-orchestra/lite",
|
||||
"clean": "scripts/run-script.js --parallel clean",
|
||||
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",
|
||||
|
||||
@@ -161,9 +161,19 @@ requestTimeout = 0
|
||||
|
||||
# These mount points are exposed only when authenticated.
|
||||
[http.mounts]
|
||||
'/' = '../xo-web/dist/'
|
||||
# Uncomment to setup a default version.
|
||||
# Otherwise, XO5 will be the default for stable channel and XO6 for latest and source
|
||||
# '/' = '../xo-web/dist/'
|
||||
|
||||
'/v5' = '../xo-web/dist/'
|
||||
'/v6' = '../../@xen-orchestra/web/dist/'
|
||||
|
||||
[http.proxies]
|
||||
# [port] is used to reuse the same port declared in [http.listen.0]
|
||||
'/v5/api' = 'ws://localhost:[port]/api'
|
||||
'/v5/api/updater' = 'ws://localhost:9001'
|
||||
'/v5/rest' = 'http://localhost:[port]/rest'
|
||||
|
||||
[logs]
|
||||
# Display all logs matching this filter, regardless of their level
|
||||
#filter = 'xo:load-balancer'
|
||||
|
||||
@@ -152,6 +152,8 @@ port = 80
|
||||
# List of files/directories which will be served.
|
||||
[http.mounts]
|
||||
#'/any/url' = '/path/to/directory'
|
||||
#'/' = '../xo-web/dist/' # Uncomment this line to get v5 by default
|
||||
#'/' = '../../@xen-orchestra/web/dist/' # Uncomment this line to get v6 by default
|
||||
|
||||
# List of proxied URLs (HTTP & WebSockets).
|
||||
[http.proxies]
|
||||
|
||||
@@ -6,8 +6,8 @@ html
|
||||
meta(name = 'viewport' content = 'width=device-width, initial-scale=1.0')
|
||||
title Xen Orchestra
|
||||
meta(name = 'author' content = 'Vates SAS')
|
||||
link(rel = 'stylesheet' href = 'index.css')
|
||||
link(rel = 'icon' href = 'assets/favicon.svg' type = 'image/svg+xml')
|
||||
link(rel = 'stylesheet' href = `${xo5Mount}index.css`)
|
||||
link(rel = 'icon' href = `${xo5Mount}assets/favicon.svg` type = 'image/svg+xml')
|
||||
style.
|
||||
body {
|
||||
color: #F8F8F8;
|
||||
@@ -31,7 +31,7 @@ html
|
||||
body(style = 'display: flex; height: 100vh;')
|
||||
div(style = 'margin: auto; width: 20em;')
|
||||
div(style = 'display: flex;')
|
||||
img(src = 'assets/logo.svg' style = 'margin: auto; max-width: 100%;')
|
||||
img(src = `${xo5Mount}assets/logo.svg` style = 'margin: auto; max-width: 100%;')
|
||||
h2.text-xs-center.mb-2 Welcome to
|
||||
svg(viewbox='65 622 735 64' xmlns='http://www.w3.org/2000/svg' fill='currentColor')
|
||||
g
|
||||
|
||||
@@ -188,10 +188,16 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
|
||||
} else {
|
||||
errorMsg = req.flash('error')[0]
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// The login page uses certain files from the xo-web package (css,svg)
|
||||
// remove once XO5 is no more used and update `signin.pug` to use `public/logo.svg`, ...
|
||||
const { xo5 } = await xo.config.getGuiRoutes()
|
||||
res.send(
|
||||
signInPage({
|
||||
error: errorMsg,
|
||||
strategies,
|
||||
xo5Mount: xo5.url + '/',
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
@@ -544,6 +550,8 @@ const setUpProxies = (express, opts, xo) => {
|
||||
return
|
||||
}
|
||||
|
||||
const userPort = xo.config.get('http.listen.0.port')
|
||||
|
||||
const proxy = httpProxy
|
||||
.createServer({
|
||||
changeOrigin: true,
|
||||
@@ -566,6 +574,25 @@ const setUpProxies = (express, opts, xo) => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {URL} targetUrl
|
||||
*/
|
||||
function getTargetUrl(url) {
|
||||
let target = url
|
||||
if (url.includes('[port]')) {
|
||||
target = url.replace(/\[port\]/g, userPort)
|
||||
}
|
||||
|
||||
const targetUrl = new URL(target)
|
||||
if (targetUrl.port === 443) {
|
||||
targetUrl.protocol = targetUrl.protocol === 'ws:' ? 'wss:' : 'https:'
|
||||
}
|
||||
|
||||
return targetUrl
|
||||
}
|
||||
|
||||
// TODO: sort proxies by descending prefix length.
|
||||
|
||||
// HTTP request proxy.
|
||||
@@ -574,11 +601,11 @@ const setUpProxies = (express, opts, xo) => {
|
||||
|
||||
for (const prefix in opts) {
|
||||
if (url.startsWith(prefix)) {
|
||||
const target = opts[prefix]
|
||||
const target = getTargetUrl(opts[prefix])
|
||||
|
||||
proxy.web(req, res, {
|
||||
agent: new URL(target).hostname === 'localhost' ? undefined : xo.httpAgent,
|
||||
target: target + url.slice(prefix.length),
|
||||
agent: target.hostname === 'localhost' ? undefined : xo.httpAgent,
|
||||
target: target.href + url.slice(prefix.length),
|
||||
})
|
||||
|
||||
return
|
||||
@@ -599,11 +626,11 @@ const setUpProxies = (express, opts, xo) => {
|
||||
|
||||
for (const prefix in opts) {
|
||||
if (url.startsWith(prefix)) {
|
||||
const target = opts[prefix]
|
||||
const target = getTargetUrl(opts[prefix])
|
||||
|
||||
proxy.ws(req, socket, head, {
|
||||
agent: new URL(target).hostname === 'localhost' ? undefined : xo.httpAgent,
|
||||
target: target + url.slice(prefix.length),
|
||||
target: target.href + url.slice(prefix.length),
|
||||
})
|
||||
|
||||
return
|
||||
@@ -935,13 +962,18 @@ export default async function main(args) {
|
||||
|
||||
setUpProxies(express, config.http.proxies, xo)
|
||||
|
||||
setUpStaticFiles(express, config.http.mounts)
|
||||
|
||||
if (!safeMode) {
|
||||
await registerPlugins(xo)
|
||||
xo.emit('plugins:registered')
|
||||
}
|
||||
|
||||
const mounts = {}
|
||||
for (const guiRoute of Object.values(await xo.config.getGuiRoutes())) {
|
||||
mounts[guiRoute.url] = guiRoute.path
|
||||
}
|
||||
|
||||
setUpStaticFiles(express, mounts)
|
||||
|
||||
// Gracefully shutdown on signals.
|
||||
//
|
||||
// TODO: implements a timeout? (or maybe it is the services launcher
|
||||
|
||||
Reference in New Issue
Block a user