Compare commits
1 Commits
api-utils
...
ManonMerci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45d021e6aa |
@@ -123,7 +123,7 @@ const onProgress = makeOnProgress({
|
||||
onTaskUpdate(taskLog) {},
|
||||
})
|
||||
|
||||
Task.run({ properties: { name: 'my task' }, onProgress }, asyncFn)
|
||||
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
|
||||
```
|
||||
|
||||
It can also be fed event logs directly:
|
||||
|
||||
@@ -139,7 +139,7 @@ const onProgress = makeOnProgress({
|
||||
onTaskUpdate(taskLog) {},
|
||||
})
|
||||
|
||||
Task.run({ properties: { name: 'my task' }, onProgress }, asyncFn)
|
||||
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
|
||||
```
|
||||
|
||||
It can also be fed event logs directly:
|
||||
|
||||
@@ -6,16 +6,12 @@
|
||||
### Enhancements
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [SR] show an icon on SR during VDI coalescing (with XCP-ng 8.3+) (PR [#7241](https://github.com/vatesfr/xen-orchestra/pull/7241))
|
||||
|
||||
- [VDI/Export] Expose NBD settings in the XO and REST APIs api (PR [#7251](https://github.com/vatesfr/xen-orchestra/pull/7251))
|
||||
- [Menu/Proxies] Added a warning icon if unable to check proxies upgrade (PR [#7237](https://github.com/vatesfr/xen-orchestra/pull/7237))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Backup/Report] Missing report for Mirror Backup (PR [#7254](https://github.com/vatesfr/xen-orchestra/pull/7254))
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
### Packages to release
|
||||
|
||||
@@ -58,18 +58,18 @@ Please only use this if you have issues with [the default way to deploy XOA](ins
|
||||
|
||||
### Via a bash script
|
||||
|
||||
Alternatively, you can deploy it by connecting to your XenServer host and executing the following:
|
||||
Alternatively, you can deploy it by connecting to your XCP-ng/XenServer host and executing the following:
|
||||
|
||||
```sh
|
||||
bash -c "$(wget -qO- https://xoa.io/deploy)"
|
||||
```
|
||||
|
||||
:::tip
|
||||
This won't write or modify anything on your XenServer host: it will just import the XOA VM into your default storage repository.
|
||||
This won't write or modify anything on your XCP-ng/XenServer host: it will just import the XOA VM into your default storage repository.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
If you are using an old XenServer version, you may get a `curl` error:
|
||||
If you are using an old XCP-ng/XenServer version, you may get a `curl` error:
|
||||
|
||||
```
|
||||
curl: (35) error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version
|
||||
|
||||
@@ -157,32 +157,33 @@ function extractFlags(args) {
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
function parseValue(value) {
|
||||
if (value.startsWith('json:')) {
|
||||
return JSON.parse(value.slice(5))
|
||||
}
|
||||
if (value === 'true') {
|
||||
return true
|
||||
}
|
||||
if (value === 'false') {
|
||||
return false
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const PARAM_RE = /^([^=]+)=([^]*)$/
|
||||
function parseParameters(args) {
|
||||
if (args[0] === '--') {
|
||||
return args.slice(1).map(parseValue)
|
||||
}
|
||||
|
||||
const params = {}
|
||||
forEach(args, function (arg) {
|
||||
let matches
|
||||
if (!(matches = arg.match(PARAM_RE))) {
|
||||
throw new Error('invalid arg: ' + arg)
|
||||
}
|
||||
params[matches[1]] = parseValue(matches[2])
|
||||
const name = matches[1]
|
||||
let value = matches[2]
|
||||
|
||||
if (value.startsWith('json:')) {
|
||||
value = JSON.parse(value.slice(5))
|
||||
}
|
||||
|
||||
if (name === '@') {
|
||||
params['@'] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (value === 'true') {
|
||||
value = true
|
||||
} else if (value === 'false') {
|
||||
value = false
|
||||
}
|
||||
|
||||
params[name] = value
|
||||
})
|
||||
|
||||
return params
|
||||
|
||||
@@ -11,7 +11,7 @@ import { defer } from 'golike-defer'
|
||||
import { format } from 'json-rpc-peer'
|
||||
import { FAIL_ON_QUEUE } from 'limit-concurrency-decorator'
|
||||
import { getStreamAsBuffer } from 'get-stream'
|
||||
import { ignoreErrors, timeout } from 'promise-toolbox'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
import { invalidParameters, noSuchObject, unauthorized } from 'xo-common/api-errors.js'
|
||||
import { Ref } from 'xen-api'
|
||||
|
||||
@@ -1027,7 +1027,11 @@ start.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const stop = defer(async function ($defer, { vm, force, forceShutdownDelay, bypassBlockedOperation = force }) {
|
||||
// TODO: implements timeout.
|
||||
// - if !force → clean shutdown
|
||||
// - if force is true → hard shutdown
|
||||
// - if force is integer → clean shutdown and after force seconds, hard shutdown.
|
||||
export const stop = defer(async function ($defer, { vm, force, bypassBlockedOperation = force }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
if (bypassBlockedOperation) {
|
||||
@@ -1049,14 +1053,13 @@ export const stop = defer(async function ($defer, { vm, force, forceShutdownDela
|
||||
|
||||
// Clean shutdown
|
||||
try {
|
||||
await timeout.call(xapi.shutdownVm(vm._xapiRef), forceShutdownDelay, () =>
|
||||
xapi.shutdownVm(vm._xapiRef, { hard: true })
|
||||
)
|
||||
await xapi.shutdownVm(vm._xapiRef)
|
||||
} catch (error) {
|
||||
const { code } = error
|
||||
if (code === 'VM_MISSING_PV_DRIVERS' || code === 'VM_LACKS_FEATURE_SHUTDOWN') {
|
||||
throw invalidParameters('clean shutdown requires PV drivers')
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
})
|
||||
@@ -1064,7 +1067,6 @@ export const stop = defer(async function ($defer, { vm, force, forceShutdownDela
|
||||
stop.params = {
|
||||
id: { type: 'string' },
|
||||
force: { type: 'boolean', optional: true },
|
||||
forceShutdownDelay: { type: 'number', default: 0 },
|
||||
bypassBlockedOperation: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
@@ -1398,7 +1400,7 @@ export async function importMultipleFromEsxi({
|
||||
await asyncEach(
|
||||
vms,
|
||||
async vm => {
|
||||
await Task.run({ properties: { name: `importing vm ${vm}` } }, async () => {
|
||||
await Task.run({ data: { name: `importing vm ${vm}` } }, async () => {
|
||||
try {
|
||||
const vmUuid = await this.migrationfromEsxi({
|
||||
host,
|
||||
|
||||
@@ -190,8 +190,6 @@ export default class Redis extends Collection {
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
model = this._unserialize(model) ?? model
|
||||
model.id = id
|
||||
return model
|
||||
})
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ export class PluginsMetadata extends Collection {
|
||||
throw new Error('no such plugin metadata')
|
||||
}
|
||||
|
||||
await this.update({
|
||||
return /* await */ this.update({
|
||||
...pluginMetadata,
|
||||
...data,
|
||||
})
|
||||
|
||||
@@ -251,19 +251,9 @@ export default class Api {
|
||||
|
||||
constructor(app) {
|
||||
this._logger = null
|
||||
this._methods = { __proto__: null }
|
||||
this._app = app
|
||||
|
||||
const defer =
|
||||
const seq = async methods => {
|
||||
for (const method of methods) {
|
||||
await this.#callApiMethod(method[0], method[1])
|
||||
}
|
||||
}
|
||||
seq.validate = ajv.compile({ type: 'array', minLength: 1, items: { type: ['array', 'string'] } })
|
||||
const if =
|
||||
|
||||
this._methods = { __proto__: null, seq }
|
||||
|
||||
this.addApiMethods(methods)
|
||||
app.hooks.on('start', async () => {
|
||||
this._logger = await app.getLogger('api')
|
||||
@@ -377,7 +367,8 @@ export default class Api {
|
||||
}
|
||||
|
||||
async callApiMethod(connection, name, params = {}) {
|
||||
if (!Object.hasOwn(this._methods, name)) {
|
||||
const method = this._methods[name]
|
||||
if (!method) {
|
||||
throw new MethodNotFound(name)
|
||||
}
|
||||
|
||||
@@ -392,12 +383,11 @@ export default class Api {
|
||||
apiContext.permission = 'none'
|
||||
}
|
||||
|
||||
return this.#apiContext.run(apiContext, () => this.#callApiMethod(name, params))
|
||||
return this.#apiContext.run(apiContext, () => this.#callApiMethod(name, method, params))
|
||||
}
|
||||
|
||||
async #callApiMethod(name, params) {
|
||||
async #callApiMethod(name, method, params) {
|
||||
const app = this._app
|
||||
const method = this._methods[name]
|
||||
const startTime = Date.now()
|
||||
|
||||
const { connection, user } = this.apiContext
|
||||
|
||||
@@ -3,7 +3,11 @@ import { noSuchObject } from 'xo-common/api-errors.js'
|
||||
import Collection from '../collection/redis.mjs'
|
||||
import patch from '../patch.mjs'
|
||||
|
||||
class CloudConfigs extends Collection {}
|
||||
class CloudConfigs extends Collection {
|
||||
get(properties) {
|
||||
return super.get(properties)
|
||||
}
|
||||
}
|
||||
|
||||
export default class {
|
||||
constructor(app) {
|
||||
@@ -31,7 +35,7 @@ export default class {
|
||||
async updateCloudConfig({ id, name, template }) {
|
||||
const cloudConfig = await this.getCloudConfig(id)
|
||||
patch(cloudConfig, { name, template })
|
||||
await this._db.update(cloudConfig)
|
||||
return this._db.update(cloudConfig)
|
||||
}
|
||||
|
||||
deleteCloudConfig(id) {
|
||||
|
||||
@@ -19,31 +19,47 @@ const log = createLogger('xo:jobs')
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class JobsDb extends Collection {
|
||||
_serialize(job) {
|
||||
Object.keys(job).forEach(key => {
|
||||
const value = job[key]
|
||||
if (typeof value !== 'string') {
|
||||
job[key] = JSON.stringify(job[key])
|
||||
const normalize = job => {
|
||||
Object.keys(job).forEach(key => {
|
||||
try {
|
||||
const value = (job[key] = JSON.parse(job[key]))
|
||||
|
||||
// userId are always strings, even if the value is numeric, which might to
|
||||
// them being parsed as numbers.
|
||||
//
|
||||
// The issue has been introduced by
|
||||
// 48b2297bc151df582160be7c1bf1e8ee160320b8.
|
||||
if (key === 'userId' && typeof value === 'number') {
|
||||
job[key] = String(value)
|
||||
}
|
||||
})
|
||||
} catch (_) {}
|
||||
})
|
||||
return job
|
||||
}
|
||||
|
||||
const serialize = job => {
|
||||
Object.keys(job).forEach(key => {
|
||||
const value = job[key]
|
||||
if (typeof value !== 'string') {
|
||||
job[key] = JSON.stringify(job[key])
|
||||
}
|
||||
})
|
||||
return job
|
||||
}
|
||||
|
||||
class JobsDb extends Collection {
|
||||
async create(job) {
|
||||
return normalize(await this.add(serialize(job)))
|
||||
}
|
||||
|
||||
_unserialize(job) {
|
||||
Object.keys(job).forEach(key => {
|
||||
try {
|
||||
const value = (job[key] = JSON.parse(job[key]))
|
||||
async save(job) {
|
||||
await this.update(serialize(job))
|
||||
}
|
||||
|
||||
// userId are always strings, even if the value is numeric, which might to
|
||||
// them being parsed as numbers.
|
||||
//
|
||||
// The issue has been introduced by
|
||||
// 48b2297bc151df582160be7c1bf1e8ee160320b8.
|
||||
if (key === 'userId' && typeof value === 'number') {
|
||||
job[key] = String(value)
|
||||
}
|
||||
} catch (_) {}
|
||||
})
|
||||
async get(properties) {
|
||||
const jobs = await super.get(properties)
|
||||
jobs.forEach(normalize)
|
||||
return jobs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +90,7 @@ export default class Jobs {
|
||||
app.addConfigManager(
|
||||
'jobs',
|
||||
() => jobsDb.get(),
|
||||
jobs => jobsDb.update(jobs),
|
||||
jobs => Promise.all(jobs.map(job => jobsDb.save(job))),
|
||||
['users']
|
||||
)
|
||||
})
|
||||
@@ -134,7 +150,7 @@ export default class Jobs {
|
||||
}
|
||||
|
||||
createJob(job) {
|
||||
return this._jobs.add(job)
|
||||
return this._jobs.create(job)
|
||||
}
|
||||
|
||||
async updateJob(job, merge = true) {
|
||||
@@ -143,7 +159,7 @@ export default class Jobs {
|
||||
job = await this.getJob(id)
|
||||
patch(job, props)
|
||||
}
|
||||
await this._jobs.update(job)
|
||||
return /* await */ this._jobs.save(job)
|
||||
}
|
||||
|
||||
registerJobExecutor(type, executor) {
|
||||
@@ -171,7 +187,7 @@ export default class Jobs {
|
||||
|
||||
const runJobId = logger.notice(`Starting execution of ${id}.`, {
|
||||
data:
|
||||
type === 'backup' || type === 'metadataBackup' || type === 'mirrorBackup'
|
||||
type === 'backup' || type === 'metadataBackup'
|
||||
? {
|
||||
mode: job.mode,
|
||||
reportWhen: job.settings['']?.reportWhen ?? 'failure',
|
||||
|
||||
@@ -154,7 +154,7 @@ export default class MigrateVm {
|
||||
}
|
||||
|
||||
#connectToEsxi(host, user, password, sslVerify) {
|
||||
return Task.run({ properties: { name: `connecting to ${host}` } }, async () => {
|
||||
return Task.run({ data: { name: `connecting to ${host}` } }, async () => {
|
||||
const esxi = new Esxi(host, user, password, sslVerify)
|
||||
await fromEvent(esxi, 'ready')
|
||||
return esxi
|
||||
@@ -174,7 +174,7 @@ export default class MigrateVm {
|
||||
const app = this._app
|
||||
const esxi = await this.#connectToEsxi(host, user, password, sslVerify)
|
||||
|
||||
const esxiVmMetadata = await Task.run({ properties: { name: `get metadata of ${vmId}` } }, async () => {
|
||||
const esxiVmMetadata = await Task.run({ data: { name: `get metadata of ${vmId}` } }, async () => {
|
||||
return esxi.getTransferableVmMetadata(vmId)
|
||||
})
|
||||
|
||||
@@ -182,7 +182,7 @@ export default class MigrateVm {
|
||||
const isRunning = powerState !== 'poweredOff'
|
||||
|
||||
const chainsByNodes = await Task.run(
|
||||
{ properties: { name: `build disks and snapshots chains for ${vmId}` } },
|
||||
{ data: { name: `build disks and snapshots chains for ${vmId}` } },
|
||||
async () => {
|
||||
return this.#buildDiskChainByNode(disks, snapshots)
|
||||
}
|
||||
@@ -191,7 +191,7 @@ export default class MigrateVm {
|
||||
const sr = app.getXapiObject(srId)
|
||||
const xapi = sr.$xapi
|
||||
|
||||
const vm = await Task.run({ properties: { name: 'creating MV on XCP side' } }, async () => {
|
||||
const vm = await Task.run({ data: { name: 'creating MV on XCP side' } }, async () => {
|
||||
// got data, ready to start creating
|
||||
const vm = await xapi._getOrWaitObject(
|
||||
await xapi.VM_create({
|
||||
@@ -236,7 +236,7 @@ export default class MigrateVm {
|
||||
|
||||
const vhds = await Promise.all(
|
||||
Object.keys(chainsByNodes).map(async (node, userdevice) =>
|
||||
Task.run({ properties: { name: `Cold import of disks ${node}` } }, async () => {
|
||||
Task.run({ data: { name: `Cold import of disks ${node}` } }, async () => {
|
||||
const chainByNode = chainsByNodes[node]
|
||||
const vdi = await xapi._getOrWaitObject(
|
||||
await xapi.VDI_create({
|
||||
@@ -289,11 +289,11 @@ export default class MigrateVm {
|
||||
|
||||
if (isRunning && stopSource) {
|
||||
// it the vm was running, we stop it and transfer the data in the active disk
|
||||
await Task.run({ properties: { name: 'powering down source VM' } }, () => esxi.powerOff(vmId))
|
||||
await Task.run({ data: { name: 'powering down source VM' } }, () => esxi.powerOff(vmId))
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(chainsByNodes).map(async (node, userdevice) => {
|
||||
await Task.run({ properties: { name: `Transfering deltas of ${userdevice}` } }, async () => {
|
||||
await Task.run({ data: { name: `Transfering deltas of ${userdevice}` } }, async () => {
|
||||
const chainByNode = chainsByNodes[node]
|
||||
const disk = chainByNode[chainByNode.length - 1]
|
||||
const { fileName, path, datastore, isFull } = disk
|
||||
@@ -322,7 +322,7 @@ export default class MigrateVm {
|
||||
)
|
||||
}
|
||||
|
||||
await Task.run({ properties: { name: 'Finishing transfer' } }, async () => {
|
||||
await Task.run({ data: { name: 'Finishing transfer' } }, async () => {
|
||||
// remove the importing in label
|
||||
await vm.set_name_label(esxiVmMetadata.name_label)
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class {
|
||||
app.addConfigManager(
|
||||
'remotes',
|
||||
() => this._remotes.get(),
|
||||
remotes => this._remotes.update(remotes)
|
||||
remotes => Promise.all(remotes.map(remote => this._remotes.update(remote)))
|
||||
)
|
||||
})
|
||||
app.hooks.on('start', async () => {
|
||||
|
||||
@@ -7,16 +7,23 @@ import { noSuchObject } from 'xo-common/api-errors.js'
|
||||
import Collection from '../collection/redis.mjs'
|
||||
import patch from '../patch.mjs'
|
||||
|
||||
const normalize = schedule => {
|
||||
const { enabled } = schedule
|
||||
if (typeof enabled !== 'boolean') {
|
||||
schedule.enabled = enabled === 'true'
|
||||
}
|
||||
if ('job' in schedule) {
|
||||
schedule.jobId = schedule.job
|
||||
delete schedule.job
|
||||
}
|
||||
return schedule
|
||||
}
|
||||
|
||||
class Schedules extends Collection {
|
||||
_unserialize(schedule) {
|
||||
const { enabled } = schedule
|
||||
if (typeof enabled !== 'boolean') {
|
||||
schedule.enabled = enabled === 'true'
|
||||
}
|
||||
if ('job' in schedule) {
|
||||
schedule.jobId = schedule.job
|
||||
delete schedule.job
|
||||
}
|
||||
async get(properties) {
|
||||
const schedules = await super.get(properties)
|
||||
schedules.forEach(normalize)
|
||||
return schedules
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +55,7 @@ export default class Scheduling {
|
||||
() => db.get(),
|
||||
schedules =>
|
||||
asyncMapSettled(schedules, async schedule => {
|
||||
await db.update(schedule)
|
||||
await db.update(normalize(schedule))
|
||||
this._start(schedule.id)
|
||||
}),
|
||||
['jobs']
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class {
|
||||
app.addConfigManager(
|
||||
'groups',
|
||||
() => groupsDb.get(),
|
||||
groups => groupsDb.update(groups),
|
||||
groups => Promise.all(groups.map(group => groupsDb.update(group))),
|
||||
['users']
|
||||
)
|
||||
app.addConfigManager(
|
||||
@@ -83,7 +83,10 @@ export default class {
|
||||
properties.pw_hash = await hash(password)
|
||||
}
|
||||
|
||||
return this._users.create(properties)
|
||||
// TODO: use plain objects
|
||||
const user = await this._users.create(properties)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
async deleteUser(id) {
|
||||
@@ -333,8 +336,11 @@ export default class {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
createGroup({ name, provider, providerGroupId }) {
|
||||
return this._groups.create(name, provider, providerGroupId)
|
||||
async createGroup({ name, provider, providerGroupId }) {
|
||||
// TODO: use plain objects.
|
||||
const group = await this._groups.create(name, provider, providerGroupId)
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
async deleteGroup(id) {
|
||||
|
||||
@@ -2703,8 +2703,6 @@ const messages = {
|
||||
proxiesNeedUpgrade: 'Some proxies need to be upgraded.',
|
||||
upgradeNeededForProxies: 'Some proxies need to be upgraded. Click here to get more information.',
|
||||
xoProxyConcreteGuide: 'XO Proxy: a concrete guide',
|
||||
someProxiesHaveErrors:
|
||||
'{n, number} prox{n, plural, one {y} other {ies}} ha{n, plural, one {s} other {ve}} error{n, plural, one {} other {s}}',
|
||||
|
||||
// ----- Utils -----
|
||||
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',
|
||||
|
||||
@@ -512,14 +512,7 @@ subscribeHostMissingPatches.forceRefresh = host => {
|
||||
const proxiesApplianceUpdaterState = {}
|
||||
export const subscribeProxiesApplianceUpdaterState = (proxyId, cb) => {
|
||||
if (proxiesApplianceUpdaterState[proxyId] === undefined) {
|
||||
proxiesApplianceUpdaterState[proxyId] = createSubscription(async () => {
|
||||
try {
|
||||
return await getProxyApplianceUpdaterState(proxyId)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { state: 'error' }
|
||||
}
|
||||
})
|
||||
proxiesApplianceUpdaterState[proxyId] = createSubscription(() => getProxyApplianceUpdaterState(proxyId))
|
||||
}
|
||||
return proxiesApplianceUpdaterState[proxyId](cb)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
getXoaState,
|
||||
isAdmin,
|
||||
} from 'selectors'
|
||||
import { countBy, every, forEach, identity, isEmpty, isEqual, map, pick, size, some } from 'lodash'
|
||||
import { every, forEach, identity, isEmpty, isEqual, map, pick, size, some } from 'lodash'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@@ -111,10 +111,7 @@ export default class Menu extends Component {
|
||||
() => this.state.proxyStates,
|
||||
proxyStates => some(proxyStates, state => state.endsWith('-upgrade-needed'))
|
||||
)
|
||||
_getNProxiesErrors = createSelector(
|
||||
() => this.state.proxyStates,
|
||||
proxyStates => countBy(proxyStates).error
|
||||
)
|
||||
|
||||
_checkPermissions = createSelector(
|
||||
() => this.props.isAdmin,
|
||||
() => this.props.permissions,
|
||||
@@ -216,7 +213,6 @@ export default class Menu extends Component {
|
||||
const noOperatablePools = this._getNoOperatablePools()
|
||||
const noResourceSets = this._getNoResourceSets()
|
||||
const noNotifications = this._getNoNotifications()
|
||||
const nProxiesErrors = this._getNProxiesErrors()
|
||||
|
||||
const missingPatchesWarning = this._hasMissingPatches() ? (
|
||||
<Tooltip content={_('homeMissingPatches')}>
|
||||
@@ -472,10 +468,6 @@ export default class Menu extends Component {
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : nProxiesErrors > 0 ? (
|
||||
<Tooltip content={_('someProxiesHaveErrors', { n: nProxiesErrors })}>
|
||||
<span className='tag tag-pill tag-danger'>{nProxiesErrors}</span>
|
||||
</Tooltip>
|
||||
) : null,
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user