Compare commits
106 Commits
xo-server-
...
log-v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95def95678 | ||
|
|
8274a00f91 | ||
|
|
3c6c4976cd | ||
|
|
0fd35b1679 | ||
|
|
3c931604be | ||
|
|
02dddbd662 | ||
|
|
675763d039 | ||
|
|
63acc7ef32 | ||
|
|
cbd78bdfef | ||
|
|
a09a2ed6c3 | ||
|
|
7d18a6d8a9 | ||
|
|
65a5984d4c | ||
|
|
d5f519bf5a | ||
|
|
bede39c8f3 | ||
|
|
a9e3682776 | ||
|
|
87c3c8732f | ||
|
|
0011bfea8c | ||
|
|
e047649c3b | ||
|
|
de397b63c5 | ||
|
|
75b7726fca | ||
|
|
d83a2366c2 | ||
|
|
2d4d653c55 | ||
|
|
c7a1d55f6f | ||
|
|
46b5c5ccd1 | ||
|
|
4607417e7a | ||
|
|
76887c7e25 | ||
|
|
ab7cae5816 | ||
|
|
b1ce389ad8 | ||
|
|
52aa5ff780 | ||
|
|
30372e511e | ||
|
|
dc15a6282a | ||
|
|
97dcc204ef | ||
|
|
ecda3e0174 | ||
|
|
8342bb2bc8 | ||
|
|
51a137c4e5 | ||
|
|
a26ced5de9 | ||
|
|
85f0c69c03 | ||
|
|
3aac757ef5 | ||
|
|
91541d0ba4 | ||
|
|
dfd66a56c3 | ||
|
|
60f9393d29 | ||
|
|
cdced7cdc1 | ||
|
|
69709009ed | ||
|
|
bf14560709 | ||
|
|
775b629ee9 | ||
|
|
ec9717dafb | ||
|
|
0cd84ee250 | ||
|
|
b3681e7c39 | ||
|
|
a7a7597d9a | ||
|
|
bed3da81e1 | ||
|
|
c43dc31a55 | ||
|
|
c5a21922d1 | ||
|
|
2ae660a46b | ||
|
|
f6fcae4489 | ||
|
|
e0a3b8ace8 | ||
|
|
b67231c56b | ||
|
|
aa5b3dc426 | ||
|
|
1a528adfbb | ||
|
|
64d295ee3f | ||
|
|
b940ade902 | ||
|
|
37a906a233 | ||
|
|
e76603ce7e | ||
|
|
aca9aa0a7a | ||
|
|
6d20ef5d51 | ||
|
|
4d18ab1ae0 | ||
|
|
fa5c707fbc | ||
|
|
37b9d8ec10 | ||
|
|
61db0269a2 | ||
|
|
a8ad13f60e | ||
|
|
f14dd04ea7 | ||
|
|
0add8cd5a3 | ||
|
|
16cc539a57 | ||
|
|
5ba25a34cb | ||
|
|
61de65fc21 | ||
|
|
5195539a95 | ||
|
|
ce93fb0e4c | ||
|
|
3cb58ed700 | ||
|
|
bb48c960fe | ||
|
|
286a0031dd | ||
|
|
dcbd7e1113 | ||
|
|
0a43454c8a | ||
|
|
f5f1491e47 | ||
|
|
e935ae567f | ||
|
|
3f08f099fe | ||
|
|
18a5ba0029 | ||
|
|
c426d0328f | ||
|
|
91b2456c15 | ||
|
|
585aa74e0c | ||
|
|
eefaec5abd | ||
|
|
c7a5eebff6 | ||
|
|
f077528936 | ||
|
|
39728974b1 | ||
|
|
e14585895b | ||
|
|
0999042718 | ||
|
|
4e2e669533 | ||
|
|
de266ae6a8 | ||
|
|
d7cd87a6e4 | ||
|
|
c5aabbadc2 | ||
|
|
36a5e3c2ab | ||
|
|
f475261b9a | ||
|
|
62dce8f92a | ||
|
|
e6d90d2154 | ||
|
|
b5d823ec1a | ||
|
|
a786c68e8b | ||
|
|
e6fa00c4d8 | ||
|
|
5721fac793 |
10
.travis.yml
10
.travis.yml
@@ -2,7 +2,6 @@ language: node_js
|
||||
node_js:
|
||||
#- stable # disable for now due to an issue of indirect dep upath with Node 9
|
||||
- 8
|
||||
- 6
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
@@ -10,9 +9,9 @@ sudo: false
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- qemu-utils
|
||||
- blktap-utils
|
||||
- vmdk-stream-converter
|
||||
- qemu-utils
|
||||
- blktap-utils
|
||||
- vmdk-stream-converter
|
||||
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash
|
||||
@@ -22,5 +21,4 @@ cache:
|
||||
yarn: true
|
||||
|
||||
script:
|
||||
- yarn run test
|
||||
- yarn run test-integration
|
||||
- yarn run travis-tests
|
||||
|
||||
@@ -8,7 +8,12 @@ const MAX_DELAY = 2 ** 31 - 1
|
||||
class Job {
|
||||
constructor (schedule, fn) {
|
||||
const wrapper = () => {
|
||||
const result = fn()
|
||||
let result
|
||||
try {
|
||||
result = fn()
|
||||
} catch (_) {
|
||||
// catch any thrown value to ensure it does not break the job
|
||||
}
|
||||
let then
|
||||
if (result != null && typeof (then = result.then) === 'function') {
|
||||
then.call(result, scheduleNext, scheduleNext)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import getStream from 'get-stream'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { fromCallback, fromEvent, ignoreErrors } from 'promise-toolbox'
|
||||
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
|
||||
import { type Readable, type Writable } from 'stream'
|
||||
import { parse } from 'xo-remote-parser'
|
||||
|
||||
@@ -17,12 +17,18 @@ type File = FileDescriptor | string
|
||||
|
||||
const checksumFile = file => file + '.checksum'
|
||||
|
||||
export const DEFAULT_TIMEOUT = 10000
|
||||
|
||||
export default class RemoteHandlerAbstract {
|
||||
_remote: Object
|
||||
constructor (remote: any) {
|
||||
this._remote = { ...remote, ...parse(remote.url) }
|
||||
if (this._remote.type !== this.type) {
|
||||
throw new Error('Incorrect remote type')
|
||||
if (remote.url === 'test://') {
|
||||
this._remote = remote
|
||||
} else {
|
||||
this._remote = { ...remote, ...parse(remote.url) }
|
||||
if (this._remote.type !== this.type) {
|
||||
throw new Error('Incorrect remote type')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +127,7 @@ export default class RemoteHandlerAbstract {
|
||||
newPath: string,
|
||||
{ checksum = false }: Object = {}
|
||||
) {
|
||||
let p = this._rename(oldPath, newPath)
|
||||
let p = timeout.call(this._rename(oldPath, newPath), DEFAULT_TIMEOUT)
|
||||
if (checksum) {
|
||||
p = Promise.all([
|
||||
p,
|
||||
@@ -142,7 +148,7 @@ export default class RemoteHandlerAbstract {
|
||||
prependDir = false,
|
||||
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
|
||||
): Promise<string[]> {
|
||||
let entries = await this._list(dir)
|
||||
let entries = await timeout.call(this._list(dir), DEFAULT_TIMEOUT)
|
||||
if (filter !== undefined) {
|
||||
entries = entries.filter(filter)
|
||||
}
|
||||
@@ -165,28 +171,30 @@ export default class RemoteHandlerAbstract {
|
||||
{ checksum = false, ignoreMissingChecksum = false, ...options }: Object = {}
|
||||
): Promise<LaxReadable> {
|
||||
const path = typeof file === 'string' ? file : file.path
|
||||
const streamP = this._createReadStream(file, options).then(stream => {
|
||||
// detect early errors
|
||||
let promise = fromEvent(stream, 'readable')
|
||||
const streamP = timeout
|
||||
.call(this._createReadStream(file, options), DEFAULT_TIMEOUT)
|
||||
.then(stream => {
|
||||
// detect early errors
|
||||
let promise = fromEvent(stream, 'readable')
|
||||
|
||||
// try to add the length prop if missing and not a range stream
|
||||
if (
|
||||
stream.length === undefined &&
|
||||
options.end === undefined &&
|
||||
options.start === undefined
|
||||
) {
|
||||
promise = Promise.all([
|
||||
promise,
|
||||
ignoreErrors.call(
|
||||
this.getSize(file).then(size => {
|
||||
stream.length = size
|
||||
})
|
||||
),
|
||||
])
|
||||
}
|
||||
// try to add the length prop if missing and not a range stream
|
||||
if (
|
||||
stream.length === undefined &&
|
||||
options.end === undefined &&
|
||||
options.start === undefined
|
||||
) {
|
||||
promise = Promise.all([
|
||||
promise,
|
||||
ignoreErrors.call(
|
||||
this.getSize(file).then(size => {
|
||||
stream.length = size
|
||||
})
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
return promise.then(() => stream)
|
||||
})
|
||||
return promise.then(() => stream)
|
||||
})
|
||||
|
||||
if (!checksum) {
|
||||
return streamP
|
||||
@@ -224,7 +232,10 @@ export default class RemoteHandlerAbstract {
|
||||
}
|
||||
|
||||
async openFile (path: string, flags?: string): Promise<FileDescriptor> {
|
||||
return { fd: await this._openFile(path, flags), path }
|
||||
return {
|
||||
fd: await timeout.call(this._openFile(path, flags), DEFAULT_TIMEOUT),
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
async _openFile (path: string, flags?: string): Promise<mixed> {
|
||||
@@ -232,7 +243,7 @@ export default class RemoteHandlerAbstract {
|
||||
}
|
||||
|
||||
async closeFile (fd: FileDescriptor): Promise<void> {
|
||||
await this._closeFile(fd.fd)
|
||||
await timeout.call(this._closeFile(fd.fd), DEFAULT_TIMEOUT)
|
||||
}
|
||||
|
||||
async _closeFile (fd: mixed): Promise<void> {
|
||||
@@ -252,10 +263,13 @@ export default class RemoteHandlerAbstract {
|
||||
{ checksum = false, ...options }: Object = {}
|
||||
): Promise<LaxWritable> {
|
||||
const path = typeof file === 'string' ? file : file.path
|
||||
const streamP = this._createOutputStream(file, {
|
||||
flags: 'wx',
|
||||
...options,
|
||||
})
|
||||
const streamP = timeout.call(
|
||||
this._createOutputStream(file, {
|
||||
flags: 'wx',
|
||||
...options,
|
||||
}),
|
||||
DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
if (!checksum) {
|
||||
return streamP
|
||||
@@ -290,7 +304,7 @@ export default class RemoteHandlerAbstract {
|
||||
ignoreErrors.call(this._unlink(checksumFile(file)))
|
||||
}
|
||||
|
||||
await this._unlink(file)
|
||||
await timeout.call(this._unlink(file), DEFAULT_TIMEOUT)
|
||||
}
|
||||
|
||||
async _unlink (file: mixed): Promise<void> {
|
||||
@@ -298,7 +312,7 @@ export default class RemoteHandlerAbstract {
|
||||
}
|
||||
|
||||
async getSize (file: mixed): Promise<number> {
|
||||
return this._getSize(file)
|
||||
return timeout.call(this._getSize(file), DEFAULT_TIMEOUT)
|
||||
}
|
||||
|
||||
async _getSize (file: mixed): Promise<number> {
|
||||
|
||||
111
@xen-orchestra/fs/src/abstract.spec.js
Normal file
111
@xen-orchestra/fs/src/abstract.spec.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { TimeoutError } from 'promise-toolbox'
|
||||
|
||||
import AbstractHandler, { DEFAULT_TIMEOUT } from './abstract'
|
||||
|
||||
class TestHandler extends AbstractHandler {
|
||||
constructor (impl) {
|
||||
super({ url: 'test://' })
|
||||
|
||||
Object.keys(impl).forEach(method => {
|
||||
this[`_${method}`] = impl[method]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe('rename()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
rename: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.rename('oldPath', 'newPath')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('list()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
list: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.list()
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createReadStream()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
createReadStream: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.createReadStream('file')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('openFile()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
openFile: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.openFile('path')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('closeFile()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
closeFile: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.closeFile({ fd: undefined, path: '' })
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createOutputStream()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
createOutputStream: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.createOutputStream('File')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unlink()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
unlink: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.unlink('')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSize()', () => {
|
||||
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
getSize: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.getSize('')
|
||||
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/log",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
|
||||
@@ -86,7 +86,7 @@ export const catchGlobalErrors = logger => {
|
||||
const { prototype } = EventEmitter
|
||||
const { emit } = prototype
|
||||
function patchedEmit (event, error) {
|
||||
if (event === 'error' && !this.listenerCount(event)) {
|
||||
if (event === 'error' && this.listenerCount(event) === 0) {
|
||||
logger.error('unhandled error event', { error })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -38,6 +38,17 @@ for (const name in LEVELS) {
|
||||
const level = LEVELS[name]
|
||||
|
||||
prototype[name.toLowerCase()] = function (message, data) {
|
||||
if (typeof message !== 'string') {
|
||||
if (message instanceof Error) {
|
||||
data = { error: message }
|
||||
;({ message = 'an error has occured' } = message)
|
||||
} else {
|
||||
return this.warn('incorrect value passed to logger', {
|
||||
level,
|
||||
value: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
global[symbol](new Log(data, level, this._namespace, message, new Date()))
|
||||
}
|
||||
}
|
||||
|
||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -4,6 +4,62 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Usage Report] Add IOPS read/write/total per VM [#3309](https://github.com/vatesfr/xen-orchestra/issues/3309) (PR [#3455](https://github.com/vatesfr/xen-orchestra/pull/3455))
|
||||
- [Self service] Sort resource sets by name (PR [#3507](https://github.com/vatesfr/xen-orchestra/pull/3507))
|
||||
- [Usage Report] Add top 3 SRs which use the most IOPS read/write/total [#3306](https://github.com/vatesfr/xen-orchestra/issues/3306) (PR [#3508](https://github.com/vatesfr/xen-orchestra/pull/3508))
|
||||
- [New VM] Display a warning when the memory is below the template memory static min [#3496](https://github.com/vatesfr/xen-orchestra/issues/3496) (PR [#3513](https://github.com/vatesfr/xen-orchestra/pull/3513))
|
||||
- [Backup NG form] Add link to plugins setting [#3457](https://github.com/vatesfr/xen-orchestra/issues/3457) (PR [#3514](https://github.com/vatesfr/xen-orchestra/pull/3514))
|
||||
- [Backup reports] Add job and run ID [#3488](https://github.com/vatesfr/xen-orchestra/issues/3488) (PR [#3516](https://github.com/vatesfr/xen-orchestra/pull/3516))
|
||||
- [Usage Report] Add top 3 VMs which use the most IOPS read/write/total [#3308](https://github.com/vatesfr/xen-orchestra/issues/3308) (PR [#3463](https://github.com/vatesfr/xen-orchestra/pull/3463))
|
||||
- [Settings/logs] Homogenize action buttons in table and enable bulk deletion [#3179](https://github.com/vatesfr/xen-orchestra/issues/3179) (PR [#3528](https://github.com/vatesfr/xen-orchestra/pull/3528))
|
||||
- [Settings/acls] Add bulk deletion [#3179](https://github.com/vatesfr/xen-orchestra/issues/3179) (PR [#3536](https://github.com/vatesfr/xen-orchestra/pull/3536))
|
||||
- [Home] Improve search usage: raw numbers also match in names [#2906](https://github.com/vatesfr/xen-orchestra/issues/2906) (PR [#3552](https://github.com/vatesfr/xen-orchestra/pull/3552))
|
||||
- [Backup NG] Timeout of a job is now in hours [#3550](https://github.com/vatesfr/xen-orchestra/issues/3550) (PR [#3553](https://github.com/vatesfr/xen-orchestra/pull/3553))
|
||||
- [Backup NG] Explicit error if a VM is missing [#3434](https://github.com/vatesfr/xen-orchestra/issues/3434) (PR [#3522](https://github.com/vatesfr/xen-orchestra/pull/3522))
|
||||
- [Backup NG] Show all advanced settings with non-default values in overview [#3549](https://github.com/vatesfr/xen-orchestra/issues/3549) (PR [#3554](https://github.com/vatesfr/xen-orchestra/pull/3554))
|
||||
- [Backup NG] Collapse advanced settings by default [#3551](https://github.com/vatesfr/xen-orchestra/issues/3551) (PR [#3559](https://github.com/vatesfr/xen-orchestra/pull/3559))
|
||||
- [Scheduling] Merge selection and interval tabs [#1902](https://github.com/vatesfr/xen-orchestra/issues/1902) (PR [#3519](https://github.com/vatesfr/xen-orchestra/pull/3519))
|
||||
- [Backup NG/Restore] The backup selector now also shows the job name [#3366](https://github.com/vatesfr/xen-orchestra/issues/3366) (PR [#3564](https://github.com/vatesfr/xen-orchestra/pull/3564))
|
||||
- Sort buttons by criticality in tables [#3168](https://github.com/vatesfr/xen-orchestra/issues/3168) (PR [#3545](https://github.com/vatesfr/xen-orchestra/pull/3545))
|
||||
- [Usage Report] Ability to send a daily report [#3544](https://github.com/vatesfr/xen-orchestra/issues/3544) (PR [#3582](https://github.com/vatesfr/xen-orchestra/pull/3582))
|
||||
- [Backup NG logs] Disable state filters with no entries [#3438](https://github.com/vatesfr/xen-orchestra/issues/3438) (PR [#3442](https://github.com/vatesfr/xen-orchestra/pull/3442))
|
||||
- [ACLs] Global performance improvement on UI for non-admin users [#3578](https://github.com/vatesfr/xen-orchestra/issues/3578) (PR [#3584](https://github.com/vatesfr/xen-orchestra/pull/3584))
|
||||
- [Backup NG] Improve the Schedule's view (Replace table by list) [#3491](https://github.com/vatesfr/xen-orchestra/issues/3491) (PR [#3586](https://github.com/vatesfr/xen-orchestra/pull/3586))
|
||||
- ([Host/Storage], [Sr/hosts]) add bulk deletion [#3179](https://github.com/vatesfr/xen-orchestra/issues/3179) (PR [#3539](https://github.com/vatesfr/xen-orchestra/pull/3539))
|
||||
- [xo-server] Use @xen-orchestra/log for basic logging [#3555](https://github.com/vatesfr/xen-orchestra/issues/3555) (PR [#3579](https://github.com/vatesfr/xen-orchestra/pull/3579))
|
||||
- [Backup Report] Log error when job failed [#3458](https://github.com/vatesfr/xen-orchestra/issues/3458) (PR [#3593](https://github.com/vatesfr/xen-orchestra/pull/3593))
|
||||
- [Backup NG] Display logs for backup restoration [#2511](https://github.com/vatesfr/xen-orchestra/issues/2511) (PR [#3609](https://github.com/vatesfr/xen-orchestra/pull/3609))
|
||||
- [XOA] Display product version and list of all installed packages [#3560](https://github.com/vatesfr/xen-orchestra/issues/3560) (PR [#3621](https://github.com/vatesfr/xen-orchestra/pull/3621))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Remotes] Fix removal of broken remotes [#3327](https://github.com/vatesfr/xen-orchestra/issues/3327) (PR [#3521](https://github.com/vatesfr/xen-orchestra/pull/3521))
|
||||
- [Backups] Fix stuck backups due to broken NFS remotes [#3467](https://github.com/vatesfr/xen-orchestra/issues/3467) (PR [#3534](https://github.com/vatesfr/xen-orchestra/pull/3534))
|
||||
- [New VM] Fix missing cloud config when creating multiple VMs at once in some cases [#3532](https://github.com/vatesfr/xen-orchestra/issues/3532) (PR [#3535](https://github.com/vatesfr/xen-orchestra/pull/3535))
|
||||
- [VM] Fix an error when an admin tried to add a disk on a Self VM whose resource set had been deleted [#2814](https://github.com/vatesfr/xen-orchestra/issues/2814) (PR [#3530](https://github.com/vatesfr/xen-orchestra/pull/3530))
|
||||
- [Self/Create VM] Fix some quotas based on the template instead of the user inputs [#2683](https://github.com/vatesfr/xen-orchestra/issues/2683) (PR [#3546](https://github.com/vatesfr/xen-orchestra/pull/3546))
|
||||
- [Self] Ignore DR and CR VMs when computing quotas [#3064](https://github.com/vatesfr/xen-orchestra/issues/3064) (PR [#3561](https://github.com/vatesfr/xen-orchestra/pull/3561))
|
||||
- [Patches] Wrongly requiring to eject CDs from halted VMs and snapshots before installing patches (PR [#3611](https://github.com/vatesfr/xen-orchestra/pull/3611))
|
||||
- [Jobs] Ensure the scheduling is not interrupted in rare cases (PR [#3617](https://github.com/vatesfr/xen-orchestra/pull/3617))
|
||||
- [Home] Fix `server.getAll` error at login when user is not admin [#2335](https://github.com/vatesfr/xen-orchestra/issues/2335) (PR [#3613](https://github.com/vatesfr/xen-orchestra/pull/3613))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-backup-reports v0.15.0
|
||||
- xo-common v0.1.2
|
||||
- @xen-orchestra/log v0.1.0
|
||||
- @xen-orchestra/fs v0.4.0
|
||||
- complex-matcher v0.5.0
|
||||
- vhd-lib v0.4.0
|
||||
- xen-api v0.20.0
|
||||
- xo-server-usage-report v0.7.0
|
||||
- xo-server v5.29.0
|
||||
- xo-web v5.29.0
|
||||
|
||||
## **5.28.0** (2018-10-05)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Host/Networks] Remove "Add network" button [#3386](https://github.com/vatesfr/xen-orchestra/issues/3386) (PR [#3478](https://github.com/vatesfr/xen-orchestra/pull/3478))
|
||||
- [Host/networks] Private networks table [#3387](https://github.com/vatesfr/xen-orchestra/issues/3387) (PR [#3481](https://github.com/vatesfr/xen-orchestra/pull/3481))
|
||||
- [Home/pool] Patch count pill now shows the number of unique patches in the pool [#3321](https://github.com/vatesfr/xen-orchestra/issues/3321) (PR [#3483](https://github.com/vatesfr/xen-orchestra/pull/3483))
|
||||
|
||||
@@ -39,6 +39,10 @@ You can check if a coalesce job is currently active by running `ps axf | grep vh
|
||||
|
||||
If you don't see any running coalesce jobs, and can't find any other reason that XenServer has not started one, you can attempt to make it start a coalesce job by rescanning the SR. This is harmless to try, but will not always result in a coalesce. Visit the problematic SR in the XOA UI, then click the "Rescan All Disks" button towards the top right: it looks like a refresh circle icon. This should begin the coalesce process - if you click the Advanced tab in the SR view, the "disks needing to be coalesced" list should become smaller and smaller.
|
||||
|
||||
### Parse Error
|
||||
|
||||
This is most likely due to running a backup job that uses Delta functionality (eg: delta backups, or continuous replication) on a version of XenServer older than 6.5. To use delta functionality you must run [XenServer 6.5 or later](https://xen-orchestra.com/docs/supported-version.html).
|
||||
|
||||
### SR_BACKEND_FAILURE_44 (insufficient space)
|
||||
|
||||
> This message can be triggered by any backup method.
|
||||
@@ -72,4 +76,4 @@ To check your free space, enter your XOA and run `xoa check` to check free syste
|
||||
|
||||
This is happening when you have a *smart backup job* that doesn't match any VMs. For example: you created a job to backup all running VMs. If no VMs are running on backup schedule, you'll have this message. This could also happen if you lost connection with your pool master (the VMs aren't visible anymore from Xen Orchestra).
|
||||
|
||||
Edit your job and try to see matching VMs or check if your pool is connected to XOA.
|
||||
Edit your job and try to see matching VMs or check if your pool is connected to XOA.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# xo-cli
|
||||
|
||||
This is another client of `xo-server` - this time in command line form.
|
||||
@@ -106,3 +105,4 @@ encoding by prefixing with `json:`:
|
||||
```
|
||||
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
||||
```
|
||||
> Note: `xo-cli` only supports the import of XVA files. It will not import OVA files. To import OVA images, you must use the XOA web UI.
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"timers": "fake",
|
||||
"collectCoverage": true,
|
||||
"projects": [
|
||||
"<rootDir>"
|
||||
@@ -57,7 +58,8 @@
|
||||
"prepare": "scripts/run-script prepare",
|
||||
"pretest": "eslint --ignore-path .gitignore .",
|
||||
"test": "jest \"^(?!.*\\.integ\\.spec\\.js$)\"",
|
||||
"test-integration": "jest \".integ\\.spec\\.js$\""
|
||||
"test-integration": "jest \".integ\\.spec\\.js$\"",
|
||||
"travis-tests": "scripts/travis-tests"
|
||||
},
|
||||
"workspaces": [
|
||||
"@xen-orchestra/*",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const ast = new CM.And([
|
||||
new CM.Or([new CM.String('wonderwoman'), new CM.String('batman')])
|
||||
),
|
||||
new CM.TruthyProperty('hasCape'),
|
||||
new CM.Property('age', new CM.Number(32)),
|
||||
new CM.Property('age', new CM.NumberOrStringNode('32')),
|
||||
new CM.GlobPattern('chi*go'),
|
||||
new CM.RegExp('^foo/bar\\.', 'i'),
|
||||
])
|
||||
|
||||
@@ -153,6 +153,34 @@ export class NumberNode extends Node {
|
||||
}
|
||||
export { NumberNode as Number }
|
||||
|
||||
export class NumberOrStringNode extends Node {
|
||||
constructor (value) {
|
||||
super()
|
||||
|
||||
this.value = value
|
||||
|
||||
// should not be enumerable for the tests
|
||||
Object.defineProperty(this, 'match', {
|
||||
value: this.match.bind(this, value.toLowerCase(), +value),
|
||||
})
|
||||
}
|
||||
|
||||
match (lcValue, numValue, value) {
|
||||
return (
|
||||
value === numValue ||
|
||||
(typeof value === 'string'
|
||||
? value.toLowerCase().indexOf(lcValue) !== -1
|
||||
: (Array.isArray(value) || isPlainObject(value)) &&
|
||||
some(value, this.match))
|
||||
)
|
||||
}
|
||||
|
||||
toString () {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
export { NumberOrStringNode as NumberOrString }
|
||||
|
||||
export class Property extends Node {
|
||||
constructor (name, child) {
|
||||
super()
|
||||
@@ -564,7 +592,7 @@ const parser = P.grammar({
|
||||
const asNum = +str
|
||||
return Number.isNaN(asNum)
|
||||
? new GlobPattern(str)
|
||||
: new NumberNode(asNum)
|
||||
: new NumberOrStringNode(str)
|
||||
})
|
||||
),
|
||||
ws: P.regex(/\s*/),
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
GlobPattern,
|
||||
Null,
|
||||
NumberNode,
|
||||
NumberOrStringNode,
|
||||
parse,
|
||||
setPropertyClause,
|
||||
} from './'
|
||||
@@ -32,7 +33,7 @@ describe('parse', () => {
|
||||
|
||||
node = parse('32')
|
||||
expect(node.match(32)).toBe(true)
|
||||
expect(node.match('32')).toBe(false)
|
||||
expect(node.match('32')).toBe(true)
|
||||
expect(node.toString()).toBe('32')
|
||||
|
||||
node = parse('"32"')
|
||||
@@ -54,6 +55,12 @@ describe('Number', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('NumberOrStringNode', () => {
|
||||
it('match a string', () => {
|
||||
expect(new NumberOrStringNode('123').match([{ foo: '123' }])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setPropertyClause', () => {
|
||||
it('creates a node if none passed', () => {
|
||||
expect(setPropertyClause(undefined, 'foo', 'bar').toString()).toBe(
|
||||
|
||||
@@ -9,13 +9,12 @@ export default async function main (args) {
|
||||
}
|
||||
|
||||
const handler = getHandler({ url: 'file:///' })
|
||||
const stream = await createSyntheticStream(handler, path.resolve(args[0]))
|
||||
return new Promise((resolve, reject) => {
|
||||
createSyntheticStream(handler, path.resolve(args[0]))
|
||||
.on('error', reject)
|
||||
.pipe(
|
||||
createWriteStream(args[1])
|
||||
.on('error', reject)
|
||||
.on('finish', resolve)
|
||||
)
|
||||
stream.on('error', reject).pipe(
|
||||
createWriteStream(args[1])
|
||||
.on('error', reject)
|
||||
.on('finish', resolve)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -268,14 +268,18 @@ test('coalesce works in normal cases', async () => {
|
||||
|
||||
test('createSyntheticStream passes vhd-util check', async () => {
|
||||
const initalSize = 4
|
||||
const expectedVhdSize = 4197888
|
||||
await createRandomFile('randomfile', initalSize)
|
||||
await convertFromRawToVhd('randomfile', 'randomfile.vhd')
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const stream = createSyntheticStream(handler, 'randomfile.vhd')
|
||||
const stream = await createSyntheticStream(handler, 'randomfile.vhd')
|
||||
expect(stream.length).toEqual(expectedVhdSize)
|
||||
await fromEvent(
|
||||
stream.pipe(await fs.createWriteStream('recovered.vhd')),
|
||||
'finish'
|
||||
)
|
||||
await checkFile('recovered.vhd')
|
||||
const stats = await fs.stat('recovered.vhd')
|
||||
expect(stats.size).toEqual(expectedVhdSize)
|
||||
await execa('qemu-img', ['compare', 'recovered.vhd', 'randomfile'])
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ import { set as setBitmap } from './_bitmap'
|
||||
const VHD_BLOCK_SIZE_SECTORS = VHD_BLOCK_SIZE_BYTES / SECTOR_SIZE
|
||||
|
||||
/**
|
||||
* @returns {Array} an array of occupation bitmap, each bit mapping an input block size of bytes
|
||||
* @returns currentVhdPositionSector the first free sector after the data
|
||||
*/
|
||||
function createBAT (
|
||||
firstBlockPosition,
|
||||
@@ -36,9 +36,10 @@ function createBAT (
|
||||
(bitmapSize + VHD_BLOCK_SIZE_BYTES) / SECTOR_SIZE
|
||||
}
|
||||
})
|
||||
return currentVhdPositionSector
|
||||
}
|
||||
|
||||
export default asyncIteratorToStream(async function * (
|
||||
export default async function createReadableStream (
|
||||
diskSize,
|
||||
incomingBlockSize,
|
||||
blockAddressList,
|
||||
@@ -79,7 +80,14 @@ export default asyncIteratorToStream(async function * (
|
||||
const bitmapSize =
|
||||
Math.ceil(VHD_BLOCK_SIZE_SECTORS / 8 / SECTOR_SIZE) * SECTOR_SIZE
|
||||
const bat = Buffer.alloc(tablePhysicalSizeBytes, 0xff)
|
||||
createBAT(firstBlockPosition, blockAddressList, ratio, bat, bitmapSize)
|
||||
const endOfData = createBAT(
|
||||
firstBlockPosition,
|
||||
blockAddressList,
|
||||
ratio,
|
||||
bat,
|
||||
bitmapSize
|
||||
)
|
||||
const fileSize = endOfData * SECTOR_SIZE + FOOTER_SIZE
|
||||
let position = 0
|
||||
function * yieldAndTrack (buffer, expectedPosition) {
|
||||
if (expectedPosition !== undefined) {
|
||||
@@ -120,9 +128,16 @@ export default asyncIteratorToStream(async function * (
|
||||
}
|
||||
yield * yieldAndTrack(currentBlockWithBitmap)
|
||||
}
|
||||
yield * yieldAndTrack(footer, 0)
|
||||
yield * yieldAndTrack(header, FOOTER_SIZE)
|
||||
yield * yieldAndTrack(bat, FOOTER_SIZE + HEADER_SIZE)
|
||||
yield * generateFileContent(blockIterator, bitmapSize, ratio)
|
||||
yield * yieldAndTrack(footer)
|
||||
})
|
||||
|
||||
async function * iterator () {
|
||||
yield * yieldAndTrack(footer, 0)
|
||||
yield * yieldAndTrack(header, FOOTER_SIZE)
|
||||
yield * yieldAndTrack(bat, FOOTER_SIZE + HEADER_SIZE)
|
||||
yield * generateFileContent(blockIterator, bitmapSize, ratio)
|
||||
yield * yieldAndTrack(footer)
|
||||
}
|
||||
|
||||
const stream = asyncIteratorToStream(iterator())
|
||||
stream.length = fileSize
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -15,18 +15,24 @@ import { test as mapTestBit } from './_bitmap'
|
||||
const resolveRelativeFromFile = (file, path) =>
|
||||
resolve('/', dirname(file), path).slice(1)
|
||||
|
||||
export default asyncIteratorToStream(function * (handler, path) {
|
||||
export default async function createSyntheticStream (handler, path) {
|
||||
const fds = []
|
||||
|
||||
const cleanup = () => {
|
||||
for (let i = 0, n = fds.length; i < n; ++i) {
|
||||
handler.closeFile(fds[i]).catch(error => {
|
||||
console.warn('createReadStream, closeFd', i, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
try {
|
||||
const vhds = []
|
||||
while (true) {
|
||||
const fd = yield handler.openFile(path, 'r')
|
||||
const fd = await handler.openFile(path, 'r')
|
||||
fds.push(fd)
|
||||
const vhd = new Vhd(handler, fd)
|
||||
vhds.push(vhd)
|
||||
yield vhd.readHeaderAndFooter()
|
||||
yield vhd.readBlockAllocationTable()
|
||||
await vhd.readHeaderAndFooter()
|
||||
await vhd.readBlockAllocationTable()
|
||||
|
||||
if (vhd.footer.diskType === DISK_TYPE_DYNAMIC) {
|
||||
break
|
||||
@@ -64,14 +70,8 @@ export default asyncIteratorToStream(function * (handler, path) {
|
||||
const nBlocks = Math.ceil(footer.currentSize / header.blockSize)
|
||||
|
||||
const blocksOwner = new Array(nBlocks)
|
||||
for (
|
||||
let iBlock = 0,
|
||||
blockOffset = Math.ceil(
|
||||
(header.tableOffset + bat.length) / SECTOR_SIZE
|
||||
);
|
||||
iBlock < nBlocks;
|
||||
++iBlock
|
||||
) {
|
||||
let blockOffset = Math.ceil((header.tableOffset + bat.length) / SECTOR_SIZE)
|
||||
for (let iBlock = 0; iBlock < nBlocks; ++iBlock) {
|
||||
let blockSector = BLOCK_UNUSED
|
||||
for (let i = 0; i < nVhds; ++i) {
|
||||
if (vhds[i].containsBlock(iBlock)) {
|
||||
@@ -83,71 +83,78 @@ export default asyncIteratorToStream(function * (handler, path) {
|
||||
}
|
||||
bat.writeUInt32BE(blockSector, iBlock * 4)
|
||||
}
|
||||
const fileSize = blockOffset * SECTOR_SIZE + FOOTER_SIZE
|
||||
|
||||
footer = fuFooter.pack(footer)
|
||||
checksumStruct(footer, fuFooter)
|
||||
yield footer
|
||||
const iterator = function * () {
|
||||
try {
|
||||
footer = fuFooter.pack(footer)
|
||||
checksumStruct(footer, fuFooter)
|
||||
yield footer
|
||||
|
||||
header = fuHeader.pack(header)
|
||||
checksumStruct(header, fuHeader)
|
||||
yield header
|
||||
header = fuHeader.pack(header)
|
||||
checksumStruct(header, fuHeader)
|
||||
yield header
|
||||
|
||||
yield bat
|
||||
yield bat
|
||||
|
||||
// TODO: for generic usage the bitmap needs to be properly computed for each block
|
||||
const bitmap = Buffer.alloc(vhd.bitmapSize, 0xff)
|
||||
for (let iBlock = 0; iBlock < nBlocks; ++iBlock) {
|
||||
const owner = blocksOwner[iBlock]
|
||||
if (owner === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
yield bitmap
|
||||
|
||||
const blocksByVhd = new Map()
|
||||
const emitBlockSectors = function * (iVhd, i, n) {
|
||||
const vhd = vhds[iVhd]
|
||||
const isRootVhd = vhd === rootVhd
|
||||
if (!vhd.containsBlock(iBlock)) {
|
||||
if (isRootVhd) {
|
||||
yield Buffer.alloc((n - i) * SECTOR_SIZE)
|
||||
} else {
|
||||
yield * emitBlockSectors(iVhd + 1, i, n)
|
||||
// TODO: for generic usage the bitmap needs to be properly computed for each block
|
||||
const bitmap = Buffer.alloc(vhd.bitmapSize, 0xff)
|
||||
for (let iBlock = 0; iBlock < nBlocks; ++iBlock) {
|
||||
const owner = blocksOwner[iBlock]
|
||||
if (owner === undefined) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
let block = blocksByVhd.get(vhd)
|
||||
if (block === undefined) {
|
||||
block = yield vhd._readBlock(iBlock)
|
||||
blocksByVhd.set(vhd, block)
|
||||
}
|
||||
const { bitmap, data } = block
|
||||
if (isRootVhd) {
|
||||
yield data.slice(i * SECTOR_SIZE, n * SECTOR_SIZE)
|
||||
return
|
||||
}
|
||||
while (i < n) {
|
||||
const hasData = mapTestBit(bitmap, i)
|
||||
const start = i
|
||||
do {
|
||||
++i
|
||||
} while (i < n && mapTestBit(bitmap, i) === hasData)
|
||||
if (hasData) {
|
||||
yield data.slice(start * SECTOR_SIZE, i * SECTOR_SIZE)
|
||||
} else {
|
||||
yield * emitBlockSectors(iVhd + 1, start, i)
|
||||
|
||||
yield bitmap
|
||||
|
||||
const blocksByVhd = new Map()
|
||||
const emitBlockSectors = function * (iVhd, i, n) {
|
||||
const vhd = vhds[iVhd]
|
||||
const isRootVhd = vhd === rootVhd
|
||||
if (!vhd.containsBlock(iBlock)) {
|
||||
if (isRootVhd) {
|
||||
yield Buffer.alloc((n - i) * SECTOR_SIZE)
|
||||
} else {
|
||||
yield * emitBlockSectors(iVhd + 1, i, n)
|
||||
}
|
||||
return
|
||||
}
|
||||
let block = blocksByVhd.get(vhd)
|
||||
if (block === undefined) {
|
||||
block = yield vhd._readBlock(iBlock)
|
||||
blocksByVhd.set(vhd, block)
|
||||
}
|
||||
const { bitmap, data } = block
|
||||
if (isRootVhd) {
|
||||
yield data.slice(i * SECTOR_SIZE, n * SECTOR_SIZE)
|
||||
return
|
||||
}
|
||||
while (i < n) {
|
||||
const hasData = mapTestBit(bitmap, i)
|
||||
const start = i
|
||||
do {
|
||||
++i
|
||||
} while (i < n && mapTestBit(bitmap, i) === hasData)
|
||||
if (hasData) {
|
||||
yield data.slice(start * SECTOR_SIZE, i * SECTOR_SIZE)
|
||||
} else {
|
||||
yield * emitBlockSectors(iVhd + 1, start, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
yield * emitBlockSectors(owner, 0, sectorsPerBlockData)
|
||||
}
|
||||
yield footer
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
yield * emitBlockSectors(owner, 0, sectorsPerBlockData)
|
||||
}
|
||||
|
||||
yield footer
|
||||
} finally {
|
||||
for (let i = 0, n = fds.length; i < n; ++i) {
|
||||
handler.closeFile(fds[i]).catch(error => {
|
||||
console.warn('createReadStream, closeFd', i, error)
|
||||
})
|
||||
}
|
||||
const stream = asyncIteratorToStream(iterator())
|
||||
stream.length = fileSize
|
||||
return stream
|
||||
} catch (e) {
|
||||
cleanup()
|
||||
throw e
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,12 +107,13 @@ test('ReadableSparseVHDStream can handle a sparse file', async () => {
|
||||
},
|
||||
]
|
||||
const fileSize = blockSize * 110
|
||||
const stream = createReadableSparseStream(
|
||||
const stream = await createReadableSparseStream(
|
||||
fileSize,
|
||||
blockSize,
|
||||
blocks.map(b => b.offsetBytes),
|
||||
blocks
|
||||
)
|
||||
expect(stream.length).toEqual(4197888)
|
||||
const pipe = stream.pipe(createWriteStream('output.vhd'))
|
||||
await fromEvent(pipe, 'finish')
|
||||
await execa('vhd-util', ['check', '-t', '-i', '-n', 'output.vhd'])
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
forEach,
|
||||
isArray,
|
||||
isInteger,
|
||||
isObject,
|
||||
map,
|
||||
noop,
|
||||
omit,
|
||||
@@ -137,8 +136,8 @@ const parseUrl = url => {
|
||||
const {
|
||||
create: createObject,
|
||||
defineProperties,
|
||||
defineProperty,
|
||||
freeze: freezeObject,
|
||||
keys: getKeys,
|
||||
} = Object
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -190,10 +189,6 @@ const getKey = o => o.$id
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const EMPTY_ARRAY = freezeObject([])
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const getTaskResult = task => {
|
||||
const { status } = task
|
||||
if (status === 'cancelled') {
|
||||
@@ -215,6 +210,15 @@ const getTaskResult = task => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const RESERVED_FIELDS = {
|
||||
id: true,
|
||||
pool: true,
|
||||
ref: true,
|
||||
type: true,
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const CONNECTED = 'connected'
|
||||
const CONNECTING = 'connecting'
|
||||
const DISCONNECTED = 'disconnected'
|
||||
@@ -229,6 +233,7 @@ export class Xapi extends EventEmitter {
|
||||
this._auth = opts.auth
|
||||
this._pool = null
|
||||
this._readOnly = Boolean(opts.readOnly)
|
||||
this._RecordsByType = createObject(null)
|
||||
this._sessionId = null
|
||||
const url = (this._url = parseUrl(opts.url))
|
||||
|
||||
@@ -259,8 +264,8 @@ export class Xapi extends EventEmitter {
|
||||
const objects = (this._objects = new Collection())
|
||||
objects.getKey = getKey
|
||||
|
||||
this._objectsByRefs = createObject(null)
|
||||
this._objectsByRefs[NULL_REF] = undefined
|
||||
this._objectsByRef = createObject(null)
|
||||
this._objectsByRef[NULL_REF] = undefined
|
||||
|
||||
this._taskWatchers = Object.create(null)
|
||||
|
||||
@@ -462,7 +467,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
const object =
|
||||
this._objects.all[idOrUuidOrRef] || this._objectsByRefs[idOrUuidOrRef]
|
||||
this._objects.all[idOrUuidOrRef] || this._objectsByRef[idOrUuidOrRef]
|
||||
|
||||
if (object !== undefined) return object
|
||||
|
||||
@@ -474,7 +479,7 @@ export class Xapi extends EventEmitter {
|
||||
// Returns the object for a given opaque reference (internal to
|
||||
// XAPI).
|
||||
getObjectByRef (ref, defaultValue) {
|
||||
const object = this._objectsByRefs[ref]
|
||||
const object = this._objectsByRef[ref]
|
||||
|
||||
if (object !== undefined) return object
|
||||
|
||||
@@ -497,16 +502,9 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
async getRecord (type, ref) {
|
||||
const record = await this._sessionCall(`${type}.get_record`, [ref])
|
||||
|
||||
// All custom properties are read-only and non enumerable.
|
||||
defineProperties(record, {
|
||||
$id: { value: record.uuid || ref },
|
||||
$ref: { value: ref },
|
||||
$type: { value: type },
|
||||
})
|
||||
|
||||
return record
|
||||
return this._wrapRecord(
|
||||
await this._sessionCall(`${type}.get_record`, [ref])
|
||||
)
|
||||
}
|
||||
|
||||
async getRecordByUuid (type, uuid) {
|
||||
@@ -669,7 +667,7 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
setFieldEntries (record, field, entries) {
|
||||
return Promise.all(
|
||||
Object.keys(entries).map(entry => {
|
||||
getKeys(entries).map(entry => {
|
||||
const value = entries[entry]
|
||||
if (value !== undefined) {
|
||||
return value === null
|
||||
@@ -710,7 +708,7 @@ export class Xapi extends EventEmitter {
|
||||
let watcher = watchers[ref]
|
||||
if (watcher === undefined) {
|
||||
// sync check if the task is already settled
|
||||
const task = this._objectsByRefs[ref]
|
||||
const task = this._objectsByRef[ref]
|
||||
if (task !== undefined) {
|
||||
const result = getTaskResult(task)
|
||||
if (result !== undefined) {
|
||||
@@ -775,78 +773,29 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
_addObject (type, ref, object) {
|
||||
const { _objectsByRefs: objectsByRefs } = this
|
||||
|
||||
const reservedKeys = {
|
||||
id: true,
|
||||
pool: true,
|
||||
ref: true,
|
||||
type: true,
|
||||
}
|
||||
const getKey = (key, obj) =>
|
||||
reservedKeys[key] && obj === object ? `$$${key}` : `$${key}`
|
||||
|
||||
// Creates resolved properties.
|
||||
forEach(object, function resolveObject (value, key, object) {
|
||||
if (isArray(value)) {
|
||||
if (!value.length) {
|
||||
// If the array is empty, it isn't possible to be sure that
|
||||
// it is not supposed to contain links, therefore, in
|
||||
// benefice of the doubt, a resolved property is defined.
|
||||
defineProperty(object, getKey(key, object), {
|
||||
value: EMPTY_ARRAY,
|
||||
})
|
||||
|
||||
// Minor memory optimization, use the same empty array for
|
||||
// everyone.
|
||||
object[key] = EMPTY_ARRAY
|
||||
} else if (isOpaqueRef(value[0])) {
|
||||
// This is an array of refs.
|
||||
defineProperty(object, getKey(key, object), {
|
||||
get: () => freezeObject(map(value, ref => objectsByRefs[ref])),
|
||||
})
|
||||
|
||||
freezeObject(value)
|
||||
}
|
||||
} else if (isObject(value)) {
|
||||
forEach(value, resolveObject)
|
||||
|
||||
freezeObject(value)
|
||||
} else if (isOpaqueRef(value)) {
|
||||
defineProperty(object, getKey(key, object), {
|
||||
get: () => objectsByRefs[value],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// All custom properties are read-only and non enumerable.
|
||||
defineProperties(object, {
|
||||
$id: { value: object.uuid || ref },
|
||||
$pool: { get: this._getPool },
|
||||
$ref: { value: ref },
|
||||
$type: { value: type },
|
||||
})
|
||||
object = this._wrapRecord(type, ref, object)
|
||||
|
||||
// Finally freezes the object.
|
||||
freezeObject(object)
|
||||
|
||||
const objects = this._objects
|
||||
const objectsByRef = this._objectsByRef
|
||||
|
||||
// An object's UUID can change during its life.
|
||||
const prev = objectsByRefs[ref]
|
||||
const prev = objectsByRef[ref]
|
||||
let prevUuid
|
||||
if (prev && (prevUuid = prev.uuid) && prevUuid !== object.uuid) {
|
||||
objects.remove(prevUuid)
|
||||
}
|
||||
|
||||
this._objects.set(object)
|
||||
objectsByRefs[ref] = object
|
||||
objectsByRef[ref] = object
|
||||
|
||||
if (type === 'pool') {
|
||||
this._pool = object
|
||||
|
||||
const eventWatchers = this._eventWatchers
|
||||
Object.keys(object.other_config).forEach(key => {
|
||||
getKeys(object.other_config).forEach(key => {
|
||||
const eventWatcher = eventWatchers[key]
|
||||
if (eventWatcher !== undefined) {
|
||||
delete eventWatchers[key]
|
||||
@@ -871,7 +820,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
_removeObject (type, ref) {
|
||||
const byRefs = this._objectsByRefs
|
||||
const byRefs = this._objectsByRef
|
||||
const object = byRefs[ref]
|
||||
if (object !== undefined) {
|
||||
this._objects.unset(object.$id)
|
||||
@@ -1028,6 +977,90 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
return getAllObjects().then(watchEvents)
|
||||
}
|
||||
|
||||
_wrapRecord (type, ref, data) {
|
||||
const RecordsByType = this._RecordsByType
|
||||
let Record = RecordsByType[type]
|
||||
if (Record === undefined) {
|
||||
const fields = getKeys(data)
|
||||
const nFields = fields.length
|
||||
const xapi = this
|
||||
|
||||
const objectsByRef = this._objectsByRef
|
||||
const getObjectByRef = ref => objectsByRef[ref]
|
||||
|
||||
Record = function (ref, data) {
|
||||
defineProperties(this, {
|
||||
$id: { value: data.uuid || ref },
|
||||
$ref: { value: ref },
|
||||
})
|
||||
for (let i = 0; i < nFields; ++i) {
|
||||
const field = fields[i]
|
||||
this[field] = data[field]
|
||||
}
|
||||
}
|
||||
|
||||
const getters = { $pool: this._getPool }
|
||||
const props = { $type: type }
|
||||
fields.forEach(field => {
|
||||
props[`set_${field}`] = function (value) {
|
||||
return xapi.setField(this, field, value)
|
||||
}
|
||||
|
||||
const $field = (field in RESERVED_FIELDS ? '$$' : '$') + field
|
||||
|
||||
const value = data[field]
|
||||
if (isArray(value)) {
|
||||
if (value.length === 0 || isOpaqueRef(value[0])) {
|
||||
getters[$field] = function () {
|
||||
const value = this[field]
|
||||
return value.length === 0 ? value : value.map(getObjectByRef)
|
||||
}
|
||||
}
|
||||
|
||||
props[`add_to_${field}`] = function (...values) {
|
||||
return xapi
|
||||
.call(`${type}.add_${field}`, this.$ref, values)
|
||||
.then(noop)
|
||||
}
|
||||
} else if (value !== null && typeof value === 'object') {
|
||||
getters[$field] = function () {
|
||||
const value = this[field]
|
||||
const result = {}
|
||||
getKeys(value).forEach(key => {
|
||||
result[key] = objectsByRef[value[key]]
|
||||
})
|
||||
return result
|
||||
}
|
||||
props[`update_${field}`] = function (entries) {
|
||||
return xapi.setFieldEntries(this, field, entries)
|
||||
}
|
||||
} else if (isOpaqueRef(value)) {
|
||||
getters[$field] = function () {
|
||||
return objectsByRef[this[field]]
|
||||
}
|
||||
}
|
||||
})
|
||||
const descriptors = {}
|
||||
getKeys(getters).forEach(key => {
|
||||
descriptors[key] = {
|
||||
configurable: true,
|
||||
get: getters[key],
|
||||
}
|
||||
})
|
||||
getKeys(props).forEach(key => {
|
||||
descriptors[key] = {
|
||||
configurable: true,
|
||||
value: props[key],
|
||||
writable: true,
|
||||
}
|
||||
})
|
||||
defineProperties(Record.prototype, descriptors)
|
||||
|
||||
RecordsByType[type] = Record
|
||||
}
|
||||
return new Record(ref, data)
|
||||
}
|
||||
}
|
||||
|
||||
Xapi.prototype._transportCall = reduce(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-common",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Code shared between [XO](https://xen-orchestra.com) server and clients",
|
||||
"keywords": [],
|
||||
|
||||
@@ -20,7 +20,8 @@ class XoError extends BaseError {
|
||||
const create = (code, getProps) => {
|
||||
const factory = (...args) => new XoError({ ...getProps(...args), code })
|
||||
factory.is = (error, predicate) =>
|
||||
error.code === code && iteratee(predicate)(error)
|
||||
error.code === code &&
|
||||
(predicate === undefined || iteratee(predicate)(error))
|
||||
|
||||
return factory
|
||||
}
|
||||
@@ -33,7 +34,7 @@ export const notImplemented = create(0, () => ({
|
||||
|
||||
export const noSuchObject = create(1, (id, type) => ({
|
||||
data: { id, type },
|
||||
message: 'no such object',
|
||||
message: `no such ${type || 'object'} ${id}`,
|
||||
}))
|
||||
|
||||
export const unauthorized = create(2, () => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-backup-reports",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Backup reports plugin for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -50,6 +50,7 @@ const ICON_FAILURE = '🚨'
|
||||
const ICON_INTERRUPTED = '⚠️'
|
||||
const ICON_SKIPPED = '⏩'
|
||||
const ICON_SUCCESS = '✔'
|
||||
const ICON_WARNING = '⚠️'
|
||||
|
||||
const STATUS_ICON = {
|
||||
failure: ICON_FAILURE,
|
||||
@@ -99,12 +100,13 @@ const isSkippedError = error =>
|
||||
error.message === UNHEALTHY_VDI_CHAIN_ERROR ||
|
||||
error.message === NO_SUCH_OBJECT_ERROR
|
||||
|
||||
const INDENT = ' '
|
||||
const createGetTemporalDataMarkdown = formatDate => (
|
||||
start,
|
||||
end,
|
||||
nbIndent = 0
|
||||
) => {
|
||||
const indent = ' '.repeat(nbIndent)
|
||||
const indent = INDENT.repeat(nbIndent)
|
||||
|
||||
const markdown = [`${indent}- **Start time**: ${formatDate(start)}`]
|
||||
if (end !== undefined) {
|
||||
@@ -117,6 +119,17 @@ const createGetTemporalDataMarkdown = formatDate => (
|
||||
return markdown
|
||||
}
|
||||
|
||||
const addWarnings = (text, warnings, nbIndent = 0) => {
|
||||
if (warnings === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const indent = INDENT.repeat(nbIndent)
|
||||
warnings.forEach(({ message }) => {
|
||||
text.push(`${indent}- **${ICON_WARNING} ${message}**`)
|
||||
})
|
||||
}
|
||||
|
||||
class BackupReportsXoPlugin {
|
||||
constructor (xo) {
|
||||
this._xo = xo
|
||||
@@ -180,13 +193,14 @@ class BackupReportsXoPlugin {
|
||||
let markdown = [
|
||||
`## Global status: ${log.status}`,
|
||||
'',
|
||||
`- **Job ID**: ${log.jobId}`,
|
||||
`- **Run ID**: ${runJobId}`,
|
||||
`- **mode**: ${mode}`,
|
||||
...getTemporalDataMarkdown(log.start, log.end),
|
||||
`- **Error**: ${log.result.message}`,
|
||||
'---',
|
||||
'',
|
||||
`*${pkg.name} v${pkg.version}*`,
|
||||
]
|
||||
addWarnings(markdown, log.warnings)
|
||||
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
|
||||
|
||||
markdown = markdown.join('\n')
|
||||
return this._sendReport({
|
||||
@@ -228,6 +242,7 @@ class BackupReportsXoPlugin {
|
||||
`- **UUID**: ${vm !== undefined ? vm.uuid : vmId}`,
|
||||
...getTemporalDataMarkdown(taskLog.start, taskLog.end),
|
||||
]
|
||||
addWarnings(text, taskLog.warnings)
|
||||
|
||||
const failedSubTasks = []
|
||||
const snapshotText = []
|
||||
@@ -262,6 +277,7 @@ class BackupReportsXoPlugin {
|
||||
}** (${id}) ${icon}`,
|
||||
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 2)
|
||||
)
|
||||
addWarnings(remotesText, subTaskLog.warnings, 2)
|
||||
if (subTaskLog.status === 'failure') {
|
||||
failedSubTasks.push(remote !== undefined ? remote.name : id)
|
||||
remotesText.push('', errorMessage)
|
||||
@@ -278,6 +294,7 @@ class BackupReportsXoPlugin {
|
||||
` - **${srName}** (${srUuid}) ${icon}`,
|
||||
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 2)
|
||||
)
|
||||
addWarnings(srsText, subTaskLog.warnings, 2)
|
||||
if (subTaskLog.status === 'failure') {
|
||||
failedSubTasks.push(sr !== undefined ? sr.name_label : id)
|
||||
srsText.push('', errorMessage)
|
||||
@@ -293,6 +310,7 @@ class BackupReportsXoPlugin {
|
||||
}
|
||||
|
||||
const operationInfoText = []
|
||||
addWarnings(operationInfoText, operationLog.warnings, 3)
|
||||
if (operationLog.status === 'success') {
|
||||
const size = operationLog.result.size
|
||||
if (operationLog.message === 'merge') {
|
||||
@@ -395,6 +413,8 @@ class BackupReportsXoPlugin {
|
||||
let markdown = [
|
||||
`## Global status: ${log.status}`,
|
||||
'',
|
||||
`- **Job ID**: ${log.jobId}`,
|
||||
`- **Run ID**: ${runJobId}`,
|
||||
`- **mode**: ${mode}`,
|
||||
...getTemporalDataMarkdown(log.start, log.end),
|
||||
`- **Successes**: ${nSuccesses} / ${nVms}`,
|
||||
@@ -406,6 +426,7 @@ class BackupReportsXoPlugin {
|
||||
if (globalMergeSize !== 0) {
|
||||
markdown.push(`- **Merge size**: ${formatSize(globalMergeSize)}`)
|
||||
}
|
||||
addWarnings(markdown, log.warnings)
|
||||
markdown.push('')
|
||||
|
||||
if (nFailures !== 0) {
|
||||
|
||||
@@ -34,9 +34,11 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"handlebars": "^4.0.6",
|
||||
"html-minifier": "^3.5.8",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.10.1"
|
||||
},
|
||||
|
||||
@@ -83,10 +83,6 @@
|
||||
border-top: 1px solid #95a5a6;
|
||||
}
|
||||
|
||||
.page .global tr:nth-last-child(2) td {
|
||||
border-bottom: 1px solid #95a5a6;
|
||||
}
|
||||
|
||||
.top table{
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
@@ -149,9 +145,9 @@
|
||||
|
||||
<div class="page">
|
||||
|
||||
<table class ="global">
|
||||
<table class="global">
|
||||
<tr>
|
||||
<td id="title" rowspan="13">VMs</td>
|
||||
<td id="title" rowspan="8">VMs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Number:</td>
|
||||
@@ -160,37 +156,37 @@
|
||||
<tr>
|
||||
<td>CPU:</td>
|
||||
<td>{{normaliseValue global.vms.cpu}} % {{normaliseEvolution global.vmsEvolution.cpu}}</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RAM:</td>
|
||||
<td>{{normaliseValue global.vms.ram}} GiB {{normaliseEvolution global.vmsEvolution.ram}}</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Disk read:</td>
|
||||
<td>{{normaliseValue global.vms.diskRead}} MiB {{normaliseEvolution global.vmsEvolution.diskRead}}
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Disk write:</td>
|
||||
<td>{{normaliseValue global.vms.diskWrite}} MiB {{normaliseEvolution global.vmsEvolution.diskWrite}}
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Network RX:</td>
|
||||
<td>{{normaliseValue global.vms.netReception}} KiB {{normaliseEvolution global.vmsEvolution.netReception}}
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Network TX:</td>
|
||||
<td>{{normaliseValue global.vms.netTransmission}} KiB {{normaliseEvolution global.vmsEvolution.netTransmission}}
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="top">
|
||||
|
||||
<table>
|
||||
<caption>3rd top usages</caption>
|
||||
<caption>Top VMs</caption>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>UUID</th>
|
||||
@@ -239,6 +235,9 @@
|
||||
<td>{{normaliseValue this.value}} MiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
{{getTopIops topVms}}
|
||||
|
||||
<tr>
|
||||
<td rowspan='{{math topVms.netReception.length "+" 1}}' class="tableHeader">Network RX</td>
|
||||
</tr>
|
||||
@@ -264,9 +263,9 @@
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
<table class ="global">
|
||||
<table class="global">
|
||||
<tr>
|
||||
<td id="title" rowspan="13">Hosts</td>
|
||||
<td id="title" rowspan="7">Hosts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Number:</td>
|
||||
@@ -277,34 +276,33 @@
|
||||
<td>CPU:</td>
|
||||
<td>{{normaliseValue global.hosts.cpu}} % {{normaliseEvolution global.hostsEvolution.cpu}}
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RAM:</td>
|
||||
<td>{{normaliseValue global.hosts.ram}} GiB {{normaliseEvolution global.hostsEvolution.ram}}
|
||||
</td>
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Load average:</td>
|
||||
<td>{{normaliseValue global.hosts.load}} {{normaliseEvolution global.hostsEvolution.load}}
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Network RX:</td>
|
||||
<td>{{normaliseValue global.hosts.netReception}} KiB {{normaliseEvolution global.hostsEvolution.netReception}}
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Network TX:</td>
|
||||
<td>{{normaliseValue global.hosts.netTransmission}} KiB {{normaliseEvolution global.hostsEvolution.netTransmission}}
|
||||
</td>
|
||||
<tr>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="top">
|
||||
|
||||
<table>
|
||||
<caption>3rd top usages</caption>
|
||||
<caption>Top hosts</caption>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>UUID</th>
|
||||
@@ -368,19 +366,14 @@
|
||||
<div class="page">
|
||||
<div class="top">
|
||||
<table>
|
||||
<caption>Most used storages </caption>
|
||||
<caption>Top SRs</caption>
|
||||
<tr>
|
||||
<th />
|
||||
<th>UUID</th>
|
||||
<th>Name</th>
|
||||
<th>value</th>
|
||||
</tr>
|
||||
{{#each topSrs}}
|
||||
<tr>
|
||||
<td>{{shortUUID this.uuid}}</td>
|
||||
<td>{{this.name}}</td>
|
||||
<td>{{normaliseValue this.value}} GiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{getTopSrs topSrs}}
|
||||
</table>
|
||||
<table>
|
||||
<caption>Hosts missing patches</caption>
|
||||
@@ -531,6 +524,9 @@
|
||||
<th>RAM (GiB)</th>
|
||||
<th>Disk read (MiB)</th>
|
||||
<th>Disk write (MiB)</th>
|
||||
<th>IOPS read</th>
|
||||
<th>IOPS write</th>
|
||||
<th>IOPS total</th>
|
||||
<th>Network RX (KiB)</th>
|
||||
<th>Network TX (KiB)</th>
|
||||
</tr>
|
||||
@@ -542,6 +538,9 @@
|
||||
<td>{{normaliseValue this.ram}} {{normaliseEvolution this.evolution.ram}}</td>
|
||||
<td>{{normaliseValue this.diskRead}} {{normaliseEvolution this.evolution.diskRead}}</td>
|
||||
<td>{{normaliseValue this.diskWrite}} {{normaliseEvolution this.evolution.diskWrite}}</td>
|
||||
<td>{{formatIops this.iopsRead}} {{normaliseEvolution this.evolution.iopsRead}}</td>
|
||||
<td>{{formatIops this.iopsWrite}} {{normaliseEvolution this.evolution.iopsWrite}}</td>
|
||||
<td>{{formatIops this.iopsTotal}} {{normaliseEvolution this.evolution.iopsTotal}}</td>
|
||||
<td>{{normaliseValue this.netReception}} {{normaliseEvolution this.evolution.netReception}}</td>
|
||||
<td>{{normaliseValue this.netTransmission}} {{normaliseEvolution this.evolution.netTransmission}}</td>
|
||||
</tr>
|
||||
@@ -584,8 +583,8 @@
|
||||
<td>{{shortUUID this.uuid}}</td>
|
||||
<td>{{this.name}}</td>
|
||||
<td>{{normaliseValue this.total}} {{normaliseEvolution this.evolution.total}}</td>
|
||||
<td>{{normaliseValue this.used}}</td>
|
||||
<td>{{normaliseValue this.free}}</td>
|
||||
<td>{{normaliseValue this.usedSpace}}</td>
|
||||
<td>{{normaliseValue this.freeSpace}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import Handlebars from 'handlebars'
|
||||
import humanFormat from 'human-format'
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
import { minify } from 'html-minifier'
|
||||
import {
|
||||
@@ -21,6 +23,8 @@ import { readFile, writeFile } from 'fs'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const GRANULARITY = 'days'
|
||||
|
||||
const pReadFile = promisify(readFile)
|
||||
const pWriteFile = promisify(writeFile)
|
||||
|
||||
@@ -75,7 +79,7 @@ export const configurationSchema = {
|
||||
},
|
||||
periodicity: {
|
||||
type: 'string',
|
||||
enum: ['monthly', 'weekly'],
|
||||
enum: ['monthly', 'weekly', 'daily'],
|
||||
description:
|
||||
'If you choose weekly you will receive the report every sunday and if you choose monthly you will receive it every first day of the month.',
|
||||
},
|
||||
@@ -87,6 +91,24 @@ export const configurationSchema = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const shortUuid = uuid => {
|
||||
if (typeof uuid === 'string') {
|
||||
return uuid.split('-')[0]
|
||||
}
|
||||
}
|
||||
|
||||
const formatIops = value =>
|
||||
isFinite(value)
|
||||
? humanFormat(value, {
|
||||
unit: 'IOPS',
|
||||
decimals: 2,
|
||||
})
|
||||
: '-'
|
||||
|
||||
const normaliseValue = value => (isFinite(value) ? round(value, 2) : '-')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
Handlebars.registerHelper('compare', function (
|
||||
lvalue,
|
||||
operator,
|
||||
@@ -122,29 +144,62 @@ Handlebars.registerHelper('math', function (lvalue, operator, rvalue, options) {
|
||||
return mathOperators[operator](+lvalue, +rvalue)
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('shortUUID', uuid => {
|
||||
if (typeof uuid === 'string') {
|
||||
return uuid.split('-')[0]
|
||||
}
|
||||
})
|
||||
Handlebars.registerHelper('shortUUID', shortUuid)
|
||||
|
||||
Handlebars.registerHelper(
|
||||
'normaliseValue',
|
||||
value => (isFinite(value) ? round(value, 2) : '-')
|
||||
)
|
||||
Handlebars.registerHelper('normaliseValue', normaliseValue)
|
||||
|
||||
Handlebars.registerHelper(
|
||||
'normaliseEvolution',
|
||||
value =>
|
||||
new Handlebars.SafeString(
|
||||
isFinite(+value) && +value !== 0
|
||||
? (value = round(value, 2)) > 0
|
||||
isFinite((value = round(value, 2))) && value !== 0
|
||||
? value > 0
|
||||
? `(<b style="color: green;">▲ ${value}%</b>)`
|
||||
: `(<b style="color: red;">▼ ${String(value).slice(1)}%</b>)`
|
||||
: ''
|
||||
)
|
||||
)
|
||||
|
||||
Handlebars.registerHelper('formatIops', formatIops)
|
||||
|
||||
const getHeader = (label, size) => `
|
||||
<tr>
|
||||
<td rowspan='${size + 1}' class="tableHeader">${label}</td>
|
||||
</tr>
|
||||
`
|
||||
|
||||
const getBody = ({ uuid, name, value }, transformValue, unit) => `
|
||||
<tr>
|
||||
<td>${shortUuid(uuid)}</td>
|
||||
<td>${name}</td>
|
||||
<td>${transformValue(value)}${unit !== undefined ? ` ${unit}` : ''}</td>
|
||||
</tr>
|
||||
`
|
||||
|
||||
const getTopIops = ({ iopsRead, iopsWrite, iopsTotal }) => `
|
||||
${getHeader('IOPS read', iopsRead.length)}
|
||||
${iopsRead.map(obj => getBody(obj, formatIops)).join('')}
|
||||
${getHeader('IOPS write', iopsWrite.length)}
|
||||
${iopsWrite.map(obj => getBody(obj, formatIops)).join('')}
|
||||
${getHeader('IOPS total', iopsTotal.length)}
|
||||
${iopsTotal.map(obj => getBody(obj, formatIops)).join('')}
|
||||
`
|
||||
|
||||
Handlebars.registerHelper(
|
||||
'getTopSrs',
|
||||
({ usedSpace, iopsRead, iopsWrite, iopsTotal }) =>
|
||||
new Handlebars.SafeString(`
|
||||
${getHeader('Used space', usedSpace.length)}
|
||||
${usedSpace.map(obj => getBody(obj, normaliseValue, 'GiB')).join('')}
|
||||
${getTopIops({ iopsRead, iopsWrite, iopsTotal })}
|
||||
`)
|
||||
)
|
||||
|
||||
Handlebars.registerHelper(
|
||||
'getTopIops',
|
||||
props => new Handlebars.SafeString(getTopIops(props))
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function computeMean (values) {
|
||||
@@ -217,26 +272,36 @@ function getMemoryUsedMetric ({ memory, memoryFree = memory }) {
|
||||
return map(memory, (value, key) => value - memoryFree[key])
|
||||
}
|
||||
|
||||
const METRICS_MEAN = {
|
||||
cpu: computeDoubleMean,
|
||||
disk: value => computeDoubleMean(values(value)) / mibPower,
|
||||
iops: value => computeDoubleMean(values(value)),
|
||||
load: computeMean,
|
||||
net: value => computeDoubleMean(value) / kibPower,
|
||||
ram: stats => computeMean(getMemoryUsedMetric(stats)) / gibPower,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
async function getVmsStats ({ runningVms, xo }) {
|
||||
return orderBy(
|
||||
await Promise.all(
|
||||
map(runningVms, async vm => {
|
||||
const vmStats = await xo.getXapiVmStats(vm, 'days')
|
||||
const { stats } = await xo.getXapiVmStats(vm, GRANULARITY)
|
||||
const iopsRead = METRICS_MEAN.iops(get(stats.iops, 'r'))
|
||||
const iopsWrite = METRICS_MEAN.iops(get(stats.iops, 'w'))
|
||||
return {
|
||||
uuid: vm.uuid,
|
||||
name: vm.name_label,
|
||||
cpu: computeDoubleMean(vmStats.stats.cpus),
|
||||
ram: computeMean(getMemoryUsedMetric(vmStats.stats)) / gibPower,
|
||||
diskRead:
|
||||
computeDoubleMean(values(get(vmStats.stats.xvds, 'r'))) / mibPower,
|
||||
diskWrite:
|
||||
computeDoubleMean(values(get(vmStats.stats.xvds, 'w'))) / mibPower,
|
||||
netReception:
|
||||
computeDoubleMean(get(vmStats.stats.vifs, 'rx')) / kibPower,
|
||||
netTransmission:
|
||||
computeDoubleMean(get(vmStats.stats.vifs, 'tx')) / kibPower,
|
||||
cpu: METRICS_MEAN.cpu(stats.cpus),
|
||||
ram: METRICS_MEAN.ram(stats),
|
||||
diskRead: METRICS_MEAN.disk(get(stats.xvds, 'r')),
|
||||
diskWrite: METRICS_MEAN.disk(get(stats.xvds, 'w')),
|
||||
iopsRead,
|
||||
iopsWrite,
|
||||
iopsTotal: iopsRead + iopsWrite,
|
||||
netReception: METRICS_MEAN.net(get(stats.vifs, 'rx')),
|
||||
netTransmission: METRICS_MEAN.net(get(stats.vifs, 'tx')),
|
||||
}
|
||||
})
|
||||
),
|
||||
@@ -249,17 +314,15 @@ async function getHostsStats ({ runningHosts, xo }) {
|
||||
return orderBy(
|
||||
await Promise.all(
|
||||
map(runningHosts, async host => {
|
||||
const hostStats = await xo.getXapiHostStats(host, 'days')
|
||||
const { stats } = await xo.getXapiHostStats(host, GRANULARITY)
|
||||
return {
|
||||
uuid: host.uuid,
|
||||
name: host.name_label,
|
||||
cpu: computeDoubleMean(hostStats.stats.cpus),
|
||||
ram: computeMean(getMemoryUsedMetric(hostStats.stats)) / gibPower,
|
||||
load: computeMean(hostStats.stats.load),
|
||||
netReception:
|
||||
computeDoubleMean(get(hostStats.stats.pifs, 'rx')) / kibPower,
|
||||
netTransmission:
|
||||
computeDoubleMean(get(hostStats.stats.pifs, 'tx')) / kibPower,
|
||||
cpu: METRICS_MEAN.cpu(stats.cpus),
|
||||
ram: METRICS_MEAN.ram(stats),
|
||||
load: METRICS_MEAN.load(stats.load),
|
||||
netReception: METRICS_MEAN.net(get(stats.pifs, 'rx')),
|
||||
netTransmission: METRICS_MEAN.net(get(stats.pifs, 'tx')),
|
||||
}
|
||||
})
|
||||
),
|
||||
@@ -268,24 +331,43 @@ async function getHostsStats ({ runningHosts, xo }) {
|
||||
)
|
||||
}
|
||||
|
||||
function getSrsStats (xoObjects) {
|
||||
async function getSrsStats ({ xo, xoObjects }) {
|
||||
return orderBy(
|
||||
map(filter(xoObjects, obj => obj.type === 'SR' && obj.size > 0), sr => {
|
||||
const total = sr.size / gibPower
|
||||
const used = sr.physical_usage / gibPower
|
||||
let name = sr.name_label
|
||||
if (!sr.shared) {
|
||||
name += ` (${find(xoObjects, { id: sr.$container }).name_label})`
|
||||
await asyncMap(
|
||||
filter(
|
||||
xoObjects,
|
||||
obj => obj.type === 'SR' && obj.size > 0 && obj.$PBDs.length > 0
|
||||
),
|
||||
async sr => {
|
||||
const totalSpace = sr.size / gibPower
|
||||
const usedSpace = sr.physical_usage / gibPower
|
||||
let name = sr.name_label
|
||||
// [Bug in XO] a SR with not container can be found (SR attached to a PBD with no host attached)
|
||||
let container
|
||||
if (
|
||||
!sr.shared &&
|
||||
(container = find(xoObjects, { id: sr.$container })) !== undefined
|
||||
) {
|
||||
name += ` (${container.name_label})`
|
||||
}
|
||||
|
||||
const { stats } = await xo.getXapiSrStats(sr.id, GRANULARITY)
|
||||
const iopsRead = computeMean(get(stats.iops, 'r'))
|
||||
const iopsWrite = computeMean(get(stats.iops, 'w'))
|
||||
|
||||
return {
|
||||
uuid: sr.uuid,
|
||||
name,
|
||||
total: totalSpace,
|
||||
usedSpace,
|
||||
freeSpace: totalSpace - usedSpace,
|
||||
iopsRead,
|
||||
iopsWrite,
|
||||
iopsTotal: iopsRead + iopsWrite,
|
||||
}
|
||||
}
|
||||
return {
|
||||
uuid: sr.uuid,
|
||||
name,
|
||||
total,
|
||||
used,
|
||||
free: total - used,
|
||||
}
|
||||
}),
|
||||
'total',
|
||||
),
|
||||
'name',
|
||||
'desc'
|
||||
)
|
||||
}
|
||||
@@ -351,6 +433,9 @@ function getTopVms ({ vmsStats, xo }) {
|
||||
'ram',
|
||||
'diskRead',
|
||||
'diskWrite',
|
||||
'iopsRead',
|
||||
'iopsWrite',
|
||||
'iopsTotal',
|
||||
'netReception',
|
||||
'netTransmission',
|
||||
])
|
||||
@@ -366,8 +451,8 @@ function getTopHosts ({ hostsStats, xo }) {
|
||||
])
|
||||
}
|
||||
|
||||
function getTopSrs ({ srsStats, xo }) {
|
||||
return getTop(srsStats, ['used']).used
|
||||
function getTopSrs (srsStats) {
|
||||
return getTop(srsStats, ['usedSpace', 'iopsRead', 'iopsWrite', 'iopsTotal'])
|
||||
}
|
||||
|
||||
async function getHostsMissingPatches ({ runningHosts, xo }) {
|
||||
@@ -376,6 +461,13 @@ async function getHostsMissingPatches ({ runningHosts, xo }) {
|
||||
let hostsPatches = await xo
|
||||
.getXapi(host)
|
||||
.listMissingPoolPatchesOnHost(host._xapiId)
|
||||
.catch(error => {
|
||||
console.error(
|
||||
'[WARN] error on fetching hosts missing patches:',
|
||||
JSON.stringify(error)
|
||||
)
|
||||
return []
|
||||
})
|
||||
|
||||
if (host.license_params.sku_type === 'free') {
|
||||
hostsPatches = filter(hostsPatches, { paid: false })
|
||||
@@ -417,6 +509,9 @@ async function computeEvolution ({ storedStatsPath, ...newStats }) {
|
||||
'ram',
|
||||
'diskRead',
|
||||
'diskWrite',
|
||||
'iopsRead',
|
||||
'iopsWrite',
|
||||
'iopsTotal',
|
||||
'netReception',
|
||||
'netTransmission',
|
||||
],
|
||||
@@ -506,7 +601,7 @@ async function dataBuilder ({ xo, storedStatsPath, all }) {
|
||||
xo.getAllUsers(),
|
||||
getVmsStats({ xo, runningVms }),
|
||||
getHostsStats({ xo, runningHosts }),
|
||||
getSrsStats(xoObjects),
|
||||
getSrsStats({ xo, xoObjects }),
|
||||
getHostsMissingPatches({ xo, runningHosts }),
|
||||
])
|
||||
|
||||
@@ -522,7 +617,7 @@ async function dataBuilder ({ xo, storedStatsPath, all }) {
|
||||
computeGlobalHostsStats({ xo, hostsStats, haltedHosts }),
|
||||
getTopVms({ xo, vmsStats }),
|
||||
getTopHosts({ xo, hostsStats }),
|
||||
getTopSrs({ xo, srsStats }),
|
||||
getTopSrs(srsStats),
|
||||
getAllUsersEmail(users),
|
||||
])
|
||||
|
||||
@@ -571,6 +666,12 @@ async function dataBuilder ({ xo, storedStatsPath, all }) {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const CRON_BY_PERIODICITY = {
|
||||
monthly: '0 6 1 * *',
|
||||
weekly: '0 6 * * 0',
|
||||
daily: '0 6 * * *',
|
||||
}
|
||||
|
||||
class UsageReportPlugin {
|
||||
constructor ({ xo, getDataDir }) {
|
||||
this._xo = xo
|
||||
@@ -591,7 +692,7 @@ class UsageReportPlugin {
|
||||
}
|
||||
|
||||
this._job = createSchedule(
|
||||
configuration.periodicity === 'monthly' ? '00 06 1 * *' : '00 06 * * 0'
|
||||
CRON_BY_PERIODICITY[configuration.periodicity]
|
||||
).createJob(async () => {
|
||||
try {
|
||||
await this._sendReport(true)
|
||||
|
||||
@@ -11,21 +11,8 @@ require('../better-stacks')
|
||||
// less memory usage.
|
||||
global.Promise = require('bluebird')
|
||||
|
||||
// Make unhandled rejected promises visible.
|
||||
process.on('unhandledRejection', function (reason) {
|
||||
console.warn('[Warn] Possibly unhandled rejection:', reason && reason.stack || reason)
|
||||
})
|
||||
|
||||
;(function (EE) {
|
||||
var proto = EE.prototype
|
||||
var emit = proto.emit
|
||||
proto.emit = function patchedError (event, error) {
|
||||
if (event === 'error' && !this.listenerCount(event)) {
|
||||
return console.warn('[Warn] Unhandled error event:', error && error.stack || error)
|
||||
}
|
||||
|
||||
return emit.apply(this, arguments)
|
||||
}
|
||||
})(require('events').EventEmitter)
|
||||
require('@xen-orchestra/log/configure').catchGlobalErrors(
|
||||
require('@xen-orchestra/log').default('xo:xo-server')
|
||||
)
|
||||
|
||||
require('exec-promise')(require('../'))
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/emit-async": "^0.0.0",
|
||||
"@xen-orchestra/fs": "^0.3.1",
|
||||
"@xen-orchestra/log": "^0.1.0",
|
||||
"@xen-orchestra/mixin": "^0.0.0",
|
||||
"ajv": "^6.1.1",
|
||||
"app-conf": "^0.5.0",
|
||||
@@ -118,7 +119,7 @@
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.3.0",
|
||||
"xo-collection": "^0.4.1",
|
||||
"xo-common": "^0.1.1",
|
||||
"xo-common": "^0.1.2",
|
||||
"xo-remote-parser": "^0.5.0",
|
||||
"xo-vmdk-to-vhd": "^0.1.5",
|
||||
"yazl": "^2.4.3"
|
||||
|
||||
@@ -46,14 +46,12 @@
|
||||
|
||||
# Configuration of the embedded HTTP server.
|
||||
http:
|
||||
|
||||
# Hosts & ports on which to listen.
|
||||
#
|
||||
# By default, the server listens on [::]:80.
|
||||
listen:
|
||||
# Basic HTTP.
|
||||
-
|
||||
# Address on which the server is listening on.
|
||||
- # Address on which the server is listening on.
|
||||
#
|
||||
# Sets it to 'localhost' for IP to listen only on the local host.
|
||||
#
|
||||
@@ -124,23 +122,20 @@ http:
|
||||
|
||||
# Connection to the Redis server.
|
||||
redis:
|
||||
# Unix sockets can be used
|
||||
#
|
||||
# Default: undefined
|
||||
#socket: /var/run/redis/redis.sock
|
||||
|
||||
# Syntax: redis://[db[:password]@]hostname[:port][/db-number]
|
||||
#
|
||||
# Default: redis://localhost:6379/0
|
||||
#uri: redis://redis.company.lan/42
|
||||
|
||||
# List of aliased commands.
|
||||
#
|
||||
# See http://redis.io/topics/security#disabling-of-specific-commands
|
||||
#renameCommands:
|
||||
# del: '3dda29ad-3015-44f9-b13b-fa570de92489'
|
||||
# srem: '3fd758c9-5610-4e9d-a058-dbf4cb6d8bf0'
|
||||
|
||||
# Unix sockets can be used
|
||||
#
|
||||
# Default: undefined
|
||||
#socket: /var/run/redis/redis.sock
|
||||
# Syntax: redis://[db[:password]@]hostname[:port][/db-number]
|
||||
#
|
||||
# Default: redis://localhost:6379/0
|
||||
#uri: redis://redis.company.lan/42
|
||||
# List of aliased commands.
|
||||
#
|
||||
# See http://redis.io/topics/security#disabling-of-specific-commands
|
||||
#renameCommands:
|
||||
# del: '3dda29ad-3015-44f9-b13b-fa570de92489'
|
||||
# srem: '3fd758c9-5610-4e9d-a058-dbf4cb6d8bf0'
|
||||
|
||||
# Directory containing the database of XO.
|
||||
# Currently used for logs.
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import archiver from 'archiver'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { basename } from 'path'
|
||||
import { format } from 'json-rpc-peer'
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
const log = createLogger('xo:backup')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function list ({ remote }) {
|
||||
@@ -62,7 +65,7 @@ function handleFetchFiles (
|
||||
|
||||
const archive = archiver(archiveFormat)
|
||||
archive.on('error', error => {
|
||||
console.error(error)
|
||||
log.error(error)
|
||||
res.end(format.error(0, error))
|
||||
})
|
||||
|
||||
@@ -74,7 +77,7 @@ function handleFetchFiles (
|
||||
archive.pipe(res)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
log.error(error)
|
||||
res.writeHead(500)
|
||||
res.end(format.error(0, error))
|
||||
})
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import pump from 'pump'
|
||||
import { format } from 'json-rpc-peer'
|
||||
import { unauthorized } from 'xo-common/api-errors'
|
||||
import { noSuchObject, unauthorized } from 'xo-common/api-errors'
|
||||
|
||||
import { parseSize } from '../utils'
|
||||
|
||||
const log = createLogger('xo:disk')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function create ({ name, size, sr, vm, bootable, position, mode }) {
|
||||
const attach = vm !== undefined
|
||||
|
||||
let resourceSet
|
||||
if (attach && (resourceSet = vm.resourceSet) != null) {
|
||||
await this.checkResourceSetConstraints(resourceSet, this.user.id, [sr.id])
|
||||
await this.allocateLimitsInResourceSet({ disk: size }, resourceSet)
|
||||
} else if (
|
||||
!(await this.hasPermissions(this.user.id, [[sr.id, 'administrate']]))
|
||||
) {
|
||||
throw unauthorized()
|
||||
}
|
||||
do {
|
||||
let resourceSet
|
||||
if (attach && (resourceSet = vm.resourceSet) != null) {
|
||||
try {
|
||||
await this.checkResourceSetConstraints(resourceSet, this.user.id, [
|
||||
sr.id,
|
||||
])
|
||||
await this.allocateLimitsInResourceSet({ disk: size }, resourceSet)
|
||||
|
||||
break
|
||||
} catch (error) {
|
||||
if (!noSuchObject.is(error, { data: { id: resourceSet } })) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// the resource set does not exist, falls back to normal check
|
||||
}
|
||||
|
||||
if (!(await this.hasPermissions(this.user.id, [[sr.id, 'administrate']]))) {
|
||||
throw unauthorized()
|
||||
}
|
||||
} while (false)
|
||||
|
||||
const xapi = this.getXapi(sr)
|
||||
const vdi = await xapi.createVdi({
|
||||
@@ -72,7 +89,7 @@ async function handleExportContent (req, res, { xapi, id }) {
|
||||
)
|
||||
pump(stream, res, error => {
|
||||
if (error != null) {
|
||||
console.warn('disk.exportContent', error)
|
||||
log.warn('disk.exportContent', { error })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,10 +68,12 @@ export async function create (params) {
|
||||
const xapi = this.getXapi(template)
|
||||
|
||||
const objectIds = [template.id]
|
||||
const cpus = extract(params, 'CPUs')
|
||||
const memoryMax = extract(params, 'memoryMax')
|
||||
const limits = {
|
||||
cpus: template.CPUs.number,
|
||||
cpus: cpus !== undefined ? cpus : template.CPUs.number,
|
||||
disk: 0,
|
||||
memory: template.memory.dynamic[1],
|
||||
memory: memoryMax !== undefined ? memoryMax : template.memory.dynamic[1],
|
||||
vms: 1,
|
||||
}
|
||||
const vdiSizesByDevice = {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from 'debug'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import defer from 'golike-defer'
|
||||
import execa from 'execa'
|
||||
import fs from 'fs-extra'
|
||||
@@ -12,7 +12,7 @@ import { includes, remove, filter, find, range } from 'lodash'
|
||||
import { asInteger } from '../xapi/utils'
|
||||
import { parseXml, ensureArray } from '../utils'
|
||||
|
||||
const debug = createLogger('xo:xosan')
|
||||
const log = createLogger('xo:xosan')
|
||||
|
||||
const SSH_KEY_FILE = 'id_rsa_xosan'
|
||||
const DEFAULT_NETWORK_PREFIX = '172.31.100.'
|
||||
@@ -73,7 +73,7 @@ async function rateLimitedRetry (action, shouldRetry, retryCount = 20) {
|
||||
let result
|
||||
while (retryCount > 0 && (result = await action()) && shouldRetry(result)) {
|
||||
retryDelay *= 1.1
|
||||
debug('waiting ' + retryDelay + 'ms and retrying')
|
||||
log.debug(`waiting ${retryDelay} ms and retrying`)
|
||||
await delay(retryDelay)
|
||||
retryCount--
|
||||
}
|
||||
@@ -305,7 +305,7 @@ async function copyVm (xapi, originalVm, sr) {
|
||||
}
|
||||
|
||||
async function callPlugin (xapi, host, command, params) {
|
||||
debug('calling plugin', host.address, command)
|
||||
log.debug(`calling plugin ${host.address} ${command}`)
|
||||
return JSON.parse(
|
||||
await xapi.call('host.call_plugin', host.$ref, 'xosan.py', command, params)
|
||||
)
|
||||
@@ -346,15 +346,12 @@ async function remoteSsh (glusterEndpoint, cmd, ignoreError = false) {
|
||||
}
|
||||
}
|
||||
}
|
||||
debug(
|
||||
result.command.join(' '),
|
||||
'\n =>exit:',
|
||||
result.exit,
|
||||
'\n =>err :',
|
||||
result.stderr,
|
||||
'\n =>out (1000 chars) :',
|
||||
result.stdout.substring(0, 1000)
|
||||
)
|
||||
|
||||
log.debug(`result of ${result.command.join(' ')}`, {
|
||||
exit: result.exit,
|
||||
err: result.stderr,
|
||||
out: result.stdout.substring(0, 1000),
|
||||
})
|
||||
// 255 seems to be ssh's own error codes.
|
||||
if (result.exit !== 255) {
|
||||
if (!ignoreError && result.exit !== 0) {
|
||||
@@ -552,7 +549,7 @@ async function configureGluster (
|
||||
creation +
|
||||
' ' +
|
||||
brickVms.map(ipAndHost => ipAndHost.brickName).join(' ')
|
||||
debug('creating volume: ', volumeCreation)
|
||||
log.debug(`creating volume: ${volumeCreation}`)
|
||||
await glusterCmd(glusterEndpoint, volumeCreation)
|
||||
await glusterCmd(
|
||||
glusterEndpoint,
|
||||
@@ -762,7 +759,7 @@ export const createSR = defer(async function (
|
||||
glusterType,
|
||||
arbiter
|
||||
)
|
||||
debug('xosan gluster volume started')
|
||||
log.debug('xosan gluster volume started')
|
||||
// We use 10 IPs of the gluster VM range as backup, in the hope that even if the first VM gets destroyed we find at least
|
||||
// one VM to give mount the volfile.
|
||||
// It is not possible to edit the device_config after the SR is created and this data is only used at mount time when rebooting
|
||||
@@ -785,7 +782,7 @@ export const createSR = defer(async function (
|
||||
true,
|
||||
{}
|
||||
)
|
||||
debug('sr created')
|
||||
log.debug('sr created')
|
||||
// we just forget because the cleanup actions are stacked in the $onFailure system
|
||||
$defer.onFailure(() => xapi.forgetSr(xosanSrRef))
|
||||
if (arbiter) {
|
||||
@@ -809,7 +806,7 @@ export const createSR = defer(async function (
|
||||
redundancy,
|
||||
})
|
||||
CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 6 }
|
||||
debug('scanning new SR')
|
||||
log.debug('scanning new SR')
|
||||
await xapi.call('SR.scan', xosanSrRef)
|
||||
await this.rebindLicense({
|
||||
licenseId: license.id,
|
||||
@@ -1139,12 +1136,12 @@ async function _prepareGlusterVm (
|
||||
.find(vdi => vdi && vdi.name_label === 'xosan_root')
|
||||
const rootDiskSize = rootDisk.virtual_size
|
||||
await xapi.startVm(newVM)
|
||||
debug('waiting for boot of ', ip)
|
||||
log.debug(`waiting for boot of ${ip}`)
|
||||
// wait until we find the assigned IP in the networks, we are just checking the boot is complete
|
||||
const vmIsUp = vm =>
|
||||
Boolean(vm.$guest_metrics && includes(vm.$guest_metrics.networks, ip))
|
||||
const vm = await xapi._waitObjectState(newVM.$id, vmIsUp)
|
||||
debug('booted ', ip)
|
||||
log.debug(`booted ${ip}`)
|
||||
const localEndpoint = { xapi: xapi, hosts: [host], addresses: [ip] }
|
||||
const srFreeSpace = sr.physical_size - sr.physical_utilisation
|
||||
// we use a percentage because it looks like the VDI overhead is proportional
|
||||
|
||||
@@ -3,7 +3,7 @@ import assert from 'assert'
|
||||
import bind from 'lodash/bind'
|
||||
import blocked from 'blocked'
|
||||
import createExpress from 'express'
|
||||
import createLogger from 'debug'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import has from 'lodash/has'
|
||||
import helmet from 'helmet'
|
||||
import includes from 'lodash/includes'
|
||||
@@ -40,13 +40,20 @@ import passport from 'passport'
|
||||
import { parse as parseCookies } from 'cookie'
|
||||
import { Strategy as LocalStrategy } from 'passport-local'
|
||||
|
||||
import transportConsole from '@xen-orchestra/log/transports/console'
|
||||
import { configure } from '@xen-orchestra/log/configure'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const debug = createLogger('xo:main')
|
||||
configure([
|
||||
{
|
||||
filter: process.env.DEBUG,
|
||||
level: 'info',
|
||||
transport: transportConsole(),
|
||||
},
|
||||
])
|
||||
|
||||
const warn = (...args) => {
|
||||
console.warn('[Warn]', ...args)
|
||||
}
|
||||
const log = createLogger('xo:main')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -58,12 +65,12 @@ async function loadConfiguration () {
|
||||
ignoreUnknownFormats: true,
|
||||
})
|
||||
|
||||
debug('Configuration loaded.')
|
||||
log.info('Configuration loaded.')
|
||||
|
||||
// Print a message if deprecated entries are specified.
|
||||
forEach(DEPRECATED_ENTRIES, entry => {
|
||||
if (has(config, entry)) {
|
||||
warn(`${entry} configuration is deprecated.`)
|
||||
log.warn(`${entry} configuration is deprecated.`)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -248,18 +255,18 @@ async function registerPlugin (pluginPath, pluginName) {
|
||||
)
|
||||
}
|
||||
|
||||
const debugPlugin = createLogger('xo:plugin')
|
||||
const logPlugin = createLogger('xo:plugin')
|
||||
|
||||
function registerPluginWrapper (pluginPath, pluginName) {
|
||||
debugPlugin('register %s', pluginName)
|
||||
logPlugin.info(`register ${pluginName}`)
|
||||
|
||||
return registerPlugin.call(this, pluginPath, pluginName).then(
|
||||
() => {
|
||||
debugPlugin(`successfully register ${pluginName}`)
|
||||
logPlugin.info(`successfully register ${pluginName}`)
|
||||
},
|
||||
error => {
|
||||
debugPlugin(`failed register ${pluginName}`)
|
||||
debugPlugin(error)
|
||||
logPlugin.info(`failed register ${pluginName}`)
|
||||
logPlugin.info(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -323,20 +330,20 @@ async function makeWebServerListen (
|
||||
}
|
||||
try {
|
||||
const niceAddress = await webServer.listen(opts)
|
||||
debug(`Web server listening on ${niceAddress}`)
|
||||
log.info(`Web server listening on ${niceAddress}`)
|
||||
} catch (error) {
|
||||
if (error.niceAddress) {
|
||||
warn(`Web server could not listen on ${error.niceAddress}`)
|
||||
log.warn(`Web server could not listen on ${error.niceAddress}`, { error })
|
||||
|
||||
const { code } = error
|
||||
if (code === 'EACCES') {
|
||||
warn(' Access denied.')
|
||||
warn(' Ports < 1024 are often reserved to privileges users.')
|
||||
log.warn(' Access denied.')
|
||||
log.warn(' Ports < 1024 are often reserved to privileges users.')
|
||||
} else if (code === 'EADDRINUSE') {
|
||||
warn(' Address already in use.')
|
||||
log.warn(' Address already in use.')
|
||||
}
|
||||
} else {
|
||||
warn('Web server could not listen:', error.message)
|
||||
log.warn('Web server could not listen:', { error })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,7 +424,7 @@ const setUpStaticFiles = (express, opts) => {
|
||||
}
|
||||
|
||||
forEach(paths, path => {
|
||||
debug('Setting up %s → %s', url, path)
|
||||
log.info(`Setting up ${url} → ${path}`)
|
||||
|
||||
express.use(url, serveStatic(path))
|
||||
})
|
||||
@@ -435,7 +442,7 @@ const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
|
||||
const onConnection = (socket, upgradeReq) => {
|
||||
const { remoteAddress } = upgradeReq.socket
|
||||
|
||||
debug('+ WebSocket connection (%s)', remoteAddress)
|
||||
log.info(`+ WebSocket connection (${remoteAddress})`)
|
||||
|
||||
// Create the abstract XO object for this connection.
|
||||
const connection = xo.createUserConnection()
|
||||
@@ -453,7 +460,7 @@ const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
|
||||
|
||||
// Close the XO connection with this WebSocket.
|
||||
socket.once('close', () => {
|
||||
debug('- WebSocket connection (%s)', remoteAddress)
|
||||
log.info(`- WebSocket connection (${remoteAddress})`)
|
||||
|
||||
connection.close()
|
||||
})
|
||||
@@ -465,7 +472,7 @@ const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
|
||||
|
||||
const onSend = error => {
|
||||
if (error) {
|
||||
warn('WebSocket send:', error.stack)
|
||||
log.warn('WebSocket send:', { error })
|
||||
}
|
||||
}
|
||||
jsonRpc.on('data', data => {
|
||||
@@ -513,9 +520,9 @@ const setUpConsoleProxy = (webServer, xo) => {
|
||||
}
|
||||
|
||||
const { remoteAddress } = socket
|
||||
debug('+ Console proxy (%s - %s)', user.name, remoteAddress)
|
||||
log.info(`+ Console proxy (${user.name} - ${remoteAddress})`)
|
||||
socket.on('close', () => {
|
||||
debug('- Console proxy (%s - %s)', user.name, remoteAddress)
|
||||
log.info(`- Console proxy (${user.name} - ${remoteAddress})`)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -549,10 +556,10 @@ export default async function main (args) {
|
||||
}
|
||||
|
||||
{
|
||||
const debug = createLogger('xo:perf')
|
||||
const logPerf = createLogger('xo:perf')
|
||||
blocked(
|
||||
ms => {
|
||||
debug('blocked for %sms', ms | 0)
|
||||
logPerf.info(`blocked for ${ms | 0}ms`)
|
||||
},
|
||||
{
|
||||
threshold: 500,
|
||||
@@ -569,14 +576,14 @@ export default async function main (args) {
|
||||
const { user, group } = config
|
||||
if (group) {
|
||||
process.setgid(group)
|
||||
debug('Group changed to', group)
|
||||
log.info(`Group changed to ${group}`)
|
||||
}
|
||||
if (user) {
|
||||
process.setuid(user)
|
||||
debug('User changed to', user)
|
||||
log.info(`User changed to ${user}`)
|
||||
}
|
||||
} catch (error) {
|
||||
warn('Failed to change user/group:', error)
|
||||
log.warn('Failed to change user/group:', { error })
|
||||
}
|
||||
|
||||
// Creates main object.
|
||||
@@ -604,7 +611,7 @@ export default async function main (args) {
|
||||
})
|
||||
|
||||
if (port === undefined) {
|
||||
warn('Could not setup HTTPs redirection: no HTTPs port found')
|
||||
log.warn('Could not setup HTTPs redirection: no HTTPs port found')
|
||||
} else {
|
||||
express.use((req, res, next) => {
|
||||
if (req.secure) {
|
||||
@@ -653,17 +660,17 @@ export default async function main (args) {
|
||||
|
||||
process.on(signal, () => {
|
||||
if (alreadyCalled) {
|
||||
warn('forced exit')
|
||||
log.warn('forced exit')
|
||||
process.exit(1)
|
||||
}
|
||||
alreadyCalled = true
|
||||
|
||||
debug('%s caught, closing…', signal)
|
||||
log.info(`${signal} caught, closing…`)
|
||||
xo.stop()
|
||||
})
|
||||
})
|
||||
|
||||
await fromEvent(xo, 'stopped')
|
||||
|
||||
debug('bye :-)')
|
||||
log.info('bye :-)')
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import Collection from '../collection/redis'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import Model from '../model'
|
||||
import { forEach } from '../utils'
|
||||
|
||||
const log = createLogger('xo:plugin-metadata')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class PluginMetadata extends Model {}
|
||||
@@ -44,10 +47,7 @@ export class PluginsMetadata extends Collection {
|
||||
pluginMetadata.configuration =
|
||||
configuration && JSON.parse(configuration)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'cannot parse pluginMetadata.configuration:',
|
||||
configuration
|
||||
)
|
||||
log.warn(`cannot parse pluginMetadata.configuration: ${configuration}`)
|
||||
pluginMetadata.configuration = []
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import createDebug from 'debug'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import partialStream from 'partial-stream'
|
||||
import { connect } from 'tls'
|
||||
import { parse } from 'url'
|
||||
|
||||
const debug = createDebug('xo:proxy-console')
|
||||
const log = createLogger('xo:proxy-console')
|
||||
|
||||
export default function proxyConsole (ws, vmConsole, sessionId) {
|
||||
const url = parse(vmConsole.location)
|
||||
let { hostname } = url
|
||||
if (hostname === null || hostname === '') {
|
||||
console.warn(
|
||||
'host is missing in console (%s) URI (%s)',
|
||||
vmConsole.uuid,
|
||||
vmConsole.location
|
||||
)
|
||||
const { address } = vmConsole.$VM.$resident_on
|
||||
console.warn(' using host address (%s) as fallback', address)
|
||||
hostname = address
|
||||
|
||||
log.warn(
|
||||
`host is missing in console (${vmConsole.uuid}) URI (${
|
||||
vmConsole.location
|
||||
}) using host address (${address}) as fallback`
|
||||
)
|
||||
}
|
||||
|
||||
let closed = false
|
||||
@@ -41,10 +41,7 @@ export default function proxyConsole (ws, vmConsole, sessionId) {
|
||||
|
||||
const onSend = error => {
|
||||
if (error) {
|
||||
debug(
|
||||
'error sending to the XO client: %s',
|
||||
error.stack || error.message || error
|
||||
)
|
||||
log.debug('error sending to the XO client:', { error })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +49,7 @@ export default function proxyConsole (ws, vmConsole, sessionId) {
|
||||
.pipe(
|
||||
partialStream('\r\n\r\n', headers => {
|
||||
// TODO: check status code 200.
|
||||
debug('connected')
|
||||
log.debug('connected')
|
||||
})
|
||||
)
|
||||
.on('data', data => {
|
||||
@@ -63,7 +60,7 @@ export default function proxyConsole (ws, vmConsole, sessionId) {
|
||||
.on('end', () => {
|
||||
if (!closed) {
|
||||
closed = true
|
||||
debug('disconnected from the console')
|
||||
log.debug('disconnected from the console')
|
||||
}
|
||||
|
||||
ws.close()
|
||||
@@ -71,10 +68,7 @@ export default function proxyConsole (ws, vmConsole, sessionId) {
|
||||
|
||||
ws.on('error', error => {
|
||||
closed = true
|
||||
debug(
|
||||
'error from the XO client: %s',
|
||||
error.stack || error.message || error
|
||||
)
|
||||
log.debug('error from the XO client:', { error })
|
||||
|
||||
socket.end()
|
||||
})
|
||||
@@ -86,7 +80,7 @@ export default function proxyConsole (ws, vmConsole, sessionId) {
|
||||
.on('close', () => {
|
||||
if (!closed) {
|
||||
closed = true
|
||||
debug('disconnected from the XO client')
|
||||
log.debug('disconnected from the XO client')
|
||||
}
|
||||
|
||||
socket.end()
|
||||
@@ -94,7 +88,7 @@ export default function proxyConsole (ws, vmConsole, sessionId) {
|
||||
}
|
||||
).on('error', error => {
|
||||
closed = true
|
||||
debug('error from the console: %s', error.stack || error.message || error)
|
||||
log.debug('error from the console:', { error })
|
||||
|
||||
ws.close()
|
||||
})
|
||||
|
||||
15
packages/xo-server/src/schemas/log/taskWarning.js
Normal file
15
packages/xo-server/src/schemas/log/taskWarning.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
event: {
|
||||
enum: ['task.warning'],
|
||||
},
|
||||
taskId: {
|
||||
type: 'string',
|
||||
description: 'identifier of the parent task or job',
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
required: ['event', 'taskId'],
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import humanFormat from 'human-format'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isString from 'lodash/isString'
|
||||
import keys from 'lodash/keys'
|
||||
import kindOf from 'kindof'
|
||||
import multiKeyHashInt from 'multikey-hash'
|
||||
import pick from 'lodash/pick'
|
||||
import tmp from 'tmp'
|
||||
@@ -192,35 +191,6 @@ export const noop = () => {}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Usage: pDebug(promise, name) or promise::pDebug(name)
|
||||
export function pDebug (promise, name) {
|
||||
if (arguments.length === 1) {
|
||||
name = promise
|
||||
promise = this
|
||||
}
|
||||
|
||||
Promise.resolve(promise).then(
|
||||
value => {
|
||||
console.log(
|
||||
'%s',
|
||||
`Promise ${name} resolved${
|
||||
value !== undefined ? ` with ${kindOf(value)}` : ''
|
||||
}`
|
||||
)
|
||||
},
|
||||
reason => {
|
||||
console.log(
|
||||
'%s',
|
||||
`Promise ${name} rejected${
|
||||
reason !== undefined ? ` with ${kindOf(reason)}` : ''
|
||||
}`
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
// Given a collection (array or object) which contains promises,
|
||||
// return a promise that is fulfilled when all the items in the
|
||||
// collection are either fulfilled or rejected.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable camelcase */
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import concurrency from 'limit-concurrency-decorator'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import deferrable from 'golike-defer'
|
||||
import fatfs from 'fatfs'
|
||||
import mixin from '@xen-orchestra/mixin'
|
||||
@@ -55,7 +56,6 @@ import { type DeltaVmExport } from './'
|
||||
import {
|
||||
asBoolean,
|
||||
asInteger,
|
||||
debug,
|
||||
extractOpaqueRef,
|
||||
filterUndefineds,
|
||||
getNamespaceForType,
|
||||
@@ -68,6 +68,8 @@ import {
|
||||
prepareXapiParam,
|
||||
} from './utils'
|
||||
|
||||
const log = createLogger('xo:xapi')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const TAG_BASE_DELTA = 'xo:base_delta'
|
||||
@@ -364,7 +366,7 @@ export default class Xapi extends XapiBase {
|
||||
async emergencyShutdownHost (hostId) {
|
||||
const host = this.getObject(hostId)
|
||||
const vms = host.$resident_VMs
|
||||
debug(`Emergency shutdown: ${host.name_label}`)
|
||||
log.debug(`Emergency shutdown: ${host.name_label}`)
|
||||
await pSettle(
|
||||
mapToArray(vms, vm => {
|
||||
if (!vm.is_control_domain) {
|
||||
@@ -447,7 +449,7 @@ export default class Xapi extends XapiBase {
|
||||
// Clone a VM: make a fast copy by fast copying each of its VDIs
|
||||
// (using snapshots where possible) on the same SRs.
|
||||
_cloneVm (vm, nameLabel = vm.name_label) {
|
||||
debug(
|
||||
log.debug(
|
||||
`Cloning VM ${vm.name_label}${
|
||||
nameLabel !== vm.name_label ? ` as ${nameLabel}` : ''
|
||||
}`
|
||||
@@ -466,7 +468,7 @@ export default class Xapi extends XapiBase {
|
||||
snapshot = await this._snapshotVm(vm)
|
||||
}
|
||||
|
||||
debug(
|
||||
log.debug(
|
||||
`Copying VM ${vm.name_label}${
|
||||
nameLabel !== vm.name_label ? ` as ${nameLabel}` : ''
|
||||
}${sr ? ` on ${sr.name_label}` : ''}`
|
||||
@@ -587,7 +589,7 @@ export default class Xapi extends XapiBase {
|
||||
version,
|
||||
xenstore_data,
|
||||
}) {
|
||||
debug(`Creating VM ${name_label}`)
|
||||
log.debug(`Creating VM ${name_label}`)
|
||||
|
||||
return this.call(
|
||||
'VM.create',
|
||||
@@ -648,7 +650,7 @@ export default class Xapi extends XapiBase {
|
||||
force = false,
|
||||
forceDeleteDefaultTemplate = false
|
||||
) {
|
||||
debug(`Deleting VM ${vm.name_label}`)
|
||||
log.debug(`Deleting VM ${vm.name_label}`)
|
||||
|
||||
const { $ref } = vm
|
||||
|
||||
@@ -689,13 +691,13 @@ export default class Xapi extends XapiBase {
|
||||
asyncMap(disks, ({ $ref: vdiRef }) => {
|
||||
let onFailure = () => {
|
||||
onFailure = vdi => {
|
||||
console.error(
|
||||
log.error(
|
||||
`cannot delete VDI ${vdi.name_label} (from VM ${vm.name_label})`
|
||||
)
|
||||
forEach(vdi.$VBDs, vbd => {
|
||||
if (vbd.VM !== $ref) {
|
||||
const vm = vbd.$VM
|
||||
console.error('- %s (%s)', vm.name_label, vm.uuid)
|
||||
log.error(`- ${vm.name_label} (${vm.uuid})`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1092,6 +1094,7 @@ export default class Xapi extends XapiBase {
|
||||
transferSize += sizeStream.size
|
||||
})
|
||||
sizeStream.task = stream.task
|
||||
sizeStream.length = stream.length
|
||||
await this._importVdiContent(vdi, sizeStream, VDI_FORMAT_VHD)
|
||||
}
|
||||
}),
|
||||
@@ -1217,7 +1220,7 @@ export default class Xapi extends XapiBase {
|
||||
{ vdi }
|
||||
).catch(error => {
|
||||
if (error.code !== 'XENAPI_PLUGIN_FAILURE') {
|
||||
console.warn('_callInstallationPlugin', error)
|
||||
log.warn('_callInstallationPlugin', { error })
|
||||
throw error
|
||||
}
|
||||
})
|
||||
@@ -1491,7 +1494,7 @@ export default class Xapi extends XapiBase {
|
||||
@concurrency(2)
|
||||
@cancelable
|
||||
async _snapshotVm ($cancelToken, vm, nameLabel = vm.name_label) {
|
||||
debug(
|
||||
log.debug(
|
||||
`Snapshotting VM ${vm.name_label}${
|
||||
nameLabel !== vm.name_label ? ` as ${nameLabel}` : ''
|
||||
}`
|
||||
@@ -1553,7 +1556,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
async _startVm (vm, host, force) {
|
||||
debug(`Starting VM ${vm.name_label}`)
|
||||
log.debug(`Starting VM ${vm.name_label}`)
|
||||
|
||||
if (force) {
|
||||
await this._updateObjectMapProperty(vm, 'blocked_operations', {
|
||||
@@ -1701,7 +1704,7 @@ export default class Xapi extends XapiBase {
|
||||
vdi = this.getObject(vdi)
|
||||
vm = this.getObject(vm)
|
||||
|
||||
debug(`Creating VBD for VDI ${vdi.name_label} on VM ${vm.name_label}`)
|
||||
log.debug(`Creating VBD for VDI ${vdi.name_label} on VM ${vm.name_label}`)
|
||||
|
||||
if (userdevice == null) {
|
||||
const allowed = await this.call('VM.get_allowed_VBD_devices', vm.$ref)
|
||||
@@ -1744,7 +1747,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
_cloneVdi (vdi) {
|
||||
debug(`Cloning VDI ${vdi.name_label}`)
|
||||
log.debug(`Cloning VDI ${vdi.name_label}`)
|
||||
|
||||
return this.call('VDI.clone', vdi.$ref)
|
||||
}
|
||||
@@ -1766,7 +1769,7 @@ export default class Xapi extends XapiBase {
|
||||
sr = SR !== undefined && SR !== NULL_REF ? SR : this.pool.default_SR,
|
||||
}) {
|
||||
sr = this.getObject(sr)
|
||||
debug(`Creating VDI ${name_label} on ${sr.name_label}`)
|
||||
log.debug(`Creating VDI ${name_label} on ${sr.name_label}`)
|
||||
|
||||
return this._getOrWaitObject(
|
||||
await this.call('VDI.create', {
|
||||
@@ -1793,7 +1796,7 @@ export default class Xapi extends XapiBase {
|
||||
return // nothing to do
|
||||
}
|
||||
|
||||
debug(
|
||||
log.debug(
|
||||
`Moving VDI ${vdi.name_label} from ${vdi.$SR.name_label} to ${
|
||||
sr.name_label
|
||||
}`
|
||||
@@ -1826,13 +1829,15 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// TODO: check whether the VDI is attached.
|
||||
async _deleteVdi (vdi) {
|
||||
debug(`Deleting VDI ${vdi.name_label}`)
|
||||
log.debug(`Deleting VDI ${vdi.name_label}`)
|
||||
|
||||
await this.call('VDI.destroy', vdi.$ref)
|
||||
}
|
||||
|
||||
_resizeVdi (vdi, size) {
|
||||
debug(`Resizing VDI ${vdi.name_label} from ${vdi.virtual_size} to ${size}`)
|
||||
log.debug(
|
||||
`Resizing VDI ${vdi.name_label} from ${vdi.virtual_size} to ${size}`
|
||||
)
|
||||
|
||||
return this.call('VDI.resize', vdi.$ref, size)
|
||||
}
|
||||
@@ -1963,7 +1968,7 @@ export default class Xapi extends XapiBase {
|
||||
query.base = base.$ref
|
||||
}
|
||||
|
||||
debug(
|
||||
log.debug(
|
||||
`exporting VDI ${vdi.name_label}${
|
||||
base ? ` (from base ${vdi.name_label})` : ''
|
||||
}`
|
||||
@@ -2034,7 +2039,7 @@ export default class Xapi extends XapiBase {
|
||||
qos_algorithm_type = '',
|
||||
} = {}
|
||||
) {
|
||||
debug(
|
||||
log.debug(
|
||||
`Creating VIF for VM ${vm.name_label} on network ${network.name_label}`
|
||||
)
|
||||
|
||||
@@ -2297,9 +2302,9 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// ignore errors, I (JFT) don't understand why they are emitted
|
||||
// because it works
|
||||
await this._importVdiContent(vdi, buffer, VDI_FORMAT_RAW).catch(
|
||||
console.warn
|
||||
)
|
||||
await this._importVdiContent(vdi, buffer, VDI_FORMAT_RAW).catch(error => {
|
||||
log.warn('importVdiContent: ', { error })
|
||||
})
|
||||
|
||||
await this.createVbd({ vdi, vm })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import deferrable from 'golike-defer'
|
||||
import every from 'lodash/every'
|
||||
import filter from 'lodash/filter'
|
||||
@@ -20,7 +21,9 @@ import {
|
||||
parseXml,
|
||||
} from '../../utils'
|
||||
|
||||
import { debug, extractOpaqueRef, useUpdateSystem } from '../utils'
|
||||
import { extractOpaqueRef, useUpdateSystem } from '../utils'
|
||||
|
||||
const log = createLogger('xo:xapi')
|
||||
|
||||
export default {
|
||||
// FIXME: should be static
|
||||
@@ -227,7 +230,7 @@ export default {
|
||||
return this.getObjectByUuid(uuid)
|
||||
} catch (error) {}
|
||||
|
||||
debug('downloading patch %s', uuid)
|
||||
log.debug(`downloading patch ${uuid}`)
|
||||
|
||||
const patchInfo = (await this._getXenUpdates()).patches[uuid]
|
||||
if (!patchInfo) {
|
||||
@@ -255,7 +258,7 @@ export default {
|
||||
|
||||
// patform_version >= 2.1.1 ----------------------------------------
|
||||
async _getUpdateVdi ($defer, patchUuid, hostId) {
|
||||
debug('downloading patch %s', patchUuid)
|
||||
log.debug(`downloading patch ${patchUuid}`)
|
||||
|
||||
const patchInfo = (await this._getXenUpdates()).patches[patchUuid]
|
||||
if (!patchInfo) {
|
||||
@@ -339,7 +342,7 @@ export default {
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async installPoolPatchOnHost (patchUuid, host) {
|
||||
debug('installing patch %s', patchUuid)
|
||||
log.debug(`installing patch ${patchUuid}`)
|
||||
if (!isObject(host)) {
|
||||
host = this.getObject(host)
|
||||
}
|
||||
@@ -380,7 +383,7 @@ export default {
|
||||
}),
|
||||
|
||||
async installPoolPatchOnAllHosts (patchUuid) {
|
||||
debug('installing patch %s on all hosts', patchUuid)
|
||||
log.debug(`installing patch ${patchUuid} on all hosts`)
|
||||
|
||||
return useUpdateSystem(this.pool.$master)
|
||||
? this._installPatchUpdateOnAllHosts(patchUuid)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { forEach, groupBy } from 'lodash'
|
||||
|
||||
import { mapToArray } from '../../utils'
|
||||
|
||||
const log = createLogger('xo:storage')
|
||||
|
||||
export default {
|
||||
_connectAllSrPbds (sr) {
|
||||
return Promise.all(mapToArray(sr.$PBDs, pbd => this._plugPbd(pbd)))
|
||||
@@ -58,7 +61,7 @@ export default {
|
||||
length += this._getUnhealthyVdiChainLength(parent, childrenMap, cache)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Xapi#_getUnhealthyVdiChainLength(%s)', uuid, error)
|
||||
log.warn(`Xapi#_getUnhealthyVdiChainLength(${uuid})`, { error })
|
||||
}
|
||||
cache[uuid] = length
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// import isFinite from 'lodash/isFinite'
|
||||
import camelCase from 'lodash/camelCase'
|
||||
import createDebug from 'debug'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import pickBy from 'lodash/pickBy'
|
||||
@@ -61,10 +60,6 @@ export const prepareXapiParam = param => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const debug = createDebug('xo:xapi')
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const OPAQUE_REF_RE = /OpaqueRef:[0-9a-z-]+/
|
||||
export const extractOpaqueRef = str => {
|
||||
const matches = OPAQUE_REF_RE.exec(str)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import createDebug from 'debug'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import kindOf from 'kindof'
|
||||
import ms from 'ms'
|
||||
import schemaInspector from 'schema-inspector'
|
||||
@@ -12,7 +12,7 @@ import * as errors from 'xo-common/api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const debug = createDebug('xo:api')
|
||||
const log = createLogger('xo:api')
|
||||
|
||||
const PERMISSIONS = {
|
||||
none: 0,
|
||||
@@ -284,12 +284,10 @@ export default class Api {
|
||||
result = true
|
||||
}
|
||||
|
||||
debug(
|
||||
'%s | %s(...) [%s] ==> %s',
|
||||
userName,
|
||||
name,
|
||||
ms(Date.now() - startTime),
|
||||
kindOf(result)
|
||||
log.debug(
|
||||
`${userName} | ${name}(...) [${ms(
|
||||
Date.now() - startTime
|
||||
)}] ==> ${kindOf(result)}`
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -308,19 +306,12 @@ export default class Api {
|
||||
this._logger.error(message, data)
|
||||
|
||||
if (this._xo._config.verboseLogsOnErrors) {
|
||||
debug(message)
|
||||
|
||||
const stack = error && error.stack
|
||||
if (stack) {
|
||||
console.error(stack)
|
||||
}
|
||||
log.warn(message, { error })
|
||||
} else {
|
||||
debug(
|
||||
'%s | %s(...) [%s] =!> %s',
|
||||
userName,
|
||||
name,
|
||||
ms(Date.now() - startTime),
|
||||
error
|
||||
log.warn(
|
||||
`${userName} | ${name}(...) [${ms(
|
||||
Date.now() - startTime
|
||||
)}] =!> ${error}`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import ms from 'ms'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
@@ -6,6 +7,7 @@ import Token, { Tokens } from '../models/token'
|
||||
import { forEach, generateToken } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
const log = createLogger('xo:authentification')
|
||||
|
||||
const noSuchAuthenticationToken = id => noSuchObject(id, 'authenticationToken')
|
||||
|
||||
@@ -103,7 +105,7 @@ export default class {
|
||||
// DEPRECATED: Authentication providers may just throw `null`
|
||||
// to indicate they could not authenticate the user without
|
||||
// any special errors.
|
||||
if (error) console.error(error.stack || error)
|
||||
if (error) log.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { forEach } from 'lodash'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
const isSkippedError = error =>
|
||||
error.message === 'no disks found' ||
|
||||
error.message === 'no such object' ||
|
||||
noSuchObject.is(error) ||
|
||||
error.message === 'no VMs match this pattern' ||
|
||||
error.message === 'unhealthy VDI chain'
|
||||
|
||||
@@ -49,10 +50,16 @@ const taskTimeComparator = ({ start: s1, end: e1 }, { start: s2, end: e2 }) => {
|
||||
|
||||
export default {
|
||||
async getBackupNgLogs (runId?: string) {
|
||||
const { runningJobs } = this
|
||||
const [jobLogs, restoreLogs] = await Promise.all([
|
||||
this.getLogs('jobs'),
|
||||
this.getLogs('restore'),
|
||||
])
|
||||
|
||||
const { runningJobs, runningRestores } = this
|
||||
const consolidated = {}
|
||||
const started = {}
|
||||
forEach(await this.getLogs('jobs'), ({ data, time, message }, id) => {
|
||||
|
||||
const handleLog = ({ data, time, message }, id) => {
|
||||
const { event } = data
|
||||
if (event === 'job.start') {
|
||||
if (
|
||||
@@ -82,17 +89,26 @@ export default {
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.start') {
|
||||
const parent = started[data.parentId]
|
||||
if (parent !== undefined) {
|
||||
;(parent.tasks || (parent.tasks = [])).push(
|
||||
(started[id] = {
|
||||
data: data.data,
|
||||
id,
|
||||
message,
|
||||
start: time,
|
||||
status: parent.status,
|
||||
})
|
||||
)
|
||||
const task = {
|
||||
data: data.data,
|
||||
id,
|
||||
message,
|
||||
start: time,
|
||||
}
|
||||
const { parentId } = data
|
||||
let parent
|
||||
if (parentId === undefined && (runId === undefined || runId === id)) {
|
||||
// top level task
|
||||
task.status =
|
||||
message === 'restore' && !runningRestores.has(id)
|
||||
? 'interrupted'
|
||||
: 'pending'
|
||||
consolidated[id] = started[id] = task
|
||||
} else if ((parent = started[parentId]) !== undefined) {
|
||||
// sub-task for which the parent exists
|
||||
task.status = parent.status
|
||||
started[id] = task
|
||||
;(parent.tasks || (parent.tasks = [])).push(task)
|
||||
}
|
||||
} else if (event === 'task.end') {
|
||||
const { taskId } = data
|
||||
@@ -106,6 +122,13 @@ export default {
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.warning') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.warnings || (parent.warnings = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'jobCall.start') {
|
||||
const parent = started[data.runJobId]
|
||||
if (parent !== undefined) {
|
||||
@@ -133,7 +156,11 @@ export default {
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
forEach(jobLogs, handleLog)
|
||||
forEach(restoreLogs, handleLog)
|
||||
|
||||
return runId === undefined ? consolidated : consolidated[runId]
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// $FlowFixMe
|
||||
import type RemoteHandler from '@xen-orchestra/fs'
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import defer from 'golike-defer'
|
||||
import limitConcurrency from 'limit-concurrency-decorator'
|
||||
import { type Pattern, createPredicate } from 'value-matcher'
|
||||
@@ -23,12 +24,18 @@ import {
|
||||
sum,
|
||||
values,
|
||||
} from 'lodash'
|
||||
import { CancelToken, pFromEvent, ignoreErrors } from 'promise-toolbox'
|
||||
import {
|
||||
CancelToken,
|
||||
ignoreErrors,
|
||||
pFinally,
|
||||
pFromEvent,
|
||||
} from 'promise-toolbox'
|
||||
import Vhd, {
|
||||
chainVhd,
|
||||
createSyntheticStream as createVhdReadStream,
|
||||
} from 'vhd-lib'
|
||||
|
||||
import type Logger from '../logs/loggers/abstract'
|
||||
import { type CallJob, type Executor, type Job } from '../jobs'
|
||||
import { type Schedule } from '../scheduling'
|
||||
|
||||
@@ -50,6 +57,8 @@ import {
|
||||
|
||||
import { translateLegacyJob } from './migration'
|
||||
|
||||
const log = createLogger('xo:xo-mixins:backups-ng')
|
||||
|
||||
export type Mode = 'full' | 'delta'
|
||||
export type ReportWhen = 'always' | 'failure' | 'never'
|
||||
|
||||
@@ -202,11 +211,13 @@ const importers: $Dict<
|
||||
metadataFilename: string,
|
||||
metadata: Metadata,
|
||||
xapi: Xapi,
|
||||
sr: { $id: string }
|
||||
sr: { $id: string },
|
||||
taskId: string,
|
||||
logger: Logger
|
||||
) => Promise<string>,
|
||||
Mode
|
||||
> = {
|
||||
async delta (handler, metadataFilename, metadata, xapi, sr) {
|
||||
async delta (handler, metadataFilename, metadata, xapi, sr, taskId, logger) {
|
||||
metadata = ((metadata: any): MetadataDelta)
|
||||
const { vdis, vhds, vm } = metadata
|
||||
|
||||
@@ -231,15 +242,26 @@ const importers: $Dict<
|
||||
},
|
||||
}
|
||||
|
||||
const { vm: newVm } = await xapi.importDeltaVm(delta, {
|
||||
detectBase: false,
|
||||
disableStartAfterImport: false,
|
||||
srId: sr,
|
||||
// TODO: support mapVdisSrs
|
||||
})
|
||||
const { vm: newVm } = await wrapTask(
|
||||
{
|
||||
logger,
|
||||
message: 'transfer',
|
||||
parentId: taskId,
|
||||
result: ({ transferSize, vm: { $id: id } }) => ({
|
||||
size: transferSize,
|
||||
id,
|
||||
}),
|
||||
},
|
||||
xapi.importDeltaVm(delta, {
|
||||
detectBase: false,
|
||||
disableStartAfterImport: false,
|
||||
srId: sr,
|
||||
// TODO: support mapVdisSrs
|
||||
})
|
||||
)
|
||||
return newVm.$id
|
||||
},
|
||||
async full (handler, metadataFilename, metadata, xapi, sr) {
|
||||
async full (handler, metadataFilename, metadata, xapi, sr, taskId, logger) {
|
||||
metadata = ((metadata: any): MetadataFull)
|
||||
|
||||
const xva = await handler.createReadStream(
|
||||
@@ -249,7 +271,16 @@ const importers: $Dict<
|
||||
ignoreMissingChecksum: true, // provide an easy way to opt-out
|
||||
}
|
||||
)
|
||||
const vm = await xapi.importVm(xva, { srId: sr.$id })
|
||||
|
||||
const vm = await wrapTask(
|
||||
{
|
||||
logger,
|
||||
message: 'transfer',
|
||||
parentId: taskId,
|
||||
result: ({ $id: id }) => ({ size: xva.length, id }),
|
||||
},
|
||||
xapi.importVm(xva, { srId: sr.$id })
|
||||
)
|
||||
await Promise.all([
|
||||
xapi.addTag(vm.$id, 'restored from backup'),
|
||||
xapi.editVm(vm.$id, {
|
||||
@@ -387,6 +418,35 @@ const wrapTaskFn = <T>(
|
||||
}
|
||||
}
|
||||
|
||||
const extractIdsFromSimplePattern = (pattern: mixed) => {
|
||||
if (pattern === null || typeof pattern !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
let keys = Object.keys(pattern)
|
||||
if (keys.length !== 1 || keys[0] !== 'id') {
|
||||
return
|
||||
}
|
||||
|
||||
pattern = pattern.id
|
||||
if (typeof pattern === 'string') {
|
||||
return [pattern]
|
||||
}
|
||||
if (pattern === null || typeof pattern !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
keys = Object.keys(pattern)
|
||||
if (
|
||||
keys.length === 1 &&
|
||||
keys[0] === '__or' &&
|
||||
Array.isArray((pattern = pattern.__or)) &&
|
||||
pattern.every(_ => typeof _ === 'string')
|
||||
) {
|
||||
return pattern
|
||||
}
|
||||
}
|
||||
|
||||
// File structure on remotes:
|
||||
//
|
||||
// <remote>
|
||||
@@ -426,11 +486,21 @@ export default class BackupNg {
|
||||
removeJob: (id: string) => Promise<void>,
|
||||
worker: $Dict<any>,
|
||||
}
|
||||
_logger: Logger
|
||||
_runningRestores: Set<string>
|
||||
|
||||
get runningRestores () {
|
||||
return this._runningRestores
|
||||
}
|
||||
|
||||
constructor (app: any) {
|
||||
this._app = app
|
||||
this._logger = undefined
|
||||
this._runningRestores = new Set()
|
||||
|
||||
app.on('start', async () => {
|
||||
this._logger = await app.getLogger('restore')
|
||||
|
||||
app.on('start', () => {
|
||||
const executor: Executor = async ({
|
||||
cancelToken,
|
||||
data: vmsId,
|
||||
@@ -444,21 +514,48 @@ export default class BackupNg {
|
||||
}
|
||||
|
||||
const job: BackupJob = (job_: any)
|
||||
const vmsPattern = job.vms
|
||||
|
||||
const vms: $Dict<Vm> = app.getObjects({
|
||||
filter: createPredicate({
|
||||
type: 'VM',
|
||||
...(vmsId !== undefined
|
||||
? {
|
||||
id: {
|
||||
__or: vmsId,
|
||||
},
|
||||
}
|
||||
: job.vms),
|
||||
}),
|
||||
})
|
||||
if (isEmpty(vms)) {
|
||||
throw new Error('no VMs match this pattern')
|
||||
let vms: $Dict<Vm>
|
||||
if (
|
||||
vmsId !== undefined ||
|
||||
(vmsId = extractIdsFromSimplePattern(vmsPattern)) !== undefined
|
||||
) {
|
||||
vms = vmsId
|
||||
.map(id => {
|
||||
try {
|
||||
return app.getObject(id, 'VM')
|
||||
} catch (error) {
|
||||
const taskId: string = logger.notice(
|
||||
`Starting backup of ${id}. (${job.id})`,
|
||||
{
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
data: {
|
||||
type: 'VM',
|
||||
id,
|
||||
},
|
||||
}
|
||||
)
|
||||
logger.error(`Backuping ${id} has failed. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
taskId,
|
||||
status: 'failure',
|
||||
result: serializeError(error),
|
||||
})
|
||||
}
|
||||
})
|
||||
.filter(vm => vm !== undefined)
|
||||
} else {
|
||||
vms = app.getObjects({
|
||||
filter: createPredicate({
|
||||
type: 'VM',
|
||||
...vmsPattern,
|
||||
}),
|
||||
})
|
||||
if (isEmpty(vms)) {
|
||||
throw new Error('no VMs match this pattern')
|
||||
}
|
||||
}
|
||||
const jobId = job.id
|
||||
const srs = unboxIds(job.srs).map(id => {
|
||||
@@ -628,14 +725,33 @@ export default class BackupNg {
|
||||
}
|
||||
|
||||
const xapi = app.getXapi(srId)
|
||||
|
||||
return importer(
|
||||
handler,
|
||||
metadataFilename,
|
||||
metadata,
|
||||
xapi,
|
||||
xapi.getObject(srId)
|
||||
)
|
||||
const { jobId, timestamp: time } = metadata
|
||||
const logger = this._logger
|
||||
return wrapTaskFn(
|
||||
{
|
||||
data: {
|
||||
jobId,
|
||||
srId,
|
||||
time,
|
||||
},
|
||||
logger,
|
||||
message: 'restore',
|
||||
},
|
||||
taskId => {
|
||||
this._runningRestores.add(taskId)
|
||||
return importer(
|
||||
handler,
|
||||
metadataFilename,
|
||||
metadata,
|
||||
xapi,
|
||||
xapi.getObject(srId),
|
||||
taskId,
|
||||
logger
|
||||
)::pFinally(() => {
|
||||
this._runningRestores.delete(taskId)
|
||||
})
|
||||
}
|
||||
)()
|
||||
}
|
||||
|
||||
async listVmBackupsNg (remotes: string[]) {
|
||||
@@ -686,7 +802,7 @@ export default class BackupNg {
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.warn('[Warn] listVmBackups for remote %s:', remoteId, error)
|
||||
log.warn(`listVmBackups for remote ${remoteId}:`, { error })
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -1526,7 +1642,7 @@ export default class BackupNg {
|
||||
// Do not fail on corrupted VHDs (usually uncleaned temporary files),
|
||||
// they are probably inconsequent to the backup process and should not
|
||||
// fail it.
|
||||
console.warn('BackupNg#_deleteVhd', path, error)
|
||||
log.warn(`BackupNg#_deleteVhd ${path}`, { error })
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1578,7 +1694,7 @@ export default class BackupNg {
|
||||
backups.push(metadata)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('_listVmBackups', path, error)
|
||||
log.warn(`_listVmBackups ${path}`, { error })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import deferrable from 'golike-defer'
|
||||
import escapeStringRegexp from 'escape-string-regexp'
|
||||
import execa from 'execa'
|
||||
@@ -51,6 +52,7 @@ const TAG_SOURCE_VM = 'xo:source_vm'
|
||||
const TAG_EXPORT_TIME = 'xo:export_time'
|
||||
|
||||
const shortDate = utcFormat('%Y-%m-%d')
|
||||
const log = createLogger('xo:xo-mixins:backups')
|
||||
|
||||
// Test if a file is a vdi backup. (full or delta)
|
||||
const isVdiBackup = name => /^\d+T\d+Z_(?:full|delta)\.vhd$/.test(name)
|
||||
@@ -225,7 +227,7 @@ const mountPartition = (device, partitionId) =>
|
||||
unmount: once(() => execa('umount', ['--lazy', path])),
|
||||
}),
|
||||
error => {
|
||||
console.log(error)
|
||||
log.error(error)
|
||||
|
||||
throw error
|
||||
}
|
||||
@@ -300,14 +302,6 @@ const mountLvmPv = (device, partition) => {
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
this._xo = xo
|
||||
|
||||
// clean any LVM volumes that might have not been properly
|
||||
// unmounted
|
||||
xo.on('start', () =>
|
||||
Promise.all([execa('losetup', ['-D']), execa('vgchange', ['-an'])]).then(
|
||||
() => execa('pvscan', ['--cache'])
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async listRemoteBackups (remoteId) {
|
||||
@@ -557,9 +551,9 @@ export default class {
|
||||
|
||||
try {
|
||||
mergedDataSize += await mergeVhd(handler, parent, handler, backup)
|
||||
} catch (e) {
|
||||
console.error('Unable to use vhd-util.', e)
|
||||
throw e
|
||||
} catch (error) {
|
||||
log.error('unable to use vhd-util', { error })
|
||||
throw error
|
||||
}
|
||||
|
||||
await handler.unlink(backup)
|
||||
@@ -731,7 +725,7 @@ export default class {
|
||||
fulFilledVdiBackups.push(vdiBackup)
|
||||
} else {
|
||||
error = vdiBackup.reason()
|
||||
console.error('Rejected backup:', error)
|
||||
log.error('Rejected backup:', { error })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import createDebug from 'debug'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import DepTree from 'deptree'
|
||||
import { mapValues } from 'lodash'
|
||||
import { pAll } from 'promise-toolbox'
|
||||
|
||||
const debug = createDebug('xo:config-management')
|
||||
const log = createLogger('xo:config-management')
|
||||
|
||||
export default class ConfigManagement {
|
||||
constructor (app) {
|
||||
@@ -33,7 +33,7 @@ export default class ConfigManagement {
|
||||
|
||||
const data = config[key]
|
||||
if (data !== undefined) {
|
||||
debug('importing', key)
|
||||
log.debug(`importing ${key}`)
|
||||
await manager.importer(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,13 @@ export default class BackupNgFileRestore {
|
||||
constructor (app) {
|
||||
this._app = app
|
||||
this._mounts = { __proto__: null }
|
||||
|
||||
// clean any LVM volumes that might have not been properly
|
||||
// unmounted
|
||||
app.on('start', async () => {
|
||||
await Promise.all([execa('losetup', ['-D']), execa('vgchange', ['-an'])])
|
||||
await execa('pvscan', ['--cache'])
|
||||
})
|
||||
}
|
||||
|
||||
@defer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import createLogger from 'debug'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import emitAsync from '@xen-orchestra/emit-async'
|
||||
|
||||
const debug = createLogger('xo:hooks')
|
||||
const log = createLogger('xo:xo-mixins:hooks')
|
||||
|
||||
const makeSingletonHook = (hook, postEvent) => {
|
||||
let promise
|
||||
@@ -19,20 +19,16 @@ const makeSingletonHook = (hook, postEvent) => {
|
||||
}
|
||||
|
||||
const runHook = (app, hook) => {
|
||||
debug(`${hook} start…`)
|
||||
log.debug(`${hook} start…`)
|
||||
const promise = emitAsync.call(
|
||||
app,
|
||||
{
|
||||
onError: error =>
|
||||
console.error(
|
||||
`[WARN] hook ${hook} failure:`,
|
||||
(error != null && error.stack) || error
|
||||
),
|
||||
onError: error => log.warn(`hook ${hook} failure:`, { error }),
|
||||
},
|
||||
hook
|
||||
)
|
||||
promise.then(() => {
|
||||
debug(`${hook} finished`)
|
||||
log.debug(`${hook} finished`)
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
@@ -144,38 +144,35 @@ export default class Jobs {
|
||||
executors.call = executeCall
|
||||
|
||||
xo.on('clean', () => jobsDb.rebuildIndexes())
|
||||
xo.on('start', () => {
|
||||
xo.on('start', async () => {
|
||||
this._logger = await xo.getLogger('jobs')
|
||||
|
||||
xo.addConfigManager(
|
||||
'jobs',
|
||||
() => jobsDb.get(),
|
||||
jobs => Promise.all(mapToArray(jobs, job => jobsDb.save(job))),
|
||||
['users']
|
||||
)
|
||||
|
||||
xo.getLogger('jobs').then(logger => {
|
||||
this._logger = logger
|
||||
})
|
||||
|
||||
// it sends a report for the interrupted backup jobs
|
||||
this._app.on('plugins:registered', () =>
|
||||
asyncMap(this._jobs.get(), job => {
|
||||
// only the interrupted backup jobs have the runId property
|
||||
if (job.runId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this._app.emit(
|
||||
'job:terminated',
|
||||
undefined,
|
||||
job,
|
||||
undefined,
|
||||
// This cast can be removed after merging the PR: https://github.com/vatesfr/xen-orchestra/pull/3209
|
||||
String(job.runId)
|
||||
)
|
||||
return this.updateJob({ id: job.id, runId: null })
|
||||
})
|
||||
)
|
||||
})
|
||||
// it sends a report for the interrupted backup jobs
|
||||
xo.on('plugins:registered', () =>
|
||||
asyncMap(this._jobs.get(), job => {
|
||||
// only the interrupted backup jobs have the runId property
|
||||
if (job.runId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
xo.emit(
|
||||
'job:terminated',
|
||||
undefined,
|
||||
job,
|
||||
undefined,
|
||||
// This cast can be removed after merging the PR: https://github.com/vatesfr/xen-orchestra/pull/3209
|
||||
String(job.runId)
|
||||
)
|
||||
return this.updateJob({ id: job.id, runId: null })
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
cancelJobRun (id: string) {
|
||||
@@ -293,8 +290,8 @@ export default class Jobs {
|
||||
runs[runJobId] = { cancel }
|
||||
|
||||
let session
|
||||
const app = this._app
|
||||
try {
|
||||
const app = this._app
|
||||
session = app.createUserConnection()
|
||||
session.set('user_id', job.userId)
|
||||
|
||||
@@ -319,11 +316,16 @@ export default class Jobs {
|
||||
|
||||
app.emit('job:terminated', status, job, schedule, runJobId)
|
||||
} catch (error) {
|
||||
logger.error(`The execution of ${id} has failed.`, {
|
||||
event: 'job.end',
|
||||
runJobId,
|
||||
error: serializeError(error),
|
||||
})
|
||||
await logger.error(
|
||||
`The execution of ${id} has failed.`,
|
||||
{
|
||||
event: 'job.end',
|
||||
runJobId,
|
||||
error: serializeError(error),
|
||||
},
|
||||
true
|
||||
)
|
||||
app.emit('job:terminated', undefined, job, schedule, runJobId)
|
||||
throw error
|
||||
} finally {
|
||||
;this.updateJob({ id, runId: null })::ignoreErrors()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Ajv from 'ajv'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
|
||||
import { PluginsMetadata } from '../models/plugin-metadata'
|
||||
import { invalidParameters, noSuchObject } from 'xo-common/api-errors'
|
||||
@@ -6,6 +7,8 @@ import { isFunction, mapToArray } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const log = createLogger('xo:xo-mixins:plugins')
|
||||
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
this._ajv = new Ajv({
|
||||
@@ -73,7 +76,7 @@ export default class {
|
||||
if (metadata !== undefined) {
|
||||
;({ autoload, configuration } = metadata)
|
||||
} else {
|
||||
console.log(`[NOTICE] register plugin ${name} for the first time`)
|
||||
log.info(`[NOTICE] register plugin ${name} for the first time`)
|
||||
await this._pluginsMetadata.save({
|
||||
id,
|
||||
autoload,
|
||||
|
||||
@@ -44,12 +44,12 @@ export default class {
|
||||
})
|
||||
}
|
||||
|
||||
async getRemoteHandler (remote, ignoreDisabled) {
|
||||
async getRemoteHandler (remote) {
|
||||
if (typeof remote === 'string') {
|
||||
remote = await this.getRemote(remote)
|
||||
}
|
||||
|
||||
if (!(ignoreDisabled || remote.enabled)) {
|
||||
if (!remote.enabled) {
|
||||
throw new Error('remote is disabled')
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default class {
|
||||
}
|
||||
|
||||
async testRemote (remote) {
|
||||
const handler = await this.getRemoteHandler(remote, true)
|
||||
const handler = await this.getRemoteHandler(remote)
|
||||
return handler.test()
|
||||
}
|
||||
|
||||
@@ -126,8 +126,11 @@ export default class {
|
||||
}
|
||||
|
||||
async removeRemote (id) {
|
||||
const handler = await this.getRemoteHandler(id, true)
|
||||
await handler.forget()
|
||||
const handler = this._handlers[id]
|
||||
if (handler !== undefined) {
|
||||
ignoreErrors.call(handler.forget())
|
||||
}
|
||||
|
||||
await this._remotes.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +333,9 @@ export default class {
|
||||
if (
|
||||
object.$type !== 'vm' ||
|
||||
object.is_a_snapshot ||
|
||||
('start' in object.blocked_operations &&
|
||||
(object.tags.includes('Disaster Recovery') ||
|
||||
object.tags.includes('Continuous Replication'))) ||
|
||||
// No set for this VM.
|
||||
!(id = xapi.xo.getData(object, 'resourceSet')) ||
|
||||
// Not our set.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { filter, includes } from 'lodash'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
import { hash, needsRehash, verify } from 'hashy'
|
||||
@@ -9,6 +10,8 @@ import { forEach, isEmpty, lightSet, mapToArray } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const log = createLogger('xo:xo-mixins:subjects')
|
||||
|
||||
const addToArraySet = (set, value) =>
|
||||
set && !includes(set, value) ? set.concat(value) : [value]
|
||||
const removeFromArraySet = (set, value) =>
|
||||
@@ -69,12 +72,7 @@ export default class {
|
||||
const password = 'admin'
|
||||
|
||||
await this.createUser({ email, password, permission: 'admin' })
|
||||
console.log(
|
||||
'[INFO] Default user created:',
|
||||
email,
|
||||
' with password',
|
||||
password
|
||||
)
|
||||
log.info(`Default user created: ${email} with password ${password}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
import { type Remote, getHandler } from '@xen-orchestra/fs'
|
||||
import { mergeVhd as mergeVhd_ } from 'vhd-lib'
|
||||
|
||||
// Use Bluebird for all promises as it provides better performance and
|
||||
// less memory usage.
|
||||
global.Promise = require('bluebird')
|
||||
|
||||
export function mergeVhd (
|
||||
parentRemote: Remote,
|
||||
parentPath: string,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
@@ -16,6 +17,8 @@ import { Servers } from '../models/server'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const log = createLogger('xo:xo-mixins:xen-servers')
|
||||
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
this._objectConflicts = { __proto__: null } // TODO: clean when a server is disconnected.
|
||||
@@ -42,10 +45,10 @@ export default class {
|
||||
for (const server of servers) {
|
||||
if (server.enabled) {
|
||||
this.connectXenServer(server.id).catch(error => {
|
||||
console.error(
|
||||
`[WARN] ${server.host}:`,
|
||||
error[0] || error.stack || error.code || error
|
||||
)
|
||||
log.warn('failed to connect to XenServer', {
|
||||
host: server.host,
|
||||
error,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -178,7 +181,7 @@ export default class {
|
||||
objects.set(xoId, xoObject)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ERROR: xapiObjectToXo', error)
|
||||
log.error('xapiObjectToXo', { error })
|
||||
|
||||
toRetry[xapiId] = xapiObject
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import XoCollection from 'xo-collection'
|
||||
import XoUniqueIndex from 'xo-collection/unique-index'
|
||||
import mixin from '@xen-orchestra/mixin'
|
||||
@@ -21,6 +22,8 @@ import { generateToken, noop } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const log = createLogger('xo:xo')
|
||||
|
||||
@mixin(mapToArray(mixins))
|
||||
export default class Xo extends EventEmitter {
|
||||
constructor (config) {
|
||||
@@ -151,7 +154,7 @@ export default class Xo extends EventEmitter {
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('HTTP request error', error.stack || error)
|
||||
log.error('HTTP request error', { error })
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.27.1",
|
||||
"version": "5.28.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -30,7 +30,6 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@julien-f/freactal": "0.4.0",
|
||||
"@nraynaud/novnc": "0.6.1",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/defined": "^0.0.0",
|
||||
@@ -98,6 +97,7 @@
|
||||
"promise-toolbox": "^0.10.1",
|
||||
"prop-types": "^15.6.0",
|
||||
"random-password": "^0.1.2",
|
||||
"reaclette": "^0.7.0",
|
||||
"react": "^15.4.1",
|
||||
"react-addons-shallow-compare": "^15.6.2",
|
||||
"react-addons-test-utils": "^15.6.2",
|
||||
@@ -136,7 +136,7 @@
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.3.0",
|
||||
"xo-common": "^0.1.1",
|
||||
"xo-common": "^0.1.2",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.5.0",
|
||||
"xo-vmdk-to-vhd": "^0.1.5"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ActionButton from 'action-button'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { cloneElement } from 'react'
|
||||
import { noop } from 'lodash'
|
||||
|
||||
@@ -28,12 +28,12 @@ export const Action = ({
|
||||
)
|
||||
|
||||
Action.propTypes = {
|
||||
display: propTypes.oneOf(['icon', 'both']),
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
label: propTypes.node,
|
||||
pending: propTypes.bool,
|
||||
redirectOnSuccess: propTypes.string,
|
||||
display: PropTypes.oneOf(['icon', 'both']),
|
||||
handler: PropTypes.func.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.node,
|
||||
pending: PropTypes.bool,
|
||||
redirectOnSuccess: PropTypes.string,
|
||||
}
|
||||
|
||||
const ActionBar = ({ children, handlerParam = noop, display = 'both' }) => (
|
||||
@@ -54,7 +54,7 @@ const ActionBar = ({ children, handlerParam = noop, display = 'both' }) => (
|
||||
)
|
||||
|
||||
ActionBar.propTypes = {
|
||||
display: propTypes.oneOf(['icon', 'both']),
|
||||
handlerParam: propTypes.any,
|
||||
display: PropTypes.oneOf(['icon', 'both']),
|
||||
handlerParam: PropTypes.any,
|
||||
}
|
||||
export { ActionBar as default }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { isFunction, startsWith } from 'lodash'
|
||||
|
||||
@@ -5,54 +6,55 @@ import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import logError from './log-error'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tooltip from './tooltip'
|
||||
import UserError from './user-error'
|
||||
import { error as _error } from './notification'
|
||||
|
||||
@propTypes({
|
||||
// React element to use as button content
|
||||
children: propTypes.node,
|
||||
|
||||
// whether this button is disabled (default to false)
|
||||
disabled: propTypes.bool,
|
||||
|
||||
// form identifier
|
||||
//
|
||||
// if provided, this button and its action are associated to this
|
||||
// form for the submit event
|
||||
form: propTypes.string,
|
||||
|
||||
// function to call when the action is triggered (via a clik on the
|
||||
// button or submit on the form)
|
||||
handler: propTypes.func.isRequired,
|
||||
|
||||
// optional value which will be passed as first param to the handler
|
||||
//
|
||||
// if you need multiple values, you can provide `data-*` props instead of
|
||||
// `handlerParam`
|
||||
handlerParam: propTypes.any,
|
||||
|
||||
// XO icon to use for this button
|
||||
icon: propTypes.string.isRequired,
|
||||
|
||||
// the color of the xo icon
|
||||
iconColor: propTypes.string,
|
||||
|
||||
// whether the action of this action is already underway
|
||||
pending: propTypes.bool,
|
||||
|
||||
// path to redirect to when the triggered action finish successfully
|
||||
//
|
||||
// if a function, it will be called with the result of the action to
|
||||
// compute the path
|
||||
redirectOnSuccess: propTypes.oneOfType([propTypes.func, propTypes.string]),
|
||||
|
||||
// React element to use tooltip for the component
|
||||
tooltip: propTypes.node,
|
||||
})
|
||||
export default class ActionButton extends Component {
|
||||
static contextTypes = {
|
||||
router: propTypes.object,
|
||||
router: PropTypes.object,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
// React element to use as button content
|
||||
children: PropTypes.node,
|
||||
|
||||
// whether this button is disabled (default to false)
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
// form identifier
|
||||
//
|
||||
// if provided, this button and its action are associated to this
|
||||
// form for the submit event
|
||||
form: PropTypes.string,
|
||||
|
||||
// function to call when the action is triggered (via a clik on the
|
||||
// button or submit on the form)
|
||||
handler: PropTypes.func.isRequired,
|
||||
|
||||
// optional value which will be passed as first param to the handler
|
||||
//
|
||||
// if you need multiple values, you can provide `data-*` props instead of
|
||||
// `handlerParam`
|
||||
handlerParam: PropTypes.any,
|
||||
|
||||
// XO icon to use for this button
|
||||
icon: PropTypes.string.isRequired,
|
||||
|
||||
// the color of the xo icon
|
||||
iconColor: PropTypes.string,
|
||||
|
||||
// whether the action of this action is already underway
|
||||
pending: PropTypes.bool,
|
||||
|
||||
// path to redirect to when the triggered action finish successfully
|
||||
//
|
||||
// if a function, it will be called with the result of the action to
|
||||
// compute the path
|
||||
redirectOnSuccess: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
|
||||
|
||||
// React element to use tooltip for the component
|
||||
tooltip: PropTypes.node,
|
||||
}
|
||||
|
||||
async _execute () {
|
||||
@@ -110,11 +112,15 @@ export default class ActionButton extends Component {
|
||||
|
||||
// ignore when undefined because it usually means that the action has been canceled
|
||||
if (error !== undefined) {
|
||||
logError(error)
|
||||
_error(
|
||||
children || tooltip || error.name,
|
||||
error.message || String(error)
|
||||
)
|
||||
if (error instanceof UserError) {
|
||||
_error(error.title, error.body)
|
||||
} else {
|
||||
logError(error)
|
||||
_error(
|
||||
children || tooltip || error.name,
|
||||
error.message || String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const ActionToggle = ({ className, value, ...props }) => (
|
||||
<ActionButton
|
||||
@@ -10,7 +10,8 @@ const ActionToggle = ({ className, value, ...props }) => (
|
||||
icon={value ? 'toggle-on' : 'toggle-off'}
|
||||
/>
|
||||
)
|
||||
ActionToggle.propTypes = {
|
||||
value: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default propTypes({
|
||||
value: propTypes.bool,
|
||||
})(ActionToggle)
|
||||
export { ActionToggle as default }
|
||||
|
||||
2
packages/xo-web/src/common/apply-decorators.js
Normal file
2
packages/xo-web/src/common/apply-decorators.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const apply = (value, fn) => fn(value)
|
||||
export default fns => fns.reduceRight(apply)
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from './button'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const ButtonLink = ({ to, ...props }, { router }) => {
|
||||
props.onClick = () => {
|
||||
@@ -12,17 +12,12 @@ const ButtonLink = ({ to, ...props }, { router }) => {
|
||||
return <Button {...props} />
|
||||
}
|
||||
|
||||
propTypes(
|
||||
{
|
||||
to: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.object,
|
||||
propTypes.string,
|
||||
]),
|
||||
},
|
||||
{
|
||||
router: routerShape,
|
||||
}
|
||||
)(ButtonLink)
|
||||
ButtonLink.contextTypes = {
|
||||
router: routerShape,
|
||||
}
|
||||
|
||||
ButtonLink.propTypes = {
|
||||
to: PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.string]),
|
||||
}
|
||||
|
||||
export { ButtonLink as default }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const Button = ({
|
||||
active,
|
||||
@@ -27,9 +26,9 @@ const Button = ({
|
||||
return <button {...props}>{children}</button>
|
||||
}
|
||||
|
||||
propTypes({
|
||||
active: propTypes.bool,
|
||||
block: propTypes.bool,
|
||||
Button.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
block: PropTypes.bool,
|
||||
|
||||
// Bootstrap button style
|
||||
//
|
||||
@@ -37,7 +36,7 @@ propTypes({
|
||||
//
|
||||
// The default value (secondary) is not listed here because it does
|
||||
// not make sense to explicit it.
|
||||
btnStyle: propTypes.oneOf([
|
||||
btnStyle: PropTypes.oneOf([
|
||||
'danger',
|
||||
'info',
|
||||
'link',
|
||||
@@ -46,8 +45,8 @@ propTypes({
|
||||
'warning',
|
||||
]),
|
||||
|
||||
outline: propTypes.bool,
|
||||
size: propTypes.oneOf(['large', 'small']),
|
||||
})(Button)
|
||||
outline: PropTypes.bool,
|
||||
size: PropTypes.oneOf(['large', 'small']),
|
||||
}
|
||||
|
||||
export { Button as default }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const CARD_STYLE = {
|
||||
minHeight: '100%',
|
||||
@@ -16,9 +15,7 @@ const CARD_HEADER_STYLE = {
|
||||
textAlign: 'center',
|
||||
}
|
||||
|
||||
export const Card = propTypes({
|
||||
shadow: propTypes.bool,
|
||||
})(({ shadow, ...props }) => {
|
||||
export const Card = ({ shadow, ...props }) => {
|
||||
props.className = 'card'
|
||||
props.style = {
|
||||
...props.style,
|
||||
@@ -26,18 +23,26 @@ export const Card = propTypes({
|
||||
}
|
||||
|
||||
return <div {...props} />
|
||||
})
|
||||
}
|
||||
|
||||
export const CardHeader = propTypes({
|
||||
className: propTypes.string,
|
||||
})(({ children, className }) => (
|
||||
Card.propTypes = {
|
||||
shadow: PropTypes.bool,
|
||||
}
|
||||
|
||||
export const CardHeader = ({ children, className }) => (
|
||||
<h4 className={`card-header ${className || ''}`} style={CARD_HEADER_STYLE}>
|
||||
{children}
|
||||
</h4>
|
||||
))
|
||||
)
|
||||
|
||||
export const CardBlock = propTypes({
|
||||
className: propTypes.string,
|
||||
})(({ children, className }) => (
|
||||
CardHeader.propTypes = {
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
export const CardBlock = ({ children, className }) => (
|
||||
<div className={`card-block ${className || ''}`}>{children}</div>
|
||||
))
|
||||
)
|
||||
|
||||
CardBlock.propTypes = {
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
@propTypes({
|
||||
buttonText: propTypes.any.isRequired,
|
||||
children: propTypes.any.isRequired,
|
||||
className: propTypes.string,
|
||||
defaultOpen: propTypes.bool,
|
||||
size: propTypes.string,
|
||||
})
|
||||
export default class Collapse extends Component {
|
||||
static propTypes = {
|
||||
buttonText: PropTypes.any.isRequired,
|
||||
children: PropTypes.any.isRequired,
|
||||
className: PropTypes.string,
|
||||
defaultOpen: PropTypes.bool,
|
||||
size: PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
isOpened: this.props.defaultOpen,
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { isEmpty, map } from 'lodash'
|
||||
import { DropdownButton, MenuItem } from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
@uncontrollableInput({
|
||||
defaultValue: '',
|
||||
})
|
||||
@propTypes({
|
||||
disabled: propTypes.bool,
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.string),
|
||||
propTypes.objectOf(propTypes.string),
|
||||
]),
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.string.isRequired,
|
||||
})
|
||||
export default class Combobox extends Component {
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
options: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
PropTypes.objectOf(PropTypes.string),
|
||||
]),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
this.props.onChange(event.target.value)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import classNames from 'classnames'
|
||||
import React, { createElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import _ from '../intl'
|
||||
import Button from '../button'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const Copiable = propTypes({
|
||||
data: propTypes.string,
|
||||
tagName: propTypes.string,
|
||||
})(({ className, tagName = 'span', ...props }) =>
|
||||
const Copiable = ({ className, tagName = 'span', ...props }) =>
|
||||
createElement(
|
||||
tagName,
|
||||
{
|
||||
@@ -30,5 +27,10 @@ const Copiable = propTypes({
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
)
|
||||
)
|
||||
|
||||
Copiable.propTypes = {
|
||||
data: PropTypes.string,
|
||||
tagName: PropTypes.string,
|
||||
}
|
||||
|
||||
export { Copiable as default }
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import Component from 'base-component'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import ReactDropzone from 'react-dropzone'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
onDrop: propTypes.func,
|
||||
message: propTypes.node,
|
||||
multiple: propTypes.bool,
|
||||
})
|
||||
export default class Dropzone extends Component {
|
||||
static propTypes = {
|
||||
onDrop: PropTypes.func,
|
||||
message: PropTypes.node,
|
||||
multiple: PropTypes.bool,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { onDrop, message, multiple } = this.props
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
findKey,
|
||||
isEmpty,
|
||||
@@ -15,7 +16,6 @@ import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import Icon from '../icon'
|
||||
import logError from '../log-error'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import Tooltip from '../tooltip'
|
||||
import { formatSize } from '../utils'
|
||||
import { SizeInput } from '../form'
|
||||
@@ -38,10 +38,11 @@ import styles from './index.css'
|
||||
|
||||
const LONG_CLICK = 400
|
||||
|
||||
@propTypes({
|
||||
alt: propTypes.node.isRequired,
|
||||
})
|
||||
class Hover extends Component {
|
||||
static propTypes = {
|
||||
alt: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
@@ -64,13 +65,14 @@ class Hover extends Component {
|
||||
|
||||
// it supports 'data-*': optional params,
|
||||
// wich will be passed as an object to the 'onChange' and the 'onUndo' functions
|
||||
@propTypes({
|
||||
onChange: propTypes.func.isRequired,
|
||||
onUndo: propTypes.oneOfType([propTypes.bool, propTypes.func]),
|
||||
useLongClick: propTypes.bool,
|
||||
value: propTypes.any.isRequired,
|
||||
})
|
||||
class Editable extends Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onUndo: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
||||
useLongClick: PropTypes.bool,
|
||||
value: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
get value () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
@@ -221,14 +223,15 @@ class Editable extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
autoComplete: propTypes.string,
|
||||
maxLength: propTypes.number,
|
||||
minLength: propTypes.number,
|
||||
pattern: propTypes.string,
|
||||
value: propTypes.string.isRequired,
|
||||
})
|
||||
export class Text extends Editable {
|
||||
static propTypes = {
|
||||
autoComplete: PropTypes.string,
|
||||
maxLength: PropTypes.number,
|
||||
minLength: PropTypes.number,
|
||||
pattern: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { input } = this.refs
|
||||
|
||||
@@ -302,11 +305,12 @@ export class Password extends Text {
|
||||
_isPassword = true
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
nullable: propTypes.bool,
|
||||
value: propTypes.number,
|
||||
})
|
||||
export class Number extends Component {
|
||||
static propTypes = {
|
||||
nullable: PropTypes.bool,
|
||||
value: PropTypes.number,
|
||||
}
|
||||
|
||||
get value () {
|
||||
return +this.refs.input.value
|
||||
}
|
||||
@@ -337,11 +341,13 @@ export class Number extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
options: propTypes.oneOfType([propTypes.array, propTypes.object]).isRequired,
|
||||
renderer: propTypes.func,
|
||||
})
|
||||
export class Select extends Editable {
|
||||
static propTypes = {
|
||||
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
.isRequired,
|
||||
renderer: PropTypes.func,
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (
|
||||
props.value !== this.props.value ||
|
||||
@@ -418,10 +424,11 @@ const MAP_TYPE_SELECT = {
|
||||
'VM-template': SelectVmTemplate,
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.object]),
|
||||
})
|
||||
export class XoSelect extends Editable {
|
||||
static propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state.value
|
||||
}
|
||||
@@ -461,10 +468,11 @@ export class XoSelect extends Editable {
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
value: propTypes.number.isRequired,
|
||||
})
|
||||
export class Size extends Editable {
|
||||
static propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import * as Grid from './grid'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
export const LabelCol = propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
})(({ children }) => (
|
||||
export const LabelCol = ({ children }) => (
|
||||
<label className='col-md-2 form-control-label'>{children}</label>
|
||||
))
|
||||
)
|
||||
|
||||
export const InputCol = propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
})(({ children }) => <Grid.Col mediumSize={10}>{children}</Grid.Col>)
|
||||
LabelCol.propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
export const Row = propTypes({
|
||||
children: propTypes.arrayOf(propTypes.element).isRequired,
|
||||
})(({ children }) => <Grid.Row className='form-group'>{children}</Grid.Row>)
|
||||
export const InputCol = ({ children }) => (
|
||||
<Grid.Col mediumSize={10}>{children}</Grid.Col>
|
||||
)
|
||||
|
||||
InputCol.propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
export const Row = ({ children }) => (
|
||||
<Grid.Row className='form-group'>{children}</Grid.Row>
|
||||
)
|
||||
|
||||
Row.propTypes = {
|
||||
children: PropTypes.arrayOf(PropTypes.element).isRequired,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import classNames from 'classnames'
|
||||
import defined from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import PropTypes from 'prop-types'
|
||||
import randomPassword from 'random-password'
|
||||
import React from 'react'
|
||||
import round from 'lodash/round'
|
||||
@@ -13,7 +14,6 @@ import { DropdownButton, MenuItem } from 'react-bootstrap-4/lib'
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { formatSizeRaw, parseSize } from '../utils'
|
||||
|
||||
export Number from './number'
|
||||
@@ -21,10 +21,11 @@ export Select from './select'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
enableGenerator: propTypes.bool,
|
||||
})
|
||||
export class Password extends Component {
|
||||
static propTypes = {
|
||||
enableGenerator: PropTypes.bool,
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.refs.field.value
|
||||
}
|
||||
@@ -86,19 +87,21 @@ export class Password extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
max: propTypes.number.isRequired,
|
||||
min: propTypes.number.isRequired,
|
||||
onChange: propTypes.func,
|
||||
step: propTypes.number,
|
||||
value: propTypes.number,
|
||||
})
|
||||
export class Range extends Component {
|
||||
componentDidMount () {
|
||||
const { min, onChange, value } = this.props
|
||||
static propTypes = {
|
||||
max: PropTypes.number.isRequired,
|
||||
min: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
required: PropTypes.bool,
|
||||
step: PropTypes.number,
|
||||
value: PropTypes.number,
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
onChange && onChange(min)
|
||||
componentDidMount () {
|
||||
const { min, onChange, required, value } = this.props
|
||||
|
||||
if (value === undefined && required) {
|
||||
onChange !== undefined && onChange(min)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +114,7 @@ export class Range extends Component {
|
||||
<Container>
|
||||
<SingleLineRow>
|
||||
<Col size={2}>
|
||||
<span className='pull-right'>{value}</span>
|
||||
{value !== undefined && <span className='pull-right'>{value}</span>}
|
||||
</Col>
|
||||
<Col size={10}>
|
||||
<input
|
||||
@@ -121,7 +124,7 @@ export class Range extends Component {
|
||||
onChange={this._onChange}
|
||||
step={step}
|
||||
type='range'
|
||||
value={value}
|
||||
value={value !== undefined ? value : min}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
@@ -134,19 +137,21 @@ export Toggle from './toggle'
|
||||
|
||||
const UNITS = ['kiB', 'MiB', 'GiB']
|
||||
const DEFAULT_UNIT = 'GiB'
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
className: propTypes.string,
|
||||
defaultUnit: propTypes.oneOf(UNITS),
|
||||
defaultValue: propTypes.number,
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.string,
|
||||
readOnly: propTypes.bool,
|
||||
required: propTypes.bool,
|
||||
style: propTypes.object,
|
||||
value: propTypes.oneOfType([propTypes.number, propTypes.oneOf([null])]),
|
||||
})
|
||||
|
||||
export class SizeInput extends BaseComponent {
|
||||
static propTypes = {
|
||||
autoFocus: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
defaultUnit: PropTypes.oneOf(UNITS),
|
||||
defaultValue: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf([null])]),
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { injectState, provideState } from '@julien-f/freactal'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
|
||||
const Number_ = [
|
||||
import decorate from '../apply-decorators'
|
||||
|
||||
const Number_ = decorate([
|
||||
provideState({
|
||||
effects: {
|
||||
onChange: (_, { target: { value } }) => (state, props) => {
|
||||
@@ -30,7 +32,7 @@ const Number_ = [
|
||||
value={value === undefined ? '' : String(value)}
|
||||
/>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
])
|
||||
|
||||
Number_.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
@uncontrollableInput()
|
||||
@propTypes({
|
||||
className: propTypes.string,
|
||||
onChange: propTypes.func.isRequired,
|
||||
icon: propTypes.string,
|
||||
iconOn: propTypes.string,
|
||||
iconOff: propTypes.string,
|
||||
iconSize: propTypes.number,
|
||||
value: propTypes.bool.isRequired,
|
||||
})
|
||||
export default class Toggle extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
icon: PropTypes.string,
|
||||
iconOn: PropTypes.string,
|
||||
iconOff: PropTypes.string,
|
||||
iconSize: PropTypes.number,
|
||||
value: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
iconOn: 'toggle-on',
|
||||
iconOff: 'toggle-off',
|
||||
|
||||
@@ -1,61 +1,64 @@
|
||||
import classNames from 'classnames'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// A column can contain content or a row.
|
||||
export const Col = propTypes({
|
||||
className: propTypes.string,
|
||||
size: propTypes.number,
|
||||
smallSize: propTypes.number,
|
||||
mediumSize: propTypes.number,
|
||||
largeSize: propTypes.number,
|
||||
offset: propTypes.number,
|
||||
smallOffset: propTypes.number,
|
||||
mediumOffset: propTypes.number,
|
||||
largeOffset: propTypes.number,
|
||||
})(
|
||||
({
|
||||
children,
|
||||
className,
|
||||
size = 12,
|
||||
smallSize = size,
|
||||
mediumSize,
|
||||
largeSize,
|
||||
offset,
|
||||
smallOffset = offset,
|
||||
mediumOffset,
|
||||
largeOffset,
|
||||
style,
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
smallSize && `col-xs-${smallSize}`,
|
||||
mediumSize && `col-md-${mediumSize}`,
|
||||
largeSize && `col-lg-${largeSize}`,
|
||||
smallOffset && `offset-xs-${smallOffset}`,
|
||||
mediumOffset && `offset-md-${mediumOffset}`,
|
||||
largeOffset && `offset-lg-${largeOffset}`
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
export const Col = ({
|
||||
children,
|
||||
className,
|
||||
size = 12,
|
||||
smallSize = size,
|
||||
mediumSize,
|
||||
largeSize,
|
||||
offset,
|
||||
smallOffset = offset,
|
||||
mediumOffset,
|
||||
largeOffset,
|
||||
style,
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
smallSize && `col-xs-${smallSize}`,
|
||||
mediumSize && `col-md-${mediumSize}`,
|
||||
largeSize && `col-lg-${largeSize}`,
|
||||
smallOffset && `offset-xs-${smallOffset}`,
|
||||
mediumOffset && `offset-md-${mediumOffset}`,
|
||||
largeOffset && `offset-lg-${largeOffset}`
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
Col.propTypes = {
|
||||
className: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
smallSize: PropTypes.number,
|
||||
mediumSize: PropTypes.number,
|
||||
largeSize: PropTypes.number,
|
||||
offset: PropTypes.number,
|
||||
smallOffset: PropTypes.number,
|
||||
mediumOffset: PropTypes.number,
|
||||
largeOffset: PropTypes.number,
|
||||
}
|
||||
|
||||
// This is the root component of the grid layout, containers should not be
|
||||
// nested.
|
||||
export const Container = propTypes({
|
||||
className: propTypes.string,
|
||||
})(({ children, className }) => (
|
||||
export const Container = ({ children, className }) => (
|
||||
<div className={classNames(className, 'container-fluid')}>{children}</div>
|
||||
))
|
||||
)
|
||||
|
||||
Container.propTypes = {
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
// Only columns can be children of a row.
|
||||
export const Row = propTypes({
|
||||
className: propTypes.string,
|
||||
})(({ children, className }) => (
|
||||
export const Row = ({ children, className }) => (
|
||||
<div className={`${className || ''} row`}>{children}</div>
|
||||
))
|
||||
)
|
||||
|
||||
Row.propTypes = {
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tags from './tags'
|
||||
|
||||
@propTypes({
|
||||
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
onAdd: propTypes.func,
|
||||
onChange: propTypes.func,
|
||||
onDelete: propTypes.func,
|
||||
type: propTypes.string,
|
||||
})
|
||||
export default class HomeTags extends Component {
|
||||
static propTypes = {
|
||||
labels: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onAdd: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
type: PropTypes.string,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
router: PropTypes.object,
|
||||
}
|
||||
|
||||
_onClick = label => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-overlays'
|
||||
import { forEach, isEmpty, keys, map } from 'lodash'
|
||||
@@ -6,7 +7,6 @@ import _ from './intl'
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import Link from './link'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import SortedTable from './sorted-table'
|
||||
import TabButton from './tab-button'
|
||||
import { connectStore } from './utils'
|
||||
@@ -203,21 +203,22 @@ class HostsPatchesTableByPool extends Component {
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
const HostsPatches = props =>
|
||||
props.displayPools ? (
|
||||
<HostsPatchesTableByPool {...props} />
|
||||
) : (
|
||||
<HostsPatchesTable {...props} />
|
||||
)
|
||||
|
||||
export default propTypes({
|
||||
buttonsGroupContainer: propTypes.func.isRequired,
|
||||
container: propTypes.any,
|
||||
displayPools: propTypes.bool,
|
||||
hosts: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.object),
|
||||
propTypes.objectOf(propTypes.object),
|
||||
HostsPatches.propTypes = {
|
||||
buttonsGroupContainer: PropTypes.func.isRequired,
|
||||
container: PropTypes.any,
|
||||
displayPools: PropTypes.bool,
|
||||
hosts: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.object),
|
||||
PropTypes.objectOf(PropTypes.object),
|
||||
]).isRequired,
|
||||
useTabButton: propTypes.bool,
|
||||
})(
|
||||
props =>
|
||||
props.displayPools ? (
|
||||
<HostsPatchesTableByPool {...props} />
|
||||
) : (
|
||||
<HostsPatchesTable {...props} />
|
||||
)
|
||||
)
|
||||
useTabButton: PropTypes.bool,
|
||||
}
|
||||
|
||||
export { HostsPatches as default }
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import classNames from 'classnames'
|
||||
import isInteger from 'lodash/isInteger'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const Icon = ({ icon, size = 1, color, fixedWidth, ...props }) => {
|
||||
props.className = classNames(
|
||||
props.className,
|
||||
@@ -15,10 +14,12 @@ const Icon = ({ icon, size = 1, color, fixedWidth, ...props }) => {
|
||||
|
||||
return <i {...props} />
|
||||
}
|
||||
propTypes(Icon)({
|
||||
color: propTypes.string,
|
||||
fixedWidth: propTypes.bool,
|
||||
icon: propTypes.string,
|
||||
size: propTypes.oneOfType([propTypes.string, propTypes.number]),
|
||||
})
|
||||
export default Icon
|
||||
|
||||
Icon.propTypes = {
|
||||
color: PropTypes.string,
|
||||
fixedWidth: PropTypes.bool,
|
||||
icon: PropTypes.string,
|
||||
size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
}
|
||||
|
||||
export { Icon as default }
|
||||
|
||||
@@ -198,9 +198,14 @@ const messages = {
|
||||
stateEnabled: 'Enabled',
|
||||
|
||||
// ----- Labels -----
|
||||
labelBackup: 'Backup',
|
||||
labelMerge: 'Merge',
|
||||
labelRestore: 'Restore',
|
||||
labelSize: 'Size',
|
||||
labelSpeed: 'Speed',
|
||||
labelSr: 'SR',
|
||||
labelTransfer: 'Transfer',
|
||||
labelVm: 'VM',
|
||||
|
||||
// ----- Forms -----
|
||||
formCancel: 'Cancel',
|
||||
@@ -251,19 +256,11 @@ const messages = {
|
||||
// --- Dates/Scheduler ---
|
||||
|
||||
schedulingMonth: 'Month',
|
||||
schedulingEveryNMonth: 'Every N month',
|
||||
schedulingEachSelectedMonth: 'Each selected month',
|
||||
schedulingDay: 'Day',
|
||||
schedulingEveryNDay: 'Every N day',
|
||||
schedulingEachSelectedDay: 'Each selected day',
|
||||
schedulingSetWeekDayMode: 'Switch to week days',
|
||||
schedulingSetMonthDayMode: 'Switch to month days',
|
||||
schedulingHour: 'Hour',
|
||||
schedulingEachSelectedHour: 'Each selected hour',
|
||||
schedulingEveryNHour: 'Every N hour',
|
||||
schedulingMinute: 'Minute',
|
||||
schedulingEachSelectedMinute: 'Each selected minute',
|
||||
schedulingEveryNMinute: 'Every N minute',
|
||||
selectTableAllMonth: 'Every month',
|
||||
selectTableAllDay: 'Every day',
|
||||
selectTableAllHour: 'Every hour',
|
||||
@@ -358,6 +355,9 @@ const messages = {
|
||||
createRemoteMessage:
|
||||
'No remotes found, please click on the remotes settings button to create one!',
|
||||
remotesSettings: 'Remotes settings',
|
||||
pluginsSettings: 'Plugins settings',
|
||||
pluginsWarning:
|
||||
'To receive the report, the plugins backup-reports and transport-email need to be loaded.',
|
||||
scheduleAdd: 'Add a schedule',
|
||||
scheduleDelete: 'Delete',
|
||||
scheduleRun: 'Run schedule',
|
||||
@@ -389,6 +389,7 @@ const messages = {
|
||||
|
||||
// ------ New backup -----
|
||||
newBackupAdvancedSettings: 'Advanced settings',
|
||||
newBackupSettings: 'Settings',
|
||||
reportWhenAlways: 'Always',
|
||||
reportWhenFailure: 'Failure',
|
||||
reportWhenNever: 'Never',
|
||||
@@ -405,7 +406,8 @@ const messages = {
|
||||
offlineSnapshot: 'Offline snapshot',
|
||||
offlineSnapshotInfo: 'Shutdown VMs before snapshotting them',
|
||||
timeout: 'Timeout',
|
||||
timeoutInfo: 'Number of seconds after which a job is considered failed',
|
||||
timeoutInfo: 'Number of hours after which a job is considered failed',
|
||||
timeoutUnit: 'in hours',
|
||||
dbAndDrRequireEntreprisePlan: 'Delta Backup and DR require Entreprise plan',
|
||||
crRequiresPremiumPlan: 'CR requires Premium plan',
|
||||
smartBackupModeTitle: 'Smart mode',
|
||||
@@ -543,7 +545,11 @@ const messages = {
|
||||
deleteGroupConfirm: 'Are you sure you want to delete this group?',
|
||||
removeUserFromGroup: 'Remove user from Group',
|
||||
deleteUserConfirm: 'Are you sure you want to delete this user?',
|
||||
deleteUser: 'Delete User',
|
||||
deleteUser: 'Delete user',
|
||||
deleteSelectedUsers: 'Delete selected users',
|
||||
deleteUsersModalTitle: 'Delete user{nUsers, plural, one {} other {s}}',
|
||||
deleteUsersModalMessage:
|
||||
'Are you sure you want to delete {nUsers, number} user{nUsers, plural, one {} other {s}}?',
|
||||
noUser: 'no user',
|
||||
unknownUser: 'unknown user',
|
||||
noGroupFound: 'No group found',
|
||||
@@ -562,6 +568,11 @@ const messages = {
|
||||
noUserInGroup: 'No user in group',
|
||||
countUsers: '{users, number} user{users, plural, one {} other {s}}',
|
||||
selectPermission: 'Select Permission',
|
||||
deleteAcl: 'Delete ACL',
|
||||
deleteSelectedAcls: 'Delete selected ACLs',
|
||||
deleteAclsModalTitle: 'Delete ACL{nAcls, plural, one {} other {s}}',
|
||||
deleteAclsModalMessage:
|
||||
'Are you sure you want to delete {nAcls, number} ACL{nAcls, plural, one {} other {s}}?',
|
||||
|
||||
// ----- Plugins ------
|
||||
noPlugins: 'No plugins found',
|
||||
@@ -1175,6 +1186,7 @@ const messages = {
|
||||
newVmPerfPanel: 'Performances',
|
||||
newVmVcpusLabel: 'vCPUs',
|
||||
newVmRamLabel: 'RAM',
|
||||
newVmRamWarning: 'The memory is below the threshold ({threshold})',
|
||||
newVmStaticMaxLabel: 'Static memory max',
|
||||
newVmDynamicMinLabel: 'Dynamic memory min',
|
||||
newVmDynamicMaxLabel: 'Dynamic memory max',
|
||||
@@ -1369,6 +1381,7 @@ const messages = {
|
||||
'Are you sure you want to delete all the backups from {nVms, number} VM{nVms, plural, one {} other {s}}?',
|
||||
deleteVmBackupsBulkConfirmText:
|
||||
'delete {nBackups} backup{nBackups, plural, one {} other {s}}',
|
||||
unknownJob: 'Unknown job',
|
||||
|
||||
// ----- Restore files view -----
|
||||
listRemoteBackups: 'List remote backups',
|
||||
@@ -1537,6 +1550,18 @@ const messages = {
|
||||
destroyTasksModalTitle: 'Destroy task{nTasks, plural, one {} other {s}}',
|
||||
destroyTasksModalMessage:
|
||||
'Are you sure you want to destroy {nTasks, number} task{nTasks, plural, one {} other {s}}?',
|
||||
forgetHostFromSrModalTitle: 'Forget host',
|
||||
forgetHostFromSrModalMessage:
|
||||
'Are you sure you want to forget this host? This will disconnect the SR from the host by removing the link between them (PBD).',
|
||||
forgetHostsFromSrModalTitle: 'Forget host{nPbds, plural, one {} other {s}}',
|
||||
forgetHostsFromSrModalMessage:
|
||||
'Are you sure you want to forget {nPbds, number} host{nPbds, plural, one {} other {s}}? This will disconnect the SR from these hosts by removing the links between the SR and the hosts (PBDs).',
|
||||
forgetSrFromHostModalTitle: 'Forget SR',
|
||||
forgetSrFromHostModalMessage:
|
||||
'Are you sure you want to forget this SR? This will disconnect the SR from the host by removing the link between them (PBD).',
|
||||
forgetSrsFromHostModalTitle: 'Forget SR{nPbds, plural, one {} other {s}}',
|
||||
forgetSrsFromHostModalMessage:
|
||||
'Are you sure you want to forget {nPbds, number} SR{nPbds, plural, one {} other {s}}? This will disconnect the SRs from the host by removing the links between the host and the SRs (PBDs).',
|
||||
|
||||
// ----- Servers -----
|
||||
serverLabel: 'Label',
|
||||
@@ -1775,12 +1800,15 @@ const messages = {
|
||||
logNoParams: 'No params',
|
||||
logDelete: 'Delete log',
|
||||
logsDelete: 'Delete logs',
|
||||
logsThreePerPage: '3 / page',
|
||||
logsTenPerPage: '10 / page',
|
||||
logsJobId: 'Job ID',
|
||||
logsJobName: 'Job name',
|
||||
logsJobTime: 'Job time',
|
||||
logsVmNotFound: 'VM not found!',
|
||||
logDeleteMultiple: 'Delete log{nLogs, plural, one {} other {s}}',
|
||||
logDeleteMultipleMessage:
|
||||
'Are you sure you want to delete {nLogs, number} log{nLogs, plural, one {} other {s}}?',
|
||||
logDeleteAll: 'Delete all logs',
|
||||
logDeleteAllTitle: 'Delete all logs',
|
||||
logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
|
||||
logIndicationToEnable: 'Click to enable',
|
||||
logIndicationToDisable: 'Click to disable',
|
||||
reportBug: 'Report a bug',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import _ from 'intl'
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import Icon from 'icon'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert } from 'modal'
|
||||
import { isAdmin } from 'selectors'
|
||||
@@ -18,9 +18,6 @@ import {
|
||||
createSelector,
|
||||
} from './selectors'
|
||||
|
||||
@propTypes({
|
||||
vm: propTypes.object.isRequired,
|
||||
})
|
||||
@addSubscriptions({
|
||||
resourceSets: subscribeResourceSets,
|
||||
})
|
||||
@@ -44,6 +41,10 @@ import {
|
||||
}
|
||||
})
|
||||
export default class IsoDevice extends Component {
|
||||
static propTypes = {
|
||||
vm: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
_getPredicate = createSelector(
|
||||
() => this.props.vm.$pool,
|
||||
() => this.props.vm.$container,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { filter, map } from 'lodash'
|
||||
@@ -5,22 +6,22 @@ import { filter, map } from 'lodash'
|
||||
import _ from '../intl'
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { EMPTY_ARRAY } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class ObjectInput extends Component {
|
||||
static propTypes = {
|
||||
depth: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
label: PropTypes.any.isRequired,
|
||||
required: PropTypes.bool,
|
||||
schema: PropTypes.object.isRequired,
|
||||
uiSchema: PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
use: this.props.required || forceDisplayOptionalAttr(this.props),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
@@ -28,16 +28,17 @@ const InputByType = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class GenericInput extends Component {
|
||||
static propTypes = {
|
||||
depth: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
label: PropTypes.any.isRequired,
|
||||
required: PropTypes.bool,
|
||||
schema: PropTypes.object.isRequired,
|
||||
uiSchema: PropTypes.object,
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
const { name, onChange } = this.props
|
||||
onChange && onChange(getEventValue(event), name)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { createSelector } from 'reselect'
|
||||
@@ -5,22 +6,22 @@ import { keyBy, map } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class ObjectInput extends Component {
|
||||
static propTypes = {
|
||||
depth: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
label: PropTypes.any.isRequired,
|
||||
required: PropTypes.bool,
|
||||
schema: PropTypes.object.isRequired,
|
||||
uiSchema: PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
use: this.props.required || forceDisplayOptionalAttr(this.props),
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
password: propTypes.bool,
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class StringInput extends Component {
|
||||
static propTypes = {
|
||||
password: PropTypes.bool,
|
||||
}
|
||||
|
||||
// the value of this input is undefined not '' when empty to make
|
||||
// it homogenous with when the user has never touched this input
|
||||
_onChange = event => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Link from 'react-router/lib/Link'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -18,11 +18,12 @@ const _IGNORED_TAGNAMES = {
|
||||
SELECT: true,
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
className: propTypes.string,
|
||||
tagName: propTypes.string,
|
||||
})
|
||||
export class BlockLink extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
tagName: PropTypes.string,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
router: routerShape,
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import isArray from 'lodash/isArray'
|
||||
import isString from 'lodash/isString'
|
||||
import map from 'lodash/map'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import { createSelector } from 'selectors'
|
||||
import { identity, isArray, isString, map } from 'lodash'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Modal as ReactModal } from 'react-bootstrap-4/lib'
|
||||
|
||||
import _, { messages } from './intl'
|
||||
import BaseComponent from './base-component'
|
||||
import ActionButton from './action-button'
|
||||
import Button from './button'
|
||||
import decorate from './apply-decorators'
|
||||
import getEventValue from './get-event-value'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tooltip from './tooltip'
|
||||
import { generateRandomId } from './utils'
|
||||
import {
|
||||
@@ -46,21 +47,22 @@ const _addRef = (component, ref) => {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@propTypes({
|
||||
buttons: propTypes.arrayOf(
|
||||
propTypes.shape({
|
||||
btnStyle: propTypes.string,
|
||||
icon: propTypes.string,
|
||||
label: propTypes.node.isRequired,
|
||||
tooltip: propTypes.node,
|
||||
value: propTypes.any,
|
||||
})
|
||||
).isRequired,
|
||||
children: propTypes.node.isRequired,
|
||||
icon: propTypes.string,
|
||||
title: propTypes.node.isRequired,
|
||||
})
|
||||
class GenericModal extends Component {
|
||||
static propTypes = {
|
||||
buttons: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
btnStyle: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
label: PropTypes.node.isRequired,
|
||||
tooltip: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
})
|
||||
).isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
icon: PropTypes.string,
|
||||
title: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
_getBodyValue = () => {
|
||||
const { body } = this.refs
|
||||
if (body !== undefined) {
|
||||
@@ -143,16 +145,17 @@ export const chooseAction = ({ body, buttons, icon, title }) => {
|
||||
})
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
body: propTypes.node,
|
||||
strongConfirm: propTypes.object.isRequired,
|
||||
icon: propTypes.string,
|
||||
reject: propTypes.func,
|
||||
resolve: propTypes.func,
|
||||
title: propTypes.node.isRequired,
|
||||
})
|
||||
@injectIntl
|
||||
class StrongConfirm extends Component {
|
||||
static propTypes = {
|
||||
body: PropTypes.node,
|
||||
strongConfirm: PropTypes.object.isRequired,
|
||||
icon: PropTypes.string,
|
||||
reject: PropTypes.func,
|
||||
resolve: PropTypes.func,
|
||||
title: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
buttons: [{ btnStyle: 'danger', label: _('confirmOk'), disabled: true }],
|
||||
}
|
||||
@@ -259,57 +262,127 @@ export const confirm = ({ body, icon = 'alarm', title, strongConfirm }) =>
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const preventDefault = event => event.preventDefault()
|
||||
|
||||
class FormModal extends BaseComponent {
|
||||
state = {
|
||||
value: this.props.defaultValue,
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { body, formId } = this.props
|
||||
return (
|
||||
<form id={formId} onSubmit={preventDefault}>
|
||||
{cloneElement(body, {
|
||||
value: this.state.value,
|
||||
onChange: this.linkState('value'),
|
||||
})}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const form = ({ body, defaultValue, icon, title, size }) => {
|
||||
const formId = generateRandomId()
|
||||
const buttons = [
|
||||
{
|
||||
btnStyle: 'primary',
|
||||
label: _('formOk'),
|
||||
form: formId,
|
||||
},
|
||||
]
|
||||
return new Promise((resolve, reject) => {
|
||||
modal(
|
||||
<GenericModal
|
||||
buttons={buttons}
|
||||
icon={icon}
|
||||
reject={reject}
|
||||
resolve={resolve}
|
||||
title={title}
|
||||
>
|
||||
<FormModal body={body} defaultValue={defaultValue} formId={formId} />
|
||||
</GenericModal>,
|
||||
reject,
|
||||
{
|
||||
bsSize: size,
|
||||
}
|
||||
)
|
||||
let formModalState
|
||||
export const form = ({
|
||||
component,
|
||||
defaultValue,
|
||||
handler = identity,
|
||||
header,
|
||||
render,
|
||||
size,
|
||||
}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
formModalState.component = component
|
||||
formModalState.handler = handler
|
||||
formModalState.header = header
|
||||
formModalState.opened = true
|
||||
formModalState.reject = reject
|
||||
formModalState.render = render
|
||||
formModalState.resolve = resolve
|
||||
formModalState.size = size
|
||||
formModalState.value = defaultValue
|
||||
disableShortcuts()
|
||||
})
|
||||
}
|
||||
|
||||
const getInitialState = () => ({
|
||||
component: undefined,
|
||||
handler: undefined,
|
||||
header: undefined,
|
||||
isHandlerRunning: false,
|
||||
opened: false,
|
||||
reject: undefined,
|
||||
render: undefined,
|
||||
resolve: undefined,
|
||||
size: undefined,
|
||||
value: undefined,
|
||||
})
|
||||
export const FormModal = decorate([
|
||||
provideState({
|
||||
initialState: getInitialState,
|
||||
effects: {
|
||||
initialize () {
|
||||
if (formModalState !== undefined) {
|
||||
throw new Error('FormModal is a singleton!')
|
||||
}
|
||||
formModalState = this.state
|
||||
},
|
||||
finalize: () => {
|
||||
formModalState = undefined
|
||||
},
|
||||
onChange: (_, value) => () => ({
|
||||
value: getEventValue(value),
|
||||
}),
|
||||
onCancel () {
|
||||
const { state } = this
|
||||
if (!state.isHandlerRunning) {
|
||||
state.opened = false
|
||||
state.reject()
|
||||
}
|
||||
},
|
||||
async onSubmit ({ close }) {
|
||||
const { state } = this
|
||||
state.isHandlerRunning = true
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await state.handler(state.value)
|
||||
} finally {
|
||||
state.isHandlerRunning = false
|
||||
}
|
||||
state.opened = false
|
||||
state.resolve(result)
|
||||
},
|
||||
reset: () => () => {
|
||||
enableShortcuts()
|
||||
return getInitialState()
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formId: generateRandomId,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state, effects }) => (
|
||||
<ReactModal
|
||||
bsSize={state.size}
|
||||
onExited={effects.reset}
|
||||
onHide={effects.onCancel}
|
||||
show={state.opened}
|
||||
>
|
||||
<ReactModal.Header closeButton>
|
||||
<ReactModal.Title>{state.header}</ReactModal.Title>
|
||||
</ReactModal.Header>
|
||||
|
||||
<ReactModal.Body>
|
||||
<form id={state.formId}>
|
||||
{/* It should be better to use a computed to avoid calling the render function on each render,
|
||||
but reaclette(v0.4.0) not allow us to access to the effects from a computed */}
|
||||
{state.component ||
|
||||
(state.render !== undefined &&
|
||||
state.render({
|
||||
onChange: effects.onChange,
|
||||
value: state.value,
|
||||
}))}
|
||||
</form>
|
||||
</ReactModal.Body>
|
||||
|
||||
<ReactModal.Footer>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
form={state.formId}
|
||||
handler={effects.onSubmit}
|
||||
icon='save'
|
||||
size='large'
|
||||
>
|
||||
{_('formOk')}
|
||||
</ActionButton>{' '}
|
||||
<ActionButton handler={effects.onCancel} icon='cancel' size='large'>
|
||||
{_('formCancel')}
|
||||
</ActionButton>
|
||||
</ReactModal.Footer>
|
||||
</ReactModal>
|
||||
),
|
||||
])
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// This component returns :
|
||||
// - A loading icon when the objects are not fetched
|
||||
// - A default message if the objects are fetched and the collection is empty
|
||||
@@ -32,10 +31,10 @@ const NoObjects = props => {
|
||||
)
|
||||
}
|
||||
|
||||
propTypes(NoObjects)({
|
||||
children: propTypes.func,
|
||||
collection: propTypes.oneOfType([propTypes.array, propTypes.object]),
|
||||
component: propTypes.func,
|
||||
emptyMessage: propTypes.node.isRequired,
|
||||
})
|
||||
export default NoObjects
|
||||
NoObjects.propTypes = {
|
||||
children: PropTypes.func,
|
||||
collection: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
component: PropTypes.func,
|
||||
emptyMessage: PropTypes.node.isRequired,
|
||||
}
|
||||
export { NoObjects as default }
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import assign from 'lodash/assign'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// Deprecated because :
|
||||
// - unnecessary
|
||||
// - not standard in the React ecosystem
|
||||
if (__DEV__) {
|
||||
console.warn(`DEPRECATED: use prop-types directly:
|
||||
class MyComponent extends React.Component {
|
||||
static propTypes = {
|
||||
foo: PropTypes.string.isRequired
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
// Decorators to help declaring properties and context types on React
|
||||
// components without using the tedious static properties syntax.
|
||||
//
|
||||
// ```js
|
||||
// @propTypes({
|
||||
// children: propTypes.node.isRequired
|
||||
// }, {
|
||||
// store: propTypes.object.isRequired
|
||||
// })
|
||||
// class MyComponent extends React.Component {}
|
||||
// ```
|
||||
const propTypes = (propTypes, contextTypes) => target => {
|
||||
if (propTypes !== undefined) {
|
||||
target.propTypes = {
|
||||
...target.propTypes,
|
||||
...propTypes,
|
||||
}
|
||||
}
|
||||
if (contextTypes !== undefined) {
|
||||
target.contextTypes = {
|
||||
...target.contextTypes,
|
||||
...contextTypes,
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
assign(propTypes, PropTypes)
|
||||
|
||||
export { propTypes as default }
|
||||
@@ -1,7 +1,6 @@
|
||||
// TODO: remove these functions once the PR: https://github.com/julien-f/freactal/pull/5 has been merged
|
||||
// TODO: remove these functions once the PR: https://github.com/JsCommunity/reaclette/pull/5 has been merged
|
||||
// It only supports native inputs
|
||||
export const linkState = (_, { target }) => state => ({
|
||||
...state,
|
||||
export const linkState = (_, { target }) => () => ({
|
||||
[target.name]:
|
||||
target.nodeName.toLowerCase() === 'input' &&
|
||||
target.type.toLowerCase() === 'checkbox'
|
||||
@@ -9,7 +8,6 @@ export const linkState = (_, { target }) => state => ({
|
||||
: target.value,
|
||||
})
|
||||
|
||||
export const toggleState = (_, { target: { name } }) => state => ({
|
||||
...state,
|
||||
export const toggleState = (_, { currentTarget: { name } }) => state => ({
|
||||
[name]: !state[name],
|
||||
})
|
||||
12
packages/xo-web/src/common/react-novnc.js
vendored
12
packages/xo-web/src/common/react-novnc.js
vendored
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import RFB from '@nraynaud/novnc/lib/rfb'
|
||||
import URL from 'url-parse'
|
||||
@@ -7,8 +8,6 @@ import {
|
||||
disable as disableShortcuts,
|
||||
} from 'shortcuts'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const PROTOCOL_ALIASES = {
|
||||
'http:': 'ws:',
|
||||
'https:': 'wss:',
|
||||
@@ -20,11 +19,12 @@ const fixProtocol = url => {
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
onClipboardChange: propTypes.func,
|
||||
url: propTypes.string.isRequired,
|
||||
})
|
||||
export default class NoVnc extends Component {
|
||||
static propTypes = {
|
||||
onClipboardChange: PropTypes.func,
|
||||
url: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this._rfb = null
|
||||
|
||||
@@ -4,9 +4,9 @@ import React from 'react'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import { startsWith } from 'lodash'
|
||||
|
||||
import decorate from './apply-decorators'
|
||||
import Icon from './icon'
|
||||
import Link from './link'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import { addSubscriptions, connectStore, formatSize } from './utils'
|
||||
import { createGetObject, createSelector } from './selectors'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
@@ -50,7 +50,7 @@ const XO_ITEM_PROP_TYPES = {
|
||||
id: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export const VmItem = [
|
||||
export const VmItem = decorate([
|
||||
connectStore(() => {
|
||||
const getVm = createGetObject()
|
||||
return {
|
||||
@@ -72,11 +72,11 @@ export const VmItem = [
|
||||
)}
|
||||
</XoItem>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
])
|
||||
|
||||
VmItem.propTypes = XO_ITEM_PROP_TYPES
|
||||
|
||||
export const SrItem = [
|
||||
export const SrItem = decorate([
|
||||
connectStore(() => {
|
||||
const getSr = createGetObject()
|
||||
return {
|
||||
@@ -101,11 +101,11 @@ export const SrItem = [
|
||||
)}
|
||||
</XoItem>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
])
|
||||
|
||||
SrItem.propTypes = XO_ITEM_PROP_TYPES
|
||||
|
||||
export const RemoteItem = [
|
||||
export const RemoteItem = decorate([
|
||||
addSubscriptions(({ id }) => ({
|
||||
remote: cb =>
|
||||
subscribeRemotes(remotes => {
|
||||
@@ -121,11 +121,11 @@ export const RemoteItem = [
|
||||
)}
|
||||
</XoItem>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
])
|
||||
|
||||
RemoteItem.propTypes = XO_ITEM_PROP_TYPES
|
||||
|
||||
export const PoolItem = [
|
||||
export const PoolItem = decorate([
|
||||
connectStore(() => ({
|
||||
pool: createGetObject(),
|
||||
})),
|
||||
@@ -138,13 +138,13 @@ export const PoolItem = [
|
||||
)}
|
||||
</XoItem>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
])
|
||||
|
||||
PoolItem.propTypes = XO_ITEM_PROP_TYPES
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SrResourceSetItem = [
|
||||
export const SrResourceSetItem = decorate([
|
||||
connectStore(() => {
|
||||
const getSr = createGetObject()
|
||||
return (state, props) => ({
|
||||
@@ -164,34 +164,34 @@ export const SrResourceSetItem = [
|
||||
)}
|
||||
</XoItem>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
])
|
||||
|
||||
SrResourceSetItem.propTypes = XO_ITEM_PROP_TYPES
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Host, Network, VM-template.
|
||||
const PoolObjectItem = propTypes({
|
||||
object: propTypes.object.isRequired,
|
||||
})(
|
||||
connectStore(() => {
|
||||
const getPool = createGetObject((_, props) => props.object.$pool)
|
||||
const PoolObjectItem = connectStore(() => {
|
||||
const getPool = createGetObject((_, props) => props.object.$pool)
|
||||
|
||||
return (state, props) => ({
|
||||
pool: getPool(state, props),
|
||||
})
|
||||
})(({ object, pool }) => {
|
||||
const icon = OBJECT_TYPE_TO_ICON[object.type]
|
||||
const { id } = object
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon={icon} /> {`${object.name_label || id} `}
|
||||
{pool && `(${pool.name_label || pool.id})`}
|
||||
</span>
|
||||
)
|
||||
return (state, props) => ({
|
||||
pool: getPool(state, props),
|
||||
})
|
||||
)
|
||||
})(({ object, pool }) => {
|
||||
const icon = OBJECT_TYPE_TO_ICON[object.type]
|
||||
const { id } = object
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon={icon} /> {`${object.name_label || id} `}
|
||||
{pool && `(${pool.name_label || pool.id})`}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
PoolObjectItem.propTypes = {
|
||||
object: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
const VgpuItem = connectStore(() => ({
|
||||
vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),
|
||||
@@ -318,6 +318,9 @@ const xoItemToRender = {
|
||||
{backup.mode}
|
||||
</span>{' '}
|
||||
<span className='tag tag-warning'>{backup.remote.name}</span>{' '}
|
||||
<span className='tag tag-success'>
|
||||
{backup.jobName !== undefined ? backup.jobName : _('unknownJob')}
|
||||
</span>{' '}
|
||||
<FormattedDate
|
||||
value={new Date(backup.timestamp)}
|
||||
month='long'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import _ from 'intl'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import ActionRowButton from './action-row-button'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
export const CAN_REPORT_BUG = process.env.XOA_PLAN > 1
|
||||
|
||||
const reportBug = ({ formatMessage, message, title }) => {
|
||||
export const reportBug = ({ formatMessage, message, title }) => {
|
||||
const encodedTitle = encodeURIComponent(title)
|
||||
const encodedMessage = encodeURIComponent(
|
||||
formatMessage !== undefined ? formatMessage(message) : message
|
||||
@@ -41,11 +41,11 @@ const ReportBugButton = ({
|
||||
)
|
||||
}
|
||||
|
||||
propTypes(ReportBugButton)({
|
||||
formatMessage: propTypes.func,
|
||||
message: propTypes.string.isRequired,
|
||||
rowButton: propTypes.bool,
|
||||
title: propTypes.string.isRequired,
|
||||
})
|
||||
ReportBugButton.propTypes = {
|
||||
formatMessage: PropTypes.func,
|
||||
message: PropTypes.string.isRequired,
|
||||
rowButton: PropTypes.bool,
|
||||
title: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default ReportBugButton
|
||||
export { ReportBugButton as default }
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import classNames from 'classnames'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
import { forEach, includes, isArray, map, sortedIndex } from 'lodash'
|
||||
import { FormattedDate, FormattedTime } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { flatten, forEach, identity, isArray, map, sortedIndex } from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import decorate from './apply-decorators'
|
||||
import TimezonePicker from './timezone-picker'
|
||||
import Icon from './icon'
|
||||
import Tooltip from './tooltip'
|
||||
import { Card, CardHeader, CardBlock } from './card'
|
||||
import { Col, Row } from './grid'
|
||||
@@ -25,11 +26,6 @@ const PREVIEW_SLIDER_STYLE = { width: '400px' }
|
||||
|
||||
const UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']
|
||||
|
||||
const MINUTES_RANGE = [2, 30]
|
||||
const HOURS_RANGE = [2, 12]
|
||||
const MONTH_DAYS_RANGE = [2, 15]
|
||||
const MONTHS_RANGE = [2, 6]
|
||||
|
||||
const MIN_PREVIEWS = 5
|
||||
const MAX_PREVIEWS = 20
|
||||
|
||||
@@ -117,11 +113,12 @@ const getDayName = dayNum => (
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
timezone: propTypes.string,
|
||||
})
|
||||
export class SchedulePreview extends Component {
|
||||
static propTypes = {
|
||||
cronPattern: PropTypes.string.isRequired,
|
||||
timezone: PropTypes.string,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
getXoServerTimezone.then(serverTimezone => {
|
||||
this.setState({
|
||||
@@ -145,7 +142,8 @@ export class SchedulePreview extends Component {
|
||||
min={MIN_PREVIEWS}
|
||||
max={MAX_PREVIEWS}
|
||||
onChange={this.linkState('value')}
|
||||
value={+value}
|
||||
value={value && +value}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<ul className='list-group'>
|
||||
@@ -163,13 +161,14 @@ export class SchedulePreview extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
onChange: propTypes.func.isRequired,
|
||||
tdId: propTypes.number.isRequired,
|
||||
value: propTypes.bool.isRequired,
|
||||
})
|
||||
class ToggleTd extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
tdId: PropTypes.number.isRequired,
|
||||
value: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
_onClick = () => {
|
||||
const { props } = this
|
||||
props.onChange(props.tdId, !props.value)
|
||||
@@ -191,46 +190,34 @@ class ToggleTd extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
labelId: propTypes.string.isRequired,
|
||||
options: propTypes.array.isRequired,
|
||||
optionRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.array.isRequired,
|
||||
})
|
||||
class TableSelect extends Component {
|
||||
static defaultProps = {
|
||||
optionRenderer: value => value,
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
this.props.onChange([])
|
||||
}
|
||||
|
||||
_handleChange = (tdId, tdValue) => {
|
||||
const { props } = this
|
||||
|
||||
const newValue = props.value.slice()
|
||||
const index = sortedIndex(newValue, tdId)
|
||||
|
||||
if (tdValue) {
|
||||
// Add
|
||||
if (newValue[index] !== tdId) {
|
||||
newValue.splice(index, 0, tdId)
|
||||
}
|
||||
} else {
|
||||
// Remove
|
||||
if (newValue[index] === tdId) {
|
||||
newValue.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
props.onChange(newValue)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { labelId, options, optionRenderer, value } = this.props
|
||||
|
||||
const TableSelect = decorate([
|
||||
provideState({
|
||||
effects: {
|
||||
onChange: (_, tdId, add) => (_, { value, onChange, options }) => {
|
||||
let newValue = [...value]
|
||||
const index = sortedIndex(newValue, tdId)
|
||||
if (add) {
|
||||
newValue[index] !== tdId && newValue.splice(index, 0, tdId)
|
||||
} else if (newValue[index] === tdId) {
|
||||
if (newValue.length > 1) {
|
||||
newValue.splice(index, 1)
|
||||
} else {
|
||||
newValue = [options[0][0]]
|
||||
}
|
||||
}
|
||||
onChange(newValue)
|
||||
},
|
||||
selectAll: () => ({ optionsValues }, { onChange }) => {
|
||||
onChange(optionsValues)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
optionsValues: (_, { options }) => flatten(options),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state, effects, labelId, options, optionRenderer = identity, value }) => {
|
||||
let k = 0
|
||||
return (
|
||||
<div>
|
||||
<table className='table table-bordered table-sm'>
|
||||
@@ -242,146 +229,105 @@ class TableSelect extends Component {
|
||||
children={optionRenderer(tdOption)}
|
||||
tdId={tdOption}
|
||||
key={tdOption}
|
||||
onChange={this._handleChange}
|
||||
value={includes(value, tdOption)}
|
||||
onChange={effects.onChange}
|
||||
value={
|
||||
k < value.length && value[k] === tdOption && (++k, true)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Button className='pull-right' onClick={this._reset}>
|
||||
{_(`selectTableAll${labelId}`)}{' '}
|
||||
{value && !value.length && <Icon icon='success' />}
|
||||
<Button className='pull-right' onClick={effects.selectAll}>
|
||||
{_(`selectTableAll${labelId}`)}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
TableSelect.propTypes = {
|
||||
labelId: PropTypes.string.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
optionRenderer: PropTypes.func,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.array.isRequired,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// "2,7" => [2,7] "*/2" => 2 "*" => []
|
||||
const cronToValue = (cron, range) => {
|
||||
if (cron.indexOf('/') === 1) {
|
||||
return +cron.split('/')[1]
|
||||
}
|
||||
const TimePicker = decorate([
|
||||
provideState({
|
||||
effects: {
|
||||
onChange: (_, value) => ({ optionsValues }, { onChange }) => {
|
||||
if (isArray(value)) {
|
||||
value = value.length === optionsValues.length ? '*' : value.join(',')
|
||||
} else {
|
||||
value = `*/${value}`
|
||||
}
|
||||
|
||||
if (cron === '*') {
|
||||
return []
|
||||
}
|
||||
onChange(value)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
maxStep: ({ optionsValues }) => Math.floor(optionsValues.length / 2),
|
||||
optionsValues: (_, { options }) => flatten(options),
|
||||
|
||||
return map(cron.split(','), Number)
|
||||
}
|
||||
// '*' or '*/1' => all values
|
||||
// '2,7' => [2,7]
|
||||
// '*/2' => [min + 2 * 0, min + 2 * 1, ..., min + 2 * n <= max]
|
||||
tableValue: ({ optionsValues, step }, { value }) =>
|
||||
step === 1
|
||||
? optionsValues
|
||||
: step !== undefined
|
||||
? optionsValues.filter((_, i) => i % step === 0)
|
||||
: value.split(',').map(Number),
|
||||
|
||||
// [2,7] => "2,7" 2 => "*/2" [] => "*"
|
||||
const valueToCron = value => {
|
||||
if (!isArray(value)) {
|
||||
return `*/${value}`
|
||||
}
|
||||
// '*' => 1
|
||||
// '*/2' => 2
|
||||
// otherwise => undefined
|
||||
step: (_, { value }) =>
|
||||
value === '*'
|
||||
? 1
|
||||
: value.indexOf('/') === 1
|
||||
? +value.split('/')[1]
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state, effects, ...props }) => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{_(`scheduling${props.labelId}`)}
|
||||
{props.headerAddon}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<TableSelect
|
||||
labelId={props.labelId}
|
||||
onChange={effects.onChange}
|
||||
optionRenderer={props.optionRenderer}
|
||||
options={props.options}
|
||||
value={state.tableValue}
|
||||
/>
|
||||
<Range
|
||||
max={state.maxStep}
|
||||
min={1}
|
||||
onChange={effects.onChange}
|
||||
value={state.step}
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
),
|
||||
])
|
||||
|
||||
if (!value.length) {
|
||||
return '*'
|
||||
}
|
||||
|
||||
return value.join(',')
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
headerAddon: propTypes.node,
|
||||
optionRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
range: propTypes.array,
|
||||
labelId: propTypes.string.isRequired,
|
||||
value: propTypes.any.isRequired,
|
||||
})
|
||||
class TimePicker extends Component {
|
||||
_update = cron => {
|
||||
const { tableValue, rangeValue } = this.state
|
||||
|
||||
const newValue = cronToValue(cron)
|
||||
const periodic = !isArray(newValue)
|
||||
|
||||
this.setState({
|
||||
periodic,
|
||||
tableValue: periodic ? tableValue : newValue,
|
||||
rangeValue: periodic ? newValue : rangeValue,
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.value !== this.props.value) {
|
||||
this._update(props.value)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._update(this.props.value)
|
||||
}
|
||||
|
||||
_onChange = value => {
|
||||
this.props.onChange(valueToCron(value))
|
||||
}
|
||||
|
||||
_tableTab = () => this._onChange(this.state.tableValue || [])
|
||||
_periodicTab = () =>
|
||||
this._onChange(this.state.rangeValue || this.props.range[0])
|
||||
|
||||
render () {
|
||||
const { headerAddon, labelId, options, optionRenderer, range } = this.props
|
||||
|
||||
const { periodic, tableValue, rangeValue } = this.state
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{_(`scheduling${labelId}`)}
|
||||
{headerAddon}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{range && (
|
||||
<ul className='nav nav-tabs mb-1'>
|
||||
<li className='nav-item'>
|
||||
<a
|
||||
onClick={this._tableTab}
|
||||
className={classNames('nav-link', !periodic && 'active')}
|
||||
style={CLICKABLE}
|
||||
>
|
||||
{_(`schedulingEachSelected${labelId}`)}
|
||||
</a>
|
||||
</li>
|
||||
<li className='nav-item'>
|
||||
<a
|
||||
onClick={this._periodicTab}
|
||||
className={classNames('nav-link', periodic && 'active')}
|
||||
style={CLICKABLE}
|
||||
>
|
||||
{_(`schedulingEveryN${labelId}`)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{periodic ? (
|
||||
<Range
|
||||
ref='range'
|
||||
min={range[0]}
|
||||
max={range[1]}
|
||||
onChange={this._onChange}
|
||||
value={rangeValue}
|
||||
/>
|
||||
) : (
|
||||
<TableSelect
|
||||
labelId={labelId}
|
||||
onChange={this._onChange}
|
||||
options={options}
|
||||
optionRenderer={optionRenderer}
|
||||
value={tableValue || []}
|
||||
/>
|
||||
)}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
TimePicker.propTypes = {
|
||||
headerAddon: PropTypes.node,
|
||||
labelId: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
optionRenderer: PropTypes.func,
|
||||
options: PropTypes.array.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
|
||||
@@ -392,11 +338,12 @@ const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
|
||||
return weekDayPattern !== '*'
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
monthDayPattern: propTypes.string.isRequired,
|
||||
weekDayPattern: propTypes.string.isRequired,
|
||||
})
|
||||
class DayPicker extends Component {
|
||||
static propTypes = {
|
||||
monthDayPattern: PropTypes.string.isRequired,
|
||||
weekDayPattern: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
weekDayMode: isWeekDayMode(this.props),
|
||||
}
|
||||
@@ -415,12 +362,7 @@ class DayPicker extends Component {
|
||||
}
|
||||
|
||||
_onChange = cron => {
|
||||
const isMonthDayPattern = !this.state.weekDayMode || includes(cron, '/')
|
||||
|
||||
this.props.onChange([
|
||||
isMonthDayPattern ? cron : '*',
|
||||
isMonthDayPattern ? '*' : cron,
|
||||
])
|
||||
this.props.onChange(this.state.weekDayMode ? ['*', cron] : [cron, '*'])
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -448,11 +390,9 @@ class DayPicker extends Component {
|
||||
headerAddon={dayModeToggle}
|
||||
key={weekDayMode ? 'week' : 'month'}
|
||||
labelId='Day'
|
||||
onChange={this._onChange}
|
||||
optionRenderer={weekDayMode ? getDayName : undefined}
|
||||
options={weekDayMode ? WEEK_DAYS : DAYS}
|
||||
onChange={this._onChange}
|
||||
range={MONTH_DAYS_RANGE}
|
||||
setWeekDayMode={this._setWeekDayMode}
|
||||
value={weekDayMode ? weekDayPattern : monthDayPattern}
|
||||
/>
|
||||
)
|
||||
@@ -461,16 +401,17 @@ class DayPicker extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string,
|
||||
onChange: propTypes.func,
|
||||
timezone: propTypes.string,
|
||||
value: propTypes.shape({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
timezone: propTypes.string,
|
||||
}),
|
||||
})
|
||||
export default class Scheduler extends Component {
|
||||
static propTypes = {
|
||||
cronPattern: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
timezone: PropTypes.string,
|
||||
value: PropTypes.shape({
|
||||
cronPattern: PropTypes.string.isRequired,
|
||||
timezone: PropTypes.string,
|
||||
}),
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
@@ -523,7 +464,6 @@ export default class Scheduler extends Component {
|
||||
optionRenderer={getMonthName}
|
||||
options={MONTHS}
|
||||
onChange={this._monthChange}
|
||||
range={MONTHS_RANGE}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['month']]}
|
||||
/>
|
||||
</Col>
|
||||
@@ -540,7 +480,6 @@ export default class Scheduler extends Component {
|
||||
<TimePicker
|
||||
labelId='Hour'
|
||||
options={HOURS}
|
||||
range={HOURS_RANGE}
|
||||
onChange={this._hourChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
|
||||
/>
|
||||
@@ -549,7 +488,6 @@ export default class Scheduler extends Component {
|
||||
<TimePicker
|
||||
labelId='Minute'
|
||||
options={MINS}
|
||||
range={MINUTES_RANGE}
|
||||
onChange={this._minuteChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
@propTypes({
|
||||
multi: propTypes.bool,
|
||||
label: propTypes.node,
|
||||
onChange: propTypes.func.isRequired,
|
||||
})
|
||||
export default class SelectFiles extends Component {
|
||||
static propTypes = {
|
||||
multi: PropTypes.bool,
|
||||
label: PropTypes.node,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
_onChange = e => {
|
||||
const { multi, onChange } = this.props
|
||||
const { files } = e.target
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
find,
|
||||
forEach,
|
||||
groupBy,
|
||||
identity,
|
||||
isArray,
|
||||
isArrayLike,
|
||||
isFunction,
|
||||
@@ -242,14 +243,31 @@ export const getCheckPermissions = invoke(() => {
|
||||
})
|
||||
|
||||
const _getPermissionsPredicate = invoke(() => {
|
||||
const getCache = create(identity, () => ({ __proto__: null }))
|
||||
|
||||
const getPredicate = create(
|
||||
state => state.permissions,
|
||||
state => state.objects,
|
||||
(permissions, objects) => {
|
||||
const cache = getCache(permissions)
|
||||
objects = objects.all
|
||||
const getObject = id => objects[id] || EMPTY_OBJECT
|
||||
|
||||
return id => checkPermissions(permissions, getObject, id.id || id, 'view')
|
||||
return id => {
|
||||
if (typeof id !== 'string') {
|
||||
id = id.id
|
||||
}
|
||||
let allowed = cache[id]
|
||||
if (allowed === undefined) {
|
||||
allowed = cache[id] = checkPermissions(
|
||||
permissions,
|
||||
getObject,
|
||||
id,
|
||||
'view'
|
||||
)
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { cloneElement } from 'react'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const SINGLE_LINE_STYLE = { display: 'flex' }
|
||||
const COL_STYLE = { marginTop: 'auto', marginBottom: 'auto' }
|
||||
|
||||
const SingleLineRow = propTypes({
|
||||
className: propTypes.string,
|
||||
})(({ children, className }) => (
|
||||
const SingleLineRow = ({ children, className }) => (
|
||||
<div className={`${className || ''} row`} style={SINGLE_LINE_STYLE}>
|
||||
{React.Children.map(
|
||||
children,
|
||||
child => child && cloneElement(child, { style: COL_STYLE })
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)
|
||||
|
||||
SingleLineRow.propTypes = {
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
export { SingleLineRow as default }
|
||||
|
||||
@@ -4,6 +4,7 @@ import classNames from 'classnames'
|
||||
import defined, { get } from '@xen-orchestra/defined'
|
||||
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
import { Input as DebouncedInput } from 'debounce-input-decorator'
|
||||
@@ -11,14 +12,17 @@ import { Portal } from 'react-overlays'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
import { Set } from 'immutable'
|
||||
import { Dropdown, MenuItem } from 'react-bootstrap-4/lib'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import {
|
||||
ceil,
|
||||
filter,
|
||||
findIndex,
|
||||
forEach,
|
||||
get as getProperty,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
map,
|
||||
sortBy,
|
||||
startsWith,
|
||||
} from 'lodash'
|
||||
|
||||
@@ -26,9 +30,9 @@ import ActionRowButton from '../action-row-button'
|
||||
import Button from '../button'
|
||||
import ButtonGroup from '../button-group'
|
||||
import Component from '../base-component'
|
||||
import decorate from '../apply-decorators'
|
||||
import Icon from '../icon'
|
||||
import Pagination from '../pagination'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import SingleLineRow from '../single-line-row'
|
||||
import Tooltip from '../tooltip'
|
||||
import { BlockLink } from '../link'
|
||||
@@ -46,12 +50,13 @@ import styles from './index.css'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
filters: propTypes.object,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.string.isRequired,
|
||||
})
|
||||
class TableFilter extends Component {
|
||||
static propTypes = {
|
||||
filters: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
_cleanFilter = () => this._setFilter('')
|
||||
|
||||
_setFilter = filterValue => {
|
||||
@@ -121,13 +126,14 @@ class TableFilter extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
columnId: propTypes.number.isRequired,
|
||||
name: propTypes.node,
|
||||
sort: propTypes.func,
|
||||
sortIcon: propTypes.string,
|
||||
})
|
||||
class ColumnHead extends Component {
|
||||
static propTypes = {
|
||||
columnId: PropTypes.number.isRequired,
|
||||
name: PropTypes.node,
|
||||
sort: PropTypes.func,
|
||||
sortIcon: PropTypes.string,
|
||||
}
|
||||
|
||||
_sort = () => {
|
||||
const { props } = this
|
||||
props.sort(props.columnId)
|
||||
@@ -162,10 +168,11 @@ class ColumnHead extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
indeterminate: propTypes.bool.isRequired,
|
||||
})
|
||||
class Checkbox extends Component {
|
||||
static propTypes = {
|
||||
indeterminate: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const {
|
||||
props: { indeterminate },
|
||||
@@ -191,57 +198,49 @@ class Checkbox extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const actionsShape = propTypes.arrayOf(
|
||||
propTypes.shape({
|
||||
const actionsShape = PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
// groupedActions: the function will be called with an array of the selected items in parameters
|
||||
// individualActions: the function will be called with the related item in parameters
|
||||
disabled: propTypes.oneOfType([propTypes.bool, propTypes.func]),
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
label: propTypes.oneOfType([propTypes.node, propTypes.func]).isRequired,
|
||||
level: propTypes.oneOf(['primary', 'warning', 'danger']),
|
||||
redirectOnSuccess: propTypes.oneOfType([propTypes.func, propTypes.string]),
|
||||
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
||||
handler: PropTypes.func.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||
level: PropTypes.oneOf(['primary', 'warning', 'danger']),
|
||||
redirectOnSuccess: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
|
||||
})
|
||||
)
|
||||
|
||||
class IndividualAction extends Component {
|
||||
_getIsDisabled = createSelector(
|
||||
() => this.props.disabled,
|
||||
() => this.props.item,
|
||||
() => this.props.userData,
|
||||
(disabled, item, userData) =>
|
||||
isFunction(disabled) ? disabled(item, userData) : disabled
|
||||
)
|
||||
_getLabel = createSelector(
|
||||
() => this.props.label,
|
||||
() => this.props.item,
|
||||
() => this.props.userData,
|
||||
(label, item, userData) =>
|
||||
isFunction(label) ? label(item, userData) : label
|
||||
)
|
||||
|
||||
_executeAction = () => {
|
||||
const p = this.props
|
||||
return p.handler(p.item, p.userData)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { icon, item, level, redirectOnSuccess, userData } = this.props
|
||||
|
||||
return (
|
||||
<ActionRowButton
|
||||
btnStyle={level}
|
||||
data-item={item}
|
||||
data-userData={userData}
|
||||
disabled={this._getIsDisabled()}
|
||||
handler={this._executeAction}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
tooltip={this._getLabel()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
const IndividualAction = decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
disabled: ({ item }, { disabled, userData }) =>
|
||||
isFunction(disabled) ? disabled(item, userData) : disabled,
|
||||
handler: ({ item }, { handler, userData }) => () =>
|
||||
handler(item, userData),
|
||||
icon: ({ item }, { icon, userData }) =>
|
||||
isFunction(icon) ? icon(item, userData) : icon,
|
||||
item: (_, { item, grouped }) => (grouped ? [item] : item),
|
||||
label: ({ item }, { label, userData }) =>
|
||||
isFunction(label) ? label(item, userData) : label,
|
||||
level: ({ item }, { level, userData }) =>
|
||||
isFunction(level) ? level(item, userData) : level,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state, redirectOnSuccess, userData }) => (
|
||||
<ActionRowButton
|
||||
btnStyle={state.level}
|
||||
data-item={state.item}
|
||||
data-userData={userData}
|
||||
disabled={state.disabled}
|
||||
handler={state.handler}
|
||||
icon={state.icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
tooltip={state.label}
|
||||
/>
|
||||
),
|
||||
])
|
||||
|
||||
class GroupedAction extends Component {
|
||||
_getIsDisabled = createSelector(
|
||||
@@ -279,68 +278,80 @@ class GroupedAction extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const LEVELS = [undefined, 'primary', 'warning', 'danger']
|
||||
// page number and sort info are optional for backward compatibility
|
||||
const URL_STATE_RE = /^(?:(\d+)(?:_(\d+)(_desc)?)?-)?(.*)$/
|
||||
|
||||
@propTypes(
|
||||
{
|
||||
defaultColumn: propTypes.number,
|
||||
defaultFilter: propTypes.string,
|
||||
collection: propTypes.oneOfType([propTypes.array, propTypes.object])
|
||||
export default class SortedTable extends Component {
|
||||
static propTypes = {
|
||||
defaultColumn: PropTypes.number,
|
||||
defaultFilter: PropTypes.string,
|
||||
collection: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
.isRequired,
|
||||
columns: propTypes.arrayOf(
|
||||
propTypes.shape({
|
||||
component: propTypes.func,
|
||||
default: propTypes.bool,
|
||||
name: propTypes.node,
|
||||
itemRenderer: propTypes.func,
|
||||
sortCriteria: propTypes.oneOfType([propTypes.func, propTypes.string]),
|
||||
sortOrder: propTypes.string,
|
||||
textAlign: propTypes.string,
|
||||
columns: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
default: PropTypes.bool,
|
||||
name: PropTypes.node,
|
||||
sortCriteria: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
|
||||
sortOrder: PropTypes.string,
|
||||
textAlign: PropTypes.string,
|
||||
|
||||
// for the cell render, you can use component or itemRenderer or valuePath
|
||||
//
|
||||
// item and userData will be injected in the component as props
|
||||
// component: <Component />
|
||||
component: PropTypes.func,
|
||||
|
||||
// itemRenderer: (item, userData) => <span />
|
||||
itemRenderer: PropTypes.func,
|
||||
|
||||
// the path to the value, it's also the sort criteria default value
|
||||
// valuePath: 'a.b.c'
|
||||
valuePath: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
filterContainer: propTypes.func,
|
||||
filters: propTypes.object,
|
||||
actions: propTypes.arrayOf(
|
||||
propTypes.shape({
|
||||
filterContainer: PropTypes.func,
|
||||
filters: PropTypes.object,
|
||||
actions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
// regroup individual actions and grouped actions
|
||||
disabled: propTypes.oneOfType([propTypes.bool, propTypes.func]),
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
individualDisabled: propTypes.oneOfType([
|
||||
propTypes.bool,
|
||||
propTypes.func,
|
||||
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
||||
handler: PropTypes.func.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
individualDisabled: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.func,
|
||||
]),
|
||||
individualHandler: propTypes.func,
|
||||
individualLabel: propTypes.node,
|
||||
label: propTypes.node.isRequired,
|
||||
level: propTypes.oneOf(['primary', 'warning', 'danger']),
|
||||
individualHandler: PropTypes.func,
|
||||
individualLabel: PropTypes.node,
|
||||
label: PropTypes.node.isRequired,
|
||||
level: PropTypes.oneOf(['primary', 'warning', 'danger']),
|
||||
})
|
||||
),
|
||||
groupedActions: actionsShape,
|
||||
individualActions: actionsShape,
|
||||
itemsPerPage: propTypes.number,
|
||||
paginationContainer: propTypes.func,
|
||||
rowAction: propTypes.func,
|
||||
rowLink: propTypes.oneOfType([propTypes.func, propTypes.string]),
|
||||
rowTransform: propTypes.func,
|
||||
itemsPerPage: PropTypes.number,
|
||||
paginationContainer: PropTypes.func,
|
||||
rowAction: PropTypes.func,
|
||||
rowLink: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
|
||||
rowTransform: PropTypes.func,
|
||||
// DOM node selector like body or .my-class
|
||||
// The shortcuts will be enabled when the node is focused
|
||||
shortcutsTarget: propTypes.string,
|
||||
stateUrlParam: propTypes.string,
|
||||
shortcutsTarget: PropTypes.string,
|
||||
stateUrlParam: PropTypes.string,
|
||||
|
||||
// @deprecated, use `data-${key}` instead
|
||||
userData: propTypes.any,
|
||||
},
|
||||
{
|
||||
router: routerShape,
|
||||
userData: PropTypes.any,
|
||||
}
|
||||
)
|
||||
export default class SortedTable extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
itemsPerPage: 10,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
router: routerShape,
|
||||
}
|
||||
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
@@ -428,9 +439,10 @@ export default class SortedTable extends Component {
|
||||
)
|
||||
),
|
||||
createSelector(
|
||||
() => this._getSelectedColumn().valuePath,
|
||||
() => this._getSelectedColumn().sortCriteria,
|
||||
this._getUserData,
|
||||
(sortCriteria, userData) =>
|
||||
(valuePath, sortCriteria = valuePath, userData) =>
|
||||
typeof sortCriteria === 'function'
|
||||
? object => sortCriteria(object, userData)
|
||||
: sortCriteria
|
||||
@@ -705,9 +717,38 @@ export default class SortedTable extends Component {
|
||||
() => this.props.groupedActions,
|
||||
() => this.props.actions,
|
||||
(groupedActions, actions) =>
|
||||
groupedActions !== undefined && actions !== undefined
|
||||
? groupedActions.concat(actions)
|
||||
: groupedActions || actions
|
||||
sortBy(
|
||||
groupedActions !== undefined && actions !== undefined
|
||||
? groupedActions.concat(actions)
|
||||
: groupedActions || actions,
|
||||
action => LEVELS.indexOf(action.level)
|
||||
)
|
||||
)
|
||||
|
||||
_getIndividualActions = createSelector(
|
||||
() => this.props.individualActions,
|
||||
() => this.props.actions,
|
||||
(individualActions, actions) => {
|
||||
const normalizedActions = map(actions, a => ({
|
||||
disabled:
|
||||
a.individualDisabled !== undefined
|
||||
? a.individualDisabled
|
||||
: a.disabled,
|
||||
grouped: a.individualHandler === undefined,
|
||||
handler:
|
||||
a.individualHandler !== undefined ? a.individualHandler : a.handler,
|
||||
icon: a.icon,
|
||||
label: a.individualLabel !== undefined ? a.individualLabel : a.label,
|
||||
level: a.level,
|
||||
}))
|
||||
|
||||
return sortBy(
|
||||
individualActions !== undefined && actions !== undefined
|
||||
? individualActions.concat(normalizedActions)
|
||||
: individualActions || normalizedActions,
|
||||
action => LEVELS.indexOf(action.level)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
_renderItem = (item, i) => {
|
||||
@@ -721,10 +762,12 @@ export default class SortedTable extends Component {
|
||||
|
||||
const columns = map(
|
||||
props.columns,
|
||||
({ component: Component, itemRenderer, textAlign }, key) => (
|
||||
({ component: Component, itemRenderer, valuePath, textAlign }, key) => (
|
||||
<td className={textAlign && `text-xs-${textAlign}`} key={key}>
|
||||
{Component !== undefined ? (
|
||||
<Component item={item} userData={userData} />
|
||||
) : valuePath !== undefined ? (
|
||||
getProperty(item, valuePath)
|
||||
) : (
|
||||
itemRenderer(item, userData)
|
||||
)}
|
||||
@@ -748,7 +791,7 @@ export default class SortedTable extends Component {
|
||||
<td>
|
||||
<div className='pull-right'>
|
||||
<ButtonGroup>
|
||||
{map(individualActions, (props, key) => (
|
||||
{map(this._getIndividualActions(), (props, key) => (
|
||||
<IndividualAction
|
||||
{...props}
|
||||
item={item}
|
||||
@@ -756,17 +799,6 @@ export default class SortedTable extends Component {
|
||||
userData={userData}
|
||||
/>
|
||||
))}
|
||||
{map(actions, (props, key) => (
|
||||
<IndividualAction
|
||||
{...props}
|
||||
disabled={props.individualDisabled || props.disabled}
|
||||
handler={props.individualHandler || props.handler}
|
||||
item={props.individualHandler !== undefined ? item : [item]}
|
||||
key={key}
|
||||
label={props.individualLabel || props.label}
|
||||
userData={userData}
|
||||
/>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</td>
|
||||
@@ -934,7 +966,11 @@ export default class SortedTable extends Component {
|
||||
columnId={key}
|
||||
key={key}
|
||||
name={column.name}
|
||||
sort={column.sortCriteria && this._sort}
|
||||
sort={
|
||||
(column.sortCriteria !== undefined ||
|
||||
column.valuePath !== undefined) &&
|
||||
this._sort
|
||||
}
|
||||
sortIcon={
|
||||
state.selectedColumn === key ? state.sortOrder : 'sort'
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// do not forward `state` to ActionButton
|
||||
const Button = styled(p => <ActionButton {...omit(p, 'state')} />)`
|
||||
@@ -41,6 +41,8 @@ const StateButton = ({
|
||||
</Button>
|
||||
)
|
||||
|
||||
export default propTypes({
|
||||
state: propTypes.bool.isRequired,
|
||||
})(StateButton)
|
||||
StateButton.propTypes = {
|
||||
state: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export { StateButton as default }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user