{`hosts/${object.$container}`}>{object.name_label}</Link>\n    case 'host':\n      return <Link to={`hosts/${object.id}`}>{object.name_label}</Link>\n    default:\n      return null\n  }\n})\n\nconst AlarmColPool = connectStore(() => ({\n  pool: createGetObject(),\n}))(({ pool }) => {\n  if (!pool) {\n    return null\n  }\n  return <Link to={`pools/${pool.id}`}>{pool.name_label}</Link>\n})\n\nconst DUPLICATED_MAC_ADDRESSES_COLUMNS = [\n  {\n    name: _('vifMacLabel'),\n    itemRenderer: ({ mac }) => <pre>{mac}</pre>,\n    sortCriteria: ({ mac }) => mac,\n  },\n  {\n    name: _('vifs'),\n    itemRenderer: ({ vifs }) => (\n      <div>\n        {vifs.map(vif => (\n          <Row key={vif.id}>\n            <Col>\n              {_('vifOnVmWithNetwork', {\n                network: <Network id={vif.$network} />,\n                vifDevice: vif.device,\n                vm: <Vm id={vif.$VM} link />,\n              })}\n            </Col>\n          </Row>\n        ))}\n      </div>\n    ),\n  },\n]\n\nconst DUPLICATED_MAC_ADDRESSES_FILTERS = {\n  filterOnlyRunningVms: 'nRunningVms:>1',\n}\n\nconst LOCAL_DEFAULT_SRS_COLUMNS = [\n  {\n    name: _('pool'),\n    itemRenderer: pool => <Pool id={pool.id} link />,\n    sortCriteria: 'name_label',\n  },\n  {\n    name: _('sr'),\n    itemRenderer: pool => <Sr container={false} id={pool.default_SR} link spaceLeft={false} />,\n  },\n  {\n    name: _('host'),\n    itemRenderer: (pool, { srs }) => <Host id={srs[pool.default_SR].$container} link pool={false} />,\n    sortCriteria: (pool, { hosts, srs }) => hosts[srs[pool.default_SR].$container].name_label,\n  },\n]\n\nconst POOLS_WITHOUT_DEFAULT_SR_COLUMNS = [\n  {\n    name: _('pool'),\n    itemRenderer: pool => <Pool id={pool.id} link />,\n    sortCriteria: 'name_label',\n  },\n]\n\nconst SR_COLUMNS = [\n  {\n    name: _('srName'),\n    itemRenderer: sr => sr.name_label,\n    sortCriteria: sr => sr.name_label,\n  },\n  {\n    name: _('srPool'),\n    itemRenderer: sr => <SrColContainer id={sr.$container} />,\n  },\n  {\n    name: _('srFormat'),\n    itemRenderer: sr => sr.SR_type,\n    sortCriteria: sr => sr.SR_type,\n  },\n  {\n    name: <span className='text-capitalize'>{_('srFree')}</span>,\n    itemRenderer: sr => formatSize(sr.size - sr.physical_usage),\n    sortCriteria: sr => sr.size - sr.physical_usage,\n  },\n  {\n    name: _('srSize'),\n    itemRenderer: sr => formatSize(sr.size),\n    sortCriteria: sr => sr.size,\n  },\n  {\n    default: true,\n    name: _('srUsage'),\n    itemRenderer: sr =>\n      sr.size > 1 && (\n        <Tooltip\n          content={_('spaceLeftTooltip', {\n            used: Math.round((sr.physical_usage / sr.size) * 100),\n            free: formatSize(sr.size - sr.physical_usage),\n          })}\n        >\n          <meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90' />\n        </Tooltip>\n      ),\n    sortCriteria: sr => sr.physical_usage / sr.size,\n    sortOrder: 'desc',\n  },\n]\n\nconst ORPHANED_VDI_COLUMNS = [\n  {\n    name: _('vdiNameLabel'),\n    itemRenderer: vdi => (\n      <span>\n        {vdi.name_label}\n        {vdi.type === 'VDI-snapshot' && (\n          <span className='tag tag-info ml-1'>\n            <Icon icon='vm-snapshot' />\n          </span>\n        )}\n      </span>\n    ),\n    sortCriteria: vdi => vdi.name_label,\n    default: true,\n  },\n  {\n    name: _('vdiNameDescription'),\n    itemRenderer: vdi => vdi.name_description,\n    sortCriteria: vdi => vdi.name_description,\n  },\n  {\n    name: _('snapshotDate'),\n    itemRenderer: vdi => {\n      if (vdi.type === 'VDI') {\n        // Normal VDIs don't have a creation date\n        return null\n      }\n\n      return (\n        <span>\n          <FormattedTime\n            day='numeric'\n            hour='numeric'\n            minute='numeric'\n            month='long'\n            value={vdi.snapshot_time * 1000}\n            year='numeric'\n          />{' '}\n          (<FormattedRelative value={vdi.snapshot_time * 1000} />)\n        </span>\n      )\n    },\n    sortCriteria: vdi => vdi.snapshot_time,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('vdiSize'),\n    itemRenderer: vdi => formatSize(vdi.size),\n    sortCriteria: vdi => vdi.size,\n  },\n  {\n    name: _('vdiSr'),\n    itemRenderer: vdi => <Sr id={vdi.$SR} link spaceLeft={false} />,\n  },\n]\n\nconst ORPHAN_VDI_FILTERS = {\n  filterOnlySnapshots: 'type:VDI-snapshot',\n  filterOnlyRegular: 'type:/^VDI$/',\n}\n\nconst ORPHANED_VDI_ACTIONS = [\n  {\n    handler: deleteOrphanedVdis,\n    individualHandler: deleteVdi,\n    individualLabel: _('deleteOrphanedVdi'),\n    icon: 'delete',\n    label: _('deleteSelectedOrphanedVdis'),\n    level: 'danger',\n  },\n]\n\nconst ORPHANED_VDI_INDIVIDUAL_ACTIONS = [\n  {\n    handler: vdi => copy(vdi.uuid),\n    icon: 'clipboard',\n    label: vdi => _('copyUuid', { uuid: vdi.uuid }),\n  },\n]\n\nconst CONTROL_DOMAIN_VDIS_ACTIONS = [\n  {\n    handler: deleteVbds,\n    individualHandler: deleteVbd,\n    icon: 'vdi-forget',\n    label: _('vdiForget'),\n    level: 'danger',\n  },\n]\n\nconst AttachedVdisTable = decorate([\n  connectStore({\n    pools: createGetObjectsOfType('pool'),\n    srs: createGetObjectsOfType('SR'),\n    vbds: createGetObjectsOfType('VBD').pick(\n      createSelector(\n        createFilter(createGetObjectsOfType('VM-controller'), (_, props) => props.poolPredicate),\n        createCollectionWrapper(vmControllers => flatten(map(vmControllers, '$VBDs')))\n      )\n    ),\n    vdis: createGetObjectsOfType('VDI'),\n    vdiSnapshots: createGetObjectsOfType('VDI-snapshot'),\n  }),\n  ({ columns, rowTransform }) =>\n    ({ pools, srs, vbds, vdis, vdiSnapshots }) => (\n      <NoObjects\n        actions={CONTROL_DOMAIN_VDIS_ACTIONS}\n        collection={vbds}\n        columns={columns}\n        component={SortedTable}\n        data-pools={pools}\n        data-srs={srs}\n        data-vdis={vdis}\n        data-vdiSnapshots={vdiSnapshots}\n        emptyMessage={_('noControlDomainVdis')}\n        rowTransform={rowTransform}\n        stateUrlParam='s_controldomain'\n      />\n    ),\n  {\n    columns: [\n      {\n        name: _('vdiNameLabel'),\n        itemRenderer: ({ vdi }) => (\n          <span>\n            {vdi.name_label}\n            {vdi.type === 'VDI-snapshot' && [' ', <Icon icon='vm-snapshot' key='1' />]}\n          </span>\n        ),\n        sortCriteria: ({ vdi }) => vdi.name_label,\n      },\n      {\n        name: _('vdiNameDescription'),\n        itemRenderer: ({ vdi }) => vdi.name_description,\n        sortCriteria: ({ vdi }) => vdi.name_description,\n      },\n      {\n        name: _('vdiPool'),\n        itemRenderer: ({ pool }) =>\n          pool === undefined ? null : <Link to={`pools/${pool.id}`}>{pool.name_label}</Link>,\n        sortCriteria: ({ pool }) => pool != null && pool.name_label,\n      },\n      {\n        name: _('vdiSize'),\n        itemRenderer: ({ vdi }) => formatSize(vdi.size),\n        sortCriteria: ({ vdi }) => vdi.size,\n      },\n      {\n        name: _('vdiSr'),\n        itemRenderer: ({ sr }) => (sr === undefined ? null : <Sr id={sr.id} link spaceLeft={false} />),\n        sortCriteria: ({ sr }) => sr != null && sr.name_label,\n      },\n    ],\n    rowTransform: (vbd, { pools, srs, vdis, vdiSnapshots }) => {\n      const vdi = vdis[vbd.VDI] || vdiSnapshots[vbd.VDI]\n\n      return {\n        id: vbd.id,\n        vbd,\n        vdi,\n        sr: srs[vdi.$SR],\n        pool: pools[vbd.$poolId],\n      }\n    },\n  },\n])\n\nconst VM_COLUMNS = [\n  {\n    name: _('snapshotDate'),\n    itemRenderer: vm => (\n      <span>\n        <FormattedTime\n          day='numeric'\n          hour='numeric'\n          minute='numeric'\n          month='long'\n          value={vm.snapshot_time * 1000}\n          year='numeric'\n        />{' '}\n        (<FormattedRelative value={vm.snapshot_time * 1000} />)\n      </span>\n    ),\n    sortCriteria: vm => vm.snapshot_time,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('vmNameLabel'),\n    itemRenderer: vm => vm.name_label,\n    sortCriteria: vm => vm.name_label,\n  },\n  {\n    name: _('vmNameDescription'),\n    itemRenderer: vm => vm.name_description,\n    sortCriteria: vm => vm.name_description,\n  },\n  {\n    name: _('vmContainer'),\n    itemRenderer: vm => <VmColContainer id={vm.$container} />,\n  },\n]\n\nconst VM_ACTIONS = [\n  {\n    handler: deleteVms,\n    individualHandler: deleteVm,\n    individualLabel: _('deleteVmLabel'),\n    icon: 'delete',\n    label: _('deleteSelectedVmsLabel'),\n    level: 'danger',\n  },\n]\n\nconst TOO_MANY_SNAPSHOT_COLUMNS = [\n  {\n    name: _('vmNameLabel'),\n    itemRenderer: vm => <Link to={`vms/${vm.id}/snapshots`}>{vm.name_label}</Link>,\n    sortCriteria: vm => vm.name_label,\n  },\n  {\n    name: _('vmNameDescription'),\n    itemRenderer: vm => vm.name_description,\n    sortCriteria: vm => vm.name_description,\n  },\n  {\n    name: _('vmContainer'),\n    itemRenderer: vm => <VmColContainer id={vm.$container} />,\n  },\n  {\n    default: true,\n    name: _('numberOfSnapshots'),\n    itemRenderer: vm => vm.snapshots.length,\n    sortCriteria: vm => vm.snapshots.length,\n    sortOrder: 'desc',\n  },\n]\n\nconst GUEST_TOOLS_COLUMNS = [\n  {\n    name: _('vmNameLabel'),\n    itemRenderer: vm => <Link to={`vms/${vm.id}`}>{vm.name_label}</Link>,\n    sortCriteria: vm => vm.name_label,\n  },\n  {\n    name: _('vmNameDescription'),\n    itemRenderer: vm => vm.name_description,\n    sortCriteria: vm => vm.name_description,\n  },\n  {\n    name: _('vmContainer'),\n    itemRenderer: vm => <VmColContainer id={vm.$container} />,\n  },\n  {\n    default: true,\n    name: _('guestToolStatusColumn'),\n    itemRenderer: vm => {\n      if (!vm.pvDriversDetected) {\n        return _('noToolsDetected')\n      }\n      if (!vm.managementAgentDetected) {\n        return _('managementAgentNotDetected')\n      }\n\n      const version = getDefined(() => vm.pvDriversVersion.split('.')[0]) > 0 ? vm.pvDriversVersion : ''\n\n      return _('managementAgentOutOfDate', {\n        version,\n      })\n    },\n    sortCriteria: vm => (!vm.pvDriversDetected ? 0 : !vm.managementAgentDetected ? 1 : 2),\n  },\n]\n\nconst ALARM_COLUMNS = [\n  {\n    name: _('alarmDate'),\n    itemRenderer: message => (\n      <span>\n        <FormattedTime\n          day='numeric'\n          hour='numeric'\n          minute='numeric'\n          month='long'\n          value={message.time * 1000}\n          year='numeric'\n        />{' '}\n        (<FormattedRelative value={message.time * 1000} />)\n      </span>\n    ),\n    sortCriteria: message => message.time,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('alarmContent'),\n    itemRenderer: ({ formatted, body }) =>\n      formatted ? (\n        <div>\n          <Row>\n            <Col mediumSize={6}>\n              <strong>{formatted.name}</strong>\n            </Col>\n            <Col mediumSize={6}>{formatted.value}</Col>\n          </Row>\n          <br />\n          {map(formatted.alarmAttributes, (value, label) => (\n            <Row>\n              <Col mediumSize={6}>{label}</Col>\n              <Col mediumSize={6}>{value}</Col>\n            </Row>\n          ))}\n        </div>\n      ) : (\n        <pre style={{ whiteSpace: 'pre-wrap' }}>{body}</pre>\n      ),\n    sortCriteria: message => message.body,\n  },\n  {\n    name: _('alarmObject'),\n    itemRenderer: message => <AlarmColObject id={message.$object} />,\n  },\n  {\n    name: _('alarmPool'),\n    itemRenderer: message => <AlarmColPool id={message.$pool} />,\n  },\n]\n\nconst ALARM_ACTIONS = [\n  {\n    handler: deleteMessages,\n    individualHandler: deleteMessage,\n    individualLabel: _('logDelete'),\n    icon: 'delete',\n    label: _('logsDelete'),\n    level: 'danger',\n  },\n]\n\nconst HANDLED_VDI_TYPES = new Set(['system', 'user', 'ephemeral'])\n\nconst THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000\n\n@addSubscriptions({\n  schedules: cb =>\n    subscribeSchedules(schedules => {\n      cb(keyBy(schedules, 'id'))\n    }),\n})\n@connectStore(() => {\n  const getSrs = createGetObjectsOfType('SR')\n  const getOrphanVdis = createSort(\n    createFilter(\n      createSelector(createGetObjectsOfType('VDI'), createGetObjectsOfType('VDI-snapshot'), (vdis, snapshotVdis) =>\n        Object.assign({}, vdis, snapshotVdis)\n      ),\n      createSelector(getSrs, srs => vdi => {\n        if (\n          vdi.$VBDs.length !== 0 || // vdi with a vbd aren't orphans\n          !HANDLED_VDI_TYPES.has(vdi.VDI_type) || // only for vdi with handled types\n          vdi.size === 0 || // empty vdi aren't considered as orphans\n          (vdi.name_label === 'PVS cache VDI' && vdi.name_description === 'PVS cache VDI') // see https://github.com/vatesfr/xen-orchestra/issues/7938\n        ) {\n          return false\n        }\n\n        const sr = srs[vdi.$SR]\n        return (\n          sr !== undefined &&\n          // Condition copied from iso-device.js\n          sr.SR_type !== 'iso' &&\n          (sr.SR_type !== 'udev' || !sr.size)\n        )\n      })\n    )\n  )\n  const getOrphanVmSnapshots = createGetObjectsOfType('VM-snapshot')\n    .filter([snapshot => !snapshot.$snapshot_of])\n    .sort()\n  const getVms = createGetObjectsOfType('VM')\n  const MAX_HEALTHY_SNAPSHOT_COUNT = 5\n  const getTooManySnapshotsVms = getVms.filter([vm => vm.snapshots.length > MAX_HEALTHY_SNAPSHOT_COUNT]).sort()\n  const getGuestToolsVms = getVms\n    .filter([vm => vm.power_state === 'Running' && (!vm.managementAgentDetected || !vm.pvDriversUpToDate)])\n    .sort()\n  const getUserSrs = getSrs.filter([isSrWritable])\n  const getAlertMessages = createGetObjectsOfType('message').filter([\n    message => ['ALARM', 'BOND_STATUS_CHANGED', 'MULTIPATH_PERIODIC_ALERT'].includes(message.name),\n  ])\n  const getVifsByMac = createGetObjectsOfType('VIF')\n    .pick(\n      createCollectionWrapper(\n        createSelector(getVms, vms => {\n          const vifs = []\n          forEach(vms, ({ blockedOperations: ops, VIFs }) => {\n            if (!('start' in ops && 'start_on' in ops)) {\n              Array.prototype.push.apply(vifs, VIFs)\n            }\n          })\n          return vifs.sort()\n        })\n      )\n    )\n    .groupBy('MAC')\n\n  return {\n    alertMessages: getAlertMessages,\n    areObjectsFetched,\n    hosts: createGetObjectsOfType('host'),\n    orphanVdis: getOrphanVdis,\n    orphanVmSnapshots: getOrphanVmSnapshots,\n    snapshots: createGetObjectsOfType('VM-snapshot'),\n    pools: createGetObjectsOfType('pool'),\n    tooManySnapshotsVms: getTooManySnapshotsVms,\n    guestToolsVms: getGuestToolsVms,\n    userSrs: getUserSrs,\n    vifsByMac: getVifsByMac,\n    vms: getVms,\n  }\n})\nexport default class Health extends Component {\n  state = {\n    pools: [],\n  }\n\n  componentWillReceiveProps(props) {\n    if (props.alertMessages !== this.props.alertMessages) {\n      this._updateAlarms(props)\n    }\n  }\n\n  componentDidMount() {\n    this._updateAlarms(this.props)\n  }\n\n  _updateAlarms = props => {\n    formatLogs(props.alertMessages).then(formattedMessages => {\n      this.setState({\n        messages: map(formattedMessages, ({ id, ...formattedMessage }) => ({\n          formatted: formattedMessage,\n          ...props.alertMessages[id],\n        })),\n      })\n    }, noop)\n  }\n\n  _getSrUrl = sr => `srs/${sr.id}`\n\n  _getDuplicatedMacAddresses = createCollectionWrapper(\n    createSelector(\n      () => this._getVifsByMac(),\n      () => this.props.vms,\n      (vifsByMac, vms) => {\n        const duplicatedMacAddresses = []\n        for (const [macAddress, vifs] of Object.entries(vifsByMac)) {\n          if (vifs.length > 1) {\n            duplicatedMacAddresses.push({\n              mac: macAddress,\n              nRunningVms: vifs.filter(vif => vms[vif.$VM].power_state === 'Running').length,\n              vifs,\n            })\n          }\n        }\n        return duplicatedMacAddresses.sort()\n      }\n    )\n  )\n\n  _getPoolIds = createCollectionWrapper(createSelector(() => this.state.pools, resolveIds))\n\n  _getSelectedPools = createCollectionWrapper(\n    createSelector(\n      () => this.props.pools,\n      this._getPoolIds,\n      (pools, poolIds) => (isEmpty(poolIds) ? pools : pick(pools, poolIds))\n    )\n  )\n\n  _getLocalDefaultSrs = createCollectionWrapper(\n    createSelector(\n      () => this.props.hosts,\n      () => this.props.userSrs,\n      this._getSelectedPools,\n      (hosts, userSrs, selectedPools) => {\n        const nbHostsPerPool = countBy(hosts, host => host.$pool)\n        return filter(selectedPools, pool => {\n          const { default_SR } = pool\n          return (\n            default_SR !== undefined &&\n            userSrs[default_SR] !== undefined &&\n            !userSrs[default_SR].shared &&\n            nbHostsPerPool[pool.id] > 1\n          )\n        })\n      }\n    )\n  )\n\n  _getPoolsWithNoDefaultSr = createCollectionWrapper(\n    createSelector(this._getSelectedPools, selectedPools =>\n      filter(selectedPools, ({ default_SR }) => default_SR === undefined)\n    )\n  )\n\n  _getPoolPredicate = createSelector(this._getPoolIds, poolIds =>\n    isEmpty(poolIds) ? undefined : item => includes(poolIds, item.$pool)\n  )\n\n  _getUserSrs = createFilter(() => this.props.userSrs, this._getPoolPredicate)\n\n  _getOrphanVdis = createFilter(() => this.props.orphanVdis, this._getPoolPredicate)\n\n  _getOrphanVmSnapshots = createFilter(() => this.props.orphanVmSnapshots, this._getPoolPredicate)\n\n  _getOldSnapshots = createSelector(\n    () => this.props.snapshots,\n    () => this.props.schedules,\n    (snapshots, schedules) => {\n      const thresholdDate = Date.now() - THIRTY_DAYS\n      return Object.values(snapshots).filter(snapshot => {\n        if (snapshot.snapshot_time * 1000 > thresholdDate) {\n          return false\n        }\n\n        const scheduleId = snapshot.other?.['xo:backup:schedule']\n        const schedule = scheduleId !== undefined ? schedules?.[scheduleId] : undefined\n\n        return !schedule?.enabled\n      })\n    }\n  )\n\n  _getTooManySnapshotsVms = createFilter(() => this.props.tooManySnapshotsVms, this._getPoolPredicate)\n\n  _getGuestToolsVms = createFilter(() => this.props.guestToolsVms, this._getPoolPredicate)\n\n  _getAlertMessages = createFilter(() => this.props.alertMessages, this._getPoolPredicate)\n\n  _getMessages = createFilter(() => this.state.messages, this._getPoolPredicate)\n\n  _getVifsByMac = createFilter(\n    () => this.props.vifsByMac,\n    createSelector(this._getPoolIds, poolIds =>\n      isEmpty(poolIds) ? undefined : vifs => vifs.some(vif => poolIds.includes(vif.$pool))\n    )\n  )\n\n  render() {\n    const { props, state } = this\n\n    const duplicatedMacAddresses = this._getDuplicatedMacAddresses()\n    const localDefaultSrs = this._getLocalDefaultSrs()\n    const userSrs = this._getUserSrs()\n    const orphanVdis = this._getOrphanVdis()\n    const poolsWithNoDefaultSr = this._getPoolsWithNoDefaultSr()\n\n    return (\n      <Container>\n        <Row className='mb-1'>\n          <SelectPool multi onChange={this.linkState('pools')} value={state.pools} />\n        </Row>\n        <Row>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='disk' /> {_('srStatePanel')}\n              </CardHeader>\n              <CardBlock>\n                <NoObjects collection={props.areObjectsFetched ? userSrs : null} emptyMessage={_('noSrs')}>\n                  {() => (\n                    <Row>\n                      <Col>\n                        <SortedTable\n                          collection={userSrs}\n                          columns={SR_COLUMNS}\n                          rowLink={this._getSrUrl}\n                          shortcutsTarget='body'\n                          stateUrlParam='s_srs'\n                        />\n                      </Col>\n                    </Row>\n                  )}\n                </NoObjects>\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        {localDefaultSrs.length > 0 && (\n          <Row>\n            <Col>\n              <Card>\n                <CardHeader>\n                  <Icon icon='disk' /> {_('localDefaultSrs')}\n                </CardHeader>\n                <CardBlock>\n                  <p>\n                    <Icon icon='info' /> <em>{_('localDefaultSrsStatusTip')}</em>\n                  </p>\n                  <NoObjects\n                    collection={props.areObjectsFetched ? localDefaultSrs : null}\n                    emptyMessage={_('noLocalDefaultSrs')}\n                  >\n                    {() => (\n                      <Row>\n                        <Col>\n                          <SortedTable\n                            collection={localDefaultSrs}\n                            columns={LOCAL_DEFAULT_SRS_COLUMNS}\n                            data-hosts={props.hosts}\n                            data-srs={userSrs}\n                            shortcutsTarget='body'\n                            stateUrlParam='s_local_default_srs'\n                          />\n                        </Col>\n                      </Row>\n                    )}\n                  </NoObjects>\n                </CardBlock>\n              </Card>\n            </Col>\n          </Row>\n        )}\n        {poolsWithNoDefaultSr.length > 0 && (\n          <Row>\n            <Col>\n              <Card>\n                <CardHeader>\n                  <Icon icon='pool' /> {_('poolsWithNoDefaultSr')}\n                </CardHeader>\n                <CardBlock>\n                  <NoObjects collection={props.areObjectsFetched ? poolsWithNoDefaultSr : null}>\n                    {() => (\n                      <Row className='no-default-sr'>\n                        <Col>\n                          <SortedTable\n                            collection={poolsWithNoDefaultSr}\n                            columns={POOLS_WITHOUT_DEFAULT_SR_COLUMNS}\n                            data-hosts={props.hosts}\n                            data-srs={userSrs}\n                            shortcutsTarget='.no-default-sr'\n                            stateUrlParam='s_no_default_sr'\n                          />\n                        </Col>\n                      </Row>\n                    )}\n                  </NoObjects>\n                </CardBlock>\n              </Card>\n            </Col>\n          </Row>\n        )}\n        {props.areObjectsFetched && <UnhealthyVdis />}\n        <Row>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='disk' /> {_('orphanedVdis')}\n              </CardHeader>\n              <CardBlock>\n                <p>\n                  <Icon icon='info' /> <em>{_('orphanVdisTip')}</em>\n                </p>\n                <NoObjects\n                  collection={props.areObjectsFetched ? orphanVdis : null}\n                  emptyMessage={_('noOrphanedObject')}\n                >\n                  {() => (\n                    <SortedTable\n                      actions={ORPHANED_VDI_ACTIONS}\n                      collection={orphanVdis}\n                      columns={ORPHANED_VDI_COLUMNS}\n                      filters={ORPHAN_VDI_FILTERS}\n                      individualActions={ORPHANED_VDI_INDIVIDUAL_ACTIONS}\n                      stateUrlParam='s_vdis'\n                    />\n                  )}\n                </NoObjects>\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='disk' /> {_('vdisOnControlDomain')}\n              </CardHeader>\n              <CardBlock>\n                <AttachedVdisTable poolPredicate={this._getPoolPredicate()} />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='vm' /> {_('oldSnapshots')}\n              </CardHeader>\n              <CardBlock>\n                <NoObjects\n                  actions={VM_ACTIONS}\n                  collection={props.areObjectsFetched ? this._getOldSnapshots() : null}\n                  columns={VM_COLUMNS}\n                  component={SortedTable}\n                  emptyMessage={_('noOldSnapshots')}\n                />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row className='orphaned-vms'>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='vm' /> {_('orphanedVms')}\n              </CardHeader>\n              <CardBlock>\n                <NoObjects\n                  actions={VM_ACTIONS}\n                  collection={props.areObjectsFetched ? this._getOrphanVmSnapshots() : null}\n                  columns={VM_COLUMNS}\n                  component={SortedTable}\n                  emptyMessage={_('noOrphanedObject')}\n                  shortcutsTarget='.orphaned-vms'\n                  stateUrlParam='s_orphan_vms'\n                />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row className='too-many-snapshots-vms'>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='vm-snapshot' /> {_('tooManySnapshots')}\n              </CardHeader>\n              <CardBlock>\n                <p>\n                  <Icon icon='info' /> <em>{_('tooManySnapshotsTip')}</em>\n                </p>\n                <NoObjects\n                  collection={props.areObjectsFetched ? this._getTooManySnapshotsVms() : null}\n                  columns={TOO_MANY_SNAPSHOT_COLUMNS}\n                  component={SortedTable}\n                  emptyMessage={_('noTooManySnapshotsObject')}\n                  shortcutsTarget='.too-many-snapshots-vms'\n                  stateUrlParam='s_too_many_snapshots_vms'\n                />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='network' /> {_('duplicatedMacAddresses')}\n              </CardHeader>\n              <CardBlock>\n                <NoObjects\n                  collection={props.areObjectsFetched ? duplicatedMacAddresses : null}\n                  columns={DUPLICATED_MAC_ADDRESSES_COLUMNS}\n                  component={SortedTable}\n                  emptyMessage={_('noDuplicatedMacAddresses')}\n                  filters={DUPLICATED_MAC_ADDRESSES_FILTERS}\n                  stateUrlParam='s_duplicated_mac_addresses'\n                />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row className='guest-tools-vms'>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='administration' /> {_('guestToolStatus')}\n              </CardHeader>\n              <CardBlock>\n                <p>\n                  <Icon icon='info' /> <em>{_('guestToolStatusTip')}</em>\n                </p>\n                <NoObjects\n                  collection={props.areObjectsFetched ? this._getGuestToolsVms() : null}\n                  columns={GUEST_TOOLS_COLUMNS}\n                  component={SortedTable}\n                  emptyMessage={_('noGuestToolStatusObject')}\n                  shortcutsTarget='.guest-tools-vms'\n                  stateUrlParam='s_guest_tools_vms'\n                />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='alarm' /> {_('alarmMessage')}\n              </CardHeader>\n              <CardBlock>\n                <NoObjects\n                  collection={props.areObjectsFetched ? this._getAlertMessages() : null}\n                  emptyMessage={_('noAlarms')}\n                >\n                  {() => (\n                    <SortedTable\n                      actions={ALARM_ACTIONS}\n                      collection={this._getMessages()}\n                      columns={ALARM_COLUMNS}\n                      stateUrlParam='s_alarm'\n                    />\n                  )}\n                </NoObjects>\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport SortedTable from 'sorted-table'\nimport Tooltip from 'tooltip'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { connectStore } from 'utils'\nimport { Col, Row } from 'grid'\nimport { createGetObjectsOfType } from 'selectors'\nimport { injectState, provideState } from 'reaclette'\nimport { forEach, isEmpty, map } from 'lodash'\nimport { Sr, Vdi } from 'render-xo-item'\nimport { subscribeSrsUnhealthyVdiChainsLength, VDIS_TO_COALESCE_LIMIT } from 'xo'\n\nconst COLUMNS = [\n  {\n    itemRenderer: (srId, { vdisHealthBySr }) => (\n      <div>\n        <Sr id={srId} link />{' '}\n        {vdisHealthBySr[srId].nUnhealthyVdis >= VDIS_TO_COALESCE_LIMIT && (\n          <Tooltip content={_('srVdisToCoalesceWarning', { nVdis: vdisHealthBySr[srId].nUnhealthyVdis })}>\n            <span className='text-warning'>\n              <Icon icon='alarm' />\n            </span>\n          </Tooltip>\n        )}\n      </div>\n    ),\n    name: _('sr'),\n    sortCriteria: 'name_label',\n  },\n  {\n    itemRenderer: (srId, { vdisHealthBySr }) => (\n      <div>\n        {map(vdisHealthBySr[srId].unhealthyVdis, (unhealthyVdiLength, vdiId) => (\n          <SingleLineRow key={vdiId}>\n            <Col>\n              <Vdi id={vdiId} />\n            </Col>\n            <Col>\n              <span>{_('length', { length: unhealthyVdiLength })}</span>\n            </Col>\n          </SingleLineRow>\n        ))}\n      </div>\n    ),\n    name: _('vdisToCoalesce'),\n  },\n  {\n    itemRenderer: (srId, { vdisHealthBySr }) => (\n      <div>\n        {Object.keys(vdisHealthBySr[srId].vdisWithUnknownVhdParent).map(vdiId => (\n          <Vdi id={vdiId} key={vdiId} />\n        ))}\n      </div>\n    ),\n    name: _('vdisWithInvalidVhdParent'),\n  },\n]\n\nconst UnhealthyVdis = decorate([\n  connectStore({\n    srs: createGetObjectsOfType('SR'),\n  }),\n  addSubscriptions({\n    vdisHealthBySr: subscribeSrsUnhealthyVdiChainsLength,\n  }),\n  provideState({\n    computed: {\n      srIds: (_, { srs, vdisHealthBySr = {} }) => {\n        const srIds = []\n        forEach(vdisHealthBySr, ({ unhealthyVdis, vdisWithUnknownVhdParent }, srId) => {\n          if ((srs[srId] !== undefined && !isEmpty(unhealthyVdis)) || vdisWithUnknownVhdParent.length > 0) {\n            srIds.push(srId)\n          }\n        })\n        return srIds\n      },\n    },\n  }),\n  injectState,\n  ({ state: { srIds }, vdisHealthBySr }) => (\n    <Row>\n      <Col>\n        <Card>\n          <CardHeader>\n            <Icon icon='disk' /> {_('unhealthyVdis')}\n          </CardHeader>\n          <CardBlock>\n            <Row>\n              <Col>\n                <SortedTable\n                  data-vdisHealthBySr={vdisHealthBySr}\n                  collection={srIds}\n                  columns={COLUMNS}\n                  stateUrlParam='s_unhealthy_vdis'\n                />\n              </Col>\n            </Row>\n          </CardBlock>\n        </Card>\n      </Col>\n    </Row>\n  ),\n])\n\nexport default UnhealthyVdis\n","import _ from 'intl'\nimport Icon from 'icon'\nimport Page from '../page'\nimport React from 'react'\nimport { routes } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { NavLink, NavTabs } from 'nav'\n\nimport Health from './health'\nimport Overview from './overview'\nimport Stats from './stats'\nimport Visualizations from './visualizations'\n\nconst HEADER = (\n  <Container>\n    <Row>\n      <Col mediumSize={3}>\n        <h2>\n          <Icon icon='menu-dashboard' /> {_('dashboardPage')}\n        </h2>\n      </Col>\n      <Col mediumSize={9}>\n        <NavTabs className='pull-right'>\n          <NavLink to='/dashboard/overview'>\n            <Icon icon='menu-dashboard-overview' /> {_('overviewDashboardPage')}\n          </NavLink>\n          <NavLink to='/dashboard/visualizations'>\n            <Icon icon='menu-dashboard-visualization' /> {_('overviewVisualizationDashboardPage')}\n          </NavLink>\n          <NavLink to='/dashboard/stats'>\n            <Icon icon='menu-dashboard-stats' /> {_('overviewStatsDashboardPage')}\n          </NavLink>\n          <NavLink to='/dashboard/health'>\n            <Icon icon='menu-dashboard-health' /> {_('overviewHealthDashboardPage')}\n          </NavLink>\n        </NavTabs>\n      </Col>\n    </Row>\n  </Container>\n)\n\nconst Dashboard = routes('overview', {\n  health: Health,\n  overview: Overview,\n  stats: Stats,\n  visualizations: Visualizations,\n})(({ children }) => (\n  <Page header={HEADER} title='dashboardPage' formatTitle>\n    {children}\n  </Page>\n))\n\nexport default Dashboard\n","module.exports = {\n    \"bigCardContent\": \"mcb58e8535_bigCardContent\"\n};","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport ButtonGroup from 'button-group'\nimport ChartistGraph from 'react-chartist'\nimport Component from 'base-component'\nimport HostsPatchesTable from 'hosts-patches-table'\nimport Icon from 'icon'\nimport Link, { BlockLink } from 'link'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport ResourceSetQuotas from 'resource-set-quotas'\nimport { addSubscriptions, connectStore, formatSize } from 'utils'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { Container, Row, Col } from 'grid'\nimport { compact, filter, forEach, includes, isEmpty, map, size } from 'lodash'\nimport { injectIntl } from 'react-intl'\nimport { SelectHost, SelectPool } from 'select-objects'\nimport {\n  createCollectionWrapper,\n  createCounter,\n  createFilter,\n  createGetHostMetrics,\n  createGetObjectsOfType,\n  createSelector,\n  createTop,\n  isAdmin,\n} from 'selectors'\nimport {\n  isSrWritable,\n  sendUsageReport,\n  subscribePermissions,\n  subscribePlugins,\n  subscribeResourceSets,\n  subscribeUsers,\n} from 'xo'\n\nimport styles from './index.css'\n\n// ===================================================================\n\nconst PIE_GRAPH_OPTIONS = { donut: true, donutWidth: 40, showLabel: false }\n\n// ===================================================================\n\nclass PatchesCard extends Component {\n  static propTypes = {\n    hosts: PropTypes.object.isRequired,\n  }\n\n  _getContainer = () => this.refs.container\n\n  render() {\n    return (\n      <Card>\n        <CardHeader>\n          <Icon icon='host-patch-update' /> {_('update')}\n          <div ref='container' className='pull-right' />\n        </CardHeader>\n        <CardBlock>\n          <HostsPatchesTable\n            buttonsGroupContainer={this._getContainer}\n            container={ButtonGroup}\n            displayPools\n            hosts={this.props.hosts}\n          />\n        </CardBlock>\n      </Card>\n    )\n  }\n}\n\n@connectStore({\n  hosts: createGetObjectsOfType('host'),\n  isAdmin,\n  pools: createGetObjectsOfType('pool'),\n  srs: createGetObjectsOfType('SR').filter([sr => isSrWritable(sr) && sr.SR_type !== 'udev']),\n  vms: createGetObjectsOfType('VM'),\n  alarmMessages: createGetObjectsOfType('message').filter([message => message.name === 'ALARM']),\n  tasks: createGetObjectsOfType('task').filter([task => task.status === 'pending']),\n})\n@addSubscriptions(\n  ({ isAdmin }) =>\n    isAdmin && {\n      plugins: subscribePlugins,\n      users: subscribeUsers,\n    }\n)\n@injectIntl\nclass DefaultCard extends Component {\n  _getPoolWisePredicate = createSelector(\n    createCollectionWrapper(() => map(this.state.pools, 'id')),\n    poolsIds => item => isEmpty(poolsIds) || includes(poolsIds, item.$pool)\n  )\n\n  _getPredicate = createSelector(\n    this._getPoolWisePredicate,\n    createCollectionWrapper(() => map(this.state.hosts, 'id')),\n    (poolWisePredicate, hostsIds) => item =>\n      isEmpty(hostsIds) ? poolWisePredicate(item) : includes(hostsIds, item.$container || item.$host)\n  )\n\n  _onPoolsChange = pools => {\n    const { hosts } = this.state\n    const poolIds = map(pools, 'id')\n    this.setState({\n      pools,\n      hosts: isEmpty(pools) ? hosts : filter(hosts, host => includes(poolIds, host.$pool)),\n    })\n  }\n\n  _getHosts = createSelector(\n    createFilter(() => this.props.hosts, this._getPoolWisePredicate),\n    () => this.state.hosts,\n    (hosts, selectedHosts) => (isEmpty(selectedHosts) ? hosts : selectedHosts)\n  )\n\n  _getVms = createFilter(() => this.props.vms, this._getPredicate)\n\n  _getSrs = createFilter(() => this.props.srs, this._getPredicate)\n\n  _getPoolsNumber = createCounter(\n    createSelector(\n      () => this.props.pools,\n      () => this.state.pools,\n      (pools, selectedPools) => (isEmpty(selectedPools) ? pools : selectedPools)\n    )\n  )\n\n  _getHostsNumber = createCounter(this._getHosts)\n\n  _getVmsNumber = createCounter(this._getVms)\n\n  _getAlarmMessagesNumber = createCounter(createFilter(() => this.props.alarmMessages, this._getPoolWisePredicate))\n\n  _getTasksNumber = createCounter(createFilter(() => this.props.tasks, this._getPredicate))\n\n  _getHostMetrics = createGetHostMetrics(this._getHosts)\n\n  _getVmMetrics = createCollectionWrapper(\n    createSelector(this._getVms, vms => {\n      const metrics = {\n        vcpus: 0,\n        running: 0,\n        halted: 0,\n        other: 0,\n      }\n      forEach(vms, vm => {\n        if (vm.power_state === 'Running') {\n          metrics.running++\n          metrics.vcpus += vm.CPUs.number\n        } else if (vm.power_state === 'Halted') {\n          metrics.halted++\n        } else metrics.other++\n      })\n      return metrics\n    })\n  )\n\n  _getSrMetrics = createCollectionWrapper(\n    createSelector(this._getSrs, srs => {\n      const metrics = {\n        srTotal: 0,\n        srUsage: 0,\n      }\n      forEach(srs, sr => {\n        metrics.srUsage += sr.physical_usage\n        metrics.srTotal += sr.size\n      })\n      return metrics\n    })\n  )\n\n  _getTopSrs = createTop(this._getSrs, [sr => sr.physical_usage / sr.size], 5)\n\n  _onHostsChange = hosts => {\n    this.setState({\n      hosts: compact(hosts),\n    })\n  }\n\n  _canSendTheReport = createSelector(\n    () => this.props.plugins,\n    (plugins = []) => {\n      let count = 0\n      for (const { id, loaded } of plugins) {\n        if ((id === 'usage-report' || id === 'transport-email') && loaded && ++count === 2) {\n          return true\n        }\n      }\n    }\n  )\n\n  render() {\n    const { props, state } = this\n    const users = props.users\n    const nUsers = size(users)\n    const canSendTheReport = this._canSendTheReport()\n    const nPools = this._getPoolsNumber()\n    const nHosts = this._getHostsNumber()\n    const nVms = this._getVmsNumber()\n    const nAlarmMessages = this._getAlarmMessagesNumber()\n    const hostMetrics = this._getHostMetrics()\n    const vmMetrics = this._getVmMetrics()\n    const srMetrics = this._getSrMetrics()\n    const topSrs = this._getTopSrs()\n\n    const { formatMessage } = props.intl\n\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={6}>\n            <SelectPool multi onChange={this._onPoolsChange} value={state.pools} />\n          </Col>\n          <Col mediumSize={6}>\n            <SelectHost\n              multi\n              onChange={this._onHostsChange}\n              predicate={this._getPoolWisePredicate()}\n              value={state.hosts}\n            />\n          </Col>\n        </Row>\n        <br />\n        <Row>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='pool' /> {_('poolPanel', { pools: nPools })}\n              </CardHeader>\n              <CardBlock>\n                <p className={styles.bigCardContent}>\n                  <Link to='/home?t=pool'>{nPools}</Link>\n                </p>\n              </CardBlock>\n            </Card>\n          </Col>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='host' /> {_('hostPanel', { hosts: nHosts })}\n              </CardHeader>\n              <CardBlock>\n                <p className={styles.bigCardContent}>\n                  <Link to='/home?t=host'>{nHosts}</Link>\n                </p>\n              </CardBlock>\n            </Card>\n          </Col>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='vm' /> {_('vmPanel', { vms: nVms })}\n              </CardHeader>\n              <CardBlock>\n                <p className={styles.bigCardContent}>\n                  <Link to='/home?s=&t=VM'>{nVms}</Link>\n                </p>\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='memory' /> {_('memoryStatePanel')}\n              </CardHeader>\n              <CardBlock className='dashboardItem'>\n                <ChartistGraph\n                  data={{\n                    labels: [formatMessage(messages.usedMemory), formatMessage(messages.totalMemory)],\n                    series: [hostMetrics.memoryUsage, hostMetrics.memoryTotal - hostMetrics.memoryUsage],\n                  }}\n                  options={PIE_GRAPH_OPTIONS}\n                  type='Pie'\n                />\n                <p className='text-xs-center'>\n                  {_('ofUsage', {\n                    total: formatSize(hostMetrics.memoryTotal),\n                    usage: formatSize(hostMetrics.memoryUsage),\n                  })}\n                </p>\n              </CardBlock>\n            </Card>\n          </Col>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='cpu' /> {_('cpuStatePanel')}\n              </CardHeader>\n              <CardBlock>\n                <div className='ct-chart dashboardItem'>\n                  <ChartistGraph\n                    data={{\n                      labels: [formatMessage(messages.usedVCpus), formatMessage(messages.totalCpus)],\n                      series: [vmMetrics.vcpus, hostMetrics.cpus],\n                    }}\n                    options={{\n                      showLabel: false,\n                      showGrid: false,\n                      distributeSeries: true,\n                    }}\n                    type='Bar'\n                  />\n                  <p className='text-xs-center'>\n                    {_('ofCpusUsage', {\n                      nCpus: hostMetrics.cpus,\n                      nVcpus: vmMetrics.vcpus,\n                    })}\n                  </p>\n                </div>\n              </CardBlock>\n            </Card>\n          </Col>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='disk' /> {_('srUsageStatePanel')}\n              </CardHeader>\n              <CardBlock>\n                <div className='ct-chart dashboardItem'>\n                  <BlockLink to='/dashboard/health'>\n                    <ChartistGraph\n                      data={{\n                        labels: [formatMessage(messages.usedSpace), formatMessage(messages.totalSpace)],\n                        series: [srMetrics.srUsage, srMetrics.srTotal - srMetrics.srUsage],\n                      }}\n                      options={PIE_GRAPH_OPTIONS}\n                      type='Pie'\n                    />\n                    <p className='text-xs-center'>\n                      {_('ofUsage', {\n                        total: formatSize(srMetrics.srTotal),\n                        usage: formatSize(srMetrics.srUsage),\n                      })}\n                    </p>\n                  </BlockLink>\n                </div>\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='alarm' /> {_('alarmMessage')}\n              </CardHeader>\n              <CardBlock>\n                <p className={styles.bigCardContent}>\n                  <Link to='/dashboard/health' className={nAlarmMessages > 0 ? 'text-warning' : ''}>\n                    {nAlarmMessages}\n                  </Link>\n                </p>\n              </CardBlock>\n            </Card>\n          </Col>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='task' /> {_('taskStatePanel')}\n              </CardHeader>\n              <CardBlock>\n                <p className={styles.bigCardContent}>\n                  <Link to='/tasks'>{this._getTasksNumber()}</Link>\n                </p>\n              </CardBlock>\n            </Card>\n          </Col>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='user' /> {_('usersStatePanel')}\n              </CardHeader>\n              <CardBlock>\n                <p className={styles.bigCardContent}>\n                  {props.isAdmin ? <Link to='/settings/users'>{nUsers}</Link> : <p>{nUsers}</p>}\n                </p>\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Col mediumSize={4}>\n            <Card>\n              <CardHeader>\n                <Icon icon='vm-force-shutdown' /> {_('vmStatePanel')}\n              </CardHeader>\n              <CardBlock className='dashboardItem'>\n                <BlockLink to='/home?t=VM'>\n                  <ChartistGraph\n                    data={{\n                      labels: [\n                        formatMessage(messages.vmStateRunning),\n                        formatMessage(messages.vmStateHalted),\n                        formatMessage(messages.vmStateOther),\n                      ],\n                      series: [vmMetrics.running, vmMetrics.halted, vmMetrics.other],\n                    }}\n                    options={{ showLabel: false }}\n                    type='Pie'\n                  />\n                  <p className='text-xs-center'>\n                    {_('vmsStates', {\n                      running: vmMetrics.running,\n                      halted: vmMetrics.halted,\n                    })}\n                  </p>\n                </BlockLink>\n              </CardBlock>\n            </Card>\n          </Col>\n          <Col mediumSize={8}>\n            <Card>\n              <CardHeader>\n                <Icon icon='disk' /> {_('srTopUsageStatePanel')}\n              </CardHeader>\n              <CardBlock className='dashboardItem'>\n                <BlockLink to='/dashboard/health'>\n                  <ChartistGraph\n                    style={{ strokeWidth: '30px' }}\n                    data={{\n                      labels: map(topSrs, 'name_label'),\n                      series: map(topSrs, sr => (sr.physical_usage / sr.size) * 100),\n                    }}\n                    options={{\n                      showLabel: false,\n                      showGrid: false,\n                      distributeSeries: true,\n                      high: 100,\n                    }}\n                    type='Bar'\n                  />\n                </BlockLink>\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        {props.isAdmin && (\n          <Row>\n            <Col>\n              <Card>\n                <CardHeader>\n                  <Icon icon='menu-dashboard-stats' /> {_('dashboardReport')}\n                </CardHeader>\n                <CardBlock className='text-xs-center'>\n                  <ActionButton btnStyle='primary' disabled={!canSendTheReport} handler={sendUsageReport} icon=''>\n                    {_('dashboardSendReport')}\n                  </ActionButton>\n                  <br />\n                  {!canSendTheReport && (\n                    <span>\n                      <Link to='/settings/plugins' className='text-info'>\n                        <Icon icon='info' /> {_('dashboardSendReportInfo')}\n                      </Link>\n                      <br />\n                    </span>\n                  )}\n                  {_('dashboardSendReportMessage')}\n                </CardBlock>\n              </Card>\n            </Col>\n          </Row>\n        )}\n        <Row>\n          <Col>\n            <PatchesCard hosts={this._getHosts()} />\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n\n// ===================================================================\n\n@addSubscriptions({\n  resourceSets: subscribeResourceSets,\n  permissions: subscribePermissions,\n})\n@connectStore({\n  isAdmin,\n})\nexport default class Overview extends Component {\n  render() {\n    const { props } = this\n    const showResourceSets = !isEmpty(props.resourceSets) && !props.isAdmin\n    const authorized = !isEmpty(props.permissions) || props.isAdmin\n\n    if (!authorized && !showResourceSets) {\n      return <em>{_('notEnoughPermissionsError')}</em>\n    }\n\n    return (\n      <Container>\n        {showResourceSets ? (\n          map(props.resourceSets, resourceSet => (\n            <Row key={resourceSet.id}>\n              <Card>\n                <CardHeader>\n                  <Icon icon='menu-self-service' /> {resourceSet.name}\n                </CardHeader>\n                <CardBlock>\n                  <ResourceSetQuotas limits={resourceSet.limits} />\n                </CardBlock>\n              </Card>\n            </Row>\n          ))\n        ) : (\n          <DefaultCard isAdmin={props.isAdmin} />\n        )}\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport cloneDeep from 'lodash/cloneDeep'\nimport Component from 'base-component'\nimport forEach from 'lodash/forEach'\nimport Icon from 'icon'\nimport map from 'lodash/map'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport renderXoItem from 'render-xo-item'\nimport sortBy from 'lodash/sortBy'\nimport Upgrade from 'xoa-upgrade'\nimport XoWeekCharts from 'xo-week-charts'\nimport XoWeekHeatmap from 'xo-week-heatmap'\nimport { Container, Row, Col } from 'grid'\nimport { error } from 'notification'\nimport { SelectHostVm } from 'select-objects'\nimport { createGetObjectsOfType } from 'selectors'\nimport { connectStore, formatSize, getMemoryUsedMetric, mapPlus } from 'utils'\nimport { fetchHostStats, fetchVmStats } from 'xo'\n\n// ===================================================================\n\nconst computeMetricArray = (stats, { metricKey, metrics, objectId, timestampStart, valueRenderer }) => {\n  if (!stats) {\n    return\n  }\n\n  if (!metrics[metricKey]) {\n    metrics[metricKey] = {\n      key: metricKey,\n      renderer: valueRenderer,\n      values: {}, // Stats of all object for one metric.\n    }\n  }\n\n  // Stats of one object.\n  metrics[metricKey].values[objectId] = map(stats, (value, i) => ({\n    value: +value,\n    date: timestampStart + 3600000 * i,\n  }))\n}\n\n// ===================================================================\n\nconst computeCpusMetric = (cpus, { objectId, ...params }) => {\n  forEach(cpus, (cpu, index) => {\n    computeMetricArray(cpu, {\n      metricKey: `CPU ${index}`,\n      objectId,\n      ...params,\n    })\n  })\n\n  const nCpus = cpus.length\n\n  if (!nCpus) {\n    return\n  }\n\n  const { metrics } = params\n  const cpusAvg = cloneDeep(metrics['CPU 0'].values[objectId])\n\n  for (let i = 1; i < nCpus; i++) {\n    forEach(metrics[`CPU ${i}`].values[objectId], (value, index) => {\n      cpusAvg[index].value += value.value\n    })\n  }\n\n  forEach(cpusAvg, value => {\n    value.value /= nCpus\n  })\n\n  const allCpusKey = 'All CPUs'\n\n  if (!metrics[allCpusKey]) {\n    metrics[allCpusKey] = {\n      key: allCpusKey,\n      values: {},\n    }\n  }\n\n  metrics[allCpusKey].values[objectId] = cpusAvg\n}\n\nconst computeVifsMetric = (vifs, params) => {\n  forEach(vifs, (vifs, vifsType) => {\n    const rw = vifsType === 'rx' ? 'out' : 'in'\n\n    forEach(vifs, (vif, index) => {\n      computeMetricArray(vif, {\n        metricKey: `Network ${index} ${rw}`,\n        valueRenderer: formatSize,\n        ...params,\n      })\n    })\n  })\n}\n\nconst computePifsMetric = (pifs, params) => {\n  forEach(pifs, (pifs, pifsType) => {\n    const rw = pifsType === 'rx' ? 'out' : 'in'\n\n    forEach(pifs, (pif, index) => {\n      computeMetricArray(pif, {\n        metricKey: `NIC ${index} ${rw}`,\n        valueRenderer: formatSize,\n        ...params,\n      })\n    })\n  })\n}\n\nconst computeXvdsMetric = (xvds, params) => {\n  forEach(xvds, (xvds, xvdsType) => {\n    const rw = xvdsType === 'r' ? 'read' : 'write'\n\n    forEach(xvds, (xvd, index) => {\n      computeMetricArray(xvd, {\n        metricKey: `Disk ${index} ${rw}`,\n        valueRenderer: formatSize,\n        ...params,\n      })\n    })\n  })\n}\n\nconst computeLoadMetric = (load, params) => {\n  computeMetricArray(load, {\n    metricKey: 'Load',\n    ...params,\n  })\n}\n\nconst computeMemoryUsedMetric = (memoryUsed, params) => {\n  computeMetricArray(memoryUsed, {\n    metricKey: 'RAM used',\n    valueRenderer: formatSize,\n    ...params,\n  })\n}\n\n// ===================================================================\n\nconst METRICS_LOADING = 1\nconst METRICS_LOADED = 2\n\nconst runningObjectsPredicate = object => object.power_state === 'Running'\n\nconst STATS_TYPE_TO_COMPUTE_FNC = {\n  cpus: computeCpusMetric,\n  vifs: computeVifsMetric,\n  pifs: computePifsMetric,\n  xvds: computeXvdsMetric,\n  load: computeLoadMetric,\n  memoryUsed: computeMemoryUsedMetric,\n}\n\n@connectStore(() => {\n  const getRunningHosts = createGetObjectsOfType('host').filter([runningObjectsPredicate]).sort()\n  const getRunningVms = createGetObjectsOfType('VM').filter([runningObjectsPredicate]).sort()\n\n  return {\n    hosts: getRunningHosts,\n    vms: getRunningVms,\n  }\n})\nclass SelectMetric extends Component {\n  static propTypes = {\n    onChange: PropTypes.func.isRequired,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      objects: [],\n      predicate: runningObjectsPredicate,\n    }\n  }\n\n  _handleSelection = objects => {\n    this.setState({\n      metricsState: undefined,\n      metrics: undefined,\n      objects,\n      predicate: objects.length\n        ? object => runningObjectsPredicate(object) && object.type === objects[0].type\n        : runningObjectsPredicate,\n    })\n  }\n\n  _resetSelection = () => {\n    this._handleSelection([])\n  }\n\n  _selectAllHosts = () => {\n    this.setState({\n      metricsState: undefined,\n      metrics: undefined,\n      objects: this.props.hosts,\n      predicate: object => runningObjectsPredicate(object) && object.type === 'host',\n    })\n  }\n\n  _selectAllVms = () => {\n    this.setState({\n      metricsState: undefined,\n      metrics: undefined,\n      objects: this.props.vms,\n      predicate: object => runningObjectsPredicate(object) && object.type === 'VM',\n    })\n  }\n\n  _validSelection = async () => {\n    this.setState({ metricsState: METRICS_LOADING })\n\n    const { objects } = this.state\n    const getStats = (objects[0].type === 'host' && fetchHostStats) || fetchVmStats\n\n    const metrics = {}\n\n    await Promise.all(\n      map(objects, object => {\n        return getStats(object, 'hours')\n          .then(result => {\n            const { stats } = result\n\n            if (stats === undefined) {\n              throw new Error('No stats')\n            }\n\n            const params = {\n              metrics,\n              objectId: object.id,\n              timestampStart: (result.endTimestamp - 3600 * (stats.memory.length - 1)) * 1000,\n            }\n\n            stats.memoryUsed = getMemoryUsedMetric(stats)\n            forEach(stats, (stats, type) => {\n              const fnc = STATS_TYPE_TO_COMPUTE_FNC[type]\n\n              if (fnc) {\n                fnc(stats, params)\n              }\n            })\n          })\n          .catch(() => {\n            error(\n              _('statsDashboardGenericErrorTitle'),\n              <span>\n                {_('statsDashboardGenericErrorMessage')} {object.name_label || object.id}\n              </span>\n            )\n          })\n      })\n    )\n\n    this.setState({\n      metricsState: METRICS_LOADED,\n      metrics: sortBy(metrics, metric => metric.key),\n    })\n  }\n\n  _handleSelectedMetric = event => {\n    const { value } = event.target\n    const { state } = this\n\n    this.props.onChange(value !== '' && state.metrics[value], state.objects)\n  }\n\n  render() {\n    const { metricsState, metrics, objects, predicate } = this.state\n\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={6}>\n            <div className='form-group'>\n              <SelectHostVm multi onChange={this._handleSelection} predicate={predicate} value={objects} />\n            </div>\n            <div className='btn-group mt-1' role='group'>\n              <ActionButton handler={this._resetSelection} icon='remove' tooltip={_('dashboardStatsButtonRemoveAll')} />\n              <ActionButton handler={this._selectAllHosts} icon='host' tooltip={_('dashboardStatsButtonAddAllHost')} />\n              <ActionButton handler={this._selectAllVms} icon='vm' tooltip={_('dashboardStatsButtonAddAllVM')} />\n              <ActionButton disabled={!objects.length} handler={this._validSelection} icon='success'>\n                {_('statsDashboardSelectObjects')}\n              </ActionButton>\n            </div>\n          </Col>\n          <Col mediumSize={6}>\n            {metricsState === METRICS_LOADING ? (\n              <div>\n                <Icon icon='loading' /> {_('metricsLoading')}\n              </div>\n            ) : (\n              metricsState === METRICS_LOADED && (\n                <select className='form-control' onChange={this._handleSelectedMetric}>\n                  {_('noSelectedMetric', message => (\n                    <option value=''>{message}</option>\n                  ))}\n                  {map(metrics, (metric, key) => (\n                    <option key={key} value={key}>\n                      {metric.key}\n                    </option>\n                  ))}\n                </select>\n              )\n            )}\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n\n// ===================================================================\n\nclass MetricViewer extends Component {\n  static propTypes = {\n    metricRenderer: PropTypes.func.isRequired,\n    title: PropTypes.any.isRequired,\n  }\n\n  _handleSelectedMetric = (selectedMetric, objects) => {\n    this.setState({ selectedMetric, objects })\n  }\n\n  render() {\n    const {\n      props: { metricRenderer, title },\n      state: { selectedMetric, objects },\n    } = this\n\n    return (\n      <div>\n        <h3>{title}</h3>\n        <SelectMetric onChange={this._handleSelectedMetric} />\n        <hr />\n        {selectedMetric && (\n          <Container>\n            <Row>\n              <Col>{map(objects, object => renderXoItem(object, { className: 'mr-1' }))}</Col>\n            </Row>\n            <Row>\n              <Col>{metricRenderer(selectedMetric)}</Col>\n            </Row>\n          </Container>\n        )}\n      </div>\n    )\n  }\n}\n\n// ===================================================================\n\nconst weekHeatmapRenderer = metric => (\n  <div>\n    <XoWeekHeatmap\n      cellRenderer={metric.renderer}\n      data={mapPlus(metric.values, (arr, push) => {\n        forEach(arr, value => push(value))\n      })}\n    />\n    <hr />\n  </div>\n)\n\nconst weekChartsRenderer = metric => (\n  <XoWeekCharts\n    series={map(metric.values, (data, id) => ({\n      data,\n      objectId: id,\n    }))}\n    valueRenderer={metric.renderer}\n  />\n)\n\nconst Stats = () =>\n  process.env.XOA_PLAN > 2 ? (\n    <div>\n      <MetricViewer metricRenderer={weekHeatmapRenderer} title={_('weeklyHeatmap')} />\n      <MetricViewer metricRenderer={weekChartsRenderer} title={_('weeklyCharts')} />\n    </div>\n  ) : (\n    <Container>\n      <Upgrade place='dashboardStats' available={3} />\n    </Container>\n  )\n\nexport { Stats as default }\n","import Component from 'base-component'\nimport React from 'react'\nimport XoParallelChart from 'xo-parallel-chart'\nimport forEach from 'lodash/forEach'\nimport invoke from 'invoke'\nimport map from 'lodash/map'\nimport mapValues from 'lodash/mapValues'\nimport Upgrade from 'xoa-upgrade'\nimport { Container, Row, Col } from 'grid'\nimport { createFilter, createGetObjectsOfType, createPicker, createSelector } from 'selectors'\nimport { connectStore, formatSize } from 'utils'\n\n// ===================================================================\n\n// Columns order is defined by the attributes declaration order.\n// FIXME translation\nconst DATA_LABELS = {\n  nVCpus: 'vCPUs number',\n  ram: 'RAM quantity',\n  nVifs: 'VIF number',\n  nVdis: 'VDI number',\n  vdisSize: 'Total space',\n}\n\nconst DATA_RENDERERS = {\n  ram: formatSize,\n  vdisSize: formatSize,\n}\n\n// ===================================================================\n\n@connectStore(() => {\n  const getVms = createGetObjectsOfType('VM')\n  const getVdisByVm = invoke(() => {\n    let current = {}\n    const getVdisByVmSelectors = createSelector(\n      vms => vms,\n      vms => {\n        const previous = current\n        current = {}\n\n        forEach(vms, vm => {\n          const { id } = vm\n          current[id] =\n            previous[id] ||\n            createPicker(\n              (vm, vbds, vdis) => vdis,\n              createSelector(\n                createFilter(\n                  createPicker(\n                    (vm, vbds) => vbds,\n                    vm => vm.$VBDs\n                  ),\n                  [vbd => !vbd.is_cd_drive && vbd.attached]\n                ),\n                vbds => map(vbds, vbd => vbd.VDI)\n              )\n            )\n        })\n\n        return current\n      }\n    )\n\n    return createSelector(getVms, createGetObjectsOfType('VBD'), createGetObjectsOfType('VDI'), (vms, vbds, vdis) =>\n      mapValues(getVdisByVmSelectors(vms), (getVdis, vmId) => getVdis(vms[vmId], vbds, vdis))\n    )\n  })\n\n  return {\n    vms: getVms,\n    vdisByVm: getVdisByVm,\n  }\n})\nexport default class Visualizations extends Component {\n  _getData = createSelector(\n    () => this.props.vms,\n    () => this.props.vdisByVm,\n    (vms, vdisByVm) =>\n      map(vms, (vm, vmId) => {\n        let vdisSize = 0\n        let nVdis = 0\n\n        forEach(vdisByVm[vmId], vdi => {\n          vdisSize += vdi.size\n          nVdis++\n        })\n\n        return {\n          objectId: vmId,\n          label: vm.name_label,\n          data: {\n            nVCpus: vm.CPUs.number,\n            nVdis,\n            nVifs: vm.VIFs.length,\n            ram: vm.memory.size,\n            vdisSize,\n          },\n        }\n      })\n  )\n\n  render() {\n    return process.env.XOA_PLAN > 3 ? (\n      <Container>\n        <Row>\n          <Col>\n            <XoParallelChart dataSet={this._getData()} labels={DATA_LABELS} renderers={DATA_RENDERERS} />\n          </Col>\n        </Row>\n      </Container>\n    ) : (\n      <Container>\n        <Upgrade place='health' available={4} />\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport Collapse from 'collapse'\nimport decorate from 'apply-decorators'\nimport Dropzone from 'dropzone'\nimport fromEvent from 'promise-toolbox/fromEvent'\nimport Icon from 'icon'\nimport React from 'react'\nimport { Container } from 'grid'\nimport { formatSize } from 'utils'\nimport { generateId, linkState } from 'reaclette-utils'\nimport { importDisks } from 'xo'\nimport { injectIntl } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport { Input } from 'debounce-input-decorator'\nimport { InputCol, LabelCol, Row } from 'form-grid'\nimport { Select, Toggle } from 'form'\nimport map from 'lodash/map.js'\nimport { readCapacityAndGrainTable } from 'xo-vmdk-to-vhd'\nimport { SelectSr } from 'select-objects'\nimport { isSrWritableOrIso } from '../../common/xo'\n\n// ===================================================================\n\nconst FROM_URL_FILE_TYPES = [\n  {\n    label: 'ISO',\n    value: 'iso',\n  },\n]\n\n// ===================================================================\n\nconst getInitialState = () => ({\n  disks: [],\n  fileType: FROM_URL_FILE_TYPES[0],\n  isFromUrl: false,\n  mapDescriptions: {},\n  mapNames: {},\n  sr: undefined,\n  url: '',\n  loadingDisks: false,\n})\n\nconst FILE_GROUP_TYPE = {\n  // .raw is supported for all types of SRs\n  raw: ['.iso', '.raw'],\n  other: ['.vmdk', '.vhd', '.raw'],\n}\n\nconst DiskImport = decorate([\n  provideState({\n    initialState: getInitialState,\n    effects: {\n      handleDrop: async function (_, files) {\n        this.state.loadingDisks = true\n        const disks = await Promise.all(\n          map(files, async file => {\n            const { name } = file\n            const extIndex = name.lastIndexOf('.')\n            const fileExtension = extIndex >= 0 ? name.slice(extIndex).toLowerCase() : undefined\n            const isRaw = FILE_GROUP_TYPE.raw.includes(fileExtension)\n\n            if (isRaw || FILE_GROUP_TYPE.other.includes(fileExtension)) {\n              let vmdkData\n              if (fileExtension === '.vmdk') {\n                const parsed = await readCapacityAndGrainTable(async (start, end) => {\n                  /* global FileReader */\n                  const reader = new FileReader()\n                  reader.readAsArrayBuffer(file.slice(start, end))\n                  return (await fromEvent(reader, 'loadend')).target.result\n                })\n                const table = await parsed.tablePromise\n                vmdkData = {\n                  grainLogicalAddressList: table.grainLogicalAddressList,\n                  grainFileOffsetList: table.grainFileOffsetList,\n                  capacity: parsed.capacityBytes,\n                }\n              }\n\n              return {\n                id: generateId(),\n                file,\n                name,\n                sr: this.state.sr,\n                type: isRaw ? 'iso' : fileExtension.slice(1),\n                vmdkData,\n              }\n            }\n          })\n        )\n        return { disks: disks.filter(disk => disk !== undefined), loadingDisks: false }\n      },\n      importFromUrl:\n        () =>\n        async ({ fileType, mapDescriptions, mapNames, sr, url }) => {\n          await importDisks(\n            [\n              {\n                description: mapDescriptions.urlDescription?.trim(),\n                name: mapNames.urlName.trim(),\n                type: fileType.value,\n                url,\n              },\n            ],\n            sr\n          )\n        },\n      import:\n        () =>\n        async ({ disks, mapDescriptions, mapNames, sr }) => {\n          await importDisks(\n            disks.map(({ id, name, ...disk }) => ({\n              ...disk,\n              name: mapNames[id] || name,\n              description: mapDescriptions[id],\n            })),\n            sr\n          )\n        },\n      linkState,\n      onChangeDescription:\n        (_, { target: { name, value } }) =>\n        ({ mapDescriptions }) => {\n          mapDescriptions[name] = value\n          return { mapDescriptions }\n        },\n      onChangeFileType: (_, fileType) => ({ fileType }),\n      onChangeName:\n        (_, { target: { name, value } }) =>\n        ({ mapNames }) => ({\n          mapNames: { ...mapNames, [name]: value },\n        }),\n      onChangeSr: (_, sr) => ({ sr }),\n      onChangeUrl:\n        (_, { target: { value } }) =>\n        ({ mapNames }) => {\n          mapNames.urlName = decodeURIComponent(value.slice(value.lastIndexOf('/') + 1))\n          return {\n            url: value,\n            mapNames,\n          }\n        },\n      toggleIsFromUrl: (_, isFromUrl) => ({ isFromUrl }),\n      reset: getInitialState,\n    },\n    computed: {\n      isSrIso: ({ sr }) => sr?.content_type === 'iso',\n    },\n  }),\n  injectIntl,\n  injectState,\n  ({ effects, state: { disks, fileType, loadingDisks, mapDescriptions, mapNames, sr, isFromUrl, isSrIso, url } }) => (\n    <Container>\n      <form id='import-form'>\n        <div className='mb-1'>\n          <a\n            className='text-info'\n            href='https://xcp-ng.org/blog/2022/05/05/how-to-create-a-local-iso-repository-in-xcp-ng/'\n            rel='noreferrer'\n            target='_blank'\n          >\n            <Icon icon='info' /> {_('isoImportRequirement')}\n          </a>\n        </div>\n        <Row>\n          <Toggle className='align-middle' value={isFromUrl} onChange={effects.toggleIsFromUrl} /> {_('fromUrl')}\n        </Row>\n        <Row>\n          <LabelCol>{_('importToSr')}</LabelCol>\n          <InputCol>\n            <SelectSr onChange={effects.onChangeSr} required value={sr} predicate={isSrWritableOrIso} />\n          </InputCol>\n        </Row>\n        {sr !== undefined && (\n          <div>\n            {isFromUrl ? (\n              !isSrIso ? (\n                <p className='text-danger'>{_('UrlImportSrsCompatible')}</p>\n              ) : (\n                <div>\n                  <Row>\n                    <LabelCol>{_('url')}</LabelCol>\n                    <InputCol>\n                      <Input\n                        className='form-control'\n                        name='url'\n                        onChange={effects.onChangeUrl}\n                        placeholder='https://my-company.net/vdi.iso'\n                        type='url'\n                        value={url}\n                      />\n                    </InputCol>\n                  </Row>\n                  <Row>\n                    <LabelCol>{_('fileType')}</LabelCol>\n                    <InputCol>\n                      <Select\n                        onChange={effects.onChangeFileType}\n                        options={FROM_URL_FILE_TYPES}\n                        required\n                        value={fileType}\n                      />\n                    </InputCol>\n                  </Row>\n                  <Row>\n                    <LabelCol>{_('name')}</LabelCol>\n                    <InputCol>\n                      <Input\n                        className='form-control'\n                        name='urlName'\n                        onChange={effects.onChangeName}\n                        type='text'\n                        value={mapNames.urlName}\n                      />\n                    </InputCol>\n                  </Row>\n                  <Row>\n                    <LabelCol>{_('description')}</LabelCol>\n                    <InputCol>\n                      <Input\n                        className='form-control'\n                        name='urlDescription'\n                        onChange={effects.onChangeDescription}\n                        type='text'\n                        value={mapDescriptions.urlDescription}\n                      />\n                    </InputCol>\n                  </Row>\n                </div>\n              )\n            ) : (\n              <Dropzone\n                onDrop={effects.handleDrop}\n                message={_('dropDisksFiles', { types: isSrIso ? ['ISO', 'RAW'] : ['VHD', 'VMDK', 'RAW'] })}\n                accept={isSrIso ? FILE_GROUP_TYPE.raw : FILE_GROUP_TYPE.other}\n              />\n            )}\n            {loadingDisks && <Icon icon='loading' />}\n            {(disks.length > 0 || url.trim() !== '') && (\n              <div>\n                <div>\n                  {disks.map(({ file: { name, size }, id }) => (\n                    <Collapse buttonText={`${name} - ${formatSize(size)}`} key={id} size='small' className='mb-1'>\n                      <div className='mt-1'>\n                        <Row>\n                          <LabelCol>{_('formName')}</LabelCol>\n                          <InputCol>\n                            <input\n                              className='form-control'\n                              name={id}\n                              onChange={effects.onChangeName}\n                              placeholder={name}\n                              type='text'\n                              value={mapNames[id]}\n                            />\n                          </InputCol>\n                        </Row>\n                        <Row>\n                          <LabelCol>{_('formDescription')}</LabelCol>\n                          <InputCol>\n                            <input\n                              className='form-control'\n                              name={id}\n                              onChange={effects.onChangeDescription}\n                              type='text'\n                              value={mapDescriptions[id]}\n                            />\n                          </InputCol>\n                        </Row>\n                      </div>\n                    </Collapse>\n                  ))}\n                </div>\n                <div className='form-group pull-right'>\n                  <ActionButton\n                    btnStyle='primary'\n                    className='mr-1'\n                    form='import-form'\n                    handler={isFromUrl ? effects.importFromUrl : effects.import}\n                    icon='import'\n                    redirectOnSuccess={`/srs/${sr.id}/disks`}\n                    type='submit'\n                  >\n                    {_('newImport')}\n                  </ActionButton>\n                  <Button onClick={effects.reset}>{_('formReset')}</Button>\n                </div>\n              </div>\n            )}\n          </div>\n        )}\n      </form>\n    </Container>\n  ),\n])\nexport { DiskImport as default }\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Ellipsis, { EllipsisContainer } from 'ellipsis'\nimport Icon from 'icon'\nimport Link, { BlockLink } from 'link'\nimport map from 'lodash/map'\nimport React from 'react'\nimport semver from 'semver'\nimport SingleLineRow from 'single-line-row'\nimport HomeTags from 'home-tags'\nimport Tooltip from 'tooltip'\nimport { Col } from 'grid'\nimport { Text } from 'editable'\nimport {\n  addTag,\n  editHost,\n  fetchHostStats,\n  isHostTimeConsistentWithXoaTime,\n  isPubKeyTooShort,\n  removeTag,\n  startHost,\n  stopHost,\n  subscribeHvSupportedVersions,\n  subscribeMdadmHealth,\n} from 'xo'\nimport { addSubscriptions, connectStore, formatSizeShort, hasLicenseRestrictions, osFamily } from 'utils'\nimport {\n  createDoesHostNeedRestart,\n  createGetHostState,\n  createGetObject,\n  createGetObjectsOfType,\n  createSelector,\n} from 'selectors'\nimport { injectState } from 'reaclette'\nimport { Host, Pool } from 'render-xo-item'\n\nimport MiniStats from './mini-stats'\nimport styles from './index.css'\n\nimport BulkIcons from '../../common/bulk-icons'\nimport { LICENSE_WARNING_BODY } from '../host/license-warning'\nimport { getXoaPlan, SOURCES } from '../../common/xoa-plans'\n\n@addSubscriptions(props => ({\n  hvSupportedVersions: subscribeHvSupportedVersions,\n  mdadmHealth: subscribeMdadmHealth(props.item),\n}))\n@connectStore(() => ({\n  container: createGetObject((_, props) => props.item.$pool),\n  needsRestart: createDoesHostNeedRestart((_, props) => props.item),\n  nVms: createGetObjectsOfType('VM').count(\n    createSelector(\n      (_, props) => props.item.id,\n      hostId => obj => obj.$container === hostId\n    )\n  ),\n  hostState: createGetHostState((_, props) => props.item),\n}))\n@injectState\nexport default class HostItem extends Component {\n  state = {\n    isHostTimeConsistentWithXoaTime: true,\n    isPubKeyTooShort: false,\n  }\n\n  componentWillMount() {\n    isPubKeyTooShort(this.props.item).then(value =>\n      this.setState({\n        isPubKeyTooShort: value,\n      })\n    )\n    Promise.resolve(isHostTimeConsistentWithXoaTime(this.props.item)).then(value =>\n      this.setState({\n        isHostTimeConsistentWithXoaTime: value,\n      })\n    )\n  }\n\n  get _isRunning() {\n    const host = this.props.item\n    return host && host.power_state === 'Running'\n  }\n\n  _isMaintained = createSelector(\n    () => this.props.hvSupportedVersions,\n    () => this.props.item,\n    (supportedVersions, host) =>\n      // If could not fetch the list of maintained versions, consider this host up to date\n      supportedVersions?.[host.productBrand] === undefined\n        ? true\n        : semver.satisfies(host.version, supportedVersions[host.productBrand])\n  )\n\n  _addTag = tag => addTag(this.props.item.id, tag)\n  _fetchStats = () => fetchHostStats(this.props.item.id)\n  _removeTag = tag => removeTag(this.props.item.id, tag)\n  _setNameDescription = nameDescription => editHost(this.props.item, { name_description: nameDescription })\n  _setNameLabel = nameLabel => editHost(this.props.item, { name_label: nameLabel })\n  _start = () => startHost(this.props.item)\n  _stop = () => stopHost(this.props.item)\n  _toggleExpanded = () => this.setState({ expanded: !this.state.expanded })\n  _onSelect = () => this.props.onSelect(this.props.item.id)\n  _getProSupportStatus = () => {\n    const { state: reacletteState, item: host } = this.props\n    if (host.productBrand !== 'XCP-ng') {\n      return\n    }\n\n    const { supportLevel } = reacletteState.poolLicenseInfoByPoolId[host.$poolId]\n    const license = reacletteState.xcpngLicenseByBoundObjectId?.[host.id]\n    if (license !== undefined) {\n      license.expires = license.expires ?? Infinity\n    }\n\n    let level = 'warning'\n    let message = 'hostNoSupport'\n\n    if (getXoaPlan() === SOURCES) {\n      message = 'poolSupportSourceUsers'\n      level = 'warning'\n    }\n\n    if (supportLevel === 'total') {\n      message = 'hostSupportEnabled'\n      level = 'success'\n    }\n\n    if (supportLevel === 'partial' && (license === undefined || license.expires < Date.now())) {\n      message = 'hostNoLicensePartialProSupport'\n      level = 'danger'\n    }\n\n    return {\n      level,\n      icon: <Icon icon='menu-support' className={`text-${level}`} />,\n      message,\n    }\n  }\n  _getAreHostsVersionsEqual = () => this.props.state.areHostsVersionsEqualByPool[this.props.item.$pool]\n\n  _getAlerts = createSelector(\n    () => this.props.needsRestart,\n    () => this.props.item,\n    this._isMaintained,\n    () => this.state.isHostTimeConsistentWithXoaTime,\n    this._getAreHostsVersionsEqual,\n    () => this.props.state.hostsByPoolId[this.props.item.$pool],\n    () => this.state.isPubKeyTooShort,\n    () => this.props.mdadmHealth,\n    (\n      needsRestart,\n      host,\n      isMaintained,\n      isHostTimeConsistentWithXoaTime,\n      areHostsVersionsEqual,\n      poolHosts,\n      isPubKeyTooShort,\n      mdadmHealth\n    ) => {\n      const alerts = []\n\n      if (needsRestart) {\n        alerts.push({\n          level: 'warning',\n          render: (\n            <Link className='text-warning' to={`/hosts/${host.id}/patches`}>\n              <Icon icon='alarm' /> {_('rebootUpdateHostLabel')}\n            </Link>\n          ),\n        })\n      }\n\n      if (!isMaintained) {\n        alerts.push({\n          level: 'warning',\n          render: (\n            <p>\n              <Icon icon='alarm' /> {_('noMoreMaintained')}\n            </p>\n          ),\n        })\n      }\n\n      if (!isHostTimeConsistentWithXoaTime) {\n        alerts.push({\n          level: 'danger',\n          render: (\n            <p>\n              <Icon icon='alarm' /> {_('warningHostTimeTooltip')}\n            </p>\n          ),\n        })\n      }\n\n      if (hasLicenseRestrictions(host)) {\n        alerts.push({\n          level: 'danger',\n          render: (\n            <span>\n              <Icon icon='alarm' /> {_('licenseRestrictionsModalTitle')} {LICENSE_WARNING_BODY}\n            </span>\n          ),\n        })\n      }\n\n      const proSupportStatus = this._getProSupportStatus()\n      if (proSupportStatus !== undefined && proSupportStatus.level !== 'success') {\n        alerts.push({\n          level: proSupportStatus.level,\n          render: (\n            <span>\n              {proSupportStatus.icon} {_(proSupportStatus.message)}\n            </span>\n          ),\n        })\n      }\n\n      if (isPubKeyTooShort) {\n        alerts.push({\n          level: 'warning',\n          render: (\n            <span>\n              <Icon icon='alarm' /> {_('pubKeyTooShort')}\n              <ul>\n                <li>{_('longerCustomCertficate')}</li>\n                <li>{_('longerDefaultCertificate')}</li>\n              </ul>\n              <a\n                href='https://docs.xcp-ng.org/releases/release-8-3/#host-certificate-key-too-small-prevents-upgrade'\n                target='_blank'\n                rel='noreferrer'\n              >\n                <Icon icon='info' /> {_('clickLinkForDetails')}\n              </a>\n            </span>\n          ),\n        })\n      }\n\n      if (!host.hvmCapable) {\n        alerts.push({\n          level: 'warning',\n          render: (\n            <span>\n              <Icon icon='alarm' /> {_('hostHvmDisabled')}\n            </span>\n          ),\n        })\n      }\n\n      if (!areHostsVersionsEqual) {\n        alerts.push({\n          level: 'danger',\n          render: (\n            <div>\n              <p>\n                <Icon icon='alarm' /> {_('notAllHostsHaveTheSameVersion', { pool: <Pool id={host.$pool} link /> })}\n              </p>\n              <ul>\n                {map(poolHosts, host => (\n                  <li>{_('keyValue', { key: <Host id={host.id} />, value: host.version })}</li>\n                ))}\n              </ul>\n            </div>\n          ),\n        })\n      }\n\n      if (mdadmHealth?.raid?.State !== undefined && !['clean', 'active'].includes(mdadmHealth.raid.State)) {\n        alerts.push({\n          level: 'danger',\n          render: (\n            <span>\n              <Icon icon='alarm' className='text-danger' /> {_('raidStateWarning', { state: mdadmHealth.raid.State })}\n            </span>\n          ),\n        })\n      }\n\n      return alerts\n    }\n  )\n\n  render() {\n    const { container, expandAll, item: host, nVms, selected, hostState } = this.props\n    const proSupportStatus = this._getProSupportStatus()\n    return (\n      <div className={styles.item}>\n        <BlockLink to={`/hosts/${host.id}`}>\n          <SingleLineRow>\n            <Col smallSize={10} mediumSize={9} largeSize={3}>\n              <EllipsisContainer>\n                <input type='checkbox' checked={selected} onChange={this._onSelect} value={host.id} />\n                &nbsp;&nbsp;\n                <Tooltip\n                  content={\n                    <span>\n                      {_(`powerState${hostState}`)}\n                      {hostState === 'Busy' && (\n                        <span>\n                          {' ('}\n                          {map(host.current_operations)[0]}\n                          {')'}\n                        </span>\n                      )}\n                    </span>\n                  }\n                >\n                  <Icon icon={hostState.toLowerCase()} />\n                </Tooltip>\n                &nbsp;&nbsp;\n                <Ellipsis>\n                  <Text value={host.name_label} onChange={this._setNameLabel} useLongClick />\n                </Ellipsis>\n                &nbsp;\n                {container && host.id === container.master && (\n                  <span className='tag tag-pill tag-info'>{_('pillMaster')}</span>\n                )}\n                &nbsp;\n                <BulkIcons alerts={this._getAlerts()} />\n                &nbsp;\n                {proSupportStatus?.level === 'success' && (\n                  <Tooltip content={_(proSupportStatus.message)}>{proSupportStatus.icon}</Tooltip>\n                )}\n              </EllipsisContainer>\n            </Col>\n            <Col mediumSize={3} className='hidden-lg-down'>\n              <EllipsisContainer>\n                <span className={styles.itemActionButons}>\n                  <Tooltip\n                    content={\n                      <span>\n                        {nVms}x {_('vmsTabName')}\n                      </span>\n                    }\n                  >\n                    {nVms > 0 ? (\n                      <Link to={`/home?s=$container:${host.id}&t=VM`}>\n                        <Icon icon='vm' size='1' fixedWidth />\n                      </Link>\n                    ) : (\n                      <Icon icon='vm' size='1' fixedWidth />\n                    )}\n                  </Tooltip>\n                  &nbsp;\n                  {this._isRunning ? (\n                    <span>\n                      <Tooltip content={_('stopHostLabel')}>\n                        <a onClick={this._stop}>\n                          <Icon icon='host-stop' size='1' />\n                        </a>\n                      </Tooltip>\n                    </span>\n                  ) : (\n                    <span>\n                      <Tooltip content={_('startHostLabel')}>\n                        <a onClick={this._start}>\n                          <Icon icon='host-start' size='1' />\n                        </a>\n                      </Tooltip>\n                    </span>\n                  )}\n                </span>\n                <Icon className='text-info' icon={host.os_version && osFamily(host.os_version.distro)} fixedWidth />{' '}\n                <Ellipsis>\n                  <Text value={host.name_description} onChange={this._setNameDescription} useLongClick />\n                </Ellipsis>\n              </EllipsisContainer>\n            </Col>\n            <Col largeSize={2} className='hidden-md-down'>\n              <span>\n                <Tooltip\n                  content={_('memoryLeftTooltip', {\n                    used: Math.round((host.memory.usage / host.memory.size) * 100),\n                    free: formatSizeShort(host.memory.size - host.memory.usage),\n                  })}\n                >\n                  <progress\n                    style={{ margin: 0 }}\n                    className='progress'\n                    value={(host.memory.usage / host.memory.size) * 100}\n                    max='100'\n                  />\n                </Tooltip>\n              </span>\n            </Col>\n            <Col largeSize={2} className='hidden-lg-down'>\n              <span className='tag tag-info tag-ip'>{host.address}</span>\n            </Col>\n            {container && (\n              <Col mediumSize={2} className='hidden-sm-down'>\n                <Link to={`/${container.type}s/${container.id}`}>{container.name_label}</Link>\n              </Col>\n            )}\n            <Col mediumSize={1} offset={container ? undefined : 2} className={styles.itemExpandRow}>\n              <a className={styles.itemExpandButton} onClick={this._toggleExpanded}>\n                <Icon icon='nav' fixedWidth />\n                &nbsp;&nbsp;&nbsp;\n              </a>\n            </Col>\n          </SingleLineRow>\n        </BlockLink>\n        {(this.state.expanded || expandAll) && (\n          <SingleLineRow>\n            <Col mediumSize={2} className={styles.itemExpanded}>\n              <span>\n                {host.cpus.cores}x <Icon icon='cpu' /> &nbsp; {formatSizeShort(host.memory.size)} <Icon icon='memory' />\n              </span>\n            </Col>\n            <Col mediumSize={1} className={styles.itemExpanded}>\n              {host.productBrand} {host.version}\n            </Col>\n            <Col mediumSize={3} className={styles.itemExpanded}>\n              <div style={{ fontSize: '1.4em' }}>\n                <HomeTags type='host' labels={host.tags} onDelete={this._removeTag} onAdd={this._addTag} />\n              </div>\n            </Col>\n            <Col mediumSize={6} className={styles.itemExpanded}>\n              <MiniStats fetch={this._fetchStats} />\n            </Col>\n          </SingleLineRow>\n        )}\n      </div>\n    )\n  }\n}\n","module.exports = {\n    \"itemRowHeader\": \"mcb2bfb78d_itemRowHeader\",\n    \"itemContainer\": \"mcb2bfb78d_itemContainer\",\n    \"itemContainerHeader\": \"mcb2bfb78d_itemContainerHeader\",\n    \"item\": \"mcb2bfb78d_item\",\n    \"itemActionButons\": \"mcb2bfb78d_itemActionButons\",\n    \"itemExpanded\": \"mcb2bfb78d_itemExpanded\",\n    \"itemExpandButton\": \"mcb2bfb78d_itemExpandButton\",\n    \"itemExpandRow\": \"mcb2bfb78d_itemExpandRow\",\n    \"selectObject\": \"mcb2bfb78d_selectObject\",\n    \"highlight\": \"mcb2bfb78d_highlight\"\n};","import * as ComplexMatcher from 'complex-matcher'\nimport _ from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport CenterPanel from 'center-panel'\nimport classNames from 'classnames'\nimport Component from 'base-component'\nimport cookies from 'js-cookie'\nimport defined, { get } from '@xen-orchestra/defined'\nimport homeFilters from 'home-filters'\nimport Icon from 'icon'\nimport invoke from 'invoke'\nimport Link from 'link'\nimport Page from '../page'\nimport Pagination from 'pagination'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport Shortcuts from 'shortcuts'\nimport Tooltip from 'tooltip'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport {\n  ceil,\n  compact,\n  debounce,\n  escapeRegExp,\n  filter,\n  find,\n  forEach,\n  groupBy,\n  identity,\n  includes,\n  isEmpty,\n  keys,\n  map,\n  mapValues,\n  omit,\n  pick,\n  pickBy,\n  size,\n  some,\n  sumBy,\n} from 'lodash'\nimport {\n  addCustomFilter,\n  copyVms,\n  deleteTemplates,\n  deleteVms,\n  disconnectAllHostsSrs,\n  emergencyShutdownHosts,\n  forgetSrs,\n  migrateVms,\n  pauseVms,\n  reconnectAllHostsSrs,\n  rescanSrs,\n  restartHosts,\n  restartHostsAgents,\n  restartVms,\n  snapshotVms,\n  startVms,\n  stopHosts,\n  stopVms,\n  subscribeBackupNgJobs,\n  subscribeResourceSets,\n  subscribeSchedules,\n  subscribeServers,\n  suspendVms,\n  ITEMS_PER_PAGE_OPTIONS,\n} from 'xo'\nimport { Container, Row, Col } from 'grid'\nimport { createPredicate } from 'value-matcher'\nimport { SelectHost, SelectPool, SelectResourceSet, SelectTag } from 'select-objects'\nimport { addSubscriptions, connectStore, noop } from 'utils'\nimport {\n  areObjectsFetched,\n  createCounter,\n  createFilter,\n  createGetObjectsOfType,\n  createPager,\n  createSelector,\n  createSort,\n  getIsPoolAdmin,\n  getUser,\n  isAdmin,\n} from 'selectors'\nimport { DropdownButton, MenuItem, OverlayTrigger, Popover } from 'react-bootstrap-4/lib'\nimport { Select } from 'form'\n\nimport styles from './index.css'\nimport HostItem from './host-item'\nimport PoolItem from './pool-item'\nimport VmItem from './vm-item'\nimport TemplateItem from './template-item'\nimport SrItem from './sr-item'\n\nconst DEFAULT_ITEMS_PER_PAGE = 20\n\nconst OPTIONS = {\n  host: {\n    defaultFilter: 'power_state:running ',\n    filters: homeFilters.host,\n    mainActions: [\n      { handler: stopHosts, icon: 'host-stop', tooltip: _('stopHostLabel') },\n      {\n        handler: restartHostsAgents,\n        icon: 'host-restart-agent',\n        tooltip: _('restartHostAgent'),\n      },\n      {\n        handler: emergencyShutdownHosts,\n        icon: 'host-emergency-shutdown',\n        tooltip: _('emergencyModeLabel'),\n      },\n      {\n        handler: restartHosts,\n        icon: 'host-reboot',\n        tooltip: _('rebootHostLabel'),\n      },\n    ],\n    Item: HostItem,\n    showPoolsSelector: true,\n    sortOptions: [\n      { labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },\n      {\n        labelId: 'homeSortByPowerstate',\n        sortBy: 'power_state',\n        sortOrder: 'desc',\n      },\n      { labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },\n      {\n        labelId: 'homeSortByCpus',\n        sortBy: 'CPUs.cpu_count',\n        sortOrder: 'desc',\n      },\n      {\n        labelId: 'homeSortByPool',\n        sortBy: 'container.name_label',\n        sortOrder: 'asc',\n      },\n    ],\n  },\n  VM: {\n    defaultFilter: 'power_state:running ',\n    filters: homeFilters.VM,\n    mainActions: [\n      { handler: stopVms, icon: 'vm-stop', tooltip: _('stopVmLabel') },\n      { handler: startVms, icon: 'vm-start', tooltip: _('startVmLabel') },\n      { handler: restartVms, icon: 'vm-reboot', tooltip: _('rebootVmLabel') },\n      { handler: migrateVms, icon: 'vm-migrate', tooltip: _('migrateVmLabel') },\n      { handler: copyVms, icon: 'vm-copy', tooltip: _('copyVmLabel') },\n    ],\n    otherActions: [\n      {\n        handler: pauseVms,\n        icon: 'vm-pause',\n        labelId: 'pauseVmLabel',\n      },\n      {\n        handler: suspendVms,\n        icon: 'vm-suspend',\n        labelId: 'suspendVmLabel',\n      },\n      {\n        handler: restartVms,\n        icon: 'vm-force-reboot',\n        labelId: 'forceRebootVmLabel',\n        params: true,\n      },\n      {\n        handler: stopVms,\n        icon: 'vm-force-shutdown',\n        labelId: 'forceShutdownVmLabel',\n        params: true,\n      },\n      {\n        handler: snapshotVms,\n        icon: 'vm-snapshot',\n        labelId: 'snapshotVmLabel',\n      },\n      {\n        handler: (vmIds, _, { setHomeVmIdsSelection }, { router }) => {\n          setHomeVmIdsSelection(vmIds)\n          router.push('backup/new/vms')\n        },\n        icon: 'backup',\n        labelId: 'backupLabel',\n      },\n      {\n        handler: deleteVms,\n        icon: 'vm-delete',\n        labelId: 'vmRemoveButton',\n      },\n    ],\n    Item: VmItem,\n    showPoolsSelector: true,\n    showHostsSelector: true,\n    showResourceSetsSelector: true,\n    sortOptions: [\n      { labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' },\n      {\n        labelId: 'sortByDisksUsage',\n        sortBy: 'vdisUsage',\n        sortOrder: 'desc',\n      },\n      { labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },\n      {\n        labelId: 'homeSortByPowerstate',\n        sortBy: 'power_state',\n        sortOrder: 'desc',\n      },\n      { labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },\n      {\n        labelId: 'homeSortVmsBySnapshots',\n        sortBy: 'snapshots.length',\n        sortOrder: 'desc',\n      },\n      {\n        labelId: 'homeSortByContainer',\n        sortBy: 'container.name_label',\n        sortOrder: 'asc',\n      },\n      {\n        labelId: 'homeSortByStartTime',\n        sortBy: 'startTime',\n\n        // move VM with no start time at the end\n        sortByFn: ({ startTime }) => (startTime === null ? -Infinity : startTime),\n        sortOrder: 'desc',\n      },\n      {\n        labelId: 'homeSortByInstallTime',\n        sortBy: 'installTime',\n        sortByFn: ({ installTime }) => (installTime === null ? -Infinity : installTime),\n        sortOrder: 'desc',\n      },\n    ],\n  },\n  pool: {\n    defaultFilter: '',\n    filters: homeFilters.pool,\n    getActions: noop,\n    Item: PoolItem,\n    sortOptions: [{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' }],\n  },\n  'VM-template': {\n    defaultFilter: '',\n    filters: homeFilters['VM-template'],\n    mainActions: [\n      {\n        handler: vms => copyVms(vms, 'VM-template'),\n        icon: 'vm-copy',\n        tooltip: _('copyVmLabel'),\n      },\n      {\n        handler: deleteTemplates,\n        icon: 'delete',\n        tooltip: _('templateDelete'),\n      },\n    ],\n    Item: TemplateItem,\n    showPoolsSelector: true,\n    sortOptions: [\n      { labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },\n      { labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },\n      { labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' },\n      {\n        labelId: 'homeSortByPool',\n        sortBy: 'container.name_label',\n        sortOrder: 'asc',\n      },\n    ],\n  },\n  SR: {\n    defaultFilter: '',\n    filters: homeFilters.SR,\n    mainActions: [\n      { handler: rescanSrs, icon: 'refresh', tooltip: _('srRescan') },\n      {\n        handler: reconnectAllHostsSrs,\n        icon: 'sr-reconnect-all',\n        tooltip: _('srReconnectAll'),\n      },\n      {\n        handler: disconnectAllHostsSrs,\n        icon: 'sr-disconnect-all',\n        tooltip: _('srDisconnectAll'),\n      },\n      { handler: forgetSrs, icon: 'sr-forget', tooltip: _('srsForget') },\n    ],\n    Item: SrItem,\n    showPoolsSelector: true,\n    sortOptions: [\n      { labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },\n      {\n        labelId: 'homeSortBySize',\n        sortBy: 'size',\n        sortOrder: 'desc',\n        default: true,\n      },\n      { labelId: 'homeSortByShared', sortBy: 'shared', sortOrder: 'desc' },\n      {\n        labelId: 'homeSortByUsage',\n        sortBy: 'physicalUsageBySize',\n        sortOrder: 'desc',\n      },\n      { labelId: 'homeSortByType', sortBy: 'SR_type', sortOrder: 'asc' },\n      {\n        labelId: 'homeSortByPool',\n        sortBy: 'container.name_label',\n        sortOrder: 'asc',\n      },\n    ],\n  },\n}\n\nconst TYPES = {\n  VM: _('homeTypeVm'),\n  'VM-template': _('homeTypeVmTemplate'),\n  host: _('homeTypeHost'),\n  pool: _('homeTypePool'),\n  SR: _('homeSrPage'),\n}\n\nconst DEFAULT_TYPE = 'VM'\n\nconst BACKUP_FILTERS = [\n  { value: 'all', label: _('allVms') },\n  { value: 'backedUpVms', label: _('backedUpVms') },\n  { value: 'notBackedUpVms', label: _('notBackedUpVms') },\n]\n\nconst POWER_STATE_HOST = [\n  { value: 'halted', label: _('powerStateHalted') },\n  { value: 'running', label: _('powerStateRunning') },\n]\n\nconst POWER_STATE_VM = [\n  { value: 'halted', label: _('powerStateHalted') },\n  { value: 'paused', label: _('powerStatePaused') },\n  { value: 'running', label: _('powerStateRunning') },\n  { value: 'suspended', label: _('powerStateSuspended') },\n]\n\n@connectStore(() => {\n  const noServersConnected = invoke(createGetObjectsOfType('host'), hosts => state => isEmpty(hosts(state)))\n\n  return {\n    noServersConnected,\n  }\n})\nclass NoObjectsWithoutServers extends Component {\n  static propTypes = {\n    isAdmin: PropTypes.bool.isRequired,\n    isPoolAdmin: PropTypes.bool.isRequired,\n    noResourceSets: PropTypes.bool.isRequired,\n  }\n\n  render() {\n    const { isAdmin, isPoolAdmin, noRegisteredServers, noResourceSets, noServersConnected } = this.props\n\n    if (noServersConnected && isAdmin) {\n      return (\n        <CenterPanel>\n          <Card shadow>\n            <CardHeader>{_('homeWelcome')}</CardHeader>\n            <CardBlock>\n              <Link to='/settings/servers'>\n                <Icon icon='pool' size={4} />\n                <h4>{noRegisteredServers ? _('homeAddServer') : _('homeConnectServer')}</h4>\n              </Link>\n              <p className='text-muted'>{noRegisteredServers ? _('homeWelcomeText') : _('homeConnectServerText')}</p>\n              <br />\n              <br />\n              <h3>{_('homeHelp')}</h3>\n              <Row>\n                <Col mediumSize={6}>\n                  <a\n                    href='https://docs.xen-orchestra.com/'\n                    rel='noopener noreferrer'\n                    target='_blank'\n                    className='btn btn-link'\n                  >\n                    <Icon icon='menu-about' size={4} />\n                    <h4>{_('homeOnlineDoc')}</h4>\n                  </a>\n                </Col>\n                <Col mediumSize={6}>\n                  <a\n                    href='https://xen-orchestra.com/#!/member/support'\n                    rel='noopener noreferrer'\n                    target='_blank'\n                    className='btn btn-link'\n                  >\n                    <Icon icon='menu-settings-users' size={4} />\n                    <h4>{_('homeProSupport')}</h4>\n                  </a>\n                </Col>\n              </Row>\n            </CardBlock>\n          </Card>\n        </CenterPanel>\n      )\n    }\n\n    return (\n      <CenterPanel>\n        <Card shadow>\n          <CardHeader>{_('homeNoVms')}</CardHeader>\n          {(isAdmin || (isPoolAdmin && process.env.XOA_PLAN > 3) || !noResourceSets) && (\n            <CardBlock>\n              <Row>\n                <Col>\n                  <Link to='/vms/new'>\n                    <Icon icon='vm' size={4} />\n                    <h4>{_('homeNewVm')}</h4>\n                  </Link>\n                  <p className='text-muted'>{_('homeNewVmMessage')}</p>\n                </Col>\n              </Row>\n              {isAdmin && (\n                <div>\n                  <h2>{_('homeNoVmsOr')}</h2>\n                  <Row>\n                    <Col mediumSize={6}>\n                      <Link to='/import'>\n                        <Icon icon='menu-new-import' size={4} />\n                        <h4>{_('homeImportVm')}</h4>\n                      </Link>\n                      <p className='text-muted'>{_('homeImportVmMessage')}</p>\n                    </Col>\n                    <Col mediumSize={6}>\n                      <Link to='/backup/restore'>\n                        <Icon icon='backup' size={4} />\n                        <h4>{_('homeRestoreBackup')}</h4>\n                      </Link>\n                      <p className='text-muted'>{_('homeRestoreBackupMessage')}</p>\n                    </Col>\n                  </Row>\n                </div>\n              )}\n            </CardBlock>\n          )}\n        </Card>\n      </CenterPanel>\n    )\n  }\n}\n\nconst NoObjectsWithServers = addSubscriptions({\n  noRegisteredServers: cb => subscribeServers(data => cb(isEmpty(data))),\n})(NoObjectsWithoutServers)\n\nconst NoObjects = props =>\n  props.isAdmin ? <NoObjectsWithServers {...props} /> : <NoObjectsWithoutServers {...props} />\n\n@connectStore(() => {\n  const getType = (_, props) => props.location.query.t || DEFAULT_TYPE\n  const getContainers = createSelector(\n    createGetObjectsOfType('host'),\n    createGetObjectsOfType('pool'),\n    (hosts, pools) => ({ ...hosts, ...pools })\n  )\n  const getItems = createSelector(getContainers, createGetObjectsOfType(getType), (containers, items) =>\n    mapValues(items, item =>\n      // ComplexMatcher works on own enumerable properties, therefore the\n      // injected properties should be non-enumerable\n      Object.defineProperties(\n        { ...item },\n        {\n          container: { value: containers[item.$container || item.$pool] },\n          physicalUsageBySize: {\n            value: item.type === 'SR' ? (item.size > 0 ? item.physical_usage / item.size : 0) : undefined,\n          },\n        }\n      )\n    )\n  )\n  // VMs are handled separately because we need to inject their 'vdisUsage'\n  const getVms = createSelector(\n    getContainers,\n    createGetObjectsOfType('VM'),\n    createGetObjectsOfType('VBD'),\n    createGetObjectsOfType('VDI'),\n    createGetObjectsOfType('VIF'),\n    (containers, vms, vbds, vdis, vifs) =>\n      mapValues(vms, vm =>\n        // ComplexMatcher works on own enumerable properties, therefore the\n        // injected properties should be non-enumerable\n        Object.defineProperties(\n          {\n            ...vm,\n\n            MACs: vm.VIFs.reduce((acc, vifId) => {\n              const vif = vifs[vifId]\n              if (vif !== undefined) {\n                acc.push(vif.MAC)\n              }\n              return acc\n            }, []),\n            vulnerable: Object.values(vm.vulnerabilities).filter(Boolean).length > 0,\n          },\n          {\n            container: { value: containers[vm.$container || vm.$pool] },\n            vdisUsage: { value: sumBy(compact(map(vm.$VBDs, vbdId => get(() => vdis[vbds[vbdId].VDI]))), 'usage') },\n          }\n        )\n      )\n  )\n\n  return (state, props) => {\n    const type = getType(state, props)\n\n    return {\n      areObjectsFetched: areObjectsFetched(state, props),\n      isAdmin: isAdmin(state, props),\n      isPoolAdmin: getIsPoolAdmin(state, props),\n      items: type === 'VM' ? getVms(state, props) : getItems(state, props),\n      type,\n      user: getUser(state, props),\n    }\n  }\n})\n@addSubscriptions(({ isAdmin }) => {\n  const noResourceSets = cb => subscribeResourceSets(data => cb(isEmpty(data)))\n  return isAdmin\n    ? {\n        jobs: subscribeBackupNgJobs,\n        noResourceSets,\n        schedulesByJob: cb => subscribeSchedules(schedules => cb(groupBy(schedules, 'jobId'))),\n      }\n    : { noResourceSets }\n})\nexport default class Home extends Component {\n  static contextTypes = {\n    router: PropTypes.object,\n  }\n\n  state = {\n    homeItemsPerPage: +defined(cookies.get('homeItemsPerPage'), DEFAULT_ITEMS_PER_PAGE),\n    selectedItems: {},\n  }\n\n  componentWillMount() {\n    this._initFilterAndSortBy(this.props)\n  }\n\n  componentWillReceiveProps(props) {\n    const { type } = props\n\n    if (this._getFilter() !== this._getFilter(props)) {\n      this._initFilterAndSortBy(props)\n    }\n    if (type !== this.props.type) {\n      this.setState({ highlighted: undefined })\n    }\n  }\n\n  componentDidUpdate() {\n    const { selectedItems } = this.state\n\n    // Unselect items that are no longer visible\n    if (\n      (this._visibleItemsRecomputations || 0) <\n      (this._visibleItemsRecomputations = this._getVisibleItems.recomputations())\n    ) {\n      const newSelectedItems = pick(selectedItems, map(this._getVisibleItems(), 'id'))\n      if (size(newSelectedItems) < this._getNumberOfSelectedItems()) {\n        this.setState({ selectedItems: newSelectedItems })\n      }\n    }\n  }\n\n  _getNumberOfItems = createCounter(() => this.props.items)\n  _getNumberOfSelectedItems = createCounter(() => this.state.selectedItems, [identity])\n\n  _setNItemsPerPage(nItems) {\n    this.setState({ homeItemsPerPage: nItems })\n    cookies.set('homeItemsPerPage', nItems)\n\n    // changing the number of items per page should send back to the first page\n    //\n    // see https://github.com/vatesfr/xen-orchestra/issues/7350\n    this._onPageSelection(1)\n  }\n\n  _getPage() {\n    const {\n      location: { query },\n    } = this.props\n    const queryPage = +query.p\n    return Number.isNaN(queryPage) ? 1 : queryPage\n  }\n\n  _getType() {\n    return this.props.type\n  }\n\n  _setType(type) {\n    const { pathname, query } = this.props.location\n    this.context.router.push({\n      pathname,\n      query: {\n        ...query,\n        backup: undefined,\n        p: 1,\n        s: undefined,\n        s_backup: undefined,\n        t: type,\n      },\n    })\n  }\n\n  // Filter and sort -----------------------------------------------------------\n\n  _getDefaultFilter(props = this.props) {\n    const { type } = props\n    const preferences = get(() => props.user.preferences)\n    const defaultFilterName = get(() => preferences.defaultHomeFilters[type])\n    return defined(\n      defaultFilterName &&\n        defined(\n          () => homeFilters[type][defaultFilterName],\n          () => preferences.filters[type][defaultFilterName]\n        ),\n      OPTIONS[type].defaultFilter\n    )\n  }\n\n  _getDefaultSort(props = this.props) {\n    const { sortOptions } = OPTIONS[props.type]\n    const defaultSort = find(sortOptions, 'default')\n    const urlSort = find(sortOptions, { sortBy: props.location.query.sortBy })\n\n    return {\n      sortBy: defined(\n        () => urlSort.sortBy,\n        () => defaultSort.sortBy,\n        'name_label'\n      ),\n      sortOrder: defined(\n        () => urlSort.sortOrder,\n        () => defaultSort.sortOrder,\n        'asc'\n      ),\n    }\n  }\n\n  _setSort(event) {\n    const { sortBy, sortOrder } = event.currentTarget.dataset\n    const { pathname, query } = this.props.location\n\n    this.setState({ sortBy, sortOrder })\n    this.context.router.replace({\n      pathname,\n      query: { ...query, sortBy },\n    })\n  }\n  _setSort = this._setSort.bind(this)\n\n  _initFilterAndSortBy(props) {\n    const filter = this._getFilter(props)\n\n    // If filter is null, set a default filter.\n    if (filter == null) {\n      const defaultFilter = this._getDefaultFilter(props)\n\n      if (defaultFilter != null) {\n        this._setFilter(defaultFilter, props, true)\n      }\n      return\n    }\n\n    // If the filter is already set, do nothing.\n    if (filter === this.props.filter) {\n      return\n    }\n\n    let properties\n    try {\n      properties = ComplexMatcher.getPropertyClausesStrings(ComplexMatcher.parse(filter))\n    } catch (_) {\n      properties = {}\n    }\n\n    const sort = this._getDefaultSort(props)\n\n    this.setState({\n      selectedHosts: properties.$container,\n      selectedPools: properties.$pool,\n      selectedPowerStates: properties.power_state,\n      selectedResourceSets: properties.resourceSet,\n      selectedTags: properties.tags,\n      ...sort,\n    })\n\n    const { filterInput } = this.refs\n    if (filterInput && filterInput.value !== filter) {\n      filterInput.value = filter\n    }\n  }\n\n  // Optionally can take the props to be able to use it in\n  // componentWillReceiveProps().\n  _getFilter(props = this.props) {\n    return props.location.query.s\n  }\n\n  _getParsedFilter = createSelector(\n    props => this._getFilter(),\n    filter => {\n      try {\n        return ComplexMatcher.parse(filter)\n      } catch (_) {}\n    }\n  )\n\n  _getFilterFunction = createSelector(this._getParsedFilter, filter => filter !== undefined && filter.createPredicate())\n\n  // Optionally can take the props to be able to use it in\n  // componentWillReceiveProps().\n  _setFilter(filter, props = this.props, replace) {\n    if (typeof filter !== 'string') {\n      filter = filter.toString()\n    }\n\n    const { pathname, query } = props.location\n    this.context.router[replace ? 'replace' : 'push']({\n      pathname,\n      query: { ...query, s: filter, p: 1 },\n    })\n  }\n\n  _clearFilter = () => this._setFilter('')\n\n  _onFilterChange = invoke(() => {\n    const setFilter = debounce(filter => {\n      this._setFilter(filter)\n    }, 500)\n\n    return event => setFilter(event.target.value)\n  })\n\n  _getFilteredItems = createSort(\n    createSelector(\n      createFilter(() => this.props.items, this._getFilterFunction),\n      createSelector(\n        () => this.props.location.query.backup,\n        () => this.props.jobs,\n        () => this.props.schedulesByJob,\n        (backup, jobs, schedulesByJob) => {\n          if (backup !== undefined) {\n            const pattern = {\n              __or: jobs\n                .filter(job => schedulesByJob[job.id].some(_ => _.enabled))\n                .map(job => omit(job.vms, 'power_state')),\n            }\n            return createPredicate(backup === 'true' ? pattern : { __not: pattern })\n          }\n        }\n      ),\n      (filteredVms, predicate) => (predicate === undefined ? filteredVms : filter(filteredVms, predicate))\n    ),\n    createSelector(\n      () => this.state.sortBy,\n      sortBy => {\n        const { sortOptions } = OPTIONS[this.props.type]\n        const sort = find(sortOptions, { sortBy })\n\n        return [(sort && sort.sortByFn) || sortBy, 'name_label']\n      }\n    ),\n    () => this.state.sortOrder\n  )\n\n  _getVisibleItems = createPager(\n    this._getFilteredItems,\n    () => this._getPage(),\n    () => this.state.homeItemsPerPage\n  )\n\n  _expandAll = () => this.setState({ expandAll: !this.state.expandAll })\n\n  _onPageSelection = page => {\n    const { pathname, query } = this.props.location\n    this.context.router.replace({\n      pathname,\n      query: { ...query, p: page },\n    })\n  }\n\n  _tick = isCriteria => <Icon icon={isCriteria ? 'success' : undefined} fixedWidth />\n\n  // High level filters --------------------------------------------------------\n\n  _typesDropdownItems = map(TYPES, (label, type) => (\n    <MenuItem key={type} onClick={() => this._setType(type)}>\n      {label}\n    </MenuItem>\n  ))\n\n  _updateSelectedPowerStates = powerStates =>\n    this._setFilter(\n      ComplexMatcher.setPropertyClause(\n        this._getParsedFilter(),\n        'power_state',\n        powerStates.length === 0\n          ? undefined\n          : new ComplexMatcher.Or(powerStates.map(_ => new ComplexMatcher.String(_.value)))\n      )\n    )\n\n  _updateSelectedPools = pools => {\n    const filter = this._getParsedFilter()\n\n    this._setFilter(\n      pools.length\n        ? ComplexMatcher.setPropertyClause(\n            filter,\n            '$pool',\n            new ComplexMatcher.Or(map(pools, pool => new ComplexMatcher.String(pool.id)))\n          )\n        : ComplexMatcher.setPropertyClause(filter, '$pool', undefined)\n    )\n  }\n  _updateSelectedHosts = hosts => {\n    const filter = this._getParsedFilter()\n\n    this._setFilter(\n      hosts.length\n        ? ComplexMatcher.setPropertyClause(\n            filter,\n            '$container',\n            new ComplexMatcher.Or(map(hosts, host => new ComplexMatcher.String(host.id)))\n          )\n        : ComplexMatcher.setPropertyClause(filter, '$container', undefined)\n    )\n  }\n  _updateSelectedTags = tags => {\n    const filter = this._getParsedFilter()\n\n    this._setFilter(\n      tags.length\n        ? ComplexMatcher.setPropertyClause(\n            filter,\n            'tags',\n            new ComplexMatcher.Or(map(tags, tag => new ComplexMatcher.RegExp(`^${escapeRegExp(tag.id)}$`)))\n          )\n        : ComplexMatcher.setPropertyClause(filter, 'tags', undefined)\n    )\n  }\n  _updateSelectedResourceSets = resourceSets => {\n    const filter = this._getParsedFilter()\n\n    this._setFilter(\n      resourceSets.length\n        ? ComplexMatcher.setPropertyClause(\n            filter,\n            'resourceSet',\n            new ComplexMatcher.Or(map(resourceSets, set => new ComplexMatcher.String(set.id)))\n          )\n        : ComplexMatcher.setPropertyClause(filter, 'resourceSet', undefined)\n    )\n  }\n  _addCustomFilter = () => {\n    return addCustomFilter(this._getType(), this._getFilter())\n  }\n  _getCustomFilters() {\n    const { preferences } = this.props.user || {}\n\n    if (!preferences) {\n      return\n    }\n\n    const customFilters = preferences.filters || {}\n    return customFilters[this._getType()]\n  }\n\n  // Checkboxes ----------------------------------------------------------------\n\n  _getIsAllSelected = createSelector(\n    () => this.state.selectedItems,\n    this._getVisibleItems,\n    (selectedItems, visibleItems) => size(visibleItems) > 0 && size(filter(selectedItems)) === size(visibleItems)\n  )\n  _getIsSomeSelected = createSelector(() => this.state.selectedItems, some)\n  _toggleMaster = () => {\n    const selectedItems = {}\n    if (!this._getIsAllSelected()) {\n      forEach(this._getVisibleItems(), ({ id }) => {\n        selectedItems[id] = true\n      })\n    }\n    this.setState({ selectedItems })\n  }\n  _getSelectedItemsIds = createSelector(\n    () => this.state.selectedItems,\n    items => keys(pickBy(items))\n  )\n\n  // Shortcuts -----------------------------------------------------------------\n\n  _getShortcutsHandler = createSelector(\n    () => this._getVisibleItems(),\n    items => (command, event) => {\n      event.preventDefault()\n      switch (command) {\n        case 'SEARCH':\n          this.refs.filterInput.focus()\n          break\n        case 'NAV_DOWN':\n          this.setState({\n            highlighted: (this.state.highlighted + 1) % items.length || 0,\n          })\n          break\n        case 'NAV_UP':\n          this.setState({\n            highlighted: this.state.highlighted > 0 ? this.state.highlighted - 1 : items.length - 1,\n          })\n          break\n        case 'SELECT': {\n          const itemId = items[this.state.highlighted].id\n          this.setState({\n            selectedItems: {\n              ...this.state.selectedItems,\n              [itemId]: !this.state.selectedItems[itemId],\n            },\n          })\n          break\n        }\n        case 'JUMP_INTO': {\n          const item = items[this.state.highlighted]\n          if (includes(['VM', 'host', 'pool', 'SR'], item && item.type)) {\n            this.context.router.push({\n              pathname: `${item.type.toLowerCase()}s/${item.id}`,\n            })\n          }\n        }\n      }\n    }\n  )\n\n  // Header --------------------------------------------------------------------\n\n  _getBackupFilter = createSelector(\n    () => this.props.location.query.backup,\n    backup => (backup === undefined ? 'all' : backup === 'true' ? 'backedUpVms' : 'notBackedUpVms')\n  )\n\n  _setBackupFilter = backupFilter => {\n    const { pathname, query } = this.props.location\n    this.context.router.push({\n      pathname,\n      query: {\n        ...query,\n        backup: backupFilter === 'all' ? undefined : backupFilter === 'backedUpVms',\n        p: 1,\n        s_backup: undefined,\n      },\n    })\n  }\n\n  _renderHeader() {\n    const customFilters = this._getCustomFilters()\n    const filteredItems = this._getFilteredItems()\n    const nItems = this._getNumberOfItems()\n    const { isAdmin, isPoolAdmin, items, noResourceSets, type } = this.props\n\n    const {\n      homeItemsPerPage,\n      selectedHosts,\n      selectedPools,\n      selectedPowerStates,\n      selectedResourceSets,\n      selectedTags,\n      sortBy,\n    } = this.state\n\n    const options = OPTIONS[type]\n    const { filters, mainActions, otherActions, showHostsSelector, showPoolsSelector, showResourceSetsSelector } =\n      options\n\n    return (\n      <Container>\n        <Row className={styles.itemRowHeader}>\n          <Col mediumSize={3}>\n            <DropdownButton id='typeMenu' bsStyle='info' title={TYPES[this._getType()]}>\n              {this._typesDropdownItems}\n            </DropdownButton>\n          </Col>\n          <Col mediumSize={6}>\n            <div className='input-group'>\n              <span className='input-group-btn'>\n                <DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>\n                  <MenuItem onClick={this._addCustomFilter}>{_('filterSaveAs')}</MenuItem>\n                  <MenuItem divider />\n                  {!isEmpty(customFilters) && [\n                    map(customFilters, (filter, name) => (\n                      <MenuItem key={`custom-${name}`} onClick={() => this._setFilter(filter)}>\n                        {name}\n                      </MenuItem>\n                    )),\n                    <MenuItem key='divider' divider />,\n                  ]}\n                  {map(filters, (filter, label) => (\n                    <MenuItem key={label} onClick={() => this._setFilter(filter)}>\n                      {_(label)}\n                    </MenuItem>\n                  ))}\n                </DropdownButton>\n              </span>\n              <input\n                className='form-control'\n                defaultValue={this._getFilter()}\n                onChange={this._onFilterChange}\n                ref='filterInput'\n                type='text'\n              />\n              <Tooltip content={_('filterSyntaxLinkTooltip')}>\n                <a\n                  className='input-group-addon'\n                  href='https://xen-orchestra.com/docs/manage_infrastructure.html#live-filter-search'\n                  rel='noopener noreferrer'\n                  target='_blank'\n                >\n                  <Icon icon='info' />\n                </a>\n              </Tooltip>\n              <span className='input-group-btn'>\n                <Button onClick={this._clearFilter}>\n                  <Icon icon='clear-search' />\n                </Button>\n              </span>\n            </div>\n          </Col>\n          {(isAdmin || (isPoolAdmin && process.env.XOA_PLAN > 3) || !noResourceSets) && (\n            <Col mediumSize={3} className='text-xs-right'>\n              <Link className='btn btn-success' to='/vms/new'>\n                <Icon icon='vm-new' /> {_('homeNewVm')}\n              </Link>\n            </Col>\n          )}\n        </Row>\n        <Row className={classNames(styles.itemRowHeader, 'mt-1')}>\n          <Col smallSize={6} mediumSize={2}>\n            <span>\n              <input\n                checked={this._getIsAllSelected()}\n                onChange={this._toggleMaster}\n                ref='masterCheckbox'\n                type='checkbox'\n              />{' '}\n              <span className='text-muted'>\n                {this._getNumberOfSelectedItems()\n                  ? _('homeSelectedItems', {\n                      icon: <Icon icon={type.toLowerCase()} />,\n                      selected: this._getNumberOfSelectedItems(),\n                      total: nItems,\n                    })\n                  : _('homeDisplayedItems', {\n                      displayed: filteredItems.length,\n                      icon: <Icon icon={type.toLowerCase()} />,\n                      total: nItems,\n                    })}\n              </span>\n            </span>\n          </Col>\n          <Col mediumSize={8} className='text-xs-right hidden-sm-down'>\n            {this._getNumberOfSelectedItems() ? (\n              <div>\n                {mainActions && (\n                  <div className='btn-group'>\n                    {map(mainActions, (action, key) => (\n                      <Tooltip content={action.tooltip} key={key}>\n                        <ActionButton {...action} handlerParam={this._getSelectedItemsIds()} />\n                      </Tooltip>\n                    ))}\n                  </div>\n                )}\n                {otherActions && (\n                  <DropdownButton bsStyle='secondary' id='advanced' title={_('homeMore')}>\n                    {map(otherActions, (action, key) => (\n                      <MenuItem\n                        key={key}\n                        onClick={() => {\n                          action.handler(this._getSelectedItemsIds(), action.params, this.props, this.context)\n                        }}\n                      >\n                        <Icon icon={action.icon} fixedWidth /> {_(action.labelId)}\n                      </MenuItem>\n                    ))}\n                  </DropdownButton>\n                )}\n              </div>\n            ) : (\n              <div>\n                {(type === 'VM' || type === 'host') && (\n                  <OverlayTrigger\n                    trigger='click'\n                    rootClose\n                    placement='bottom'\n                    overlay={\n                      <Popover className={styles.selectObject} id='powerStatePopover'>\n                        <Select\n                          autoFocus\n                          multi\n                          onChange={this._updateSelectedPowerStates}\n                          openOnFocus\n                          options={type === 'VM' ? POWER_STATE_VM : POWER_STATE_HOST}\n                          value={selectedPowerStates}\n                        />\n                      </Popover>\n                    }\n                  >\n                    <Button btnStyle='link'>\n                      <Icon icon='powerState' /> {_('powerState')}\n                    </Button>\n                  </OverlayTrigger>\n                )}\n                {isAdmin && type === 'VM' && (\n                  <OverlayTrigger\n                    trigger='click'\n                    rootClose\n                    placement='bottom'\n                    overlay={\n                      <Popover className={styles.selectObject} id='backupPopover'>\n                        <Select\n                          autoFocus\n                          onChange={this._setBackupFilter}\n                          openOnFocus\n                          options={BACKUP_FILTERS}\n                          required\n                          simpleValue\n                          value={this._getBackupFilter()}\n                        />\n                      </Popover>\n                    }\n                  >\n                    <Button btnStyle='link'>\n                      <Icon icon='backup' /> {_('backup')}\n                    </Button>\n                  </OverlayTrigger>\n                )}\n                {showPoolsSelector && (\n                  <OverlayTrigger\n                    trigger='click'\n                    rootClose\n                    placement='bottom'\n                    overlay={\n                      <Popover className={styles.selectObject} id='poolPopover'>\n                        <SelectPool autoFocus multi onChange={this._updateSelectedPools} value={selectedPools} />\n                      </Popover>\n                    }\n                  >\n                    <Button btnStyle='link'>\n                      <Icon icon='pool' /> {_('homeAllPools')}\n                    </Button>\n                  </OverlayTrigger>\n                )}\n                {showHostsSelector && (\n                  <OverlayTrigger\n                    trigger='click'\n                    rootClose\n                    placement='bottom'\n                    overlay={\n                      <Popover className={styles.selectObject} id='HostPopover'>\n                        <SelectHost autoFocus multi onChange={this._updateSelectedHosts} value={selectedHosts} />\n                      </Popover>\n                    }\n                  >\n                    <Button btnStyle='link'>\n                      <Icon icon='host' /> {_('homeAllHosts')}\n                    </Button>\n                  </OverlayTrigger>\n                )}\n                <OverlayTrigger\n                  autoFocus\n                  trigger='click'\n                  rootClose\n                  placement='bottom'\n                  overlay={\n                    <Popover className={styles.selectObject} id='tagPopover'>\n                      <SelectTag\n                        autoFocus\n                        multi\n                        objects={items}\n                        onChange={this._updateSelectedTags}\n                        value={selectedTags}\n                      />\n                    </Popover>\n                  }\n                >\n                  <Button btnStyle='link'>\n                    <Icon icon='tags' /> {_('homeAllTags')}\n                  </Button>\n                </OverlayTrigger>\n                {showResourceSetsSelector && isAdmin && !noResourceSets && (\n                  <OverlayTrigger\n                    trigger='click'\n                    rootClose\n                    placement='bottom'\n                    overlay={\n                      <Popover className={styles.selectObject} id='resourceSetPopover'>\n                        <SelectResourceSet\n                          autoFocus\n                          multi\n                          onChange={this._updateSelectedResourceSets}\n                          value={selectedResourceSets}\n                        />\n                      </Popover>\n                    }\n                  >\n                    <Button btnStyle='link'>\n                      <Icon icon='resource-set' /> {_('homeAllResourceSets')}\n                    </Button>\n                  </OverlayTrigger>\n                )}\n                <DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>\n                  {map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (\n                    <MenuItem key={key} data-sort-by={_sortBy} data-sort-order={sortOrder} onClick={this._setSort}>\n                      {this._tick(_sortBy === sortBy)}\n                      {_sortBy === sortBy ? <strong>{_(labelId)}</strong> : _(labelId)}\n                    </MenuItem>\n                  ))}\n                </DropdownButton>\n              </div>\n            )}\n          </Col>\n          <Col smallSize={6} mediumSize={2} className='text-xs-right'>\n            <Button onClick={this._expandAll}>\n              <Icon icon='nav' />\n            </Button>{' '}\n            <DropdownButton bsStyle='info' id='itemsPerPage' title={homeItemsPerPage}>\n              {ITEMS_PER_PAGE_OPTIONS.map(nItems => (\n                <MenuItem key={nItems} onClick={() => this._setNItemsPerPage(nItems)}>\n                  {nItems}\n                </MenuItem>\n              ))}\n            </DropdownButton>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n\n  // ---------------------------------------------------------------------------\n\n  render() {\n    const {\n      areObjectsFetched,\n      isAdmin,\n      isPoolAdmin,\n      location: {\n        query: { backup },\n      },\n      noResourceSets,\n      type,\n    } = this.props\n\n    if (!areObjectsFetched) {\n      return (\n        <CenterPanel>\n          <h2>\n            <img src='assets/loading.svg' />\n          </h2>\n        </CenterPanel>\n      )\n    }\n\n    const nItems = this._getNumberOfItems()\n\n    if (nItems < 1) {\n      return <NoObjects isAdmin={isAdmin} isPoolAdmin={isPoolAdmin} noResourceSets={noResourceSets} />\n    }\n\n    const filteredItems = this._getFilteredItems()\n    const visibleItems = this._getVisibleItems()\n    const { Item } = OPTIONS[type]\n    const { expandAll, highlighted, homeItemsPerPage, selectedItems } = this.state\n\n    // Necessary because indeterminate cannot be used as an attribute\n    if (this.refs.masterCheckbox) {\n      this.refs.masterCheckbox.indeterminate = this._getIsSomeSelected() && !this._getIsAllSelected()\n    }\n\n    return (\n      <Page header={this._renderHeader()}>\n        <Shortcuts handler={this._getShortcutsHandler()} isolate name='Home' targetNodeSelector='body' />\n        <div>\n          {backup !== undefined && <h5>{backup === 'true' ? _('backedUpVms') : _('notBackedUpVms')}</h5>}\n          <div className={styles.itemContainer}>\n            {isEmpty(filteredItems) ? (\n              <p className='text-xs-center mt-1'>\n                <a className='btn btn-link' onClick={this._clearFilter}>\n                  <Icon icon='info' /> {_('homeNoMatches')}\n                </a>\n              </p>\n            ) : (\n              map(visibleItems, (item, index) => (\n                <div key={item.id} className={highlighted === index ? styles.highlight : undefined}>\n                  <Item\n                    expandAll={expandAll}\n                    item={item}\n                    key={item.id}\n                    onSelect={this.toggleState(`selectedItems.${item.id}`)}\n                    selected={Boolean(selectedItems[item.id])}\n                  />\n                </div>\n              ))\n            )}\n          </div>\n          {filteredItems.length > homeItemsPerPage && (\n            <Row>\n              <div style={{ display: 'flex', width: '100%' }}>\n                <div style={{ margin: 'auto' }}>\n                  <Pagination\n                    onChange={this._onPageSelection}\n                    pages={ceil(filteredItems.length / homeItemsPerPage)}\n                    value={this._getPage()}\n                  />\n                </div>\n              </div>\n            </Row>\n          )}\n        </div>\n      </Page>\n    )\n  }\n}\n","import Component from 'base-component'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { Col, Row } from 'grid'\nimport { CpuSparkLines, LoadSparkLines, NetworkSparkLines, XvdSparkLines } from 'xo-sparklines'\n\nimport styles from './index.css'\n\nconst MINI_STATS_PROPS = {\n  height: 10,\n  strokeWidth: 0.2,\n  width: 50,\n}\n\nexport default class MiniStats extends Component {\n  static propTypes = {\n    fetchStats: PropTypes.func.isRequired,\n  }\n\n  state = {\n    statsIsPending: false,\n  }\n\n  _fetch = () => {\n    if (this.state.statsIsPending) {\n      return\n    }\n    this.setState({\n      statsIsPending: true,\n    })\n    this.props.fetch().then(stats => {\n      this.setState({ stats, statsIsPending: false })\n    })\n  }\n\n  componentWillMount() {\n    this._fetch()\n    this._interval = setInterval(this._fetch, 5e3)\n  }\n\n  componentWillUnmount() {\n    clearInterval(this._interval)\n  }\n\n  render() {\n    const { stats } = this.state\n\n    if (stats === undefined) {\n      return <Icon icon='loading' />\n    }\n\n    return (\n      <Row>\n        <Col mediumSize={4} className={styles.itemExpanded}>\n          <CpuSparkLines data={stats} {...MINI_STATS_PROPS} />\n        </Col>\n        <Col mediumSize={4} className={styles.itemExpanded}>\n          <NetworkSparkLines data={stats} {...MINI_STATS_PROPS} />\n        </Col>\n        <Col mediumSize={4} className={styles.itemExpanded}>\n          {stats.stats.load !== undefined ? (\n            <LoadSparkLines data={stats} {...MINI_STATS_PROPS} />\n          ) : (\n            <XvdSparkLines data={stats} {...MINI_STATS_PROPS} />\n          )}\n        </Col>\n      </Row>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Ellipsis, { EllipsisContainer } from 'ellipsis'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport HomeTags from 'home-tags'\nimport Tooltip from 'tooltip'\nimport Link, { BlockLink } from 'link'\nimport { Col } from 'grid'\nimport { Text } from 'editable'\nimport { addTag, editPool, getHostMissingPatches, removeTag } from 'xo'\nimport { connectStore, formatSizeShort } from 'utils'\nimport { compact, flatten, map, size, uniq } from 'lodash'\nimport { createGetObjectsOfType, createGetHostMetrics, createSelector } from 'selectors'\nimport { Host, Pool } from 'render-xo-item'\nimport { injectState } from 'reaclette'\n\nimport styles from './index.css'\n\nimport BulkIcons from '../../common/bulk-icons'\nimport { isAdmin } from '../../common/selectors'\nimport { ShortDate } from '../../common/utils'\nimport { getXoaPlan, SOURCES } from '../../common/xoa-plans'\n\n@connectStore(() => {\n  const getPoolHosts = createGetObjectsOfType('host').filter(\n    createSelector(\n      (_, props) => props.item.id,\n      poolId => host => host.$pool === poolId\n    )\n  )\n\n  const getMissingPatches = createSelector(getPoolHosts, hosts => {\n    return Promise.all(map(hosts, host => getHostMissingPatches(host))).then(patches =>\n      compact(uniq(map(flatten(patches), 'name')))\n    )\n  })\n\n  const getHostMetrics = createGetHostMetrics(getPoolHosts)\n\n  const getNumberOfSrs = createGetObjectsOfType('SR').count(\n    createSelector(\n      (_, props) => props.item.id,\n      poolId => obj => obj.$pool === poolId\n    )\n  )\n\n  const getNumberOfVms = createGetObjectsOfType('VM').count(\n    createSelector(\n      (_, props) => props.item.id,\n      poolId => obj => obj.$pool === poolId\n    )\n  )\n\n  return {\n    hostMetrics: getHostMetrics,\n    isAdmin,\n    missingPatches: getMissingPatches,\n    poolHosts: getPoolHosts,\n    nSrs: getNumberOfSrs,\n    nVms: getNumberOfVms,\n  }\n})\n@injectState\nexport default class PoolItem extends Component {\n  _addTag = tag => addTag(this.props.item.id, tag)\n  _removeTag = tag => removeTag(this.props.item.id, tag)\n  _setNameDescription = nameDescription => editPool(this.props.item, { name_description: nameDescription })\n  _setNameLabel = nameLabel => editPool(this.props.item, { name_label: nameLabel })\n  _toggleExpanded = () => this.setState({ expanded: !this.state.expanded })\n  _onSelect = () => this.props.onSelect(this.props.item.id)\n\n  componentWillMount() {\n    this.props.missingPatches.then(patches => this.setState({ missingPatchCount: size(patches) }))\n  }\n\n  _getPoolLicenseIconTooltip() {\n    const { state: reacletteState, item: pool } = this.props\n    const { earliestExpirationDate, nHostsUnderLicense, nHosts, supportLevel } =\n      reacletteState.poolLicenseInfoByPoolId[pool.id]\n    let tooltip = _('poolNoSupport')\n\n    if (getXoaPlan() === SOURCES) {\n      tooltip = _('poolSupportSourceUsers')\n    }\n    if (supportLevel === 'total') {\n      tooltip = _('earliestExpirationDate', {\n        dateString:\n          earliestExpirationDate === Infinity ? _('noExpiration') : <ShortDate timestamp={earliestExpirationDate} />,\n      })\n    }\n    if (supportLevel === 'partial') {\n      tooltip = _('poolPartialSupport', { nHostsLicense: nHostsUnderLicense, nHosts })\n    }\n    return tooltip\n  }\n\n  _isXcpngPool() {\n    return Object.values(this.props.poolHosts)[0].productBrand === 'XCP-ng'\n  }\n\n  _getPoolLicenseInfo = () => this.props.state.poolLicenseInfoByPoolId[this.props.item.id]\n\n  _getAreHostsVersionsEqual = () => this.props.state.areHostsVersionsEqualByPool[this.props.item.id]\n\n  _getAlerts = createSelector(\n    () => this.props.isAdmin,\n    this._getPoolLicenseInfo,\n    this._getAreHostsVersionsEqual,\n    () => this.props.poolHosts,\n    () => this.props.item.id,\n    (isAdmin, poolLicenseInfo, areHostsVersionsEqual, hosts, poolId) => {\n      const alerts = []\n\n      if (isAdmin && this._isXcpngPool()) {\n        const { icon, supportLevel } = poolLicenseInfo\n        if (supportLevel !== 'total') {\n          alerts.push({\n            level: 'warning',\n            render: (\n              <p>\n                {icon()} {this._getPoolLicenseIconTooltip()}\n              </p>\n            ),\n          })\n        }\n      }\n\n      if (!areHostsVersionsEqual) {\n        alerts.push({\n          level: 'danger',\n          render: (\n            <div>\n              <p>\n                <Icon icon='alarm' /> {_('notAllHostsHaveTheSameVersion', { pool: <Pool id={poolId} link /> })}\n              </p>\n              <ul>\n                {map(hosts, host => (\n                  <li>{_('keyValue', { key: <Host id={host.id} />, value: host.version })}</li>\n                ))}\n              </ul>\n            </div>\n          ),\n        })\n      }\n\n      return alerts\n    }\n  )\n\n  render() {\n    const { item: pool, expandAll, selected, hostMetrics, poolHosts, nSrs, nVms } = this.props\n    const { missingPatchCount } = this.state\n    const { icon, supportLevel } = this._getPoolLicenseInfo()\n    const master = poolHosts[pool.master]\n\n    return (\n      <div className={styles.item}>\n        <BlockLink to={`/pools/${pool.id}`}>\n          <SingleLineRow>\n            <Col smallSize={10} mediumSize={9} largeSize={3}>\n              <EllipsisContainer>\n                <input type='checkbox' checked={selected} onChange={this._onSelect} value={pool.id} />\n                &nbsp;&nbsp;\n                <Ellipsis>\n                  <Text value={pool.name_label} onChange={this._setNameLabel} useLongClick />\n                </Ellipsis>\n                &nbsp;\n                <BulkIcons alerts={this._getAlerts()} />\n                &nbsp;\n                {missingPatchCount > 0 && (\n                  <span>\n                    <Tooltip content={_('homeMissingPatches')}>\n                      <span className='tag tag-pill tag-danger'>{missingPatchCount}</span>\n                    </Tooltip>\n                  </span>\n                )}\n                &nbsp;\n                {isAdmin && this._isXcpngPool() && supportLevel === 'total' && icon(this._getPoolLicenseIconTooltip())}\n                &nbsp;\n                {pool.HA_enabled && (\n                  <span>\n                    &nbsp;&nbsp;\n                    <Tooltip content={_('highAvailability')}>\n                      <span className='fa-stack'>\n                        <Icon icon='pool' />\n                        <Icon icon='success' className='fa-stack-1x' />\n                      </span>\n                    </Tooltip>\n                  </span>\n                )}\n              </EllipsisContainer>\n            </Col>\n            <Col mediumSize={1} className='hidden-md-down'>\n              <EllipsisContainer>\n                <span className={styles.itemActionButons}>\n                  <Tooltip\n                    content={\n                      <span>\n                        {hostMetrics.count}x {_('hostsTabName')}\n                      </span>\n                    }\n                  >\n                    {hostMetrics.count > 0 ? (\n                      <Link to={`/home?s=$pool:${pool.id}&t=host`}>\n                        <Icon icon='host' size='1' fixedWidth />\n                      </Link>\n                    ) : (\n                      <Icon icon='host' size='1' fixedWidth />\n                    )}\n                  </Tooltip>\n                  &nbsp;\n                  <Tooltip\n                    content={\n                      <span>\n                        {nVms}x {_('vmsTabName')}\n                      </span>\n                    }\n                  >\n                    {nVms > 0 ? (\n                      <Link to={`/home?s=$pool:${pool.id}&t=VM`}>\n                        <Icon icon='vm' size='1' fixedWidth />\n                      </Link>\n                    ) : (\n                      <Icon icon='vm' size='1' fixedWidth />\n                    )}\n                  </Tooltip>\n                  &nbsp;\n                  <Tooltip\n                    content={\n                      <span>\n                        {nSrs}x {_('srsTabName')}\n                      </span>\n                    }\n                  >\n                    {nSrs > 0 ? (\n                      <Link to={`/home?s=$pool:${pool.id}&t=SR`}>\n                        <Icon icon='sr' size='1' fixedWidth />\n                      </Link>\n                    ) : (\n                      <Icon icon='sr' size='1' fixedWidth />\n                    )}\n                  </Tooltip>\n                </span>\n              </EllipsisContainer>\n            </Col>\n            <Col mediumSize={4} className='hidden-md-down'>\n              <EllipsisContainer>\n                <Ellipsis>\n                  <Text value={pool.name_description} onChange={this._setNameDescription} useLongClick />\n                </Ellipsis>\n              </EllipsisContainer>\n            </Col>\n            <Col largeSize={4} className='hidden-lg-down'>\n              <span>\n                <Tooltip\n                  content={_('memoryLeftTooltip', {\n                    used: Math.round((hostMetrics.memoryUsage / hostMetrics.memoryTotal) * 100),\n                    free: formatSizeShort(hostMetrics.memoryTotal - hostMetrics.memoryUsage),\n                  })}\n                >\n                  <progress\n                    style={{ margin: 0 }}\n                    className='progress'\n                    value={(hostMetrics.memoryUsage / hostMetrics.memoryTotal) * 100}\n                    max='100'\n                  />\n                </Tooltip>\n              </span>\n            </Col>\n            <Col mediumSize={1} className={styles.itemExpandRow}>\n              <a className={styles.itemExpandButton} onClick={this._toggleExpanded}>\n                <Icon icon='nav' fixedWidth />\n                &nbsp;&nbsp;&nbsp;\n              </a>\n            </Col>\n          </SingleLineRow>\n        </BlockLink>\n        {(this.state.expanded || expandAll) && (\n          <SingleLineRow>\n            <Col mediumSize={3} className={styles.itemExpanded}>\n              <span>\n                {hostMetrics.count}x <Icon icon='host' /> {nVms}x <Icon icon='vm' /> {nSrs}x <Icon icon='sr' />{' '}\n                {hostMetrics.cpus}\n                x <Icon icon='cpu' /> {formatSizeShort(hostMetrics.memoryTotal)} <Icon icon='memory' />\n              </span>\n            </Col>\n            <Col mediumSize={1} className={styles.itemExpanded}>\n              {master.productBrand} {master.version}\n            </Col>\n            <Col mediumSize={5}>\n              <div style={{ fontSize: '1.4em' }}>\n                <HomeTags type='pool' labels={pool.tags} onDelete={this._removeTag} onAdd={this._addTag} />\n              </div>\n            </Col>\n            <Col mediumSize={3} className={styles.itemExpanded}>\n              <span>\n                {_('homePoolMaster')}{' '}\n                <Link to={`/hosts/${pool.master}`}>{poolHosts && poolHosts[pool.master].name_label}</Link>\n              </span>\n            </Col>\n          </SingleLineRow>\n        )}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport BulkIcons from 'bulk-icons'\nimport Component from 'base-component'\nimport Ellipsis, { EllipsisContainer } from 'ellipsis'\nimport Icon from 'icon'\nimport Link, { BlockLink } from 'link'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport Tooltip from 'tooltip'\nimport HomeTags from 'home-tags'\nimport { Col } from 'grid'\nimport { map, size, sum, some } from 'lodash'\nimport { Text } from 'editable'\nimport { createGetObject, createGetObjectsOfType, createSelector } from 'selectors'\nimport { addTag, editSr, isSrShared, reconnectAllHostsSr, removeTag, setDefaultSr } from 'xo'\nimport { connectStore, formatSizeShort, getIscsiPaths } from 'utils'\nimport { injectState } from 'reaclette'\n\nimport styles from './index.css'\n\n@connectStore({\n  coalesceTask: createSelector(\n    (_, props) => props.item,\n    createGetObjectsOfType('task').groupBy('applies_to'),\n    (sr, tasks) => tasks[sr.uuid]?.[0]\n  ),\n  container: createGetObject((_, props) => props.item.$container),\n  isHa: createSelector(\n    (_, props) => props.item,\n    createGetObject((_, props) => props.item.$poolId),\n    (sr, pool) => pool?.haSrs.includes(sr.id) ?? false\n  ),\n  isDefaultSr: createSelector(\n    createGetObjectsOfType('pool').find((_, props) => pool => props.item.$pool === pool.id),\n    (_, props) => props.item,\n    (pool, sr) => pool && pool.default_SR === sr.id\n  ),\n  isShared: createSelector((_, props) => props.item, isSrShared),\n  status: createSelector(\n    (_, props) => Boolean(props.item.sm_config.multipathable),\n    createGetObjectsOfType('PBD').filter((_, props) => pbd => pbd.SR === props.item.id),\n    (multipathable, pbds) => {\n      const nbAttached = sum(map(pbds, pbd => (pbd.attached ? 1 : 0)))\n      const nbPbds = size(pbds)\n      if (!nbPbds) {\n        return -1\n      }\n      if (!nbAttached) {\n        return 0\n      }\n      if (nbAttached < nbPbds) {\n        return 1\n      }\n\n      const hasInactivePath =\n        multipathable &&\n        some(pbds, pbd => {\n          const [nActives, nPaths] = getIscsiPaths(pbd)\n          return nActives !== nPaths\n        })\n\n      return hasInactivePath ? 3 : 2\n    }\n  ),\n})\n@injectState\nexport default class SrItem extends Component {\n  _addTag = tag => addTag(this.props.item.id, tag)\n  _removeTag = tag => removeTag(this.props.item.id, tag)\n  _setNameLabel = nameLabel => editSr(this.props.item, { nameLabel })\n  _toggleExpanded = () => this.setState({ expanded: !this.state.expanded })\n  _onSelect = () => this.props.onSelect(this.props.item.id)\n\n  _reconnectAllHostSr = () => reconnectAllHostsSr(this.props.item)\n  _setDefaultSr = () => setDefaultSr(this.props.item)\n\n  _getStatusPill = () => {\n    switch (this.props.status) {\n      case -1:\n        return (\n          <Tooltip content={_('srAllDisconnected')}>\n            <Icon icon='all-disconnected' />\n          </Tooltip>\n        )\n      case 0:\n        return (\n          <Tooltip content={_('srAllDisconnected')}>\n            <Icon icon='all-disconnected' />\n          </Tooltip>\n        )\n      case 1:\n        return (\n          <Tooltip content={_('srSomeConnected')}>\n            <Icon icon='some-connected' />\n          </Tooltip>\n        )\n      case 2:\n        return (\n          <Tooltip content={_('srAllConnected')}>\n            <Icon icon='all-connected' />\n          </Tooltip>\n        )\n      case 3:\n        return (\n          <Tooltip content={_('hasInactivePath')}>\n            <Icon icon='some-connected' />\n          </Tooltip>\n        )\n    }\n  }\n\n  getXostorLicenseInfo = createSelector(\n    () => this.props.state.xostorLicenseInfoByXostorId,\n    () => this.props.item,\n    (xostorLicenseInfoByXostorId, sr) => xostorLicenseInfoByXostorId?.[sr.id]\n  )\n\n  render() {\n    const { coalesceTask, container, expandAll, isDefaultSr, isHa, isShared, item: sr, selected } = this.props\n\n    const xostorLicenseInfo = this.getXostorLicenseInfo()\n\n    return (\n      <div className={styles.item}>\n        <BlockLink to={`/srs/${sr.id}`}>\n          <SingleLineRow>\n            <Col smallSize={9} mediumSize={8} largeSize={3}>\n              <EllipsisContainer>\n                <input type='checkbox' checked={selected} onChange={this._onSelect} value={sr.id} />\n                &nbsp;&nbsp;\n                {this._getStatusPill()}\n                &nbsp;&nbsp;\n                <Ellipsis>\n                  <Text value={sr.name_label} onChange={this._setNameLabel} useLongClick />\n                </Ellipsis>\n                {isDefaultSr && <span className='tag tag-pill tag-info ml-1'>{_('defaultSr')}</span>}\n                {isHa && (\n                  <Tooltip content={_('srHaTooltip')}>\n                    <span className='tag tag-pill tag-info ml-1'>{_('ha')}</span>\n                  </Tooltip>\n                )}\n                {coalesceTask !== undefined && (\n                  <Tooltip content={`${coalesceTask.name_label} ${Math.round(coalesceTask.progress * 100)}%`}>\n                    <Icon icon='coalesce' fixedWidth />\n                  </Tooltip>\n                )}\n                {sr.inMaintenanceMode && <span className='tag tag-pill tag-warning ml-1'>{_('maintenanceMode')}</span>}\n                {xostorLicenseInfo?.supportEnabled && (\n                  <Tooltip content={_('xostorProSupportEnabled')}>\n                    <Icon icon='pro-support' fixedWidth className='text-success ml-1' />\n                  </Tooltip>\n                )}\n                {xostorLicenseInfo?.alerts.length > 0 && <BulkIcons alerts={xostorLicenseInfo.alerts} />}\n              </EllipsisContainer>\n            </Col>\n            <Col largeSize={1} className='hidden-md-down'>\n              <EllipsisContainer>\n                <span className={styles.itemActionButons}>\n                  <Tooltip content={_('srReconnectAll')}>\n                    <a onClick={this._reconnectAllHostSr}>\n                      <Icon icon='sr-reconnect-all' size='1' />\n                    </a>\n                  </Tooltip>{' '}\n                  <Tooltip content={_('setAsDefaultSr')}>\n                    <a onClick={this._setDefaultSr}>\n                      <Icon icon='disk' size='1' />\n                    </a>\n                  </Tooltip>\n                </span>\n              </EllipsisContainer>\n            </Col>\n            <Col largeSize={2} className='hidden-md-down'>\n              {isShared ? _('srSharedType', { type: sr.SR_type }) : sr.SR_type}\n            </Col>\n            <Col smallSize={2} mediumSize={2} largeSize={2}>\n              {formatSizeShort(sr.size)}\n            </Col>\n            <Col largeSize={2} className='hidden-md-down'>\n              {sr.size > 0 && (\n                <Tooltip\n                  content={_('spaceLeftTooltip', {\n                    used: String(Math.round((sr.physical_usage / sr.size) * 100)),\n                    free: formatSizeShort(sr.size - sr.physical_usage),\n                  })}\n                >\n                  <progress\n                    style={{ margin: 0 }}\n                    className='progress'\n                    value={(sr.physical_usage / sr.size) * 100}\n                    max='100'\n                  />\n                </Tooltip>\n              )}\n            </Col>\n            <Col mediumSize={1} largeSize={1} className='hidden-sm-down'>\n              {container && <Link to={`/${container.type}s/${container.id}`}>{container.name_label}</Link>}\n            </Col>\n            <Col mediumSize={1} className={styles.itemExpandRow}>\n              <a className={styles.itemExpandButton} onClick={this._toggleExpanded}>\n                <Icon icon='nav' fixedWidth />\n                &nbsp;&nbsp;&nbsp;\n              </a>\n            </Col>\n          </SingleLineRow>\n        </BlockLink>\n        {(this.state.expanded || expandAll) && (\n          <SingleLineRow>\n            <Col mediumSize={1} className={styles.itemExpanded}>\n              {sr.VDIs.length}x <Icon icon='disk' />\n            </Col>\n            <Col mediumSize={4}>\n              <div style={{ fontSize: '1.4em' }}>\n                <HomeTags type='SR' labels={sr.tags} onDelete={this._removeTag} onAdd={this._addTag} />\n              </div>\n            </Col>\n          </SingleLineRow>\n        )}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport defined from '@xen-orchestra/defined'\nimport Ellipsis, { EllipsisContainer } from 'ellipsis'\nimport Icon from 'icon'\nimport Link from 'link'\nimport map from 'lodash/map'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport HomeTags from 'home-tags'\nimport Tooltip from 'tooltip'\nimport { Row, Col } from 'grid'\nimport { Number, Size, Text } from 'editable'\nimport { addTag, editVm, removeTag } from 'xo'\nimport { connectStore, osFamily } from 'utils'\nimport { createGetObject } from 'selectors'\n\nimport styles from './index.css'\n\n@connectStore({\n  container: createGetObject((_, props) => props.item.$container),\n})\nexport default class TemplateItem extends Component {\n  _addTag = tag => addTag(this.props.item.id, tag)\n  _onSelect = () => this.props.onSelect(this.props.item.id)\n  _removeTag = tag => removeTag(this.props.item.id, tag)\n  _setNameDescription = nameDescription => editVm(this.props.item, { name_description: nameDescription })\n  _setNameLabel = nameLabel => editVm(this.props.item, { name_label: nameLabel })\n  _setCpus = nCpus => editVm(this.props.item, { CPUs: nCpus })\n  _setMemory = memory => editVm(this.props.item, { memory })\n\n  render() {\n    const { item: vm, container, expandAll, selected } = this.props\n    return (\n      <div className={styles.item}>\n        <SingleLineRow>\n          <Col smallSize={10} mediumSize={9} largeSize={5}>\n            <EllipsisContainer>\n              <input type='checkbox' checked={selected} onChange={this._onSelect} value={vm.id} />\n              &nbsp;&nbsp;\n              <Ellipsis>\n                <Text\n                  value={vm.name_label}\n                  onChange={this._setNameLabel}\n                  placeholder={_('templateHomeNamePlaceholder')}\n                />\n              </Ellipsis>\n            </EllipsisContainer>\n          </Col>\n          <Col mediumSize={4} className='hidden-md-down'>\n            <EllipsisContainer>\n              <Tooltip content={vm.os_version ? vm.os_version.name : _('unknownOsName')}>\n                <Icon className='text-info' icon={vm.os_version && osFamily(vm.os_version.distro)} fixedWidth />\n              </Tooltip>{' '}\n              <Ellipsis>\n                <Text\n                  value={vm.name_description}\n                  onChange={this._setNameDescription}\n                  placeholder={_('templateHomeDescriptionPlaceholder')}\n                />\n              </Ellipsis>\n            </EllipsisContainer>\n          </Col>\n          <Col mediumSize={2} className='hidden-sm-down'>\n            {container && <Link to={`/${container.type}s/${container.id}`}>{container.name_label}</Link>}\n          </Col>\n          <Col mediumSize={1} className={styles.itemExpandRow}>\n            <a className={styles.itemExpandButton} onClick={this.toggleState('expanded')}>\n              <Icon icon='nav' fixedWidth />\n              &nbsp;&nbsp;&nbsp;\n            </a>\n          </Col>\n        </SingleLineRow>\n        {(this.state.expanded || expandAll) && (\n          <Row>\n            <Col mediumSize={4} className={styles.itemExpanded}>\n              <span>\n                <Number value={vm.CPUs.number} onChange={this._setCpus} />x <Icon icon='cpu' className='mr-1' />\n                <Size value={defined(vm.memory.size, null)} onChange={this._setMemory} /> <Icon icon='memory' />\n              </span>\n            </Col>\n            <Col largeSize={4} className={styles.itemExpanded}>\n              {map(vm.addresses, address => (\n                <span key={address} className='tag tag-info tag-ip'>\n                  {address}\n                </span>\n              ))}\n            </Col>\n            <Col mediumSize={4}>\n              <div style={{ fontSize: '1.4em' }}>\n                <HomeTags type='VM-template' labels={vm.tags} onDelete={this._removeTag} onAdd={this._addTag} />\n              </div>\n            </Col>\n          </Row>\n        )}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport BulkIcons from 'bulk-icons'\nimport Component from 'base-component'\nimport Ellipsis, { EllipsisContainer } from 'ellipsis'\nimport Icon from 'icon'\nimport Link, { BlockLink } from 'link'\nimport React from 'react'\nimport renderXoItem from 'render-xo-item'\nimport SingleLineRow from 'single-line-row'\nimport HomeTags from 'home-tags'\nimport Tooltip from 'tooltip'\nimport { Row, Col } from 'grid'\nimport { Text, XoSelect } from 'editable'\nimport { isEmpty, map } from 'lodash'\nimport { addTag, editVm, fetchVmStats, migrateVm, removeTag, startVm, stopVm, subscribeResourceSets } from 'xo'\nimport { addSubscriptions, connectStore, formatSizeShort, osFamily } from 'utils'\nimport { createFinder, createGetObject, createGetVmDisks, createSelector, createSumBy, isAdmin } from 'selectors'\n\nimport MiniStats from './mini-stats'\nimport styles from './index.css'\n\n@addSubscriptions({\n  resourceSets: subscribeResourceSets,\n})\n@connectStore(() => ({\n  container: createGetObject((_, props) => props.item.$container),\n  isAdmin,\n  totalDiskSize: createSumBy(\n    createGetVmDisks((_, props) => props.item),\n    'size'\n  ),\n}))\nexport default class VmItem extends Component {\n  get _isRunning() {\n    const vm = this.props.item\n    return vm && vm.power_state === 'Running'\n  }\n\n  compareContainers = (pool1, pool2) => {\n    const { $pool: poolId } = this.props.item\n    return pool1.id === poolId ? -1 : pool2.id === poolId ? 1 : 0\n  }\n\n  _getResourceSet = createFinder(\n    () => this.props.resourceSets,\n    createSelector(\n      () => this.props.item.resourceSet,\n      id => resourceSet => resourceSet.id === id\n    )\n  )\n\n  _addTag = tag => addTag(this.props.item.id, tag)\n  _fetchStats = () => fetchVmStats(this.props.item.id)\n  _migrateVm = host => migrateVm(this.props.item, host)\n  _removeTag = tag => removeTag(this.props.item.id, tag)\n  _setNameDescription = nameDescription => editVm(this.props.item, { name_description: nameDescription })\n  _setNameLabel = nameLabel => editVm(this.props.item, { name_label: nameLabel })\n  _start = () => startVm(this.props.item)\n  _stop = () => stopVm(this.props.item)\n  _toggleExpanded = () => this.setState({ expanded: !this.state.expanded })\n  _onSelect = () => this.props.onSelect(this.props.item.id)\n\n  _getVmState = createSelector(\n    () => this.props.item.power_state,\n    () => this.props.item.current_operations,\n    (powerState, operations) => (!isEmpty(operations) ? 'Busy' : powerState)\n  )\n\n  _getAlerts = createSelector(\n    () => this.props.item,\n    vm => {\n      const alerts = []\n\n      if (vm.vulnerabilities.xsa468 && !vm.tags.includes('HIDE_XSA468')) {\n        const { reason, driver, version } = vm.vulnerabilities.xsa468\n\n        if (reason === 'no-pv-drivers-detected') {\n          alerts.push({\n            level: 'warning',\n            render: (\n              <p>\n                <span>\n                  We cannot detect the version of Windows PV drivers on this VM. You may be running an outdated version.\n                  Check XCP-ng's{' '}\n                  <a href='https://docs.xcp-ng.org/vms/#windows-guest-tools-security' target='_blank' rel='noreferrer'>\n                    Windows Guest Tools Security documentation\n                  </a>{' '}\n                  for more details.\n                </span>\n                <br />\n                <br />\n                Still seeing this message even though you updated PV drivers? Please update your XCP-ng.\n              </p>\n            ),\n          })\n        } else {\n          alerts.push({\n            level: 'danger',\n            render: (\n              <p>\n                <span>\n                  This VM is running a Windows PV driver vulnerable to XSA-468 ({driver} {version}). You must upgrade\n                  your Windows PV drivers now. See{' '}\n                  <a\n                    href='https://docs.xcp-ng.org/vms/#xsa-468-multiple-windows-pv-driver-vulnerabilities'\n                    target='_blank'\n                    rel='noreferrer'\n                  >\n                    XCP-ng's documentation\n                  </a>{' '}\n                  for more details.\n                </span>\n              </p>\n            ),\n          })\n        }\n      }\n\n      return alerts\n    }\n  )\n\n  render() {\n    const { item: vm, container, expandAll, isAdmin, selected } = this.props\n    const resourceSet = this._getResourceSet()\n    const state = this._getVmState()\n\n    return (\n      <div className={styles.item}>\n        <BlockLink to={`/vms/${vm.id}`}>\n          <SingleLineRow>\n            <Col smallSize={10} mediumSize={6} largeSize={5}>\n              <EllipsisContainer>\n                <input type='checkbox' checked={selected} onChange={this._onSelect} value={vm.id} />\n                &nbsp;&nbsp;\n                <Tooltip\n                  content={\n                    <span>\n                      {_(`powerState${state}`)}\n                      {state === 'Busy' && (\n                        <span>\n                          {' ('}\n                          {map(vm.current_operations)[0]}\n                          {')'}\n                        </span>\n                      )}\n                    </span>\n                  }\n                >\n                  <Icon icon={state.toLowerCase()} />\n                </Tooltip>\n                &nbsp;&nbsp;\n                <Ellipsis>\n                  <Text\n                    value={vm.name_label}\n                    onChange={this._setNameLabel}\n                    placeholder={_('vmHomeNamePlaceholder')}\n                    useLongClick\n                  />\n                </Ellipsis>\n                &nbsp;\n                <BulkIcons alerts={this._getAlerts()} />\n              </EllipsisContainer>\n            </Col>\n            <Col mediumSize={4} className='hidden-md-down'>\n              <EllipsisContainer>\n                <span className={styles.itemActionButons}>\n                  {this._isRunning ? (\n                    <span>\n                      <Tooltip content={_('vmConsoleLabel')}>\n                        <Link to={`/vms/${vm.id}/console`}>\n                          <Icon icon='vm-console' size='1' fixedWidth />\n                        </Link>\n                      </Tooltip>\n                      <Tooltip content={_('stopVmLabel')}>\n                        <a onClick={this._stop}>\n                          <Icon icon='vm-stop' size='1' fixedWidth />\n                        </a>\n                      </Tooltip>\n                    </span>\n                  ) : (\n                    <span>\n                      <Icon fixedWidth />\n                      <Tooltip content={_('startVmLabel')}>\n                        <a onClick={this._start}>\n                          <Icon icon='vm-start' size='1' fixedWidth />\n                        </a>\n                      </Tooltip>\n                    </span>\n                  )}\n                </span>\n                <Tooltip content={vm.os_version ? vm.os_version.name : _('unknownOsName')}>\n                  <Icon className='text-info' icon={vm.os_version && osFamily(vm.os_version.distro)} fixedWidth />\n                </Tooltip>{' '}\n                <Ellipsis>\n                  <Text\n                    value={vm.name_description}\n                    onChange={this._setNameDescription}\n                    placeholder={_('vmHomeDescriptionPlaceholder')}\n                    useLongClick\n                  />\n                </Ellipsis>\n              </EllipsisContainer>\n            </Col>\n            <Col mediumSize={2} className='hidden-sm-down'>\n              {this._isRunning && container ? (\n                <XoSelect\n                  compareContainers={this.compareContainers}\n                  labelProp='name_label'\n                  onChange={this._migrateVm}\n                  placeholder={_('homeMigrateTo')}\n                  useLongClick\n                  value={container}\n                  xoType='host'\n                >\n                  <Link to={`/${container.type}s/${container.id}`}>{renderXoItem(container)}</Link>\n                </XoSelect>\n              ) : (\n                container && <Link to={`/${container.type}s/${container.id}`}>{renderXoItem(container)}</Link>\n              )}\n            </Col>\n            <Col mediumSize={1} className={styles.itemExpandRow}>\n              <a className={styles.itemExpandButton} onClick={this._toggleExpanded}>\n                <Icon icon='nav' fixedWidth />\n                &nbsp;&nbsp;&nbsp;\n              </a>\n            </Col>\n          </SingleLineRow>\n        </BlockLink>\n        {(this.state.expanded || expandAll) && (\n          <Row>\n            <Col mediumSize={4} className={styles.itemExpanded}>\n              <span>\n                {vm.CPUs.number}x <Icon icon='cpu' /> &nbsp; {formatSizeShort(vm.memory.size)} <Icon icon='memory' />{' '}\n                &nbsp; {formatSizeShort(this.props.totalDiskSize)} <Icon icon='disk' /> &nbsp;{' '}\n                {isEmpty(vm.snapshots) ? null : (\n                  <span>\n                    {vm.snapshots.length}x <Icon icon='vm-snapshot' />\n                  </span>\n                )}\n                {vm.docker ? <Icon icon='vm-docker' /> : null}\n              </span>\n            </Col>\n            <Col mediumSize={2} className='hidden-sm-down'>\n              <EllipsisContainer>\n                <Ellipsis>\n                  {resourceSet && (\n                    <span>\n                      {_('homeResourceSet', {\n                        resourceSet: isAdmin ? (\n                          <Link to={`self?resourceSet=${resourceSet.id}`}>{resourceSet.name}</Link>\n                        ) : (\n                          resourceSet.name\n                        ),\n                      })}\n                    </span>\n                  )}\n                </Ellipsis>\n              </EllipsisContainer>\n            </Col>\n            <Col mediumSize={6} className={styles.itemExpanded}>\n              {map(vm.addresses, address => (\n                <span key={address} className='tag tag-info tag-ip'>\n                  {address}\n                </span>\n              ))}\n            </Col>\n            <Col mediumSize={6}>\n              <div style={{ fontSize: '1.4em' }}>\n                <HomeTags type='VM' labels={vm.tags} onDelete={this._removeTag} onAdd={this._addTag} />\n              </div>\n            </Col>\n            <Col mediumSize={6} className={styles.itemExpanded}>\n              {this._isRunning && <MiniStats fetch={this._fetchStats} />}\n            </Col>\n          </Row>\n        )}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionBar, { Action } from 'action-bar'\nimport React from 'react'\nimport {\n  // disableHost,\n  emergencyShutdownHost,\n  restartHost,\n  restartHostAgent,\n  startHost,\n  stopHost,\n} from 'xo'\n\nconst hostActionBarByState = {\n  Running: ({ host }) => (\n    <ActionBar display='icon' handlerParam={host}>\n      <Action handler={stopHost} icon='host-stop' label={_('stopHostLabel')} />\n      <Action handler={restartHostAgent} icon='host-restart-agent' label={_('restartHostAgent')} />\n      <Action handler={emergencyShutdownHost} icon='host-emergency-shutdown' label={_('emergencyModeLabel')} />\n      <Action handler={restartHost} icon='host-reboot' label={_('rebootHostLabel')} />\n    </ActionBar>\n  ),\n  Halted: ({ host }) => (\n    <ActionBar display='icon' handlerParam={host}>\n      <Action handler={startHost} icon='host-start' label={_('startHostLabel')} />\n    </ActionBar>\n  ),\n}\n\nconst HostActionBar = ({ host }) => {\n  const ActionBar = hostActionBarByState[host.power_state]\n\n  if (!ActionBar) {\n    return <p>No action bar for state {host.power_state}</p>\n  }\n\n  return <ActionBar host={host} />\n}\nexport default HostActionBar\n","import _ from 'intl'\nimport InconsistentHostTimeWarning from 'inconsistent-host-time-warning'\nimport Copiable from 'copiable'\nimport HostActionBar from './action-bar'\nimport Icon from 'icon'\nimport Link from 'link'\nimport { NavLink, NavTabs } from 'nav'\nimport Page from '../page'\nimport PropTypes from 'prop-types'\nimport React, { cloneElement, Component } from 'react'\nimport Tooltip from 'tooltip'\nimport { Text } from 'editable'\nimport { Container, Row, Col } from 'grid'\nimport { Pool } from 'render-xo-item'\nimport { editHost, fetchHostStats, subscribeHostMissingPatches } from 'xo'\nimport { connectStore, routes } from 'utils'\nimport {\n  createDoesHostNeedRestart,\n  createFilter,\n  createGetHostState,\n  createGetObject,\n  createGetObjectsOfType,\n  createSelector,\n} from 'selectors'\nimport { isEmpty, map, mapValues, pick, sortBy } from 'lodash'\n\nimport TabAdvanced from './tab-advanced'\nimport TabConsole from './tab-console'\nimport TabGeneral from './tab-general'\nimport TabLogs from './tab-logs'\nimport TabNetwork from './tab-network'\nimport TabPatches from './tab-patches'\nimport TabStats from './tab-stats'\nimport TabStorage from './tab-storage'\n\nconst isRunning = host => host && host.power_state === 'Running'\n\n// ===================================================================\n\n@routes('general', {\n  advanced: TabAdvanced,\n  console: TabConsole,\n  general: TabGeneral,\n  logs: TabLogs,\n  network: TabNetwork,\n  patches: TabPatches,\n  stats: TabStats,\n  storage: TabStorage,\n})\n@connectStore(() => {\n  const getHost = createGetObject()\n\n  const getPool = createGetObject((state, props) => getHost(state, props).$pool)\n\n  const getVmController = createGetObjectsOfType('VM-controller').find(\n    createSelector(\n      getHost,\n      ({ id }) =>\n        obj =>\n          obj.$container === id\n    )\n  )\n\n  const getHostVms = createGetObjectsOfType('VM').filter(\n    createSelector(\n      getHost,\n      ({ id }) =>\n        obj =>\n          obj.$container === id\n    )\n  )\n\n  const getNumberOfVms = getHostVms.count()\n\n  const getLogs = createGetObjectsOfType('message')\n    .filter(\n      createSelector(\n        getHost,\n        getVmController,\n        (host, controller) =>\n          ({ $object }) =>\n            $object === host.id || $object === (controller !== undefined && controller.id)\n      )\n    )\n    .sort()\n\n  const getPifs = createGetObjectsOfType('PIF')\n    .pick(createSelector(getHost, host => host.$PIFs))\n    .sort()\n\n  const getNetworks = createGetObjectsOfType('network').pick(\n    createSelector(getPifs, pifs => map(pifs, pif => pif.$network))\n  )\n\n  const getDecoratedPifs = createSelector(getPifs, getNetworks, (pifs, networks) =>\n    mapValues(pifs, pif => {\n      const network = networks[pif.$network]\n      if (network === undefined) {\n        return pif\n      }\n      return { ...pif, nework$name_label: network.name_label }\n    })\n  )\n\n  const getPrivateNetworks = createFilter(\n    createGetObjectsOfType('network'),\n    createSelector(getPool, pool => network => network.$pool === pool.id && isEmpty(network.PIFs))\n  )\n\n  const getHostPatches = createGetObjectsOfType('patch').pick(createSelector(getHost, host => host.patches))\n\n  const doesNeedRestart = createDoesHostNeedRestart(getHost)\n\n  const getHostState = createGetHostState(getHost)\n\n  return (state, props) => {\n    const host = getHost(state, props)\n    if (!host) {\n      return {}\n    }\n\n    return {\n      host,\n      hostPatches: host.productBrand !== 'XCP-ng' && getHostPatches(state, props),\n      logs: getLogs(state, props),\n      needsRestart: doesNeedRestart(state, props),\n      networks: getNetworks(state, props),\n      nVms: getNumberOfVms(state, props),\n      pifs: getDecoratedPifs(state, props),\n      pool: getPool(state, props),\n      privateNetworks: getPrivateNetworks(state, props),\n      state: getHostState(state, props),\n      vmController: getVmController(state, props),\n      vms: getHostVms(state, props),\n    }\n  }\n})\nexport default class Host extends Component {\n  static contextTypes = {\n    router: PropTypes.object,\n  }\n\n  loop(host = this.props.host) {\n    if (host == null) {\n      return\n    }\n\n    if (this.cancel) {\n      this.cancel()\n    }\n\n    if (!isRunning(host)) {\n      return\n    }\n\n    let cancelled = false\n    this.cancel = () => {\n      cancelled = true\n    }\n\n    fetchHostStats(host).then(stats => {\n      if (cancelled) {\n        return\n      }\n      this.cancel = null\n\n      clearTimeout(this.timeout)\n      this.setState(\n        {\n          statsOverview: stats,\n        },\n        () => {\n          this.timeout = setTimeout(this.loop, stats.interval * 1000)\n        }\n      )\n    })\n  }\n  loop = ::this.loop\n\n  componentDidMount() {\n    this.loop()\n    this._subscribePatches(this.props.host)\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this.timeout)\n    this.unsubscribeHostMissingPatches()\n  }\n\n  componentWillReceiveProps(props) {\n    const hostNext = props.host\n    const hostCur = this.props.host\n\n    if (hostCur && !hostNext) {\n      return this.context.router.push('/')\n    }\n\n    if (!hostNext) {\n      return\n    }\n\n    this._subscribePatches(hostNext)\n\n    if (!isRunning(hostCur) && isRunning(hostNext)) {\n      this.loop(hostNext)\n    } else if (isRunning(hostCur) && !isRunning(hostNext)) {\n      this.setState({\n        statsOverview: undefined,\n      })\n    }\n  }\n\n  _subscribePatches(host) {\n    if (host === undefined) {\n      return\n    }\n\n    if (this.unsubscribeHostMissingPatches !== undefined) {\n      this.unsubscribeHostMissingPatches()\n    }\n\n    this.unsubscribeHostMissingPatches = subscribeHostMissingPatches(host, missingPatches =>\n      this.setState({\n        missingPatches: missingPatches && sortBy(missingPatches, patch => -patch.time),\n      })\n    )\n  }\n\n  _setNameDescription = nameDescription => editHost(this.props.host, { name_description: nameDescription })\n  _setNameLabel = nameLabel => editHost(this.props.host, { name_label: nameLabel })\n\n  header() {\n    const { host, pool, state } = this.props\n    const { missingPatches } = this.state || {}\n    if (!host) {\n      return <Icon icon='loading' />\n    }\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={6} className='header-title'>\n            {pool !== undefined && <Pool id={pool.id} link />}\n            <h2>\n              <Tooltip\n                content={\n                  <span>\n                    {_(`powerState${state}`)}\n                    {state === 'Busy' && (\n                      <span>\n                        {' ('}\n                        {map(host.current_operations)[0]}\n                        {')'}\n                      </span>\n                    )}\n                  </span>\n                }\n              >\n                <Icon icon={`host-${state.toLowerCase()}`} />\n              </Tooltip>{' '}\n              <Text value={host.name_label} onChange={this._setNameLabel} />\n              {this.props.needsRestart && (\n                <Tooltip content={_('rebootUpdateHostLabel')}>\n                  <Link to={`/hosts/${host.id}/patches`}>\n                    <Icon icon='alarm' />\n                  </Link>\n                </Tooltip>\n              )}\n              &nbsp;\n              <InconsistentHostTimeWarning host={host} />\n            </h2>\n            <Copiable tagName='pre' className='text-muted mb-0'>\n              {host.uuid}\n            </Copiable>\n            <Text value={host.name_description} onChange={this._setNameDescription} />\n          </Col>\n          <Col mediumSize={6}>\n            <div className='text-xs-center'>\n              <HostActionBar host={host} />\n            </div>\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <NavTabs>\n              <NavLink to={`/hosts/${host.id}/general`}>{_('generalTabName')}</NavLink>\n              <NavLink to={`/hosts/${host.id}/stats`}>{_('statsTabName')}</NavLink>\n              <NavLink to={`/hosts/${host.id}/console`}>{_('consoleTabName')}</NavLink>\n              <NavLink to={`/hosts/${host.id}/network`}>{_('networkTabName')}</NavLink>\n              <NavLink to={`/hosts/${host.id}/storage`}>{_('storageTabName')}</NavLink>\n              <NavLink to={`/hosts/${host.id}/patches`}>\n                {_('patchesTabName')}{' '}\n                {isEmpty(missingPatches) ? null : (\n                  <span className='tag tag-pill tag-danger'>{missingPatches.length}</span>\n                )}\n              </NavLink>\n              <NavLink to={`/hosts/${host.id}/logs`}>{_('logsTabName')}</NavLink>\n              <NavLink to={`/hosts/${host.id}/advanced`}>{_('advancedTabName')}</NavLink>\n            </NavTabs>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n\n  render() {\n    const { host, pool } = this.props\n    if (!host) {\n      return <h1>{_('statusLoading')}</h1>\n    }\n    const childProps = Object.assign(\n      pick(this.props, [\n        'host',\n        'hostPatches',\n        'logs',\n        'memoryUsed',\n        'networks',\n        'nVms',\n        'pbds',\n        'pifs',\n        'privateNetworks',\n        'srs',\n        'vmController',\n        'vms',\n      ]),\n      pick(this.state, ['missingPatches', 'statsOverview'])\n    )\n    return (\n      <Page header={this.header()} title={`${host.name_label}${pool ? ` (${pool.name_label})` : ''}`}>\n        {cloneElement(this.props.children, childProps)}\n      </Page>\n    )\n  }\n}\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport { Col, Container } from 'grid'\nimport { form } from 'modal'\nimport { generateId } from 'reaclette-utils'\nimport { installCertificateOnHost } from 'xo'\nimport { provideState, injectState } from 'reaclette'\nimport { Textarea as DebounceTextarea } from 'debounce-input-decorator'\n\nconst InstallCertificateModal = decorate([\n  provideState({\n    effects: {\n      onChange(_, { target: { name, value } }) {\n        const { props } = this\n        props.onChange({\n          ...props.value,\n          [name]: value,\n        })\n      },\n    },\n    computed: {\n      inputCertificateChainId: generateId,\n      inputCertificateId: generateId,\n      inputPrivateKeyId: generateId,\n    },\n  }),\n  injectState,\n  ({ state, effects, value }) => (\n    <Container>\n      <SingleLineRow>\n        <Col mediumSize={4}>\n          <label htmlFor={state.inputCertificateId}>\n            <strong>{_('certificate')}</strong>\n          </label>\n        </Col>\n        <Col mediumSize={8}>\n          <DebounceTextarea\n            className='form-control text-monospace'\n            id={state.inputCertificateId}\n            name='certificate'\n            onChange={effects.onChange}\n            required\n            value={value.certificate}\n          />\n        </Col>\n      </SingleLineRow>\n      <SingleLineRow className='mt-1'>\n        <Col mediumSize={4}>\n          <label htmlFor={state.inputPrivateKeyId}>\n            <strong>{_('privateKey')}</strong>\n          </label>\n        </Col>\n        <Col mediumSize={8}>\n          <DebounceTextarea\n            className='form-control text-monospace'\n            id={state.inputPrivateKeyId}\n            name='privateKey'\n            onChange={effects.onChange}\n            required\n            value={value.privateKey}\n          />\n        </Col>\n      </SingleLineRow>\n      <SingleLineRow className='mt-1'>\n        <Col mediumSize={4}>\n          <label htmlFor={state.inputCertificateChainId}>\n            <strong>{_('certificateChain')}</strong>\n          </label>\n        </Col>\n        <Col mediumSize={8}>\n          <DebounceTextarea\n            className='form-control text-monospace'\n            id={state.inputCertificateChainId}\n            name='certificateChain'\n            onChange={effects.onChange}\n            value={value.certificateChain}\n          />\n        </Col>\n      </SingleLineRow>\n    </Container>\n  ),\n])\n\nconst installCertificate = async ({ id, isNewInstallation = false }) => {\n  const { certificate, certificateChain, privateKey } = await form({\n    defaultValue: {\n      certificate: '',\n      certificateChain: '',\n      privateKey: '',\n    },\n    render: props => <InstallCertificateModal {...props} />,\n    header: (\n      <span>\n        <Icon icon='upload' /> {isNewInstallation ? _('installNewCertificate') : _('replaceExistingCertificate')}\n      </span>\n    ),\n  })\n\n  await installCertificateOnHost(id, {\n    certificate: certificate.trim(),\n    chain: certificateChain.trim(),\n    privateKey: privateKey.trim(),\n  })\n}\n\nexport { installCertificate }\n","import _ from 'intl'\nimport React from 'react'\nimport Icon from 'icon'\nimport Tooltip from 'tooltip'\nimport { alert } from 'modal'\n\nexport const LICENSE_WARNING_BODY = (\n  <span>\n    <a href='https://xcp-ng.com/pricing.html#xcpngvsxenserver' rel='noopener noreferrer' target='_blank'>\n      {_('actionsRestricted')}\n    </a>{' '}\n    {_('counterRestrictionsOptions')}\n    <ul>\n      <li>\n        <a href='https://github.com/xcp-ng/xcp/wiki/Upgrade-from-XenServer' rel='noopener noreferrer' target='_blank'>\n          {_('counterRestrictionsOptionsXcp')}\n        </a>\n      </li>\n      <li>{_('counterRestrictionsOptionsXsLicense')}</li>\n    </ul>\n  </span>\n)\n\nconst showInfo = () => alert(_('licenseRestrictionsModalTitle'), LICENSE_WARNING_BODY)\n\nconst LicenseWarning = ({ iconSize = 'sm' }) => (\n  <Tooltip content={_('licenseRestrictions')}>\n    <a className='text-danger' style={{ cursor: 'pointer' }} onClick={showInfo}>\n      <Icon icon='alarm' size={iconSize} />\n    </a>\n  </Tooltip>\n)\n\nexport default LicenseWarning\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport BulkIcons from 'bulk-icons'\nimport Component from 'base-component'\nimport Copiable from 'copiable'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SelectFiles from 'select-files'\nimport StateButton from 'state-button'\nimport TabButton from 'tab-button'\nimport Tooltip from 'tooltip'\nimport Upgrade from 'xoa-upgrade'\nimport { addSubscriptions, compareVersions, connectStore, formatSize, getIscsiPaths } from 'utils'\nimport { confirm, form } from 'modal'\nimport { Container, Row, Col } from 'grid'\nimport { CustomFields } from 'custom-fields'\nimport { createGetObject, createGetObjectsOfType, createSelector } from 'selectors'\nimport { forEach, isEmpty, map, noop } from 'lodash'\nimport { FormattedRelative, FormattedTime } from 'react-intl'\nimport { Sr } from 'render-xo-item'\nimport { Text } from 'editable'\nimport { Toggle, Select, SizeInput } from 'form'\nimport * as xoaPlans from 'xoa-plans'\nimport {\n  detachHost,\n  disableHost,\n  editHost,\n  editPusb,\n  enableAdvancedLiveTelemetry,\n  enableHost,\n  forgetHost,\n  hidePcis,\n  installSupplementalPack,\n  isHyperThreadingEnabledHost,\n  isPciHidden,\n  isPciPassthroughAvailable,\n  isNetDataInstalledOnHost,\n  getPlugin,\n  getSmartctlHealth,\n  getSmartctlInformation,\n  restartHost,\n  setControlDomainMemory,\n  setHostsMultipathing,\n  setRemoteSyslogHost,\n  setSchedulerGranularity,\n  subscribeMdadmHealth,\n  subscribeSchedulerGranularity,\n  toggleMaintenanceMode,\n} from 'xo'\n\nimport SortedTable from 'sorted-table'\nimport { installCertificate } from './install-certificate'\n\nconst ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1\nconst ALLOW_SMART_REBOOT = xoaPlans.CURRENT.value >= xoaPlans.PREMIUM.value\nconst PUSBS_COLUMNS = [\n  {\n    name: _('vendorId'),\n    itemRenderer: pusb => pusb.vendorId,\n  },\n  {\n    name: _('description'),\n    itemRenderer: pusb => pusb.description,\n  },\n  {\n    name: _('version'),\n    itemRenderer: pusb => pusb.version,\n  },\n  {\n    name: _('labelSpeed'),\n    itemRenderer: pusb => pusb.speed,\n  },\n  {\n    name: _('enabled'),\n    itemRenderer: pusb => {\n      const _editPusb = value => editPusb(pusb, { enabled: value })\n      return <Toggle value={pusb.passthroughEnabled} onChange={_editPusb} />\n    },\n  },\n]\nconst PCIS_COLUMNS = [\n  {\n    name: _('id'),\n    itemRenderer: pci => {\n      const { uuid } = pci\n      return (\n        <Copiable data={uuid} tagName='p'>\n          {uuid.slice(4, 8)}\n        </Copiable>\n      )\n    },\n  },\n  {\n    default: true,\n    name: _('pciId'),\n    itemRenderer: pci => pci.pci_id,\n    sortCriteria: pci => pci.pci_id,\n  },\n  {\n    name: _('className'),\n    itemRenderer: pci => pci.class_name,\n    sortCriteria: pci => pci.class_name,\n  },\n  {\n    name: _('deviceName'),\n    itemRenderer: pci => pci.device_name,\n    sortCriteria: pci => pci.device_name,\n  },\n  {\n    name: _('passthroughEnabled'),\n    itemRenderer: (pci, { pciStateById, isPciPassthroughAvailable }) => {\n      if (pciStateById === undefined) {\n        return <Icon icon='loading' />\n      }\n\n      if (!isPciPassthroughAvailable) {\n        return (\n          <Tooltip content={_('onlyAvailableXcp8.3OrHigher')}>\n            <Toggle disabled />\n          </Tooltip>\n        )\n      }\n\n      const { error, isHidden } = pciStateById[pci.id]\n      if (error !== undefined) {\n        return (\n          <Tooltip content={error}>\n            <Icon icon='alarm' color='text-danger' />\n          </Tooltip>\n        )\n      }\n\n      const _hidePcis = value => hidePcis([pci], value)\n      return <Toggle value={isHidden} onChange={_hidePcis} />\n    },\n    sortCriteria: (pci, { pciStateById }) => pciStateById?.[pci.id]?.isHidden,\n  },\n]\nconst PCIS_ACTIONS = [\n  {\n    handler: pcis => hidePcis(pcis, false),\n    icon: 'toggle-off',\n    label: _('disable'),\n    level: 'primary',\n  },\n  {\n    handler: pcis => hidePcis(pcis, true),\n    icon: 'toggle-on',\n    label: _('enable'),\n    level: 'primary',\n  },\n]\n\nconst SCHED_GRAN_TYPE_OPTIONS = [\n  {\n    label: _('core'),\n    value: 'core',\n  },\n  {\n    label: _('cpu'),\n    value: 'cpu',\n  },\n  {\n    label: _('socket'),\n    value: 'socket',\n  },\n]\n\nconst downloadLogs = async uuid => {\n  await confirm({\n    title: _('hostDownloadLogs'),\n    body: _('hostDownloadLogsContainEntireHostLogs'),\n  })\n  window.open(`./rest/v0/hosts/${uuid}/logs.tgz`)\n}\n\nconst forceReboot = host => restartHost(host, true)\n\nconst smartReboot = ALLOW_SMART_REBOOT\n  ? host => restartHost(host, false, true, false, false) // don't force, suspend resident VMs, don't bypass blocked suspend, don't bypass current VM check\n  : () => {}\n\nconst formatPack = ({ name, author, description, version }, key) => (\n  <tr key={key}>\n    <th>{_('supplementalPackTitle', { author, name })}</th>\n    <td>{description}</td>\n    <td>{version}</td>\n  </tr>\n)\n\nconst getPackId = ({ author, name }) => `${author}\\0${name}`\n\nconst SetControlDomainMemory = ({ value, onChange }) => (\n  <Container>\n    <Row className='mb-1'>\n      <Col>\n        <Icon icon='error' /> {_('setControlDomainMemoryMessage')}\n      </Col>\n    </Row>\n    <Row>\n      <Col size={6}>{_('vmMemory')}</Col>\n      <Col size={6}>\n        <SizeInput required value={value} onChange={onChange} />\n      </Col>\n    </Row>\n  </Container>\n)\n\nconst MultipathableSrs = decorate([\n  connectStore({\n    pbds: createGetObjectsOfType('PBD').filter(\n      (_, { hostId }) =>\n        pbd =>\n          pbd.host === hostId && Boolean(pbd.otherConfig.multipathed)\n    ),\n  }),\n  ({ pbds }) =>\n    isEmpty(pbds) ? (\n      <div>{_('hostNoIscsiSr')}</div>\n    ) : (\n      <Container>\n        {map(pbds, pbd => {\n          const [nActives, nPaths] = getIscsiPaths(pbd)\n          const nSessions = pbd.otherConfig.iscsi_sessions\n          return (\n            <Row key={pbd.id}>\n              <Col>\n                <Sr id={pbd.SR} link newTab container={false} />{' '}\n                {nActives !== undefined &&\n                  nPaths !== undefined &&\n                  _('hostMultipathingPaths', {\n                    nActives,\n                    nPaths,\n                  })}{' '}\n                {nSessions !== undefined && _('iscsiSessions', { nSessions })}\n              </Col>\n            </Row>\n          )\n        })}\n      </Container>\n    ),\n])\n\nMultipathableSrs.propTypes = {\n  hostId: PropTypes.string.isRequired,\n}\n\n@addSubscriptions(props => ({\n  schedGran: cb => subscribeSchedulerGranularity(props.host.id, cb),\n  mdadmHealth: subscribeMdadmHealth(props.host),\n}))\n@connectStore(() => {\n  const getControlDomain = createGetObject((_, { host }) => host.controlDomain)\n\n  const getPgpus = createGetObjectsOfType('PGPU')\n    .pick((_, { host }) => host.$PGPUs)\n    .sort()\n\n  const getPcis = createGetObjectsOfType('PCI').filter(\n    (_, { host }) =>\n      pci =>\n        pci.$host === host.id\n  )\n\n  const getPusbs = createGetObjectsOfType('PUSB').filter(\n    (_, { host }) =>\n      pusb =>\n        pusb.host === host.id\n  )\n\n  return {\n    controlDomain: getControlDomain,\n    pcis: getPcis,\n    pgpus: getPgpus,\n    pusbs: getPusbs,\n  }\n})\nexport default class extends Component {\n  async componentDidMount() {\n    const { host } = this.props\n    const plugin = await getPlugin('netdata')\n    const isNetDataPluginCorrectlySet = plugin !== undefined && plugin.loaded\n    this.setState({ isNetDataPluginCorrectlySet })\n    if (isNetDataPluginCorrectlySet) {\n      this.setState({\n        isNetDataPluginInstalledOnHost: await isNetDataInstalledOnHost(host),\n      })\n    }\n\n    const smartctlHealth = await getSmartctlHealth(host).catch(console.error)\n    const isSmartctlHealthEnabled = smartctlHealth != null\n    const smartctlUnhealthyDevices = isSmartctlHealthEnabled\n      ? Object.keys(smartctlHealth).filter(deviceName => smartctlHealth[deviceName] !== 'PASSED')\n      : undefined\n\n    let unhealthyDevicesAlerts\n    if (smartctlUnhealthyDevices?.length > 0) {\n      const unhealthyDeviceInformations = await getSmartctlInformation(host, smartctlUnhealthyDevices)\n      unhealthyDevicesAlerts = map(unhealthyDeviceInformations, (value, key) => ({\n        level: 'warning',\n        render: <pre>{_('keyValue', { key, value: JSON.stringify(value, null, 2) })}</pre>,\n      }))\n    }\n\n    const _isPciPassthroughAvailable = isPciPassthroughAvailable(host)\n    const pciStateById = {}\n    if (_isPciPassthroughAvailable) {\n      await Promise.all(\n        Object.keys(this.props.pcis).map(async id => {\n          const pciSate = {}\n          try {\n            pciSate.isHidden = await isPciHidden(id)\n          } catch (error) {\n            console.error(error)\n            pciSate.error = error.message\n          }\n          pciStateById[id] = pciSate\n        })\n      )\n    }\n\n    this.setState({\n      isHtEnabled: await isHyperThreadingEnabledHost(host).catch(() => null),\n      isSmartctlHealthEnabled,\n      pciStateById,\n      smartctlUnhealthyDevices,\n      unhealthyDevicesAlerts,\n      isPciPassthroughAvailable: _isPciPassthroughAvailable,\n    })\n  }\n\n  _getPacks = createSelector(\n    () => this.props.host.supplementalPacks,\n    packs => {\n      const uniqPacks = {}\n      let packId, previousPack\n      forEach(packs, pack => {\n        packId = getPackId(pack)\n        if (\n          (previousPack = uniqPacks[packId]) === undefined ||\n          compareVersions(pack.version, previousPack.version) > 0\n        ) {\n          uniqPacks[packId] = pack\n        }\n      })\n      return uniqPacks\n    }\n  )\n\n  displayMdadmStatus = createSelector(\n    () => this.props.mdadmHealth,\n    mdadmHealth => {\n      if (mdadmHealth == null) {\n        return _('installRaidPlugin')\n      }\n\n      const raidState = mdadmHealth.raid?.State\n      if (raidState === undefined) {\n        return _('noRaidInformationAvailable')\n      }\n\n      return ['clean', 'active'].includes(raidState) ? _('raidHealthy') : _('raidStateWarning', { state: raidState })\n    }\n  )\n\n  _setSchedulerGranularity = value => setSchedulerGranularity(this.props.host.id, value)\n\n  _setHostIscsiIqn = iscsiIqn =>\n    confirm({\n      icon: 'alarm',\n      title: _('editHostIscsiIqnTitle'),\n      body: (\n        <div>\n          <p>{_('editHostIscsiIqnMessage')}</p>\n          <p className='text-muted'>\n            <Icon icon='info' /> {_('uniqueHostIscsiIqnInfo')}\n          </p>\n        </div>\n      ),\n    }).then(() => editHost(this.props.host, { iscsiIqn }), noop)\n\n  _setRemoteSyslogHost = value => setRemoteSyslogHost(this.props.host, value === '' ? null : value)\n\n  _setControlDomainMemory = () =>\n    form({\n      component: SetControlDomainMemory,\n      defaultValue: this.props.controlDomain.memory.size,\n      header: (\n        <span>\n          <Icon icon='memory' /> {_('setControlDomainMemory')}\n        </span>\n      ),\n    }).then(memory => setControlDomainMemory(this.props.host.id, memory), noop)\n\n  _accessAdvancedLiveTelemetry = () => window.open(`./netdata/host/${encodeURIComponent(this.props.host.hostname)}/`)\n\n  _enableAdvancedLiveTelemetry = async host => {\n    await enableAdvancedLiveTelemetry(host)\n    this.setState({\n      isNetDataPluginInstalledOnHost: await isNetDataInstalledOnHost(host),\n    })\n  }\n\n  render() {\n    const { controlDomain, host, pcis, pgpus, pusbs, schedGran } = this.props\n    const {\n      isHtEnabled,\n      isNetDataPluginInstalledOnHost,\n      isNetDataPluginCorrectlySet,\n      isSmartctlHealthEnabled,\n      unhealthyDevicesAlerts,\n      smartctlUnhealthyDevices,\n    } = this.state\n\n    const _isXcpNgHost = host.productBrand === 'XCP-ng'\n\n    const telemetryButton = isNetDataPluginInstalledOnHost ? (\n      <TabButton\n        btnStyle='success'\n        handler={this._accessAdvancedLiveTelemetry}\n        handlerParam={host}\n        icon='telemetry'\n        labelId='advancedLiveTelemetry'\n      />\n    ) : (\n      <TabButton\n        btnStyle='success'\n        disabled={!_isXcpNgHost || !isNetDataPluginCorrectlySet}\n        handler={this._enableAdvancedLiveTelemetry}\n        handlerParam={host}\n        icon='telemetry'\n        labelId='enableAdvancedLiveTelemetry'\n      />\n    )\n\n    return (\n      <Container>\n        <Row>\n          <Col className='text-xs-right'>\n            {!isNetDataPluginCorrectlySet ? (\n              <Tooltip content={_('pluginNetDataIsNecessary')}>{telemetryButton}</Tooltip>\n            ) : !_isXcpNgHost ? (\n              <Tooltip content={_('xcpOnlyFeature')}>{telemetryButton}</Tooltip>\n            ) : (\n              telemetryButton\n            )}\n            <TabButton\n              btnStyle='warning'\n              handler={downloadLogs}\n              handlerParam={host.uuid}\n              icon='logs'\n              labelId='hostDownloadLogs'\n            />\n            {host.power_state === 'Running' && [\n              <TabButton\n                key='smart-reboot'\n                btnStyle='warning'\n                disabled={!ALLOW_SMART_REBOOT}\n                handler={smartReboot}\n                handlerParam={host}\n                icon='freeze'\n                labelId='smartRebootHostLabel'\n                tooltip={ALLOW_SMART_REBOOT ? _('smartRebootHostTooltip') : _('availableXoaPremium')}\n              />,\n              <TabButton\n                key='force-reboot'\n                btnStyle='warning'\n                handler={forceReboot}\n                handlerParam={host}\n                icon='host-force-reboot'\n                labelId='forceRebootHostLabel'\n              />,\n            ]}\n            {host.enabled ? (\n              <TabButton\n                btnStyle='warning'\n                handler={toggleMaintenanceMode}\n                handlerParam={host}\n                icon='host-disable'\n                labelId='enableMaintenanceMode'\n                tooltip={_('maintenanceHostTooltip')}\n              />\n            ) : (\n              <TabButton\n                btnStyle='success'\n                handler={toggleMaintenanceMode}\n                handlerParam={host}\n                icon='host-enable'\n                labelId='disableMaintenanceMode'\n              />\n            )}\n            {host.enabled ? (\n              <TabButton\n                btnStyle='warning'\n                handler={disableHost}\n                handlerParam={host}\n                icon='host-forget'\n                labelId='disableHostLabel'\n              />\n            ) : (\n              <TabButton\n                btnStyle='success'\n                handler={enableHost}\n                handlerParam={host}\n                icon='host-enable'\n                labelId='enableHostLabel'\n              />\n            )}\n\n            <TabButton\n              btnStyle='danger'\n              handler={detachHost}\n              handlerParam={host}\n              icon='host-eject'\n              labelId='detachHost'\n            />\n            {host.power_state !== 'Running' && (\n              <TabButton\n                btnStyle='danger'\n                handler={forgetHost}\n                handlerParam={host}\n                icon='host-forget'\n                labelId='forgetHost'\n              />\n            )}\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <h3>{_('xenSettingsLabel')}</h3>\n            <table className='table'>\n              <tbody>\n                <tr>\n                  <th>{_('hostAddress')}</th>\n                  <Copiable tagName='td'>{host.address}</Copiable>\n                </tr>\n                <tr>\n                  <th>{_('hostStatus')}</th>\n                  <td>{host.enabled ? _('hostStatusEnabled') : _('hostStatusDisabled')}</td>\n                </tr>\n                {host.chipset_info.iommu !== undefined && (\n                  <tr>\n                    <th>\n                      <Tooltip content={_('hostIommuTooltip')}>{_('hostIommu')}</Tooltip>\n                    </th>\n                    <td>{host.chipset_info.iommu ? _('stateEnabled') : _('stateDisabled')}</td>\n                  </tr>\n                )}\n                <tr>\n                  <th>{_('hostPowerOnMode')}</th>\n                  <td>\n                    <Toggle disabled onChange={noop} value={Boolean(host.powerOnMode)} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('hostControlDomainMemory')}</th>\n                  <td>\n                    {controlDomain !== undefined && (\n                      <span>\n                        {formatSize(controlDomain.memory.size)}{' '}\n                        <Tooltip content={host.enabled ? _('maintenanceModeRequired') : _('setControlDomainMemory')}>\n                          <ActionButton\n                            btnStyle='primary'\n                            disabled={host.enabled}\n                            handler={this._setControlDomainMemory}\n                            icon='edit'\n                            size='small'\n                          />\n                        </Tooltip>\n                      </span>\n                    )}\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('hostStartedSince')}</th>\n                  <td>\n                    {_('started', {\n                      ago: <FormattedRelative value={host.startTime * 1000} />,\n                    })}\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('hostStackStartedSince')}</th>\n                  <td>\n                    {_('started', {\n                      ago: <FormattedRelative value={host.agentStartTime * 1000} />,\n                    })}\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('hostXenServerVersion')}</th>\n                  <Copiable tagName='td' data={host.version}>\n                    {host.license_params.sku_marketing_name} {host.version} ({host.license_params.sku_type})\n                  </Copiable>\n                </tr>\n                <tr>\n                  <th>{_('hostBuildNumber')}</th>\n                  <Copiable tagName='td'>{host.build}</Copiable>\n                </tr>\n                <tr>\n                  <th>{_('hostIscsiIqn')}</th>\n                  <td>\n                    <Text onChange={this._setHostIscsiIqn} value={host.iscsiIqn} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('multipathing')}</th>\n                  <td>\n                    <StateButton\n                      className='mb-1'\n                      data-host={host}\n                      data-multipathing={!host.multipathing}\n                      disabledLabel={_('stateDisabled')}\n                      disabledTooltip={_('enableMultipathing')}\n                      enabledLabel={_('stateEnabled')}\n                      enabledTooltip={_('disableMultipathing')}\n                      handler={setHostsMultipathing}\n                      state={host.multipathing}\n                    />\n                    {host.multipathing && <MultipathableSrs hostId={host.id} />}\n                  </td>\n                </tr>\n                {schedGran != null && (\n                  <tr>\n                    <th>{_('schedulerGranularity')}</th>\n                    <td>\n                      <Select\n                        onChange={this._setSchedulerGranularity}\n                        options={SCHED_GRAN_TYPE_OPTIONS}\n                        required\n                        simpleValue\n                        value={schedGran}\n                      />\n                      <small>{_('rebootUpdateHostLabel')}</small>\n                    </td>\n                  </tr>\n                )}\n                <tr>\n                  <th>{_('hostRemoteSyslog')}</th>\n                  <td>\n                    <Text value={host.logging.syslog_destination || ''} onChange={this._setRemoteSyslogHost} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('customFields')}</th>\n                  <td>\n                    <CustomFields object={host.id} />\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n            <br />\n            <h3>{_('hardwareHostSettingsLabel')}</h3>\n            <table className='table'>\n              <tbody>\n                <tr>\n                  <th>{_('hostCpusModel')}</th>\n                  <Copiable tagName='td'>{host.CPUs.modelname}</Copiable>\n                </tr>\n                <tr>\n                  <th>{_('hostGpus')}</th>\n                  <td>{map(pgpus, pgpu => pcis[pgpu.pci].device_name).join(', ')}</td>\n                </tr>\n                <tr>\n                  <th>{_('hostCpusNumber')}</th>\n                  <td>\n                    {host.cpus.cores} ({host.cpus.sockets})\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('hyperThreading')}</th>\n                  <td>\n                    {isHtEnabled === null\n                      ? _('hyperThreadingNotAvailable')\n                      : isHtEnabled\n                        ? _('stateEnabled')\n                        : _('stateDisabled')}\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('hostManufacturerinfo')}</th>\n                  <Copiable tagName='td'>\n                    {host.bios_strings['system-manufacturer']} ({host.bios_strings['system-product-name']})\n                  </Copiable>\n                </tr>\n                <tr>\n                  <th>{_('hostBiosinfo')}</th>\n                  <td>\n                    {host.bios_strings['bios-vendor']} ({host.bios_strings['bios-version']})\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('systemDisksHealth')}</th>\n                  <td>\n                    {isSmartctlHealthEnabled !== undefined &&\n                      (isSmartctlHealthEnabled ? (\n                        smartctlUnhealthyDevices?.length === 0 ? (\n                          _('disksSystemHealthy')\n                        ) : (\n                          <BulkIcons alerts={unhealthyDevicesAlerts ?? []} />\n                        )\n                      ) : (\n                        _('smartctlPluginNotInstalled')\n                      ))}\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('raidStatus')}</th>\n                  <td>{this.displayMdadmStatus()}</td>\n                </tr>\n              </tbody>\n            </table>\n            <br />\n            <h3>{_('pusbDevices')}</h3>\n            <SortedTable collection={pusbs} columns={PUSBS_COLUMNS} />\n            <br />\n            <h3>{_('pciDevices')}</h3>\n            <SortedTable\n              groupedActions={PCIS_ACTIONS}\n              collection={pcis}\n              columns={PCIS_COLUMNS}\n              data-pciStateById={this.state.pciStateById}\n              data-isPciPassthroughAvailable={this.state.isPciPassthroughAvailable}\n              stateUrlParam='s_pcis'\n            />\n            <h3>{_('licenseHostSettingsLabel')}</h3>\n            <table className='table'>\n              <tbody>\n                <tr>\n                  <th>{_('hostLicenseType')}</th>\n                  <td>{host.license_params.sku_type}</td>\n                </tr>\n                <tr>\n                  <th>{_('hostLicenseSocket')}</th>\n                  <td>{host.license_params.sockets}</td>\n                </tr>\n                <tr>\n                  <th>{_('hostLicenseExpiry')}</th>\n                  <td>\n                    <FormattedTime value={host.license_expiry * 1000} day='numeric' month='long' year='numeric' />\n                    <br />\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n            <h3>{_('supplementalPacks')}</h3>\n            <table className='table'>\n              <tbody>\n                {map(this._getPacks(), formatPack)}\n                {ALLOW_INSTALL_SUPP_PACK && (\n                  <tr>\n                    <th>{_('supplementalPackNew')}</th>\n                    <td>\n                      <SelectFiles type='file' onChange={file => installSupplementalPack(host, file)} />\n                    </td>\n                  </tr>\n                )}\n              </tbody>\n            </table>\n            {!ALLOW_INSTALL_SUPP_PACK && [\n              <h3>{_('supplementalPackNew')}</h3>,\n              <Container>\n                <Upgrade place='supplementalPacks' available={2} />\n              </Container>,\n            ]}\n            {host.certificates !== undefined && (\n              <div>\n                <h3>\n                  {_('installedCertificates')}{' '}\n                  <ActionButton\n                    btnStyle='success'\n                    data-id={host.id}\n                    data-isNewInstallation={host.certificates.length === 0}\n                    handler={installCertificate}\n                    icon='upload'\n                  >\n                    {host.certificates.length > 0 ? _('replaceExistingCertificate') : _('installNewCertificate')}\n                  </ActionButton>\n                </h3>\n                {host.certificates.length > 0 ? (\n                  <ul className='list-group'>\n                    {host.certificates.map(({ fingerprint, notAfter }) => (\n                      <li className='list-group-item' key={fingerprint}>\n                        <Container>\n                          <Row>\n                            <Col mediumSize={2}>\n                              <strong>{_('fingerprint')}</strong>\n                            </Col>\n                            <Col mediumSize={10}>\n                              <Copiable tagName='pre'>{fingerprint}</Copiable>\n                            </Col>\n                          </Row>\n                          <Row>\n                            <Col mediumSize={2}>\n                              <strong>{_('expiry')}</strong>\n                            </Col>\n                            <Col mediumSize={10}>\n                              <FormattedTime value={notAfter * 1e3} day='numeric' month='long' year='numeric' />\n                            </Col>\n                          </Row>\n                        </Container>\n                      </li>\n                    ))}\n                  </ul>\n                ) : (\n                  <span>{_('hostNoCertificateInstalled')}</span>\n                )}\n              </div>\n            )}\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport Button from 'button'\nimport Component from 'base-component'\nimport CopyToClipboard from 'react-copy-to-clipboard'\nimport debounce from 'lodash/debounce'\nimport Icon from 'icon'\nimport invoke from 'invoke'\nimport NoVnc from 'react-novnc'\nimport React from 'react'\nimport { resolveUrl } from 'xo'\nimport { Container, Row, Col } from 'grid'\n\nimport { CpuSparkLines, MemorySparkLines, NetworkSparkLines, LoadSparkLines } from 'xo-sparklines'\n\nexport default class extends Component {\n  state = { scale: 1 }\n\n  _sendCtrlAltDel = () => {\n    this.refs.noVnc.sendCtrlAltDel()\n  }\n\n  _getRemoteClipboard = clipboard => {\n    this.setState({ clipboard })\n    this.refs.clipboard.value = clipboard\n  }\n  _setRemoteClipboard = invoke(() => {\n    const setRemoteClipboard = debounce(value => {\n      this.setState({ clipboard: value })\n      this.refs.noVnc.setClipboard(value)\n    }, 200)\n    return event => setRemoteClipboard(event.target.value)\n  })\n\n  _getClipboardContent = () => this.refs.clipboard && this.refs.clipboard.value\n\n  _onChangeScaleValue = event => {\n    const value = event.target.value\n    this.setState({ scale: value / 100 })\n  }\n\n  render() {\n    const { vmController, statsOverview } = this.props\n    const { scale } = this.state\n\n    return (\n      <Container>\n        {statsOverview && (\n          <Row className='text-xs-center'>\n            <Col mediumSize={3}>\n              <Icon icon='cpu' size={2} /> <CpuSparkLines data={statsOverview} />\n            </Col>\n            <Col mediumSize={3}>\n              <Icon icon='memory' size={2} /> <MemorySparkLines data={statsOverview} />\n            </Col>\n            <Col mediumSize={3}>\n              <Icon icon='network' size={2} /> <NetworkSparkLines data={statsOverview} />\n            </Col>\n            <Col mediumSize={3}>\n              <Icon icon='disk' size={2} /> <LoadSparkLines data={statsOverview} />\n            </Col>\n          </Row>\n        )}\n        <br />\n        <Row>\n          <Col mediumSize={7}>\n            <div className='input-group'>\n              <input type='text' className='form-control' ref='clipboard' onChange={this._setRemoteClipboard} />\n              <span className='input-group-btn'>\n                <CopyToClipboard text={this.state.clipboard || ''}>\n                  <Button>\n                    <Icon icon='clipboard' /> {_('copyToClipboardLabel')}\n                  </Button>\n                </CopyToClipboard>\n              </span>\n            </div>\n          </Col>\n          <Col mediumSize={2}>\n            <Button onClick={this._sendCtrlAltDel}>\n              <Icon icon='vm-keyboard' /> {_('ctrlAltDelButtonLabel')}\n            </Button>\n          </Col>\n          <Col mediumSize={2}>\n            <input\n              className='form-control'\n              max={3}\n              min={0.1}\n              type='range'\n              onChange={this.linkState('scale')}\n              step={0.1}\n              value={scale}\n            />\n          </Col>\n          <Col mediumSize={1}>\n            <input\n              className='form-control'\n              onChange={this._onChangeScaleValue}\n              step='1'\n              type='number'\n              value={Math.round(this.state.scale * 100)}\n              min={1}\n              max={300}\n            />\n          </Col>\n        </Row>\n        <Row className='console'>\n          <Col>\n            <NoVnc\n              scale={scale}\n              ref='noVnc'\n              url={resolveUrl(`consoles/${vmController.id}`)}\n              onClipboardChange={this._getRemoteClipboard}\n            />\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import * as CM from 'complex-matcher'\nimport _ from 'intl'\nimport Copiable from 'copiable'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport store from 'store'\nimport HomeTags from 'home-tags'\nimport { addTag, getHostBiosInfo, removeTag, subscribeIpmiSensors } from 'xo'\nimport { BlockLink } from 'link'\nimport { Container, Row, Col } from 'grid'\nimport { FormattedRelative } from 'react-intl'\nimport { addSubscriptions, formatSize, formatSizeShort, hasLicenseRestrictions } from 'utils'\nimport { injectState, provideState } from 'reaclette'\nimport Usage, { UsageElement } from 'usage'\nimport { getObject } from 'selectors'\nimport { CpuSparkLines, MemorySparkLines, NetworkSparkLines, LoadSparkLines } from 'xo-sparklines'\nimport { Pool } from 'render-xo-item'\nimport { isEmpty } from 'lodash'\nimport LicenseWarning from './license-warning'\n\nconst getSensorName = sensor => sensor.name.split('_')[0]\n\nexport default decorate([\n  addSubscriptions(({ host }) => ({\n    ipmiSensors: subscribeIpmiSensors(host),\n  })),\n  provideState({\n    computed: {\n      areHostsVersionsEqual: ({ areHostsVersionsEqualByPool }, { host }) => areHostsVersionsEqualByPool[host.$pool],\n      inMemoryVms: (_, { vms }) => {\n        const result = []\n        for (const key of Object.keys(vms)) {\n          const vm = vms[key]\n          const { power_state } = vm\n          if (power_state === 'Running' || power_state === 'Paused') {\n            result.push(vm)\n          }\n        }\n        return result\n      },\n      cpuHighestTemp: (_, { ipmiSensors }) => {\n        let cpu\n        let cpuTemp = 0\n        ipmiSensors?.cpuTemp?.forEach(cpuInfo => {\n          const temp = parseFloat(cpuInfo.value.split(' ')[0])\n          if (temp > cpuTemp) {\n            cpuTemp = temp\n            cpuInfo.temp = temp\n            cpu = cpuInfo\n          }\n        })\n        return cpu\n      },\n\n      fanHighestSpeed: (_, { ipmiSensors }) => {\n        let fan\n        let fanSpeed = 0\n        ipmiSensors?.fanSpeed?.forEach(fanInfo => {\n          const speed = parseFloat(fanInfo.value.split(' ')[0])\n          if (speed > fanSpeed) {\n            fanSpeed = speed\n            fanInfo.speed = speed\n            fan = fanInfo\n          }\n        })\n        return fan\n      },\n      fansKo: (_, { ipmiSensors }) => ipmiSensors?.fanStatus?.filter(fanStatus => fanStatus.event !== 'ok'),\n      psusKo: (_, { ipmiSensors }) =>\n        ipmiSensors?.psuStatus?.filter(psuStatus => {\n          const psuName = getSensorName(psuStatus)\n          const _psuPower = ipmiSensors.psuPower.find(sensor => getSensorName(sensor) === psuName)\n          return psuStatus.event !== 'ok' || _psuPower === undefined || Number(_psuPower.value.split(' ')[0]) === 0\n        }),\n      nFansOk: ({ fansKo }, { ipmiSensors }) => (ipmiSensors?.fanStatus?.length ?? 0) - (fansKo?.length ?? 0),\n      nPsusOk: ({ psusKo }, { ipmiSensors }) => (ipmiSensors?.psuStatus?.length ?? 0) - (psusKo?.length ?? 0),\n      biosData: async (_, { host }) => {\n        const biosInfo = await getHostBiosInfo(host)\n        return typeof biosInfo === 'object' && biosInfo !== null ? biosInfo : undefined\n      },\n    },\n  }),\n  injectState,\n  ({\n    statsOverview,\n    host,\n    nVms,\n    vmController,\n    ipmiSensors,\n    state: {\n      areHostsVersionsEqual,\n      biosData,\n      cpuHighestTemp,\n      fanHighestSpeed,\n      fansKo,\n      inMemoryVms,\n      nFansOk,\n      nPsusOk,\n      psusKo,\n    },\n  }) => {\n    const pool = getObject(store.getState(), host.$pool)\n    const vmsFilter = encodeURIComponent(new CM.Property('$container', new CM.String(host.id)).toString())\n\n    return (\n      <Container>\n        <br />\n        <Row className='text-xs-center'>\n          <Col mediumSize={3}>\n            <h2>\n              {host.CPUs.cpu_count}x <Icon icon='cpu' size='lg' />\n            </h2>\n            <BlockLink to={`/hosts/${host.id}/stats`}>\n              {statsOverview && <CpuSparkLines data={statsOverview} />}\n            </BlockLink>\n          </Col>\n          <Col mediumSize={3}>\n            <h2>\n              {formatSize(host.memory.size)} <Icon icon='memory' size='lg' />\n            </h2>\n            <BlockLink to={`/hosts/${host.id}/stats`}>\n              {statsOverview && <MemorySparkLines data={statsOverview} />}\n            </BlockLink>\n          </Col>\n          <Col mediumSize={3}>\n            <BlockLink to={`/hosts/${host.id}/network`}>\n              <h2>\n                {host.$PIFs.length}x <Icon icon='network' size='lg' />\n              </h2>\n            </BlockLink>\n            <BlockLink to={`/hosts/${host.id}/stats`}>\n              {statsOverview && <NetworkSparkLines data={statsOverview} />}\n            </BlockLink>\n          </Col>\n          <Col mediumSize={3}>\n            <BlockLink to={`/hosts/${host.id}/storage`}>\n              <h2>\n                {host.$PBDs.length}x <Icon icon='disk' size='lg' />\n              </h2>\n            </BlockLink>\n            <BlockLink to={`/hosts/${host.id}/stats`}>\n              {statsOverview && <LoadSparkLines data={statsOverview} />}\n            </BlockLink>\n          </Col>\n        </Row>\n        <br />\n        <Row className='text-xs-center'>\n          <Col mediumSize={3}>\n            <p className='text-xs-center'>\n              {_('started', {\n                ago: <FormattedRelative value={host.startTime * 1000} />,\n              })}\n            </p>\n          </Col>\n          <Col mediumSize={3}>\n            <p>\n              {host.productBrand} {host.version} (\n              {host.productBrand !== 'XCP-ng' ? host.license_params.sku_type : 'GPLv2'}){' '}\n              {hasLicenseRestrictions(host) && <LicenseWarning iconSize='lg' />}\n            </p>\n          </Col>\n          <Col mediumSize={3}>\n            <Copiable tagName='p'>{host.address}</Copiable>\n          </Col>\n          <Col mediumSize={3}>\n            <p>\n              {host.bios_strings['system-manufacturer']} {host.bios_strings['system-product-name']}\n            </p>\n          </Col>\n        </Row>\n        <br />\n        <Row>\n          <Col className='text-xs-center'>\n            <BlockLink to={`/home?t=VM&s=${vmsFilter}`}>\n              <h2>\n                {nVms}x <Icon icon='vm' size='lg' />\n              </h2>\n            </BlockLink>\n          </Col>\n        </Row>\n        <br />\n        <Row>\n          <Col className='text-xs-center'>\n            <h5>{_('hostTitleRamUsage')}</h5>\n          </Col>\n        </Row>\n        <Row>\n          <Col smallOffset={1} mediumSize={10}>\n            <Usage total={host.memory.size}>\n              <UsageElement\n                highlight\n                tooltip={`${host.productBrand} (${formatSize(vmController.memory.size)})`}\n                value={vmController.memory.size}\n              />\n              {inMemoryVms.map(vm => (\n                <UsageElement\n                  tooltip={`${vm.name_label} (${formatSize(vm.memory.size)})`}\n                  key={vm.id}\n                  value={vm.memory.size}\n                  href={`#/vms/${vm.id}`}\n                />\n              ))}\n            </Usage>\n          </Col>\n        </Row>\n        <Row>\n          <Col className='text-xs-center'>\n            <h5>\n              {_('memoryHostState', {\n                memoryUsed: formatSizeShort(host.memory.usage),\n                memoryTotal: formatSizeShort(host.memory.size),\n                memoryFree: formatSizeShort(host.memory.size - host.memory.usage),\n              })}\n            </h5>\n          </Col>\n        </Row>\n        <Row>\n          {pool && host.id === pool.master && (\n            <Row className='text-xs-center'>\n              <Col>\n                <h3>\n                  <span className='tag tag-pill tag-info'>{_('pillMaster')}</span>\n                </h3>\n              </Col>\n            </Row>\n          )}\n        </Row>\n        <Row>\n          <Col>\n            <h2 className='text-xs-center'>\n              <HomeTags\n                type='host'\n                labels={host.tags}\n                onDelete={tag => removeTag(host.id, tag)}\n                onAdd={tag => addTag(host.id, tag)}\n              />\n            </h2>\n          </Col>\n        </Row>\n        {!areHostsVersionsEqual && (\n          <Row className='text-xs-center text-danger'>\n            <Col>\n              <p>\n                <Icon icon='alarm' /> {_('notAllHostsHaveTheSameVersion', { pool: <Pool id={host.$pool} link /> })}\n              </p>\n            </Col>\n          </Row>\n        )}\n        {biosData !== undefined && (\n          <Row className='text-xs-center'>\n            <Col>\n              <Icon icon='bios-version' size={3} />\n              <p>\n                {_('currentBiosVersion', { version: biosData.currentBiosVersion })}{' '}\n                <Icon\n                  icon={biosData.isUpToDate ? 'success' : 'false'}\n                  className={biosData.isUpToDate ? 'text-success' : 'text-danger'}\n                />\n                <br />\n                {!biosData.isUpToDate && (\n                  <a href={biosData.biosLink} target='_blank' rel='noreferrer'>\n                    {_('downloadBiosUpdate', { version: biosData.latestBiosVersion })}\n                  </a>\n                )}\n              </p>\n            </Col>\n          </Row>\n        )}\n        {ipmiSensors !== undefined && !isEmpty(ipmiSensors) && (\n          <Row className='mt-3'>\n            <Col>\n              <Row>\n                <IpmiSensorCard icon='ipmi'>\n                  {_('keyValue', {\n                    key: _('ipmi'),\n                    value: (\n                      <b>\n                        {ipmiSensors.bmcStatus?.event === 'ok' ? (\n                          ipmiSensors.ip !== undefined ? (\n                            <a href={`http://${ipmiSensors.ip.value}`} target='_blank' rel='noopener noreferrer'>\n                              {ipmiSensors.ip.value}\n                            </a>\n                          ) : (\n                            <p>{_('unknown')}</p>\n                          )\n                        ) : (\n                          <span className='text-danger'>{ipmiSensors.bmcStatus?.event ?? _('unknown')}</span>\n                        )}\n                      </b>\n                    ),\n                  })}\n                </IpmiSensorCard>\n                <IpmiSensorCard icon='total-power'>\n                  {_('keyValue', {\n                    key: _('totalPower'),\n                    value: <b>{ipmiSensors.totalPower?.value ?? _('unknown')}</b>,\n                  })}\n                </IpmiSensorCard>\n                <IpmiSensorCard icon='psu'>\n                  {psusKo !== undefined && psusKo.length !== 0 && (\n                    <span>\n                      {_('nPsuStatus', {\n                        n: (ipmiSensors.psuStatus?.length ?? 0) - nPsusOk,\n                        status: (\n                          <b>\n                            <Icon icon='false' className='text-danger' />\n                          </b>\n                        ),\n                      })}{' '}\n                      ({psusKo.map(getSensorName).join(', ')})\n                      <br />\n                    </span>\n                  )}\n                  {nPsusOk !== 0 &&\n                    _('nPsuStatus', {\n                      n: nPsusOk,\n                      status: (\n                        <b>\n                          <Icon icon='success' className='text-success' />\n                        </b>\n                      ),\n                    })}\n                </IpmiSensorCard>\n                <IpmiSensorCard icon='cpu-temperature'>\n                  {_('highestCpuTemperature', {\n                    n: ipmiSensors.cpuTemp?.length ?? 0,\n                    degres: <b>{cpuHighestTemp?.value ?? _('unknown')}</b>,\n                  })}\n                </IpmiSensorCard>\n              </Row>\n              <Row className='mt-1'>\n                <IpmiSensorCard icon='fan-status'>\n                  {fansKo !== undefined && fansKo.length !== 0 && (\n                    <span>\n                      {_('nFanStatus', {\n                        n: (ipmiSensors.fanStatus?.length ?? 0) - nFansOk,\n                        status: (\n                          <b>\n                            <Icon icon='false' className='text-danger' />\n                          </b>\n                        ),\n                      })}{' '}\n                      ({fansKo.map(getSensorName).join(', ')})\n                      <br />\n                    </span>\n                  )}\n                  {nFansOk !== 0 &&\n                    _('nFanStatus', {\n                      n: nFansOk,\n                      status: (\n                        <b>\n                          <Icon icon='success' className='text-success' />\n                        </b>\n                      ),\n                    })}\n                </IpmiSensorCard>\n                <IpmiSensorCard icon='fan-speed'>\n                  {_('highestFanSpeed', {\n                    n: ipmiSensors.fanSpeed?.length ?? 0,\n                    speed: <b>{fanHighestSpeed?.value ?? _('unknown')}</b>,\n                  })}\n                </IpmiSensorCard>\n                <IpmiSensorCard icon='inlet'>\n                  {_('keyValue', {\n                    key: _('inletTemperature'),\n                    value: <b>{ipmiSensors.inletTemp?.value ?? _('unknown')}</b>,\n                  })}\n                </IpmiSensorCard>\n                <IpmiSensorCard icon='outlet'>\n                  {_('keyValue', {\n                    key: _('outletTemperature'),\n                    value: <b>{ipmiSensors.outletTemp?.value ?? _('unknown')}</b>,\n                  })}\n                </IpmiSensorCard>\n              </Row>\n            </Col>\n          </Row>\n        )}\n      </Container>\n    )\n  },\n])\n\nconst IpmiSensorCard = ({ children, icon, label, value, ...props }) => (\n  <Col mediumSize={3} {...props}>\n    <div>\n      <h2 className='text-xs-center'>\n        <Icon icon={icon} size='lg' />\n      </h2>\n      <div className='text-xs-center'>{children}</div>\n    </div>\n  </Col>\n)\n","import _ from 'intl'\nimport Component from 'base-component'\nimport isEmpty from 'lodash/isEmpty'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport { createPager } from 'selectors'\nimport { Row, Col } from 'grid'\nimport { deleteMessage, deleteMessages } from 'xo'\nimport { formatLogs } from 'utils'\nimport { FormattedRelative, FormattedTime } from 'react-intl'\nimport map from 'lodash/map.js'\n\nconst LOG_BODY_STYLE = {\n  whiteSpace: 'pre-wrap',\n}\n\nconst LOG_COLUMNS = [\n  {\n    name: _('logDate'),\n    itemRenderer: log => (\n      <span>\n        <FormattedTime\n          value={log.time * 1000}\n          minute='numeric'\n          hour='numeric'\n          day='numeric'\n          month='long'\n          year='numeric'\n        />{' '}\n        (<FormattedRelative value={log.time * 1000} />)\n      </span>\n    ),\n    sortCriteria: log => log.time,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('logName'),\n    itemRenderer: log => log.name,\n    sortCriteria: log => log.name,\n  },\n  {\n    name: _('logContent'),\n    itemRenderer: ({ formatted, body }) =>\n      formatted !== undefined ? (\n        <div>\n          <Row>\n            <Col mediumSize={6}>\n              <strong>{formatted.name}</strong>\n            </Col>\n            <Col mediumSize={6}>{formatted.value}</Col>\n          </Row>\n          <br />\n          {map(formatted.alarmAttributes, (value, label) => (\n            <Row key={label}>\n              <Col mediumSize={6}>{label}</Col>\n              <Col mediumSize={6}>{value}</Col>\n            </Row>\n          ))}\n        </div>\n      ) : (\n        <pre style={LOG_BODY_STYLE}>{body}</pre>\n      ),\n    sortCriteria: log => log.body,\n  },\n]\n\nconst LOG_ACTIONS = [\n  {\n    handler: deleteMessages,\n    individualHandler: deleteMessage,\n    individualLabel: _('logDelete'),\n    icon: 'delete',\n    label: _('logsDelete'),\n    level: 'danger',\n  },\n]\n\nexport default class TabLogs extends Component {\n  constructor() {\n    super()\n\n    this.getLogs = createPager(\n      () => this.state.logs,\n      () => this.state.page,\n      10\n    )\n\n    this.state = {\n      page: 1,\n    }\n  }\n\n  componentDidMount() {\n    this._formatLogs(this.props.logs)\n  }\n\n  componentDidUpdate(props) {\n    if (props.logs !== this.props.logs) {\n      this._formatLogs(this.props.logs)\n    }\n  }\n\n  _formatLogs = logs =>\n    formatLogs(logs).then(formattedLogs => {\n      this.setState({\n        logs: map(formattedLogs, ({ id, ...formattedLogs }) => ({\n          formatted: formattedLogs,\n          ...logs[id],\n        })),\n      })\n    })\n\n  _nextPage = () => this.setState({ page: this.state.page + 1 })\n  _previousPage = () => this.setState({ page: this.state.page - 1 })\n\n  render() {\n    const logs = this.getLogs()\n\n    if (isEmpty(logs)) {\n      return (\n        <Row>\n          <Col mediumSize={6} className='text-xs-center'>\n            <br />\n            <h4>{_('noLogs')}</h4>\n          </Col>\n        </Row>\n      )\n    }\n\n    return <SortedTable actions={LOG_ACTIONS} collection={logs} columns={LOG_COLUMNS} stateUrlParam='s_logs' />\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport copy from 'copy-to-clipboard'\nimport humanFormat from 'human-format'\nimport React from 'react'\nimport Icon from 'icon'\nimport SingleLineRow from 'single-line-row'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport Tooltip from 'tooltip'\nimport { confirm } from 'modal'\nimport { connectStore, noop } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { createGetObjectsOfType, createSelector } from 'selectors'\nimport { error } from 'notification'\nimport { get } from '@xen-orchestra/defined'\nimport { Select, Number } from 'editable'\nimport { Toggle } from 'form'\nimport { isEmpty, pick, some } from 'lodash'\nimport {\n  connectPif,\n  deletePif,\n  deletePifs,\n  disconnectPif,\n  editNetwork,\n  editPif,\n  getIpv4ConfigModes,\n  getIpv6ConfigModes,\n  reconfigurePifIp,\n  scanHostPifs,\n} from 'xo'\n\nconst EDIT_BUTTON_STYLE = { color: '#999', cursor: 'pointer' }\n\nconst _toggleDefaultLockingMode = (component, tooltip) =>\n  tooltip ? <Tooltip content={tooltip}>{component}</Tooltip> : component\n\nclass ConfigureIpModal extends Component {\n  constructor(props) {\n    super(props)\n\n    const { pif } = props\n    if (pif) {\n      const ipv6 = pif.ipv6?.[0]?.trim()\n      this.state = {\n        ...pick(pif, ['ip', 'netmask', 'dns', 'gateway']),\n        ipv6: ipv6 === '' ? undefined : ipv6,\n      }\n    }\n  }\n\n  get value() {\n    return this.state\n  }\n\n  render() {\n    const { ip, ipv6, netmask, dns, gateway } = this.state\n\n    return (\n      <div>\n        <SingleLineRow>\n          <Col size={6}>{_('staticIp')}</Col>\n          <Col size={6}>\n            <input className='form-control' onChange={this.linkState('ip')} value={ip} />\n          </Col>\n        </SingleLineRow>\n        &nbsp;\n        <SingleLineRow>\n          <Col size={6}>{_('staticIpv6')}</Col>\n          <Col size={6}>\n            <input className='form-control' onChange={this.linkState('ipv6')} value={ipv6} />\n          </Col>\n        </SingleLineRow>\n        &nbsp;\n        <SingleLineRow>\n          <Col size={6}>{_('netmask')}</Col>\n          <Col size={6}>\n            <input className='form-control' onChange={this.linkState('netmask')} value={netmask} />\n          </Col>\n        </SingleLineRow>\n        &nbsp;\n        <SingleLineRow>\n          <Col size={6}>{_('dns')}</Col>\n          <Col size={6}>\n            <input className='form-control' onChange={this.linkState('dns')} value={dns} />\n          </Col>\n        </SingleLineRow>\n        &nbsp;\n        <SingleLineRow>\n          <Col size={6}>{_('gateway')}</Col>\n          <Col size={6}>\n            <input className='form-control' onChange={this.linkState('gateway')} value={gateway} />\n          </Col>\n        </SingleLineRow>\n      </div>\n    )\n  }\n}\n\nclass PifItemVlan extends Component {\n  _editPif = vlan => editPif(this.props.item, { vlan })\n  render() {\n    const pif = this.props.item\n    return (\n      <div>\n        {pif.vlan === -1 ? (\n          'None'\n        ) : (\n          <Number value={pif.vlan} onChange={this._editPif}>\n            {pif.vlan}\n          </Number>\n        )}\n      </div>\n    )\n  }\n}\n\nconst reconfigureIp = (pif, mode) => {\n  if (mode === 'Static') {\n    return confirm({\n      icon: 'ip',\n      title: _('pifConfigureIp'),\n      body: <ConfigureIpModal pif={pif} />,\n    }).then(params => {\n      if (params.ip === undefined && params.ipv6 === undefined) {\n        return error(_('configIpErrorTitle'), _('ipRequired'))\n      }\n      if (params.ip !== undefined && params.netmask === undefined) {\n        return error(_('configIpErrorTitle'), _('netmaskRequired'))\n      }\n      if (params.ipv6 !== undefined) {\n        params.ipv6Mode = mode\n      }\n      return reconfigurePifIp(pif, { mode, ...params })\n    }, noop)\n  }\n  return reconfigurePifIp(pif, { [pif.primaryAddressType === 'IPv6' ? 'ipv6Mode' : 'mode']: mode })\n}\n\nclass PifItemIp extends Component {\n  _onEditIp = () => reconfigureIp(this.props.pif, 'Static')\n\n  render() {\n    const { pif } = this.props\n    const pifIp = pif.primaryAddressType === 'IPv6' ? pif.ipv6?.[0] : pif.ip\n    return (\n      <div>\n        {pifIp}{' '}\n        {pifIp && (\n          <a className='hidden-md-down' onClick={this._onEditIp} style={EDIT_BUTTON_STYLE}>\n            <Icon icon='edit' size='1' fixedWidth />\n          </a>\n        )}\n      </div>\n    )\n  }\n}\n\nclass PifItemMode extends Component {\n  state = { configModes: [] }\n\n  async componentDidMount() {\n    const [ipv4ConfigModes, ipv6ConfigModes] = await Promise.all([getIpv4ConfigModes(), getIpv6ConfigModes()])\n    this.setState({ ipv4ConfigModes, ipv6ConfigModes })\n  }\n\n  _configIp = mode => mode != null && reconfigureIp(this.props.pif, mode.value)\n\n  _isIpv6 = createSelector(\n    () => this.props.pif.primaryAddressType,\n    primaryAddressType => primaryAddressType === 'IPv6'\n  )\n\n  _getOptions = createSelector(\n    this._isIpv6,\n    () => this.state.ipv4ConfigModes,\n    () => this.state.ipv6ConfigModes,\n    (isIpv6, ipv4ConfigModes, ipv6ConfigModes) =>\n      (isIpv6 ? ipv6ConfigModes : ipv4ConfigModes)?.map(mode => ({ label: mode, value: mode }))\n  )\n\n  _getValue = createSelector(\n    this._isIpv6,\n    () => this.props.pif.mode,\n    () => this.props.pif.ipv6Mode,\n    (isIpv6, mode, ipv6Mode) => {\n      mode = isIpv6 ? ipv6Mode : mode\n      return { label: mode, value: mode }\n    }\n  )\n\n  render() {\n    const isIpv6 = this._isIpv6()\n    return (\n      <Select onChange={this._configIp} options={this._getOptions()} value={this._getValue()}>\n        {this.props.pif[isIpv6 ? 'ipv6Mode' : 'mode']}\n      </Select>\n    )\n  }\n}\n\n@connectStore(() => ({\n  vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network'),\n}))\nclass PifItemLock extends Component {\n  _editNetwork = () => {\n    const { pif, networks } = this.props\n    return editNetwork(pif.$network, {\n      defaultIsLocked: !networks[pif.$network].defaultIsLocked,\n    })\n  }\n\n  render() {\n    const { networks, pif, vifsByNetwork } = this.props\n\n    const network = networks[pif.$network]\n    if (network === undefined) {\n      return null\n    }\n\n    const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)\n    return _toggleDefaultLockingMode(\n      <Toggle disabled={pifInUse} onChange={this._editNetwork} value={network.defaultIsLocked} />,\n      pifInUse && _('pifInUse')\n    )\n  }\n}\n\nconst PIF_COLUMNS = [\n  {\n    default: true,\n    itemRenderer: pif => pif.device,\n    name: _('pifDeviceLabel'),\n    sortCriteria: 'device',\n  },\n  {\n    itemRenderer: (pif, userData) => (\n      <span>\n        {get(() => userData.networks[pif.$network].name_label)}\n        {pif.management && <span className='ml-1 tag tag-pill tag-info'>{_('networkManagement')}</span>}\n      </span>\n    ),\n    name: _('pifNetworkLabel'),\n    sortCriteria: (pif, userData) => get(() => userData.networks[pif.$network].name_label),\n  },\n  {\n    component: PifItemVlan,\n    name: _('pifVlanLabel'),\n    sortCriteria: 'vlan',\n  },\n  {\n    itemRenderer: (pif, userData) => <PifItemIp pif={pif} networks={userData.networks} />,\n    name: _('pifAddressLabel'),\n    sortCriteria: 'ip',\n  },\n  {\n    itemRenderer: (pif, userData) => <PifItemMode pif={pif} networks={userData.networks} />,\n    name: _('pifModeLabel'),\n    sortCriteria: 'mode',\n  },\n  {\n    itemRenderer: pif => pif.mac,\n    name: _('pifMacLabel'),\n    sortCriteria: 'mac',\n  },\n  {\n    itemRenderer: pif => pif.mtu,\n    name: _('pifMtuLabel'),\n    sortCriteria: 'mtu',\n  },\n  {\n    itemRenderer: ({ speed }) => speed !== undefined && humanFormat(speed * 1e6, { unit: 'b/s' }), // 1e6: convert Mb to b\n    name: _('pifSpeedLabel'),\n    sortCriteria: 'speed',\n  },\n  {\n    itemRenderer: (pif, userData) => <PifItemLock pif={pif} networks={userData.networks} />,\n    name: _('defaultLockingMode'),\n  },\n\n  {\n    itemRenderer: (pif, { nbd, networks, insecure_nbd }) => {\n      if (networks[pif.$network]?.nbd) {\n        return (\n          <Tooltip content={_('nbdSecureTooltip')}>\n            <Icon icon='lock' />\n          </Tooltip>\n        )\n      }\n      if (networks[pif.$network]?.insecure_nbd) {\n        ;<Tooltip content={_('nbdInsecureTooltip')}>\n          <Icon icon='unlock' />\n          <Icon icon='error' />\n        </Tooltip>\n      }\n      return null\n    },\n    name: <Tooltip content={_('nbdTootltip')}>{_('nbd')}</Tooltip>,\n  },\n  {\n    itemRenderer: pif => (\n      <div>\n        <StateButton\n          disabledLabel={_('pifDisconnected')}\n          disabledHandler={connectPif}\n          disabledTooltip={_('connectPif')}\n          enabledLabel={_('pifConnected')}\n          enabledHandler={disconnectPif}\n          enabledTooltip={_('disconnectPif')}\n          disabled={pif.attached && (pif.management || pif.disallowUnplug)}\n          handlerParam={pif}\n          state={pif.attached}\n        />{' '}\n        <Tooltip content={pif.carrier ? _('pifPhysicallyConnected') : _('pifPhysicallyDisconnected')}>\n          <Icon icon='network' size='lg' className={pif.carrier ? 'text-success' : 'text-muted'} />\n        </Tooltip>\n      </div>\n    ),\n    name: _('pifStatusLabel'),\n  },\n]\n\nconst PIF_INDIVIDUAL_ACTIONS = [\n  {\n    handler: pif => copy(pif.uuid),\n    icon: 'clipboard',\n    label: pif => _('copyUuid', { uuid: pif.uuid }),\n  },\n  {\n    handler: deletePif,\n    icon: 'delete',\n    label: _('deletePif'),\n    level: 'danger',\n  },\n]\n\nconst PIF_GROUPED_ACTIONS = [\n  {\n    handler: deletePifs,\n    icon: 'delete',\n    label: _('deletePifs'),\n    level: 'danger',\n  },\n]\n\nconst PVT_NETWORK_COLUMNS = [\n  {\n    name: _('poolNetworkNameLabel'),\n    itemRenderer: network => network.name_label,\n  },\n  {\n    name: _('poolNetworkDescription'),\n    itemRenderer: network => network.name_description,\n  },\n  {\n    name: _('poolNetworkMTU'),\n    itemRenderer: network => network.MTU,\n  },\n  {\n    name: (\n      <div className='text-xs-center'>\n        <Tooltip content={_('defaultLockingMode')}>\n          <Icon size='lg' icon='lock' />\n        </Tooltip>\n      </div>\n    ),\n    itemRenderer: network => <Icon icon={network.defaultIsLocked ? 'lock' : 'unlock'} />,\n  },\n]\n\nconst PVT_NETWORK_ACTIONS = [\n  {\n    handler: network => copy(network.uuid),\n    icon: 'clipboard',\n    label: network => _('copyUuid', { uuid: network.uuid }),\n  },\n]\n\nexport default ({ host, networks, pifs, privateNetworks }) => {\n  return (\n    <Container>\n      <Row>\n        <Col>\n          <h1>\n            {_('poolNetworkPif')}\n            <ActionButton className='ml-1' handler={scanHostPifs} handlerParam={host.id} icon='refresh'>\n              {_('refresh')}\n            </ActionButton>\n          </h1>\n          <SortedTable\n            collection={pifs}\n            columns={PIF_COLUMNS}\n            data-networks={networks}\n            groupedActions={PIF_GROUPED_ACTIONS}\n            individualActions={PIF_INDIVIDUAL_ACTIONS}\n            stateUrlParam='s'\n          />\n        </Col>\n      </Row>\n      {!isEmpty(privateNetworks) && (\n        <Row>\n          <Col>\n            <h1>\n              {_('privateNetworks')}\n              <ActionButton\n                className='ml-1'\n                handler={noop}\n                icon='edit'\n                redirectOnSuccess={`/pools/${host.$pool}/network?s=${encodeURIComponent('!PIFs:length?')}`}\n              >\n                {_('manage')}\n              </ActionButton>\n            </h1>\n            <SortedTable\n              collection={privateNetworks}\n              columns={PVT_NETWORK_COLUMNS}\n              individualActions={PVT_NETWORK_ACTIONS}\n              stateUrlParam='s_private'\n            />\n          </Col>\n        </Row>\n      )}\n    </Container>\n  )\n}\n","import _ from 'intl'\nimport PropTypes from 'prop-types'\nimport React, { Component } from 'react'\nimport SortedTable from 'sorted-table'\nimport TabButton from 'tab-button'\nimport Upgrade from 'xoa-upgrade'\nimport { alert, chooseAction } from 'modal'\nimport { connectStore, formatSize } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { createDoesHostNeedRestart } from 'selectors'\nimport { FormattedRelative, FormattedTime } from 'react-intl'\nimport { installAllPatchesOnHost, restartHost } from 'xo'\nimport isEmpty from 'lodash/isEmpty.js'\nimport { isXsHostWithCdnPatches } from 'xo/utils'\n\nimport { createGetObject } from '../../common/selectors'\n\nconst MISSING_PATCH_COLUMNS = [\n  {\n    name: _('patchNameLabel'),\n    itemRenderer: patch => patch.name,\n    sortCriteria: patch => patch.name,\n  },\n  {\n    name: _('patchDescription'),\n    itemRenderer: patch => (\n      <a href={patch.documentationUrl} rel='noopener noreferrer' target='_blank' style={{ whiteSpace: 'pre-line' }}>\n        {patch.description}\n      </a>\n    ),\n    sortCriteria: patch => patch.description,\n  },\n  {\n    name: _('patchReleaseDate'),\n    itemRenderer: patch => (\n      <span>\n        <FormattedTime value={patch.date} day='numeric' month='long' year='numeric' /> (\n        <FormattedRelative value={patch.date} />)\n      </span>\n    ),\n    sortCriteria: patch => patch.date,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('patchGuidance'),\n    itemRenderer: patch => patch.guidance,\n    sortCriteria: patch => patch.guidance,\n  },\n]\n\nconst MISSING_PATCH_COLUMNS_XCP = [\n  {\n    name: _('patchNameLabel'),\n    itemRenderer: patch => patch.name,\n    sortCriteria: 'name',\n  },\n  {\n    name: _('patchDescription'),\n    itemRenderer: patch => patch.description,\n    sortCriteria: 'description',\n  },\n  {\n    name: _('patchVersion'),\n    itemRenderer: patch => patch.version,\n  },\n  {\n    name: _('patchRelease'),\n    itemRenderer: patch => patch.release,\n  },\n  {\n    name: _('patchSize'),\n    itemRenderer: patch => formatSize(patch.size),\n    sortCriteria: 'size',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS_XCP = [\n  {\n    disabled: patch => patch.changelog === null,\n    handler: patch =>\n      alert(\n        _('changelog'),\n        <Container>\n          <Row className='mb-1'>\n            <Col size={3}>\n              <strong>{_('changelogPatch')}</strong>\n            </Col>\n            <Col size={9}>{patch.name}</Col>\n          </Row>\n          <Row className='mb-1'>\n            <Col size={3}>\n              <strong>{_('changelogDate')}</strong>\n            </Col>\n            <Col size={9}>\n              <FormattedTime value={patch.changelog.date * 1000} day='numeric' month='long' year='numeric' />\n            </Col>\n          </Row>\n          <Row className='mb-1'>\n            <Col size={3}>\n              <strong>{_('changelogAuthor')}</strong>\n            </Col>\n            <Col size={9}>{patch.changelog.author}</Col>\n          </Row>\n          <Row>\n            <Col size={3}>\n              <strong>{_('changelogDescription')}</strong>\n            </Col>\n            <Col size={9}>{patch.changelog.description}</Col>\n          </Row>\n        </Container>\n      ),\n    icon: 'preview',\n    label: _('showChangelog'),\n  },\n]\n\nconst INSTALLED_PATCH_COLUMNS = [\n  {\n    name: _('patchNameLabel'),\n    itemRenderer: patch => patch.name,\n    sortCriteria: patch => patch.name,\n  },\n  {\n    name: _('patchDescription'),\n    itemRenderer: patch => patch.description,\n    sortCriteria: patch => patch.description,\n  },\n  {\n    default: true,\n    name: _('patchApplied'),\n    itemRenderer: patch => {\n      const time = patch.time * 1000\n      return (\n        <span>\n          <FormattedTime value={time} day='numeric' month='long' year='numeric' /> (<FormattedRelative value={time} />)\n        </span>\n      )\n    },\n    sortCriteria: patch => patch.time,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('patchSize'),\n    itemRenderer: patch => formatSize(patch.size),\n    sortCriteria: patch => patch.size,\n  },\n]\n\nclass XcpPatches extends Component {\n  render() {\n    const { missingPatches, host, installAllPatches, pool } = this.props\n    const hasMissingPatches = !isEmpty(missingPatches)\n    return (\n      <Container>\n        <Row>\n          <Col className='text-xs-right'>\n            {this.props.needsRestart && (\n              <TabButton\n                btnStyle='warning'\n                handler={restartHost}\n                handlerParam={host}\n                icon='host-reboot'\n                labelId='rebootUpdateHostLabel'\n              />\n            )}\n            <TabButton\n              disabled={!hasMissingPatches || pool?.HA_enabled}\n              btnStyle={hasMissingPatches ? 'primary' : undefined}\n              handler={installAllPatches}\n              icon={hasMissingPatches ? 'host-patch-update' : 'success'}\n              labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}\n              tooltip={pool?.HA_enabled ? _('highAvailabilityNotDisabledTooltip') : undefined}\n            />\n          </Col>\n        </Row>\n        {hasMissingPatches && (\n          <Row>\n            <Col>\n              <SortedTable\n                columns={MISSING_PATCH_COLUMNS_XCP}\n                collection={missingPatches}\n                individualActions={INDIVIDUAL_ACTIONS_XCP}\n                stateUrlParam='s_missing'\n              />\n            </Col>\n          </Row>\n        )}\n      </Container>\n    )\n  }\n}\n\n@connectStore(() => ({\n  needsRestart: createDoesHostNeedRestart((_, props) => props.host),\n}))\nclass XenServerPatches extends Component {\n  render() {\n    const { host, hostPatches, installAllPatches, missingPatches, pool } = this.props\n    const hasMissingPatches = !isEmpty(missingPatches)\n    const _isXsHostWithCdnPatches = isXsHostWithCdnPatches(host)\n    return (\n      <Container>\n        <Row>\n          <Col className='text-xs-right'>\n            {this.props.needsRestart && (\n              <TabButton\n                btnStyle='warning'\n                handler={restartHost}\n                handlerParam={host}\n                icon='host-reboot'\n                labelId='rebootUpdateHostLabel'\n              />\n            )}\n            <TabButton\n              disabled={!hasMissingPatches || pool.HA_enabled}\n              btnStyle={hasMissingPatches ? 'primary' : undefined}\n              handler={installAllPatches}\n              icon={hasMissingPatches ? 'host-patch-update' : 'success'}\n              labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}\n              tooltip={pool.HA_enabled ? _('highAvailabilityNotDisabledTooltip') : undefined}\n            />\n          </Col>\n        </Row>\n        {hasMissingPatches && (\n          <Row>\n            <Col>\n              <h3>{_('hostMissingPatches')}</h3>\n              <SortedTable collection={missingPatches} columns={MISSING_PATCH_COLUMNS} stateUrlParam='s_missing' />\n            </Col>\n          </Row>\n        )}\n        {!_isXsHostWithCdnPatches && (\n          <Row>\n            <Col>\n              <h3>{_('hostAppliedPatches')}</h3>\n              <SortedTable collection={hostPatches} columns={INSTALLED_PATCH_COLUMNS} stateUrlParam='s_installed' />\n            </Col>\n          </Row>\n        )}\n      </Container>\n    )\n  }\n}\n\n@connectStore(() => ({\n  pool: createGetObject((_, props) => props.host.$pool),\n}))\nexport default class TabPatches extends Component {\n  static contextTypes = {\n    router: PropTypes.object,\n  }\n\n  _installAllPatches = () => {\n    const { host } = this.props\n    const { $pool: pool, productBrand } = host\n\n    if (productBrand === 'XCP-ng') {\n      return installAllPatchesOnHost({ host })\n    }\n\n    return chooseAction({\n      body: <p>{_('installAllPatchesContent')}</p>,\n      buttons: [{ label: _('installAllPatchesRedirect'), value: 'goToPool' }],\n      icon: 'host-patch-update',\n      title: _('installAllPatchesTitle'),\n    }).then(() => this.context.router.push(`/pools/${pool}/patches`))\n  }\n\n  render() {\n    if (process.env.XOA_PLAN < 2) {\n      return (\n        <Container>\n          <Upgrade place='hostPatches' available={2} />\n        </Container>\n      )\n    }\n    if (this.props.missingPatches === null) {\n      return <em>{_('updatePluginNotInstalled')}</em>\n    }\n    const Patches = this.props.host.productBrand === 'XCP-ng' ? XcpPatches : XenServerPatches\n    return <Patches {...this.props} installAllPatches={this._installAllPatches} />\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { Container, Row, Col } from 'grid'\nimport { DEFAULT_GRANULARITY, fetchStats, SelectGranularity, INTERVAL_BY_GRANULARITY } from 'stats'\nimport { Toggle } from 'form'\nimport { CpuLineChart, MemoryLineChart, PifLineChart, LoadLineChart } from 'xo-line-chart'\n\nexport default class HostStats extends Component {\n  state = {\n    granularity: DEFAULT_GRANULARITY,\n    useCombinedValues: false,\n    statsIsPending: false,\n  }\n\n  fetchHostStats = () => {\n    if (this.state.statsIsPending) {\n      return\n    }\n\n    const host = this.props.host\n\n    if (host.power_state !== 'Running') {\n      return\n    }\n\n    if (this.props.statsOverview?.interval === INTERVAL_BY_GRANULARITY[this.state.granularity.granularity]) {\n      this.setState({\n        stats: this.props.statsOverview,\n        selectStatsLoading: false,\n      })\n      return\n    }\n\n    this.setState({\n      statsIsPending: true,\n    })\n    fetchStats(host, 'host', this.state.granularity).then(stats => {\n      this.setState({\n        stats,\n        selectStatsLoading: false,\n        statsIsPending: false,\n      })\n    })\n  }\n\n  initFetchHostStats() {\n    this.fetchHostStats()\n    this.interval = setInterval(this.fetchHostStats, INTERVAL_BY_GRANULARITY[this.state.granularity.granularity] * 1000)\n  }\n  initFetchHostStats = ::this.initFetchHostStats\n\n  componentWillMount() {\n    this.initFetchHostStats()\n  }\n\n  componentWillUnmount() {\n    clearInterval(this.interval)\n  }\n\n  componentWillReceiveProps(props) {\n    const hostCur = this.props.host\n    const hostNext = props.host\n\n    if (hostCur.power_state !== 'Running' && hostNext.power_state === 'Running') {\n      this.initFetchHostStats(hostNext)\n    } else if (hostCur.power_state === 'Running' && hostNext.power_state !== 'Running') {\n      this.setState({\n        stats: undefined,\n      })\n    }\n  }\n\n  handleSelectStats(granularity) {\n    clearInterval(this.interval)\n\n    this.setState(\n      {\n        granularity,\n        selectStatsLoading: true,\n      },\n      this.initFetchHostStats\n    )\n  }\n  handleSelectStats = ::this.handleSelectStats\n\n  render() {\n    const { granularity, selectStatsLoading, stats, useCombinedValues } = this.state\n\n    return !stats ? (\n      <p>No stats.</p>\n    ) : (\n      <Container>\n        <Row>\n          <Col mediumSize={5}>\n            <div className='form-group'>\n              <Tooltip content={_('useStackedValuesOnStats')}>\n                <Toggle value={useCombinedValues} onChange={this.linkState('useCombinedValues')} />\n              </Tooltip>\n            </div>\n          </Col>\n          <Col mediumSize={1}>\n            {selectStatsLoading && (\n              <div className='text-xs-right'>\n                <Icon icon='loading' size={2} />\n              </div>\n            )}\n          </Col>\n          <Col mediumSize={6}>\n            <SelectGranularity onChange={this.handleSelectStats} required value={granularity} />\n          </Col>\n        </Row>\n        <Row>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='cpu' size={1} /> {_('statsCpu')}\n            </h5>\n            <CpuLineChart addSumSeries={useCombinedValues} data={stats} />\n          </Col>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='memory' size={1} /> {_('statsMemory')}\n            </h5>\n            <MemoryLineChart data={stats} />\n          </Col>\n        </Row>\n        <br />\n        <hr />\n        <Row>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='network' size={1} /> {_('statsNetwork')}\n            </h5>\n            <PifLineChart addSumSeries={useCombinedValues} data={stats} />\n          </Col>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='disk' size={1} /> {_('statLoad')}\n            </h5>\n            <LoadLineChart data={stats} />\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport Icon from 'icon'\nimport Link from 'link'\nimport map from 'lodash/map'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport Tooltip from 'tooltip'\nimport { confirm } from 'modal'\nimport { connectPbd, disconnectPbd, deletePbd, deletePbds, editSr, isSrShared } from 'xo'\nimport { connectStore, formatSize, noop } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { createGetObjectsOfType, createSelector } from 'selectors'\nimport { filter, isEmpty, some } from 'lodash'\nimport { TabButtonLink } from 'tab-button'\nimport { Text } from 'editable'\n\nconst forgetSr = ({ pbdId }) =>\n  confirm({\n    title: _('forgetSrFromHostModalTitle'),\n    body: _('forgetSrFromHostModalMessage'),\n  }).then(() => deletePbd(pbdId), noop)\n\nconst forgetSrs = pbds =>\n  confirm({\n    title: _('forgetSrsFromHostModalTitle', { nPbds: pbds.length }),\n    body: _('forgetSrsFromHostModalMessage', { nPbds: pbds.length }),\n  }).then(() => deletePbds(pbds), noop)\n\nconst SR_COLUMNS = [\n  {\n    name: _('srName'),\n    itemRenderer: ({ id, nameLabel, coalesceTask }) => (\n      <Link to={`/srs/${id}`}>\n        <Text onChange={nameLabel => editSr(id, { nameLabel })} useLongClick value={nameLabel} />\n        {coalesceTask !== undefined && (\n          <Tooltip content={`${coalesceTask.name_label} ${Math.round(coalesceTask.progress * 100)}%`}>\n            <Icon icon='coalesce' />\n          </Tooltip>\n        )}\n      </Link>\n    ),\n    sortCriteria: 'nameLabel',\n  },\n  {\n    name: _('srFormat'),\n    itemRenderer: storage => storage.format,\n    sortCriteria: 'format',\n  },\n  {\n    name: _('srSize'),\n    itemRenderer: storage => formatSize(storage.size),\n    sortCriteria: 'size',\n  },\n  {\n    default: true,\n    name: _('srUsage'),\n    itemRenderer: storage =>\n      storage.size !== 0 && (\n        <Tooltip\n          content={_('spaceLeftTooltip', {\n            used: storage.usagePercentage,\n            free: formatSize(storage.free),\n          })}\n        >\n          <meter value={storage.usagePercentage} min='0' max='100' optimum='40' low='80' high='90' />\n        </Tooltip>\n      ),\n    sortCriteria: storage => storage.usagePercentage,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('srType'),\n    itemRenderer: storage => (storage.shared ? _('srShared') : _('srNotShared')),\n    sortCriteria: 'shared',\n  },\n  {\n    name: _('pbdDetails'),\n    itemRenderer: ({ pbdDeviceConfig }) => {\n      const keys = Object.keys(pbdDeviceConfig)\n      return (\n        <ul className='list-unstyled'>\n          {keys.map(key => (\n            <li key={key}>{_.keyValue(key, pbdDeviceConfig[key])}</li>\n          ))}\n        </ul>\n      )\n    },\n  },\n  {\n    name: _('pbdStatus'),\n    itemRenderer: storage => (\n      <StateButton\n        disabledLabel={_('pbdStatusDisconnected')}\n        disabledHandler={connectPbd}\n        disabledTooltip={_('pbdConnect')}\n        enabledLabel={_('pbdStatusConnected')}\n        enabledHandler={disconnectPbd}\n        enabledTooltip={_('pbdDisconnect')}\n        handlerParam={storage.pbdId}\n        state={storage.attached}\n      />\n    ),\n  },\n]\n\nconst SR_ACTIONS = [\n  {\n    disabled: selectedItems => some(selectedItems, 'attached'),\n    handler: selectedItems => forgetSrs(map(selectedItems, 'pbdId')),\n    icon: 'sr-forget',\n    individualDisabled: storage => storage.attached,\n    individualHandler: forgetSr,\n    label: _('pbdForget'),\n    level: 'danger',\n  },\n]\n\nexport default connectStore(() => {\n  const pbds = createGetObjectsOfType('PBD').pick((_, props) => props.host.$PBDs)\n  const srs = createGetObjectsOfType('SR').pick(createSelector(pbds, pbds => map(pbds, pbd => pbd.SR)))\n  const coalesceTasks = createGetObjectsOfType('task').groupBy('applies_to')\n  const storages = createSelector(pbds, srs, coalesceTasks, (pbds, srs, coalesceTasks) =>\n    map(\n      filter(pbds, pbd => srs[pbd.SR] !== undefined),\n      pbd => {\n        const sr = srs[pbd.SR]\n        const { physical_usage: usage, size } = sr\n        return {\n          attached: pbd.attached,\n          pbdDeviceConfig: pbd.device_config,\n          format: sr.SR_type,\n          free: size > 0 ? size - usage : 0,\n          id: sr.id,\n          nameLabel: sr.name_label,\n          pbdId: pbd.id,\n          shared: isSrShared(sr),\n          size: size > 0 ? size : 0,\n          usagePercentage: size > 0 && Math.round((100 * usage) / size),\n          coalesceTask: coalesceTasks[pbd.SR]?.[0], // there can be only one coalesce task by SR\n        }\n      }\n    )\n  )\n\n  return { storages }\n})(({ host, storages }) => (\n  <Container>\n    <Row>\n      <Col className='text-xs-right'>\n        <TabButtonLink icon='add' labelId='addSrDeviceButton' to={`/new/sr?host=${host.id}`} />\n      </Col>\n    </Row>\n    <Row>\n      <Col>\n        {isEmpty(storages) ? (\n          <h4 className='text-xs-center'>{_('pbdNoSr')}</h4>\n        ) : (\n          <SortedTable actions={SR_ACTIONS} columns={SR_COLUMNS} collection={storages} stateUrlParam='s' />\n        )}\n      </Col>\n    </Row>\n  </Container>\n))\n","import _ from 'intl'\nimport Icon from 'icon'\nimport React from 'react'\n\nimport { Container, Col, Row } from 'grid'\nimport { getXoaPlan, routes, TryXoa } from 'utils'\nimport { NavLink, NavTabs } from 'nav'\n\nimport Page from '../page'\nimport Recipes from './recipes'\nimport Templates from './templates'\n\n// ==================================================================\n\nconst Header = (\n  <Container>\n    <Row>\n      <Col mediumSize={3}>\n        <h2>\n          <Icon icon='menu-hub' /> {_('hubPage')}\n        </h2>\n      </Col>\n      <Col mediumSize={9}>\n        <NavTabs className='pull-right'>\n          <NavLink to='/hub/templates'>\n            <Icon icon='hub-template' /> {_('templatesLabel')}\n          </NavLink>\n          <NavLink to='/hub/recipes'>\n            <Icon icon='hub-recipe' /> {_('recipesLabel')}\n          </NavLink>\n        </NavTabs>\n      </Col>\n    </Row>\n  </Container>\n)\n\nconst Hub = routes('hub', {\n  templates: Templates,\n  recipes: Recipes,\n})(({ children }) => (\n  <Page header={Header} title='hubPage' formatTitle>\n    {getXoaPlan() === 'Community' ? (\n      <Container>\n        <h2 className='text-info'>{_('hubCommunity')}</h2>\n        <p>\n          <TryXoa page='hub' />\n        </p>\n      </Container>\n    ) : (\n      children\n    )}\n  </Page>\n))\n\nexport default Hub\n","import decorate from 'apply-decorators'\nimport React from 'react'\nimport { adminOnly } from 'utils'\nimport { Container, Col, Row } from 'grid'\n\nimport Recipe from './recipe'\n\n// ==================================================================\n\nexport default decorate([\n  adminOnly,\n  () => (\n    <Container>\n      <Row>\n        <Col mediumSize={4}>\n          <Recipe />\n        </Col>\n      </Row>\n    </Container>\n  ),\n])\n","import * as FormGrid from 'form-grid'\nimport _, { messages } from 'intl'\nimport decorate from 'apply-decorators'\nimport React from 'react'\nimport { Container } from 'grid'\nimport { compareVersions } from 'compare-versions'\nimport { createLogger } from '@xen-orchestra/log'\nimport { get } from '@xen-orchestra/defined'\nimport { injectIntl } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport { isSrWritable } from 'xo'\nimport { SelectPool, SelectNetwork, SelectSr } from 'select-objects'\nimport { Select } from 'form'\n\nconst logger = createLogger('kubernetes-recipe')\n\nconst FAULT_TOLERANCE = [\n  {\n    label: _('recipeNoneFaultTolerance'),\n    value: 0,\n  },\n  {\n    label: _('recipeOneFaultTolerance'),\n    value: 1,\n  },\n  {\n    label: _('recipeTwoFaultTolerance'),\n    value: 2,\n  },\n  {\n    label: _('recipeThreeFaultTolerance'),\n    value: 3,\n  },\n]\n\nexport default decorate([\n  injectIntl,\n  provideState({\n    effects: {\n      onChangePool(__, pool) {\n        const { onChange, value } = this.props\n        onChange({\n          ...value,\n          pool,\n        })\n      },\n      onChangeSr(__, sr) {\n        const { onChange, value } = this.props\n        onChange({\n          ...value,\n          sr,\n        })\n      },\n      onChangeNetwork(__, network) {\n        const { onChange, value } = this.props\n        onChange({\n          ...value,\n          network,\n        })\n      },\n      onChangeValue(__, ev) {\n        const { name, value } = ev.target\n        const { onChange, value: prevValue } = this.props\n        onChange({\n          ...prevValue,\n          [name]: value,\n        })\n      },\n      onChangeFaultTolerance(__, faultTolerance) {\n        const { onChange, value } = this.props\n        onChange({\n          ...value,\n          faultTolerance: faultTolerance.value,\n          // n * 2 + 1 is the formula to meet the quorum of RAFT consensus algorithm\n          controlPlanePoolSize: faultTolerance.value * 2 + 1,\n        })\n      },\n      onChangeK8sVersion(__, k8sVersion) {\n        const { onChange, value } = this.props\n        onChange({\n          ...value,\n          k8sVersion: k8sVersion.value,\n        })\n      },\n      onChangeWorkerIp(__, ev) {\n        const { name, value } = ev.target\n        const { onChange, value: prevValue } = this.props\n        const workerNodeIpAddresses = prevValue.workerNodeIpAddresses ?? []\n\n        workerNodeIpAddresses[name.split('.')[1]] = value\n        onChange({\n          ...prevValue,\n          workerNodeIpAddresses,\n        })\n      },\n      onChangeCpIp(__, ev) {\n        const { name, value } = ev.target\n        const { onChange, value: prevValue } = this.props\n        const controlPlaneIpAddresses = prevValue.controlPlaneIpAddresses ?? []\n\n        controlPlaneIpAddresses[name.split('.')[1]] = value\n        onChange({\n          ...prevValue,\n          controlPlaneIpAddresses,\n        })\n      },\n      onChangeNameserver(__, ev) {\n        const { value } = ev.target\n        const { onChange, value: prevValue } = this.props\n        const nameservers = value.split(',').map(nameserver => nameserver.trim())\n        onChange({\n          ...prevValue,\n          nameservers,\n        })\n      },\n      onChangeSearch(__, ev) {\n        const input = ev.target.value.trim()\n        const { onChange, value: prevValue } = this.props\n        onChange({\n          ...prevValue,\n          searches: input.length === 0 ? undefined : input.split(',').map(search => search.trim()),\n        })\n      },\n      toggleStaticIpAddress(__, ev) {\n        const { name } = ev.target\n        const { onChange, value: prevValue } = this.props\n        onChange({\n          ...prevValue,\n          [name]: ev.target.checked,\n        })\n      },\n    },\n    computed: {\n      networkPredicate:\n        (_, { value: { pool } }) =>\n        network =>\n          pool.id === network.$pool,\n      srPredicate:\n        (_, { value }) =>\n        sr =>\n          sr.$pool === get(() => value.pool.id) && isSrWritable(sr),\n      versionList: async () => {\n        const res = await fetch('https://api.github.com/repos/canonical/microk8s/releases')\n        if (res.ok) {\n          const rawList = await res.json()\n          const versionList = rawList\n            .filter(version => !version.prerelease)\n            .map(({ tag_name }) => ({\n              label: tag_name,\n              value: tag_name.slice(1),\n            }))\n            .sort(({ value: a }, { value: b }) => -compareVersions(a, b))\n          return versionList\n        } else {\n          logger.error('HTTP response: ' + res.status)\n        }\n      },\n    },\n  }),\n  injectState,\n  ({ effects, install, intl: { formatMessage }, state, value }) => (\n    <Container>\n      <FormGrid.Row>\n        <label>{_('vmImportToPool')}</label>\n        <SelectPool className='mb-1' onChange={effects.onChangePool} required value={value.pool} />\n      </FormGrid.Row>\n      <FormGrid.Row>\n        <label>{_('vmImportToSr')}</label>\n        <SelectSr onChange={effects.onChangeSr} predicate={state.srPredicate} required value={value.sr} />\n      </FormGrid.Row>\n      <FormGrid.Row>\n        <label>{_('network')}</label>\n        <SelectNetwork\n          className='mb-1'\n          onChange={effects.onChangeNetwork}\n          required\n          value={value.network}\n          predicate={state.networkPredicate}\n        />\n      </FormGrid.Row>\n      <FormGrid.Row>\n        <label>{_('recipeSelectK8sVersion')}</label>\n        <Select\n          className='mb-1'\n          name='k8sVersion'\n          onChange={effects.onChangeK8sVersion}\n          options={state.versionList}\n          required\n          value={value.k8sVersion}\n        />\n      </FormGrid.Row>\n      <FormGrid.Row>\n        <label>{_('recipeClusterNameLabel')}</label>\n        <input\n          className='form-control'\n          name='clusterName'\n          onChange={effects.onChangeValue}\n          placeholder={formatMessage(messages.recipeClusterNameLabel)}\n          required\n          type='text'\n          value={value.clusterName}\n        />\n      </FormGrid.Row>\n      <FormGrid.Row>\n        <label>{_('recipeNumberOfNodesLabel')}</label>\n        <input\n          className='form-control'\n          name='nbNodes'\n          min='1'\n          onChange={effects.onChangeValue}\n          placeholder={formatMessage(messages.recipeNumberOfNodesLabel)}\n          required\n          type='number'\n          value={value.nbNodes}\n        />\n      </FormGrid.Row>\n      <FormGrid.Row>\n        <label>{_('recipeFaultTolerance')}</label>\n        <Select\n          className='mb-1'\n          name='faultTolerance'\n          onChange={effects.onChangeFaultTolerance}\n          options={FAULT_TOLERANCE}\n          required\n          value={value.faultTolerance}\n        />\n      </FormGrid.Row>\n      <FormGrid.Row>\n        <label>\n          <input\n            className='mt-1'\n            name='staticIpAddress'\n            onChange={effects.toggleStaticIpAddress}\n            type='checkbox'\n            value={value.staticIpAddress}\n          />\n          &nbsp;\n          {_('recipeStaticIpAddresses')}\n        </label>\n      </FormGrid.Row>\n      {value.nbNodes > 0 &&\n        value.staticIpAddress && [\n          value.faultTolerance > 0\n            ? [\n                Array.from({ length: value.controlPlanePoolSize }).map((v, i) => (\n                  <FormGrid.Row key={i}>\n                    <label>{_('recipeHaControPlaneIpAddress', { i: i + 1 })}</label>\n                    <input\n                      className='form-control'\n                      name={`controlPlaneIpAddress.${i}`}\n                      onChange={effects.onChangeCpIp}\n                      placeholder={formatMessage(messages.recipeHaControPlaneIpAddress, { i: i + 1 })}\n                      required\n                      type='text'\n                      value={value[`controlPlaneIpAddress.${i}`]}\n                    />\n                  </FormGrid.Row>\n                )),\n                <FormGrid.Row key='vipAddrRow'>\n                  <label>{_('recipeVip')}</label>\n                  <input\n                    className='form-control'\n                    name='vipAddress'\n                    onChange={effects.onChangeValue}\n                    placeholder={formatMessage(messages.recipeVip)}\n                    required\n                    type='text'\n                    value={value.vipAddress}\n                  />\n                </FormGrid.Row>,\n              ]\n            : [\n                <FormGrid.Row key='controlPlaneIpAddrRow'>\n                  <label>{_('recipeControlPlaneIpAddress')}</label>\n                  <input\n                    className='form-control'\n                    name='controlPlaneIpAddress'\n                    onChange={effects.onChangeValue}\n                    placeholder={formatMessage(messages.recipeControlPlaneIpAddress)}\n                    required\n                    type='text'\n                    value={value.controlPlaneIpAddress}\n                  />\n                </FormGrid.Row>,\n              ],\n          <FormGrid.Row key='gatewayRow'>\n            <label>{_('recipeGatewayIpAddress')}</label>\n            <input\n              className='form-control'\n              name='gatewayIpAddress'\n              onChange={effects.onChangeValue}\n              placeholder={formatMessage(messages.recipeGatewayIpAddress)}\n              required\n              type='text'\n              value={value.gatewayIpAddress}\n            />\n          </FormGrid.Row>,\n          <FormGrid.Row key='nameserverRow'>\n            <label>{_('recipeNameserverAddresses')}</label>\n            <input\n              className='form-control'\n              name='nameservers'\n              onChange={effects.onChangeNameserver}\n              placeholder={formatMessage(messages.recipeNameserverAddressesExample)}\n              required\n              type='text'\n              value={value.nameservers}\n            />\n          </FormGrid.Row>,\n          <FormGrid.Row key='searchRow'>\n            <label>{_('recipeSearches')}</label>\n            <input\n              className='form-control'\n              name='search'\n              onChange={effects.onChangeSearch}\n              placeholder={formatMessage(messages.recipeSearchesExample)}\n              type='text'\n              value={value.search}\n            />\n          </FormGrid.Row>,\n          [...Array(+value.nbNodes)].map((v, i) => (\n            <FormGrid.Row key={v}>\n              <label>{_('recipeWorkerIpAddress', { i: i + 1 })}</label>\n              <input\n                className='form-control'\n                name={`workerNodeIpAddress.${i}`}\n                onChange={effects.onChangeWorkerIp}\n                placeholder={formatMessage(messages.recipeWorkerIpAddress, { i: i + 1 })}\n                required\n                type='text'\n                value={value[`workerNodeIpAddress.${i}`]}\n              />\n            </FormGrid.Row>\n          )),\n        ]}\n      <FormGrid.Row>\n        <label>{_('recipeSshKeyLabel')}</label>\n        <input\n          className='form-control'\n          name='sshKey'\n          onChange={effects.onChangeValue}\n          placeholder={formatMessage(messages.recipeSshKeyLabel)}\n          required\n          type='text'\n          value={value.sshKey}\n        />\n      </FormGrid.Row>\n    </Container>\n  ),\n])\n","import * as ComplexMatcher from 'complex-matcher'\nimport _ from 'intl'\nimport ActionButton from 'action-button'\nimport ButtonLink from 'button-link'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport marked from 'marked'\nimport React from 'react'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport escapeRegExp from 'lodash/escapeRegExp.js'\nimport { form } from 'modal'\nimport { connectStore } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\nimport { createKubernetesCluster } from 'xo'\nimport { injectState, provideState } from 'reaclette'\nimport { success } from 'notification'\nimport { withRouter } from 'react-router'\n\nimport RecipeForm from './recipe-form'\n\nconst RECIPE_INFO = {\n  id: '05abc8a8-ebf4-41a6-b1ed-efcb2dbf893d',\n  name: 'Kubernetes cluster',\n  description: 'Creates a Kubernetes cluster composed of a configurable number of control planes and worker nodes.',\n}\n\nexport default decorate([\n  withRouter,\n  connectStore(() => ({\n    pools: createGetObjectsOfType('pool'),\n    recipeCreatingResources: state => state.recipeCreatingResources,\n  })),\n  provideState({\n    initialState: () => ({\n      selectedInstallPools: [],\n    }),\n    effects: {\n      async create() {\n        const { markRecipeAsCreating, markRecipeAsDone } = this.props\n        const recipeParams = await form({\n          defaultValue: {\n            pool: {},\n          },\n          render: props => <RecipeForm {...props} value={{ nbNodes: 1, ...props.value }} />,\n          header: (\n            <span>\n              <Icon icon='hub-recipe' /> {RECIPE_INFO.name}\n            </span>\n          ),\n          size: 'medium',\n        })\n\n        const {\n          clusterName,\n          controlPlaneIpAddress,\n          controlPlaneIpAddresses,\n          controlPlanePoolSize,\n          gatewayIpAddress,\n          k8sVersion,\n          nameservers,\n          nbNodes,\n          network,\n          searches,\n          sr,\n          sshKey,\n          vipAddress,\n          workerNodeIpAddresses,\n        } = recipeParams\n\n        markRecipeAsCreating(RECIPE_INFO.id)\n        const tag = await createKubernetesCluster({\n          clusterName,\n          controlPlaneIpAddress,\n          controlPlaneIpAddresses,\n          controlPlanePoolSize,\n          gatewayIpAddress,\n          k8sVersion,\n          nameservers,\n          nbNodes: +nbNodes,\n          network: network.id,\n          searches,\n          sr: sr.id,\n          sshKey,\n          vipAddress,\n          workerNodeIpAddresses,\n        })\n        markRecipeAsDone(RECIPE_INFO.id)\n\n        const filter = new ComplexMatcher.Property('tags', new ComplexMatcher.RegExp(`^${escapeRegExp(tag)}$`))\n\n        success(\n          _('recipeCreatedSuccessfully'),\n          <ButtonLink btnStyle='success' size='small' to={`/home?s=${encodeURIComponent(filter)}`}>\n            {_('recipeViewCreatedVms')}\n          </ButtonLink>,\n          8e3\n        )\n      },\n    },\n  }),\n  injectState,\n  ({ effects, recipeCreatingResources }) => (\n    <Card shadow>\n      <CardHeader>{RECIPE_INFO.name}</CardHeader>\n      <CardBlock>\n        <div\n          className='text-muted'\n          dangerouslySetInnerHTML={{\n            __html: marked(RECIPE_INFO.description),\n          }}\n        />\n        <hr />\n        <ActionButton block handler={effects.create} icon='deploy' pending={recipeCreatingResources[RECIPE_INFO.id]}>\n          {_('create')}\n        </ActionButton>\n      </CardBlock>\n    </Card>\n  ),\n])\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport { addSubscriptions, adminOnly } from 'utils'\nimport { Container, Col, Row } from 'grid'\nimport { injectState, provideState } from 'reaclette'\nimport { isEmpty, map, omit, orderBy } from 'lodash'\nimport { subscribeHubResourceCatalog } from 'xo'\n\nimport Resource from './resource'\n\n// ==================================================================\n\nexport default decorate([\n  adminOnly,\n  addSubscriptions({\n    catalog: subscribeHubResourceCatalog,\n  }),\n  provideState({\n    computed: {\n      resources: (_, { catalog }) =>\n        orderBy(\n          map(omit(catalog, '_namespaces'), (entry, namespace) => ({\n            namespace,\n            ...entry.xva,\n          })),\n          'name',\n          'asc'\n        ),\n    },\n  }),\n  injectState,\n  ({ state: { resources } }) => (\n    <Container>\n      <Row>\n        {isEmpty(resources) ? (\n          <Col>\n            <h2 className='text-muted'>\n              &nbsp; {_('vmNoAvailable')}\n              <Icon icon='alarm' color='yellow' />\n            </h2>\n          </Col>\n        ) : (\n          resources.map(data => (\n            <Col key={data.namespace} mediumSize={6} largeSize={4}>\n              <Resource {...data} />\n            </Col>\n          ))\n        )}\n      </Row>\n    </Container>\n  ),\n])\n","import * as FormGrid from 'form-grid'\nimport _ from 'intl'\nimport decorate from 'apply-decorators'\nimport defined from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport Tooltip from 'tooltip'\nimport { Container, Col } from 'grid'\nimport { isEmpty, sortBy } from 'lodash'\nimport { injectState, provideState } from 'reaclette'\nimport { isSrWritable } from 'xo'\nimport { Pool } from 'render-xo-item'\nimport { SelectPool, SelectSr } from 'select-objects'\n\nexport default decorate([\n  provideState({\n    effects: {\n      onChangePools(__, pools) {\n        const { multi, onChange, value } = this.props\n        onChange({\n          ...value,\n          [multi ? 'pools' : 'pool']: pools,\n        })\n      },\n      onChangeSr(__, sr) {\n        const { onChange, value } = this.props\n        onChange({\n          ...value,\n          mapPoolsSrs: {\n            ...value.mapPoolsSrs,\n            [sr.$pool]: sr.id,\n          },\n        })\n      },\n    },\n    computed: {\n      sortedPools: (_, { value }) => sortBy(value.pools, 'name_label'),\n    },\n  }),\n  injectState,\n  ({ effects, install, multi, poolPredicate, state, value }) => (\n    <Container>\n      <FormGrid.Row>\n        <label>\n          {_('vmImportToPool')}\n          &nbsp;\n          {install && (\n            <Tooltip content={_('hideInstalledPool')}>\n              <Icon icon='info' />\n            </Tooltip>\n          )}\n        </label>\n        <SelectPool\n          className='mb-1'\n          multi={multi}\n          onChange={effects.onChangePools}\n          predicate={poolPredicate}\n          required\n          value={multi ? value.pools : value.pool}\n        />\n      </FormGrid.Row>\n      {install && multi && !isEmpty(value.pools) && (\n        <div>\n          <SingleLineRow>\n            <Col size={6}>\n              <strong>{_('pool')}</strong>\n            </Col>\n            <Col size={6}>\n              <strong>{_('sr')}</strong>\n            </Col>\n          </SingleLineRow>\n          <hr />\n          {state.sortedPools.map(pool => (\n            <SingleLineRow key={pool.id} className='mt-1'>\n              <Col size={6}>\n                <Pool id={pool.id} link />\n              </Col>\n              <Col size={6}>\n                <SelectSr\n                  onChange={effects.onChangeSr}\n                  predicate={sr => sr.$pool === pool.id && isSrWritable(sr)}\n                  required\n                  value={defined(value.mapPoolsSrs[pool.id], pool.default_SR)}\n                />\n              </Col>\n            </SingleLineRow>\n          ))}\n        </div>\n      )}\n    </Container>\n  ),\n])\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport decorate from 'apply-decorators'\nimport defined from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport marked from 'marked'\nimport React from 'react'\nimport { alert, form } from 'modal'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { Col, Row } from 'grid'\nimport { connectStore, formatSize } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\nimport { deleteTemplates, downloadAndInstallResource, pureDeleteVm } from 'xo'\nimport { error, success } from 'notification'\nimport { find, filter, isEmpty, map, omit, startCase } from 'lodash'\nimport { injectState, provideState } from 'reaclette'\nimport { withRouter } from 'react-router'\n\nimport ResourceForm from './resource-form'\n\nconst Li = props => <li {...props} className='list-group-item' />\nconst Ul = props => <ul {...props} className='list-group' />\n\n// Template <id> : specific to a template version\n// Template <namespace> : general template identifier (can have multiple versions)\n// Template <any> : a default hub metadata, please don't remove it from BANNED_FIELDS\n\nconst BANNED_FIELDS = ['any', 'description'] // These fields will not be displayed on description modal\nconst EXCLUSIVE_FIELDS = ['longDescription'] // These fields will not have a label\nconst MARKDOWN_FIELDS = ['longDescription', 'description']\nconst STATIC_FIELDS = [...EXCLUSIVE_FIELDS, ...BANNED_FIELDS] // These fields will not be displayed with dynamic fields\n\nexport default decorate([\n  withRouter,\n  connectStore(() => {\n    const getTemplates = createGetObjectsOfType('VM-template').sort()\n    const getPools = createGetObjectsOfType('pool')\n    return {\n      templates: getTemplates,\n      pools: getPools,\n      hubInstallingResources: state => state.hubInstallingResources,\n    }\n  }),\n  provideState({\n    initialState: () => ({\n      selectedInstallPools: [],\n    }),\n    effects: {\n      async install() {\n        const { id, name, namespace, markHubResourceAsInstalled, markHubResourceAsInstalling, templates, version } =\n          this.props\n        const { isTemplateInstalled } = this.state\n        const resourceParams = await form({\n          defaultValue: {\n            mapPoolsSrs: {},\n            pools: [],\n          },\n          render: props => <ResourceForm install multi poolPredicate={isTemplateInstalled} {...props} />,\n          header: (\n            <span>\n              <Icon icon='add-vm' /> {name}\n            </span>\n          ),\n          size: 'medium',\n        })\n\n        markHubResourceAsInstalling(id)\n        try {\n          await Promise.all(\n            resourceParams.pools.map(async pool => {\n              await downloadAndInstallResource({\n                namespace,\n                id,\n                version,\n                sr: defined(resourceParams.mapPoolsSrs[pool.id], pool.default_SR),\n                templateOnly: true,\n              })\n              const oldTemplates = filter(\n                templates,\n                template => pool.$pool === template.$pool && template.other['xo:resource:namespace'] === namespace\n              )\n              await Promise.all(oldTemplates.map(template => pureDeleteVm(template)))\n            })\n          )\n          success(_('hubImportNotificationTitle'), _('successfulInstall'))\n        } catch (_error) {\n          error(_('hubImportNotificationTitle'), _error.message)\n        }\n        markHubResourceAsInstalled(id)\n      },\n      async create() {\n        const { isPoolCreated, installedTemplates } = this.state\n        const { name } = this.props\n        const resourceParams = await form({\n          defaultValue: {\n            pool: undefined,\n          },\n          render: props => <ResourceForm poolPredicate={isPoolCreated} {...props} />,\n          header: (\n            <span>\n              <Icon icon='add-vm' /> {name}\n            </span>\n          ),\n          size: 'medium',\n        })\n        const { $pool } = resourceParams.pool\n        const template = find(installedTemplates, { $pool })\n        if (template !== undefined) {\n          this.props.router.push(`/vms/new?pool=${$pool}&template=${template.id}`)\n        } else {\n          throw new Error(`can't find template for pool: ${$pool}`)\n        }\n      },\n      async deleteTemplates(__, { name }) {\n        const { isPoolCreated } = this.state\n        const resourceParams = await form({\n          defaultValue: {\n            pools: [],\n          },\n          render: props => <ResourceForm delete multi poolPredicate={isPoolCreated} {...props} />,\n          header: (\n            <span>\n              <Icon icon='vm-delete' /> {name}\n            </span>\n          ),\n          size: 'medium',\n        })\n        const _templates = filter(this.state.installedTemplates, template =>\n          find(resourceParams.pools, { $pool: template.$pool })\n        )\n        await deleteTemplates(_templates)\n      },\n      updateSelectedInstallPools(_, selectedInstallPools) {\n        return {\n          selectedInstallPools,\n        }\n      },\n      updateSelectedCreatePool(_, selectedCreatePool) {\n        return {\n          selectedCreatePool,\n        }\n      },\n      redirectToTaskPage() {\n        this.props.router.push('/tasks')\n      },\n      showDescription() {\n        const {\n          data: { public: _public },\n          name,\n        } = this.props\n        alert(\n          name,\n          <div>\n            {isEmpty(omit(_public, BANNED_FIELDS)) ? (\n              <p>{_('hubTemplateDescriptionNotAvailable')}</p>\n            ) : (\n              <div>\n                <Ul>\n                  {EXCLUSIVE_FIELDS.map(fieldKey => {\n                    const field = _public[fieldKey]\n                    if (field !== undefined) {\n                      return (\n                        <Li key={fieldKey}>\n                          {MARKDOWN_FIELDS.includes(fieldKey) ? (\n                            <div\n                              dangerouslySetInnerHTML={{\n                                __html: marked(field),\n                              }}\n                            />\n                          ) : (\n                            field\n                          )}\n                        </Li>\n                      )\n                    }\n                    return null\n                  })}\n                </Ul>\n                <br />\n                <Ul>\n                  {map(omit(_public, STATIC_FIELDS), (value, key) => (\n                    <Li key={key}>\n                      {startCase(key)}\n                      <span className='pull-right'>\n                        {typeof value === 'boolean' ? (\n                          <Icon color={value ? 'green' : 'red'} icon={value ? 'true' : 'false'} />\n                        ) : key.toLowerCase().endsWith('size') ? (\n                          <strong>{formatSize(value)}</strong>\n                        ) : (\n                          <strong>{value}</strong>\n                        )}\n                      </span>\n                    </Li>\n                  ))}\n                </Ul>\n              </div>\n            )}\n          </div>\n        )\n      },\n    },\n    computed: {\n      description: (\n        _,\n        {\n          data: {\n            public: { description },\n          },\n          description: _description,\n        }\n      ) =>\n        (description !== undefined || _description !== undefined) && (\n          <div\n            className='text-muted'\n            dangerouslySetInnerHTML={{\n              __html: marked(defined(description, _description)),\n            }}\n          />\n        ),\n      installedTemplates: (_, { id, templates }) => filter(templates, ['other.xo:resource:xva:id', id]),\n      isTemplateInstalledOnAllPools: ({ installedTemplates }, { pools }) =>\n        installedTemplates.length > 0 &&\n        pools.every(pool => installedTemplates.find(template => template.$pool === pool.id) !== undefined),\n      isTemplateInstalled:\n        ({ installedTemplates }) =>\n        pool =>\n          installedTemplates.find(template => template.$pool === pool.id) === undefined,\n      isPoolCreated:\n        ({ installedTemplates }) =>\n        pool =>\n          installedTemplates.find(template => template.$pool === pool.id) !== undefined,\n    },\n  }),\n  injectState,\n  ({ effects, hubInstallingResources, id, name, size, state, totalDiskSize }) => (\n    <Card shadow>\n      <CardHeader>\n        {name}\n        <ActionButton\n          className='pull-right'\n          color='light'\n          data-name={name}\n          disabled={state.installedTemplates.length === 0}\n          handler={effects.deleteTemplates}\n          icon='delete'\n          size='small'\n          style={{ border: 'none' }}\n          tooltip={_('remove')}\n        />\n        <br />\n      </CardHeader>\n      <CardBlock>\n        {state.description}\n        <ActionButton className='pull-right' color='light' handler={effects.showDescription} icon='info' size='small'>\n          {_('moreDetails')}\n        </ActionButton>\n        <div>\n          <span className='text-muted'>{_('size')}</span>\n          {'  '}\n          <strong>{formatSize(size)}</strong>\n        </div>\n        <div>\n          <span className='text-muted'>{_('totalDiskSize')}</span>\n          {'  '}\n          <strong>{formatSize(totalDiskSize)}</strong>\n        </div>\n        <hr />\n        <Row>\n          <Col mediumSize={6}>\n            <ActionButton\n              block\n              disabled={state.isTemplateInstalledOnAllPools}\n              form={state.idInstallForm}\n              handler={effects.install}\n              icon='add'\n              pending={hubInstallingResources[id]}\n            >\n              {_('install')}\n            </ActionButton>\n          </Col>\n          <Col mediumSize={6}>\n            <ActionButton\n              block\n              disabled={state.installedTemplates.length === 0}\n              form={state.idCreateForm}\n              handler={effects.create}\n              icon='deploy'\n            >\n              {_('create')}\n            </ActionButton>\n          </Col>\n        </Row>\n      </CardBlock>\n    </Card>\n  ),\n])\n","import _ from 'intl'\nimport Icon from 'icon'\nimport Page from '../page'\nimport React from 'react'\nimport { Col, Container, Row } from 'grid'\nimport { NavLink, NavTabs } from 'nav'\nimport { routes } from 'utils'\n\nimport DiskImport from '../disk-import'\nimport EsxiImport from '../vm-import/esxi/esxi-import'\nimport VmImport from '../vm-import'\n\nconst HEADER = (\n  <Container>\n    <Row>\n      <Col mediumSize={3}>\n        <h2>\n          <Icon icon='import' /> {_('newImport')}\n        </h2>\n      </Col>\n      <Col mediumSize={9}>\n        <NavTabs className='pull-right'>\n          <NavLink to='/import/vm'>\n            <Icon icon='vm' /> {_('labelVm')}\n          </NavLink>\n          <NavLink to='/import/disk'>\n            <Icon icon='disk' /> {_('labelDisk')}\n          </NavLink>\n          <NavLink to='/import/vmware'>\n            <Icon icon='vm' /> {_('fromVmware')}\n          </NavLink>\n        </NavTabs>\n      </Col>\n    </Row>\n  </Container>\n)\n\nconst Import = routes('vm', {\n  disk: DiskImport,\n  vm: VmImport,\n  vmware: EsxiImport,\n})(({ children }) => (\n  <Page header={HEADER} title='newImport' formatTitle>\n    {children}\n  </Page>\n))\n\nexport default Import\n","import Component from 'base-component'\nimport cookies from 'js-cookie'\nimport DocumentTitle from 'react-document-title'\nimport every from 'lodash/every'\nimport Icon from 'icon'\nimport Link from 'link'\nimport map from 'lodash/map'\nimport mapValues from 'lodash/mapValues'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport Shortcuts from 'shortcuts'\nimport themes from 'themes'\nimport _, { IntlProvider } from 'intl'\n// TODO: Replace all `getXoaPlan` by `getXoaPlan` from \"xoa-plans\"\nimport { addSubscriptions, connectStore, getXoaPlan, noop, routes } from 'utils'\nimport { blockXoaAccess, isTrialRunning } from 'xoa-updater'\nimport { checkXoa, clearXoaCheckCache } from 'xo'\nimport { forEach, groupBy, keyBy, pick } from 'lodash'\nimport { Host as HostItem } from 'render-xo-item'\nimport { Notification } from 'notification'\nimport { productId2Plan } from 'xoa-plans'\nimport { provideState } from 'reaclette'\nimport { ShortcutManager } from 'react-shortcuts'\nimport { ThemeProvider } from 'styled-components'\nimport { TooltipViewer } from 'tooltip'\nimport { Container, Row, Col } from 'grid'\n// import {\n//   keyHandler\n// } from 'react-key-handler'\n\nimport About from './about'\nimport Backup from './backup'\nimport Dashboard from './dashboard'\nimport Home from './home'\nimport Host from './host'\nimport Hub from './hub'\nimport Jobs from './jobs'\nimport Menu from './menu'\nimport Modal, { alert, FormModal } from 'modal'\nimport New from './new'\nimport NewVm from './new-vm'\nimport Pool from './pool'\nimport Proxies from './proxies'\nimport Self from './self'\nimport Settings from './settings'\nimport Sr from './sr'\nimport Tasks from './tasks'\nimport User from './user'\nimport Vm from './vm'\nimport Xoa from './xoa'\nimport XoaUpdates from './xoa/update'\nimport Xostor from './xostor'\nimport Import from './import'\n\nimport keymap, { help } from '../keymap'\nimport Tooltip from '../common/tooltip'\nimport { createCollectionWrapper, createGetObjectsOfType } from '../common/selectors'\nimport {\n  bindXcpngLicense,\n  rebindLicense,\n  subscribeXcpngLicenses,\n  subscribeXostorLicenses,\n  subscribeSelfLicenses,\n} from '../common/xo'\nimport { SOURCES } from '../common/xoa-plans'\nimport { getLicenseNearExpiration } from '../common/xoa-updater'\n\nconst shortcutManager = new ShortcutManager(keymap)\n\nconst CONTAINER_STYLE = {\n  display: 'flex',\n  minHeight: '100vh',\n\n  // FIXME: The size of `xo-main` matches the size of the window\n  // thanks to the, flex growing feature.\n  //\n  // Therefore, when there is a scrollbar on the right side, `xo-main`\n  // is too large (since the scrollbar uses a few, pixels) which makes\n  // an almost useless horizontal scrollbar appear.\n  overflow: 'hidden',\n}\nconst BODY_WRAPPER_STYLE = {\n  flex: 1,\n  position: 'relative',\n}\nconst BODY_STYLE = {\n  height: '100%',\n  left: 0,\n  overflow: 'auto',\n  position: 'absolute',\n  top: 0,\n  width: '100%',\n}\n\nconst WrapperIconPoolLicense = ({ children }) => (\n  <a href='https://xcp-ng.com' rel='noreferrer noopener' target='_blank'>\n    {children}\n  </a>\n)\n\nexport const ICON_POOL_LICENSE = {\n  total: tooltip => (\n    <Tooltip content={tooltip}>\n      <WrapperIconPoolLicense>\n        <Icon icon='pro-support' className='text-success' />\n      </WrapperIconPoolLicense>\n    </Tooltip>\n  ),\n  partial: () => (\n    <WrapperIconPoolLicense>\n      <Icon icon='alarm' className='text-warning' />\n    </WrapperIconPoolLicense>\n  ),\n  any: () => (\n    <WrapperIconPoolLicense>\n      <Icon icon='alarm' className='text-warning' />\n    </WrapperIconPoolLicense>\n  ),\n}\n\n@routes('home', {\n  about: About,\n  backup: Backup,\n  'backup-ng/*': {\n    onEnter: ({ location }, replace) => replace(location.pathname.replace('/backup-ng', '/backup')),\n  },\n  dashboard: Dashboard,\n  home: Home,\n  'hosts/:id': Host,\n  jobs: Jobs,\n  new: New,\n  'pools/:id': Pool,\n  self: Self,\n  settings: Settings,\n  'srs/:id': Sr,\n  tasks: Tasks,\n  user: User,\n  'vms/new': NewVm,\n  'vms/:id': Vm,\n  xoa: Xoa,\n  xostor: Xostor,\n  import: Import,\n  hub: Hub,\n  proxies: Proxies,\n})\n@addSubscriptions({\n  xcpLicenses: subscribeXcpngLicenses,\n  xostorLicenses: subscribeXostorLicenses,\n  selfLicences: subscribeSelfLicenses,\n})\n@connectStore(state => {\n  const getHosts = createGetObjectsOfType('host')\n  const getXostors = createGetObjectsOfType('SR').filter([sr => sr.SR_type === 'linstor'])\n  const getXsa468VulnerableVms = createGetObjectsOfType('VM').filter([\n    vm => vm.vulnerabilities?.xsa468?.reason === 'pv-driver-version-vulnerable' && !vm.tags.includes('HIDE_XSA468'),\n  ])\n  return {\n    trial: state.xoaTrialState,\n    registerNeeded: state.xoaUpdaterState === 'registerNeeded',\n    signedUp: !!state.user,\n    hosts: getHosts(state),\n    xsa468VulnerableVms: getXsa468VulnerableVms(state),\n    xostors: getXostors(state),\n  }\n})\n@provideState({\n  initialState: () => ({ checkXoaCount: 0 }),\n  effects: {\n    async forceRefreshXoaStatus() {\n      await clearXoaCheckCache()\n      await this.effects.refreshXoaStatus()\n    },\n    refreshXoaStatus() {\n      this.state.checkXoaCount += 1\n    },\n    async bindXcpngLicenses(_, xcpngLicensesByHost) {\n      await Promise.all(\n        map(xcpngLicensesByHost, ({ productId, id, boundObjectId }, hostId) =>\n          boundObjectId !== undefined\n            ? rebindLicense(productId, id, boundObjectId, hostId)\n            : bindXcpngLicense(id, hostId)\n        )\n      )\n    },\n  },\n  computed: {\n    // In case a host have more than 1 license, it's an issue.\n    // poolLicenseInfoByPoolId can be impacted because the license expiration check may not yield the right information.\n    xcpngLicenseByBoundObjectId: (_, { xcpLicenses }) =>\n      xcpLicenses === undefined ? undefined : keyBy(xcpLicenses, 'boundObjectId'),\n    xostorLicensesByBoundObjectId: (_, { xostorLicenses }) =>\n      xostorLicenses === undefined ? undefined : groupBy(xostorLicenses, 'boundObjectId'),\n    xcpngLicenseById: (_, { xcpLicenses }) => keyBy(xcpLicenses, 'id'),\n    hostsByPoolId: createCollectionWrapper((_, { hosts }) =>\n      groupBy(\n        map(hosts, host => pick(host, ['$poolId', 'id', 'version'])),\n        '$poolId'\n      )\n    ),\n    poolLicenseInfoByPoolId: ({ hostsByPoolId, xcpngLicenseByBoundObjectId }) => {\n      const poolLicenseInfoByPoolId = {}\n\n      forEach(hostsByPoolId, (hosts, poolId) => {\n        const nHosts = hosts.length\n        let earliestExpirationDate\n        let nHostsUnderLicense = 0\n\n        if (getXoaPlan() === SOURCES.name) {\n          poolLicenseInfoByPoolId[poolId] = {\n            nHostsUnderLicense,\n            icon: () => <Icon icon='unknown-status' className='text-warning' />,\n            nHosts,\n          }\n          return\n        }\n\n        for (const host of hosts) {\n          const license = xcpngLicenseByBoundObjectId?.[host.id]\n          if (license === undefined) {\n            continue\n          }\n          license.expires = license.expires ?? Infinity\n\n          if (license.expires > Date.now()) {\n            nHostsUnderLicense++\n            if (earliestExpirationDate === undefined || license.expires < earliestExpirationDate) {\n              earliestExpirationDate = license.expires\n            }\n          }\n        }\n\n        const supportLevel = nHostsUnderLicense === 0 ? 'any' : nHostsUnderLicense === nHosts ? 'total' : 'partial'\n\n        poolLicenseInfoByPoolId[poolId] = {\n          earliestExpirationDate,\n          icon: ICON_POOL_LICENSE[supportLevel],\n          nHosts,\n          nHostsUnderLicense,\n          supportLevel,\n        }\n      })\n\n      return poolLicenseInfoByPoolId\n    },\n    xoaStatus: {\n      get({ checkXoaCount }) {\n        // To avoid aggressive minification which would remove destructuration\n        noop(checkXoaCount)\n        return getXoaPlan() === 'Community' ? '' : checkXoa().catch(() => 'XOA status not available')\n      },\n      placeholder: '',\n    },\n    isXoaStatusOk: ({ xoaStatus }) => !xoaStatus.includes('✖'),\n    areHostsVersionsEqualByPool: ({ hostsByPoolId }) =>\n      mapValues(hostsByPoolId, hosts => every(hosts, host => host.version === hosts[0].version)),\n    xostorLicenseInfoByXostorId: (\n      { xcpngLicenseByBoundObjectId, xostorLicensesByBoundObjectId, hostsByPoolId },\n      { xostors }\n    ) => {\n      if (xcpngLicenseByBoundObjectId === undefined || xostorLicensesByBoundObjectId === undefined) {\n        return\n      }\n      const xostorLicenseInfoByXostorId = {}\n      const now = Date.now()\n\n      forEach(xostors, xostor => {\n        const xostorId = xostor.id\n        const hosts = hostsByPoolId[xostor.$pool]\n\n        const alerts = []\n        let supportEnabled = true\n\n        hosts.forEach(host => {\n          const hostId = host.id\n          const xostorLicenses = xostorLicensesByBoundObjectId[hostId]\n\n          if (xostorLicenses === undefined) {\n            supportEnabled = false\n            alerts.push({\n              level: 'danger',\n              render: <p>{_('hostHasNoXostorLicense', { host: <HostItem id={hostId} /> })}</p>,\n            })\n          }\n\n          if (xostorLicenses?.length > 1) {\n            alerts.push({\n              level: 'warning',\n              render: (\n                <p>\n                  {_('hostBoundToMultipleXostorLicenses', { host: <HostItem id={hostId} /> })}\n                  <br />\n                  {xostorLicenses.map(license => license.id.slice(-4)).join(',')}\n                </p>\n              ),\n            })\n          }\n\n          const expiredXostorLicenses = xostorLicenses?.filter(license => license.expires < now)\n          if (expiredXostorLicenses?.length > 0) {\n            let level = 'warning'\n            if (expiredXostorLicenses.length === xostorLicenses.length) {\n              supportEnabled = false\n              level = 'danger'\n            }\n            alerts.push({\n              level,\n              render: (\n                <p>\n                  {_('licenseExpiredXostorWarning', {\n                    licenseIds: expiredXostorLicenses.map(license => license.id.slice(-4)).join(','),\n                    nLicenseIds: expiredXostorLicenses.length,\n                    host: <HostItem id={hostId} />,\n                  })}\n                </p>\n              ),\n            })\n          }\n        })\n\n        xostorLicenseInfoByXostorId[xostorId] = {\n          alerts,\n          supportEnabled,\n        }\n      })\n\n      return xostorLicenseInfoByXostorId\n    },\n  },\n})\nexport default class XoApp extends Component {\n  static contextTypes = {\n    router: PropTypes.object,\n  }\n  static childContextTypes = {\n    shortcuts: PropTypes.object.isRequired,\n  }\n  getChildContext = () => ({ shortcuts: shortcutManager })\n\n  state = {\n    dismissedSourceBanner: Boolean(cookies.get('dismissedSourceBanner')),\n    dismissedTrialBanner: Boolean(cookies.get('dismissedTrialBanner')),\n  }\n\n  displayOpenSourceDisclaimer() {\n    const previousDisclaimer = cookies.get('previousDisclaimer')\n    const now = Math.floor(Date.now() / 1e3)\n    const oneWeekAgo = now - 7 * 24 * 3600\n    if (!previousDisclaimer || previousDisclaimer < oneWeekAgo) {\n      alert(\n        _('disclaimerTitle'),\n        <div>\n          <p>{_('disclaimerText1')}</p>\n          <p>\n            {_('disclaimerText2')}{' '}\n            <a\n              href='https://vates.tech/deploy/?pk_campaign=xoa_source_upgrade&pk_kwd=ossmodal'\n              target='_blank'\n              rel='noreferrer'\n            >\n              XOA (turnkey appliance)\n            </a>\n          </p>\n          <p>{_('disclaimerText3')}</p>\n        </div>\n      )\n      cookies.set('previousDisclaimer', now)\n    }\n  }\n\n  dismissSourceBanner = () => {\n    cookies.set('dismissedSourceBanner', true, { expires: 1 }) // 1 day\n    this.setState({ dismissedSourceBanner: true })\n  }\n\n  dismissTrialBanner = () => {\n    cookies.set('dismissedTrialBanner', true, { expires: 1 })\n    this.setState({ dismissedTrialBanner: true })\n  }\n\n  componentDidMount() {\n    this.refs.bodyWrapper.style.minHeight = this.refs.menu.getWrappedInstance().height + 'px'\n    if (+process.env.XOA_PLAN === 5) {\n      this.displayOpenSourceDisclaimer()\n    }\n  }\n\n  componentDidUpdate(prev) {\n    if (prev.location.pathname !== this.props.location.pathname) {\n      Modal.close()\n    }\n  }\n\n  _shortcutsHandler = (command, event) => {\n    event.preventDefault()\n    switch (command) {\n      case 'GO_TO_HOSTS':\n        this.context.router.push('home?t=host')\n        break\n      case 'GO_TO_POOLS':\n        this.context.router.push('home?t=pool')\n        break\n      case 'GO_TO_VMS':\n        this.context.router.push('home?t=VM')\n        break\n      case 'GO_TO_SRS':\n        this.context.router.push('home?t=SR')\n        break\n      case 'CREATE_VM':\n        this.context.router.push('vms/new')\n        break\n      case 'UNFOCUS':\n        if (event.target.tagName === 'INPUT') {\n          event.target.blur()\n        }\n        break\n      case 'HELP':\n        alert(\n          <span>\n            <Icon icon='shortcuts' /> {_('shortcutModalTitle')}\n          </span>,\n          <Container>\n            {map(\n              help,\n              (context, contextKey) =>\n                context.name && [\n                  <Row className='mt-1' key={contextKey}>\n                    <Col>\n                      <h4>{context.name}</h4>\n                    </Col>\n                  </Row>,\n                  ...map(\n                    context.shortcuts,\n                    ({ message, keys }, key) =>\n                      message && (\n                        <Row key={`${contextKey}_${key}`}>\n                          <Col size={2} className='text-xs-right'>\n                            <strong>{Array.isArray(keys) ? keys[0] : keys}</strong>\n                          </Col>\n                          <Col size={10}>{message}</Col>\n                        </Row>\n                      )\n                  ),\n                ]\n            )}\n          </Container>\n        )\n        break\n    }\n  }\n\n  render() {\n    const { signedUp, trial, registerNeeded, xsa468VulnerableVms } = this.props\n    const { pathname } = this.context.router.location\n    const licenseNearExpiration = this.props.selfLicences && getLicenseNearExpiration(this.props.selfLicences, trial)\n    // If we are under expired or unstable trial (signed up only)\n    const blocked =\n      signedUp &&\n      (blockXoaAccess(trial) || licenseNearExpiration?.blocked === true) &&\n      !(pathname.startsWith('/xoa/') || pathname === '/backup/restore')\n    const plan = getXoaPlan()\n\n    return (\n      <IntlProvider>\n        <ThemeProvider theme={themes.base}>\n          <DocumentTitle title='Xen Orchestra'>\n            <div>\n              {plan !== 'Community' && registerNeeded && (\n                <div className='alert alert-danger mb-0'>\n                  {_('notRegisteredDisclaimerInfo')}{' '}\n                  <a href='https://xen-orchestra.com/#!/signup' rel='noopener noreferrer' target='_blank'>\n                    {_('notRegisteredDisclaimerCreateAccount')}\n                  </a>{' '}\n                  <Link to='/xoa/update'>{_('notRegisteredDisclaimerRegister')}</Link>\n                </div>\n              )}\n              {plan === 'Community' && !this.state.dismissedSourceBanner && (\n                <div className='alert alert-danger mb-0'>\n                  <a\n                    href='https://vates.tech/deploy/?pk_campaign=xo_source_banner'\n                    rel='noopener noreferrer'\n                    target='_blank'\n                  >\n                    {_('disclaimerText3')}\n                  </a>{' '}\n                  <a\n                    href='https://docs.xen-orchestra.com/installation#banner-and-warnings'\n                    rel='noopener noreferrer'\n                    target='_blank'\n                  >\n                    {_('disclaimerText4')}\n                  </a>\n                  <button className='close' onClick={this.dismissSourceBanner}>\n                    &times;\n                  </button>\n                </div>\n              )}\n              {isTrialRunning(trial.trial) && !this.state.dismissedTrialBanner && (\n                <div className='alert alert-info mb-0'>\n                  {_('trialLicenseInfo', {\n                    edition: getXoaPlan(productId2Plan[trial.trial.productId]),\n                    date: new Date(trial.trial.end),\n                  })}\n                  <button className='close' onClick={this.dismissTrialBanner}>\n                    &times;\n                  </button>\n                </div>\n              )}\n              {licenseNearExpiration && (\n                <div className={`alert alert-info mb-0 ${licenseNearExpiration.popupClass ?? 'alert-info'}`}>\n                  {_(licenseNearExpiration.strCode, {\n                    duration: licenseNearExpiration.textDuration,\n                    date: new Date(licenseNearExpiration.license.expires),\n                  })}\n                </div>\n              )}\n              {Object.keys(xsa468VulnerableVms).length > 0 && (\n                <div className='alert alert-danger mb-0'>\n                  IMPORTANT! Some of your VMs are vulnerable.{' '}\n                  <Link to='/home?s=vulnerable%3F'>Please check immediately.</Link>\n                </div>\n              )}\n              <div style={CONTAINER_STYLE}>\n                <Shortcuts\n                  name='XoApp'\n                  handler={this._shortcutsHandler}\n                  targetNodeSelector='body'\n                  stopPropagation={false}\n                />\n                <Menu ref='menu' />\n                <div ref='bodyWrapper' style={BODY_WRAPPER_STYLE}>\n                  <div style={BODY_STYLE}>\n                    {blocked ? <XoaUpdates /> : signedUp ? this.props.children : <p>Still loading</p>}\n                  </div>\n                </div>\n                <Modal />\n                <FormModal />\n                <Notification />\n                <TooltipViewer />\n              </div>\n            </div>\n          </DocumentTitle>\n        </ThemeProvider>\n      </IntlProvider>\n    )\n  }\n}\n","import New from '../new'\nimport React from 'react'\n\nexport default props => <New id={props.routeParams.id} />\n","import _ from 'intl'\nimport Icon from 'icon'\nimport Page from '../page'\nimport React from 'react'\nimport { Container, Row, Col } from 'grid'\nimport { NavLink, NavTabs } from 'nav'\nimport { adminOnly, routes } from 'utils'\n\nimport Edit from './edit'\nimport New from './new'\nimport Overview from './overview'\nimport Schedules from './schedules'\nimport EditSchedule from './schedules/edit'\n\nconst HEADER = (\n  <Container>\n    <Row>\n      <Col mediumSize={3}>\n        <h2>\n          <Icon icon='jobs' /> {_('jobsPage')}\n        </h2>\n      </Col>\n      <Col mediumSize={9}>\n        <NavTabs className='pull-right'>\n          <NavLink to='/jobs/overview'>\n            <Icon icon='menu-jobs-overview' /> {_('jobsOverviewPage')}\n          </NavLink>\n          <NavLink to='/jobs/new'>\n            <Icon icon='menu-jobs-new' /> {_('jobsNewPage')}\n          </NavLink>\n          <NavLink to='/jobs/schedules'>\n            <Icon icon='menu-jobs-schedule' /> {_('jobsSchedulingPage')}\n          </NavLink>\n        </NavTabs>\n      </Col>\n    </Row>\n  </Container>\n)\n\nconst Jobs = routes('overview', {\n  ':id/edit': Edit,\n  new: New,\n  overview: Overview,\n  schedules: Schedules,\n  'schedules/:id/edit': EditSchedule,\n})(\n  adminOnly(({ children }) => (\n    <Page header={HEADER} title='jobsPage' formatTitle>\n      {children}\n    </Page>\n  ))\n)\n\nexport default Jobs\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport Component from 'base-component'\nimport copy from 'copy-to-clipboard'\nimport defined from '@xen-orchestra/defined'\nimport GenericInput from 'json-schema-input'\nimport Icon from 'icon'\nimport React from 'react'\nimport Select from 'form/select'\nimport SortedTable from 'sorted-table'\nimport Tooltip from 'tooltip'\nimport Upgrade from 'xoa-upgrade'\nimport { addSubscriptions } from 'utils'\nimport { createSelector } from 'selectors'\nimport { error } from 'notification'\nimport { generateUiSchema } from 'xo-json-schema-input'\nimport { injectIntl } from 'react-intl'\nimport { SelectSubject } from 'select-objects'\nimport { delay, find, forEach, includes, isEmpty, mapValues, size } from 'lodash'\nimport {\n  apiMethods,\n  createJob,\n  deleteJob,\n  deleteJobs,\n  editJob,\n  runJob,\n  subscribeCurrentUser,\n  subscribeJobs,\n  subscribeUsers,\n} from 'xo'\n\nconst JOB_KEY = 'genericTask'\n\nconst COLUMNS = [\n  {\n    itemRenderer: (job, { isJobUserMissing }) => {\n      const { id } = job\n\n      return (\n        <div>\n          {job.name} <span className='text-muted'>({id.slice(4, 8)})</span>\n          {isJobUserMissing[id] && (\n            <Tooltip content={_('jobUserNotFound')}>\n              <Icon className='ml-1' icon='error' />\n            </Tooltip>\n          )}\n        </div>\n      )\n    },\n    name: _('jobName'),\n    sortCriteria: 'name',\n  },\n  {\n    itemRenderer: job => job.method,\n    name: _('jobAction'),\n    sortCriteria: 'method',\n  },\n]\n\nconst ACTIONS = [\n  {\n    handler: deleteJobs,\n    individualHandler: deleteJob,\n    individualLabel: _('jobDelete'),\n    icon: 'delete',\n    label: _('deleteSelectedJobs'),\n    level: 'danger',\n  },\n]\n\nconst getType = function (param) {\n  if (!param) {\n    return\n  }\n  if (Array.isArray(param.type)) {\n    if (includes(param.type, 'integer')) {\n      return 'integer'\n    }\n    if (includes(param.type, 'number')) {\n      return 'number'\n    }\n    return 'string'\n  }\n  return param.type\n}\n\n/**\n * Tries extracting Object targeted property\n */\nconst reduceObject = (value, propertyName = 'id') => (value != null && value[propertyName]) || value\n\n/**\n * Adapts all data \"arrayed\" by UI-multiple-selectors to job's cross-product trick\n */\nconst dataToParamVectorItems = function (params, data) {\n  const items = []\n  forEach(params, (param, name) => {\n    if (Array.isArray(data[name]) && param.items) {\n      // We have an array for building cross product, the \"real\" type was $type\n      const values = []\n      if (data[name].length === 1) {\n        // One value, no need to engage cross-product\n        data[name] = data[name].pop()\n      } else {\n        forEach(data[name], value => {\n          values.push({ [name]: reduceObject(value, name) })\n        })\n        if (values.length) {\n          items.push({\n            type: 'set',\n            values,\n          })\n        }\n        delete data[name]\n      }\n    }\n  })\n  if (size(data)) {\n    items.push({\n      type: 'set',\n      values: [mapValues(data, reduceObject)],\n    })\n  }\n  return items\n}\n\n@addSubscriptions({\n  users: subscribeUsers,\n  currentUser: subscribeCurrentUser,\n})\n@injectIntl\nexport default class Jobs extends Component {\n  constructor(props) {\n    super(props)\n\n    this.state = {\n      action: undefined,\n      actions: undefined,\n      job: undefined,\n      jobs: undefined,\n    }\n    new Promise((resolve, reject) => {\n      this._resolveLoaded = resolve\n    }).then(() => {\n      const { id } = this.props\n      if (id) {\n        this._edit(id)\n      }\n    })\n  }\n\n  componentWillMount() {\n    this.componentWillUnmount = subscribeJobs(jobs => {\n      const j = {}\n      for (const id in jobs) {\n        const job = jobs[id]\n        job && job.key === JOB_KEY && (j[id] = job)\n      }\n      this.setState({ jobs: j }, this._resolveLoaded)\n    })\n\n    const jobCompliantMethods = [\n      'acl.add',\n      'acl.remove',\n      'host.detach',\n      'host.disable',\n      'host.emergencyShutdownHost',\n      'host.enable',\n      'host.installAllPatches',\n      'host.restart',\n      'host.restartAgent',\n      'host.set',\n      'host.start',\n      'host.stop',\n      'job.runSequence',\n      'pool.rollingUpdate',\n      'pool.rollingReboot',\n      'vm.attachDisk',\n      'vm.clone',\n      'vm.convertToTemplate',\n      'vm.copy',\n      'vm.createInterface',\n      'vm.delete',\n      'vm.migrate',\n      'vm.migrate',\n      'vm.pause',\n      'vm.restart',\n      'vm.revert',\n      'vm.set',\n      'vm.setBootOrder',\n      'vm.snapshot',\n      'vm.start',\n      'vm.stop',\n      'vm.suspend',\n    ]\n    apiMethods.then(methods => {\n      const actions = []\n\n      for (const method in methods) {\n        if (includes(jobCompliantMethods, method)) {\n          const [group, command] = method.split('.')\n          const info = { ...methods[method] }\n          info.type = 'object'\n\n          const properties = { ...info.params }\n          delete info.params\n\n          const required = []\n          for (const key in properties) {\n            const property = { ...properties[key] }\n            const type = getType(property)\n\n            const modifyProperty = (prop, type) => {\n              const titles = {\n                Host: 'Host(s)',\n                Pool: 'Pool(s)',\n                Remote: 'Remote(s)',\n                Role: 'Role(s)',\n                Snapshot: 'Snapshot(s)',\n                Sr: 'Storage(s)',\n                Subject: 'Subject(s)',\n                Vdi: 'Vdi(s)',\n                Vm: 'VM(s)',\n                XoObject: 'Object(s)',\n              }\n              prop.type = 'array'\n              prop.items = {\n                type: 'string',\n                $type: type,\n              }\n              prop.title = titles[type]\n            }\n\n            if (type === 'string') {\n              if (group === 'acl') {\n                if (key === 'object') {\n                  modifyProperty(property, 'XoObject')\n                } else if (key === 'action') {\n                  modifyProperty(property, 'Role')\n                } else if (key === 'subject') {\n                  modifyProperty(property, 'Subject')\n                }\n              } else if (group === 'host' && key === 'id') {\n                modifyProperty(property, 'Host')\n              } else if (group === 'vm' && key === 'id') {\n                modifyProperty(property, 'Vm')\n              } else {\n                if (includes(['pool', 'pool_id', 'target_pool_id'], key)) {\n                  modifyProperty(property, 'Pool')\n                } else if (includes(['sr', 'sr_id', 'target_sr_id'], key)) {\n                  modifyProperty(property, 'Sr')\n                } else if (includes(['affinityHost', 'host', 'host_id', 'target_host_id', 'targetHost'], key)) {\n                  modifyProperty(property, 'Host')\n                } else if (includes(['vm'], key)) {\n                  modifyProperty(property, 'Vm')\n                } else if (includes(['snapshot'], key)) {\n                  modifyProperty(property, 'Snapshot')\n                } else if (includes(['remote', 'remoteId'], key)) {\n                  modifyProperty(property, 'Remote')\n                } else if (includes(['vdi'], key)) {\n                  modifyProperty(property, 'Vdi')\n                }\n              }\n            }\n            if (!property.optional) {\n              required.push(key)\n            }\n            properties[key] = property\n          }\n          !isEmpty(required) && (info.required = required)\n          info.properties = properties\n\n          actions.push({\n            method,\n            group,\n            command,\n            info,\n            uiSchema: generateUiSchema(info),\n          })\n        }\n      }\n\n      this.setState({ actions })\n    })\n  }\n\n  _handleSelectMethod = action => {\n    this.setState({ action })\n\n    // reset parameters\n    //\n    // see https://xcp-ng.org/forum/post/69299\n    const { params } = this.refs\n    // params is undefined if no method was previously selected\n    if (params !== undefined) {\n      this.refs.params.value = undefined\n    }\n  }\n\n  _handleSubmit = () => {\n    const { name, method, params } = this.refs\n\n    const { job, owner, timeout } = this.state\n    const _job = {\n      type: 'call',\n      name: name.value,\n      key: JOB_KEY,\n      method: method.value.method,\n      paramsVector: {\n        type: 'crossProduct',\n        items: dataToParamVectorItems(method.value.info.properties, params.value),\n      },\n      userId: owner !== undefined ? owner : this.props.currentUser.id,\n      timeout: timeout ? timeout * 1e3 : undefined,\n    }\n\n    job && (_job.id = job.id)\n    const saveJob = job ? editJob : createJob\n\n    return saveJob(_job)\n      .then(this._reset)\n      .catch(err => error('Create Job', err.message || String(err)))\n  }\n\n  _edit = job => {\n    if (typeof job === 'string') {\n      job = find(this.state.jobs, { id: job })\n    }\n\n    if (job === undefined) {\n      error('Job edition', 'This job was not found, or may not longer exists.')\n      return\n    }\n\n    const { name, method } = this.refs\n    const action = find(this.state.actions, { method: job.method })\n    name.value = job.name\n    method.value = action\n    this.setState(\n      {\n        job,\n        action,\n      },\n      () => delay(this._populateForm, 250, job)\n    ) // Work around.\n    // Without the delay, some selects are not always ready to load a value\n    // Values are displayed, but html5 compliant browsers say the value is required and empty on submit\n  }\n\n  _populateForm = job => {\n    const data = {}\n    const paramsVector = job.paramsVector\n    if (paramsVector) {\n      if (paramsVector.type !== 'crossProduct') {\n        throw new Error(`Unknown parameter-vector type ${paramsVector.type}`)\n      }\n      forEach(paramsVector.items, item => {\n        if (item.type !== 'set') {\n          throw new Error(`Unknown parameter-vector item type ${item.type}`)\n        }\n        forEach(item.values, valueItem => {\n          forEach(valueItem, (value, key) => {\n            if (data[key] === undefined) {\n              data[key] = value\n            } else if (Array.isArray(data[key])) {\n              data[key].push(value)\n            } else {\n              data[key] = [data[key], value]\n            }\n          })\n        })\n      })\n    }\n    const { params } = this.refs\n    params.value = data\n    this.setState({\n      owner: job.userId,\n      timeout: job.timeout && job.timeout / 1e3,\n    })\n  }\n\n  _reset = () => {\n    const { name, method } = this.refs\n    name.value = ''\n    method.value = undefined\n    this.setState({\n      action: undefined,\n      job: undefined,\n      owner: undefined,\n      timeout: '',\n    })\n  }\n\n  _getIsJobUserMissing = createSelector(\n    () => this.state.jobs,\n    () => this.props.users,\n    (jobs, users) => {\n      const isJobUserMissing = {}\n      forEach(jobs, job => {\n        isJobUserMissing[job.id] = !find(users, user => user.id === job.userId)\n      })\n\n      return isJobUserMissing\n    }\n  )\n\n  _subjectPredicate = ({ type, permission }) => type === 'user' && permission === 'admin'\n\n  _individualActions = [\n    {\n      disabled: (job, { isJobUserMissing }) => isJobUserMissing[job.id],\n      handler: runJob,\n      icon: 'run-schedule',\n      label: _('runJob'),\n      level: 'warning',\n    },\n    {\n      handler: this._edit,\n      icon: 'edit',\n      label: _('jobEdit'),\n      level: 'primary',\n    },\n    {\n      handler: ({ id }) => copy(id),\n      icon: 'clipboard',\n      label: _('copyToClipboard'),\n      level: 'secondary',\n    },\n  ]\n\n  render() {\n    const { props, state } = this\n    const { action, actions, job, jobs } = state\n    const { formatMessage } = this.props.intl\n\n    return (\n      <div>\n        <h1>{_('jobsPage')}</h1>\n        <form id='newJobForm' className='mb-2'>\n          <SelectSubject\n            onChange={this.linkState('owner', 'id')}\n            placeholder={_('jobOwnerPlaceholder')}\n            predicate={this._subjectPredicate}\n            required\n            value={defined(state.owner, () => props.currentUser.id)}\n          />\n          <input\n            type='text'\n            ref='name'\n            className='form-control mb-1 mt-1'\n            placeholder={formatMessage(messages.jobNamePlaceholder)}\n            pattern='[^_]+'\n            required\n          />\n          <Select\n            labelKey='method'\n            onChange={this._handleSelectMethod}\n            options={actions}\n            placeholder={_('jobActionPlaceHolder')}\n            ref='method'\n            valueKey='method'\n          />\n          <input\n            type='number'\n            onChange={this.linkState('timeout')}\n            value={state.timeout || ''}\n            className='form-control mb-1 mt-1'\n            placeholder={formatMessage(messages.jobTimeoutPlaceHolder)}\n          />\n          {action && (\n            <fieldset>\n              <GenericInput\n                ref='params'\n                schema={action.info}\n                uiSchema={action.uiSchema}\n                label={action.method}\n                required\n              />\n              {job && (\n                <p className='text-warning'>\n                  {_('jobEditMessage', {\n                    name: job.name,\n                    id: job.id.slice(4, 8),\n                  })}\n                </p>\n              )}\n              {process.env.XOA_PLAN > 3 ? (\n                <span>\n                  <ActionButton form='newJobForm' handler={this._handleSubmit} icon='save' btnStyle='primary'>\n                    {_('saveResourceSet')}\n                  </ActionButton>{' '}\n                  <Button onClick={this._reset}>{_('resetResourceSet')}</Button>\n                </span>\n              ) : (\n                <span>\n                  <Upgrade place='health' available={4} />\n                </span>\n              )}\n            </fieldset>\n          )}\n        </form>\n        {jobs !== undefined && (\n          <SortedTable\n            actions={ACTIONS}\n            collection={jobs}\n            columns={COLUMNS}\n            data-isJobUserMissing={this._getIsJobUserMissing()}\n            individualActions={this._individualActions}\n            shortcutsTarget='body'\n            stateUrlParam='s'\n          />\n        )}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport Icon from 'icon'\nimport Link from 'link'\nimport LogList from '../../logs'\nimport PropTypes from 'prop-types'\nimport React, { Component } from 'react'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport Tooltip from 'tooltip'\nimport Upgrade from 'xoa-upgrade'\nimport { addSubscriptions } from 'utils'\nimport { Container } from 'grid'\nimport { createSelector } from 'selectors'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { filter, forEach, keyBy } from 'lodash'\nimport {\n  deleteSchedule,\n  deleteSchedules,\n  disableSchedule,\n  enableSchedule,\n  runJob,\n  subscribeJobs,\n  subscribeSchedules,\n  subscribeUsers,\n} from 'xo'\n\n// ===================================================================\n\nconst jobKeyToLabel = {\n  genericTask: _('customJob'),\n}\n\nconst SCHEDULES_COLUMNS = [\n  {\n    itemRenderer: schedule => <span>{`${schedule.name} (${schedule.id.slice(4, 8)})`}</span>,\n    name: _('schedule'),\n    sortCriteria: 'name',\n  },\n  {\n    itemRenderer: (schedule, { jobs, isScheduleUserMissing }) => {\n      const jobId = schedule.jobId\n      const job = jobs[jobId]\n\n      return (\n        job !== undefined && (\n          <div>\n            <span>{`${job.name} - ${job.method} (${jobId.slice(4, 8)})`}</span>{' '}\n            {isScheduleUserMissing[schedule.id] && (\n              <Tooltip content={_('jobUserNotFound')}>\n                <Icon className='mr-1' icon='error' />\n              </Tooltip>\n            )}\n            <Link className='btn btn-sm btn-primary ml-1' to={`/jobs/${job.id}/edit`}>\n              <Tooltip content={_('jobEdit')}>\n                <Icon icon='edit' />\n              </Tooltip>\n            </Link>\n          </div>\n        )\n      )\n    },\n    name: _('job'),\n    sortCriteria: (schedule, { jobs }) => {\n      const job = jobs[schedule.jobId]\n      return job !== undefined && job.name\n    },\n  },\n  {\n    itemRenderer: schedule => schedule.cron,\n    name: _('jobScheduling'),\n  },\n  {\n    itemRenderer: schedule => (\n      <StateButton\n        disabledLabel={_('stateDisabled')}\n        disabledHandler={enableSchedule}\n        disabledTooltip={_('logIndicationToEnable')}\n        enabledLabel={_('stateEnabled')}\n        enabledHandler={disableSchedule}\n        enabledTooltip={_('logIndicationToDisable')}\n        handlerParam={schedule.id}\n        state={schedule.enabled}\n      />\n    ),\n    name: _('state'),\n  },\n]\n\nconst ACTIONS = [\n  {\n    handler: deleteSchedules,\n    icon: 'delete',\n    individualHandler: deleteSchedule,\n    individualLabel: _('scheduleDelete'),\n    label: _('deleteSelectedSchedules'),\n    level: 'danger',\n  },\n]\n\n// ===================================================================\n\n@addSubscriptions({\n  jobs: [cb => subscribeJobs(jobs => cb(keyBy(jobs, 'id'))), {}],\n  schedules: [subscribeSchedules, []],\n  userIds: [cb => subscribeUsers(users => cb(new Set(users.map(_ => _.id)))), new Set()],\n})\nexport default class Overview extends Component {\n  static contextTypes = {\n    router: PropTypes.object,\n  }\n\n  _getGenericSchedules = createSelector(\n    () => this.props.schedules,\n    () => this.props.jobs,\n\n    // Get only generic jobs\n    (schedules, jobs) =>\n      filter(schedules, schedule => {\n        const job = jobs[schedule.jobId]\n        return job !== undefined && job.key in jobKeyToLabel\n      })\n  )\n\n  _getIsScheduleUserMissing = createSelector(\n    this._getGenericSchedules,\n    () => this.props.userIds,\n    () => this.props.jobs,\n    (schedules, userIds, jobs) => {\n      const isScheduleUserMissing = {}\n\n      forEach(schedules, schedule => {\n        isScheduleUserMissing[schedule.id] = !userIds.has(jobs[schedule.jobId].userId)\n      })\n\n      return isScheduleUserMissing\n    }\n  )\n\n  _individualActions = [\n    {\n      disabled: (schedule, { isScheduleUserMissing }) => isScheduleUserMissing[schedule.id],\n      handler: schedule => runJob(schedule.jobId),\n      icon: 'run-schedule',\n      label: _('scheduleRun'),\n      level: 'warning',\n    },\n    {\n      handler: schedule =>\n        this.context.router.push({\n          pathname: `/jobs/schedules/${schedule.id}/edit`,\n        }),\n      icon: 'edit',\n      label: _('scheduleEdit'),\n      level: 'primary',\n    },\n  ]\n\n  render() {\n    return process.env.XOA_PLAN > 3 ? (\n      <Container>\n        <Card>\n          <CardHeader>\n            <Icon icon='schedule' /> {_('backupSchedules')}\n          </CardHeader>\n          <CardBlock>\n            <SortedTable\n              actions={ACTIONS}\n              collection={this._getGenericSchedules()}\n              columns={SCHEDULES_COLUMNS}\n              data-isScheduleUserMissing={this._getIsScheduleUserMissing()}\n              data-jobs={this.props.jobs}\n              individualActions={this._individualActions}\n              shortcutsTarget='body'\n              stateUrlParam='s'\n            />\n          </CardBlock>\n        </Card>\n        <LogList jobKeys={Object.keys(jobKeyToLabel)} />\n      </Container>\n    ) : (\n      <Container>\n        <Upgrade place='health' available={4} />\n      </Container>\n    )\n  }\n}\n","import Schedule from '..'\nimport React from 'react'\n\nexport default props => <Schedule id={props.routeParams.id} />\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport Component from 'base-component'\nimport find from 'lodash/find'\nimport isEmpty from 'lodash/isEmpty'\nimport moment from 'moment-timezone'\nimport SortedTable from 'sorted-table'\nimport Upgrade from 'xoa-upgrade'\nimport React from 'react'\nimport Scheduler, { SchedulePreview } from 'scheduling'\nimport { error } from 'notification'\nimport { injectIntl } from 'react-intl'\nimport { Select, Toggle } from 'form'\nimport { createSchedule, deleteSchedule, deleteSchedules, subscribeJobs, subscribeSchedules, editSchedule } from 'xo'\n\nconst JOB_KEY = 'genericTask'\nconst DEFAULT_CRON_PATTERN = '0 0 * * *'\nconst DEFAULT_TIMEZONE = moment.tz.guess()\n\nconst COLUMNS = [\n  {\n    itemRenderer: schedule => (\n      <span>\n        {schedule.name} <span className='text-muted'>({schedule.id.slice(4, 8)})</span>\n      </span>\n    ),\n    name: _('jobName'),\n    sortCriteria: 'name',\n    default: true,\n  },\n  {\n    itemRenderer: (schedule, userData) => {\n      const job = userData.jobs[schedule.jobId]\n      if (job !== undefined) {\n        return (\n          <span>\n            {job.name} - {job.method} <span className='text-muted'>({schedule.jobId.slice(4, 8)})</span>\n          </span>\n        )\n      }\n    },\n    name: _('job'),\n    sortCriteria: (schedule, userData) => userData.jobs[schedule.jobId].name,\n  },\n  {\n    itemRenderer: schedule => schedule.cron,\n    name: _('jobScheduling'),\n  },\n  {\n    itemRenderer: schedule => schedule.timezone || _('jobServerTimezone'),\n    name: _('jobTimezone'),\n    sortCriteria: 'timezone',\n  },\n]\nconst GROUPED_ACTIONS = [\n  {\n    handler: deleteSchedules,\n    icon: 'delete',\n    label: _('deleteSelectedSchedules'),\n    level: 'danger',\n  },\n]\n\n@injectIntl\nexport default class Schedules extends Component {\n  constructor(props) {\n    super(props)\n    this.state = {\n      action: undefined,\n      actions: undefined,\n      cronPattern: DEFAULT_CRON_PATTERN,\n      enabled: true,\n      job: undefined,\n      jobs: undefined,\n      jobsList: [],\n      timezone: DEFAULT_TIMEZONE,\n    }\n    this.loaded = new Promise((resolve, reject) => {\n      this._resolveLoaded = resolve\n    }).then(() => {\n      const { id } = this.props\n      if (id) {\n        this._edit(id)\n      }\n    })\n  }\n\n  componentWillMount() {\n    const unsubscribeJobs = subscribeJobs(jobs => {\n      const j = {}\n      for (const id in jobs) {\n        const job = jobs[id]\n        if (job && job.key === JOB_KEY) {\n          const _job = { ...job }\n          _job.label = `${_job.name} - ${_job.method} (${_job.id})`\n          j[job.id] = _job\n        }\n      }\n      this.setState({ jobs: j, jobsList: Object.values(jobs).sort(({ label: a }, { label: b }) => (a < b ? 1 : -1)) })\n    })\n\n    const unsubscribeSchedules = subscribeSchedules(schedules => {\n      const s = {}\n      const { jobs } = this.state\n      if (isEmpty(jobs)) {\n        return\n      }\n      for (const id in schedules) {\n        const schedule = schedules[id]\n        const scheduleJob = find(jobs, job => job.id === schedule.jobId)\n        if (scheduleJob && scheduleJob.key === JOB_KEY) {\n          s[id] = schedule\n        }\n      }\n      this.setState({ schedules: s }, this._resolveLoaded)\n    })\n\n    this.componentWillUnmount = () => {\n      unsubscribeJobs()\n      unsubscribeSchedules()\n    }\n  }\n\n  _handleSubmit = () => {\n    const { name, job } = this.refs\n    const { cronPattern, enabled, schedule, timezone } = this.state\n    let save\n    if (schedule) {\n      schedule.jobId = job.value.id\n      schedule.cron = cronPattern\n      schedule.enabled = enabled\n      schedule.name = name.value\n      schedule.timezone = timezone\n      save = editSchedule(schedule)\n    } else {\n      save = createSchedule(job.value.id, {\n        cron: cronPattern,\n        enabled,\n        name: name.value,\n        timezone,\n      })\n    }\n    return save.then(this._reset).catch(err => error('Save Schedule', err.message || String(err)))\n  }\n\n  _edit = id => {\n    const { schedules, jobs } = this.state\n    const schedule = find(schedules, schedule => schedule.id === id)\n    if (!schedule) {\n      error('Schedule edition', 'This schedule was not found, or may not longer exists.')\n      return\n    }\n\n    const { name, job } = this.refs\n    name.value = schedule.name\n    job.value = jobs[schedule.jobId]\n    this.setState({\n      cronPattern: schedule.cron,\n      enabled: schedule.enabled,\n      schedule,\n      timezone: schedule.timezone,\n    })\n  }\n\n  _reset = () => {\n    this.setState(\n      {\n        cronPattern: DEFAULT_CRON_PATTERN,\n        enabled: true,\n        schedule: undefined,\n        timezone: DEFAULT_TIMEZONE,\n      },\n      () => {\n        const { name, job } = this.refs\n        name.value = ''\n        job.value = undefined\n      }\n    )\n  }\n\n  _updateCronPattern = value => {\n    this.setState(value)\n  }\n\n  individualActions = [\n    {\n      handler: job => this._edit(job.id),\n      icon: 'edit',\n      label: _('scheduleEdit'),\n      level: 'primary',\n    },\n    {\n      handler: deleteSchedule,\n      icon: 'delete',\n      label: _('scheduleDelete'),\n      level: 'danger',\n    },\n  ]\n\n  render() {\n    const { cronPattern, enabled, jobs, jobsList, schedule, schedules, timezone } = this.state\n    const userData = { jobs }\n    return (\n      <div>\n        <h2>{_('newSchedule')}</h2>\n        <form id='newScheduleForm'>\n          <div className='form-group'>\n            <input\n              type='text'\n              ref='name'\n              className='form-control'\n              placeholder={this.props.intl.formatMessage(messages.jobScheduleNamePlaceHolder)}\n              required\n            />\n          </div>\n          <div className='form-group'>\n            <Select\n              labelKey='name'\n              ref='job'\n              options={jobsList}\n              valueKey='id'\n              placeholder={this.props.intl.formatMessage(messages.jobScheduleJobPlaceHolder)}\n            />\n          </div>\n          <div className='form-group'>\n            <Toggle onChange={this.toggleState('enabled')} value={enabled} />{' '}\n            <label>{_('scheduleEnableAfterCreation')}</label>\n          </div>\n        </form>\n        <fieldset>\n          <Scheduler cronPattern={cronPattern} onChange={this._updateCronPattern} timezone={timezone} />\n          <SchedulePreview cronPattern={cronPattern} timezone={timezone} />\n        </fieldset>\n        <br />\n        <div className='form-group'>\n          {schedule && (\n            <p className='text-warning'>\n              {_('scheduleEditMessage', {\n                name: schedule.name,\n                id: schedule.id,\n              })}\n            </p>\n          )}\n          {process.env.XOA_PLAN > 3 ? (\n            <span>\n              <ActionButton form='newScheduleForm' handler={this._handleSubmit} icon='save' btnStyle='primary'>\n                {_('saveBackupJob')}\n              </ActionButton>{' '}\n              <Button onClick={this._reset}>{_('selectTableReset')}</Button>\n            </span>\n          ) : (\n            <span>\n              <Upgrade place='health' available={4} />\n            </span>\n          )}\n        </div>\n        {schedules !== undefined && (\n          <div>\n            <h2>{_('jobSchedules')}</h2>\n            <SortedTable\n              collection={schedules}\n              columns={COLUMNS}\n              groupedActions={GROUPED_ACTIONS}\n              individualActions={this.individualActions}\n              userData={userData}\n              stateUrlParam='s'\n            />\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n","import * as CM from 'complex-matcher'\nimport _, { FormattedDuration } from 'intl'\nimport ActionButton from 'action-button'\nimport addSubscriptions from 'add-subscriptions'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport NoObjects from 'no-objects'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport Tooltip from 'tooltip'\nimport { alert } from 'modal'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { connectStore, formatSize, NumericDate } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\nimport { get } from '@xen-orchestra/defined'\nimport { injectState, provideState } from 'reaclette'\nimport { isEmpty, filter, map, keyBy } from 'lodash'\nimport { withRouter } from 'react-router'\nimport {\n  subscribeBackupNgJobs,\n  subscribeBackupNgLogs,\n  subscribeMetadataBackupJobs,\n  subscribeMirrorBackupJobs,\n} from 'xo'\n\nimport LogAlertBody from './log-alert-body'\nimport LogAlertHeader from './log-alert-header'\n\nimport { STATUS_LABELS, LOG_FILTERS } from '../utils'\n\nconst UL_STYLE = { listStyleType: 'none' }\n\nconst LI_STYLE = {\n  whiteSpace: 'nowrap',\n}\n\nconst showTasks = id => alert(<LogAlertHeader id={id} />, <LogAlertBody id={id} />)\n\nexport const LogStatus = ({ log, tooltip = _('logDisplayDetails') }) => {\n  const { className, label } = STATUS_LABELS[log.status]\n  return (\n    <ActionButton\n      btnStyle={className}\n      handler={showTasks}\n      handlerParam={log.id}\n      icon='preview'\n      size='small'\n      tooltip={tooltip}\n    >\n      {_(label)}\n    </ActionButton>\n  )\n}\n\nconst CURSOR_POINTER_STYLE = { cursor: 'pointer' }\nconst GoToJob = decorate([\n  withRouter,\n  provideState({\n    effects: {\n      goTo() {\n        const { jobId, location, router, scrollIntoJobs } = this.props\n        router.replace({\n          ...location,\n          query: { ...location.query, s: new CM.Property('id', new CM.String(jobId)).toString() },\n        })\n        scrollIntoJobs()\n      },\n    },\n  }),\n  injectState,\n  ({ effects, children }) => (\n    <Tooltip content={_('goToThisJob')}>\n      <p onClick={effects.goTo} style={CURSOR_POINTER_STYLE}>\n        {children}\n      </p>\n    </Tooltip>\n  ),\n])\n\nGoToJob.propTypes = {\n  jobId: PropTypes.string.isRequired,\n  scrollIntoJobs: PropTypes.func.isRequired,\n}\n\nconst COLUMNS = [\n  {\n    name: _('jobId'),\n    itemRenderer: (log, { scrollIntoJobs }) => (\n      <GoToJob jobId={log.jobId} scrollIntoJobs={scrollIntoJobs}>\n        {log.jobId.slice(4, 8)}\n      </GoToJob>\n    ),\n    sortCriteria: log => log.jobId,\n  },\n  {\n    name: _('jobName'),\n    itemRenderer: (log, { jobs }) => get(() => jobs[log.jobId].name),\n    sortCriteria: (log, { jobs }) => get(() => jobs[log.jobId].name),\n  },\n  {\n    name: _('jobStart'),\n    itemRenderer: log => <NumericDate timestamp={log.start} />,\n    sortCriteria: 'start',\n    sortOrder: 'desc',\n  },\n  {\n    default: true,\n    name: _('jobEnd'),\n    itemRenderer: log => log.end !== undefined && <NumericDate timestamp={log.end} />,\n    sortCriteria: log => log.end || log.start,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('jobDuration'),\n    itemRenderer: log => log.end !== undefined && <FormattedDuration duration={log.end - log.start} />,\n    sortCriteria: log => log.end - log.start,\n  },\n  {\n    name: _('jobStatus'),\n    itemRenderer: log => <LogStatus log={log} />,\n    sortCriteria: 'status',\n  },\n  {\n    name: _('labelSize'),\n    itemRenderer: ({ tasks: vmTasks, jobId }, { jobs }) => {\n      if (!['backup', 'mirrorBackup'].includes(get(() => jobs[jobId].type)) || isEmpty(vmTasks)) {\n        return null\n      }\n\n      let transferSize = 0\n      let mergeSize = 0\n      vmTasks.forEach(({ tasks: targetSnapshotTasks = [] }) => {\n        let vmTransferSize\n        let vmMergeSize\n        targetSnapshotTasks.forEach(({ message, tasks: operationTasks }) => {\n          if (message !== 'export' || isEmpty(operationTasks)) {\n            return\n          }\n          operationTasks.forEach(operationTask => {\n            if (operationTask.status !== 'success') {\n              return\n            }\n            if (operationTask.message === 'transfer' && vmTransferSize === undefined) {\n              vmTransferSize = operationTask.result?.size\n            }\n            if (operationTask.message === 'merge' && vmMergeSize === undefined) {\n              vmMergeSize = operationTask.result?.size\n            }\n\n            if (vmTransferSize !== undefined && vmMergeSize !== undefined) {\n              return false\n            }\n          })\n        })\n        vmTransferSize !== undefined && (transferSize += vmTransferSize)\n        vmMergeSize !== undefined && (mergeSize += vmMergeSize)\n      })\n      return (\n        <ul style={UL_STYLE}>\n          {transferSize > 0 && <li style={LI_STYLE}>{_.keyValue(_('labelTransfer'), formatSize(transferSize))}</li>}\n          {mergeSize > 0 && <li style={LI_STYLE}>{_.keyValue(_('labelMerge'), formatSize(mergeSize))}</li>}\n        </ul>\n      )\n    },\n  },\n]\n\nexport default decorate([\n  connectStore({\n    vms: createGetObjectsOfType('VM'),\n  }),\n  addSubscriptions({\n    logs: cb => subscribeBackupNgLogs(logs => cb(logs && filter(logs, log => log.message === 'backup'))),\n    jobs: cb => subscribeBackupNgJobs(jobs => cb(keyBy(jobs, 'id'))),\n    metadataJobs: cb => subscribeMetadataBackupJobs(jobs => cb(keyBy(jobs, 'id'))),\n    mirrorBackupJobs: cb => subscribeMirrorBackupJobs(jobs => cb(keyBy(jobs, 'id'))),\n  }),\n  provideState({\n    computed: {\n      logs: (_, { logs, vms }) =>\n        logs &&\n        logs.map(log =>\n          log.tasks !== undefined\n            ? {\n                ...log,\n                // \"vmNames\" can contains undefined entries\n                vmNames: map(log.tasks, ({ data }) => get(() => vms[data.id].name_label)),\n              }\n            : log\n        ),\n      jobs: (_, { jobs, metadataJobs, mirrorBackupJobs }) => ({ ...jobs, ...metadataJobs, ...mirrorBackupJobs }),\n    },\n  }),\n  injectState,\n  ({ state, scrollIntoJobs, jobs }) => (\n    <Card>\n      <CardHeader>\n        <Icon icon='logs' /> {_('logTitle')}\n      </CardHeader>\n      <CardBlock>\n        <NoObjects\n          collection={state.logs}\n          columns={COLUMNS}\n          component={SortedTable}\n          data-jobs={state.jobs}\n          data-scrollIntoJobs={scrollIntoJobs}\n          emptyMessage={_('noLogs')}\n          filters={LOG_FILTERS}\n          stateUrlParam='s_logs'\n        />\n      </CardBlock>\n    </Card>\n  ),\n])\n","import _, { FormattedDuration } from 'intl'\nimport * as CM from 'complex-matcher'\nimport ActionButton from 'action-button'\nimport ButtonGroup from 'button-group'\nimport decorate from 'apply-decorators'\nimport defined, { get } from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport Pagination from 'pagination'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SearchBar from 'search-bar'\nimport Select from 'form/select'\nimport TASK_STATUS from 'task-status'\nimport Tooltip from 'tooltip'\nimport { addSubscriptions, connectStore, formatSize, formatSpeed, NumericDate } from 'utils'\nimport { countBy, cloneDeep, filter, map } from 'lodash'\nimport { createGetObjectsOfType } from 'selectors'\nimport { injectState, provideState } from 'reaclette'\nimport { runBackupNgJob, subscribeBackupNgLogs } from 'xo'\nimport { Vm, Sr, Remote, Pool } from 'render-xo-item'\nimport BaseComponent from 'base-component'\n\nconst hasTaskFailed = ({ status }) => status !== 'success' && status !== 'pending'\n\nconst TaskStateInfos = ({ status }) => {\n  const { icon, label } = TASK_STATUS[status]\n  return (\n    <Tooltip content={_(label)}>\n      <Icon icon={icon} />\n    </Tooltip>\n  )\n}\n\nconst TaskDate = ({ value }) => <NumericDate timestamp={value} />\n\nconst TaskStart = ({ task }) => <div>{_.keyValue(_('taskStart'), <TaskDate value={task.start} />)}</div>\nconst TaskEnd = ({ task }) =>\n  task.end !== undefined ? <div>{_.keyValue(_('taskEnd'), <TaskDate value={task.end} />)}</div> : null\nconst TaskDuration = ({ task }) =>\n  task.end !== undefined ? (\n    <div>{_.keyValue(_('taskDuration'), <FormattedDuration duration={task.end - task.start} />)}</div>\n  ) : null\n\nconst UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'\nconst UNHEALTHY_VDI_CHAIN_LINK = 'https://docs.xen-orchestra.com/backup_troubleshooting#vdi-chain-protection'\n\nconst TaskError = ({ task }) => {\n  let message\n  if (\n    !hasTaskFailed(task) ||\n    (message = defined(\n      () => task.result.message,\n      () => task.result.code\n    )) === undefined\n  ) {\n    return null\n  }\n\n  if (message === UNHEALTHY_VDI_CHAIN_ERROR) {\n    return (\n      <div>\n        <Tooltip content={_('clickForMoreInformation')}>\n          <a className='text-info' href={UNHEALTHY_VDI_CHAIN_LINK} rel='noopener noreferrer' target='_blank'>\n            <Icon icon='info' /> {_('unhealthyVdiChainError')}\n          </a>\n        </Tooltip>\n      </div>\n    )\n  }\n\n  const [label, className] =\n    task.status === 'skipped' ? [_('taskReason'), 'text-info'] : [_('taskError'), 'text-danger']\n\n  return (\n    <div>\n      {_.keyValue(label, <span className={className}>{message}</span>)}\n      {task.result.name === 'XapiError' && <span className='d-block'>{_('logXapiError')}</span>}\n    </div>\n  )\n}\n\nclass TaskWarning extends React.Component {\n  constructor(props) {\n    super(props)\n    this.state = {\n      expanded: false,\n    }\n  }\n\n  render() {\n    const className = `text-warning ${this.props.data ? 'message-expandable' : ''}`\n\n    return (\n      <div>\n        <span className={className} onClick={() => this.setState({ expanded: !this.state.expanded })}>\n          <Icon icon='alarm' /> {this.props.message}\n        </span>\n        {this.state.expanded && this.props.data && (\n          <ul className='task-warning'>\n            {Object.keys(this.props.data).map(key => (\n              <li key={key}>\n                <strong>{key}</strong>\n                <span>{JSON.stringify(this.props.data[key])}</span>\n              </li>\n            ))}\n          </ul>\n        )}\n      </div>\n    )\n  }\n}\n\nTaskWarning.propTypes = {\n  message: PropTypes.string.isRequired,\n  data: PropTypes.object,\n}\n\nconst TaskWarnings = ({ warnings }) =>\n  warnings !== undefined ? (\n    <div>\n      {warnings.map(({ message, data }, key) => (\n        <TaskWarning message={message} data={data} key={key} />\n      ))}\n    </div>\n  ) : null\n\nTaskWarnings.propTypes = {\n  warnings: PropTypes.arrayOf(PropTypes.shape(TaskWarning.propTypes)),\n}\n\nclass TaskInfo extends BaseComponent {\n  constructor(props) {\n    super(props)\n    this.state = {\n      expanded: false,\n    }\n  }\n  render() {\n    const className = `text-info ${this.props.data ? 'message-expandable' : ''}`\n    return (\n      <div>\n        <span className={className} onClick={this.toggleState('expanded')}>\n          <Icon icon='info' /> {this.props.message}\n        </span>\n        {this.state.expanded && this.props.data && (\n          <ul className='task-info'>\n            {Object.keys(this.props.data).map(key => (\n              <li key={key}>\n                <strong>{key}</strong>\n                <span>{JSON.stringify(this.props.data[key])}</span>\n              </li>\n            ))}\n          </ul>\n        )}\n      </div>\n    )\n  }\n}\n\nTaskInfo.propTypes = {\n  message: PropTypes.string.isRequired,\n  data: PropTypes.object,\n}\n\nconst TaskInfos = ({ infos }) =>\n  infos !== undefined ? (\n    <div>\n      {infos.map(({ message, data }, key) => (\n        <TaskInfo message={message} data={data} key={key} />\n      ))}\n    </div>\n  ) : null\n\nTaskInfos.propTypes = {\n  infos: PropTypes.arrayOf(PropTypes.shape(TaskInfo.propTypes)),\n}\n\nconst VmTask = ({ children, className, restartVmJob, task }) => (\n  <li className={className}>\n    <Vm id={task.data.id} name={task.data.name_label} link newTab /> <TaskStateInfos status={task.status} />{' '}\n    {restartVmJob !== undefined && hasTaskFailed(task) && (\n      <ButtonGroup>\n        <ActionButton\n          data-vm={task.data.id}\n          handler={restartVmJob}\n          icon='run'\n          size='small'\n          tooltip={_('backupRestartVm')}\n        />\n        <ActionButton\n          btnStyle='warning'\n          data-force\n          data-vm={task.data.id}\n          handler={restartVmJob}\n          icon='force-restart'\n          size='small'\n          tooltip={_('backupForceRestartVm')}\n        />\n      </ButtonGroup>\n    )}\n    <TaskWarnings warnings={task.warnings} />\n    <TaskInfos infos={task.infos} />\n    {children}\n    <TaskStart task={task} />\n    <TaskEnd task={task} />\n    <TaskDuration task={task} />\n    <TaskError task={task} />\n    {task.transfer !== undefined && (\n      <div>\n        {_.keyValue(_('taskTransferredDataSize'), formatSize(task.transfer.size))}\n        <br />\n        {_.keyValue(_('taskTransferredDataSpeed'), formatSpeed(task.transfer.size, task.transfer.duration))}\n      </div>\n    )}\n    {task.merge !== undefined && (\n      <div>\n        {_.keyValue(_('taskMergedDataSize'), formatSize(task.merge.size))}\n        <br />\n        {_.keyValue(_('taskMergedDataSpeed'), formatSpeed(task.merge.size, task.merge.duration))}\n      </div>\n    )}\n    {task.isFull !== undefined && _.keyValue(_('exportType'), task.isFull ? 'full' : 'delta')}\n  </li>\n)\n\nconst PoolTask = ({ children, className, task }) => (\n  <li className={className}>\n    <Pool id={task.data.id} link newTab /> <TaskStateInfos status={task.status} />\n    <TaskWarnings warnings={task.warnings} />\n    <TaskInfos infos={task.infos} />\n    {children}\n    <TaskStart task={task} />\n    <TaskEnd task={task} />\n    <TaskDuration task={task} />\n    <TaskError task={task} />\n  </li>\n)\n\nconst XoTask = ({ children, className, task }) => (\n  <li className={className}>\n    <Icon icon='menu-xoa' /> XO <TaskStateInfos status={task.status} />\n    <TaskWarnings warnings={task.warnings} />\n    <TaskInfos infos={task.infos} />\n    {children}\n    <TaskStart task={task} />\n    <TaskEnd task={task} />\n    <TaskDuration task={task} />\n    <TaskError task={task} />\n  </li>\n)\n\nconst SnapshotTask = ({ className, task }) => (\n  <li className={className}>\n    <Icon icon='task' /> {_('snapshotVmLabel')} <TaskStateInfos status={task.status} />\n    <TaskWarnings warnings={task.warnings} />\n    <TaskInfos infos={task.infos} />\n    <TaskStart task={task} />\n    <TaskEnd task={task} />\n    <TaskError task={task} />\n  </li>\n)\n\nconst CleanVmTask = ({ children, className, task }) =>\n  task.warnings?.length > 0 ? (\n    <li className={className}>\n      <Icon icon='clean-vm' /> {_('cleanVm')} <TaskStateInfos status={task.status} />\n      <TaskWarnings warnings={task.warnings} />\n      <TaskInfos infos={task.infos} />\n      {children}\n      <TaskStart task={task} />\n      <TaskEnd task={task} />\n      <TaskError task={task} />\n    </li>\n  ) : null\nconst HealthCheckTask = ({ children, className, task }) => (\n  <li className={className}>\n    <Icon icon='health' /> {task.message} <TaskStateInfos status={task.status} />{' '}\n    <TaskWarnings warnings={task.warnings} />\n    <TaskInfos infos={task.infos} />\n    {children}\n    <TaskStart task={task} />\n    <TaskEnd task={task} />\n    <TaskError task={task} />\n  </li>\n)\nconst HealthCheckVmStartTask = ({ children, className, task }) => (\n  <li className={className}>\n    <Icon icon='run' /> {task.message} <TaskStateInfos status={task.status} />\n    <TaskInfos infos={task.infos} />\n    <TaskStart task={task} />\n    <TaskEnd task={task} />\n    <TaskError task={task} />\n  </li>\n)\n\nconst RemoteTask = ({ children, className, task }) => (\n  <li className={className}>\n    <Remote id={task.data.id} link newTab /> <TaskStateInfos status={task.status} />\n    <TaskWarnings warnings={task.warnings} />\n    {children}\n    <TaskStart task={task} />\n    <TaskEnd task={task} />\n    <TaskDuration task={task} />\n    <TaskError task={task} />\n  </li>\n)\n\nconst SrTask = ({ children, className, task }) => (\n  <li className={className}>\n    <Sr id={task.data.id} name={task.data.name_label} link newTab /> <TaskStateInfos status={task.status} />\n    <TaskWarnings warnings={task.warnings} />\n    <TaskInfos infos={task.infos} />\n    {children}\n    <TaskStart task={task} />\n    <TaskEnd task={task} />\n    <TaskDuration task={task} />\n    <TaskError task={task} />\n  </li>\n)\n\nconst TransferMergeTask = ({ className, task }) => {\n  const size = defined(() => task.result.size, 0)\n  if (task.status === 'success' && size === 0 && task.warnings?.length === 0) {\n    return null\n  }\n\n  return (\n    <li className={className}>\n      {task.message === 'transfer' ? (\n        task.parent?.message === 'health check' ? (\n          <Icon icon='download' />\n        ) : (\n          <Icon icon='upload' />\n        )\n      ) : (\n        <Icon icon='task' />\n      )}{' '}\n      {task.message}\n      <TaskStateInfos status={task.status} />\n      <TaskWarnings warnings={task.warnings} />\n      <TaskInfos infos={task.infos} />\n      <TaskStart task={task} />\n      <TaskEnd task={task} />\n      <TaskDuration task={task} />\n      <TaskError task={task} />\n      {size > 0 && (\n        <div>\n          {_.keyValue(_('operationSize'), formatSize(size))}\n          <br />\n          {_.keyValue(_('operationSpeed'), formatSpeed(size, task.end - task.start))}\n        </div>\n      )}\n    </li>\n  )\n}\n\nconst CloningVmTask = ({ className, task }) => {\n  return (\n    <li className={className}>\n      Cloning Vm\n      <TaskStateInfos status={task.status} />\n      <TaskWarnings warnings={task.warnings} />\n      <TaskInfos infos={task.infos} />\n      <TaskStart task={task} />\n      <TaskEnd task={task} />\n      <TaskDuration task={task} />\n      <TaskError task={task} />\n    </li>\n  )\n}\n\nconst CopyingVmTask = ({ className, task }) => {\n  return (\n    <li className={className}>\n      Copying Vm\n      <TaskStateInfos status={task.status} />\n      <TaskWarnings warnings={task.warnings} />\n      <TaskInfos infos={task.infos} />\n      <TaskStart task={task} />\n      <TaskEnd task={task} />\n      <TaskDuration task={task} />\n      <TaskError task={task} />\n    </li>\n  )\n}\n\nconst COMPONENT_BY_TYPE = {\n  vm: VmTask,\n  remote: RemoteTask,\n  sr: SrTask,\n  pool: PoolTask,\n  xo: XoTask,\n}\n\nconst COMPONENT_BY_MESSAGE = {\n  snapshot: SnapshotTask,\n  merge: TransferMergeTask,\n  transfer: TransferMergeTask,\n  'health check': HealthCheckTask,\n  vmstart: HealthCheckVmStartTask,\n  'clean-vm': CleanVmTask,\n  'cloning-vm': CloningVmTask,\n  'copying-vm': CopyingVmTask,\n}\n\nconst TaskLi = ({ task, ...props }) => {\n  let Component\n  if (\n    (Component = defined(\n      () => COMPONENT_BY_TYPE[task.data.type.toLowerCase()],\n\n      // work-around to not let defined handle the component as a safety function\n      () => COMPONENT_BY_MESSAGE[task.message]\n    )) === undefined\n  ) {\n    return null\n  }\n  return <Component task={task} {...props} />\n}\n\nconst SEARCH_BAR_FILTERS = { name: 'name:' }\n\nconst ITEMS_PER_PAGE = 5\nexport default decorate([\n  addSubscriptions(({ id }) => ({\n    log: cb =>\n      subscribeBackupNgLogs(logs => {\n        const linkParent = parent => {\n          const { tasks } = parent\n          if (tasks !== undefined) {\n            for (const task of tasks) {\n              // parent should not be enumerable as it would create a cycle and break JSON.stringify\n              Object.defineProperty(task, parent, { value: parent })\n              linkParent(task)\n            }\n          }\n        }\n        const log = logs[id]\n        if (log !== undefined) {\n          linkParent(log)\n        }\n        cb(log)\n      }),\n  })),\n  connectStore({\n    pools: createGetObjectsOfType('pool'),\n    vms: createGetObjectsOfType('VM'),\n  }),\n  provideState({\n    initialState: () => ({\n      _status: undefined,\n      filter: '',\n      page: 1,\n    }),\n    effects: {\n      onPageChange(_, page) {\n        this.state.page = page\n      },\n      onFilterChange(_, filter) {\n        this.state.filter = filter\n        this.state.page = 1\n      },\n      onStatusChange(_, status) {\n        this.state._status = status\n        this.state.page = 1\n      },\n      restartVmJob:\n        (_, params) =>\n        async (_, { log: { scheduleId, jobId } }) => {\n          await runBackupNgJob({\n            force: get(() => params.force),\n            id: jobId,\n            schedule: scheduleId,\n            vm: get(() => params.vm),\n          })\n        },\n    },\n    computed: {\n      log: (_, { log, pools, vms }) => {\n        if (log === undefined) {\n          return {}\n        }\n\n        if (log.tasks === undefined) {\n          return log\n        }\n\n        const newLog = cloneDeep(log)\n        newLog.tasks.forEach(task => {\n          const type = get(() => task.data.type)\n          if (type !== 'VM' && type !== 'xo' && type !== 'pool') {\n            return\n          }\n\n          task.name =\n            type === 'VM'\n              ? get(() => vms[task.data.id].name_label)\n              : type === 'pool'\n                ? get(() => pools[task.data.id].name_label)\n                : 'xo'\n\n          if (task.tasks !== undefined) {\n            const subTaskWithIsFull = task.tasks.find(({ data = {} }) => data.isFull !== undefined)\n            task.isFull = get(() => subTaskWithIsFull.data.isFull)\n          }\n        })\n\n        return newLog\n      },\n      preFilteredTasksLogs: ({ log, filter }) => {\n        try {\n          return log.tasks.filter(CM.parse(filter).createPredicate())\n        } catch (_) {\n          return []\n        }\n      },\n      tasksFilteredByStatus: ({ preFilteredTasksLogs, status }) =>\n        status === 'all' ? preFilteredTasksLogs : filter(preFilteredTasksLogs, task => task.status === status),\n      displayedTasks: ({ tasksFilteredByStatus, page }) => {\n        const start = (page - 1) * ITEMS_PER_PAGE\n        return tasksFilteredByStatus.slice(start, start + ITEMS_PER_PAGE)\n      },\n      optionRenderer:\n        ({ countByStatus }) =>\n        ({ label, value }) => (\n          <span>\n            {_(label)} ({countByStatus[value] || 0})\n          </span>\n        ),\n      countByStatus: ({ preFilteredTasksLogs }) => ({\n        all: get(() => preFilteredTasksLogs.length),\n        ...countBy(preFilteredTasksLogs, 'status'),\n      }),\n      options: ({ countByStatus }) => [\n        { label: 'allTasks', value: 'all' },\n        {\n          disabled: countByStatus.failure === undefined,\n          label: 'taskFailed',\n          value: 'failure',\n        },\n        {\n          disabled: countByStatus.pending === undefined,\n          label: 'taskStarted',\n          value: 'pending',\n        },\n        {\n          disabled: countByStatus.interrupted === undefined,\n          label: 'taskInterrupted',\n          value: 'interrupted',\n        },\n        {\n          disabled: countByStatus.skipped === undefined,\n          label: 'taskSkipped',\n          value: 'skipped',\n        },\n        {\n          disabled: countByStatus.success === undefined,\n          label: 'taskSuccess',\n          value: 'success',\n        },\n      ],\n      status: ({ _status, countByStatus }) =>\n        defined(_status, () => {\n          if (countByStatus.pending > 0) {\n            return 'pending'\n          }\n\n          if (countByStatus.failure > 0) {\n            return 'failure'\n          }\n\n          if (countByStatus.interrupted > 0) {\n            return 'interrupted'\n          }\n\n          return 'all'\n        }),\n      nPages: ({ tasksFilteredByStatus }) => Math.ceil(tasksFilteredByStatus.length / ITEMS_PER_PAGE),\n    },\n  }),\n  injectState,\n  ({ state, effects }) => {\n    const { scheduleId, warnings, infos, tasks = [] } = state.log\n    return tasks.length === 0 ? (\n      <div>\n        <TaskWarnings warnings={warnings} />\n        <TaskInfos infos={infos} />\n        <TaskError task={state.log} />\n      </div>\n    ) : (\n      <div>\n        <SearchBar\n          className='mb-1'\n          filters={SEARCH_BAR_FILTERS}\n          onChange={effects.onFilterChange}\n          value={state.filter}\n        />\n        <Select\n          labelKey='label'\n          onChange={effects.onStatusChange}\n          optionRenderer={state.optionRenderer}\n          options={state.options}\n          required\n          simpleValue\n          value={state.status}\n          valueKey='value'\n        />\n        <TaskWarnings warnings={warnings} />\n        <TaskInfos infos={infos} />\n        <br />\n        <ul className='list-group'>\n          {map(state.displayedTasks, taskLog => {\n            return (\n              <TaskLi\n                className='list-group-item'\n                key={taskLog.id}\n                restartVmJob={scheduleId && effects.restartVmJob}\n                task={taskLog}\n              >\n                <ul>\n                  {map(taskLog.tasks, subTaskLog => (\n                    <TaskLi key={subTaskLog.id} task={subTaskLog}>\n                      <ul>\n                        {map(subTaskLog.tasks, subSubTaskLog => (\n                          <TaskLi task={subSubTaskLog} key={subSubTaskLog.id}>\n                            <ul>\n                              {map(subSubTaskLog.tasks, subSubSubTaskLog => (\n                                <TaskLi task={subSubSubTaskLog} key={subSubSubTaskLog.id} />\n                              ))}\n                            </ul>\n                          </TaskLi>\n                        ))}\n                      </ul>\n                    </TaskLi>\n                  ))}\n                </ul>\n              </TaskLi>\n            )\n          })}\n        </ul>\n        {state.tasksFilteredByStatus.length > ITEMS_PER_PAGE && (\n          <div className='text-xs-center'>\n            <Pagination onChange={effects.onPageChange} pages={state.nPages} value={state.page} />\n          </div>\n        )}\n      </div>\n    )\n  },\n])\n","import _ from 'intl'\nimport * as xoaPlans from 'xoa-plans'\nimport ActionButton from 'action-button'\nimport addSubscriptions from 'add-subscriptions'\nimport Button from 'button'\nimport ButtonGroup from 'button-group'\nimport CopyToClipboard from 'react-copy-to-clipboard'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport ReportBugButton from 'report-bug-button'\nimport Tooltip from 'tooltip'\nimport { createBlobFromString, downloadLog, safeDateFormat } from 'utils'\nimport { get, ifDef } from '@xen-orchestra/defined'\nimport { injectState, provideState } from 'reaclette'\nimport keyBy from 'lodash/keyBy.js'\nimport { runBackupNgJob, subscribeBackupNgJobs, subscribeBackupNgLogs, subscribeSchedules } from 'xo'\n\nexport default decorate([\n  addSubscriptions(({ id }) => ({\n    log: cb =>\n      subscribeBackupNgLogs(logs => {\n        cb(logs[id])\n      }),\n    jobs: cb => subscribeBackupNgJobs(jobs => cb(keyBy(jobs, 'id'))),\n    schedules: cb => subscribeSchedules(schedules => cb(keyBy(schedules, 'id'))),\n  })),\n  provideState({\n    effects: {\n      _downloadLog:\n        () =>\n        (_, { log }) =>\n          downloadLog({ log, date: log.start, type: 'backup NG' }),\n      restartFailedVms:\n        (_, params) =>\n        async (_, { log: { jobId: id, scheduleId: schedule, tasks, infos } }) => {\n          let vms\n          if (tasks !== undefined) {\n            const scheduledVms = get(() => infos.find(({ message }) => message === 'vms').data.vms)\n\n            if (scheduledVms !== undefined) {\n              vms = new Set(scheduledVms)\n              tasks.forEach(({ status, data: { id } }) => {\n                status === 'success' && vms.delete(id)\n              })\n              vms = Array.from(vms)\n            } else {\n              vms = []\n              tasks.forEach(({ status, data: { id } }) => {\n                status !== 'success' && vms.push(id)\n              })\n            }\n          }\n          await runBackupNgJob({\n            force: get(() => params.force),\n            id,\n            schedule,\n            vms,\n          })\n        },\n    },\n    computed: {\n      formattedLog: (_, { log }) => JSON.stringify(log, null, 2),\n      jobFailed: (_, { log = {} }) => log.status !== 'success' && log.status !== 'pending',\n      reportBugProps: ({ formattedLog }, { log = {}, schedules = {}, jobs = {} }) => {\n        const props = {\n          size: 'small',\n          title: 'Backup job failed',\n        }\n        if (xoaPlans.CURRENT === xoaPlans.SOURCES) {\n          props.message = `\\`\\`\\`json\\n${formattedLog}\\n\\`\\`\\``\n        } else {\n          const formattedDate = ifDef(log.start, safeDateFormat)\n          props.files = [\n            {\n              content: createBlobFromString(formattedLog),\n              name: `${formattedDate} - log.json`,\n            },\n          ]\n          const job = jobs[log.jobId]\n          if (job !== undefined) {\n            props.files.push({\n              content: createBlobFromString(JSON.stringify(job, null, 2)),\n              name: 'job.json',\n            })\n          }\n          const schedule = schedules[log.scheduleId]\n          if (schedule !== undefined) {\n            props.files.push({\n              content: createBlobFromString(JSON.stringify(schedule, null, 2)),\n              name: 'schedule.json',\n            })\n          }\n        }\n        return props\n      },\n    },\n  }),\n  injectState,\n  ({ state, effects, log = {}, jobs }) => (\n    <span>\n      {get(() => jobs[log.jobId].name) || 'Job'} ({get(() => log.jobId.slice(4, 8))}){' '}\n      <span style={{ fontSize: '0.5em' }} className='text-muted'>\n        {log.id}\n      </span>{' '}\n      <ButtonGroup>\n        <Tooltip content={_('copyToClipboard')}>\n          <CopyToClipboard text={state.formattedLog}>\n            <Button size='small'>\n              <Icon icon='clipboard' />\n            </Button>\n          </CopyToClipboard>\n        </Tooltip>\n        <Tooltip content={_('logDownload')}>\n          <Button size='small' onClick={effects._downloadLog}>\n            <Icon icon='download' />\n          </Button>\n        </Tooltip>\n        <ReportBugButton {...state.reportBugProps} />\n        {state.jobFailed && log.scheduleId !== undefined && (\n          <ButtonGroup>\n            <ActionButton\n              handler={effects.restartFailedVms}\n              icon='run'\n              size='small'\n              tooltip={_('backupRestartFailedVms')}\n            />\n            <ActionButton\n              btnStyle='warning'\n              data-force\n              handler={effects.restartFailedVms}\n              icon='force-restart'\n              size='small'\n              tooltip={_('backupForceRestartFailedVms')}\n            />\n          </ButtonGroup>\n        )}\n      </ButtonGroup>\n    </span>\n  ),\n])\n","import _, { FormattedDuration } from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport BaseComponent from 'base-component'\nimport classnames from 'classnames'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport NoObjects from 'no-objects'\nimport PropTypes from 'prop-types'\nimport React, { Component } from 'react'\nimport renderXoItem from 'render-xo-item'\nimport Select from 'form/select'\nimport SortedTable from 'sorted-table'\nimport Tooltip from 'tooltip'\nimport { alert } from 'modal'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { connectStore, formatSize, formatSpeed, NumericDate } from 'utils'\nimport { createGetObject, createSelector } from 'selectors'\nimport { filter, forEach, includes, keyBy, map, orderBy } from 'lodash'\nimport { get } from '@xen-orchestra/defined'\nimport { deleteJobsLogs, subscribeJobs, subscribeJobsLogs, subscribeBackupNgJobs } from 'xo'\n\n// ===================================================================\n\nconst jobKeyToLabel = {\n  continuousReplication: _('continuousReplication'),\n  deltaBackup: _('deltaBackup'),\n  disasterRecovery: _('disasterRecovery'),\n  genericTask: _('customJob'),\n  rollingBackup: _('backup'),\n  rollingSnapshot: _('rollingSnapshot'),\n}\n\n// ===================================================================\n\n@connectStore(() => ({ object: createGetObject() }))\nclass JobParam extends Component {\n  render() {\n    const { object, paramKey, id } = this.props\n\n    return object != null ? _.keyValue(object.type || paramKey, renderXoItem(object)) : _.keyValue(paramKey, String(id))\n  }\n}\n\n@connectStore(() => ({ object: createGetObject() }))\nclass JobReturn extends Component {\n  render() {\n    const { object, id } = this.props\n\n    return (\n      <span>\n        <Icon icon='arrow-right' /> {object ? renderXoItem(object) : String(id)}\n      </span>\n    )\n  }\n}\n\nconst JobCallStateInfos = ({ end, error, isJobInterrupted }) => {\n  const [icon, tooltip] =\n    error !== undefined\n      ? isSkippedError(error)\n        ? ['skipped', 'jobCallSkipped']\n        : ['halted', 'failedJobCall']\n      : end !== undefined\n        ? ['running', 'successfulJobCall']\n        : isJobInterrupted\n          ? ['halted', 'jobInterrupted']\n          : ['busy', 'jobCallInProgess']\n\n  return (\n    <Tooltip content={_(tooltip)}>\n      <Icon icon={icon} />\n    </Tooltip>\n  )\n}\n\nconst JobDataInfos = ({\n  jobDuration,\n  size,\n\n  transferDuration = jobDuration,\n  transferSize = size,\n  mergeDuration,\n  mergeSize,\n}) => (\n  <div>\n    {transferSize && transferDuration ? (\n      <div>\n        <strong>{_('jobTransferredDataSize')}</strong> {formatSize(transferSize)}\n        <br />\n        <strong>{_('jobTransferredDataSpeed')}</strong> {formatSpeed(transferSize, transferDuration)}\n      </div>\n    ) : null}\n    {mergeSize && mergeDuration ? (\n      <div>\n        <strong>{_('jobMergedDataSize')}</strong> {formatSize(mergeSize)}\n        <br />\n        <strong>{_('jobMergedDataSpeed')}</strong> {formatSpeed(mergeSize, mergeDuration)}\n      </div>\n    ) : null}\n  </div>\n)\n\nconst DEFAULT_CALL_FILTER = { label: 'allJobCalls', value: 'all' }\nconst CALL_FILTER_OPTIONS = [\n  DEFAULT_CALL_FILTER,\n  { label: 'failedJobCall', value: 'error' },\n  { label: 'jobCallInProgess', value: 'running' },\n  { label: 'jobCallSkipped', value: 'skipped' },\n  { label: 'successfulJobCall', value: 'success' },\n  { label: 'jobInterrupted', value: 'interrupted' },\n]\n\nconst PREDICATES = {\n  all: () => () => true,\n  error: () => call => call.error !== undefined && !isSkippedError(call.error),\n  interrupted: isInterrupted => call => call.end === undefined && call.error === undefined && isInterrupted,\n  running: isInterrupted => call => call.end === undefined && call.error === undefined && !isInterrupted,\n  skipped: () => call => call.error !== undefined && isSkippedError(call.error),\n  success: () => call => call.end !== undefined && call.error === undefined,\n}\n\nconst NO_OBJECTS_MATCH_THIS_PATTERN = 'no objects match this pattern'\nconst UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'\nconst NO_SUCH_OBJECT_ERROR = 'no such object'\nconst UNHEALTHY_VDI_CHAIN_LINK = 'https://docs.xen-orchestra.com/backup_troubleshooting#vdi-chain-protection'\n\nconst isSkippedError = error => error.message === UNHEALTHY_VDI_CHAIN_ERROR || error.message === NO_SUCH_OBJECT_ERROR\n\nclass Log extends BaseComponent {\n  state = {\n    filter: DEFAULT_CALL_FILTER,\n  }\n\n  _getIsJobInterrupted = createSelector(\n    () => this.props.log.id,\n    () => get(() => this.props.job.runId),\n    (logId, runId) => logId !== runId\n  )\n\n  _getCallsByState = createSelector(\n    () => this.props.log.calls,\n    this._getIsJobInterrupted,\n    (calls, isInterrupted) => {\n      const callsByState = {}\n      forEach(CALL_FILTER_OPTIONS, ({ value }) => {\n        callsByState[value] = filter(calls, PREDICATES[value](isInterrupted))\n      })\n      return callsByState\n    }\n  )\n\n  _getFilteredCalls = createSelector(\n    () => this.state.filter.value,\n    this._getCallsByState,\n    (value, calls) => calls[value]\n  )\n\n  _getFilterOptionRenderer = createSelector(this._getCallsByState, calls => ({ label, value }) => (\n    <span>\n      {_(label)} ({calls[value].length})\n    </span>\n  ))\n\n  render() {\n    const { error } = this.props.log\n    return error !== undefined ? (\n      <span className={error.message === NO_OBJECTS_MATCH_THIS_PATTERN ? 'text-info' : 'text-danger'}>\n        <Icon icon='alarm' /> {error.message}\n      </span>\n    ) : (\n      <div>\n        <Select\n          labelKey='label'\n          onChange={this.linkState('filter')}\n          optionRenderer={this._getFilterOptionRenderer()}\n          options={CALL_FILTER_OPTIONS}\n          required\n          value={this.state.filter}\n          valueKey='value'\n        />\n        <br />\n        <ul className='list-group'>\n          {map(this._getFilteredCalls(), call => {\n            const { end, error, returnedValue, start } = call\n\n            let id\n            if (returnedValue != null) {\n              id = returnedValue.id\n              if (id === undefined && typeof returnedValue === 'string') {\n                id = returnedValue\n              }\n            }\n\n            const jobDuration = end - start\n\n            return (\n              <li key={call.callKey} className='list-group-item'>\n                <strong className='text-info'>{call.method}: </strong>\n                <JobCallStateInfos end={end} error={error} isJobInterrupted={this._getIsJobInterrupted()} />\n                <br />\n                {map(call.params, (value, key) => [<JobParam id={value} paramKey={key} key={key} />, <br />])}\n                {_.keyValue(_('jobStart'), <NumericDate timestamp={start} />)}\n                <br />\n                {end !== undefined && (\n                  <div>\n                    {_.keyValue(_('jobEnd'), <NumericDate timestamp={end} />)}\n                    <br />\n                    {_.keyValue(_('jobDuration'), <FormattedDuration duration={jobDuration} />)}\n                  </div>\n                )}\n                {returnedValue != null && <JobDataInfos jobDuration={jobDuration} {...returnedValue} />}\n                {id !== undefined && (\n                  <span>\n                    {' '}\n                    <JobReturn id={id} />\n                  </span>\n                )}\n                {error != null &&\n                  (error.message === UNHEALTHY_VDI_CHAIN_ERROR ? (\n                    <Tooltip content={_('clickForMoreInformation')}>\n                      <a\n                        className='text-info'\n                        href={UNHEALTHY_VDI_CHAIN_LINK}\n                        rel='noopener noreferrer'\n                        target='_blank'\n                      >\n                        <Icon icon='info' /> {_('unhealthyVdiChainError')}\n                      </a>\n                    </Tooltip>\n                  ) : (\n                    <span className={isSkippedError(error) ? 'text-info' : 'text-danger'}>\n                      <Icon icon={isSkippedError(error) ? 'alarm' : 'error'} />{' '}\n                      {error.message !== undefined ? <strong>{error.message}</strong> : JSON.stringify(error)}\n                    </span>\n                  ))}\n              </li>\n            )\n          })}\n        </ul>\n      </div>\n    )\n  }\n}\n\nconst showCalls = (log, { jobs }) =>\n  alert(_('jobModalTitle', { job: log.jobId }), <Log log={log} job={jobs[log.jobId]} />)\n\nconst LOG_ACTIONS = [\n  {\n    handler: deleteJobsLogs,\n    icon: 'delete',\n    label: _('remove'),\n  },\n]\n\nconst LOG_ACTIONS_INDIVIDUAL = [\n  {\n    handler: showCalls,\n    icon: 'preview',\n    label: _('logDisplayDetails'),\n  },\n]\n\nconst getCallTag = log => log.calls[0].params.tag\n\nconst LOG_COLUMNS = [\n  {\n    name: _('jobId'),\n    itemRenderer: log => log.jobId.slice(4, 8),\n    sortCriteria: log => log.jobId,\n  },\n  {\n    name: _('jobType'),\n    itemRenderer: log => jobKeyToLabel[log.key],\n    sortCriteria: log => log.key,\n  },\n  {\n    name: _('jobTag'),\n    itemRenderer: log => get(getCallTag, log),\n    sortCriteria: log => get(getCallTag, log),\n  },\n  {\n    name: _('jobStart'),\n    itemRenderer: log => log.start && <NumericDate timestamp={log.start} />,\n    sortCriteria: log => log.start,\n    sortOrder: 'desc',\n  },\n  {\n    default: true,\n    name: _('jobEnd'),\n    itemRenderer: log => log.end && <NumericDate timestamp={log.end} />,\n    sortCriteria: log => log.end || log.start,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('jobDuration'),\n    itemRenderer: log => log.duration && <FormattedDuration duration={log.duration} />,\n    sortCriteria: log => log.duration,\n  },\n  {\n    name: _('jobStatus'),\n    itemRenderer: (log, { jobs }) => (\n      <span>\n        {log.status === 'finished' ? (\n          <span\n            className={classnames('tag', log.hasErrors ? 'tag-danger' : log.callSkipped ? 'tag-info' : 'tag-success')}\n          >\n            {_('jobFinished')}\n          </span>\n        ) : log.status === 'started' ? (\n          log.id === get(() => jobs[log.jobId].runId) ? (\n            <span className='tag tag-warning'>{_('jobStarted')}</span>\n          ) : (\n            <span className='tag tag-danger'>{_('jobInterrupted')}</span>\n          )\n        ) : (\n          <span className='tag tag-default'>{_('jobUnknown')}</span>\n        )}\n      </span>\n    ),\n    sortCriteria: log => (log.hasErrors ? ' ' : log.status),\n  },\n]\n\nconst LOG_FILTERS = {\n  onError: 'hasErrors?',\n  successful: 'status:finished !hasErrors?',\n  jobCallSkipped: '!hasErrors? callSkipped?',\n}\n\nconst Logs = decorate([\n  addSubscriptions(({ jobKeys }) => ({\n    logs: cb =>\n      subscribeJobsLogs(rawLogs => {\n        const logs = {}\n        forEach(rawLogs, (log, id) => {\n          const data = log.data\n          const { time } = log\n          if (data.event === 'job.start' && (jobKeys === undefined || includes(jobKeys, data.key))) {\n            logs[id] = {\n              id,\n              jobId: data.jobId,\n              key: data.key,\n              userId: data.userId,\n              start: time,\n              calls: {},\n              time,\n            }\n          } else {\n            const runJobId = data.runJobId\n            const entry = logs[runJobId]\n            if (!entry) {\n              return\n            }\n            if (data.event === 'job.end') {\n              entry.end = time\n              entry.duration = time - entry.start\n              entry.status = 'finished'\n\n              if (data.error !== undefined) {\n                entry.error = data.error\n                if (data.error.message === NO_OBJECTS_MATCH_THIS_PATTERN) {\n                  entry.callSkipped = true\n                } else {\n                  entry.hasErrors = true\n                }\n              }\n            } else if (data.event === 'jobCall.start') {\n              entry.calls[id] = {\n                callKey: id,\n                params: data.params,\n                method: data.method,\n                start: time,\n                time,\n              }\n            } else if (data.event === 'jobCall.end') {\n              const call = entry.calls[data.runCallId]\n\n              if (data.error) {\n                call.error = data.error\n                if (isSkippedError(data.error)) {\n                  entry.callSkipped = true\n                } else {\n                  entry.hasErrors = true\n                }\n                entry.meta = 'error'\n              } else {\n                call.returnedValue = data.returnedValue\n                call.end = time\n              }\n            }\n          }\n        })\n\n        forEach(logs, log => {\n          if (log.end === undefined) {\n            log.status = 'started'\n          } else if (!log.meta) {\n            log.meta = 'success'\n          }\n          log.calls = orderBy(log.calls, ['time'], ['desc'])\n        })\n\n        cb(orderBy(logs, ['time'], ['desc']))\n      }),\n    jobs: cb => subscribeJobs(jobs => cb(keyBy(jobs, 'id'))),\n    ngJobs: cb => subscribeBackupNgJobs(jobs => cb(keyBy(jobs, 'id'))),\n  })),\n  ({ logs, jobs, ngJobs }) => (\n    <Card>\n      <CardHeader>\n        <Icon icon='log' /> Logs\n      </CardHeader>\n      <CardBlock>\n        <NoObjects\n          actions={LOG_ACTIONS}\n          collection={logs}\n          columns={LOG_COLUMNS}\n          component={SortedTable}\n          data-jobs={{ ...jobs, ...ngJobs }}\n          emptyMessage={_('noLogs')}\n          filters={LOG_FILTERS}\n          individualActions={LOG_ACTIONS_INDIVIDUAL}\n          stateUrlParam='s_logs'\n        />\n      </CardBlock>\n    </Card>\n  ),\n])\n\nLogs.propTypes = {\n  jobKeys: PropTypes.array,\n}\n\nexport { Logs as default }\n","import _, { FormattedDuration } from 'intl'\nimport ActionButton from 'action-button'\nimport addSubscriptions from 'add-subscriptions'\nimport Copiable from 'copiable'\nimport copy from 'copy-to-clipboard'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport NoObjects from 'no-objects'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport { alert } from 'modal'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { connectStore, downloadLog, NumericDate } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\nimport filter from 'lodash/filter.js'\nimport { Pool } from 'render-xo-item'\nimport { subscribeBackupNgLogs } from 'xo'\n\nimport { STATUS_LABELS, LOG_FILTERS } from './utils'\n\nconst showError = error => alert(_('logError'), <pre>{JSON.stringify(error, null, 2).replace(/\\\\n/g, '\\n')}</pre>)\n\nconst COLUMNS = [\n  {\n    name: _('job'),\n    itemRenderer: ({ data }) => (\n      <Copiable data={data.jobId} tagName='div'>\n        {data.jobName || data.jobId.slice(4, 8)}\n      </Copiable>\n    ),\n    sortCriteria: 'data.jobId',\n  },\n  {\n    name: _('item'),\n    itemRenderer: ({ data }, { pools }) =>\n      data.pool === undefined ? (\n        'Xen Orchestra'\n      ) : pools[data.pool.uuid] !== undefined ? (\n        <Pool id={data.pool.uuid} link newTab />\n      ) : (\n        <Copiable data={data.pool.uuid} tagName='div'>\n          {data.pool.name_label || data.poolMaster.name_label}\n        </Copiable>\n      ),\n    sortCriteria: ({ data }) => (data.pool !== undefined ? data.pool.uuid : data.jobId),\n  },\n  {\n    name: _('logsBackupTime'),\n    itemRenderer: ({ data: { timestamp } }) => <NumericDate timestamp={timestamp} />,\n    sortCriteria: 'data.timestamp',\n  },\n  {\n    default: true,\n    name: _('logsRestoreTime'),\n    itemRenderer: task => <NumericDate timestamp={task.start} />,\n    sortCriteria: 'start',\n    sortOrder: 'desc',\n  },\n  {\n    name: _('jobDuration'),\n    itemRenderer: task => task.end !== undefined && <FormattedDuration duration={task.end - task.start} />,\n    sortCriteria: task => task.end - task.start,\n  },\n  {\n    name: _('jobStatus'),\n    itemRenderer: task => {\n      const { className, label } = STATUS_LABELS[task.status]\n\n      // failed task\n      if (task.status !== 'success' && task.status !== 'pending') {\n        return (\n          <ActionButton\n            btnStyle={className}\n            handler={showError}\n            handlerParam={task.result}\n            icon='preview'\n            size='small'\n            tooltip={_('clickToShowError')}\n          >\n            {_(label)}\n          </ActionButton>\n        )\n      }\n\n      return <span className={`tag tag-${className}`}>{_(label)}</span>\n    },\n    sortCriteria: 'status',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    icon: 'download',\n    label: _('logDownload'),\n    handler: task =>\n      downloadLog({\n        log: task,\n        date: task.start,\n        type: 'Metadata restore',\n      }),\n  },\n  {\n    icon: 'clipboard',\n    label: _('copyLogToClipboard'),\n    handler: task => copy(JSON.stringify(task, null, 2)),\n  },\n]\n\nexport default decorate([\n  connectStore({\n    pools: createGetObjectsOfType('pool'),\n  }),\n  addSubscriptions({\n    logs: cb => subscribeBackupNgLogs(logs => cb(logs && filter(logs, log => log.message === 'metadataRestore'))),\n  }),\n  ({ logs, pools }) => (\n    <Card>\n      <CardHeader>\n        <Icon icon='logs' /> {_('logTitle')}\n      </CardHeader>\n      <CardBlock>\n        <NoObjects\n          collection={logs}\n          columns={COLUMNS}\n          component={SortedTable}\n          data-pools={pools}\n          emptyMessage={_('noLogs')}\n          filters={LOG_FILTERS}\n          individualActions={INDIVIDUAL_ACTIONS}\n          stateUrlParam='s_logs'\n        />\n      </CardBlock>\n    </Card>\n  ),\n])\n","import _, { FormattedDuration } from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport Copiable from 'copiable'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport NoObjects from 'no-objects'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport Tooltip from 'tooltip'\nimport { alert } from 'modal'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { connectStore, formatSize, formatSpeed, NumericDate } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\nimport { filter, keyBy } from 'lodash'\nimport { get } from '@xen-orchestra/defined'\nimport { subscribeBackupNgLogs, subscribeBackupNgJobs } from 'xo'\nimport { Vm, Sr } from 'render-xo-item'\n\nimport { STATUS_LABELS, LOG_FILTERS } from './utils'\n\nconst showRestoreError = ({ currentTarget: { dataset } }) =>\n  alert(\n    _('logsFailedRestoreTitle'),\n    <Copiable data={dataset.error} className='text-danger' tagName='div'>\n      <Icon icon='alarm' /> {dataset.message}\n    </Copiable>\n  )\n\nconst COLUMNS = [\n  {\n    name: _('logsJobId'),\n    itemRenderer: ({ data: { jobId } }) => jobId.slice(4, 8),\n    sortCriteria: 'data.jobId',\n  },\n  {\n    name: _('logsJobName'),\n    itemRenderer: ({ data: { jobId } }, { jobs }) => get(() => jobs[jobId].name),\n    sortCriteria: ({ data: { jobId } }, { jobs }) => get(() => jobs[jobId].name),\n  },\n  {\n    name: _('logsBackupTime'),\n    itemRenderer: ({ data: { time } }) => <NumericDate timestamp={time} />,\n    sortCriteria: 'data.time',\n  },\n  {\n    name: _('labelVm'),\n    itemRenderer: ({ id, vm, status }) => (\n      <div>\n        {vm !== undefined && <Vm id={vm.id} link newTab />}\n        {vm === undefined && status === 'success' && <span className='text-warning'>{_('logsVmNotFound')}</span>}{' '}\n        <span style={{ fontSize: '0.5em' }} className='text-muted'>\n          {id}\n        </span>\n      </div>\n    ),\n    sortCriteria: ({ vm }) => vm !== undefined && vm.name_label,\n  },\n  {\n    default: true,\n    name: _('logsRestoreTime'),\n    itemRenderer: log => <NumericDate timestamp={log.start} />,\n    sortCriteria: 'start',\n    sortOrder: 'desc',\n  },\n  {\n    name: _('jobDuration'),\n    itemRenderer: log => log.end !== undefined && <FormattedDuration duration={log.end - log.start} />,\n    sortCriteria: log => log.end - log.start,\n  },\n  {\n    name: _('labelSr'),\n    itemRenderer: ({ data: { srId } }) => <Sr id={srId} link newTab />,\n    sortCriteria: ({ data: { srId } }, { srs }) => get(() => srs[srId].name_label),\n  },\n  {\n    name: _('jobStatus'),\n    itemRenderer: task => {\n      const { className, label } = STATUS_LABELS[task.status]\n      return (\n        <div>\n          <span className={`tag tag-${className}`}>{_(label)}</span>{' '}\n          {task.status === 'failure' &&\n            // 2021-03-19 - JFT: can be undefined due to bug, see a95b10239\n            task.result !== undefined && (\n              <Tooltip content={_('logsFailedRestoreError')}>\n                <a\n                  className='text-danger'\n                  onClick={showRestoreError}\n                  data-message={task.result.message}\n                  data-error={JSON.stringify(task.result)}\n                  style={{ cursor: 'pointer' }}\n                >\n                  <Icon icon='alarm' />\n                </a>\n              </Tooltip>\n            )}\n        </div>\n      )\n    },\n    sortCriteria: 'status',\n  },\n  {\n    name: _('labelSize'),\n    itemRenderer: ({ dataSize }) => dataSize !== undefined && formatSize(dataSize),\n    sortCriteria: 'dataSize',\n  },\n  {\n    name: _('labelSpeed'),\n    itemRenderer: task => {\n      const duration = task.end - task.start\n      return task.dataSize !== undefined && duration > 0 && formatSpeed(task.dataSize, duration)\n    },\n    sortCriteria: task => {\n      const duration = task.end - task.start\n      return task.dataSize !== undefined && duration > 0 && task.dataSize / duration\n    },\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    handler: task => window.open('./rest/v0/restore/logs/' + task.id),\n    icon: 'api',\n    label: _('taskOpenRawLog'),\n  },\n]\n\nconst ROW_TRANSFORM = (task, { vms }) => {\n  let vm, dataSize\n  if (task.status === 'success') {\n    const result = task.tasks.find(({ message }) => message === 'transfer').result\n    dataSize = result.size\n    vm = vms && vms[result.id]\n  }\n\n  return {\n    ...task,\n    dataSize,\n    vm,\n  }\n}\n\nexport default decorate([\n  connectStore({\n    srs: createGetObjectsOfType('SR'),\n    vms: createGetObjectsOfType('VM'),\n  }),\n  addSubscriptions({\n    logs: cb => subscribeBackupNgLogs(logs => cb(logs && filter(logs, log => log.message === 'restore'))),\n    jobs: cb => subscribeBackupNgJobs(jobs => cb(keyBy(jobs, 'id'))),\n  }),\n  ({ logs, jobs, srs, vms }) => (\n    <Card>\n      <CardHeader>\n        <Icon icon='logs' /> {_('logTitle')}\n      </CardHeader>\n      <CardBlock>\n        <NoObjects\n          collection={logs}\n          columns={COLUMNS}\n          component={SortedTable}\n          data-jobs={jobs}\n          data-srs={srs}\n          data-vms={vms}\n          emptyMessage={_('noLogs')}\n          filters={LOG_FILTERS}\n          individualActions={INDIVIDUAL_ACTIONS}\n          rowTransform={ROW_TRANSFORM}\n          stateUrlParam='s_logs'\n        />\n      </CardBlock>\n    </Card>\n  ),\n])\n","export const STATUS_LABELS = {\n  failure: {\n    className: 'danger',\n    label: 'jobFailed',\n  },\n  skipped: {\n    className: 'info',\n    label: 'jobSkipped',\n  },\n  success: {\n    className: 'success',\n    label: 'jobSuccess',\n  },\n  pending: {\n    className: 'warning',\n    label: 'jobStarted',\n  },\n  interrupted: {\n    className: 'danger',\n    label: 'jobInterrupted',\n  },\n}\n\nexport const LOG_FILTERS = {\n  jobFailed: 'status: failure',\n  jobInterrupted: 'status: interrupted',\n  jobSkipped: 'status: skipped',\n  jobStarted: 'status: pending',\n  jobSuccess: 'status: success',\n}\n","module.exports = {\n    \"brand\": \"mc1dc15474_brand\",\n    \"hiddenUncollapsed\": \"mc1dc15474_hiddenUncollapsed\",\n    \"collapsed\": \"mc1dc15474_collapsed\",\n    \"hiddenCollapsed\": \"mc1dc15474_hiddenCollapsed\",\n    \"centerCollapsed\": \"mc1dc15474_centerCollapsed\"\n};","import _ from 'intl'\nimport classNames from 'classnames'\nimport Component from 'base-component'\nimport Icon, { StackedIcons } from 'icon'\nimport Link from 'link'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { injectState } from 'reaclette'\nimport { UpdateTag } from '../xoa/update'\nimport { NotificationTag } from '../xoa/notifications'\nimport { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils'\nimport {\n  connect,\n  signOut,\n  subscribeHostMissingPatches,\n  subscribeNotifications,\n  subscribePermissions,\n  subscribeProxies,\n  subscribeProxyApplianceUpdaterState,\n  subscribeResourceSets,\n  subscribeSrsUnhealthyVdiChainsLength,\n  VDIS_TO_COALESCE_LIMIT,\n} from 'xo'\nimport {\n  createFilter,\n  createGetObjectsOfType,\n  createSelector,\n  getIsPoolAdmin,\n  getResolvedPendingTasks,\n  getStatus,\n  getUser,\n  getXoaState,\n  isAdmin,\n} from 'selectors'\nimport { countBy, every, forEach, identity, isEmpty, isEqual, map, pick, size, some } from 'lodash'\n\nimport styles from './index.css'\n\nconst LINK_STYLE = {\n  display: 'flex',\n}\n\nconst returnTrue = () => true\n\n@connectStore(\n  () => {\n    const getHosts = createGetObjectsOfType('host')\n    return (state, props) => ({\n      hosts: getHosts(state, props),\n      isAdmin: isAdmin(state, props),\n      isPoolAdmin: getIsPoolAdmin(state, props),\n      nHosts: getHosts.count()(state, props),\n      // true: useResourceSet to bypass permissions\n      nResolvedTasks: getResolvedPendingTasks(state, props, true).length,\n      pools: createGetObjectsOfType('pool')(state, props),\n      srs: createGetObjectsOfType('SR')(state, props),\n      status: getStatus(state, props),\n      user: getUser(state, props),\n      xoaState: getXoaState(state, props),\n    })\n  },\n  {\n    withRef: true,\n  }\n)\n@addSubscriptions({\n  notifications: subscribeNotifications,\n  permissions: subscribePermissions,\n  proxyIds: cb =>\n    subscribeProxies(proxies => {\n      cb(map(proxies, 'id').sort())\n    }),\n  resourceSets: subscribeResourceSets,\n  unhealthyVdiChainsLength: subscribeSrsUnhealthyVdiChainsLength,\n})\n@injectState\nexport default class Menu extends Component {\n  componentWillMount() {\n    const updateCollapsed = () => {\n      this.setState({ collapsed: window.innerWidth < 1200 })\n    }\n    updateCollapsed()\n\n    window.addEventListener('resize', updateCollapsed)\n    this._removeListener = () => {\n      window.removeEventListener('resize', updateCollapsed)\n      this._removeListener = noop\n    }\n\n    this._updateMissingPatchesSubscriptions()\n    this._updateProxiesSubscriptions()\n  }\n\n  componentWillUnmount() {\n    this._removeListener()\n    this._unsubscribeMissingPatches()\n    this._unsubscribeProxiesApplianceUpdaterState()\n  }\n\n  componentDidUpdate(prevProps) {\n    if (!isEqual(Object.keys(prevProps.hosts).sort(), Object.keys(this.props.hosts).sort())) {\n      this._updateMissingPatchesSubscriptions()\n    }\n\n    if (!isEqual(prevProps.proxyIds, this.props.proxyIds)) {\n      this._updateProxiesSubscriptions()\n    }\n  }\n\n  _areProxiesOutOfDate = createSelector(\n    () => this.state.proxyStates,\n    proxyStates => some(proxyStates, state => state.endsWith('-upgrade-needed'))\n  )\n  _getNProxiesErrors = createSelector(\n    () => this.state.proxyStates,\n    proxyStates => countBy(proxyStates).error\n  )\n  _checkPermissions = createSelector(\n    () => this.props.isAdmin,\n    () => this.props.permissions,\n    (isAdmin, permissions) =>\n      isAdmin ? returnTrue : ({ id }) => permissions && permissions[id] && permissions[id].operate\n  )\n\n  _getNoOperatablePools = createSelector(\n    createFilter(() => this.props.pools, this._checkPermissions),\n    isEmpty\n  )\n\n  _getNoResourceSets = createSelector(() => this.props.resourceSets, isEmpty)\n\n  _getNoNotifications = createSelector(\n    () => this.props.notifications,\n    notifications => every(notifications, { read: true })\n  )\n\n  get height() {\n    return this.refs.content.offsetHeight\n  }\n\n  _hasMissingPatches = createSelector(\n    () => this.state.missingPatches,\n    missingPatches => some(missingPatches, _ => _)\n  )\n\n  _hasUnhealthyVdis = createSelector(\n    () => this.state.unhealthyVdiChainsLength,\n    unhealthyVdiChainsLength =>\n      some(unhealthyVdiChainsLength, vdiChainsLength => size(vdiChainsLength) >= VDIS_TO_COALESCE_LIMIT)\n  )\n\n  _toggleCollapsed = event => {\n    event.preventDefault()\n    this._removeListener()\n    this.setState({ collapsed: !this.state.collapsed })\n  }\n\n  _connect = event => {\n    event.preventDefault()\n    return connect()\n  }\n\n  _signOut = event => {\n    event.preventDefault()\n    return signOut()\n  }\n\n  _updateMissingPatchesSubscriptions = () => {\n    this.setState(({ missingPatches }) => ({\n      missingPatches: pick(missingPatches, Object.keys(this.props.hosts)),\n    }))\n\n    const unsubs = map(this.props.hosts, host =>\n      subscribeHostMissingPatches(host, patches => {\n        this.setState(state => ({\n          missingPatches: {\n            ...state.missingPatches,\n            [host.id]: patches != null && patches.length > 0,\n          },\n        }))\n      })\n    )\n\n    if (this._unsubscribeMissingPatches !== undefined) {\n      this._unsubscribeMissingPatches()\n    }\n\n    this._unsubscribeMissingPatches = () => forEach(unsubs, unsub => unsub())\n  }\n\n  _updateProxiesSubscriptions = () => {\n    this.setState(({ proxyStates }) => ({\n      proxyStates: pick(proxyStates, this.props.proxyIds),\n    }))\n\n    const unsubs = map(this.props.proxyIds, proxyId =>\n      subscribeProxyApplianceUpdaterState(proxyId, ({ state: proxyState = '' }) => {\n        this.setState(state => ({\n          proxyStates: {\n            ...state.proxyStates,\n            [proxyId]: proxyState,\n          },\n        }))\n      })\n    )\n\n    if (this._unsubscribeProxiesApplianceUpdaterState !== undefined) {\n      this._unsubscribeProxiesApplianceUpdaterState()\n    }\n\n    this._unsubscribeProxiesApplianceUpdaterState = () => forEach(unsubs, unsub => unsub())\n  }\n\n  render() {\n    const { isAdmin, isPoolAdmin, nResolvedTasks, state, status, user, pools, nHosts, srs, xoaState } = this.props\n    const noOperatablePools = this._getNoOperatablePools()\n    const noResourceSets = this._getNoResourceSets()\n    const noNotifications = this._getNoNotifications()\n    const nProxiesErrors = this._getNProxiesErrors()\n\n    const missingPatchesWarning = this._hasMissingPatches() ? (\n      <Tooltip content={_('homeMissingPatches')}>\n        <span className='text-warning'>\n          <Icon icon='alarm' />\n        </span>\n      </Tooltip>\n    ) : null\n\n    const unhealthyVdisWarning = this._hasUnhealthyVdis() ? (\n      <Tooltip content={_('homeUnhealthyVdis')}>\n        <span className='text-warning'>\n          <Icon icon='alarm' />\n        </span>\n      </Tooltip>\n    ) : null\n\n    /* eslint-disable object-property-newline */\n    const items = [\n      {\n        to: '/home',\n        icon: 'menu-home',\n        label: 'homePage',\n        extra: [missingPatchesWarning],\n        subMenu: [\n          { to: '/home?t=VM', icon: 'vm', label: 'homeVmPage' },\n          nHosts !== 0 && {\n            to: '/home?t=host',\n            icon: 'host',\n            label: 'homeHostPage',\n          },\n          !isEmpty(pools) && {\n            to: '/home?t=pool',\n            icon: 'pool',\n            label: 'homePoolPage',\n            extra: [missingPatchesWarning],\n          },\n          isAdmin && {\n            to: '/home?t=VM-template',\n            icon: 'template',\n            label: 'homeTemplatePage',\n          },\n          !isEmpty(srs) && {\n            to: '/home?t=SR',\n            icon: 'sr',\n            label: 'homeSrPage',\n          },\n        ],\n      },\n      {\n        to: '/dashboard/overview',\n        icon: 'menu-dashboard',\n        label: 'dashboardPage',\n        extra: [unhealthyVdisWarning],\n        subMenu: [\n          {\n            to: '/dashboard/overview',\n            icon: 'menu-dashboard-overview',\n            label: 'overviewDashboardPage',\n          },\n          {\n            to: '/dashboard/visualizations',\n            icon: 'menu-dashboard-visualization',\n            label: 'overviewVisualizationDashboardPage',\n          },\n          {\n            to: '/dashboard/stats',\n            icon: 'menu-dashboard-stats',\n            label: 'overviewStatsDashboardPage',\n          },\n          {\n            to: '/dashboard/health',\n            icon: 'menu-dashboard-health',\n            label: 'overviewHealthDashboardPage',\n            extra: [unhealthyVdisWarning],\n          },\n        ],\n      },\n      isAdmin && {\n        to: '/self',\n        icon: 'menu-self-service',\n        label: 'selfServicePage',\n      },\n      isAdmin && {\n        to: '/backup/overview',\n        icon: 'menu-backup',\n        label: 'backupPage',\n        subMenu: [\n          {\n            to: '/backup/overview',\n            icon: 'menu-backup-overview',\n            label: 'backupOverviewPage',\n          },\n          {\n            to: '/backup/sequences',\n            icon: 'menu-backup-sequence',\n            label: 'sequences',\n          },\n          {\n            to: '/backup/new',\n            icon: 'menu-backup-new',\n            label: 'backupNewPage',\n          },\n          {\n            to: '/backup/restore',\n            icon: 'menu-backup-restore',\n            label: 'backupRestorePage',\n          },\n          {\n            to: '/backup/file-restore',\n            icon: 'menu-backup-file-restore',\n            label: 'backupFileRestorePage',\n          },\n          {\n            to: '/backup/health',\n            icon: 'menu-dashboard-health',\n            label: 'overviewHealthDashboardPage',\n          },\n        ],\n      },\n      {\n        to: isAdmin ? 'xoa/update' : 'xoa/notifications',\n        icon: 'menu-xoa',\n        label: 'xoa',\n        extra: [\n          !isAdmin || xoaState === 'upToDate' ? null : <UpdateTag key='update' />,\n          noNotifications ? null : <NotificationTag key='notification' />,\n        ],\n        subMenu: [\n          isAdmin && {\n            to: 'xoa/update',\n            icon: 'menu-update',\n            label: 'updatePage',\n            extra: <UpdateTag />,\n          },\n          isAdmin && {\n            to: 'xoa/licenses',\n            icon: 'menu-license',\n            label: 'licensesPage',\n          },\n          {\n            to: 'xoa/notifications',\n            icon: 'menu-notification',\n            label: 'notificationsPage',\n            extra: <NotificationTag />,\n          },\n          isAdmin && {\n            to: 'xoa/support',\n            icon: 'menu-support',\n            label: 'supportPage',\n          },\n        ],\n      },\n      isAdmin && {\n        to: '/settings/servers',\n        icon: 'menu-settings',\n        label: 'settingsPage',\n        subMenu: [\n          {\n            to: '/settings/servers',\n            icon: 'menu-settings-servers',\n            label: 'settingsServersPage',\n          },\n          {\n            to: '/settings/users',\n            icon: 'menu-settings-users',\n            label: 'settingsUsersPage',\n          },\n          {\n            to: '/settings/groups',\n            icon: 'menu-settings-groups',\n            label: 'settingsGroupsPage',\n          },\n          {\n            to: '/settings/acls',\n            icon: 'menu-settings-acls',\n            label: 'settingsAclsPage',\n          },\n          {\n            to: '/settings/remotes',\n            icon: 'menu-backup-remotes',\n            label: 'backupRemotesPage',\n          },\n          {\n            to: '/settings/plugins',\n            icon: 'menu-settings-plugins',\n            label: 'settingsPluginsPage',\n          },\n          {\n            to: '/settings/logs',\n            icon: 'menu-settings-logs',\n            label: 'settingsLogsPage',\n          },\n          {\n            to: '/settings/audit',\n            icon: 'audit',\n            label: 'settingsAuditPage',\n          },\n          { to: '/settings/ips', icon: 'ip', label: 'settingsIpsPage' },\n          {\n            to: '/settings/cloud-configs',\n            icon: 'template',\n            label: 'settingsCloudConfigsPage',\n          },\n          {\n            to: '/settings/config',\n            icon: 'menu-settings-config',\n            label: 'xoConfig',\n          },\n        ],\n      },\n      isAdmin && {\n        to: '/jobs/overview',\n        icon: 'menu-jobs',\n        label: 'jobsPage',\n        subMenu: [\n          {\n            to: '/jobs/overview',\n            icon: 'menu-jobs-overview',\n            label: 'jobsOverviewPage',\n          },\n          { to: '/jobs/new', icon: 'menu-jobs-new', label: 'jobsNewPage' },\n          {\n            to: '/jobs/schedules',\n            icon: 'menu-jobs-schedule',\n            label: 'jobsSchedulingPage',\n          },\n        ],\n      },\n      isAdmin && {\n        to: '/hub/templates',\n        icon: 'menu-hub',\n        label: 'hubPage',\n        subMenu: [\n          {\n            to: '/hub/templates',\n            icon: 'hub-template',\n            label: 'templatesLabel',\n          },\n          {\n            to: '/hub/recipes',\n            icon: 'hub-recipe',\n            label: 'recipesLabel',\n          },\n        ],\n      },\n      isAdmin && {\n        to: '/proxies',\n        icon: 'proxy',\n        label: 'proxies',\n        extra: [\n          this._areProxiesOutOfDate() ? (\n            <Tooltip content={_('proxiesNeedUpgrade')}>\n              <StackedIcons\n                icons={[\n                  { color: 'text-success', icon: 'circle', size: 2 },\n                  { icon: 'menu-update', size: 1 },\n                ]}\n              />\n            </Tooltip>\n          ) : nProxiesErrors > 0 ? (\n            <Tooltip content={_('someProxiesHaveErrors', { n: nProxiesErrors })}>\n              <span className='tag tag-pill tag-danger'>{nProxiesErrors}</span>\n            </Tooltip>\n          ) : null,\n        ],\n      },\n      isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },\n      {\n        to: '/tasks',\n        icon: 'task',\n        label: 'taskMenu',\n        pill: nResolvedTasks,\n      },\n      isAdmin && {\n        to: '/xostor',\n        label: 'xostor',\n        icon: 'menu-xostor',\n      },\n      !noOperatablePools && {\n        to: '/import/vm',\n        icon: 'menu-new-import',\n        label: 'newImport',\n        subMenu: [\n          {\n            to: '/import/vm',\n            icon: 'vm',\n            label: 'labelVm',\n          },\n          {\n            to: '/import/disk',\n            icon: 'disk',\n            label: 'labelDisk',\n          },\n          {\n            to: '/import/vmware',\n            icon: 'vm',\n            label: 'fromVmware',\n          },\n        ],\n      },\n      !(noOperatablePools && noResourceSets) && {\n        to: '/vms/new',\n        icon: 'menu-new',\n        label: 'newMenu',\n        subMenu: [\n          (isAdmin || (isPoolAdmin && process.env.XOA_PLAN > 3) || !noResourceSets) && {\n            to: '/vms/new',\n            icon: 'menu-new-vm',\n            label: 'newVmPage',\n          },\n          isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },\n          isPoolAdmin && {\n            to: '/new/network',\n            icon: 'menu-new-network',\n            label: 'newNetworkPage',\n          },\n          isAdmin && {\n            to: '/settings/servers',\n            icon: 'menu-settings-servers',\n            label: 'newServerPage',\n          },\n        ],\n      },\n    ]\n    /* eslint-enable object-property-newline */\n\n    return (\n      <div className={classNames('xo-menu', this.state.collapsed && styles.collapsed)}>\n        <ul className='nav nav-sidebar nav-pills nav-stacked' ref='content'>\n          <li>\n            <span>\n              <a className={styles.brand} href='#'>\n                <span className={styles.hiddenUncollapsed}>XO</span>\n                <span className={styles.hiddenCollapsed}>Xen Orchestra</span>\n              </a>\n            </span>\n          </li>\n          <li>\n            <a className='nav-link' onClick={this._toggleCollapsed} href='#'>\n              <Icon icon='menu-collapse' size='lg' fixedWidth />\n            </a>\n          </li>\n          {map(items, (item, index) => item && <MenuLinkItem key={index} item={item} />)}\n          <li>&nbsp;</li>\n          <li>&nbsp;</li>\n          {!state.isXoaStatusOk && (\n            <li className='nav-item xo-menu-item'>\n              <Link className='nav-link' style={LINK_STYLE} to='/xoa/support'>\n                <span className={classNames(styles.hiddenCollapsed, 'text-warning')}>\n                  <Icon icon='diagnosis' size='lg' fixedWidth /> {_('checkXoa')}\n                </span>\n                <span className={classNames(styles.hiddenUncollapsed, 'text-warning')}>\n                  <Icon icon='diagnosis' size='lg' fixedWidth />\n                </span>\n              </Link>\n            </li>\n          )}\n          {(isAdmin || +process.env.XOA_PLAN === 5) && (\n            <li className='nav-item xo-menu-item'>\n              <Link className='nav-link' style={{ display: 'flex' }} to='/about'>\n                {+process.env.XOA_PLAN === 5 ? (\n                  <span>\n                    <span className={classNames(styles.hiddenCollapsed, 'text-warning')}>\n                      <Icon icon='alarm' size='lg' fixedWidth /> {_('noSupport')}\n                    </span>\n                    <span className={classNames(styles.hiddenUncollapsed, 'text-warning')}>\n                      <Icon icon='alarm' size='lg' fixedWidth />\n                    </span>\n                  </span>\n                ) : +process.env.XOA_PLAN === 1 ? (\n                  <span>\n                    <span className={classNames(styles.hiddenCollapsed, 'text-warning')}>\n                      <Icon icon='info' size='lg' fixedWidth /> {_('freeUpgrade')}\n                    </span>\n                    <span className={classNames(styles.hiddenUncollapsed, 'text-warning')}>\n                      <Icon icon='info' size='lg' fixedWidth />\n                    </span>\n                  </span>\n                ) : (\n                  <span>\n                    <span className={classNames(styles.hiddenCollapsed, 'text-success')}>\n                      <Icon icon='info' size='lg' fixedWidth /> {getXoaPlan()}\n                    </span>\n                    <span className={classNames(styles.hiddenUncollapsed, 'text-success')}>\n                      <Icon icon='info' size='lg' fixedWidth />\n                    </span>\n                  </span>\n                )}\n              </Link>\n            </li>\n          )}\n          <li>&nbsp;</li>\n          <li>&nbsp;</li>\n          <li className='nav-item xo-menu-item'>\n            <a className='nav-link' onClick={this._signOut} href='#'>\n              <Icon icon='sign-out' size='lg' fixedWidth />\n              <span className={styles.hiddenCollapsed}> {_('signOut')}</span>\n            </a>\n          </li>\n          <li className='nav-item xo-menu-item'>\n            <Link className='nav-link text-xs-center' to='/user'>\n              <Tooltip\n                content={_('editUserProfile', {\n                  username: user ? user.email : '',\n                })}\n              >\n                <Icon icon='user' size='lg' />\n              </Tooltip>\n            </Link>\n          </li>\n          <li>&nbsp;</li>\n          <li>&nbsp;</li>\n          {status === 'connecting' ? (\n            <li className='nav-item text-xs-center'>{_('statusConnecting')}</li>\n          ) : (\n            status === 'disconnected' && (\n              <li className='nav-item text-xs-center xo-menu-item'>\n                <a className='nav-link' onClick={this._connect} href='#'>\n                  <Icon icon='alarm' size='lg' fixedWidth /> {_('statusDisconnected')}\n                </a>\n              </li>\n            )\n          )}\n        </ul>\n      </div>\n    )\n  }\n}\n\nconst MenuLinkItem = props => {\n  const { item } = props\n  const { to, icon, label, subMenu, pill, extra } = item\n  const _extra = extra !== undefined ? extra.find(e => e !== null) : undefined\n\n  return (\n    <li className='nav-item xo-menu-item'>\n      <Link activeClassName='active' className={classNames('nav-link', styles.centerCollapsed)} to={to}>\n        <Icon\n          className={classNames((pill || _extra) && styles.hiddenCollapsed)}\n          icon={`${icon}`}\n          size='lg'\n          fixedWidth\n        />\n        <span className={styles.hiddenCollapsed}>\n          {' '}\n          {typeof label === 'string' ? _(label) : label}\n          &nbsp;\n        </span>\n        {pill > 0 && <span className='tag tag-pill tag-primary'>{pill}</span>}\n        <span className={styles.hiddenUncollapsed}>{_extra}</span>\n        <span className={styles.hiddenCollapsed}>{extra !== undefined && extra.map(identity)}</span>\n      </Link>\n      {subMenu && <SubMenu items={subMenu} />}\n    </li>\n  )\n}\n\nconst SubMenu = props => {\n  return (\n    <ul className='nav nav-pills nav-stacked xo-sub-menu'>\n      {map(\n        props.items,\n        (item, index) =>\n          item && (\n            <li key={index} className='nav-item xo-menu-item'>\n              <Link activeClassName='active' className='nav-link' to={item.to}>\n                <Icon icon={`${item.icon}`} size='lg' fixedWidth /> {_(item.label)} {item.extra}\n              </Link>\n            </li>\n          )\n      )}\n    </ul>\n  )\n}\n","module.exports = {\n    \"inlineSelect\": \"mc217f7268_inlineSelect\",\n    \"button\": \"mc217f7268_button\",\n    \"sizeInput\": \"mc217f7268_sizeInput\",\n    \"lineItem\": \"mc217f7268_lineItem\",\n    \"item\": \"mc217f7268_item\",\n    \"input\": \"mc217f7268_input\",\n    \"sectionContent\": \"mc217f7268_sectionContent\",\n    \"sectionContentColumn\": \"mc217f7268_sectionContentColumn\",\n    \"summary\": \"mc217f7268_summary\",\n    \"submitSection\": \"mc217f7268_submitSection\",\n    \"refreshNames\": \"mc217f7268_refreshNames\",\n    \"customConfig\": \"mc217f7268_customConfig\",\n    \"fixedWidth\": \"mc217f7268_fixedWidth\",\n    \"tags\": \"mc217f7268_tags\"\n};","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport BaseComponent from 'base-component'\nimport Button from 'button'\nimport classNames from 'classnames'\nimport defined, { get } from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport isIp from 'is-ip'\nimport Link from 'link'\nimport Page from '../page'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport renderXoItem from 'render-xo-item'\nimport SelectBootFirmware from 'select-boot-firmware'\nimport SelectCoresPerSocket from 'select-cores-per-socket'\nimport store from 'store'\nimport Tags from 'tags'\nimport Tooltip from 'tooltip'\nimport Wizard, { Section } from 'wizard'\nimport { compileTemplate } from '@xen-orchestra/template'\nimport { confirm } from 'modal'\nimport { Container, Row, Col } from 'grid'\nimport { injectIntl } from 'react-intl'\nimport {\n  AvailableTemplateVars,\n  DEFAULT_CLOUD_CONFIG_TEMPLATE,\n  DEFAULT_NETWORK_CONFIG_TEMPLATE,\n  NetworkConfigInfo,\n} from 'cloud-config'\nimport { Input as DebounceInput, Textarea as DebounceTextarea } from 'debounce-input-decorator'\nimport { Limits } from 'usage'\nimport {\n  clamp,\n  every,\n  filter,\n  find,\n  forEach,\n  includes,\n  isEmpty,\n  isEqual,\n  join,\n  map,\n  size,\n  slice,\n  sum,\n  sumBy,\n} from 'lodash'\nimport {\n  addSshKey,\n  createVm,\n  createVms,\n  getCloudInitConfig,\n  getPoolGuestSecureBootReadiness,\n  isSrShared,\n  subscribeCurrentUser,\n  subscribeIpPools,\n  subscribeResourceSets,\n  XEN_DEFAULT_CPU_CAP,\n  XEN_DEFAULT_CPU_WEIGHT,\n} from 'xo'\nimport {\n  SelectCloudConfig,\n  SelectHost,\n  SelectIp,\n  SelectNetwork,\n  SelectNetworkConfig,\n  SelectPool,\n  SelectResourceSet,\n  SelectResourceSetIp,\n  SelectResourceSetsNetwork,\n  SelectResourceSetsSr,\n  SelectResourceSetsVdi,\n  SelectResourceSetsVmTemplate,\n  SelectRole,\n  SelectSr,\n  SelectSshKey,\n  SelectSubject,\n  SelectVdi,\n  SelectVgpuType,\n  SelectVmTemplate,\n} from 'select-objects'\nimport { SizeInput, Toggle } from 'form'\nimport { addSubscriptions, connectStore, formatSize, generateReadableRandomString, resolveIds } from 'utils'\nimport {\n  createFilter,\n  createFinder,\n  createGetObject,\n  createGetObjectsOfType,\n  createSelector,\n  getIsPoolAdmin,\n  getResolvedResourceSets,\n  getUser,\n} from 'selectors'\nimport { CURRENT as XOA_PLAN, ENTERPRISE } from 'xoa-plans'\n\nimport styles from './index.css'\n\nconst MULTIPLICAND = 2\nconst NB_VMS_MIN = 2\nconst NB_VMS_MAX = 100\nconst ACL_LEVELS = {\n  admin: 'danger',\n  operator: 'primary',\n  viewer: 'success',\n}\n\n/* eslint-disable camelcase */\n\nconst getObject = createGetObject((_, id) => id)\n\n// Sub-components\n\nconst SectionContent = ({ column, children }) => (\n  <div className={classNames('form-inline', styles.sectionContent, column && styles.sectionContentColumn)}>\n    {children}\n  </div>\n)\n\nconst LineItem = ({ children }) => <div className={styles.lineItem}>{children}</div>\n\nconst Item = ({ label, children, className }) => (\n  <span className={styles.item}>\n    {label && (\n      <span>\n        {label}\n        &nbsp;\n      </span>\n    )}\n    <span className={classNames(styles.input, className)}>{children}</span>\n  </span>\n)\n\n@addSubscriptions({\n  // eslint-disable-next-line standard/no-callback-literal\n  ipPoolsConfigured: cb => subscribeIpPools(ipPools => cb(ipPools.length > 0)),\n})\n@injectIntl\nclass Vif extends BaseComponent {\n  _getIpPoolPredicate = createSelector(\n    () => this.props.vif,\n    vif => ipPool => includes(ipPool.networks, vif.network)\n  )\n\n  render() {\n    const {\n      intl: { formatMessage },\n      ipPoolsConfigured,\n      networkPredicate,\n      onChangeAddresses,\n      onChangeMac,\n      onChangeNetwork,\n      onDelete,\n      pool,\n      resourceSet,\n      vif,\n    } = this.props\n\n    return (\n      <LineItem>\n        <Item label={_('newVmMacLabel')}>\n          <DebounceInput\n            className='form-control'\n            onChange={onChangeMac}\n            placeholder={formatMessage(messages.newVmMacPlaceholder)}\n            rows={7}\n            value={vif.mac}\n          />\n        </Item>\n        <Item label={_('newVmNetworkLabel')}>\n          <span className={styles.inlineSelect}>\n            {pool ? (\n              <SelectNetwork onChange={onChangeNetwork} predicate={networkPredicate} value={vif.network} />\n            ) : (\n              <SelectResourceSetsNetwork\n                onChange={onChangeNetwork}\n                predicate={networkPredicate}\n                resourceSet={resourceSet}\n                value={vif.network}\n              />\n            )}\n          </span>\n        </Item>\n        {ipPoolsConfigured && (\n          <LineItem>\n            <span className={styles.inlineSelect}>\n              {pool ? (\n                <SelectIp\n                  containerPredicate={this._getIpPoolPredicate()}\n                  multi\n                  onChange={onChangeAddresses}\n                  value={vif.addresses}\n                />\n              ) : (\n                <SelectResourceSetIp\n                  containerPredicate={this._getIpPoolPredicate()}\n                  multi\n                  onChange={onChangeAddresses}\n                  resourceSetId={resourceSet.id}\n                  value={vif.addresses}\n                />\n              )}\n            </span>\n          </LineItem>\n        )}\n        <Item>\n          <Button onClick={onDelete}>\n            <Icon icon='new-vm-remove' />\n          </Button>\n        </Item>\n      </LineItem>\n    )\n  }\n}\n\nclass AddAclsModal extends BaseComponent {\n  get value() {\n    return this.state\n  }\n\n  render() {\n    const { action, subjects } = this.state\n    return (\n      <form>\n        <div className='form-group'>\n          <SelectSubject multi onChange={this.linkState('subjects')} value={subjects} />\n        </div>\n        <div className='form-group'>\n          <SelectRole onChange={this.linkState('action')} value={action} />\n        </div>\n      </form>\n    )\n  }\n}\n\n// =============================================================================\n\nconst isVdiPresent = vdi => !vdi.missing\n\n@addSubscriptions({\n  resourceSets: subscribeResourceSets,\n  user: subscribeCurrentUser,\n})\n@connectStore(() => {\n  const getIsAdmin = createSelector(getUser, user => user && user.permission === 'admin')\n  const getNetworks = createGetObjectsOfType('network').sort()\n  const getPool = createGetObject((_, props) => props.location.query.pool)\n  const getPools = createGetObjectsOfType('pool')\n  const getSrs = createGetObjectsOfType('SR')\n  const getTemplate = createGetObject((_, props) => props.location.query.template)\n  const getTemplates = createGetObjectsOfType('VM-template').sort()\n  const getUserSshKeys = createSelector(\n    (_, props) => {\n      const user = props.user\n      return user && user.preferences && user.preferences.sshKeys\n    },\n    keys => keys\n  )\n  const getHosts = createGetObjectsOfType('host')\n  return (state, props) => ({\n    isAdmin: getIsAdmin(state, props),\n    isPoolAdmin: getIsPoolAdmin(state, props),\n    networks: getNetworks(state, props),\n    pool: getPool(state, props),\n    pools: getPools(state, props),\n    resolvedResourceSets: getResolvedResourceSets(\n      state,\n      props,\n      props.pool === undefined // to get objects as a self user\n    ),\n    srs: getSrs(state, props),\n    template: getTemplate(state, props, props.pool === undefined),\n    templates: getTemplates(state, props),\n    userSshKeys: getUserSshKeys(state, props),\n    hosts: getHosts(state, props),\n  })\n})\n@injectIntl\nexport default class NewVm extends BaseComponent {\n  static contextTypes = {\n    router: PropTypes.object,\n  }\n\n  constructor() {\n    super()\n\n    this._uniqueId = 0\n    // NewVm's form's state is stored in this.state.state instead of this.state\n    // so it can be emptied easily with this.setState({ state: {} })\n    this.state = { state: {} }\n  }\n\n  componentDidMount() {\n    this._reset(() => {\n      const { template } = this.props\n      if (template !== undefined) {\n        this._initTemplate(this.props.template)\n      }\n    })\n  }\n\n  async componentDidUpdate(prevProps) {\n    const template = this.props.template\n    if (get(() => prevProps.template.id) !== get(() => template.id)) {\n      this._initTemplate(template)\n    }\n\n    if (\n      !isEqual(prevProps.resourceSets, this.props.resourceSets) ||\n      prevProps.location.query.resourceSet !== this.props.location.query.resourceSet\n    ) {\n      this._setState({\n        share: this._getResourceSet()?.shareByDefault ?? false,\n      })\n    }\n\n    const pool = this.props.pool\n    if (\n      get(() => prevProps.pool.id) !== get(() => pool.id) ||\n      (pool === undefined && get(() => template.id) !== get(() => prevProps.template.id))\n    ) {\n      const poolId = pool?.id ?? template?.$pool\n      this.setState({\n        poolGuestSecurebootReadiness: poolId === undefined ? undefined : await getPoolGuestSecureBootReadiness(poolId),\n      })\n    }\n  }\n\n  _getResourceSet = createFinder(\n    () => this.props.resourceSets,\n    createSelector(\n      () => this.props.location.query.resourceSet,\n      resourceSetId => resourceSet => (resourceSet !== undefined ? resourceSetId === resourceSet.id : undefined)\n    )\n  )\n\n  _getResolvedResourceSet = createFinder(\n    () => this.props.resolvedResourceSets,\n    createSelector(this._getResourceSet, resourceSet =>\n      resourceSet !== undefined ? resolvedResourceSet => resolvedResourceSet.id === resourceSet.id : false\n    )\n  )\n\n  // Utils -----------------------------------------------------------------------\n\n  get _isDiskTemplate() {\n    const { template } = this.props\n    return template && template.$VBDs.length !== 0 && template.name_label !== 'Other install media'\n  }\n  _setState = (newValues, callback) => {\n    this.setState(\n      {\n        state: {\n          ...this.state.state,\n          ...newValues,\n        },\n      },\n      callback\n    )\n  }\n  _replaceState = (state, callback) => this.setState({ state }, callback)\n  _linkState = (path, targetPath) => this.linkState(`state.${path}`, targetPath)\n  _toggleState = path => this.toggleState(`state.${path}`)\n\n  // Actions ---------------------------------------------------------------------\n\n  _reset = callback => {\n    this._replaceState(\n      {\n        acls: [],\n        bootAfterCreate: true,\n        copyHostBiosStrings: this._templateHasBiosStrings(),\n        coresPerSocket: undefined,\n        CPUs: '',\n        cpuCap: '',\n        cpusMax: '',\n        cpuWeight: '',\n        destroyCloudConfigVdiAfterBoot: false,\n        existingDisks: {},\n        fastClone: true,\n        hvmBootFirmware: '',\n        installMethod: 'noConfigDrive',\n        multipleVms: false,\n        name_label: '',\n        name_description: '',\n        nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),\n        namePattern: '{name}%',\n        nbVms: NB_VMS_MIN,\n        VDIs: [],\n        VIFs: [],\n        secureBoot: false,\n        seqStart: 1,\n        share: this._getResourceSet()?.shareByDefault ?? false,\n        tags: [],\n        createVtpm: this._templateNeedsVtpm(),\n      },\n      callback\n    )\n  }\n\n  _selfCreate = () => {\n    const { VDIs, existingDisks, memoryDynamicMax } = this.state.state\n    const { template } = this.props\n    const disksSize = sumBy(VDIs, 'size') + sumBy(existingDisks, 'size')\n    const templateDisksSize = sumBy(template.template_info.disks, 'size')\n    const templateMemoryDynamicMax = template.memory.dynamic[1]\n    const templateVcpusMax = template.CPUs.max\n\n    return this._getCpusMax() > MULTIPLICAND * templateVcpusMax ||\n      memoryDynamicMax > MULTIPLICAND * templateMemoryDynamicMax ||\n      disksSize > MULTIPLICAND * templateDisksSize\n      ? confirm({\n          title: _('createVmModalTitle'),\n          body: _('createVmModalWarningMessage'),\n        }).then(this._create)\n      : this._create()\n  }\n\n  _create = () => {\n    const { state } = this.state\n    let installation\n    switch (state.installMethod) {\n      case 'ISO':\n        installation = {\n          method: 'cdrom',\n          repository: state.installIso.id,\n        }\n        break\n      case 'network':\n        const matches = /^(http|ftp|nfs)/i.exec(state.installNetwork)\n        if (!matches) {\n          throw new Error('invalid network URL')\n        }\n        installation = {\n          method: matches[1].toLowerCase(),\n          repository: state.installNetwork,\n        }\n        break\n      case 'PXE':\n        installation = {\n          method: 'network',\n          repository: 'pxe',\n        }\n    }\n\n    let cloudConfig\n    let cloudConfigs\n    let networkConfig\n    if (state.installMethod !== 'noConfigDrive') {\n      if (state.installMethod === 'SSH') {\n        const format = hostname => hostname.replace(/^\\s+|\\s+$/g, '').replace(/\\s+/g, '-')\n        const stringifiedKeys = join(\n          map(state.sshKeys, keyId => {\n            return this.props.userSshKeys[keyId] ? `  - ${this.props.userSshKeys[keyId].key}\\n` : ''\n          }),\n          ''\n        )\n\n        cloudConfig = `#cloud-config\\nhostname: ${format(state.name_label)}\\nssh_authorized_keys:\\n${stringifiedKeys}`\n        if (state.multipleVms) {\n          cloudConfigs = map(\n            state.nameLabels,\n            nameLabel => `#cloud-config\\nhostname: ${format(nameLabel)}\\nssh_authorized_keys:\\n${stringifiedKeys}`\n          )\n        }\n      } else if (state.installMethod === 'customConfig') {\n        const replacer = this._buildTemplate(defined(state.customConfig, DEFAULT_CLOUD_CONFIG_TEMPLATE))\n        cloudConfig = replacer(this.state.state, 0)\n        if (state.multipleVms) {\n          const seqStart = state.seqStart\n          cloudConfigs = map(state.nameLabels, (_, i) => replacer(state, i + +seqStart))\n        }\n        networkConfig = state.networkConfig\n      }\n    } else if (this._isCoreOs()) {\n      cloudConfig = state.cloudConfig\n      if (state.multipleVms) {\n        cloudConfigs = new Array(state.nbVms).fill(state.cloudConfig)\n      }\n    }\n\n    // Split allowed IPs into IPv4 and IPv6\n    const { VIFs } = state\n    const _VIFs = map(VIFs, vif => {\n      const _vif = { ...vif }\n      if (_vif.mac?.trim() === '') {\n        delete _vif.mac\n      }\n      delete _vif.addresses\n      _vif.allowedIpv4Addresses = []\n      _vif.allowedIpv6Addresses = []\n      forEach(vif.addresses, ip => {\n        if (!isIp(ip)) {\n          return\n        }\n        if (isIp.v4(ip)) {\n          _vif.allowedIpv4Addresses.push(ip)\n        } else {\n          _vif.allowedIpv6Addresses.push(ip)\n        }\n      })\n      return _vif\n    })\n\n    const resourceSet = this._getResourceSet()\n    const { template } = this.props\n\n    // Either use `memory` OR `memory*` params\n    let { memory, memoryStaticMax, memoryDynamicMin, memoryDynamicMax } = state\n    if ((memoryStaticMax != null || memoryDynamicMin != null) && memoryDynamicMax == null) {\n      memoryDynamicMax = memory\n    }\n    if (memoryDynamicMax != null) {\n      memory = undefined\n    }\n\n    const data = {\n      acls: state.acls.map(acl => ({ subject: acl.subject.id, action: acl.action.id })),\n      affinityHost: state.affinityHost && state.affinityHost.id,\n      clone: this._isDiskTemplate && state.fastClone,\n      existingDisks: state.existingDisks,\n      installation,\n      name_label: state.name_label,\n      template: template.id,\n      VDIs: state.VDIs,\n      VIFs: _VIFs,\n      resourceSet: resourceSet && resourceSet.id,\n      // vm.set parameters\n      coresPerSocket: state.coresPerSocket === null ? undefined : state.coresPerSocket,\n      CPUs: state.CPUs,\n      cpusMax: this._getCpusMax(),\n      cpuWeight: state.cpuWeight === '' ? null : state.cpuWeight,\n      cpuCap: state.cpuCap === '' ? null : state.cpuCap,\n      name_description: state.name_description,\n      memory,\n      memoryMax: memoryDynamicMax,\n      memoryMin: memoryDynamicMin,\n      memoryStaticMax,\n      pv_args: state.pv_args,\n      autoPoweron: state.autoPoweron,\n      bootAfterCreate: state.bootAfterCreate,\n      copyHostBiosStrings:\n        state.hvmBootFirmware !== 'uefi' && !this._templateHasBiosStrings() && state.copyHostBiosStrings,\n      createVtpm: state.createVtpm,\n      destroyCloudConfigVdiAfterBoot: state.destroyCloudConfigVdiAfterBoot,\n      secureBoot: state.secureBoot,\n      share: state.share,\n      cloudConfig,\n      networkConfig: this._isCoreOs() ? undefined : networkConfig,\n      coreOs: this._isCoreOs(),\n      tags: state.tags,\n      vgpuType: get(() => state.vgpuType.id),\n      gpuGroup: get(() => state.vgpuType.gpuGroup),\n      hvmBootFirmware: state.hvmBootFirmware === '' ? undefined : state.hvmBootFirmware,\n    }\n\n    return state.multipleVms ? createVms(data, state.nameLabels, cloudConfigs) : createVm(data)\n  }\n\n  _onChangeTemplate = template => {\n    const { pathname, query } = this.props.location\n    this.context.router.push({\n      pathname,\n      query: { ...query, template: template && template.id },\n    })\n  }\n\n  _initTemplate = template => {\n    if (!template) {\n      return this._reset()\n    }\n\n    const storeState = store.getState()\n    const isInResourceSet = this._getIsInResourceSet()\n    const { state } = this.state\n    const { pool } = this.props\n    const resourceSet = this._getResolvedResourceSet()\n\n    const existingDisks = {}\n    forEach(template.$VBDs, vbdId => {\n      const vbd = getObject(storeState, vbdId, resourceSet)\n      if (!vbd || vbd.is_cd_drive) {\n        return\n      }\n      const vdi = getObject(storeState, vbd.VDI, resourceSet)\n      if (vdi) {\n        existingDisks[vbd.position] = {\n          name_label: vdi.name_label,\n          name_description: vdi.name_description,\n          size: vdi.size,\n          $SR: pool || isInResourceSet(vdi.$SR) ? vdi.$SR : this._getDefaultSr(template),\n        }\n      }\n    })\n\n    let VIFs = []\n    const defaultNetworkIds = this._getDefaultNetworkIds(template)\n    forEach(\n      // iterate template VIFs in device order\n      template.VIFs.map(id => getObject(storeState, id, resourceSet)).sort((a, b) => a.device - b.device),\n\n      vif => {\n        VIFs.push({\n          network: pool || isInResourceSet(vif.$network) ? vif.$network : defaultNetworkIds[0],\n        })\n      }\n    )\n    if (VIFs.length === 0) {\n      VIFs = defaultNetworkIds.map(id => ({ network: id }))\n    }\n    const name_label = state.name_label === '' || !state.name_labelHasChanged ? template.name_label : state.name_label\n    const name_description =\n      state.name_description === '' || !state.name_descriptionHasChanged\n        ? template.other.default_template === 'true' || template.name_description === undefined\n          ? ''\n          : template.name_description\n        : state.name_description\n    const replacer = this._buildVmsNameTemplate()\n    this._setState({\n      // infos\n      name_label,\n      name_description,\n      nameLabels: map(Array(+state.nbVms), (_, index) =>\n        replacer({ name_label, name_description, template }, index + 1)\n      ),\n      copyHostBiosStrings: !isEmpty(template.bios_strings),\n      // performances\n      CPUs: template.CPUs.number,\n      cpusMax: template.CPUs.max,\n      cpuCap: '',\n      cpuWeight: '',\n      hvmBootFirmware: defined(() => template.boot.firmware, ''),\n      memory: template.memory.dynamic[1],\n      // installation\n      installMethod: (template.install_methods != null && template.install_methods[0]) || 'noConfigDrive',\n      sshKeys: this.props.userSshKeys && this.props.userSshKeys.length && [0],\n      // interfaces\n      VIFs,\n      // disks\n      existingDisks,\n      VDIs: map(template.template_info.disks, disk => {\n        return {\n          ...disk,\n          name_description: disk.name_description || 'Created by XO',\n          name_label: (name_label || 'disk') + '_' + generateReadableRandomString(5),\n          SR: this._getDefaultSr(template),\n        }\n      }),\n      // settings\n      secureBoot: template.secureBoot,\n      createVtpm: this._templateNeedsVtpm(),\n    })\n\n    if (this._isCoreOs()) {\n      getCloudInitConfig(template.id).then(\n        cloudConfig => this._setState({ cloudConfig, coreOsDefaultTemplateError: false }),\n        () => this._setState({ coreOsDefaultTemplateError: true })\n      )\n    }\n  }\n\n  // Selectors -------------------------------------------------------------------\n\n  _getIsInPool = createSelector(\n    () => {\n      const { pool } = this.props\n      return pool && pool.id\n    },\n    poolId =>\n      ({ $pool }) =>\n        $pool === poolId\n  )\n  _getIsInResourceSet = createSelector(\n    () => {\n      const resourceSet = this._getResourceSet()\n      return resourceSet && resourceSet.objects\n    },\n    objectsIds => id => includes(objectsIds, id)\n  )\n\n  _getVmPredicate = createSelector(\n    this._getIsInPool,\n    this._getIsInResourceSet,\n    (isInPool, isInResourceSet) => vm => isInResourceSet(vm.id) || isInPool(vm)\n  )\n  _getSrPredicate = createSelector(\n    this._getIsInPool,\n    this._getIsInResourceSet,\n    () => this.props.template,\n    () => this.props.pool === undefined,\n    (isInPool, isInResourceSet, template, self) => disk =>\n      (self ? isInResourceSet(disk.id) : isInPool(disk)) &&\n      disk.content_type !== 'iso' &&\n      disk.size > 0 &&\n      template !== undefined &&\n      template.$pool === disk.$pool\n  )\n  _getIsoPredicate = createSelector(\n    () => this.props.pool && this.props.pool.id,\n    poolId => sr => (poolId == null || poolId === sr.$pool) && sr.SR_type === 'iso'\n  )\n  _getNetworkPredicate = createSelector(\n    this._getIsInPool,\n    this._getIsInResourceSet,\n    () => this.props.pool === undefined,\n    () => this.props.template,\n    (isInPool, isInResourceSet, self, template) => network =>\n      (self ? isInResourceSet(network.id) : isInPool(network)) &&\n      template !== undefined &&\n      template.$pool === network.$pool\n  )\n  _getPoolNetworks = createSelector(\n    () => this.props.networks,\n    () => {\n      const { pool } = this.props\n      return pool && pool.id\n    },\n    (networks, poolId) => filter(networks, network => network.$pool === poolId)\n  )\n\n  _getAffinityHostPredicate = createSelector(\n    () => this.props.pool,\n    () => this.state.state.existingDisks,\n    () => this.state.state.VDIs,\n    () => this.props.srs,\n    (pool, existingDisks, VDIs, srs) => {\n      if (!srs) {\n        return false\n      }\n\n      const containers = [\n        ...map(existingDisks, disk => get(() => srs[disk.$SR].$container)),\n        ...map(VDIs, disk => get(() => srs[disk.SR].$container)),\n      ]\n      return host =>\n        host.$pool === pool.id && every(containers, container => container === pool.id || container === host.id)\n    }\n  )\n\n  _getAutomaticNetworks = createSelector(\n    createFilter(this._getPoolNetworks, [network => network.automatic]),\n    networks => networks.map(_ => _.id)\n  )\n\n  _getDefaultNetworkIds = template => {\n    if (template === undefined) {\n      return []\n    }\n\n    if (this.props.pool === undefined) {\n      const network = find(this._getResolvedResourceSet().objectsByType.network, {\n        $pool: template.$pool,\n      })\n      return network !== undefined ? [network.id] : []\n    }\n\n    const automaticNetworks = this._getAutomaticNetworks()\n    if (automaticNetworks.length !== 0) {\n      return automaticNetworks\n    }\n\n    const network = find(this._getPoolNetworks(), network => {\n      const pif = getObject(store.getState(), network.PIFs[0])\n      return pif && pif.management\n    })\n\n    return network !== undefined ? [network.id] : []\n  }\n\n  _buildVmsNameTemplate = createSelector(\n    () => this.state.state.namePattern,\n    namePattern => this._buildTemplate(namePattern)\n  )\n\n  _buildTemplate = pattern =>\n    compileTemplate(pattern, {\n      '{index}': (_, i) => i,\n      '{name}': state => state.name_label || '',\n      '%': (state, i) => (state.multipleVms ? i : '%'),\n    })\n\n  _templateHasBiosStrings = createSelector(\n    () => this.props.template,\n    template => template !== undefined && !isEmpty(template.bios_strings)\n  )\n\n  _getVgpuTypePredicate = createSelector(\n    () => this.props.pool,\n    pool => vgpuType => pool !== undefined && pool.id === vgpuType.$pool\n  )\n\n  _isCoreOs = createSelector(\n    () => this.props.template,\n    template => template && template.name_label === 'CoreOS'\n  )\n\n  _isHvm = createSelector(\n    () => this.props.template,\n    template => template && template.virtualizationMode === 'hvm'\n  )\n\n  _templateNeedsVtpm = () => this.props.template?.needsVtpm\n\n  // On change -------------------------------------------------------------------\n\n  _onChangeSshKeys = keys => this._setState({ sshKeys: map(keys, key => key.id) })\n\n  _updateNbVms = () => {\n    const { nbVms, nameLabels, seqStart } = this.state.state\n    const nbVmsClamped = clamp(nbVms, NB_VMS_MIN, NB_VMS_MAX)\n    const newNameLabels = [...nameLabels]\n\n    if (nbVmsClamped < nameLabels.length) {\n      this._setState({ nameLabels: slice(newNameLabels, 0, nbVmsClamped) })\n    } else {\n      const replacer = this._buildVmsNameTemplate()\n      for (let i = +seqStart + nameLabels.length; i <= +seqStart + nbVmsClamped - 1; i++) {\n        newNameLabels.push(replacer(this.state.state, i))\n      }\n      this._setState({ nameLabels: newNameLabels })\n    }\n  }\n  _updateNameLabels = () => {\n    const { nameLabels, seqStart } = this.state.state\n    const nbVms = nameLabels.length\n    const newNameLabels = []\n    const replacer = this._buildVmsNameTemplate()\n\n    for (let i = +seqStart; i <= +seqStart + nbVms - 1; i++) {\n      newNameLabels.push(replacer(this.state.state, i))\n    }\n    this._setState({ nameLabels: newNameLabels })\n  }\n  _selectResourceSet = resourceSet => {\n    const { pathname } = this.props.location\n\n    this.context.router.push({\n      pathname,\n      query: resourceSet && { resourceSet: resourceSet.id },\n    })\n    this._reset()\n  }\n  _selectPool = pool => {\n    const { pathname } = this.props.location\n\n    this.context.router.push({\n      pathname,\n      query: pool && { pool: pool.id },\n    })\n    this._reset()\n  }\n  _getDefaultSr = template => {\n    const { pool } = this.props\n\n    if (pool !== undefined) {\n      return pool.default_SR\n    }\n\n    if (template === undefined) {\n      return\n    }\n\n    const defaultSr = getObject(store.getState(), template.$pool, true).default_SR\n\n    return includes(\n      resolveIds(\n        filter(\n          this._getResolvedResourceSet().objectsByType.SR,\n          sr => sr.$pool === template.$pool && sr.content_type !== 'iso' && sr.size > 0\n        )\n      ),\n      defaultSr\n    )\n      ? defaultSr\n      : undefined\n  }\n  _addVdi = () => {\n    const { state } = this.state\n    const { template } = this.props\n\n    this._setState({\n      VDIs: [\n        ...state.VDIs,\n        {\n          name_description: 'Created by XO',\n          name_label: (state.name_label || 'disk') + '_' + generateReadableRandomString(5),\n          SR: this._getDefaultSr(template),\n          type: 'system',\n        },\n      ],\n    })\n  }\n  _removeVdi = index => {\n    const { VDIs } = this.state.state\n\n    this._setState({\n      VDIs: [...VDIs.slice(0, index), ...VDIs.slice(index + 1)],\n    })\n  }\n  _addInterface = () => {\n    const { template } = this.props\n    const { state } = this.state\n\n    this._setState({\n      VIFs: [...state.VIFs, { network: this._getDefaultNetworkIds(template)[0] }],\n    })\n  }\n  _removeInterface = index => {\n    const { VIFs } = this.state.state\n\n    this._setState({\n      VIFs: [...VIFs.slice(0, index), ...VIFs.slice(index + 1)],\n    })\n  }\n\n  _addNewSshKey = () => {\n    const { newSshKey, sshKeys } = this.state.state\n    const { userSshKeys } = this.props\n    const splitKey = newSshKey.split(' ')\n    const title = splitKey.length === 3 ? splitKey[2].split('\\n')[0] : newSshKey.slice(-10)\n\n    // save key\n    addSshKey({\n      title,\n      key: newSshKey,\n    }).then(() => {\n      // select key\n      this._setState({\n        sshKeys: [...(sshKeys || []), userSshKeys ? userSshKeys.length : 0],\n        newSshKey: '',\n      })\n    })\n  }\n\n  _getRedirectionUrl = id => (this.state.state.multipleVms ? '/home' : `/vms/${id}`)\n\n  _handleBootFirmware = value =>\n    this._setState({\n      hvmBootFirmware: value,\n      secureBoot: false,\n      createVtpm: value === 'uefi' ? this._templateNeedsVtpm() : false,\n    })\n\n  _addAcls = async () => {\n    const { action, subjects } = await confirm({\n      title: _('vmAddAcls'),\n      icon: 'menu-settings-acls',\n      body: <AddAclsModal />,\n    })\n\n    if (action == null) {\n      return\n    }\n\n    // Remove ACLs that are being re-assigned\n    const subjectIds = subjects.map(subject => subject.id)\n    const acls = this.state.state.acls.filter(acl => !subjectIds.includes(acl.subject.id))\n\n    if (isEmpty(subjects)) {\n      return\n    }\n\n    this._setState({ acls: [...acls, ...subjects.map(subject => ({ action, subject }))] })\n  }\n\n  _removeAcl = event => {\n    const { action, subject } = event.currentTarget.dataset\n    this._setState({\n      acls: this.state.state.acls.filter(acl => acl.action.id !== action || acl.subject.id !== subject),\n    })\n  }\n\n  // MAIN ------------------------------------------------------------------------\n\n  _renderHeader = () => {\n    const { isAdmin, isPoolAdmin, pool, resourceSets } = this.props\n    const selectPool = (\n      <span className={styles.inlineSelect}>\n        <SelectPool onChange={this._selectPool} value={pool} />\n      </span>\n    )\n    const selectResourceSet = (\n      <span className={styles.inlineSelect}>\n        <SelectResourceSet onChange={this._selectResourceSet} value={this.props.location.query.resourceSet} />\n      </span>\n    )\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={12}>\n            <h2>\n              {isAdmin || (isPoolAdmin && process.env.XOA_PLAN > 3) || !isEmpty(resourceSets)\n                ? _('newVmCreateNewVmOn', {\n                    select: isAdmin || isPoolAdmin ? selectPool : selectResourceSet,\n                  })\n                : _('newVmCreateNewVmNoPermission')}\n            </h2>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n\n  render() {\n    const { pool } = this.props\n    return (\n      <Page header={this._renderHeader()}>\n        {(pool || this._getResourceSet()) && (\n          <form id='vmCreation'>\n            <Wizard>\n              {this._renderInfo()}\n              {this._renderPerformances()}\n              {this._renderInstallSettings()}\n              {this._renderInterfaces()}\n              {this._renderDisks()}\n              {this._renderAdvanced()}\n              {this._renderSummary()}\n            </Wizard>\n            <div className={styles.submitSection}>\n              <ActionButton className={styles.button} handler={this._reset} icon='new-vm-reset'>\n                {_('newVmReset')}\n              </ActionButton>\n              <ActionButton\n                btnStyle='primary'\n                className={styles.button}\n                disabled={\n                  !(\n                    this._isInfoDone() &&\n                    this._isPerformancesDone() &&\n                    this._isInstallSettingsDone() &&\n                    this._isInterfacesDone() &&\n                    this._isDisksDone() &&\n                    this._isAdvancedDone()\n                  ) || !this._availableResources()\n                }\n                form='vmCreation'\n                handler={pool === undefined ? this._selfCreate : this._create}\n                icon='new-vm-create'\n                redirectOnSuccess={this._getRedirectionUrl}\n              >\n                {_('newVmCreate')}\n              </ActionButton>\n            </div>\n          </form>\n        )}\n      </Page>\n    )\n  }\n\n  // INFO ------------------------------------------------------------------------\n\n  _renderInfo = () => {\n    const { name_description, name_label } = this.state.state\n    const { template } = this.props\n    return (\n      <Section icon='new-vm-infos' title='newVmInfoPanel' done={this._isInfoDone()}>\n        <SectionContent>\n          <Item label={_('newVmTemplateLabel')}>\n            <span className={styles.inlineSelect}>\n              {this.props.pool ? (\n                <SelectVmTemplate\n                  onChange={this._onChangeTemplate}\n                  placeholder={_('newVmSelectTemplate')}\n                  predicate={this._getVmPredicate()}\n                  value={template}\n                />\n              ) : (\n                <SelectResourceSetsVmTemplate\n                  onChange={this._onChangeTemplate}\n                  placeholder={_('newVmSelectTemplate')}\n                  resourceSet={this._getResolvedResourceSet()}\n                  value={template}\n                />\n              )}\n            </span>\n          </Item>\n          <Item label={_('newVmNameLabel')}>\n            <DebounceInput className='form-control' onChange={this._linkState('name_label')} value={name_label} />\n          </Item>\n          <Item label={_('newVmDescriptionLabel')}>\n            <DebounceInput\n              className='form-control'\n              onChange={this._linkState('name_description')}\n              value={name_description}\n            />\n          </Item>\n        </SectionContent>\n      </Section>\n    )\n  }\n  _isInfoDone = () => {\n    const { name_label } = this.state.state\n    const { template } = this.props\n    return name_label && template\n  }\n\n  _getCpusMax = createSelector(\n    () => this.state.state.CPUs,\n    () => this.state.state.cpusMax,\n    Math.max\n  )\n\n  _renderPerformances = () => {\n    const { coresPerSocket, CPUs, memory, memoryDynamicMax } = this.state.state\n    const { template } = this.props\n    const { pool } = this.props\n    const memoryThreshold = get(() => template.memory.static[0])\n    const selectCoresPerSocket = (\n      <SelectCoresPerSocket\n        disabled={pool === undefined || template === undefined}\n        maxCores={get(() => pool.cpus.cores)}\n        maxVcpus={this._getCpusMax()}\n        onChange={this._linkState('coresPerSocket')}\n        value={coresPerSocket}\n      />\n    )\n\n    return (\n      <Section icon='new-vm-perf' title='newVmPerfPanel' done={this._isPerformancesDone()}>\n        <SectionContent>\n          <Item label={_('newVmVcpusLabel')}>\n            <DebounceInput\n              className='form-control'\n              min={0}\n              onChange={this._linkState('CPUs')}\n              type='number'\n              value={CPUs}\n            />\n          </Item>\n          <Item label={_('newVmRamLabel')}>\n            <SizeInput\n              className={styles.sizeInput}\n              onChange={this._linkState('memory')}\n              value={defined(memory, null)}\n            />{' '}\n            {memoryDynamicMax == null && memory != null && memory < memoryThreshold && (\n              <Tooltip\n                content={_('newVmRamWarning', {\n                  threshold: formatSize(memoryThreshold),\n                })}\n              >\n                <Icon icon='alarm' className='text-warning' size='lg' />\n              </Tooltip>\n            )}\n          </Item>\n          <Item label={_('vmCpuTopology')}>\n            {pool !== undefined ? (\n              selectCoresPerSocket\n            ) : (\n              <Tooltip content={_('requiresAdminPermissions')}>{selectCoresPerSocket}</Tooltip>\n            )}\n          </Item>\n        </SectionContent>\n      </Section>\n    )\n  }\n  _isPerformancesDone = () => {\n    const { CPUs, memory, memoryDynamicMax } = this.state.state\n    return CPUs && (memory != null || memoryDynamicMax != null)\n  }\n\n  // INSTALL SETTINGS ------------------------------------------------------------\n\n  _onChangeCloudConfig = cloudConfig => {\n    this._setState({\n      customConfig: get(() => cloudConfig.template),\n    })\n  }\n\n  _onChangeNetworkConfig = networkConfig =>\n    this._setState({\n      networkConfig: get(() => networkConfig.template),\n    })\n\n  _renderInstallSettings = () => {\n    const { coreOsDefaultTemplateError } = this.state.state\n    const { template } = this.props\n    if (!template) {\n      return\n    }\n    const {\n      cloudConfig,\n      customConfig,\n      networkConfig,\n      installIso,\n      installMethod,\n      installNetwork,\n      newSshKey,\n      pv_args,\n      sshKeys,\n    } = this.state.state\n    const { formatMessage } = this.props.intl\n    return (\n      <Section icon='new-vm-install-settings' title='newVmInstallSettingsPanel' done={this._isInstallSettingsDone()}>\n        {this._isDiskTemplate ? (\n          <SectionContent key='diskTemplate' column>\n            <LineItem>\n              <label>\n                <input\n                  checked={installMethod === 'noConfigDrive'}\n                  name='installMethod'\n                  onChange={this._linkState('installMethod')}\n                  type='radio'\n                  value='noConfigDrive'\n                />\n                &nbsp;\n                {_('noConfigDrive')}\n              </label>\n            </LineItem>\n            <br />\n            <LineItem>\n              <label>\n                <input\n                  checked={installMethod === 'SSH'}\n                  name='installMethod'\n                  onChange={this._linkState('installMethod')}\n                  type='radio'\n                  value='SSH'\n                />\n                &nbsp;\n                {_('newVmSshKey')}\n              </label>\n              &nbsp;\n              <span className={classNames('input-group', styles.fixedWidth)}>\n                <DebounceInput\n                  className='form-control'\n                  disabled={installMethod !== 'SSH'}\n                  onChange={this._linkState('newSshKey')}\n                  value={newSshKey}\n                />\n                <span className='input-group-btn'>\n                  <Button onClick={this._addNewSshKey} disabled={!newSshKey}>\n                    <Icon icon='add' />\n                  </Button>\n                </span>\n              </span>\n              {this.props.userSshKeys && this.props.userSshKeys.length > 0 && (\n                <span className={styles.fixedWidth}>\n                  <SelectSshKey\n                    disabled={installMethod !== 'SSH'}\n                    onChange={this._onChangeSshKeys}\n                    multi\n                    value={sshKeys || []}\n                  />\n                </span>\n              )}\n            </LineItem>\n            <br />\n            <LineItem>\n              <label>\n                <input\n                  checked={installMethod === 'customConfig'}\n                  name='installMethod'\n                  onChange={this._linkState('installMethod')}\n                  type='radio'\n                  value='customConfig'\n                />\n                &nbsp;\n                {_('newVmCustomConfig')}\n              </label>\n              &nbsp;\n              <AvailableTemplateVars />\n              &nbsp;\n            </LineItem>\n            <LineItem>\n              <Item>\n                <label className='text-muted'>\n                  {_('newVmUserConfigLabel')}\n                  <br />\n                  <SelectCloudConfig disabled={installMethod !== 'customConfig'} onChange={this._onChangeCloudConfig} />\n                  <DebounceTextarea\n                    className='form-control text-monospace'\n                    disabled={installMethod !== 'customConfig'}\n                    onChange={this._linkState('customConfig')}\n                    rows={7}\n                    value={defined(customConfig, DEFAULT_CLOUD_CONFIG_TEMPLATE)}\n                  />\n                </label>\n              </Item>\n              {!this._isCoreOs() && (\n                <Item>\n                  <label className='text-muted'>\n                    {_('newVmNetworkConfigLabel')} <NetworkConfigInfo />\n                    <br />\n                    <SelectNetworkConfig\n                      disabled={installMethod !== 'customConfig'}\n                      onChange={this._onChangeNetworkConfig}\n                    />\n                    <DebounceTextarea\n                      className='form-control text-monospace'\n                      disabled={installMethod !== 'customConfig'}\n                      onChange={this._linkState('networkConfig')}\n                      rows={7}\n                      value={defined(networkConfig, DEFAULT_NETWORK_CONFIG_TEMPLATE)}\n                    />\n                  </label>\n                </Item>\n              )}\n            </LineItem>\n          </SectionContent>\n        ) : (\n          <SectionContent>\n            {template.virtualizationMode === 'pv' ? (\n              <span>\n                <Item>\n                  <input\n                    checked={installMethod === 'network'}\n                    name='installMethod'\n                    onChange={this._linkState('installMethod')}\n                    type='radio'\n                    value='network'\n                  />{' '}\n                  <span>{_('newVmNetworkLabel')}</span>{' '}\n                  <DebounceInput\n                    className='form-control'\n                    disabled={installMethod !== 'network'}\n                    key='networkInput'\n                    onChange={this._linkState('installNetwork')}\n                    placeholder={formatMessage(messages.newVmInstallNetworkPlaceHolder)}\n                    value={installNetwork}\n                  />\n                </Item>\n                <Item label={_('newVmPvArgsLabel')} key='pv'>\n                  <DebounceInput className='form-control' onChange={this._linkState('pv_args')} value={pv_args} />\n                </Item>\n              </span>\n            ) : (\n              <Item>\n                <input\n                  checked={installMethod === 'PXE'}\n                  name='installMethod'\n                  onChange={this._linkState('installMethod')}\n                  type='radio'\n                  value='PXE'\n                />{' '}\n                <span>{_('newVmPxeLabel')}</span>\n              </Item>\n            )}\n          </SectionContent>\n        )}\n        <SectionContent>\n          <span className={styles.item}>\n            <input\n              checked={installMethod === 'ISO'}\n              name='installMethod'\n              onChange={this._linkState('installMethod')}\n              type='radio'\n              value='ISO'\n            />\n            &nbsp;\n            <span>{_('newVmIsoDvdLabel')}</span>\n            &nbsp;\n            <span className={styles.inlineSelect}>\n              {this.props.pool ? (\n                <SelectVdi\n                  disabled={installMethod !== 'ISO'}\n                  onChange={this._linkState('installIso')}\n                  predicate={isVdiPresent}\n                  srPredicate={this._getIsoPredicate()}\n                  value={installIso}\n                />\n              ) : (\n                <SelectResourceSetsVdi\n                  disabled={installMethod !== 'ISO'}\n                  onChange={this._linkState('installIso')}\n                  predicate={isVdiPresent}\n                  resourceSet={this._getResolvedResourceSet()}\n                  srPredicate={this._getIsoPredicate()}\n                  value={installIso}\n                />\n              )}\n            </span>\n          </span>\n        </SectionContent>\n        {this._isCoreOs() && (\n          <div>\n            <label>{_('newVmCloudConfig')}</label>{' '}\n            {!coreOsDefaultTemplateError ? (\n              <DebounceTextarea\n                className='form-control text-monospace'\n                onChange={this._linkState('cloudConfig')}\n                rows={7}\n                value={cloudConfig}\n              />\n            ) : (\n              <Link to='settings/logs' target='_blank' className='text-danger'>\n                <Icon icon='alarm' /> {_('coreOsDefaultTemplateError')}\n              </Link>\n            )}\n          </div>\n        )}\n      </Section>\n    )\n  }\n  _isInstallSettingsDone = () => {\n    const { customConfig, installIso, installMethod, installNetwork, sshKeys } = this.state.state\n    const { template } = this.props\n    switch (installMethod) {\n      case 'customConfig':\n        return customConfig === undefined || customConfig.trim() !== '' || installMethod === 'noConfigDrive'\n      case 'ISO':\n        return installIso\n      case 'network':\n        return /^(http|ftp|nfs)/i.exec(installNetwork)\n      case 'PXE':\n        return true\n      case 'SSH':\n        return !isEmpty(sshKeys) || installMethod === 'noConfigDrive'\n      default:\n        return template && this._isDiskTemplate && installMethod === 'noConfigDrive'\n    }\n  }\n\n  // INTERFACES ------------------------------------------------------------------\n\n  _renderInterfaces = () => {\n    const {\n      state: { VIFs },\n    } = this.state\n\n    return (\n      <Section icon='new-vm-interfaces' title='newVmInterfacesPanel' done={this._isInterfacesDone()}>\n        <SectionContent column>\n          {map(VIFs, (vif, index) => (\n            <div key={index}>\n              <Vif\n                networkPredicate={this._getNetworkPredicate()}\n                onChangeAddresses={this._linkState(`VIFs.${index}.addresses`, '*.id')}\n                onChangeMac={this._linkState(`VIFs.${index}.mac`)}\n                onChangeNetwork={this._linkState(`VIFs.${index}.network`, 'id')}\n                onDelete={() => this._removeInterface(index)}\n                pool={this.props.pool}\n                resourceSet={this._getResolvedResourceSet()}\n                vif={vif}\n              />\n              {index < VIFs.length - 1 && <hr />}\n            </div>\n          ))}\n          <Item>\n            <Button onClick={this._addInterface}>\n              <Icon icon='new-vm-add' /> {_('newVmAddInterface')}\n            </Button>\n          </Item>\n        </SectionContent>\n      </Section>\n    )\n  }\n  _isInterfacesDone = () => every(this.state.state.VIFs, vif => vif.network)\n\n  // DISKS -----------------------------------------------------------------------\n\n  _getDiskSrs = createSelector(\n    () => this.state.state.existingDisks,\n    () => this.state.state.VDIs,\n    (existingDisks, vdis) => {\n      const diskSrs = new Set()\n      forEach(existingDisks, disk => diskSrs.add(disk.$SR))\n      vdis.forEach(disk => diskSrs.add(disk.SR))\n      return [...diskSrs]\n    }\n  )\n\n  _srsNotOnSameHost = createSelector(\n    this._getDiskSrs,\n    () => this.props.srs,\n    (diskSrs, srs) => {\n      let container\n      let sr\n      return diskSrs.some(srId => {\n        sr = srs[srId]\n        return (\n          sr !== undefined &&\n          !isSrShared(sr) &&\n          (container !== undefined ? container !== sr.$container : ((container = sr.$container), false))\n        )\n      })\n    }\n  )\n\n  _renderDisks = () => {\n    const {\n      state: { installMethod, existingDisks, VDIs },\n    } = this.state\n    const { pool } = this.props\n    let i = 0\n    const resourceSet = this._getResolvedResourceSet()\n\n    return (\n      <Section icon='new-vm-disks' title='newVmDisksPanel' done={this._isDisksDone()}>\n        <SectionContent column>\n          {/* Existing disks */}\n          {map(existingDisks, (disk, index) => (\n            <div key={i}>\n              <LineItem>\n                <Item label={_('newVmSrLabel')}>\n                  <span className={styles.inlineSelect}>\n                    {pool ? (\n                      <SelectSr\n                        onChange={this._linkState(`existingDisks.${index}.$SR`, 'id')}\n                        predicate={this._getSrPredicate()}\n                        value={disk.$SR}\n                      />\n                    ) : (\n                      <SelectResourceSetsSr\n                        onChange={this._linkState(`existingDisks.${index}.$SR`, 'id')}\n                        predicate={this._getSrPredicate()}\n                        resourceSet={resourceSet}\n                        value={disk.$SR}\n                      />\n                    )}\n                  </span>\n                </Item>{' '}\n                <Item label={_('newVmNameLabel')}>\n                  <DebounceInput\n                    className='form-control'\n                    onChange={this._linkState(`existingDisks.${index}.name_label`)}\n                    value={disk.name_label}\n                  />\n                </Item>\n                <Item label={_('newVmDescriptionLabel')}>\n                  <DebounceInput\n                    className='form-control'\n                    onChange={this._linkState(`existingDisks.${index}.name_description`)}\n                    value={disk.name_description}\n                  />\n                </Item>\n                <Item label={_('newVmSizeLabel')}>\n                  <SizeInput\n                    className={styles.sizeInput}\n                    onChange={this._linkState(`existingDisks.${index}.size`)}\n                    readOnly={installMethod === 'noConfigDrive'}\n                    value={defined(disk.size, null)}\n                  />\n                </Item>\n              </LineItem>\n              {i++ < size(existingDisks) + VDIs.length - 1 && <hr />}\n            </div>\n          ))}\n\n          {/* VDIs */}\n          {map(VDIs, (vdi, index) => (\n            <div key={index}>\n              <LineItem>\n                <Item label={_('newVmSrLabel')}>\n                  <span className={styles.inlineSelect}>\n                    {pool ? (\n                      <SelectSr\n                        onChange={this._linkState(`VDIs.${index}.SR`, 'id')}\n                        predicate={this._getSrPredicate()}\n                        value={vdi.SR}\n                      />\n                    ) : (\n                      <SelectResourceSetsSr\n                        onChange={this._linkState(`VDIs.${index}.SR`, 'id')}\n                        predicate={this._getSrPredicate()}\n                        resourceSet={resourceSet}\n                        value={vdi.SR}\n                      />\n                    )}\n                  </span>\n                </Item>\n                <Item label={_('newVmNameLabel')}>\n                  <DebounceInput\n                    className='form-control'\n                    onChange={this._linkState(`VDIs.${index}.name_label`)}\n                    value={vdi.name_label}\n                  />\n                </Item>\n                <Item label={_('newVmDescriptionLabel')}>\n                  <DebounceInput\n                    className='form-control'\n                    onChange={this._linkState(`VDIs.${index}.name_description`)}\n                    value={vdi.name_description}\n                  />\n                </Item>\n                <Item label={_('newVmSizeLabel')}>\n                  <SizeInput\n                    className={styles.sizeInput}\n                    onChange={this._linkState(`VDIs.${index}.size`)}\n                    value={defined(vdi.size, null)}\n                  />\n                </Item>\n                <Item>\n                  <Button onClick={() => this._removeVdi(index)}>\n                    <Icon icon='new-vm-remove' />\n                  </Button>\n                </Item>\n              </LineItem>\n              {index < VDIs.length - 1 && <hr />}\n            </div>\n          ))}\n          {this._srsNotOnSameHost() && (\n            <span className='text-danger'>\n              <Icon icon='alarm' /> {_('newVmSrsNotOnSameHost')}\n            </span>\n          )}\n          <Item>\n            <Button onClick={this._addVdi}>\n              <Icon icon='new-vm-add' /> {_('newVmAddDisk')}\n            </Button>\n          </Item>\n        </SectionContent>\n      </Section>\n    )\n  }\n  _isDisksDone = () =>\n    every(this.state.state.VDIs, vdi => vdi.SR && vdi.name_label && vdi.size !== undefined) &&\n    every(this.state.state.existingDisks, (vdi, index) => vdi.$SR && vdi.name_label && vdi.size !== undefined)\n\n  // ADVANCED --------------------------------------------------------------------\n\n  _renderAdvanced = () => {\n    const {\n      acls,\n      affinityHost,\n      autoPoweron,\n      bootAfterCreate,\n      copyHostBiosStrings,\n      cpuCap,\n      cpusMax,\n      cpuWeight,\n      createVtpm,\n      destroyCloudConfigVdiAfterBoot,\n      hvmBootFirmware,\n      installMethod,\n      memoryDynamicMin,\n      memoryDynamicMax,\n      memoryStaticMax,\n      multipleVms,\n      nameLabels,\n      namePattern,\n      nbVms,\n      secureBoot,\n      seqStart,\n      share,\n      showAdvanced,\n      tags,\n    } = this.state.state\n    const { isAdmin, pool } = this.props\n    const { formatMessage } = this.props.intl\n    const isHvm = this._isHvm()\n    const _copyHostBiosStrings =\n      isAdmin && isHvm ? (\n        <label>\n          <input\n            checked={hvmBootFirmware !== 'uefi' && (this._templateHasBiosStrings() || copyHostBiosStrings)}\n            className='form-control'\n            disabled={hvmBootFirmware === 'uefi' || this._templateHasBiosStrings()}\n            onChange={this._toggleState('copyHostBiosStrings')}\n            type='checkbox'\n          />\n          &nbsp;\n          {_('copyHostBiosStrings')}\n        </label>\n      ) : null\n\n    const isVtpmSupported = pool?.vtpmSupported ?? true\n\n    return (\n      <Section icon='new-vm-advanced' title='newVmAdvancedPanel' done={this._isAdvancedDone()}>\n        <SectionContent column>\n          <Button onClick={this._toggleState('showAdvanced')}>\n            {showAdvanced ? _('newVmHideAdvanced') : _('newVmShowAdvanced')}\n          </Button>\n        </SectionContent>\n        {showAdvanced && [\n          <hr key='hr' />,\n          <SectionContent key='advanced'>\n            <Item>\n              <input checked={bootAfterCreate} onChange={this._linkState('bootAfterCreate')} type='checkbox' />\n              &nbsp;\n              {_('newVmBootAfterCreate')}\n            </Item>\n            <Item>\n              <input checked={autoPoweron} onChange={this._linkState('autoPoweron')} type='checkbox' />\n              &nbsp;\n              {_('autoPowerOn')}\n            </Item>\n            <Item className={styles.tags}>\n              <Tags labels={tags} onChange={this._linkState('tags')} />\n            </Item>\n          </SectionContent>,\n          <SectionContent key='destroyCloudConfigVdi'>\n            <Item>\n              <input\n                checked={destroyCloudConfigVdiAfterBoot}\n                disabled={installMethod === 'noConfigDrive' || !bootAfterCreate}\n                id='destroyCloudConfigDisk'\n                onChange={this._toggleState('destroyCloudConfigVdiAfterBoot')}\n                type='checkbox'\n              />\n              <label htmlFor='destroyCloudConfigDisk'>\n                &nbsp;\n                {_('destroyCloudConfigVdiAfterBoot')}\n              </label>\n            </Item>\n          </SectionContent>,\n          this._getResourceSet() !== undefined && (\n            <SectionContent>\n              <Item>\n                <input checked={share} onChange={this._linkState('share')} type='checkbox' />\n                &nbsp;\n                {_('newVmShare')}\n              </Item>\n            </SectionContent>\n          ),\n          <SectionContent key='newVmCpu'>\n            <Item label={_('newVmCpuWeightLabel')}>\n              <DebounceInput\n                className='form-control'\n                min={0}\n                max={65535}\n                onChange={this._linkState('cpuWeight')}\n                placeholder={formatMessage(messages.newVmDefaultCpuWeight, {\n                  value: XEN_DEFAULT_CPU_WEIGHT,\n                })}\n                type='number'\n                value={cpuWeight}\n              />\n            </Item>\n            <Item label={_('newVmCpuCapLabel')}>\n              <DebounceInput\n                className='form-control'\n                min={0}\n                onChange={this._linkState('cpuCap')}\n                placeholder={formatMessage(messages.newVmDefaultCpuCap, {\n                  value: XEN_DEFAULT_CPU_CAP,\n                })}\n                type='number'\n                value={cpuCap}\n              />\n            </Item>\n            <Item label={_('cpusMax')}>\n              <DebounceInput\n                className='form-control'\n                onChange={this._linkState('cpusMax')}\n                type='number'\n                value={cpusMax}\n              />\n            </Item>\n          </SectionContent>,\n          <SectionContent key='newVmDynamic'>\n            <Item label={_('newVmDynamicMinLabel')}>\n              <SizeInput\n                value={defined(memoryDynamicMin, null)}\n                onChange={this._linkState('memoryDynamicMin')}\n                className={styles.sizeInput}\n              />\n            </Item>\n            <Item label={_('newVmDynamicMaxLabel')}>\n              <SizeInput\n                value={defined(memoryDynamicMax, null)}\n                onChange={this._linkState('memoryDynamicMax')}\n                className={styles.sizeInput}\n              />\n            </Item>\n            <Item label={_('newVmStaticMaxLabel')}>\n              <SizeInput\n                value={defined(memoryStaticMax, null)}\n                onChange={this._linkState('memoryStaticMax')}\n                className={styles.sizeInput}\n              />\n            </Item>\n          </SectionContent>,\n          <SectionContent key='newVmMultipleVms'>\n            <Item label={_('newVmMultipleVms')}>\n              <Toggle value={multipleVms} onChange={this._linkState('multipleVms')} />\n            </Item>\n            <Item label={_('newVmMultipleVmsPattern')}>\n              <DebounceInput\n                className='form-control'\n                disabled={!multipleVms}\n                onChange={this._linkState('namePattern')}\n                placeholder={formatMessage(messages.newVmMultipleVmsPatternPlaceholder)}\n                value={namePattern}\n              />\n              &nbsp;\n              <AvailableTemplateVars />\n            </Item>\n            <Item label={_('newVmFirstIndex')}>\n              <DebounceInput\n                className='form-control'\n                disabled={!multipleVms}\n                onChange={this._linkState('seqStart')}\n                type='number'\n                value={seqStart}\n              />\n            </Item>\n            <Item>\n              <Tooltip content={_('newVmNameRefresh')}>\n                <a className={styles.refreshNames} onClick={this._updateNameLabels}>\n                  <Icon icon='refresh' />\n                </a>\n              </Tooltip>\n            </Item>\n            <Item className='input-group'>\n              <DebounceInput\n                className='form-control'\n                disabled={!multipleVms}\n                max={NB_VMS_MAX}\n                min={NB_VMS_MIN}\n                onChange={this._linkState('nbVms')}\n                type='number'\n                value={nbVms}\n              />\n              <span className='input-group-btn'>\n                <Tooltip content={_('newVmNumberRecalculate')}>\n                  <Button disabled={!multipleVms} onClick={this._updateNbVms}>\n                    <Icon icon='arrow-right' />\n                  </Button>\n                </Tooltip>\n              </span>\n            </Item>\n            {multipleVms && (\n              <LineItem>\n                {map(nameLabels, (nameLabel, index) => (\n                  <Item key={`nameLabel_${index}`}>\n                    <input\n                      type='text'\n                      className='form-control'\n                      value={nameLabel}\n                      onChange={this._linkState(`nameLabels.${index}`)}\n                    />\n                  </Item>\n                ))}\n              </LineItem>\n            )}\n          </SectionContent>,\n          isAdmin && (\n            <SectionContent>\n              <Item label={_('newVmAffinityHost')}>\n                <SelectHost\n                  onChange={this._linkState('affinityHost')}\n                  predicate={this._getAffinityHostPredicate()}\n                  value={affinityHost}\n                />\n              </Item>\n            </SectionContent>\n          ),\n          isHvm && (\n            <SectionContent>\n              <Item label={_('vmVgpu')}>\n                <SelectVgpuType onChange={this._linkState('vgpuType')} predicate={this._getVgpuTypePredicate()} />\n              </Item>\n            </SectionContent>\n          ),\n          isHvm && (\n            <SectionContent>\n              <Item label={_('vmBootFirmware')}>\n                <SelectBootFirmware\n                  host={affinityHost == null ? get(() => pool.master) : affinityHost.id}\n                  onChange={this._handleBootFirmware}\n                  value={hvmBootFirmware}\n                />\n              </Item>\n            </SectionContent>\n          ),\n          hvmBootFirmware === 'uefi' && [\n            <SectionContent key='secureBoot'>\n              <Item label={_('secureBoot')}>\n                <Toggle onChange={this._toggleState('secureBoot')} value={secureBoot} />\n              </Item>\n              {secureBoot && this.state.poolGuestSecurebootReadiness === 'not_ready' && (\n                <span className='align-self-center text-danger ml-1'>\n                  <a\n                    href='https://docs.xcp-ng.org/guides/guest-UEFI-Secure-Boot/'\n                    rel='noopener noreferrer'\n                    className='text-danger'\n                    target='_blank'\n                  >\n                    <Icon icon='alarm' /> {_('secureBootNotSetup')}\n                  </a>\n                </span>\n              )}\n            </SectionContent>,\n            <SectionContent key='vtpm'>\n              <Item label={_('enableVtpm')} className='d-inline-flex'>\n                <Tooltip content={!isVtpmSupported ? _('vtpmNotSupported') : undefined}>\n                  <Toggle onChange={this._toggleState('createVtpm')} value={createVtpm} disabled={!isVtpmSupported} />\n                </Tooltip>\n                {/* FIXME: link to VTPM documentation when ready */}\n                {/* &nbsp;\n                <Tooltip content={_('seeVtpmDocumentation')}>\n                  <a className='text-info align-self-center' style={{ cursor: 'pointer' }} href='#'>\n                    <Icon icon='info' />\n                  </a>\n                </Tooltip> */}\n                {!createVtpm && this._templateNeedsVtpm() && (\n                  <span className='align-self-center text-warning ml-1'>\n                    <Icon icon='alarm' /> {_('warningVtpmRequired')}\n                  </span>\n                )}\n              </Item>\n            </SectionContent>,\n          ],\n          isAdmin && isHvm && (\n            <SectionContent>\n              <Item>\n                {hvmBootFirmware === 'uefi' || this._templateHasBiosStrings() ? (\n                  <Tooltip\n                    content={hvmBootFirmware === 'uefi' ? _('vmBootFirmwareIsUefi') : _('templateHasBiosStrings')}\n                  >\n                    {_copyHostBiosStrings}\n                  </Tooltip>\n                ) : (\n                  _copyHostBiosStrings\n                )}\n              </Item>\n            </SectionContent>\n          ),\n          isAdmin && (\n            <SectionContent>\n              <Container className='w-100'>\n                <Row>\n                  <Col>\n                    <span className='mr-1'>{_('vmAcls')}</span>\n                    <ActionButton\n                      btnStyle='primary'\n                      disabled={XOA_PLAN.value < ENTERPRISE.value}\n                      handler={this._addAcls}\n                      icon='add'\n                      size='small'\n                      tooltip={\n                        XOA_PLAN.value < ENTERPRISE.value\n                          ? _('availableXoaPlan', { plan: ENTERPRISE.name })\n                          : _('vmAddAcls')\n                      }\n                    />\n                  </Col>\n                </Row>\n                {acls.map(({ subject, action }) => (\n                  <Row key={`${subject.id}.${action.id}`}>\n                    <Col>\n                      <span>{renderXoItem(subject)}</span>{' '}\n                      <span className={`tag tag-pill tag-${ACL_LEVELS[action.id]}`}>{action.name}</span>{' '}\n                      <Tooltip content={_('removeAcl')}>\n                        <a data-action={action.id} data-subject={subject.id} onClick={this._removeAcl} role='button'>\n                          <Icon icon='remove' />\n                        </a>\n                      </Tooltip>\n                    </Col>\n                  </Row>\n                ))}\n              </Container>\n            </SectionContent>\n          ),\n        ]}\n      </Section>\n    )\n  }\n  _isAdvancedDone = () => {\n    const lowerThan = (small, big) => small == null || big == null || small <= big\n    const { memoryDynamicMin, memoryDynamicMax, memoryStaticMax } = this.state.state\n\n    return lowerThan(memoryDynamicMin, memoryDynamicMax) && lowerThan(memoryDynamicMax, memoryStaticMax)\n  }\n\n  // SUMMARY ---------------------------------------------------------------------\n\n  _renderSummary = () => {\n    const { CPUs, existingDisks, fastClone, memory, memoryDynamicMax, multipleVms, nameLabels, VDIs, VIFs } =\n      this.state.state\n\n    const factor = multipleVms ? nameLabels.length : 1\n    const resourceSet = this._getResourceSet()\n    const limits = resourceSet && resourceSet.limits\n    const cpusLimits = limits && limits.cpus\n    const memoryLimits = limits && limits.memory\n    const diskLimits = limits && limits.disk\n\n    const _memory = memoryDynamicMax || memory || 0\n\n    return (\n      <Section icon='new-vm-summary' title='newVmSummaryPanel' summary>\n        <Container>\n          <Row>\n            <Col size={3} className='text-xs-center'>\n              <h2>\n                {CPUs || 0}x <Icon icon='cpu' />\n              </h2>\n            </Col>\n            <Col size={3} className='text-xs-center'>\n              <h2>\n                {_memory ? formatSize(_memory) : '0 B'} <Icon icon='memory' />\n              </h2>\n            </Col>\n            <Col size={3} className='text-xs-center'>\n              <h2>\n                {size(existingDisks) + VDIs.length || 0}x <Icon icon='disk' />\n              </h2>\n            </Col>\n            <Col size={3} className='text-xs-center'>\n              <h2>\n                {VIFs.length}x <Icon icon='network' />\n              </h2>\n            </Col>\n          </Row>\n          {limits && (\n            <Row>\n              <Col size={3}>\n                {cpusLimits?.total !== undefined && (\n                  <Limits limit={cpusLimits.total} toBeUsed={CPUs * factor} used={cpusLimits.usage} />\n                )}\n              </Col>\n              <Col size={3}>\n                {memoryLimits?.total !== undefined && (\n                  <Limits limit={memoryLimits.total} toBeUsed={_memory * factor} used={memoryLimits.usage} />\n                )}\n              </Col>\n              <Col size={3}>\n                {diskLimits?.total !== undefined && (\n                  <Limits\n                    limit={diskLimits.total}\n                    toBeUsed={(sumBy(VDIs, 'size') + sum(map(existingDisks, disk => disk.size))) * factor}\n                    used={diskLimits.usage}\n                  />\n                )}\n              </Col>\n            </Row>\n          )}\n        </Container>\n        {this._isDiskTemplate && (\n          <div style={{ display: 'flex' }}>\n            <span style={{ margin: 'auto' }}>\n              <input checked={fastClone} onChange={this._linkState('fastClone')} type='checkbox' />{' '}\n              <Icon icon='vm-fast-clone' /> {_('fastCloneVmLabel')}\n            </span>\n          </div>\n        )}\n      </Section>\n    )\n  }\n\n  _availableResources = () => {\n    const resourceSet = this._getResourceSet()\n\n    if (!resourceSet) {\n      return true\n    }\n\n    const { CPUs, existingDisks, memory, memoryDynamicMax, VDIs, multipleVms, nameLabels } = this.state.state\n    const _memory = memoryDynamicMax || memory || 0\n    const factor = multipleVms ? nameLabels.length : 1\n\n    return !(\n      CPUs * factor > get(() => resourceSet.limits.cpus.total - resourceSet.limits.cpus.usage) ||\n      _memory * factor > get(() => resourceSet.limits.memory.total - resourceSet.limits.memory.usage) ||\n      (sumBy(VDIs, 'size') + sum(map(existingDisks, disk => disk.size))) * factor >\n        get(() => resourceSet.limits.disk.total - resourceSet.limits.disk.usage)\n    )\n  }\n}\n/* eslint-enable camelcase */\n","import { routes } from 'utils'\n\nimport Network from './network'\nimport Sr from './sr'\n\nconst New = routes('vm', {\n  network: Network,\n  sr: Sr,\n})(({ children }) => children)\n\nexport default New\n","module.exports = {\n    \"inlineSelect\": \"mc3b669f55_inlineSelect\",\n    \"lineItem\": \"mc3b669f55_lineItem\",\n    \"item\": \"mc3b669f55_item\",\n    \"input\": \"mc3b669f55_input\"\n};","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport classNames from 'classnames'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React, { Component } from 'react'\nimport Wizard, { Section } from 'wizard'\nimport { addSubscriptions, connectStore } from 'utils'\nimport { createBondedNetwork, createNetwork, createPrivateNetwork, getBondModes, subscribePlugins } from 'xo'\nimport { isAdmin, createGetObject, createGetObjectsOfType, getIsPoolAdmin } from 'selectors'\nimport { injectIntl } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport { linkState } from 'reaclette-utils'\nimport map from 'lodash/map.js'\nimport { Select, Toggle } from 'form'\nimport { SelectHost, SelectPif, SelectPool } from 'select-objects'\n\nimport Page from '../../page'\nimport styles from './index.css'\n\nconst EMPTY = {\n  bonded: false,\n  bondMode: undefined,\n  networkCenter: undefined,\n  description: '',\n  encapsulation: 'gre',\n  encrypted: false,\n  isPrivate: false,\n  mtu: '',\n  name: '',\n  nbd: undefined,\n  networks: [],\n  pif: undefined,\n  pifs: [],\n  vlan: '',\n}\n\nconst LineItem = ({ children }) => <div className={styles.lineItem}>{children}</div>\n\nconst Item = ({ label, children, className }) => (\n  <span className={styles.item}>\n    {label && (\n      <span>\n        {label}\n        &nbsp;\n      </span>\n    )}\n    <span className={classNames(styles.input, className)}>{children}</span>\n  </span>\n)\n\n/*\nFrom XAPI doc, a tunnel can only be created on:\n- Physical PIF\n- Bond master PIF\n- VLAN PIF\nIf and only if the PIF:\n- Has an IP configuration\n- is NOT a bond slave\n\nFor more info see: https://xapi-project.github.io/xapi/design/tunnelling.html\n*/\nconst canSupportPrivateNetwork = (pool, pif) => {\n  if (pool === undefined || pif === undefined) {\n    return false\n  }\n\n  return (\n    (pif.isBondMaster || pif.physical || pif.vlan !== -1) &&\n    pif.mode !== 'None' &&\n    !pif.isBondSlave &&\n    pif.$host === pool.master\n  )\n}\n\nconst NewNetwork = decorate([\n  connectStore(() => ({\n    isAdmin,\n    isPoolAdmin: getIsPoolAdmin,\n    nPools: createGetObjectsOfType('pool').count(),\n    pool: createGetObject((_, props) => props.location.query.pool),\n  })),\n  addSubscriptions(\n    ({ isAdmin }) =>\n      isAdmin && {\n        plugins: subscribePlugins,\n      }\n  ),\n  injectIntl,\n  provideState({\n    initialState: () => ({ ...EMPTY, bondModes: undefined }),\n    effects: {\n      addPool() {\n        const { state } = this\n        state.networks = [...state.networks, { pool: undefined, pif: undefined }]\n      },\n      onChangeNetwork(_, key, pool, pif) {\n        const networks = [...this.state.networks]\n        const entry = networks[key]\n        if (pool !== undefined) {\n          entry.pool = pool\n        }\n        if (pif !== undefined) {\n          entry.pif = pif\n        }\n        this.state.networks = networks\n      },\n      onChangeNbd: (_, nbd) => ({ nbd: nbd?.value }),\n      initialize: async () => ({ bondModes: await getBondModes() }),\n      linkState,\n      onChangeMode: (_, bondMode) => ({ bondMode }),\n      onChangePif:\n        (_, value) =>\n        ({ bonded }) =>\n          bonded ? { pifs: value } : { pif: value },\n      onChangeEncapsulation(_, encapsulation) {\n        return { encapsulation: encapsulation.value }\n      },\n      onChangeCenter(_, networkCenter) {\n        this.state.networkCenter = networkCenter\n      },\n      onDeletePool(_, { currentTarget: { dataset } }) {\n        const networks = [...this.state.networks]\n        networks.splice(dataset.position, 1)\n        this.state.networks = networks\n      },\n      reset: () => EMPTY,\n      toggleBonded() {\n        const { bonded, isPrivate } = this.state\n        return {\n          ...EMPTY,\n          bonded: !bonded,\n          isPrivate: bonded ? isPrivate : false,\n        }\n      },\n      togglePrivate() {\n        const { bonded, isPrivate } = this.state\n        return {\n          ...EMPTY,\n          isPrivate: !isPrivate,\n          bonded: isPrivate ? bonded : false,\n        }\n      },\n      toggleEncrypted() {\n        return { encrypted: !this.state.encrypted }\n      },\n    },\n    computed: {\n      disableAddPool: ({ networks }, { nPools }) => networks.length >= nPools - 1,\n      modeOptions: ({ bondModes }) =>\n        bondModes !== undefined\n          ? bondModes.map(mode => ({\n              label: mode,\n              value: mode,\n            }))\n          : [],\n      hostPredicate:\n        ({ networks }, { pool }) =>\n        host =>\n          host.$pool === pool.id || networks.some(({ pool }) => pool !== undefined && pool.id === host.$pool),\n      pifPredicate:\n        ({ bonded }, { pool }) =>\n        pif =>\n          !pif.isBondSlave && !(bonded && pif.isBondMaster) && pif.vlan === -1 && pif.$host === (pool && pool.master),\n      pifPredicateSdnController:\n        (_, { pool }) =>\n        pif =>\n          canSupportPrivateNetwork(pool, pif),\n      networkPifPredicate:\n        ({ networks }) =>\n        (pif, key) =>\n          canSupportPrivateNetwork(networks[key].pool, pif),\n      networkPoolPredicate:\n        ({ networks }, { pool: rootPool }) =>\n        (pool, index) =>\n          pool.id !== rootPool.id &&\n          !networks.some(\n            ({ pool: networksPool = {} }, networksIndex) => pool.id === networksPool.id && index !== networksIndex\n          ),\n      isSdnControllerLoaded: (state, { plugins = [] }) =>\n        plugins.some(plugin => plugin.name === 'sdn-controller' && plugin.loaded),\n    },\n  }),\n  injectState,\n  class extends Component {\n    static contextTypes = {\n      router: PropTypes.object,\n    }\n\n    _create = () => {\n      const { pool, state } = this.props\n      const {\n        bonded,\n        bondMode,\n        networkCenter,\n        isPrivate,\n        description,\n        encapsulation,\n        encrypted,\n        name,\n        nbd,\n        networks,\n        pif,\n        pifs,\n      } = state\n\n      let { mtu, vlan } = state\n      mtu = mtu === '' ? undefined : +mtu\n      vlan = vlan === '' ? undefined : +vlan\n\n      return bonded\n        ? createBondedNetwork({\n            bondMode: bondMode.value,\n            description,\n            mtu,\n            name,\n            pifs: map(pifs, 'id'),\n            pool: pool.id,\n          })\n        : isPrivate\n          ? (() => {\n              const poolIds = [pool.id]\n              const pifIds = [pif.id]\n              for (const network of networks) {\n                poolIds.push(network.pool.id)\n                pifIds.push(network.pif.id)\n              }\n              return createPrivateNetwork({\n                poolIds,\n                pifIds,\n                name,\n                description,\n                encapsulation,\n                encrypted,\n                mtu,\n                preferredCenter: networkCenter,\n              })\n            })()\n          : createNetwork({\n              description,\n              mtu,\n              name,\n              nbd,\n              pif: pif == null ? undefined : pif.id,\n              pool: pool.id,\n              vlan,\n            })\n    }\n\n    _selectPool = pool => {\n      const {\n        effects,\n        location: { pathname },\n      } = this.props\n      effects.reset()\n      this.context.router.push({\n        pathname,\n        query: pool !== null && { pool: pool.id },\n      })\n    }\n\n    _renderHeader = () => {\n      const { isPoolAdmin, pool } = this.props\n      return (\n        <h2>\n          {isPoolAdmin\n            ? _('createNewNetworkOn', {\n                select: (\n                  <span className={styles.inlineSelect}>\n                    <SelectPool onChange={this._selectPool} value={pool} />\n                  </span>\n                ),\n              })\n            : _('createNewNetworkNoPermission')}\n        </h2>\n      )\n    }\n\n    render() {\n      const { state, effects, intl, pool } = this.props\n      const {\n        bonded,\n        bondMode,\n        networkCenter,\n        hostPredicate,\n        isPrivate,\n        description,\n        encapsulation,\n        encrypted,\n        modeOptions,\n        mtu,\n        name,\n        nbd,\n        pif,\n        pifPredicate,\n        pifPredicateSdnController,\n        pifs,\n        vlan,\n        isSdnControllerLoaded,\n      } = state\n      const { formatMessage } = intl\n      return (\n        <Page header={this._renderHeader()}>\n          {pool !== undefined && (\n            <form id='networkCreation'>\n              <Wizard>\n                <Section icon='network' title='newNetworkType'>\n                  <div>\n                    <Toggle onChange={effects.toggleBonded} value={bonded} /> <label>{_('bondedNetwork')}</label>\n                  </div>\n                  <div>\n                    <Toggle disabled={!isSdnControllerLoaded} onChange={effects.togglePrivate} value={isPrivate} />{' '}\n                    <label>{_('privateNetwork')}</label>\n                    <div>\n                      <em>\n                        <Icon icon='info' />{' '}\n                        <a\n                          href='https://docs.xen-orchestra.com/sdn_controller#requirements'\n                          target='_blank'\n                          rel='noreferrer'\n                        >\n                          {_('newNetworkSdnControllerTip')}\n                        </a>\n                      </em>\n                    </div>\n                  </div>\n                </Section>\n                <Section icon='info' title='newNetworkInfo'>\n                  <div className='form-group'>\n                    <label>{_('newNetworkInterface')}</label>\n                    <SelectPif\n                      multi={bonded}\n                      onChange={effects.onChangePif}\n                      predicate={isPrivate ? pifPredicateSdnController : pifPredicate}\n                      required={bonded || isPrivate}\n                      value={bonded ? pifs : pif}\n                    />\n                    <label>{_('newNetworkName')}</label>\n                    <input\n                      className='form-control'\n                      name='name'\n                      onChange={effects.linkState}\n                      required\n                      type='text'\n                      value={name}\n                    />\n                    <label>{_('newNetworkDescription')}</label>\n                    <input\n                      className='form-control'\n                      name='description'\n                      onChange={effects.linkState}\n                      type='text'\n                      value={description}\n                    />\n                    <label>{_('newNetworkMtu')}</label>\n                    <input\n                      className='form-control'\n                      name='mtu'\n                      onChange={effects.linkState}\n                      placeholder={formatMessage(messages.newNetworkDefaultMtu)}\n                      type='text'\n                      value={mtu}\n                    />\n                    {isPrivate ? (\n                      <div>\n                        <label>{_('newNetworkEncapsulation')}</label>\n                        <Select\n                          name='encapsulation'\n                          onChange={effects.onChangeEncapsulation}\n                          options={[\n                            { label: 'GRE', value: 'gre' },\n                            { label: 'VxLAN', value: 'vxlan' },\n                          ]}\n                          value={encapsulation}\n                        />\n                        <Toggle onChange={effects.toggleEncrypted} value={encrypted} />{' '}\n                        <label>{_('newNetworkEncrypted')}</label>\n                        <div>\n                          <em>\n                            <Icon icon='info' /> {_('encryptionWarning')}\n                          </em>\n                        </div>\n                        <label>{_('newNetworkPreferredCenter')}</label>\n                        <SelectHost onChange={effects.onChangeCenter} predicate={hostPredicate} value={networkCenter} />\n                        <div>\n                          <em>\n                            <Icon icon='info' /> {_('preferredCenterTip')}\n                          </em>\n                        </div>\n                        <div className='mt-1'>\n                          {state.networks.map(({ pool, pif }, key) => (\n                            <div key={key}>\n                              <LineItem>\n                                <Item label={_('homeTypePool')}>\n                                  <span className={styles.inlineSelect}>\n                                    <SelectPool\n                                      onChange={value => effects.onChangeNetwork(key, value)}\n                                      value={pool}\n                                      predicate={pool => state.networkPoolPredicate(pool, key)}\n                                      required\n                                    />\n                                  </span>\n                                </Item>\n                                <Item label={_('pif')}>\n                                  <span className={styles.inlineSelect}>\n                                    <SelectPif\n                                      onChange={value => effects.onChangeNetwork(key, undefined, value)}\n                                      value={pif}\n                                      predicate={pif => state.networkPifPredicate(pif, key)}\n                                      required\n                                    />\n                                  </span>\n                                </Item>\n                                <Item>\n                                  <Button onClick={effects.onDeletePool} data-position={key}>\n                                    <Icon icon='new-vm-remove' />\n                                  </Button>\n                                </Item>\n                              </LineItem>\n                            </div>\n                          ))}\n                          <ActionButton handler={effects.addPool} disabled={state.disableAddPool} icon='add'>\n                            {_('addPool')}\n                          </ActionButton>\n                        </div>\n                      </div>\n                    ) : (\n                      <div>\n                        {bonded ? (\n                          <div>\n                            <label>{_('newNetworkBondMode')}</label>\n                            <Select onChange={effects.onChangeMode} options={modeOptions} required value={bondMode} />\n                          </div>\n                        ) : (\n                          <div>\n                            <label>{_('newNetworkVlan')}</label>\n                            <input\n                              className='form-control'\n                              name='vlan'\n                              onChange={effects.linkState}\n                              placeholder={formatMessage(messages.newNetworkDefaultVlan)}\n                              type='text'\n                              value={vlan}\n                            />\n                            <label>{_('nbd')}</label>\n                            <Select\n                              name='nbd'\n                              onChange={effects.onChangeNbd}\n                              options={[\n                                { label: _('noNbdConnection'), value: false },\n                                { label: _('nbdConnection'), value: true },\n                              ]}\n                              value={nbd}\n                            />\n                          </div>\n                        )}\n                      </div>\n                    )}\n                  </div>\n                </Section>\n              </Wizard>\n              <div className='form-group pull-right'>\n                <ActionButton\n                  btnStyle='primary'\n                  className='mr-1'\n                  form='networkCreation'\n                  handler={this._create}\n                  icon='new-network-create'\n                  redirectOnSuccess={`pools/${pool.id}/network`}\n                >\n                  {_('newNetworkCreate')}\n                </ActionButton>\n                <ActionButton handler={effects.reset} icon='reset'>\n                  {_('formReset')}\n                </ActionButton>\n              </div>\n            </form>\n          )}\n        </Page>\n      )\n    }\n  },\n])\nexport { NewNetwork as default }\n","/* eslint-disable react/no-string-refs */\nimport _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport includes from 'lodash/includes'\nimport info, { error } from 'notification'\nimport isEmpty from 'lodash/isEmpty'\nimport { isIpV6 } from 'ip-utils'\nimport map from 'lodash/map'\nimport Page from '../../page'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport store from 'store'\nimport Wizard, { Section } from 'wizard'\nimport { confirm } from 'modal'\nimport { adminOnly, connectStore, formatSize } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { injectIntl } from 'react-intl'\nimport { Password, Select } from 'form'\nimport { SelectHost } from 'select-objects'\nimport { Sr } from 'render-xo-item'\nimport { createCollectionWrapper, createFilter, createGetObjectsOfType, createSelector, getObject } from 'selectors'\nimport {\n  createSrExt,\n  createSrIso,\n  createSrIscsi,\n  createSrLvm,\n  createSrNfs,\n  createSrHba,\n  createSrSmb,\n  createSrZfs,\n  probeSrIscsiExists,\n  probeSrIscsiIqns,\n  probeSrIscsiLuns,\n  probeSrNfs,\n  probeSrNfsExists,\n  probeSrHba,\n  probeSrHbaExists,\n  probeZfs,\n} from 'xo'\n\n// ===================================================================\n\nconst NFS_VERSIONS = ['4', '4.1']\n\n// ===================================================================\n\nclass SelectScsiId extends Component {\n  static propTypes = {\n    onChange: PropTypes.func.isRequired,\n    options: PropTypes.array.isRequired,\n  }\n\n  _getOptions = createSelector(\n    () => this.props.options,\n    options =>\n      map(options, ({ id, path, scsiId, size, vendor, lun }) => ({\n        label: `${vendor} ${id}: ${scsiId} - LUN: ${lun} - ${path} (${formatSize(size)})`,\n        value: scsiId,\n      }))\n  )\n\n  _handleChange = opt => {\n    const { props } = this\n\n    this.setState({ value: opt.value }, () => props.onChange(opt.value))\n  }\n\n  componentDidMount() {\n    return this.componentDidUpdate()\n  }\n\n  componentDidUpdate() {\n    let options\n    if (this.state.value === null && (options = this._getOptions()).length === 1) {\n      this._handleChange(options[0])\n    }\n  }\n\n  state = { value: null }\n\n  render() {\n    return (\n      <Select clearable={false} onChange={this._handleChange} options={this._getOptions()} value={this.state.value} />\n    )\n  }\n}\n\nclass SelectIqn extends Component {\n  _getOptions = createSelector(\n    () => this.props.options,\n    options =>\n      map(options, ({ ip, iqn }, index) => ({\n        label: `${iqn} (${ip})`,\n        value: index,\n      }))\n  )\n\n  _handleChange = ({ value }) => {\n    const { props } = this\n\n    this.setState({ value }, () => props.onChange(props.options[value]))\n  }\n\n  componentDidMount() {\n    return this.componentDidUpdate()\n  }\n\n  componentDidUpdate() {\n    let options\n    if (this.state.value === null && (options = this._getOptions()).length === 1) {\n      this._handleChange(options[0])\n    }\n  }\n\n  state = { value: null }\n\n  render() {\n    return (\n      <Select clearable={false} onChange={this._handleChange} options={this._getOptions()} value={this.state.value} />\n    )\n  }\n}\n\n@injectIntl\nclass SelectLun extends Component {\n  static propTypes = {\n    onChange: PropTypes.func.isRequired,\n    options: PropTypes.array.isRequired,\n  }\n\n  _getOptions = createSelector(\n    () => this.props.options,\n    () => this.props.intl.formatMessage,\n    (options, formatMessage) =>\n      map(options, (lun, index) => ({\n        label: `LUN ${lun.id}: ${lun.serial} - ${\n          lun.size !== undefined ? formatSize(+lun.size) : formatMessage(messages.unknownSize)\n        } - (${lun.vendor})`,\n        value: index,\n      }))\n  )\n\n  _handleChange = ({ value }) => {\n    const { props } = this\n    this.setState({ value }, () => props.onChange(props.options[value]))\n  }\n\n  componentDidMount() {\n    return this.componentDidUpdate()\n  }\n\n  componentDidUpdate() {\n    let options\n    if (this.state.value === null && (options = this._getOptions()).length === 1) {\n      this._handleChange(options[0])\n    }\n  }\n\n  state = { value: null }\n\n  render() {\n    return (\n      <Select clearable={false} onChange={this._handleChange} options={this._getOptions()} value={this.state.value} />\n    )\n  }\n}\n\n// ===================================================================\n\nconst SR_TYPE_TO_LABEL = {\n  ext: 'ext (local)',\n  hba: 'HBA',\n  iscsi: 'iSCSI',\n  local: 'Local',\n  lvm: 'LVM (local)',\n  nfs: 'NFS',\n  nfsiso: 'NFS ISO',\n  smb: 'SMB',\n  smbiso: 'SMB ISO',\n  zfs: 'ZFS (local)',\n}\n\nconst SR_GROUP_TO_LABEL = {\n  vdisr: 'VDI SR',\n  isosr: 'ISO SR',\n}\n\nconst SR_TYPE_REQUIRE_DISK_FORMATTING = ['ext', 'lvm']\n\nconst typeGroups = {\n  vdisr: ['ext', 'hba', 'iscsi', 'lvm', 'nfs', 'smb', 'zfs'],\n  isosr: ['local', 'nfsiso', 'smbiso'],\n}\n\nconst getSrPath = id => (id !== undefined ? `/srs/${id}` : undefined)\n\n// ===================================================================\n\n@adminOnly\n@injectIntl\n@connectStore(() => ({\n  hosts: createGetObjectsOfType('host'),\n  srs: createGetObjectsOfType('SR'),\n}))\nexport default class New extends Component {\n  constructor(props) {\n    super(props)\n\n    const hostId = props.location.query.host\n\n    this.state = {\n      description: undefined,\n      existingSrs: undefined,\n      host: hostId && getObject(store.getState(), hostId),\n      iqn: undefined,\n      iqns: undefined,\n      loading: 0,\n      lockCreation: undefined,\n      lun: undefined,\n      luns: undefined,\n      nfsVersion: '',\n      hbaDevices: undefined,\n      name: undefined,\n      path: undefined,\n      paths: undefined,\n      type: undefined,\n      usage: undefined,\n      zfsPools: undefined,\n    }\n  }\n\n  getHostSrs = createFilter(\n    () => this.props.srs,\n    createSelector(\n      () => this.state.host,\n      host => host != null && (sr => sr.$container === host.$pool || sr.$container === host.id)\n    )\n  )\n\n  getUsedSrs = createCollectionWrapper(\n    createSelector(\n      this.getHostSrs,\n      () => this.state.existingSrs,\n      (hostSrs, existingSrs) => {\n        if (existingSrs === undefined) {\n          return []\n        }\n\n        const usedSrs = []\n        existingSrs.forEach(({ uuid }) => {\n          if (uuid in hostSrs) {\n            usedSrs.push(hostSrs[uuid])\n          }\n        })\n\n        return usedSrs\n      }\n    )\n  )\n\n  getUnusedSrs = createFilter(\n    () => this.state.existingSrs,\n    createSelector(this.getUsedSrs, usedSrs => existingSr => !usedSrs.some(sr => sr.uuid === existingSr.uuid))\n  )\n\n  _handleSubmit = async srUuid => {\n    const { description, device, localPath, name, password, port, server, username, zfsLocation } = this.refs\n    const { host, iqn, lun, nfsOptions, nfsVersion, path, scsiId, type } = this.state\n\n    const createMethodFactories = {\n      nfs: () =>\n        createSrNfs(\n          host.id,\n          name.value,\n          description.value,\n          server.value,\n          path,\n          nfsVersion !== '' ? nfsVersion : undefined,\n          nfsOptions,\n          srUuid\n        ),\n      smb: () => createSrSmb(host.id, name.value, description.value, server.value, username.value, password.value),\n      hba: () => createSrHba(host.id, name.value, description.value, scsiId, srUuid),\n      iscsi: async () => {\n        if (srUuid === undefined) {\n          const previous = await probeSrIscsiExists(\n            host.id,\n            iqn.ip,\n            iqn.iqn,\n            lun.scsiId,\n            +port.value,\n            username && username.value,\n            password && password.value\n          )\n          if (previous && previous.length > 0) {\n            try {\n              await confirm({\n                title: _('existingLunModalTitle'),\n                body: <p>{_('existingLunModalText')}</p>,\n              })\n            } catch (error) {\n              return\n            }\n          }\n        }\n        return createSrIscsi(\n          host.id,\n          name.value,\n          description.value,\n          iqn.ip,\n          iqn.iqn,\n          lun.scsiId,\n          +port.value,\n          username && username.value,\n          password && password.value,\n          srUuid\n        )\n      },\n      lvm: () => createSrLvm(host.id, name.value, description.value, device.value),\n      ext: () => createSrExt(host.id, name.value, description.value, device.value),\n      zfs: () => createSrZfs(host.id, name.value, description.value, zfsLocation.value),\n      local: () => createSrIso(host.id, name.value, description.value, localPath.value, 'local'),\n      nfsiso: () =>\n        createSrIso(\n          host.id,\n          name.value,\n          description.value,\n          isIpV6(server.value) ? `[${server.value}]:${path}` : `${server.value}:${path}`,\n          'nfs',\n          username && username.value,\n          password && password.value,\n          nfsVersion !== '' ? nfsVersion : undefined,\n          nfsOptions,\n          srUuid\n        ),\n      smbiso: () =>\n        createSrIso(\n          host.id,\n          name.value,\n          description.value,\n          server.value,\n          'smb',\n          username && username.value,\n          password && password.value,\n          undefined,\n          undefined,\n          srUuid\n        ),\n    }\n\n    try {\n      if (SR_TYPE_REQUIRE_DISK_FORMATTING.includes(type)) {\n        await confirm({\n          title: _('newSr'),\n          body: <p>{_('newSrConfirm', { name: device.value })}</p>,\n        })\n      }\n      const existingSrsLength = this.state.existingSrs?.length ?? 0\n      // `existingSrs` is defined if the SR type is `NFS` or `ISCSI` and if at least one SR is detected\n      // Ignore NFS type because it is not supposed to erase data\n      if (type !== 'nfs' && existingSrsLength !== 0 && srUuid === undefined) {\n        await confirm({\n          title: _('newSr'),\n          body: _('newSrExistingSr', { path: <b>{this.state.path}</b>, n: existingSrsLength }),\n          strongConfirm: {\n            messageId: 'newSrTitle',\n          },\n        })\n      }\n      return await createMethodFactories[type]()\n    } catch (err) {\n      if (err === undefined) {\n        return\n      }\n      error('SR Creation', err.message || String(err))\n    }\n  }\n\n  _handleSrHostSelection = async host => {\n    this.setState({ host })\n    await this._probe(host, this.state.type)\n  }\n  _handleNameChange = event => this.setState({ name: event.target.value })\n  _handleDescriptionChange = event => this.setState({ description: event.target.value })\n\n  _handleSrTypeSelection = async event => {\n    const type = event.target.value\n    this.setState({\n      existingSrs: undefined,\n      hbaDevices: undefined,\n      iqns: undefined,\n      paths: undefined,\n      summary: includes(['ext', 'lvm', 'local', 'smb', 'smbiso', 'hba', 'zfs'], type),\n      type,\n      usage: undefined,\n    })\n    await this._probe(this.state.host, type)\n  }\n\n  _handleSrHbaSelection = async scsiId => {\n    this.setState({\n      existingSrs: await probeSrHbaExists(this.state.host.id, scsiId),\n      scsiId,\n      usage: true,\n    })\n  }\n\n  _handleSrIqnSelection = async iqn => {\n    const { username, password } = this.refs\n    const { host } = this.state\n\n    try {\n      this.setState(({ loading }) => ({ loading: loading + 1 }))\n      const luns = await probeSrIscsiLuns(\n        host.id,\n        iqn.ip,\n        iqn.iqn,\n        username && username.value,\n        password && password.value\n      )\n      this.setState({\n        iqn,\n        luns,\n      })\n    } catch (err) {\n      error('LUNs Detection', err.message || String(err))\n    } finally {\n      this.setState(({ loading }) => ({ loading: loading - 1 }))\n    }\n  }\n\n  _handleSrLunSelection = async lun => {\n    const { password, port, username } = this.refs\n    const { host, iqn } = this.state\n\n    try {\n      this.setState(({ loading }) => ({ loading: loading + 1 }))\n      const list = await probeSrIscsiExists(\n        host.id,\n        iqn.ip,\n        iqn.iqn,\n        lun.scsiId,\n        +port.value,\n        username && username.value,\n        password && password.value\n      )\n      this.setState({\n        existingSrs: list,\n        lun,\n        usage: true,\n        summary: true,\n      })\n    } catch (err) {\n      error('iSCSI Error', err.message || String(err))\n    } finally {\n      this.setState(({ loading }) => ({ loading: loading - 1 }))\n    }\n  }\n\n  _handleAuthChoice = () => {\n    const auth = this.refs.auth.checked\n    this.setState({\n      auth,\n    })\n  }\n\n  _handleSearchServer = async () => {\n    const { password, port, server, username } = this.refs\n\n    const { host, nfsVersion, type } = this.state\n\n    try {\n      if (type === 'nfs' || type === 'nfsiso') {\n        const paths = await probeSrNfs(host.id, server.value, nfsVersion !== '' ? nfsVersion : undefined)\n        this.setState({\n          usage: undefined,\n          paths,\n        })\n      } else if (type === 'iscsi') {\n        const iqns = await probeSrIscsiIqns(\n          host.id,\n          server.value,\n          +port.value,\n          username && username.value,\n          password && password.value\n        )\n        if (!iqns.length) {\n          info('iSCSI Detection', 'No IQNs found')\n        } else {\n          this.setState({\n            usage: undefined,\n            iqns,\n          })\n        }\n      }\n    } catch (err) {\n      error('Server Detection', err.message || String(err))\n    }\n  }\n\n  _handleSrPathSelection = async path => {\n    const { server } = this.refs\n    const { host, nfsVersion } = this.state\n\n    try {\n      this.setState(({ loading }) => ({ loading: loading + 1 }))\n      this.setState({\n        existingSrs: await probeSrNfsExists(host.id, server.value, path, nfsVersion !== '' ? nfsVersion : undefined),\n        path,\n        usage: true,\n        summary: true,\n      })\n    } catch (err) {\n      this.setState({ summary: false, usage: false })\n      error('NFS Error', err.message || String(err))\n    } finally {\n      this.setState(({ loading }) => ({ loading: loading - 1 }))\n    }\n  }\n\n  _handleNfsVersion = ({ target: { value } }) => {\n    this.setState({\n      nfsVersion: value,\n    })\n  }\n\n  _probe = async (host, type) => {\n    const probeMethodFactories = {\n      hba: async hostId => ({\n        hbaDevices: await probeSrHba(hostId),\n      }),\n      zfs: async hostId => ({\n        zfsPools: await probeZfs(hostId),\n      }),\n    }\n    if (probeMethodFactories[type] !== undefined && host != null) {\n      this.setState(({ loading }) => ({ loading: loading + 1 }))\n      try {\n        const probeResult = await probeMethodFactories[type](host.id)\n        this.setState(probeResult)\n      } catch (err) {\n        error('Device detection failed', err.message || String(err))\n      } finally {\n        this.setState(({ loading }) => ({ loading: loading - 1 }))\n      }\n    }\n  }\n\n  _renderHeader() {\n    return (\n      <Container>\n        <Row>\n          <Col>\n            <h2>\n              <Icon icon='sr' /> {_('newSrTitle')}\n            </h2>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n\n  render() {\n    const { hosts } = this.props\n    const {\n      auth,\n      host,\n      iqns,\n      hbaDevices,\n      loading,\n      lockCreation,\n      lun,\n      luns,\n      nfsVersion,\n      nfsSubdir,\n      path,\n      paths,\n      selectedMainPath,\n      summary,\n      type,\n      usage,\n      zfsPools,\n    } = this.state\n    const { formatMessage } = this.props.intl\n\n    const used = this.getUsedSrs()\n    const unused = this.getUnusedSrs()\n\n    return (\n      <Page header={this._renderHeader()}>\n        <form id='newSrForm'>\n          <Wizard>\n            <Section icon='sr' title='newSrGeneral'>\n              <fieldset className='form-group'>\n                <label>{_('newSrHost')}</label>\n                <SelectHost value={host} options={hosts} onChange={this._handleSrHostSelection} />\n                <label htmlFor='srName'>{_('newSrName')}</label>\n                <input\n                  id='srName'\n                  className='form-control'\n                  placeholder={formatMessage(messages.newSrNamePlaceHolder)}\n                  ref='name'\n                  onBlur={this._handleNameChange}\n                  required\n                  type='text'\n                />\n                <label htmlFor='srDescription'>{_('newSrDescription')}</label>\n                <input\n                  id='srDescription'\n                  className='form-control'\n                  placeholder={formatMessage(messages.newSrDescPlaceHolder)}\n                  ref='description'\n                  onBlur={this._handleDescriptionChange}\n                  required\n                  type='text'\n                />\n                <label htmlFor='selectSrType'>{_('newSrTypeSelection')}</label>\n                <select\n                  className='form-control'\n                  defaultValue={null}\n                  id='selectSrType'\n                  onChange={this._handleSrTypeSelection}\n                  required\n                >\n                  <option value={null}>{formatMessage(messages.noSelectedValue)}</option>\n                  {map(typeGroups, (types, group) => (\n                    <optgroup key={group} label={SR_GROUP_TO_LABEL[group]}>\n                      {map(types, type => (\n                        <option key={type} value={type}>\n                          {SR_TYPE_TO_LABEL[type]}\n                        </option>\n                      ))}\n                    </optgroup>\n                  ))}\n                </select>\n              </fieldset>\n            </Section>\n            <Section icon='settings' title='newSrSettings'>\n              {host && (\n                <fieldset>\n                  {(type === 'nfs' || type === 'nfsiso') && [\n                    <fieldset key='nfsServer'>\n                      <label htmlFor='srServer'>{_('newSrServer')}</label>\n                      <div className='input-group'>\n                        <input\n                          id='srServer'\n                          className='form-control'\n                          placeholder={formatMessage(messages.newSrNfsAddressPlaceHolder)}\n                          ref='server'\n                          required\n                          type='text'\n                        />\n                        <span className='input-group-btn'>\n                          <ActionButton icon='search' handler={this._handleSearchServer} />\n                        </span>\n                      </div>\n                    </fieldset>,\n                    <fieldset key='nfsVersion'>\n                      <label htmlFor='selectNfsVersion'>{_('newSrNfs')}</label>\n                      <select\n                        className='form-control'\n                        id='selectNfsVersion'\n                        onChange={this._handleNfsVersion}\n                        value={nfsVersion}\n                      >\n                        <option value=''>{formatMessage(messages.newSrNfsDefaultVersion)}</option>\n                        {map(NFS_VERSIONS, option => (\n                          <option key={option} value={option}>\n                            {option}\n                          </option>\n                        ))}\n                      </select>\n                    </fieldset>,\n                    <fieldset key='nfsOptions'>\n                      <label>{_('newSrNfsOptions')}</label>\n                      <input\n                        className='form-control'\n                        onChange={this.linkState('nfsOptions')}\n                        type='text'\n                        value={this.state.nfsOptions}\n                      />\n                    </fieldset>,\n                  ]}\n                  {type === 'hba' && (\n                    <fieldset>\n                      <label>{_('newSrLun')}</label>\n                      <div>\n                        {!isEmpty(hbaDevices) ? (\n                          <SelectScsiId options={hbaDevices} onChange={this._handleSrHbaSelection} />\n                        ) : (\n                          <em>{_('newSrNoHba')}</em>\n                        )}\n                      </div>\n                    </fieldset>\n                  )}\n                  {paths && (\n                    <fieldset>\n                      <label htmlFor='selectSrPath'>{_('newSrPath')}</label>\n                      <select\n                        className='form-control'\n                        defaultValue=''\n                        id='selectSrPath'\n                        onChange={event => {\n                          const selectedPath = event.target.value\n                          this.setState({ selectedMainPath: selectedPath })\n                          this._handleSrPathSelection(selectedPath)\n                        }}\n                        ref='path'\n                        required\n                      >\n                        <option disabled value=''>\n                          {formatMessage(messages.noSelectedValue)}\n                        </option>\n                        {map(paths, (item, key) => (\n                          <option key={key} value={item.path}>\n                            {item.path}\n                          </option>\n                        ))}\n                      </select>\n                      {(type === 'nfs' || type === 'nfsiso') && selectedMainPath !== undefined && (\n                        <div>\n                          <label htmlFor='nfsSubdirectory'>{_('subdirectory')}</label>\n                          <div className='input-group'>\n                            <span className='input-group-addon'>/</span>\n                            <input\n                              className='form-control'\n                              id='nfsSubdirectory'\n                              type='text'\n                              onChange={this.linkState('nfsSubdir')}\n                            />\n                            <span className='input-group-btn'>\n                              <ActionButton\n                                icon='search'\n                                handler={() => {\n                                  this._handleSrPathSelection(selectedMainPath.concat('/' + (nfsSubdir?.trim() ?? '')))\n                                }}\n                              />\n                            </span>\n                          </div>\n                        </div>\n                      )}\n                    </fieldset>\n                  )}\n                  {type === 'iscsi' && (\n                    <fieldset>\n                      <label htmlFor='srServer'>\n                        {_('newSrServer')} ({_('newSrAuth')}\n                        <input\n                          type='checkbox'\n                          ref='auth'\n                          onChange={event => {\n                            this._handleAuthChoice()\n                          }}\n                        />\n                        )\n                      </label>\n                      <div className='form-inline'>\n                        <input\n                          id='srServer'\n                          className='form-control'\n                          placeholder={formatMessage(messages.newSrIscsiAddressPlaceHolder)}\n                          ref='server'\n                          required\n                          type='text'\n                        />\n                        {' : '}\n                        <input\n                          id='srServer'\n                          className='form-control'\n                          placeholder={formatMessage(messages.newSrPortPlaceHolder)}\n                          ref='port'\n                          type='text'\n                        />\n                        <ActionButton icon='search' handler={this._handleSearchServer} />\n                      </div>\n                      {auth && (\n                        <fieldset>\n                          <label htmlFor='srServerUser'>{_('newSrUsername')}</label>\n                          <input\n                            id='srServerUser'\n                            className='form-control'\n                            placeholder={formatMessage(messages.newSrUsernamePlaceHolder)}\n                            ref='username'\n                            required\n                            type='text'\n                          />\n                          <label>{_('newSrPassword')}</label>\n                          <Password\n                            placeholder={formatMessage(messages.newSrPasswordPlaceHolder)}\n                            ref='password'\n                            required\n                          />\n                        </fieldset>\n                      )}\n                    </fieldset>\n                  )}\n                  {iqns && (\n                    <fieldset>\n                      <label>{_('newSrIqn')}</label>\n                      <SelectIqn options={iqns} onChange={this._handleSrIqnSelection} />\n                    </fieldset>\n                  )}\n                  {luns && (\n                    <fieldset>\n                      <label>{_('newSrLun')}</label>\n                      <SelectLun options={luns} onChange={this._handleSrLunSelection} />\n                    </fieldset>\n                  )}\n                  {(type === 'smb' || type === 'smbiso') && (\n                    <fieldset>\n                      <label htmlFor='srServer'>{_('newSrServer')}</label>\n                      <input\n                        id='srServer'\n                        className='form-control'\n                        placeholder={formatMessage(messages.newSrSmbAddressPlaceHolder)}\n                        ref='server'\n                        required\n                        type='text'\n                      />\n                      <label htmlFor='srServerUser'>{_('newSrUsername')}</label>\n                      <input\n                        id='srServerUser'\n                        className='form-control'\n                        placeholder={formatMessage(messages.newSrUsernamePlaceHolder)}\n                        ref='username'\n                        type='text'\n                      />\n                      <label>{_('newSrPassword')}</label>\n                      <Password placeholder={formatMessage(messages.newSrPasswordPlaceHolder)} ref='password' />\n                    </fieldset>\n                  )}\n                  {(type === 'lvm' || type === 'ext') && (\n                    <fieldset>\n                      <label htmlFor='srDevice'>{_('newSrDevice')}</label>\n                      <input\n                        id='srDevice'\n                        className='form-control'\n                        placeholder={formatMessage(messages.newSrLvmDevicePlaceHolder)}\n                        ref='device'\n                        required\n                        type='text'\n                      />\n                    </fieldset>\n                  )}\n                  {type === 'local' && (\n                    <fieldset>\n                      <label htmlFor='srPath'>{_('newSrPath')}</label>\n                      <input\n                        id='srPath'\n                        className='form-control'\n                        placeholder={formatMessage(messages.newSrLocalPathPlaceHolder)}\n                        ref='localPath'\n                        required\n                        type='text'\n                      />\n                    </fieldset>\n                  )}\n                  {type === 'zfs' && (\n                    <fieldset>\n                      <label htmlFor='selectSrLocation'>{_('srLocation')}</label>\n                      <select className='form-control' defaultValue='' id='selectSrLocation' ref='zfsLocation' required>\n                        <option value=''>\n                          {isEmpty(zfsPools)\n                            ? formatMessage(messages.noSharedZfsAvailable)\n                            : formatMessage(messages.noSelectedValue)}\n                        </option>\n                        {map(zfsPools, (pool, poolName) => (\n                          <option key={poolName} value={pool.mountpoint}>\n                            {poolName} - {pool.mountpoint}\n                          </option>\n                        ))}\n                      </select>\n                    </fieldset>\n                  )}\n                </fieldset>\n              )}\n              {loading !== 0 && <Icon icon='loading' />}\n            </Section>\n            <Section icon='shown' title='newSrUsage'>\n              {usage && (\n                <div>\n                  {map(unused, sr => (\n                    <p key={sr.uuid}>\n                      {sr.uuid}\n                      <span className='pull-right'>\n                        <ActionButton\n                          btnStyle='primary'\n                          handler={this._handleSubmit}\n                          handlerParam={sr.uuid}\n                          icon='connect'\n                          tooltip={_('reattachNewSrTooltip')}\n                        />\n                      </span>\n                    </p>\n                  ))}\n                  {map(used, sr => (\n                    <p key={sr.id}>\n                      <Sr id={sr.id} link />\n                      <span className='pull-right'>\n                        {/* FIXME Goes to sr view */}\n                        <a className='btn btn-warning'>{_('newSrInUse')}</a>\n                      </span>\n                    </p>\n                  ))}\n                </div>\n              )}\n            </Section>\n            <Section icon='summary' title='newSrSummary'>\n              {summary && (\n                <div>\n                  <dl className='dl-horizontal'>\n                    <dt>{_('newSrName')}</dt>\n                    <dd>{this.refs.name && this.refs.name.value}</dd>\n                    <dt>{_('newSrDescription')}</dt>\n                    <dd>{this.refs.description && this.refs.description.value}</dd>\n                    <dt>{_('newSrType')}</dt>\n                    <dd>{type}</dd>\n                  </dl>\n                  {type === 'iscsi' && (\n                    <dl className='dl-horizontal'>\n                      <dt>{_('newSrSize')}</dt>\n                      <dd>{lun.size !== undefined ? formatSize(+lun.size) : _('unknownSize')}</dd>\n                    </dl>\n                  )}\n                  {includes(['nfs', 'hba'], type) && (\n                    <dl className='dl-horizontal'>\n                      <dt>{_('newSrPath')}</dt>\n                      <dd>{path}</dd>\n                    </dl>\n                  )}\n                  <ActionButton\n                    btnStyle='primary'\n                    disabled={lockCreation}\n                    form='newSrForm'\n                    handler={this._handleSubmit}\n                    icon='run'\n                    redirectOnSuccess={getSrPath}\n                  >\n                    {_('newSrCreate')}\n                  </ActionButton>\n                </div>\n              )}\n            </Section>\n          </Wizard>\n        </form>\n      </Page>\n    )\n  }\n}\n/* eslint-enable react/no-string-refs */\n","module.exports = {\n    \"container\": \"mc672246fa_container\",\n    \"header\": \"mc672246fa_header\",\n    \"content\": \"mc672246fa_content\"\n};","import { messages } from 'intl'\nimport DocumentTitle from 'react-document-title'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { injectIntl } from 'react-intl'\n\nimport styles from './index.css'\n\nconst Page = ({ children, collapsedHeader, formatTitle, header, intl, title }) => {\n  const { formatMessage } = intl\n\n  const content = (\n    <div className={styles.container}>\n      {!collapsedHeader && <nav className={'page-header ' + styles.header}>{header}</nav>}\n      <div className={styles.content}>{children}</div>\n    </div>\n  )\n\n  return title ? (\n    <DocumentTitle title={formatTitle ? formatMessage(messages[title]) : title}>{content}</DocumentTitle>\n  ) : (\n    content\n  )\n}\n\nPage.propTypes = {\n  children: PropTypes.node,\n  collapsedHeader: PropTypes.bool,\n  formatTitle: PropTypes.bool,\n  header: PropTypes.node,\n  title: PropTypes.string,\n}\n\nexport default injectIntl(Page)\n","import _ from 'intl'\nimport ActionBar, { Action } from 'action-bar'\nimport Component from 'base-component'\nimport React from 'react'\nimport { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'\nimport find from 'lodash/find.js'\nimport { addSubscriptions, connectStore, noop } from 'utils'\nimport { addHostsToPool, disableServer, subscribeServers } from 'xo'\n\n@connectStore({\n  hosts: createGetObjectsOfType('host'),\n  isAdmin,\n})\n@addSubscriptions(\n  ({ isAdmin }) =>\n    isAdmin && {\n      servers: subscribeServers,\n    }\n)\nexport default class PoolActionBar extends Component {\n  _getMasterAddress = createSelector(\n    () => this.props.pool && this.props.pool.master,\n    () => this.props.hosts,\n    (poolMaster, hosts) => {\n      const master = find(hosts, { id: poolMaster })\n\n      return master && master.address\n    }\n  )\n\n  _getServer = createSelector(\n    this._getMasterAddress,\n    () => this.props.servers,\n    (masterAddress, servers) => find(servers, { host: masterAddress })\n  )\n\n  _disconnectServer = () => disableServer(this._getServer())\n\n  render() {\n    const { pool } = this.props\n\n    return (\n      <ActionBar display='icon' handlerParam={pool}>\n        <Action handler={noop} icon='add-sr' label={_('addSrLabel')} redirectOnSuccess={`new/sr?host=${pool.master}`} />\n        <Action handler={noop} icon='add-vm' label={_('addVmLabel')} redirectOnSuccess={`vms/new?pool=${pool.id}`} />\n        <Action handler={addHostsToPool} icon='add-host' label={_('addHostsLabel')} />\n        <Action\n          handler={this._disconnectServer}\n          icon='disconnect'\n          label={_('disconnectServer')}\n          redirectOnSuccess='/home'\n        />\n      </ActionBar>\n    )\n  }\n}\n","import _ from 'intl'\nimport Copiable from 'copiable'\nimport Icon from 'icon'\nimport PoolActionBar from './action-bar'\nimport Page from '../page'\nimport pick from 'lodash/pick'\nimport React, { cloneElement, Component } from 'react'\nimport { NavLink, NavTabs } from 'nav'\nimport { Text } from 'editable'\nimport { editPool } from 'xo'\nimport { Container, Row, Col } from 'grid'\nimport { connectStore, routes } from 'utils'\nimport { createGetObject, createGetObjectMessages, createGetObjectsOfType, createSelector } from 'selectors'\n\nimport TabAdvanced from './tab-advanced'\nimport TabGeneral from './tab-general'\nimport TabStats from './tab-stats'\nimport TabLogs from './tab-logs'\nimport TabNetwork from './tab-network'\nimport TabPatches from './tab-patches'\n\n// ===================================================================\n\n@routes('general', {\n  advanced: TabAdvanced,\n  general: TabGeneral,\n  logs: TabLogs,\n  network: TabNetwork,\n  patches: TabPatches,\n  stats: TabStats,\n})\n@connectStore(() => {\n  const getPool = createGetObject()\n\n  const getMaster = createGetObject((state, props) => getPool(state, props).master)\n\n  const getNetworks = createGetObjectsOfType('network')\n    .filter(\n      createSelector(\n        getPool,\n        ({ id }) =>\n          network =>\n            network.$pool === id\n      )\n    )\n    .sort()\n\n  const getPifs = createGetObjectsOfType('PIF')\n    .filter(\n      createSelector(\n        getPool,\n        ({ id }) =>\n          pif =>\n            pif.$pool === id\n      )\n    )\n    .sort()\n\n  const getHosts = createGetObjectsOfType('host')\n    .filter(\n      createSelector(\n        getPool,\n        ({ id }) =>\n          obj =>\n            obj.$pool === id\n      )\n    )\n    .sort()\n\n  const getPoolSrs = createGetObjectsOfType('SR')\n    .filter(\n      createSelector(\n        getPool,\n        ({ id }) =>\n          sr =>\n            sr.$pool === id\n      )\n    )\n    .sort()\n\n  const getNumberOfVms = createGetObjectsOfType('VM').count(\n    createSelector(\n      getPool,\n      ({ id }) =>\n        obj =>\n          obj.$pool === id\n    )\n  )\n\n  const getLogs = createGetObjectMessages(getPool)\n\n  return (state, props) => {\n    const pool = getPool(state, props)\n    if (!pool) {\n      return {}\n    }\n\n    return {\n      hosts: getHosts(state, props),\n      logs: getLogs(state, props),\n      master: getMaster(state, props),\n      networks: getNetworks(state, props),\n      nVms: getNumberOfVms(state, props),\n      pifs: getPifs(state, props),\n      pool,\n      srs: getPoolSrs(state, props),\n    }\n  }\n})\nexport default class Pool extends Component {\n  _setNameDescription = nameDescription => editPool(this.props.pool, { name_description: nameDescription })\n  _setNameLabel = nameLabel => editPool(this.props.pool, { name_label: nameLabel })\n\n  header() {\n    const { pool } = this.props\n    if (!pool) {\n      return <Icon icon='loading' />\n    }\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={6} className='header-title'>\n            <h2>\n              <Icon icon='pool' /> <Text value={pool.name_label} onChange={this._setNameLabel} />\n            </h2>\n            <Copiable tagName='pre' className='text-muted mb-0'>\n              {pool.uuid}\n            </Copiable>\n            <span>\n              <Text value={pool.name_description} onChange={this._setNameDescription} />\n            </span>\n          </Col>\n          <Col mediumSize={6}>\n            <div className='text-xs-center'>\n              <PoolActionBar pool={pool} />\n            </div>\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <NavTabs>\n              <NavLink to={`/pools/${pool.id}/general`}>{_('generalTabName')}</NavLink>\n              <NavLink to={`/pools/${pool.id}/stats`}>{_('statsTabName')}</NavLink>\n              <NavLink to={`/pools/${pool.id}/network`}>{_('networkTabName')}</NavLink>\n              <NavLink to={`/pools/${pool.id}/patches`}>{_('patchesTabName')}</NavLink>\n              <NavLink to={`/pools/${pool.id}/logs`}>{_('logsTabName')}</NavLink>\n              <NavLink to={`/pools/${pool.id}/advanced`}>{_('advancedTabName')}</NavLink>\n            </NavTabs>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n\n  render() {\n    const { pool } = this.props\n    if (!pool) {\n      return <h1>{_('statusLoading')}</h1>\n    }\n    const childProps = Object.assign(\n      pick(this.props, ['hosts', 'logs', 'master', 'networks', 'nVms', 'pifs', 'pool', 'srs'])\n    )\n    return (\n      <Page header={this.header()} title={pool.name_label}>\n        {cloneElement(this.props.children, childProps)}\n      </Page>\n    )\n  }\n}\n","import React from 'react'\n\nimport _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport ActionRowButton from 'action-row-button'\nimport Component from 'base-component'\nimport Copiable from 'copiable'\nimport Icon from 'icon'\nimport renderXoItem, { Network, Sr } from 'render-xo-item'\nimport SelectFiles from 'select-files'\nimport TabButton from 'tab-button'\nimport Tooltip from 'tooltip'\nimport Upgrade from 'xoa-upgrade'\nimport { addSubscriptions, connectStore } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { CustomFields } from 'custom-fields'\nimport { injectIntl } from 'react-intl'\nimport { forEach, map, values } from 'lodash'\nimport { SelectSr } from 'select-objects'\nimport { Text, XoSelect } from 'editable'\nimport { Toggle } from 'form'\nimport {\n  createGetObject,\n  createGetObjectsOfType,\n  createGroupBy,\n  createCollectionWrapper,\n  createSelector,\n} from 'selectors'\nimport {\n  editPool,\n  enableHa,\n  disableHa,\n  installSupplementalPackOnAllHosts,\n  isSrWritable,\n  rollingPoolReboot,\n  setDefaultSr,\n  setHostsMultipathing,\n  setPoolMaster,\n  setRemoteSyslogHost,\n  setRemoteSyslogHosts,\n  subscribeHvSupportedVersions,\n  subscribePlugins,\n  subscribeXcpngLicenses,\n  synchronizeNetbox,\n} from 'xo'\nimport { injectState, provideState } from 'reaclette'\nimport { SelectSuspendSr } from 'select-suspend-sr'\nimport { satisfies } from 'semver'\n\nimport decorate from '../../common/apply-decorators'\nimport PoolBindLicenseModal from '../../common/xo/pool-bind-licenses-modal/ index'\nimport { confirm } from '../../common/modal'\nimport { error } from '../../common/notification'\nimport { Host, Pool } from '../../common/render-xo-item'\nimport { isAdmin } from '../../common/selectors'\nimport { ENTERPRISE, SOURCES, getXoaPlan } from '../../common/xoa-plans'\n\nconst BindLicensesButton = decorate([\n  addSubscriptions({\n    hvSupportedVersions: subscribeHvSupportedVersions,\n    xcpLicenses: subscribeXcpngLicenses,\n  }),\n  connectStore({\n    hosts: createGetObjectsOfType('host'),\n  }),\n  provideState({\n    effects: {\n      async handleBindLicense() {\n        const { poolHosts, xcpLicenses } = this.props\n\n        if (xcpLicenses.length < poolHosts.length) {\n          return error(_('licensesBinding'), _('notEnoughXcpngLicenses'))\n        }\n\n        const hostsWithoutLicense = poolHosts.filter(host => {\n          const license = this.state.xcpngLicenseByBoundObjectId?.[host.id]\n          return license === undefined || license.expires < Date.now()\n        })\n        const licenseIdByHost = await confirm({\n          body: <PoolBindLicenseModal hosts={hostsWithoutLicense} />,\n          icon: 'connect',\n          title: _('licensesBinding'),\n        })\n        const licensesByHost = {}\n\n        // Pass values into a Set in order to remove duplicated licenseId\n        const nLicensesToBind = new Set(values(licenseIdByHost)).size\n\n        if (nLicensesToBind !== hostsWithoutLicense.length) {\n          return error(_('licensesBinding'), _('allHostsMustBeBound'))\n        }\n\n        const fullySupportedPoolIds = []\n        const unsupportedXcpngHostIds = []\n        forEach(licenseIdByHost, (licenseId, hostId) => {\n          const license = this.state.xcpngLicenseById[licenseId]\n          const boundHost = this.props.hosts[license.boundObjectId]\n          const hostToBind = this.props.hosts[hostId]\n          const poolId = boundHost?.$pool\n          const poolLicenseInfo = this.state.poolLicenseInfoByPoolId[poolId]\n          licensesByHost[hostId] = license\n\n          if (poolLicenseInfo !== undefined && poolLicenseInfo.supportLevel === 'total' && poolLicenseInfo.nHosts > 1) {\n            fullySupportedPoolIds.push(poolId)\n          }\n\n          if (!satisfies(hostToBind.version, this.props.hvSupportedVersions['XCP-ng'])) {\n            unsupportedXcpngHostIds.push(hostToBind.id)\n          }\n        })\n\n        if (fullySupportedPoolIds.length !== 0) {\n          await confirm({\n            body: (\n              <div>\n                <p>{_('confirmRebindLicenseFromFullySupportedPool')}</p>\n                <ul>\n                  {fullySupportedPoolIds.map(poolId => (\n                    <li key={poolId}>\n                      <Pool id={poolId} link newTab />\n                    </li>\n                  ))}\n                </ul>\n              </div>\n            ),\n            title: _('licensesBinding'),\n          })\n        }\n\n        if (unsupportedXcpngHostIds.length !== 0) {\n          await confirm({\n            body: (\n              <div>\n                <p>{_('confirmBindingOnUnsupportedHost', { nLicenses: unsupportedXcpngHostIds.length })}</p>\n                <ul>\n                  {unsupportedXcpngHostIds.map(hostId => (\n                    <li key={hostId}>\n                      <Host id={hostId} link newTab />\n                    </li>\n                  ))}\n                </ul>\n              </div>\n            ),\n            title: _('licensesBinding'),\n          })\n        }\n\n        await this.effects.bindXcpngLicenses(licensesByHost)\n      },\n    },\n    computed: {\n      isBindLicenseAvailable: (state, props) =>\n        getXoaPlan() !== SOURCES && state.poolLicenseInfoByPoolId[props.pool.id].supportLevel !== 'total',\n      isXcpngPool: (_, { poolHosts }) => poolHosts[0].productBrand === 'XCP-ng',\n    },\n  }),\n  injectState,\n  ({ effects, state }) => (\n    <ActionButton\n      btnStyle='primary'\n      disabled={!state.isXcpngPool || !state.isBindLicenseAvailable}\n      handler={effects.handleBindLicense}\n      icon='connect'\n      tooltip={\n        getXoaPlan() === SOURCES\n          ? _('poolSupportSourceUsers')\n          : !state.isXcpngPool\n            ? _('poolSupportXcpngOnly')\n            : state.isBindLicenseAvailable\n              ? undefined\n              : _('poolLicenseAlreadyFullySupported')\n      }\n    >\n      {_('bindXcpngLicenses')}\n    </ActionButton>\n  ),\n])\n\n@connectStore(() => ({\n  master: createGetObjectsOfType('host').find((_, { pool }) => ({\n    id: pool.master,\n  })),\n}))\nclass PoolMaster extends Component {\n  _getPoolMasterPredicate = host => host.$pool === this.props.pool.id\n\n  _onChange = host => setPoolMaster(host)\n\n  render() {\n    const { pool, master } = this.props\n\n    return (\n      <XoSelect onChange={this._onChange} predicate={this._getPoolMasterPredicate} value={pool.master} xoType='host'>\n        {master.name_label}\n      </XoSelect>\n    )\n  }\n}\n\n@connectStore(() => ({\n  defaultSr: createGetObject((_, { pool }) => pool.default_SR),\n}))\nclass SelectDefaultSr extends Component {\n  render() {\n    const { defaultSr, pool } = this.props\n\n    return (\n      <XoSelect onChange={setDefaultSr} value={defaultSr} xoType='SR' predicate={sr => sr.$pool === pool.id}>\n        {defaultSr !== undefined ? <Sr id={defaultSr.id} /> : _('noValue')}\n      </XoSelect>\n    )\n  }\n}\n\nclass EnableHaModal extends Component {\n  state = {\n    srs: Object.values(this.props.srs),\n  }\n\n  get value() {\n    return this.state.srs\n  }\n  render() {\n    return (\n      <div>\n        <strong>{_('poolHaSelectSrs')}</strong>\n        <br />\n        {_('poolHaSelectSrsDetails')}\n        <SelectSr\n          multi\n          value={this.state.srs}\n          onChange={srs => this.setState({ srs: srs.map(sr => sr.id) })}\n          predicate={sr => sr.shared && isSrWritable(sr)}\n        />\n      </div>\n    )\n  }\n}\n\nclass ToggleHa extends Component {\n  // state.busy is used to prevent interaction with toggle while HA is being enabled or disabled\n  state = {\n    busy: false,\n  }\n\n  _onChange = async value => {\n    if (value) {\n      const haSrs = await confirm({\n        body: <EnableHaModal srs={this.props.pool.haSrs ?? []} />,\n        title: _('poolEnableHa'),\n        icon: 'pool',\n      })\n\n      try {\n        this.setState({ busy: true })\n        await enableHa({\n          pool: this.props.pool,\n          heartbeatSrs: haSrs,\n          configuration: this.props.pool.ha_configuration ?? {},\n        })\n      } finally {\n        this.setState({ busy: false })\n      }\n    } else {\n      await confirm({\n        title: _('poolDisableHa'),\n        body: _('poolDisableHaConfirm'),\n      })\n      try {\n        this.setState({ busy: true })\n        await disableHa(this.props.pool)\n      } finally {\n        this.setState({ busy: false })\n      }\n    }\n  }\n\n  render() {\n    return <Toggle value={this.props.pool.HA_enabled} onChange={this._onChange} disabled={this.state.busy} />\n  }\n}\n\n@injectIntl\n@connectStore(() => {\n  const getHosts = createGetObjectsOfType('host')\n    .filter((_, { pool }) => ({ $pool: pool.id }))\n    .sort()\n  return {\n    backupNetwork: createGetObject((_, { pool }) => pool.otherConfig['xo:backupNetwork']),\n    hosts: getHosts,\n    hostsByMultipathing: createGroupBy(\n      getHosts,\n      () =>\n        ({ multipathing }) =>\n          multipathing ? 'enabled' : 'disabled'\n    ),\n    gpuGroups: createGetObjectsOfType('gpuGroup')\n      .filter((_, { pool }) => ({ $pool: pool.id }))\n      .sort(),\n    isAdmin,\n    migrationNetwork: createGetObject((_, { pool }) => pool.otherConfig['xo:migrationNetwork']),\n  }\n})\n@addSubscriptions({\n  plugins: subscribePlugins,\n})\nexport default class TabAdvanced extends Component {\n  _getNetworkPredicate = createSelector(\n    createCollectionWrapper(\n      createSelector(\n        () => this.props.pifs,\n        pifs => {\n          const networkIds = new Set()\n          pifs.forEach(pif => {\n            if (pif.ip !== '') {\n              networkIds.add(pif.$network)\n            }\n          })\n          return networkIds\n        }\n      )\n    ),\n    networkIds => network => networkIds.has(network.id)\n  )\n\n  _isNetboxPluginLoaded = createSelector(\n    () => this.props.plugins,\n    plugins => plugins !== undefined && plugins.some(plugin => plugin.name === 'netbox' && plugin.loaded)\n  )\n\n  _onChangeAutoPoweron = value => editPool(this.props.pool, { auto_poweron: value })\n\n  _onChangeMigrationCompression = value => editPool(this.props.pool, { migrationCompression: value })\n\n  _onChangeBackupNetwork = backupNetwork => editPool(this.props.pool, { backupNetwork: backupNetwork.id })\n\n  _removeBackupNetwork = () => editPool(this.props.pool, { backupNetwork: null })\n\n  _onChangeMigrationNetwork = migrationNetwork => editPool(this.props.pool, { migrationNetwork: migrationNetwork.id })\n\n  _removeMigrationNetwork = () => editPool(this.props.pool, { migrationNetwork: null })\n\n  _onChangeCrashDumpSr = sr => editPool(this.props.pool, { crashDumpSr: sr.id })\n\n  _onRemoveCrashDumpSr = () => editPool(this.props.pool, { crashDumpSr: null })\n\n  _setRemoteSyslogHosts = () =>\n    setRemoteSyslogHosts(this.props.hosts, this.state.syslogDestination).then(() =>\n      this.setState({ editRemoteSyslog: false, syslogDestination: '' })\n    )\n\n  _getCrashDumpSrPredicate = createSelector(\n    () => this.props.pool,\n    pool => sr => isSrWritable(sr) && sr.$pool === pool.id\n  )\n\n  render() {\n    const { backupNetwork, hosts, isAdmin, gpuGroups, pool, hostsByMultipathing, migrationNetwork } = this.props\n    const { state } = this\n    const { editRemoteSyslog } = state\n    const { enabled: hostsEnabledMultipathing, disabled: hostsDisabledMultipathing } = hostsByMultipathing\n    const { crashDumpSr } = pool\n    const crashDumpSrPredicate = this._getCrashDumpSrPredicate()\n    const isEnterprisePlan = getXoaPlan().value >= ENTERPRISE.value\n    const isMigrationCompressionAvailable = pool.migrationCompression !== undefined\n\n    return (\n      <div>\n        <Container>\n          <Row>\n            <Col className='text-xs-right'>\n              <TabButton\n                btnStyle='warning'\n                handler={rollingPoolReboot}\n                handlerParam={pool}\n                icon='pool-rolling-reboot'\n                labelId='rollingPoolReboot'\n                disabled={!isEnterprisePlan}\n                tooltip={!isEnterprisePlan ? _('onlyAvailableToEnterprise') : undefined}\n              />\n              {this._isNetboxPluginLoaded() && (\n                <TabButton\n                  btnStyle='primary'\n                  handler={synchronizeNetbox}\n                  handlerParam={[pool]}\n                  icon='refresh'\n                  labelId='syncNetbox'\n                />\n              )}\n            </Col>\n          </Row>\n          <Row>\n            <Col>\n              <h3>{_('xenSettingsLabel')}</h3>\n              <table className='table'>\n                <tbody>\n                  <tr>\n                    <th>{_('autoPowerOn')}</th>\n                    <td>\n                      <Toggle value={pool.auto_poweron} onChange={this._onChangeAutoPoweron} />\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('migrationCompression')}</th>\n                    <td>\n                      <Tooltip\n                        content={isMigrationCompressionAvailable ? undefined : _('migrationCompressionDisabled')}\n                      >\n                        <Toggle\n                          value={pool.migrationCompression}\n                          onChange={this._onChangeMigrationCompression}\n                          disabled={!isMigrationCompressionAvailable}\n                        />\n                      </Tooltip>\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('poolHeartbeatSr')}</th>\n                    <td>\n                      <ul>\n                        {map(pool.haSrs, sr => (\n                          <li key={sr}>\n                            <Sr id={sr} />\n                          </li>\n                        ))}\n                      </ul>\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('poolHaStatus')}</th>\n                    <td>\n                      <ToggleHa pool={pool} />\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('setpoolMaster')}</th>\n                    <td>\n                      <PoolMaster pool={pool} />\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('customFields')}</th>\n                    <td>\n                      <CustomFields object={pool.id} />\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('syslogRemoteHost')}</th>\n                    <td>\n                      <ul className='pl-0'>\n                        {map(hosts, host => (\n                          <li key={host.id}>\n                            <span>{`${host.name_label}: `}</span>\n                            <Text\n                              value={host.logging.syslog_destination || ''}\n                              onChange={value => setRemoteSyslogHost(host, value)}\n                            />\n                          </li>\n                        ))}\n                      </ul>\n                      <ActionRowButton\n                        btnStyle={editRemoteSyslog ? 'info' : 'primary'}\n                        handler={this.toggleState('editRemoteSyslog')}\n                        icon='edit'\n                      >\n                        {_('poolEditAll')}\n                      </ActionRowButton>\n                      {editRemoteSyslog && (\n                        <form id='formRemoteSyslog' className='form-inline mt-1'>\n                          <div className='form-group'>\n                            <input\n                              className='form-control'\n                              onChange={this.linkState('syslogDestination')}\n                              placeholder={this.props.intl.formatMessage(messages.poolRemoteSyslogPlaceHolder)}\n                              type='text'\n                              value={state.syslogDestination}\n                            />\n                          </div>\n                          <div className='form-group ml-1'>\n                            <ActionButton\n                              btnStyle='primary'\n                              form='formRemoteSyslog'\n                              handler={this._setRemoteSyslogHosts}\n                              icon='save'\n                            >\n                              {_('confirmOk')}\n                            </ActionButton>\n                          </div>\n                        </form>\n                      )}\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('defaultSr')}</th>\n                    <td>\n                      <SelectDefaultSr pool={pool} />\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('suspendSr')}</th>\n                    <td>\n                      <SelectSuspendSr pool={pool} />\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('crashDumpSr')}</th>\n                    <td>\n                      <XoSelect\n                        onChange={this._onChangeCrashDumpSr}\n                        predicate={crashDumpSrPredicate}\n                        value={crashDumpSr}\n                        xoType='SR'\n                      >\n                        {crashDumpSr !== undefined ? <Sr id={crashDumpSr} /> : _('noValue')}\n                      </XoSelect>{' '}\n                      {crashDumpSr !== undefined && (\n                        <a onClick={this._onRemoveCrashDumpSr} role='button'>\n                          <Icon icon='remove' />\n                        </a>\n                      )}\n                    </td>\n                  </tr>\n                </tbody>\n              </table>\n            </Col>\n          </Row>\n        </Container>\n        {isAdmin && (\n          <div>\n            <h3>{_('licenses')}</h3>\n            <BindLicensesButton poolHosts={hosts} pool={pool} />\n          </div>\n        )}\n        <h3 className='mt-1 mb-1'>{_('poolGpuGroups')}</h3>\n        <Container>\n          <Row>\n            <Col size={9}>\n              <ul className='list-group'>\n                {map(gpuGroups, gpuGroup => (\n                  <li key={gpuGroup.id} className='list-group-item'>\n                    {renderXoItem(gpuGroup)}\n                  </li>\n                ))}\n              </ul>\n            </Col>\n          </Row>\n        </Container>\n        <h3 className='mt-1 mb-1'>{_('multipathing')}</h3>\n        <div>\n          <ActionButton\n            btnStyle='success'\n            data-hosts={hostsDisabledMultipathing}\n            data-multipathing\n            disabled={hostsDisabledMultipathing === undefined}\n            handler={setHostsMultipathing}\n            icon='host'\n          >\n            {_('enableAllHostsMultipathing')}\n          </ActionButton>{' '}\n          <ActionButton\n            btnStyle='danger'\n            data-hosts={hostsEnabledMultipathing}\n            data-multipathing={false}\n            disabled={hostsEnabledMultipathing === undefined}\n            handler={setHostsMultipathing}\n            icon='host'\n          >\n            {_('disableAllHostsMultipathing')}\n          </ActionButton>\n        </div>\n        <h3 className='mt-1 mb-1'>{_('supplementalPackPoolNew')}</h3>\n        <Upgrade place='poolSupplementalPacks' required={2}>\n          <SelectFiles onChange={file => installSupplementalPackOnAllHosts(pool, file)} />\n        </Upgrade>\n        <h3 className='mt-1 mb-1'>{_('miscLabel')}</h3>\n        <Container>\n          <Row>\n            <Col>\n              <table className='table'>\n                <tbody>\n                  <tr>\n                    <th>{_('defaultMigrationNetwork')}</th>\n                    <td>\n                      <XoSelect\n                        onChange={this._onChangeMigrationNetwork}\n                        predicate={this._getNetworkPredicate()}\n                        value={migrationNetwork}\n                        xoType='network'\n                      >\n                        {pool.otherConfig['xo:migrationNetwork'] === undefined ? (\n                          _('noValue')\n                        ) : migrationNetwork !== undefined ? (\n                          <Network id={migrationNetwork.id} />\n                        ) : (\n                          <span className='text-danger'>\n                            {_('updateMissingNetwork', {\n                              networkID: (\n                                <Copiable data={pool.otherConfig['xo:migrationNetwork']}>\n                                  <strong>{pool.otherConfig['xo:migrationNetwork']}</strong>\n                                </Copiable>\n                              ),\n                            })}\n                          </span>\n                        )}\n                      </XoSelect>{' '}\n                      {pool.otherConfig['xo:migrationNetwork'] !== undefined && (\n                        <a role='button' onClick={this._removeMigrationNetwork}>\n                          <Icon icon='remove' />\n                        </a>\n                      )}\n                    </td>\n                  </tr>\n                  <tr>\n                    <th>{_('backupNetwork')}</th>\n                    <td>\n                      <XoSelect\n                        onChange={this._onChangeBackupNetwork}\n                        predicate={this._getNetworkPredicate()}\n                        value={backupNetwork}\n                        xoType='network'\n                      >\n                        {pool.otherConfig['xo:backupNetwork'] === undefined ? (\n                          _('noValue')\n                        ) : backupNetwork !== undefined ? (\n                          <Network id={backupNetwork.id} />\n                        ) : (\n                          <span className='text-danger'>\n                            {_('updateMissingNetwork', {\n                              networkID: (\n                                <Copiable data={pool.otherConfig['xo:backupNetwork']}>\n                                  <strong>{pool.otherConfig['xo:backupNetwork']}</strong>\n                                </Copiable>\n                              ),\n                            })}\n                          </span>\n                        )}\n                      </XoSelect>{' '}\n                      {pool.otherConfig['xo:backupNetwork'] !== undefined && (\n                        <a role='button' onClick={this._removeBackupNetwork}>\n                          <Icon icon='remove' />\n                        </a>\n                      )}\n                    </td>\n                  </tr>\n                </tbody>\n              </table>\n            </Col>\n          </Row>\n        </Container>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport find from 'lodash/find'\nimport Icon from 'icon'\nimport map from 'lodash/map'\nimport React from 'react'\nimport sumBy from 'lodash/sumBy'\nimport HomeTags from 'home-tags'\nimport { addTag, removeTag } from 'xo'\nimport Link, { BlockLink } from 'link'\nimport { Container, Row, Col } from 'grid'\nimport Usage, { UsageElement } from 'usage'\nimport { formatSizeShort } from 'utils'\nimport Tooltip from 'tooltip'\nimport { injectState, provideState } from 'reaclette'\nimport { Pool } from 'render-xo-item'\n\nexport default decorate([\n  provideState({\n    computed: {\n      areHostsVersionsEqual: ({ areHostsVersionsEqualByPool }, { pool }) => areHostsVersionsEqualByPool[pool.id],\n    },\n  }),\n  injectState,\n  ({ hosts, nVms, pool, srs, state: { areHostsVersionsEqual } }) => (\n    <Container>\n      <br />\n      <Row className='text-xs-center'>\n        <Col mediumSize={4}>\n          <Tooltip content={_('displayAllHosts')}>\n            <BlockLink to={`/home?s=$pool:${pool.id}&t=host`}>\n              <h2>\n                {hosts.length}x <Icon icon='host' size='lg' />\n              </h2>\n            </BlockLink>\n          </Tooltip>\n        </Col>\n        <Col mediumSize={4}>\n          <Tooltip content={_('displayAllStorages')}>\n            <BlockLink to={`/home?s=$pool:${pool.id}&t=SR`}>\n              <h2>\n                {srs.length}x <Icon icon='sr' size='lg' />\n              </h2>\n            </BlockLink>\n          </Tooltip>\n        </Col>\n        <Col mediumSize={4}>\n          <Tooltip content={_('displayAllVMs')}>\n            <BlockLink to={`/home?s=$pool:${pool.id}`}>\n              <h2>\n                {nVms}x <Icon icon='vm' size='lg' />\n              </h2>\n            </BlockLink>\n          </Tooltip>\n        </Col>\n      </Row>\n      <br />\n      <Row>\n        <Col className='text-xs-center'>\n          <h5>{_('poolTitleRamUsage')}</h5>\n        </Col>\n      </Row>\n      <Row>\n        <Col smallOffset={1} mediumSize={10}>\n          <Usage total={sumBy(hosts, 'memory.size')}>\n            {map(hosts, host => (\n              <UsageElement\n                tooltip={`${host.name_label} (${formatSizeShort(host.memory.usage)})`}\n                key={host.id}\n                value={host.memory.usage}\n                href={`#/hosts/${host.id}`}\n              />\n            ))}\n          </Usage>\n        </Col>\n      </Row>\n      <Row>\n        <Col className='text-xs-center'>\n          <h5>\n            {_('poolRamUsage', {\n              used: formatSizeShort(sumBy(hosts, 'memory.usage')),\n              total: formatSizeShort(sumBy(hosts, 'memory.size')),\n              free: formatSizeShort(sumBy(hosts, host => host.memory.size - host.memory.usage)),\n            })}\n          </h5>\n        </Col>\n      </Row>\n      <Row className='text-xs-center'>\n        <Col>\n          {_('poolMaster')}{' '}\n          <Link to={`/hosts/${pool.master}`}>{find(hosts, host => host.id === pool.master).name_label}</Link>\n        </Col>\n      </Row>\n      <Row className='text-xs-center'>\n        <Col>\n          <h2>\n            <HomeTags\n              type='pool'\n              labels={pool.tags}\n              onDelete={tag => removeTag(pool.id, tag)}\n              onAdd={tag => addTag(pool.id, tag)}\n            />\n          </h2>\n        </Col>\n      </Row>\n      {!areHostsVersionsEqual && (\n        <Row className='text-xs-center text-danger'>\n          <Col>\n            <p>\n              <Icon icon='alarm' /> {_('notAllHostsHaveTheSameVersion', { pool: <Pool id={pool.id} link /> })}\n            </p>\n          </Col>\n        </Row>\n      )}\n    </Container>\n  ),\n])\n","import _ from 'intl'\nimport React, { Component } from 'react'\nimport SortedTable from 'sorted-table'\nimport { FormattedRelative, FormattedTime } from 'react-intl'\nimport { deleteMessage, deleteMessages } from 'xo'\n\nconst LOG_COLUMNS = [\n  {\n    default: true,\n    itemRenderer: log => (\n      <div>\n        <FormattedTime\n          value={log.time * 1000}\n          minute='numeric'\n          hour='numeric'\n          day='numeric'\n          month='long'\n          year='numeric'\n        />{' '}\n        (<FormattedRelative value={log.time * 1000} />)\n      </div>\n    ),\n    name: _('logDate'),\n    sortCriteria: 'time',\n  },\n  {\n    itemRenderer: log => log.name,\n    name: _('logName'),\n    sortCriteria: 'name',\n  },\n  {\n    itemRenderer: log => log.body,\n    name: _('logContent'),\n    sortCriteria: 'body',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    handler: deleteMessage,\n    icon: 'delete',\n    label: _('logDelete'),\n    level: 'danger',\n  },\n]\n\nconst GROUPED_ACTIONS = [\n  {\n    handler: deleteMessages,\n    icon: 'delete',\n    label: _('logsDelete'),\n    level: 'danger',\n  },\n]\n\nexport default class TabLogs extends Component {\n  render() {\n    return (\n      <SortedTable\n        collection={this.props.logs}\n        columns={LOG_COLUMNS}\n        groupedActions={GROUPED_ACTIONS}\n        individualActions={INDIVIDUAL_ACTIONS}\n        stateUrlParam='s'\n      />\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionRowButton from 'action-row-button'\nimport BaseComponent from 'base-component'\nimport ButtonGroup from 'button-group'\nimport copy from 'copy-to-clipboard'\nimport Icon from 'icon'\nimport isEmpty from 'lodash/isEmpty'\nimport React, { Component } from 'react'\nimport some from 'lodash/some'\nimport SortedTable from 'sorted-table'\nimport PifsColumn from 'sorted-table/pifs-column'\nimport Tooltip, { conditionalTooltip } from 'tooltip'\nimport { connectStore } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { TabButtonLink } from 'tab-button'\nimport { Text, Number } from 'editable'\nimport { Select, Toggle } from 'form'\nimport { createGetObjectsOfType, createSelector } from 'selectors'\nimport { deleteNetwork, editNetwork, editPif } from 'xo'\n\n// =============================================================================\n\n@connectStore(() => ({\n  isBonded: createSelector(\n    createGetObjectsOfType('PIF').pick((_, props) => props && props.network.PIFs),\n    pifs => some(pifs, 'isBondMaster')\n  ),\n}))\nclass Name extends Component {\n  _editName = value => editNetwork(this.props.network, { name_label: value })\n\n  render() {\n    const { isBonded, network } = this.props\n\n    return (\n      <span>\n        <Text value={network.name_label} onChange={this._editName} />{' '}\n        {isBonded && <span className='tag tag-pill tag-info'>{_('pillBonded')}</span>}\n      </span>\n    )\n  }\n}\n\n// -----------------------------------------------------------------------------\n\nclass AutomaticNetwork extends Component {\n  _editAutomaticNetwork = automatic => editNetwork(this.props.network, { automatic })\n\n  render() {\n    const { network } = this.props\n\n    return <Toggle onChange={this._editAutomaticNetwork} value={network.automatic} />\n  }\n}\n\n// -----------------------------------------------------------------------------\n\nclass Description extends Component {\n  _editDescription = value => editNetwork(this.props.network, { name_description: value })\n\n  render() {\n    const { network } = this.props\n\n    return <Text value={network.name_description} onChange={this._editDescription} />\n  }\n}\n\n// -----------------------------------------------------------------------------\n\nclass Mtu extends Component {\n  _editMtu = value => editNetwork(this.props.network, { mtu: value })\n\n  render() {\n    const { network } = this.props\n\n    return <Number value={network.MTU} onChange={this._editMtu} />\n  }\n}\n\nclass DefaultPif extends BaseComponent {\n  _editPif = vlan => editPif(this.props.network.defaultPif, { vlan })\n\n  render() {\n    const { defaultPif } = this.props.network\n\n    if (!defaultPif) {\n      return null\n    }\n\n    return <span>{defaultPif.device}</span>\n  }\n}\n\nclass Nbd extends Component {\n  NBD_FILTER_OPTIONS = [\n    {\n      labelId: 'noNbdConnection',\n      value: false,\n    },\n    {\n      labelId: 'nbdConnection',\n      value: true,\n    },\n  ]\n  INSECURE_OPTION = [\n    {\n      labelId: 'insecureNbdConnection',\n      value: 'insecure_nbd',\n      disabled: true,\n    },\n  ]\n\n  _getOptionRenderer = ({ labelId }) => _(labelId)\n\n  _editNbdConnection = value => {\n    editNetwork(this.props.network, { nbd: value.value })\n  }\n\n  render() {\n    const { network } = this.props\n\n    return (\n      <Select\n        onChange={this._editNbdConnection}\n        optionRenderer={this._getOptionRenderer}\n        // We chose not to show the unsecure_nbd option unless the user has already activated it through another client.\n        // The reason is that we don't want them to know about it since the option is not allowed in XO.\n        options={network.insecureNbd ? [...this.NBD_FILTER_OPTIONS, ...this.INSECURE_OPTION] : this.NBD_FILTER_OPTIONS}\n        value={network.nbd ? true : network.insecureNbd ? 'insecure_nbd' : false}\n      />\n    )\n  }\n}\n\nclass Vlan extends BaseComponent {\n  _editPif = vlan => editPif(this.props.network.defaultPif, { vlan })\n\n  render() {\n    const { defaultPif } = this.props.network\n\n    if (!defaultPif) {\n      return null\n    }\n\n    return (\n      <span>\n        <Number value={defaultPif.vlan} onChange={this._editPif}>\n          {defaultPif.vlan === -1 ? 'None' : defaultPif.vlan}\n        </Number>\n      </span>\n    )\n  }\n}\n\n// -----------------------------------------------------------------------------\n\n@connectStore(() => ({\n  isInUse: createSelector(\n    createGetObjectsOfType('VIF').pick((_, props) => props && props.network.VIFs),\n    vifs => some(vifs, 'attached')\n  ),\n}))\nclass ToggleDefaultLockingMode extends Component {\n  _editDefaultIsLocked = () => {\n    const { network } = this.props\n    editNetwork(network, { defaultIsLocked: !network.defaultIsLocked })\n  }\n\n  render() {\n    const { isInUse, network } = this.props\n    return conditionalTooltip(\n      <Toggle disabled={isInUse} onChange={this._editDefaultIsLocked} value={network.defaultIsLocked} />,\n      isInUse ? _('networkInUse') : undefined\n    )\n  }\n}\n\n// -----------------------------------------------------------------------------\n\n@connectStore(() => {\n  const disablePifUnplug = pif => pif.attached && !pif.isBondMaster && (pif.management || pif.disallowUnplug)\n\n  const getDisableNetworkDelete = createSelector(\n    () => createGetObjectsOfType('PIF').pick((_, props) => props.network.PIFs),\n    (_, props) => props && props.network.name_label,\n    (pifs, nameLabel) => nameLabel === 'Host internal management network' || some(pifs, disablePifUnplug)\n  )\n\n  return {\n    disableNetworkDelete: getDisableNetworkDelete,\n  }\n})\nclass NetworkActions extends Component {\n  render() {\n    const { network, disableNetworkDelete } = this.props\n\n    return (\n      <ButtonGroup>\n        <ActionRowButton\n          handler={() => copy(network.uuid)}\n          icon='clipboard'\n          tooltip={_('copyUuid', { uuid: network.uuid })}\n        />\n        <ActionRowButton\n          disabled={disableNetworkDelete}\n          handler={deleteNetwork}\n          handlerParam={network}\n          icon='delete'\n          tooltip={_('deleteNetwork')}\n        />\n      </ButtonGroup>\n    )\n  }\n}\n\n// =============================================================================\n\nconst NETWORKS_COLUMNS = [\n  {\n    name: _('poolNetworkNameLabel'),\n    itemRenderer: network => <Name network={network} />,\n    sortCriteria: network => network.name_label,\n  },\n  {\n    name: _('poolNetworkDescription'),\n    itemRenderer: network => <Description network={network} />,\n    sortCriteria: network => network.name_description,\n  },\n  {\n    name: _('pif'),\n    itemRenderer: network => <DefaultPif network={network} />,\n  },\n  {\n    name: _('pifVlanLabel'),\n    itemRenderer: network => <Vlan network={network} />,\n\n    // push entries without VLAN at the end\n    sortCriteria: ({ defaultPif }) => (defaultPif === undefined || defaultPif.vlan === -1 ? Infinity : defaultPif.vlan),\n  },\n  {\n    name: _('poolNetworkMTU'),\n    itemRenderer: network => <Mtu network={network} />,\n  },\n\n  {\n    itemRenderer: network => <Nbd network={network} />,\n    name: <Tooltip content={_('nbdTootltip')}>{_('nbd')}</Tooltip>,\n  },\n  {\n    name: (\n      <div className='text-xs-center'>\n        <Tooltip content={_('defaultLockingMode')}>\n          <Icon size='lg' icon='lock' />\n        </Tooltip>\n      </div>\n    ),\n    itemRenderer: network => <ToggleDefaultLockingMode network={network} />,\n  },\n  {\n    name: _('poolNetworkPif'),\n    itemRenderer: network => !isEmpty(network.PIFs) && <PifsColumn network={network} />,\n  },\n  {\n    name: _('poolNetworkAutomatic'),\n    itemRenderer: network => <AutomaticNetwork network={network} />,\n    tooltip: _('networkAutomaticTooltip'),\n  },\n  {\n    name: '',\n    itemRenderer: network => <NetworkActions network={network} />,\n    textAlign: 'right',\n  },\n]\n\n// =============================================================================\n\n@connectStore(() => ({\n  pifs: createGetObjectsOfType('PIF'),\n}))\nexport default class TabNetworks extends Component {\n  _getNetworks = createSelector(\n    () => this.props.master,\n    () => this.props.networks,\n    () => this.props.pifs,\n    (master, networks, pifs) =>\n      networks.map(network => {\n        for (const pifId of network.PIFs) {\n          const pif = pifs[pifId]\n          if (pif !== undefined && pif.$host === master.id) {\n            return Object.defineProperty({ VLAN: pif.vlan, ...network }, 'defaultPif', { value: pif })\n          }\n        }\n\n        return network\n      })\n  )\n\n  render() {\n    const networks = this._getNetworks()\n\n    return (\n      <Container>\n        <Row>\n          <Col className='text-xs-right'>\n            <TabButtonLink icon='add' labelId='networkCreateButton' to={`new/network?pool=${this.props.pool.id}`} />\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            {!isEmpty(networks) ? (\n              <SortedTable collection={networks} columns={NETWORKS_COLUMNS} stateUrlParam='s' />\n            ) : (\n              <h4 className='text-xs-center'>{_('poolNoNetwork')}</h4>\n            )}\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport React, { Component } from 'react'\nimport SortedTable from 'sorted-table'\nimport TabButton from 'tab-button'\nimport Upgrade from 'xoa-upgrade'\nimport { addSubscriptions, connectStore, formatSize } from 'utils'\nimport { alert } from 'modal'\nimport { Col, Container, Row } from 'grid'\nimport { createGetObjectsOfType, createSelector } from 'selectors'\nimport { FormattedRelative, FormattedTime } from 'react-intl'\nimport { getXoaPlan, ENTERPRISE } from 'xoa-plans'\nimport {\n  installAllPatchesOnPool,\n  installPatches,\n  isSrShared,\n  isSrWritable,\n  rollingPoolUpdate,\n  subscribeCurrentUser,\n  subscribeHostMissingPatches,\n} from 'xo'\nimport filter from 'lodash/filter.js'\nimport isEmpty from 'lodash/isEmpty.js'\nimport size from 'lodash/size.js'\nimport some from 'lodash/some.js'\nimport { isXsHostWithCdnPatches } from 'xo/utils'\n\nconst ROLLING_POOL_UPDATES_AVAILABLE = getXoaPlan().value >= ENTERPRISE.value\n\nconst MISSING_PATCH_COLUMNS = [\n  {\n    name: _('patchNameLabel'),\n    itemRenderer: _ => _.name,\n    sortCriteria: 'name',\n  },\n  {\n    name: _('patchDescription'),\n    itemRenderer: ({ description, documentationUrl }) => (\n      <a href={documentationUrl} rel='noopener noreferrer' target='_blank'>\n        {description}\n      </a>\n    ),\n    sortCriteria: 'description',\n  },\n  {\n    name: _('patchReleaseDate'),\n    itemRenderer: ({ date }) => (\n      <span>\n        <FormattedTime value={date} day='numeric' month='long' year='numeric' /> (<FormattedRelative value={date} />)\n      </span>\n    ),\n    sortCriteria: 'date',\n    sortOrder: 'desc',\n  },\n  {\n    name: _('patchGuidance'),\n    itemRenderer: _ => _.guidance,\n    sortCriteria: 'guidance',\n  },\n]\n\nconst ACTIONS = [\n  {\n    disabled: (_, { isXsHostWithCdnPatches, pool, needsCredentials }) =>\n      pool.HA_enabled || needsCredentials || isXsHostWithCdnPatches,\n    handler: (patches, { pool }) => installPatches(patches, pool),\n    icon: 'host-patch-update',\n    label: _('install'),\n    level: 'primary',\n  },\n]\n\nconst MISSING_PATCH_COLUMNS_XCP = [\n  {\n    name: _('patchNameLabel'),\n    itemRenderer: _ => _.name,\n    sortCriteria: 'name',\n  },\n  {\n    name: _('patchDescription'),\n    itemRenderer: _ => _.description,\n    sortCriteria: 'description',\n  },\n  {\n    name: _('patchVersion'),\n    itemRenderer: _ => _.version,\n  },\n  {\n    name: _('patchRelease'),\n    itemRenderer: _ => _.release,\n  },\n  {\n    name: _('patchSize'),\n    itemRenderer: _ => formatSize(_.size),\n    sortCriteria: 'size',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS_XCP = [\n  {\n    disabled: _ => _.changelog === null,\n    handler: ({ name, changelog: { author, date, description } }) =>\n      alert(\n        _('changelog'),\n        <Container>\n          <Row className='mb-1'>\n            <Col size={3}>\n              <strong>{_('changelogPatch')}</strong>\n            </Col>\n            <Col size={9}>{name}</Col>\n          </Row>\n          <Row className='mb-1'>\n            <Col size={3}>\n              <strong>{_('changelogDate')}</strong>\n            </Col>\n            <Col size={9}>\n              <FormattedTime value={date * 1000} day='numeric' month='long' year='numeric' />\n            </Col>\n          </Row>\n          <Row className='mb-1'>\n            <Col size={3}>\n              <strong>{_('changelogAuthor')}</strong>\n            </Col>\n            <Col size={9}>{author}</Col>\n          </Row>\n          <Row>\n            <Col size={3}>\n              <strong>{_('changelogDescription')}</strong>\n            </Col>\n            <Col size={9}>{description}</Col>\n          </Row>\n        </Container>\n      ),\n    icon: 'preview',\n    label: _('showChangelog'),\n  },\n]\n\nconst INSTALLED_PATCH_COLUMNS = [\n  {\n    name: _('patchNameLabel'),\n    itemRenderer: _ => _.name,\n    sortCriteria: 'name',\n  },\n  {\n    name: _('patchDescription'),\n    itemRenderer: _ => _.description,\n    sortCriteria: 'description',\n  },\n  {\n    default: true,\n    name: _('patchApplied'),\n    itemRenderer: patch => {\n      const time = patch.time * 1000\n      return (\n        <span>\n          <FormattedTime value={time} day='numeric' month='long' year='numeric' /> (<FormattedRelative value={time} />)\n        </span>\n      )\n    },\n    sortCriteria: 'time',\n    sortOrder: 'desc',\n  },\n  {\n    name: _('patchSize'),\n    itemRenderer: _ => formatSize(_.size),\n    sortCriteria: 'size',\n  },\n]\n\n@addSubscriptions(({ master }) => ({\n  missingPatches: cb => subscribeHostMissingPatches(master, cb),\n  userPreferences: cb => subscribeCurrentUser(user => cb(user.preferences)),\n}))\n@connectStore(() => {\n  const getSrs = createGetObjectsOfType('SR')\n  const getPoolSrs = (state, props) =>\n    getSrs.filter(\n      createSelector(\n        (_, props) => props.pool.id,\n        poolId => sr => sr.$pool === poolId\n      )\n    )(state, props)\n  return {\n    hostPatches: createGetObjectsOfType('patch').pick((_, { master }) => master.patches),\n    poolHosts: createGetObjectsOfType('host').filter(\n      createSelector(\n        (_, props) => props.pool.id,\n        poolId => host => host.$pool === poolId\n      )\n    ),\n    runningVms: createGetObjectsOfType('VM').filter(\n      createSelector(\n        (_, props) => props.pool.id,\n        poolId => vm => vm.$pool === poolId && vm.power_state === 'Running'\n      )\n    ),\n    vbds: createGetObjectsOfType('VBD'),\n    vdis: createGetObjectsOfType('VDI'),\n    srs: getSrs,\n    poolSrs: getPoolSrs,\n  }\n})\nexport default class TabPatches extends Component {\n  getNVmsRunningOnLocalStorage = createSelector(\n    () => this.props.runningVms,\n    () => this.props.vbds,\n    () => this.props.vdis,\n    () => this.props.srs,\n    (runningVms, vbds, vdis, srs) =>\n      filter(runningVms, vm =>\n        some(vm.$VBDs, vbdId => {\n          const vbd = vbds[vbdId]\n          const vdi = vdis[vbd?.VDI]\n          const sr = srs[vdi?.$SR]\n          return !isSrShared(sr) && isSrWritable(sr)\n        })\n      ).length\n  )\n\n  render() {\n    const {\n      hostPatches,\n      master: { productBrand, version },\n      missingPatches = [],\n      pool,\n      poolHosts,\n      userPreferences,\n    } = this.props\n\n    const _isXsHostWithCdnPatches = isXsHostWithCdnPatches({ version, productBrand })\n    const needsCredentials =\n      productBrand !== 'XCP-ng' && !_isXsHostWithCdnPatches && userPreferences.xsCredentials === undefined\n\n    const isSingleHost = size(poolHosts) < 2\n\n    const hasMultipleVmsRunningOnLocalStorage = this.getNVmsRunningOnLocalStorage() > 0\n\n    return (\n      <Upgrade place='poolPatches' required={2}>\n        <Container>\n          <Row>\n            <Col className='text-xs-right'>\n              {ROLLING_POOL_UPDATES_AVAILABLE && (\n                <TabButton\n                  btnStyle='primary'\n                  disabled={isEmpty(missingPatches) || hasMultipleVmsRunningOnLocalStorage || isSingleHost}\n                  handler={rollingPoolUpdate}\n                  handlerParam={pool.id}\n                  icon='pool-rolling-update'\n                  labelId='rollingPoolUpdate'\n                  tooltip={\n                    hasMultipleVmsRunningOnLocalStorage\n                      ? _('nVmsRunningOnLocalStorage', {\n                          nVms: this.getNVmsRunningOnLocalStorage(),\n                        })\n                      : isSingleHost\n                        ? _('multiHostPoolUpdate')\n                        : undefined\n                  }\n                />\n              )}\n              <TabButton\n                btnStyle='primary'\n                data-pool={pool}\n                disabled={isEmpty(missingPatches) || pool.HA_enabled || needsCredentials}\n                handler={installAllPatchesOnPool}\n                icon='host-patch-update'\n                labelId='installPoolPatches'\n                tooltip={\n                  pool.HA_enabled\n                    ? _('highAvailabilityNotDisabledTooltip')\n                    : needsCredentials\n                      ? _('xsCredentialsMissingShort')\n                      : undefined\n                }\n              />\n            </Col>\n          </Row>\n          {productBrand === 'XCP-ng' ? (\n            <Row>\n              <Col>\n                <h3>{_('hostMissingPatches')}</h3>\n                <SortedTable\n                  columns={MISSING_PATCH_COLUMNS_XCP}\n                  collection={missingPatches}\n                  individualActions={INDIVIDUAL_ACTIONS_XCP}\n                  stateUrlParam='s_missing'\n                />\n              </Col>\n            </Row>\n          ) : (\n            <div>\n              <Row>\n                <Col>\n                  <h3>{_('hostMissingPatches')}</h3>\n                  {needsCredentials && (\n                    <div className='alert alert-danger'>\n                      {_('xsCredentialsMissing', {\n                        link: (\n                          <a\n                            href='https://docs.xen-orchestra.com/updater#xenserver-updates'\n                            target='_blank'\n                            rel='noreferrer'\n                          >\n                            https://docs.xen-orchestra.com/updater\n                          </a>\n                        ),\n                      })}\n                    </div>\n                  )}\n                  <SortedTable\n                    actions={ACTIONS}\n                    collection={missingPatches}\n                    columns={MISSING_PATCH_COLUMNS}\n                    data-isXsHostWithCdnPatches={_isXsHostWithCdnPatches}\n                    data-pool={pool}\n                    data-needsCredentials={needsCredentials}\n                    stateUrlParam='s_missing'\n                  />\n                </Col>\n              </Row>\n              {!_isXsHostWithCdnPatches && (\n                <Row>\n                  <Col>\n                    <h3>{_('hostAppliedPatches')}</h3>\n                    <SortedTable\n                      collection={hostPatches}\n                      columns={INSTALLED_PATCH_COLUMNS}\n                      stateUrlParam='s_installed'\n                    />\n                  </Col>\n                </Row>\n              )}\n            </div>\n          )}\n        </Container>\n      </Upgrade>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { connectStore } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { createGetObjectsOfType, createSelector } from 'selectors'\nimport { DEFAULT_GRANULARITY, fetchStats, SelectGranularity } from 'stats'\nimport map from 'lodash/map.js'\nimport { Toggle } from 'form'\nimport { PoolCpuLineChart, PoolMemoryLineChart, PoolPifLineChart, PoolLoadLineChart } from 'xo-line-chart'\n\n@connectStore({\n  hosts: createGetObjectsOfType('host').filter(\n    createSelector(\n      (state, props) => props.pool.id,\n      poolId => host => host.power_state === 'Running' && host.$pool === poolId\n    )\n  ),\n})\nexport default class PoolStats extends Component {\n  state = {\n    granularity: DEFAULT_GRANULARITY,\n    useCombinedValues: false,\n  }\n\n  _loop = () => {\n    if (this.cancel) {\n      this.cancel()\n    }\n\n    let cancelled = false\n    this.cancel = () => {\n      cancelled = true\n    }\n\n    Promise.all(\n      map(this.props.hosts, host =>\n        fetchStats(host, 'host', this.state.granularity).then(stats => ({\n          host: host.name_label,\n          ...stats,\n        }))\n      )\n    ).then(stats => {\n      if (cancelled || !stats[0]) {\n        return\n      }\n      this.cancel = null\n\n      clearTimeout(this.timeout)\n      this.setState(\n        {\n          stats,\n          selectStatsLoading: false,\n        },\n        () => {\n          this.timeout = setTimeout(this._loop, stats[0].interval * 1000)\n        }\n      )\n    })\n  }\n\n  componentDidMount() {\n    this._loop()\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this.timeout)\n  }\n\n  _handleSelectStats = granularity => {\n    clearTimeout(this.timeout)\n\n    this.setState(\n      {\n        granularity,\n        selectStatsLoading: true,\n      },\n      this._loop\n    )\n  }\n\n  render() {\n    const { granularity, selectStatsLoading, stats, useCombinedValues } = this.state\n\n    return stats ? (\n      <Container>\n        <Row>\n          <Col mediumSize={5}>\n            <div className='form-group'>\n              <Tooltip content={_('useStackedValuesOnStats')}>\n                <Toggle value={useCombinedValues} onChange={this.linkState('useCombinedValues')} />\n              </Tooltip>\n            </div>\n          </Col>\n          <Col mediumSize={1}>\n            {selectStatsLoading && (\n              <div className='text-xs-right'>\n                <Icon icon='loading' size={2} />\n              </div>\n            )}\n          </Col>\n          <Col mediumSize={6}>\n            <SelectGranularity onChange={this._handleSelectStats} required value={granularity} />\n          </Col>\n        </Row>\n        <Row>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='cpu' /> {_('statsCpu')}\n            </h5>\n            <PoolCpuLineChart addSumSeries={useCombinedValues} data={stats} />\n          </Col>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='memory' /> {_('statsMemory')}\n            </h5>\n            <PoolMemoryLineChart addSumSeries={useCombinedValues} data={stats} />\n          </Col>\n        </Row>\n        <br />\n        <hr />\n        <Row>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='network' /> {_('statsNetwork')}\n            </h5>\n            {/* key: workaround that unmounts and re-mounts the chart to make sure the legend updates when toggling \"stacked values\"\n              FIXME: remove key prop once this issue is fixed: https://github.com/CodeYellowBV/chartist-plugin-legend/issues/5 */}\n            <PoolPifLineChart\n              key={useCombinedValues ? 'stacked' : 'unstacked'}\n              addSumSeries={useCombinedValues}\n              data={stats}\n            />\n          </Col>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='disk' /> {_('statLoad')}\n            </h5>\n            <PoolLoadLineChart addSumSeries={useCombinedValues} data={stats} />\n          </Col>\n        </Row>\n      </Container>\n    ) : (\n      <p>{_('poolNoStats')}</p>\n    )\n  }\n}\n","import _, { messages } from 'intl'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport Tooltip from 'tooltip'\nimport { alert, chooseAction, form } from 'modal'\nimport { Col, Container } from 'grid'\nimport { connectStore } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\nimport { generateId } from 'reaclette-utils'\nimport { get } from '@xen-orchestra/defined'\nimport { injectIntl } from 'react-intl'\nimport { provideState, injectState } from 'reaclette'\nimport { Select } from 'form'\nimport { SelectNetwork, SelectSr } from 'select-objects'\nimport { createProxyTrialLicense, deployProxyAppliance, getLicenses, isSrWritable } from 'xo'\n\nconst Label = ({ children, ...props }) => (\n  <label {...props} style={{ cursor: 'pointer' }}>\n    <strong>{children}</strong>\n  </label>\n)\n\nconst NETWORK_MODE_OPTIONS = [\n  {\n    label: _('dhcp'),\n    value: 'dhcp',\n  },\n  {\n    label: _('static'),\n    value: 'static',\n  },\n]\n\nconst DEFAULT_DNS = '8.8.8.8'\nconst DEFAULT_NETMASK = '255.255.255.0'\n\nconst Modal = decorate([\n  connectStore({\n    hosts: createGetObjectsOfType('host'),\n    pbds: createGetObjectsOfType('PBD'),\n  }),\n  provideState({\n    effects: {\n      onSrChange(_, sr) {\n        this.props.onChange({\n          ...this.props.value,\n          sr,\n        })\n      },\n      onNetworkChange(_, network) {\n        this.props.onChange({\n          ...this.props.value,\n          network,\n        })\n      },\n      onNetworkModeChange(_, networkMode) {\n        this.props.onChange({\n          ...this.props.value,\n          networkMode,\n        })\n      },\n      onInputChange(_, { target: { name, value } }) {\n        this.props.onChange({\n          ...this.props.value,\n          [name]: value,\n        })\n      },\n    },\n    computed: {\n      idDnsInput: generateId,\n      idGatewayInput: generateId,\n      idHttpProxyInput: generateId,\n      idIpInput: generateId,\n      idNetmaskInput: generateId,\n      idSelectNetwork: generateId,\n      idSelectNetworkMode: generateId,\n      idSelectSr: generateId,\n\n      isStaticMode: (state, { value }) => value.networkMode === 'static',\n      srPredicate:\n        (state, { pbds, hosts }) =>\n        sr =>\n          isSrWritable(sr) && sr.$PBDs.some(pbd => get(() => hosts[pbds[pbd].host].hvmCapable)),\n      networkPredicate: (state, { value }) => value.sr && (network => value.sr.$pool === network.$pool),\n    },\n  }),\n  injectState,\n  injectIntl,\n  ({ effects, redeploy, state, value, intl: { formatMessage } }) => (\n    <Container>\n      <SingleLineRow>\n        <Col mediumSize={4}>\n          <Label htmlFor={state.idSelectSr}>\n            {_('destinationSR')}{' '}\n            <Tooltip content={_('proxySrPredicateInfo')}>\n              <Icon icon='info' />\n            </Tooltip>\n          </Label>\n        </Col>\n        <Col mediumSize={8}>\n          <SelectSr\n            id={state.idSelectSr}\n            onChange={effects.onSrChange}\n            predicate={state.srPredicate}\n            required\n            value={value.sr}\n          />\n        </Col>\n      </SingleLineRow>\n      <SingleLineRow className='mt-1'>\n        <Col mediumSize={4}>\n          <Label htmlFor={state.idSelectNetwork}>{_('destinationNetwork')}</Label>\n        </Col>\n        <Col mediumSize={8}>\n          <SelectNetwork\n            disabled={value.sr === undefined}\n            id={state.idSelectNetwork}\n            onChange={effects.onNetworkChange}\n            predicate={state.networkPredicate}\n            value={value.network}\n          />\n        </Col>\n      </SingleLineRow>\n      <SingleLineRow className='mt-1'>\n        <Col mediumSize={4}>\n          <Label htmlFor={state.idHttpProxyInput}>{_('httpProxy')}</Label>\n        </Col>\n        <Col mediumSize={8}>\n          <input\n            className='form-control'\n            id={state.idHttpProxyInput}\n            placeholder={formatMessage(messages.httpProxyPlaceholder)}\n            name='httpProxy'\n            onChange={effects.onInputChange}\n            value={value.httpProxy}\n          />\n        </Col>\n      </SingleLineRow>\n      <SingleLineRow className='mt-1'>\n        <Col mediumSize={4}>\n          <Label htmlFor={state.idSelectNetworkMode}>{_('networkConfiguration')}</Label>\n        </Col>\n        <Col mediumSize={8}>\n          <Select\n            id={state.idSelectNetworkMode}\n            onChange={effects.onNetworkModeChange}\n            options={NETWORK_MODE_OPTIONS}\n            required\n            simpleValue\n            value={value.networkMode}\n          />\n        </Col>\n      </SingleLineRow>\n      {state.isStaticMode && (\n        <div>\n          <SingleLineRow className='mt-1'>\n            <Col mediumSize={4}>\n              <Label htmlFor={state.idIpInput}>{_('ip')}</Label>\n            </Col>\n            <Col mediumSize={8}>\n              <input\n                className='form-control'\n                id={state.idIpInput}\n                name='ip'\n                onChange={effects.onInputChange}\n                pattern='[^\\s]+'\n                required={state.isStaticMode}\n                value={value.ip}\n              />\n            </Col>\n          </SingleLineRow>\n          <SingleLineRow className='mt-1'>\n            <Col mediumSize={4}>\n              <Label htmlFor={state.idNetmaskInput}>{_('netmask')}</Label>\n            </Col>\n            <Col mediumSize={8}>\n              <input\n                className='form-control'\n                id={state.idNetmaskInput}\n                name='netmask'\n                onChange={effects.onInputChange}\n                placeholder={formatMessage(messages.proxyNetworkNetmaskPlaceHolder, {\n                  netmask: DEFAULT_NETMASK,\n                })}\n                value={value.netmask}\n              />\n            </Col>\n          </SingleLineRow>\n          <SingleLineRow className='mt-1'>\n            <Col mediumSize={4}>\n              <Label htmlFor={state.idGatewayInput}>{_('gateway')}</Label>\n            </Col>\n            <Col mediumSize={8}>\n              <input\n                className='form-control'\n                id={state.idGatewayInput}\n                name='gateway'\n                onChange={effects.onInputChange}\n                pattern='[^\\s]+'\n                required={state.isStaticMode}\n                value={value.gateway}\n              />\n            </Col>\n          </SingleLineRow>\n          <SingleLineRow className='mt-1'>\n            <Col mediumSize={4}>\n              <Label htmlFor={state.idDnsInput}>{_('dns')}</Label>\n            </Col>\n            <Col mediumSize={8}>\n              <input\n                className='form-control'\n                id={state.idDnsInput}\n                name='dns'\n                onChange={effects.onInputChange}\n                placeholder={formatMessage(messages.proxyNetworkDnsPlaceHolder, {\n                  dns: DEFAULT_DNS,\n                })}\n                value={value.dns}\n              />\n            </Col>\n          </SingleLineRow>\n        </div>\n      )}\n      {redeploy && (\n        <SingleLineRow className='mt-1'>\n          <Col className='text-warning'>\n            <Icon icon='alarm' /> {_('redeployProxyWarning')}\n          </Col>\n        </SingleLineRow>\n      )}\n    </Container>\n  ),\n])\n\nconst deployProxy = async ({ proxy } = {}) => {\n  const licenses = await getLicenses({ productType: 'xoproxy' })\n  const isRedeployMode = proxy !== undefined\n\n  let license\n  if (isRedeployMode) {\n    license = licenses.find(license => !(license.expires < Date.now()) && license.boundObjectId === proxy.vmUuid)\n  }\n\n  // in case of deploying a proxy or when the associated proxy VM doesn't have a license\n  if (license === undefined) {\n    license = licenses.find(license => !(license.expires < Date.now()) && license.boundObjectId === undefined)\n  }\n\n  const title = isRedeployMode ? _('redeployProxy') : _('deployProxy')\n  if (license === undefined) {\n    // it rejects with undefined when the start trial option isn't chosen\n    await chooseAction({\n      body: (\n        <div className='text-muted'>\n          <Icon icon='info' /> {_('noLicenseAvailable')}\n        </div>\n      ),\n      buttons: [\n        {\n          btnStyle: 'success',\n          icon: 'trial',\n          label: _('trialStartButton'),\n        },\n      ],\n      icon: 'proxy',\n      title,\n    })\n\n    try {\n      license = await createProxyTrialLicense()\n    } catch (error) {\n      await alert(\n        _('trialStartButton'),\n        <span className='text-danger'>\n          <Icon icon='alarm' /> {error.message}\n        </span>\n      )\n\n      // throw undefined to interrupt the deployment process and let the ActionButton properly ignore this error\n      throw undefined\n    }\n  }\n\n  return form({\n    defaultValue: {\n      dns: '',\n      gateway: '',\n      httpProxy: '',\n      ip: '',\n      netmask: '',\n      networkMode: 'dhcp',\n    },\n    render: props => <Modal {...props} redeploy={isRedeployMode} />,\n    header: (\n      <span>\n        <Icon icon='proxy' /> {title}\n      </span>\n    ),\n  }).then(({ httpProxy, sr, network, networkMode, ip, netmask, gateway, dns }) =>\n    deployProxyAppliance(license, sr, {\n      httpProxy: (httpProxy = httpProxy.trim()) !== '' ? httpProxy : undefined,\n      network: network === null ? undefined : network,\n      networkConfiguration:\n        networkMode === 'static'\n          ? {\n              dns: (dns = dns.trim()) === '' ? DEFAULT_DNS : dns,\n              gateway,\n              ip,\n              netmask: (netmask = netmask.trim()) === '' ? DEFAULT_NETMASK : netmask,\n            }\n          : undefined,\n      proxy,\n    })\n  )\n}\n\nexport { deployProxy as default }\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport addSubscriptions from 'add-subscriptions'\nimport copy from 'copy-to-clipboard'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport Link from 'link'\nimport NoObjects from 'no-objects'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport { adminOnly, ShortDate } from 'utils'\nimport { confirm } from 'modal'\nimport groupBy from 'lodash/groupBy.js'\nimport { incorrectState } from 'xo-common/api-errors'\nimport { provideState, injectState } from 'reaclette'\nimport { Text } from 'editable'\nimport { Vm } from 'render-xo-item'\nimport { withRouter } from 'react-router'\nimport {\n  checkProxyHealth,\n  destroyProxyAppliances,\n  editProxyAppliance,\n  forgetProxyAppliances,\n  getLicenses,\n  getProxyApplianceUpdaterState,\n  openTunnelOnProxy,\n  registerProxy,\n  subscribeProxies,\n  upgradeProxyAppliance,\n  EXPIRES_SOON_DELAY,\n} from 'xo'\n\nimport Page from '../page'\n\nimport deployProxy from './deploy-proxy'\nimport { updateApplianceSettings } from './update-appliance-settings'\n\nimport Tooltip from '../../common/tooltip'\nimport { getXoaPlan, SOURCES } from '../../common/xoa-plans'\n\nconst _editProxy = (value, { name, proxy }) => {\n  if (typeof value === 'string') {\n    value = value.trim()\n    value = value === '' ? null : value\n  }\n  return editProxyAppliance(proxy, { [name]: value })\n}\n\nconst HEADER = (\n  <h2>\n    <Icon icon='proxy' /> {_('proxies')}\n  </h2>\n)\n\nconst ACTIONS = [\n  {\n    collapsed: true,\n    handler: forgetProxyAppliances,\n    icon: 'forget',\n    label: _('forgetProxies'),\n    level: 'danger',\n  },\n  {\n    collapsed: true,\n    handler: destroyProxyAppliances,\n    icon: 'destroy',\n    label: _('destroyProxies'),\n    level: 'danger',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    collapsed: true,\n    disabled: ({ url }) => url === undefined,\n    handler: ({ url }) => copy(url),\n    icon: 'clipboard',\n    label: ({ url }) => (\n      <Tooltip content={url !== undefined ? _('copyValue', { value: url }) : _('urlNotFound')}>\n        {_('proxyCopyUrl')}\n      </Tooltip>\n    ),\n  },\n  {\n    collapsed: true,\n    handler: (proxy, { deployProxy }) =>\n      deployProxy({\n        proxy,\n      }),\n    icon: 'refresh',\n    label: _('redeployProxyAction'),\n    level: 'warning',\n  },\n  {\n    handler: checkProxyHealth,\n    icon: 'diagnosis',\n    label: _('checkProxyHealth'),\n    level: 'primary',\n  },\n  {\n    collapsed: true,\n    disabled: ({ vmUuid }) => vmUuid === undefined,\n    handler: proxy => updateApplianceSettings(proxy),\n    icon: 'settings',\n    label: _('updateProxyApplianceSettings'),\n    level: 'primary',\n  },\n  {\n    collapsed: true,\n    disabled: ({ vmUuid }) => vmUuid === undefined,\n    handler: (proxy, { upgradeAppliance }) => upgradeAppliance(proxy.id, { force: true }),\n    icon: 'upgrade',\n    label: _('forceUpgrade'),\n    level: 'primary',\n  },\n  {\n    collapsed: true,\n    handler: proxy => openTunnelOnProxy(proxy),\n    icon: 'open-tunnel',\n    label: _('openTunnel'),\n    level: 'primary',\n  },\n  {\n    handler: ({ id }, { router }) =>\n      router.push({\n        pathname: '/settings/remotes',\n        query: {\n          l: `proxy:${id}`,\n          nfs: `proxy:${id}`,\n          smb: `proxy:${id}`,\n        },\n      }),\n    icon: 'remote',\n    label: _('proxyLinkedRemotes'),\n  },\n  {\n    handler: ({ id }, { router }) =>\n      router.push({\n        pathname: '/backup/overview',\n        query: {\n          s: `proxy:${id}`,\n        },\n      }),\n    icon: 'backup',\n    label: _('proxyLinkedBackups'),\n  },\n]\n\nconst COLUMNS = [\n  {\n    default: true,\n    itemRenderer: proxy => <Text data-name='name' data-proxy={proxy} onChange={_editProxy} value={proxy.name} />,\n    name: _('name'),\n    sortCriteria: 'name',\n  },\n  {\n    itemRenderer: proxy => <Vm id={proxy.vmUuid} link />,\n    name: _('vm'),\n  },\n  {\n    itemRenderer: proxy => (\n      <Text data-name='address' data-proxy={proxy} value={proxy.address ?? ''} onChange={_editProxy} />\n    ),\n    name: _('address'),\n  },\n  {\n    name: _('license'),\n    itemRenderer: (proxy, { isAdmin, licensesByVmUuid }) => {\n      if (proxy.vmUuid === undefined) {\n        return (\n          <span className='text-danger'>\n            {_('proxyUnknownVm')}{' '}\n            <a href='https://xen-orchestra.com/' target='_blank' rel='noreferrer'>\n              {_('contactUs')}\n            </a>\n          </span>\n        )\n      }\n\n      const licenses = licensesByVmUuid[proxy.vmUuid]\n\n      // Proxy bound to multiple licenses\n      if (licenses?.length > 1) {\n        return (\n          <span className='text-danger'>\n            {_('proxyMultipleLicenses')} <a href='https://xen-orchestra.com/'>{_('contactUs')}</a>\n          </span>\n        )\n      }\n\n      const license = licenses?.[0]\n      // Proxy not bound to any license, not even trial\n      if (license === undefined) {\n        return (\n          <span>\n            {_('noLicenseAvailable')} <Link to={`/xoa/licenses?s_proxies=id:${proxy.id}`}>{_('unlockNow')}</Link>\n          </span>\n        )\n      }\n\n      const now = Date.now()\n      const expiresSoon = license.expires - now < EXPIRES_SOON_DELAY\n      const expired = license.expires < now\n      return (\n        <span>\n          {license.expires === undefined ? (\n            '✔'\n          ) : expired ? (\n            <span>\n              {_('licenseHasExpired')} {isAdmin && <Link to='/xoa/licenses'>{_('updateLicenseMessage')}</Link>}\n            </span>\n          ) : (\n            <span className={expiresSoon && 'text-danger'}>\n              {_('licenseExpiresDate', {\n                date: <ShortDate timestamp={license.expires} />,\n              })}{' '}\n              {expiresSoon && isAdmin && <Link to='/xoa/licenses'>{_('updateLicenseMessage')}</Link>}\n            </span>\n          )}\n        </span>\n      )\n    },\n  },\n  {\n    itemRenderer: (proxy, { upgradesByProxy, upgradeAppliance }) => {\n      const globalState = upgradesByProxy[proxy.id]\n      if (globalState === undefined) {\n        return\n      }\n\n      const { state } = globalState\n      if (state.endsWith('-upgrade-needed')) {\n        return (\n          <div>\n            <ActionButton btnStyle='success' handler={upgradeAppliance} handlerParam={proxy.id} icon='upgrade'>\n              {_('upgrade')}\n            </ActionButton>\n            <p className='text-warning'>\n              <Icon icon='alarm' />\n              &nbsp;{_('upgradesAvailable')}\n            </p>\n          </div>\n        )\n      }\n\n      if (\n        state === 'xoa-up-to-date' ||\n        state === 'xoa-upgraded' ||\n        state === 'updater-upgraded' ||\n        state === 'installer-upgraded'\n      ) {\n        return <p className='text-success'>{_('proxyUpToDate')}</p>\n      }\n\n      return (\n        <div>\n          <ActionButton\n            btnStyle='success'\n            disabled={proxy.vmUuid === undefined}\n            handler={upgradeAppliance}\n            handlerParam={proxy.id}\n            icon='upgrade'\n          >\n            {_('upgrade')}\n          </ActionButton>\n          <p className='text-danger'>\n            <Icon icon='alarm' />\n            &nbsp;{globalState.message}\n          </p>\n        </div>\n      )\n    },\n    name: _('upgrade'),\n  },\n  {\n    itemRenderer: proxy => <p>{proxy?.version ?? _('unknown')}</p>,\n    name: _('version'),\n  },\n]\n\nconst Proxies = decorate([\n  provideState({\n    initialState: () => ({\n      upgradesByProxy: {},\n      licensesByVmUuid: {},\n      fetchUpgradesTimeout: undefined,\n    }),\n    effects: {\n      async initialize({ fetchProxyUpgrades }) {\n        fetchProxyUpgrades()\n\n        this.state.licensesByVmUuid = groupBy(\n          await getLicenses({ productType: 'xoproxy' }).catch(error => {\n            console.warn(error)\n            return []\n          }),\n          'boundObjectId'\n        )\n      },\n      finalize() {\n        clearTimeout(this.state.fetchUpgradesTimeout)\n      },\n      async fetchProxyUpgrades({ fetchProxyUpgrades }) {\n        clearTimeout(this.state.fetchUpgradesTimeout)\n\n        try {\n          const upgradesByProxy = { ...this.state.upgradesByProxy }\n          await Promise.all(\n            this.props.proxies.map(async ({ id }) => {\n              upgradesByProxy[id] = await getProxyApplianceUpdaterState(id).catch(e => ({\n                state: 'error',\n                message: _('proxyUpgradesError'),\n              }))\n            })\n          )\n          this.state.upgradesByProxy = upgradesByProxy\n        } catch (error) {\n          console.warn('fetchProxyUpgrades', error)\n        }\n\n        this.state.fetchUpgradesTimeout = setTimeout(fetchProxyUpgrades, 30e3)\n      },\n      async deployProxy({ fetchProxyUpgrades }, proxy) {\n        await deployProxy(proxy)\n\n        return fetchProxyUpgrades()\n      },\n      async upgradeAppliance({ fetchProxyUpgrades }, id, options) {\n        try {\n          await upgradeProxyAppliance(id, options)\n        } catch (error) {\n          if (!incorrectState.is(error)) {\n            throw error\n          }\n\n          try {\n            await confirm({\n              title: _('upgrade'),\n              body: _('proxyRunningBackupsMessage', {\n                nJobs: error.data.actual.length,\n              }),\n            })\n          } catch (_) {\n            return\n          }\n\n          await upgradeProxyAppliance(id, { ignoreRunningJobs: true })\n        }\n        return fetchProxyUpgrades()\n      },\n    },\n    computed: {\n      isFromSource: () => getXoaPlan() === SOURCES,\n    },\n  }),\n  withRouter,\n  injectState,\n  ({ effects, proxies, router, state }) => (\n    <Page header={HEADER} title='proxies' formatTitle>\n      <div>\n        <div className='mt-1 mb-1'>\n          <ActionButton\n            btnStyle='success'\n            disabled={state.isFromSource}\n            handler={effects.deployProxy}\n            icon='proxy'\n            size='large'\n            tooltip={state.isFromSource ? _('onlyAvailableXoaUsers') : undefined}\n          >\n            {_('deployProxy')}\n          </ActionButton>\n          <ActionButton\n            className='ml-1'\n            btnStyle='success'\n            disabled={state.isFromSource}\n            handler={registerProxy}\n            icon='connect'\n            size='large'\n            tooltip={state.isFromSource ? _('onlyAvailableXoaUsers') : undefined}\n          >\n            {_('registerProxy')}\n          </ActionButton>\n        </div>\n        <NoObjects\n          actions={ACTIONS}\n          collection={proxies}\n          columns={COLUMNS}\n          component={SortedTable}\n          data-deployProxy={effects.deployProxy}\n          data-licensesByVmUuid={state.licensesByVmUuid}\n          data-router={router}\n          data-upgradesByProxy={state.upgradesByProxy}\n          data-upgradeAppliance={effects.upgradeAppliance}\n          emptyMessage={\n            <span className='text-muted'>\n              <Icon icon='alarm' />\n              &nbsp;\n              {_('noProxiesAvailable')}\n            </span>\n          }\n          individualActions={INDIVIDUAL_ACTIONS}\n          stateUrlParam='s'\n        />\n      </div>\n    </Page>\n  ),\n])\n\nexport default decorate([\n  adminOnly,\n  addSubscriptions({\n    proxies: subscribeProxies,\n  }),\n  ({ proxies }) => (proxies === undefined ? _('statusLoading') : <Proxies proxies={proxies} />),\n])\n","import _, { messages } from 'intl'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport { Col, Container } from 'grid'\nimport { form } from 'modal'\nimport { generateId } from 'reaclette-utils'\nimport { injectIntl } from 'react-intl'\nimport { provideState, injectState } from 'reaclette'\nimport { updateProxyApplianceSettings } from 'xo'\n\nconst UpdateApplianceSettingsModal = decorate([\n  provideState({\n    effects: {\n      onHttpProxyChange(_, { target: { value } }) {\n        this.props.onChange({\n          ...this.props.value,\n          httpProxy: value,\n        })\n      },\n    },\n    computed: {\n      idHttpProxyInput: generateId,\n    },\n  }),\n  injectIntl,\n  injectState,\n  ({ intl: { formatMessage }, effects, state, value }) => (\n    <Container>\n      <SingleLineRow>\n        <Col mediumSize={4}>\n          <label htmlFor={state.idHttpProxyInput}>\n            <strong>{_('httpProxy')}</strong>\n          </label>\n        </Col>\n        <Col mediumSize={8}>\n          <input\n            className='form-control'\n            id={state.idHttpProxyInput}\n            onChange={effects.onHttpProxyChange}\n            placeholder={formatMessage(messages.httpProxyPlaceholder)}\n            value={value.httpProxy}\n          />\n        </Col>\n      </SingleLineRow>\n      <SingleLineRow className='mt-1'>\n        <Col className='text-info'>\n          <Icon icon='info' /> {_('proxyApplianceSettingsInfo')}\n        </Col>\n      </SingleLineRow>\n    </Container>\n  ),\n])\n\nconst updateApplianceSettings = async proxy => {\n  let { httpProxy } = await form({\n    defaultValue: {\n      httpProxy: '',\n    },\n    render: props => <UpdateApplianceSettingsModal {...props} />,\n    header: (\n      <span>\n        <Icon icon='settings' /> {_('settings')}\n      </span>\n    ),\n  })\n\n  await updateProxyApplianceSettings(proxy.id, {\n    httpProxy: (httpProxy = httpProxy.trim()) !== '' ? httpProxy : null,\n  })\n}\n\nexport { updateApplianceSettings }\n","import _ from 'intl'\nimport filter from 'lodash/filter'\nimport forEach from 'lodash/forEach'\nimport includes from 'lodash/includes'\nimport intersection from 'lodash/intersection'\nimport keyBy from 'lodash/keyBy'\nimport map from 'lodash/map'\nimport PropTypes from 'prop-types'\nimport React, { Component } from 'react'\nimport reduce from 'lodash/reduce'\nimport renderXoItem from 'render-xo-item'\nimport { resolveIds } from 'utils'\n\nimport { subscribeGroups, subscribeUsers } from 'xo'\n\n// ===================================================================\n\nexport class Subjects extends Component {\n  static propTypes = {\n    subjects: PropTypes.array.isRequired,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      groups: {},\n      users: {},\n    }\n  }\n\n  componentWillMount() {\n    const unsubscribeGroups = subscribeGroups(groups => {\n      this.setState({\n        groups: keyBy(groups, 'id'),\n      })\n    })\n    const unsubscribeUsers = subscribeUsers(users => {\n      this.setState({\n        users: keyBy(users, 'id'),\n      })\n    })\n\n    this.componentWillUnmount = () => {\n      unsubscribeGroups()\n      unsubscribeUsers()\n    }\n  }\n\n  render() {\n    const { state } = this\n\n    return (\n      <div>\n        {map(this.props.subjects, id => {\n          if (state.users[id]) {\n            return renderXoItem(\n              { type: 'user', ...state.users[id] },\n              {\n                className: 'mr-1',\n              }\n            )\n          }\n\n          if (state.groups[id]) {\n            return renderXoItem(\n              { type: 'group', ...state.groups[id] },\n              {\n                className: 'mr-1',\n              }\n            )\n          }\n\n          return (\n            <span key={id} className='mr-1'>\n              {_('unknownResourceSetValue')}\n            </span>\n          )\n        })}\n      </div>\n    )\n  }\n}\n\nexport const computeAvailableHosts = (pools, srs, hostsByPool) => {\n  const validHosts = reduce(\n    hostsByPool,\n    (result, hosts, poolId) => (includes(resolveIds(pools), poolId) ? result.concat(hosts) : result),\n    []\n  )\n\n  const availableHosts = filter(validHosts, host => {\n    let kept = false\n\n    forEach(srs, sr => !(kept = intersection(sr.$PBDs, host.$PBDs).length > 0))\n\n    return kept\n  })\n  return availableHosts\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Collapse from 'collapse'\nimport Component from 'base-component'\nimport defined from '@xen-orchestra/defined'\nimport differenceBy from 'lodash/differenceBy'\nimport filter from 'lodash/filter'\nimport forEach from 'lodash/forEach'\nimport get from 'lodash/get'\nimport Icon from 'icon'\nimport includes from 'lodash/includes'\nimport intersection from 'lodash/intersection'\nimport isEmpty from 'lodash/isEmpty'\nimport keyBy from 'lodash/keyBy'\nimport keys from 'lodash/keys'\nimport Link from 'link'\nimport map from 'lodash/map'\nimport mapKeys from 'lodash/mapKeys'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport remove from 'lodash/remove'\nimport renderXoItem from 'render-xo-item'\nimport ResourceSetQuotas from 'resource-set-quotas'\nimport size from 'lodash/size'\nimport some from 'lodash/some'\nimport Tags from 'tags'\nimport Upgrade from 'xoa-upgrade'\nimport { Container, Row, Col } from 'grid'\nimport { injectIntl } from 'react-intl'\nimport { SizeInput } from 'form'\nimport { addSubscriptions, adminOnly, connectStore, resolveIds } from 'utils'\nimport { createGetObjectsOfType, createSelector, getResolvedResourceSets } from 'selectors'\nimport {\n  createResourceSet,\n  deleteResourceSet,\n  editResourceSet,\n  recomputeResourceSetsLimits,\n  subscribeIpPools,\n  subscribeResourceSets,\n} from 'xo'\nimport { SelectIpPool, SelectNetwork, SelectPool, SelectSr, SelectSubject, SelectVmTemplate } from 'select-objects'\n\nimport { computeAvailableHosts, Subjects } from './helpers'\n\nimport Page from '../page'\n\n// ===================================================================\n\nconst TAGS_WRAPPER_STYLES = { fontSize: '1.4em' }\n\n// ===================================================================\n\nconst HEADER = (\n  <Container>\n    <Row>\n      <Col mediumSize={12}>\n        <h2>\n          <Icon icon='menu-self-service' /> {_('selfServicePage')}\n        </h2>\n      </Col>\n    </Row>\n  </Container>\n)\n\n// ===================================================================\n\nconst Hosts = ({ eligibleHosts, excludedHosts }) => (\n  <div>\n    <Row>\n      <Col mediumSize={6}>\n        <h5>{_('availableHosts')}</h5>\n        <p className='text-muted'>{_('availableHostsDescription')}</p>\n      </Col>\n      <Col mediumSize={6}>\n        <h5>{_('excludedHosts')}</h5>\n      </Col>\n    </Row>\n    <Row>\n      <Col mediumSize={6}>\n        <ul className='list-group'>\n          {eligibleHosts.length ? (\n            map(eligibleHosts, (host, key) => (\n              <li key={key} className='list-group-item'>\n                {renderXoItem(host)}\n              </li>\n            ))\n          ) : (\n            <li className='list-group-item'>{_('noHostsAvailable')}</li>\n          )}\n        </ul>\n      </Col>\n      <Col mediumSize={6}>\n        <ul className='list-group'>\n          {excludedHosts.length ? (\n            map(excludedHosts, (host, key) => (\n              <li key={key} className='list-group-item'>\n                {renderXoItem(host)}\n              </li>\n            ))\n          ) : (\n            <li className='list-group-item'>\n              <s>{_('noHostsAvailable')}</s>\n            </li>\n          )}\n        </ul>\n      </Col>\n    </Row>\n  </div>\n)\n\nHosts.propTypes = {\n  eligibleHosts: PropTypes.array.isRequired,\n  excludedHosts: PropTypes.array.isRequired,\n}\n\n// ===================================================================\n\n@connectStore(() => {\n  const getHosts = createGetObjectsOfType('host').sort()\n  const getHostsByPool = getHosts.groupBy('$pool')\n\n  return {\n    hosts: getHosts,\n    hostsByPool: getHostsByPool,\n  }\n})\nexport class Edit extends Component {\n  static propTypes = {\n    onCancel: PropTypes.func.isRequired,\n    onSave: PropTypes.func.isRequired,\n    resourceSet: PropTypes.object,\n  }\n\n  constructor(props) {\n    super(props)\n\n    this.state = {\n      cpus: '',\n      disk: null,\n      eligibleHosts: [],\n      excludedHosts: props.hosts,\n      ipPools: [],\n      memory: null,\n      name: '',\n      networks: [],\n      pools: [],\n      shareByDefault: false,\n      srs: [],\n      subjects: [],\n      templates: [],\n      tags: [],\n    }\n  }\n\n  componentDidMount() {\n    const { resourceSet } = this.props\n\n    if (resourceSet) {\n      // Objects\n      const { objectsByType } = resourceSet\n      const pools = {}\n      forEach(objectsByType, objects => {\n        forEach(objects, object => {\n          pools[object.$pool] = true\n        })\n      })\n\n      this._updateSelectedPools(keys(pools), objectsByType.SR, objectsByType.network)\n\n      // Limits and others\n      const { ipPools: rawIpPools, limits } = resourceSet\n\n      const ipPools = []\n      forEach(rawIpPools, ipPool => {\n        ipPools.push({\n          id: ipPool,\n          quantity: get(limits, `[ipPool:${ipPool}].total`),\n        })\n      })\n\n      this.setState({\n        cpus: get(limits, 'cpus.total', ''),\n        disk: get(limits, 'disk.total', null),\n        ipPools,\n        memory: get(limits, 'memory.total', null),\n        name: resourceSet.name,\n        shareByDefault: resourceSet.shareByDefault || false,\n        subjects: resourceSet.subjects,\n        tags: resourceSet.tags || [],\n        templates: objectsByType['VM-template'] || [],\n      })\n    }\n  }\n\n  _save = async () => {\n    const { cpus, disk, ipPools, memory, name, networks, shareByDefault, srs, subjects, tags, templates } = this.state\n\n    const set = this.props.resourceSet || (await createResourceSet(name))\n    const objects = [...templates, ...srs, ...networks]\n\n    const ipPoolsLimits = {}\n    forEach(ipPools, ipPool => {\n      if (ipPool.quantity) {\n        ipPoolsLimits[`ipPool:${ipPool.id}`] = +ipPool.quantity\n      }\n    })\n\n    await editResourceSet(set.id, {\n      name,\n      limits: {\n        cpus: cpus === '' ? undefined : +cpus,\n        memory: memory === null ? undefined : memory,\n        disk: disk === null ? undefined : disk,\n        ...ipPoolsLimits,\n      },\n      objects: resolveIds(objects),\n      shareByDefault,\n      subjects: resolveIds(subjects),\n      tags,\n      ipPools: resolveIds(ipPools),\n    })\n\n    this.props.onSave()\n  }\n\n  _reset = () => {\n    this._updateSelectedPools([], [], [])\n\n    this.setState({\n      cpus: '',\n      disk: null,\n      ipPools: [],\n      memory: null,\n      newIpPool: undefined,\n      newIpPoolQuantity: '',\n      shareByDefault: false,\n      subjects: [],\n      tags: [],\n    })\n  }\n\n  // -----------------------------------------------------------------------------\n\n  _updateSelectedPools = (newPools, newSrs, newNetworks) => {\n    const predicate = object => includes(resolveIds(newPools), object.$pool)\n    const internalNetworkPredicate = network => predicate(network) && isEmpty(network.PIFs)\n\n    this.setState(\n      {\n        internalNetworkPredicate,\n        pools: newPools,\n        srPredicate: predicate,\n        vmTemplatePredicate: predicate,\n      },\n      () => this._updateSelectedSrs(newSrs || this.state.srs, newNetworks)\n    )\n  }\n\n  _updateSelectedSrs = (newSrs, newNetworks) => {\n    const availableHosts = computeAvailableHosts(this.state.pools, newSrs, this.props.hostsByPool)\n    const networkPredicate = network =>\n      this.state.internalNetworkPredicate(network) ||\n      some(availableHosts, host => intersection(network.PIFs, host.PIFs).length > 0)\n\n    this.setState(\n      {\n        availableHosts,\n        networkPredicate,\n        srs: newSrs,\n      },\n      () => this._updateSelectedNetworks(newNetworks || this.state.networks)\n    )\n  }\n\n  _updateSelectedNetworks = newNetworks => {\n    const { availableHosts, srs } = this.state\n\n    const eligibleHosts = filter(availableHosts, host => {\n      let keptBySr = false\n      let keptByNetwork = false\n\n      forEach(srs, sr => !(keptBySr = intersection(sr.$PBDs, host.$PBDs).length > 0))\n\n      if (keptBySr) {\n        forEach(newNetworks, network => !(keptByNetwork = intersection(network.PIFs, host.PIFs).length > 0))\n      }\n\n      return keptBySr && keptByNetwork\n    })\n\n    this.setState({\n      eligibleHosts,\n      excludedHosts: differenceBy(this.props.hosts, eligibleHosts, host => host.id),\n      networks: newNetworks,\n    })\n  }\n\n  // -----------------------------------------------------------------------------\n\n  _onChangeIpPool = newIpPool => {\n    const { ipPools, newIpPoolQuantity } = this.state\n\n    this.setState({\n      ipPools: [...ipPools, { id: newIpPool.id, quantity: newIpPoolQuantity }],\n      newIpPoolQuantity: '',\n    })\n  }\n\n  _removeIpPool = index => {\n    const ipPools = [...this.state.ipPools]\n    remove(ipPools, (_, i) => index === i)\n    this.setState({ ipPools })\n  }\n\n  _getIpPoolPredicate = createSelector(\n    () => this.state.ipPools,\n    ipPools => {\n      const ipPoolsById = keyBy(ipPools, 'id')\n      const { hasOwnProperty } = Object.prototype\n      return ipPool => !hasOwnProperty.call(ipPoolsById, ipPool.id)\n    }\n  )\n\n  // -----------------------------------------------------------------------------\n\n  _onRemoveTag = tag =>\n    this.setState(prevState => ({\n      tags: prevState.tags.filter(_tag => tag === _tag),\n    }))\n\n  _onAddTag = tag =>\n    this.setState(prevState => ({\n      tags: prevState.tags.concat(tag),\n    }))\n\n  // -----------------------------------------------------------------------------\n\n  render() {\n    const { state } = this\n    const { onCancel, resourceSet } = this.props\n\n    return (\n      <div>\n        <li className='list-group-item'>\n          <form id='resource-set-form' className='card-block'>\n            <div className='form-group'>\n              <Row>\n                <Col mediumSize={4}>\n                  <strong>{_('resourceSetName')}</strong>\n                </Col>\n                <Col mediumSize={4}>\n                  <strong>{_('resourceSetUsers')}</strong>\n                </Col>\n                <Col mediumSize={4}>\n                  <strong>{_('resourceSetPools')}</strong>\n                </Col>\n              </Row>\n              <Row>\n                <Col mediumSize={4}>\n                  <input\n                    className='form-control'\n                    onChange={this.linkState('name')}\n                    required\n                    type='text'\n                    value={state.name}\n                  />\n                </Col>\n                <Col mediumSize={4}>\n                  <SelectSubject\n                    autoSelectSingleOption={false}\n                    hasSelectAll\n                    multi\n                    onChange={this.linkState('subjects')}\n                    required\n                    value={state.subjects}\n                  />\n                </Col>\n                <Col mediumSize={4}>\n                  <SelectPool\n                    autoSelectSingleOption={false}\n                    hasSelectAll\n                    multi\n                    onChange={this._updateSelectedPools}\n                    required\n                    value={state.pools}\n                  />\n                </Col>\n              </Row>\n            </div>\n            <div className='form-group'>\n              <Row>\n                <Col mediumSize={4}>\n                  <strong>{_('resourceSetTemplates')}</strong>\n                </Col>\n                <Col mediumSize={4}>\n                  <strong>{_('resourceSetSrs')}</strong>\n                </Col>\n                <Col mediumSize={4}>\n                  <strong>{_('resourceSetNetworks')}</strong>\n                </Col>\n              </Row>\n              <Row>\n                <Col mediumSize={4}>\n                  <SelectVmTemplate\n                    autoSelectSingleOption={false}\n                    disabled={isEmpty(state.pools)}\n                    hasSelectAll\n                    multi\n                    onChange={this.linkState('templates')}\n                    predicate={state.vmTemplatePredicate}\n                    required\n                    value={state.templates}\n                  />\n                </Col>\n                <Col mediumSize={4}>\n                  <SelectSr\n                    autoSelectSingleOption={false}\n                    disabled={isEmpty(state.pools)}\n                    hasSelectAll\n                    multi\n                    onChange={this._updateSelectedSrs}\n                    predicate={state.srPredicate}\n                    required\n                    value={state.srs}\n                  />\n                </Col>\n                <Col mediumSize={4}>\n                  <SelectNetwork\n                    autoSelectSingleOption={false}\n                    disabled={isEmpty(state.pools)}\n                    hasSelectAll\n                    multi\n                    onChange={this._updateSelectedNetworks}\n                    predicate={state.networkPredicate || state.internalNetworkPredicate}\n                    required\n                    value={state.networks}\n                  />\n                </Col>\n              </Row>\n            </div>\n            <div className='form-group'>\n              <Row>\n                <Col mediumSize={4}>\n                  <strong>{_('maxCpus')}</strong>\n                </Col>\n                <Col mediumSize={4}>\n                  <strong>{_('maxRam')}</strong>\n                </Col>\n                <Col mediumSize={4}>\n                  <strong>{_('maxDiskSpace')}</strong>\n                </Col>\n              </Row>\n              <Row>\n                <Col mediumSize={4}>\n                  <input\n                    className='form-control'\n                    min={0}\n                    onChange={this.linkState('cpus')}\n                    type='number'\n                    value={state.cpus}\n                  />\n                </Col>\n                <Col mediumSize={4}>\n                  <SizeInput onChange={this.linkState('memory')} value={state.memory} />\n                </Col>\n                <Col mediumSize={4}>\n                  <SizeInput onChange={this.linkState('disk')} value={state.disk} />\n                </Col>\n              </Row>\n            </div>\n            <div>\n              <Row>\n                <Col mediumSize={4}>\n                  <Row>\n                    <Col mediumSize={3}>\n                      <strong>{_('quantity')}</strong>\n                    </Col>\n                    <Col mediumSize={7}>\n                      <strong>{_('ipPool')}</strong>\n                    </Col>\n                  </Row>\n                  {map(state.ipPools, (ipPool, index) => (\n                    <Row className='mb-1' key={index}>\n                      <Col mediumSize={3}>\n                        <input\n                          className='form-control'\n                          type='number'\n                          min={0}\n                          onChange={this.linkState(`ipPools.${index}.quantity`)}\n                          value={defined(ipPool.quantity, '')}\n                          placeholder='∞'\n                        />\n                      </Col>\n                      <Col mediumSize={7}>\n                        <SelectIpPool onChange={this.linkState(`ipPools.${index}.id`, 'id')} value={ipPool.id} />\n                      </Col>\n                      <Col mediumSize={2}>\n                        <ActionButton icon='delete' handler={this._removeIpPool} handlerParam={index} />\n                      </Col>\n                    </Row>\n                  ))}\n                  <Row>\n                    <Col mediumSize={3}>\n                      <input\n                        className='form-control'\n                        type='number'\n                        min={0}\n                        onChange={this.linkState('newIpPoolQuantity')}\n                        value={state.newIpPoolQuantity || ''}\n                        placeholder='∞'\n                      />\n                    </Col>\n                    <Col mediumSize={7}>\n                      <SelectIpPool\n                        onChange={this._onChangeIpPool}\n                        predicate={this._getIpPoolPredicate()}\n                        value={null}\n                      />\n                    </Col>\n                  </Row>\n                </Col>\n                <Col mediumSize={4}>\n                  <Row>\n                    <Col>\n                      <strong>{_('defaultTags')}</strong>\n                    </Col>\n                  </Row>\n                  <Row>\n                    <Col>\n                      <span style={TAGS_WRAPPER_STYLES}>\n                        <Tags labels={state.tags} onAdd={this._onAddTag} onDelete={this._onRemoveTag} />\n                      </span>\n                    </Col>\n                  </Row>\n                </Col>\n              </Row>\n            </div>\n            <div className='mt-1'>\n              <label>\n                <input checked={state.shareByDefault} type='checkbox' onChange={this.toggleState('shareByDefault')} />\n                &nbsp;\n                <strong>{_('shareVmsByDefault')}</strong>\n              </label>\n            </div>\n            <hr />\n            <Hosts excludedHosts={state.excludedHosts} eligibleHosts={state.eligibleHosts} />\n          </form>\n        </li>\n        <li className='list-group-item text-xs-center'>\n          <div className='btn-toolbar'>\n            <ActionButton btnStyle='primary' icon='save' handler={this._save} type='submit'>\n              {_('saveResourceSet')}\n            </ActionButton>\n            <ActionButton icon='cancel' handler={onCancel}>\n              {_('formCancel')}\n            </ActionButton>\n            <ActionButton icon='reset' handler={this._reset}>\n              {_('resetResourceSet')}\n            </ActionButton>\n            {resourceSet && (\n              <ActionButton btnStyle='danger' icon='delete' handler={deleteResourceSet} handlerParam={resourceSet}>\n                {_('deleteResourceSet')}\n              </ActionButton>\n            )}\n          </div>\n        </li>\n      </div>\n    )\n  }\n}\n\n@addSubscriptions({\n  ipPools: subscribeIpPools,\n})\n@connectStore({\n  vms: createGetObjectsOfType('VM').filter((state, props) => vm => vm.resourceSet === props.resourceSet.id),\n})\n@injectIntl\nclass ResourceSet extends Component {\n  _renderDisplay = () => {\n    const { resourceSet, vms } = this.props\n    const resolvedIpPools = mapKeys(this.props.ipPools, 'id')\n    const { limits, ipPools, subjects, objectsByType, tags } = resourceSet\n\n    return [\n      <li key='subjects' className='list-group-item'>\n        <Subjects subjects={subjects} />\n      </li>,\n      ...map(objectsByType, (objectsSet, type) => (\n        <li key={type} className='list-group-item'>\n          {map(objectsSet, object => renderXoItem(object, { className: 'mr-1' }))}\n        </li>\n      )),\n      !isEmpty(ipPools) && (\n        <li key='ipPools' className='list-group-item'>\n          {map(ipPools, pool => {\n            const resolvedIpPool = resolvedIpPools[pool]\n            const ipPoolLimits = limits && get(limits, `[ipPool:${pool}]`)\n            const available = ipPoolLimits && ipPoolLimits.available\n            const total = ipPoolLimits && ipPoolLimits.total\n            return (\n              <span className='mr-1' key={pool}>\n                {renderXoItem({\n                  name: resolvedIpPool && resolvedIpPool.name,\n                  type: 'ipPool',\n                })}\n                {ipPoolLimits && (\n                  <span>\n                    {' '}\n                    ({available}/{total})\n                  </span>\n                )}\n              </span>\n            )\n          })}\n        </li>\n      ),\n      <li key='tags' className='list-group-item'>\n        <Icon icon='tags' /> {tags.join(', ')}\n      </li>,\n      <li key='graphs' className='list-group-item'>\n        <ResourceSetQuotas limits={limits} />\n        <Link to={`/home?s=resourceSet:${resourceSet.id}&t=VM`}>\n          <Icon icon='preview' /> {_('nVmsInResourceSet', { nVms: size(vms) })}\n        </Link>\n      </li>,\n      <li key='actions' className='list-group-item text-xs-center'>\n        <div className='btn-toolbar'>\n          <ActionButton btnStyle='primary' icon='edit' handler={this.toggleState('editionMode')}>\n            {_('editResourceSet')}\n          </ActionButton>\n          <ActionButton btnStyle='danger' icon='delete' handler={deleteResourceSet} handlerParam={resourceSet}>\n            {_('deleteResourceSet')}\n          </ActionButton>\n        </div>\n      </li>,\n    ]\n  }\n\n  _autoExpand = ref => {\n    if (ref && ref.scrollIntoView && this.props.autoExpand) {\n      ref.scrollIntoView()\n    }\n  }\n\n  render() {\n    const { resourceSet, autoExpand } = this.props\n\n    return (\n      <div className='mb-1' ref={this._autoExpand}>\n        <Collapse buttonText={`${resourceSet.name} (${resourceSet.id})`} defaultOpen={autoExpand}>\n          <ul className='list-group'>\n            {this.state.editionMode ? (\n              <Edit\n                resourceSet={this.props.resourceSet}\n                onCancel={this.toggleState('editionMode')}\n                onSave={this.toggleState('editionMode')}\n              />\n            ) : (\n              this._renderDisplay()\n            )}\n          </ul>\n        </Collapse>\n        {resourceSet.missingObjects.length > 0 && (\n          <div className='alert alert-danger mb-0' role='alert'>\n            <strong>{_('resourceSetMissingObjects')}</strong> {resourceSet.missingObjects.join(', ')}\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n\n// ===================================================================\n\nconst compareName = (a, b) => (a.name < b.name ? -1 : 1)\n\n@adminOnly\n@addSubscriptions({ resourceSets: subscribeResourceSets })\n@connectStore({ resolvedResourceSets: getResolvedResourceSets })\nexport default class Self extends Component {\n  constructor(props) {\n    super(props)\n    this.state = {}\n  }\n\n  _getSortedResourceSets = createSelector(\n    () => this.props.resolvedResourceSets,\n    resolvedResourceSets => resolvedResourceSets.sort(compareName)\n  )\n\n  render() {\n    const resourceSets = this._getSortedResourceSets()\n\n    return (\n      <Page formatTitle header={HEADER} title='selfServicePage'>\n        {process.env.XOA_PLAN > 3 ? (\n          <div>\n            <div className='mb-1'>\n              <ActionButton\n                btnStyle='primary'\n                className='mr-1'\n                handler={this.toggleState('showNewResourceSetForm')}\n                icon='add'\n              >\n                {_('resourceSetNew')}\n              </ActionButton>\n              <ActionButton handler={recomputeResourceSetsLimits} icon='refresh'>\n                {_('recomputeResourceSets')}\n              </ActionButton>\n            </div>\n            {this.state.showNewResourceSetForm && [\n              <Edit\n                key={0}\n                onCancel={this.toggleState('showNewResourceSetForm')}\n                onSave={this.toggleState('showNewResourceSetForm')}\n              />,\n              <hr key={1} />,\n            ]}\n            {isEmpty(resourceSets)\n              ? _('noResourceSets')\n              : map(resourceSets, resourceSet => (\n                  <ResourceSet\n                    autoExpand={this.props.location.query.resourceSet === resourceSet.id}\n                    key={resourceSet.id}\n                    resourceSet={resourceSet}\n                  />\n                ))}\n          </div>\n        ) : (\n          <Container>\n            <Upgrade place='selfDashboard' available={4} />\n          </Container>\n        )}\n      </Page>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport ButtonGroup from 'button-group'\nimport Component from 'base-component'\nimport decorate from 'apply-decorators'\nimport filter from 'lodash/filter'\nimport forEach from 'lodash/forEach'\nimport isEmpty from 'lodash/isEmpty'\nimport keyBy from 'lodash/keyBy'\nimport map from 'lodash/map'\nimport React from 'react'\nimport renderXoItem, { renderXoItemFromId } from 'render-xo-item'\nimport some from 'lodash/some'\nimport SortedTable from 'sorted-table'\nimport toArray from 'lodash/toArray'\nimport Upgrade from 'xoa-upgrade'\nimport store from 'store'\nimport { addSubscriptions, connectStore } from 'utils'\nimport { Container } from 'grid'\nimport { error } from 'notification'\nimport { injectState, provideState } from 'reaclette'\nimport { SelectHighLevelObject, SelectRole, SelectSubject, GenericSelectTag } from 'select-objects'\n\nimport { createGetObjectsOfType, createSelector } from 'selectors'\n\nimport {\n  addAcl,\n  editAcl,\n  removeAcl,\n  removeAcls,\n  subscribeAcls,\n  subscribeGroups,\n  subscribeRoles,\n  subscribeUsers,\n} from 'xo'\n\nconst TYPES = ['VM', 'host', 'pool', 'SR', 'network']\n\nconst ACL_COLUMNS = [\n  {\n    name: _('subjectName'),\n    itemRenderer: acl => (acl.subject.id ? renderXoItem(acl.subject) : renderXoItemFromId(acl.subject)),\n    sortCriteria: acl => (acl.subject.name || acl.subject.email || '').toLowerCase(),\n  },\n  {\n    name: _('objectName'),\n    itemRenderer: acl => (acl.object.id ? renderXoItem(acl.object) : renderXoItemFromId(acl.object)),\n    sortCriteria: acl => (acl.object.name || acl.object.name_label || '').toLowerCase(),\n  },\n  {\n    name: _('roleName'),\n    itemRenderer: acl => (\n      <SelectRole clearable={false} onChange={action => action && editAcl(acl, { action })} value={acl.action} />\n    ),\n    sortCriteria: acl => (acl.action.name || '').toLowerCase(),\n  },\n]\n\nconst ACL_ACTIONS = [\n  {\n    handler: removeAcls,\n    icon: 'delete',\n    individualHandler: removeAcl,\n    individualLabel: _('deleteAcl'),\n    label: _('deleteSelectedAcls'),\n    level: 'danger',\n  },\n]\n\nconst AclTable = decorate([\n  connectStore({\n    hosts: createGetObjectsOfType('host'),\n    networks: createGetObjectsOfType('network'),\n    pools: createGetObjectsOfType('pool'),\n    srs: createGetObjectsOfType('SR'),\n    vms: createGetObjectsOfType('VM'),\n  }),\n  addSubscriptions({\n    acls: subscribeAcls,\n    roles: subscribeRoles,\n    groups: subscribeGroups,\n    users: subscribeUsers,\n  }),\n  provideState({\n    computed: {\n      acls: ({ groups, roles, users }, { acls, hosts, networks, pools, srs, vms }) =>\n        filter(\n          map(acls, ({ id, subject, object, action }) => ({\n            id,\n            subject: users[subject] || groups[subject],\n            object: hosts[object] || networks[object] || pools[object] || srs[object] || vms[object],\n            action: roles[action],\n          })),\n          ({ subject, object, action }) => subject !== undefined && object !== undefined && action !== undefined\n        ),\n      groups: (_, { groups }) => keyBy(groups, 'id'),\n      roles: (_, { roles }) => keyBy(roles, 'id'),\n      users: (_, { users }) => keyBy(users, 'id'),\n    },\n  }),\n  injectState,\n  ({ state }) => <SortedTable actions={ACL_ACTIONS} collection={state.acls} columns={ACL_COLUMNS} stateUrlParam='s' />,\n])\n\nexport default class Acls extends Component {\n  constructor(props) {\n    super(props)\n    this.state = {\n      action: undefined,\n      objects: [],\n      subjects: [],\n      typeFilters: {},\n      tags: [],\n    }\n  }\n\n  _toggleTypeFilter = type => {\n    const { someTypeFilters, typeFilters, objects } = this.state\n\n    const newTypeFilters = { ...typeFilters, [type]: !typeFilters[type] }\n    const newSomeTypeFilters = some(newTypeFilters)\n\n    // If some objects need to be removed from the selected objects\n    if (!newTypeFilters[type] || (!someTypeFilters && newSomeTypeFilters)) {\n      this.setState({\n        objects: filter(objects, ({ type }) => !newSomeTypeFilters || newTypeFilters[type]),\n      })\n    }\n\n    this.setState(\n      {\n        typeFilters: { ...typeFilters, [type]: !typeFilters[type] },\n        someTypeFilters: some(newTypeFilters),\n      },\n      () => {\n        // If some objects need to be removed from the selected objects\n        if (!this.state.typeFilters[type] || (!someTypeFilters && this.state.someTypeFilters)) {\n          this.setState({\n            objects: filter(objects, this._getObjectPredicate()),\n          })\n        }\n      }\n    )\n  }\n\n  _getObjectPredicate = createSelector(\n    () => this.state.typeFilters,\n    () => this.state.someTypeFilters,\n    () => this.state.tags,\n    (typeFilters, someTypeFilters, selectedTags) =>\n      ({ type, tags }) =>\n        (!someTypeFilters || typeFilters[type]) &&\n        (selectedTags.length === 0 || selectedTags.some(tag => tags?.includes(tag.value)))\n  )\n\n  _selectAll = () => {\n    const { someTypeFilters, typeFilters, tags: selectedTags } = this.state\n\n    const state = store.getState()\n\n    const objects = []\n    forEach(TYPES, type => {\n      if (!someTypeFilters || typeFilters[type]) {\n        const typeObjects = toArray(state.objects.byType[type])\n        const filteredObjects =\n          selectedTags.length === 0\n            ? typeObjects\n            : typeObjects.filter(({ tags }) => selectedTags.some(tag => tags?.includes(tag.value)))\n        objects.push(...filteredObjects)\n      }\n    })\n    this.setState({ objects })\n  }\n\n  _addAcl = async () => {\n    const { subjects, objects, action } = this.state\n    try {\n      const promises = []\n      forEach(subjects, subject => {\n        forEach(objects, object => {\n          promises.push(addAcl({ subject, object, action }))\n        })\n      })\n      await Promise.all(promises)\n\n      this.setState({\n        subjects: [],\n        objects: [],\n        action: undefined,\n      })\n    } catch (err) {\n      error('Add ACL(s)', err.message || String(err))\n    }\n  }\n\n  render() {\n    const { typeFilters, objects, action, subjects, tags } = this.state\n\n    return process.env.XOA_PLAN > 2 ? (\n      <Container>\n        <form>\n          <div className='form-group'>\n            <SelectSubject multi onChange={this.linkState('subjects')} value={subjects} />\n          </div>\n          <div className='form-group mb-1 d-flex'>\n            <div style={{ flexShrink: 0 }}>\n              <ButtonGroup>\n                {map(TYPES, type => (\n                  <ActionButton\n                    btnStyle={typeFilters[type] ? 'success' : 'secondary'}\n                    handler={this._toggleTypeFilter}\n                    handlerParam={type}\n                    icon={type.toLowerCase()}\n                    key={type}\n                    size='small'\n                    tooltip={_('settingsAclsButtonTooltip' + type)}\n                  />\n                ))}\n              </ButtonGroup>{' '}\n              <ActionButton tooltip='Select all' size='small' icon='add' handler={this._selectAll} />\n            </div>\n            <GenericSelectTag\n              className='ml-1'\n              multi\n              onChange={this.linkState('tags')}\n              value={tags}\n              placeholder={_('filterByTags')}\n            />\n          </div>\n          <div className='form-group'>\n            <SelectHighLevelObject\n              multi\n              onChange={this.linkState('objects')}\n              value={objects}\n              predicate={this._getObjectPredicate()}\n            />\n          </div>\n          <div className='form-group'>\n            <SelectRole onChange={this.linkState('action')} value={action} />\n          </div>\n          <ActionButton\n            icon='add'\n            btnStyle='success'\n            handler={this._addAcl}\n            disabled={isEmpty(subjects) || isEmpty(objects) || !action}\n          >\n            {_('aclCreate')}\n          </ActionButton>\n        </form>\n        <br />\n        <AclTable />\n      </Container>\n    ) : (\n      <Container>\n        <Upgrade place='dashboard' available={3} />\n      </Container>\n    )\n  }\n}\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport Copiable from 'copiable'\nimport CopyToClipboard from 'react-copy-to-clipboard'\nimport decorate from 'apply-decorators'\nimport Dropzone from 'dropzone'\nimport Icon from 'icon'\nimport Link from 'link'\nimport NoObjects from 'no-objects'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport Tooltip from 'tooltip'\nimport Upgrade from 'xoa-upgrade'\nimport { alert, chooseAction, form } from 'modal'\nimport { alteredAuditRecord, missingAuditRecord } from 'xo-common/api-errors'\nimport { injectIntl } from 'react-intl'\nimport { get } from '@xen-orchestra/defined'\nimport { injectState, provideState } from 'reaclette'\nimport { noop, startCase } from 'lodash'\nimport { NumericDate, formatSize } from 'utils'\nimport { PREMIUM } from 'xoa-plans'\nimport { User } from 'render-xo-item'\nimport {\n  checkAuditRecordsIntegrity,\n  exportAuditRecords,\n  fetchAuditRecords,\n  generateAuditFingerprint,\n  getPlugin,\n  importAuditRecords,\n} from 'xo'\nimport RichText from 'rich-text'\n\nconst getIntegrityErrorRender = ({ nValid, error }) => (\n  <p className='text-danger'>\n    <Icon icon='alarm' />{' '}\n    {_(missingAuditRecord.is(error) ? 'auditMissingRecord' : 'auditAlteredRecord', {\n      id: (\n        <Tooltip content={_('copyToClipboard')}>\n          <CopyToClipboard text={error.data.id}>\n            <span style={{ cursor: 'pointer' }}>{error.data.id.slice(4, 8)}</span>\n          </CopyToClipboard>\n        </Tooltip>\n      ),\n      n: nValid,\n    })}\n  </p>\n)\n\nconst openGeneratedFingerprintModal = ({ fingerprint, nValid, error }) =>\n  alert(\n    <span>\n      <Icon icon='diagnosis' /> {_('auditNewFingerprint')}\n    </span>,\n    <div>\n      {error !== undefined ? (\n        <div>\n          {getIntegrityErrorRender({ nValid, error })}\n          <p>{_('auditSaveFingerprintInErrorInfo')}</p>\n        </div>\n      ) : (\n        <p>{_('auditSaveFingerprintInfo')}</p>\n      )}\n      <p className='input-group mt-1'>\n        <input className='form-control' value={fingerprint} disabled />\n        <span className='input-group-btn'>\n          <Tooltip content={_('auditCopyFingerprintToClipboard')}>\n            <CopyToClipboard text={fingerprint}>\n              <Button>\n                <Icon icon='clipboard' />\n              </Button>\n            </CopyToClipboard>\n          </Tooltip>\n        </span>\n      </p>\n    </div>\n  ).catch(noop)\n\nconst openIntegrityFeedbackModal = error =>\n  chooseAction({\n    icon: 'diagnosis',\n    title: _('auditCheckIntegrity'),\n    body:\n      error !== undefined ? (\n        getIntegrityErrorRender(error)\n      ) : (\n        <p className='text-success'>\n          {_('auditIntegrityVerified')} <Icon icon='success' />\n        </p>\n      ),\n    buttons: [\n      {\n        btnStyle: 'success',\n        label: _('auditGenerateNewFingerprint'),\n      },\n    ],\n  }).then(\n    () => true,\n    () => false\n  )\n\nconst FingerPrintModalBody = injectIntl(({ intl: { formatMessage }, onChange, value }) => (\n  <div>\n    <p>{_('auditEnterFingerprintInfo')}</p>\n    <div className='form-group'>\n      <input\n        className='form-control'\n        onChange={onChange}\n        pattern='[^|]+\\|[^|]+'\n        placeholder={formatMessage(messages.auditEnterFingerprint)}\n        value={value}\n      />\n    </div>\n  </div>\n))\n\nconst openFingerprintPromptModal = () =>\n  form({\n    render: ({ onChange, value }) => <FingerPrintModalBody onChange={onChange} value={value} />,\n    header: (\n      <span>\n        <Icon icon='diagnosis' /> {_('auditCheckIntegrity')}\n      </span>\n    ),\n  }).then((value = '') => value.trim(), noop)\n\nconst checkIntegrity = async ({ handleCheck }) => {\n  const fingerprint = await openFingerprintPromptModal()\n  if (fingerprint === undefined) {\n    return\n  }\n\n  let recentRecord\n  if (fingerprint !== '') {\n    const [oldest, newest] = fingerprint.split('|')\n    recentRecord = newest\n\n    const result = await checkAuditRecordsIntegrity(oldest, newest).then(noop, error => {\n      if (missingAuditRecord.is(error) || alteredAuditRecord.is(error)) {\n        return {\n          nValid: error.data.nValid,\n          error,\n        }\n      }\n      throw error\n    })\n\n    handleCheck(\n      oldest,\n      newest,\n      get(() => result.error)\n    )\n\n    const shouldGenerateFingerprint = await openIntegrityFeedbackModal(result)\n    if (!shouldGenerateFingerprint) {\n      return\n    }\n  }\n\n  const generatedFingerprint = await generateAuditFingerprint(recentRecord)\n\n  // display coherence feedback\n  handleCheck(...generatedFingerprint.fingerprint.split('|'), generatedFingerprint.error)\n\n  await openGeneratedFingerprintModal(generatedFingerprint)\n}\n\nconst displayRecord = record =>\n  alert(\n    <span>\n      <Icon icon='audit' /> {_('auditRecord')}\n    </span>,\n    <RichText copiable message={JSON.stringify(record, null, 2)} />\n  )\n\nconst renderImportStatus = ({\n  recordsFile,\n  importError,\n  importResults: { nInvalidRecords, nMissingRecords, lastLogId } = {},\n  importStatus,\n}) => {\n  switch (importStatus) {\n    case 'noFile':\n      return _('noAuditRecordsFile')\n    case 'selectedFile':\n      return <span>{`${recordsFile?.name} (${formatSize(recordsFile?.size)})`}</span>\n    case 'start':\n      return <Icon icon='loading' />\n    case 'end':\n      if (nInvalidRecords === 0 && nMissingRecords === 0) {\n        return <span className='text-success'>{_('importAuditRecordsSuccess')}</span>\n      } else {\n        return (\n          <span className='text-warning'>\n            {_('importAuditRecordsSuccessWithProblems', { nInvalidRecords, nMissingRecords, lastLogId })}\n          </span>\n        )\n      }\n    case 'importError':\n      return <span className='text-danger'>{_('importAuditRecordsError', { importError: importError || '' })}</span>\n  }\n}\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    handler: displayRecord,\n    icon: 'preview',\n    label: _('displayAuditRecord'),\n  },\n]\n\nconst COLUMNS = [\n  {\n    itemRenderer: ({ subject: { userId, userName } }) =>\n      userId !== undefined ? (\n        <User\n          defaultRender={\n            <Copiable tagName='p' text={userId} className='text-muted'>\n              {_('deletedUser', { name: userName })}\n            </Copiable>\n          }\n          id={userId}\n          link\n          newTab\n        />\n      ) : (\n        <p className='text-muted'>{_('noUser')}</p>\n      ),\n    name: _('user'),\n    sortCriteria: 'subject.userName',\n  },\n  {\n    name: _('ip'),\n    valuePath: 'subject.userIp',\n  },\n  {\n    itemRenderer: ({ data, event }) => (event === 'apiCall' ? data.method : startCase(event)),\n    name: _('auditActionEvent'),\n    sortCriteria: ({ data, event }) => (event === 'apiCall' ? data.method : event),\n  },\n  {\n    itemRenderer: ({ time }) => <NumericDate timestamp={time} />,\n    name: _('date'),\n    sortCriteria: 'time',\n    sortOrder: 'desc',\n  },\n  {\n    itemRenderer: ({ id }, { checkedRecords, missingRecord }) => {\n      if (missingRecord !== id && checkedRecords[id] === undefined) {\n        return\n      }\n\n      if (missingRecord === id) {\n        return (\n          <span className='text-danger'>\n            <Icon icon='error' /> {_('missing')}\n          </span>\n        )\n      }\n\n      if (checkedRecords[id]) {\n        return (\n          <span className='text-success'>\n            <Icon icon='success' /> {_('verified')}\n          </span>\n        )\n      }\n\n      return (\n        <span className='text-danger'>\n          <Icon icon='error' /> {_('altered')}\n        </span>\n      )\n    },\n    name: _('integrity'),\n  },\n]\n\nexport default decorate([\n  provideState({\n    initialState: () => ({\n      _records: undefined,\n      checkedRecords: {},\n      goTo: undefined,\n      importError: undefined,\n      importStatus: 'noFile',\n      importResults: undefined,\n      missingRecord: undefined,\n      recordsFile: undefined,\n      showImportDropzone: false,\n    }),\n    effects: {\n      initialize({ fetchRecords }) {\n        return fetchRecords()\n      },\n      async fetchRecords() {\n        this.state._records = await fetchAuditRecords()\n      },\n      showImportDropzone() {\n        this.state.showImportDropzone = !this.state.showImportDropzone\n      },\n      startImportRecords() {\n        this.state.importStatus = 'start'\n\n        return importAuditRecords(this.state.recordsFile)\n          .then(\n            importResults => {\n              this.state.recordsFile = undefined\n              this.state.importStatus = 'end'\n              this.state.importResults = importResults\n            },\n            error => {\n              this.state.recordsFile = undefined\n              this.state.importStatus = 'importError'\n              this.state.importError = error?.message\n            }\n          )\n          .finally(this.effects.fetchRecords)\n      },\n      handleDrop(_, files) {\n        this.state.recordsFile = files && files[0]\n        this.state.importStatus = 'selectedFile'\n      },\n      unselectFile() {\n        this.state.recordsFile = undefined\n        this.state.importStatus = 'noFile'\n      },\n      handleRef(_, ref) {\n        if (ref !== null) {\n          const component = ref.getWrappedInstance()\n          this.state.goTo = component.goTo.bind(component)\n        }\n      },\n      handleCheck(_, oldest, newest, error) {\n        const { state } = this\n        const checkedRecords = { ...state.checkedRecords }\n\n        if (error !== undefined) {\n          const { id } = error.data\n          oldest = id\n\n          if (missingAuditRecord.is(error)) {\n            state.missingRecord = id\n          } else {\n            checkedRecords[id] = false\n          }\n\n          state.goTo(id)\n\n          // the newest is inaccessible or altered\n          if (id === newest) {\n            return\n          }\n        }\n\n        const records = state._records\n        let i = records.findIndex(({ id }) => id === newest)\n        let record\n        do {\n          record = records[i]\n          checkedRecords[record.id] = true\n          i++\n        } while (record.previousId !== oldest)\n\n        state.checkedRecords = checkedRecords\n      },\n    },\n    computed: {\n      records: ({ _records, missingRecord }) =>\n        _records !== undefined && missingRecord !== undefined\n          ? [\n              ..._records,\n              {\n                id: missingRecord,\n                subject: {},\n                time: 0,\n              },\n            ]\n          : _records,\n      isUserActionsRecordInactive: async () => {\n        const { configuration: { active } = {} } = await getPlugin('audit')\n\n        return !active\n      },\n    },\n  }),\n  injectState,\n  ({ state, effects }) => (\n    <Upgrade place='audit' available={PREMIUM.value}>\n      <div>\n        <div className='mt-1 mb-1'>\n          <ActionButton btnStyle='primary' handler={effects.fetchRecords} icon='refresh' size='large'>\n            {_('refreshAuditRecordsList')}\n          </ActionButton>{' '}\n          <ActionButton btnStyle='primary' handler={exportAuditRecords} icon='download' size='large'>\n            {_('downloadAuditRecords')}\n          </ActionButton>{' '}\n          <ActionButton\n            btnStyle='success'\n            data-handleCheck={effects.handleCheck}\n            handler={checkIntegrity}\n            icon='diagnosis'\n            size='large'\n          >\n            {_('auditCheckIntegrity')}\n          </ActionButton>{' '}\n          <ActionButton\n            btnStyle='warning'\n            disabled={state.records?.length > 0}\n            handler={effects.showImportDropzone}\n            icon='upload'\n            size='large'\n            tooltip={_('importAuditRecordsTooltip')}\n          >\n            {_('importAuditRecords')}\n          </ActionButton>\n        </div>\n\n        {!!state.showImportDropzone && (\n          <div>\n            <Dropzone onDrop={effects.handleDrop} message={_('importRecordsTip')} />\n            {renderImportStatus(state)}\n            <div className='form-group pull-right'>\n              <ActionButton\n                btnStyle='primary'\n                className='mr-1'\n                disabled={!state.recordsFile}\n                handler={effects.startImportRecords}\n                icon='import'\n                type='submit'\n              >\n                {_('importAuditRecordsButton')}\n              </ActionButton>\n              <Button onClick={effects.unselectFile}>{_('importAuditRecordsCleanList')}</Button>\n            </div>\n          </div>\n        )}\n\n        {state.isUserActionsRecordInactive && (\n          <p>\n            <Link\n              className='text-warning'\n              to={{\n                pathname: '/settings/plugins',\n                query: {\n                  s: 'id:/^audit$/',\n                },\n              }}\n            >\n              <Icon icon='alarm' /> {_('auditInactiveUserActionsRecord')}\n            </Link>\n          </p>\n        )}\n        <NoObjects\n          collection={state.records}\n          columns={COLUMNS}\n          component={SortedTable}\n          componentRef={effects.handleRef}\n          data-checkedRecords={state.checkedRecords}\n          data-missingRecord={state.missingRecord}\n          defaultColumn={3}\n          emptyMessage={\n            <span className='text-muted'>\n              <Icon icon='alarm' />\n              &nbsp;\n              {_('noAuditRecordAvailable')}\n            </span>\n          }\n          individualActions={INDIVIDUAL_ACTIONS}\n          stateUrlParam='s'\n        />\n      </div>\n    </Upgrade>\n  ),\n])\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport decorate from 'apply-decorators'\nimport defined from '@xen-orchestra/defined'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport { addSubscriptions } from 'utils'\nimport { AvailableTemplateVars, DEFAULT_CLOUD_CONFIG_TEMPLATE, DEFAULT_NETWORK_CONFIG_TEMPLATE } from 'cloud-config'\nimport { Container, Col } from 'grid'\nimport find from 'lodash/find.js'\nimport { generateId } from 'reaclette-utils'\nimport { injectState, provideState } from 'reaclette'\nimport { Text } from 'editable'\nimport { Textarea as DebounceTextarea } from 'debounce-input-decorator'\nimport {\n  createCloudConfig,\n  createNetworkConfig,\n  deleteCloudConfigs,\n  deleteNetworkConfigs,\n  editCloudConfig,\n  editNetworkConfig,\n  subscribeCloudConfigs,\n  subscribeNetworkConfigs,\n} from 'xo'\n\n// ===================================================================\n\nconst COLUMNS = [\n  {\n    itemRenderer: _ => _.id.slice(4, 8),\n    name: _('formId'),\n    sortCriteria: _ => _.id.slice(4, 8),\n  },\n  {\n    itemRenderer: ({ id, name }) => <Text value={name} onChange={name => editCloudConfig(id, { name })} />,\n    sortCriteria: 'name',\n    name: _('formName'),\n    default: true,\n  },\n]\n\nconst ACTIONS = [\n  {\n    handler: (ids, { type }) => (type === 'network' ? deleteNetworkConfigs(ids) : deleteCloudConfigs(ids)),\n    icon: 'delete',\n    individualLabel: _('deleteCloudConfig'),\n    label: _('deleteSelectedCloudConfigs'),\n    level: 'danger',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    handler: (cloudConfig, { populateForm }) => populateForm(cloudConfig),\n    icon: 'edit',\n    label: _('editCloudConfig'),\n    level: 'primary',\n  },\n]\n\nconst initialParams = {\n  cloudConfigToEditId: undefined,\n  name: '',\n  networkConfigToEditId: undefined,\n  networkConfigName: '',\n  networkConfigTemplate: undefined,\n  template: undefined,\n}\n\nexport default decorate([\n  addSubscriptions({\n    cloudConfigs: subscribeCloudConfigs,\n    networkConfigs: subscribeNetworkConfigs,\n  }),\n  provideState({\n    initialState: () => initialParams,\n    effects: {\n      setInputValue:\n        (_, { target: { name, value } }) =>\n        state => ({\n          ...state,\n          [name]: value,\n        }),\n      reset: () => state => ({\n        ...state,\n        cloudConfigToEditId: initialParams.cloudConfigToEditId,\n        name: initialParams.name,\n        template: initialParams.template,\n      }),\n      resetNetworkForm: () => state => ({\n        ...state,\n        networkConfigToEditId: initialParams.networkConfigToEditId,\n        networkConfigName: initialParams.networkConfigName,\n        networkConfigTemplate: initialParams.networkConfigTemplate,\n      }),\n      createCloudConfig:\n        ({ reset }) =>\n        async ({ name, template = DEFAULT_CLOUD_CONFIG_TEMPLATE }) => {\n          await createCloudConfig({ name, template })\n          reset()\n        },\n      createNetworkConfig:\n        ({ resetNetworkForm }) =>\n        async ({ networkConfigName, networkConfigTemplate = DEFAULT_NETWORK_CONFIG_TEMPLATE }) => {\n          await createNetworkConfig({ name: networkConfigName, template: networkConfigTemplate })\n          resetNetworkForm()\n        },\n      editCloudConfig:\n        ({ reset }) =>\n        async ({ name, template, cloudConfigToEditId }, { cloudConfigs }) => {\n          const oldCloudConfig = find(cloudConfigs, { id: cloudConfigToEditId })\n          if (oldCloudConfig.name !== name || oldCloudConfig.template !== template) {\n            await editCloudConfig(cloudConfigToEditId, { name, template })\n          }\n          reset()\n        },\n      editNetworkConfig:\n        ({ resetNetworkForm }) =>\n        async ({ networkConfigName, networkConfigTemplate, networkConfigToEditId }, { networkConfigs }) => {\n          const oldNetworkConfig = find(networkConfigs, { id: networkConfigToEditId })\n          if (oldNetworkConfig.name !== networkConfigName || oldNetworkConfig.template !== networkConfigTemplate) {\n            await editNetworkConfig(networkConfigToEditId, { name: networkConfigName, template: networkConfigTemplate })\n          }\n          resetNetworkForm()\n        },\n      populateNetworkForm:\n        (_, { id, name, template }) =>\n        state => ({\n          ...state,\n          networkConfigName: name,\n          networkConfigToEditId: id,\n          networkConfigTemplate: template,\n        }),\n      populateForm:\n        (_, { id, name, template }) =>\n        state => ({\n          ...state,\n          name,\n          cloudConfigToEditId: id,\n          template,\n        }),\n    },\n    computed: {\n      formId: generateId,\n      inputNameId: generateId,\n      inputTemplateId: generateId,\n      isInvalid: ({ name, template }) => name.trim() === '' || (template !== undefined && template.trim() === ''),\n      isNetworkInvalid: props =>\n        props.networkConfigName.trim() === '' ||\n        (props.networkConfigTemplate !== undefined && props.networkConfigTemplate.trim() === ''),\n    },\n  }),\n  injectState,\n  ({ cloudConfigs, effects, networkConfigs, state }) => (\n    <div>\n      <Container>\n        <Col mediumSize={6}>\n          <h2>{_('cloudConfig')}</h2>\n          <form id={state.formId}>\n            <div className='form-group'>\n              <label htmlFor={state.inputNameId}>\n                <strong>{_('formName')}</strong>{' '}\n              </label>\n              <input\n                className='form-control'\n                id={state.inputNameId}\n                name='name'\n                onChange={effects.setInputValue}\n                type='text'\n                value={state.name}\n              />\n            </div>{' '}\n            <div className='form-group'>\n              <label htmlFor={state.inputTemplateId}>\n                <strong>{_('settingsCloudConfigTemplate')}</strong>{' '}\n              </label>{' '}\n              <AvailableTemplateVars />\n              <DebounceTextarea\n                className='form-control text-monospace'\n                id={state.inputTemplateId}\n                name='template'\n                onChange={effects.setInputValue}\n                rows={12}\n                value={defined(state.template, DEFAULT_CLOUD_CONFIG_TEMPLATE)}\n              />\n            </div>{' '}\n            {state.cloudConfigToEditId !== undefined ? (\n              <ActionButton\n                btnStyle='primary'\n                disabled={state.isInvalid}\n                form={state.formId}\n                handler={effects.editCloudConfig}\n                icon='edit'\n              >\n                {_('formEdit')}\n              </ActionButton>\n            ) : (\n              <ActionButton\n                btnStyle='success'\n                disabled={state.isInvalid}\n                form={state.formId}\n                handler={effects.createCloudConfig}\n                icon='add'\n              >\n                {_('formCreate')}\n              </ActionButton>\n            )}\n            <ActionButton className='pull-right' handler={effects.reset} icon='cancel'>\n              {_('formCancel')}\n            </ActionButton>\n          </form>\n        </Col>\n        <Col mediumSize={6}>\n          <SortedTable\n            actions={ACTIONS}\n            collection={cloudConfigs}\n            columns={COLUMNS}\n            data-populateForm={effects.populateForm}\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='s'\n          />\n        </Col>\n      </Container>\n      <Container className='mt-2'>\n        <Col mediumSize={6}>\n          <h2>{_('networkConfig')}</h2>\n          <form>\n            <div className='form-group'>\n              <label>\n                <strong>{_('formName')}</strong>\n              </label>\n              <input\n                className='form-control'\n                name='networkConfigName'\n                onChange={effects.setInputValue}\n                type='text'\n                value={state.networkConfigName}\n              />\n            </div>\n            <div className='form-group'>\n              <label htmlFor={state.inputTemplateId}>\n                <strong>{_('settingsCloudConfigTemplate')}</strong>\n              </label>\n              <DebounceTextarea\n                className='form-control text-monospace'\n                id={state.inputTemplateId}\n                name='networkConfigTemplate'\n                onChange={effects.setInputValue}\n                rows={12}\n                value={defined(state.networkConfigTemplate, DEFAULT_NETWORK_CONFIG_TEMPLATE)}\n              />\n            </div>\n            {state.networkConfigToEditId !== undefined ? (\n              <ActionButton\n                btnStyle='primary'\n                disabled={state.isNetworkInvalid}\n                handler={effects.editNetworkConfig}\n                icon='edit'\n              >\n                {_('formEdit')}\n              </ActionButton>\n            ) : (\n              <ActionButton\n                btnStyle='success'\n                disabled={state.isNetworkInvalid}\n                handler={effects.createNetworkConfig}\n                icon='add'\n              >\n                {_('formCreate')}\n              </ActionButton>\n            )}\n            <ActionButton className='pull-right' handler={effects.resetNetworkForm} icon='cancel'>\n              {_('formCancel')}\n            </ActionButton>\n          </form>\n        </Col>\n        <Col mediumSize={6}>\n          <SortedTable\n            actions={ACTIONS}\n            collection={networkConfigs}\n            columns={COLUMNS}\n            data-populateForm={effects.populateNetworkForm}\n            data-type='network'\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='n'\n          />\n        </Col>\n      </Container>\n    </div>\n  ),\n])\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport Component from 'base-component'\nimport Dropzone from 'dropzone'\nimport Icon from 'icon'\nimport React from 'react'\nimport { formatSize } from 'utils'\nimport { getXoaPlan, SOURCES } from 'xoa-plans'\nimport { importConfig, exportConfig } from 'xo'\n\nimport CloudConfig from './xo-cloud-config'\n\n// ===================================================================\n\nexport default class Config extends Component {\n  componentWillMount() {\n    this.state = { importStatus: 'noFile' }\n  }\n\n  _importConfig = () => {\n    this.setState({ importStatus: 'start' }, () =>\n      importConfig(this.state.configFile).then(\n        imported => {\n          if (imported !== false) {\n            this.setState({ configFile: undefined, importStatus: 'end' })\n          } else {\n            this.setState({ importStatus: 'selectedFile' })\n          }\n        },\n        () => this.setState({ configFile: undefined, importStatus: 'importError' })\n      )\n    )\n  }\n\n  _handleDrop = files =>\n    this.setState({\n      configFile: files && files[0],\n      importStatus: 'selectedFile',\n    })\n\n  _unselectFile = () => this.setState({ configFile: undefined, importStatus: 'noFile' })\n\n  _renderImportStatus = () => {\n    const { configFile, importStatus } = this.state\n\n    switch (importStatus) {\n      case 'noFile':\n        return _('noConfigFile')\n      case 'selectedFile':\n        return <span>{`${configFile.name} (${formatSize(configFile.size)})`}</span>\n      case 'start':\n        return <Icon icon='loading' />\n      case 'end':\n        return <span className='text-success'>{_('importConfigSuccess')}</span>\n      case 'importError':\n        return <span className='text-danger'>{_('importConfigError')}</span>\n    }\n  }\n\n  render() {\n    const { configFile } = this.state\n\n    return (\n      <div>\n        {getXoaPlan() !== SOURCES && <CloudConfig />}\n        <div className='mb-1 mt-1'>\n          <h2>\n            <Icon icon='import' /> {_('importConfig')}\n          </h2>\n          <form id='import-form'>\n            <Dropzone onDrop={this._handleDrop} message={_('importTip')} />\n            {this._renderImportStatus()}\n            <div className='form-group pull-right'>\n              <ActionButton\n                btnStyle='primary'\n                className='mr-1'\n                disabled={!configFile}\n                form='import-form'\n                handler={this._importConfig}\n                icon='import'\n                type='submit'\n              >\n                {_('importConfig')}\n              </ActionButton>\n              <Button onClick={this._unselectFile}>{_('importVmsCleanList')}</Button>\n            </div>\n          </form>\n        </div>\n        <br />\n        <div className='mt-1'>\n          <h2>\n            <Icon icon='export' /> {_('exportConfig')}\n          </h2>\n          <Button btnStyle='primary' onClick={exportConfig}>\n            {_('downloadConfig')}\n          </Button>\n        </div>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport BaseComponent from 'base-component'\nimport React from 'react'\nimport { Password } from 'form'\n\nclass BackupXoConfigModal extends BaseComponent {\n  get value() {\n    return {\n      passphrase: this.state.passphrase,\n    }\n  }\n\n  render() {\n    return (\n      <div>\n        <label>{_('xoCloudConfigEnterPassphrase')}</label>\n        <Password autoFocus onChange={this.linkState('passphrase')} value={this.state.passphrase} />\n      </div>\n    )\n  }\n}\n\nexport default BackupXoConfigModal\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport addSubscriptions from 'add-subscriptions'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport { confirm } from 'modal'\nimport { injectState, provideState } from 'reaclette'\nimport { SelectXoCloudConfig } from 'select-objects'\nimport { subscribeCloudXoConfig, subscribeCloudXoConfigBackups } from 'xo'\n\nimport BackupXoConfigModal from './backup-xo-config-modal'\nimport RestoreXoConfigModal from './restore-xo-config-modal'\n\nconst CloudConfig = decorate([\n  addSubscriptions({\n    cloudXoConfig: subscribeCloudXoConfig,\n    cloudXoConfigBackups: subscribeCloudXoConfigBackups,\n  }),\n  provideState({\n    initialState: () => ({ config: undefined }),\n    effects: {\n      downloadCloudXoConfig:\n        () =>\n        ({ config, isConfigDefined }) => {\n          if (isConfigDefined) {\n            window.open(config.content_href, '_blank')\n          }\n        },\n      restoreCloudXoConfig:\n        () =>\n        async ({ config, isConfigDefined }) => {\n          if (isConfigDefined) {\n            const { passphrase } = await confirm({\n              icon: 'backup',\n              title: _('xoConfigCloudBackup'),\n              body: <RestoreXoConfigModal />,\n            })\n\n            const resp = await fetch(`./rest/v0/cloud/xo-config/backups/${config.id}/actions/import?sync`, {\n              method: 'POST',\n              headers: {\n                'content-type': 'application/json',\n              },\n              body: JSON.stringify({\n                passphrase,\n              }),\n            })\n            if (!resp.ok) {\n              throw new Error(resp.statusText)\n            }\n            return {\n              config: undefined,\n            }\n          }\n        },\n      onChangeCloudXoConfig: (_, config) => ({\n        config,\n      }),\n      toggleEnableCloudXoConfig:\n        () =>\n        async (state, { cloudXoConfig }) => {\n          let passphrase\n          if (!cloudXoConfig?.enabled) {\n            const params = await confirm({\n              icon: 'backup',\n              title: _('xoConfigCloudBackup'),\n              body: <BackupXoConfigModal />,\n            })\n            passphrase = params.passphrase\n          }\n\n          const resp = await fetch('./rest/v0/cloud/xo-config', {\n            method: 'PATCH',\n            headers: {\n              'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({\n              enabled: !cloudXoConfig?.enabled,\n              passphrase,\n            }),\n          })\n          if (!resp.ok) {\n            throw new Error(resp.statusText)\n          }\n          subscribeCloudXoConfig.forceRefresh()\n        },\n    },\n    computed: {\n      isConfigDefined: ({ config }) => config != null,\n    },\n  }),\n  injectState,\n  ({ effects, state, cloudXoConfig }) => (\n    <div>\n      <div className='mb-1'>\n        <h2>\n          <Icon icon='backup' /> {_('manageXoConfigCloudBackup')}\n        </h2>\n        <em>\n          <Icon icon='info' /> {_('xoConfigCloudBackupTips')}\n        </em>\n        <br />\n        <ActionButton\n          btnStyle={cloudXoConfig?.enabled ? 'warning' : 'primary'}\n          handler={effects.toggleEnableCloudXoConfig}\n          icon='backup'\n        >\n          {cloudXoConfig?.enabled ? _('disable') : _('enable')}\n        </ActionButton>\n      </div>\n      <div>\n        <h2>\n          <Icon icon='xo-cloud-config' /> {_('backedUpXoConfigs')}\n        </h2>\n        <SelectXoCloudConfig onChange={effects.onChangeCloudXoConfig} value={state.config} />\n        <div className='mt-1'>\n          <ActionButton\n            handler={effects.restoreCloudXoConfig}\n            btnStyle='warning'\n            icon='upload'\n            disabled={!state.isConfigDefined}\n          >\n            {_('restore')}\n          </ActionButton>{' '}\n          <ActionButton\n            btnStyle='primary'\n            icon='download'\n            handler={effects.downloadCloudXoConfig}\n            disabled={!state.isConfigDefined}\n          >\n            {_('download')}\n          </ActionButton>\n        </div>\n      </div>\n    </div>\n  ),\n])\n\nexport default CloudConfig\n","import _ from 'intl'\nimport BaseComponent from 'base-component'\nimport React from 'react'\nimport { Password } from 'form'\n\nclass RestoreXoConfigModal extends BaseComponent {\n  get value() {\n    return {\n      passphrase: this.state.passphrase,\n    }\n  }\n\n  render() {\n    return (\n      <div>\n        <label>{_('xoCloudConfigRestoreEnterPassphrase')}</label>\n        <Password autoFocus onChange={this.linkState('passphrase')} value={this.state.passphrase} />\n      </div>\n    )\n  }\n}\n\nexport default RestoreXoConfigModal\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport { conditionalTooltip } from 'tooltip'\nimport { addSubscriptions } from 'utils'\nimport { createSelector } from 'selectors'\nimport { includes, isEmpty, keyBy, map, size } from 'lodash'\nimport { injectIntl } from 'react-intl'\nimport { SelectSubject } from 'select-objects'\nimport { Text } from 'editable'\n\nimport {\n  addUserToGroup,\n  createGroup,\n  deleteGroup,\n  deleteGroups,\n  removeUserFromGroup,\n  setGroupName,\n  subscribeGroups,\n  subscribePlugins,\n  subscribeUsers,\n  synchronizeLdapGroups,\n} from 'xo'\n\n@addSubscriptions({\n  users: cb => subscribeUsers(users => cb(keyBy(users, 'id'))),\n})\nclass UserDisplay extends Component {\n  static propTypes = {\n    id: PropTypes.string.isRequired, // user id\n    group: PropTypes.object.isRequired, // group\n  }\n\n  _removeUser = () => {\n    const { id, group } = this.props\n    return removeUserFromGroup(id, group)\n  }\n\n  render() {\n    const { id, users, canRemove } = this.props\n\n    return (\n      <span>\n        {(id && users && users[id] && users[id].email) || (\n          <em>\n            &lt;\n            {_('unknownUser')}\n            &gt;\n          </em>\n        )}{' '}\n        {canRemove && (\n          <ActionButton\n            className='pull-right'\n            btnStyle='primary'\n            size='small'\n            icon='remove'\n            handler={this._removeUser}\n          />\n        )}\n      </span>\n    )\n  }\n}\n\nclass GroupMembersDisplay extends Component {\n  _toggle = () => this.setState({ open: !this.state.open })\n\n  render() {\n    const { group } = this.props\n    return (\n      <div>\n        {isEmpty(group.users) ? (\n          <em>{_('noUserInGroup')}</em>\n        ) : (\n          <div>\n            <div>\n              {_('countUsers', { users: size(group.users) })}\n              <ActionButton\n                className='pull-right'\n                size='small'\n                icon={this.state.open ? 'minus' : 'plus'}\n                handler={this._toggle}\n              />\n            </div>\n            {this.state.open && (\n              <div className='mt-1'>\n                <ul className='list-group'>\n                  {map(group.users, user => (\n                    <li className='list-group-item' key={user}>\n                      <UserDisplay id={user} group={group} canRemove={group.provider === undefined} />\n                    </li>\n                  ))}\n                </ul>\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n\nconst getPredicate = users => entity => entity.email && !includes(users, entity.id) // Entity is a user and is not already in list\n\nconst GROUP_COLUMNS = [\n  {\n    name: _('groupNameColumn'),\n    itemRenderer: group =>\n      group.provider === undefined ? (\n        <Text value={group.name} onChange={value => setGroupName(group, value)} />\n      ) : (\n        group.name\n      ),\n    sortCriteria: group => group.name,\n  },\n  {\n    name: _('groupUsersColumn'),\n    itemRenderer: group => <GroupMembersDisplay group={group} />,\n  },\n  {\n    name: _('addUserToGroupColumn'),\n    itemRenderer: group =>\n      group.provider === undefined ? (\n        <SelectSubject\n          predicate={getPredicate(group.users)}\n          onChange={user => user && addUserToGroup(user, group)}\n          value={null}\n        />\n      ) : null,\n  },\n]\n\nconst ACTIONS = [\n  {\n    handler: deleteGroups,\n    icon: 'delete',\n    individualHandler: deleteGroup,\n    individualLabel: _('deleteGroup'),\n    label: _('deleteSelectedGroups'),\n    level: 'danger',\n  },\n]\n\n@addSubscriptions({\n  groups: subscribeGroups,\n  plugins: subscribePlugins,\n})\n@injectIntl\nexport default class Groups extends Component {\n  _createGroup = () => {\n    const { name } = this.refs\n    if (name) {\n      return createGroup(name.value).then(() => {\n        name.value = ''\n      })\n    }\n  }\n\n  _isLdapGroupSyncConfigured = createSelector(\n    () => this.props.plugins,\n    plugins => {\n      if (plugins === undefined) {\n        return false\n      }\n\n      const ldapPlugin = plugins.find(({ name }) => name === 'auth-ldap')\n      if (ldapPlugin === undefined) {\n        return false\n      }\n\n      return ldapPlugin.loaded && ldapPlugin.configuration.groups !== undefined\n    }\n  )\n\n  render() {\n    const { groups, intl } = this.props\n    const disableLdapGroupSync = !this._isLdapGroupSyncConfigured()\n\n    return (\n      <div>\n        {conditionalTooltip(\n          <ActionButton\n            btnStyle='primary'\n            className='mr-1 mb-1'\n            disabled={disableLdapGroupSync}\n            handler={synchronizeLdapGroups}\n            icon='refresh'\n          >\n            {_('syncLdapGroups')}\n          </ActionButton>,\n          disableLdapGroupSync ? _('ldapPluginNotConfigured') : undefined\n        )}\n        <form id='newGroupForm' className='form-inline'>\n          <div className='form-group'>\n            <input\n              type='text'\n              ref='name'\n              placeholder={intl.formatMessage(messages.newGroupName)}\n              required\n              className='form-control'\n            />\n          </div>{' '}\n          <div className='form-group'>\n            <ActionButton form='newGroupForm' icon='add' btnStyle='success' handler={this._createGroup}>\n              {_('createGroupButton')}\n            </ActionButton>\n          </div>\n        </form>\n        <hr />\n        {isEmpty(groups) ? (\n          <p>\n            <em>{_('noGroupFound')}</em>\n          </p>\n        ) : (\n          <SortedTable actions={ACTIONS} collection={groups} columns={GROUP_COLUMNS} stateUrlParam='s' />\n        )}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport Icon from 'icon'\nimport Page from '../page'\nimport React from 'react'\nimport { adminOnly, routes } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { NavLink, NavTabs } from 'nav'\n\nimport Acls from './acls'\nimport Audit from './audit'\nimport CloudConfigs from './cloud-configs'\nimport Config from './config'\nimport Groups from './groups'\nimport Ips from './ips'\nimport Logs from './logs'\nimport Plugins from './plugins'\nimport Remotes from './remotes'\nimport Servers from './servers'\nimport Users from './users'\n\nconst HEADER = (\n  <Container>\n    <Row>\n      <Col mediumSize={3}>\n        <h2>\n          <Icon icon='menu-settings' /> {_('settingsPage')}\n        </h2>\n      </Col>\n      <Col mediumSize={9}>\n        <NavTabs className='pull-right'>\n          <NavLink to='/settings/servers'>\n            <Icon icon='menu-settings-servers' /> {_('settingsServersPage')}\n          </NavLink>\n          <NavLink to='/settings/users'>\n            <Icon icon='menu-settings-users' /> {_('settingsUsersPage')}\n          </NavLink>\n          <NavLink to='/settings/groups'>\n            <Icon icon='menu-settings-groups' /> {_('settingsGroupsPage')}\n          </NavLink>\n          <NavLink to='/settings/acls'>\n            <Icon icon='menu-settings-acls' /> {_('settingsAclsPage')}\n          </NavLink>\n          <NavLink to='/settings/remotes'>\n            <Icon icon='menu-backup-remotes' /> {_('backupRemotesPage')}\n          </NavLink>\n          <NavLink to='/settings/plugins'>\n            <Icon icon='menu-settings-plugins' /> {_('settingsPluginsPage')}\n          </NavLink>\n          <NavLink to='/settings/logs'>\n            <Icon icon='menu-settings-logs' /> {_('settingsLogsPage')}\n          </NavLink>\n          <NavLink to='/settings/audit'>\n            <Icon icon='audit' /> {_('settingsAuditPage')}\n          </NavLink>\n          <NavLink to='/settings/ips'>\n            <Icon icon='ip' /> {_('settingsIpsPage')}\n          </NavLink>\n          <NavLink to='/settings/cloud-configs'>\n            <Icon icon='template' /> {_('settingsCloudConfigsPage')}\n          </NavLink>\n          <NavLink to='/settings/config'>\n            <Icon icon='menu-settings-config' /> {_('xoConfig')}\n          </NavLink>\n        </NavTabs>\n      </Col>\n    </Row>\n  </Container>\n)\n\nconst Settings = routes('servers', {\n  acls: Acls,\n  audit: Audit,\n  'cloud-configs': CloudConfigs,\n  config: Config,\n  groups: Groups,\n  ips: Ips,\n  logs: Logs,\n  plugins: Plugins,\n  remotes: Remotes,\n  servers: Servers,\n  users: Users,\n})(\n  adminOnly(({ children }) => (\n    <Page header={HEADER} title='settingsPage' formatTitle>\n      {children}\n    </Page>\n  ))\n)\n\nexport default Settings\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport ActionRowButton from 'action-row-button'\nimport BaseComponent from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport SortedTable from 'sorted-table'\nimport Upgrade from 'xoa-upgrade'\nimport { addSubscriptions, connectStore } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { createGetObjectsOfType, createSelector } from 'selectors'\n// eslint-disable-next-line n/no-extraneous-import\nimport { formatIps, getNextIpV4, parseIpPattern } from 'ip-utils'\nimport { injectIntl } from 'react-intl'\nimport { Input as DebounceInput } from 'debounce-input-decorator'\nimport { renderXoItemFromId } from 'render-xo-item'\nimport { SelectNetwork } from 'select-objects'\nimport { Text } from 'editable'\nimport { some, findIndex, forEach, includes, isEmpty, isObject, keys, map } from 'lodash'\nimport { createIpPool, deleteIpPool, setIpPool, subscribeIpPools } from 'xo'\n\nconst FULL_WIDTH = { width: '100%' }\nconst NETWORK_FORM_STYLE = { maxWidth: '40em' }\nconst IPS_PATTERN = (() => {\n  const ipRe = '\\\\d{1,3}(\\\\.\\\\d{1,3}){3}'\n  const ipOrRangeRe = `${ipRe}(-${ipRe})?`\n  return `${ipOrRangeRe}(;${ipOrRangeRe})*`\n})()\n\n@connectStore(() => ({\n  networks: createGetObjectsOfType('network').groupBy('id'),\n  vifs: createGetObjectsOfType('VIF').groupBy('id'),\n}))\nclass IpsCell extends BaseComponent {\n  _addIps = () => {\n    const addresses = {}\n    forEach(parseIpPattern(this.state.newIps), ip => {\n      addresses[ip] = {}\n    })\n    setIpPool(this.props.ipPool.id, { addresses })\n    this.setState({ newIps: '' })\n  }\n\n  _deleteIp = ip => {\n    const toBeRemoved = {}\n    if (isObject(ip)) {\n      let currentIp = ip.first\n      while (currentIp !== ip.last) {\n        toBeRemoved[currentIp] = null\n        currentIp = getNextIpV4(currentIp)\n      }\n      toBeRemoved[currentIp] = null\n    } else {\n      toBeRemoved[ip] = null\n    }\n    setIpPool(this.props.ipPool.id, { addresses: toBeRemoved })\n  }\n\n  render() {\n    const { ipPool, networks, vifs } = this.props\n    const { newIps, showNewIpForm } = this.state\n\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={6} offset={5}>\n            <strong>{_('ipsVifs')}</strong>\n          </Col>\n        </Row>\n        {ipPool.addresses &&\n          map(formatIps(keys(ipPool.addresses)), (ip, key) => {\n            if (isObject(ip)) {\n              // Range of IPs\n              return (\n                <Row key={key}>\n                  <Col mediumSize={5}>\n                    <strong>\n                      {ip.first} <Icon icon='arrow-right' /> {ip.last}\n                    </strong>\n                  </Col>\n                  <Col mediumSize={1} offset={6}>\n                    <ActionRowButton handler={this._deleteIp} handlerParam={ip} icon='delete' />\n                  </Col>\n                </Row>\n              )\n            }\n            const addressVifs = ipPool.addresses[ip].vifs\n            return (\n              <Row>\n                <Col mediumSize={5}>\n                  <strong>{ip}</strong>\n                </Col>\n                <Col mediumSize={6}>\n                  {!isEmpty(addressVifs) ? (\n                    map(addressVifs, (vifId, index) => {\n                      const vif = vifs[vifId] && vifs[vifId][0]\n                      const network = vif && networks[vif.$network] && networks[vif.$network][0]\n                      return (\n                        <span key={index} className='mr-1'>\n                          {network && vif ? `${network.name_label} #${vif.device}` : <em>{_('ipPoolUnknownVif')}</em>}\n                        </span>\n                      )\n                    })\n                  ) : (\n                    <em>{_('ipsNotUsed')}</em>\n                  )}\n                </Col>\n                <Col mediumSize={1}>\n                  <ActionRowButton handler={this._deleteIp} handlerParam={ip} icon='delete' />\n                </Col>\n              </Row>\n            )\n          })}\n        <Row>\n          <Col>\n            {showNewIpForm ? (\n              <form id='newIpForm' className='form-inline'>\n                <ActionButton btnStyle='danger' handler={this.toggleState('showNewIpForm')} icon='remove' />{' '}\n                <DebounceInput\n                  autoFocus\n                  onChange={this.linkState('newIps')}\n                  type='text'\n                  className='form-control'\n                  required\n                  value={newIps || ''}\n                />{' '}\n                <ActionButton form='newIpForm' icon='save' btnStyle='primary' handler={this._addIps} />\n              </form>\n            ) : (\n              <ActionButton btnStyle='success' size='small' handler={this.toggleState('showNewIpForm')} icon='add' />\n            )}\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n\nclass NetworksCell extends BaseComponent {\n  state = { newNetworks: [] }\n\n  _addNetworks = () => {\n    if (isEmpty(this.state.newNetworks)) {\n      return this._toggleNewNetworks()\n    }\n    const { ipPool } = this.props\n    setIpPool(ipPool.id, {\n      networks: [...ipPool.networks, ...this.state.newNetworks],\n    })\n    this._toggleNewNetworks()\n    this.setState({ newNetworks: [] })\n  }\n\n  _deleteNetwork = networkId => {\n    const _networks = [...this.props.ipPool.networks]\n    const index = findIndex(_networks, network => network === networkId)\n    if (index !== -1) {\n      _networks.splice(index, 1)\n      setIpPool(this.props.ipPool.id, { networks: _networks })\n    }\n  }\n\n  _toggleNewNetworks = () => this.setState({ showNewNetworkForm: !this.state.showNewNetworkForm })\n  _getNetworkPredicate = createSelector(\n    () => this.props.ipPool && this.props.ipPool.networks,\n    networks => network => !includes(networks, network.id)\n  )\n\n  render() {\n    const { ipPool } = this.props\n    const { newNetworks, showNewNetworkForm } = this.state\n\n    return (\n      <Container>\n        {map(ipPool.networks, networkId => (\n          <Row key={networkId}>\n            <Col mediumSize={11}>{renderXoItemFromId(networkId)}</Col>\n            <Col mediumSize={1}>\n              <ActionRowButton handler={this._deleteNetwork} handlerParam={networkId} icon='delete' size='small' />\n            </Col>\n          </Row>\n        ))}\n        <Row>\n          {showNewNetworkForm ? (\n            <form id='newNetworkForm' style={NETWORK_FORM_STYLE}>\n              <Col mediumSize={10}>\n                <SelectNetwork\n                  autoFocus\n                  multi\n                  onChange={this.linkState('newNetworks', '*.id')}\n                  predicate={this._getNetworkPredicate()}\n                  value={newNetworks}\n                />\n              </Col>\n              <Col mediumSize={2}>\n                <ActionButton form='newNetworkForm' icon='save' btnStyle='primary' handler={this._addNetworks} />\n              </Col>\n            </form>\n          ) : (\n            <Col>\n              <ActionButton btnStyle='success' size='small' handler={this._toggleNewNetworks} icon='add' />\n            </Col>\n          )}\n        </Row>\n      </Container>\n    )\n  }\n}\n\n@addSubscriptions({\n  ipPools: subscribeIpPools,\n})\n@injectIntl\nexport default class Ips extends BaseComponent {\n  _create = () => {\n    const { name, ips, networks } = this.state\n\n    this.setState({ creatingIpPool: true })\n    return createIpPool({\n      ips: parseIpPattern(ips),\n      name,\n      networks: map(networks, 'id'),\n    }).then(() => {\n      this.setState({\n        creatingIpPool: false,\n        ips: undefined,\n        name: undefined,\n        networks: [],\n      })\n    })\n  }\n\n  _getNameAlreadyExists = createSelector(\n    () => this.props.ipPools,\n    ipPools => name => some(ipPools, { name })\n  )\n\n  _disableCreation = createSelector(\n    this._getNameAlreadyExists,\n    () => this.state,\n    (nameAlreadyExists, { name, ips, networks }) =>\n      !name || isEmpty(ips) || isEmpty(networks) || nameAlreadyExists(name)\n  )\n\n  _onChangeIpPoolName = (ipPool, name) => {\n    if (some(this.props.ipPools, { name })) {\n      throw new Error(this.props.intl.formatMessage(messages.ipPoolNameAlreadyExists))\n    }\n\n    return setIpPool(ipPool, { name })\n  }\n\n  _ipColumns = [\n    {\n      default: true,\n      name: _('ipPoolName'),\n      itemRenderer: ipPool => <Text onChange={name => this._onChangeIpPoolName(ipPool, name)} value={ipPool.name} />,\n      sortCriteria: ipPool => ipPool.name,\n    },\n    {\n      name: _('ipPoolIps'),\n      itemRenderer: ipPool => <IpsCell ipPool={ipPool} />,\n    },\n    {\n      name: _('ipPoolNetworks'),\n      itemRenderer: ipPool => <NetworksCell ipPool={ipPool} />,\n    },\n    {\n      name: '',\n      itemRenderer: ipPool => (\n        <span className='pull-right'>\n          <ActionButton handler={deleteIpPool} handlerParam={ipPool.id} icon='delete' />\n        </span>\n      ),\n    },\n  ]\n\n  render() {\n    if (process.env.XOA_PLAN < 4) {\n      return (\n        <Container>\n          <Upgrade place='health' available={4} />\n        </Container>\n      )\n    }\n\n    const { ipPools, intl } = this.props\n    const { creatingIpPool, ips, name, networks } = this.state\n\n    return (\n      <div>\n        <Row>\n          <Col size={6}>\n            <form id='newIpPoolForm' className='form-inline'>\n              <SingleLineRow>\n                <Col mediumSize={6}>\n                  <input\n                    className='form-control'\n                    disabled={creatingIpPool}\n                    onChange={this.linkState('name')}\n                    placeholder={intl.formatMessage(messages.ipPoolName)}\n                    required\n                    style={FULL_WIDTH}\n                    type='text'\n                    value={name || ''}\n                  />\n                </Col>\n                <Col mediumSize={6}>\n                  <input\n                    className='form-control'\n                    disabled={creatingIpPool}\n                    onChange={this.linkState('ips')}\n                    pattern={IPS_PATTERN}\n                    placeholder={intl.formatMessage(messages.ipPoolIps)}\n                    required\n                    style={FULL_WIDTH}\n                    type='text'\n                    value={ips || ''}\n                  />\n                </Col>\n              </SingleLineRow>\n              <br />\n              <SingleLineRow>\n                <Col mediumSize={12}>\n                  <SelectNetwork\n                    disabled={creatingIpPool}\n                    multi\n                    onChange={this.linkState('networks')}\n                    value={networks}\n                  />\n                </Col>\n              </SingleLineRow>\n              <br />\n              <SingleLineRow>\n                <Col mediumSize={6}>\n                  <ActionButton\n                    btnStyle='success'\n                    disabled={this._disableCreation()}\n                    form='newIpPoolForm'\n                    icon='add'\n                    handler={this._create}\n                  >\n                    {_('ipsCreate')}\n                  </ActionButton>\n                </Col>\n              </SingleLineRow>\n            </form>\n          </Col>\n        </Row>\n        <hr />\n        {isEmpty(ipPools) ? (\n          <p>\n            <em>{_('ipsNoIpPool')}</em>\n          </p>\n        ) : (\n          <SortedTable collection={ipPools} columns={this._ipColumns} stateUrlParam='s' />\n        )}\n      </div>\n    )\n  }\n}\n","module.exports = {\n    \"widthLimit\": \"mc6ad38729_widthLimit\"\n};","import React from 'react'\nimport { find, map } from 'lodash'\n\nimport _ from 'intl'\nimport BaseComponent from 'base-component'\nimport NoObjects from 'no-objects'\nimport SortedTable from 'sorted-table'\nimport styles from './index.css'\nimport { addSubscriptions, downloadLog, NumericDate } from 'utils'\nimport { alert } from 'modal'\nimport { createSelector } from 'selectors'\nimport { get } from '@xen-orchestra/defined'\nimport { reportBug } from 'report-bug-button'\nimport { deleteApiLog, deleteApiLogs, subscribeApiLogs, subscribeUsers } from 'xo'\nimport RichText from 'rich-text'\n\nconst formatMessage = data =>\n  `\\`\\`\\`\\n${data.method}\\n${JSON.stringify(data.params, null, 2)}\\n${JSON.stringify(data.error, null, 2).replace(\n    /\\\\n/g,\n    '\\n'\n  )}\\n\\`\\`\\``\n\nconst formatLog = log =>\n  `${log.data.method}\\n${JSON.stringify(log.data.params, null, 2)}\\n${JSON.stringify(log.data.error, null, 2).replace(\n    /\\\\n/g,\n    '\\n'\n  )}`\n\nconst LogMessage = ({ item: log }) => {\n  const { error } = log.data\n  return (\n    <span>\n      <pre className={styles.widthLimit}>{get(() => error.message)}</pre>\n      {get(() => error.code) === 'LICENCE_RESTRICTION' ? (\n        <a href='https://xcp-ng.org/' rel='noopener noreferrer' target='_blank'>\n          {_('logSuggestXcpNg')}\n        </a>\n      ) : get(() => error.name) === 'XapiError' ? (\n        _('logXapiError')\n      ) : null}\n    </span>\n  )\n}\n\nconst COLUMNS = [\n  {\n    name: _('logUser'),\n    itemRenderer: (log, { users }) => {\n      if (log.data.userId == null) {\n        return _('noUser')\n      }\n      if (!users) {\n        return '...'\n      }\n      const user = find(users, user => user.id === log.data.userId)\n      return user ? user.email : _('unknownUser')\n    },\n    sortCriteria: log => log.data.userId,\n  },\n  {\n    name: _('logMessage'),\n    component: LogMessage,\n    sortCriteria: log => log.data.error && log.data.error.message,\n  },\n  {\n    default: true,\n    name: _('logTime'),\n    itemRenderer: log => <span>{log.time && <NumericDate timestamp={log.time} />}</span>,\n    sortCriteria: log => log.time,\n    sortOrder: 'desc',\n  },\n]\n\nconst ACTIONS = [\n  {\n    handler: deleteApiLogs,\n    individualHandler: deleteApiLog,\n    individualLabel: _('logDelete'),\n    icon: 'delete',\n    label: _('logsDelete'),\n    level: 'danger',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    handler: log => alert(_('logError'), <RichText copiable message={formatLog(log)} />),\n    icon: 'preview',\n    label: _('logDisplayDetails'),\n  },\n  {\n    handler: log => downloadLog({ log: formatLog(log), date: log.time, type: 'XO' }),\n    icon: 'download',\n    label: _('logDownload'),\n  },\n  {\n    handler: log =>\n      reportBug({\n        formatMessage,\n        message: log.data,\n        title: `Error on ${log.data.method}`,\n      }),\n    icon: 'bug',\n    label: _('reportBug'),\n  },\n]\n\n@addSubscriptions({\n  logs: subscribeApiLogs,\n  users: subscribeUsers,\n})\nexport default class Logs extends BaseComponent {\n  _getLogs = createSelector(\n    () => this.props.logs,\n    logs => logs && map(logs, (log, id) => ({ ...log, id }))\n  )\n\n  _getPredicate = logs => logs != null\n\n  render() {\n    const logs = this._getLogs()\n\n    return (\n      <NoObjects collection={logs} message={_('noLogs')} predicate={this._getPredicate}>\n        {() => (\n          <SortedTable\n            actions={ACTIONS}\n            collection={logs}\n            columns={COLUMNS}\n            data-users={this.props.users}\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='s'\n          />\n        )}\n      </NoObjects>\n    )\n  }\n}\n","import * as ComplexMatcher from 'complex-matcher'\nimport _ from 'intl'\nimport ActionButton from 'action-button'\nimport ActionToggle from 'action-toggle'\nimport Button from 'button'\nimport Component from 'base-component'\nimport decorate from 'apply-decorators'\nimport escapeRegExp from 'lodash/escapeRegExp'\nimport GenericInput from 'json-schema-input'\nimport Icon from 'icon'\nimport isEmpty from 'lodash/isEmpty'\nimport Link from 'link'\nimport map from 'lodash/map'\nimport merge from 'lodash/merge'\nimport orderBy from 'lodash/orderBy'\nimport pFinally from 'promise-toolbox/finally'\nimport React from 'react'\nimport size from 'lodash/size'\nimport { addSubscriptions } from 'utils'\nimport { alert } from 'modal'\nimport { createSelector } from 'reselect'\nimport { generateUiSchema } from 'xo-json-schema-input'\nimport { get } from '@xen-orchestra/defined'\nimport { injectState, provideState } from 'reaclette'\nimport { Row, Col } from 'grid'\nimport {\n  configurePlugin,\n  disablePluginAutoload,\n  enablePluginAutoload,\n  loadPlugin,\n  purgePluginConfiguration,\n  subscribePlugins,\n  testPlugin,\n  unloadPlugin,\n} from 'xo'\n\nclass Plugin extends Component {\n  constructor(props) {\n    super(props)\n\n    this.configFormId = `form-config-${props.id}`\n    this.testFormId = `form-test-${props.id}`\n  }\n\n  _getPluginLink = createSelector(\n    () => this.props.name,\n    name => {\n      const s = new ComplexMatcher.Property(\n        'name',\n        new ComplexMatcher.RegExp('^' + escapeRegExp(name) + '$')\n      ).toString()\n      return location => ({ ...location, query: { ...location.query, s } })\n    }\n  )\n\n  _getUiSchema = createSelector(() => this.props.configurationSchema, generateUiSchema)\n\n  _updateExpanded = () => {\n    this.setState({\n      expanded: !this.state.expanded,\n    })\n  }\n\n  _setAutoload = event => {\n    if (this._updateAutoload) {\n      return\n    }\n\n    this._updateAutoload = true\n\n    const method = event.target.checked ? enablePluginAutoload : disablePluginAutoload\n    method(this.props.id)::pFinally(() => {\n      this._updateAutoload = false\n    })\n  }\n\n  _updateLoad = () => {\n    const { props } = this\n    const { id } = props\n\n    if (!props.loaded) {\n      enablePluginAutoload(id).catch(console.warn)\n      return loadPlugin(id)\n    }\n    if (props.unloadable !== false) {\n      disablePluginAutoload(id).catch(console.warn)\n      return unloadPlugin(id)\n    }\n  }\n\n  _saveConfiguration = async () => {\n    await configurePlugin(this.props.id, this.state.editedConfig)\n    this._stopEditing()\n  }\n\n  _deleteConfiguration = async () => {\n    await purgePluginConfiguration(this.props.id)\n    this._stopEditing()\n  }\n\n  _stopEditing = event => {\n    event && event.preventDefault()\n\n    this.setState({\n      editedConfig: undefined,\n    })\n  }\n\n  _applyPredefinedConfiguration = () => {\n    const configName = this.refs.selectPredefinedConfiguration.value\n    this.setState({\n      editedConfig: merge(undefined, this.state.editedConfig, this.props.configurationPresets[configName]),\n    })\n  }\n\n  _test = async () => {\n    try {\n      const { testInput } = this.refs\n      await testPlugin(this.props.id, testInput && testInput.value)\n      alert(_('pluginTest'), <p>{_('pluginConfirmation')}</p>)\n    } catch (err) {\n      await alert(\n        'You have an error!',\n        <div>\n          <p>Code: {err.code}</p>\n          <p>Message: {err.message}</p>\n          {err.data && <pre>{JSON.stringify(err.data, null, 2)}</pre>}\n        </div>\n      )\n      throw err\n    }\n  }\n\n  render() {\n    const { props, state } = this\n    const { editedConfig, expanded } = state\n    const { configurationPresets, configurationSchema, loaded } = props\n    const description = get(() => props.description.trim())\n\n    return (\n      <div className='card-block'>\n        <Row>\n          <Col mediumSize={8}>\n            <h5 className='form-inline clearfix'>\n              <ActionToggle disabled={loaded && props.unloadable === false} handler={this._updateLoad} value={loaded} />{' '}\n              <Link to={this._getPluginLink()}>{props.name}</Link> <span>{`(v${props.version}) `}</span>\n              {description !== undefined && description !== '' && (\n                <span className='text-muted small'> - {description}</span>\n              )}\n              <div className='checkbox small'>\n                <label className='text-muted'>\n                  {_('autoloadPlugin')} <input type='checkbox' checked={props.autoload} onChange={this._setAutoload} />\n                </label>\n              </div>\n            </h5>\n          </Col>\n          {configurationSchema !== undefined && (\n            <Col className='text-xs-right' mediumSize={4}>\n              <Button btnStyle='primary' onClick={this._updateExpanded}>\n                <Icon icon={expanded ? 'minus' : 'plus'} />\n              </Button>\n            </Col>\n          )}\n        </Row>\n        {expanded && (\n          <form id={this.configFormId} onReset={this._stopEditing}>\n            {size(configurationPresets) > 0 && (\n              <div>\n                <legend>{_('pluginConfigurationPresetTitle')}</legend>\n                <span className='text-muted'>\n                  <p>{_('pluginConfigurationChoosePreset')}</p>\n                </span>\n                <div className='input-group'>\n                  <select className='form-control' ref='selectPredefinedConfiguration'>\n                    {map(configurationPresets, (_, name) => (\n                      <option key={name} value={name}>\n                        {name}\n                      </option>\n                    ))}\n                  </select>\n                  <span className='input-group-btn'>\n                    <Button btnStyle='primary' onClick={this._applyPredefinedConfiguration}>\n                      {_('applyPluginPreset')}\n                    </Button>\n                  </span>\n                </div>\n                <hr />\n              </div>\n            )}\n            <GenericInput\n              label='Configuration'\n              required\n              schema={configurationSchema}\n              uiSchema={this._getUiSchema()}\n              onChange={this.linkState('editedConfig')}\n              value={editedConfig || props.configuration}\n            />\n            <div className='form-group pull-right'>\n              <div className='btn-toolbar'>\n                <div className='btn-group'>\n                  <ActionButton\n                    btnStyle='danger'\n                    disabled={!props.configuration}\n                    handler={this._deleteConfiguration}\n                    icon='delete'\n                  >\n                    {_('deletePluginConfiguration')}\n                  </ActionButton>\n                </div>\n                <div className='btn-group'>\n                  <Button disabled={!editedConfig} type='reset'>\n                    {_('cancelPluginEdition')}\n                  </Button>\n                </div>\n                <div className='btn-group'>\n                  <ActionButton\n                    btnStyle='primary'\n                    disabled={!editedConfig}\n                    form={this.configFormId}\n                    handler={this._saveConfiguration}\n                    icon='save'\n                  >\n                    {_('savePluginConfiguration')}\n                  </ActionButton>\n                </div>\n              </div>\n            </div>\n          </form>\n        )}\n        {expanded && props.testable && (\n          <form id={this.testFormId}>\n            {props.testSchema && (\n              <GenericInput\n                label='Test data'\n                schema={props.testSchema}\n                uiSchema={generateUiSchema(props.testSchema)}\n                required\n                ref='testInput'\n              />\n            )}\n            <div className='form-group pull-right'>\n              <ActionButton\n                btnStyle='primary'\n                disabled={!loaded}\n                form={this.testFormId}\n                handler={this._test}\n                icon='diagnosis'\n                tooltip={loaded ? undefined : _('disabledTestPluginTootltip')}\n              >\n                Test plugin\n              </ActionButton>\n            </div>\n          </form>\n        )}\n      </div>\n    )\n  }\n}\n\nexport default decorate([\n  addSubscriptions({\n    plugins: subscribePlugins,\n  }),\n  provideState({\n    effects: {\n      onSearchChange(_, { target: { value } }) {\n        const { location, router } = this.props\n        router.replace({\n          ...location,\n          query: {\n            ...location.query,\n            s: value,\n          },\n        })\n      },\n    },\n    computed: {\n      search: (\n        _,\n        {\n          location: {\n            query: { s = '' },\n          },\n        }\n      ) => s,\n      filteredPlugins: ({ predicate }, { plugins }) => (predicate === undefined ? plugins : plugins.filter(predicate)),\n      predicate: ({ search }) => {\n        if (search.trim() === '') {\n          return\n        }\n\n        try {\n          return ComplexMatcher.parse(search).createPredicate()\n        } catch (error) {\n          console.warn(error)\n        }\n      },\n      sortedPlugins: ({ filteredPlugins }) => orderBy(filteredPlugins, 'name'),\n    },\n  }),\n  injectState,\n  ({ effects, state, plugins }) =>\n    isEmpty(plugins) ? (\n      <p>\n        <em>{_('noPlugins')}</em>\n      </p>\n    ) : (\n      <div>\n        <p>\n          <input className='form-control' onChange={effects.onSearchChange} value={state.search} />\n        </p>\n        <span>\n          {_('homeDisplayedItems', {\n            displayed: state.sortedPlugins.length,\n            icon: <Icon icon='plugin' />,\n            total: plugins.length,\n          })}\n        </span>\n        <ul style={{ paddingLeft: 0 }}>\n          {state.sortedPlugins.map(plugin => (\n            <li key={plugin.id} className='list-group-item clearfix'>\n              <Plugin {...plugin} />\n            </li>\n          ))}\n        </ul>\n      </div>\n    ),\n])\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport Tooltip from 'tooltip'\nimport { addSubscriptions, formatSize, formatSpeed, generateRandomId, noop } from 'utils'\nimport { alert } from 'modal'\nimport { format, parse } from 'xo-remote-parser'\nimport { get } from '@xen-orchestra/defined'\nimport { groupBy, map, isEmpty } from 'lodash'\nimport { injectIntl } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport { Number, Password, Text, XoSelect } from 'editable'\nimport { Proxy } from 'render-xo-item'\n\nimport {\n  deleteRemote,\n  deleteRemotes,\n  disableRemote,\n  editRemote,\n  enableRemote,\n  subscribeRemotes,\n  subscribeRemotesInfo,\n  testRemote,\n} from 'xo'\n\nimport Remote from './remote'\n\nconst formatError = error => (typeof error === 'string' ? error : JSON.stringify(error, null, 2).replace(/\\\\n/g, '\\n'))\n\nconst _changeUrlElement = (value, { remote, element }) =>\n  editRemote(remote, {\n    url: format({ ...parse(remote.url), [element]: value === null ? undefined : value }),\n  })\nconst _showError = remote => alert(_('remoteConnectionFailed'), <pre>{formatError(remote.error)}</pre>)\nconst _editRemoteName = (name, { remote }) => editRemote(remote, { name })\nconst _editRemoteOptions = (options, { remote }) => editRemote(remote, { options: options !== '' ? options : null })\n\nconst COLUMN_NAME = {\n  itemRenderer: (remote, { formatMessage }) => (\n    <Text\n      data-remote={remote}\n      onChange={_editRemoteName}\n      placeholder={formatMessage(messages.remoteMyNamePlaceHolder)}\n      value={remote.name}\n    />\n  ),\n  name: _('remoteName'),\n  sortCriteria: 'name',\n}\nconst COLUMN_STATE = {\n  itemRenderer: remote => (\n    <div>\n      <StateButton\n        disabledLabel={_('remoteDisabled')}\n        disabledHandler={enableRemote}\n        disabledTooltip={_('enableRemote')}\n        enabledLabel={_('remoteEnabled')}\n        enabledHandler={disableRemote}\n        enabledTooltip={_('disableRemote')}\n        handlerParam={remote}\n        state={remote.enabled}\n      />{' '}\n      {remote.error && (\n        <Tooltip content={_('remoteConnectionFailed')}>\n          <a className='text-danger btn btn-link' onClick={() => _showError(remote)} style={{ padding: '0px' }}>\n            <Icon icon='alarm' size='lg' />\n          </a>\n        </Tooltip>\n      )}\n    </div>\n  ),\n  name: _('remoteState'),\n}\nconst COLUMN_DISK = {\n  itemRenderer: remote =>\n    remote.info !== undefined &&\n    remote.info.used !== undefined &&\n    remote.info.size !== undefined && (\n      <span>{`${formatSize(remote.info.used)} / ${formatSize(remote.info.size)}`}</span>\n    ),\n  name: _('remoteDisk'),\n}\nconst COLUMN_SPEED = {\n  itemRenderer: remote => {\n    const benchmark = get(() => remote.benchmarks[remote.benchmarks.length - 1])\n\n    return (\n      benchmark !== undefined &&\n      benchmark.readRate !== undefined &&\n      benchmark.writeRate !== undefined && (\n        <span>{`${formatSpeed(benchmark.writeRate, 1e3)} / ${formatSpeed(benchmark.readRate, 1e3)}`}</span>\n      )\n    )\n  },\n  name: (\n    <span>\n      {_('remoteSpeed')}{' '}\n      <Tooltip content={_('remoteSpeedInfo')}>\n        <Icon icon='info' size='lg' />\n      </Tooltip>\n    </span>\n  ),\n}\n\nconst COLUMN_PROXY = {\n  itemRenderer: remote => (\n    <XoSelect onChange={proxy => editRemote(remote, { proxy })} value={remote.proxy} xoType='proxy'>\n      {remote.proxy !== undefined ? (\n        <div>\n          <Proxy id={remote.proxy} />{' '}\n          <a role='button' onClick={() => editRemote(remote, { proxy: null })}>\n            <Icon icon='remove' />\n          </a>\n        </div>\n      ) : (\n        _('noValue')\n      )}\n    </XoSelect>\n  ),\n  name: _('proxy'),\n}\n\nconst COLUMN_ENCRYPTION = {\n  itemRenderer: remote => {\n    // remote.info?.encryption undefined means that remote is not enabled and synced\n    // we don't have the algorithm used at this step\n    if (remote.info?.encryption === undefined) {\n      return remote.encryptionKey !== undefined ? <Icon size='lg' icon='lock' /> : null\n    } else {\n      // remote enabled and not encrypted\n      if (remote.info.encryption.algorithm === 'none') {\n        return null\n      }\n      const { algorithm, isLegacy, recommendedAlgorithm } = remote.info.encryption\n      return (\n        <span>\n          <Tooltip content={algorithm}>\n            <Icon className='mr-1' icon='lock' size='lg' />\n          </Tooltip>\n\n          {isLegacy && (\n            <Tooltip content={_('remoteEncryptionLegacy', { algorithm, recommendedAlgorithm })}>\n              <Icon icon='error' size='lg' />\n            </Tooltip>\n          )}\n        </span>\n      )\n    }\n  },\n  name: _('encryption'),\n}\n\nconst fixRemoteUrl = remote => editRemote(remote, { url: format(remote) })\nconst COLUMNS_LOCAL_REMOTE = [\n  COLUMN_NAME,\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <Text\n        data-element='path'\n        data-remote={remote}\n        onChange={_changeUrlElement}\n        placeholder={formatMessage(messages.remoteLocalPlaceHolderPath)}\n        value={remote.path}\n      />\n    ),\n    name: _('remotePath'),\n  },\n  COLUMN_STATE,\n  COLUMN_DISK,\n  COLUMN_ENCRYPTION,\n  COLUMN_SPEED,\n  COLUMN_PROXY,\n]\nconst COLUMNS_NFS_REMOTE = [\n  COLUMN_NAME,\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <span>\n        <strong className='text-info'>\\\\</strong>\n        <Text\n          data-element='host'\n          data-remote={remote}\n          onChange={_changeUrlElement}\n          placeholder={formatMessage(messages.remoteNfsPlaceHolderHost)}\n          value={remote.host}\n        />\n        :\n        <Number\n          data-element='port'\n          data-remote={remote}\n          nullable\n          onChange={_changeUrlElement}\n          placeholder={formatMessage(messages.remoteNfsPlaceHolderPort)}\n          value={remote.port || ''}\n        />\n        :\n        <Text\n          data-element='path'\n          data-remote={remote}\n          onChange={_changeUrlElement}\n          placeholder={formatMessage(messages.remoteNfsPlaceHolderPath)}\n          value={remote.path}\n        />{' '}\n        {remote.invalidUrl && (\n          <ActionButton\n            btnStyle='danger'\n            handler={fixRemoteUrl}\n            handlerParam={remote}\n            icon='alarm'\n            size='small'\n            tooltip={_('remoteErrorMessage', {\n              url: remote.url,\n              newUrl: format(remote),\n            })}\n          />\n        )}\n      </span>\n    ),\n\n    name: _('remoteDevice'),\n  },\n  {\n    name: _('remoteOptions'),\n    itemRenderer: remote => <Text data-remote={remote} onChange={_editRemoteOptions} value={remote.options || ''} />,\n  },\n  COLUMN_STATE,\n  COLUMN_DISK,\n  COLUMN_ENCRYPTION,\n  COLUMN_SPEED,\n  COLUMN_PROXY,\n]\nconst COLUMNS_SMB_REMOTE = [\n  COLUMN_NAME,\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <span>\n        <strong className='text-info'>\\\\</strong>\n        <Text data-element='host' data-remote={remote} onChange={_changeUrlElement} value={remote.host} />\n        <strong className='text-info'>\\</strong>\n        <span>\n          <Text\n            data-element='path'\n            data-remote={remote}\n            onChange={_changeUrlElement}\n            placeholder={formatMessage(messages.remoteSmbPlaceHolderRemotePath)}\n            value={remote.path}\n          />\n        </span>\n      </span>\n    ),\n    name: _('remoteShare'),\n  },\n  {\n    name: _('remoteOptions'),\n    itemRenderer: remote => <Text data-remote={remote} onChange={_editRemoteOptions} value={remote.options || ''} />,\n  },\n  COLUMN_STATE,\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <span>\n        <Text data-element='username' data-remote={remote} onChange={_changeUrlElement} value={remote.username} />\n        :\n        <Password\n          data-element='password'\n          data-remote={remote}\n          onChange={_changeUrlElement}\n          placeholder={formatMessage(messages.remotePlaceHolderPassword)}\n          value=''\n        />\n        @\n        <Text data-element='domain' data-remote={remote} onChange={_changeUrlElement} value={remote.domain} />\n      </span>\n    ),\n    name: _('remoteAuth'),\n  },\n  COLUMN_ENCRYPTION,\n  COLUMN_SPEED,\n  COLUMN_PROXY,\n]\n\nconst COLUMNS_S3_REMOTE = [\n  COLUMN_NAME,\n  {\n    itemRenderer: remote => remote.protocol === 'https' && <Icon icon='success' />,\n    name: <span>{_('remoteS3LabelUseHttps')} </span>,\n  },\n  {\n    itemRenderer: remote => remote.allowUnauthorized && <Icon icon='success' />,\n    name: <span>{_('remoteS3LabelAllowInsecure')} </span>,\n  },\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <Text\n        data-element='host'\n        data-remote={remote}\n        onChange={_changeUrlElement}\n        placeholder='AWS endpoint'\n        value={remote.host}\n      />\n    ),\n    name: 'AWS S3 Endpoint',\n  },\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <Text\n        data-element='path'\n        data-remote={remote}\n        onChange={_changeUrlElement}\n        placeholder='bucket placeholder'\n        value={remote.path}\n      />\n    ),\n    name: 'Bucket',\n  },\n  COLUMN_STATE,\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <span>\n        <Text data-element='username' data-remote={remote} onChange={_changeUrlElement} value={remote.username} />\n        :\n        <Password\n          data-element='password'\n          data-remote={remote}\n          onChange={_changeUrlElement}\n          placeholder='Click to change Secret Key'\n          value=''\n        />\n      </span>\n    ),\n    name: 'Key',\n  },\n  COLUMN_ENCRYPTION,\n  COLUMN_SPEED,\n  COLUMN_PROXY,\n]\n\nconst COLUMNS_AZURE_REMOTE = [\n  COLUMN_NAME,\n  {\n    itemRenderer: remote => remote.protocol === 'https' && <Icon icon='success' />,\n    name: <span>{_('remoteAzureLabelUseHttps')} </span>,\n  },\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <Text\n        data-element='host'\n        data-remote={remote}\n        onChange={_changeUrlElement}\n        placeholder='Azure host'\n        value={remote.host}\n      />\n    ),\n    name: 'Azure Endpoint',\n  },\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <Text\n        data-element='path'\n        data-remote={remote}\n        onChange={_changeUrlElement}\n        placeholder='Azure placeholder'\n        value={remote.path}\n      />\n    ),\n    name: 'Path',\n  },\n  COLUMN_STATE,\n  {\n    itemRenderer: (remote, { formatMessage }) => (\n      <span>\n        <Text data-element='username' data-remote={remote} onChange={_changeUrlElement} value={remote.username} />\n        :\n        <Password\n          data-element='password'\n          data-remote={remote}\n          onChange={_changeUrlElement}\n          placeholder='Click to change Secret Key'\n          value=''\n        />\n      </span>\n    ),\n    name: 'Account',\n  },\n  COLUMN_ENCRYPTION,\n  COLUMN_SPEED,\n  COLUMN_PROXY,\n]\n\nconst GROUPED_ACTIONS = [\n  {\n    handler: deleteRemotes,\n    icon: 'delete',\n    label: _('remoteDeleteSelected'),\n    level: 'danger',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    disabled: remote => !remote.enabled,\n    handler: remote =>\n      testRemote(remote).then(\n        answer =>\n          answer.success\n            ? alert(\n                <span>\n                  <Icon icon='success' /> {_('remoteTestSuccess', { name: remote.name })}\n                </span>,\n                _('remoteTestSuccessMessage')\n              )\n            : alert(\n                <span>\n                  <Icon icon='error' /> {_('remoteTestFailure', { name: remote.name })}\n                </span>,\n                <p>\n                  <dl className='dl-horizontal'>\n                    <dt>{_('remoteTestError')}</dt>\n                    <dd>\n                      <pre>{formatError(answer.error)}</pre>\n                    </dd>\n                    <dt>{_('remoteTestStep')}</dt>\n                    <dd>{answer.step}</dd>\n                  </dl>\n                </p>\n              ),\n        noop\n      ),\n    icon: 'diagnosis',\n    label: _('remoteTestTip'),\n    level: 'primary',\n  },\n  {\n    handler: (remote, { reset, editRemote }) => {\n      reset()\n      editRemote(remote)\n    },\n    icon: 'edit',\n    label: _('formEdit'),\n    level: 'primary',\n  },\n  {\n    handler: deleteRemote,\n    icon: 'delete',\n    label: _('remoteDeleteTip'),\n    level: 'danger',\n  },\n]\nconst FILTERS = {\n  filterRemotesOnlyConnected: 'enabled?',\n  filterRemotesOnlyDisconnected: '!enabled?',\n}\n\nexport default decorate([\n  addSubscriptions({\n    remotes: subscribeRemotes,\n    remotesInfo: subscribeRemotesInfo,\n  }),\n  injectIntl,\n  provideState({\n    initialState: () => ({\n      formKey: generateRandomId(),\n      remote: undefined,\n    }),\n    effects: {\n      reset: () => () => ({\n        formKey: generateRandomId(),\n        remote: undefined,\n      }),\n      editRemote: (_, remote) => () => ({\n        remote,\n      }),\n    },\n    computed: {\n      remoteWithInfo: (_, { remotes, remotesInfo }) =>\n        groupBy(\n          map(remotes, remote => ({\n            ...parse(remote.url),\n            ...remote,\n            info: remotesInfo !== undefined ? remotesInfo[remote.id] : {},\n          })),\n          'type'\n        ),\n    },\n  }),\n  injectState,\n  ({ state, effects, remotes = {}, intl: { formatMessage } }) => (\n    <div>\n      {!isEmpty(state.remoteWithInfo.file) && (\n        <div>\n          <h2>{_('remoteTypeLocal')}</h2>\n          <SortedTable\n            collection={state.remoteWithInfo.file}\n            columns={COLUMNS_LOCAL_REMOTE}\n            data-editRemote={effects.editRemote}\n            data-formatMessage={formatMessage}\n            data-reset={effects.reset}\n            filters={FILTERS}\n            groupedActions={GROUPED_ACTIONS}\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='l'\n          />\n        </div>\n      )}\n\n      {!isEmpty(state.remoteWithInfo.nfs) && (\n        <div>\n          <h2>{_('remoteTypeNfs')}</h2>\n          <SortedTable\n            collection={state.remoteWithInfo.nfs}\n            columns={COLUMNS_NFS_REMOTE}\n            data-editRemote={effects.editRemote}\n            data-formatMessage={formatMessage}\n            data-reset={effects.reset}\n            filters={FILTERS}\n            groupedActions={GROUPED_ACTIONS}\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='nfs'\n          />\n        </div>\n      )}\n\n      {!isEmpty(state.remoteWithInfo.smb) && (\n        <div>\n          <h2>{_('remoteTypeSmb')}</h2>\n          <SortedTable\n            collection={state.remoteWithInfo.smb}\n            columns={COLUMNS_SMB_REMOTE}\n            data-editRemote={effects.editRemote}\n            data-formatMessage={formatMessage}\n            data-reset={effects.reset}\n            filters={FILTERS}\n            groupedActions={GROUPED_ACTIONS}\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='smb'\n          />\n        </div>\n      )}\n\n      {!isEmpty(state.remoteWithInfo.s3) && (\n        <div>\n          <h2>{_('remoteTypeS3')}</h2>\n          <SortedTable\n            collection={state.remoteWithInfo.s3}\n            columns={COLUMNS_S3_REMOTE}\n            data-editRemote={effects.editRemote}\n            data-formatMessage={formatMessage}\n            data-reset={effects.reset}\n            filters={FILTERS}\n            groupedActions={GROUPED_ACTIONS}\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='s3'\n          />\n        </div>\n      )}\n      {!isEmpty(state.remoteWithInfo.azure) && (\n        <div>\n          <h2>{_('remoteTypeAzure')}</h2>\n          <SortedTable\n            collection={state.remoteWithInfo.azure}\n            columns={COLUMNS_AZURE_REMOTE}\n            data-editRemote={effects.editRemote}\n            data-formatMessage={formatMessage}\n            data-reset={effects.reset}\n            filters={FILTERS}\n            groupedActions={GROUPED_ACTIONS}\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='azure'\n          />\n        </div>\n      )}\n      {!isEmpty(state.remoteWithInfo.azurite) && (\n        <div>\n          <h2>{_('remoteTypeAzurite')}</h2>\n          <SortedTable\n            collection={state.remoteWithInfo.azurite}\n            columns={COLUMNS_AZURE_REMOTE}\n            data-editRemote={effects.editRemote}\n            data-formatMessage={formatMessage}\n            data-reset={effects.reset}\n            filters={FILTERS}\n            groupedActions={GROUPED_ACTIONS}\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='azurite'\n          />\n        </div>\n      )}\n      <Remote formatMessage={formatMessage} key={state.formKey} />\n    </div>\n  ),\n])\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { addSubscriptions, resolveId } from 'utils'\nimport { alert, confirm } from 'modal'\nimport { createRemote, editRemote, subscribeRemotes } from 'xo'\nimport { error } from 'notification'\nimport { format } from 'xo-remote-parser'\nimport { generateId, linkState } from 'reaclette-utils'\nimport { injectState, provideState } from 'reaclette'\nimport { map, some, trimStart } from 'lodash'\nimport { Password, Number, Toggle } from 'form'\nimport { SelectProxy } from 'select-objects'\n\nconst remoteTypes = {\n  file: 'remoteTypeLocal',\n  nfs: 'remoteTypeNfs',\n  smb: 'remoteTypeSmb',\n  s3: 'remoteTypeS3',\n  azure: 'remoteTypeAzure',\n  azurite: 'remoteTypeAzurite',\n}\n\nexport default decorate([\n  addSubscriptions({\n    remotes: subscribeRemotes,\n  }),\n  provideState({\n    initialState: () => ({\n      domain: undefined,\n      host: undefined,\n      name: undefined,\n      options: undefined,\n      password: undefined,\n      path: undefined,\n      port: undefined,\n      proxyId: undefined,\n      type: undefined,\n      username: undefined,\n      directory: undefined,\n      bucket: undefined,\n      container: undefined,\n      pathInContainer: undefined,\n      protocol: undefined,\n      region: undefined,\n      allowUnauthorized: undefined,\n      useVhdDirectory: undefined,\n      encryptionKey: undefined,\n    }),\n    effects: {\n      linkState,\n      setPort: (_, port) => state => ({\n        port: port === undefined && state.remote !== undefined ? '' : port,\n      }),\n      setProxy(_, proxy) {\n        this.state.proxyId = resolveId(proxy)\n      },\n      editRemote:\n        ({ reset }) =>\n        state => {\n          const {\n            remote,\n            domain = remote.domain || '',\n            host = remote.host,\n            name,\n            options = remote.options || '',\n            password = remote.password, // FIXME: remote.password is obfuscated, don't use it!\n            port = remote.port,\n            proxyId = remote.proxy,\n            type = remote.type,\n            username = remote.username,\n            protocol = remote.protocol || 'https',\n            region = remote.region,\n            encryptionKey = remote.encryptionKey,\n          } = state\n\n          let {\n            path = remote.path,\n            useVhdDirectory = remote.useVhdDirectory,\n            allowUnauthorized = remote.allowUnauthorized,\n          } = state\n\n          // making it undefined if falsish won't save it in the remote url\n          allowUnauthorized = allowUnauthorized ? true : undefined\n          useVhdDirectory = useVhdDirectory ? true : undefined\n          if (type === 's3') {\n            const { parsedPath, bucket = parsedPath.split('/')[0], directory = parsedPath.split('/')[1] } = state\n            path = bucket + '/' + directory\n            useVhdDirectory = true // always on for s3\n          }\n          if (type === 'azure' || type === 'azurite') {\n            const {\n              parsedPath,\n              container = parsedPath.split('/')[0],\n              pathInContainer = parsedPath.indexOf('/') === -1 ? '/' : parsedPath.slice(parsedPath.indexOf('/')),\n            } = state\n            path = container + '/' + pathInContainer\n            useVhdDirectory = true // always on for Azure\n          }\n\n          return editRemote(remote, {\n            name,\n            url: format({\n              domain,\n              host,\n              password,\n              path,\n              port: port || undefined,\n              type,\n              username,\n              protocol,\n              region,\n              allowUnauthorized,\n              useVhdDirectory,\n              encryptionKey: encryptionKey?.trim() !== '' ? encryptionKey : undefined,\n            }),\n            options: options !== '' ? options : null,\n            proxy: proxyId,\n          }).then(reset)\n        },\n      createRemote:\n        ({ reset }) =>\n        async (state, { remotes }) => {\n          if (some(remotes, { name: state.name })) {\n            return alert(\n              <span>\n                <Icon icon='error' /> {_('remoteTestName')}\n              </span>,\n              <p>{_('remoteTestNameFailure')}</p>\n            )\n          }\n\n          const {\n            domain = 'WORKGROUP',\n            host,\n            name,\n            options,\n            password,\n            path,\n            port,\n            proxyId,\n            type = 'nfs',\n            username,\n            useVhdDirectory = undefined,\n            encryptionKey = '',\n          } = state\n\n          const urlParams = {\n            host,\n            path,\n            port,\n            type,\n            useVhdDirectory,\n            encryptionKey: encryptionKey.trim() !== '' ? encryptionKey : undefined,\n          }\n          if (type === 's3') {\n            const { allowUnauthorized, bucket, directory, protocol = 'https', region } = state\n            urlParams.path = bucket + '/' + directory\n            urlParams.allowUnauthorized = allowUnauthorized\n            urlParams.useVhdDirectory = true // always on for s3\n            urlParams.region = region\n            urlParams.protocol = protocol\n          }\n          if (type === 'azure' || type === 'azurite') {\n            const { allowUnauthorized, protocol = 'https', container, pathInContainer } = state\n            urlParams.path = container + '/' + pathInContainer\n            urlParams.allowUnauthorized = allowUnauthorized\n            urlParams.protocol = protocol\n            urlParams.useVhdDirectory = true // always on for Azure\n          }\n          username && (urlParams.username = username)\n          password && (urlParams.password = password)\n          domain && (urlParams.domain = domain)\n\n          if (type === 'file') {\n            await confirm({\n              title: _('localRemoteWarningTitle'),\n              body: _('localRemoteWarningMessage'),\n            })\n          }\n\n          const url = format(urlParams)\n          return createRemote(name, url, options !== '' ? options : undefined, proxyId === null ? undefined : proxyId)\n            .then(reset)\n            .catch(err => error('Create Remote', err.message || String(err)))\n        },\n      setSecretKey(_, { target: { value } }) {\n        this.state.password = value\n      },\n      setInsecure(_, value) {\n        this.state.protocol = value ? 'https' : 'http'\n      },\n      setAllowUnauthorized(_, value) {\n        this.state.allowUnauthorized = value\n      },\n      setUseVhdDirectory(_, value) {\n        this.state.useVhdDirectory = value\n      },\n    },\n    computed: {\n      formId: generateId,\n      inputTypeId: generateId,\n      parsedPath: ({ remote }) => remote && trimStart(remote.path, '/'),\n    },\n  }),\n  injectState,\n  ({ state, effects, formatMessage }) => {\n    const {\n      remote = {},\n      domain = remote.domain || 'WORKGROUP',\n      host = remote.host || '',\n      name = remote.name || '',\n      options = remote.options || '',\n      password = remote.password || '',\n      protocol = remote.protocol || 'https',\n      region = remote.region || '',\n      parsedPath,\n      path = parsedPath || '',\n      parsedBucket = parsedPath != null && parsedPath.split('/')[0],\n      container = parsedPath != null ? parsedPath.split('/')[0] : '',\n      pathInContainer = parsedPath != null\n        ? parsedPath.indexOf('/') === -1\n          ? '/'\n          : parsedPath.slice(parsedPath.indexOf('/'))\n        : '',\n      bucket = parsedBucket || '',\n      parsedDirectory = parsedPath != null && parsedPath.split('/')[1],\n      directory = parsedDirectory || '',\n      port = remote.port,\n      proxyId = remote.proxy,\n      type = remote.type || 'nfs',\n      username = remote.username || '',\n      allowUnauthorized = remote.allowUnauthorized || false,\n      useVhdDirectory = remote.useVhdDirectory || type === 's3',\n      encryptionKey = remote.encryptionKey || '',\n    } = state\n\n    const isEncrypted = encryptionKey.trim() !== ''\n\n    return (\n      <div>\n        <h2>{_('newRemote')}</h2>\n        <form id={state.formId}>\n          <div className='form-group'>\n            <label htmlFor={state.inputTypeId}>{_('remoteType')}</label>\n            <select\n              className='form-control'\n              id={state.inputTypeId}\n              name='type'\n              onChange={effects.linkState}\n              required\n              value={type}\n            >\n              {map(remoteTypes, (label, key) => _({ key }, label, message => <option value={key}>{message}</option>))}\n            </select>\n            {type === 'smb' && <em className='text-warning'>{_('remoteSmbWarningMessage')}</em>}\n          </div>\n          <div className='form-group'>\n            <input\n              className='form-control'\n              name='name'\n              onChange={effects.linkState}\n              placeholder={formatMessage(messages.remoteMyNamePlaceHolder)}\n              required\n              type='text'\n              value={name}\n            />\n          </div>\n          <div className='form-group'>\n            <SelectProxy onChange={effects.setProxy} value={proxyId} />\n          </div>\n          {type === 'file' && (\n            <fieldset className='form-group'>\n              <div className='input-group'>\n                <span className='input-group-addon'>/</span>\n                <input\n                  className='form-control'\n                  name='path'\n                  onChange={effects.linkState}\n                  pattern='^(([^/]+)+(/[^/]+)*)?$'\n                  placeholder={formatMessage(messages.remoteLocalPlaceHolderPath)}\n                  required\n                  type='text'\n                  value={path}\n                />\n              </div>\n            </fieldset>\n          )}\n          {type === 'nfs' && (\n            <fieldset>\n              <div className='form-group'>\n                <input\n                  className='form-control'\n                  name='host'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.remoteNfsPlaceHolderHost)}\n                  required\n                  type='text'\n                  value={host}\n                />\n                <br />\n                <Number\n                  onChange={effects.setPort}\n                  placeholder={formatMessage(messages.remoteNfsPlaceHolderPort)}\n                  value={port}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <span className='input-group-addon'>/</span>\n                <input\n                  className='form-control'\n                  name='path'\n                  onChange={effects.linkState}\n                  pattern='^(([^/]+)+(/[^/]+)*)?$'\n                  placeholder={formatMessage(messages.remoteNfsPlaceHolderPath)}\n                  required\n                  type='text'\n                  value={path}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <span className='input-group-addon'>-o</span>\n                <input\n                  className='form-control'\n                  name='options'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.remoteNfsPlaceHolderOptions)}\n                  type='text'\n                  value={options}\n                />\n              </div>\n            </fieldset>\n          )}\n          {type === 'smb' && (\n            <fieldset>\n              <div className='input-group form-group'>\n                <span className='input-group-addon'>\\\\</span>\n                <input\n                  className='form-control'\n                  name='host'\n                  onChange={effects.linkState}\n                  pattern='^[^\\\\/]+\\\\[^\\\\/]+$'\n                  placeholder={formatMessage(messages.remoteSmbPlaceHolderAddressShare)}\n                  required\n                  type='text'\n                  value={host}\n                />\n                <span className='input-group-addon'>\\</span>\n                <input\n                  className='form-control'\n                  name='path'\n                  onChange={effects.linkState}\n                  pattern='^([^\\\\/]+(\\\\[^\\\\/]+)*)?$'\n                  placeholder={formatMessage(messages.remoteSmbPlaceHolderRemotePath)}\n                  type='text'\n                  value={path}\n                />\n              </div>\n              <div className='form-group'>\n                <input\n                  className='form-control'\n                  name='username'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.remoteSmbPlaceHolderUsername)}\n                  required\n                  type='text'\n                  value={username}\n                />\n              </div>\n              <div className='form-group'>\n                <Password\n                  name='password'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.remoteSmbPlaceHolderPassword)}\n                  required\n                  value={password}\n                />\n              </div>\n              <div className='form-group'>\n                <input\n                  className='form-control'\n                  onChange={effects.linkState}\n                  name='domain'\n                  placeholder={formatMessage(messages.remoteSmbPlaceHolderDomain)}\n                  required\n                  type='text'\n                  value={domain}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <span className='input-group-addon'>-o</span>\n                <input\n                  className='form-control'\n                  name='options'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.remoteSmbPlaceHolderOptions)}\n                  type='text'\n                  value={options}\n                />\n              </div>\n            </fieldset>\n          )}\n          {type === 's3' && (\n            <fieldset className='form-group form-group'>\n              <div className='input-group form-group'>\n                <span className='align-middle'>\n                  {_('remoteS3LabelUseHttps')}{' '}\n                  <Tooltip content={_('remoteS3TooltipProtocol')}>\n                    <Icon icon='info' size='lg' />\n                  </Tooltip>\n                </span>\n                <Toggle\n                  className='align-middle pull-right'\n                  onChange={effects.setInsecure}\n                  value={protocol === 'https'}\n                />\n              </div>\n\n              <div className='input-group form-group'>\n                <span className='align-middle '>\n                  {_('remoteS3LabelAllowInsecure')}{' '}\n                  <Tooltip content={_('remoteS3TooltipAcceptInsecure')}>\n                    <Icon icon='info' size='lg' />\n                  </Tooltip>\n                </span>\n                <Toggle\n                  className='align-middle pull-right'\n                  disabled={protocol !== 'https'}\n                  onChange={effects.setAllowUnauthorized}\n                  value={allowUnauthorized}\n                />\n              </div>\n\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='host'\n                  onChange={effects.linkState}\n                  // pattern='^[^\\\\/]+\\\\[^\\\\/]+$'\n                  placeholder={formatMessage(messages.remoteS3PlaceHolderEndpoint)}\n                  required\n                  type='text'\n                  value={host}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='region'\n                  onChange={effects.linkState}\n                  pattern='[a-z0-9-]+'\n                  placeholder={formatMessage(messages.remoteS3Region)}\n                  type='text'\n                  value={region}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='bucket'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.remoteS3PlaceHolderBucket)}\n                  required\n                  type='text'\n                  value={bucket}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='directory'\n                  onChange={effects.linkState}\n                  pattern='^(([^/]+)+(/[^/]+)*)?$'\n                  placeholder={formatMessage(messages.remoteS3PlaceHolderDirectory)}\n                  required\n                  type='text'\n                  value={directory}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='username'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.remoteS3PlaceHolderAccessKeyID)}\n                  required\n                  type='text'\n                  value={username}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='password'\n                  onChange={effects.setSecretKey}\n                  placeholder={formatMessage(messages.remoteS3PlaceHolderSecret)}\n                  autoComplete='off'\n                  type='text'\n                />\n              </div>\n            </fieldset>\n          )}\n          {(type === 'azure' || type === 'azurite') && (\n            <fieldset className='form-group form-group'>\n              {type === 'azurite' && (\n                <div className='input-group form-group'>\n                  <span className='align-middle'>\n                    {_('remoteAzureLabelUseHttps')}{' '}\n                    <Tooltip content={_('remoteAzureTooltipProtocol')}>\n                      <Icon icon='info' size='lg' />\n                    </Tooltip>\n                  </span>\n                  <Toggle\n                    className='align-middle pull-right'\n                    onChange={effects.setInsecure}\n                    value={protocol === 'https'}\n                  />\n                </div>\n              )}\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='host'\n                  onChange={effects.linkState}\n                  placeholder={`Host (e.g. ${type === 'azure' ? '<username>.blob.core.windows.net' : '127.0.0.1:10000'})`}\n                  required\n                  type='text'\n                  value={host}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='username'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.remoteAccountName)}\n                  pattern='^[^A-Z]+$'\n                  required\n                  type='text'\n                  value={username}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <Password\n                  name='password'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.key)}\n                  required\n                  value={password}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='container'\n                  onChange={effects.linkState}\n                  placeholder={formatMessage(messages.remoteContainer)}\n                  pattern='^(?!.*--)(?!-)[a-z0-9]+(-[a-z0-9]+)*$'\n                  minLength={3}\n                  maxLength={63}\n                  required\n                  type='text'\n                  value={container}\n                />\n              </div>\n              <div className='input-group form-group'>\n                <input\n                  className='form-control'\n                  name='pathInContainer'\n                  onChange={effects.linkState}\n                  placeholder='/path/to/backup'\n                  required\n                  type='text'\n                  value={pathInContainer}\n                />\n              </div>\n            </fieldset>\n          )}\n          <div className='form-group'>\n            <label>{_('remoteEncryptionKey')}</label>\n            {isEncrypted && !useVhdDirectory && (\n              <p className='text-warning'>\n                <Icon icon='alarm' /> {_('remoteEncryptionMustUseVhd')}\n              </p>\n            )}\n            <ul className='small'>\n              <li>{_('remoteEncryptionEncryptedfiles')}</li>\n              <li>{_('remoteEncryptionKeyStorageLocation')}</li>\n              <li>{_('remoteEncryptionBackupSize')}</li>\n            </ul>\n            <input\n              autoComplete='new-password'\n              className='form-control'\n              name='encryptionKey'\n              placeholder={formatMessage(messages.remoteS3PlaceHolderEncryptionKey)}\n              onChange={effects.linkState}\n              pattern='^.{32}$'\n              type='password'\n              value={encryptionKey}\n            />\n          </div>\n          {type !== 's3' && type !== 'azure' && type !== 'azurite' && (\n            <fieldset className='form-group form-group'>\n              <div className='input-group form-group'>\n                <span className='align-middle'>\n                  {_('remoteUseVhdDirectory')}{' '}\n                  <Tooltip content={_('remoteUseVhdDirectoryTooltip')}>\n                    <Icon icon='info' size='lg' />\n                  </Tooltip>\n                </span>\n                <Toggle\n                  className='align-middle pull-right'\n                  onChange={effects.setUseVhdDirectory}\n                  value={useVhdDirectory === true}\n                />\n              </div>\n            </fieldset>\n          )}\n          <div className='form-group'>\n            <ActionButton\n              btnStyle='primary'\n              form={state.formId}\n              handler={state.remote === undefined ? effects.createRemote : effects.editRemote}\n              icon='save'\n              type='submit'\n            >\n              {_('savePluginConfiguration')}\n            </ActionButton>\n            <ActionButton className='pull-right' handler={effects.reset} icon='reset' type='reset'>\n              {_('formReset')}\n            </ActionButton>\n          </div>\n        </form>\n      </div>\n    )\n  },\n])\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport Tooltip from 'tooltip'\nimport { addSubscriptions } from 'utils'\nimport { alert, confirm } from 'modal'\nimport { Container } from 'grid'\nimport { Password as EditablePassword, Text } from 'editable'\nimport { Password, Toggle } from 'form'\nimport { Pool } from 'render-xo-item'\nimport { injectIntl } from 'react-intl'\nimport noop from 'lodash/noop.js'\nimport { addServer, disableServer, editServer, enableServer, removeServer, subscribeServers } from 'xo'\n\nconst showInfo = () => alert(_('serverAllowUnauthorizedCertificates'), _('serverUnauthorizedCertificatesInfo'))\nconst showServerError = server => {\n  const { code, message } = server.error\n\n  if (code === 'DEPTH_ZERO_SELF_SIGNED_CERT') {\n    return confirm({\n      title: _('serverSelfSignedCertError'),\n      body: _('serverSelfSignedCertQuestion'),\n    }).then(() => editServer(server, { allowUnauthorized: true }).then(() => enableServer(server)), noop)\n  }\n\n  if (code === 'SESSION_AUTHENTICATION_FAILED') {\n    return alert(_('serverAuthFailed'), message)\n  }\n\n  return alert(code || _('serverUnknownError'), message)\n}\n\nconst COLUMNS = [\n  {\n    itemRenderer: (server, formatMessage) => (\n      <Text\n        value={server.label || ''}\n        onChange={label => editServer(server, { label: label || null })}\n        placeholder={formatMessage(messages.serverPlaceHolderLabel)}\n      />\n    ),\n    default: true,\n    name: _('serverLabel'),\n    sortCriteria: _ => _.label,\n  },\n  {\n    itemRenderer: (server, formatMessage) => (\n      <Text\n        value={server.host}\n        onChange={host => editServer(server, { host })}\n        placeholder={formatMessage(messages.serverPlaceHolderAddress)}\n      />\n    ),\n    name: _('serverHost'),\n    sortCriteria: _ => _.host,\n  },\n  {\n    itemRenderer: (server, formatMessage) => (\n      <Text\n        value={server.username}\n        onChange={username => editServer(server, { username })}\n        placeholder={formatMessage(messages.serverPlaceHolderUser)}\n      />\n    ),\n    name: _('serverUsername'),\n    sortCriteria: _ => _.username,\n  },\n  {\n    itemRenderer: (server, formatMessage) => (\n      <EditablePassword\n        value=''\n        onChange={password => editServer(server, { password })}\n        placeholder={formatMessage(messages.serverPlaceHolderPassword)}\n      />\n    ),\n    name: _('serverPassword'),\n  },\n  {\n    itemRenderer: server => (\n      <div>\n        <StateButton\n          disabledLabel={_('serverDisabled')}\n          disabledHandler={enableServer}\n          disabledTooltip={_('serverEnable')}\n          enabledLabel={_('serverEnabled')}\n          enabledHandler={disableServer}\n          enabledTooltip={_('serverDisable')}\n          handlerParam={server}\n          state={server.enabled}\n        />{' '}\n        {server.error != null && (\n          <Tooltip content={_('serverConnectionFailed')}>\n            <a className='text-danger btn btn-link btn-sm' onClick={() => showServerError(server)}>\n              <Icon icon='alarm' size='lg' />\n            </a>\n          </Tooltip>\n        )}\n      </div>\n    ),\n    name: _('serverStatus'),\n    sortCriteria: _ => _.status,\n  },\n  {\n    itemRenderer: server => <Toggle onChange={readOnly => editServer(server, { readOnly })} value={server.readOnly} />,\n    name: _('serverReadOnly'),\n    sortCriteria: _ => _.readOnly,\n  },\n  {\n    itemRenderer: server => (\n      <Toggle\n        value={Boolean(server.allowUnauthorized)}\n        onChange={allowUnauthorized => editServer(server, { allowUnauthorized })}\n      />\n    ),\n    name: (\n      <span>\n        {_('serverUnauthorizedCertificates')}{' '}\n        <Tooltip content={_('serverAllowUnauthorizedCertificates')}>\n          <a className='text-info' onClick={showInfo}>\n            <Icon icon='info' size='lg' />\n          </a>\n        </Tooltip>\n      </span>\n    ),\n    sortCriteria: _ => _.allowUnauthorized,\n  },\n  {\n    itemRenderer: ({ poolId }) => poolId !== undefined && <Pool id={poolId} link />,\n    name: _('pool'),\n  },\n  {\n    itemRenderer: (server, formatMessage) => (\n      <Text\n        value={server.httpProxy || ''}\n        // force a null value for falsish value to ensure the value is removed from object if set to ''\n        onChange={httpProxy => editServer(server, { httpProxy: httpProxy || null })}\n        placeholder={formatMessage(messages.serverHttpProxyPlaceHolder)}\n      />\n    ),\n    name: _('serverHttpProxy'),\n    sortCriteria: _ => _.httpProxy,\n  },\n]\nconst INDIVIDUAL_ACTIONS = [\n  {\n    handler: removeServer,\n    icon: 'delete',\n    label: _('remove'),\n    level: 'danger',\n  },\n]\n\n@addSubscriptions({\n  servers: subscribeServers,\n})\n@injectIntl\nexport default class Servers extends Component {\n  state = {\n    allowUnauthorized: false,\n  }\n\n  _addServer = async () => {\n    const { label, host, password, username, allowUnauthorized, httpProxy } = this.state\n    await addServer(host, username, password, label, allowUnauthorized, httpProxy)\n\n    this.setState({\n      allowUnauthorized: false,\n      host: '',\n      httpProxy: '',\n      label: '',\n      password: '',\n      username: '',\n    })\n  }\n\n  render() {\n    const {\n      props: {\n        intl: { formatMessage },\n        servers,\n      },\n      state,\n    } = this\n\n    return (\n      <Container>\n        <SortedTable\n          collection={servers}\n          columns={COLUMNS}\n          individualActions={INDIVIDUAL_ACTIONS}\n          userData={formatMessage}\n          stateUrlParam='s'\n        />\n        <form className='form-inline' id='form-add-server'>\n          <div className='form-group'>\n            <input\n              className='form-control'\n              onChange={this.linkState('label')}\n              placeholder={formatMessage(messages.serverPlaceHolderLabel)}\n              type='text'\n              value={state.label}\n            />\n          </div>{' '}\n          <div className='form-group'>\n            <input\n              className='form-control'\n              onChange={this.linkState('host')}\n              placeholder={formatMessage(messages.serverPlaceHolderAddress)}\n              required\n              type='text'\n              value={state.host}\n            />\n          </div>{' '}\n          <div className='form-group'>\n            <input\n              className='form-control'\n              onChange={this.linkState('username')}\n              placeholder={formatMessage(messages.serverPlaceHolderUser)}\n              required\n              type='text'\n              value={state.username}\n            />\n          </div>{' '}\n          <div className='form-group'>\n            <Password\n              disabled={!this.state.username}\n              onChange={this.linkState('password')}\n              placeholder={formatMessage(messages.serverPlaceHolderPassword)}\n              required\n              value={state.password}\n            />\n          </div>{' '}\n          <div className='form-group'>\n            <Tooltip content={_('serverAllowUnauthorizedCertificates')}>\n              <Toggle onChange={this.linkState('allowUnauthorized')} value={state.allowUnauthorized} />\n            </Tooltip>\n          </div>{' '}\n          <div className='form-group'>\n            <input\n              className='form-control'\n              onChange={this.linkState('httpProxy')}\n              placeholder={formatMessage(messages.serverHttpProxy)}\n              type='text'\n              value={state.httpProxy || ''}\n            />\n          </div>{' '}\n          <ActionButton btnStyle='primary' form='form-add-server' handler={this._addServer} icon='save'>\n            {_('serverConnect')}\n          </ActionButton>\n        </form>\n      </Container>\n    )\n  }\n}\n","import * as Editable from 'editable'\nimport _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport isEmpty from 'lodash/isEmpty'\nimport keyBy from 'lodash/keyBy'\nimport Link from 'link'\nimport map from 'lodash/map'\nimport React from 'react'\nimport renderXoItem from 'render-xo-item'\nimport SortedTable from 'sorted-table'\nimport Tooltip from 'tooltip'\nimport { addSubscriptions } from 'utils'\nimport { get } from '@xen-orchestra/defined'\nimport { injectIntl } from 'react-intl'\nimport { Password, Select } from 'form'\n\nimport {\n  createUser,\n  deleteUser,\n  deleteUsers,\n  editUser,\n  removeOtp,\n  removeUserAuthProvider,\n  subscribeGroups,\n  subscribeUsers,\n} from 'xo'\n\nconst permissions = {\n  none: {\n    label: _('userLabel'),\n    value: 'none',\n  },\n  admin: {\n    label: _('adminLabel'),\n    value: 'admin',\n  },\n}\n\nconst USER_COLUMNS = [\n  {\n    name: _('userNameColumn'),\n    itemRenderer: user =>\n      isEmpty(user.authProviders) ? (\n        <Editable.Text onChange={email => editUser(user, { email })} value={user.email} />\n      ) : (\n        user.email\n      ),\n    sortCriteria: user => user.email,\n  },\n  {\n    name: _('userGroupsColumn'),\n    itemRenderer: (user, { groups }) => {\n      const nGroups = user.groups.length\n      const nGroupsLabel = _('userCountGroups', { nGroups })\n      return nGroups !== 0 ? (\n        <Tooltip\n          content={\n            <div>\n              {user.groups.map(id => (\n                <div key={id}>{get(() => renderXoItem(groups[id]))}</div>\n              ))}\n            </div>\n          }\n        >\n          {nGroupsLabel}\n        </Tooltip>\n      ) : (\n        nGroupsLabel\n      )\n    },\n  },\n  {\n    name: _('userPermissionColumn'),\n    itemRenderer: user => (\n      <Select\n        clearable={false}\n        value={user.permission || permissions.none.value}\n        ref='permission'\n        onChange={permission => editUser(user, { permission: permission.value })}\n        options={map(permissions)}\n      />\n    ),\n    sortCriteria: user => user.permission,\n  },\n  {\n    name: _('userAuthColumn'),\n    itemRenderer: user => {\n      const { authProviders } = user\n      return isEmpty(authProviders) ? (\n        <Editable.Password onChange={password => editUser(user, { password })} value='' />\n      ) : (\n        <ul className='list-group'>\n          {Object.keys(authProviders)\n            .sort()\n            .map(id => {\n              const shortId = id.split(':')[0]\n              const plugin = 'auth-' + shortId\n              return (\n                <li key={id} className='list-group-item'>\n                  <Link to={`/settings/plugins/?s=${encodeURIComponent(`name=^${plugin}$`)}`}>{shortId}</Link>\n                  <ActionButton\n                    className='pull-right'\n                    btnStyle='warning'\n                    size='small'\n                    icon='remove'\n                    handler={removeUserAuthProvider}\n                    data-userId={user.id}\n                    data-authProviderId={id}\n                  />\n                </li>\n              )\n            })}\n        </ul>\n      )\n    },\n  },\n  {\n    name: 'OTP',\n    itemRenderer: user =>\n      user.preferences.otp !== undefined ? (\n        <Button btnStyle='danger' onClick={() => removeOtp(user)} size='small'>\n          <Icon icon='remove' /> {_('remove')}\n        </Button>\n      ) : (\n        _('notConfigured')\n      ),\n  },\n]\n\nconst USER_ACTIONS = [\n  {\n    handler: deleteUsers,\n    icon: 'delete',\n    individualHandler: deleteUser,\n    individualLabel: _('deleteUser'),\n    label: _('deleteSelectedUsers'),\n    level: 'danger',\n  },\n]\n\n@addSubscriptions({\n  groups: cb => subscribeGroups(groups => cb(keyBy(groups, 'id'))),\n  users: cb => subscribeUsers(users => cb(keyBy(users, 'id'))),\n})\n@injectIntl\nexport default class Users extends Component {\n  state = {\n    email: '',\n    password: '',\n    permission: permissions.none,\n  }\n\n  _create = () => {\n    const { email, password, permission } = this.state\n    return createUser(email, password, permission.value).then(() => {\n      this.setState({ email: '', password: '', permission: permissions.none })\n    })\n  }\n\n  render() {\n    const { groups, users, intl } = this.props\n    const { email, password, permission } = this.state\n\n    return (\n      <div>\n        <form id='newUserForm' className='form-inline'>\n          <div className='form-group'>\n            <input\n              className='form-control'\n              onChange={this.linkState('email')}\n              placeholder={intl.formatMessage(messages.userName)}\n              required\n              type='text'\n              value={email}\n            />\n          </div>{' '}\n          <div className='form-group'>\n            <Select\n              clearable={false}\n              onChange={this.linkState('permission')}\n              options={map(permissions)}\n              placeholder={intl.formatMessage(messages.selectPermission)}\n              required\n              value={permission}\n            />\n          </div>{' '}\n          <div className='form-group'>\n            <Password\n              disabled={!this.state.email}\n              enableGenerator\n              onChange={this.linkState('password')}\n              placeholder={intl.formatMessage(messages.userPassword)}\n              required\n              value={password}\n            />\n          </div>{' '}\n          <ActionButton form='newUserForm' icon='add' btnStyle='success' handler={this._create}>\n            {_('createUserButton')}\n          </ActionButton>\n        </form>\n        <hr />\n        {isEmpty(users) ? (\n          <p>\n            <em>{_('noUserFound')}</em>\n          </p>\n        ) : (\n          <SortedTable\n            actions={USER_ACTIONS}\n            collection={users}\n            columns={USER_COLUMNS}\n            data-groups={groups}\n            stateUrlParam='s'\n          />\n        )}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionBar, { Action } from 'action-bar'\nimport React from 'react'\nimport { forgetSr, rescanSr, reconnectAllHostsSr, disconnectAllHostsSr } from 'xo'\n\nconst SrActionBar = ({ sr }) => (\n  <ActionBar display='icon' handlerParam={sr}>\n    <Action handler={rescanSr} label={_('srRescan')} icon='refresh' />\n    <Action handler={reconnectAllHostsSr} label={_('srReconnectAll')} icon='sr-reconnect-all' />\n    <Action handler={disconnectAllHostsSr} label={_('srDisconnectAll')} icon='sr-disconnect-all' />\n    <Action handler={forgetSr} label={_('srForget')} icon='sr-forget' />\n  </ActionBar>\n)\nexport default SrActionBar\n","import _ from 'intl'\nimport Component from 'base-component'\nimport React from 'react'\nimport { SelectSr } from 'select-objects'\nimport { SizeInput } from 'form'\nimport { Container, Row, Col } from 'grid'\nimport { createSelector } from 'selectors'\nimport { map, min } from 'lodash'\n\nexport default class AddSubvolumeModalBody extends Component {\n  get value() {\n    return this.state\n  }\n\n  _getSrPredicate = createSelector(\n    () => this.props.sr.$pool,\n    poolId => sr => sr.SR_type === 'lvm' && sr.$pool === poolId\n  )\n\n  _selectSrs = srs => {\n    this.setState({\n      srs,\n      brickSize: min(map(srs, sr => sr.size - sr.physical_usage)),\n    })\n  }\n\n  render() {\n    return (\n      <Container>\n        <Row className='mb-1'>\n          <Col size={6}>{_('xosanSelectNSrs', { nSrs: this.props.subvolumeSize })}</Col>\n          <Col size={6}>\n            <SelectSr multi onChange={this._selectSrs} predicate={this._getSrPredicate()} value={this.state.srs} />\n          </Col>\n        </Row>\n        <Row className='mb-1'>\n          <Col size={6}>{_('xosanSize')}</Col>\n          <Col size={6}>\n            <SizeInput onChange={this.linkState('brickSize')} required value={this.state.brickSize} />\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Copiable from 'copiable'\nimport Icon from 'icon'\nimport Link from 'link'\nimport Page from '../page'\nimport PropTypes from 'prop-types'\nimport React, { cloneElement } from 'react'\nimport SrActionBar from './action-bar'\nimport { Container, Row, Col } from 'grid'\nimport { editSr } from 'xo'\nimport { NavLink, NavTabs } from 'nav'\nimport { Text } from 'editable'\nimport { map, pick } from 'lodash'\nimport { connectStore, routes } from 'utils'\nimport { createGetObject, createGetObjectMessages, createGetObjectsOfType, createSelector } from 'selectors'\n\nimport TabAdvanced from './tab-advanced'\nimport TabDisks from './tab-disks'\nimport TabGeneral from './tab-general'\nimport TabHosts from './tab-host'\nimport TabLogs from './tab-logs'\nimport TabStats from './tab-stats'\nimport TabXosan from './tab-xosan'\nimport TabXostor from './tab-xostor'\n\n// ===================================================================\n\n@routes('general', {\n  advanced: TabAdvanced,\n  disks: TabDisks,\n  general: TabGeneral,\n  hosts: TabHosts,\n  logs: TabLogs,\n  stats: TabStats,\n  xosan: TabXosan,\n  xostor: TabXostor,\n})\n@connectStore(() => {\n  const getSr = createGetObject()\n\n  const getContainer = createGetObject((state, props) => getSr(state, props).$container)\n\n  const getPbds = createGetObjectsOfType('PBD').pick(createSelector(getSr, sr => sr.$PBDs))\n\n  const getSrHosts = createGetObjectsOfType('host').pick(createSelector(getPbds, pbds => map(pbds, pbd => pbd.host)))\n\n  // -----------------------------------------------------------------\n\n  const getLogs = createGetObjectMessages(getSr)\n\n  // -----------------------------------------------------------------\n\n  const getVdiIds = (state, props) => getSr(state, props).VDIs\n\n  const getVdis = createGetObjectsOfType('VDI').pick(getVdiIds).sort()\n  const getVdiSnapshots = createGetObjectsOfType('VDI-snapshot').pick(getVdiIds).sort()\n  const getUnmanagedVdis = createGetObjectsOfType('VDI-unmanaged').pick(createSelector(getSr, sr => sr.VDIs))\n\n  // -----------------------------------------------------------------\n\n  return (state, props) => {\n    const sr = getSr(state, props)\n    if (!sr) {\n      return {}\n    }\n\n    return {\n      container: getContainer(state, props),\n      hosts: getSrHosts(state, props),\n      pbds: getPbds(state, props),\n      logs: getLogs(state, props),\n      vdis: getVdis(state, props),\n      unmanagedVdis: getUnmanagedVdis(state, props),\n      vdiSnapshots: getVdiSnapshots(state, props),\n      sr,\n    }\n  }\n})\nexport default class Sr extends Component {\n  static contextTypes = {\n    router: PropTypes.object,\n  }\n\n  componentWillReceiveProps(props) {\n    if (this.props.sr && !props.sr) {\n      this.context.router.push('/')\n    }\n  }\n\n  header() {\n    const { sr, container } = this.props\n    if (!sr) {\n      return <Icon icon='loading' />\n    }\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={6} className='header-title'>\n            <h2>\n              <Icon icon='sr' /> <Text value={sr.name_label} onChange={nameLabel => editSr(sr, { nameLabel })} />\n              {sr.inMaintenanceMode && <span className='tag tag-pill tag-warning ml-1'>{_('maintenanceMode')}</span>}\n            </h2>\n            <Copiable tagName='pre' className='text-muted mb-0'>\n              {sr.uuid}\n            </Copiable>\n            <span>\n              <Text value={sr.name_description} onChange={nameDescription => editSr(sr, { nameDescription })} />\n              {container && (\n                <span className='text-muted'>\n                  {' - '}\n                  <Link to={`/${container.type}s/${container.id}`}>{container.name_label}</Link>\n                </span>\n              )}\n            </span>\n          </Col>\n          <Col mediumSize={6}>\n            <div className='text-xs-center'>\n              <SrActionBar sr={sr} />\n            </div>\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <NavTabs>\n              <NavLink to={`/srs/${sr.id}/general`}>{_('generalTabName')}</NavLink>\n              <NavLink to={`/srs/${sr.id}/stats`}>{_('statsTabName')}</NavLink>\n              <NavLink to={`/srs/${sr.id}/disks`}>{_('disksTabName', { disks: sr.VDIs.length })}</NavLink>\n              {sr.SR_type === 'xosan' && <NavLink to={`/srs/${sr.id}/xosan`}>XOSAN</NavLink>}\n              <NavLink to={`/srs/${sr.id}/hosts`}>{_('hostsTabName')}</NavLink>\n              <NavLink to={`/srs/${sr.id}/logs`}>{_('logsTabName')}</NavLink>\n              {sr.SR_type === 'linstor' && <NavLink to={`/srs/${sr.id}/xostor`}>{_('xostor')}</NavLink>}\n              <NavLink to={`/srs/${sr.id}/advanced`}>{_('advancedTabName')}</NavLink>\n            </NavTabs>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n\n  render() {\n    const { container, sr } = this.props\n    if (!sr) {\n      return <h1>{_('statusLoading')}</h1>\n    }\n    const childProps = Object.assign(\n      pick(this.props, ['hosts', 'logs', 'pbds', 'sr', 'vdis', 'unmanagedVdis', 'vdiSnapshots'])\n    )\n    return (\n      <Page header={this.header()} title={`${sr.name_label}${container ? ` (${container.name_label})` : ''}`}>\n        {cloneElement(this.props.children, childProps)}\n      </Page>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport React from 'react'\nimport { SelectSr } from 'select-objects'\nimport { Toggle, SizeInput } from 'form'\nimport { Container, Row, Col } from 'grid'\nimport { createSelector } from 'selectors'\n\nexport default class ReplaceBrickModalBody extends Component {\n  get value() {\n    return this.state\n  }\n\n  _getSrPredicate = createSelector(\n    () => this.props.vm,\n    () => this.state.onSameVm,\n    (vm, onSameVm) => {\n      if (vm === undefined) {\n        return sr => sr.SR_type === 'lvm'\n      }\n\n      return onSameVm\n        ? sr => sr.$container === vm.$container && sr.SR_type === 'lvm'\n        : sr => sr.$pool === vm.$pool && sr.SR_type === 'lvm'\n    }\n  )\n\n  _toggleOnSameVm = () =>\n    this.setState({\n      onSameVm: !this.state.onSameVm,\n      sr: undefined,\n    })\n\n  _selectSr = sr => {\n    this.setState({\n      sr,\n      brickSize: sr.size - sr.physical_usage,\n    })\n  }\n\n  render() {\n    return (\n      <Container>\n        {this.props.vm !== undefined && (\n          <Row className='mb-1'>\n            <Col size={6}>\n              <strong>{_('xosanOnSameVm')}</strong>\n            </Col>\n            <Col size={6}>\n              <Toggle onChange={this._toggleOnSameVm} value={this.state.onSameVm} />\n            </Col>\n          </Row>\n        )}\n        <Row className='mb-1'>\n          <Col size={6}>\n            <strong>{_('xosanUnderlyingStorage')}</strong>\n          </Col>\n          <Col size={6}>\n            <SelectSr onChange={this._selectSr} predicate={this._getSrPredicate()} value={this.state.sr} />\n          </Col>\n        </Row>\n        <Row className='mb-1'>\n          <Col size={6}>\n            <strong>{_('xosanBrickSize')}</strong>\n          </Col>\n          <Col size={6}>\n            <SizeInput onChange={this.linkState('brickSize')} value={this.state.brickSize} />\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport Copiable from 'copiable'\nimport defined from '@xen-orchestra/defined'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport TabButton from 'tab-button'\nimport { addSubscriptions, connectStore, formatSize } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { CustomFields } from 'custom-fields'\nimport { createGetObjectsOfType } from 'selectors'\nimport { createSelector } from 'reselect'\nimport { createSrUnhealthyVdiChainsLengthSubscription, deleteSr, reclaimSrSpace, toggleSrMaintenanceMode } from 'xo'\nimport { flowRight, isEmpty, keys } from 'lodash'\n\n// ===================================================================\n\nconst COLUMNS = [\n  {\n    itemRenderer: _ => <span>{_.name_label}</span>,\n    name: _('srUnhealthyVdiNameLabel'),\n    sortCriteria: 'name_label',\n  },\n  {\n    itemRenderer: vdi => formatSize(vdi.size),\n    name: _('srUnhealthyVdiSize'),\n    sortCriteria: 'size',\n  },\n  {\n    itemRenderer: (vdi, chains) => chains[vdi.uuid],\n    name: _('srUnhealthyVdiDepth'),\n    sortCriteria: (vdi, chains) => chains[vdi.uuid],\n  },\n  {\n    itemRenderer: _ => <Copiable tagName='div'>{_.uuid}</Copiable>,\n    name: _('srUnhealthyVdiUuid'),\n    sortCriteria: 'uuid',\n  },\n]\n\nconst UnhealthyVdiChains = flowRight(\n  addSubscriptions(props => ({\n    chains: createSrUnhealthyVdiChainsLengthSubscription(props.sr),\n  })),\n  connectStore(() => ({\n    vdis: createGetObjectsOfType('VDI').pick(createSelector((_, props) => props.chains?.unhealthyVdis, keys)),\n  }))\n)(({ chains: { nUnhealthyVdis, unhealthyVdis } = {}, vdis }) =>\n  isEmpty(vdis) ? null : (\n    <div>\n      <hr />\n      <h3>{_('srUnhealthyVdiTitle', { total: nUnhealthyVdis })}</h3>\n      <SortedTable collection={vdis} columns={COLUMNS} stateUrlParam='s_unhealthy_vdis' userData={unhealthyVdis} />\n    </div>\n  )\n)\n\nexport default ({ sr }) => (\n  <Container>\n    <Row>\n      <Col className='text-xs-right'>\n        <TabButton\n          btnStyle='primary'\n          handler={reclaimSrSpace}\n          handlerParam={sr}\n          icon='sr-reclaim-space'\n          labelId='srReclaimSpace'\n        />\n        {sr.inMaintenanceMode ? (\n          <TabButton\n            btnStyle='warning'\n            handler={toggleSrMaintenanceMode}\n            handlerParam={sr}\n            icon='sr-disable'\n            labelId='disableMaintenanceMode'\n          />\n        ) : (\n          <TabButton\n            btnStyle='warning'\n            handler={toggleSrMaintenanceMode}\n            handlerParam={sr}\n            icon='sr-enable'\n            labelId='enableMaintenanceMode'\n          />\n        )}\n        <TabButton btnStyle='danger' handler={deleteSr} handlerParam={sr} icon='sr-remove' labelId='srRemoveButton' />\n      </Col>\n    </Row>\n    <Row>\n      <Col>\n        <table className='table'>\n          <tbody>\n            <tr>\n              <th>{_('provisioning')}</th>\n              <td>{defined(sr.allocationStrategy, _('unknown'))}</td>\n            </tr>\n            <tr>\n              <th>{_('customFields')}</th>\n              <td>\n                <CustomFields object={sr.id} />\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </Col>\n    </Row>\n    <Row>\n      <Col>\n        <UnhealthyVdiChains sr={sr} />\n      </Col>\n    </Row>\n  </Container>\n)\n","import * as CM from 'complex-matcher'\nimport _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport ActionRowButton from 'action-row-button'\nimport ButtonGroup from 'button-group'\nimport Component from 'base-component'\nimport copy from 'copy-to-clipboard'\nimport Icon from 'icon'\nimport Link from 'link'\nimport MigrateVdiModalBody from 'xo/migrate-vdi-modal'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport TabButton from 'tab-button'\nimport Tooltip from 'tooltip'\nimport renderXoItem, { Vdi, Vm } from 'render-xo-item'\nimport { confirm } from 'modal'\nimport { injectIntl } from 'react-intl'\nimport { Text } from 'editable'\nimport { SizeInput, Toggle } from 'form'\nimport { Container, Row, Col } from 'grid'\nimport { connectStore, formatSize, noop } from 'utils'\nimport { concat, every, groupBy, isEmpty, map, mapValues, pick, some, sortBy } from 'lodash'\nimport { createCollectionWrapper, createGetObjectsOfType, createSelector, getCheckPermissions } from 'selectors'\nimport {\n  connectVbd,\n  createDisk,\n  deleteVbd,\n  deleteVdi,\n  deleteVdis,\n  disconnectVbd,\n  editVdi,\n  exportVdi,\n  importVdi,\n  isVmRunning,\n  isSrIso,\n  isSrShared,\n  migrateVdi,\n  setCbt,\n} from 'xo'\nimport { error } from 'notification'\n\n// ===================================================================\n\nconst COLUMNS = [\n  {\n    name: _('vdiNameLabel'),\n    itemRenderer: (vdi, { vdisByBaseCopy }) => {\n      const activeVdis = vdisByBaseCopy[vdi.id]\n      const isMetadataVdi = vdi.VDI_type === 'cbt_metadata'\n      return (\n        <span>\n          <Text value={vdi.name_label} onChange={value => editVdi(vdi, { name_label: value })} />{' '}\n          {vdi.type === 'VDI-snapshot' && (\n            <span className='tag tag-info'>\n              <Icon icon='vm-snapshot' />\n            </span>\n          )}\n          {isMetadataVdi && (\n            <span className='tag tag-info' style={{ marginLeft: '0.4em' }}>\n              <Tooltip content={_('isMetadataVdi')}>\n                <Icon icon='file' />\n              </Tooltip>\n            </span>\n          )}\n          {vdi.type === 'VDI-unmanaged' &&\n            (activeVdis !== undefined ? (\n              <span>\n                (\n                <Link\n                  to={`/srs/${activeVdis[0].$SR}/disks?s=${encodeURIComponent(\n                    new CM.Property(\n                      'id',\n                      new CM.Or(activeVdis.map(activeVdi => new CM.String(activeVdi.id)))\n                    ).toString()\n                  )}`}\n                >\n                  {activeVdis.length > 1 ? (\n                    _('multipleActiveVdis', { firstVdi: <Vdi id={activeVdis[0].id} />, nVdis: activeVdis.length - 1 })\n                  ) : (\n                    <Vdi id={activeVdis[0].id} showSize />\n                  )}\n                </Link>\n                )\n              </span>\n            ) : (\n              <span>({_('noActiveVdi')})</span>\n            ))}\n        </span>\n      )\n    },\n    sortCriteria: vdi => vdi.name_label,\n  },\n  {\n    name: _('vdiNameDescription'),\n    itemRenderer: vdi => (\n      <Text value={vdi.name_description} onChange={value => editVdi(vdi, { name_description: value })} />\n    ),\n  },\n  {\n    name: _('vdiTags'),\n    itemRenderer: vdi => vdi.tags,\n  },\n  {\n    name: _('vdiSize'),\n    itemRenderer: vdi => formatSize(vdi.size),\n    sortCriteria: vdi => vdi.size,\n  },\n  {\n    name: _('vbdCbt'),\n    itemRenderer: vdi => <Toggle value={vdi.cbt_enabled} onChange={cbt => setCbt(vdi, cbt)} />,\n    sortCriteria: vdi => vdi.cbt_enabled,\n  },\n  {\n    name: _('vdiVms'),\n    component: connectStore(() => {\n      const getVbds = createGetObjectsOfType('VBD')\n        .pick((_, props) => props.item.$VBDs)\n        .sort()\n      const getVmIds = createSelector(getVbds, vbds => map(vbds, 'VM'))\n      const getVms = createGetObjectsOfType('VM').pick(getVmIds)\n      const getVmControllers = createGetObjectsOfType('VM-controller').pick(getVmIds)\n      const getVmSnapshots = createGetObjectsOfType('VM-snapshot').pick(getVmIds)\n      const getVmTemplates = createGetObjectsOfType('VM-template').pick(getVmIds)\n      const getAllVms = createSelector(\n        getVms,\n        getVmControllers,\n        getVmSnapshots,\n        getVmTemplates,\n        (vms, vmControllers, vmSnapshots, vmTemplates) => ({\n          ...vms,\n          ...vmControllers,\n          ...vmSnapshots,\n          ...vmTemplates,\n        })\n      )\n\n      return (state, props) => ({\n        vms: getAllVms(state, props),\n        vbds: getVbds(state, props),\n      })\n    })(({ item: vdi, vbds, vms, userData: { vmSnapshotsBySuspendVdi } }) => {\n      const vmSnapshot = vmSnapshotsBySuspendVdi[vdi.uuid]?.[0]\n\n      return (\n        <Container>\n          {vmSnapshot === undefined ? (\n            map(vbds, (vbd, index) => {\n              const vm = vms[vbd.VM]\n\n              if (vm === undefined) {\n                return null\n              }\n\n              const type = vm.type\n              let link\n              if (type === 'VM') {\n                link = `/vms/${vm.id}`\n              } else if (type === 'VM-template') {\n                link = `/home?s=${vm.id}&t=VM-template`\n              } else {\n                link = vm.$snapshot_of === undefined ? '/dashboard/health' : `/vms/${vm.$snapshot_of}/snapshots`\n              }\n\n              return (\n                <Row className={index > 0 && 'mt-1'}>\n                  <Col mediumSize={8}>\n                    <Link to={link}>{renderXoItem(vm)}</Link>\n                  </Col>\n                  <Col mediumSize={4}>\n                    <ButtonGroup>\n                      {vbd.attached ? (\n                        <ActionRowButton\n                          btnStyle='danger'\n                          handler={disconnectVbd}\n                          handlerParam={vbd}\n                          icon='disconnect'\n                          tooltip={_('vbdDisconnect')}\n                        />\n                      ) : (\n                        <ActionRowButton\n                          btnStyle='primary'\n                          disabled={some(vbds, 'attached') || !isVmRunning(vm)}\n                          handler={connectVbd}\n                          handlerParam={vbd}\n                          icon='connect'\n                          tooltip={_('vbdConnect')}\n                        />\n                      )}\n                      <ActionRowButton\n                        btnStyle='danger'\n                        handler={deleteVbd}\n                        handlerParam={vbd}\n                        icon='vdi-forget'\n                        tooltip={_('vdiForget')}\n                      />\n                    </ButtonGroup>\n                  </Col>\n                </Row>\n              )\n            })\n          ) : (\n            <Col mediumSize={8}>\n              <Link to={`/vms/${vmSnapshot.$snapshot_of}/snapshots`}>\n                <Vm id={vmSnapshot.$snapshot_of} />\n              </Link>\n            </Col>\n          )}\n        </Container>\n      )\n    }),\n  },\n]\n\nconst GROUPED_ACTIONS = [\n  {\n    disabled: vdis => some(vdis, { type: 'VDI-unmanaged' }),\n    handler: deleteVdis,\n    icon: 'delete',\n    label: _('destroySelectedVdis'),\n    level: 'danger',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  ...(process.env.XOA_PLAN > 1\n    ? [\n        {\n          disabled: ({ id, type }, { isVdiAttached }) => isVdiAttached[id] || type === 'VDI-unmanaged',\n          handler: importVdi,\n          icon: 'import',\n          label: _('importVdi'),\n        },\n        {\n          disabled: ({ type }) => type === 'VDI-unmanaged',\n          handler: exportVdi,\n          icon: 'export',\n          label: _('exportVdi'),\n        },\n      ]\n    : []),\n  {\n    handler: vdi => copy(vdi.uuid),\n    icon: 'clipboard',\n    label: vdi => _('copyUuid', { uuid: vdi.uuid }),\n  },\n  {\n    disabled: ({ type }) => type === 'VDI-unmanaged',\n    handler: deleteVdi,\n    icon: 'delete',\n    label: _('destroyVdi'),\n    level: 'danger',\n  },\n]\n\nconst FILTERS = {\n  filterOnlyManaged: 'type:!VDI-unmanaged',\n  filterOnlyRegular: '!type:|(VDI-snapshot VDI-unmanaged)',\n  filterOnlySnapshots: 'type:VDI-snapshot',\n  filterOnlyOrphaned: 'type:!VDI-unmanaged $VBDs:!\"\"',\n  filterOnlyUnmanaged: 'type:VDI-unmanaged',\n}\n\n// ===================================================================\n\n@injectIntl\nclass NewDisk extends Component {\n  static propTypes = {\n    onClose: PropTypes.func,\n    sr: PropTypes.object.isRequired,\n  }\n\n  _createDisk = () => {\n    const { sr, onClose = noop } = this.props\n    const { name, readOnly, size } = this.state\n\n    return createDisk(name, size, sr, {\n      mode: readOnly ? 'RO' : 'RW',\n    }).then(onClose)\n  }\n\n  render() {\n    const { formatMessage } = this.props.intl\n    const { name, readOnly, size } = this.state\n\n    return (\n      <form id='newDiskForm' className='form-inline'>\n        <div className='form-group'>\n          <input\n            autoFocus\n            className='form-control'\n            onChange={this.linkState('name')}\n            placeholder={formatMessage(messages.vbdNamePlaceHolder)}\n            required\n            type='text'\n            value={name}\n          />\n        </div>\n        <div className='form-group ml-1'>\n          <SizeInput\n            onChange={this.linkState('size')}\n            placeholder={formatMessage(messages.vbdSizePlaceHolder)}\n            required\n            value={size}\n          />\n        </div>\n        <div className='form-group ml-1'>\n          <span>\n            {_('vbdReadonly')} <Toggle onChange={this.toggleState('readOnly')} value={readOnly} />\n          </span>\n        </div>\n        <ActionButton\n          className='pull-right'\n          btnStyle='primary'\n          form='newDiskForm'\n          handler={this._createDisk}\n          icon='add'\n        >\n          {_('vbdCreate')}\n        </ActionButton>\n      </form>\n    )\n  }\n}\n\n@connectStore(() => {\n  const getVbds = createGetObjectsOfType('VBD')\n  const getVmSnapshotsBySuspendVdi = createGetObjectsOfType('VM-snapshot').groupBy('suspendVdi')\n\n  return (state, props) => ({\n    checkPermissions: getCheckPermissions(state, props),\n    vbds: getVbds(state, props),\n    vmSnapshotsBySuspendVdi: getVmSnapshotsBySuspendVdi(state, props),\n  })\n})\nexport default class SrDisks extends Component {\n  _closeNewDiskForm = () => this.setState({ newDisk: false })\n\n  _getAllVdis = createSelector(\n    () => this.props.vdis,\n    () => this.props.vdiSnapshots,\n    () => this.props.unmanagedVdis,\n    (vdis, vdiSnapshots, unmanagedVdis) => concat(vdis, vdiSnapshots, sortBy(unmanagedVdis, 'id'))\n  )\n\n  _getIsSrAdmin = createSelector(\n    () => this.props.checkPermissions,\n    () => this.props.sr.id,\n    (check, id) => check(id, 'administrate')\n  )\n\n  _getIsVdiAttached = createSelector(\n    createSelector(\n      () => this.props.vbds,\n      createCollectionWrapper(() => map(this.props.vdis, 'id')),\n      (vbds, vdis) => pick(groupBy(vbds, 'VDI'), vdis)\n    ),\n    vbdsByVdi => mapValues(vbdsByVdi, vbds => some(vbds, 'attached'))\n  )\n\n  // the warning will be displayed if the SR is local\n  // or the VDIs contain at least one VBD.\n  _getGenerateWarningBeforeMigrate = createSelector(\n    createCollectionWrapper(_ => _),\n    vdis => sr =>\n      sr === undefined || isSrShared(sr) || every(vdis, _ => isEmpty(_.$VBDs)) ? null : (\n        <span className='text-warning'>\n          <Icon icon='alarm' /> {_('migrateVdiMessage')}\n        </span>\n      )\n  )\n\n  _migrateVdis = vdis =>\n    confirm({\n      title: _('vdiMigrate'),\n      body: (\n        <MigrateVdiModalBody\n          pool={this.props.sr.$pool}\n          warningBeforeMigrate={this._getGenerateWarningBeforeMigrate(vdis)}\n          isoSr={isSrIso(this.props.sr)}\n        />\n      ),\n    }).then(({ sr }) => {\n      if (sr === undefined) {\n        return error(_('vdiMigrateNoSr'), _('vdiMigrateNoSrMessage'))\n      }\n\n      return Promise.all(map(vdis, vdi => migrateVdi(vdi, sr)))\n    }, noop)\n\n  _actions = [\n    {\n      disabled: vdis => some(vdis, ({ type }) => type === 'VDI-unmanaged' || type === 'VDI-snapshot'),\n      handler: this._migrateVdis,\n      icon: 'vdi-migrate',\n      individualLabel: vdis => {\n        const { type } = vdis[0]\n        return type === 'VDI-unmanaged' || type === 'VDI-snapshot' ? _('disabledVdiMigrateTooltip') : _('vdiMigrate')\n      },\n      label: vdis => {\n        return some(vdis, ({ type }) => type === 'VDI-unmanaged' || type === 'VDI-snapshot')\n          ? _('disabledVdiMigrateTooltip')\n          : _('migrateSelectedVdis')\n      },\n    },\n  ]\n\n  _getVdisByBaseCopy = createSelector(\n    () => this.props.vdis,\n    () => this.props.unmanagedVdis,\n    (vdis, unmanagedVdis) => {\n      const vdisByBaseCopy = {}\n\n      vdis.forEach(vdi => {\n        let baseCopy = unmanagedVdis[vdi.parent]\n\n        while (baseCopy !== undefined) {\n          const baseCopyId = baseCopy.id\n\n          if (vdisByBaseCopy[baseCopyId] === undefined) {\n            vdisByBaseCopy[baseCopyId] = []\n          }\n          vdisByBaseCopy[baseCopyId].push(vdi)\n          baseCopy = unmanagedVdis[baseCopy.parent]\n        }\n      })\n      return vdisByBaseCopy\n    }\n  )\n\n  render() {\n    const vdis = this._getAllVdis()\n    const { newDisk } = this.state\n\n    return (\n      <Container>\n        {this._getIsSrAdmin() && [\n          <Row key='new-disk'>\n            <Col className='text-xs-right'>\n              <TabButton\n                btnStyle={newDisk ? 'info' : 'primary'}\n                handler={this.toggleState('newDisk')}\n                icon='add'\n                labelId='vbdCreateDeviceButton'\n              />\n            </Col>\n          </Row>,\n          newDisk && (\n            <Row key='new-disk-form'>\n              <Col>\n                <NewDisk sr={this.props.sr} onClose={this._closeNewDiskForm} />\n                <hr />\n              </Col>\n            </Row>\n          ),\n        ]}\n        <Row>\n          <Col>\n            {!isEmpty(vdis) ? (\n              <SortedTable\n                actions={this._actions}\n                collection={vdis}\n                columns={COLUMNS}\n                data-isVdiAttached={this._getIsVdiAttached()}\n                data-vdisByBaseCopy={this._getVdisByBaseCopy()}\n                data-vmSnapshotsBySuspendVdi={this.props.vmSnapshotsBySuspendVdi}\n                defaultFilter='filterOnlyManaged'\n                filters={FILTERS}\n                groupedActions={GROUPED_ACTIONS}\n                individualActions={INDIVIDUAL_ACTIONS}\n                shortcutsTarget='body'\n                stateUrlParam='s'\n              />\n            ) : (\n              <h4 className='text-xs-center'>{_('srNoVdis')}</h4>\n            )}\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import * as CM from 'complex-matcher'\nimport _ from 'intl'\nimport Component from 'base-component'\nimport decorate from 'apply-decorators'\nimport HomeTags from 'home-tags'\nimport Icon from 'icon'\nimport React from 'react'\nimport Usage, { UsageElement } from 'usage'\nimport { addTag, removeTag, getLicense } from 'xo'\nimport { connectStore, formatSize } from 'utils'\nimport { Container, Row, Col } from 'grid'\nimport { createCollectionWrapper, createGetObjectsOfType, createSelector } from 'selectors'\nimport { flattenDeep, flatMap, forEach, groupBy, keyBy, map, mapValues, pick, sumBy, uniq } from 'lodash'\nimport { get } from '@xen-orchestra/defined'\nimport { injectState, provideState } from 'reaclette'\n\nconst nestedUlStyle = { margin: '0.1em', marginLeft: '0.5em', padding: 0 }\nconst ulStyle = { margin: 0, padding: 0 }\n\nconst UsageTooltip = decorate([\n  connectStore(() => {\n    const getVbds = createGetObjectsOfType('VBD').pick(\n      createCollectionWrapper(\n        createSelector(\n          (_, { group }) => group.vdis,\n          vdis => flatMap(vdis, '$VBDs')\n        )\n      )\n    )\n\n    const getVms = createGetObjectsOfType('VM').pick(\n      createCollectionWrapper(createSelector(getVbds, vbds => map(vbds, 'VM')))\n    )\n\n    return {\n      vbds: getVbds,\n      vms: getVms,\n    }\n  }),\n  provideState({\n    computed: {\n      baseCopiesUsage: (_, { group: { baseCopies } }) => formatSize(sumBy(baseCopies, 'usage')),\n      vdis: (_, { group: { vdis } }) => keyBy(vdis, 'id'),\n      vdisUsage: (_, { group: { vdis } }) => formatSize(sumBy(vdis, 'usage')),\n      snapshotsUsage: (_, { group: { snapshots } }) => formatSize(sumBy(snapshots, 'usage')),\n      vmNamesByVdi: createCollectionWrapper(({ vdis }, { vbds, vms }) =>\n        mapValues(vdis, vdi => get(() => vms[vbds[vdi.$VBDs[0]].VM].name_label))\n      ),\n    },\n  }),\n  injectState,\n  class extends Component {\n    _getVdiTooltip = vdi => {\n      const vmName = this.props.state.vmNamesByVdi[vdi.id]\n      return (\n        <span>\n          {vmName === undefined\n            ? _('diskTooltip', {\n                name: vdi.name_label,\n                usage: formatSize(vdi.usage),\n              })\n            : _('vdiOnVmTooltip', {\n                name: vdi.name_label,\n                usage: formatSize(vdi.usage),\n                vmName,\n              })}\n        </span>\n      )\n    }\n\n    render() {\n      const { group, state } = this.props\n      const { baseCopies, name_label: name, snapshots, type, usage, vdis } = group\n      return (\n        <div>\n          {type === 'orphanedSnapshot' ? (\n            <span>\n              {_('diskTooltip', {\n                name,\n                usage: formatSize(usage),\n              })}\n            </span>\n          ) : baseCopies.length === 0 && snapshots.length === 0 ? (\n            this._getVdiTooltip(vdis[0])\n          ) : (\n            <ul style={ulStyle}>\n              <li>\n                {_('baseCopyTooltip', {\n                  n: baseCopies.length,\n                  usage: state.baseCopiesUsage,\n                })}\n              </li>\n              <li>\n                {_('snapshotsTooltip', {\n                  n: snapshots.length,\n                  usage: state.snapshotsUsage,\n                })}\n                <ul style={nestedUlStyle}>\n                  {snapshots.map(snapshot => (\n                    <li key={snapshot.id}>\n                      {_('diskTooltip', {\n                        name: snapshot.name_label,\n                        usage: formatSize(snapshot.usage),\n                      })}\n                    </li>\n                  ))}\n                </ul>\n              </li>\n              <li>\n                {_('vdisTooltip', {\n                  n: vdis.length,\n                  usage: state.vdisUsage,\n                })}\n                <ul style={nestedUlStyle}>\n                  {vdis.map(vdi => (\n                    <li key={vdi.id}>{this._getVdiTooltip(vdi)}</li>\n                  ))}\n                </ul>\n              </li>\n            </ul>\n          )}\n        </div>\n      )\n    }\n  },\n])\n\nexport default class TabGeneral extends Component {\n  componentDidMount() {\n    const { sr } = this.props\n\n    if (sr.SR_type === 'xosan') {\n      getLicense('xosan.trial', sr.id).then(() => this.setState({ licenseRestriction: true }))\n    }\n  }\n\n  _getDiskGroups = createSelector(\n    () => this.props.vdis,\n    () => this.props.vdiSnapshots,\n    () => this.props.unmanagedVdis,\n    (vdis, vdiSnapshots, unmanagedVdis) => {\n      const groups = []\n      const snapshotsByVdi = groupBy(vdiSnapshots, '$snapshot_of')\n\n      let orphanedVdiSnapshots\n      if ((orphanedVdiSnapshots = snapshotsByVdi[undefined]) !== undefined) {\n        groups.push(\n          ...orphanedVdiSnapshots.map(snapshot => ({\n            id: snapshot.id,\n            name_label: snapshot.name_label,\n            usage: snapshot.usage,\n            type: 'orphanedSnapshot',\n          }))\n        )\n      }\n      // search root base copy for each VDI\n      const vdisInfo = vdis.map(({ id, parent, name_label, usage, $VBDs }) => {\n        const baseCopies = new Set()\n        let baseCopy\n        let root = id\n        while ((baseCopy = unmanagedVdis[parent]) !== undefined) {\n          root = baseCopy.id\n          parent = baseCopy.parent\n          baseCopies.add(baseCopy)\n        }\n        let snapshots\n        if ((snapshots = snapshotsByVdi[id]) !== undefined) {\n          // snapshot can have base copy without active VDI\n          snapshots.forEach(({ parent }) => {\n            while ((baseCopy = unmanagedVdis[parent]) !== undefined && !baseCopies.has(baseCopy)) {\n              parent = baseCopy.parent\n              baseCopies.add(baseCopy)\n            }\n          })\n        }\n        return {\n          baseCopies,\n          id,\n          name_label,\n          root,\n          snapshots: snapshots === undefined ? [] : snapshots,\n          usage,\n          $VBDs,\n        }\n      })\n      // group VDIs by their root base copy.\n      const vdisByRoot = groupBy(vdisInfo, 'root')\n\n      // group collection of VDIs and their snapshots and base copies.\n      forEach(vdisByRoot, vdis => {\n        let baseCopies = []\n        let snapshots = []\n        let vdisUsage = 0\n        vdis.forEach(vdi => {\n          vdisUsage += vdi.usage\n          baseCopies = baseCopies.concat(...vdi.baseCopies)\n          snapshots = snapshots.concat(vdi.snapshots)\n        })\n        baseCopies = uniq(baseCopies)\n        snapshots = uniq(snapshots)\n        groups.push({\n          id: vdis[0].id,\n          vdis,\n          baseCopies,\n          usage: vdisUsage + sumBy(baseCopies, 'usage') + sumBy(snapshots, 'usage'),\n          snapshots,\n        })\n      })\n      return groups\n    }\n  )\n\n  _getGenerateLink = createSelector(\n    this._getDiskGroups,\n    diskGroups => ids =>\n      `#/srs/${this.props.sr.id}/disks?s=${encodeURIComponent(\n        new CM.Property(\n          'id',\n          new CM.Or(\n            flattenDeep(\n              map(pick(keyBy(diskGroups, 'id'), ids), ({ id, baseCopies, vdis, snapshots, type }) =>\n                type === 'orphanedSnapshot' ? id : [map(baseCopies, 'id'), map(vdis, 'id'), map(snapshots, 'id')]\n              )\n            )\n              .sort()\n              .map(_ => new CM.String(_))\n          )\n        ).toString()\n      )}`\n  )\n\n  render() {\n    const { sr } = this.props\n    return (\n      <Container>\n        <Row className='text-xs-center'>\n          <Col mediumSize={4}>\n            <h2>\n              {sr.VDIs.length}x <Icon icon='disk' size='lg' />\n            </h2>\n          </Col>\n          <Col mediumSize={4}>\n            <h2>\n              {formatSize(sr.size)} <Icon icon='sr' size='lg' />\n            </h2>\n            <p>Type: {sr.SR_type}</p>\n            {this.state.licenseRestriction && <p className='text-danger'>{_('xosanLicenseRestricted')}</p>}\n          </Col>\n          <Col mediumSize={4}>\n            <h2>\n              {sr.$PBDs.length}x <Icon icon='host' size='lg' />\n            </h2>\n          </Col>\n        </Row>\n        <Row>\n          <Col className='text-xs-center'>\n            <h5>\n              {formatSize(sr.physical_usage)} {_('srUsed')} ({formatSize(sr.size - sr.physical_usage)} {_('srFree')})\n            </h5>\n          </Col>\n        </Row>\n        <Row>\n          <Col smallOffset={1} mediumSize={10}>\n            <Usage total={sr.size} type='disk' link={this._getGenerateLink()}>\n              {this._getDiskGroups().map(group => (\n                <UsageElement\n                  highlight={group.type === 'orphanedSnapshot'}\n                  id={group.id}\n                  key={group.id}\n                  tooltip={<UsageTooltip group={group} />}\n                  value={group.usage}\n                />\n              ))}\n            </Usage>\n          </Col>\n        </Row>\n        <Row className='text-xs-center'>\n          <Col>\n            <h2 className='text-xs-center'>\n              <HomeTags\n                type='SR'\n                labels={sr.tags}\n                onDelete={tag => removeTag(sr.id, tag)}\n                onAdd={tag => addTag(sr.id, tag)}\n              />\n            </h2>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport Link from 'link'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport { confirm } from 'modal'\nimport { Container, Row, Col } from 'grid'\nimport { editHost, connectPbd, disconnectPbd, deletePbd, deletePbds } from 'xo'\nimport { get } from '@xen-orchestra/defined'\nimport { getIscsiPaths, noop } from 'utils'\nimport { isEmpty, some } from 'lodash'\nimport { provideState, injectState } from 'reaclette'\nimport { Text } from 'editable'\n\nconst forgetHost = pbd =>\n  confirm({\n    title: _('forgetHostFromSrModalTitle'),\n    body: _('forgetHostFromSrModalMessage'),\n  }).then(() => deletePbd(pbd), noop)\n\nconst forgetHosts = pbds =>\n  confirm({\n    title: _('forgetHostsFromSrModalTitle', { nPbds: pbds.length }),\n    body: _('forgetHostsFromSrModalMessage', { nPbds: pbds.length }),\n  }).then(() => deletePbds(pbds), noop)\n\nconst HOST_COLUMNS = [\n  {\n    name: _('hostNameLabel'),\n    itemRenderer: (pbd, hosts) => {\n      const host = hosts[pbd.host]\n      return (\n        <Link to={`/hosts/${host.id}`}>\n          <Text value={host.name_label} onChange={value => editHost(host, { name_label: value })} useLongClick />\n        </Link>\n      )\n    },\n    sortCriteria: (pbd, hosts) => hosts[pbd.host].name_label,\n  },\n  {\n    name: _('hostDescription'),\n    itemRenderer: (pbd, hosts) => {\n      const host = hosts[pbd.host]\n      return <Text value={host.name_description} onChange={value => editHost(host, { name_description: value })} />\n    },\n    sortCriteria: (pbd, hosts) => hosts[pbd.host].name_description,\n  },\n  {\n    name: _('pbdDetails'),\n    itemRenderer: ({ device_config: deviceConfig }) => {\n      const keys = Object.keys(deviceConfig)\n      return (\n        <ul className='list-unstyled'>\n          {keys.map(key => (\n            <li key={key}>{_.keyValue(key, deviceConfig[key])}</li>\n          ))}\n        </ul>\n      )\n    },\n  },\n  {\n    name: _('pbdStatus'),\n    itemRenderer: pbd => (\n      <StateButton\n        disabledLabel={_('pbdStatusDisconnected')}\n        disabledHandler={connectPbd}\n        disabledTooltip={_('pbdConnect')}\n        enabledLabel={_('pbdStatusConnected')}\n        enabledHandler={disconnectPbd}\n        enabledTooltip={_('pbdDisconnect')}\n        handlerParam={pbd}\n        state={pbd.attached}\n      />\n    ),\n    sortCriteria: 'attached',\n  },\n]\n\nconst HOST_ACTIONS = [\n  {\n    disabled: pbds => some(pbds, 'attached'),\n    handler: forgetHosts,\n    icon: 'sr-forget',\n    individualDisabled: pbd => pbd.attached,\n    individualHandler: forgetHost,\n    label: _('pbdForget'),\n  },\n]\n\nconst HOST_WITH_PATHS_COLUMNS = [\n  ...HOST_COLUMNS,\n  {\n    name: _('paths'),\n    itemRenderer: (pbd, hosts) => {\n      if (!pbd.attached) {\n        return _('pbdDisconnected')\n      }\n\n      if (!get(() => hosts[pbd.host].multipathing)) {\n        return _('multipathingDisabled')\n      }\n\n      const [nActives, nPaths] = getIscsiPaths(pbd)\n      const nSessions = pbd.otherConfig.iscsi_sessions\n      return (\n        <span>\n          {nActives !== undefined &&\n            nPaths !== undefined &&\n            _('hostMultipathingPaths', {\n              nActives,\n              nPaths,\n            })}{' '}\n          {nSessions !== undefined && _('iscsiSessions', { nSessions })}\n        </span>\n      )\n    },\n    sortCriteria: (pbd, hosts) => get(() => hosts[pbd.host].multipathing),\n  },\n]\n\nexport default decorate([\n  provideState({\n    computed: {\n      columns: (_, { sr }) => (sr.sm_config.multipathable ? HOST_WITH_PATHS_COLUMNS : HOST_COLUMNS),\n    },\n  }),\n  injectState,\n  ({ state, hosts, pbds }) => (\n    <Container>\n      <Row>\n        <Col>\n          {!isEmpty(hosts) ? (\n            <SortedTable\n              actions={HOST_ACTIONS}\n              collection={pbds}\n              columns={state.columns}\n              stateUrlParam='s'\n              userData={hosts}\n            />\n          ) : (\n            <h4 className='text-xs-center'>{_('noHost')}</h4>\n          )}\n        </Col>\n      </Row>\n    </Container>\n  ),\n])\n","import _ from 'intl'\nimport ActionRow from 'action-row-button'\nimport Button from 'button'\nimport isEmpty from 'lodash/isEmpty'\nimport map from 'lodash/map'\nimport React, { Component } from 'react'\nimport TabButton from 'tab-button'\nimport { deleteMessage } from 'xo'\nimport { createPager } from 'selectors'\nimport { FormattedRelative, FormattedTime } from 'react-intl'\nimport { Container, Row, Col } from 'grid'\n\nexport default class TabLogs extends Component {\n  constructor() {\n    super()\n\n    this.getLogs = createPager(\n      () => this.props.logs,\n      () => this.state.page,\n      10\n    )\n\n    this.state = {\n      page: 1,\n    }\n  }\n\n  _deleteAllLogs = () => map(this.props.logs, deleteMessage)\n  _nextPage = () => this.setState({ page: this.state.page + 1 })\n  _previousPage = () => this.setState({ page: this.state.page - 1 })\n\n  render() {\n    const logs = this.getLogs()\n\n    return (\n      <Container>\n        {isEmpty(logs) ? (\n          <Row>\n            <Col mediumSize={6} className='text-xs-center'>\n              <br />\n              <h4>{_('noLogs')}</h4>\n            </Col>\n          </Row>\n        ) : (\n          <div>\n            <Row>\n              <Col className='text-xs-right'>\n                <Button size='large' onClick={this._previousPage}>\n                  &lt;\n                </Button>\n                <Button size='large' onClick={this._nextPage}>\n                  &gt;\n                </Button>\n                <TabButton btnStyle='danger' handler={this._deleteAllLogs} icon='delete' labelId='logRemoveAll' />\n              </Col>\n            </Row>\n            <Row>\n              <Col>\n                <table className='table'>\n                  <thead className='thead-default'>\n                    <tr>\n                      <th>{_('logDate')}</th>\n                      <th>{_('logName')}</th>\n                      <th>{_('logContent')}</th>\n                      <th>{_('logAction')}</th>\n                    </tr>\n                  </thead>\n                  <tbody>\n                    {map(logs, log => (\n                      <tr key={log.id}>\n                        <td>\n                          <FormattedTime\n                            value={log.time * 1000}\n                            minute='numeric'\n                            hour='numeric'\n                            day='numeric'\n                            month='long'\n                            year='numeric'\n                          />{' '}\n                          (<FormattedRelative value={log.time * 1000} />)\n                        </td>\n                        <td>{log.name}</td>\n                        <td>{log.body}</td>\n                        <td>\n                          <ActionRow btnStyle='danger' handler={deleteMessage} handlerParam={log} icon='delete' />\n                        </td>\n                      </tr>\n                    ))}\n                  </tbody>\n                </table>\n              </Col>\n            </Row>\n          </div>\n        )}\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { Container, Row, Col } from 'grid'\nimport { DEFAULT_GRANULARITY, fetchStats, SelectGranularity } from 'stats'\nimport get from 'lodash/get.js'\nimport { Toggle } from 'form'\nimport { IopsLineChart, IoThroughputChart, IowaitChart, LatencyChart } from 'xo-line-chart'\n\nexport default class SrStats extends Component {\n  state = {\n    granularity: DEFAULT_GRANULARITY,\n  }\n\n  _loop(sr = get(this.props, 'sr')) {\n    if (sr === undefined) {\n      this._loop()\n    }\n\n    if (this.cancel !== undefined) {\n      this.cancel()\n    }\n\n    let cancelled = false\n    this.cancel = () => {\n      cancelled = true\n    }\n\n    fetchStats(sr, 'sr', this.state.granularity).then(data => {\n      if (cancelled) {\n        return\n      }\n      this.cancel = undefined\n\n      clearTimeout(this.timeout)\n      this.setState(\n        {\n          data,\n          selectStatsLoading: false,\n        },\n        () => {\n          this.timeout = setTimeout(this._loop, data.interval * 1e3)\n        }\n      )\n    })\n  }\n\n  _loop = ::this._loop\n\n  componentWillMount() {\n    this._loop()\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this.timeout)\n  }\n\n  _onGranularityChange = granularity => {\n    clearTimeout(this.timeout)\n    this.setState(\n      {\n        granularity,\n        selectStatsLoading: true,\n      },\n      this._loop\n    )\n  }\n\n  render() {\n    const { data, granularity, selectStatsLoading, useCombinedValues } = this.state\n\n    return data === undefined ? (\n      <span>{_('srNoStats')}</span>\n    ) : (\n      <Container>\n        <Row>\n          <Col mediumSize={5}>\n            <div className='form-group'>\n              <Tooltip content={_('useStackedValuesOnStats')}>\n                <Toggle value={useCombinedValues} onChange={this.linkState('useCombinedValues')} />\n              </Tooltip>\n            </div>\n          </Col>\n          <Col mediumSize={1}>\n            {selectStatsLoading && (\n              <div className='text-xs-right'>\n                <Icon icon='loading' size={2} />\n              </div>\n            )}\n          </Col>\n          <Col mediumSize={6}>\n            <SelectGranularity onChange={this._onGranularityChange} required value={granularity} />\n          </Col>\n        </Row>\n        <Row>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='iops' size={1} /> {_('statsIops')}\n            </h5>\n            <IopsLineChart addSumSeries={useCombinedValues} data={data} />\n          </Col>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='disk' size={1} /> {_('statsIoThroughput')}\n            </h5>\n            <IoThroughputChart addSumSeries={useCombinedValues} data={data} />\n          </Col>\n        </Row>\n        <br />\n        <hr />\n        <Row>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='latency' size={1} /> {_('statsLatency')}\n            </h5>\n            <LatencyChart addSumSeries={useCombinedValues} data={data} />\n          </Col>\n          <Col mediumSize={6}>\n            <h5 className='text-xs-center'>\n              <Icon icon='iowait' size={1} /> {_('statsIowait')}\n            </h5>\n            <IowaitChart addSumSeries={useCombinedValues} data={data} />\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Collapse from 'collapse'\nimport Copiable from 'copiable'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport Link from 'link'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport SingleLineRow from 'single-line-row'\nimport { confirm } from 'modal'\nimport { error } from 'notification'\nimport { Toggle } from 'form'\nimport { Container, Col, Row } from 'grid'\nimport { find, forEach, isEmpty, map, reduce, sum } from 'lodash'\nimport { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'\nimport { addSubscriptions, connectStore, formatSize } from 'utils'\nimport {\n  addXosanBricks,\n  getLicense,\n  fixHostNotInXosanNetwork,\n  // TODO: uncomment when implementing subvolume deletion\n  // removeXosanBricks,\n  replaceXosanBrick,\n  startVm,\n  subscribePlugins,\n  subscribeVolumeInfo,\n} from 'xo'\n\nimport { INFO_TYPES } from '../xosan'\n\nimport ReplaceBrickModalBody from './replace-brick-modal'\nimport AddSubvolumeModalBody from './add-subvolume-modal'\n\nconst ISSUE_CODE_TO_MESSAGE = {\n  VMS_DOWN: 'xosanVmsNotRunning',\n  VMS_NOT_FOUND: 'xosanVmsNotFound',\n  FILES_NEED_HEALING: 'xosanFilesNeedHealing',\n  HOST_NOT_IN_NETWORK: 'xosanHostNotInNetwork',\n}\n\nconst BORDERS = {\n  border: 'solid 2px #ccc',\n  borderRadius: '5px',\n  borderTop: 'none',\n}\n\nconst Issues = ({ issues }) => (\n  <Container>\n    {map(issues, issue => (\n      <Row key={issue.key || issue.code} className='alert alert-danger mb-1' role='alert'>\n        <Col>\n          <Icon icon='error' /> <strong>{_(ISSUE_CODE_TO_MESSAGE[issue.code], issue.params)}</strong>\n          {issue.fix && (\n            <Tooltip content={issue.fix.title}>\n              <ActionButton btnStyle='danger' className='ml-1' handler={issue.fix.action} icon='fix' size='small'>\n                {_('xosanFixIssue')}\n              </ActionButton>\n            </Tooltip>\n          )}\n        </Col>\n      </Row>\n    ))}\n  </Container>\n)\n\nconst Field = ({ title, children }) => (\n  <SingleLineRow>\n    <Col size={3}>\n      <strong>{title}</strong>\n    </Col>\n    <Col size={9}>{children}</Col>\n  </SingleLineRow>\n)\n\n@connectStore({\n  srs: createGetObjectsOfType('SR'),\n  vms: createGetObjectsOfType('VM'),\n})\nclass Node extends Component {\n  _replaceBrick = async ({ brick, vm }) => {\n    const {\n      sr,\n      brickSize,\n      onSameVm = false,\n    } = await confirm({\n      icon: 'refresh',\n      title: _('xosanReplace'),\n      body: <ReplaceBrickModalBody vm={vm} />,\n    })\n\n    if (sr == null || brickSize == null) {\n      return error(_('xosanReplaceBrickErrorTitle'), _('xosanReplaceBrickErrorMessage'))\n    }\n\n    await replaceXosanBrick(this.props.sr, brick, sr, brickSize, onSameVm)\n  }\n\n  _getSizeUsage = createSelector(\n    () => this.props.node.statusDetail,\n    statusDetail => ({\n      used: String(Math.round(100 - (+statusDetail.sizeFree / +statusDetail.sizeTotal) * 100)),\n      free: formatSize(+statusDetail.sizeFree),\n    })\n  )\n\n  _getInodesUsage = createSelector(\n    () => this.props.node.statusDetail,\n    statusDetail => ({\n      used: String(Math.round(100 - (+statusDetail.inodesFree / +statusDetail.inodesTotal) * 100)),\n      free: formatSize(+statusDetail.inodesFree),\n    })\n  )\n\n  render() {\n    const { srs } = this.props\n    const { showAdvanced } = this.state\n\n    const { config, heal, size, status, statusDetail, uuid, vm } = this.props.node\n\n    return (\n      <Collapse\n        buttonText={\n          <span>\n            <Icon\n              color={heal ? (heal.status === 'Connected' ? 'text-success' : 'text-warning') : 'text-danger'}\n              icon='disk'\n            />{' '}\n            {srs[config.underlyingSr].name_label}\n          </span>\n        }\n        className='mb-1'\n      >\n        <div style={BORDERS}>\n          <Container className='p-1'>\n            <Field title={_('xosanVm')}>\n              {vm !== undefined ? (\n                <span>\n                  <Tooltip content={_(`powerState${vm.power_state}`)}>\n                    <Icon icon={vm.power_state.toLowerCase()} />\n                  </Tooltip>{' '}\n                  <Link to={`/vms/${config.vm.id}`}>{vm.name_label}</Link>\n                  {vm.power_state !== 'Running' && (\n                    <Tooltip content={_('xosanRun')}>\n                      <ActionButton handler={startVm} handlerParam={vm} icon='vm-start' size='small' />\n                    </Tooltip>\n                  )}\n                </span>\n              ) : (\n                <span style={{ color: 'red' }}>\n                  <Icon icon='alarm' /> {_('xosanCouldNotFindVm')}\n                </span>\n              )}\n            </Field>\n            <Field title={_('xosanUnderlyingStorage')}>\n              <Link to={`/srs/${config.underlyingSr}`}>{srs[config.underlyingSr].name_label}</Link>\n              {' - '}\n              {size != null && _('xosanUnderlyingStorageUsage', { usage: formatSize(size) })}\n            </Field>\n            <Field title={_('xosanStatus')}>{heal ? heal.status : 'unknown'}</Field>\n            {statusDetail && (\n              <Field title={_('xosanUsedSpace')}>\n                <span\n                  style={{\n                    display: 'inline-block',\n                    width: '20em',\n                    height: '1em',\n                  }}\n                >\n                  <Tooltip content={_('spaceLeftTooltip', this._getSizeUsage())}>\n                    <progress\n                      className='progress'\n                      max='100'\n                      value={100 - (+statusDetail.sizeFree / +statusDetail.sizeTotal) * 100}\n                    />\n                  </Tooltip>\n                </span>\n              </Field>\n            )}\n            {config.arbiter === 'True' && <Field title={_('xosanArbiter')} />}\n            <Row className='mt-1'>\n              <Col>\n                <ActionButton\n                  btnStyle='success'\n                  icon='refresh'\n                  handler={this._replaceBrick}\n                  handlerParam={{ brick: config.brickName, vm }}\n                >\n                  {_('xosanReplace')}\n                </ActionButton>\n              </Col>\n            </Row>\n            <Row className='mt-1'>\n              <Col>\n                <h3>\n                  <Toggle iconSize={1} onChange={this.toggleState('showAdvanced')} value={showAdvanced} />{' '}\n                  {_('xosanAdvanced')}\n                </h3>\n              </Col>\n            </Row>\n            {showAdvanced && [\n              <Field title={_('xosanBrickName')}>\n                <Copiable tagName='div'>{config.brickName}</Copiable>\n              </Field>,\n              <Field title={_('xosanBrickUuid')}>\n                <Copiable tagName='div'>{uuid}</Copiable>\n              </Field>,\n              <div>\n                {statusDetail && [\n                  <Field key='usedInodes' title={_('xosanUsedInodes')}>\n                    <span\n                      style={{\n                        display: 'inline-block',\n                        width: '20em',\n                        height: '1em',\n                      }}\n                    >\n                      <Tooltip content={_('spaceLeftTooltip', this._getInodesUsage())}>\n                        <progress\n                          className='progress'\n                          max='100'\n                          value={100 - (+statusDetail.inodesFree / +statusDetail.inodesTotal) * 100}\n                        />\n                      </Tooltip>\n                    </span>\n                  </Field>,\n                  <Field key='blockSize' title={_('xosanBlockSize')}>\n                    {statusDetail.blockSize}\n                  </Field>,\n                  <Field key='device' title={_('xosanDevice')}>\n                    {statusDetail.device}\n                  </Field>,\n                  <Field key='fsName' title={_('xosanFsName')}>\n                    {statusDetail.fsName}\n                  </Field>,\n                  <Field key='mountOptions' title={_('xosanMountOptions')}>\n                    {statusDetail.mntOptions}\n                  </Field>,\n                  <Field key='path' title={_('xosanPath')}>\n                    {statusDetail.path}\n                  </Field>,\n                ]}\n              </div>,\n              <div>\n                {status && status.length !== 0 && (\n                  <Row className='mt-1'>\n                    <Col>\n                      <table className='table' style={{ maxWidth: '50em' }}>\n                        <thead>\n                          <th>{_('xosanJob')}</th>\n                          <th>{_('xosanPath')}</th>\n                          <th>{_('xosanStatus')}</th>\n                          <th>{_('xosanPid')}</th>\n                          <th>{_('xosanPort')}</th>\n                        </thead>\n                        <tbody>\n                          {map(status, job => (\n                            <tr key={job.pid}>\n                              <td>{job.hostname}</td>\n                              <td>{job.path}</td>\n                              <td>{job.status}</td>\n                              <td>{job.pid}</td>\n                              <td>{job.port}</td>\n                            </tr>\n                          ))}\n                        </tbody>\n                      </table>\n                    </Col>\n                  </Row>\n                )}\n              </div>,\n              <div>\n                {heal && heal.file && heal.file.length !== 0 && (\n                  <div>\n                    <h4>{_('xosanFilesNeedingHealing')}</h4>\n                    {map(heal.file, file => (\n                      <Row key={file.gfid}>\n                        <Col size={5}>{file._}</Col>\n                        <Col size={4}>{file.gfid}</Col>\n                      </Row>\n                    ))}\n                  </div>\n                )}\n              </div>,\n            ]}\n          </Container>\n        </div>\n      </Collapse>\n    )\n  }\n}\n\n// -----------------------------------------------------------------------------\n\n@connectStore(() => ({\n  isAdmin,\n  vms: createGetObjectsOfType('VM'),\n  hosts: createGetObjectsOfType('host'),\n  vbds: createGetObjectsOfType('VBD'),\n  vdis: createGetObjectsOfType('VDI'),\n}))\n@addSubscriptions(({ sr }) => {\n  const subscriptions = {}\n  forEach(INFO_TYPES, infoType => {\n    subscriptions[`${infoType}_`] = cb => subscribeVolumeInfo({ sr, infoType }, cb)\n  })\n\n  subscriptions.plugins = subscribePlugins\n\n  return subscriptions\n})\nexport default class TabXosan extends Component {\n  componentDidMount() {\n    const { id } = this.props.sr\n\n    getLicense('xosan', id)\n      .catch(() => getLicense('xosan.trial', id))\n      .then(\n        license => this.setState({ license }),\n        error => this.setState({ licenseError: error })\n      )\n  }\n\n  _addSubvolume = async () => {\n    const { srs, brickSize } = await confirm({\n      icon: 'add',\n      title: _('xosanAddSubvolume'),\n      body: <AddSubvolumeModalBody sr={this.props.sr} subvolumeSize={this._getSubvolumeSize()} />,\n    })\n\n    if (brickSize == null || (srs && srs.length) !== this._getSubvolumeSize()) {\n      return error(\n        _('xosanAddSubvolumeErrorTitle'),\n        _('xosanAddSubvolumeErrorMessage', { nSrs: this._getSubvolumeSize() })\n      )\n    }\n\n    return this._addBricks({ srs, brickSize })\n  }\n\n  // TODO: uncomment when implementing subvolume deletion\n  // async _removeSubVolume (bricks) {\n  //   await removeXosanBricks(this.props.sr.id, bricks)\n  // }\n\n  async _addBricks({ srs, brickSize }) {\n    await addXosanBricks(\n      this.props.sr.id,\n      srs.map(sr => sr.id),\n      brickSize\n    )\n  }\n\n  _getStrippedVolumeInfo = createSelector(\n    () => this.props.info_,\n    info => (info && info.commandStatus ? info.result : null)\n  )\n\n  _getSubvolumeSize = createSelector(this._getStrippedVolumeInfo, strippedVolumeInfo =>\n    strippedVolumeInfo ? +strippedVolumeInfo.disperseCount || +strippedVolumeInfo.replicaCount : null\n  )\n\n  // TODO: uncomment when implementing subvolume deletion\n  // _getSubvolumes = createSelector(\n  //   this._getStrippedVolumeInfo,\n  //   this._getSubvolumeSize,\n  //   (strippedVolumeInfo, subvolumeSize) => {\n  //     const subVolumes = []\n  //     if (strippedVolumeInfo) {\n  //       for (let i = 0; i < strippedVolumeInfo.bricks.length; i += subvolumeSize) {\n  //         subVolumes.push(strippedVolumeInfo.bricks.slice(i, i + subvolumeSize))\n  //       }\n  //     }\n  //\n  //     return subVolumes\n  //   }\n  // )\n\n  _getMissingXoaPlugin = createSelector(\n    () => this.props.plugins,\n    plugins => {\n      if (plugins === undefined) {\n        return _('xosanInstallXoaPlugin')\n      }\n\n      const xoaPlugin = find(plugins, { id: 'xoa' })\n      if (xoaPlugin === undefined) {\n        return _('xosanInstallXoaPlugin')\n      }\n\n      if (!xoaPlugin.loaded) {\n        return _('xosanLoadXoaPlugin')\n      }\n    }\n  )\n\n  _getConfig = createSelector(\n    () => this.props.sr && this.props.sr.other_config['xo:xosan_config'],\n    otherConfig => (otherConfig ? JSON.parse(otherConfig) : null)\n  )\n\n  _getBrickByName = createSelector(\n    this._getConfig,\n    () => this.props.vms,\n    () => this.props.vdis,\n    () => this.props.vbds,\n    () => this.props.heal_,\n    () => this.props.status_,\n    () => this.props.statusDetail_,\n    this._getStrippedVolumeInfo,\n    (xosanConfig, vms, vdis, vbds, heal, status, statusDetail, strippedVolumeInfo) => {\n      const nodes = xosanConfig && xosanConfig.nodes\n\n      const brickByName = {}\n      forEach(nodes, node => {\n        const vm = vms[node.vm.id]\n\n        brickByName[node.brickName] = {\n          config: node,\n          uuid: '-',\n          size: isEmpty(vm && vm.$VBDs)\n            ? null\n            : sum(\n                map(vm.$VBDs, vbdId => {\n                  const vdi = vdis[vbds[vbdId].VDI]\n                  return vdi === undefined ? 0 : vdi.size\n                })\n              ),\n          vm,\n        }\n      })\n\n      const brickByUuid = {}\n      if (strippedVolumeInfo) {\n        forEach(strippedVolumeInfo.bricks, brick => {\n          brickByName[brick.name] = brickByName[brick.name] || {}\n          brickByName[brick.name].info = brick\n          brickByName[brick.name].uuid = brick.hostUuid\n          brickByUuid[brick.hostUuid] = brickByUuid[brick.hostUuid] || brickByName[brick.name]\n        })\n      }\n\n      if (heal && heal.commandStatus) {\n        forEach(heal.result.bricks, brick => {\n          brickByName[brick.name] = brickByName[brick.name] || {}\n          brickByName[brick.name].heal = brick\n          brickByName[brick.name].uuid = brick.hostUuid\n          brickByUuid[brick.hostUuid] = brickByUuid[brick.hostUuid] || brickByName[brick.name]\n        })\n      }\n\n      if (status && status.commandStatus) {\n        forEach(brickByUuid, (brick, uuid) => {\n          brick.status = status.result.nodes[uuid]\n        })\n      }\n\n      if (statusDetail && statusDetail.commandStatus) {\n        forEach(brickByUuid, (brick, uuid) => {\n          if (uuid in statusDetail.result.nodes) {\n            brick.statusDetail = statusDetail.result.nodes[uuid][0]\n          }\n        })\n      }\n\n      return brickByName\n    }\n  )\n\n  _getOrderedBrickList = createSelector(this._getConfig, this._getBrickByName, (xosanConfig, brickByName) => {\n    if (!xosanConfig || !xosanConfig.nodes) {\n      return\n    }\n\n    return map(xosanConfig.nodes, node => brickByName[node.brickName])\n  })\n\n  _getIssues = createSelector(\n    this._getOrderedBrickList,\n    () => this.props.hosts_,\n    () => this.props.hosts,\n    () => this.props.sr,\n    (orderedBrickList, hosts_, hosts, sr) => {\n      if (orderedBrickList == null) {\n        return\n      }\n\n      const issues = []\n      if (\n        reduce(\n          orderedBrickList,\n          (hasStopped, node) => hasStopped || (node.vm && node.vm.power_state !== 'Running'),\n          false\n        )\n      ) {\n        issues.push({ code: 'VMS_DOWN' })\n      }\n\n      if (reduce(orderedBrickList, (hasNotFound, node) => hasNotFound || node.vm === undefined, false)) {\n        issues.push({ code: 'VMS_NOT_FOUND' })\n      }\n\n      if (\n        reduce(\n          orderedBrickList,\n          (hasFileToHeal, node) => hasFileToHeal || (node.heal && node.heal.file && node.heal.file.length !== 0),\n          false\n        )\n      ) {\n        issues.push({ code: 'FILES_NEED_HEALING' })\n      }\n\n      forEach(hosts_, ({ host }) => {\n        issues.push({\n          code: 'HOST_NOT_IN_NETWORK',\n          key: 'HOST_NOT_IN_NETWORK' + host,\n          params: { hostName: hosts[host].name_label },\n          fix: {\n            action: () => fixHostNotInXosanNetwork(sr.id, host),\n            title: _('xosanIssueHostNotInNetwork'),\n          },\n        })\n      })\n\n      return issues\n    }\n  )\n\n  render() {\n    const { license, licenseError, showAdvanced } = this.state\n    const { heal_, info_, sr, status_, statusDetail_, vbds, vdis, isAdmin } = this.props\n\n    const missingXoaPlugin = this._getMissingXoaPlugin()\n    if (missingXoaPlugin !== undefined) {\n      return <em>{missingXoaPlugin}</em>\n    }\n\n    const xosanConfig = this._getConfig()\n    if ((license === undefined && licenseError === undefined) || xosanConfig === undefined) {\n      return <em>{_('statusLoading')}</em>\n    }\n\n    if (licenseError !== undefined && licenseError.message !== 'No license found') {\n      return <span className='text-danger'>{_('xosanCheckLicenseError')}</span>\n    }\n\n    if (\n      licenseError !== undefined ||\n      (license !== undefined && license.productId !== 'xosan' && license.productId !== 'xosan.trial')\n    ) {\n      return (\n        <span className='text-danger'>\n          {_('xosanAdminNoLicenseDisclaimer')} {isAdmin && <Link to='/xoa/licenses'>{_('licensesManage')}</Link>}\n        </span>\n      )\n    }\n\n    if (license.expires < Date.now()) {\n      return (\n        <span className='text-danger'>\n          {_('xosanAdminExpiredLicenseDisclaimer')} {isAdmin && <Link to='/xoa/licenses'>{_('licensesManage')}</Link>}\n        </span>\n      )\n    }\n\n    if (!xosanConfig.version) {\n      return <div>{_('xosanWarning')}</div>\n    }\n\n    const strippedVolumeInfo = this._getStrippedVolumeInfo()\n    // const subVolumes = this._getSubvolumes() // TODO: uncomment when implementing subvolume deletion\n    const orderedBrickList = this._getOrderedBrickList()\n\n    return (\n      <Container>\n        <Row className='text-xs-center mb-1 mt-1'>\n          <Col size={3}>\n            <h2>\n              <Icon\n                icon='sr'\n                size='lg'\n                color={status_ ? (status_.commandStatus ? 'text-success' : status_.error) : 'text-info'}\n              />\n            </h2>\n          </Col>\n          <Col size={3}>\n            <h2>\n              <Icon\n                icon='health'\n                size='lg'\n                color={heal_ ? (heal_.commandStatus ? 'text-success' : heal_.error) : 'text-info'}\n              />\n            </h2>\n          </Col>\n          <Col size={3}>\n            <h2>\n              <Icon\n                icon='settings'\n                size='lg'\n                color={\n                  statusDetail_ ? (statusDetail_.commandStatus ? 'text-success' : statusDetail_.error) : 'text-info'\n                }\n              />\n            </h2>\n          </Col>\n          <Col size={3}>\n            <h2>\n              <Icon\n                icon='info'\n                size='lg'\n                color={info_ ? (info_.commandStatus ? 'text-success' : info_.error) : 'text-info'}\n              />\n            </h2>\n          </Col>\n        </Row>\n        <Row className='mb-1'>\n          <Col>\n            <Issues issues={this._getIssues()} />\n          </Col>\n        </Row>\n        {map(orderedBrickList, node => (\n          <Row key={node.config.brickName}>\n            <Col>\n              <Node\n                heal_={heal_}\n                info_={info_}\n                node={node}\n                sr={sr}\n                status_={status_}\n                statusDetail_={statusDetail_}\n                vbds={vbds}\n                vdis={vdis}\n              />\n            </Col>\n          </Row>\n        ))}\n        <Row>\n          <Col>\n            <ActionButton btnStyle='success' handler={this._addSubvolume} icon='add'>\n              {_('xosanAddSubvolume')}\n            </ActionButton>\n            <hr />\n          </Col>\n        </Row>\n        {/* We will implement this later */}\n        {/* <Row>\n        <Col>\n          <h2>{_('xosanRemoveSubvolumes')}</h2>\n          <table className='table'>\n            {map(subVolumes, (subvolume, i) => <tr key={i}>\n              <td>\n                <ul>{map(subvolume, (brick, j) => <li key={j}>{brick.name}</li>)}</ul>\n              </td>\n              <td>\n                <ActionButton\n                  btnStyle='danger'\n                  icon='remove'\n                  handler={::this._removeSubVolume}\n                  handlerParam={map(subvolume, brick => brick.name)}\n                >\n                  {_('xosanRemove')}\n                </ActionButton>\n              </td>\n            </tr>)}\n          </table>\n          <hr />\n        </Col>\n      </Row> */}\n        <Row>\n          <Col>\n            <h2>\n              <Toggle iconSize={1} onChange={this.toggleState('showAdvanced')} value={showAdvanced} />{' '}\n              {_('xosanAdvanced')}\n            </h2>\n            {strippedVolumeInfo && showAdvanced && (\n              <div>\n                <h3>{_('xosanVolume')}</h3>\n                <Container>\n                  <Field title='Name'>{strippedVolumeInfo.name}</Field>\n                  <Field title='Status'>{strippedVolumeInfo.statusStr}</Field>\n                  <Field title='Type'>{strippedVolumeInfo.typeStr}</Field>\n                  <Field title='Brick Count'>{strippedVolumeInfo.brickCount}</Field>\n                  <Field title='Stripe Count'>{strippedVolumeInfo.stripeCount}</Field>\n                  <Field title='Replica Count'>{strippedVolumeInfo.replicaCount}</Field>\n                  <Field title='Arbiter Count'>{strippedVolumeInfo.arbiterCount}</Field>\n                  <Field title='Disperse Count'>{strippedVolumeInfo.disperseCount}</Field>\n                  <Field title='Redundancy Count'>{strippedVolumeInfo.redundancyCount}</Field>\n                </Container>\n                <h3 className='mt-1'>{_('xosanVolumeOptions')}</h3>\n                <Container>\n                  {map(strippedVolumeInfo.options, option => (\n                    <Field key={option.name} title={option.name}>\n                      {option.value}\n                    </Field>\n                  ))}\n                </Container>\n              </div>\n            )}\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport copy from 'copy-to-clipboard'\nimport Icon from 'icon'\nimport PifsColumn from 'sorted-table/pifs-column'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport { addSubscriptions, connectStore, TryXoa } from 'utils'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { Container, Row, Col } from 'grid'\nimport { createCollectionWrapper, createSelector, createGetObjectsOfType } from 'selectors'\nimport {\n  createXostorInterface,\n  destroyXostorInterfaces,\n  setXostor,\n  subscribeXostorHealthCheck,\n  subscribeXostorInterfaces,\n} from 'xo'\nimport { find } from 'lodash'\nimport { generateId } from 'reaclette-utils'\nimport { getXoaPlan, SOURCES } from 'xoa-plans'\nimport { Host, Vdi } from 'render-xo-item'\nimport { injectState } from 'reaclette'\n\nconst RESOURCE_COLUMNS = [\n  {\n    name: 'Resource name',\n    itemRenderer: ({ resourceName }) => resourceName,\n    sortCriteria: ({ resourceName }) => resourceName,\n  },\n  {\n    name: _('node'),\n    itemRenderer: ({ host }) => <Host id={host.id} link />,\n    sortCriteria: ({ host }) => host.name_label,\n  },\n  {\n    name: _('nodeStatus'),\n    itemRenderer: ({ nodeStatus }) => nodeStatus,\n    sortCriteria: ({ nodeStatus }) => nodeStatus,\n  },\n  {\n    name: _('vdi'),\n    itemRenderer: ({ vdiId }) => vdiId !== '' && <Vdi id={vdiId} />,\n  },\n  {\n    name: _('inUse'),\n    itemRenderer: ({ inUse }) => <Icon icon={String(inUse)} />,\n    sortCriteria: ({ inUse }) => inUse,\n  },\n  {\n    name: _('state'),\n    itemRenderer: ({ resourceState }) => resourceState,\n    sortCriteria: ({ resourceState }) => resourceState,\n  },\n  {\n    name: _('diskState'),\n    itemRenderer: ({ volume }) => volume['disk-state'],\n    sortCriteria: ({ volume }) => volume['disk-state'],\n  },\n]\n\nconst INTERFACES_COLUMNS = [\n  {\n    name: _('name'),\n    itemRenderer: iface => iface.name,\n    sortCriteria: iface => iface.name,\n  },\n  {\n    name: _('pifs'),\n    itemRenderer: (iface, { pifIdsByIfaceName }) => <PifsColumn pifs={pifIdsByIfaceName[iface.name]} />,\n  },\n]\n\n@connectStore({\n  hostByHostname: createGetObjectsOfType('host')\n    .filter((_, props) => host => host.$pool === props.sr.$pool)\n    .groupBy('hostname'),\n  pifs: createGetObjectsOfType('PIF').filter((_, props) => pif => pif.$pool === props.sr.$pool),\n})\n@addSubscriptions(({ sr }) => ({\n  healthCheck: subscribeXostorHealthCheck(sr),\n  interfaces: subscribeXostorInterfaces(sr),\n}))\n@injectState\nexport default class TabXostor extends Component {\n  _actions = [\n    {\n      handler: ifaces =>\n        destroyXostorInterfaces(\n          this.props.sr,\n          ifaces.map(iface => iface.name)\n        ),\n      icon: 'delete',\n      label: _('delete'),\n      level: 'danger',\n      individualDisabled: ifaces => ifaces[0].name === 'default',\n    },\n  ]\n\n  _individualActions = [\n    {\n      handler: iface => setXostor(this.props.sr, { preferredInterface: iface.name }),\n      icon: 'favorite',\n      label: _('setAsPreferred'),\n      level: 'primary',\n    },\n  ]\n\n  _individualActionsResourceList = [\n    {\n      handler: ({ vdiId }) => copy(vdiId),\n      icon: 'clipboard',\n      label: _('copyToClipboardVdiUuid'),\n      level: 'secondary',\n      disabled: ({ vdiId }) => vdiId === '',\n    },\n  ]\n\n  getComputedIfaces = createCollectionWrapper(\n    createSelector(\n      () => this.props.interfaces,\n      ifaces => {\n        if (ifaces === undefined) {\n          return {}\n        }\n        const computedIfaces = {}\n        for (const ifaceName in ifaces) {\n          computedIfaces[ifaceName] = {\n            id: generateId(),\n            name: ifaceName,\n            nodeIfaces: ifaces[ifaceName],\n          }\n        }\n        return computedIfaces\n      }\n    )\n  )\n\n  getPifsByIfaceName = createCollectionWrapper(\n    createSelector(\n      () => this.props.interfaces,\n      () => this.props.pifs,\n      (ifaces, pifs) => {\n        if (ifaces === undefined) {\n          return {}\n        }\n        const pifsByIfaceName = {}\n        for (const ifaceName in ifaces) {\n          pifsByIfaceName[ifaceName] = ifaces[ifaceName].map(\n            iface => find(pifs, pif => pif.ip === iface.address || pif.ipv6 === iface.address)?.id\n          )\n        }\n        return pifsByIfaceName\n      }\n    )\n  )\n\n  getResourceInfos = createSelector(\n    () => this.props.healthCheck,\n    healthCheck => {\n      // healthCheck.resources can be undefined if the user use an old version of the `linstor-manager` plugin\n      if (healthCheck?.resources === undefined) {\n        return []\n      }\n\n      const resourceNames = Object.keys(healthCheck.resources).filter(\n        // nodes can be undefined if the user use an old version of the `linstor-manager` plugin\n        resourceName => healthCheck.resources[resourceName].nodes !== undefined\n      )\n\n      return resourceNames.flatMap(resourceName =>\n        Object.entries(healthCheck.resources[resourceName].nodes).reduce((acc, [hostname, nodeInfo]) => {\n          const volume = nodeInfo.volumes[0] // Max only one volume\n          if (volume !== undefined) {\n            const nodeStatus = healthCheck.nodes[hostname]\n            const host = this.props.hostByHostname[hostname][0]\n\n            const resourceState = _(\n              `${nodeInfo['tie-breaker'] ? 'tieBreaker' : nodeInfo.diskful ? 'diskful' : 'diskless'}`\n            )\n            acc.push({\n              inUse: nodeInfo['in-use'],\n              vdiId: healthCheck.resources[resourceName].uuid,\n              volume,\n              nodeStatus,\n              host,\n              resourceName,\n              resourceState,\n            })\n          }\n          return acc\n        }, [])\n      )\n    }\n  )\n\n  getXostorLicenseInfo = createSelector(\n    () => this.props.state.xostorLicenseInfoByXostorId,\n    () => this.props.sr,\n    (xostorLicenseInfoByXostorId, sr) => xostorLicenseInfoByXostorId?.[sr.id]\n  )\n\n  render() {\n    if (getXoaPlan() === SOURCES) {\n      return (\n        <Container>\n          <h2 className='text-info'>{_('xostorAvailableInXoa')}</h2>\n          <p>\n            <TryXoa page='xostor' />\n          </p>\n        </Container>\n      )\n    }\n\n    const resourceInfos = this.getResourceInfos()\n    const xostorLicenseInfo = this.getXostorLicenseInfo()\n\n    if (xostorLicenseInfo === undefined) {\n      return _('statusLoading')\n    }\n\n    if (!xostorLicenseInfo.supportEnabled) {\n      return (\n        <div>\n          <p>{_('manageXostorWarning')}</p>\n          <ul>\n            {xostorLicenseInfo.alerts\n              .filter(alert => alert.level === 'danger')\n              .map((alert, index) => (\n                <li key={index} className='text-danger'>\n                  {alert.render}\n                </li>\n              ))}\n          </ul>\n        </div>\n      )\n    }\n\n    return (\n      <Container>\n        <Row>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='disk' /> {_('resourceList')}\n              </CardHeader>\n              <CardBlock>\n                <SortedTable\n                  collection={resourceInfos}\n                  columns={RESOURCE_COLUMNS}\n                  stateUrlParam='r'\n                  individualActions={this._individualActionsResourceList}\n                />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <Card>\n              <CardHeader>\n                <Icon icon='network' /> {_('interfaces')}\n              </CardHeader>\n              <CardBlock>\n                <ActionButton\n                  btnStyle='primary'\n                  handler={createXostorInterface}\n                  handlerParam={this.props.sr}\n                  icon='add'\n                >\n                  {_('createInterface')}\n                </ActionButton>\n                <SortedTable\n                  actions={this._actions}\n                  collection={this.getComputedIfaces()}\n                  columns={INTERFACES_COLUMNS}\n                  data-pifIdsByIfaceName={this.getPifsByIfaceName()}\n                  individualActions={this._individualActions}\n                  stateUrlParam='s'\n                />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _, { FormattedDuration, messages } from 'intl'\nimport Collapse from 'collapse'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport Link from 'link'\nimport React from 'react'\nimport renderXoItem, { Pool, renderXoItemFromId } from 'render-xo-item'\nimport SortedTable from 'sorted-table'\nimport TASK_STATUS from 'task-status'\nimport Tooltip from 'tooltip'\nimport { addSubscriptions, connectStore, NumericDate, resolveIds } from 'utils'\nimport { FormattedRelative, injectIntl } from 'react-intl'\nimport { SelectPool } from 'select-objects'\nimport { Col, Container, Row } from 'grid'\nimport { differenceBy, isEmpty, map, some } from 'lodash'\nimport {\n  createFilter,\n  createGetObject,\n  createGetObjectsOfType,\n  createSelector,\n  getResolvedPendingTasks,\n  isAdmin,\n} from 'selectors'\nimport {\n  abortXoTask,\n  cancelTask,\n  cancelTasks,\n  deleteXoTaskLog,\n  destroyTask,\n  destroyTasks,\n  subscribePermissions,\n  subscribeXoTasks,\n} from 'xo'\n\nimport Page from '../page'\n\nconst HEADER = (\n  <Container>\n    <Row>\n      <Col mediumSize={12}>\n        <h2>\n          <Icon icon='task' /> {_('taskMenu')}\n        </h2>\n      </Col>\n    </Row>\n  </Container>\n)\n\nconst Ul = props => <ul {...props} style={{ listStyleType: 'none' }} />\nconst Li = props => (\n  <li\n    {...props}\n    style={{\n      whiteSpace: 'nowrap',\n    }}\n  />\n)\n\nconst TASK_ITEM_STYLE = {\n  // Remove all margin; otherwise, it breaks vertical alignment.\n  margin: 0,\n}\n\nconst FILTERS = {\n  filterOutShortTasks: '!name_label: |(SR.scan host.call_plugin \"/rrd_updates\")',\n  filterKeepFailed: 'status:failure',\n}\n\n@connectStore(() => ({\n  host: createGetObject((_, props) => props.item.$host),\n  appliesTo: createGetObject((_, props) => props.item.applies_to),\n}))\nexport class TaskItem extends Component {\n  render() {\n    const { appliesTo, host, item: task } = this.props\n    // garbage collection task has an uuid in the desc\n    const showDesc = task.name_description && task.name_label !== 'Garbage Collection'\n    return (\n      <div>\n        {task.name_label} ({showDesc && `${task.name_description} `}\n        on {host ? <Link to={`/hosts/${host.id}`}>{host.name_label}</Link> : `unknown host − ${task.$host}`})\n        {appliesTo !== undefined && (\n          <span>\n            , applies to <Link to={`/srs/${appliesTo.id}`}>{appliesTo.name_label}</Link>\n          </span>\n        )}\n        {task.disappeared === undefined && ` ${Math.round(task.progress * 100)}%`}\n      </div>\n    )\n  }\n}\n\nconst taskObjectsRenderer = ({ objects }) => (\n  <Ul>\n    {map(objects, obj => {\n      const { id, type } = obj\n      return type === 'VDI' || type === 'network' ? (\n        <Li key={id}>{renderXoItem(obj)}</Li>\n      ) : (\n        <Li key={id}>\n          <Link to={`/${type}s/${id}`}>{renderXoItem(obj)}</Link>\n        </Li>\n      )\n    })}\n  </Ul>\n)\n\nconst COMMON = [\n  {\n    component: TaskItem,\n    name: _('task'),\n    sortCriteria: 'name_label',\n  },\n  {\n    itemRenderer: taskObjectsRenderer,\n    name: _('objects'),\n  },\n]\n\nconst COLUMNS = [\n  {\n    itemRenderer: ({ $poolId }) => <Pool id={$poolId} link />,\n    name: _('pool'),\n    sortCriteria: (task, userData) => {\n      const pool = userData.pools[task.$poolId]\n      return pool !== undefined && pool.name_label\n    },\n  },\n  ...COMMON,\n  {\n    itemRenderer: task => (\n      <progress style={TASK_ITEM_STYLE} className='progress' value={task.progress * 100} max='100' />\n    ),\n    name: _('progress'),\n    sortCriteria: 'progress',\n  },\n  {\n    default: true,\n    itemRenderer: task => <FormattedRelative value={task.created * 1000} />,\n    name: _('taskStarted'),\n    sortCriteria: 'created',\n    sortOrder: 'desc',\n  },\n  {\n    itemRenderer: task => {\n      const started = task.created * 1000\n      const { progress } = task\n\n      const elapsed = Date.now() - started\n      if (progress === 0 || progress === 1 || elapsed < 10e3) {\n        return // not yet started, already finished or too early to estimate end\n      }\n\n      return <FormattedRelative value={started + elapsed / progress} />\n    },\n    name: _('taskEstimatedEnd'),\n  },\n]\n\nconst FINISHED_TASKS_COLUMNS = [\n  {\n    itemRenderer: ({ $poolId }) => <Pool id={$poolId} link />,\n    name: _('pool'),\n  },\n  ...COMMON,\n  {\n    default: true,\n    itemRenderer: task => <NumericDate timestamp={task.disappeared} />,\n    name: _('taskLastSeen'),\n    sortCriteria: task => task.disappeared,\n    sortOrder: 'desc',\n  },\n]\n\nconst XO_TASKS_COLUMNS = [\n  {\n    itemRenderer: task => task.properties?.name ?? task.name,\n    name: _('name'),\n  },\n  {\n    itemRenderer: task => {\n      const { objectId } = task.properties ?? task\n      return objectId === undefined ? null : renderXoItemFromId(task.objectId, { link: true })\n    },\n    name: _('object'),\n  },\n  {\n    itemRenderer: task => {\n      const progress = task.properties?.progress\n\n      return progress === undefined ? null : (\n        <progress style={TASK_ITEM_STYLE} className='progress' value={progress} max='100' />\n      )\n    },\n    name: _('progress'),\n    sortCriteria: 'progress',\n  },\n  {\n    default: true,\n    itemRenderer: task => (task.start === undefined ? null : <NumericDate timestamp={task.start} />),\n    name: _('taskStarted'),\n    sortCriteria: 'start',\n    sortOrder: 'desc',\n  },\n  {\n    itemRenderer: task => (task.end === undefined ? null : <FormattedDuration duration={task.end - task.start} />),\n    name: _('taskDuration'),\n    sortCriteria: task => task.end - task.start,\n    sortOrder: 'desc',\n  },\n  {\n    itemRenderer: task => {\n      const { icon, label } = TASK_STATUS[task.status] ?? TASK_STATUS.unknown\n      return (\n        <Tooltip content={_(label)}>\n          <Icon icon={icon} />\n        </Tooltip>\n      )\n    },\n    name: _('status'),\n    sortCriteria: 'status',\n  },\n]\n\nconst isNotCancelable = task => !task.allowedOperations.includes('cancel')\nconst isNotDestroyable = task => !task.allowedOperations.includes('destroy')\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    disabled: isNotCancelable,\n    handler: cancelTask,\n    icon: 'task-cancel',\n    label: _('cancelTask'),\n    level: 'danger',\n  },\n  {\n    disabled: isNotDestroyable,\n    handler: destroyTask,\n    icon: 'task-destroy',\n    label: _('destroyTask'),\n    level: 'danger',\n  },\n]\n\nconst XO_TASKS_ACTIONS = [\n  {\n    handler: tasks => Promise.all(tasks.map(deleteXoTaskLog)),\n    icon: 'task-destroy',\n    label: _('taskDeleteLog'),\n    level: 'warning',\n  },\n]\n\nconst XO_TASKS_INDIVIDUAL_ACTIONS = [\n  {\n    handler: task => window.open(task.href),\n    icon: 'api',\n    label: _('taskOpenRawLog'),\n  },\n  {\n    disabled: task => !(task.status === 'pending' && task.abortionRequestedAt === undefined),\n    handler: abortXoTask,\n    icon: 'task-cancel',\n    label: _('cancelTask'),\n    level: 'danger',\n  },\n]\n\nconst GROUPED_ACTIONS = [\n  {\n    disabled: tasks => some(tasks, isNotCancelable),\n    handler: cancelTasks,\n    icon: 'task-cancel',\n    label: _('cancelTasks'),\n    level: 'danger',\n  },\n  {\n    disabled: tasks => some(tasks, isNotDestroyable),\n    handler: destroyTasks,\n    icon: 'task-destroy',\n    label: _('destroyTasks'),\n    level: 'danger',\n  },\n]\n\n@addSubscriptions({\n  permissions: subscribePermissions,\n  xoTasks: subscribeXoTasks,\n})\n@connectStore(() => {\n  const getPools = createGetObjectsOfType('pool').pick(\n    createSelector(getResolvedPendingTasks, resolvedPendingTasks => resolvedPendingTasks.map(task => task.$poolId))\n  )\n\n  return (state, props) => {\n    // true: useResourceSet to bypass permissions\n    const resolvedPendingTasks = getResolvedPendingTasks(state, props, true)\n    return {\n      isAdmin: isAdmin(state, props),\n      nResolvedTasks: resolvedPendingTasks.length,\n      pools: getPools(state, props, true),\n      resolvedPendingTasks,\n    }\n  }\n})\n@injectIntl\nexport default class Tasks extends Component {\n  state = {\n    finishedTasks: [],\n  }\n\n  componentWillReceiveProps(props) {\n    const finishedTasks = differenceBy(this.props.resolvedPendingTasks, props.resolvedPendingTasks, 'id')\n    if (!isEmpty(finishedTasks)) {\n      this.setState({\n        finishedTasks: finishedTasks\n          .map(task => ({ ...task, disappeared: Date.now() }))\n          .concat(this.state.finishedTasks),\n      })\n    }\n  }\n\n  _getPoolFilter = createSelector(\n    createSelector(() => this.state.pools, resolveIds),\n    poolIds => (isEmpty(poolIds) ? null : ({ $poolId }) => poolIds.includes($poolId))\n  )\n\n  _getTasks = createFilter(() => this.props.resolvedPendingTasks, this._getPoolFilter)\n\n  _getFinishedTasks = createFilter(() => this.state.finishedTasks, this._getPoolFilter)\n\n  _getItemsPerPageContainer = () => this.state.itemsPerPageContainer\n\n  render() {\n    const { props } = this\n    const { intl, nResolvedTasks, pools } = props\n    const { formatMessage } = intl\n\n    return (\n      <Page header={HEADER} title={`(${nResolvedTasks}) ${formatMessage(messages.taskPage)}`}>\n        <h2>{_('poolTasks')}</h2>\n        <Container>\n          <Row className='mb-1'>\n            <Col mediumSize={7}>\n              <SelectPool multi onChange={this.linkState('pools')} />\n            </Col>\n            <Col mediumSize={4}>\n              <div ref={container => this.setState({ filterContainer: container })} />\n            </Col>\n            <Col mediumSize={1}>\n              <div ref={container => this.setState({ itemsPerPageContainer: container })} />\n            </Col>\n          </Row>\n          <Row>\n            <Col>\n              <SortedTable\n                collection={this._getTasks()}\n                columns={COLUMNS}\n                defaultFilter='filterOutShortTasks'\n                filterContainer={() => this.state.filterContainer}\n                filters={FILTERS}\n                itemsPerPageContainer={() => this.state.itemsPerPageContainer}\n                groupedActions={GROUPED_ACTIONS}\n                individualActions={INDIVIDUAL_ACTIONS}\n                stateUrlParam='s'\n                userData={{ pools }}\n              />\n            </Col>\n          </Row>\n          <Row>\n            <Col>\n              <Collapse buttonText={_('previousTasks')}>\n                <SortedTable\n                  className='mt-1'\n                  collection={this._getFinishedTasks()}\n                  columns={FINISHED_TASKS_COLUMNS}\n                  filters={FILTERS}\n                  stateUrlParam='s_previous'\n                />\n              </Collapse>\n            </Col>\n          </Row>\n        </Container>\n        <h2 className='mt-2'>{_('xoTasks')}</h2>\n        <Container>\n          <Row>\n            <Col>\n              <SortedTable\n                actions={XO_TASKS_ACTIONS}\n                collection={props.xoTasks}\n                columns={XO_TASKS_COLUMNS}\n                individualActions={XO_TASKS_INDIVIDUAL_ACTIONS}\n                stateUrlParam='s_xo'\n                filters={FILTERS}\n                defaultFilter='filterKeepFailed'\n              />\n            </Col>\n          </Row>\n        </Container>\n      </Page>\n    )\n  }\n}\n","import * as FormGrid from 'form-grid'\nimport _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport Copiable from 'copiable'\nimport homeFilters from 'home-filters'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SelectFiles from 'select-files'\nimport SortedTable from 'sorted-table'\nimport Tooltip from 'tooltip'\nimport { Text } from 'editable'\nimport { alert, confirm } from 'modal'\nimport { Container, Row, Col } from 'grid'\nimport { error, success } from 'notification'\nimport { getLang } from 'selectors'\nimport { isEmpty, map } from 'lodash'\nimport { injectIntl } from 'react-intl'\nimport { Select } from 'form'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { addSubscriptions, connectStore, noop, NumericDate } from 'utils'\nimport {\n  addAuthToken,\n  addSshKey,\n  changePassword,\n  deleteAuthToken,\n  deleteAuthTokens,\n  deleteSshKey,\n  deleteSshKeys,\n  editAuthToken,\n  editXsCredentials,\n  editCustomFilter,\n  removeCustomFilter,\n  setDefaultHomeFilter,\n  signOutFromEverywhereElse,\n  subscribeUserAuthTokens,\n  subscribeCurrentUser,\n} from 'xo'\n\nimport Page from '../page'\nimport Otp from './otp'\n\n// ===================================================================\n\nconst HEADER = (\n  <Container>\n    <Row>\n      <Col>\n        <h2>\n          <Icon icon='user' /> {_('userPage')}\n        </h2>\n      </Col>\n    </Row>\n  </Container>\n)\n\n// ===================================================================\n\nconst FILTER_TYPE_TO_LABEL_ID = {\n  host: 'homeTypeHost',\n  pool: 'homeTypePool',\n  VM: 'homeTypeVm',\n  'VM-template': 'homeTypeVmTemplate',\n  SR: 'homeTypeSr',\n}\n\nconst SSH_KEY_STYLE = { wordWrap: 'anywhere' }\n\nconst getDefaultFilter = (defaultFilters, type) => {\n  if (defaultFilters == null) {\n    return ''\n  }\n\n  return defaultFilters[type] || ''\n}\n\nconst getUserPreferences = user => user.preferences || {}\n\n// ===================================================================\n\n@addSubscriptions({\n  user: subscribeCurrentUser,\n})\nclass XsClientId extends Component {\n  async editXsCredentials(file) {\n    if (file === undefined) {\n      error(_('noFileSelected'))\n      return\n    }\n\n    try {\n      await new Promise((resolve, reject) => {\n        const fr = new window.FileReader()\n        fr.onload = event => {\n          try {\n            const { username, apikey } = JSON.parse(event.target.result)\n            if (username === undefined || apikey === undefined) {\n              reject(new Error('Could not find username and apikey in file'))\n            }\n\n            editXsCredentials({ username, apikey }).then(resolve, reject)\n          } catch (err) {\n            reject(err)\n          }\n        }\n        fr.readAsText(file)\n      })\n      success(_('setXsCredentialsSuccess'))\n    } catch (err) {\n      error(_('setXsCredentialsError'), err.message)\n    }\n  }\n\n  async deleteXsCredentials() {\n    await confirm({\n      icon: 'delete',\n      title: _('forgetClientId'),\n      body: _('forgetXsCredentialsConfirm'),\n    })\n    try {\n      await editXsCredentials(null)\n      success(_('forgetXsCredentialsSuccess'))\n    } catch (err) {\n      error('forgetXsCredentialsError', err.message)\n    }\n  }\n\n  render() {\n    const isConfigured = this.props.user?.preferences?.xsCredentials !== undefined\n    return (\n      <Container>\n        <Row>\n          <Col smallSize={2}>\n            <strong>{_('xsClientId')}</strong>{' '}\n            <a href='https://docs.xen-orchestra.com/updater#xenserver-updates' target='_blank' rel='noreferrer'>\n              <Icon icon='info' />\n            </a>\n          </Col>\n          <Col smallSize={10}>\n            <span className='mr-1'>{isConfigured ? _('configured') : _('notConfigured')}</span>\n            <SelectFiles onChange={this.editXsCredentials} label={_('uploadClientId')} />{' '}\n            {isConfigured && (\n              <ActionButton btnStyle='danger' handler={this.deleteXsCredentials} icon='delete'>\n                {_('forgetClientId')}\n              </ActionButton>\n            )}\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n\n// ===================================================================\n\nclass DefaultFilterPicker extends Component {\n  static propTypes = {\n    customFilters: PropTypes.object,\n    defaultFilter: PropTypes.string.isRequired,\n    filters: PropTypes.object.isRequired,\n    type: PropTypes.string.isRequired,\n  }\n\n  _computeOptions(props) {\n    const { customFilters, filters } = props\n\n    // Custom filters.\n    const options = [\n      {\n        label: _('customFilters'),\n        disabled: true,\n      },\n    ]\n\n    options.push.apply(\n      options,\n      map(customFilters, (filter, name) => ({\n        label: name,\n        value: name,\n      }))\n    )\n\n    // Default filters\n    options.push({\n      label: _('defaultFilters'),\n      disabled: true,\n    })\n\n    options.push.apply(\n      options,\n      map(filters, (filter, labelId) => ({\n        label: _(labelId),\n        value: labelId,\n      }))\n    )\n\n    this.setState({ options })\n  }\n\n  _handleDefaultFilter = value => setDefaultHomeFilter(this.props.type, value && value.value).catch(noop)\n\n  componentWillMount() {\n    this._computeOptions(this.props)\n  }\n\n  componentWillReceiveProps(props) {\n    this._computeOptions(props)\n  }\n\n  render() {\n    return (\n      <Row>\n        <Col>\n          <FormGrid.Row>\n            <FormGrid.LabelCol>\n              <strong>{_('defaultFilter')}</strong>\n            </FormGrid.LabelCol>\n            <FormGrid.InputCol>\n              <Select\n                onChange={this._handleDefaultFilter}\n                options={this.state.options}\n                value={this.props.defaultFilter}\n              />\n            </FormGrid.InputCol>\n          </FormGrid.Row>\n        </Col>\n      </Row>\n    )\n  }\n}\n\n// ===================================================================\n\nclass UserFilters extends Component {\n  static propTypes = {\n    user: PropTypes.object.isRequired,\n  }\n\n  _removeFilter = ({ name, type }) => removeCustomFilter(type, name)\n\n  render() {\n    const { defaultHomeFilters, filters: customFiltersByType } = getUserPreferences(this.props.user)\n\n    return (\n      <Container>\n        <Row>\n          <Col>\n            <h4>{_('customizeFilters')}</h4>\n            <div>\n              {map(homeFilters, (filters, type) => {\n                const labelId = FILTER_TYPE_TO_LABEL_ID[type]\n                if (!labelId) {\n                  return\n                }\n\n                const customFilters = customFiltersByType && customFiltersByType[type]\n                const defaultFilter = getDefaultFilter(defaultHomeFilters, type)\n\n                return (\n                  <div key={type}>\n                    <h5>{_(labelId)}</h5>\n                    <hr />\n                    <DefaultFilterPicker\n                      customFilters={customFilters}\n                      defaultFilter={defaultFilter}\n                      filters={filters}\n                      type={type}\n                    />\n                    {map(customFilters, (filter, name) => (\n                      <Row key={name} className='pb-1'>\n                        <Col mediumSize={4}>\n                          <div className='input-group'>\n                            <Text onChange={newName => editCustomFilter(type, name, { newName })} value={name} />\n                          </div>\n                        </Col>\n                        <Col mediumSize={7}>\n                          <div className='input-group'>\n                            <Text onChange={newValue => editCustomFilter(type, name, { newValue })} value={filter} />\n                          </div>\n                        </Col>\n                        <Col mediumSize={1}>\n                          <ActionButton\n                            btnStyle='danger'\n                            className='pull-right'\n                            handler={this._removeFilter}\n                            handlerParam={{ name, type }}\n                            icon='delete'\n                          />\n                        </Col>\n                      </Row>\n                    ))}\n                  </div>\n                )\n              })}\n            </div>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n\n// ===================================================================\nconst COLUMNS = [\n  {\n    default: true,\n    itemRenderer: sshKey => sshKey.title,\n    name: _('title'),\n    sortCriteria: 'title',\n  },\n  {\n    itemRenderer: sshKey => <span style={SSH_KEY_STYLE}>{sshKey.key}</span>,\n    name: _('key'),\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    handler: deleteSshKey,\n    icon: 'delete',\n    label: _('deleteSshKey'),\n    level: 'danger',\n  },\n]\n\nconst GROUPED_ACTIONS = [\n  {\n    handler: deleteSshKeys,\n    icon: 'delete',\n    label: _('deleteSshKeys'),\n    level: 'danger',\n  },\n]\n\nconst SshKeys = addSubscriptions({\n  user: subscribeCurrentUser,\n})(({ user }) => {\n  const sshKeys = user && user.preferences && user.preferences.sshKeys\n\n  const sshKeysWithIds = map(sshKeys, sshKey => ({\n    ...sshKey,\n    id: sshKey.key,\n  }))\n\n  return (\n    <div>\n      <Card>\n        <CardHeader>\n          <Icon icon='ssh-key' /> {_('sshKeys')}\n          <ActionButton className='btn-success pull-right' icon='add' handler={addSshKey}>\n            {_('newSshKey')}\n          </ActionButton>\n        </CardHeader>\n        <CardBlock>\n          <SortedTable\n            collection={sshKeysWithIds}\n            columns={COLUMNS}\n            groupedActions={GROUPED_ACTIONS}\n            individualActions={INDIVIDUAL_ACTIONS}\n            stateUrlParam='s'\n          />\n        </CardBlock>\n      </Card>\n    </div>\n  )\n})\n\n// ===================================================================\nconst COLUMNS_AUTH_TOKENS = [\n  {\n    itemRenderer: ({ id }) => (\n      <Copiable tagName='pre' data={id}>\n        {id.slice(0, 5)}…\n      </Copiable>\n    ),\n    name: _('authToken'),\n  },\n  {\n    itemRenderer: token => (\n      <Text value={token.description ?? ''} onChange={description => editAuthToken({ ...token, description })} />\n    ),\n    name: _('description'),\n    sortCriteria: 'description',\n  },\n  {\n    itemRenderer: ({ last_use_ip, last_uses }) => {\n      if (last_use_ip !== undefined) {\n        return (\n          <span>\n            <NumericDate timestamp={last_uses[last_use_ip].timestamp} /> by <code>{last_use_ip}</code>\n          </span>\n        )\n      }\n      return _('notDefined')\n    },\n    name: _('authTokenLastUse'),\n    sortCriteria: ({ last_use_ip, last_uses }) => last_use_ip && last_uses[last_use_ip].timestamp,\n  },\n  {\n    default: true,\n    itemRenderer: ({ expiration }) => <NumericDate timestamp={expiration} />,\n    name: _('expiration'),\n    sortCriteria: 'expiration',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS_AUTH_TOKENS = [\n  {\n    handler: deleteAuthToken,\n    icon: 'delete',\n    label: _('delete'),\n    level: 'danger',\n  },\n]\n\nconst GROUPED_ACTIONS_AUTH_TOKENS = [\n  {\n    handler: deleteAuthTokens,\n    icon: 'delete',\n    label: _('deleteAuthTokens'),\n    level: 'danger',\n  },\n]\n\nconst UserAuthTokens = addSubscriptions({\n  userAuthTokens: cb =>\n    subscribeUserAuthTokens(tokens => {\n      cb(\n        tokens.map(token => {\n          // find and inject last_use_ip from last_uses dictionary\n          const { last_uses } = token\n          if (last_uses !== undefined) {\n            const ips = Object.keys(last_uses)\n            const n = ips.length\n            if (n !== 0) {\n              let lastIp = ips[0]\n              let lastTimestamp = last_uses[lastIp].timestamp\n              for (let i = 1; i < n; ++i) {\n                const ip = ips[i]\n                const { timestamp } = last_uses[ip]\n                if (timestamp > lastTimestamp) {\n                  lastIp = ip\n                  lastTimestamp = timestamp\n                }\n              }\n              return { ...token, last_use_ip: lastIp }\n            }\n          }\n\n          return token\n        })\n      )\n    }),\n})(({ userAuthTokens }) => (\n  <div>\n    <Card>\n      <CardHeader>\n        <Icon icon='user' /> {_('authTokens')}\n        <ActionButton className='btn-success pull-right' icon='add' handler={addAuthToken}>\n          {_('newAuthToken')}\n        </ActionButton>\n      </CardHeader>\n      <CardBlock>\n        <SortedTable\n          collection={userAuthTokens}\n          columns={COLUMNS_AUTH_TOKENS}\n          stateUrlParam='s_auth_tokens'\n          groupedActions={GROUPED_ACTIONS_AUTH_TOKENS}\n          individualActions={INDIVIDUAL_ACTIONS_AUTH_TOKENS}\n        />\n      </CardBlock>\n    </Card>\n  </div>\n))\n\n// ===================================================================\n\n@addSubscriptions({\n  user: subscribeCurrentUser,\n})\n@connectStore({\n  lang: getLang,\n})\n@injectIntl\nexport default class User extends Component {\n  handleSelectLang = event => {\n    this.props.selectLang(event.target.value)\n  }\n\n  _handleSavePassword = () => {\n    const { oldPassword, newPassword, confirmPassword } = this.state\n    if (newPassword !== confirmPassword) {\n      return alert(_('confirmationPasswordError'), _('confirmationPasswordErrorBody'))\n    }\n    return changePassword(oldPassword, newPassword).then(() =>\n      this.setState({\n        oldPassword: undefined,\n        newPassword: undefined,\n        confirmPassword: undefined,\n      })\n    )\n  }\n\n  _handleOldPasswordChange = event => this.setState({ oldPassword: event.target.value })\n  _handleNewPasswordChange = event => this.setState({ newPassword: event.target.value })\n  _handleConfirmPasswordChange = event => this.setState({ confirmPassword: event.target.value })\n\n  render() {\n    const { lang, user } = this.props\n\n    if (!user) {\n      return <p>Loading…</p>\n    }\n\n    const { formatMessage } = this.props.intl\n    const { confirmPassword, newPassword, oldPassword } = this.state\n\n    return (\n      <Page header={HEADER} title={user.email}>\n        <Container>\n          <Row>\n            <Col smallSize={2}>\n              <strong>{_('username')}</strong>\n            </Col>\n            <Col smallSize={10}>{user.email}</Col>\n          </Row>\n          <br />\n          {isEmpty(user.authProviders) && (\n            <Row className='mb-1'>\n              <Col smallSize={2}>\n                <strong>{_('password')}</strong>\n              </Col>\n              <Col smallSize={10}>\n                <form className='form-inline' id='changePassword'>\n                  <input\n                    autoComplete='off'\n                    className='form-control'\n                    onChange={this._handleOldPasswordChange}\n                    placeholder={formatMessage(messages.oldPasswordPlaceholder)}\n                    required\n                    type='password'\n                    value={oldPassword || ''}\n                  />{' '}\n                  <input\n                    type='password'\n                    autoComplete='off'\n                    className='form-control'\n                    onChange={this._handleNewPasswordChange}\n                    placeholder={formatMessage(messages.newPasswordPlaceholder)}\n                    required\n                    value={newPassword}\n                  />{' '}\n                  <input\n                    autoComplete='off'\n                    className='form-control'\n                    onChange={this._handleConfirmPasswordChange}\n                    placeholder={formatMessage(messages.confirmPasswordPlaceholder)}\n                    required\n                    type='password'\n                    value={confirmPassword}\n                  />{' '}\n                  <ActionButton icon='save' form='changePassword' btnStyle='primary' handler={this._handleSavePassword}>\n                    {_('changePasswordOk')}\n                  </ActionButton>\n                </form>\n              </Col>\n            </Row>\n          )}\n          <Row>\n            <Col smallSize={10} offset={2}>\n              <Tooltip content={_('forgetTokensExplained')}>\n                <ActionButton btnStyle='danger' handler={signOutFromEverywhereElse} icon='disconnect'>\n                  {_('forgetTokens')}\n                </ActionButton>\n              </Tooltip>\n            </Col>\n          </Row>\n          <br />\n          <Row>\n            <Col smallSize={2}>\n              <strong>{_('language')}</strong>\n            </Col>\n            <Col smallSize={10}>\n              <select className='form-control' onChange={this.handleSelectLang} value={lang} style={{ width: '10em' }}>\n                <option value='en'>English</option>\n                <option value='ru'>Русский</option>\n                <option value='es'>Español</option>\n                <option value='fa'>Persian</option>\n                <option value='fr'>Français</option>\n                <option value='hu'>Magyar</option>\n                <option value='it'>Italiano</option>\n                <option value='pl'>Polski</option>\n                <option value='pt'>Português</option>\n                <option value='sv'>Svenska</option>\n                <option value='tr'>Türkçe</option>\n                <option value='he'>עברי</option>\n                <option value='zh'>简体中文</option>\n                <option value='ja'>日本語</option>\n                <option value='de'>Deutsch - XO6</option>\n                <option value='cs'>čeština - XO6</option>\n                <option value='uk'>Українська - XO6</option>\n                <option value='nl'>Nederlands - XO6</option>\n              </select>\n            </Col>\n          </Row>\n        </Container>\n        <hr />\n        {(process.env.XOA_PLAN > 2 || user.preferences.otp !== undefined) && [\n          <Otp user={user} key='otp' />,\n          <hr key='hr' />,\n        ]}\n        <XsClientId user={user} />\n        <hr />\n        <SshKeys />\n        <hr />\n        <UserAuthTokens />\n        <hr />\n        <UserFilters user={user} />\n      </Page>\n    )\n  }\n}\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport qrcode from 'qrcode'\nimport React from 'react'\nimport { addOtp, removeOtp } from 'xo'\nimport { injectState, provideState } from 'reaclette'\n\nimport authenticator from '../../common/otp-authenticator.js'\nimport { Container, Row, Col } from '../../common/grid'\nimport { Toggle } from '../../common/form'\n\nexport default decorate([\n  provideState({\n    effects: {\n      _handleOtp(_, isChecked) {\n        return isChecked ? addOtp(authenticator.generateSecret()) : removeOtp()\n      },\n    },\n    computed: {\n      qrcode: /* async */ ({ secret }, props) =>\n        secret && qrcode.toDataURL(authenticator.keyuri(props.user.email, 'XenOrchestra', secret)),\n      secret: (_, { user }) => user?.preferences?.otp,\n    },\n  }),\n  injectState,\n  ({ state: { qrcode, secret }, effects, user }) => (\n    <Container>\n      <Row>\n        <Col smallSize={2}>\n          <strong>{_('OtpAuthentication')}</strong>\n        </Col>\n        <Col smallSize={10}>\n          <Row>\n            <Toggle className='align-middle' value={user.preferences.otp !== undefined} onChange={effects._handleOtp} />\n            {secret !== undefined && ' ' + secret}\n          </Row>\n          {qrcode !== undefined && (\n            <Row>\n              <img src={qrcode} alt='qrcode' />\n            </Row>\n          )}\n        </Col>\n      </Row>\n    </Container>\n  ),\n])\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport Collapse from 'collapse'\nimport Component from 'base-component'\nimport React from 'react'\nimport { connectStore, resolveId } from 'utils'\nimport { createGetObjectsOfType, createSelector } from 'selectors'\nimport { esxiListVms, importVmsFromEsxi, isSrWritable } from 'xo'\nimport { find, isEmpty, keyBy, map, pick } from 'lodash'\nimport { injectIntl } from 'react-intl'\nimport { Input } from 'debounce-input-decorator'\nimport { InputCol, LabelCol, Row } from 'form-grid'\nimport { Password, Select, Toggle } from 'form'\nimport { SelectNetwork, SelectRemote, SelectPool, SelectSr, SelectVmTemplate } from 'select-objects'\n\nimport VmData from './vm-data'\nimport { getRedirectionUrl } from '../utils'\n\nconst N_IMPORT_VMS_IN_PARALLEL = 2\n\n@injectIntl\n@connectStore({\n  hostsById: createGetObjectsOfType('host'),\n  pifsById: createGetObjectsOfType('PIF'),\n})\nclass EsxiImport extends Component {\n  state = {\n    concurrency: N_IMPORT_VMS_IN_PARALLEL,\n    hostIp: '',\n    isConnected: false,\n    password: '',\n    skipSslVerify: false,\n    stopSource: false,\n    stopOnError: true,\n    template: undefined,\n    user: '',\n    workDirRemote: undefined,\n  }\n\n  _getDefaultNetwork = createSelector(\n    () => this.state.pool?.master,\n    () => this.props.hostsById,\n    () => this.props.pifsById,\n    (master, hostsById, pifsById) =>\n      master === undefined ? undefined : find(pick(pifsById, hostsById[master].$PIFs), pif => pif.management)?.$network\n  )\n\n  _getSelectVmOptions = createSelector(\n    () => this.state.vmsById,\n    vmsById =>\n      map(vmsById, vm => ({\n        label: vm.nameLabel,\n        value: vm.id,\n      }))\n  )\n\n  _getNetworkPredicate = createSelector(\n    () => this.state.pool?.id,\n    poolId => (poolId === undefined ? undefined : network => network.$poolId === poolId)\n  )\n\n  _getSrPredicate = createSelector(\n    () => this.state.pool?.id,\n    poolId => (poolId === undefined ? undefined : sr => isSrWritable(sr) && sr.$poolId === poolId)\n  )\n\n  _importVms = () => {\n    const {\n      concurrency,\n      hostIp,\n      network,\n      password,\n      skipSslVerify,\n      sr,\n      stopSource,\n      stopOnError,\n      user,\n      template,\n      vms,\n      workDirRemote,\n    } = this.state\n    return importVmsFromEsxi({\n      concurrency: +concurrency,\n      host: hostIp,\n      network: network?.id ?? this._getDefaultNetwork(),\n      password,\n      sr: resolveId(sr),\n      sslVerify: !skipSslVerify,\n      stopOnError,\n      stopSource,\n      template: template.id,\n      user,\n      vms: vms.map(vm => vm.value),\n      workDirRemote: workDirRemote?.id,\n    })\n  }\n\n  _connect = async () => {\n    const { hostIp, skipSslVerify, password, user } = this.state\n    const vms = await esxiListVms(hostIp, user, password, !skipSslVerify)\n    this.setState({ isConnected: true, vmsById: keyBy(vms, 'id') })\n  }\n\n  _disconnect = () => {\n    this.setState({ isConnected: false })\n  }\n\n  _onChangePool = pool => {\n    this.setState({ pool, sr: pool.default_SR })\n  }\n\n  _resetConnectForm = () => {\n    this.setState({\n      skipSslVerify: false,\n      hostIp: '',\n      isConnected: false,\n      password: '',\n      user: '',\n    })\n  }\n\n  _resetImportForm = () => {\n    this.setState({\n      concurrency: N_IMPORT_VMS_IN_PARALLEL,\n      network: undefined,\n      pool: undefined,\n      sr: undefined,\n      stopSource: false,\n      stopOnError: true,\n      vms: undefined,\n    })\n  }\n\n  render() {\n    const { intl } = this.props\n    const {\n      concurrency,\n      hostIp,\n      isConnected,\n      network = this._getDefaultNetwork(),\n      password,\n      pool,\n      skipSslVerify,\n      sr,\n      stopSource,\n      stopOnError,\n      user,\n      vms,\n      vmsById,\n      workDirRemote,\n    } = this.state\n\n    if (!isConnected) {\n      return (\n        <form>\n          <Row>\n            <LabelCol>{_('hostIp')}</LabelCol>\n            <InputCol>\n              <Input\n                className='form-control'\n                onChange={this.linkState('hostIp')}\n                placeholder='192.168.2.20'\n                required\n                value={hostIp}\n              />\n            </InputCol>\n          </Row>\n          <Row>\n            <LabelCol>{_('user')}</LabelCol>\n            <InputCol>\n              <Input\n                className='form-control'\n                onChange={this.linkState('user')}\n                placeholder={intl.formatMessage(messages.user)}\n                required\n                value={user}\n              />\n            </InputCol>\n          </Row>\n          <Row>\n            <LabelCol>{_('password')}</LabelCol>\n            <InputCol>\n              <Password\n                onChange={this.linkState('password')}\n                placeholder={intl.formatMessage(messages.password)}\n                required\n                value={password}\n              />\n            </InputCol>\n          </Row>\n          <Row>\n            <LabelCol>{_('esxiImportSslCertificate')}</LabelCol>\n            <InputCol>\n              <Toggle onChange={this.toggleState('skipSslVerify')} value={skipSslVerify} />\n            </InputCol>\n          </Row>\n          <div className='form-group pull-right'>\n            <ActionButton btnStyle='primary' className='mr-1' handler={this._connect} icon='connect' type='submit'>\n              {_('serverConnect')}\n            </ActionButton>\n            <Button onClick={this._resetConnectForm}>{_('formReset')}</Button>\n          </div>\n        </form>\n      )\n    }\n    // check if at least one VM has at least one disk chain\n    // with at least one extent stored on vsan\n    const useExportVmMigration =\n      !isEmpty(vms) &&\n      vms.some(({ value }) => {\n        return vmsById[value].hasAllExtentsListed === false\n      })\n    return (\n      <form>\n        <Row>\n          <LabelCol>{_('nImportVmsInParallel')}</LabelCol>\n          <InputCol>\n            <input\n              className='form-control'\n              onChange={this.linkState('concurrency')}\n              type='number'\n              value={concurrency}\n            />\n          </InputCol>\n        </Row>\n        <Row>\n          <LabelCol>{_('vms')}</LabelCol>\n          <InputCol>\n            <Select\n              disabled={isEmpty(vmsById)}\n              multi\n              onChange={this.linkState('vms')}\n              options={this._getSelectVmOptions()}\n              required\n              value={vms}\n            />\n          </InputCol>\n        </Row>\n        <Row>\n          <LabelCol>{_('vmImportToPool')}</LabelCol>\n          <InputCol>\n            <SelectPool onChange={this._onChangePool} required value={pool} />\n          </InputCol>\n        </Row>\n        <Row>\n          <LabelCol>{_('vmImportToSr')}</LabelCol>\n          <InputCol>\n            <SelectSr\n              disabled={pool === undefined}\n              onChange={this.linkState('sr')}\n              predicate={this._getSrPredicate()}\n              required\n              value={sr}\n            />\n          </InputCol>\n        </Row>\n        <Row>\n          <LabelCol>{_('network')}</LabelCol>\n          <InputCol>\n            <SelectNetwork\n              disabled={pool === undefined}\n              onChange={this.linkState('network')}\n              predicate={this._getNetworkPredicate()}\n              required\n              value={network}\n            />\n          </InputCol>\n        </Row>\n        <Row>\n          <LabelCol>{_('esxiImportStopSource')}</LabelCol>\n          <InputCol>\n            <Toggle onChange={this.toggleState('stopSource')} value={stopSource} />\n            <small className='form-text text-muted'>{_('esxiImportStopSourceDescription')}</small>\n          </InputCol>\n        </Row>\n        <Row>\n          <LabelCol>{_('stopOnError')}</LabelCol>\n          <InputCol>\n            <Toggle onChange={this.toggleState('stopOnError')} value={stopOnError} />\n            <small className='form-text text-muted'>{_('esxiImportStopOnErrorDescription')}</small>\n          </InputCol>\n        </Row>\n        {useExportVmMigration && (\n          <Row>\n            <LabelCol>{_('workDirLabel')}</LabelCol>\n            <InputCol>\n              <SelectRemote required value={workDirRemote?.id} onChange={this.linkState('workDirRemote')} />\n            </InputCol>\n          </Row>\n        )}\n        <Row>\n          <LabelCol>{_('originalTemplate')}</LabelCol>\n          <InputCol>\n            <SelectVmTemplate\n              autoSelectSingleOption={false}\n              disabled={isEmpty(pool)}\n              hasSelectAll\n              multi={false}\n              onChange={this.linkState('template')}\n              required\n            />\n          </InputCol>\n        </Row>\n\n        {!isEmpty(vms) && (\n          <div>\n            <hr />\n            <h5>{_('vmsToImport', { nVms: vms.length })}</h5>\n            {vms.map(vm => (\n              <Collapse className='mt-1 mb-1' buttonText={vm.label} key={vm.value} size='small'>\n                <div className='mt-1'>\n                  <VmData data={vmsById[vm.value]} />\n                </div>\n              </Collapse>\n            ))}\n          </div>\n        )}\n        {useExportVmMigration && 'warningVsanImport'}\n        <div className='form-group pull-right'>\n          <ActionButton\n            btnStyle='primary'\n            className='mr-1'\n            disabled={isEmpty(vms)}\n            handler={this._importVms}\n            icon='import'\n            redirectOnSuccess={getRedirectionUrl}\n            type='submit'\n          >\n            {_('newImport')}\n          </ActionButton>\n          <Button className='mr-1' onClick={this._disconnect}>\n            {_('disconnectServer')}\n          </Button>\n          <Button onClick={this._resetImportForm}>{_('formReset')}</Button>\n        </div>\n      </form>\n    )\n  }\n}\n\nexport default EsxiImport\n","import _ from 'intl'\nimport React from 'react'\nimport { Col, Row } from 'grid'\nimport { formatSize } from 'utils'\n\nconst VmData = ({ data }) => (\n  <div>\n    <Row>\n      <Col mediumSize={6}>\n        <div>{_('keyValue', { key: _('vmNameLabel'), value: data.nameLabel })}</div>\n        <div>\n          {_('keyValue', {\n            key: _('powerState'),\n            value: data.powerState === 'poweredOn' ? _('powerStateRunning') : _('powerStateHalted'),\n          })}\n        </div>\n      </Col>\n      <Col mediumSize={6}>\n        <div>{_('keyValue', { key: _('nCpus'), value: data.nCpus })}</div>\n        <div>{_('keyValue', { key: _('vmMemory'), value: formatSize(data.memory) })}</div>\n      </Col>\n    </Row>\n    <Row>\n      <Col mediumSize={6}>\n        <div>{_('keyValue', { key: _('firmware'), value: data.firmware })}</div>\n      </Col>\n      <Col mediumSize={6}>\n        <div>\n          {_('keyValue', {\n            key: _('guestToolStatus'),\n            value: data.guestToolsInstalled ? _('noToolsInstalled') : _('toolsInstalled'),\n          })}\n        </div>\n      </Col>\n    </Row>\n    <Row>\n      <Col mediumSize={12}>\n        <div>\n          <span>\n            {_('vmSrUsage', {\n              free: formatSize(data.storage.free),\n              total: formatSize(data.storage.used + data.storage.free),\n              used: formatSize(data.storage.used),\n            })}\n          </span>\n        </div>\n      </Col>\n    </Row>\n  </div>\n)\n\nexport default VmData\n","module.exports = {\n    \"vmContainer\": \"mc580d5799_vmContainer\"\n};","import * as FormGrid from 'form-grid'\nimport _ from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport Component from 'base-component'\nimport Dropzone from 'dropzone'\nimport isEmpty from 'lodash/isEmpty'\nimport map from 'lodash/map'\nimport orderBy from 'lodash/orderBy'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { Container, Col, Row } from 'grid'\nimport { importVm, importVms, isSrWritable } from 'xo'\nimport { Select, SizeInput, Toggle } from 'form'\nimport { createFinder, createGetObject, createGetObjectsOfType, createSelector } from 'selectors'\nimport { connectStore, formatSize, mapPlus, noop } from 'utils'\nimport { Input } from 'debounce-input-decorator'\n\nimport { SelectNetwork, SelectPool, SelectSr } from 'select-objects'\n\nimport parseOvaFile from './ova'\n\nimport styles from './index.css'\nimport { getRedirectionUrl } from './utils'\n\n// ===================================================================\n\nconst FILE_TYPES = [\n  {\n    label: 'XVA',\n    value: 'xva',\n  },\n]\n\nconst FORMAT_TO_HANDLER = {\n  ova: parseOvaFile,\n  xva: noop,\n}\n\n// ===================================================================\n\n@connectStore(\n  () => {\n    const getHostMaster = createGetObject((_, props) => props.pool.master)\n    const getPifs = createGetObjectsOfType('PIF').pick((state, props) => getHostMaster(state, props).$PIFs)\n    const getDefaultNetworkId = createSelector(createFinder(getPifs, [pif => pif.management]), pif => pif.$network)\n\n    return {\n      defaultNetwork: getDefaultNetworkId,\n    }\n  },\n  { withRef: true }\n)\nclass VmData extends Component {\n  static propTypes = {\n    descriptionLabel: PropTypes.string,\n    disks: PropTypes.objectOf(\n      PropTypes.shape({\n        capacity: PropTypes.number.isRequired,\n        descriptionLabel: PropTypes.string.isRequired,\n        nameLabel: PropTypes.string.isRequired,\n        path: PropTypes.string.isRequired,\n        compression: PropTypes.string,\n      })\n    ),\n    memory: PropTypes.number,\n    nameLabel: PropTypes.string,\n    nCpus: PropTypes.number,\n    networks: PropTypes.array,\n    pool: PropTypes.object.isRequired,\n  }\n\n  get value() {\n    const { props, refs } = this\n    return {\n      descriptionLabel: refs.descriptionLabel.value,\n      disks: map(props.disks, ({ capacity, path, compression, position }, diskId) => ({\n        capacity,\n        descriptionLabel: refs[`disk-description-${diskId}`].value,\n        nameLabel: refs[`disk-name-${diskId}`].value,\n        path,\n        position,\n        compression,\n      })),\n      memory: +refs.memory.value,\n      nameLabel: refs.nameLabel.value,\n      networks: map(props.networks, (_, networkId) => {\n        const network = refs[`network-${networkId}`].value\n        return network.id ? network.id : network\n      }),\n      nCpus: +refs.nCpus.value,\n      tables: props.tables,\n    }\n  }\n\n  _getNetworkPredicate = createSelector(\n    () => this.props.pool.id,\n    id => network => network.$pool === id\n  )\n\n  render() {\n    const { descriptionLabel, defaultNetwork, disks, memory, nameLabel, nCpus, networks } = this.props\n\n    return (\n      <div>\n        <Row>\n          <Col mediumSize={6}>\n            <div className='form-group'>\n              <label>{_('vmNameLabel')}</label>\n              <input className='form-control' ref='nameLabel' defaultValue={nameLabel} type='text' required />\n            </div>\n            <div className='form-group'>\n              <label>{_('vmNameDescription')}</label>\n              <input className='form-control' ref='descriptionLabel' defaultValue={descriptionLabel} type='text' />\n            </div>\n          </Col>\n          <Col mediumSize={6}>\n            <div className='form-group'>\n              <label>{_('nCpus')}</label>\n              <input className='form-control' ref='nCpus' defaultValue={nCpus} type='number' required />\n            </div>\n            <div className='form-group'>\n              <label>{_('vmMemory')}</label>\n              <SizeInput defaultValue={memory} ref='memory' required />\n            </div>\n          </Col>\n        </Row>\n        <Row>\n          <Col mediumSize={6}>\n            {!isEmpty(disks)\n              ? map(disks, (disk, diskId) => (\n                  <Row key={diskId}>\n                    <Col mediumSize={6}>\n                      <div className='form-group'>\n                        <label>\n                          {_('diskInfo', {\n                            position: `${disk.position}`,\n                            capacity: formatSize(disk.capacity),\n                          })}\n                        </label>\n                        <input\n                          className='form-control'\n                          ref={`disk-name-${diskId}`}\n                          defaultValue={disk.nameLabel}\n                          type='text'\n                          required\n                        />\n                      </div>\n                    </Col>\n                    <Col mediumSize={6}>\n                      <div className='form-group'>\n                        <label>{_('diskDescription')}</label>\n                        <input\n                          className='form-control'\n                          ref={`disk-description-${diskId}`}\n                          defaultValue={disk.descriptionLabel}\n                          type='text'\n                          required\n                        />\n                      </div>\n                    </Col>\n                  </Row>\n                ))\n              : _('noDisks')}\n          </Col>\n          <Col mediumSize={6}>\n            {networks.length > 0\n              ? map(networks, (name, networkId) => (\n                  <div className='form-group' key={networkId}>\n                    <label>{_('networkInfo', { name })}</label>\n                    <SelectNetwork\n                      defaultValue={defaultNetwork}\n                      ref={`network-${networkId}`}\n                      predicate={this._getNetworkPredicate()}\n                    />\n                  </div>\n                ))\n              : _('noNetworks')}\n          </Col>\n        </Row>\n      </div>\n    )\n  }\n}\n\n// ===================================================================\n\nconst parseFile = async (file, type, func) => {\n  try {\n    return {\n      data: await func(file),\n      file,\n      type,\n    }\n  } catch (error) {\n    console.error(error)\n    return { error, file, type }\n  }\n}\n\nexport default class Import extends Component {\n  constructor(props) {\n    super(props)\n    this.state = {\n      isFromUrl: false,\n      type: {\n        label: 'XVA',\n        value: 'xva',\n      },\n      url: '',\n      vms: [],\n    }\n  }\n\n  _import = () => {\n    const { state } = this\n    return importVms(\n      mapPlus(state.vms, (vm, push, vmIndex) => {\n        if (!vm.error) {\n          const ref = this.refs[`vm-data-${vmIndex}`]\n          push({\n            ...vm,\n            data: ref && ref.value,\n          })\n        }\n      }),\n      state.sr\n    )\n  }\n\n  _importVmFromUrl = () => {\n    const { type, url } = this.state\n    const file = {\n      name: decodeURIComponent(url.slice(url.lastIndexOf('/') + 1)),\n    }\n    return importVm(file, type.value, undefined, this.state.sr, url)\n  }\n\n  _handleDrop = async files => {\n    this.setState({\n      vms: [],\n    })\n\n    const vms = await Promise.all(\n      mapPlus(files, (file, push) => {\n        const { name } = file\n        const extIndex = name.lastIndexOf('.')\n\n        let func\n        let type\n\n        if (extIndex >= 0 && (type = name.slice(extIndex + 1).toLowerCase()) && (func = FORMAT_TO_HANDLER[type])) {\n          push(parseFile(file, type, func))\n        }\n      })\n    )\n\n    this.setState({\n      vms: orderBy(vms, vm => [vm.error != null, vm.type, vm.file.name]),\n    })\n  }\n\n  _handleCleanSelectedVms = () => {\n    this.setState({\n      vms: [],\n    })\n  }\n\n  _handleSelectedPool = pool => {\n    if (pool === '') {\n      this.setState({\n        pool: undefined,\n        sr: undefined,\n        srPredicate: undefined,\n      })\n    } else {\n      this.setState({\n        pool,\n        sr: pool.default_SR,\n        srPredicate: sr => sr.$pool === this.state.pool.id && isSrWritable(sr),\n      })\n    }\n  }\n\n  _handleSelectedSr = sr => {\n    this.setState({\n      sr: sr === '' ? undefined : sr,\n    })\n  }\n\n  render() {\n    const { isFromUrl, pool, sr, srPredicate, type, url, vms } = this.state\n\n    return (\n      <Container>\n        <form id='import-form'>\n          <p>\n            <Toggle value={isFromUrl} onChange={this.toggleState('isFromUrl')} /> {_('fromUrl')}\n          </p>\n          <FormGrid.Row>\n            <FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>\n            <FormGrid.InputCol>\n              <SelectPool value={pool} onChange={this._handleSelectedPool} required />\n            </FormGrid.InputCol>\n          </FormGrid.Row>\n          <FormGrid.Row>\n            <FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>\n            <FormGrid.InputCol>\n              <SelectSr\n                disabled={!pool}\n                onChange={this._handleSelectedSr}\n                predicate={srPredicate}\n                required\n                value={sr}\n              />\n            </FormGrid.InputCol>\n          </FormGrid.Row>\n          {sr &&\n            (!isFromUrl ? (\n              <div>\n                <Dropzone onDrop={this._handleDrop} message={_('importVmsList')} />\n                <hr />\n                <h5>{_('vmsToImport', { nVms: vms.length })}</h5>\n                {vms.length > 0 ? (\n                  <div>\n                    {map(vms, ({ data, error, file, type }, vmIndex) => (\n                      <div key={file.preview} className={styles.vmContainer}>\n                        <strong>{file.name}</strong>\n                        <span className='pull-right'>\n                          <strong>{`(${formatSize(file.size)})`}</strong>\n                        </span>\n                        {!error ? (\n                          data && (\n                            <div>\n                              <hr />\n                              <div className='alert alert-info' role='alert'>\n                                <strong>{_('vmImportFileType', { type })}</strong> {_('vmImportConfigAlert')}\n                              </div>\n                              <VmData {...data} ref={`vm-data-${vmIndex}`} pool={pool} />\n                            </div>\n                          )\n                        ) : (\n                          <div>\n                            <hr />\n                            <div className='alert alert-danger' role='alert'>\n                              <strong>{_('vmImportError')}</strong>{' '}\n                              {(error && error.message) || _('noVmImportErrorDescription')}\n                            </div>\n                          </div>\n                        )}\n                      </div>\n                    ))}\n                  </div>\n                ) : (\n                  <p>{_('noSelectedVms')}</p>\n                )}\n                <hr />\n                <div className='form-group pull-right'>\n                  <ActionButton\n                    btnStyle='primary'\n                    disabled={!vms.length}\n                    className='mr-1'\n                    form='import-form'\n                    handler={this._import}\n                    icon='import'\n                    redirectOnSuccess={getRedirectionUrl}\n                    type='submit'\n                  >\n                    {_('newImport')}\n                  </ActionButton>\n                  <Button onClick={this._handleCleanSelectedVms}>{_('importVmsCleanList')}</Button>\n                </div>\n              </div>\n            ) : (\n              <div>\n                <FormGrid.Row>\n                  <FormGrid.LabelCol>{_('url')}</FormGrid.LabelCol>\n                  <FormGrid.InputCol>\n                    <Input\n                      className='form-control'\n                      onChange={this.linkState('url')}\n                      placeholder='https://my-company.net/vm.xva'\n                      type='url'\n                    />\n                  </FormGrid.InputCol>\n                </FormGrid.Row>\n                <FormGrid.Row>\n                  <FormGrid.LabelCol>{_('fileType')}</FormGrid.LabelCol>\n                  <FormGrid.InputCol>\n                    <Select onChange={this.linkState('type')} options={FILE_TYPES} required value={type} />\n                  </FormGrid.InputCol>\n                </FormGrid.Row>\n                <ActionButton\n                  btnStyle='primary'\n                  className='mr-1 mt-1'\n                  disabled={isEmpty(url)}\n                  form='import-form'\n                  handler={this._importVmFromUrl}\n                  icon='import'\n                  redirectOnSuccess={getRedirectionUrl}\n                  type='submit'\n                >\n                  {_('newImport')}\n                </ActionButton>\n              </div>\n            ))}\n        </form>\n      </Container>\n    )\n  }\n}\n","import fromEvent from 'promise-toolbox/fromEvent'\nimport { parseOVAFile, ParsableFile } from 'xo-vmdk-to-vhd'\n\n/* global FileReader */\n\nclass BrowserParsableFile extends ParsableFile {\n  constructor(file) {\n    super()\n    this._file = file\n  }\n\n  slice(start, end) {\n    return new BrowserParsableFile(this._file.slice(start, end))\n  }\n\n  async read() {\n    const reader = new FileReader()\n    reader.readAsArrayBuffer(this._file)\n    return (await fromEvent(reader, 'loadend')).target.result\n  }\n}\n\nasync function parseTarFile(file) {\n  return await parseOVAFile(new BrowserParsableFile(file), (buffer, encoding) =>\n    new TextDecoder(encoding).decode(buffer)\n  )\n}\n\nexport { parseTarFile as default }\n","import * as CM from 'complex-matcher'\nimport { resolveIds } from 'utils'\n\nexport const getRedirectionUrl = (vms = []) => {\n  const vmIds = resolveIds(typeof vms === 'object' ? Object.values(vms) : vms)\n  return vmIds.length === 0\n    ? undefined // no redirect\n    : vmIds.length === 1\n      ? `/vms/${vmIds[0]}`\n      : `/home?s=${encodeURIComponent(\n          new CM.Property('id', new CM.Or(vmIds.map(vm => new CM.String(vm)))).toString()\n        )}&t=VM`\n}\n","import _ from 'intl'\nimport ActionBar, { Action } from 'action-bar'\nimport React from 'react'\nimport { addSubscriptions, connectStore } from 'utils'\nimport { find, includes } from 'lodash'\nimport { createSelector, getCheckPermissions, getUser } from 'selectors'\nimport { cloneVm, copyVm, exportVm, migrateVm, restartVm, snapshotVm, startVm, stopVm, subscribeResourceSets } from 'xo'\n\nconst vmActionBarByState = {\n  Running: ({ vm, isSelfUser, canAdministrate }) => (\n    <ActionBar display='icon' handlerParam={vm}>\n      <Action\n        handler={stopVm}\n        icon='vm-stop'\n        label={_('stopVmLabel')}\n        pending={includes(vm.current_operations, 'clean_shutdown')}\n      />\n      <Action\n        handler={restartVm}\n        icon='vm-reboot'\n        label={_('rebootVmLabel')}\n        pending={includes(vm.current_operations, 'clean_reboot')}\n      />\n      {!isSelfUser && (\n        <Action\n          handler={migrateVm}\n          icon='vm-migrate'\n          label={_('migrateVmLabel')}\n          pending={includes(vm.current_operations, 'migrate_send') || includes(vm.current_operations, 'pool_migrate')}\n        />\n      )}\n      <Action\n        handler={snapshotVm}\n        icon='vm-snapshot'\n        label={_('snapshotVmLabel')}\n        pending={includes(vm.current_operations, 'snapshot')}\n      />\n      {!isSelfUser && canAdministrate && (\n        <Action\n          handler={exportVm}\n          icon='export'\n          label={_('exportVmLabel')}\n          pending={includes(vm.current_operations, 'export')}\n        />\n      )}\n      {!isSelfUser && canAdministrate && (\n        <Action\n          handler={copyVm}\n          icon='vm-copy'\n          label={_('copyVmLabel')}\n          pending={includes(vm.current_operations, 'copy')}\n        />\n      )}\n    </ActionBar>\n  ),\n  Halted: ({ vm, isSelfUser, canAdministrate }) => (\n    <ActionBar display='icon' handlerParam={vm}>\n      <Action\n        handler={startVm}\n        icon='vm-start'\n        label={_('startVmLabel')}\n        pending={includes(vm.current_operations, 'start')}\n      />\n      {!isSelfUser && canAdministrate && (\n        <Action\n          handler={cloneVm}\n          icon='vm-fast-clone'\n          label={_('fastCloneVmLabel')}\n          pending={includes(vm.current_operations, 'clone')}\n        />\n      )}\n      {!isSelfUser && (\n        <Action\n          handler={migrateVm}\n          icon='vm-migrate'\n          label={_('migrateVmLabel')}\n          pending={includes(vm.current_operations, 'migrate_send') || includes(vm.current_operations, 'pool_migrate')}\n        />\n      )}\n      {!isSelfUser && (\n        <Action\n          handler={snapshotVm}\n          icon='vm-snapshot'\n          label={_('snapshotVmLabel')}\n          pending={includes(vm.current_operations, 'snapshot')}\n        />\n      )}\n      {!isSelfUser && canAdministrate && (\n        <Action\n          handler={exportVm}\n          icon='export'\n          label={_('exportVmLabel')}\n          pending={includes(vm.current_operations, 'export')}\n        />\n      )}\n      {!isSelfUser && canAdministrate && (\n        <Action\n          handler={copyVm}\n          icon='vm-copy'\n          label={_('copyVmLabel')}\n          pending={includes(vm.current_operations, 'copy')}\n        />\n      )}\n    </ActionBar>\n  ),\n  Suspended: ({ vm, isSelfUser, canAdministrate }) => (\n    <ActionBar display='icon' handlerParam={vm}>\n      <Action\n        handler={startVm}\n        icon='vm-start'\n        label={_('resumeVmLabel')}\n        pending={includes(vm.current_operations, 'start')}\n      />\n      {!isSelfUser && (\n        <Action\n          handler={snapshotVm}\n          icon='vm-snapshot'\n          label={_('snapshotVmLabel')}\n          pending={includes(vm.current_operations, 'snapshot')}\n        />\n      )}\n      {!isSelfUser && canAdministrate && (\n        <Action\n          handler={exportVm}\n          icon='export'\n          label={_('exportVmLabel')}\n          pending={includes(vm.current_operations, 'export')}\n        />\n      )}\n      {!isSelfUser && canAdministrate && (\n        <Action\n          handler={copyVm}\n          icon='vm-copy'\n          label={_('copyVmLabel')}\n          pending={includes(vm.current_operations, 'copy')}\n        />\n      )}\n    </ActionBar>\n  ),\n  Paused: ({ vm, isSelfUser }) => (\n    <ActionBar display='icon' handlerParam={vm}>\n      <Action\n        handler={startVm}\n        icon='vm-start'\n        label={_('resumeVmLabel')}\n        pending={includes(vm.current_operations, 'unpause')}\n      />\n      {!isSelfUser && (\n        <Action\n          handler={snapshotVm}\n          icon='vm-snapshot'\n          label={_('snapshotVmLabel')}\n          pending={includes(vm.current_operations, 'snapshot')}\n        />\n      )}\n    </ActionBar>\n  ),\n}\n\nconst VmActionBar = addSubscriptions(() => ({\n  resourceSets: subscribeResourceSets,\n}))(\n  connectStore(() => ({\n    checkPermissions: getCheckPermissions,\n    user: getUser,\n  }))(({ checkPermissions, vm, user, resourceSets }) => {\n    // Is the user in the same resource set as the VM\n    const _getIsSelfUser = createSelector(\n      () => resourceSets,\n      resourceSets => {\n        const vmResourceSet = vm.resourceSet && find(resourceSets, { id: vm.resourceSet })\n\n        return (\n          vmResourceSet &&\n          (includes(vmResourceSet.subjects, user.id) ||\n            user.groups.some(groupId => includes(vmResourceSet.subjects, groupId)))\n        )\n      }\n    )\n\n    const _getCanAdministrate = createSelector(\n      () => checkPermissions,\n      () => vm.id,\n      (check, vmId) => check(vmId, 'administrate')\n    )\n\n    const ActionBar = vmActionBarByState[vm.power_state]\n    if (!ActionBar) {\n      return <p>No action bar for state {vm.power_state}</p>\n    }\n\n    return <ActionBar vm={vm} isSelfUser={_getIsSelfUser()} canAdministrate={_getCanAdministrate()} />\n  })\n)\nexport default VmActionBar\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport HTML5Backend from 'react-dnd-html5-backend'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { DragDropContext, DragSource, DropTarget } from 'react-dnd'\nimport { Toggle } from 'form'\nimport { forEach, map } from 'lodash'\nimport { setVmBootOrder } from 'xo'\n\nconst parseBootOrder = bootOrder => {\n  // FIXME missing translation\n  const bootOptions = {\n    c: 'Hard-Drive',\n    d: 'DVD-Drive',\n    n: 'Network',\n  }\n  const order = []\n  if (bootOrder) {\n    for (const id of bootOrder) {\n      if (id in bootOptions) {\n        order.push({ id, text: bootOptions[id], active: true })\n        delete bootOptions[id]\n      }\n    }\n  }\n  forEach(bootOptions, (text, id) => {\n    order.push({ id, text, active: false })\n  })\n  return order\n}\n\nconst orderItemSource = {\n  beginDrag: props => ({\n    id: props.id,\n    index: props.index,\n  }),\n}\n\nconst orderItemTarget = {\n  hover: (props, monitor, component) => {\n    const dragIndex = monitor.getItem().index\n    const hoverIndex = props.index\n\n    if (dragIndex === hoverIndex) {\n      return\n    }\n\n    props.move(dragIndex, hoverIndex)\n    monitor.getItem().index = hoverIndex\n  },\n}\n\nconst GRAB_STYLE = { cursor: 'grab' }\n@DropTarget('orderItem', orderItemTarget, connect => ({\n  connectDropTarget: connect.dropTarget(),\n}))\n@DragSource('orderItem', orderItemSource, (connect, monitor) => ({\n  connectDragSource: connect.dragSource(),\n  isDragging: monitor.isDragging(),\n}))\nclass OrderItem extends Component {\n  static propTypes = {\n    connectDragSource: PropTypes.func.isRequired,\n    connectDropTarget: PropTypes.func.isRequired,\n    index: PropTypes.number.isRequired,\n    isDragging: PropTypes.bool.isRequired,\n    id: PropTypes.any.isRequired,\n    item: PropTypes.object.isRequired,\n    move: PropTypes.func.isRequired,\n  }\n\n  _toggle = checked => {\n    const { item } = this.props\n    item.active = checked\n    this.forceUpdate()\n  }\n\n  render() {\n    const { item, connectDragSource, connectDropTarget } = this.props\n    return connectDragSource(\n      connectDropTarget(\n        <li className='list-group-item'>\n          <span className='mr-1' style={GRAB_STYLE}>\n            <Icon icon='grab' /> <Icon icon='grab' />\n          </span>\n          {item.text}\n          <span className='pull-right'>\n            <Toggle value={item.active} onChange={this._toggle} />\n          </span>\n        </li>\n      )\n    )\n  }\n}\n\n@DragDropContext(HTML5Backend)\nexport default class BootOrder extends Component {\n  static propTypes = {\n    onClose: PropTypes.func,\n    vm: PropTypes.object.isRequired,\n  }\n\n  constructor(props) {\n    super(props)\n    const { vm } = props\n    const order = parseBootOrder(vm.boot && vm.boot.order)\n    this.state = { order }\n  }\n\n  _moveOrderItem = (dragIndex, hoverIndex) => {\n    const order = this.state.order.slice()\n    const dragItem = order.splice(dragIndex, 1)\n    if (dragItem.length) {\n      order.splice(hoverIndex, 0, dragItem.pop())\n      this.setState({ order })\n    }\n  }\n\n  _reset = () => {\n    const { vm } = this.props\n    const order = parseBootOrder(vm.boot && vm.boot.order)\n    this.setState({ order })\n  }\n\n  _save = () => {\n    const { vm } = this.props\n    const { order: newOrder } = this.state\n    let order = ''\n    forEach(newOrder, item => {\n      item.active && (order += item.id)\n    })\n    return setVmBootOrder(vm, order)\n  }\n\n  render() {\n    const { order } = this.state\n\n    return (\n      <form>\n        <ul className='pl-0'>\n          {map(order, (item, index) => (\n            <OrderItem\n              key={index}\n              index={index}\n              id={item.id}\n              // FIXME missing translation\n              item={item}\n              move={this._moveOrderItem}\n            />\n          ))}\n        </ul>\n        <fieldset className='form-inline'>\n          <span className='pull-right'>\n            <ActionButton icon='save' btnStyle='primary' handler={this._save}>\n              {_('saveBootOption')}\n            </ActionButton>{' '}\n            <ActionButton icon='reset' handler={this._reset}>\n              {_('resetBootOption')}\n            </ActionButton>\n          </span>\n        </fieldset>\n      </form>\n    )\n  }\n}\n","import _ from 'intl'\nimport BaseComponent from 'base-component'\nimport Copiable from 'copiable'\nimport Icon from 'icon'\nimport Tooltip from 'tooltip'\nimport { NavLink, NavTabs } from 'nav'\nimport PropTypes from 'prop-types'\nimport React, { cloneElement } from 'react'\nimport { Host, Pool } from 'render-xo-item'\nimport { Text, XoSelect } from 'editable'\nimport { isEmpty, map, pick } from 'lodash'\nimport { editVm, fetchVmStats, isVmRunning, migrateVm } from 'xo'\nimport { Container, Row, Col } from 'grid'\nimport { connectStore, routes } from 'utils'\nimport {\n  createGetObject,\n  createGetObjectsOfType,\n  createGetVmDisks,\n  createSelector,\n  createSumBy,\n  getCheckPermissions,\n  isAdmin,\n} from 'selectors'\n\nimport Page from '../page'\nimport TabGeneral from './tab-general'\nimport TabStats from './tab-stats'\nimport TabConsole from './tab-console'\nimport TabContainers from './tab-containers'\nimport TabDisks from './tab-disks'\nimport TabNetwork from './tab-network'\nimport TabSnapshots from './tab-snapshots'\nimport TabBackups from './tab-backups'\nimport TabLogs from './tab-logs'\nimport TabAdvanced from './tab-advanced'\nimport VmActionBar from './action-bar'\n\n// ===================================================================\n\n@routes('general', {\n  advanced: TabAdvanced,\n  backups: TabBackups,\n  console: TabConsole,\n  containers: TabContainers,\n  disks: TabDisks,\n  general: TabGeneral,\n  logs: TabLogs,\n  network: TabNetwork,\n  snapshots: TabSnapshots,\n  stats: TabStats,\n})\n@connectStore(() => {\n  const getVm = createGetObject()\n\n  const getContainer = createGetObject((state, props) => getVm(state, props).$container)\n\n  const getPool = createGetObject((state, props) => getVm(state, props).$pool)\n\n  const getVbds = createGetObjectsOfType('VBD')\n    .pick((state, props) => getVm(state, props).$VBDs)\n    .sort()\n  const getVdis = createGetVmDisks(getVm)\n  const getSrs = createGetObjectsOfType('SR').pick(createSelector(getVdis, vdis => map(vdis, '$SR')))\n\n  const getVmTotalDiskSpace = createSumBy(createGetVmDisks(getVm), 'size')\n\n  return (state, props) => {\n    const vm = getVm(state, props)\n    if (!vm) {\n      return {}\n    }\n\n    return {\n      checkPermissions: getCheckPermissions(state, props),\n      container: getContainer(state, props),\n      isAdmin: isAdmin(state, props),\n      pool: getPool(state, props),\n      srs: getSrs(state, props),\n      vbds: getVbds(state, props),\n      // Workaround to get the VDI object when the permissions cache isn't up to date:\n      // when a VDI is created on a VM, the user permissions might be checked on the\n      // VBD *before* it's attached to the VM so the permissions cache will store that\n      // the user doesn't have permissions on the VDI even after it's been attached\n      vdis: getVdis(state, props, true),\n      vm,\n      vmTotalDiskSpace: getVmTotalDiskSpace(state, props),\n    }\n  }\n})\nexport default class Vm extends BaseComponent {\n  static contextTypes = {\n    router: PropTypes.object,\n  }\n\n  loop(vm = this.props.vm) {\n    if (this.cancel) {\n      this.cancel()\n    }\n\n    if (!isVmRunning(vm)) {\n      return\n    }\n\n    let cancelled = false\n    this.cancel = () => {\n      cancelled = true\n    }\n\n    fetchVmStats(vm).then(stats => {\n      if (cancelled) {\n        return\n      }\n      this.cancel = null\n\n      clearTimeout(this.timeout)\n      this.setState(\n        {\n          statsOverview: stats,\n        },\n        () => {\n          this.timeout = setTimeout(this.loop, stats.interval * 1000)\n        }\n      )\n    })\n  }\n  loop = ::this.loop\n\n  componentWillMount() {\n    this.loop()\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this.timeout)\n  }\n\n  componentWillReceiveProps(props) {\n    const vmCur = this.props.vm\n    const vmNext = props.vm\n\n    if (vmCur && !vmNext) {\n      this.context.router.push('/')\n    }\n\n    if (!isVmRunning(vmCur) && isVmRunning(vmNext)) {\n      this.loop(vmNext)\n    } else if (isVmRunning(vmCur) && !isVmRunning(vmNext)) {\n      this.setState({\n        statsOverview: undefined,\n      })\n    }\n  }\n\n  compareContainers = (pool1, pool2) => {\n    const { $pool: poolId } = this.props.vm\n    return pool1.id === poolId ? -1 : pool2.id === poolId ? 1 : 0\n  }\n\n  _getCanSnapshot = createSelector(\n    () => this.props.checkPermissions,\n    () => this.props.vm,\n    () => this.props.srs,\n    (checkPermissions, vm, srs) => checkPermissions(vm.id, 'operate')\n  )\n\n  _setNameDescription = nameDescription => editVm(this.props.vm, { name_description: nameDescription })\n  _setNameLabel = nameLabel => editVm(this.props.vm, { name_label: nameLabel })\n  _migrateVm = host => migrateVm(this.props.vm, host)\n\n  _getVmState = createSelector(\n    () => this.props.vm.power_state,\n    () => this.props.vm.current_operations,\n    (powerState, operations) => (!isEmpty(operations) ? 'Busy' : powerState)\n  )\n\n  header() {\n    const { isAdmin, vm, container, pool } = this.props\n    if (!vm) {\n      return <Icon icon='loading' />\n    }\n    const state = this._getVmState()\n\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={6} className='header-title'>\n            <span>\n              <span className='text-muted'>\n                {pool !== undefined && <Pool id={pool.id} link />}\n                {vm.power_state === 'Running' && (\n                  <span>\n                    {container !== undefined && pool !== undefined && (\n                      <span>\n                        {' '}\n                        <Icon icon='next' />{' '}\n                      </span>\n                    )}\n                    {container !== undefined && (\n                      <XoSelect\n                        compareContainers={this.compareContainers}\n                        onChange={this._migrateVm}\n                        useLongClick\n                        value={container}\n                        xoType='host'\n                      >\n                        <Host id={container.id} pool={false} link />\n                      </XoSelect>\n                    )}\n                  </span>\n                )}\n              </span>\n            </span>\n            <h2>\n              <Tooltip\n                content={\n                  <span>\n                    {_(`powerState${state}`)}\n                    {state === 'Busy' && (\n                      <span>\n                        {' ('}\n                        {map(vm.current_operations)[0]}\n                        {')'}\n                      </span>\n                    )}\n                  </span>\n                }\n              >\n                <Icon icon={`vm-${state.toLowerCase()}`} />\n              </Tooltip>{' '}\n              <Text value={vm.name_label} onChange={this._setNameLabel} />\n            </h2>{' '}\n            <Copiable tagName='pre' className='text-muted mb-0'>\n              {vm.uuid}\n            </Copiable>\n            <Text value={vm.name_description} onChange={this._setNameDescription} />\n          </Col>\n          <Col mediumSize={6} className='text-xs-center'>\n            <div>\n              <VmActionBar vm={vm} />\n            </div>\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <NavTabs>\n              <NavLink to={`/vms/${vm.id}/general`}>{_('generalTabName')}</NavLink>\n              <NavLink to={`/vms/${vm.id}/stats`}>{_('statsTabName')}</NavLink>\n              <NavLink to={`/vms/${vm.id}/console`}>{_('consoleTabName')}</NavLink>\n              <NavLink to={`/vms/${vm.id}/network`}>{_('networkTabName')}</NavLink>\n              <NavLink to={`/vms/${vm.id}/disks`}>{_('disksTabName', { disks: vm.$VBDs.length })}</NavLink>\n              {this._getCanSnapshot() && (\n                <NavLink to={`/vms/${vm.id}/snapshots`}>\n                  {_('snapshotsTabName')}{' '}\n                  {vm.snapshots.length !== 0 && <span className='tag tag-pill tag-default'>{vm.snapshots.length}</span>}\n                </NavLink>\n              )}\n              {isAdmin && <NavLink to={`/vms/${vm.id}/backups`}>{_('backup')}</NavLink>}\n              <NavLink to={`/vms/${vm.id}/logs`}>{_('logsTabName')}</NavLink>\n              {vm.docker && <NavLink to={`/vms/${vm.id}/containers`}>{_('containersTabName')}</NavLink>}\n              <NavLink to={`/vms/${vm.id}/advanced`}>{_('advancedTabName')}</NavLink>\n            </NavTabs>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n\n  _toggleHeader = () => this.setState({ collapsedHeader: !this.state.collapsedHeader })\n\n  render() {\n    const { container, vm } = this.props\n    if (!vm) {\n      return <h1>{_('statusLoading')}</h1>\n    }\n\n    const childProps = Object.assign(\n      pick(this.props, ['container', 'pool', 'removeTag', 'srs', 'vbds', 'vdis', 'vm', 'vmTotalDiskSpace']),\n      pick(this.state, ['statsOverview'])\n    )\n    return (\n      <Page\n        header={this.header()}\n        collapsedHeader={this.state.collapsedHeader}\n        title={`${vm.name_label}${container ? ` (${container.name_label})` : ''}`}\n      >\n        {cloneElement(this.props.children, {\n          ...childProps,\n          toggleHeader: this._toggleHeader,\n        })}\n      </Page>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport React from 'react'\nimport renderXoItem from 'render-xo-item'\nimport Tooltip from 'tooltip'\nimport { createSelector } from 'selectors'\nimport { isPciHidden } from 'xo'\nimport { Select } from 'form'\nimport { SelectHost } from 'select-objects'\n\nexport default class PciAttachModal extends Component {\n  state = {\n    hiddenPcis: undefined,\n    host: this.props.vm.$container !== this.props.vm.$pool ? this.props.vm.$container : undefined,\n    pcis: [],\n  }\n\n  get value() {\n    return this.state.pcis\n  }\n\n  async componentDidMount() {\n    this.setState({ hiddenPcis: await this.getHiddenPcis() })\n  }\n\n  async componentDidUpdate(prevProps, prevState) {\n    if (prevState.host !== this.state.host) {\n      this.setState({\n        pcis: [],\n        hiddenPcis: await this.getHiddenPcis(),\n      })\n    }\n  }\n\n  async getHiddenPcis() {\n    const host = this.state.host\n    if (host === undefined) {\n      return undefined\n    }\n    const hidden = []\n    await Promise.all(\n      this.props.pcisByHost[host].map(async pci => {\n        if (await isPciHidden(pci.id)) {\n          hidden.push(pci)\n        }\n      })\n    )\n    return hidden\n  }\n\n  onChangeHost = host => this.linkState('host')(host?.id)\n\n  _getHostPredicate = host => this.props.vm.$pool === host.$pool\n\n  _getPcis = createSelector(\n    () => this.state.hiddenPcis,\n    () => this.props.attachedPciIds,\n    (pcis, attachedPciIds) =>\n      pcis?.filter(pci => !attachedPciIds?.includes(pci.pci_id)).map(pci => ({ value: pci.id, ...pci })) // value property is needed for multi select\n  )\n\n  render() {\n    const { pool } = this.props\n    const isHostSelected = this.state.host !== undefined\n    return (\n      <div>\n        {/* For ACL users without authorization on the pool, there is no point in displaying an empty selector */}\n        {pool !== undefined && (\n          <SelectHost onChange={this.onChangeHost} predicate={this._getHostPredicate} value={this.state.host} />\n        )}\n        <Tooltip content={isHostSelected ? undefined : _('selectHostFirst')}>\n          <Select\n            className='mt-1'\n            disabled={!isHostSelected}\n            multi\n            onChange={this.linkState('pcis')}\n            optionRenderer={renderXoItem}\n            options={this._getPcis()}\n            placeholder={_('selectPcis')}\n            value={this.state.pcis}\n          />\n        </Tooltip>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport decorate from 'apply-decorators'\nimport Copiable from 'copiable'\nimport defined, { get } from '@xen-orchestra/defined'\nimport getEventValue from 'get-event-value'\nimport Icon from 'icon'\nimport Link from 'link'\nimport React from 'react'\nimport renderXoItem from 'render-xo-item'\nimport SelectBootFirmware from 'select-boot-firmware'\nimport SelectCoresPerSocket from 'select-cores-per-socket'\nimport semver from 'semver'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport TabButton from 'tab-button'\nimport Tooltip from 'tooltip'\nimport { error, success } from 'notification'\nimport { confirm } from 'modal'\nimport { Container, Row, Col } from 'grid'\nimport { CustomFields } from 'custom-fields'\nimport { injectState, provideState } from 'reaclette'\nimport { Number, Select as EditableSelect, Size, Text, XoSelect } from 'editable'\nimport { Select, Toggle } from 'form'\nimport { SelectResourceSet, SelectRole, SelectSubject, SelectUser, SelectVgpuType } from 'select-objects'\nimport { addSubscriptions, connectStore, formatSize, getVirtualizationModeLabel, osFamily } from 'utils'\nimport { every, filter, find, isEmpty, keyBy, map, times, some, uniq } from 'lodash'\nimport {\n  addAcl,\n  changeVirtualizationMode,\n  cloneVm,\n  convertVmToTemplate,\n  createVgpu,\n  createVtpm,\n  createVusb,\n  deleteVgpu,\n  deleteVm,\n  deleteVtpm,\n  deleteVusb,\n  editVm,\n  getVmsHaValues,\n  isPciPassthroughAvailable,\n  isVmRunning,\n  coalesceLeafVm,\n  pauseVm,\n  recoveryStartVm,\n  removeAcl,\n  restartVm,\n  shareVm,\n  startVm,\n  stopVm,\n  subscribeAcls,\n  subscribeGroups,\n  subscribeResourceSets,\n  subscribeUsers,\n  suspendVm,\n  unplugVusb,\n  vmAttachPcis,\n  vmDetachPcis,\n  vmSetUefiMode,\n  vmWarmMigration,\n  XEN_DEFAULT_CPU_CAP,\n  XEN_DEFAULT_CPU_WEIGHT,\n  XEN_VIDEORAM_VALUES,\n} from 'xo'\nimport { createGetObject, createGetObjectsOfType, createSelector, isAdmin } from 'selectors'\nimport { getXoaPlan, CURRENT as XOA_PLAN, ENTERPRISE, PREMIUM } from 'xoa-plans'\nimport { SelectSuspendSr } from 'select-suspend-sr'\n\nimport BootOrder from './boot-order'\nimport VusbCreateModal from './vusb-create-modal'\nimport PciAttachModal from './pci-attach-modal'\nimport XenStoreCreateModal, { XENSTORE_PREFIX } from './xenstore-create-modal'\nimport { subscribeSecurebootReadiness, subscribeGetGuestSecurebootReadiness } from '../../common/xo'\n\n// Button's height = react-select's height(36 px) + react-select's border-width(1 px) * 2\n// https://github.com/JedWatson/react-select/blob/916ab0e62fc7394be8e24f22251c399a68de8b1c/less/select.less#L21, L22\nconst SHARE_BUTTON_STYLE = { height: '38px' }\nconst LEVELS = {\n  admin: 'danger',\n  operator: 'primary',\n  viewer: 'success',\n}\n\nconst STOP_OPERATIONS = [\n  'clean_reboot',\n  'clean_shutdown',\n  'hard_reboot',\n  'hard_shutdown',\n  'pause',\n  'suspend',\n\n  // Even though it's not recognized by `xe (as of 2021-08), it's a valid operation\n  'shutdown',\n]\n\nconst VUSB_COLUMNS = [\n  {\n    name: _('id'),\n    itemRenderer: vusb => {\n      const { uuid } = vusb\n      return (\n        <Copiable data={uuid} tagName='p'>\n          {uuid.slice(4, 8)}\n        </Copiable>\n      )\n    },\n  },\n  {\n    name: _('pusbDescription'),\n    itemRenderer: (vusb, { pusbsByUsbGroup }) => pusbsByUsbGroup[vusb.usbGroup].description,\n  },\n  {\n    name: _('pusbVersion'),\n    itemRenderer: (vusb, { pusbsByUsbGroup }) => pusbsByUsbGroup[vusb.usbGroup].version,\n  },\n  {\n    name: _('pusbSpeed'),\n    itemRenderer: (vusb, { pusbsByUsbGroup }) => pusbsByUsbGroup[vusb.usbGroup].speed,\n  },\n  {\n    name: _('status'),\n    itemRenderer: ({ currentlyAttached, uuid }) => (\n      <StateButton\n        disabled={!currentlyAttached}\n        disabledLabel={_('statusDisconnected')}\n        disabledTooltip={_('vusbRemainUnplugged')}\n        enabledLabel={_('connected')}\n        enabledHandler={unplugVusb}\n        enabledTooltip={_('vusbUnplugTooltip')}\n        state={currentlyAttached}\n        handlerParam={uuid}\n      />\n    ),\n  },\n]\n\nconst PCI_COLUMNS = [\n  {\n    name: _('id'),\n    itemRenderer: (pciId, { pciByPciId }) => {\n      const pci = pciByPciId[pciId]\n      if (pci === undefined) {\n        return _('unknown')\n      }\n      const { uuid } = pci\n      return (\n        <Copiable data={uuid} tagName='p'>\n          {uuid.slice(4, 8)}\n        </Copiable>\n      )\n    },\n  },\n  {\n    default: true,\n    name: _('pciId'),\n    itemRenderer: pciId => pciId,\n    sortCriteria: pciId => pciId,\n  },\n  {\n    name: _('className'),\n    itemRenderer: (pciId, { pciByPciId }) => {\n      const pci = pciByPciId[pciId]\n      return pci === undefined ? _('unknown') : pci.class_name\n    },\n    sortCriteria: (pciId, { pciByPciId }) => pciByPciId[pciId]?.class_name,\n  },\n  {\n    name: _('deviceName'),\n    itemRenderer: (pciId, { pciByPciId }) => {\n      const pci = pciByPciId[pciId]\n      return pci === undefined ? _('unknown') : pci.device_name\n    },\n    sortCriteria: (pciId, { pciByPciId }) => pciByPciId[pciId]?.device_name,\n  },\n]\n\nconst VUSB_INDIVIDUAL_ACTIONS = [\n  {\n    handler: deleteVusb,\n    icon: 'delete',\n    label: _('delete'),\n    level: 'danger',\n  },\n]\n\nconst PCI_ACTIONS = [\n  {\n    handler: (pciIds, { vm }) => vmDetachPcis(vm, pciIds),\n    icon: 'disconnect',\n    label: _('detach'),\n    level: 'danger',\n  },\n]\n\nconst SECUREBOOT_STATUS_MESSAGES = {\n  disabled: _('secureBootNotEnforced'),\n  first_boot: _('secureBootWantedPendingBoot'),\n  ready: _('secureBootEnforced'),\n  ready_no_dbx: _('secureBootNoDbx'),\n  setup_mode: _('secureBootWantedButDisabled'),\n  certs_incomplete: _('secureBootWantedButCertificatesMissing'),\n}\n\nconst forceReboot = vm => restartVm(vm, true)\nconst forceShutdown = vm => stopVm(vm, true)\nconst fullCopy = vm => cloneVm(vm, true)\nconst shareVmProxy = vm => shareVm(vm, vm.resourceSet)\n\n@connectStore(() => {\n  const getAffinityHost = createGetObjectsOfType('host').find((_, { vm }) => ({\n    id: vm.affinityHost,\n  }))\n\n  const getVbds = createGetObjectsOfType('VBD').pick((_, { vm }) => vm.$VBDs)\n  const getVdis = createGetObjectsOfType('VDI').pick(createSelector(getVbds, vbds => map(vbds, 'VDI')))\n  const getSrs = createGetObjectsOfType('SR').pick(createSelector(getVdis, vdis => uniq(map(vdis, '$SR'))))\n  const getSrsContainers = createSelector(getSrs, srs => uniq(map(srs, '$container')))\n\n  const getAffinityHostPredicate = createSelector(\n    getSrsContainers,\n    containers => host => every(containers, container => container === host.$pool || container === host.id)\n  )\n\n  return {\n    affinityHost: getAffinityHost,\n    affinityHostPredicate: getAffinityHostPredicate,\n  }\n})\nclass AffinityHost extends Component {\n  _editAffinityHost = host => editVm(this.props.vm, { affinityHost: host.id || null })\n\n  render() {\n    const { affinityHost, affinityHostPredicate } = this.props\n\n    return (\n      <span>\n        <XoSelect\n          onChange={this._editAffinityHost}\n          predicate={affinityHostPredicate}\n          value={affinityHost}\n          xoType='host'\n        >\n          {affinityHost ? renderXoItem(affinityHost) : _('noAffinityHost')}\n        </XoSelect>{' '}\n        {affinityHost && (\n          <a role='button' onClick={this._editAffinityHost}>\n            <Icon icon='remove' />\n          </a>\n        )}\n      </span>\n    )\n  }\n}\n\n@addSubscriptions({\n  resourceSets: subscribeResourceSets,\n})\n@connectStore({\n  isAdmin,\n})\nclass ResourceSet extends Component {\n  _getResourceSet = createSelector(\n    () => this.props.resourceSets,\n    () => this.props.vm.resourceSet,\n    (resourceSets, resourceSetId) => {\n      const resourceSet = find(resourceSets, { id: resourceSetId })\n      return resourceSet && Object.assign(resourceSet, { type: 'resourceSet' })\n    }\n  )\n\n  render() {\n    const resourceSet = this._getResourceSet()\n    const { vm, isAdmin } = this.props\n\n    return isAdmin ? (\n      <div className='input-group'>\n        <SelectResourceSet\n          onChange={resourceSet =>\n            editVm(vm, {\n              resourceSet: resourceSet != null ? resourceSet.id : resourceSet,\n            })\n          }\n          value={vm.resourceSet}\n        />\n        {resourceSet !== undefined && (\n          <span className='input-group-btn'>\n            <ActionButton\n              btnStyle='primary'\n              handler={shareVmProxy}\n              handlerParam={vm}\n              icon='vm-share'\n              style={SHARE_BUTTON_STYLE}\n              tooltip={_('vmShareButton')}\n            />\n          </span>\n        )}\n      </div>\n    ) : vm.resourceSet === undefined ? (\n      _('resourceSetNone')\n    ) : resourceSet === undefined ? (\n      _('errorUnknownItem', { type: 'resource set' })\n    ) : (\n      <span>\n        {renderXoItem(resourceSet)}{' '}\n        <ActionButton\n          btnStyle='primary'\n          handler={shareVmProxy}\n          handlerParam={vm}\n          icon='vm-share'\n          size='small'\n          tooltip={_('vmShareButton')}\n        />\n      </span>\n    )\n  }\n}\n\nclass NewVgpu extends Component {\n  get value() {\n    return this.state\n  }\n\n  _getPredicate = createSelector(\n    () => this.props.vm && this.props.vm.$pool,\n    poolId => vgpuType => poolId === vgpuType.$pool\n  )\n\n  render() {\n    return (\n      <Container>\n        <Row>\n          <Col size={6}>{_('vmSelectVgpuType')}</Col>\n          <Col size={6}>\n            <SelectVgpuType onChange={this.linkState('vgpuType')} predicate={this._getPredicate()} />\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n\nclass Vgpus extends Component {\n  _createVgpu = vgpuType =>\n    confirm({\n      icon: 'gpu',\n      title: _('vmAddVgpu'),\n      body: <NewVgpu vm={this.props.vm} />,\n    }).then(({ vgpuType }) => createVgpu(this.props.vm, { vgpuType, gpuGroup: vgpuType.gpuGroup }))\n\n  render() {\n    const { vgpus, vm } = this.props\n\n    return (\n      <div>\n        {map(vgpus, vgpu => (\n          <span key={vgpu.id} className='mb-1'>\n            {!isVmRunning(vm) && <ActionButton handler={deleteVgpu} handlerParam={vgpu} icon='delete' size='small' />}{' '}\n            {renderXoItem(vgpu)}\n          </span>\n        ))}\n        {isEmpty(vgpus) && (\n          <span>\n            {!isVmRunning(vm) && <ActionButton handler={this._createVgpu} icon='add' size='small' />} {_('vmVgpuNone')}\n          </span>\n        )}\n      </div>\n    )\n  }\n}\n\nclass CoresPerSocket extends Component {\n  _onChange = coresPerSocket => editVm(this.props.vm, { coresPerSocket })\n\n  render() {\n    const { container, vm } = this.props\n    const { coresPerSocket, CPUs: cpus } = vm\n\n    return (\n      <div>\n        {container != null ? (\n          <SelectCoresPerSocket\n            maxCores={container.cpus.cores}\n            maxVcpus={cpus.max}\n            onChange={this._onChange}\n            value={coresPerSocket}\n          />\n        ) : coresPerSocket !== undefined ? (\n          _('vmSocketsWithCoresPerSocket', {\n            nSockets: cpus.max / coresPerSocket,\n            nCores: coresPerSocket,\n          })\n        ) : (\n          _('vmCoresPerSocketNone')\n        )}\n      </div>\n    )\n  }\n}\n\nclass AddAclsModal extends Component {\n  get value() {\n    return this.state\n  }\n\n  _getPredicate = createSelector(\n    () => this.props.acls,\n    () => this.props.vm,\n    (acls, object) =>\n      ({ id: subject, permission }) =>\n        permission !== 'admin' && !some(acls, { object, subject })\n  )\n\n  render() {\n    const { action, subjects } = this.state\n    return (\n      <form>\n        <div className='form-group'>\n          <SelectSubject\n            multi\n            onChange={this.linkState('subjects')}\n            predicate={this._getPredicate()}\n            value={subjects}\n          />\n        </div>\n        <div className='form-group'>\n          <SelectRole onChange={this.linkState('action')} value={action} />\n        </div>\n      </form>\n    )\n  }\n}\n\nconst Acls = decorate([\n  addSubscriptions({\n    acls: subscribeAcls,\n    groups: cb => subscribeGroups(groups => cb(keyBy(groups, 'id'))),\n    users: cb => subscribeUsers(users => cb(keyBy(users, 'id'))),\n  }),\n  provideState({\n    effects: {\n      addAcls:\n        () =>\n        (state, { acls, vm }) =>\n          confirm({\n            title: _('vmAddAcls'),\n            icon: 'menu-settings-acls',\n            body: <AddAclsModal acls={acls} vm={vm} />,\n          })\n            .then(async ({ action, subjects }) => {\n              if (action == null || isEmpty(subjects)) {\n                error(_('addAclsErrorTitle'), _('addAclsErrorMessage'))\n                return\n              }\n\n              await Promise.all(map(subjects, subject => addAcl({ subject, object: vm, action })))\n            })\n            .catch(err => err && error(_('addAclsErrorTitle'), err.message || String(err))),\n      removeAcl:\n        (_, { currentTarget: { dataset } }) =>\n        (_, { vm: object }) =>\n          removeAcl({\n            action: dataset.action,\n            object,\n            subject: dataset.subject,\n          }),\n    },\n    computed: {\n      rawAcls: (_, { acls, vm }) => filter(acls, { object: vm }),\n      resolvedAcls: ({ rawAcls }, { users, groups }) => {\n        if (users === undefined || groups === undefined) {\n          return []\n        }\n        return rawAcls\n          .map(({ subject, ...acl }) => ({\n            ...acl,\n            subject: defined(users[subject], groups[subject]),\n          }))\n          .filter(({ subject }) => subject !== undefined)\n      },\n    },\n  }),\n  injectState,\n  ({ state: { resolvedAcls }, effects, vm }) => (\n    <Container>\n      {resolvedAcls.slice(0, 5).map(({ subject, action }) => (\n        <Row key={`${subject.id}.${action}`}>\n          <Col>\n            <span>{renderXoItem(subject)}</span> <span className={`tag tag-pill tag-${LEVELS[action]}`}>{action}</span>{' '}\n            <Tooltip content={_('removeAcl')}>\n              <a data-action={action} data-subject={subject.id} onClick={effects.removeAcl} role='button'>\n                <Icon icon='remove' />\n              </a>\n            </Tooltip>\n          </Col>\n        </Row>\n      ))}\n      {resolvedAcls.length > 5 && (\n        <Row>\n          <Col>\n            <Link to={`settings/acls?s=object:${vm}`}>{_('moreAcls', { nAcls: resolvedAcls.length - 5 })}</Link>\n          </Col>\n        </Row>\n      )}\n      <Row>\n        <Col>\n          <ActionButton\n            btnStyle='primary'\n            disabled={XOA_PLAN.value < ENTERPRISE.value}\n            handler={effects.addAcls}\n            icon='add'\n            size='small'\n            tooltip={\n              XOA_PLAN.value < ENTERPRISE.value ? _('availableXoaPlan', { plan: ENTERPRISE.name }) : _('vmAddAcls')\n            }\n          />\n        </Col>\n      </Row>\n    </Container>\n  ),\n])\n\nconst NIC_TYPE_OPTIONS = [\n  {\n    label: 'Realtek RTL8139',\n    value: '',\n  },\n  {\n    label: 'Intel e1000',\n    value: 'e1000',\n  },\n]\n\n@addSubscriptions(({ vm }) => ({\n  vmSecurebootReadiness: subscribeSecurebootReadiness(vm),\n  poolGuestSecurebootReadiness: subscribeGetGuestSecurebootReadiness(vm.$pool),\n}))\n@connectStore(() => {\n  const getVgpus = createGetObjectsOfType('vgpu').pick((_, { vm }) => vm.$VGPUs)\n  const getGpuGroup = createGetObjectsOfType('gpuGroup').pick(createSelector(getVgpus, vgpus => map(vgpus, 'gpuGroup')))\n  const getVusbs = createGetObjectsOfType('VUSB').filter(\n    (_, { vm }) =>\n      vusb =>\n        vusb.vm === vm.id\n  )\n  const getPcisbByHost = createGetObjectsOfType('PCI').groupBy('$host')\n  const getPusbs = createGetObjectsOfType('PUSB')\n  const getAvailablePusbs = getPusbs\n    .pick(\n      createSelector(createGetObjectsOfType('USB_group'), usbGroups =>\n        map(\n          filter(usbGroups, usbGroup => usbGroup.VUSBs[0] === undefined),\n          usbGroup => usbGroup.PUSBs\n        )\n      )\n    )\n    .sort()\n  const getVmHosts = createGetObjectsOfType('host').filter(\n    (_, { vm }) =>\n      host =>\n        host.$pool === vm.$pool\n  )\n\n  return (state, props) => ({\n    availablePusbs: getAvailablePusbs(state, props),\n    gpuGroup: getGpuGroup(state, props),\n    isAdmin: isAdmin(state, props),\n    vgpus: getVgpus(state, props),\n    vmPool: createGetObject((_, props) => get(() => props.vm.$pool))(state, props),\n    pcisByHost: getPcisbByHost(state, props),\n    pusbByUsbGroup: keyBy(getPusbs(state, props), 'usbGroup'),\n    vmHosts: getVmHosts(state, props),\n    vusbs: getVusbs(state, props),\n  })\n})\nexport default class TabAdvanced extends Component {\n  componentDidMount() {\n    getVmsHaValues().then(vmsHaValues => this.setState({ vmsHaValues }))\n  }\n\n  _getCpuMaskOptions = createSelector(\n    () => this.props.vmHosts,\n    vmHosts => {\n      return times(Math.max(...Object.values(vmHosts).map(({ cpus }) => cpus.cores)), number => ({\n        value: number,\n        label: `Core ${number}`,\n      }))\n    }\n  )\n\n  _getCpuMask = createSelector(\n    this._getCpuMaskOptions,\n    () => this.props.vm.cpuMask,\n    (options, cpuMask) => (cpuMask !== undefined ? options.filter(({ value }) => cpuMask.includes(value)) : undefined)\n  )\n\n  _getIsStopBlocked = createSelector(\n    () => this.props.vm && this.props.vm.blockedOperations,\n    blockedOperations => STOP_OPERATIONS.every(op => op in blockedOperations)\n  )\n\n  _getIsMigrationBlocked = createSelector(\n    () => this.props.vm?.blockedOperations,\n    blockedOperations =>\n      blockedOperations !== undefined && ['migrate_send', 'pool_migrate'].some(op => op in blockedOperations)\n  )\n\n  _onChangeBlockMigration = block => {\n    const blockedOperations = this.props.vm.blockedOperations\n\n    const toggleBlockedOperations = () =>\n      editVm(this.props.vm, {\n        blockedOperations: Object.assign.apply(\n          null,\n          ['migrate_send', 'pool_migrate'].map(op => ({ [op]: block ? true : null }))\n        ),\n      })\n\n    if (\n      blockedOperations !== undefined &&\n      ['migrate_send', 'pool_migrate'].some(op => op in blockedOperations && blockedOperations[op].trim() !== 'true')\n    ) {\n      return confirm({\n        title: _('unblockMigrationTitle'),\n        body: (\n          <p>\n            {_('unblockMigrationConfirm')}\n            <ul>\n              {Object.keys(blockedOperations).map(op => {\n                const reason = blockedOperations[op]\n                if ((op === 'migrate_send' || op === 'pool_migrate') && reason.trim() !== 'true') {\n                  return <li key={op}>{reason}</li>\n                }\n                return null\n              })}\n            </ul>\n          </p>\n        ),\n      }).then(() => toggleBlockedOperations())\n    } else {\n      return toggleBlockedOperations()\n    }\n  }\n\n  _onChangeBlockStop = block =>\n    editVm(this.props.vm, {\n      blockedOperations: Object.assign.apply(\n        null,\n        STOP_OPERATIONS.map(op => ({ [op]: block }))\n      ),\n    })\n\n  _onChangeCpuMask = cpuMask => editVm(this.props.vm, { cpuMask: map(cpuMask, 'value') })\n\n  _handleBootFirmware = value =>\n    editVm(this.props.vm, {\n      secureBoot: false,\n      hvmBootFirmware: value !== '' ? value : null,\n    })\n\n  _onNicTypeChange = value => editVm(this.props.vm, { nicType: value === '' ? null : value })\n\n  _getDisabledAddVtpmReason = createSelector(\n    () => this.props.vm,\n    () => this.props.pool,\n    (vm, pool) => {\n      if (pool?.vtpmSupported === false) {\n        return _('vtpmNotSupported')\n      }\n      if (vm.boot.firmware !== 'uefi') {\n        return _('vtpmRequireUefi')\n      }\n      if (vm.power_state !== 'Halted') {\n        return _('vmNeedToBeHalted')\n      }\n    }\n  )\n\n  _getDisabledDeleteVtpmReason = () => {\n    if (this.props.vm.power_state !== 'Halted') {\n      return _('vmNeedToBeHalted')\n    }\n  }\n\n  _handleDeleteVtpm = async vtpm => {\n    await confirm({\n      icon: 'delete',\n      title: _('deleteVtpm'),\n      body: <p>{_('deleteVtpmWarning')}</p>,\n      strongConfirm: {\n        messageId: 'deleteVtpm',\n      },\n    })\n    return deleteVtpm(vtpm)\n  }\n\n  _updateUser = user => editVm(this.props.vm, { creation: { user: user.id } })\n\n  _createVusb = async () => {\n    const pusb = await confirm({\n      title: _('createVusb'),\n      body: <VusbCreateModal pusbs={this.props.availablePusbs} />,\n      icon: 'add',\n    })\n    return createVusb(this.props.vm, pusb.usbGroup)\n  }\n\n  _attachPcis = async () => {\n    const { vm, vmPool } = this.props\n    const pcis = await confirm({\n      body: (\n        <PciAttachModal attachedPciIds={vm.attachedPcis} pcisByHost={this.props.pcisByHost} vm={vm} pool={vmPool} />\n      ),\n      icon: 'add',\n      title: _('attachPcis'),\n    })\n    await vmAttachPcis(vm, pcis)\n  }\n\n  _getPcis = createSelector(\n    () => this.props.vm,\n    () => this.props.pcisByHost,\n    (vm, pcisByHost) => {\n      if (!isVmRunning(vm) && vm.power_state !== 'Paused') {\n        // If the VM is not running, it's not attached to any host, therefore,\n        // we cannot determine which XAPI PCI object is associated with the given PCI_ID (eg: 0000:01:00.4).\n        // This determination depends on the specific host environment.\n        return {}\n      }\n      return keyBy(pcisByHost[vm.$container], 'pci_id')\n    }\n  )\n\n  _getPciAttachButtonTooltip = () => {\n    const { vm, vmHosts, vmPool } = this.props\n    const vmAttachedToHost = vm.$container !== vm.$pool\n\n    if ((vmAttachedToHost && vmHosts[vm.$container] === undefined) || (!vmAttachedToHost && vmPool === undefined)) {\n      return _('notEnoughPermissionsError')\n    }\n\n    return !isPciPassthroughAvailable(vmHosts[vmAttachedToHost ? vm.$container : vmPool.master])\n      ? _('onlyAvailableXcp8.3OrHigher')\n      : undefined\n  }\n\n  _confirmUefiMode = async () => {\n    const { vm, vmSecurebootReadiness } = this.props\n    const confirmNeeded =\n      vmSecurebootReadiness === 'disabled' ||\n      vmSecurebootReadiness === 'ready' ||\n      vmSecurebootReadiness === 'ready_no_dbx'\n\n    if (confirmNeeded) {\n      await confirm({\n        title: _('propagateCertificatesTitle'),\n        body: <p>{_('propagateCertificatesConfirm')}</p>,\n      })\n    }\n\n    await vmSetUefiMode(vm, 'user')\n    success(_('propagateCertificatesTitle'), _('propagateCertificatesSuccessful'))\n  }\n\n  _addXenstoreEntry = async () => {\n    const xsEntry = await confirm({\n      title: _('addXenStoreEntry'),\n      icon: 'add',\n      body: <XenStoreCreateModal />,\n    })\n    if (xsEntry === undefined) {\n      return\n    }\n\n    await editVm(this.props.vm, { xenStoreData: xsEntry })\n  }\n\n  _getOnChangeXenStoreEntry = key => async value => {\n    value = value.trim()\n    await editVm(this.props.vm, {\n      xenStoreData: { [key]: value === '' ? null : value },\n    })\n  }\n\n  _getXenStoreData = createSelector(\n    () => this.props.vm,\n    vm =>\n      Object.entries(vm.xenStoreData)\n        .filter(([key]) => key.startsWith(XENSTORE_PREFIX))\n        .sort()\n  )\n\n  render() {\n    const {\n      container,\n      isAdmin,\n      poolGuestSecurebootReadiness,\n      pusbByUsbGroup,\n      vgpus,\n      vm,\n      vmPool,\n      vmSecurebootReadiness,\n      vusbs,\n    } = this.props\n    const isWarmMigrationAvailable = getXoaPlan().value >= PREMIUM.value\n    const addVtpmTooltip = this._getDisabledAddVtpmReason()\n    const deleteVtpmTooltip = this._getDisabledDeleteVtpmReason()\n    const host = this.props.vmHosts[vm.$container]\n    const isAddVtpmAvailable = addVtpmTooltip === undefined\n    const isDeleteVtpmAvailable = deleteVtpmTooltip === undefined\n    const isDisabled = poolGuestSecurebootReadiness === 'not_ready' || vm.boot.firmware !== 'uefi'\n    const vtpmId = vm.VTPMs[0]\n    const pciAttachButtonTooltip = this._getPciAttachButtonTooltip()\n    const xenstoreData = this._getXenStoreData()\n\n    return (\n      <Container>\n        <Row>\n          <Col className='text-xs-right'>\n            {vm.power_state === 'Running' && (\n              <span>\n                <TabButton\n                  btnStyle='primary'\n                  handler={pauseVm}\n                  handlerParam={vm}\n                  icon='vm-pause'\n                  labelId='pauseVmLabel'\n                />\n                <TabButton\n                  btnStyle='primary'\n                  handler={suspendVm}\n                  handlerParam={vm}\n                  icon='vm-suspend'\n                  labelId='suspendVmLabel'\n                />\n                <TabButton\n                  btnStyle='primary'\n                  handler={coalesceLeafVm}\n                  handlerParam={vm}\n                  icon='vm-coalesce-leaf'\n                  labelId='coalesceLeaf'\n                />\n                <TabButton\n                  btnStyle='warning'\n                  handler={forceReboot}\n                  handlerParam={vm}\n                  icon='vm-force-reboot'\n                  labelId='forceRebootVmLabel'\n                />\n                <TabButton\n                  btnStyle='warning'\n                  handler={forceShutdown}\n                  handlerParam={vm}\n                  icon='vm-force-shutdown'\n                  labelId='forceShutdownVmLabel'\n                />\n                <TabButton\n                  btnStyle='warning'\n                  disabled={!isWarmMigrationAvailable}\n                  handler={vmWarmMigration}\n                  handlerParam={vm}\n                  icon='vm-warm-migration'\n                  labelId='vmWarmMigration'\n                  tooltip={isWarmMigrationAvailable ? undefined : _('availableXoaPremium')}\n                />\n              </span>\n            )}\n            {vm.power_state === 'Halted' && (\n              <span>\n                <TabButton\n                  btnStyle='primary'\n                  handler={recoveryStartVm}\n                  handlerParam={vm}\n                  icon='vm-recovery-mode'\n                  labelId='recoveryModeLabel'\n                />\n                <TabButton\n                  btnStyle='primary'\n                  handler={() => startVm(vm, true)}\n                  icon='vm-start'\n                  labelId='startVmOnLabel'\n                />\n                <TabButton\n                  btnStyle='primary'\n                  handler={fullCopy}\n                  handlerParam={vm}\n                  icon='vm-clone'\n                  labelId='cloneVmLabel'\n                />\n                <TabButton\n                  btnStyle='primary'\n                  handler={coalesceLeafVm}\n                  handlerParam={vm}\n                  icon='vm-coalesce-leaf'\n                  labelId='coalesceLeaf'\n                />\n              </span>\n            )}\n            {vm.power_state === 'Suspended' && (\n              <span>\n                <TabButton\n                  btnStyle='primary'\n                  handler={startVm}\n                  handlerParam={vm}\n                  icon='vm-start'\n                  labelId='resumeVmLabel'\n                />\n                <TabButton\n                  btnStyle='primary'\n                  handler={coalesceLeafVm}\n                  handlerParam={vm}\n                  icon='vm-coalesce-leaf'\n                  labelId='coalesceLeaf'\n                />\n                <TabButton\n                  btnStyle='warning'\n                  handler={forceShutdown}\n                  handlerParam={vm}\n                  icon='vm-force-shutdown'\n                  labelId='forceShutdownVmLabel'\n                />\n              </span>\n            )}\n            {vm.power_state === 'Paused' && (\n              <span>\n                <TabButton\n                  btnStyle='primary'\n                  handler={startVm}\n                  handlerParam={vm}\n                  icon='vm-start'\n                  labelId='resumeVmLabel'\n                />\n                <TabButton\n                  btnStyle='warning'\n                  handler={forceReboot}\n                  handlerParam={vm}\n                  icon='vm-force-reboot'\n                  labelId='forceRebootVmLabel'\n                />\n                <TabButton\n                  btnStyle='warning'\n                  handler={forceShutdown}\n                  handlerParam={vm}\n                  icon='vm-force-shutdown'\n                  labelId='forceShutdownVmLabel'\n                />\n              </span>\n            )}\n            <TabButton\n              btnStyle='danger'\n              disabled={vm.power_state !== 'Halted'}\n              handler={convertVmToTemplate}\n              handlerParam={vm}\n              icon='vm-create-template'\n              labelId='vmConvertToTemplateButton'\n              redirectOnSuccess='/'\n            />\n            <TabButton\n              btnStyle='danger'\n              handler={deleteVm}\n              handlerParam={vm}\n              icon='vm-delete'\n              labelId='vmRemoveButton'\n            />\n          </Col>\n        </Row>\n        {vm.virtualizationMode !== 'pv' && (\n          <Row>\n            <Col>\n              <h3>{_('vdiBootOrder')}</h3>\n              <BootOrder vm={vm} />\n            </Col>\n          </Row>\n        )}\n        <Row>\n          <Col>\n            <h3>{_('xenSettingsLabel')}</h3>\n            <table className='table'>\n              <tbody>\n                <tr>\n                  <th>{_('virtualizationMode')}</th>\n                  <td>\n                    {getVirtualizationModeLabel(vm)}{' '}\n                    {(vm.virtualizationMode === 'pv' || vm.virtualizationMode === 'hvm') && (\n                      <ActionButton\n                        btnStyle='danger'\n                        disabled={vm.power_state !== 'Halted'}\n                        handler={changeVirtualizationMode}\n                        handlerParam={vm}\n                        icon='vm-migrate'\n                        size='small'\n                      >\n                        {_('vmSwitchVirtualizationMode', {\n                          mode: vm.virtualizationMode === 'pv' ? 'HVM' : 'PV',\n                        })}\n                      </ActionButton>\n                    )}\n                  </td>\n                </tr>\n                {vm.virtualizationMode === 'pv' && (\n                  <tr>\n                    <th>{_('pvArgsLabel')}</th>\n                    <td>\n                      <Text value={vm.PV_args} onChange={value => editVm(vm, { PV_args: value })} />\n                    </td>\n                  </tr>\n                )}\n                <tr>\n                  <th>{_('startDelayLabel')}</th>\n                  <td>\n                    <Number value={vm.startDelay} onChange={value => editVm(vm, { startDelay: value })} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('cpuMaskLabel')}</th>\n                  <td>\n                    <EditableSelect\n                      multi\n                      onChange={this._onChangeCpuMask}\n                      options={this._getCpuMaskOptions()}\n                      placeholder={_('selectCpuMask')}\n                      value={this._getCpuMask()}\n                    />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('cpuWeightLabel')}</th>\n                  <td>\n                    <Number\n                      value={vm.cpuWeight == null ? null : vm.cpuWeight}\n                      onChange={value => editVm(vm, { cpuWeight: value })}\n                      nullable\n                    >\n                      {vm.cpuWeight == null\n                        ? _('defaultCpuWeight', {\n                            value: XEN_DEFAULT_CPU_WEIGHT,\n                          })\n                        : vm.cpuWeight}\n                    </Number>\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('cpuCapLabel')}</th>\n                  <td>\n                    <Number\n                      value={vm.cpuCap == null ? null : vm.cpuCap}\n                      onChange={value => editVm(vm, { cpuCap: value })}\n                      nullable\n                    >\n                      {vm.cpuCap == null ? _('defaultCpuCap', { value: XEN_DEFAULT_CPU_CAP }) : vm.cpuCap}\n                    </Number>\n                  </td>\n                </tr>\n                <tr>\n                  <th>\n                    {_('autoPowerOn')}\n                    {vm.auto_poweron && vmPool !== undefined && !vmPool.auto_poweron && (\n                      <Tooltip content={_('poolAutoPoweronDisabled')}>\n                        <a className='btn btn-link btn-sm' onClick={() => editVm(vm, { auto_poweron: true })}>\n                          {' '}\n                          <Icon icon='alarm' className='text-warning' />\n                        </a>\n                      </Tooltip>\n                    )}\n                  </th>\n                  <td>\n                    <Toggle value={Boolean(vm.auto_poweron)} onChange={value => editVm(vm, { auto_poweron: value })} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('protectFromDeletion')}</th>\n                  <td>\n                    <Toggle\n                      value={'destroy' in vm.blockedOperations}\n                      onChange={blockDeletion =>\n                        editVm(vm, {\n                          blockedOperations: { destroy: blockDeletion },\n                        })\n                      }\n                    />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('protectFromShutdown')}</th>\n                  <td>\n                    <Toggle value={this._getIsStopBlocked()} onChange={this._onChangeBlockStop} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('blockMigration')}</th>\n                  <td>\n                    <Toggle value={this._getIsMigrationBlocked()} onChange={this._onChangeBlockMigration} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('windowsUpdateTools')}</th>\n                  <td>\n                    <Toggle value={vm.hasVendorDevice} onChange={value => editVm(vm, { hasVendorDevice: value })} />\n                  </td>\n                </tr>\n                {vm.virtualizationMode === 'hvm' && (\n                  <tr>\n                    <th>\n                      {_('nestedVirt')}{' '}\n                      <Tooltip content={_('nestedVirtualizationWarning')}>\n                        <a\n                          href='https://docs.xcp-ng.org/compute/#-nested-virtualization'\n                          target='_blank'\n                          rel='noreferrer'\n                        >\n                          <Icon icon='alarm' className='text-warning' />\n                        </a>\n                      </Tooltip>\n                    </th>\n                    <td>\n                      <Toggle\n                        disabled={vm.power_state !== 'Halted'}\n                        value={vm.isNestedVirtEnabled}\n                        onChange={value => {\n                          if (semver.satisfies(String(vmPool.platform_version), '>=3.4')) {\n                            editVm(vm, { nestedVirt: value })\n                          } else {\n                            editVm(vm, { expNestedHvm: value })\n                          }\n                        }}\n                      />\n                    </td>\n                  </tr>\n                )}\n                <tr>\n                  <th>{_('ha')}</th>\n                  <td>\n                    <select\n                      className='form-control'\n                      onChange={event => editVm(vm, { high_availability: getEventValue(event) })}\n                      value={vm.high_availability}\n                    >\n                      {map(this.state.vmsHaValues, vmsHaValue => (\n                        <option key={vmsHaValue} value={vmsHaValue}>\n                          {vmsHaValue === '' ? _('vmHaDisabled') : vmsHaValue}\n                        </option>\n                      ))}\n                    </select>\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('vmAffinityHost')}</th>\n                  <td>\n                    <AffinityHost vm={vm} />\n                  </td>\n                </tr>\n                {vm.virtualizationMode === 'hvm' && (\n                  <tr>\n                    <th>{_('vmVgpus')}</th>\n                    <td>\n                      <Vgpus vgpus={vgpus} vm={vm} />\n                    </td>\n                  </tr>\n                )}\n                <tr>\n                  <th>{_('vmNicType')}</th>\n                  <td>\n                    <Select\n                      labelKey='label'\n                      onChange={this._onNicTypeChange}\n                      options={NIC_TYPE_OPTIONS}\n                      required\n                      simpleValue\n                      value={vm.nicType || ''}\n                      valueKey='value'\n                    />\n                  </td>\n                </tr>\n                {vm.virtualizationMode === 'hvm' && (\n                  <tr>\n                    <th>{_('vmVga')}</th>\n                    <td>\n                      <Toggle\n                        value={vm.vga === 'std'}\n                        onChange={value => editVm(vm, { vga: value ? 'std' : 'cirrus' })}\n                      />\n                    </td>\n                  </tr>\n                )}\n                {vm.vga === 'std' && (\n                  <tr>\n                    <th>{_('vmVideoram')}</th>\n                    <td>\n                      <select\n                        className='form-control'\n                        onChange={event => editVm(vm, { videoram: +getEventValue(event) })}\n                        value={vm.videoram}\n                      >\n                        {map(XEN_VIDEORAM_VALUES, val => (\n                          <option key={val} value={val}>\n                            {formatSize(val * 1048576)}\n                          </option>\n                        ))}\n                      </select>\n                    </td>\n                  </tr>\n                )}\n                {vm.virtualizationMode === 'hvm' && (\n                  <tr>\n                    <th>{_('vmBootFirmware')}</th>\n                    <td>\n                      <SelectBootFirmware\n                        host={vm.power_state === 'Running' ? vm.$container : get(() => vmPool.master)}\n                        onChange={this._handleBootFirmware}\n                        value={defined(() => vm.boot.firmware, '')}\n                      />\n                      {/**\n                       * Templates report UEFI is not supported\n                       * See https://xcp-ng.org/forum/post/76412\n                       */}\n                      {vm.boot.firmware !== 'uefi' && !vm.isFirmwareSupported && (\n                        <span className='text-danger font-weight-bold'>\n                          <Icon icon='error' /> {_('firmwareNotSupported')}\n                        </span>\n                      )}\n                    </td>\n                  </tr>\n                )}\n                <tr>\n                  <th>{_('secureBoot')}</th>\n                  <td>\n                    <Tooltip content={vm.boot.firmware !== 'uefi' ? _('availableForUefiOnly') : undefined}>\n                      <Toggle\n                        disabled={vm.boot.firmware !== 'uefi'}\n                        value={vm.secureBoot}\n                        onChange={value => editVm(vm, { secureBoot: value })}\n                      />\n                    </Tooltip>\n                    <a\n                      className='text-muted'\n                      href='https://docs.xcp-ng.org/guides/guest-UEFI-Secure-Boot/'\n                      rel='noreferrer'\n                      style={{ display: 'block' }}\n                      target='_blank'\n                    >\n                      <Icon icon='info' /> {_('secureBootLinkToDocumentationMessage')}\n                    </a>\n                  </td>\n                </tr>\n                {vm.boot.firmware === 'uefi' &&\n                  semver.satisfies(host?.version, '>=8.3.0') && [\n                    <tr key='secureBootStatus'>\n                      <th>{_('secureBootStatus')}</th>\n                      <td>\n                        {SECUREBOOT_STATUS_MESSAGES[vmSecurebootReadiness]}\n                        {(vmSecurebootReadiness === 'setup_mode' ||\n                          vmSecurebootReadiness === 'certs_incomplete' ||\n                          vmSecurebootReadiness === 'ready_no_dbx') &&\n                          host?.productBrand === 'XCP-ng' && (\n                            <a\n                              className='text-warning'\n                              href='https://docs.xcp-ng.org/guides/guest-UEFI-Secure-Boot/#troubleshoot-guest-secure-boot-issues'\n                              rel='noreferrer'\n                              style={{ display: 'block' }}\n                              target='_blank'\n                            >\n                              <Icon icon='alarm' /> {_('secureBootLinkToDocumentationMessage')}\n                            </a>\n                          )}\n                      </td>\n                    </tr>,\n                    <tr key='propagateCertificatesButton'>\n                      <th>{_('propagateCertificatesTitle')} </th>\n                      <td>\n                        <ActionButton\n                          btnStyle='primary'\n                          disabled={isDisabled}\n                          handler={this._confirmUefiMode}\n                          icon='vm-clone'\n                        >\n                          {_('propagateCertificates')}\n                        </ActionButton>\n                        {poolGuestSecurebootReadiness === 'not_ready' && (\n                          <a\n                            className='text-warning'\n                            href='https://docs.xcp-ng.org/guides/guest-UEFI-Secure-Boot/#configure-the-pool'\n                            rel='noreferrer'\n                            style={{ display: 'block' }}\n                            target='_blank'\n                          >\n                            <Icon icon='alarm' /> {_('noSecureBoot')}\n                          </a>\n                        )}\n                      </td>\n                    </tr>,\n                  ]}\n                <tr>\n                  <th>{_('vtpm')}</th>\n                  <td>\n                    {/*\n                    FIXME: add documentation link\n                    <a\n                      className='text-muted'\n                      href='#'\n                      rel='noopener noreferrer'\n                      style={{ display: 'block' }}\n                      target='_blank'\n                    >\n                      <Icon icon='info' /> {_('seeVtpmDocumentation')}\n                    </a> */}\n                    {vtpmId == null ? (\n                      <Tooltip content={addVtpmTooltip}>\n                        <ActionButton\n                          btnStyle='primary'\n                          disabled={!isAddVtpmAvailable}\n                          handler={createVtpm}\n                          handlerParam={vm}\n                          icon='add'\n                        >\n                          {_('createVtpm')}\n                        </ActionButton>\n                      </Tooltip>\n                    ) : (\n                      <div>\n                        <Tooltip content={deleteVtpmTooltip}>\n                          <ActionButton\n                            btnStyle='danger'\n                            disabled={!isDeleteVtpmAvailable}\n                            handler={this._handleDeleteVtpm}\n                            handlerParam={vtpmId}\n                            icon='delete'\n                          >\n                            {_('deleteVtpm')}\n                          </ActionButton>\n                        </Tooltip>\n                        <table className='table mt-1'>\n                          <tbody>\n                            <tr>\n                              <th>{_('uuid')}</th>\n                              <Copiable tagName='td' data={vtpmId}>\n                                {vtpmId.slice(0, 4)}\n                              </Copiable>\n                            </tr>\n                          </tbody>\n                        </table>\n                      </div>\n                    )}\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('customFields')}</th>\n                  <td>\n                    <CustomFields object={vm.id} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('suspendSr')}</th>\n                  <td>\n                    <SelectSuspendSr vm={vm} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('viridian')}</th>\n                  <td>\n                    <Toggle value={vm.viridian} onChange={value => editVm(vm, { viridian: value })} />\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n            <br />\n            <h3>{_('xenStore')}</h3>\n            <div className='text-info'>\n              <i className='d-block'>\n                <Icon icon='info' /> {_('rebootRequiredAfterXenStoreChanges')}\n              </i>\n              <i className='d-block'>\n                <Icon icon='info' /> {_('deleteEntryDeleteValue')}\n              </i>\n            </div>\n            <br />\n            <ActionButton btnStyle='primary' handler={this._addXenstoreEntry} icon='add'>\n              {_('addXenStoreEntry')}\n            </ActionButton>\n            <table className='mt-1 table table-hover'>\n              <tbody>\n                {xenstoreData.map(([key, value]) => (\n                  <tr key={key}>\n                    <th>{key}</th>\n                    <td>\n                      <Text value={value} onChange={this._getOnChangeXenStoreEntry(key)} />\n                    </td>\n                  </tr>\n                ))}\n              </tbody>\n            </table>\n            <br />\n            <h3>{_('vmLimitsLabel')}</h3>\n            <table className='table table-hover'>\n              <tbody>\n                <tr>\n                  <th>{_('vmCpuLimitsLabel')}</th>\n                  <td>\n                    <Number value={vm.CPUs.number} onChange={CPUs => editVm(vm, { CPUs })} />/\n                    {vm.power_state === 'Running' ? (\n                      vm.CPUs.max\n                    ) : (\n                      <Number value={vm.CPUs.max} onChange={cpusMax => editVm(vm, { cpusMax })} />\n                    )}\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('vmCpuTopology')}</th>\n                  <td>\n                    <CoresPerSocket container={container} vm={vm} />\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('vmMemoryLimitsLabel')}</th>\n                  <td>\n                    <p>\n                      Static: {formatSize(vm.memory.static[0])}/\n                      <Size\n                        value={defined(vm.memory.static[1], null)}\n                        onChange={memoryStaticMax => editVm(vm, { memoryStaticMax })}\n                      />\n                    </p>\n                    <p>\n                      Dynamic:{' '}\n                      <Size\n                        value={defined(vm.memory.dynamic[0], null)}\n                        onChange={memoryMin => editVm(vm, { memoryMin })}\n                      />\n                      /\n                      <Size\n                        value={defined(vm.memory.dynamic[1], null)}\n                        onChange={memoryMax => editVm(vm, { memoryMax })}\n                      />\n                    </p>\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n            <br />\n            <h3>{_('guestOsLabel')}</h3>\n            <table className='table table-hover'>\n              <tbody>\n                <tr>\n                  <th>{_('managementAgentVersion')}</th>\n                  <td>{defined(vm.pvDriversVersion, _('unknown'))}</td>\n                </tr>\n                <tr>\n                  <th>{_('osName')}</th>\n                  <td>\n                    {isEmpty(vm.os_version) ? (\n                      _('unknownOsName')\n                    ) : (\n                      <span>\n                        <Icon className='text-info' icon={osFamily(vm.os_version.distro)} />\n                        &nbsp;\n                        {vm.os_version.name}\n                      </span>\n                    )}\n                  </td>\n                </tr>\n                <tr>\n                  <th>{_('osKernel')}</th>\n                  <td>{(vm.os_version && vm.os_version.uname) || _('unknownOsKernel')}</td>\n                </tr>\n              </tbody>\n            </table>\n            <br />\n            <h3>{_('vusbs')}</h3>\n            <ActionButton btnStyle='primary' handler={this._createVusb} icon='add'>\n              {_('createVusb')}\n            </ActionButton>\n            <SortedTable\n              collection={vusbs}\n              columns={VUSB_COLUMNS}\n              data-pusbsByUsbGroup={pusbByUsbGroup}\n              individualActions={VUSB_INDIVIDUAL_ACTIONS}\n            />\n            <br />\n            <h3>{_('attachedPcis')}</h3>\n            <div className='text-info'>\n              <i className='d-block'>\n                <Icon icon='info' /> {_('infoUnknownPciOnNonRunningVm')}\n              </i>\n              <i className='d-block'>\n                <Icon icon='info' /> {_('attachingDetachingPciNeedVmBoot')}\n              </i>\n            </div>\n            <ActionButton\n              btnStyle='primary'\n              disabled={pciAttachButtonTooltip !== undefined}\n              handler={this._attachPcis}\n              icon='connect'\n              tooltip={pciAttachButtonTooltip}\n            >\n              {_('attachPcis')}\n            </ActionButton>\n            <SortedTable\n              actions={PCI_ACTIONS}\n              collection={vm.attachedPcis}\n              columns={PCI_COLUMNS}\n              data-pciByPciId={this._getPcis()}\n              data-vm={vm}\n              stateUrlParam='s_pcis'\n            />\n            <br />\n            <h3>{_('miscLabel')}</h3>\n            <table className='table table-hover'>\n              <tbody>\n                <tr>\n                  <th>{_('resourceSet')}</th>\n                  <td>\n                    <ResourceSet vm={vm} />\n                  </td>\n                </tr>\n                {isAdmin && (\n                  <tr>\n                    <th>{_('vmAcls')}</th>\n                    <td>\n                      <Acls vm={vm.id} />\n                    </td>\n                  </tr>\n                )}\n                {isAdmin && (\n                  <tr>\n                    <th>{_('vmCreator')}</th>\n                    <td>\n                      <SelectUser onChange={this._updateUser} value={vm.creation?.user} />\n                    </td>\n                  </tr>\n                )}\n              </tbody>\n            </table>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport decorate from 'apply-decorators'\nimport React from 'react'\nimport { adminOnly } from 'utils'\nimport { createPredicate } from 'value-matcher'\nimport { injectState, provideState } from 'reaclette'\nimport { filter, omit } from 'lodash'\nimport { subscribeBackupNgJobs } from 'xo'\n\nimport JobsTable from '../backup/overview/tab-jobs'\n\nfunction hasOnlyKey(obj, key) {\n  const keys = Object.keys(obj)\n  return keys.length === 1 && keys[0] === key\n}\n\nconst BackupTab = decorate([\n  adminOnly,\n  addSubscriptions({\n    jobs: subscribeBackupNgJobs,\n  }),\n  provideState({\n    computed: {\n      jobIds: ({ predicate }, { jobs }) => filter(jobs, predicate).map(_ => _.id),\n      predicate:\n        (_, { vm }) =>\n        ({ vms }) => {\n          if (vms === undefined) {\n            return false\n          }\n\n          // simple mode\n          if (hasOnlyKey(vms, 'id')) {\n            const { id } = vms\n            if (id === vm.id) {\n              return true\n            }\n\n            if (hasOnlyKey(id, '__or') && Array.isArray(id.__or)) {\n              return id.__or.includes(vm.id)\n            }\n          }\n\n          // smart mode\n          if (vm.tags.some(t => t.split('=', 1)[0] === 'xo:no-bak')) {\n            // handle xo:no-bak and xo:no-bak=reason tags. For example : VMs from Health Check\n            return false\n          }\n          return createPredicate(omit(vms, 'power_state'))(vm)\n        },\n    },\n  }),\n  injectState,\n  ({ state: { jobIds, predicate } }) => {\n    return (\n      <div>\n        <div>\n          <a href={`#/backup/overview?s=${encodeURIComponent(`id: |(${jobIds.join(' ')})`)}`}>{_('goToBackupPage')}</a>\n        </div>\n        <div className='mt-2'>\n          <JobsTable main={false} predicate={predicate} />\n        </div>\n      </div>\n    )\n  },\n])\n\nexport default BackupTab\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Button from 'button'\nimport Component from 'base-component'\nimport CopyToClipboard from 'react-copy-to-clipboard'\nimport cookies from 'js-cookie'\nimport debounce from 'lodash/debounce'\nimport getEventValue from 'get-event-value'\nimport Icon from 'icon'\nimport invoke from 'invoke'\nimport IsoDevice from 'iso-device'\nimport NoVnc from 'react-novnc'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { isVmRunning, resolveUrl } from 'xo'\nimport { Col, Container, Row } from 'grid'\nimport { confirm, form } from 'modal'\nimport { CpuSparkLines, MemorySparkLines, NetworkSparkLines, XvdSparkLines } from 'xo-sparklines'\n\n// add `[]` around the hostname if it's an IPv6 address\nconst formatHostname = h => (h.indexOf(':') !== -1 ? `[${h}]` : h)\n\nclass SendToClipboard extends Component {\n  state = { value: this.props.clipboard }\n\n  get value() {\n    return this.state.value\n  }\n\n  _selectContent = ref => {\n    if (ref !== null) {\n      ref.select()\n    }\n  }\n\n  render() {\n    return (\n      <div>\n        <textarea\n          className='form-control text-monospace'\n          onChange={this.linkState('value')}\n          ref={this._selectContent}\n          rows={10}\n          value={this.state.value}\n        />\n      </div>\n    )\n  }\n}\n\nexport default class TabConsole extends Component {\n  state = { clipboard: '', scale: 1 }\n\n  componentWillReceiveProps(props) {\n    if (isVmRunning(this.props.vm) && !isVmRunning(props.vm) && this.state.minimalLayout) {\n      this._toggleMinimalLayout()\n    }\n  }\n\n  _sendCtrlAltDel = async () => {\n    await confirm({\n      icon: 'vm-keyboard',\n      title: _('ctrlAltDelButtonLabel'),\n      body: _('ctrlAltDelConfirmation'),\n    })\n    this.refs.noVnc.sendCtrlAltDel()\n  }\n\n  _getRemoteClipboard = clipboard => {\n    this.setState({ clipboard })\n  }\n\n  _setRemoteClipboard = invoke(() => {\n    const setRemoteClipboard = debounce(value => {\n      this.setState({ clipboard: value })\n      this.refs.noVnc.setClipboard(value)\n    }, 200)\n    return event => setRemoteClipboard(getEventValue(event))\n  })\n\n  _openClipboardModal = async () =>\n    this._setRemoteClipboard(\n      await confirm({\n        icon: 'multiline-clipboard',\n        title: _('sendToClipboard'),\n        body: <SendToClipboard clipboard={this.state.clipboard} />,\n      })\n    )\n\n  _toggleMinimalLayout = () => {\n    this.props.toggleHeader()\n    this.setState({ minimalLayout: !this.state.minimalLayout })\n  }\n\n  _openSsh = (username = 'root') => {\n    window.location = `ssh://${encodeURIComponent(username)}@${formatHostname(this.props.vm.mainIpAddress)}`\n  }\n\n  _openSshMore = async () => {\n    const cookieKey = `${this.props.vm.uuid}/ssh-user-name`\n    const username = await form({\n      defaultValue: cookies.get(cookieKey) || 'root',\n      header: _('sshUsernameLabel'),\n      render: ({ value, onChange }) => (\n        <div>\n          <input type='text' className='form-control' onChange={onChange} value={value} />\n        </div>\n      ),\n    })\n    if (username !== (cookies.get(cookieKey) || 'root')) {\n      cookies.set(cookieKey, username, { expires: 31 }) // 31 days\n    }\n    this._openSsh(username)\n  }\n\n  _openRdp = () => {\n    window.location = `rdp://${formatHostname(this.props.vm.mainIpAddress)}`\n  }\n\n  _onChangeScaleValue = event => {\n    const value = event.target.value\n    this.setState({ scale: value / 100 })\n  }\n\n  render() {\n    const { statsOverview, vm } = this.props\n    const { minimalLayout, scale } = this.state\n    const canSshOrRdp = vm.mainIpAddress !== undefined\n\n    if (!isVmRunning(vm)) {\n      return (\n        <Container>\n          <p>Console is only available for running VMs.</p>\n        </Container>\n      )\n    }\n\n    return (\n      <Container>\n        {!minimalLayout && statsOverview && (\n          <Row className='text-xs-center'>\n            <Col mediumSize={3}>\n              <p>\n                <Icon icon='cpu' size={2} /> <CpuSparkLines data={statsOverview} />\n              </p>\n            </Col>\n            <Col mediumSize={3}>\n              <p>\n                <Icon icon='memory' size={2} /> <MemorySparkLines data={statsOverview} />\n              </p>\n            </Col>\n            <Col mediumSize={3}>\n              <p>\n                <Icon icon='network' size={2} /> <NetworkSparkLines data={statsOverview} />\n              </p>\n            </Col>\n            <Col mediumSize={3}>\n              <p>\n                <Icon icon='disk' size={2} /> <XvdSparkLines data={statsOverview} />\n              </p>\n            </Col>\n          </Row>\n        )}\n        <Row>\n          <Col mediumSize={3}>\n            <IsoDevice vm={vm} />\n          </Col>\n          <Col mediumSize={3}>\n            <div className='input-group'>\n              <span className='input-group-btn'>\n                <ActionButton\n                  handler={this._openClipboardModal}\n                  icon='multiline-clipboard'\n                  tooltip={_('multilineCopyToClipboard')}\n                />\n              </span>\n              <input\n                className='form-control'\n                onChange={this._setRemoteClipboard}\n                type='text'\n                value={this.state.clipboard}\n              />\n              <span className='input-group-btn'>\n                <CopyToClipboard text={this.state.clipboard}>\n                  <Button>\n                    <Icon icon='clipboard' /> {_('copyToClipboardLabel')}\n                  </Button>\n                </CopyToClipboard>\n              </span>\n            </div>\n          </Col>\n          <Col mediumSize={5} largeSize={3}>\n            <div className='btn-group'>\n              <span className='input-group-btn'>\n                <ActionButton\n                  handler={this._openSsh}\n                  tooltip={canSshOrRdp ? _('sshRootTooltip') : _('remoteNeedClientTools')}\n                  disabled={!canSshOrRdp}\n                  icon='remote'\n                >\n                  {_('sshRootLabel')}\n                </ActionButton>\n              </span>\n              <span className='input-group-btn'>\n                <ActionButton\n                  handler={this._openSshMore}\n                  tooltip={canSshOrRdp ? _('sshUserTooltip') : _('remoteNeedClientTools')}\n                  disabled={!canSshOrRdp}\n                  icon='remote'\n                >\n                  {_('sshUserLabel')}\n                </ActionButton>\n              </span>\n              <span className='input-group-btn'>\n                <ActionButton\n                  handler={this._openRdp}\n                  tooltip={canSshOrRdp ? _('rdpRootTooltip') : _('remoteNeedClientTools')}\n                  disabled={!canSshOrRdp}\n                  icon='rdp'\n                >\n                  {_('rdp')}\n                </ActionButton>\n              </span>\n              <span className='input-group-btn'>\n                <ActionButton handler={this._sendCtrlAltDel} tooltip={_('ctrlAltDelButtonLabel')} icon='vm-keyboard' />\n              </span>\n            </div>\n          </Col>\n          <Col mediumSize={2} className='hidden-lg-down'>\n            <Row>\n              <Col mediumSize={8}>\n                <input\n                  className='form-control'\n                  max={3}\n                  min={0.1}\n                  onChange={this.linkState('scale')}\n                  step={0.1}\n                  type='range'\n                  value={scale}\n                />\n              </Col>\n              <Col mediumSize={4}>\n                <input\n                  className='form-control'\n                  onChange={this._onChangeScaleValue}\n                  step='1'\n                  type='number'\n                  value={Math.round(this.state.scale * 100)}\n                  min={1}\n                  max={300}\n                />\n              </Col>\n            </Row>\n          </Col>\n          <Col mediumSize={1}>\n            <Tooltip content={minimalLayout ? _('showHeaderTooltip') : _('hideHeaderTooltip')}>\n              <Button onClick={this._toggleMinimalLayout}>\n                <Icon icon={minimalLayout ? 'caret' : 'caret-up'} />\n              </Button>\n            </Tooltip>\n          </Col>\n        </Row>\n        <Row className='console'>\n          <Col>\n            {vm.other.disable_pv_vnc === '1' ? (\n              _('disabledConsole')\n            ) : (\n              <NoVnc\n                onClipboardChange={this._getRemoteClipboard}\n                ref='noVnc'\n                scale={scale}\n                url={resolveUrl(`consoles/${vm.id}`)}\n              />\n            )}\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionRowButton from 'action-row-button'\nimport ButtonGroup from 'button-group'\nimport isEmpty from 'lodash/isEmpty'\nimport React, { Component } from 'react'\nimport SortedTable from 'sorted-table'\nimport Tooltip from 'tooltip'\nimport { FormattedRelative, FormattedTime } from 'react-intl'\nimport { Container, Row, Col } from 'grid'\nimport { startContainer, stopContainer, pauseContainer, unpauseContainer, restartContainer } from 'xo'\n\nconst CONTAINER_COLUMNS = [\n  {\n    name: _('containerName'),\n    itemRenderer: container => container.entry.names,\n    sortCriteria: container => container.entry.names,\n    sortOrder: 'asc',\n  },\n  {\n    name: _('containerCommand'),\n    itemRenderer: container => container.entry.command,\n    sortCriteria: container => container.entry.command,\n  },\n  {\n    name: _('containerCreated'),\n    itemRenderer: container => (\n      <span>\n        <FormattedTime\n          value={container.entry.created * 1000}\n          minute='numeric'\n          hour='numeric'\n          day='numeric'\n          month='long'\n          year='numeric'\n        />{' '}\n        (<FormattedRelative value={container.entry.created * 1000} />)\n      </span>\n    ),\n    sortCriteria: container => container.entry.created,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('containerStatus'),\n    itemRenderer: container => container.entry.status,\n    sortCriteria: container => container.entry.status,\n  },\n  {\n    action: _('containerAction'),\n    itemRenderer: (container, vm) => (\n      <ButtonGroup>\n        {container.entry.status === 'Up' && [\n          <Tooltip key={1} content={_('containerStop')}>\n            <ActionRowButton\n              btnStyle='primary'\n              handler={() => stopContainer(vm, container.entry.container)}\n              icon='vm-stop'\n            />\n          </Tooltip>,\n          <Tooltip key={2} content={_('containerRestart')}>\n            <ActionRowButton\n              btnStyle='primary'\n              handler={() => restartContainer(vm, container.entry.container)}\n              icon='vm-reboot'\n            />\n          </Tooltip>,\n          <Tooltip key={3} content={_('containerPause')}>\n            <ActionRowButton\n              btnStyle='primary'\n              handler={() => pauseContainer(vm, container.entry.container)}\n              icon='vm-suspend'\n            />\n          </Tooltip>,\n        ]}\n        {container.entry.status === 'Exited (137)' && (\n          <Tooltip content={_('containerStart')}>\n            <ActionRowButton\n              btnStyle='primary'\n              handler={() => startContainer(vm, container.entry.container)}\n              icon='vm-start'\n            />\n          </Tooltip>\n        )}\n        {container.entry.status === 'Up (Paused)' && (\n          <Tooltip content={_('containerResume')}>\n            <ActionRowButton\n              btnStyle='primary'\n              handler={() => unpauseContainer(vm, container.entry.container)}\n              icon='vm-start'\n            />\n          </Tooltip>\n        )}\n      </ButtonGroup>\n    ),\n  },\n]\n\nexport default class TabContainers extends Component {\n  render() {\n    const { vm } = this.props\n    if (isEmpty(vm.docker.containers)) {\n      return (\n        <Row>\n          <Col className='text-xs-center mt-1'>\n            <h4>{_('noContainers')}</h4>\n          </Col>\n        </Row>\n      )\n    }\n\n    return (\n      <Container>\n        <Row>\n          <Col>\n            <SortedTable\n              collection={vm.docker.containers}\n              columns={CONTAINER_COLUMNS}\n              stateUrlParam='s'\n              userData={vm}\n            />\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport copy from 'copy-to-clipboard'\nimport defined, { get as getDefined } from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport IsoDevice from 'iso-device'\nimport MigrateVdiModalBody from 'xo/migrate-vdi-modal'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport StateButton from 'state-button'\nimport SortedTable from 'sorted-table'\nimport TabButton from 'tab-button'\nimport { compact, every, filter, find, forEach, get, map, reduce, some, sortedUniq, uniq } from 'lodash'\nimport { Sr, Vdi } from 'render-xo-item'\nimport { Container, Row, Col } from 'grid'\nimport {\n  createCollectionWrapper,\n  createGetObjectsOfType,\n  createSelector,\n  createFilter,\n  createFinder,\n  getCheckPermissions,\n  getResolvedResourceSet,\n  isAdmin,\n} from 'selectors'\nimport {\n  addSubscriptions,\n  connectStore,\n  createCompare,\n  createCompareContainers,\n  formatSize,\n  generateReadableRandomString,\n  noop,\n  resolveResourceSet,\n} from 'utils'\nimport { SizeInput, Toggle } from 'form'\nimport { XoSelect, Size, Text } from 'editable'\nimport { confirm } from 'modal'\nimport { error } from 'notification'\nimport {\n  attachDiskToVm,\n  createDisk,\n  connectVbd,\n  deleteVbd,\n  deleteVbds,\n  deleteVdi,\n  deleteVdis,\n  disconnectVbd,\n  editVdi,\n  exportVdi,\n  importVdi,\n  isSrShared,\n  isSrWritable,\n  isVmRunning,\n  migrateVdi,\n  setBootableVbd,\n  setCbt,\n  subscribeResourceSets,\n} from 'xo'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { FormattedRelative, injectIntl } from 'react-intl'\nimport { SelectResourceSetsSr, SelectSr as SelectAnySr, SelectVdi } from 'select-objects'\n\nconst compareSrs = createCompare([isSrShared])\n\n@connectStore(() => ({\n  isAdmin,\n}))\nclass VdiSr extends Component {\n  _getCompareContainers = createSelector(\n    () => this.props.userData.vm.$pool,\n    poolId => createCompareContainers(poolId)\n  )\n\n  _getSrPredicate = createSelector(\n    () => this.props.userData.vm.$pool,\n    poolId => sr => sr.$pool === poolId && isSrWritable(sr)\n  )\n\n  _onChangeSr = sr => {\n    const {\n      item: { vdi },\n      userData: { resourceSet },\n    } = this.props\n    return migrateVdi(\n      vdi,\n      sr,\n      getDefined(() => resourceSet.id)\n    )\n  }\n\n  render() {\n    const {\n      isAdmin,\n      item: { vdiSr },\n      userData: { resourceSet },\n    } = this.props\n    const self = !isAdmin && resourceSet !== undefined\n    return (\n      vdiSr !== undefined && (\n        <XoSelect\n          compareContainers={this._getCompareContainers()}\n          compareOptions={compareSrs}\n          labelProp='name_label'\n          onChange={this._onChangeSr}\n          predicate={this._getSrPredicate()}\n          resourceSet={self ? resourceSet : undefined}\n          useLongClick\n          value={vdiSr}\n          xoType={self ? 'resourceSetSr' : 'SR'}\n        >\n          <Sr id={vdiSr.id} link={!self} self={self} />\n        </XoSelect>\n      )\n    )\n  }\n}\n\nconst COLUMNS_VM_PV = [\n  {\n    itemRenderer: ({ vdi }) => <Text value={vdi.name_label} onChange={value => editVdi(vdi, { name_label: value })} />,\n    name: _('vdiNameLabel'),\n    sortCriteria: 'vdi.name_label',\n  },\n  {\n    itemRenderer: ({ vdi }) => (\n      <Text value={vdi.name_description} onChange={value => editVdi(vdi, { name_description: value })} />\n    ),\n    name: _('vdiNameDescription'),\n    sortCriteria: 'vdi.name_description',\n  },\n  {\n    itemRenderer: ({ vdi }) => <Size value={defined(vdi.size, null)} onChange={size => editVdi(vdi, { size })} />,\n    name: _('vdiSize'),\n    sortCriteria: 'vdi.size',\n  },\n  {\n    itemRenderer: ({ vdi }) => <Toggle value={vdi.cbt_enabled} onChange={cbt => setCbt(vdi, cbt)} />,\n    name: _('vbdCbt'),\n    sortCriteria: 'vdi.cbt_enabled',\n  },\n  {\n    component: VdiSr,\n    name: _('vdiSr'),\n    sortCriteria: ({ vdiSr }) => vdiSr !== undefined && vdiSr.name_label,\n  },\n  {\n    default: true,\n    itemRenderer: vbd => <span>{vbd.device}</span>,\n    name: _('vbdDevice'),\n    sortCriteria: vbd => +vbd.position,\n  },\n  {\n    itemRenderer: vbd => <Toggle onChange={bootable => setBootableVbd(vbd, bootable)} value={vbd.bootable} />,\n    name: _('vbdBootableStatus'),\n    id: 'vbdBootableStatus',\n  },\n  {\n    itemRenderer: (vbd, { vm }) => (\n      <StateButton\n        disabledLabel={_('vbdStatusDisconnected')}\n        disabledHandler={connectVbd}\n        disabledTooltip={_('vbdConnect')}\n        enabledLabel={_('vbdStatusConnected')}\n        enabledHandler={disconnectVbd}\n        enabledTooltip={_('vbdDisconnect')}\n        disabled={!(vbd.attached || isVmRunning(vm))}\n        handlerParam={vbd}\n        state={vbd.attached}\n      />\n    ),\n    name: _('vbdStatus'),\n  },\n]\n\nconst COLUMNS = filter(COLUMNS_VM_PV, col => col.id !== 'vbdBootableStatus')\n\nconst PROGRESS_STYLES = { margin: 0 }\n\nconst COLUMNS_VDI_TASKS = [\n  {\n    itemRenderer: task => task.name_label,\n    name: _('name'),\n    sortCriteria: 'name_label',\n  },\n  {\n    itemRenderer: task => <Vdi id={task.details.vdiId} />,\n    name: _('object'),\n    sortCriteria: 'details.vdiName',\n  },\n  {\n    itemRenderer: task => task.details.action,\n    name: _('action'),\n    sortCriteria: 'details.action',\n  },\n  {\n    itemRenderer: task => formatSize(task.details.length),\n    name: _('size'),\n    sortCriteria: 'details.length',\n  },\n  {\n    itemRenderer: task => (\n      <progress style={PROGRESS_STYLES} className='progress' value={task.progress * 100} max='100' />\n    ),\n    name: _('progress'),\n    sortCriteria: 'progress',\n  },\n  {\n    itemRenderer: task => <FormattedRelative value={task.created * 1000} />,\n    name: _('taskStarted'),\n    sortCriteria: 'created',\n  },\n  {\n    itemRenderer: task => {\n      const started = task.created * 1000\n      const { progress } = task\n\n      if (progress === 0 || progress === 1) {\n        return // not yet started or already finished\n      }\n      return <FormattedRelative value={started + (Date.now() - started) / progress} />\n    },\n    name: _('taskEstimatedEnd'),\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  ...(process.env.XOA_PLAN > 1\n    ? [\n        {\n          handler: vbd => exportVdi(vbd.vdi),\n          icon: 'export',\n          label: _('exportVdi'),\n        },\n        {\n          disabled: vbd => vbd.attached,\n          handler: vbd => importVdi(vbd.vdi),\n          icon: 'import',\n          label: _('importVdi'),\n        },\n      ]\n    : []),\n  {\n    handler: vbd => copy(vbd.vdi.uuid),\n    icon: 'clipboard',\n    label: vbd => _('copyUuid', { uuid: vbd.vdi.uuid }),\n  },\n]\n\n@injectIntl\n@addSubscriptions({\n  resourceSets: subscribeResourceSets,\n})\n@connectStore({\n  isAdmin,\n})\nclass NewDisk extends Component {\n  static propTypes = {\n    checkSr: PropTypes.func.isRequired,\n    onClose: PropTypes.func,\n    vm: PropTypes.object.isRequired,\n  }\n\n  state = {\n    name: `${this.props.vm.name_label}_${generateReadableRandomString(5)}`,\n  }\n\n  _createDisk = () => {\n    const { vm, onClose = noop } = this.props\n    const { bootable, name, readOnly, size, sr } = this.state\n\n    return createDisk(name, size, sr, {\n      vm,\n      bootable,\n      mode: readOnly ? 'RO' : 'RW',\n    }).then(onClose)\n  }\n\n  // FIXME: duplicate code\n  _getSrPredicate = createSelector(\n    () => {\n      const { vm } = this.props\n      return vm && vm.$pool\n    },\n    poolId => sr => sr.$pool === poolId && isSrWritable(sr)\n  )\n\n  _getResourceSet = createFinder(\n    () => this.props.resourceSets,\n    createSelector(\n      () => this.props.vm.resourceSet,\n      id => resourceSet => resourceSet.id === id\n    )\n  )\n\n  _getResolvedResourceSet = createSelector(this._getResourceSet, resolveResourceSet)\n\n  _getResourceSetDiskLimit = createSelector(this._getResourceSet, resourceSet =>\n    get(resourceSet, 'limits.disk.available')\n  )\n\n  _checkSr = createSelector(\n    () => this.props.checkSr,\n    () => this.state.sr,\n    (check, sr) => check(sr)\n  )\n\n  render() {\n    const { vm, isAdmin } = this.props\n    const { formatMessage } = this.props.intl\n    const { size, sr, name, bootable, readOnly } = this.state\n\n    const diskLimit = this._getResourceSetDiskLimit()\n    const resourceSet = this._getResolvedResourceSet()\n\n    const SelectSr = isAdmin || resourceSet == null ? SelectAnySr : SelectResourceSetsSr\n\n    return (\n      <form id='newDiskForm'>\n        <div className='form-group'>\n          <SelectSr\n            onChange={this.linkState('sr')}\n            predicate={this._getSrPredicate()}\n            required\n            resourceSet={isAdmin ? undefined : resourceSet}\n            value={sr}\n          />\n        </div>\n        <fieldset className='form-inline'>\n          <div className='form-group'>\n            <input\n              type='text'\n              onChange={this.linkState('name')}\n              value={name}\n              placeholder={formatMessage(messages.vbdNamePlaceHolder)}\n              className='form-control'\n              required\n            />\n          </div>{' '}\n          <div className='form-group'>\n            <SizeInput\n              onChange={this.linkState('size')}\n              value={size}\n              placeholder={formatMessage(messages.vbdSizePlaceHolder)}\n              required\n            />\n          </div>{' '}\n          <div className='form-group'>\n            {vm.virtualizationMode === 'pv' && (\n              <span>\n                {_('vbdBootable')} <Toggle onChange={this.toggleState('bootable')} value={bootable} />{' '}\n              </span>\n            )}\n            <span>\n              {_('vbdReadonly')} <Toggle onChange={this.toggleState('readOnly')} value={readOnly} />\n            </span>\n          </div>\n          <span className='pull-right'>\n            <ActionButton\n              form='newDiskForm'\n              icon='add'\n              btnStyle='primary'\n              handler={this._createDisk}\n              disabled={!isAdmin && diskLimit < size}\n            >\n              {_('vbdCreate')}\n            </ActionButton>\n          </span>\n        </fieldset>\n        {!this._checkSr() && (\n          <div>\n            <span className='text-danger'>\n              <Icon icon='alarm' /> {_('warningVdiSr')}\n            </span>\n          </div>\n        )}\n        {resourceSet != null &&\n          diskLimit != null &&\n          (diskLimit < size ? (\n            <em className='text-danger'>\n              {_('notEnoughSpaceInResourceSet', {\n                resourceSet: <strong>{resourceSet.name}</strong>,\n                spaceLeft: formatSize(diskLimit),\n              })}\n            </em>\n          ) : (\n            <em>\n              {_('useQuotaWarning', {\n                resourceSet: <strong>{resourceSet.name}</strong>,\n                spaceLeft: formatSize(diskLimit),\n              })}\n            </em>\n          ))}\n      </form>\n    )\n  }\n}\n\n@connectStore({\n  srs: createGetObjectsOfType('SR'),\n})\nclass AttachDisk extends Component {\n  static propTypes = {\n    checkSr: PropTypes.func.isRequired,\n    onClose: PropTypes.func,\n    vbds: PropTypes.object.isRequired,\n    vm: PropTypes.object.isRequired,\n  }\n\n  _getVdiPredicate = createSelector(\n    () => {\n      const { vm } = this.props\n      return vm && vm.$pool\n    },\n    poolId => vdi => vdi.$pool === poolId\n  )\n\n  // FIXME: duplicate code\n  _getSrPredicate = createSelector(\n    () => {\n      const { vm } = this.props\n      return vm && vm.$pool\n    },\n    poolId => sr => sr.$pool === poolId && isSrWritable(sr)\n  )\n\n  _selectVdi = vdi => this.setState({ vdi })\n\n  _checkSr = createSelector(\n    () => this.props.checkSr,\n    () => this.props.srs,\n    () => this.state.vdi,\n    (check, srs, vdi) => check(srs[vdi.$SR])\n  )\n\n  _addVdi = () => {\n    const { vm, vbds, onClose = noop } = this.props\n    const { bootable, readOnly, vdi } = this.state\n\n    const _isFreeForWriting = vdi =>\n      vdi.$VBDs.length === 0 ||\n      every(vdi.$VBDs, id => {\n        const vbd = vbds[id]\n        return !vbd || !vbd.attached || vbd.read_only\n      })\n\n    const _attachDisk = () =>\n      attachDiskToVm(vdi, vm, {\n        bootable,\n        mode: readOnly || !_isFreeForWriting(vdi) ? 'RO' : 'RW',\n      }).then(onClose)\n\n    // check if the selected VDI is already attached to this VM.\n    return some(vbds, { VDI: vdi.id, VM: vm.id })\n      ? confirm({\n          body: _('vdiAttachDeviceConfirm'),\n          icon: 'alarm',\n          title: _('vdiAttachDevice'),\n        }).then(_attachDisk)\n      : _attachDisk()\n  }\n\n  render() {\n    const { vm } = this.props\n    const { vdi } = this.state\n\n    return (\n      <form id='attachDiskForm'>\n        <div className='form-group'>\n          <SelectVdi\n            predicate={this._getVdiPredicate()}\n            srPredicate={this._getSrPredicate()}\n            onChange={this._selectVdi}\n          />\n        </div>\n        {vdi && (\n          <fieldset className='form-inline'>\n            <div className='form-group'>\n              {vm.virtualizationMode === 'pv' && (\n                <span>\n                  {_('vbdBootable')} <Toggle ref='bootable' />{' '}\n                </span>\n              )}\n              <span>\n                {_('vbdReadonly')} <Toggle ref='readOnly' />\n              </span>\n            </div>\n            <span className='pull-right'>\n              <ActionButton icon='connect' form='attachDiskForm' btnStyle='primary' handler={this._addVdi}>\n                {_('vbdAttach')}\n              </ActionButton>\n            </span>\n            {!this._checkSr() && (\n              <div>\n                <span className='text-danger'>\n                  <Icon icon='alarm' /> {_('warningVdiSr')}\n                </span>\n              </div>\n            )}\n          </fieldset>\n        )}\n      </form>\n    )\n  }\n}\n\n@addSubscriptions(props => ({\n  // used by getResolvedResourceSet\n  resourceSet: cb => subscribeResourceSets(resourceSets => cb(find(resourceSets, { id: props.vm.resourceSet }))),\n}))\n@connectStore(() => {\n  const getAllVbds = createGetObjectsOfType('VBD')\n  const getTasks = createGetObjectsOfType('task')\n\n  const getDetailedImportVdiTasks = createSelector(\n    getTasks,\n    createFilter((state, props) => props.vdis, [vdi => vdi.other_config['xo:import:task'] !== undefined]),\n    createCollectionWrapper((tasks, vdis) =>\n      reduce(\n        vdis,\n        (acc, vdi) => {\n          const task = tasks[vdi.other_config['xo:import:task']]\n          const length = vdi.other_config['xo:import:length']\n\n          acc.push({\n            ...task,\n            details: {\n              action: 'import',\n              length: Number(length),\n              vdiId: vdi.uuid,\n              vdiName: vdi.name_label,\n            },\n          })\n\n          return acc\n        },\n        []\n      )\n    )\n  )\n\n  return (state, props) => ({\n    allVbds: getAllVbds(state, props),\n    checkPermissions: getCheckPermissions(state, props),\n    detailedImportVdiTasks: getDetailedImportVdiTasks(state, props),\n    isAdmin: isAdmin(state, props),\n    resolvedResourceSet: getResolvedResourceSet(state, props, !props.isAdmin && props.resourceSet !== undefined),\n  })\n})\nexport default class TabDisks extends Component {\n  constructor(props) {\n    super(props)\n    this.state = {\n      attachDisk: false,\n      newDisk: false,\n    }\n  }\n\n  _getVdiSrs = createSelector(\n    () => this.props.vdis,\n    createCollectionWrapper(vdis => sortedUniq(map(vdis, '$SR').sort()))\n  )\n\n  _areSrsOnSameHost = createSelector(\n    this._getVdiSrs,\n    () => this.props.srs,\n    (vdiSrs, srs) => {\n      if (some(vdiSrs, srId => srs[srId] === undefined)) {\n        return true // the user doesn't have permissions on one of the SRs: no warning\n      }\n      let container\n      let sr\n      return every(vdiSrs, srId => {\n        sr = srs[srId]\n        if (isSrShared(sr)) {\n          return true\n        }\n        return container === undefined ? ((container = sr.$container), true) : container === sr.$container\n      })\n    }\n  )\n\n  _toggleNewDisk = () =>\n    this.setState({\n      newDisk: !this.state.newDisk,\n      attachDisk: false,\n    })\n\n  _toggleAttachDisk = () =>\n    this.setState({\n      attachDisk: !this.state.attachDisk,\n      newDisk: false,\n    })\n\n  _migrateVdis = vdis => {\n    const { resolvedResourceSet, vm } = this.props\n    return confirm({\n      title: _('vdiMigrate'),\n      body: (\n        <MigrateVdiModalBody\n          pool={vm.$pool}\n          resourceSet={resolvedResourceSet}\n          warningBeforeMigrate={this._getGenerateWarningBeforeMigrate()}\n        />\n      ),\n    }).then(({ sr }) => {\n      if (sr === undefined) {\n        return error(_('vdiMigrateNoSr'), _('vdiMigrateNoSrMessage'))\n      }\n\n      return Promise.all(\n        map(vdis, vdi =>\n          migrateVdi(\n            vdi,\n            sr,\n            getDefined(() => resolvedResourceSet.id)\n          )\n        )\n      )\n    }, noop)\n  }\n\n  _getRequiredHost = createSelector(\n    this._areSrsOnSameHost,\n    this._getVdiSrs,\n    () => this.props.srs,\n    (areSrsOnSameHost, vdiSrs, srs) => {\n      if (!areSrsOnSameHost) {\n        return\n      }\n\n      let container\n      let sr\n      forEach(vdiSrs, srId => {\n        sr = srs[srId]\n        if (sr !== undefined && !isSrShared(sr)) {\n          container = sr.$container\n          return false\n        }\n      })\n      return container\n    }\n  )\n\n  _getCheckSr = createSelector(\n    this._getRequiredHost,\n    requiredHost => sr =>\n      sr === undefined || isSrShared(sr) || requiredHost === undefined || sr.$container === requiredHost\n  )\n\n  _getVbds = createSelector(\n    () => this.props.vbds,\n    () => this.props.vdis,\n    () => this.props.srs,\n    () => this.props.resolvedResourceSet,\n    (vbds, vdis, srs, resourceSet) =>\n      compact(\n        map(vbds, vbd => {\n          let vdi\n          return (\n            !vbd.is_cd_drive &&\n            ((vdi = vdis[vbd.VDI]),\n            vdi !== undefined && {\n              ...vbd,\n              vdi,\n              vdiSr: defined(\n                srs[vdi.$SR],\n                find(\n                  getDefined(() => resourceSet.objectsByType.SR),\n                  { id: vdi.$SR }\n                )\n              ),\n            })\n          )\n        })\n      )\n  )\n\n  _getGenerateWarningBeforeMigrate = createSelector(\n    this._getCheckSr,\n    check => sr =>\n      check(sr) ? null : (\n        <span className='text-warning'>\n          <Icon icon='alarm' /> {_('warningVdiSr')}\n        </span>\n      )\n  )\n\n  actions = [\n    {\n      disabled: selectedVbds => some(selectedVbds, 'attached'),\n      handler: deleteVbds,\n      individualDisabled: vbd => vbd.attached,\n      individualHandler: deleteVbd,\n      individualLabel: _('removeVdiFromVm'),\n      icon: 'vdi-forget',\n      label: _('removeSelectedVdisFromVm'),\n      level: 'danger',\n    },\n    {\n      disabled: selectedVbds => some(selectedVbds, 'attached'),\n      handler: selectedVbds => deleteVdis(uniq(map(selectedVbds, 'vdi'))),\n      individualDisabled: vbd => vbd.attached,\n      individualHandler: vbd => deleteVdi(vbd.vdi),\n      individualLabel: _('destroyVdi'),\n      icon: 'vdi-remove',\n      label: _('destroySelectedVdis'),\n      level: 'danger',\n    },\n    {\n      handler: selectedVbds => this._migrateVdis(uniq(map(selectedVbds, 'vdi'))),\n      icon: 'vdi-migrate',\n      individualLabel: _('vdiMigrate'),\n      label: _('migrateSelectedVdis'),\n    },\n  ]\n\n  render() {\n    const { allVbds, resolvedResourceSet, vm } = this.props\n\n    const { attachDisk, newDisk } = this.state\n\n    return (\n      <Container>\n        <Row>\n          <Col className='text-xs-right'>\n            <TabButton\n              btnStyle={newDisk ? 'info' : 'primary'}\n              handler={this._toggleNewDisk}\n              icon='add'\n              labelId='vbdCreateDeviceButton'\n            />\n            <TabButton\n              btnStyle={attachDisk ? 'info' : 'primary'}\n              handler={this._toggleAttachDisk}\n              icon='disk'\n              labelId='vdiAttachDevice'\n            />\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            {newDisk && (\n              <div>\n                <NewDisk checkSr={this._getCheckSr()} vm={vm} onClose={this._toggleNewDisk} />\n                <hr />\n              </div>\n            )}\n            {attachDisk && (\n              <div>\n                <AttachDisk checkSr={this._getCheckSr()} vm={vm} vbds={allVbds} onClose={this._toggleAttachDisk} />\n                <hr />\n              </div>\n            )}\n          </Col>\n        </Row>\n        <Row>\n          {!this._areSrsOnSameHost() && (\n            <div>\n              <span className='text-danger'>\n                <Icon icon='alarm' /> {_('warningVdiSr')}\n              </span>\n            </div>\n          )}\n          <Col>\n            <SortedTable\n              actions={this.actions}\n              collection={this._getVbds()}\n              columns={\n                vm.virtualizationMode === 'pv' || vm.virtualizationMode === 'pv_in_pvh' ? COLUMNS_VM_PV : COLUMNS\n              }\n              data-resourceSet={resolvedResourceSet}\n              data-vm={vm}\n              individualActions={INDIVIDUAL_ACTIONS}\n              shortcutsTarget='body'\n              stateUrlParam='s'\n            />\n          </Col>\n        </Row>\n        <Row>\n          <Col mediumSize={5}>\n            <IsoDevice vm={vm} />\n          </Col>\n        </Row>\n        <Row className='mt-1'>\n          <Col>\n            <Card>\n              <CardHeader>{_('vdiTasks')}</CardHeader>\n              <CardBlock>\n                <SortedTable\n                  collection={this.props.detailedImportVdiTasks}\n                  columns={COLUMNS_VDI_TASKS}\n                  stateUrlParam='t'\n                />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Copiable from 'copiable'\nimport decorate from 'apply-decorators'\nimport defined, { get } from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport isEmpty from 'lodash/isEmpty'\nimport map from 'lodash/map'\nimport marked from 'marked'\nimport React from 'react'\nimport HomeTags from 'home-tags'\nimport renderXoItem, { VmTemplate } from 'render-xo-item'\nimport sanitizeHtml from 'sanitize-html'\nimport semver from 'semver'\nimport Tooltip from 'tooltip'\nimport { addTag, editVm, editVmNotes, removeTag, subscribeSecurebootReadiness, subscribeUsers } from 'xo'\nimport { BlockLink } from 'link'\nimport { FormattedRelative } from 'react-intl'\nimport { Container, Row, Col } from 'grid'\nimport { Number, Size } from 'editable'\nimport {\n  createFinder,\n  createGetObject,\n  createGetObjectsOfType,\n  createGetVmLastShutdownTime,\n  createSelector,\n  getResolvedPendingTasks,\n  isAdmin,\n} from 'selectors'\nimport {\n  addSubscriptions,\n  connectStore,\n  formatSizeShort,\n  getVirtualizationModeLabel,\n  osFamily,\n  NumericDate,\n} from 'utils'\nimport { CpuSparkLines, MemorySparkLines, NetworkSparkLines, XvdSparkLines } from 'xo-sparklines'\nimport { injectState, provideState } from 'reaclette'\nimport { find } from 'lodash'\n\nconst CREATED_VM_STYLES = {\n  whiteSpace: 'pre-line',\n}\n\nconst NOTES_STYLE = {\n  maxWidth: '70%',\n  margin: 'auto',\n  border: 'dashed 1px #999',\n  padding: '1em',\n  borderRadius: '10px',\n}\n\nconst SANITIZE_OPTIONS = {\n  allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),\n}\n\nconst SECUREBOOT_STATUS_MESSAGES = {\n  disabled: _('secureBootNotEnforced'),\n  first_boot: _('secureBootWantedPendingBoot'),\n  ready: _('secureBootEnforced'),\n  ready_no_dbx: _('secureBootNoDbx'),\n  setup_mode: _('secureBootWantedButDisabled'),\n  certs_incomplete: _('secureBootWantedButCertificatesMissing'),\n}\n\nconst GuestToolsDetection = ({ vm }) => {\n  if (vm.power_state !== 'Running' || vm.pvDriversDetected === undefined) {\n    return null\n  }\n\n  if (!vm.pvDriversDetected) {\n    return (\n      <Row className='text-xs-center'>\n        <Col>\n          <Icon icon='error' /> <em>{_('noToolsDetected')}</em>\n        </Col>\n      </Row>\n    )\n  }\n\n  if (!vm.managementAgentDetected) {\n    return (\n      <Row className='text-xs-center'>\n        <Col>\n          <Icon icon='error' /> <em>{_('managementAgentNotDetected')}</em>\n        </Col>\n      </Row>\n    )\n  }\n\n  const version = get(() => vm.pvDriversVersion.split('.')[0]) > 0 ? vm.pvDriversVersion : ''\n\n  if (!vm.pvDriversUpToDate) {\n    return (\n      <Row className='text-xs-center'>\n        <Col>\n          <Icon color='text-warning' icon='alarm' />{' '}\n          <em>\n            {_('managementAgentOutOfDate', {\n              version,\n            })}\n          </em>\n        </Col>\n      </Row>\n    )\n  }\n\n  return (\n    <Row className='text-xs-center'>\n      <Col>\n        <em>\n          {_('managementAgentDetected', {\n            version,\n          })}\n        </em>\n      </Col>\n    </Row>\n  )\n}\n\nconst GeneralTab = decorate([\n  connectStore(() => {\n    const getVgpus = createGetObjectsOfType('vgpu')\n      .pick((_, { vm }) => vm.$VGPUs)\n      .sort()\n\n    const getAttachedVgpu = createFinder(getVgpus, vgpu => vgpu.currentlyAttached)\n\n    const getVgpuTypes = createGetObjectsOfType('vgpuType').pick(\n      createSelector(getVgpus, vgpus => map(vgpus, 'vgpuType'))\n    )\n\n    const getVmContainer = createGetObject((_, props) => props.vm?.$container)\n\n    const getHosts = createGetObjectsOfType('host').filter(\n      (_, { vm }) =>\n        host =>\n          host.$pool === vm.$pool\n    )\n\n    return (state, props) => ({\n      hosts: getHosts(state, props),\n      isAdmin: isAdmin(state, props),\n      vmContainer: getVmContainer(state, props),\n      lastShutdownTime: createGetVmLastShutdownTime()(state, props),\n      // true: useResourceSet to bypass permissions\n      resolvedPendingTasks: getResolvedPendingTasks(state, props, true),\n      vgpu: getAttachedVgpu(state, props),\n      vgpuTypes: getVgpuTypes(state, props),\n      vmTemplate: createGetObjectsOfType('VM-template').find(\n        (_, { pool, vm }) =>\n          template =>\n            template.$poolId === pool?.id && template.uuid === vm.creation?.template\n      )(state, props),\n    })\n  }),\n  addSubscriptions(({ isAdmin, vm }) => ({\n    vmCreator: isAdmin\n      ? cb => subscribeUsers(users => cb(find(users, user => user.id === vm.creation?.user)))\n      : () => {},\n    vmSecurebootReadiness: subscribeSecurebootReadiness(vm),\n  })),\n  provideState({\n    computed: {\n      vmResolvedPendingTasks: (_, { resolvedPendingTasks, vm }) => {\n        const vmTaskIds = Object.keys(vm.current_operations)\n        return resolvedPendingTasks.filter(task => vmTaskIds.includes(task.id))\n      },\n      host: (_, { hosts, vmContainer }) => {\n        if (vmContainer.type === 'host') {\n          return vmContainer\n        }\n        return hosts[vmContainer.master]\n      },\n    },\n  }),\n  injectState,\n  ({\n    isAdmin,\n    state: { host, vmResolvedPendingTasks },\n    lastShutdownTime,\n    statsOverview,\n    vgpu,\n    vgpuTypes,\n    vm,\n    vmCreator,\n    vmSecurebootReadiness,\n    vmTemplate,\n    vmTotalDiskSpace,\n  }) => {\n    const {\n      CPUs: cpus,\n      id,\n      installTime,\n      mainIpAddress,\n      memory,\n      os_version: osVersion,\n      power_state: powerState,\n      startTime,\n      tags,\n      VIFs: vifs,\n    } = vm\n\n    return (\n      <Container>\n        {/* TODO: use CSS style */}\n        <br />\n        <Row className='text-xs-center'>\n          <Col mediumSize={3}>\n            <h2>\n              <Number value={cpus.number} onChange={vcpus => editVm(vm, { CPUs: vcpus })} />\n              x <Icon icon='cpu' size='lg' />\n            </h2>\n            <BlockLink to={`/vms/${id}/stats`}>{statsOverview && <CpuSparkLines data={statsOverview} />}</BlockLink>\n          </Col>\n          <Col mediumSize={3}>\n            <h2 className='form-inline'>\n              <Size value={defined(memory.dynamic[1], null)} onChange={memory => editVm(vm, { memory })} />\n              &nbsp;\n              <span>\n                <Icon icon='memory' size='lg' />\n              </span>\n            </h2>\n            <BlockLink to={`/vms/${id}/stats`}>\n              {statsOverview &&\n                (vm.managementAgentDetected ? (\n                  <MemorySparkLines data={statsOverview} />\n                ) : (\n                  <span className='text-warning'>\n                    <Icon icon='alarm' /> {_('guestToolsNecessary')}\n                  </span>\n                ))}\n            </BlockLink>\n          </Col>\n          <Col mediumSize={3}>\n            <BlockLink to={`/vms/${id}/network`}>\n              <h2>\n                {vifs.length}x <Icon icon='network' size='lg' />\n              </h2>\n            </BlockLink>\n            <BlockLink to={`/vms/${id}/stats`}>{statsOverview && <NetworkSparkLines data={statsOverview} />}</BlockLink>\n          </Col>\n          <Col mediumSize={3}>\n            <BlockLink to={`/vms/${id}/disks`}>\n              <h2>\n                {formatSizeShort(vmTotalDiskSpace)} <Icon icon='disk' size='lg' />\n              </h2>\n            </BlockLink>\n            <BlockLink to={`/vms/${id}/stats`}>{statsOverview && <XvdSparkLines data={statsOverview} />}</BlockLink>\n          </Col>\n        </Row>\n        {/* TODO: use CSS style */}\n        <br />\n        <Row className='text-xs-center'>\n          <Col mediumSize={3}>\n            <p style={CREATED_VM_STYLES}>\n              {_(isAdmin ? 'vmCreatedAdmin' : 'vmCreatedNonAdmin', {\n                user: vmCreator?.email ?? _('unknown'),\n                date: installTime !== null ? <NumericDate timestamp={installTime * 1000} /> : _('unknown'),\n                template:\n                  vmTemplate !== undefined ? (\n                    <VmTemplate id={vmTemplate.id} />\n                  ) : (\n                    (vm.other.base_template_name ?? _('unknown'))\n                  ),\n              })}\n            </p>\n            {powerState === 'Running' || powerState === 'Paused' ? (\n              <div>\n                <p className='text-xs-center'>\n                  {_('started', {\n                    ago: <FormattedRelative value={startTime * 1000} />,\n                  })}\n                </p>\n              </div>\n            ) : (\n              <p className='text-xs-center'>\n                {lastShutdownTime\n                  ? _('vmHaltedSince', {\n                      ago: <FormattedRelative value={lastShutdownTime * 1000} />,\n                    })\n                  : _('vmNotRunning')}\n              </p>\n            )}\n          </Col>\n          <Col mediumSize={3}>\n            <p>{getVirtualizationModeLabel(vm)}</p>\n            {vgpu !== undefined && <p>{renderXoItem(vgpuTypes[vgpu.vgpuType])}</p>}\n          </Col>\n          <Col mediumSize={3}>\n            <BlockLink to={`/vms/${id}/network`}>\n              {mainIpAddress !== undefined ? (\n                <Copiable tagName='p'>{mainIpAddress}</Copiable>\n              ) : (\n                <p>{_('noIpv4Record')}</p>\n              )}\n            </BlockLink>\n          </Col>\n          <Col mediumSize={3}>\n            <BlockLink to={`/vms/${id}/advanced`}>\n              <Tooltip content={osVersion ? osVersion.name : _('unknownOsName')}>\n                <h1>\n                  <Icon className='text-info' icon={osVersion && osVersion.distro && osFamily(osVersion.distro)} />\n                </h1>\n              </Tooltip>\n            </BlockLink>\n          </Col>\n        </Row>\n        <GuestToolsDetection vm={vm} />\n        {/* TODO: use CSS style */}\n        <br />\n        <Row className='text-xs-center'>\n          <Col>\n            {vm.boot.firmware === 'uefi' && host !== undefined && semver.satisfies(host.version, '>=8.3.0') && (\n              <p>\n                {_('keyValue', {\n                  key: _('secureBootStatus'),\n                  value: SECUREBOOT_STATUS_MESSAGES[vmSecurebootReadiness],\n                })}\n              </p>\n            )}\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <h2 className='text-xs-center'>\n              <HomeTags type='VM' labels={tags} onDelete={tag => removeTag(id, tag)} onAdd={tag => addTag(id, tag)} />\n            </h2>\n          </Col>\n        </Row>\n        {isEmpty(vmResolvedPendingTasks) ? null : (\n          <Row className='text-xs-center'>\n            <Col>\n              <h4>{_('vmCurrentStatus')}</h4>\n              {map(vmResolvedPendingTasks, task => (\n                <p>\n                  <strong>{task.name_label}</strong>\n                  {task.progress > 0 && <span>: {Math.round(task.progress * 100)}%</span>}\n                </p>\n              ))}\n            </Col>\n          </Row>\n        )}\n        <Row className='mt-1'>\n          <div style={NOTES_STYLE}>\