Compare commits

..

19 Commits

Author SHA1 Message Date
Julien Fontanet
9c78664426 fix(xo-server-auth-ldap): get name from LDAP record not user input
Fixes vatesfr/xo-web#1655
2019-12-23 10:51:44 +01:00
Julien Fontanet
5142bf4338 chore: update dependencies 2019-12-21 13:36:55 +01:00
Rajaa.BARHTAOUI
e3532612ff feat: release 5.42.0 (#4714) 2019-12-20 12:33:02 +01:00
Pierre Donias
d25e403233 fix(xo-web/remotes): refresh remote subscription right after an edition
And keep the subscription refresh after the `remote.test` call as well
2019-12-19 10:01:10 +01:00
Pierre Donias
8a5580eae5 fix(xo-web/remotes): SMB domain default value 2019-12-19 10:01:10 +01:00
Pierre Donias
cf1251ad7b fix(xo-web/remotes/edit): reset form before populating it with remote
Otherwise, the inputs that have already been edited by the user aren't properly
overwritten by the remote's values
2019-12-19 10:01:10 +01:00
Pierre Donias
4b1d0e8786 chore(xo-web/remotes): remove *s in placeholders since they can be confusing
See support#1974
2019-12-19 10:01:10 +01:00
Pierre Donias
b6e99ce4a6 fix(xo-web/remotes): simplify SMB host inputs patterns 2019-12-19 10:01:10 +01:00
Rajaa.BARHTAOUI
920def30d7 feat: technical release (#4711) 2019-12-18 14:23:04 +01:00
GHEMID Mohamed
3839aa7419 feat(xo-web/recipes): hub recipes (#4695)
See xoa#43

Kubernetes cluster recipe
2019-12-16 16:39:17 +01:00
Rajaa.BARHTAOUI
8fde720f02 feat(xo-web/xoa): display XOA build number (#4694)
Fixes #4693
2019-12-12 16:12:26 +01:00
Julien Fontanet
c6dfaa30b5 feat(xo-server/blocked-at): increase threshold to 1s
Should be enough to detect problems without flooding the logs.
2019-12-11 10:20:59 +01:00
Julien Fontanet
0d4975ba0f feat(xo-server/blocked-at): expose settings in config 2019-12-11 10:20:32 +01:00
Julien Fontanet
77325c98a6 feat(xo-server): use blocked-at instead of blocked
It provides more info regarding why the event loop was blocked.

It could not be used before because it requires Node >= 8.
2019-12-11 10:13:39 +01:00
Julien Fontanet
01dc088a6f feat(xo-server/blocked): use ms to format the time 2019-12-11 10:10:35 +01:00
Julien Fontanet
c20e9820fe feat(xo-server/workers): expose options in config 2019-12-10 12:03:18 +01:00
Julien Fontanet
d255c116dd feat(backups-cli): create-symlink-index command
```
> xo-backups create-symlink-indexes ./xo-vm-backups vm.name_label
> ls ./xo-vm-backups/indexes/vm.name_label
Debian\ 9.5\ 64bit\ web9 -> ../../c60dc26a-49e8-be58-6ae4-175cf03b19d5/
Prod\ VM -> ../../1498796a-3d23-d0cc-74be-b25d6e598c03/
```
2019-12-09 16:36:21 +01:00
Julien Fontanet
2c1da3458a chore(backups-cli): move utils in their own module 2019-12-09 16:36:19 +01:00
Julien Fontanet
8017e42797 feat(xo-server): passport strategies can be unregistered (#4702)
Now all authentication plugins can be unloaded.
2019-12-09 10:51:35 +01:00
55 changed files with 1231 additions and 1033 deletions

View File

@@ -0,0 +1,7 @@
const curryRight = require('lodash/curryRight')
module.exports = curryRight((iterable, fn) =>
Promise.all(
Array.isArray(iterable) ? iterable.map(fn) : Array.from(iterable, fn)
)
)

View File

@@ -0,0 +1,57 @@
const { dirname } = require('path')
const fs = require('promise-toolbox/promisifyAll')(require('fs'))
module.exports = fs
fs.mktree = async function mkdirp(path) {
try {
await fs.mkdir(path)
} catch (error) {
const { code } = error
if (code === 'EEXIST') {
await fs.readdir(path)
return
}
if (code === 'ENOENT') {
await mkdirp(dirname(path))
return mkdirp(path)
}
throw error
}
}
// - easier:
// - single param for direct use in `Array#map`
// - files are prefixed with directory path
// - safer: returns empty array if path is missing or not a directory
fs.readdir2 = path =>
fs.readdir(path).then(
entries => {
entries.forEach((entry, i) => {
entries[i] = `${path}/${entry}`
})
return entries
},
error => {
if (
error != null &&
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
) {
console.warn('WARN: readdir(%s)', path, error)
return []
}
throw error
}
)
fs.symlink2 = async (target, path) => {
try {
await fs.symlink(target, path)
} catch (error) {
if (error.code === 'EEXIST' && (await fs.readlink(path)) === target) {
return
}
throw error
}
}

View File

@@ -6,27 +6,21 @@ let force
// -----------------------------------------------------------------------------
const assert = require('assert')
const flatten = require('lodash/flatten')
const getopts = require('getopts')
const lockfile = require('proper-lockfile')
const pipe = require('promise-toolbox/pipe')
const { default: Vhd } = require('vhd-lib')
const { curryRight, flatten } = require('lodash')
const { dirname, resolve } = require('path')
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants')
const { pipe, promisifyAll } = require('promise-toolbox')
const fs = promisifyAll(require('fs'))
const asyncMap = require('../_asyncMap')
const fs = require('../_fs')
const handler = require('@xen-orchestra/fs').getHandler({ url: 'file://' })
// -----------------------------------------------------------------------------
const asyncMap = curryRight((iterable, fn) =>
Promise.all(
Array.isArray(iterable) ? iterable.map(fn) : Array.from(iterable, fn)
)
)
const filter = (...args) => thisArg => thisArg.filter(...args)
const isGzipFile = async fd => {
// https://tools.ietf.org/html/rfc1952.html#page-5
const magicNumber = Buffer.allocUnsafe(2)
@@ -89,24 +83,6 @@ const isValidXva = async path => {
const noop = Function.prototype
const readDir = path =>
fs.readdir(path).then(
entries => {
entries.forEach((entry, i) => {
entries[i] = `${path}/${entry}`
})
return entries
},
error => {
// a missing dir is by definition empty
if (error != null && error.code === 'ENOENT') {
return []
}
throw error
}
)
// -----------------------------------------------------------------------------
// chain is an array of VHDs from child to parent
@@ -157,12 +133,12 @@ async function mergeVhdChain(chain) {
const listVhds = pipe([
vmDir => vmDir + '/vdis',
readDir,
asyncMap(readDir),
fs.readdir2,
asyncMap(fs.readdir2),
flatten,
asyncMap(readDir),
asyncMap(fs.readdir2),
flatten,
filter(_ => _.endsWith('.vhd')),
_ => _.filter(_ => _.endsWith('.vhd')),
])
async function handleVm(vmDir) {
@@ -239,10 +215,12 @@ async function handleVm(vmDir) {
await Promise.all(deletions)
}
const [jsons, xvas] = await readDir(vmDir).then(entries => [
entries.filter(_ => _.endsWith('.json')),
new Set(entries.filter(_ => _.endsWith('.xva'))),
])
const [jsons, xvas] = await fs
.readdir2(vmDir)
.then(entries => [
entries.filter(_ => _.endsWith('.json')),
new Set(entries.filter(_ => _.endsWith('.xva'))),
])
await asyncMap(xvas, async path => {
// check is not good enough to delete the file, the best we can do is report

View File

@@ -0,0 +1,28 @@
const filenamify = require('filenamify')
const get = require('lodash/get')
const { dirname, join, relative } = require('path')
const asyncMap = require('../_asyncMap')
const { mktree, readdir2, readFile, symlink2 } = require('../_fs')
module.exports = async function createSymlinkIndex([backupDir, fieldPath]) {
const indexDir = join(backupDir, 'indexes', filenamify(fieldPath))
await mktree(indexDir)
await asyncMap(await readdir2(backupDir), async vmDir =>
asyncMap(
(await readdir2(vmDir)).filter(_ => _.endsWith('.json')),
async json => {
const metadata = JSON.parse(await readFile(json))
const value = get(metadata, fieldPath)
if (value !== undefined) {
const target = relative(indexDir, dirname(json))
const path = join(indexDir, filenamify(String(value)))
await symlink2(target, path).catch(error => {
console.warn('symlink(%s, %s)', target, path, error)
})
}
}
)
)
}

View File

@@ -7,6 +7,12 @@ require('./_composeCommands')({
},
usage: '[--force] xo-vm-backups/*',
},
'create-symlink-index': {
get main() {
return require('./commands/create-symlink-index')
},
usage: 'xo-vm-backups <field path>',
},
})(process.argv.slice(2), 'xo-backups').catch(error => {
console.error('main', error)
process.exitCode = 1

View File

@@ -5,9 +5,10 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/fs": "^0.10.2",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"proper-lockfile": "^4.1.1",
"vhd-lib": "^0.7.2"
},

View File

@@ -21,7 +21,7 @@
"node": ">=8.10"
},
"dependencies": {
"@marsaud/smb2": "^0.14.0",
"@marsaud/smb2": "^0.15.0",
"@sindresorhus/df": "^3.1.1",
"@xen-orchestra/async-map": "^0.0.0",
"decorator-synchronized": "^0.5.0",
@@ -30,7 +30,7 @@
"get-stream": "^5.1.0",
"limit-concurrency-decorator": "^0.4.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"readable-stream": "^3.0.6",
"through2": "^3.0.0",
"tmp": "^0.1.0",

View File

@@ -31,7 +31,7 @@
},
"dependencies": {
"lodash": "^4.17.4",
"promise-toolbox": "^0.14.0"
"promise-toolbox": "^0.15.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -8,12 +8,31 @@
### Released packages
## **5.41.0** (2019-11-29)
## **5.42.0** (2019-12-20)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### Highlights
- [SDN Controller] Allow private network creation on bond and VLAN (PR [#4682](https://github.com/vatesfr/xen-orchestra/pull/4682))
- [Hub/recipes] [Ability to create a kubernetes cluster](https://xen-orchestra.com/blog/devblog-5-kubernetes-clutser-on-xo/) (PR [#4695](https://github.com/vatesfr/xen-orchestra/pull/4695))
### Enhancements
- [XOA] Display XOA build number [#4693](https://github.com/vatesfr/xen-orchestra/issues/4693) (PR [#4694](https://github.com/vatesfr/xen-orchestra/pull/4694))
### Released packages
- xo-server v5.54.0
- xo-web v5.54.0
## **5.41.0** (2019-11-29)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Highlights
- [Backup NG] Make report recipients configurable in the backup settings [#4581](https://github.com/vatesfr/xen-orchestra/issues/4581) (PR [#4646](https://github.com/vatesfr/xen-orchestra/pull/4646))
- [Host] Advanced Live Telemetry (PR [#4680](https://github.com/vatesfr/xen-orchestra/pull/4680))
- [Plugin] [Web hooks](https://xen-orchestra.com/docs/web-hooks.html) [#1946](https://github.com/vatesfr/xen-orchestra/issues/1946) (PR [#3155](https://github.com/vatesfr/xen-orchestra/pull/3155))
@@ -44,8 +63,6 @@
## **5.40.2** (2019-11-22)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Enhancements
- [Logs] Ability to report a bug with attached log (PR [#4201](https://github.com/vatesfr/xen-orchestra/pull/4201))

View File

@@ -7,8 +7,6 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [SDN Controller] Allow private network creation on bond and VLAN (PR [#4682](https://github.com/vatesfr/xen-orchestra/pull/4682))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
@@ -20,5 +18,5 @@
>
> Rule of thumb: add packages on top.
- xo-server v5.54.0
- xo-web v5.54.0
- xo-server v5.55.0
- xo-web v5.55.0

View File

@@ -17,14 +17,14 @@
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.113.0",
"flow-bin": "^0.114.0",
"globby": "^10.0.0",
"husky": "^3.0.0",
"jest": "^24.1.0",
"lint-staged": "^9.5.0",
"lodash": "^4.17.4",
"prettier": "^1.10.2",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"sorted-object": "^2.0.1"
},
"engines": {

View File

@@ -42,7 +42,7 @@
"cross-env": "^6.0.3",
"execa": "^3.2.0",
"index-modules": "^0.3.0",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"rimraf": "^3.0.0",
"tmp": "^0.1.0"
},

View File

@@ -27,8 +27,8 @@
"from2": "^2.3.0",
"fs-extra": "^8.0.1",
"limit-concurrency-decorator": "^0.4.0",
"promise-toolbox": "^0.14.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.15.0",
"struct-fu": "^1.2.0",
"uuid": "^3.0.1"
},

View File

@@ -46,7 +46,7 @@
"make-error": "^1.3.0",
"minimist": "^1.2.0",
"ms": "^2.1.1",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"pw": "0.0.4",
"xmlrpc": "^1.3.2",
"xo-collection": "^0.4.1"

View File

@@ -43,7 +43,7 @@
"nice-pipe": "0.0.0",
"pretty-ms": "^5.0.0",
"progress-stream": "^2.0.0",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"pump": "^3.0.0",
"pw": "^0.0.4",
"strip-indent": "^3.0.0",

View File

@@ -1,3 +0,0 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -1,48 +0,0 @@
{
"name": "xo-server-auth-http",
"version": "0.0.0",
"license": "AGPL-3.0",
"description": "Basic HTTP authentication plugin for XO-Server",
"keywords": [
"authorization",
"basic",
"http",
"orchestra",
"plugin",
"xen-orchestra",
"xen",
"xo-server"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-auth-http",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "packages/xo-server-auth-http",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"main": "dist/",
"files": [
"dist/"
],
"engines": {
"node": ">=8.10"
},
"dependencies": {
"passport-http": "^0.3.0"
},
"devDependencies": {
"@babel/cli": "^7.7.4",
"@babel/core": "^7.7.4",
"@babel/preset-env": "^7.7.4",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -1,46 +0,0 @@
import { BasicStrategy } from 'passport-http'
export const configurationSchema = {
type: 'object',
properties: {
realm: {
type: 'string',
},
},
required: ['realm'],
}
class Plugin {
constructor({ xo }) {
this._configuration = undefined
this._unregisterPassportStrategy = undefined
this._xo = xo
}
configure(configuration) {
this._configuration = configuration
}
load() {
const xo = this._xo
this._unregisterPassportStrategy = xo.registerPassportStrategy(
new BasicStrategy(
this._configuration,
async (username, password, done) => {
try {
const { user } = await xo.authenticateUser({ username, password })
done(null, user)
} catch (error) {
done(null, false, { message: error.message })
}
}
)
)
}
unload() {
this._unregisterPassportStrategy()
}
}
export default opts => new Plugin(opts)

View File

@@ -39,7 +39,7 @@
"inquirer": "^7.0.0",
"ldapjs": "^1.0.1",
"lodash": "^4.17.4",
"promise-toolbox": "^0.14.0"
"promise-toolbox": "^0.15.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -1,7 +1,7 @@
/* eslint no-throw-literal: 0 */
import eventToPromise from 'event-to-promise'
import noop from 'lodash/noop'
import { find, identity, noop } from 'lodash'
import { createClient } from 'ldapjs'
import { escape } from 'ldapjs/lib/filters/escape'
import { promisify } from 'promise-toolbox'
@@ -25,6 +25,22 @@ const evalFilter = (filter, vars) =>
return escape(value)
})
const makeEvalFormat = format =>
format === undefined
? identity
: (input, record) =>
format.replace(VAR_RE, (_, name) => {
if (name === 'input') {
return input
}
let tmp = find(record.attributes, _ => _.type === name)
if (tmp !== undefined && (tmp = tmp.vals).length !== 0) {
return tmp[0]
}
throw new Error(`invalid entry ${name}`)
})
export const configurationSchema = {
type: 'object',
@@ -100,6 +116,12 @@ Or something like this if you also want to filter by group:
type: 'string',
default: DEFAULTS.filter,
},
usernameFormat: {
description: `
`.trim(),
type: 'string',
},
},
required: ['uri', 'base'],
}
@@ -157,15 +179,10 @@ class AuthLdap {
}
}
const {
bind: credentials,
base: searchBase,
filter: searchFilter = DEFAULTS.filter,
} = conf
this._credentials = credentials
this._searchBase = searchBase
this._searchFilter = searchFilter
this._credentials = conf.bind
this._formatUsername = makeEvalFormat(conf.usernameFormat)
this._searchBase = conf.base
;({ filter: this._searchFilter = DEFAULTS.filter } = conf)
}
load() {
@@ -242,6 +259,9 @@ class AuthLdap {
try {
logger(`attempting to bind as ${entry.objectName}`)
await bind(entry.objectName, password)
username = this._formatUsername(username, entry)
logger(
`successfully bound as ${entry.objectName} => ${username} authenticated`
)

View File

@@ -157,6 +157,8 @@ const promptByType = {
defaultValue && defaultValue[name],
subpath
)
} else {
value[name] = schema.default
}
}

View File

@@ -30,8 +30,8 @@
"dependencies": {
"@xen-orchestra/log": "^0.2.0",
"lodash": "^4.17.11",
"node-openssl-cert": "^0.0.103",
"promise-toolbox": "^0.14.0",
"node-openssl-cert": "^0.0.116",
"promise-toolbox": "^0.15.0",
"uuid": "^3.3.2"
},
"private": true

View File

@@ -36,7 +36,7 @@
"golike-defer": "^0.4.1",
"jest": "^24.8.0",
"lodash": "^4.17.11",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"xo-collection": "^0.4.1",
"xo-common": "^0.2.0",
"xo-lib": "^0.9.0"

View File

@@ -34,7 +34,7 @@
"dependencies": {
"nodemailer": "^6.1.0",
"nodemailer-markdown": "^1.0.1",
"promise-toolbox": "^0.14.0"
"promise-toolbox": "^0.15.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -33,7 +33,7 @@
"node": ">=6"
},
"dependencies": {
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"slack-node": "^0.1.8"
},
"devDependencies": {

View File

@@ -42,7 +42,7 @@
"html-minifier": "^4.0.0",
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.14.0"
"promise-toolbox": "^0.15.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -72,6 +72,10 @@ vmBackupSizeTimeout = '2 seconds'
poolMetadataTimeout = '10 minutes'
# https://github.com/naugtur/blocked-at#params-and-return-value
[blockedAtOptions]
threshold = 1000
# Helmet handles HTTP security via headers
#
# https://helmetjs.github.io/docs/
@@ -108,5 +112,11 @@ timeout = 600e3
# see https:#github.com/vatesfr/xen-orchestra/issues/3419
# useSudo = false
# https://github.com/facebook/jest/blob/master/packages/jest-worker/README.md#options-object-optional
#
#[workerOptions]
#numWorkers = 2
[xapiOptions]
maxUncoalescedVdis = 1

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.53.0",
"version": "5.54.0",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -47,7 +47,7 @@
"async-iterator-to-stream": "^1.0.1",
"base64url": "^3.0.0",
"bind-property-descriptor": "^1.0.0",
"blocked": "^1.2.1",
"blocked-at": "^1.2.0",
"bluebird": "^3.5.1",
"body-parser": "^1.18.2",
"compression": "^1.7.3",
@@ -84,8 +84,8 @@
"julien-f-source-map-support": "0.1.0",
"julien-f-unzip": "^0.2.1",
"kindof": "^2.0.0",
"level": "^4.0.0",
"level-party": "^3.0.4",
"level": "^6.0.0",
"level-party": "^4.0.0",
"level-sublevel": "^6.6.1",
"limit-concurrency-decorator": "^0.4.0",
"lodash": "^4.17.4",
@@ -102,7 +102,7 @@
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"pretty-format": "^24.0.0",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"proxy-agent": "^3.0.0",
"pug": "^2.0.0-rc.4",
"pump": "^3.0.0",

View File

@@ -1,7 +1,7 @@
import appConf from 'app-conf'
import assert from 'assert'
import authenticator from 'otplib/authenticator'
import blocked from 'blocked'
import blocked from 'blocked-at'
import compression from 'compression'
import createExpress from 'express'
import createLogger from '@xen-orchestra/log'
@@ -9,6 +9,7 @@ import crypto from 'crypto'
import has from 'lodash/has'
import helmet from 'helmet'
import includes from 'lodash/includes'
import ms from 'ms'
import proxyConsole from './proxy-console'
import pw from 'pw'
import serveStatic from 'serve-static'
@@ -124,10 +125,6 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
strategy,
{ label = strategy.label, name = strategy.name } = {}
) => {
if (name in strategies) {
throw new TypeError('duplicate passport strategy ' + name)
}
passport.use(name, strategy)
if (name !== 'local') {
strategies[name] = label ?? name
@@ -179,9 +176,7 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
}
if (authenticator.check(req.body.otp, user.preferences.otp)) {
setToken(req, res, next).then(() =>
res.redirect(303, req.flash('return-url')[0] || '/')
)
setToken(req, res, next)
} else {
req.flash('error', 'Invalid code')
res.redirect(303, '/signin-otp')
@@ -193,7 +188,7 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
parseDuration
)
const SESSION_VALIDITY = ifDef(authCfg.sessionCookieValidity, parseDuration)
const setToken = async (req, res) => {
const setToken = async (req, res, next) => {
const { user, isPersistent } = req.session
const token = await xo.createAuthenticationToken({
expiresIn: isPersistent ? PERMANENT_VALIDITY : SESSION_VALIDITY,
@@ -210,6 +205,7 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
delete req.session.isPersistent
delete req.session.user
res.redirect(303, req.flash('return-url')[0] || '/')
}
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
@@ -229,12 +225,6 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
}
if (!user) {
if (typeof info === 'string') {
res.statusCode = 401
res.setHeader('WWW-Authenticate', info)
return res.end('unauthorized')
}
req.flash('error', info ? info.message : 'Invalid credentials')
return res.redirect(303, '/signin')
}
@@ -247,17 +237,16 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
return res.redirect(303, '/signin-otp')
}
await setToken(req, res)
res.redirect(303, req.flash('return-url')[0] || '/')
setToken(req, res, next)
})(req, res, next)
}
if (req.cookies.token) {
return next()
next()
} else {
req.flash('return-url', url)
res.redirect(authCfg.defaultSignInPage)
}
req.flash('return-url', url)
return res.redirect(authCfg.defaultSignInPage)
})
// Install the local strategy.
@@ -654,20 +643,18 @@ export default async function main(args) {
return USAGE
}
const config = await loadConfiguration()
{
const logPerf = createLogger('xo:perf')
blocked(
ms => {
logPerf.info(`blocked for ${ms | 0}ms`)
},
{
threshold: 500,
}
)
blocked((time, stack) => {
logPerf.info(`blocked for ${ms(time)}`, {
time,
stack,
})
}, config.blockedAtOptions)
}
const config = await loadConfiguration()
const webServer = await createWebServer(config.http)
// Now the web server is listening, drop privileges.

View File

@@ -9,7 +9,10 @@ export default class Workers {
app.on('start', () => {
process.env.XO_CONFIG = JSON.stringify(config)
this._worker = new Worker(require.resolve('./worker'))
this._worker = new Worker(
require.resolve('./worker'),
config.workerOptions
)
})
app.on('stop', () => this._worker.end())
}

View File

@@ -27,7 +27,7 @@
"child-process-promise": "^2.0.3",
"core-js": "^3.0.0",
"pipette": "^0.9.3",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"tmp": "^0.1.0",
"vhd-lib": "^0.7.2"
},

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.53.3",
"version": "5.54.0",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -98,7 +98,7 @@
"moment-timezone": "^0.5.14",
"notifyjs": "^3.0.0",
"otplib": "^11.0.0",
"promise-toolbox": "^0.14.0",
"promise-toolbox": "^0.15.0",
"prop-types": "^15.6.0",
"qrcode": "^1.3.2",
"random-password": "^0.1.2",

View File

@@ -897,13 +897,13 @@ export default {
remoteNamePlaceHolder: 'nom distant*',
// Original text: "Name *"
remoteMyNamePlaceHolder: 'Nom *',
remoteMyNamePlaceHolder: 'Nom',
// Original text: "/path/to/backup"
remoteLocalPlaceHolderPath: '/chemin/de/la/sauvegarde',
// Original text: "host *"
remoteNfsPlaceHolderHost: 'hôte *',
remoteNfsPlaceHolderHost: 'hôte',
// Original text: "path/to/backup"
remoteNfsPlaceHolderPath: 'chemin/de/la/sauvegarde',
@@ -922,7 +922,7 @@ export default {
remoteSmbPlaceHolderDomain: 'Domaine',
// Original text: "<address>\\<share> *"
remoteSmbPlaceHolderAddressShare: '<adresse>\\<partage> *',
remoteSmbPlaceHolderAddressShare: '<adresse>\\<partage>',
// Original text: "password(fill to edit)"
remotePlaceHolderPassword: 'mot de passe (saisir ici pour éditer)',

View File

@@ -840,13 +840,13 @@ export default {
remoteNamePlaceHolder: 'távoli név *',
// Original text: "Name *"
remoteMyNamePlaceHolder: 'Név *',
remoteMyNamePlaceHolder: 'Név',
// Original text: "/path/to/backup"
remoteLocalPlaceHolderPath: '/path/to/backup',
// Original text: "host *"
remoteNfsPlaceHolderHost: 'kiszolgáló *',
remoteNfsPlaceHolderHost: 'kiszolgáló',
// Original text: "path/to/backup"
remoteNfsPlaceHolderPath: 'path/to/backup',
@@ -864,7 +864,7 @@ export default {
remoteSmbPlaceHolderDomain: 'Domain',
// Original text: "<address>\\<share> *"
remoteSmbPlaceHolderAddressShare: '<address>\\<share> *',
remoteSmbPlaceHolderAddressShare: '<address>\\<share>',
// Original text: "password(fill to edit)"
remotePlaceHolderPassword: 'jelszó(kattintson a módosításhoz)',

View File

@@ -722,13 +722,13 @@ export default {
remoteNamePlaceHolder: 'Nazwa zdalna*',
// Original text: "Name *"
remoteMyNamePlaceHolder: 'Nazwa *',
remoteMyNamePlaceHolder: 'Nazwa',
// Original text: "/path/to/backup"
remoteLocalPlaceHolderPath: '/ścieżka/do/kopii/zapasowej',
// Original text: "host *"
remoteNfsPlaceHolderHost: 'Host *',
remoteNfsPlaceHolderHost: 'Host',
// Original text: "/path/to/backup"
remoteNfsPlaceHolderPath: '/ścieżka/do/kopii/zapasowej',
@@ -746,7 +746,7 @@ export default {
remoteSmbPlaceHolderDomain: 'Domena',
// Original text: "<address>\\<share> *"
remoteSmbPlaceHolderAddressShare: '<adres>\\<udział> *',
remoteSmbPlaceHolderAddressShare: '<adres>\\<udział>',
// Original text: "password(fill to edit)"
remotePlaceHolderPassword: 'Hasło (wypełnij)',

View File

@@ -1255,13 +1255,13 @@ export default {
remoteNamePlaceHolder: 'hedef adı *',
// Original text: "Name *"
remoteMyNamePlaceHolder: 'Ad *',
remoteMyNamePlaceHolder: 'Ad',
// Original text: "/path/to/backup"
remoteLocalPlaceHolderPath: '/yedek/için/yol',
// Original text: "host *"
remoteNfsPlaceHolderHost: 'sunucu *',
remoteNfsPlaceHolderHost: 'sunucu',
// Original text: 'Port'
remoteNfsPlaceHolderPort: undefined,
@@ -1282,7 +1282,7 @@ export default {
remoteSmbPlaceHolderDomain: 'Domain',
// Original text: "<address>\\\\<share> *"
remoteSmbPlaceHolderAddressShare: '<paylaşım>\\\\<adresi> *',
remoteSmbPlaceHolderAddressShare: '<paylaşım>\\\\<adresi>',
// Original text: "password(fill to edit)"
remotePlaceHolderPassword: 'parola(düzenlemek için doldurun)',

View File

@@ -542,9 +542,9 @@ const messages = {
remoteAuth: 'Auth',
remoteDeleteTip: 'Delete',
remoteDeleteSelected: 'Delete selected remotes',
remoteMyNamePlaceHolder: 'Name *',
remoteMyNamePlaceHolder: 'Name',
remoteLocalPlaceHolderPath: '/path/to/backup',
remoteNfsPlaceHolderHost: 'Host *',
remoteNfsPlaceHolderHost: 'Host',
remoteNfsPlaceHolderPort: 'Port',
remoteNfsPlaceHolderPath: 'path/to/backup',
remoteNfsPlaceHolderOptions: 'Custom mount options. Default: vers=3',
@@ -552,7 +552,7 @@ const messages = {
remoteSmbPlaceHolderUsername: 'Username',
remoteSmbPlaceHolderPassword: 'Password',
remoteSmbPlaceHolderDomain: 'Domain',
remoteSmbPlaceHolderAddressShare: '<address>\\\\<share> *',
remoteSmbPlaceHolderAddressShare: '<address>\\\\<share>',
remoteSmbPlaceHolderOptions: 'Custom mount options',
remotePlaceHolderPassword: 'Password(fill to edit)',
@@ -1880,6 +1880,7 @@ const messages = {
changeChannel: 'Change channel',
updaterCommunity:
'The Web updater, the release channels and the proxy settings are available in XOA.',
xoaBuild: 'XOA build:',
// ----- OS Disclaimer -----
disclaimerTitle: 'Xen Orchestra from the sources',
@@ -2174,6 +2175,7 @@ const messages = {
// Hub
hubPage: 'Hub',
hubCommunity: 'Hub is available in XOA',
noDefaultSr: 'The selected pool has no default SR',
successfulInstall: 'VM installed successfully',
vmNoAvailable: 'No VMs available ',
@@ -2187,6 +2189,15 @@ const messages = {
hubImportNotificationTitle: 'XVA import',
hubTemplateDescriptionNotAvailable:
'No description available for this template',
recipeCreatedSuccessfully: 'Recipe created successfully',
recipeViewCreatedVms: 'View created VMs',
templatesLabel: 'Templates',
recipesLabel: 'Recipes',
network: 'Network',
recipeMasterNameLabel: 'Master name',
recipeNumberOfNodesLabel: 'Number of nodes',
recipeSshKeyLabel: 'SSH key',
recipeNetworkCidr: 'Network CIDR',
// Licenses
xosanUnregisteredDisclaimer:

View File

@@ -40,7 +40,7 @@ export class Notification extends Component {
return
}
error = (title, body) =>
error = (title, body, autoCloseTimeout = 6e3) =>
notification.error(
title,
this.props.isAdmin ? (
@@ -58,10 +58,12 @@ export class Notification extends Component {
) : (
body
),
6e3
autoCloseTimeout
)
info = (title, body) => notification.info(title, body, 3e3)
success = (title, body) => notification.success(title, body, 3e3)
info = (title, body, autoCloseTimeout = 3e3) =>
notification.info(title, body, autoCloseTimeout)
success = (title, body, autoCloseTimeout = 3e3) =>
notification.success(title, body, autoCloseTimeout)
}}
/>
)

View File

@@ -66,3 +66,8 @@ export const markHubResourceAsInstalled = createAction(
'MARK_HUB_RESOURCE_AS_INSTALLED',
id => id
)
export const markRecipeAsCreating = createAction(
'MARK_RECIPE_AS_CREATING',
id => id
)
export const markRecipeAsDone = createAction('MARK_RECIPE_AS_DONE', id => id)

View File

@@ -106,6 +106,19 @@ export default {
}
),
// whether a resource is currently being created: `recipeCreatingResources[<recipe id>]`
recipeCreatingResources: combineActionHandlers(
{},
{
[actions.markRecipeAsCreating]: (prevRecipeCreatingResources, id) => ({
...prevRecipeCreatingResources,
[id]: true,
}),
[actions.markRecipeAsDone]: (prevRecipeCreatedResources, id) =>
omit(prevRecipeCreatedResources, id),
}
),
objects: combineActionHandlers(
{
all: {}, // Mutable for performance!

View File

@@ -1188,6 +1188,9 @@ export const changeVirtualizationMode = vm =>
})
)
export const createKubernetesCluster = params =>
_call('xoa.recipe.createKubernetesCluster', params)
export const deleteTemplates = templates =>
confirm({
title: _('templateDeleteModalTitle', { templates: templates.length }),
@@ -2284,6 +2287,7 @@ export const disableRemote = remote =>
export const editRemote = (remote, { name, url, options }) =>
_call('remote.set', resolveIds({ remote, name, url, options }))::tap(() => {
subscribeRemotes.forceRefresh()
testRemote(remote).catch(noop)
})
@@ -3013,3 +3017,5 @@ export const openTunnel = () =>
export const subscribeTunnelState = createSubscription(() =>
_call('xoa.supportTunnel.getState')
)
export const getApplianceInfo = () => _call('xoa.getApplianceInfo')

View File

@@ -91,6 +91,14 @@
@extend .fa;
@extend .fa-chevron-down;
}
&-hub-recipe {
@extend .fa;
@extend .fa-wpforms;
}
&-hub-template {
@extend .fa;
@extend .fa-cube;
}
&-grab {
@extend .fa;

View File

@@ -1,63 +1,55 @@
import _ from 'intl'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import { addSubscriptions, adminOnly } from 'utils'
import { Container, Col, Row } from 'grid'
import { injectState, provideState } from 'reaclette'
import { isEmpty, map, omit, orderBy } from 'lodash'
import { subscribeHubResourceCatalog } from 'xo'
import { getXoaPlan, routes, TryXoa } from 'utils'
import { NavLink, NavTabs } from 'nav'
import Page from '../page'
import Resource from './resource'
import Recipes from './recipes'
import Templates from './templates'
// ==================================================================
const HEADER = (
<h2>
<Icon icon='menu-hub' /> {_('hubPage')}
</h2>
const Header = (
<Container>
<Row>
<Col mediumSize={3}>
<h2>
<Icon icon='menu-hub' /> {_('hubPage')}
</h2>
</Col>
<Col mediumSize={9}>
<NavTabs className='pull-right'>
<NavLink to='/hub/templates'>
<Icon icon='hub-template' /> {_('templatesLabel')}
</NavLink>
<NavLink to='/hub/recipes'>
<Icon icon='hub-recipe' /> {_('recipesLabel')}
</NavLink>
</NavTabs>
</Col>
</Row>
</Container>
)
export default decorate([
adminOnly,
addSubscriptions({
catalog: subscribeHubResourceCatalog,
}),
provideState({
computed: {
resources: (_, { catalog }) =>
orderBy(
map(omit(catalog, '_namespaces'), (entry, namespace) => ({
namespace,
...entry.xva,
})),
'name',
'asc'
),
},
}),
injectState,
({ state: { resources } }) => (
<Page header={HEADER} title='hubPage' formatTitle>
const Hub = routes('hub', {
templates: Templates,
recipes: Recipes,
})(({ children }) => (
<Page header={Header} title='hubPage' formatTitle>
{getXoaPlan() === 'Community' ? (
<Container>
<Row>
{isEmpty(resources) ? (
<Col>
<h2 className='text-muted'>
&nbsp; {_('vmNoAvailable')}
<Icon icon='alarm' color='yellow' />
</h2>
</Col>
) : (
resources.map(data => (
<Col key={data.namespace} mediumSize={6} largeSize={4}>
<Resource {...data} />
</Col>
))
)}
</Row>
<h2 className='text-info'>{_('hubCommunity')}</h2>
<p>
<TryXoa page='hub' />
</p>
</Container>
</Page>
),
])
) : (
children
)}
</Page>
))
export default Hub

View File

@@ -0,0 +1,21 @@
import decorate from 'apply-decorators'
import React from 'react'
import { adminOnly } from 'utils'
import { Container, Col, Row } from 'grid'
import Recipe from './recipe'
// ==================================================================
export default decorate([
adminOnly,
() => (
<Container>
<Row>
<Col mediumSize={4}>
<Recipe />
</Col>
</Row>
</Container>
),
])

View File

@@ -0,0 +1,135 @@
import * as FormGrid from 'form-grid'
import _, { messages } from 'intl'
import decorate from 'apply-decorators'
import React from 'react'
import { Container } from 'grid'
import { get } from '@xen-orchestra/defined'
import { injectIntl } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { isSrWritable } from 'xo'
import { SelectPool, SelectNetwork, SelectSr } from 'select-objects'
export default decorate([
injectIntl,
provideState({
effects: {
onChangePool(__, pool) {
const { onChange, value } = this.props
onChange({
...value,
pool,
})
},
onChangeSr(__, sr) {
const { onChange, value } = this.props
onChange({
...value,
sr,
})
},
onChangeNetwork(__, network) {
const { onChange, value } = this.props
onChange({
...value,
network,
})
},
onChangeValue(__, ev) {
const { name, value } = ev.target
const { onChange, value: prevValue } = this.props
onChange({
...prevValue,
[name]: value,
})
},
},
computed: {
networkPredicate: (_, { value: { pool } }) => network =>
pool.id === network.$pool,
srPredicate: (_, { value }) => sr =>
sr.$pool === get(() => value.pool.id) && isSrWritable(sr),
},
}),
injectState,
({ effects, install, intl: { formatMessage }, state, value }) => (
<Container>
<FormGrid.Row>
<label>{_('vmImportToPool')}</label>
<SelectPool
className='mb-1'
onChange={effects.onChangePool}
required
value={value.pool}
/>
</FormGrid.Row>
<FormGrid.Row>
<label>{_('vmImportToSr')}</label>
<SelectSr
onChange={effects.onChangeSr}
predicate={state.srPredicate}
required
value={value.sr}
/>
</FormGrid.Row>
<FormGrid.Row>
<label>{_('network')}</label>
<SelectNetwork
className='mb-1'
onChange={effects.onChangeNetwork}
required
value={value.network}
predicate={state.networkPredicate}
/>
</FormGrid.Row>
<FormGrid.Row>
<label>{_('recipeMasterNameLabel')}</label>
<input
className='form-control'
name='masterName'
onChange={effects.onChangeValue}
placeholder={formatMessage(messages.recipeMasterNameLabel)}
required
type='text'
value={value.masterName}
/>
</FormGrid.Row>
<FormGrid.Row>
<label>{_('recipeNumberOfNodesLabel')}</label>
<input
className='form-control'
name='nbNodes'
min='1'
onChange={effects.onChangeValue}
placeholder={formatMessage(messages.recipeNumberOfNodesLabel)}
required
type='number'
value={value.nbNodes}
/>
</FormGrid.Row>
<FormGrid.Row>
<label>{_('recipeSshKeyLabel')}</label>
<input
className='form-control'
name='sshKey'
onChange={effects.onChangeValue}
placeholder={formatMessage(messages.recipeSshKeyLabel)}
required
type='text'
value={value.sshKey}
/>
</FormGrid.Row>
<FormGrid.Row>
<label>{_('recipeNetworkCidr')}</label>
<input
className='form-control'
name='cidr'
onChange={effects.onChangeValue}
placeholder={formatMessage(messages.recipeNetworkCidr)}
required
type='text'
value={value.cidr}
/>
</FormGrid.Row>
</Container>
),
])

View File

@@ -0,0 +1,109 @@
import * as ComplexMatcher from 'complex-matcher'
import _ from 'intl'
import ActionButton from 'action-button'
import ButtonLink from 'button-link'
import decorate from 'apply-decorators'
import Icon from 'icon'
import marked from 'marked'
import React from 'react'
import { Card, CardBlock, CardHeader } from 'card'
import { escapeRegExp } from 'lodash'
import { form } from 'modal'
import { connectStore } from 'utils'
import { createGetObjectsOfType } from 'selectors'
import { createKubernetesCluster } from 'xo'
import { injectState, provideState } from 'reaclette'
import { success } from 'notification'
import { withRouter } from 'react-router'
import RecipeForm from './recipe-form'
const RECIPE_INFO = {
id: '05abc8a8-ebf4-41a6-b1ed-efcb2dbf893d',
name: 'Kubernetes cluster',
description:
'Creates a Kubernetes cluster composed of 1 master and a configurable number of nodes working for the master.',
}
export default decorate([
withRouter,
connectStore(() => ({
pools: createGetObjectsOfType('pool'),
recipeCreatingResources: state => state.recipeCreatingResources,
})),
provideState({
initialState: () => ({
selectedInstallPools: [],
}),
effects: {
async create() {
const { markRecipeAsCreating, markRecipeAsDone } = this.props
const recipeParams = await form({
defaultValue: {
pool: {},
},
render: props => <RecipeForm {...props} />,
header: (
<span>
<Icon icon='hub-recipe' /> {RECIPE_INFO.name}
</span>
),
size: 'medium',
})
const { cidr, masterName, nbNodes, network, sr, sshKey } = recipeParams
markRecipeAsCreating(RECIPE_INFO.id)
const tag = await createKubernetesCluster({
cidr,
masterName,
nbNodes: +nbNodes,
network: network.id,
sr: sr.id,
sshKey,
})
markRecipeAsDone(RECIPE_INFO.id)
const filter = new ComplexMatcher.Property(
'tags',
new ComplexMatcher.RegExp(`^${escapeRegExp(tag)}$`, 'i')
)
success(
_('recipeCreatedSuccessfully'),
<ButtonLink
btnStyle='success'
size='small'
to={`/home?s=${encodeURIComponent(filter)}`}
>
{_('recipeViewCreatedVms')}
</ButtonLink>,
8e3
)
},
},
}),
injectState,
({ effects, recipeCreatingResources }) => (
<Card shadow>
<CardHeader>{RECIPE_INFO.name}</CardHeader>
<CardBlock>
<div
className='text-muted'
dangerouslySetInnerHTML={{
__html: marked(RECIPE_INFO.description),
}}
/>
<hr />
<ActionButton
block
handler={effects.create}
icon='deploy'
pending={recipeCreatingResources[RECIPE_INFO.id]}
>
{_('create')}
</ActionButton>
</CardBlock>
</Card>
),
])

View File

@@ -0,0 +1,54 @@
import _ from 'intl'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import { addSubscriptions, adminOnly } from 'utils'
import { Container, Col, Row } from 'grid'
import { injectState, provideState } from 'reaclette'
import { isEmpty, map, omit, orderBy } from 'lodash'
import { subscribeHubResourceCatalog } from 'xo'
import Resource from './resource'
// ==================================================================
export default decorate([
adminOnly,
addSubscriptions({
catalog: subscribeHubResourceCatalog,
}),
provideState({
computed: {
resources: (_, { catalog }) =>
orderBy(
map(omit(catalog, '_namespaces'), (entry, namespace) => ({
namespace,
...entry.xva,
})),
'name',
'asc'
),
},
}),
injectState,
({ state: { resources } }) => (
<Container>
<Row>
{isEmpty(resources) ? (
<Col>
<h2 className='text-muted'>
&nbsp; {_('vmNoAvailable')}
<Icon icon='alarm' color='yellow' />
</h2>
</Col>
) : (
resources.map(data => (
<Col key={data.namespace} mediumSize={6} largeSize={4}>
<Resource {...data} />
</Col>
))
)}
</Row>
</Container>
),
])

View File

@@ -8,7 +8,7 @@ import React from 'react'
import { alert, form } from 'modal'
import { Card, CardBlock, CardHeader } from 'card'
import { Col, Row } from 'grid'
import { connectStore, formatSize, getXoaPlan } from 'utils'
import { connectStore, formatSize } from 'utils'
import { createGetObjectsOfType } from 'selectors'
import { deleteTemplates, downloadAndInstallResource, pureDeleteVm } from 'xo'
import { error, success } from 'notification'
@@ -30,18 +30,6 @@ const EXCLUSIVE_FIELDS = ['longDescription'] // These fields will not have a lab
const MARKDOWN_FIELDS = ['longDescription', 'description']
const STATIC_FIELDS = [...EXCLUSIVE_FIELDS, ...BANNED_FIELDS] // These fields will not be displayed with dynamic fields
const subscribeAlert = () =>
alert(
_('hubResourceAlert'),
<div>
<p>
{_('considerSubscribe', {
link: 'https://xen-orchestra.com',
})}
</p>
</div>
)
export default decorate([
withRouter,
connectStore(() => {
@@ -69,10 +57,6 @@ export default decorate([
version,
} = this.props
const { isTemplateInstalled } = this.state
if (getXoaPlan() === 'Community') {
subscribeAlert()
return
}
const resourceParams = await form({
defaultValue: {
mapPoolsSrs: {},
@@ -127,10 +111,6 @@ export default decorate([
async create() {
const { isPoolCreated, installedTemplates } = this.state
const { name } = this.props
if (getXoaPlan() === 'Community') {
subscribeAlert()
return
}
const resourceParams = await form({
defaultValue: {
pool: undefined,

View File

@@ -330,9 +330,21 @@ export default class Menu extends Component {
],
},
isAdmin && {
to: '/hub',
to: '/hub/templates',
icon: 'menu-hub',
label: 'hubPage',
subMenu: [
{
to: '/hub/templates',
icon: 'hub-template',
label: 'templatesLabel',
},
{
to: '/hub/recipes',
icon: 'hub-recipe',
label: 'recipesLabel',
},
],
},
isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
!noOperatablePools && {

View File

@@ -312,7 +312,10 @@ const INDIVIDUAL_ACTIONS = [
level: 'primary',
},
{
handler: (remote, { editRemote }) => editRemote(remote),
handler: (remote, { reset, editRemote }) => {
reset()
editRemote(remote)
},
icon: 'edit',
label: _('formEdit'),
level: 'primary',
@@ -372,6 +375,7 @@ export default decorate([
columns={COLUMNS_LOCAL_REMOTE}
data-editRemote={effects.editRemote}
data-formatMessage={formatMessage}
data-reset={effects.reset}
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
@@ -388,6 +392,7 @@ export default decorate([
columns={COLUMNS_NFS_REMOTE}
data-editRemote={effects.editRemote}
data-formatMessage={formatMessage}
data-reset={effects.reset}
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
@@ -404,6 +409,7 @@ export default decorate([
columns={COLUMNS_SMB_REMOTE}
data-editRemote={effects.editRemote}
data-formatMessage={formatMessage}
data-reset={effects.reset}
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}

View File

@@ -43,7 +43,7 @@ export default decorate([
editRemote: ({ reset }) => state => {
const {
remote,
domain = remote.domain,
domain = remote.domain || '',
host = remote.host,
name,
options = remote.options || '',
@@ -242,7 +242,7 @@ export default decorate([
className='form-control'
name='host'
onChange={effects.linkState}
pattern='^([^\\/]+)\\([^\\/]+)$'
pattern='^[^\\/]+\\[^\\/]+$'
placeholder={formatMessage(
messages.remoteSmbPlaceHolderAddressShare
)}
@@ -255,7 +255,7 @@ export default decorate([
className='form-control'
name='path'
onChange={effects.linkState}
pattern='^(([^\\/]+)+(\\[^\\/]+)*)?$'
pattern='^([^\\/]+(\\[^\\/]+)*)?$'
placeholder={formatMessage(
messages.remoteSmbPlaceHolderRemotePath
)}

View File

@@ -15,12 +15,12 @@ import { confirm } from 'modal'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { generateId, linkState, toggleState } from 'reaclette-utils'
import { getApplianceInfo, subscribeBackupNgJobs, subscribeJobs } from 'xo'
import { injectIntl } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { Input as DebounceInput } from 'debounce-input-decorator'
import { isEmpty, map, pick, some, zipObject } from 'lodash'
import { Password, Select } from 'form'
import { subscribeBackupNgJobs, subscribeJobs } from 'xo'
import { getXoaPlan, TryXoa } from '../../../common/utils'
@@ -258,6 +258,10 @@ const Updates = decorate([
.map(name => `- ${name}: ${installedPackages[name]}`)
.join('\n'),
proxyFormId: generateId,
xoaBuild: async () => {
const { build = 'unknown' } = await getApplianceInfo()
return build
},
},
}),
injectState,
@@ -290,7 +294,7 @@ const Updates = decorate([
<CardBlock>
<fieldset disabled={COMMUNITY}>
<p>
{_('currentVersion')}{' '}
{_('xoaBuild')} {state.xoaBuild} - {_('currentVersion')}{' '}
{defined(
() => state.installedPackages['xen-orchestra'],
'unknown'

1255
yarn.lock

File diff suppressed because it is too large Load Diff