,\n\n  // Original text: \"No files selected\"\n  restoreFilesNoFilesSelected: 'Hiç dosya seçilmedi',\n\n  // Original text: \"Selected files ({files}):\"\n  restoreFilesSelectedFiles: 'Seçilen dosyalar ({files})',\n\n  // Original text: \"Error while scanning disk\"\n  restoreFilesDiskError: 'Disk tarama esnasında hata',\n\n  // Original text: \"Select all this folder's files\"\n  restoreFilesSelectAllFiles: 'Bu dizinin tüm dosyalarını seç',\n\n  // Original text: \"Unselect all files\"\n  restoreFilesUnselectAll: 'Seçimi kaldır',\n\n  // Original text: \"Emergency shutdown Host\"\n  emergencyShutdownHostModalTitle: 'Acil durum sunucu kapatması',\n\n  // Original text: \"Are you sure you want to shutdown {host}?\"\n  emergencyShutdownHostModalMessage: 'Kapatmak istediğinize emin misiniz {host}',\n\n  // Original text: \"Emergency shutdown Host{nHosts, plural, one {} other {s}}\"\n  emergencyShutdownHostsModalTitle: 'Acil durum sunucu kapatması',\n\n  // Original text: \"Are you sure you want to shutdown {nHosts, number} Host{nHosts, plural, one {} other {s}}?\"\n  emergencyShutdownHostsModalMessage: '{nHosts, number} Sunucu kapatmak istediğinize emin misiniz?',\n\n  // Original text: \"Shutdown host\"\n  stopHostModalTitle: 'Sunucuyu Kapat',\n\n  // Original text: \"This will shutdown your host. Do you want to continue? If it's the pool master, your connection to the pool will be lost\"\n  stopHostModalMessage:\n    'Bu sunucunuzu kapatacak. Devam etmek istiyor musunuz? Eğer havuzun master sunucusu ise, havuzunuzla bağlantınız kesilecek',\n\n  // Original text: \"Add host\"\n  addHostModalTitle: 'Sunucu ekle',\n\n  // Original text: \"Are you sure you want to add {host} to {pool}?\"\n  addHostModalMessage: '{host} sunucusunu {pool} havuzuna eklemek istediğinize emin misiniz?',\n\n  // Original text: \"Restart host\"\n  restartHostModalTitle: 'Sunucuyu yenden başlat',\n\n  // Original text: \"This will restart your host. Do you want to continue?\"\n  restartHostModalMessage: 'Bu işlem sunucunuzu yeniden başlatacak. Devam etmek istiyor musunuz?',\n\n  // Original text: \"Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}\"\n  restartHostsAgentsModalTitle: 'Sunucu ajanını yeniden başlat',\n\n  // Original text: \"Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?\"\n  restartHostsAgentsModalMessage: '{nHosts, number} sunucunun ajanlarını yeniden başlatmak istediğinize emin misiniz?',\n\n  // Original text: \"Restart Host{nHosts, plural, one {} other {s}}\"\n  restartHostsModalTitle: 'Sunucuyu(ları) yeniden başlat',\n\n  // Original text: \"Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}}?\"\n  restartHostsModalMessage: '{nHosts, number} sunucuyu yeniden başlatmak istediğinize emin misiniz?',\n\n  // Original text: \"Start VM{vms, plural, one {} other {s}}\"\n  startVmsModalTitle: 'VM(leri) başlat',\n\n  // Original text: \"Start a copy\"\n  cloneAndStartVM: 'Bir kopya başlat',\n\n  // Original text: \"Force start\"\n  forceStartVm: 'Zorla başlat',\n\n  // Original text: \"Forbidden operation\"\n  forceStartVmModalTitle: 'Yasak işlem',\n\n  // Original text: \"Start operation for this vm is blocked.\"\n  blockedStartVmModalMessage: 'Başlatma işlemi bu VM için engellenmiş.',\n\n  // Original text: \"Forbidden operation start for {nVms, number} vm{nVms, plural, one {} other {s}}.\"\n  blockedStartVmsModalMessage: '{nVms, number} VM için yasak işlem başladı.',\n\n  // Original text: \"Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?\"\n  startVmsModalMessage: '{vms, number} VM başlatmak istediğinize emin misiniz?',\n\n  // Original text: \"{nVms, number} vm{nVms, plural, one {} other {s}} are failed. Please see your logs to get more information\"\n  failedVmsErrorMessage:\n    '{nVms, number} VM başarısız oldu. Daha fazla bilgi almak için lütfen günlüklerinizi inceleyin',\n\n  // Original text: \"Start failed\"\n  failedVmsErrorTitle: 'Başlatma başarısız',\n\n  // Original text: \"Stop Host{nHosts, plural, one {} other {s}}\"\n  stopHostsModalTitle: 'Sunucuyu(ları) durdur',\n\n  // Original text: \"Are you sure you want to stop {nHosts, number} Host{nHosts, plural, one {} other {s}}?\"\n  stopHostsModalMessage: '{nHosts, number} sunucuyu durdurmak istediğinize emin misiniz?',\n\n  // Original text: \"Stop VM{vms, plural, one {} other {s}}\"\n  stopVmsModalTitle: 'VM(leri) durdur',\n\n  // Original text: \"Are you sure you want to stop {vms, number} VM{vms, plural, one {} other {s}}?\"\n  stopVmsModalMessage: '{vms, number} VM durdurmak istediğinize emin misiniz?',\n\n  // Original text: \"Restart VM\"\n  restartVmModalTitle: 'VM yeniden başlat',\n\n  // Original text: \"Are you sure you want to restart {name}?\"\n  restartVmModalMessage: 'Yeniden başlatmak istediğinize emin misiniz {name}?',\n\n  // Original text: \"Stop VM\"\n  stopVmModalTitle: 'VM durdur',\n\n  // Original text: \"Are you sure you want to stop {name}?\"\n  stopVmModalMessage: 'Durdurmak istediğinze emin misiniz {name}?',\n\n  // Original text: \"Suspend VM{vms, plural, one {} other {s}}\"\n  suspendVmsModalTitle: 'VM(leri) duraklat',\n\n  // Original text: \"Are you sure you want to suspend {vms, number} VM{vms, plural, one {} other {s}}?\"\n  suspendVmsModalMessage: '{vms, number} VM duraklatmak istediğinize emin misiniz?',\n\n  // Original text: \"Restart VM{vms, plural, one {} other {s}}\"\n  restartVmsModalTitle: 'VM(leri) yeniden başlat',\n\n  // Original text: \"Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?\"\n  restartVmsModalMessage: '{vms, number} VM yeniden başlatmak istediğinize emin misiniz?',\n\n  // Original text: \"Snapshot VM{vms, plural, one {} other {s}}\"\n  snapshotVmsModalTitle: 'Snapshot VM',\n\n  // Original text: \"Are you sure you want to snapshot {vms, number} VM{vms, plural, one {} other {s}}?\"\n  snapshotVmsModalMessage: '{vms, number} VM için snapshot almak istediğinize emin misiniz?',\n\n  // Original text: \"Delete VM{vms, plural, one {} other {s}}\"\n  deleteVmsModalTitle: 'VM sil',\n\n  // Original text: \"Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED\"\n  deleteVmsModalMessage: '{vms, number} VM silmek istediğinize emin misiniz? BÜTÜN VM DİSKLERİ KALDIRILACAK',\n\n  // Original text: \"delete {nVms, number} vm{nVms, plural, one {} other {s}}\"\n  deleteVmsConfirmText: '{nVms, number} vm sil',\n\n  // Original text: \"Delete VM\"\n  deleteVmModalTitle: 'VM sil',\n\n  // Original text: \"Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED\"\n  deleteVmModalMessage: \"Bu VM'i silmek istediğinize emin misiniz? BÜTÜN VM DİSKLERİ KALDIRILACAK\",\n\n  // Original text: \"Blocked operation\"\n  deleteVmBlockedModalTitle: 'Engellenmiş işlem',\n\n  // Original text: \"Removing the VM is a blocked operation. Would you like to remove it anyway?\"\n  deleteVmBlockedModalMessage: 'VM kaldırmak engellenmiş bir işlemdir. Yine de kaldırmak istiyor musunuz?',\n\n  // Original text: \"Migrate VM\"\n  migrateVmModalTitle: 'VM taşı',\n\n  // Original text: \"Select a destination host:\"\n  migrateVmSelectHost: 'Hedef sunucuyu seçin:',\n\n  // Original text: \"Select a migration network:\"\n  migrateVmSelectMigrationNetwork: 'Taşımak için kullnılacak ağı seçin:',\n\n  // Original text: \"For each VIF, select a network:\"\n  migrateVmSelectNetworks: 'Her VIF için bir ağ seçin',\n\n  // Original text: \"Select a destination SR:\"\n  migrateVmsSelectSr: \"Hedef SR'yi seçin\",\n\n  // Original text: \"Select a destination SR for local disks:\"\n  migrateVmsSelectSrIntraPool: \"Yerel diskler için hedef SR'yi seçin:\",\n\n  // Original text: \"Select a network on which to connect each VIF:\"\n  migrateVmsSelectNetwork: \"Her bir VIF'in bağlanacağı bir ağ seçin:\",\n\n  // Original text: \"Smart mapping\"\n  migrateVmsSmartMapping: 'Akıllı haritalama',\n\n  // Original text: \"VIF\"\n  migrateVmVif: 'VIF',\n\n  // Original text: \"Network\"\n  migrateVmNetwork: 'Ağ',\n\n  // Original text: \"No target host\"\n  migrateVmNoTargetHost: 'Hedef sunucu yok',\n\n  // Original text: \"A target host is required to migrate a VM\"\n  migrateVmNoTargetHostMessage: 'Vm taşımak için bir hedef sunucu gereklidir',\n\n  // Original text: \"No default SR\"\n  migrateVmNoDefaultSrError: 'Varsayılan SR yok',\n\n  // Original text: \"Default SR not connected to host\"\n  migrateVmNotConnectedDefaultSrError: 'Varsayılan SR sunucuya bağlı değil',\n\n  // Original text: \"For each VDI, select an SR:\"\n  chooseSrForEachVdisModalSelectSr: 'Her bir VDI için bir SR seçin',\n\n  // Original text: \"Select main SR…\"\n  chooseSrForEachVdisModalMainSr: \"Ana SR'yi seçin...\",\n\n  // Original text: \"VDI\"\n  chooseSrForEachVdisModalVdiLabel: 'VDI',\n\n  // Original text: \"SR*\"\n  chooseSrForEachVdisModalSrLabel: 'SR*',\n\n  // Original text: \"* optional\"\n  optionalEntry: '* opsiyonel',\n\n  // Original text: \"Delete job{nJobs, plural, one {} other {s}}\"\n  deleteJobsModalTitle: 'İşi(leri) sil',\n\n  // Original text: \"Are you sure you want to delete {nJobs, number} job{nJobs, plural, one {} other {s}}?\"\n  deleteJobsModalMessage: '{nJobs, number} işi silmek istediğinize emin misiniz?',\n\n  // Original text: \"Delete VBD{nVbds, plural, one {} other {s}}\"\n  deleteVbdsModalTitle: 'VBD sil',\n\n  // Original text: \"Are you sure you want to delete {nVbds, number} VBD{nVbds, plural, one {} other {s}}?\"\n  deleteVbdsModalMessage: '{nVbds, number} VBD silmek istediğinize emin misiniz?',\n\n  // Original text: \"Delete VDI\"\n  deleteVdiModalTitle: 'VDI sil',\n\n  // Original text: \"Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST\"\n  deleteVdiModalMessage: 'Bu diski silmek istediğinize emin misiniz? DİSK ÜZERİNDEKİ TÜM VERİ KAYBOLACAK',\n\n  // Original text: \"Delete VDI{nVdis, plural, one {} other {s}}\"\n  deleteVdisModalTitle: 'VDI sil',\n\n  // Original text: \"Are you sure you want to delete {nVdis, number} disk{nVdis, plural, one {} other {s}}? ALL DATA ON THESE DISKS WILL BE LOST\"\n  deleteVdisModalMessage:\n    '{nVdis, number} disk silmek istediğinize emin misiniz? DİSKLER ÜZERİNDEKİ TÜM VERİ KAYBOLACAK',\n\n  // Original text: \"Delete schedule{nSchedules, plural, one {} other {s}}\"\n  deleteSchedulesModalTitle: 'Zamanlama sil',\n\n  // Original text: \"Are you sure you want to delete {nSchedules, number} schedule{nSchedules, plural, one {} other {s}}?\"\n  deleteSchedulesModalMessage: '{nSchedules, number} zamanlama silmek istediğinize emin misiniz?',\n\n  // Original text: \"Delete remote{nRemotes, plural, one {} other {s}}\"\n  deleteRemotesModalTitle: 'Hedef sil',\n\n  // Original text: \"Are you sure you want to delete {nRemotes, number} remote{nRemotes, plural, one {} other {s}}?\"\n  deleteRemotesModalMessage: '{nRemotes, number} yedekleme hedefi silmek istediğinze emin misiniz?',\n\n  // Original text: \"Revert your VM\"\n  revertVmModalTitle: \"VM'i eski haline döndür\",\n\n  // Original text: \"Share your VM\"\n  shareVmInResourceSetModalTitle: \"VM'i paylaş\",\n\n  // Original text: \"This VM will be shared with all the members of the self-service {self}. Are you sure?\"\n  shareVmInResourceSetModalMessage: \"Bu VM self-servis'in tüm üyeleri ile paylaşılacak. Emin misiniz?\",\n\n  // Original text: \"Delete VIF{nVifs, plural, one {} other {s}}\"\n  deleteVifsModalTitle: 'VIF sil',\n\n  // Original text: \"Are you sure you want to delete {nVifs, number} VIF{nVifs, plural, one {} other {s}}?\"\n  deleteVifsModalMessage: '{nVifs, number} VIF silmek istediğinize emin misiniz?',\n\n  // Original text: \"Delete snapshot\"\n  deleteSnapshotModalTitle: 'Snapshot sil',\n\n  // Original text: \"Are you sure you want to delete this snapshot?\"\n  deleteSnapshotModalMessage: \"Bu snapshot'ı silmek istediğinize emin misiniz?\",\n\n  // Original text: \"Delete snapshot{nVms, plural, one {} other {s}}\"\n  deleteSnapshotsModalTitle: 'Snapshot sil',\n\n  // Original text: \"Are you sure you want to delete {nVms, number} snapshot{nVms, plural, one {} other {s}}?\"\n  deleteSnapshotsModalMessage: '{nVms, number} snapshot silmek istediğinize emin misiniz?',\n\n  // Original text: \"Disconnect VBD{nVbds, plural, one {} other {s}}\"\n  disconnectVbdsModalTitle: 'VBD bağlantısını kes',\n\n  // Original text: \"Are you sure you want to disconnect {nVbds, number} VBD{nVbds, plural, one {} other {s}}?\"\n  disconnectVbdsModalMessage: '{nVbds, number} VBD bağlantısını kesmek istediğinize emin misiniz',\n\n  // Original text: \"Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.\"\n  revertVmModalMessage: \"Bu VM'i snapshot'daki haline döndürmek istiyor musunuz? Bu işlem geri alınamaz.\",\n\n  // Original text: \"Snapshot before\"\n  revertVmModalSnapshotBefore: 'Snapshot öncesi',\n\n  // Original text: \"Import a {name} Backup\"\n  importBackupModalTitle: 'Bir {name} yedeği içe aktar',\n\n  // Original text: \"Start VM after restore\"\n  importBackupModalStart: \"Geri almadan sonra VM'i çalıştır\",\n\n  // Original text: \"Select your backup…\"\n  importBackupModalSelectBackup: 'Yedeğinizi seçin...',\n\n  // Original text: \"Select a destination SR…\"\n  importBackupModalSelectSr: 'Bir hedef SR seçin...',\n\n  // Original text: \"Are you sure you want to remove all orphaned snapshot VDIs?\"\n  removeAllOrphanedModalWarning: \"Bütün yetim VDI snapshot'larını silmek istediğinize emin misiniz?\",\n\n  // Original text: \"Remove all logs\"\n  removeAllLogsModalTitle: 'Bütün günlükleri kaldır',\n\n  // Original text: \"Are you sure you want to remove all logs?\"\n  removeAllLogsModalWarning: 'Bütün günlükleri kaldırmak istediğinize emin misiniz?',\n\n  // Original text: \"This operation is definitive.\"\n  definitiveMessageModal: 'Bu işlem kesindir.',\n\n  // Original text: \"Previous SR Usage\"\n  existingSrModalTitle: 'Önceki SR kullanımı',\n\n  // Original text: \"This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.\"\n  existingSrModalText:\n    'Bu yol XenServer sunucusu tarafından bir SR olarak kullanılmış. SR oluşturmaya devam ederseniz tüm veri kaybolacak.',\n\n  // Original text: \"Previous LUN Usage\"\n  existingLunModalTitle: 'Önceki LUN kullanımı',\n\n  // Original text: \"This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.\"\n  existingLunModalText:\n    'Bu LUN XenServer sunucusu tarafından bir SR olarak kullanılmış. SR oluşturmaya devam ederseniz tüm veri kaybolacak.',\n\n  // Original text: \"Replace current registration?\"\n  alreadyRegisteredModal: 'Mevcut kaydı değiştir?',\n\n  // Original text: \"Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?\"\n  alreadyRegisteredModalText:\n    'XO kurulumunuz zaten {email} adresine kayıtlı, Bunu unutmak ve değiştirmek istiyor musunuz?',\n\n  // Original text: \"Ready for trial?\"\n  trialReadyModal: 'Deneme için hazır mısınız?',\n\n  // Original text: \"During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!\"\n  trialReadyModalText:\n    'Deneme sürecinde XOA bir internet bağlantısı gerektirir. Bu limit satın alınan sürümlerde yoktur!',\n\n  // Original text: \"Cancel task{nTasks, plural, one {} other {s}}\"\n  cancelTasksModalTitle: 'Görev iptel et',\n\n  // Original text: \"Are you sure you want to cancel {nTasks, number} task{nTasks, plural, one {} other {s}}?\"\n  cancelTasksModalMessage: '{nTasks, number} görevi iptal etmek istediğinize emin misiniz?',\n\n  // Original text: \"Destroy task{nTasks, plural, one {} other {s}}\"\n  destroyTasksModalTitle: 'Görev imha et',\n\n  // Original text: \"Are you sure you want to destroy {nTasks, number} task{nTasks, plural, one {} other {s}}?\"\n  destroyTasksModalMessage: '{nTasks, number} görevi imha etmek istediğinize emin misiniz?',\n\n  // Original text: \"Label\"\n  serverLabel: 'Başlık',\n\n  // Original text: \"Host\"\n  serverHost: 'Sunucu',\n\n  // Original text: \"Username\"\n  serverUsername: 'Kullanıcı adı',\n\n  // Original text: \"Password\"\n  serverPassword: 'Parola',\n\n  // Original text: \"Action\"\n  serverAction: 'Aksiyon',\n\n  // Original text: \"Read Only\"\n  serverReadOnly: 'Yalnızca okunabilir',\n\n  // Original text: \"Unauthorized Certificates\"\n  serverUnauthorizedCertificates: 'Yetkisiz Sertifikalar',\n\n  // Original text: \"Allow Unauthorized Certificates\"\n  serverAllowUnauthorizedCertificates: 'Yetkisiz Sertifikalara İzin Ver',\n\n  // Original text: \"Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured.\"\n  serverUnauthorizedCertificatesInfo:\n    'Sertifikanız reddedildiğinde bunu yapın ancak bağlantınız güvenli olmayacağı için tavsiye edilmez.',\n\n  // Original text: \"username\"\n  serverPlaceHolderUser: 'kullanıcı adı',\n\n  // Original text: \"password\"\n  serverPlaceHolderPassword: 'parola',\n\n  // Original text: \"address[:port]\"\n  serverPlaceHolderAddress: 'adres[:port]',\n\n  // Original text: \"label\"\n  serverPlaceHolderLabel: 'başlık',\n\n  // Original text: \"Connect\"\n  serverConnect: 'Bağlan',\n\n  // Original text: \"Error\"\n  serverError: 'Hata',\n\n  // Original text: \"Adding server failed\"\n  serverAddFailed: 'Server ekleme başarısız',\n\n  // Original text: \"Status\"\n  serverStatus: 'Durum',\n\n  // Original text: \"Connection failed. Click for more information.\"\n  serverConnectionFailed: 'Bağlantı başarısız. Bilgi için tıklayın.',\n\n  // Original text: \"Connecting…\"\n  serverConnecting: 'Bağlanıyor...',\n\n  // Original text: \"Authentication error\"\n  serverAuthFailed: 'Kimlik doğrulama hatası',\n\n  // Original text: \"Unknown error\"\n  serverUnknownError: 'Bilinmeyen hata',\n\n  // Original text: \"Invalid self-signed certificate\"\n  serverSelfSignedCertError: 'Geçersiz kendinden imzalı sertifika',\n\n  // Original text: \"Do you want to accept self-signed certificate for this server even though it would decrease security?\"\n  serverSelfSignedCertQuestion:\n    'Güvenliği azaltmasına rağmen kendinden imzalı sertifikayı kabul etmek istiyor musunuz?',\n\n  // Original text: \"Copy VM\"\n  copyVm: 'VM kopyala',\n\n  // Original text: \"Are you sure you want to copy this VM to {SR}?\"\n  copyVmConfirm: \"Bu VM'i şuraya kopyalamak istediğinize emin misiniz {SR}?\",\n\n  // Original text: \"Name\"\n  copyVmName: 'Ad',\n\n  // Original text: \"Name pattern\"\n  copyVmNamePattern: 'Ad kalıbı',\n\n  // Original text: \"If empty: name of the copied VM\"\n  copyVmNamePlaceholder: \"Eğer boşsa: kopyalanan VM'in adı\",\n\n  // Original text: \"e.g.: \\\"\\\\{name\\\\}_COPY\\\"\"\n  copyVmNamePatternPlaceholder: 'örn: \"\\\\{name\\\\}_COPY',\n\n  // Original text: \"Select SR\"\n  copyVmSelectSr: 'SR seç',\n\n  // Original text: \"Use compression\"\n  copyVmCompress: 'Sıkıştırma kullan',\n\n  // Original text: \"No target SR\"\n  copyVmsNoTargetSr: 'Hedef SR yok',\n\n  // Original text: \"A target SR is required to copy a VM\"\n  copyVmsNoTargetSrMessage: 'VM kopyalamak için hedef bir SR gereklidir',\n\n  // Original text: \"Detach host\"\n  detachHostModalTitle: 'Sunucuyu ayır',\n\n  // Original text: \"Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.\"\n  detachHostModalMessage:\n    \"{host} sunucusunu havuzundan ayırmak istediğinize emin misiniz? BU İŞLEM YEREL DİSKİNDEKİ TÜM VM'LERİ KALDIRIR VE SUNUCU YENİDEN BAŞLAR\",\n\n  // Original text: \"Detach\"\n  detachHost: 'ayır',\n\n  // Original text: \"Forget host\"\n  forgetHostModalTitle: 'Sunucuyu unut',\n\n  // Original text: \"Are you sure you want to forget {host} from its pool? Be sure this host can't be back online, or use detach instead.\"\n  forgetHostModalMessage:\n    '{host} sunucusunu havuzunun unutmasını istediğinize emin misiniz? Bu sunucunun tekrar çevrimiçi olamayacağından emin olun veya bunun yerine ayırmayı deneyin.',\n\n  // Original text: \"Forget\"\n  forgetHost: 'Unut',\n\n  // Original text: \"Designate a new master\"\n  setPoolMasterModalTitle: 'Yeni bir master tayin et',\n\n  // Original text: \"This operation may take several minutes. Do you want to continue?\"\n  setPoolMasterModalMessage: 'Bu işlem birkaç dakika sürebilir. Devam etmek istiyor musunuz?',\n\n  // Original text: \"Create network\"\n  newNetworkCreate: 'Ağ oluştur',\n\n  // Original text: \"Create bonded network\"\n  newBondedNetworkCreate: 'Bağlı ağ oluştur',\n\n  // Original text: \"Interface\"\n  newNetworkInterface: 'Arayüz',\n\n  // Original text: \"Name\"\n  newNetworkName: 'Ad',\n\n  // Original text: \"Description\"\n  newNetworkDescription: 'Açıklama',\n\n  // Original text: \"VLAN\"\n  newNetworkVlan: 'VLAN',\n\n  // Original text: \"No VLAN if empty\"\n  newNetworkDefaultVlan: 'Boşsa VLAN yok',\n\n  // Original text: \"MTU\"\n  newNetworkMtu: 'MTU',\n\n  // Original text: \"Default: 1500\"\n  newNetworkDefaultMtu: 'Varsayılan: 1500',\n\n  // Original text: \"Name required\"\n  newNetworkNoNameErrorTitle: 'Ad gerekli',\n\n  // Original text: \"A name is required to create a network\"\n  newNetworkNoNameErrorMessage: 'Bir ağ oluşturmak için ad gereklidir',\n\n  // Original text: \"Bond mode\"\n  newNetworkBondMode: 'Bağlı mod',\n\n  // Original text: \"Delete network\"\n  deleteNetwork: 'Ağı sil',\n\n  // Original text: \"Are you sure you want to delete this network?\"\n  deleteNetworkConfirm: 'Bu ağı silmek istediğinize emin misiniz?',\n\n  // Original text: \"This network is currently in use\"\n  networkInUse: 'Bu ağ şu an kullanımda',\n\n  // Original text: \"Bonded\"\n  pillBonded: 'Bağlı',\n\n  // Original text: \"Host\"\n  addHostSelectHost: 'Sunucu',\n\n  // Original text: \"No host\"\n  addHostNoHost: 'Sunucu yok',\n\n  // Original text: \"No host selected to be added\"\n  addHostNoHostMessage: 'Eklemek için bir sunucu seçilmedi',\n\n  // Original text: \"Xen Orchestra\"\n  xenOrchestra: 'Xen Orchestra',\n\n  // Original text: \"No pro support provided!\"\n  noProSupport: 'Hiçbir profesyonel destek verilmez!',\n\n  // Original text: \"Use in production at your own risks\"\n  noProductionUse: 'Üretimde kendi risklerinizle kullanın.',\n\n  // Original text: \"You can download our turnkey appliance at {website}\"\n  downloadXoaFromWebsite: 'Bizim anahtar teslimi sunucumuzu şuradan indirebilirsiniz {website}',\n\n  // Original text: \"Bug Tracker\"\n  bugTracker: 'Hata Takibi',\n\n  // Original text: \"Issues? Report it!\"\n  bugTrackerText: 'Sorun mu var? Raporlayın!',\n\n  // Original text: \"Community\"\n  community: 'Topluluk',\n\n  // Original text: \"Join our community forum!\"\n  communityText: 'Topluluk forumumuza katılın!',\n\n  // Original text: \"Free Trial for Premium Edition!\"\n  freeTrial: 'Premium Sürüm için Bedava Deneme',\n\n  // Original text: \"Request your trial now!\"\n  freeTrialNow: 'Denemenizi hemen talep edin!',\n\n  // Original text: \"Any issue?\"\n  issues: 'Herhangi bir sorun?',\n\n  // Original text: \"Problem? Contact us!\"\n  issuesText: 'Problem? Bizimle iletişime geçin!',\n\n  // Original text: \"Documentation\"\n  documentation: 'Dokümantasyon',\n\n  // Original text: \"Read our official doc\"\n  documentationText: 'Resmi dokümanımızı okuyun',\n\n  // Original text: \"Pro support included\"\n  proSupportIncluded: 'Profesyonel destek dahildir',\n\n  // Original text: \"Access your XO Account\"\n  xoAccount: 'XO hesabınıza erişin',\n\n  // Original text: \"Report a problem\"\n  openTicket: 'Bir problemi raporlayın',\n\n  // Original text: \"Problem? Open a ticket!\"\n  openTicketText: 'Problem? Bir kayıt açın!',\n\n  // Original text: \"Upgrade needed\"\n  upgradeNeeded: 'Yükseltme gerekli',\n\n  // Original text: \"Upgrade now!\"\n  upgradeNow: 'Şimdi yükselt!',\n\n  // Original text: \"Or\"\n  or: 'Veya',\n\n  // Original text: \"Try it for free!\"\n  tryIt: 'Bedava dene!',\n\n  // Original text: \"This feature is available starting from {plan} Edition\"\n  availableIn: 'Bu özellik {plan} sürümü ile başlıyor',\n\n  // Original text: \"This feature is not available in your version, contact your administrator to know more.\"\n  notAvailable:\n    'Bu özellik sizin sürümünüzde kullanılabilir değil. Daha fazlasını öğrenmek için yöneticiniz ile temasa geçin.',\n\n  // Original text: \"Updates\"\n  updateTitle: 'Güncellemeler',\n\n  // Original text: \"Registration\"\n  registration: 'Kayıtlanma',\n\n  // Original text: \"Trial\"\n  trial: 'Deneme',\n\n  // Original text: \"Settings\"\n  settings: 'Ayarlar',\n\n  // Original text: \"Proxy settings\"\n  proxySettings: 'Proxy ayarları',\n\n  // Original text: \"Host (myproxy.example.org)\"\n  proxySettingsHostPlaceHolder: 'Sunucu (myproxy.example.org)',\n\n  // Original text: \"Port (eg: 3128)\"\n  proxySettingsPortPlaceHolder: 'Port (örn: 3128)',\n\n  // Original text: \"Username\"\n  proxySettingsUsernamePlaceHolder: 'Kullanıcı adı',\n\n  // Original text: \"Password\"\n  proxySettingsPasswordPlaceHolder: 'Parola',\n\n  // Original text: \"Your email account\"\n  updateRegistrationEmailPlaceHolder: 'Eposta hesabınız',\n\n  // Original text: \"Your password\"\n  updateRegistrationPasswordPlaceHolder: 'Parolanız',\n\n  // Original text: \"Troubleshooting documentation\"\n  updaterTroubleshootingLink: 'Sorun giderme dokümanları',\n\n  // Original text: \"Update\"\n  update: 'Güncelle',\n\n  // Original text: \"Refresh\"\n  refresh: 'Yenile',\n\n  // Original text: \"Upgrade\"\n  upgrade: 'Yükselt',\n\n  // Original text: 'Downgrade'\n  downgrade: undefined,\n\n  // Original text: \"No updater available for Community Edition\"\n  noUpdaterCommunity: 'Topluluk sürümü için güncelleştirici sunulmuyor',\n\n  // Original text: \"Please consider subscribing and trying it with all the features for free during 30 days on {link}.\"\n  considerSubscribe:\n    'Lütfen {link} adresinden abone olun ve 30 gün boyunca ücretsiz olarak tüm özellikleri kullanmayı deneyin.',\n\n  // Original text: \"Manual update could break your current installation due to dependencies issues, do it with caution\"\n  noUpdaterWarning:\n    'El ile güncelleştirme, geçerli yüklemenizi bağımlılıklar nedeniyle bozabilir, dikkatli bir şekilde yapın.',\n\n  // Original text: \"Current version:\"\n  currentVersion: 'Mevcut sürüm:',\n\n  // Original text: \"Register\"\n  register: 'Kaydol',\n\n  // Original text: \"Edit registration\"\n  editRegistration: 'Kaydı düzenle',\n\n  // Original text: \"Please, take time to register in order to enjoy your trial.\"\n  trialRegistration: 'Lütfen, denemenizin tadını çıkarmak adına kaydolmak için zaman ayırın.',\n\n  // Original text: \"Start trial\"\n  trialStartButton: 'Denemeyi başlat',\n\n  // Original text: \"You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.\"\n  trialAvailableUntil:\n    'Deneme sürümünü {date, date, medium} tarihine kadar kullanabilirsiniz. Almak için cihazınızı yeni sürüme geçirin.',\n\n  // Original text: \"Your trial has been ended. Contact us or downgrade to Free version\"\n  trialConsumed: 'Denemeniz süreniz sona erdi. Bize ulaşın veya Ücretsiz sürüme geçin.',\n\n  // Original text: \"Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service.\"\n  trialLocked: \"Xoa-updater hizmetiniz çalışmıyor gibi görünüyor. XOA'nız bu servise ulaşmadan tamamen çalışamaz.\",\n\n  // Original text: \"No update information available\"\n  noUpdateInfo: 'Güncelleme bilgisi mevcut değil',\n\n  // Original text: \"Update information may be available\"\n  waitingUpdateInfo: 'Güncelleme bilgileri mevcut olabilir',\n\n  // Original text: \"Your XOA is up-to-date\"\n  upToDate: 'XOA güncel',\n\n  // Original text: \"You need to update your XOA (new version is available)\"\n  mustUpgrade: 'XOA güncellenmeli (yeni versiyon kullanıma hazır)',\n\n  // Original text: \"Your XOA is not registered for updates\"\n  registerNeeded: \"XOA'nız güncellemeler için kayıtlı değil\",\n\n  // Original text: \"Can't fetch update information\"\n  updaterError: 'Güncelleme bilgisi alınamıyor',\n\n  // Original text: \"Upgrade successful\"\n  promptUpgradeReloadTitle: 'Yükseltme başarılı',\n\n  // Original text: \"Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?\"\n  promptUpgradeReloadMessage:\n    \"XOA'nız başarıyla yükseltildi ve tarayıcınız uygulamayı yeniden yüklemelidir. Şimdi yeniden yüklemek istiyor musunuz?\",\n\n  // Original text: \"Upgrade warning\"\n  upgradeWarningTitle: 'Yükseltme uyarısı',\n\n  // Original text: \"You have some backup jobs in progress. If you upgrade now, these jobs will be interrupted! Are you sure you want to continue?\"\n  upgradeWarningMessage:\n    'Devam eden bazı yedekleme işleriniz var. Şimdi yükseltirseniz, bu işler kesilecektir! Devam etmek istediğinize emin misiniz?',\n\n  // Original text: \"Xen Orchestra from the sources\"\n  disclaimerTitle: 'Kaynak koddan Xen Orchestra',\n\n  // Original text: \"You are using XO from the sources! That's great for a personal/non-profit usage.\"\n  disclaimerText1: \"XO'yu kaynak koddan kullanıyorsunuz! Kişisel/kar amacı gütmeyen kullanım için mükemmeldir.\",\n\n  // Original text: \"If you are a company, it's better to use it with our appliance + pro support included:\"\n  disclaimerText2: 'Bir şirket iseniz, bizim hazır uygulamamızı kullanmanız daha iyidir + pro destek dahil:',\n\n  // Original text: \"This version is not bundled with any support nor updates. Use it with caution for critical tasks.\"\n  disclaimerText3: 'Bu sürüm herhangi bir destek veya güncellemeyle birlikte verilmez. Dikkatli kullanın.',\n\n  // Original text: \"Connect PIF\"\n  connectPif: \"PIF'e bağlan\",\n\n  // Original text: \"Are you sure you want to connect this PIF?\"\n  connectPifConfirm: \"Bu PIF'e bağlanmak istediğinize emin misiniz?\",\n\n  // Original text: \"Disconnect PIF\"\n  disconnectPif: 'PIF bağlantısını kes',\n\n  // Original text: \"Are you sure you want to disconnect this PIF?\"\n  disconnectPifConfirm: 'PIF bağlantısını kesmek istediğinize emin misiniz?',\n\n  // Original text: \"Delete PIF\"\n  deletePif: 'PIF sil',\n\n  // Original text: \"Are you sure you want to delete this PIF?\"\n  deletePifConfirm: \"Bu PIF'i silmek istediğinize emin misiniz?\",\n\n  // Original text: \"Delete PIFs\"\n  deletePifs: \"PIF'leri sil\",\n\n  // Original text: \"Are you sure you want to delete {nPifs, number} PIF{nPifs, plural, one {} other {s}}?\"\n  deletePifsConfirm: '{nPifs, number} PIF silmek istediğinize emin misiniz?',\n\n  // Original text: \"Connected\"\n  pifConnected: 'Bağlı',\n\n  // Original text: \"Disconnected\"\n  pifDisconnected: 'Bağlı değil',\n\n  // Original text: \"Physically connected\"\n  pifPhysicallyConnected: 'Fiziksel olarak bağlı',\n\n  // Original text: \"Physically disconnected\"\n  pifPhysicallyDisconnected: 'Fiziksel olarak bağlı değil',\n\n  // Original text: \"Username\"\n  username: 'Kullanıcı adı',\n\n  // Original text: \"Password\"\n  password: 'Parola',\n\n  // Original text: \"Language\"\n  language: 'Dil',\n\n  // Original text: \"Old password\"\n  oldPasswordPlaceholder: 'Eski parola',\n\n  // Original text: \"New password\"\n  newPasswordPlaceholder: 'Yeni parola',\n\n  // Original text: \"Confirm new password\"\n  confirmPasswordPlaceholder: 'Yeni parolayı onayla',\n\n  // Original text: \"Confirmation password incorrect\"\n  confirmationPasswordError: 'Onay parola yanlış',\n\n  // Original text: \"Password does not match the confirm password.\"\n  confirmationPasswordErrorBody: 'Parola, onaylama parolasıyla uyuşmuyor.',\n\n  // Original text: \"Password changed\"\n  pwdChangeSuccess: 'Parola değişti',\n\n  // Original text: \"Your password has been successfully changed.\"\n  pwdChangeSuccessBody: 'Parolanız başarılı bir şekilde değiştirildi',\n\n  // Original text: \"Incorrect password\"\n  pwdChangeError: 'Yanlış parola',\n\n  // Original text: \"The old password provided is incorrect. Your password has not been changed.\"\n  pwdChangeErrorBody: 'Girilen eski parola yanlış. Parolanız değiştirilmedi.',\n\n  // Original text: \"OK\"\n  changePasswordOk: 'Tamam',\n\n  // Original text: \"SSH keys\"\n  sshKeys: 'SSH anahtarları',\n\n  // Original text: \"New SSH key\"\n  newSshKey: 'Yeni SSH anahtarı',\n\n  // Original text: \"Delete\"\n  deleteSshKey: 'Sil',\n\n  // Original text: \"Delete selected SSH keys\"\n  deleteSshKeys: 'Seçilen SSH anahtarını sil',\n\n  // Original text: \"No SSH keys\"\n  noSshKeys: 'SSH anahtarı yok',\n\n  // Original text: \"New SSH key\"\n  newSshKeyModalTitle: 'Yeni SSH anahtarı',\n\n  // Original text: \"Invalid key\"\n  sshKeyErrorTitle: 'Geçersiz anahtar',\n\n  // Original text: \"An SSH key requires both a title and a key.\"\n  sshKeyErrorMessage: 'Bir SSH anahtarı hem bir başlık hem de bir anahtar gerektirir.',\n\n  // Original text: \"Title\"\n  title: 'Başlık',\n\n  // Original text: \"Key\"\n  key: 'Anahtar',\n\n  // Original text: \"Delete SSH key\"\n  deleteSshKeyConfirm: 'SSH anahtarını sil',\n\n  // Original text: \"Are you sure you want to delete the SSH key {title}?\"\n  deleteSshKeyConfirmMessage: 'SSH anahtarını silmek istediğinizden emin misiniz {title}?',\n\n  // Original text: \"Delete SSH key{nKeys, plural, one {} other {s}}\"\n  deleteSshKeysConfirm: 'SSH anahtarı sil',\n\n  // Original text: \"Are you sure you want to delete {nKeys, number} SSH key{nKeys, plural, one {} other {s}}?\"\n  deleteSshKeysConfirmMessage: '{nKeys, number} SSH anahtarını silmek istediğinize emin misiniz?',\n\n  // Original text: \"Others\"\n  others: 'Diğerleri',\n\n  // Original text: \"Loading logs…\"\n  loadingLogs: 'Günlükler yükleniyor...',\n\n  // Original text: \"User\"\n  logUser: 'Kullanıcı',\n\n  // Original text: \"Method\"\n  logMethod: 'Metod',\n\n  // Original text: \"Params\"\n  logParams: 'Parametreler',\n\n  // Original text: \"Message\"\n  logMessage: 'Mesaj',\n\n  // Original text: \"Error\"\n  logError: 'Hata',\n\n  // Original text: 'Logs'\n  logTitle: undefined,\n\n  // Original text: \"Display details\"\n  logDisplayDetails: 'Ayrıntıları görüntüle',\n\n  // Original text: \"Date\"\n  logTime: 'Tarih',\n\n  // Original text: \"No stack trace\"\n  logNoStackTrace: 'Yığın izi yok',\n\n  // Original text: \"No params\"\n  logNoParams: 'Parametre yok',\n\n  // Original text: \"Delete log\"\n  logDelete: 'Günlük sil',\n\n  // Original text: \"Delete logs\"\n  logsDelete: 'Günlükleri sil',\n\n  // Original text: \"Delete log{nLogs, plural, one {} other {s}}\"\n  logDeleteMultiple: 'Günlük(leri) sil',\n\n  // Original text: \"Are you sure you want to delete {nLogs, number} log{nLogs, plural, one {} other {s}}?\"\n  logDeleteMultipleMessage: '{nLogs, number} günlük silmek istediğinize emin misiniz?',\n\n  // Original text: \"Delete all logs\"\n  logDeleteAll: 'Bütün günlükleri sil',\n\n  // Original text: \"Delete all logs\"\n  logDeleteAllTitle: 'Bütün günlükleri sil',\n\n  // Original text: \"Are you sure you want to delete all the logs?\"\n  logDeleteAllMessage: 'Bütün günlükleri silmek istediğinizden emin misiniz?',\n\n  // Original text: \"Click to enable\"\n  logIndicationToEnable: 'Açmak için tıklayın',\n\n  // Original text: \"Click to disable\"\n  logIndicationToDisable: 'Kapatmak için tıklayın',\n\n  // Original text: \"Report a bug\"\n  reportBug: 'Bir hata raporla',\n\n  // Original text: \"Job canceled to protect the VDI chain\"\n  unhealthyVdiChainError: 'VDI zincirini korumak için iş iptal edildi',\n\n  // Original text: \"Restart VM's backup\"\n  backupRestartVm: undefined,\n\n  // Original text: \"Click for more information\"\n  clickForMoreInformation: 'Daha fazla bilgi için tıklayın',\n\n  // Original text: \"Name\"\n  ipPoolName: 'Ad',\n\n  // Original text: \"IPs\"\n  ipPoolIps: \"IP'ler\",\n\n  // Original text: \"IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)\"\n  ipPoolIpsPlaceholder: \"IP'ler (örn: 1.0.0.12-1.0.0.17;1.0.0.23)\",\n\n  // Original text: \"Networks\"\n  ipPoolNetworks: 'Ağlar',\n\n  // Original text: \"No IP pools\"\n  ipsNoIpPool: 'IP havuzu yok',\n\n  // Original text: \"Create\"\n  ipsCreate: 'Oluştur',\n\n  // Original text: \"Delete all IP pools\"\n  ipsDeleteAllTitle: 'Bütün IP havuzlarını sil',\n\n  // Original text: \"Are you sure you want to delete all the IP pools?\"\n  ipsDeleteAllMessage: 'Bütün IP havuzlarını silmek istediğinize emin misiniz?',\n\n  // Original text: \"VIFs\"\n  ipsVifs: \"VIF'ler\",\n\n  // Original text: \"Not used\"\n  ipsNotUsed: 'Kullanılmadı',\n\n  // Original text: \"unknown VIF\"\n  ipPoolUnknownVif: 'Bilinmeyen VIF',\n\n  // Original text: \"Name already exists\"\n  ipPoolNameAlreadyExists: 'Ad zaten var',\n\n  // Original text: \"Keyboard shortcuts\"\n  shortcutModalTitle: 'Klavye kısayolu',\n\n  // Original text: \"Global\"\n  shortcut_XoApp: 'Global',\n\n  // Original text: \"Go to hosts list\"\n  shortcut_XoApp_GO_TO_HOSTS: 'Sunucu listesine git',\n\n  // Original text: \"Go to pools list\"\n  shortcut_XoApp_GO_TO_POOLS: 'Havuz listesine git',\n\n  // Original text: \"Go to VMs list\"\n  shortcut_XoApp_GO_TO_VMS: 'VM listesine git',\n\n  // Original text: \"Go to SRs list\"\n  shortcut_XoApp_GO_TO_SRS: 'SR listesine git',\n\n  // Original text: \"Create a new VM\"\n  shortcut_XoApp_CREATE_VM: 'Yeni bir VM oluştur',\n\n  // Original text: \"Unfocus field\"\n  shortcut_XoApp_UNFOCUS: 'Odaklanmamış alan',\n\n  // Original text: \"Show shortcuts key bindings\"\n  shortcut_XoApp_HELP: 'Kısayol tuşlarını göster',\n\n  // Original text: \"Home\"\n  shortcut_Home: 'Ev',\n\n  // Original text: \"Focus search bar\"\n  shortcut_Home_SEARCH: 'Arama çubuğuna odaklan',\n\n  // Original text: \"Next item\"\n  shortcut_Home_NAV_DOWN: 'Sonraki öğe',\n\n  // Original text: \"Previous item\"\n  shortcut_Home_NAV_UP: 'Önceki öğe',\n\n  // Original text: \"Select item\"\n  shortcut_Home_SELECT: 'Öğe seç',\n\n  // Original text: \"Open\"\n  shortcut_Home_JUMP_INTO: 'Aç',\n\n  // Original text: \"Supported tables\"\n  shortcut_SortedTable: 'Desteklenen tablolar',\n\n  // Original text: \"Focus the table search bar\"\n  shortcut_SortedTable_SEARCH: 'Tablo arama çubuğuna odaklan',\n\n  // Original text: \"Next item\"\n  shortcut_SortedTable_NAV_DOWN: 'Sonraki öğe',\n\n  // Original text: \"Previous item\"\n  shortcut_SortedTable_NAV_UP: 'Önceki öğe',\n\n  // Original text: \"Select item\"\n  shortcut_SortedTable_SELECT: 'Öğe seç',\n\n  // Original text: \"Action\"\n  shortcut_SortedTable_ROW_ACTION: 'Aksiyon',\n\n  // Original text: \"VM\"\n  settingsAclsButtonTooltipVM: 'VM',\n\n  // Original text: \"Hosts\"\n  settingsAclsButtonTooltiphost: 'Sunucular',\n\n  // Original text: \"Pool\"\n  settingsAclsButtonTooltippool: 'Havuz',\n\n  // Original text: \"SR\"\n  settingsAclsButtonTooltipSR: 'SR',\n\n  // Original text: \"Network\"\n  settingsAclsButtonTooltipnetwork: 'Ağ',\n\n  // Original text: 'Template'\n  settingsCloudConfigTemplate: undefined,\n\n  // Original text: 'Delete cloud config{nCloudConfigs, plural, one {} other {s}}'\n  confirmDeleteCloudConfigsTitle: undefined,\n\n  // Original text: 'Are you sure you want to delete {nCloudConfigs, number} cloud config{nCloudConfigs, plural, one {} other {s}}?'\n  confirmDeleteCloudConfigsBody: undefined,\n\n  // Original text: 'Delete cloud config'\n  deleteCloudConfig: undefined,\n\n  // Original text: 'Edit cloud config'\n  editCloudConfig: undefined,\n\n  // Original text: 'Delete selected cloud configs'\n  deleteSelectedCloudConfigs: undefined,\n\n  // Original text: \"No config file selected\"\n  noConfigFile: 'Yapılandırma dosyası seçilmedi',\n\n  // Original text: \"Try dropping a config file here, or click to select a config file to upload.\"\n  importTip: 'Bir yapılandırma dosyasını buraya sürükleyin veya seçmek için tıklayın',\n\n  // Original text: \"Config\"\n  config: 'Yapılandırma',\n\n  // Original text: \"Import\"\n  importConfig: 'İçe aktar',\n\n  // Original text: \"Config file successfully imported\"\n  importConfigSuccess: 'Yapılandırma dosyası başarıyla alındı',\n\n  // Original text: \"Error while importing config file\"\n  importConfigError: 'Yapılandırma dosyası içe aktarılırken hata oluştu',\n\n  // Original text: \"Export\"\n  exportConfig: 'Dışa aktar',\n\n  // Original text: \"Download current config\"\n  downloadConfig: 'Mevcut yapılandırmayı indir',\n\n  // Original text: \"No config import available for Community Edition\"\n  noConfigImportCommunity: 'Topluluk Sürümünde yapılandırma içe aktarma özelliği yoktur',\n\n  // Original text: \"Reconnect all hosts\"\n  srReconnectAllModalTitle: 'Tüm sunuculara tekrar bağlan',\n\n  // Original text: \"This will reconnect this SR to all its hosts.\"\n  srReconnectAllModalMessage: \"Bu işlem bu SR'yi sunucularına tekrar bağlayacak.\",\n\n  // Original text: \"This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR).\"\n  srsReconnectAllModalMessage:\n    \"Bu işlem seçili yerel SR'leri sunucularına, paylaşımlı SR'leri havuzuna tekrar bağlayacak.\",\n\n  // Original text: \"Disconnect all hosts\"\n  srDisconnectAllModalTitle: 'Tüm sunucuların bağlantısını kes',\n\n  // Original text: \"This will disconnect this SR from all its hosts.\"\n  srDisconnectAllModalMessage: \"Bu işlem bu SR'nin sunucu bağlantısını kesecek.\",\n\n  // Original text: \"This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).\"\n  srsDisconnectAllModalMessage:\n    \"Bu işlem seçili yerel SR'nin sunucu bağlantısını, paylaşımlı SR'nin havuz bağlantısını kesecek.\",\n\n  // Original text: \"Forget SR\"\n  srForgetModalTitle: 'SR Unut',\n\n  // Original text: \"Forget selected SRs\"\n  srsForgetModalTitle: \"Seçili SR'leri unut\",\n\n  // Original text: \"Are you sure you want to forget this SR? VDIs on this storage won't be removed.\"\n  srForgetModalMessage: \"Bu SR'yi unutmak istediğinize emin misinz? Bu işlem SR üzerindeki VDI'ları silmez.\",\n\n  // Original text: \"Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed.\"\n  srsForgetModalMessage: \"Seçili SR'leri unutmak istediğinize emin misinz? Bu işlem SR'ler üzerindeki VDI'ları silmez.\",\n\n  // Original text: \"Disconnected\"\n  srAllDisconnected: 'Bağlantıyı kes',\n\n  // Original text: \"Partially connected\"\n  srSomeConnected: 'Kısmen bağlı',\n\n  // Original text: \"Connected\"\n  srAllConnected: 'Bağlı',\n\n  // Original text: \"XOSAN\"\n  xosanTitle: 'XOSAN',\n\n  // Original text: \"Xen Orchestra SAN SR\"\n  xosanSrTitle: 'Xen Orchestra SAN SR',\n\n  // Original text: \"Select local SRs (lvm)\"\n  xosanAvailableSrsTitle: \"Yerel SR'leri seçin (lvm)\",\n\n  // Original text: \"Suggestions\"\n  xosanSuggestions: 'Öneriler',\n\n  // Original text: \"Warning: using disperse layout is not recommended right now. Please read {link}.\"\n  xosanDisperseWarning: 'Uyarı: dağıtma düzeninin kullanılması şu anda önerilmez. Lütfen okuyun {link}.',\n\n  // Original text: \"Name\"\n  xosanName: 'Ad',\n\n  // Original text: \"Host\"\n  xosanHost: 'Sunucu',\n\n  // Original text: \"Connected Hosts\"\n  xosanHosts: 'Bağlı Sunucular',\n\n  // Original text: \"Pool\"\n  xosanPool: 'Havuz',\n\n  // Original text: \"Volume ID\"\n  xosanVolumeId: 'Volume ID',\n\n  // Original text: \"Size\"\n  xosanSize: 'Boyut',\n\n  // Original text: \"Used space\"\n  xosanUsedSpace: 'Kullanılan alan',\n\n  // Original text: \"License\"\n  license: 'Lisans',\n\n  // Original text: \"This XOSAN has more than 1 license!\"\n  xosanMultipleLicenses: \"Bu XOSAN 1'den fazla lisansa sahip!\",\n\n  // Original text: \"XOSAN pack needs to be installed and up to date on each host of the pool.\"\n  xosanNeedPack: 'XOSAN paketinin havuzdaki tüm sunuculara kurulması gerekir.',\n\n  // Original text: \"Install it now!\"\n  xosanInstallIt: 'Şimdi kur!',\n\n  // Original text: \"Some hosts need their toolstack to be restarted before you can create an XOSAN\"\n  xosanNeedRestart: \"XOSAN oluşturmadan önce bazı sunucuların toolstack'ı yeniden başlatılmalı\",\n\n  // Original text: \"Restart toolstacks\"\n  xosanRestartAgents: \"toolstack'ları yeniden başlat\",\n\n  // Original text: \"Pool master is not running\"\n  xosanMasterOffline: \"Havuzun master'ı çalışmıyor\",\n\n  // Original text: \"Install XOSAN pack on {pool}\"\n  xosanInstallPackTitle: 'XOSAN paketini şuraya kur {pool}',\n\n  // Original text: \"Select at least 2 SRs\"\n  xosanSelect2Srs: 'En az 2 SR seçin',\n\n  // Original text: \"Layout\"\n  xosanLayout: 'Düzen',\n\n  // Original text: \"Redundancy\"\n  xosanRedundancy: 'Fazlalık',\n\n  // Original text: \"Capacity\"\n  xosanCapacity: 'Kapasite',\n\n  // Original text: \"Available space\"\n  xosanAvailableSpace: 'Kullanılabilir alan',\n\n  // Original text: \"* Can fail without data loss\"\n  xosanDiskLossLegend: '* Veri kaybı olmadan başarısız olabilir',\n\n  // Original text: \"Create\"\n  xosanCreate: 'Oluştur',\n\n  // Original text: \"Add\"\n  xosanAdd: 'Ekle',\n\n  // Original text: \"Installing XOSAN. Please wait…\"\n  xosanInstalling: 'XOSAN yükleniyor. Lütfen bekleyin...',\n\n  // Original text: \"No XOSAN available for Community Edition\"\n  xosanCommunity: 'Topluluk sürümünde XOSAN özelliği yoktur',\n\n  // Original text: \"New\"\n  xosanNew: 'Yeni',\n\n  // Original text: \"Advanced\"\n  xosanAdvanced: 'Gelişmiş',\n\n  // Original text: \"Remove subvolumes\"\n  xosanRemoveSubvolumes: \"Alt volume'leri kaldır\",\n\n  // Original text: \"Add subvolume…\"\n  xosanAddSubvolume: 'Alt volume ekle...',\n\n  // Original text: \"This version of XOSAN SR is from the first beta phase. You can keep using it, but to modify it you'll have to save your disks and re-create it.\"\n  xosanWarning:\n    \"XOSAN SR'nin bu versiyonu ilk beta sürümünden. Kullanmaya devam edebilirsiniz, ancak değiştirmek için disklerinizi kaydetmeniz ve yeniden oluşturmanız gerekir.\",\n\n  // Original text: \"VLAN\"\n  xosanVlan: 'VLAN',\n\n  // Original text: \"No XOSAN found\"\n  xosanNoSrs: 'XOSAN bulunamadı',\n\n  // Original text: \"Some SRs are detached from the XOSAN\"\n  xosanPbdsDetached: \"Bazı SR'ler XOSAN'dan ayrıldı\",\n\n  // Original text: \"Something is wrong with: {badStatuses}\"\n  xosanBadStatus: 'Şununla ilgili birşeyler yanlış: {badStatuses}',\n\n  // Original text: \"Running\"\n  xosanRunning: 'Çalışıyor',\n\n  // Original text: 'Update packs'\n  xosanUpdatePacks: undefined,\n\n  // Original text: 'Checking for updates'\n  xosanPackUpdateChecking: undefined,\n\n  // Original text: 'Error while checking XOSAN packs. Please make sure that the Cloud plugin is installed and loaded and that the updater is reachable.'\n  xosanPackUpdateError: undefined,\n\n  // Original text: 'XOSAN resources are unavailable'\n  xosanPackUpdateUnavailable: undefined,\n\n  // Original text: 'Not registered for XOSAN resources'\n  xosanPackUpdateUnregistered: undefined,\n\n  // Original text: \"✓ This pool's XOSAN packs are up to date!\"\n  xosanPackUpdateUpToDate: undefined,\n\n  // Original text: 'Update pool with latest pack v{version}'\n  xosanPackUpdateVersion: undefined,\n\n  // Original text: \"Delete XOSAN\"\n  xosanDelete: 'XOSAN sil',\n\n  // Original text: \"Fix\"\n  xosanFixIssue: 'Onar',\n\n  // Original text: \"Creating XOSAN on {pool}\"\n  xosanCreatingOn: '{pool} üzerinde XOSAN oluşturuluyor',\n\n  // Original text: \"Configuring network…\"\n  xosanState_configuringNetwork: 'Ağ yapılandırılıyor...',\n\n  // Original text: \"Importing VM…\"\n  xosanState_importingVm: 'VM içe aktarılıyor...',\n\n  // Original text: \"Copying VMs…\"\n  xosanState_copyingVms: \"VM'ler kopyalanıyor...\",\n\n  // Original text: \"Configuring VMs…\"\n  xosanState_configuringVms: \"VM'ler yapılandırılıyor...\",\n\n  // Original text: \"Configuring gluster…\"\n  xosanState_configuringGluster: 'Gluster yapılandırılıyor...',\n\n  // Original text: \"Creating SR…\"\n  xosanState_creatingSr: 'SR oluşturuluyor...',\n\n  // Original text: \"Scanning SR…\"\n  xosanState_scanningSr: 'SR taranıyor...',\n\n  // Original text: \"Install cloud plugin first\"\n  xosanInstallCloudPlugin: 'Önce XOA eklentisini kurun',\n\n  // Original text: \"Load cloud plugin first\"\n  xosanLoadCloudPlugin: 'Önce XOA eklentisini yükleyin',\n\n  // Original text: \"Register your appliance first\"\n  xosanRegister: \"Önce XOSAN'ın kaydını yapın\",\n\n  // Original text: \"Loading…\"\n  xosanLoading: 'Yükleniyor...',\n\n  // Original text: \"XOSAN is not available at the moment\"\n  xosanNotAvailable: 'XOSAN şuan kullanılabilir değil',\n\n  // Original text: \"No compatible XOSAN pack found for your XenServer versions.\"\n  xosanNoPackFound: 'XenServer sürümünüz ile uyumlu XOSAN paketi bulunamadı.',\n\n  // Original text: \"Some XOSAN Virtual Machines are not running\"\n  xosanVmsNotRunning: 'Bazı XOSAN Sanal Makinaları çalışmıyor',\n\n  // Original text: \"Some XOSAN Virtual Machines could not be found\"\n  xosanVmsNotFound: 'Bazı XOSAN Sanal Makinaları bulunamadı',\n\n  // Original text: \"Files needing healing\"\n  xosanFilesNeedingHealing: 'İyileştirme ihtiyacı olan dosyalar',\n\n  // Original text: \"Some XOSAN Virtual Machines have files needing healing\"\n  xosanFilesNeedHealing: 'Bazı XOSAN Sanal Makinalarında iyileştirilmesi gereken dosyalar var',\n\n  // Original text: \"Host {hostName} is not in XOSAN network\"\n  xosanHostNotInNetwork: '{hostName} sunucusu XOSAN ağında değil',\n\n  // Original text: \"VM controller\"\n  xosanVm: 'VM denetleyici',\n\n  // Original text: \"SR\"\n  xosanUnderlyingStorage: 'SR',\n\n  // Original text: \"Replace…\"\n  xosanReplace: 'Değiştir...',\n\n  // Original text: \"On same VM\"\n  xosanOnSameVm: 'Aynı VM üzerinde',\n\n  // Original text: \"Brick name\"\n  xosanBrickName: 'Brick adı',\n\n  // Original text: \"Brick UUID\"\n  xosanBrickUuid: 'Brick UUID',\n\n  // Original text: \"Brick size\"\n  xosanBrickSize: 'Brick boyutu',\n\n  // Original text: \"Memory size\"\n  xosanMemorySize: 'Bellek boyutu',\n\n  // Original text: \"Status\"\n  xosanStatus: 'Durum',\n\n  // Original text: \"Arbiter\"\n  xosanArbiter: 'Hakem',\n\n  // Original text: \"Used Inodes\"\n  xosanUsedInodes: 'Kullanılan düğümler',\n\n  // Original text: \"Block size\"\n  xosanBlockSize: 'Blok boyutu',\n\n  // Original text: \"Device\"\n  xosanDevice: 'Cihaz',\n\n  // Original text: \"FS name\"\n  xosanFsName: 'FS adı',\n\n  // Original text: \"Mount options\"\n  xosanMountOptions: 'Mount seçenekleri',\n\n  // Original text: \"Path\"\n  xosanPath: 'Yol',\n\n  // Original text: \"Job\"\n  xosanJob: 'İş',\n\n  // Original text: \"PID\"\n  xosanPid: 'PID',\n\n  // Original text: \"Port\"\n  xosanPort: 'Port',\n\n  // Original text: \"Missing values\"\n  xosanReplaceBrickErrorTitle: 'Eksik değerler',\n\n  // Original text: \"You need to select a SR and a size\"\n  xosanReplaceBrickErrorMessage: 'Bir SR ve bir boyut seçmelisiniz',\n\n  // Original text: \"Bad values\"\n  xosanAddSubvolumeErrorTitle: 'Kötü değerler',\n\n  // Original text: \"You need to select {nSrs, number} and a size\"\n  xosanAddSubvolumeErrorMessage: '{nSrs, number} ve bir boyut seçmelisiniz',\n\n  // Original text: \"Select {nSrs, number} SRs\"\n  xosanSelectNSrs: '{nSrs, number} SR seç',\n\n  // Original text: \"Run\"\n  xosanRun: 'Çalıştır',\n\n  // Original text: \"Remove\"\n  xosanRemove: 'Kaldır',\n\n  // Original text: \"Volume\"\n  xosanVolume: 'Volume',\n\n  // Original text: \"Volume options\"\n  xosanVolumeOptions: 'Volume seçenekleri',\n\n  // Original text: \"Could not find VM\"\n  xosanCouldNotFindVm: 'VM bulunamadı',\n\n  // Original text: \"Using {usage}\"\n  xosanUnderlyingStorageUsage: 'Kullanım {usage}',\n\n  // Original text: \"Custom IP network (/24)\"\n  xosanCustomIpNetwork: 'Özel IP ağı (/24)',\n\n  // Original text: \"Will configure the host xosan network device with a static IP address and plug it in.\"\n  xosanIssueHostNotInNetwork: 'Sunucu, xosan ağ cihazını statik bir IP adresiyle yapılandırır ve fişe takar.',\n\n  // Original text: \"Licenses\"\n  licensesTitle: 'Lisanslar',\n\n  // Original text: \"You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}\"\n  xosanUnregisteredDisclaimer: 'Kayıtlı değilsiniz ve bu nedenle XOSAN SR oluşturamaz veya yönetemezsiniz. {Link}',\n\n  // Original text: \"In order to create a XOSAN SR, you need to use the Xen Orchestra Appliance and buy a XOSAN license on {link}.\"\n  xosanSourcesDisclaimer:\n    \"Bir XOSAN SR oluşturmak için, XOA kullanmanız ve {link} 'ten bir XOSAN lisansı satın almanız gerekir.\",\n\n  // Original text: \"Register now!\"\n  registerNow: 'Şimdi kaydol!',\n\n  // Original text: \"You need to register your appliance to manage your licenses.\"\n  licensesUnregisteredDisclaimer: 'Lisanlarınızı yönetmek için kaydolmalısınız.',\n\n  // Original text: \"Product\"\n  licenseProduct: 'Ürün',\n\n  // Original text: \"Attached to\"\n  licenseBoundObject: 'Şuraya ekli',\n\n  // Original text: \"Purchaser\"\n  licensePurchaser: 'Müşteri',\n\n  // Original text: \"Expires\"\n  licenseExpires: 'Süre bitimi',\n\n  // Original text: \"You\"\n  licensePurchaserYou: 'Siz',\n\n  // Original text: \"Support\"\n  productSupport: 'Destek',\n\n  // Original text: \"No XOSAN attached\"\n  licenseNotBoundXosan: 'Ekli XOSAN yok',\n\n  // Original text: \"License attached to an unknown XOSAN\"\n  licenseBoundUnknownXosan: \"Lisans bilinmeyen bir XOSAN'a ekli\",\n\n  // Original text: \"Manage the licenses\"\n  licensesManage: 'Lisansları yönet',\n\n  // Original text: \"New license\"\n  newLicense: 'Yeni lisans',\n\n  // Original text: \"Refresh\"\n  refreshLicenses: 'Yenile',\n\n  // Original text: \"Limited size because XOSAN is in trial\"\n  xosanLicenseRestricted: 'Limitli boyut çünkü XOSAN deneme sürecinde',\n\n  // Original text: \"You need a license on this SR to manage the XOSAN.\"\n  xosanAdminNoLicenseDisclaimer: \"XOSAN'ı yönetmek için bu SR üzerinde bir lisansa ihtiyacınız var.\",\n\n  // Original text: \"Your XOSAN license has expired. You can still use the SR but cannot administrate it anymore.\"\n  xosanAdminExpiredLicenseDisclaimer:\n    \"XOSAN lisansınızın süresi doldu. SR'yi hala kullanabilirsiniz ancak artık yönetemezsiniz.\",\n\n  // Original text: \"Could not check the license on this XOSAN SR\"\n  xosanCheckLicenseError: \"Bu XOSAN SR'deki lisans kontrol edilemedi\",\n\n  // Original text: \"Could not fetch licenses\"\n  getLicensesError: 'Lisans alınamadı',\n\n  // Original text: \"License has expired.\"\n  xosanLicenseHasExpired: \"Lisnas'ın süresi doldu\",\n\n  // Original text: \"License expires on {date}.\"\n  licenseExpiresDate: 'Lisans {date} tarihinde bitecek.',\n\n  // Original text: \"Update the license now!\"\n  updateLicenseMessage: \"Lisans'ı şimdi güncelle!\",\n\n  // Original text: \"Unknown XOSAN SR.\"\n  xosanUnknownSr: 'Bilinmeyen XOSAN SR',\n\n  // Original text: \"Contact us!\"\n  contactUs: 'Bizimle iletişime geçin!',\n\n  // Original text: \"No license.\"\n  xosanNoLicense: 'Lisans yok.',\n\n  // Original text: \"Unlock now!\"\n  unlockNow: 'Şimdi kilidi kaldır!',\n\n  // Original text: \"Select a license\"\n  selectLicense: 'Bir lisans seç',\n\n  // Original text: \"Bind license\"\n  bindLicense: \"Lisans'ı bağla\",\n\n  // Original text: \"expires on {date}\"\n  expiresOn: '{date} tarihinde bitecek',\n\n  // Original text: \"Install XOA plugin first\"\n  xosanInstallXoaPlugin: 'Önce XOA eklentisini kurun',\n\n  // Original text: \"Load XOA plugin first\"\n  xosanLoadXoaPlugin: 'Önce XOA eklentisini yükleyin',\n\n  // Original text: '{seconds, plural, one {# second} other {# seconds}}'\n  secondsFormat: undefined,\n\n  // Original text: \"{days, plural, =0 {} one {# day } other {# days }}{hours, plural, =0 {} one {# hour } other {# hours }}{minutes, plural, =0 {} one {# minute } other {# minutes }}{seconds, plural, =0 {} one {# second} other {# seconds}}\"\n  durationFormat:\n    '{days, plural, =0 {} one {# day } other {# days }}{hours, plural, =0 {} one {# hour } other {# hours }}{minutes, plural, =0 {} one {# minute } other {# minutes }}{seconds, plural, =0 {} one {# second} other {# seconds}}',\n}\n","// See http://momentjs.com/docs/#/use-it/browserify/\nimport 'moment/locale/zh-cn'\n\nimport reactIntlData from 'react-intl/locale-data/zh'\nimport { addLocaleData } from 'react-intl'\naddLocaleData(reactIntlData)\n\n// ===================================================================\n\nexport default {\n  // Original text: \"Long click to edit\"\n  editableLongClickPlaceholder: '长按编辑',\n\n  // Original text: \"Click to edit\"\n  editableClickPlaceholder: '点击编辑',\n\n  // Original text: \"OK\"\n  alertOk: '确认',\n\n  // Original text: \"OK\"\n  confirmOk: '确认',\n\n  // Original text: \"Cancel\"\n  confirmCancel: '取消',\n\n  // Original text: \"On error\"\n  onError: '出现错误',\n\n  // Original text: \"Successful\"\n  successful: '成功',\n\n  // Original text: \"Home\"\n  homePage: '主页',\n\n  // Original text: \"Dashboard\"\n  dashboardPage: '仪表盘',\n\n  // Original text: \"Overview\"\n  overviewDashboardPage: '概览',\n\n  // Original text: \"Visualizations\"\n  overviewVisualizationDashboardPage: '虚拟化',\n\n  // Original text: \"Statistics\"\n  overviewStatsDashboardPage: '状态统计',\n\n  // Original text: \"Health\"\n  overviewHealthDashboardPage: '健康状态',\n\n  // Original text: \"Self service\"\n  selfServicePage: '自助服务',\n\n  // Original text: \"Dashboard\"\n  selfServiceDashboardPage: '仪表盘',\n\n  // Original text: \"Administration\"\n  selfServiceAdminPage: '管理',\n\n  // Original text: \"Backup\"\n  backupPage: '备份',\n\n  // Original text: \"Jobs\"\n  jobsPage: '任务',\n\n  // Original text: \"Updates\"\n  updatePage: '更新',\n\n  // Original text: \"Settings\"\n  settingsPage: '设置',\n\n  // Original text: \"Servers\"\n  settingsServersPage: '服务器',\n\n  // Original text: \"Users\"\n  settingsUsersPage: '用户',\n\n  // Original text: \"Groups\"\n  settingsGroupsPage: '组',\n\n  // Original text: \"ACLs\"\n  settingsAclsPage: '访问控制',\n\n  // Original text: \"Plugins\"\n  settingsPluginsPage: '插件',\n\n  // Original text: \"About\"\n  aboutPage: '关于',\n\n  // Original text: \"New\"\n  newMenu: '新建',\n\n  // Original text: \"Tasks\"\n  taskMenu: '任务',\n\n  // Original text: \"Tasks\"\n  taskPage: '任务',\n\n  // Original text: \"VM\"\n  newVmPage: '虚拟机',\n\n  // Original text: \"Storage\"\n  newSrPage: '存储',\n\n  // Original text: \"Server\"\n  newServerPage: '服务器',\n\n  // Original text: \"Import\"\n  newImport: '导入',\n\n  // Original text: \"Overview\"\n  backupOverviewPage: '概览',\n\n  // Original text: \"New\"\n  backupNewPage: '新建',\n\n  // Original text: \"Remotes\"\n  backupRemotesPage: '远程',\n\n  // Original text: \"Restore\"\n  backupRestorePage: '恢复',\n\n  // Original text: \"Schedule\"\n  schedule: '计划',\n\n  // Original text: \"New VM backup\"\n  newVmBackup: '新建虚拟机备份',\n\n  // Original text: \"Edit VM backup\"\n  editVmBackup: '编辑虚拟机备份',\n\n  // Original text: \"Backup\"\n  backup: '备份',\n\n  // Original text: \"Rolling Snapshot\"\n  rollingSnapshot: '滚动快照',\n\n  // Original text: \"Delta Backup\"\n  deltaBackup: '差异备份',\n\n  // Original text: \"Disaster Recovery\"\n  disasterRecovery: '灾难恢复',\n\n  // Original text: \"Continuous Replication\"\n  continuousReplication: '持续复制',\n\n  // Original text: \"Overview\"\n  jobsOverviewPage: '概览',\n\n  // Original text: \"New\"\n  jobsNewPage: '新建',\n\n  // Original text: \"Scheduling\"\n  jobsSchedulingPage: '计划',\n\n  // Original text: \"Custom Job\"\n  customJob: '自定义任务',\n\n  // Original text: \"User\"\n  userPage: '用户',\n\n  // Original text: \"Sign out\"\n  signOut: '注销',\n\n  // Original text: \"Fetching data…\"\n  homeFetchingData: '获取数据',\n\n  // Original text: \"Welcome on Xen Orchestra!\"\n  homeWelcome: '欢迎使用Xen Orchestra',\n\n  // Original text: \"Add your XCP-ng hosts or pools\"\n  homeWelcomeText: '添加您的XenServer主机或资源池',\n\n  // Original text: \"Want some help?\"\n  homeHelp: '需要帮助？',\n\n  // Original text: \"Add server\"\n  homeAddServer: '添加服务器',\n\n  // Original text: \"Online Doc\"\n  homeOnlineDoc: '在线文档',\n\n  // Original text: \"Pro Support\"\n  homeProSupport: '专业支持',\n\n  // Original text: \"There are no VMs!\"\n  homeNoVms: '没有可用的虚拟机',\n\n  // Original text: \"Or…\"\n  homeNoVmsOr: '或',\n\n  // Original text: \"Import VM\"\n  homeImportVm: '导入虚拟机',\n\n  // Original text: \"Import an existing VM in xva format\"\n  homeImportVmMessage: '导入一个XVA格式的虚拟机',\n\n  // Original text: \"Restore a backup\"\n  homeRestoreBackup: '恢复到备份',\n\n  // Original text: \"Restore a backup from a remote store\"\n  homeRestoreBackupMessage: '恢复到远程存储上的备份',\n\n  // Original text: \"This will create a new VM\"\n  homeNewVmMessage: '将创建一个新的虚拟机',\n\n  // Original text: \"Filters\"\n  homeFilters: '过滤器',\n\n  // Original text: \"Pool\"\n  homeTypePool: '资源池',\n\n  // Original text: \"Host\"\n  homeTypeHost: '主机',\n\n  // Original text: \"VM\"\n  homeTypeVm: '虚拟机',\n\n  // Original text: \"SR\"\n  homeTypeSr: '数据存储',\n\n  // Original text: \"VDI\"\n  homeTypeVdi: '虚拟硬盘',\n\n  // Original text: \"Sort\"\n  homeSort: '排序',\n\n  // Original text: \"Pools\"\n  homeAllPools: '资源池',\n\n  // Original text: \"Hosts\"\n  homeAllHosts: '主机',\n\n  // Original text: \"Tags\"\n  homeAllTags: '标签',\n\n  // Original text: \"New VM\"\n  homeNewVm: '新建虚拟机',\n\n  // Original text: \"Running hosts\"\n  homeFilterRunningHosts: '正在运行的主机',\n\n  // Original text: \"Disabled hosts\"\n  homeFilterDisabledHosts: '不可用的主机',\n\n  // Original text: \"Running VMs\"\n  homeFilterRunningVms: '正在运行的虚拟机',\n\n  // Original text: \"Non running VMs\"\n  homeFilterNonRunningVms: '未运行的虚拟机',\n\n  // Original text: \"Pending VMs\"\n  homeFilterPendingVms: '正在创建的虚拟机',\n\n  // Original text: \"HVM guests\"\n  homeFilterHvmGuests: 'HVM客户机',\n\n  // Original text: \"Tags\"\n  homeFilterTags: '标签',\n\n  // Original text: \"Sort by\"\n  homeSortBy: '排序方式',\n\n  // Original text: \"Name\"\n  homeSortByName: '名称',\n\n  // Original text: \"Power state\"\n  homeSortByPowerstate: '电源状态',\n\n  // Original text: \"RAM\"\n  homeSortByRAM: '内存',\n\n  // Original text: \"vCPUs\"\n  homeSortByvCPUs: '虚拟机CPU',\n\n  // Original text: \"CPUs\"\n  homeSortByCpus: 'CPU',\n\n  // Original text: \"{displayed, number}x {icon} (on {total, number})\"\n  homeDisplayedItems: undefined,\n\n  // Original text: \"{selected, number}x {icon} selected (on {total, number})\"\n  homeSelectedItems: undefined,\n\n  // Original text: \"More\"\n  homeMore: '更多',\n\n  // Original text: \"Migrate to…\"\n  homeMigrateTo: '迁移至…',\n\n  // Original text: \"Missing patches\"\n  homeMissingPatches: '缺少补丁',\n\n  // Original text: \"High Availability\"\n  highAvailability: '高可用',\n\n  // Original text: \"Add\"\n  add: '添加',\n\n  // Original text: \"Remove\"\n  remove: '删除',\n\n  // Original text: \"Preview\"\n  preview: '预览',\n\n  // Original text: \"Item\"\n  item: '项',\n\n  // Original text: \"No selected value\"\n  noSelectedValue: '没有选择的值',\n\n  // Original text: \"Choose user(s) and/or group(s)\"\n  selectSubjects: '选择用户和/或用户组',\n\n  // Original text: \"Select Object(s)…\"\n  selectObjects: '选择对象',\n\n  // Original text: \"Choose a role\"\n  selectRole: '选择一个角色',\n\n  // Original text: \"Select Host(s)…\"\n  selectHosts: '选择主机',\n\n  // Original text: \"Select object(s)…\"\n  selectHostsVms: '选择虚拟机',\n\n  // Original text: \"Select Network(s)…\"\n  selectNetworks: '选择网络',\n\n  // Original text: \"Select PIF(s)…\"\n  selectPifs: '选择网卡',\n\n  // Original text: \"Select Pool(s)…\"\n  selectPools: '选择资源池',\n\n  // Original text: \"Select Remote(s)…\"\n  selectRemotes: '选择远程',\n\n  // Original text: \"Select resource set(s)…\"\n  selectResourceSets: '选择资源集',\n\n  // Original text: \"Select template(s)…\"\n  selectResourceSetsVmTemplate: '选择模板',\n\n  // Original text: \"Select SR(s)…\"\n  selectResourceSetsSr: '选择数据存储',\n\n  // Original text: \"Select network(s)…\"\n  selectResourceSetsNetwork: '选择网络',\n\n  // Original text: \"Select disk(s)…\"\n  selectResourceSetsVdi: '选择硬盘',\n\n  // Original text: \"Select SR(s)…\"\n  selectSrs: '选择数据存储',\n\n  // Original text: \"Select VM(s)…\"\n  selectVms: '选择虚拟机',\n\n  // Original text: \"Select VM template(s)…\"\n  selectVmTemplates: '选择虚拟机模板',\n\n  // Original text: \"Select tag(s)…\"\n  selectTags: '选择标签',\n\n  // Original text: \"Select disk(s)…\"\n  selectVdis: '选择硬盘',\n\n  // Original text: \"Fill required informations.\"\n  fillRequiredInformations: '填写需要的信息',\n\n  // Original text: \"Fill informations (optional)\"\n  fillOptionalInformations: '填写信息',\n\n  // Original text: \"Reset\"\n  selectTableReset: '重置',\n\n  // Original text: \"Month\"\n  schedulingMonth: '月',\n\n  // Original text: \"Every month\"\n  schedulingEveryMonth: '每月',\n\n  // Original text: \"Each selected month\"\n  schedulingEachSelectedMonth: '每个选定月份',\n\n  // Original text: \"Day of the month\"\n  schedulingMonthDay: '本月的一天',\n\n  // Original text: \"Every day\"\n  schedulingEveryMonthDay: '每天',\n\n  // Original text: \"Each selected day\"\n  schedulingEachSelectedMonthDay: '每个选定天',\n\n  // Original text: \"Day of the week\"\n  schedulingWeekDay: '本周的一天',\n\n  // Original text: \"Every day\"\n  schedulingEveryWeekDay: '每天',\n\n  // Original text: \"Each selected day\"\n  schedulingEachSelectedWeekDay: '每个选定天',\n\n  // Original text: \"Hour\"\n  schedulingHour: '小时',\n\n  // Original text: \"Every hour\"\n  schedulingEveryHour: '每小时',\n\n  // Original text: \"Every N hour\"\n  schedulingEveryNHour: '每N小时',\n\n  // Original text: \"Each selected hour\"\n  schedulingEachSelectedHour: '每个选定小时',\n\n  // Original text: \"Minute\"\n  schedulingMinute: '分钟',\n\n  // Original text: \"Every minute\"\n  schedulingEveryMinute: '每分钟',\n\n  // Original text: \"Every N minute\"\n  schedulingEveryNMinute: '每N分钟',\n\n  // Original text: \"Each selected minute\"\n  schedulingEachSelectedMinute: '每个选定分钟',\n\n  // Original text: \"Reset\"\n  schedulingReset: '重置',\n\n  // Original text: \"Unknown\"\n  unknownSchedule: '未知',\n\n  // Original text: \"Cannot edit backup\"\n  backupEditNotFoundTitle: '不能编辑备份',\n\n  // Original text: \"Missing required info for edition\"\n  backupEditNotFoundMessage: '缺少版本所需要的信息',\n\n  // Original text: \"Job\"\n  job: '任务',\n\n  // Original text: \"Job ID\"\n  jobId: '任务ID',\n\n  // Original text: \"Name\"\n  jobName: '名称',\n\n  // Original text: \"Start\"\n  jobStart: '开始',\n\n  // Original text: \"End\"\n  jobEnd: '结束',\n\n  // Original text: \"Duration\"\n  jobDuration: '周期',\n\n  // Original text: \"Status\"\n  jobStatus: '状态',\n\n  // Original text: \"Action\"\n  jobAction: '行为',\n\n  // Original text: \"Tag\"\n  jobTag: '标签',\n\n  // Original text: \"Scheduling\"\n  jobScheduling: '计划',\n\n  // Original text: \"State\"\n  jobState: '状态',\n\n  // Original text: \"Run job\"\n  runJob: '运行任务',\n\n  // Original text: \"One shot running started. See overview for logs.\"\n  runJobVerbose: '开始一次运行，可查看概要日志',\n\n  // Original text: \"Started\"\n  jobStarted: '已经开始',\n\n  // Original text: \"Finished\"\n  jobFinished: '已完成',\n\n  // Original text: \"Save\"\n  saveBackupJob: '保存',\n\n  // Original text: \"Remove backup job\"\n  deleteBackupSchedule: '删除备份任务',\n\n  // Original text: \"Are you sure you want to delete this backup job?\"\n  deleteBackupScheduleQuestion: '你确认你要删除这个备份任务吗？',\n\n  // Original text: \"Enable immediately after creation\"\n  scheduleEnableAfterCreation: '创建后立即启用',\n\n  // Original text: \"You are editing Schedule {name} ({id}). Saving will override previous schedule state.\"\n  scheduleEditMessage: '你正在编辑计划{name} ({id}).保存将覆盖前一个计划状态.',\n\n  // Original text: \"You are editing job {name} ({id}). Saving will override previous job state.\"\n  jobEditMessage: '你正在编辑任务{name} ({id}).保存将覆盖前一个任务状态',\n\n  // Original text: \"No scheduled jobs.\"\n  noScheduledJobs: '没有计划任务',\n\n  // Original text: \"No jobs found.\"\n  noJobs: '未找到任务',\n\n  // Original text: \"No schedules found\"\n  noSchedules: '未找到计划',\n\n  // Original text: \"Select a xo-server API command\"\n  jobActionPlaceHolder: '选择一个xo-server API 命令',\n\n  // Original text: \"Select your backup type:\"\n  newBackupSelection: '选择你的备份类型',\n\n  // Original text: \"Remote stores for backup\"\n  remoteList: '远程备份存储',\n\n  // Original text: \"New File System Remote\"\n  newRemote: '新建远程文件系统',\n\n  // Original text: \"Local\"\n  remoteTypeLocal: '本地',\n\n  // Original text: \"NFS\"\n  remoteTypeNfs: 'NFS',\n\n  // Original text: \"SMB\"\n  remoteTypeSmb: 'SMB',\n\n  // Original text: \"Type\"\n  remoteType: '类型',\n\n  // Original text: \"Test your remote\"\n  remoteTestTip: '测试你的远程配置',\n\n  // Original text: \"Test Remote\"\n  testRemote: '测试远程配置',\n\n  // Original text: \"Test failed for {name}\"\n  remoteTestFailure: '失败的测试项 {name}',\n\n  // Original text: \"Test passed for {name}\"\n  remoteTestSuccess: '通过的测试项{name}',\n\n  // Original text: \"Error\"\n  remoteTestError: '错误',\n\n  // Original text: \"Test Step\"\n  remoteTestStep: '测试步骤',\n\n  // Original text: \"Test file\"\n  remoteTestFile: '测试文件',\n\n  // Original text: \"The remote appears to work correctly\"\n  remoteTestSuccessMessage: '远程配置运行正常',\n\n  // Original text: \"Create a new SR\"\n  newSrTitle: '创建一个新的数据存储',\n\n  // Original text: \"General\"\n  newSrGeneral: '常规',\n\n  // Original text: \"Select Strorage Type:\"\n  newSrTypeSelection: '选择存储类型',\n\n  // Original text: \"Settings\"\n  newSrSettings: '设置',\n\n  // Original text: \"Storage Usage\"\n  newSrUsage: '存储利用率',\n\n  // Original text: \"Summary\"\n  newSrSummary: '综述',\n\n  // Original text: \"Host\"\n  newSrHost: '主机',\n\n  // Original text: \"Type\"\n  newSrType: '类型',\n\n  // Original text: \"Name\"\n  newSrName: '名称',\n\n  // Original text: \"Description\"\n  newSrDescription: '描述',\n\n  // Original text: \"Server\"\n  newSrServer: '服务器',\n\n  // Original text: \"Path\"\n  newSrPath: '路径',\n\n  // Original text: \"IQN\"\n  newSrIqn: 'IQN',\n\n  // Original text: \"LUN\"\n  newSrLun: 'LUN',\n\n  // Original text: \"with auth.\"\n  newSrAuth: '启用认证',\n\n  // Original text: \"User Name\"\n  newSrUsername: '用户名',\n\n  // Original text: \"Password\"\n  newSrPassword: '密码',\n\n  // Original text: \"Device\"\n  newSrDevice: '设备',\n\n  // Original text: \"in use\"\n  newSrInUse: '使用中',\n\n  // Original text: \"Size\"\n  newSrSize: '大小',\n\n  // Original text: \"Create\"\n  newSrCreate: '创建',\n\n  // Original text: \"Users/Groups\"\n  subjectName: '用户/组',\n\n  // Original text: \"Object\"\n  objectName: '对象',\n\n  // Original text: \"Role\"\n  roleName: '角色',\n\n  // Original text: \"New Group Name\"\n  newGroupName: '新建组名',\n\n  // Original text: \"Create Group\"\n  createGroup: '创建组',\n\n  // Original text: \"Create\"\n  createGroupButton: '创建',\n\n  // Original text: \"Delete Group\"\n  deleteGroup: '删除组',\n\n  // Original text: \"Are you sure you want to delete this group?\"\n  deleteGroupConfirm: '你确定要删除该组？',\n\n  // Original text: \"Remove user from Group\"\n  removeUserFromGroup: '从组中删除用户',\n\n  // Original text: \"Are you sure you want to delete this user?\"\n  deleteUserConfirm: '你确定要删除该用户？',\n\n  // Original text: \"Delete User\"\n  deleteUser: '删除用户',\n\n  // Original text: \"unknown user\"\n  unknownUser: '未知用户',\n\n  // Original text: \"No group found\"\n  noGroupFound: '没有找到组',\n\n  // Original text: \"Name\"\n  groupNameColumn: '名称',\n\n  // Original text: \"Users\"\n  groupUsersColumn: '用户',\n\n  // Original text: \"Add User\"\n  addUserToGroupColumn: '增加用户',\n\n  // Original text: \"Email\"\n  userNameColumn: '邮件',\n\n  // Original text: \"Permissions\"\n  userPermissionColumn: '权限',\n\n  // Original text: \"Password\"\n  userPasswordColumn: '密码',\n\n  // Original text: \"Email\"\n  userName: '邮件',\n\n  // Original text: \"Password\"\n  userPassword: '密码',\n\n  // Original text: \"Create\"\n  createUserButton: '创建',\n\n  // Original text: \"No user found\"\n  noUserFound: '没有找到用户',\n\n  // Original text: \"User\"\n  userLabel: '用户',\n\n  // Original text: \"Admin\"\n  adminLabel: '管理',\n\n  // Original text: \"No user in group\"\n  noUserInGroup: '组中没有用户',\n\n  // Original text: \"{users} user{users, plural, one {} other {s}}\"\n  countUsers: '{users} 用户{users, plural, one {} 其他 {s}}',\n\n  // Original text: \"Select Permission\"\n  selectPermission: '选择权限',\n\n  // Original text: \"Auto-load at server start\"\n  autoloadPlugin: '服务器启动时自动加载',\n\n  // Original text: \"Save configuration\"\n  savePluginConfiguration: '保存配置',\n\n  // Original text: \"Delete configuration\"\n  deletePluginConfiguration: '删除配置',\n\n  // Original text: \"Plugin error\"\n  pluginError: '插件错误',\n\n  // Original text: \"Unknown error\"\n  unknownPluginError: '未知错误',\n\n  // Original text: \"Purge plugin configuration\"\n  purgePluginConfiguration: '清除插件配置',\n\n  // Original text: \"Are you sure you want to purge this configuration ?\"\n  purgePluginConfigurationQuestion: '你确定要清除此配置？',\n\n  // Original text: \"Edit\"\n  editPluginConfiguration: '编辑',\n\n  // Original text: \"Cancel\"\n  cancelPluginEdition: '取消',\n\n  // Original text: \"Plugin configuration\"\n  pluginConfigurationSuccess: '插件配置',\n\n  // Original text: \"Plugin configuration successfully saved!\"\n  pluginConfigurationChanges: '插件配置保存成功',\n\n  // Original text: \"Start\"\n  startVmLabel: '启动',\n\n  // Original text: \"Recovery start\"\n  recoveryModeLabel: '恢复启动',\n\n  // Original text: \"Suspend\"\n  suspendVmLabel: '暂停',\n\n  // Original text: \"Stop\"\n  stopVmLabel: '关机',\n\n  // Original text: \"Force shutdown\"\n  forceShutdownVmLabel: '强制关机',\n\n  // Original text: \"Reboot\"\n  rebootVmLabel: '重启',\n\n  // Original text: \"Force reboot\"\n  forceRebootVmLabel: '强制重启',\n\n  // Original text: \"Delete\"\n  deleteVmLabel: '删除',\n\n  // Original text: \"Migrate\"\n  migrateVmLabel: '迁移',\n\n  // Original text: \"Snapshot\"\n  snapshotVmLabel: '快照',\n\n  // Original text: \"Export\"\n  exportVmLabel: '导出',\n\n  // Original text: \"Resume\"\n  resumeVmLabel: '恢复',\n\n  // Original text: \"Copy\"\n  copyVmLabel: '复制',\n\n  // Original text: \"Clone\"\n  cloneVmLabel: '克隆',\n\n  // Original text: \"Fast clone\"\n  fastCloneVmLabel: '快速克隆',\n\n  // Original text: \"Convert to template\"\n  convertVmToTemplateLabel: '转换成模板',\n\n  // Original text: \"Console\"\n  vmConsoleLabel: '控制台',\n\n  // Original text: \"Rescan all disks\"\n  srRescan: '重新扫描所有磁盘',\n\n  // Original text: \"Connect to all hosts\"\n  srReconnectAll: '连接所有主机',\n\n  // Original text: \"Disconnect to all hosts\"\n  srDisconnectAll: '断开所有主机',\n\n  // Original text: \"Forget this SR\"\n  srForget: '移除此数据存储',\n\n  // Original text: \"Remove this SR\"\n  srRemoveButton: '删除此数据存储',\n\n  // Original text: \"No VDIs in this storage\"\n  srNoVdis: '此存储中没有VDI',\n\n  // Original text: \"Hosts\"\n  hostsTabName: '主机',\n\n  // Original text: \"High Availability\"\n  poolHaStatus: '高可用',\n\n  // Original text: \"Enabled\"\n  poolHaEnabled: '启用',\n\n  // Original text: \"Disabled\"\n  poolHaDisabled: '禁用',\n\n  // Original text: \"Name\"\n  hostNameLabel: '名称',\n\n  // Original text: \"Description\"\n  hostDescription: '描述',\n\n  // Original text: \"Memory\"\n  hostMemory: '内存',\n\n  // Original text: \"No hosts\"\n  noHost: '没有主机',\n\n  // Original text: \"Name\"\n  poolNetworkNameLabel: '名称',\n\n  // Original text: \"Description\"\n  poolNetworkDescription: '描述',\n\n  // Original text: \"PIFs\"\n  poolNetworkPif: 'PIFs',\n\n  // Original text: \"No networks\"\n  poolNoNetwork: '没有网络',\n\n  // Original text: \"MTU\"\n  poolNetworkMTU: 'MTU',\n\n  // Original text: \"Connected\"\n  poolNetworkPifAttached: '已连接',\n\n  // Original text: \"Disconnected\"\n  poolNetworkPifDetached: '未连接',\n\n  // Original text: \"Add SR\"\n  addSrLabel: '添加数据存储',\n\n  // Original text: \"Add VM\"\n  addVmLabel: '添加虚拟机',\n\n  // Original text: \"Add Host\"\n  addHostLabel: '添加主机',\n\n  // Original text: \"Disconnect\"\n  disconnectServer: '断开',\n\n  // Original text: \"Start\"\n  startHostLabel: '启动',\n\n  // Original text: \"Stop\"\n  stopHostLabel: '关机',\n\n  // Original text: \"Enable\"\n  enableHostLabel: '启用',\n\n  // Original text: \"Disable\"\n  disableHostLabel: '禁用',\n\n  // Original text: \"Restart toolstack\"\n  restartHostAgent: '重启toolstack',\n\n  // Original text: \"Force reboot\"\n  forceRebootHostLabel: '强制重启',\n\n  // Original text: \"Reboot\"\n  rebootHostLabel: '重启',\n\n  // Original text: \"Emergency mode\"\n  emergencyModeLabel: '紧急模式',\n\n  // Original text: \"Storage\"\n  storageTabName: '存储',\n\n  // Original text: \"Patches\"\n  patchesTabName: '补丁',\n\n  // Original text: \"Load average\"\n  statLoad: '负载平衡',\n\n  // Original text: \"Hardware\"\n  hardwareHostSettingsLabel: '硬件',\n\n  // Original text: \"Address\"\n  hostAddress: '地址',\n\n  // Original text: \"Status\"\n  hostStatus: '状态',\n\n  // Original text: \"Build number\"\n  hostBuildNumber: '版本号',\n\n  // Original text: \"iSCSI name\"\n  hostIscsiName: 'iSCSI名称',\n\n  // Original text: \"Version\"\n  hostXenServerVersion: '版本',\n\n  // Original text: \"Enabled\"\n  hostStatusEnabled: '启用',\n\n  // Original text: \"Disabled\"\n  hostStatusDisabled: '禁用',\n\n  // Original text: \"Power on mode\"\n  hostPowerOnMode: '开机模式',\n\n  // Original text: \"Host uptime\"\n  hostStartedSince: '系统启动时间',\n\n  // Original text: \"Toolstack uptime\"\n  hostStackStartedSince: 'Toolstack启动时间',\n\n  // Original text: \"CPU model\"\n  hostCpusModel: 'CPU型号',\n\n  // Original text: \"Core (socket)\"\n  hostCpusNumber: '核 (socket)',\n\n  // Original text: \"Manufacturer info\"\n  hostManufacturerinfo: '制造商信息',\n\n  // Original text: \"BIOS info\"\n  hostBiosinfo: 'BIOS 信息',\n\n  // Original text: \"Licence\"\n  licenseHostSettingsLabel: '授权',\n\n  // Original text: \"Type\"\n  hostLicenseType: '类型',\n\n  // Original text: \"Socket\"\n  hostLicenseSocket: '插槽',\n\n  // Original text: \"Expiry\"\n  hostLicenseExpiry: '过期',\n\n  // Original text: \"Add a network\"\n  networkCreateButton: '新建一个网络',\n\n  // Original text: \"Device\"\n  pifDeviceLabel: '设备',\n\n  // Original text: \"Network\"\n  pifNetworkLabel: '网络',\n\n  // Original text: \"VLAN\"\n  pifVlanLabel: 'VLAN',\n\n  // Original text: \"Address\"\n  pifAddressLabel: '地址',\n\n  // Original text: \"MAC\"\n  pifMacLabel: 'MAC',\n\n  // Original text: \"MTU\"\n  pifMtuLabel: 'MTU',\n\n  // Original text: \"Status\"\n  pifStatusLabel: '状态',\n\n  // Original text: \"Connected\"\n  pifStatusConnected: '已连接',\n\n  // Original text: \"Disconnected\"\n  pifStatusDisconnected: '未连接',\n\n  // Original text: \"No physical interface detected\"\n  pifNoInterface: '没有检测到物理接口',\n\n  // Original text: \"Add a storage\"\n  addSrDeviceButton: '新建存储',\n\n  // Original text: \"Name\"\n  srNameLabel: '名称',\n\n  // Original text: \"Type\"\n  srType: '类型',\n\n  // Original text: \"Status\"\n  pdbStatus: '状态',\n\n  // Original text: \"Connected\"\n  pbdStatusConnected: '已连接',\n\n  // Original text: \"Disconnected\"\n  pbdStatusDisconnected: '未连接',\n\n  // Original text: \"Shared\"\n  srShared: '已共享',\n\n  // Original text: \"Not shared\"\n  srNotShared: '未共享',\n\n  // Original text: \"No storage detected\"\n  pbdNoSr: '未检测到存储',\n\n  // Original text: \"Name\"\n  patchNameLabel: '名称',\n\n  // Original text: \"Install all patches\"\n  patchUpdateButton: '安装所有补丁',\n\n  // Original text: \"Description\"\n  patchDescription: '描述',\n\n  // Original text: \"Applied date\"\n  patchApplied: '应用日期',\n\n  // Original text: \"Size\"\n  patchSize: '大小',\n\n  // Original text: \"Status\"\n  patchStatus: '状态',\n\n  // Original text: \"Applied\"\n  patchStatusApplied: '已应用',\n\n  // Original text: \"Missing patches\"\n  patchStatusNotApplied: '缺少补丁',\n\n  // Original text: \"No patch detected\"\n  patchNothing: '未检测到补丁',\n\n  // Original text: \"Release date\"\n  patchReleaseDate: '发布日期',\n\n  // Original text: \"Guidance\"\n  patchGuidance: '导航',\n\n  // Original text: \"Action\"\n  patchAction: '操作',\n\n  // Original text: \"Applied patches\"\n  hostAppliedPatches: '已应用补丁',\n\n  // Original text: \"Missing patches\"\n  hostMissingPatches: '缺少补丁',\n\n  // Original text: \"Host up-to-date!\"\n  hostUpToDate: '主机补丁为最新',\n\n  // Original text: \"Refresh patches\"\n  refreshPatches: '刷新补丁包',\n\n  // Original text: \"Install pool patches\"\n  installPoolPatches: '安装池补丁',\n\n  // Original text: \"General\"\n  generalTabName: '常规',\n\n  // Original text: \"Stats\"\n  statsTabName: '状态',\n\n  // Original text: \"Console\"\n  consoleTabName: '控制台',\n\n  // Original text: \"Snapshots\"\n  snapshotsTabName: '快照',\n\n  // Original text: \"Logs\"\n  logsTabName: '日志',\n\n  // Original text: \"Advanced\"\n  advancedTabName: '高级',\n\n  // Original text: \"Network\"\n  networkTabName: '网络',\n\n  // Original text: \"Disk{disks, plural, one {} other {s}}\"\n  disksTabName: '磁盘{disks, plural, one {} 其他 {s}}',\n\n  // Original text: \"halted\"\n  powerStateHalted: '已停止',\n\n  // Original text: \"running\"\n  powerStateRunning: '正在运行',\n\n  // Original text: \"suspended\"\n  powerStateSuspended: '已暂停',\n\n  // Original text: \"No Xen tools detected\"\n  vmStatus: '没有检测到Xen Tools',\n\n  // Original text: \"No IPv4 record\"\n  vmName: '没有IPv4记录',\n\n  // Original text: \"No IP record\"\n  vmDescription: '没有IP记录',\n\n  // Original text: \"Started {ago}\"\n  vmSettings: '已启动 {ago}',\n\n  // Original text: \"Current status:\"\n  vmCurrentStatus: '当前状态',\n\n  // Original text: \"Not running\"\n  vmNotRunning: '没有运行',\n\n  // Original text: \"No Xen tools detected\"\n  noToolsDetected: '没有检测到Xen Tools',\n\n  // Original text: \"No IPv4 record\"\n  noIpv4Record: '没有IPv4记录',\n\n  // Original text: \"No IP record\"\n  noIpRecord: '没有IP记录',\n\n  // Original text: \"Started {ago}\"\n  started: '已启动 {ago}',\n\n  // Original text: \"Paravirtualization (PV)\"\n  paraVirtualizedMode: '半虚拟化 (PV)',\n\n  // Original text: \"Hardware virtualization (HVM)\"\n  hardwareVirtualizedMode: '硬件虚拟化 (HVM)',\n\n  // Original text: \"CPU usage\"\n  statsCpu: 'CPU利用率',\n\n  // Original text: \"Memory usage\"\n  statsMemory: '内存利用率',\n\n  // Original text: \"Network throughput\"\n  statsNetwork: '网络流量',\n\n  // Original text: \"Stacked values\"\n  useStackedValuesOnStats: 'Stacked 值',\n\n  // Original text: \"Disk throughput\"\n  statDisk: '磁盘吞吐',\n\n  // Original text: \"Last 10 minutes\"\n  statLastTenMinutes: '最近10分钟',\n\n  // Original text: \"Last 2 hours\"\n  statLastTwoHours: '最近两小时',\n\n  // Original text: \"Last week\"\n  statLastWeek: '最近一周',\n\n  // Original text: \"Last year\"\n  statLastYear: '最近一年',\n\n  // Original text: \"Copy\"\n  copyToClipboardLabel: '复制',\n\n  // Original text: \"Ctrl+Alt+Del\"\n  ctrlAltDelButtonLabel: '发送Ctrl+Alt+Del',\n\n  // Original text: \"Tip:\"\n  tipLabel: '提示',\n\n  // Original text: \"non-US keyboard could have issues with console: switch your own layout to US.\"\n  tipConsoleLabel: '非美式键盘操作控制台可能出现问题：请切换至美式键盘模式',\n\n  // Original text: \"Action\"\n  vdiAction: '操作',\n\n  // Original text: \"Attach disk\"\n  vdiAttachDevice: '附加磁盘',\n\n  // Original text: \"New disk\"\n  vbdCreateDeviceButton: '新建磁盘',\n\n  // Original text: \"Boot order\"\n  vdiBootOrder: '启动顺序',\n\n  // Original text: \"Name\"\n  vdiNameLabel: '名称',\n\n  // Original text: \"Description\"\n  vdiNameDescription: '描述',\n\n  // Original text: \"Tags\"\n  vdiTags: '标签',\n\n  // Original text: \"Size\"\n  vdiSize: '磁盘大小',\n\n  // Original text: \"SR\"\n  vdiSr: '数据存储',\n\n  // Original text: \"VM\"\n  vdiVm: '虚拟机',\n\n  // Original text: \"Boot flag\"\n  vbdBootableStatus: '启动标识',\n\n  // Original text: \"Status\"\n  vbdStatus: '状态',\n\n  // Original text: \"Connected\"\n  vbdStatusConnected: '已连接',\n\n  // Original text: \"Disconnected\"\n  vbdStatusDisconnected: '未连接',\n\n  // Original text: \"No disks\"\n  vbdNoVbd: '没有磁盘',\n\n  // Original text: \"New device\"\n  vifCreateDeviceButton: '新建设备',\n\n  // Original text: \"No interface\"\n  vifNoInterface: '没有网卡',\n\n  // Original text: \"Device\"\n  vifDeviceLabel: '设备',\n\n  // Original text: \"MAC address\"\n  vifMacLabel: 'MAC地址',\n\n  // Original text: \"MTU\"\n  vifMtuLabel: 'MTU',\n\n  // Original text: \"Network\"\n  vifNetworkLabel: '网络',\n\n  // Original text: \"Status\"\n  vifStatusLabel: '状态',\n\n  // Original text: \"Connected\"\n  vifStatusConnected: '已连接',\n\n  // Original text: \"Disconnected\"\n  vifStatusDisconnected: '未连接',\n\n  // Original text: \"IP addresses\"\n  vifIpAddresses: 'IP地址',\n\n  // Original text: \"Auto-generated if empty\"\n  vifMacAutoGenerate: '如果没有自动创建',\n\n  // Original text: \"No snapshots\"\n  noSnapshots: '没有快照',\n\n  // Original text: \"New snapshot\"\n  snapshotCreateButton: '新建快照',\n\n  // Original text: \"Just click on the snapshot button to create one!\"\n  tipCreateSnapshotLabel: '点击快照按钮来创建快照',\n\n  // Original text: \"Creation date\"\n  snapshotDate: '创建日期',\n\n  // Original text: \"Name\"\n  snapshotName: '名称',\n\n  // Original text: \"Action\"\n  snapshotAction: '操作',\n\n  // Original text: \"Remove all logs\"\n  logRemoveAll: '删除所有日志',\n\n  // Original text: \"No logs so far\"\n  noLogs: '目前没有日志',\n\n  // Original text: \"Creation date\"\n  logDate: '创建日期',\n\n  // Original text: \"Name\"\n  logName: '名称',\n\n  // Original text: \"Content\"\n  logContent: '目录',\n\n  // Original text: \"Action\"\n  logAction: '操作',\n\n  // Original text: \"Remove\"\n  vmRemoveButton: '删除',\n\n  // Original text: \"Convert\"\n  vmConvertButton: '转换',\n\n  // Original text: \"Xen settings\"\n  xenSettingsLabel: 'Xen 设置',\n\n  // Original text: \"Guest OS\"\n  guestOsLabel: '客户操作系统',\n\n  // Original text: \"Misc\"\n  miscLabel: 'Misc',\n\n  // Original text: \"UUID\"\n  uuid: 'UUID',\n\n  // Original text: \"Virtualization mode\"\n  virtualizationMode: '虚拟化模式',\n\n  // Original text: \"CPU weight\"\n  cpuWeightLabel: 'CPU权重',\n\n  // Original text: \"Default\"\n  defaultCpuWeight: '默认',\n\n  // Original text: \"PV args\"\n  pvArgsLabel: 'PV参数',\n\n  // Original text: \"Xen tools status\"\n  xenToolsStatus: 'Xen tools状态',\n\n  // Original text: \"{status}\"\n  xenToolsStatusValue: '{status}',\n\n  // Original text: \"OS name\"\n  osName: '操作系统名称',\n\n  // Original text: \"OS kernel\"\n  osKernel: '操作系统内核',\n\n  // Original text: \"Auto power on\"\n  autoPowerOn: '自动卡机',\n\n  // Original text: \"HA\"\n  ha: '高可用',\n\n  // Original text: \"Original template\"\n  originalTemplate: '来源模板',\n\n  // Original text: \"Unknown\"\n  unknownOsName: '未知',\n\n  // Original text: \"Unknown\"\n  unknownOsKernel: '未知',\n\n  // Original text: \"Unknown\"\n  unknownOriginalTemplate: '未知',\n\n  // Original text: \"VM limits\"\n  vmLimitsLabel: '虚拟机限制',\n\n  // Original text: \"CPU limits\"\n  vmCpuLimitsLabel: 'CPU限制',\n\n  // Original text: \"Memory limits (min/max)\"\n  vmMemoryLimitsLabel: '内存限制(min/max)',\n\n  // Original text: \"vCPUs max:\"\n  vmMaxVcpus: '最大虚拟CPU数',\n\n  // Original text: \"Memory max:\"\n  vmMaxRam: '最大内存',\n\n  // Original text: \"Long click to add a name\"\n  vmHomeNamePlaceholder: '长按来添加名称',\n\n  // Original text: \"Long click to add a description\"\n  vmHomeDescriptionPlaceholder: '长按来添加描述',\n\n  // Original text: \"Click to add a name\"\n  vmViewNamePlaceholder: '点击添加名称',\n\n  // Original text: \"Click to add a description\"\n  vmViewDescriptionPlaceholder: '点击添加描述',\n\n  // Original text: \"Pool{pools, plural, one {} other {s}}\"\n  poolPanel: '池{pools, plural, one {} 其他 {s}}',\n\n  // Original text: \"Host{hosts, plural, one {} other {s}}\"\n  hostPanel: '主机{hosts, plural, one {} 其他 {s}}',\n\n  // Original text: \"VM{vms, plural, one {} other {s}}\"\n  vmPanel: '虚拟机{vms, plural, one {} 其他 {s}}',\n\n  // Original text: \"RAM Usage\"\n  memoryStatePanel: '内容使用率',\n\n  // Original text: \"CPUs Usage\"\n  cpuStatePanel: 'CPU使用率',\n\n  // Original text: \"VMs Power state\"\n  vmStatePanel: '虚拟机电源状态',\n\n  // Original text: \"Pending tasks\"\n  taskStatePanel: '正在运行的任务',\n\n  // Original text: \"Users\"\n  usersStatePanel: '用户',\n\n  // Original text: \"Storage state\"\n  srStatePanel: '存储状态',\n\n  // Original text: \"{usage} (of {total})\"\n  ofUsage: '{usage} (of {total})',\n\n  // Original text: \"No storage\"\n  noSrs: '没有存储',\n\n  // Original text: \"Name\"\n  srName: '名称',\n\n  // Original text: \"Pool\"\n  srPool: '资源池',\n\n  // Original text: \"Host\"\n  srHost: '主机',\n\n  // Original text: \"Type\"\n  srFormat: '类型',\n\n  // Original text: \"Size\"\n  srSize: '大小',\n\n  // Original text: \"Usage\"\n  srUsage: '利用率',\n\n  // Original text: \"used\"\n  srUsed: '已使用',\n\n  // Original text: \"free\"\n  srFree: '剩余空间',\n\n  // Original text: \"Storage Usage\"\n  srUsageStatePanel: '存储利用率',\n\n  // Original text: \"Top 5 SR Usage (in %)\"\n  srTopUsageStatePanel: '数据存储使用率前5名(in %)',\n\n  // Original text: \"{running} running ({halted} halted)\"\n  vmsStates: '{running} 正在运行 ({halted} 已停止)',\n\n  // Original text: \"{value} {date, date, medium}\"\n  weekHeatmapData: '{value} {date, date, medium}',\n\n  // Original text: \"No data.\"\n  weekHeatmapNoData: '没有数据',\n\n  // Original text: \"Weekly Heatmap\"\n  weeklyHeatmap: '每周热图',\n\n  // Original text: \"Weekly Charts\"\n  weeklyCharts: '每周图表',\n\n  // Original text: \"Synchronize scale:\"\n  weeklyChartsScaleInfo: '同步范围',\n\n  // Original text: \"Stats error\"\n  statsDashboardGenericErrorTitle: '状态错误',\n\n  // Original text: \"There is no stats available for:\"\n  statsDashboardGenericErrorMessage: '没有可用的状态:',\n\n  // Original text: \"No selected metric\"\n  noSelectedMetric: '没有选择度量标准',\n\n  // Original text: \"Select\"\n  statsDashboardSelectObjects: '选择',\n\n  // Original text: \"Loading…\"\n  metricsLoading: '加载中….',\n\n  // Original text: \"Coming soon!\"\n  comingSoon: '即将呈现',\n\n  // Original text: \"Orphaned VDIs\"\n  orphanedVdis: '孤立的VDI',\n\n  // Original text: \"Orphaned VMs\"\n  orphanedVms: '孤立的虚拟机',\n\n  // Original text: \"No orphans\"\n  noOrphanedObject: '没有孤立的内容',\n\n  // Original text: \"Remove all orphaned VDIs\"\n  removeAllOrphanedObject: '删除所有孤立的VDI',\n\n  // Original text: \"Name\"\n  vmNameLabel: '名称',\n\n  // Original text: \"Description\"\n  vmNameDescription: '描述',\n\n  // Original text: \"Resident on\"\n  vmContainer: '位于',\n\n  // Original text: \"Alarms\"\n  alarmMessage: '警告',\n\n  // Original text: \"No alarms\"\n  noAlarms: '没有警告',\n\n  // Original text: \"Date\"\n  alarmDate: '日期',\n\n  // Original text: \"Content\"\n  alarmContent: '内容',\n\n  // Original text: \"Issue on\"\n  alarmObject: '问题',\n\n  // Original text: \"Pool\"\n  alarmPool: '资源池',\n\n  // Original text: \"Remove all alarms\"\n  alarmRemoveAll: '删除所有警告',\n\n  // Original text: \"Create a new VM on {select}\"\n  newVmCreateNewVmOn: '创建一个新的位于{select}的虚拟机',\n\n  // Original text: \"Create a new VM on {select1} or {select2}\"\n  newVmCreateNewVmOn2: '创建一个新的位于{select1} 或 {select2}的虚拟机',\n\n  // Original text: \"You have no permission to create a VM\"\n  newVmCreateNewVmNoPermission: '你没有权限创建虚拟机',\n\n  // Original text: \"Infos\"\n  newVmInfoPanel: '信息',\n\n  // Original text: \"Name\"\n  newVmNameLabel: '名称',\n\n  // Original text: \"Template\"\n  newVmTemplateLabel: '模板',\n\n  // Original text: \"Description\"\n  newVmDescriptionLabel: '描述',\n\n  // Original text: \"Performances\"\n  newVmPerfPanel: '性能',\n\n  // Original text: \"vCPUs\"\n  newVmVcpusLabel: '虚拟CPU',\n\n  // Original text: \"RAM\"\n  newVmRamLabel: '内存',\n\n  // Original text: \"Install settings\"\n  newVmInstallSettingsPanel: '安装设置',\n\n  // Original text: \"ISO/DVD\"\n  newVmIsoDvdLabel: 'ISO/DVD',\n\n  // Original text: \"Network\"\n  newVmNetworkLabel: '网络',\n\n  // Original text: \"PV Args\"\n  newVmPvArgsLabel: 'PV参数',\n\n  // Original text: \"PXE\"\n  newVmPxeLabel: 'PXE',\n\n  // Original text: \"Interfaces\"\n  newVmInterfacesPanel: '网络接口',\n\n  // Original text: \"MAC\"\n  newVmMacLabel: 'MAC',\n\n  // Original text: \"Add interface\"\n  newVmAddInterface: '添加网络接口',\n\n  // Original text: \"Disks\"\n  newVmDisksPanel: '磁盘',\n\n  // Original text: \"SR\"\n  newVmSrLabel: '数据存储',\n\n  // Original text: \"Bootable\"\n  newVmBootableLabel: '启动项',\n\n  // Original text: \"Size\"\n  newVmSizeLabel: '大小',\n\n  // Original text: \"Add disk\"\n  newVmAddDisk: '添加磁盘',\n\n  // Original text: \"Summary\"\n  newVmSummaryPanel: '概述',\n\n  // Original text: \"Create\"\n  newVmCreate: '创建',\n\n  // Original text: \"Reset\"\n  newVmReset: '重置',\n\n  // Original text: \"Select template\"\n  newVmSelectTemplate: '选择模板',\n\n  // Original text: \"SSH key\"\n  newVmSshKey: 'SSH Key',\n\n  // Original text: \"Config drive\"\n  newVmConfigDrive: '配置驱动器',\n\n  // Original text: \"Custom config\"\n  newVmCustomConfig: '自定义配置',\n\n  // Original text: \"Boot VM after creation\"\n  newVmBootAfterCreate: '创建后启动',\n\n  // Original text: \"Auto-generated if empty\"\n  newVmMacPlaceholder: '如果为空自动创建',\n\n  // Original text: \"CPU weight\"\n  newVmCpuWeightLabel: 'CPU权重',\n\n  // Original text: \"Quarter (1/4)\"\n  newVmCpuWeightQuarter: '四分之一 (1/4)',\n\n  // Original text: \"Half (1/2)\"\n  newVmCpuWeightHalf: '二分之一 (1/2)',\n\n  // Original text: \"Normal\"\n  newVmCpuWeightNormal: '普通',\n\n  // Original text: \"Double (x2)\"\n  newVmCpuWeightDouble: '双倍(x2)',\n\n  // Original text: \"Cloud config\"\n  newVmCloudConfig: '云配置',\n\n  // Original text: \"Create VMs\"\n  newVmCreateVms: '创建虚拟机',\n\n  // Original text: \"Are you sure you want to create {nbVms} VMs?\"\n  newVmCreateVmsConfirm: '你确定要创建 {nbVms} 虚拟机?',\n\n  // Original text: \"Multiple VMs:\"\n  newVmMultipleVms: '多个虚拟机',\n\n  // Original text: \"Select a resource set:\"\n  newVmSelectResourceSet: '选择资源集',\n\n  // Original text: \"Name pattern:\"\n  newVmMultipleVmsPattern: '命名模式',\n\n  // Original text: \"e.g.: \\\\{name\\\\}_%\"\n  newVmMultipleVmsPatternPlaceholder: '例如: \\\\{name\\\\}_%',\n\n  // Original text: \"First index:\"\n  newVmFirstIndex: '首要标识',\n\n  // Original text: \"Resource sets\"\n  resourceSets: '资源集',\n\n  // Original text: \"No resource sets.\"\n  noResourceSets: '没有资源集',\n\n  // Original text: \"Resource set name\"\n  resourceSetName: '资源集名称',\n\n  // Original text: \"Creation and edition\"\n  resourceSetCreation: '创建并编辑',\n\n  // Original text: \"Save\"\n  saveResourceSet: '保存',\n\n  // Original text: \"Reset\"\n  resetResourceSet: '重置',\n\n  // Original text: \"Edit\"\n  editResourceSet: '编辑',\n\n  // Original text: \"Delete\"\n  deleteResourceSet: '删除',\n\n  // Original text: \"Delete resource set\"\n  deleteResourceSetWarning: '删除资源集',\n\n  // Original text: \"Are you sure you want to delete this resource set?\"\n  deleteResourceSetQuestion: '你确定要删除此资源集',\n\n  // Original text: \"Missing objects:\"\n  resourceSetMissingObjects: '缺少对象',\n\n  // Original text: \"vCPUs\"\n  resourceSetVcpus: '虚拟CPU',\n\n  // Original text: \"Memory\"\n  resourceSetMemory: '内存',\n\n  // Original text: \"Storage\"\n  resourceSetStorage: '存储',\n\n  // Original text: \"Unknown\"\n  unknownResourceSetValue: '未知',\n\n  // Original text: \"Available hosts\"\n  availableHosts: '可用主机',\n\n  // Original text: \"Excluded hosts\"\n  excludedHosts: '被排除的主机',\n\n  // Original text: \"No hosts available.\"\n  noHostsAvailable: '没有可用主机',\n\n  // Original text: \"VMs created from this resource set shall run on the following hosts.\"\n  availableHostsDescription: '从这些资源中创建的虚拟机将运行在以下主机上',\n\n  // Original text: \"Maximum CPUs\"\n  maxCpus: '最大CPU',\n\n  // Original text: \"Maximum RAM (GiB)\"\n  maxRam: '最大内存',\n\n  // Original text: \"Maximum disk space\"\n  maxDiskSpace: '最大磁盘空间',\n\n  // Original text: \"No limits.\"\n  noResourceSetLimits: '没有限制',\n\n  // Original text: \"Total:\"\n  totalResource: '合计',\n\n  // Original text: \"Remaining:\"\n  remainingResource: '剩余',\n\n  // Original text: \"Used:\"\n  usedResource: '已使用',\n\n  // Original text: \"Try dropping some backups here, or click to select backups to upload. Accept only .xva files.\"\n  importVmsList: '尝试将备份文件拖拽到这里，或点击选择备份文件上传，仅支持.xva格式的文件',\n\n  // Original text: \"No selected VMs.\"\n  noSelectedVms: '没有选择虚拟机',\n\n  // Original text: \"To Pool:\"\n  vmImportToPool: '到资源池',\n\n  // Original text: \"To SR:\"\n  vmImportToSr: '到存储库',\n\n  // Original text: \"VM{nVms, plural, one {} other {s}} to import\"\n  vmsToImport: '导入虚拟机',\n\n  // Original text: \"Reset\"\n  importVmsCleanList: '重置',\n\n  // Original text: \"VM import success\"\n  vmImportSuccess: '虚拟机导入成功',\n\n  // Original text: \"VM import failed\"\n  vmImportFailed: '虚拟机导入失败',\n\n  // Original text: \"Import starting…\"\n  startVmImport: '开始导入',\n\n  // Original text: \"Export starting…\"\n  startVmExport: '开始导出',\n\n  // Original text: \"No pending tasks\"\n  noTasks: '没有等待中的任务',\n\n  // Original text: \"Currently, there are not any pending XenServer tasks\"\n  xsTasks: '当前，没有任何等待中的XenServer任务',\n\n  // Original text: \"List Remote\"\n  listRemote: '列出远程',\n\n  // Original text: \"simple\"\n  simpleBackup: '简单',\n\n  // Original text: \"delta\"\n  delta: '增量',\n\n  // Original text: \"Restore Backups\"\n  restoreBackups: '恢复备份',\n\n  // Original text: \"No remotes\"\n  noRemotes: '没有远程',\n\n  // Original text: \"enabled\"\n  remoteEnabled: '启用',\n\n  // Original text: \"error\"\n  remoteError: '错误',\n\n  // Original text: \"No backup available\"\n  noBackup: '没有可用的备份',\n\n  // Original text: \"VM Name\"\n  backupVmNameColumn: '虚拟机名称',\n\n  // Original text: \"Backup Tag\"\n  backupTagColumn: '备份标识',\n\n  // Original text: \"Last Backup\"\n  lastBackupColumn: '最后备份',\n\n  // Original text: \"Available Backups\"\n  availableBackupsColumn: '可用的备份',\n\n  // Original text: \"Restore\"\n  restoreColumn: '恢复',\n\n  // Original text: \"Restore VM\"\n  restoreTip: '恢复虚拟机',\n\n  // Original text: \"Import VM\"\n  importBackupTitle: '导入虚拟机',\n\n  // Original text: \"Starting your backup import\"\n  importBackupMessage: '开始你的备份导入',\n\n  // Original text: \"Emergency shutdown Host{nHosts, plural, one {} other {s}}\"\n  emergencyShutdownHostsModalTitle: '紧急关闭主机{nHosts, plural, one {} other {s}}',\n\n  // Original text: \"Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?\"\n  emergencyShutdownHostsModalMessage: '你确定要关闭 {nHosts} 主机{nHosts, plural, one {} other {s}}？',\n\n  // Original text: \"Shutdown host\"\n  stopHostModalTitle: '关闭主机',\n\n  // Original text: \"This will shutdown your host. Do you want to continue?\"\n  stopHostModalMessage: '此操作将关闭你的主机，你确定要继续吗？',\n\n  // Original text: \"Restart host\"\n  restartHostModalTitle: '重启主机',\n\n  // Original text: \"This will restart your host. Do you want to continue?\"\n  restartHostModalMessage: '此操作将重启你的主机，你确定要继续吗？',\n\n  // Original text: \"Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}\"\n  restartHostsAgentsModalTitle: '重启主机{nHosts, plural, one {} other {s}} 代理{nHosts, plural, one {} other {s}}',\n\n  // Original text: \"Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?\"\n  restartHostsAgentsModalMessage:\n    '你确定要重启{nHosts}主机{nHosts, plural, one {} other {s}} 代理{nHosts, plural, one {} other {s}}？',\n\n  // Original text: \"Restart Host{nHosts, plural, one {} other {s}}\"\n  restartHostsModalTitle: '重启主机{nHosts, plural, one {} other {s}}',\n\n  // Original text: \"Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?\"\n  restartHostsModalMessage: '你确定要重启{nHosts}主机{nHosts, plural, one {} other {s}}？',\n\n  // Original text: \"Start VM{vms, plural, one {} other {s}}\"\n  startVmsModalTitle: '启动虚拟机{vms, plural, one {} other {s}}',\n\n  // Original text: \"Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?\"\n  startVmsModalMessage: '你确定要启动 {vms} 虚拟机{vms, plural, one {} other {s}}？',\n\n  // Original text: \"Stop Host{nHosts, plural, one {} other {s}}\"\n  stopHostsModalTitle: '停止主机{nHosts, plural, one {} other {s}}',\n\n  // Original text: \"Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?\"\n  stopHostsModalMessage: '你确定要停止{nHosts}主机{nHosts, plural, one {} other {s}}？',\n\n  // Original text: \"Stop VM{vms, plural, one {} other {s}}\"\n  stopVmsModalTitle: '停止虚拟机{vms, plural, one {} other {s}}',\n\n  // Original text: \"Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?\"\n  stopVmsModalMessage: '你确定要停止{vms}虚拟机{vms, plural, one {} other {s}}？',\n\n  // Original text: \"Restart VM\"\n  restartVmModalTitle: '重新启动虚拟机',\n\n  // Original text: \"Are you sure you want to restart {name}?\"\n  restartVmModalMessage: '你确定要重新启动{name}？',\n\n  // Original text: \"Stop VM\"\n  stopVmModalTitle: '停止虚拟机',\n\n  // Original text: \"Are you sure you want to stop {name}?\"\n  stopVmModalMessage: '你确定要停止 {name}？',\n\n  // Original text: \"Restart VM{vms, plural, one {} other {s}}\"\n  restartVmsModalTitle: '重新启动虚拟机{vms, plural, one {} other {s}}',\n\n  // Original text: \"Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?\"\n  restartVmsModalMessage: '你确定要重新启动{vms}虚拟机{vms, plural, one {} other {s}}？',\n\n  // Original text: \"Snapshot VM{vms, plural, one {} other {s}}\"\n  snapshotVmsModalTitle: '执行虚拟机快照{vms, plural, one {} other {s}}',\n\n  // Original text: \"Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?\"\n  snapshotVmsModalMessage: '你确定要执行虚拟机{vms}快照{vms, plural, one {} other {s}}？',\n\n  // Original text: \"Delete VM\"\n  deleteVmModalTitle: '删除虚拟机',\n\n  // Original text: \"Delete VM{vms, plural, one {} other {s}}\"\n  deleteVmsModalTitle: '删除虚拟机{vms, plural, one {} other {s}}',\n\n  // Original text: \"Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED\"\n  deleteVmModalMessage: '你确定要删除此虚拟机？所有的虚拟机磁盘将被删除',\n\n  // Original text: \"Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED\"\n  deleteVmsModalMessage: '你确定要删除 {vms}虚拟机{vms, plural, one {} other {s}}？所有的虚拟机磁盘将被删除',\n\n  // Original text: \"Migrate VM\"\n  migrateVmModalTitle: '迁移虚拟机',\n\n  // Original text: \"Select a destination host:\"\n  migrateVmSelectHost: '选择一个目标主机',\n\n  // Original text: \"Select a migration network:\"\n  migrateVmSelectMigrationNetwork: '选择一个迁移网络',\n\n  // Original text: \"For each VDI, select an SR:\"\n  migrateVmSelectSrs: '为每个虚拟磁盘，选择存储库',\n\n  // Original text: \"For each VIF, select a network:\"\n  migrateVmSelectNetworks: '为每个虚拟网卡，选择一个网络',\n\n  // Original text: \"Select a destination SR:\"\n  migrateVmsSelectSr: '选择一个目标存储库',\n\n  // Original text: \"Select a destination SR for local disks:\"\n  migrateVmsSelectSrIntraPool: '为本地磁盘选择一个目标存储库',\n\n  // Original text: \"Select a network on which to connect each VIF:\"\n  migrateVmsSelectNetwork: '选择一个网络来连接每个虚拟网卡',\n\n  // Original text: \"Smart mapping\"\n  migrateVmsSmartMapping: '智能映射',\n\n  // Original text: \"Name\"\n  migrateVmName: '名称',\n\n  // Original text: \"SR\"\n  migrateVmSr: '存储库',\n\n  // Original text: \"VIF\"\n  migrateVmVif: '虚拟网卡',\n\n  // Original text: \"Network\"\n  migrateVmNetwork: '网络',\n\n  // Original text: \"No target host\"\n  migrateVmNoTargetHost: '没有目标主机',\n\n  // Original text: \"A target host is required to migrate a VM\"\n  migrateVmNoTargetHostMessage: '需要一个目标主机来迁移一个虚拟机',\n\n  // Original text: \"Import a {name} Backup\"\n  importBackupModalTitle: '导入一个{name}备份',\n\n  // Original text: \"Start VM after restore\"\n  importBackupModalStart: '恢复后启动虚拟机',\n\n  // Original text: \"Select your backup…\"\n  importBackupModalSelectBackup: '选择你的备份…',\n\n  // Original text: \"Are you sure you want to remove all orphaned VDIs?\"\n  removeAllOrphanedModalWarning: '你确定要删除所有孤立的虚拟磁盘？',\n\n  // Original text: \"Remove all logs\"\n  removeAllLogsModalTitle: '删除所有日志',\n\n  // Original text: \"Are you sure you want to remove all logs?\"\n  removeAllLogsModalWarning: '你确定要删除所有日志？',\n\n  // Original text: \"This operation is definitive.\"\n  definitiveMessageModal: '这个操作是不可更改的',\n\n  // Original text: \"Previous SR Usage\"\n  existingSrModalTitle: '之前存储库的使用情况',\n\n  // Original text: \"This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.\"\n  existingSrModalText: '这条路径之前已经被一台XenServer主机用来连接存储。如果你选择继续创建存储库，所有的数据将丢失。',\n\n  // Original text: \"Previous LUN Usage\"\n  existingLunModalTitle: '之前LUN使用情况',\n\n  // Original text: \"This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.\"\n  existingLunModalText: '这个LUN之前已经被一台XenServer主机使用。如果你选择继续创建存储库，所有的数据将丢失。',\n\n  // Original text: \"Replace current registration?\"\n  alreadyRegisteredModal: '替换当前的注册？',\n\n  // Original text: \"Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?\"\n  alreadyRegisteredModalText: '你的XO设备已经注册给{email}，你确定要删除并替换这个注册信息？',\n\n  // Original text: \"Ready for trial?\"\n  trialReadyModal: '准备试用？',\n\n  // Original text: \"During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!\"\n  trialReadyModalText: '在试用期内，XOA需要Internet连接才能正常使用，如果您正式付费将不受此限制',\n\n  // Original text: \"Host\"\n  serverHost: '主机',\n\n  // Original text: \"Username\"\n  serverUsername: '用户名',\n\n  // Original text: \"Password\"\n  serverPassword: '密码',\n\n  // Original text: \"Action\"\n  serverAction: '操作',\n\n  // Original text: \"Read Only\"\n  serverReadOnly: '只读',\n\n  // Original text: \"Copy VM\"\n  copyVm: '复制虚拟机',\n\n  // Original text: \"Are you sure you want to copy this VM to {SR}?\"\n  copyVmConfirm: '你确定要复制此虚拟机到{SR}',\n\n  // Original text: \"Name\"\n  copyVmName: '名称',\n\n  // Original text: \"Name pattern\"\n  copyVmNamePattern: '命名规范',\n\n  // Original text: \"If empty: name of the copied VM\"\n  copyVmNamePlaceholder: '如果复制虚拟机名称为空',\n\n  // Original text: \"e.g.: \\\"\\\\{name\\\\}_COPY\\\"\"\n  copyVmNamePatternPlaceholder: 'e.g.: \"\\\\{name\\\\}_COPY\"',\n\n  // Original text: \"Select SR\"\n  copyVmSelectSr: '选择存储库',\n\n  // Original text: \"Use compression\"\n  copyVmCompress: '使用压缩',\n\n  // Original text: \"No target SR\"\n  copyVmsNoTargetSr: '没有目标存储库',\n\n  // Original text: \"A target SR is required to copy a VM\"\n  copyVmsNoTargetSrMessage: '复制虚拟机需要选择一个目标存储库',\n\n  // Original text: \"Create network\"\n  newNetworkCreate: '创建网络',\n\n  // Original text: \"Interface\"\n  newNetworkInterface: '接口',\n\n  // Original text: \"Name\"\n  newNetworkName: '名称',\n\n  // Original text: \"Description\"\n  newNetworkDescription: '描述',\n\n  // Original text: \"VLAN\"\n  newNetworkVlan: 'VLAN',\n\n  // Original text: \"No VLAN if empty\"\n  newNetworkDefaultVlan: '如果为空则没有VLAN',\n\n  // Original text: \"MTU\"\n  newNetworkMtu: 'MTU',\n\n  // Original text: \"Default: 1500\"\n  newNetworkDefaultMtu: '默认1500',\n\n  // Original text: \"Delete network\"\n  deleteNetwork: '删除网络',\n\n  // Original text: \"Are you sure you want to delete this network?\"\n  deleteNetworkConfirm: '你确定要删除此网络',\n\n  // Original text: \"Xen Orchestra\"\n  xenOrchestra: 'Xen Orchestra',\n\n  // Original text: \"No pro support provided!\"\n  noProSupport: '不提供专业支持！',\n\n  // Original text: \"Use in production at your own risks\"\n  noProductionUse: '在生产环境中使用将存在风险',\n\n  // Original text: \"You can download our turnkey appliance at\"\n  downloadXoa: '您可以在这里下载我们的整套设备',\n\n  // Original text: \"Bug Tracker\"\n  bugTracker: '问题跟踪器',\n\n  // Original text: \"Issues? Report it!\"\n  bugTrackerText: '出现问题？报告！',\n\n  // Original text: \"Community\"\n  community: '社区',\n\n  // Original text: \"Join our community forum!\"\n  communityText: '加入我们的社区论坛',\n\n  // Original text: \"Free Trial for Premium Edition!\"\n  freeTrial: '铂金版免费试用',\n\n  // Original text: \"Request your trial now!\"\n  freeTrialNow: '立即请求试用',\n\n  // Original text: \"Any issue?\"\n  issues: '出现任何问题？',\n\n  // Original text: \"Problem? Contact us!\"\n  issuesText: '有问题？联系我们！',\n\n  // Original text: \"Documentation\"\n  documentation: '文档',\n\n  // Original text: \"Read our official doc\"\n  documentationText: '阅读我们官方文档',\n\n  // Original text: \"Pro support included\"\n  proSupportIncluded: '包含专业支持',\n\n  // Original text: \"Acces your XO Account\"\n  xoAccount: '进入你的XO账户',\n\n  // Original text: \"Report a problem\"\n  openTicket: '报告一个问题',\n\n  // Original text: \"Problem? Open a ticket !\"\n  openTicketText: '存在问题？开个Case！',\n\n  // Original text: \"Upgrade needed\"\n  upgradeNeeded: '需要升级',\n\n  // Original text: \"Upgrade now!\"\n  upgradeNow: '立即升级',\n\n  // Original text: \"Or\"\n  or: '或',\n\n  // Original text: \"Try it for free!\"\n  tryIt: '免费试用',\n\n  // Original text: \"This feature is available starting from {plan} Edition\"\n  availableIn: '这个功能将在{plan}版本中可用',\n\n  // Original text: \"Updates\"\n  updateTitle: '更新',\n\n  // Original text: \"Registration\"\n  registration: '注册',\n\n  // Original text: \"Trial\"\n  trial: '试用',\n\n  // Original text: \"Settings\"\n  settings: '设置',\n\n  // Original text: \"Update\"\n  update: '更新',\n\n  // Original text: \"Upgrade\"\n  upgrade: '升级',\n\n  // Original text: \"No updater available for Community Edition\"\n  noUpdaterCommunity: '社区版本没有可用的升级',\n\n  // Original text: \"Please consider subscribe and try it with all features for free during 30 days on\"\n  noUpdaterSubscribe: '请考虑订购或在30天内免费试用所有功能',\n\n  // Original text: \"Manual update could break your current installation due to dependencies issues, do it with caution\"\n  noUpdaterWarning: '由于相关依赖关系的问题，手动更新将跑坏你当前的安全，请小心使用',\n\n  // Original text: \"Current version:\"\n  currentVersion: '当前版本',\n\n  // Original text: \"Register\"\n  register: '注册',\n\n  // Original text: \"Please, take time to register in order to enjoy your trial.\"\n  trialRegistration: '为了您的正常使用，请考虑花时间注册',\n\n  // Original text: \"Start trial\"\n  trialStartButton: '开始试用',\n\n  // Original text: \"You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.\"\n  trialAvailableUntil: '你可以使用试用版本直到{date, date, medium}。更新你的设备来获取',\n\n  // Original text: \"Your trial has been ended. Contact us or downgrade to Free version\"\n  trialConsumed: '你的使用已经结束，联系我们或下载免费版本',\n\n  // Original text: \"Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service.\"\n  trialLocked: '你的xoa-更新服务已停止。没有此服务你的XOA不能完全正常运行',\n\n  // Original text: \"No update information available\"\n  noUpdateInfo: '没有更新信息可用',\n\n  // Original text: \"Update information may be available\"\n  waitingUpdateInfo: '更新信息可能可用',\n\n  // Original text: \"Your XOA is up-to-date\"\n  upToDate: '你的XOA是最新的',\n\n  // Original text: \"You need to update your XOA (new version is available)\"\n  mustUpgrade: '你需要更新你的XOA（有新版本可用）',\n\n  // Original text: \"Your XOA is not registered for updates\"\n  registerNeeded: '你的XOA没有注册更新',\n\n  // Original text: \"Can't fetch update information\"\n  updaterError: '不能获取更新信息',\n\n  // Original text: \"Upgrade successful\"\n  promptUpgradeReloadTitle: '更新成功',\n\n  // Original text: \"Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?\"\n  promptUpgradeReloadMessage: '你的XOA已经成功更新，你的浏览器必须重新加载，你要现在重新加载吗？',\n\n  // Original text: \"Xen Orchestra from the sources\"\n  disclaimerTitle: 'Xen Orchestra 源码版',\n\n  // Original text: \"You are using XO from the sources! That's great for a personal/non-profit usage.\"\n  disclaimerText1: '你在使用XO的源码版！这非常适合个人/非商业用途',\n\n  // Original text: \"If you are a company, it's better to use it with our appliance + pro support included:\"\n  disclaimerText2: '如果你是一个公司，建议使用我们的设备结合专业的支持',\n\n  // Original text: \"This version is not bundled with any support nor updates. Use it with caution for critical tasks.\"\n  disclaimerText3: '这个版本没有绑定任何支持或更新，请谨慎使用',\n\n  // Original text: \"Connect PIF\"\n  connectPif: '连接物理网卡',\n\n  // Original text: \"Are you sure you want to connect this PIF?\"\n  connectPifConfirm: '你确定要连接这个物理网卡？',\n\n  // Original text: \"Disconnect PIF\"\n  disconnectPif: '断开物理网卡',\n\n  // Original text: \"Are you sure you want to disconnect this PIF?\"\n  disconnectPifConfirm: '你确定要断开这个网卡网卡？',\n\n  // Original text: \"Delete PIF\"\n  deletePif: '删除物理网卡',\n\n  // Original text: \"Are you sure you want to delete this PIF?\"\n  deletePifConfirm: '你确定要删除这个物理网卡？',\n\n  // Original text: \"Username\"\n  username: '用户名',\n\n  // Original text: \"Password\"\n  password: '密码',\n\n  // Original text: \"Language\"\n  language: '语言',\n\n  // Original text: \"Old password\"\n  oldPasswordPlaceholder: '原密码',\n\n  // Original text: \"New password\"\n  newPasswordPlaceholder: '新密码',\n\n  // Original text: \"Confirm new password\"\n  confirmPasswordPlaceholder: '确认新密码',\n\n  // Original text: \"Confirmation password incorrect\"\n  confirmationPasswordError: '确认密码不正确',\n\n  // Original text: \"Password does not match the confirm password.\"\n  confirmationPasswordErrorBody: '确认密码不匹配',\n\n  // Original text: \"Password changed\"\n  pwdChangeSuccess: '密码已修改',\n\n  // Original text: \"Your password has been successfully changed.\"\n  pwdChangeSuccessBody: '你的密码已成功修改',\n\n  // Original text: \"Incorrect password\"\n  pwdChangeError: '密码错误',\n\n  // Original text: \"The old password provided is incorrect. Your password has not been changed.\"\n  pwdChangeErrorBody: '原密码错误，你的密码未更改',\n\n  // Original text: \"OK\"\n  changePasswordOk: '确认',\n\n  // Original text: \"Others\"\n  others: '其他',\n}\n","// This file is coded in ES5 and CommonJS to be compatible with\n// `create-locale`.\n\nconst forEach = require('lodash/forEach')\n\nconst messages = {\n  alpha: 'Alpha',\n  alerts: 'Alerts',\n  connected: 'Connected',\n  description: 'Description',\n  deleteSourceVm: 'Delete source VM',\n  disable: 'Disable',\n  diskState: 'Disk state',\n  download: 'Download',\n  enable: 'Enable',\n  expiration: 'Expiration',\n  hostIp: 'Host IP',\n  interfaces: 'Interfaces',\n  keyValue: '{key}: {value}',\n  esxiImportSslCertificate: 'Skip SSL check',\n  esxiImportThin: 'Thin mode',\n  esxiImportThinDescription:\n    'Disk created in thin mode (less space used). Data is read twice, no visible task or progress at first',\n  esxiImportStopSource: 'Stop the source VM',\n  esxiImportStopSourceDescription:\n    'Source VM stopped before the last delta transfer (after final snapshot). Needed to fully transfer a running VM',\n  esxiImportStopOnErrorDescription: 'Stop on the first error when importing VMs',\n  inUse: 'In use',\n  nImportVmsInParallel: 'Number of VMs to import in parallel',\n  node: 'Node',\n  pifs: 'PIFs',\n  stopOnError: 'Stop on error',\n  uuid: 'UUID',\n  vdi: 'VDI',\n  vmSrUsage: 'Storage: {used} used of {total} ({free} free)',\n\n  new: 'New',\n  nodeStatus: 'Node status',\n  notDefined: 'Not defined',\n  status: 'Status',\n  statusConnecting: 'Connecting',\n  statusDisconnected: 'Disconnected',\n  statusLoading: 'Loading…',\n  errorPageNotFound: 'Page not found',\n  errorNoSuchItem: 'No such item',\n  errorUnknownItem: 'Unknown {type}',\n  generateNewMacAddress: 'Generate new MAC addresses',\n  memoryFree: '{memoryFree} RAM free',\n  configured: 'Configured',\n  notConfigured: 'Not configured',\n  utcDate: 'UTC date',\n  utcTime: 'UTC time',\n  date: 'Date',\n  notifications: 'Notifications',\n  noNotifications: 'No notifications so far.',\n  notificationNew: 'NEW!',\n  notYetAvailableForXs8: 'Not yet available for XenServer 8',\n  moreDetails: 'More details',\n  messageSubject: 'Subject',\n  messageFrom: 'From',\n  messageReply: 'Reply',\n  sr: 'SR',\n  subdirectory: 'Subdirectory',\n  tryXoa: 'Try XOA for free and deploy it here.',\n  notInstalled: 'Not installed',\n\n  editableLongClickPlaceholder: 'Long click to edit',\n  editableClickPlaceholder: 'Click to edit',\n  browseFiles: 'Browse files',\n  showLogs: 'Show logs',\n  noValue: 'None',\n  noExpiration: 'No expiration',\n  compression: 'Compression',\n  core: 'Core',\n  cpu: 'CPU',\n  multipathing: 'Multipathing',\n  multipathingDisabled: 'Multipathing disabled',\n  enableMultipathing: 'Enable multipathing',\n  disableMultipathing: 'Disable multipathing',\n  enableAllHostsMultipathing: 'Enable multipathing for all hosts',\n  disableAllHostsMultipathing: 'Disable multipathing for all hosts',\n  paths: 'Paths',\n  pbdDisconnected: 'PBD disconnected',\n  hasInactivePath: 'Has an inactive path',\n  pools: 'Pools',\n  remotes: 'Remotes',\n  schedulerGranularity: 'Scheduler granularity',\n  setCbtError: 'Set CBT error',\n  socket: 'Socket',\n  type: 'Type',\n  restore: 'Restore',\n  delete: 'Delete',\n  vms: 'VMs',\n  cpusMax: 'Max vCPUs',\n  metadata: 'Metadata',\n  chooseBackup: 'Choose a backup',\n  temporarilyDisabled: 'Temporarily disabled',\n  clickToShowError: 'Click to show error',\n  backupJobs: 'Backup jobs',\n  iscsiSessions: '({ nSessions, number }) iSCSI session{nSessions, plural, one {} other {s}}',\n  requiresAdminPermissions: 'Requires admin permissions',\n  proxy: 'Proxy',\n  proxies: 'Proxies',\n  name: 'Name',\n  value: 'Value',\n  address: 'Address',\n  vm: 'VM',\n  destinationSR: 'Destination SR',\n  destinationNetwork: 'Destination network',\n  dhcp: 'DHCP',\n  id: 'ID',\n  ip: 'IP',\n  static: 'Static',\n  user: 'User',\n  deletedUser: 'deleted ({ name })',\n  networkConfiguration: 'Network configuration',\n  integrity: 'Integrity',\n  altered: 'Altered',\n  missing: 'Missing',\n  verified: 'Verified',\n  snapshotMode: 'Snapshot mode',\n  normal: 'Normal',\n  withMemory: 'With memory',\n  offline: 'Offline',\n  noLicenseAvailable: 'No license available',\n  emailPlaceholderExample: 'Email address, e.g.: it@company.net',\n  unknown: 'Unknown',\n  upgradesAvailable: 'Upgrades available',\n  advancedSettings: 'Advanced settings',\n  forceUpgrade: 'Force upgrade',\n  txChecksumming: 'TX checksumming',\n  thick: 'Thick',\n  thin: 'Thin',\n  unknownSize: 'Unknown size',\n  installedCertificates: 'Installed certificates',\n  expiry: 'Expiry',\n  fingerprint: 'Fingerprint',\n  certificate: 'Certificate (PEM)',\n  certificateChain: 'Certificate chain (PEM)',\n  privateKey: 'Private key (PKCS#8)',\n  installNewCertificate: 'Install new certificate',\n  replaceExistingCertificate: 'Replace existing certificate',\n  customFields: 'Custom fields',\n  addColor: 'Add color',\n  addCustomField: 'Add custom field',\n  advancedTagCreation: 'Advanced tag creation',\n  availableXoaPremium: 'Available in XOA Premium',\n  availableXoaPlan: 'Available in XOA {plan}',\n  detach: 'Detach',\n  editCustomField: 'Edit custom field',\n  deleteCustomField: 'Delete custom field',\n  onlyAvailableXoaUsers: 'Only available to XOA users',\n  removeColor: 'Remove color',\n  xcpNg: 'XCP-ng',\n  noFileSelected: 'No file selected',\n  nRetriesVmBackupFailures: 'Number of retries if VM backup fails',\n  sequence: 'Sequence',\n  sequences: 'Sequences',\n\n  // ----- Modals -----\n  alertOk: 'OK',\n  confirmOk: 'OK',\n  formOk: 'OK',\n  genericCancel: 'Cancel',\n  enterConfirmText: 'Enter the following text to confirm:',\n\n  // ----- Filters -----\n  onError: 'On error',\n  successful: 'Successful',\n  filterKeepFailed: 'Keep only failed',\n  filterOutShortTasks: 'Hide short tasks',\n  filterOnlyManaged: 'Managed disks',\n  filterOnlyOrphaned: 'Orphaned disks',\n  filterOnlyRegular: 'Normal disks',\n  filterOnlyRunningVms: 'Running VMs',\n  filterOnlySnapshots: 'Snapshot disks',\n  filterOnlyUnmanaged: 'Unmanaged disks',\n  filterSaveAs: 'Save…',\n  filterSyntaxLinkTooltip: 'Explore the search syntax in the documentation',\n  filterVifsOnlyConnected: 'Connected VIFs',\n  filterVifsOnlyDisconnected: 'Disconnected VIFs',\n  filterRemotesOnlyConnected: 'Connected remotes',\n  filterRemotesOnlyDisconnected: 'Disconnected remotes',\n\n  // ----- Copiable component -----\n  copyToClipboard: 'Copy to clipboard',\n  copyToClipboardVdiUuid: 'Copy VDI UUID',\n  copyUuid: 'Copy {uuid}',\n  copyValue: 'Copy {value}',\n\n  // ----- Pills -----\n  pillMaster: 'Master',\n\n  // ----- Titles -----\n  homePage: 'Home',\n  homeVmPage: 'VMs',\n  homeHostPage: 'Hosts',\n  homePoolPage: 'Pools',\n  homeTemplatePage: 'Templates',\n  homeSrPage: 'Storage',\n  dashboardPage: 'Dashboard',\n  overviewDashboardPage: 'Overview',\n  overviewVisualizationDashboardPage: 'Visualizations',\n  overviewStatsDashboardPage: 'Statistics',\n  overviewHealthDashboardPage: 'Health',\n  selfServicePage: 'Self service',\n  backupPage: 'Backup',\n  jobsPage: 'Jobs',\n  xoaPage: 'XOA',\n  updatePage: 'Updates',\n  licensesPage: 'Licenses',\n  notificationsPage: 'Notifications',\n  supportPage: 'Support',\n  settingsPage: 'Settings',\n  settingsAuditPage: 'Audit',\n  settingsServersPage: 'Servers',\n  settingsUsersPage: 'Users',\n  settingsGroupsPage: 'Groups',\n  settingsAclsPage: 'ACLs',\n  settingsPluginsPage: 'Plugins',\n  settingsLogsPage: 'Logs',\n  settingsCloudConfigsPage: 'Cloud configs',\n  settingsIpsPage: 'IPs',\n  aboutPage: 'About',\n  aboutXoaPlan: 'About XO {xoaPlan}',\n  newMenu: 'New',\n  taskMenu: 'Tasks',\n  taskPage: 'Tasks',\n  newNetworkPage: 'Network',\n  newVmPage: 'VM',\n  newSrPage: 'Storage',\n  newServerPage: 'Server',\n  newImport: 'Import',\n  xosan: 'XOSAN',\n  backupOverviewPage: 'Overview',\n  backupNewPage: 'New',\n  backupRemotesPage: 'Remotes',\n  backupRestorePage: 'Restore',\n  backupFileRestorePage: 'File restore',\n  schedule: 'Schedule',\n  backup: 'Backup',\n  rollingSnapshot: 'Rolling Snapshot',\n  deltaBackup: 'Delta Backup',\n  disasterRecovery: 'Disaster Recovery',\n  continuousReplication: 'Continuous Replication',\n  backupType: 'Backup type',\n  poolMetadata: 'Pool metadata',\n  xoConfig: 'XO config',\n  backupVms: 'VM Backup & Replication',\n  backupMetadata: 'XO config & Pool metadata Backup',\n  backupModeSourceRemoteRequired: 'Backup mode and source remote are required',\n  mirrorBackup: 'Mirror backup',\n  mirrorBackupVms: 'VM Mirror Backup',\n  mirrorAllVmBackups: 'Mirror all {mode} VM backups',\n  jobsOverviewPage: 'Overview',\n  jobsNewPage: 'New',\n  jobsSchedulingPage: 'Scheduling',\n  customJob: 'Custom Job',\n  userPage: 'User',\n  xoa: 'XOA',\n\n  // ----- Support -----\n  noSupport: 'No support',\n  freeUpgrade: 'Free upgrade!',\n  checkXoa: 'Check XOA',\n  xoaCheck: 'XOA check',\n  closeTunnel: 'Close tunnel',\n  createSupportTicket: 'Create a support ticket',\n  restartXoServer: 'Restart XO Server',\n  restartXoServerConfirm:\n    'Restarting XO Server will interrupt any backup job or XO task that is currently running. Xen Orchestra will also be unavailable for a few seconds. Are you sure you want to restart XO Server?',\n  openTunnel: 'Open tunnel',\n  supportCommunity: 'The XOA check and the support tunnel are available in XOA.',\n  supportTunnel: 'Support tunnel',\n  supportTunnelClosed: 'The support tunnel is closed.',\n\n  // ----- Sign out -----\n  signOut: 'Sign out',\n\n  // ----- User Profile -----\n  editUserProfile: 'Edit my settings {username}',\n  xsClientId: 'XenServer Client ID',\n  uploadClientId: 'Upload Client ID file',\n  forgetClientId: 'Forget Client ID',\n  forgetXsCredentialsConfirm: 'Are you sure you want to forget your XenServer Client ID?',\n  forgetXsCredentialsError: 'Could not forget Client ID',\n  forgetXsCredentialsSuccess: 'Client ID forgotten',\n  setXsCredentialsError: 'Could not upload Client ID',\n  setXsCredentialsSuccess: 'Client ID uploaded',\n\n  // ----- Home view ------\n  allVms: 'All VMs',\n  backedUpVms: 'Backed up VMs',\n  notBackedUpVms: 'Not backed up VMs',\n  homeFetchingData: 'Fetching data…',\n  homeWelcome: 'Welcome to Xen Orchestra!',\n  homeWelcomeText: 'Add your XCP-ng hosts or pools',\n  homeConnectServerText: 'Some XCP-ng hosts have been registered but are not connected',\n  homeHelp: 'Want some help?',\n  homeAddServer: 'Add server',\n  homeConnectServer: 'Connect servers',\n  homeOnlineDoc: 'Online doc',\n  homeProSupport: 'Pro support',\n  homeNoVms: 'There are no VMs!',\n  homeNoVmsOr: 'Or…',\n  homeImportVm: 'Import VM',\n  homeImportVmMessage: 'Import an existing VM in xva format',\n  homeRestoreBackup: 'Restore a backup',\n  homeRestoreBackupMessage: 'Restore a backup from a remote store',\n  homeNewVmMessage: 'This will create a new VM',\n  homeFilters: 'Filters',\n  homeNoMatches: 'No results! Click here to reset your filters',\n  homeTypePool: 'Pool',\n  homeTypeHost: 'Host',\n  homeTypeVm: 'VM',\n  homeTypeSr: 'SR',\n  homeTypeVmTemplate: 'Template',\n  homeSort: 'Sort',\n  homeAllPools: 'Pools',\n  homeAllHosts: 'Hosts',\n  homeAllTags: 'Tags',\n  homeAllResourceSets: 'Resource sets',\n  homeNewVm: 'New VM',\n  homeFilterNone: 'None',\n  homeFilterDisabledHosts: 'Disabled hosts',\n  homeFilterPendingVms: 'Pending VMs',\n  homeFilterHvmGuests: 'HVM guests',\n  homeSortBy: 'Sort by',\n  homeSortByCpus: 'CPUs',\n  homeSortByInstallTime: 'Install time',\n  homeSortByStartTime: 'Start time',\n  homeSortByName: 'Name',\n  homeSortByPowerstate: 'Power state',\n  homeSortByRAM: 'RAM',\n  homeSortByShared: 'Shared/Not shared',\n  homeSortBySize: 'Size',\n  homeSortByType: 'Type',\n  homeSortByUsage: 'Usage',\n  homeSortVmsBySnapshots: 'Snapshots',\n  homeSortByContainer: 'Container',\n  homeSortByPool: 'Pool',\n  homeDisplayedItems: '{displayed, number}x {icon} (of {total, number})',\n  homeSelectedItems: '{selected, number}x {icon} selected (of {total, number})',\n  homeMore: 'More',\n  homeMigrateTo: 'Migrate to…',\n  homeMissingPatches: 'Missing patches',\n  homePoolMaster: 'Master:',\n  homeResourceSet: 'Resource set: {resourceSet}',\n  homeSrVdisToCoalesce: 'Some VDIs need to be coalesced',\n  highAvailability: 'High Availability',\n  powerState: 'Power state',\n  srSharedType: 'Shared {type}',\n  warningHostTimeTooltip: 'Host time and XOA time are not consistent with each other',\n  notAllHostsHaveTheSameVersion: 'Not all hosts within {pool} have the same version',\n  sortByDisksUsage: 'Disks usage',\n\n  // ----- Home snapshots -----\n  snapshotVmsName: 'Name',\n  snapshotVmsDescription: 'Description',\n\n  // ----- Common components -----\n  sortedTableAllItemsSelected: 'All of them are selected ({nItems, number})',\n  sortedTableNoItems: 'No items found',\n  sortedTableNumberOfFilteredItems: '{nFiltered, number} of {nTotal, number} items',\n  sortedTableNumberOfItems: '{nTotal, number} items',\n  sortedTableNumberOfSelectedItems: '{nSelected, number} selected',\n  sortedTableSelectAllItems: 'Click here to select all items',\n  chooseCompressionGzipOption: 'GZIP (very slow)',\n  chooseCompressionZstdOption: 'Zstd (fast, XCP-ng only)',\n\n  // ----- state -----\n  state: 'State',\n  stateDisabled: 'Disabled',\n  stateEnabled: 'Enabled',\n\n  // ----- Labels -----\n  labelDisk: 'Disk',\n  labelMerge: 'Merge',\n  labelSize: 'Size',\n  labelSpeed: 'Speed',\n  labelSr: 'SR',\n  labelTransfer: 'Transfer',\n  labelVm: 'VM',\n\n  // ----- Forms -----\n  formCancel: 'Cancel',\n  formCreate: 'Create',\n  formDescription: 'Description',\n  formEdit: 'Edit',\n  formId: 'ID',\n  formName: 'Name',\n  formReset: 'Reset',\n  formSave: 'Save',\n  formNotes: 'Notes',\n  add: 'Add',\n  selectAll: 'Select all',\n  remove: 'Remove',\n  preview: 'Preview',\n  action: 'Action',\n  item: 'Item',\n  noSelectedValue: 'No selected value',\n  filterByTags: 'Filter by tags',\n  selectSubjects: 'Choose user(s) and/or group(s)',\n  selectObjects: 'Select object(s)…',\n  selectRole: 'Choose a role',\n  selectHostFirst: 'Select a host first',\n  selectHosts: 'Select host(s)…',\n  selectHostsVms: 'Select object(s)…',\n  selectNetworks: 'Select network(s)…',\n  selectPcis: 'Select PCI(s)…',\n  selectPifs: 'Select PIF(s)…',\n  selectPools: 'Select pool(s)…',\n  selectRemotes: 'Select remote(s)…',\n  selectProxies: 'Select proxy(ies)…',\n  selectProxy: 'Select proxy…',\n  selectResourceSets: 'Select resource set(s)…',\n  selectResourceSetsVmTemplate: 'Select template(s)…',\n  selectResourceSetsSr: 'Select SR(s)…',\n  selectResourceSetsNetwork: 'Select network(s)…',\n  selectResourceSetsVdi: 'Select disk(s)…',\n  selectSshKey: 'Select SSH key(s)…',\n  selectSrs: 'Select SR(s)…',\n  selectVms: 'Select VM(s)…',\n  selectVmSnapshots: 'Select snapshot(s)…',\n  selectVmTemplates: 'Select VM template(s)…',\n  selectTags: 'Select tag(s)…',\n  selectVdis: 'Select disk(s)…',\n  selectTimezone: 'Select timezone…',\n  selectIp: 'Select IP(s)…',\n  selectIpPool: 'Select IP pool(s)…',\n  selectVgpuType: 'Select VGPU type(s)…',\n  fillOptionalInformations: 'Fill information (optional)',\n  selectTableReset: 'Reset',\n  selectCloudConfigs: 'Select cloud config(s)…',\n  selectNetworkConfigs: 'Select network config(s)…',\n\n  // --- Dates/Scheduler ---\n\n  schedulingMonth: 'Month',\n  schedulingDay: 'Day',\n  schedulingSetWeekDayMode: 'Switch to week days',\n  schedulingSetMonthDayMode: 'Switch to month days',\n  schedulingHour: 'Hour',\n  schedulingMinute: 'Minute',\n  selectTableAllMonth: 'Every month',\n  selectTableAllDay: 'Every day',\n  selectTableAllHour: 'Every hour',\n  selectTableAllMinute: 'Every minute',\n  timezonePickerUseLocalTime: 'Web browser timezone',\n  serverTimezoneOption: 'Server timezone ({value})',\n  cronPattern: 'Cron Pattern:',\n  successfulJobCall: 'Successful',\n  failedJobCall: 'Failed',\n  jobCallSkipped: 'Skipped',\n  jobCallInProgess: 'In progress',\n  jobTransferredDataSize: 'Transfer size:',\n  jobTransferredDataSpeed: 'Transfer speed:',\n  operationSize: 'Size',\n  operationSpeed: 'Speed',\n  exportType: 'Type',\n  jobMergedDataSize: 'Merge size:',\n  jobMergedDataSpeed: 'Merge speed:',\n  allJobCalls: 'All',\n  job: 'Job',\n  jobModalTitle: 'Job {job}',\n  jobId: 'ID',\n  jobType: 'Type',\n  jobName: 'Name',\n  jobModes: 'Modes',\n  jobNamePlaceholder: 'Name of your job (forbidden: \"_\")',\n  jobStart: 'Start',\n  jobEnd: 'End',\n  jobDuration: 'Duration',\n  jobStatus: 'Status',\n  jobAction: 'Action',\n  jobTag: 'Tag',\n  jobScheduling: 'Scheduling',\n  jobTimezone: 'Timezone',\n  jobServerTimezone: 'Server',\n  runJob: 'Run job',\n  cancelJob: 'Cancel job',\n  runJobVerbose: 'Onetime job started. See overview for logs.',\n  jobEdit: 'Edit job',\n  jobDelete: 'Delete',\n  jobFinished: 'Finished',\n  jobInterrupted: 'Interrupted',\n  jobStarted: 'Started',\n  jobFailed: 'Failed',\n  jobSkipped: 'Skipped',\n  jobSuccess: 'Successful',\n  allTasks: 'All',\n  taskDeleteLog: 'Delete task log',\n  taskStart: 'Start',\n  taskEnd: 'End',\n  taskDuration: 'Duration',\n  taskSuccess: 'Successful',\n  taskFailed: 'Failed',\n  taskSkipped: 'Skipped',\n  taskStarted: 'Started',\n  taskInterrupted: 'Interrupted',\n  taskAborted: 'Aborted',\n  taskTransferredDataSize: 'Transfer size',\n  taskTransferredDataSpeed: 'Transfer speed',\n  taskMergedDataSize: 'Merge size',\n  taskMergedDataSpeed: 'Merge speed',\n  taskError: 'Error',\n  taskEstimatedEnd: 'Estimated end',\n  taskReason: 'Reason',\n  taskOpenRawLog: 'Open raw log',\n  saveBackupJob: 'Save',\n  deleteBackupSchedule: 'Remove backup job',\n  deleteBackupScheduleQuestion: 'Are you sure you want to delete this backup job?',\n  deleteSelectedJobs: 'Delete selected jobs',\n  scheduleEnableAfterCreation: 'Enable immediately after creation',\n  scheduleEditMessage: 'You are editing schedule {name} ({id}). Saving will override previous schedule state.',\n  jobEditMessage: 'You are editing job {name} ({id}). Saving will override previous job state.',\n  scheduleEdit: 'Edit schedule',\n  missingBackupName: \"A name is required to create the backup's job!\",\n  missingVms: 'Missing VMs!',\n  missingBackupMode: 'You need to choose a backup mode!',\n  missingRemote: 'Missing remote!',\n  missingRemotes: 'Missing remotes!',\n  missingSrs: 'Missing SRs!',\n  missingPools: 'Missing pools!',\n  missingSchedules: 'Missing schedules!',\n  missingRetentions: 'The modes need at least a schedule with retention higher than 0',\n  missingExportRetention: 'The Backup mode and the Delta Backup mode require backup retention to be higher than 0!',\n  missingCopyRetention: 'The CR mode and The DR mode require replication retention to be higher than 0!',\n  missingSnapshotRetention: 'The Rolling Snapshot mode requires snapshot retention to be higher than 0!',\n  deltaChainRetentionWarning: 'Either the full backup interval or the backup retention should be lower than 50.',\n  retentionNeeded: 'Requires one retention to be higher than 0!',\n  newScheduleError: 'Invalid schedule',\n  createRemoteMessage: 'No remotes found, please click on the remotes settings button to create one!',\n  remotesSettings: 'Remotes settings',\n  pluginsSettings: 'Plugins settings',\n  pluginsWarning: 'To receive the report, the plugins backup-reports and transport-email need to be loaded.',\n  selectSchedule: 'Select a schedule…',\n  scheduleAdd: 'Add a schedule',\n  scheduleDelete: 'Delete',\n  scheduleRun: 'Run schedule',\n  scheduleSequence: 'Schedule sequence',\n  unnamedSchedule: 'Unnamed schedule',\n  unnamedJob: 'Unnamed Job',\n  deleteSelectedSchedules: 'Delete selected schedules',\n  noScheduledJobs: 'No scheduled jobs.',\n  legacySnapshotsLink: 'You can delete all your legacy backup snapshots.',\n  newSchedule: 'New schedule',\n  noSchedules: 'No schedules found',\n  jobActionPlaceHolder: 'Select an xo-server API command',\n  jobTimeoutPlaceHolder: 'Timeout (number of seconds after which a VM is considered failed)',\n  jobSchedules: 'Schedules',\n  jobScheduleNamePlaceHolder: 'Name of your schedule',\n  jobScheduleJobPlaceHolder: 'Select a job',\n  jobOwnerPlaceholder: 'Job owner',\n  jobUserNotFound: \"This job's creator no longer exists\",\n  redirectToMatchingVms: 'Click here to see the matching VMs',\n  noMatchingVms: 'There are no matching VMs!',\n  allMatchingVms: '{icon} See the matching VMs ({nMatchingVms, number})',\n  runBackupNgJobConfirm: 'Are you sure you want to run {name} ({id})?',\n  runBackupJobWarningNVms: 'This job will backup {nVms, number} VM{nVms, plural, one {} other {s}}.',\n  cancelJobConfirm: 'Are you sure you want to cancel {name} ({id})?',\n  scheduleDstWarning:\n    'If your country participates in DST, it is advised that you avoid scheduling jobs at the time of change. e.g. 2AM to 3AM for US.',\n\n  // ------ New backup -----\n  checkBackup: 'Restore health check',\n  newBackupAdvancedSettings: 'Advanced settings',\n  newBackupSettings: 'Settings',\n  reportWhenAlways: 'Always',\n  reportWhenSkippedFailure: 'Skipped and failure',\n  reportWhenFailure: 'Failure',\n  reportWhenNever: 'Never',\n  reportRecipients: 'Report recipients',\n  reportWhen: 'Report when',\n  concurrency: 'Concurrency',\n  mergeBackupsSynchronously: 'Merge backups synchronously',\n  mergeBackupsSynchronouslyTooltip:\n    'This will use more resources on the backup thread, but ensure there is no locking error when chaining multiple backup jobs on the same remote',\n  newBackupSelection: 'Select your backup type:',\n  snapshotRetention: 'Snapshot retention',\n  backupName: 'Name',\n  checkpointSnapshot: 'Checkpoint snapshot',\n  offlineSnapshot: 'Offline snapshot',\n  offlineBackup: 'Offline backup',\n  offlineBackupInfo: 'Export VMs without snapshotting them. The VMs will be shutdown during the export.',\n  timeout: 'Timeout',\n  timeoutInfo: 'Number of hours after which a job is considered failed',\n  fullBackupInterval: 'Full backup interval',\n  forceFullBackup: 'Force full backup',\n  timeoutUnit: 'In hours',\n  dbAndDrRequireEnterprisePlan: 'Delta Backup and DR require an Enterprise plan',\n  crRequiresPremiumPlan: 'CR requires a Premium plan',\n  smartBackupModeTitle: 'Smart mode',\n  backupTargetRemotes: 'Target remotes (for export)',\n  backupTargetSrs: 'Target SRs (for replication)',\n  localRemoteWarningTitle: 'Local remote selected',\n  crOnThickProvisionedSrWarning:\n    'Tip: Using thin-provisioned storage will consume less space. Please click on the icon to get more information',\n  vmsOnThinProvisionedSrTip:\n    'Tip: Creating VMs on thin-provisioned storage will consume less space. Please click on the icon to get more information',\n  deltaBackupOnOutdatedXenServerWarning: 'Delta Backup and Continuous Replication require at least XenServer 6.5.',\n  backupNgLinkToDocumentationMessage: 'Click for more information about the backup methods.',\n  localRemoteWarningMessage: 'Warning: Local remotes will use limited XOA disk space. Only for advanced users.',\n  editBackupVmsTitle: 'VMs',\n  editBackupSmartStatusTitle: 'VMs statuses',\n  editBackupSmartResidentOn: 'Resident on',\n  editBackupSmartNotResidentOn: 'Not resident on',\n  editBackupSmartPools: 'Pools',\n  editBackupSmartTags: 'Tags',\n  editBackupSmartTagsInfo:\n    \"VMs with tags in the form of <b>xo:no-bak</b> or <b>xo:no-bak=Reason</b>won't be included in any backup.For example, ephemeral VMs created by health check have this tag\",\n  sampleOfMatchingVms: 'Sample of matching VMs',\n  backupReplicatedVmsInfo:\n    'Replicated VMs (VMs with Continuous Replication or Disaster Recovery tag) must be excluded!',\n  editBackupSmartTagsTitle: 'VMs Tags',\n  editBackupSmartExcludedTagsTitle: 'Excluded VMs tags',\n  deleteOldBackupsFirst: 'Delete first',\n  deleteOldBackupsFirstMessage:\n    'Delete old backups before backing up the VMs. If the new backup fails, you will lose your old backups.',\n  customTag: 'Custom tag',\n  editJobNotFound: \"The job you're trying to edit wasn't found\",\n  preferNbd: 'Use NBD to transfer disk if available',\n  preferNbdInformation: 'A network accessible by XO or the proxy must have NBD enabled.',\n  nbdConcurrency: 'Number of NBD connection per disk',\n  cbtDestroySnapshotData: 'Purge snapshot data when using CBT.',\n  cbtDestroySnapshotDataInformation:\n    \"This will automatically enable Change Block Tracking (CBT) on the disks. The snapshot won't use any notable space on the SR, won't be shown in the UI and won't be usable to do a rollback nor differential restores\",\n  cbtDestroySnapshotDataDisabledInformation:\n    'Snapshot data can be purged only when NBD is enabled and rolling snapshot is not used',\n  shorterBackupReports: 'Shorter backup reports',\n  longTermRetention: 'Long-term retention of backups',\n  numberOfDailyBackupsKept: 'Number of daily backups kept',\n  numberOfWeeklyBackupsKept: 'Number of weekly backups kept',\n  numberOfMonthlyBackupsKept: 'Number of monthly backups kept',\n  numberOfYearlyBackupsKept: 'Number of yearly backups kept',\n  hideSuccessfulItems: 'Hide successful items in failure reports',\n\n  // ------ New Remote -----\n  newRemote: 'New file system remote',\n  remoteTypeLocal: 'Local',\n  remoteTypeNfs: 'NFS',\n  remoteTypeSmb: 'SMB',\n  remoteTypeS3: 'Amazon Web Services S3',\n  remoteTypeAzure: 'Azure',\n  remoteTypeAzurite: 'Azurite',\n  remoteType: 'Type',\n  remoteSmbWarningMessage:\n    'SMB remotes are meant to work with Windows Server. For other systems (Linux Samba, which means almost all NAS), please use NFS.',\n  remoteTestTip: 'Test your remote',\n  testRemote: 'Test remote',\n  remoteTestFailure: 'Test failed for {name}',\n  remoteTestSuccess: 'Test passed for {name}',\n  remoteTestError: 'Error',\n  remoteTestStep: 'Test step',\n  remoteTestName: 'Test name',\n  remoteTestNameFailure: 'Remote name already exists!',\n  remoteTestSuccessMessage: 'The remote appears to work correctly',\n  remoteConnectionFailed: 'Connection failed',\n  remoteContainer: 'Container',\n  remoteAccountName: 'Account name',\n\n  // ------ Backup job -----\n\n  confirmDeleteBackupJobsTitle: 'Delete backup job{nJobs, plural, one {} other {s}}',\n  confirmDeleteBackupJobsBody:\n    'Are you sure you want to delete {nJobs, number} backup job{nJobs, plural, one {} other {s}}?',\n  mirrorFullBackup: 'Mirror full backup',\n  mirrorIncrementalBackup: 'Mirror incremental backup',\n  runBackupJob: 'Run backup job once',\n  speedLimit: 'Speed limit (in MiB/s)',\n  sourceRemote: 'Source remote',\n  targetRemotes: 'Target remotes',\n\n  // ------ Remote -----\n  remoteName: 'Name',\n  remotePath: 'Path',\n  remoteState: 'State',\n  remoteDevice: 'Device',\n  remoteDisk: 'Disk (Used / Total)',\n  remoteSpeed: 'Speed (Write / Read)',\n  remoteSpeedInfo: 'Read and write rate speed performed during latest test',\n  remoteOptions: 'Options',\n  remoteShare: 'Share',\n  remoteAuth: 'Auth',\n  remoteDeleteTip: 'Delete',\n  remoteDeleteSelected: 'Delete selected remotes',\n  remoteMyNamePlaceHolder: 'Name',\n  remoteLocalPlaceHolderPath: '/path/to/backup',\n  remoteNfsPlaceHolderHost: 'Host',\n  remoteNfsPlaceHolderPort: 'Port',\n  remoteNfsPlaceHolderPath: 'path/to/backup',\n  remoteNfsPlaceHolderOptions: 'Custom mount options',\n  remoteSmbPlaceHolderRemotePath: 'Subfolder [path\\\\\\\\to\\\\\\\\backup]',\n  remoteSmbPlaceHolderUsername: 'Username',\n  remoteSmbPlaceHolderPassword: 'Password',\n  remoteSmbPlaceHolderDomain: 'Domain',\n  remoteSmbPlaceHolderAddressShare: '<address>\\\\\\\\<share>',\n  remoteSmbPlaceHolderOptions: 'Custom mount options',\n  remoteS3LabelUseHttps: 'Use HTTPS',\n  remoteS3LabelAllowInsecure: 'Allow unauthorized',\n  remoteS3PlaceHolderEndpoint: 'AWS S3 endpoint (ex: s3.us-east-2.amazonaws.com)',\n  remoteS3PlaceHolderBucket: 'AWS S3 bucket name',\n  remoteS3PlaceHolderDirectory: 'Directory',\n  remoteS3PlaceHolderAccessKeyID: 'Access key ID',\n  remoteS3PlaceHolderSecret: 'Paste secret here to change it',\n  remoteS3PlaceHolderEncryptionKey: 'Enter your encryption key here (32 characters)',\n  remoteS3Region: 'Region, leave blank for default',\n  remoteS3TooltipProtocol: 'Uncheck if you want HTTP instead of HTTPS',\n  remoteS3TooltipAcceptInsecure: 'Check if you want to accept self signed certificates',\n  remoteAzureLabelUseHttps: 'Use HTTPS',\n  remoteAzureLabelAllowInsecure: 'Allow unauthorized',\n  remoteAzurePlaceHolderEndpoint: 'Azure endpoint (ex: username.blob.core.windows.net)',\n  remoteAzurePlaceHolderContainer: 'Azure container name',\n  remoteAzurePlaceHolderDirectory: 'Directory',\n  remoteAzurePlaceHolderAccessKeyID: 'Access key ID',\n  remoteAzurePlaceHolderSecret: 'Paste secret here to change it',\n  remoteAzurePlaceHolderEncryptionKey: 'Enter your encryption key here (32 characters)',\n  remoteAzureRegion: 'Region, leave blank for default',\n  remoteAzureTooltipProtocol: 'Uncheck if you want HTTP instead of HTTPS',\n  remoteAzureTooltipAcceptInsecure: 'Check if you want to accept self signed certificates',\n  remotePlaceHolderPassword: 'Password(fill to edit)',\n  remoteUseVhdDirectory:\n    'Store backup as multiple data blocks instead of a whole VHD file. (creates 500-1000 files per backed up GB but allows faster merge)',\n  remoteUseVhdDirectoryTooltip:\n    'Your remote must be able to handle parallel access (up to 16 write processes per backup) and the number of files (500-1000 files per GB of backed up data)',\n  remoteEncryptionBackupSize: 'Size of backup is not updated when using encryption.',\n  remoteEncryptionEncryptedfiles:\n    'All the files of the remote except the encryption.json are encrypted, that means you can only activate encryption or change key on an empty remote.',\n  remoteEncryptionMustUseVhd: 'Delta backup must use VHD saved as blocks',\n  remoteEncryptionKey: 'Encrypt all new data sent to this remote',\n  remoteEncryptionKeyStorageLocation:\n    \"You won't be able to get your data back if you lose the encryption key. The encryption key is saved in the XO config backup, they should be secured correctly. Be careful, if you saved it on an encrypted remote, then you won't be able to access it without the remote encryption key.\",\n  encryption: 'Encryption',\n  remoteEncryptionLegacy:\n    'A legacy encryption algorithm is used ({algorithm}), please create a new remote with the recommended algorithm {recommendedAlgorithm}',\n\n  // ------ New Storage -----\n\n  newSr: 'New SR',\n  newSrConfirm:\n    'This will erase the entire disk or partition ({name}) to create a new storage repository. Are you sure you want to continue?',\n  newSrExistingSr:\n    'SR{n, plural, one {} other {s}} already exist on this device, as noted in the Storage Usage section. Creating this SR may erase the content of {path} and cause the loss of existing SR{n, plural, one {} other {s}}. Are you sure you want to continue?',\n  newSrTitle: 'Create a new SR',\n  newSrGeneral: 'General',\n  newSrTypeSelection: 'Select storage type:',\n  newSrSettings: 'Settings',\n  newSrUsage: 'Storage usage',\n  newSrSummary: 'Summary',\n  newSrHost: 'Host',\n  newSrType: 'Type',\n  newSrName: 'Name',\n  newSrDescription: 'Description',\n  newSrServer: 'Server',\n  newSrPath: 'Path',\n  newSrIqn: 'IQN',\n  newSrLun: 'LUN',\n  newSrNoHba: 'No HBA devices',\n  newSrAuth: 'With auth.',\n  newSrUsername: 'User name',\n  newSrPassword: 'Password',\n  newSrDevice: 'Device',\n  newSrInUse: 'In use',\n  newSrSize: 'Size',\n  newSrCreate: 'Create',\n  newSrNamePlaceHolder: 'Storage name',\n  newSrDescPlaceHolder: 'Storage description',\n  newSrIscsiAddressPlaceHolder: 'e.g 93.184.216.34 or iscsi.example.net',\n  newSrNfsAddressPlaceHolder: 'e.g 93.184.216.34 or nfs.example.net',\n  newSrSmbAddressPlaceHolder: 'e.g \\\\\\\\server\\\\sharename',\n  newSrPortPlaceHolder: '[port]',\n  newSrUsernamePlaceHolder: 'Username',\n  newSrPasswordPlaceHolder: 'Password',\n  newSrLvmDevicePlaceHolder: 'Device, e.g /dev/sda…',\n  newSrLocalPathPlaceHolder: '/path/to/directory',\n  newSrNfsDefaultVersion: 'Default NFS version',\n  newSrNfsOptions: 'Comma delimited NFS options',\n  newSrNfs: 'NFS version',\n  noSharedZfsAvailable: 'No shared ZFS available',\n  reattachNewSrTooltip: 'Reattach SR',\n  srLocation: 'Storage location',\n\n  // ------ New Network -----\n  createNewNetworkNoPermission: 'You do not have permission to create a network',\n  createNewNetworkOn: 'Create a new network on {select}',\n\n  // ----- ACLs, Users, Groups ------\n  subjectName: 'Users/Groups',\n  objectName: 'Object',\n  roleName: 'Role',\n  aclCreate: 'Create',\n  newGroupName: 'New group name',\n  createGroup: 'Create group',\n  syncLdapGroups: 'Synchronize LDAP groups',\n  ldapPluginNotConfigured: 'Install and configure the auth-ldap plugin first',\n  syncLdapGroupsWarning:\n    'Are you sure you want to synchronize LDAP groups with XO? This may delete XO groups and their ACLs.',\n  createGroupButton: 'Create',\n  deleteGroup: 'Delete group',\n  deleteGroupConfirm: 'Are you sure you want to delete this group?',\n  deleteSelectedGroups: 'Delete selected groups',\n  deleteGroupsModalTitle: 'Delete group{nGroups, plural, one {} other {s}}',\n  deleteGroupsModalMessage:\n    'Are you sure you want to delete {nGroups, number} group{nGroups, plural, one {} other {s}}?',\n  removeUserFromGroup: 'Remove user from group',\n  deleteUserConfirm: 'Are you sure you want to delete this user?',\n  deleteUser: 'Delete user',\n  deleteSelectedUsers: 'Delete selected users',\n  deleteUsersModalTitle: 'Delete user{nUsers, plural, one {} other {s}}',\n  deleteUsersModalMessage: 'Are you sure you want to delete {nUsers, number} user{nUsers, plural, one {} other {s}}?',\n  noUser: 'No user',\n  unknownUser: 'Unknown user',\n  noGroupFound: 'No group found',\n  groupNameColumn: 'Name',\n  groupUsersColumn: 'Users',\n  addUserToGroupColumn: 'Add user',\n  userNameColumn: 'Username',\n  userGroupsColumn: 'Member of',\n  userCountGroups: '{nGroups, number} group{nGroups, plural, one {} other {s}}',\n  userPermissionColumn: 'Permissions',\n  userAuthColumn: 'Password / Authentication methods',\n  userName: 'Username',\n  userPassword: 'Password',\n  createUserButton: 'Create',\n  noUserFound: 'No user found',\n  userLabel: 'User',\n  adminLabel: 'Admin',\n  noUserInGroup: 'No user in group',\n  countUsers: '{users, number} user{users, plural, one {} other {s}}',\n  selectPermission: 'Select permission',\n  deleteAcl: 'Delete ACL',\n  deleteSelectedAcls: 'Delete selected ACLs',\n  deleteAclsModalTitle: 'Delete ACL{nAcls, plural, one {} other {s}}',\n  deleteAclsModalMessage: 'Are you sure you want to delete {nAcls, number} ACL{nAcls, plural, one {} other {s}}?',\n\n  // ----- Plugins ------\n  noPlugins: 'No plugins found',\n  autoloadPlugin: 'Auto-load at server start',\n  savePluginConfiguration: 'Save configuration',\n  deletePluginConfiguration: 'Delete configuration',\n  pluginConfirmation: 'The test appears to be working.',\n  pluginError: 'Plugin error',\n  pluginTest: 'Plugin test',\n  unknownPluginError: 'Unknown error',\n  purgePluginConfiguration: 'Purge plugin configuration',\n  purgePluginConfigurationQuestion: 'Are you sure you want to purge this configuration?',\n  cancelPluginEdition: 'Cancel',\n  pluginConfigurationSuccess: 'Plugin configuration',\n  pluginConfigurationChanges: 'Plugin configuration successfully saved!',\n  pluginConfigurationPresetTitle: 'Predefined configuration',\n  pluginConfigurationChoosePreset: 'Choose a predefined configuration.',\n  applyPluginPreset: 'Apply',\n  disabledTestPluginTootltip: 'This plugin is not loaded',\n\n  // ----- User preferences -----\n  saveNewUserFilterErrorTitle: 'Save filter error',\n  saveNewUserFilterErrorBody: 'Bad parameter: name must be given.',\n  filterName: 'Name:',\n  filterValue: 'Value:',\n  saveNewFilterTitle: 'Save new filter',\n  removeUserFilterTitle: 'Remove custom filter',\n  removeUserFilterBody: 'Are you sure you want to remove the custom filter?',\n  defaultFilter: 'Default filter',\n  defaultFilters: 'Default filters',\n  customFilters: 'Custom filters',\n  customizeFilters: 'Customize filters',\n\n  // ----- VM actions ------\n  cantInterPoolCopy: 'Interpool copy requires at least Enterprise plan',\n  copyExportedUrl: 'Copy the export URL of the VM',\n  downloadVm: 'Download VM',\n  startVmLabel: 'Start',\n  startVmOnLabel: 'Start on…',\n  startVmOnMissingHostTitle: 'No host selected',\n  startVmOnMissingHostMessage: 'You must select a host',\n  recoveryModeLabel: 'Recovery start',\n  suspendVmLabel: 'Suspend',\n  pauseVmLabel: 'Pause',\n  stopVmLabel: 'Stop',\n  forceShutdownVmLabel: 'Force shutdown',\n  rebootVmLabel: 'Reboot',\n  forceRebootVmLabel: 'Force reboot',\n  deleteVmLabel: 'Delete',\n  deleteSelectedVmsLabel: 'Delete selected VMs',\n  migrateVmLabel: 'Migrate',\n  snapshotVmLabel: 'Snapshot',\n  exportVmLabel: 'Export',\n  resumeVmLabel: 'Resume',\n  copyVmLabel: 'Copy',\n  cloneVmLabel: 'Clone',\n  cleanVm: 'Clean VM directory',\n  fastCloneVmLabel: 'Fast clone',\n  startMigratedVm: 'Start the migrated VM',\n  vmConsoleLabel: 'Console',\n  vmExportUrlValidity: 'The URL is valid once for a short period of time.',\n  vmWarmMigration: 'Warm migration',\n  vmWarmMigrationProcessInfo:\n    'Warm migration process will first create a copy of the VM on the destination while the source VM is still running, then shutdown the source VM and send the changes that happened during the migration to the destination to minimize downtime.',\n  backupLabel: 'Backup',\n\n  // ----- SR general tab -----\n  baseCopyTooltip: '{n, number} base cop{n, plural, one {y} other {ies}} ({usage})',\n  diskTooltip: '{name} ({usage})',\n  snapshotsTooltip: '{n, number} snapshot{n, plural, one {} other {s}} ({usage})',\n  vdiOnVmTooltip: '{name} ({usage}) on {vmName}',\n  vdisTooltip: '{n, number} VDI{n, plural, one {} other {s}} ({usage})',\n\n  // ----- SR advanced tab -----\n\n  provisioning: 'Provisioning',\n  srUnhealthyVdiDepth: 'Depth',\n  srUnhealthyVdiNameLabel: 'Name',\n  srUnhealthyVdiSize: 'Size',\n  srUnhealthyVdiTitle: 'VDI to coalesce ({total, number})',\n  srUnhealthyVdiUuid: 'UUID',\n\n  // ----- SR stats tab -----\n\n  srNoStats: 'No stats',\n  statsIops: 'IOPS',\n  statsIoThroughput: 'IO throughput',\n  statsLatency: 'Latency',\n  statsIowait: 'IOwait',\n\n  // ----- SR actions -----\n  srRescan: 'Rescan all disks',\n  srReconnectAll: 'Connect to all hosts',\n  srDisconnectAll: 'Disconnect from all hosts',\n  srForget: 'Forget this SR',\n  srsForget: 'Forget SRs',\n  nSrsForget: 'Forget {nSrs, number} SR{nSrs, plural, one {} other{s}}',\n  srRemoveButton: 'Remove this SR',\n  srNoVdis: 'No VDIs in this storage',\n  srReclaimSpace: 'Reclaim freed space',\n  srReclaimSpaceConfirm: 'Are you sure you want to reclaim freed space on this SR?',\n  srReclaimSpaceNotSupported: 'Space reclaim not supported. Only supported on block based/LVM based SRs.',\n\n  // ----- SR disks tab -----\n  isMetadataVdi: 'This snapshot has been purged of its data. It only contains the metadata and changed blocks.',\n  multipleActiveVdis: '{firstVdi} and {nVdis} more',\n  noActiveVdi: 'No active VDI',\n\n  // ----- Pool general -----\n  earliestExpirationDate: 'Earliest expiration: {dateString}',\n  poolNoSupport: 'No XCP-ng Pro support enabled on this pool',\n  poolPartialSupport:\n    'Only {nHostsLicense, number} host{nHostsLicense, plural, one {} other {s}} under license on {nHosts, number} host{nHosts, plural, one {} other {s}}. This means this pool is not supported at all until you license all its hosts.',\n  poolTitleRamUsage: 'Pool RAM usage:',\n  poolRamUsage: '{used} used of {total} ({free} free)',\n  poolMaster: 'Master:',\n  displayAllHosts: 'Display all hosts of this pool',\n  displayAllStorages: 'Display all storage for this pool',\n  displayAllVMs: 'Display all VMs of this pool',\n  licenseRestrictions: 'License restrictions',\n  licenseRestrictionsModalTitle: 'Warning: You are using a Free XenServer license',\n  actionsRestricted: 'Some functionality is restricted.',\n  counterRestrictionsOptions: 'You can:',\n  counterRestrictionsOptionsXcp: 'upgrade to XCP-ng for free to get rid of these restrictions',\n  counterRestrictionsOptionsXsLicense: 'or get a commercial Citrix license',\n  // ----- Pool tabs -----\n  hostsTabName: 'Hosts',\n  vmsTabName: 'VMs',\n  srsTabName: 'SRs',\n  // ----- Pool advanced tab -----\n  backupNetwork: 'Backup network',\n  crashDumpSr: 'Crash dump SR',\n  defaultMigrationNetwork: 'Default migration network',\n  migrationCompression: 'Migration compression',\n  migrationCompressionDisabled: 'Migration compression is not available on this pool',\n  poolDisableHa: 'Disabling high availability',\n  poolDisableHaConfirm: 'Are you sure you want to disable high availability on this pool?',\n  poolEnableHa: 'Enabling high availability',\n  poolEditAll: 'Edit all',\n  poolHaSelectSrs: 'Select heartbeat SR candidates',\n  poolHaSelectSrsDetails: 'The XAPI will pick one of these SR as heartbeat SR',\n  poolHaEnabled: 'Enabled',\n  poolHaDisabled: 'Disabled',\n  poolHaStatus: 'High Availability',\n  poolHeartbeatSr: 'Heartbeat SR',\n  poolGpuGroups: 'GPU groups',\n  poolRemoteSyslogPlaceHolder: 'Logging host',\n  poolSupportSourceUsers: 'XCP-ng Pro Support not available for source users',\n  poolSupportXcpngOnly: 'Only available for pool of XCP-ng hosts',\n  poolLicenseAlreadyFullySupported: 'The pool is already fully supported',\n  rollingPoolReboot: 'Rolling Pool Reboot',\n  rollingPoolRebootHaWarning: 'High Availability is enabled. This will automatically disable it during the reboot.',\n  rollingPoolRebootLoadBalancerWarning:\n    'Load Balancer plugin is running. This will automatically pause it during the reboot.',\n  rollingPoolRebootMessage:\n    'Are you sure you want to start a Rolling Pool Reboot? Running VMs will be migrated back and forth and this can take a while. Scheduled backups that may concern this pool will be disabled.',\n\n  setpoolMaster: 'Master',\n  syslogRemoteHost: 'Remote syslog host',\n  syncNetbox: 'Synchronize with Netbox',\n  syncNetboxWarning: 'Are you sure you want to synchronize with Netbox?',\n  updateMissingNetwork: '{networkID} not found, please select a new one',\n  // ----- Pool host tab -----\n  hostNameLabel: 'Name',\n  hostDescription: 'Description',\n  noHost: 'No hosts',\n  memoryLeftTooltip: '{used}% used ({free} free)',\n  // ----- Pool network tab -----\n  bondMode: 'Bond Mode',\n  pif: 'PIF',\n  poolNetworkAutomatic: 'Automatic',\n  poolNetworkNameLabel: 'Name',\n  poolNetworkDescription: 'Description',\n  poolNetworkPif: 'PIFs',\n  privateNetworks: 'Private networks',\n  manage: 'Manage',\n  poolNoNetwork: 'No networks',\n  poolNetworkMTU: 'MTU',\n  poolNetworkPifAttached: 'Connected',\n  poolNetworkPifDetached: 'Disconnected',\n  showPifs: 'Show PIFs',\n  hidePifs: 'Hide PIFs',\n  networkAutomaticTooltip: 'Network(s) selected by default for new VMs',\n  noNbdConnection: 'No NBD Connection',\n  nbdConnection: 'NBD Connection',\n  insecureNbdConnection: 'Insecure NBD Connection (not allowed through XO)',\n  // ----- Pool patches tab -----\n  multiHostPoolUpdate: \"Rolling pool update can only work when there's multiple hosts in a pool with a shared storage\",\n  nVmsRunningOnLocalStorage:\n    '{nVms, number} VM{nVms, plural, one {} other {s}} {nVms, plural, one {is} other {are}} currently running and using at least one local storage. A shared storage for all your VMs is needed to start a rolling pool update',\n  // ----- Pool stats tab -----\n  poolNoStats: 'No stats',\n  poolAllHosts: 'All hosts',\n  // ----- Pool actions ------\n  addSrLabel: 'Add SR',\n  addVmLabel: 'Add VM',\n  addHostsLabel: 'Add hosts',\n  clickLinkForDetails: 'Follow this link for more details',\n  missingPatchesPool:\n    'The pool needs to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may take a while.',\n  missingPatchesHost:\n    'The selected host{nHosts, plural, one {} other {s}} need{nHosts, plural, one {s} other {}} to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may take a while.',\n  patchUpdateNoInstall:\n    'The selected host{nHosts, plural, one {} other {s}} cannot be added to the pool because the patches are not homogeneous.',\n  addHostsErrorTitle: 'Adding host{nHosts, plural, one {} other {s}} failed',\n  addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',\n  disconnectServer: 'Disconnect',\n\n  // ----- Host item ------\n  host: 'Host',\n  hostHvmDisabled: 'Hardware-assisted virtualization is not enabled on this host',\n  hostNoLicensePartialProSupport:\n    'This host does not have an active license, even though it is in a pool with licensed hosts. In order for XCP-ng Pro Support to be enabled on a pool, all hosts within the pool must have an active license',\n  hostNoSupport: 'No XCP-ng Pro Support enabled on this host',\n  hostSupportEnabled: 'XCP-ng Pro Support enabled on this host',\n  noMoreMaintained: 'This host version is no longer maintained',\n  pubKeyTooShort: 'TLS key is too small to update host to XCP-ng 8.3',\n  longerCustomCertficate:\n    'If your certificate is custom, you need to install a new one with a key length of 2048 or greater.',\n  longerDefaultCertificate:\n    'If your certificate is the default self-signed certificate from host installation, you need to re-generate it from XCP-ng 8.2.1.',\n\n  // ----- Host actions ------\n  disableMaintenanceMode: 'Disable maintenance mode',\n  enableMaintenanceMode: 'Enable maintenance mode',\n  slaveHostMoreUpToDateThanMasterAfterRestart:\n    'It appears that this host will be more up-to-date than the master ({master}) after the restart. This will result in the slave being unable to contact the pool master. Please update and restart your master node first.',\n  startHostLabel: 'Start',\n  stopHostLabel: 'Stop',\n  enableHostLabel: 'Enable',\n  disableHostLabel: 'Disable',\n  restartHostAgent: 'Restart toolstack',\n  smartRebootBypassCurrentVmCheck:\n    'As the XOA is hosted on the host that is scheduled for a reboot, it will also be restarted. Consequently, XO won\\'t be able to resume VMs, and VMs with the \"Protect from accidental shutdown\" option enabled will not have this option reactivated automatically.',\n  smartRebootHostLabel: 'Smart reboot',\n  smartRebootHostTooltip: 'Suspend resident VMs, reboot host and resume VMs automatically',\n  forceRebootHostLabel: 'Force reboot',\n  forceSmartRebootHost:\n    'Smart Reboot failed because {nVms, number} VM{nVms, plural, one {} other {s}} ha{nVms, plural, one {s} other {ve}} {nVms, plural, one {its} other {their}} Suspend operation blocked. Would you like to force?',\n  restartAnyway: 'Restart anyway',\n  rebootHostLabel: 'Reboot',\n  noHostsAvailableErrorTitle: 'Error while restarting host',\n  noHostsAvailableErrorMessage:\n    'Some VMs cannot be migrated without first rebooting this host. Please try force reboot.',\n  failHostBulkRestartTitle: 'Error while restarting hosts',\n  failHostBulkRestartMessage:\n    '{failedHosts, number}/{totalHosts, number} host{failedHosts, plural, one {} other {s}} could not be restarted.',\n  rebootUpdateHostLabel: 'Reboot to apply updates',\n  emergencyModeLabel: 'Emergency mode',\n  // ----- Host tabs -----\n  storageTabName: 'Storage',\n  patchesTabName: 'Patches',\n  // ----- host stat tab -----\n  statLoad: 'Load average',\n  // ----- host advanced tab -----\n  applyChangeOnPcis:\n    'This operation will reboot the host in order to apply the change on the PCI{nPcis, plural, one {} other {s}}. Are you sure you want to continue?',\n  className: 'Class name',\n  confirmForceRebootHost: 'Force reboot?',\n  deviceName: 'Device name',\n  enabled: 'Enabled',\n  disksSystemHealthy: 'All disks are healthy ✅',\n  editHostIscsiIqnTitle: 'Edit iSCSI IQN',\n  editHostIscsiIqnMessage:\n    'Are you sure you want to edit the iSCSI IQN? This may result in failures connecting to existing SRs if the host is attached to iSCSI SRs.',\n  hostTitleRamUsage: 'Host RAM usage:',\n  hostEvacuationFailed: 'The host evacuation failed, do you want to force reboot?',\n  maintenanceHostModalMessage:\n    'Are you sure you want to enter maintenance mode? This will migrate all the VMs running on this host to other hosts of the pool.',\n  maintenanceHostModalTitle: 'Maintenance mode',\n  maintenanceHostTooltip: 'Evacuate and disable the host',\n  memoryHostState: 'RAM: {memoryUsed} used of {memoryTotal} ({memoryFree} free)',\n  hardwareHostSettingsLabel: 'Hardware',\n  hyperThreading: 'Hyper-threading (SMT)',\n  hyperThreadingNotAvailable: 'HT detection is only available on XCP-ng 7.6 and higher',\n  hostDownloadLogs: 'Download system logs',\n  hostDownloadLogsContainEntireHostLogs:\n    \"The logs you are about to download contain the entire host's logs, potentially hundreds of megabytes. Please note that these logs can be technical and complex to analyze, requiring some expertise.\",\n  hostAddress: 'Address',\n  hostStatus: 'Status',\n  hostBuildNumber: 'Build number',\n  hostIscsiIqn: 'iSCSI IQN',\n  hostIommuTooltip: 'PCI passthrough capable',\n  hostNoIscsiSr: 'Not connected to an iSCSI SR',\n  hostMultipathingSrs: 'Click to see concerned SRs',\n  hostMultipathingPaths: '{nActives, number} of {nPaths, number} path{nPaths, plural, one {} other {s}}',\n  hostMultipathingRequiredState:\n    'This action will not be fulfilled if a VM is in a running state. Please ensure that all VMs are evacuated or stopped before performing this action!',\n  hostMultipathingWarning:\n    'The host{nHosts, plural, one {} other {s}} will lose their connection to the SRs. Do you want to continue?',\n  hostXenServerVersion: 'Version',\n  hostStatusEnabled: 'Enabled',\n  hostStatusDisabled: 'Disabled',\n  hostPowerOnMode: 'Power on mode',\n  hostControlDomainMemory: 'Control domain memory',\n  setControlDomainMemory: 'Set control domain memory',\n  setControlDomainMemoryMessage:\n    'Editing the control domain memory will immediately restart the host in order to apply the changes.',\n  maintenanceModeRequired: 'The host needs to be in maintenance mode',\n  hostStartedSince: 'Host uptime',\n  hostStackStartedSince: 'Toolstack uptime',\n  hostCpusModel: 'CPU model',\n  hostGpus: 'GPUs',\n  hostCpusNumber: 'Core (socket)',\n  hostManufacturerinfo: 'Manufacturer info',\n  hostBiosinfo: 'BIOS info',\n  licenseHostSettingsLabel: 'License',\n  hostLicenseType: 'Type',\n  hostLicenseSocket: 'Socket',\n  hostLicenseExpiry: 'Expiry',\n  hostRemoteSyslog: 'Remote syslog',\n  hostIommu: 'IOMMU',\n  hostNoCertificateInstalled: 'No certificates installed on this host',\n  'onlyAvailableXcp8.3OrHigher': 'Only available for XCP-ng 8.3.0 or higher',\n  installRaidPlugin: 'To display RAID info, install raid.py plugin',\n  noRaidInformationAvailable: 'No RAID information available',\n  pciDevices: 'PCI Devices',\n  pciId: 'PCI ID',\n  pcisEnable: 'PCI{nPcis, plural, one {} other {s}} enable',\n  pcisDisable: 'PCI{nPcis, plural, one {} other {s}} disable',\n  passthroughEnabled: 'Passthrough enabled',\n  pusbDevices: 'PUSB Devices',\n  smartctlPluginNotInstalled: 'Smartctl plugin not installed',\n  supplementalPacks: 'Installed supplemental packs',\n  supplementalPackNew: 'Install new supplemental pack',\n  supplementalPackPoolNew: 'Install supplemental pack on every host',\n  supplementalPackTitle: '{name} (by {author})',\n  supplementalPackInstallStartedTitle: 'Installation started',\n  supplementalPackInstallStartedMessage: 'Installing new supplemental pack…',\n  supplementalPackInstallErrorTitle: 'Installation error',\n  supplementalPackInstallErrorMessage: 'The installation of the supplemental pack failed.',\n  supplementalPackInstallSuccessTitle: 'Installation success',\n  supplementalPackInstallSuccessMessage: 'Supplemental pack successfully installed.',\n  systemDisksHealth: 'System disks health',\n  raidHealthy: 'All mdadm RAID are healthy ✅',\n  raidStateWarning: 'RAID state needs your attention: {state}',\n  raidStatus: 'RAID Status',\n  uniqueHostIscsiIqnInfo: 'The iSCSI IQN must be unique. ',\n  vendorId: 'Vendor ID',\n  // ----- Host net tabs -----\n  networkCreateButton: 'Add a network',\n  pifDeviceLabel: 'Device',\n  pifNetworkLabel: 'Network',\n  pifVlanLabel: 'VLAN',\n  pifAddressLabel: 'Address',\n  pifModeLabel: 'Mode',\n  pifMacLabel: 'MAC',\n  pifMtuLabel: 'MTU',\n  pifSpeedLabel: 'Speed',\n  pifStatusLabel: 'Status',\n  pifInUse: 'This interface is currently in use',\n  defaultLockingMode: 'Default locking mode',\n  pifConfigureIp: 'Configure IP address',\n  configIpErrorTitle: 'Invalid parameters',\n  staticIp: 'Static IP address',\n  staticIpv6: 'Static IPv6 address',\n  netmask: 'Netmask',\n  dns: 'DNS',\n  gateway: 'Gateway',\n  ipRequired: 'An IP address is required',\n  netmaskRequired: 'Netmask required',\n  // ----- Host storage tabs -----\n  addSrDeviceButton: 'Add a storage',\n  srType: 'Type',\n  pbdDetails: 'PBD details',\n  pbdStatus: 'Status',\n  pbdStatusConnected: 'Connected',\n  pbdStatusDisconnected: 'Disconnected',\n  pbdConnect: 'Connect',\n  pbdDisconnect: 'Disconnect',\n  pbdForget: 'Forget',\n  srShared: 'Shared',\n  srNotShared: 'Not shared',\n  pbdNoSr: 'No storage detected',\n  // ----- Host patch tabs -----\n  patchNameLabel: 'Name',\n  patchUpdateButton: 'Install all patches',\n  patchDescription: 'Description',\n  patchVersion: 'Version',\n  patchApplied: 'Applied date',\n  patchSize: 'Size',\n  patchNothing: 'No patches detected',\n  patchReleaseDate: 'Release date',\n  patchGuidance: 'Guidance',\n  hostAppliedPatches: 'Applied patches',\n  hostMissingPatches: 'Missing patches',\n  hostUpToDate: 'Host up-to-date!',\n  installAllPatchesTitle: 'Install all patches',\n  installAllPatchesContent: 'To install all patches go to pool.',\n  installAllPatchesRedirect: 'Go to pool',\n  installAllPatchesOnHostContent:\n    'The pool master must always be updated FIRST. Updating will automatically restart the toolstack. Running VMs will not be affected. Are you sure you want to continue and install all patches on this host?',\n  patchRelease: 'Release',\n  updatePluginNotInstalled:\n    'An error occurred while fetching the patches. Please make sure the updater plugin is installed by running `yum install xcp-ng-updater` on the host.',\n  showChangelog: 'Show changelog',\n  changelog: 'Changelog',\n  changelogPatch: 'Patch',\n  changelogAuthor: 'Author',\n  changelogDate: 'Date',\n  changelogDescription: 'Description',\n  // ----- Pool patch tabs -----\n  install: 'Install',\n  installPatchesTitle: 'Install patch{nPatches, plural, one {} other {es}}',\n  installPatchesContent:\n    'This will automatically restart the toolstack on every host. Running VMs will not be affected. Are you sure you want to continue and install {nPatches, number} patch{nPatches, plural, one {} other {es}}?',\n  installPoolPatches: 'Install pool patches',\n  confirmPoolPatch:\n    'This will automatically restart the toolstack on every host. Running VMs will not be affected. Are you sure you want to continue and install all the patches on this pool?',\n  rollingPoolUpdate: 'Rolling pool update',\n  rollingPoolUpdateMessage:\n    'Are you sure you want to start a rolling pool update? Running VMs will be migrated back and forth and this can take a while. Scheduled backups that may concern this pool will be disabled.',\n  rollingPoolUpdateHaWarning: 'High Availability is enabled. This will automatically disable it during the update.',\n  rollingPoolUpdateLoadBalancerWarning:\n    'Load Balancer plugin is running. This will automatically pause it during the update.',\n  poolNeedsDefaultSr: 'The pool needs a default SR to install the patches.',\n  vmsHaveCds: '{nVms, number} VM{nVms, plural, one {} other {s}} {nVms, plural, one {has} other {have}} CDs',\n  ejectCds: 'Eject CDs',\n  highAvailabilityNotDisabledTooltip: 'High Availability must be disabled',\n  xsCredentialsMissing:\n    'In order to install XenServer updates, you first need to configure your XenServer Client ID. See {link}.',\n  xsCredentialsMissingShort: 'Missing XenServer Client ID',\n\n  // ----- Pool storage tabs -----\n  defaultSr: 'Default SR',\n  setAsDefaultSr: 'Set as default SR',\n  setDefaultSr: 'Set default SR:',\n\n  // ----- VM tabs -----\n  generalTabName: 'General',\n  statsTabName: 'Stats',\n  consoleTabName: 'Console',\n  containersTabName: 'Container',\n  snapshotsTabName: 'Snapshots',\n  logsTabName: 'Logs',\n  advancedTabName: 'Advanced',\n  networkTabName: 'Network',\n  disksTabName: 'Disk{disks, plural, one {} other {s}}',\n\n  powerStateHalted: 'Halted',\n  powerStateRunning: 'Running',\n  powerStateSuspended: 'Suspended',\n  powerStatePaused: 'Paused',\n  powerStateDisabled: 'Disabled',\n  powerStateBusy: 'Busy',\n\n  // ----- VM home -----\n  vmCurrentStatus: 'Current status:',\n  vmNotRunning: 'Not running',\n  vmHaltedSince: 'Halted {ago}',\n\n  // ----- VM general tab -----\n  availableForUefiOnly: 'This feature is only available for UEFI VMs.',\n  noToolsDetected: 'No Xen tools detected',\n  managementAgentDetected: 'Management agent {version} detected',\n  managementAgentOutOfDate: 'Management agent {version} out of date',\n  managementAgentNotDetected: 'Management agent not detected',\n  noIpv4Record: 'No IPv4 record',\n  noIpRecord: 'No IP record',\n  secureBootEnforced: 'Secure boot enforced',\n  secureBootNotEnforced: 'Secure boot not enforced',\n  secureBootNoDbx: 'Secure boot enforced, but no dbx present',\n  secureBootStatus: 'Secure boot status',\n  secureBootWantedButCertificatesMissing: 'Secure boot wanted, but some EFI certificates are missing',\n  secureBootWantedButDisabled: 'Secure boot wanted, but disabled due to the VM being in UEFI setup mode',\n  secureBootWantedPendingBoot: 'Secure boot wanted, pending first boot',\n  started: 'Started {ago}',\n  paraVirtualizedMode: 'Paravirtualization (PV)',\n  hardwareVirtualizedMode: 'Hardware virtualization (HVM)',\n  hvmModeWithPvDriversEnabled: 'Hardware virtualization with paravirtualization drivers enabled (PVHVM)',\n  pvInPvhMode: 'PV inside a PVH container (PV in PVH)',\n  vmCreatedAdmin: 'Created by {user}\\non {date}\\nwith template {template}',\n  vmCreatedNonAdmin: 'Created on {date}\\nwith template {template}',\n  windowsUpdateTools: 'Manage Citrix PV drivers via Windows Update',\n  windowsToolsModalTitle: 'Windows Update Tools',\n  windowsToolsModalMessage:\n    'Enabling this will allow the VM to automatically install Citrix PV drivers from Windows Update. This only includes drivers, the Citrix management agent must still be separately installed.',\n  windowsToolsModalWarning:\n    'If you have previously installed XCP-ng tools instead of Citrix tools, this option will break your VM.',\n  editVmNotes: 'Edit VM notes',\n  supportsMarkdown: 'Supports Markdown syntax',\n  vmNotesTooLong: 'VM notes cannot be longer than 2048 characters',\n\n  // ----- VM stat tab -----\n  statsCpu: 'CPU usage',\n  statsMemory: 'Memory usage',\n  statsNetwork: 'Network throughput',\n  useStackedValuesOnStats: 'Stacked values',\n  statDisk: 'Disk throughput',\n  statLastTenMinutes: 'Last 10 minutes',\n  statLastTwoHours: 'Last 2 hours',\n  statLastDay: 'Last day',\n  statLastWeek: 'Last week',\n  statLastYear: 'Last year',\n\n  // ----- VM console tab -----\n  copyToClipboardLabel: 'Copy',\n  ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',\n  ctrlAltDelConfirmation: 'Send Ctrl+Alt+Del to VM?',\n  disabledConsole: 'Console is disabled for this VM',\n  multilineCopyToClipboard: 'Multiline copy',\n  tipLabel: 'Tip:',\n  hideHeaderTooltip: 'Hide info',\n  showHeaderTooltip: 'Show info',\n  sendToClipboard: 'Send to clipboard',\n  sshRootTooltip: 'Connect using external SSH tool as root',\n  sshRootLabel: 'SSH',\n  sshUserTooltip: 'Connect using external SSH tool as user…',\n  sshUserLabel: 'SSH as…',\n  sshUsernameLabel: 'SSH user name',\n  remoteNeedClientTools: 'No IP address reported by client tools',\n  rdp: 'RDP',\n  rdpRootTooltip: 'Connect using external RDP tool',\n\n  // ----- VM container tab -----\n  containerName: 'Name',\n  containerCommand: 'Command',\n  containerCreated: 'Creation date',\n  containerStatus: 'Status',\n  containerAction: 'Action',\n  noContainers: 'No existing containers',\n  containerStop: 'Stop this container',\n  containerStart: 'Start this container',\n  containerPause: 'Pause this container',\n  containerResume: 'Resume this container',\n  containerRestart: 'Restart this container',\n\n  // ----- VM disk tab -----\n  rescanIsoSrs: 'Rescan all ISO SRs',\n  vbdCreateDeviceButton: 'New disk',\n  vdiAttachDevice: 'Attach disk',\n  vdiAttachDeviceConfirm: 'The selected VDI is already attached to this VM. Are you sure you want to continue?',\n  vdiBootOrder: 'Boot order',\n  vdiNameLabel: 'Name',\n  vdiNameDescription: 'Description',\n  vdiPool: 'Pool',\n  vdiTags: 'Tags',\n  vdiTasks: 'VDI tasks',\n  vdiSize: 'Size',\n  vdiSr: 'SR',\n  vdiVms: 'VMs',\n  vdiMigrate: 'Migrate VDI',\n  vdiMigrateSelectSr: 'Destination SR:',\n  vdiMigrateNoSr: 'No SR',\n  vdiMigrateNoSrMessage: 'A target SR is required to migrate a VDI',\n  vdiForget: 'Forget',\n  noControlDomainVdis: 'No VDIs attached to control domain',\n  vbdBootableStatus: 'Boot flag',\n  vbdDevice: 'Device',\n  vbdCbt: 'CBT',\n  vbdStatus: 'Status',\n  vbdStatusConnected: 'Connected',\n  vbdStatusDisconnected: 'Disconnected',\n  vbdConnect: 'Connect VBD',\n  vbdDisconnect: 'Disconnect VBD',\n  vbdBootable: 'Bootable',\n  vbdReadonly: 'Readonly',\n  vbdCreate: 'Create',\n  vbdAttach: 'Attach',\n  vbdNamePlaceHolder: 'Disk name',\n  vbdSizePlaceHolder: 'Size',\n  cdDriveNotInstalled: 'CD drive not completely installed',\n  cdDriveInstallation: 'Stop and start the VM to install the CD drive',\n  saveBootOption: 'Save',\n  resetBootOption: 'Reset',\n  destroySelectedVdis: 'Destroy selected VDIs',\n  destroyVdi: 'Destroy VDI',\n  exportVdi: 'Export VDI content',\n  format: 'Format',\n  importVdi: 'Import VDI content',\n  importVdiNoFile: 'No file selected',\n  selectVdiMessage: 'Drop VHD file here',\n  useQuotaWarning:\n    'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',\n  notEnoughSpaceInResourceSet: 'Not enough space in resource set {resourceSet} ({spaceLeft} left)',\n  warningVdiSr: \"The VDIs' SRs must either be shared or on the same host for the VM to be able to start.\",\n  removeSelectedVdisFromVm: 'Remove selected VDIs from this VM',\n  removeVdiFromVm: 'Remove VDI from this VM',\n  qcow2: 'QCOW2',\n  vhd: 'VHD',\n  vmdk: 'VMDK',\n  raw: 'RAW',\n\n  // ----- VM network tab -----\n\n  editVifLockingMode: 'Edit locking mode',\n  aclRuleAllow: 'Allow the traffic',\n  aclRuleProtocol: 'Select a protocol',\n  aclRulePort: 'Select a port',\n  aclRuleIpRange: 'Select an IP or an IP range (CIDR format)',\n  aclRuleDirection: 'Select a direction',\n  aclRuleAllowField: 'Traffic Enabled/Disabled',\n  aclRuleProtocolField: 'Protocol',\n  aclRulePortField: 'Port',\n  aclRuleIpRangeField: 'IP range (CIDR format)',\n  aclRuleDirectionField: 'Direction',\n  addRule: 'Add rule',\n  deleteRule: 'Delete rule',\n  hideRules: 'Hide rules',\n  sdnControllerNotLoaded: 'SDN Controller must be loaded',\n  showRules: 'Show rules',\n  vifAclRules: 'Traffic rules',\n  vifCreateDeviceButton: 'New device',\n  vifDeviceLabel: 'Device',\n  vifMacLabel: 'MAC address',\n  vifMtuLabel: 'MTU',\n  vifNetworkLabel: 'Network',\n  vifRateLimitLabel: 'Rate limit (kB/s)',\n  vifStatusLabel: 'Status',\n  vifStatusConnected: 'Connected',\n  vifStatusDisconnected: 'Disconnected',\n  vifConnect: 'Connect',\n  vifDisconnect: 'Disconnect',\n  vifRemove: 'Remove',\n  vifsRemove: 'Remove selected VIFs',\n  vifIpAddresses: 'IP addresses',\n  vifMacAutoGenerate: 'Auto-generated if empty',\n  vifAllowedIps: 'Allowed IPs',\n  selectIpFromIpPool: 'Select an IP',\n  enterIp: 'Enter an IP',\n  addAllowedIp: 'Add allowed IP',\n  addIpError: 'Error while adding allowed IP',\n  invalidIp: 'Invalid IP',\n  vifNoIps: 'No IPs',\n  vifLockingModeDisabled: 'VIF locking mode is disabled',\n  vifLockingModeUnlocked: 'VIF locking mode is unlocked',\n  vifLockingModeLocked: 'VIF locking mode is locked',\n  networkDefaultLockingModeDisabled: 'Network default locking mode is disabled',\n  networkDefaultLockingModeUnlocked: 'Network default locking mode is unlocked',\n  vifLockedNetworkNoIps: 'Network locked and no IPs are allowed for this interface',\n  vifUnlockedNetworkWithIps: 'Some IPs are unnecessarily set as allowed for this interface',\n  vifUnknownNetwork: 'Unknown network',\n  vifCreate: 'Create',\n  nbd: 'NBD',\n  nbdTootltip: 'Network Block Device status',\n  nbdInsecureTooltip: 'Use of insecure NBD is not advised',\n  nbdSecureTooltip: 'Nbd connection is secure and ready',\n\n  // ----- VM snapshot tab -----\n  noSnapshots: 'No snapshots',\n  newSnapshotWithMemory: 'New snapshot with memory',\n  newSnapshotWithMemoryConfirm:\n    'Are you sure you want to create a snapshot with memory? This could take a while and the VM will be unusable during that time.',\n  snapshotMemorySaved: 'Memory saved',\n  snapshotCreateButton: 'New snapshot',\n  tipCreateSnapshotLabel: 'Just click on the snapshot button to create one!',\n  revertSnapshot: 'Revert VM to this snapshot',\n  deleteSnapshot: 'Remove this snapshot',\n  deleteSnapshots: 'Remove selected snapshots',\n  copySnapshot: 'Create a VM from this snapshot',\n  exportSnapshot: 'Export this snapshot',\n  exportSnapshotMemory: 'Export snapshot memory',\n  secureBoot: 'Secure boot',\n  snapshotDate: 'Creation date',\n  snapshotError: 'Snapshot error',\n  snapshotName: 'Name',\n  snapshotDescription: 'Description',\n  snapshotQuiesce: 'Quiesced snapshot',\n  vmRevertSuccessfulTitle: 'Revert successful',\n  vmRevertSuccessfulMessage: 'VM successfully reverted',\n  currentSnapshot: 'Current snapshot',\n\n  // ----- VM backup tab -----\n  goToBackupPage: 'Go to the backup page.',\n\n  // ----- VM log tab -----\n  logRemoveAll: 'Remove all logs',\n  noLogs: 'No logs so far',\n  logDate: 'Creation date',\n  logName: 'Name',\n  logContent: 'Content',\n  logAction: 'Action',\n\n  // ----- VM advanced tab -----\n  addXenStoreEntry: 'Add new XenStore entry',\n  attachedPcis: 'Attached PCIs',\n  attachingDetachingPciNeedVmBoot: 'Attaching/detaching a PCI will be taken into consideration for the next VM boot.',\n  attachPcis: 'Attach PCIs',\n  createVtpm: 'Create a VTPM',\n  deleteEntryDeleteValue: 'To delete an entry, simply delete its value',\n  deleteVtpm: 'Delete the VTPM',\n  deleteVtpmWarning:\n    'If the VTPM is in use, removing it will result in a dangerous data loss. Are you sure you want to remove the VTPM?',\n  infoUnknownPciOnNonRunningVm:\n    \"When a VM is offline, it's not attached to any host, and therefore, it's impossible to determine the associated PCI devices, as it depends on the hardware environment in which it would be deployed.\",\n  coalesceLeaf: 'Coalesce leaf',\n  coalesceLeafSuccess: 'Coalesce leaf success',\n  coalesceLeafSuspendVm: 'This will suspend the VM during the operation. Do you want to continue?',\n  noSecureBoot: 'This pool was not setup for Guest UEFI SecureBoot yet',\n  propagateCertificatesTitle: 'Propagate certificates',\n  propagateCertificatesConfirm:\n    \"This will overwrite the VM's UEFI certificates with certificates defined at the pool level. Continue?\",\n  propagateCertificates: \"Copy the pool's default UEFI certificates to the VM\",\n  propagateCertificatesSuccessful: 'Certificates propagated successfully',\n  poolAutoPoweronDisabled: 'Auto power on is disabled at pool level, click to fix automatically.',\n  rebootRequiredAfterXenStoreChanges: 'A reboot is required after any XenStore changes',\n  vmRemoveButton: 'Remove',\n  vmConvertToTemplateButton: 'Convert to template',\n  vmSwitchVirtualizationMode: 'Convert to {mode}',\n  vmVirtualizationModeModalTitle: 'Change virtualization mode',\n  vmVirtualizationModeModalBody:\n    \"You must know what you are doing, because it could break your setup (if you didn't install the bootloader in the MBR while switching from PV to HVM, or even worse, in HVM to PV, if you don't have the correct PV args)\",\n  vmShareButton: 'Share',\n  xenSettingsLabel: 'Xen settings',\n  xenStore: 'XenStore',\n  guestOsLabel: 'Guest OS',\n  miscLabel: 'Misc',\n  virtualizationMode: 'Virtualization mode',\n  startDelayLabel: 'Start delay (seconds)',\n  cpuMaskLabel: 'CPU mask',\n  selectCpuMask: 'Select core(s)…',\n  cpuWeightLabel: 'CPU weight',\n  defaultCpuWeight: 'Default ({value, number})',\n  cpuCapLabel: 'CPU cap',\n  defaultCpuCap: 'Default ({value, number})',\n  pvArgsLabel: 'PV args',\n  managementAgentVersion: 'Management agent version',\n  osName: 'OS name',\n  osKernel: 'OS kernel',\n  autoPowerOn: 'Auto power on',\n  protectFromDeletion: 'Protect from accidental deletion',\n  protectFromShutdown: 'Protect from accidental shutdown',\n  blockMigration: 'Prevent migration',\n  unblockMigrationTitle: 'Allow migration',\n  unblockMigrationConfirm: 'Are you sure you want to allow migration for this VM?',\n  ha: 'HA',\n  srHaTooltip: 'SR used for High Availability',\n  nestedVirt: 'Nested virtualization',\n  nestedVirtualizationWarning: 'Unstable feature, insecure for the host, usage is discouraged. Click for more details.',\n  vmAffinityHost: 'Affinity host',\n  vmNeedToBeHalted: 'The VM needs to be halted',\n  vmVga: 'VGA',\n  vmVideoram: 'Video RAM',\n  vmNicType: 'NIC type',\n  vtpm: 'VTPM',\n  vtpmRequireUefi: 'A UEFI boot firmware is necessary to use a VTPM',\n  noAffinityHost: 'None',\n  originalTemplate: 'Original template',\n  unknownOsName: 'Unknown',\n  unknownOsKernel: 'Unknown',\n  unknownOriginalTemplate: 'Unknown',\n  vmLimitsLabel: 'VM limits',\n  resourceSet: 'Resource set',\n  resourceSetNone: 'None',\n  selectUser: 'Select user',\n  suspendSr: 'Suspend SR',\n  viridian: 'Viridian',\n  vmCpuLimitsLabel: 'CPU limits',\n  vmCpuTopology: 'Topology',\n  vmChooseCoresPerSocket: 'Default behavior',\n  vmSocketsWithCoresPerSocket:\n    '{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket',\n  vmCoresPerSocketNone: 'None',\n  vmCoresPerSocket: '{nCores, number} core{nCores, plural, one {} other {s}} per socket',\n  vmCoresPerSocketNotDivisor: \"Not a divisor of the VM's max CPUs\",\n  vmCoresPerSocketExceedsCoresLimit: 'The selected value exceeds the cores limit ({maxCores, number})',\n  vmCoresPerSocketExceedsSocketsLimit: 'The selected value exceeds the sockets limit ({maxSockets, number})',\n  vmHaDisabled: 'Disabled',\n  vmMemoryLimitsLabel: 'Memory limits (min/max)',\n  vmUuid: 'VM UUID',\n  vmVgpu: 'vGPU',\n  vmVgpus: 'GPUs',\n  vmVgpuNone: 'None',\n  vmAddVgpu: 'Add vGPU',\n  vmSelectVgpuType: 'Select vGPU type',\n  vmAcls: 'ACLs',\n  vmAddAcls: 'Add ACLs',\n  addAclsErrorTitle: 'Failed to add ACL(s)',\n  addAclsErrorMessage: 'User(s)/group(s) and role are required.',\n  createVusb: 'Create VUSB',\n  removeAcl: 'Delete',\n  moreAcls: '{nAcls, number} more…',\n  pusbDescription: 'PUSB description',\n  pusbSpeed: 'PUSB speed',\n  pusbVersion: 'PUSB version',\n  selectPusb: 'Select PUSB',\n  vmBootFirmware: 'Boot firmware',\n  vmCreator: 'VM creator',\n  vmDefaultBootFirmwareLabel: 'default (bios)',\n  vmBootFirmwareWarningMessage:\n    \"You're about to change your boot firmware. This is still experimental in CH/XCP-ng 8.0. Are you sure you want to continue?\",\n  setAndRestartVmFailed: 'Error on restarting and setting the VM: {vm}',\n  vmEditAndRestartModalTitle: 'VM is currently running',\n  vmEditAndRestartModalMessage:\n    'This VM is currently running, and needs to be stopped to modify this value. Restart VM and modify this value?',\n  firmwareNotSupported: 'Firmware not supported',\n  vusbs: 'VUSBs',\n  vusbRemainUnplugged: 'The VUSB remain unplugged until the next shutdown/start',\n  vusbUnplugTooltip: 'Unplug until the next shutdown/start',\n  // ----- VM placeholders -----\n\n  vmHomeNamePlaceholder: 'Long click to add a name',\n  vmHomeDescriptionPlaceholder: 'Long click to add a description',\n\n  // ----- Templates -----\n\n  copyToTemplate: 'Copy to template',\n  copyToTemplateMessage: 'Are you sure you want to copy this snapshot to a template?',\n  templateHomeNamePlaceholder: 'Click to add a name',\n  templateHomeDescriptionPlaceholder: 'Click to add a description',\n  templateDelete: 'Delete template',\n  templateDeleteModalTitle: 'Delete VM template{templates, plural, one {} other {s}}',\n  templateDeleteModalBody:\n    'Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?',\n  failedToDeleteTemplatesTitle: 'Delete template{nTemplates, plural, one {} other {s}} failed',\n  failedToDeleteTemplatesMessage:\n    'Failed to delete {nTemplates, number} template{nTemplates, plural, one {} other {s}}.',\n  deleteDefaultTemplatesTitle: 'Delete default template{nDefaultTemplates, plural, one {} other {s}}',\n  deleteDefaultTemplatesMessage:\n    'You are attempting to delete {nDefaultTemplates, number} default template{nDefaultTemplates, plural, one {} other {s}}. Do you want to continue?',\n  deleteProtectedTemplatesTitle: 'Delete protected template{nProtectedTemplates, plural, one {} other {s}}',\n  deleteProtectedTemplatesMessage:\n    'You are attempting to delete {nProtectedTemplates, plural, one {a} other {nProtectedTemplates}} template{nProtectedTemplates, plural, one {} other {s}} protected from accidental deletion. Do you want to continue?',\n  // ----- Dashboard -----\n  poolPanel: 'Pool{pools, plural, one {} other {s}}',\n  hostPanel: 'Host{hosts, plural, one {} other {s}}',\n  vmPanel: 'VM{vms, plural, one {} other {s}}',\n  memoryStatePanel: 'RAM Usage:',\n  usedMemory: 'Used Memory',\n  totalMemory: 'Total Memory',\n  totalCpus: 'CPUs Total',\n  usedVCpus: 'Used vCPUs',\n  usedSpace: 'Used Space',\n  totalSpace: 'Total Space',\n  cpuStatePanel: 'CPUs Usage',\n  vmStatePanel: 'VMs Power state',\n  vmStateHalted: 'Halted',\n  vmStateOther: 'Other',\n  vmStateRunning: 'Running',\n  vmStateAll: 'All',\n  taskStatePanel: 'Pending tasks',\n  usersStatePanel: 'Users',\n  srStatePanel: 'Storage state',\n  ofUsage: '{usage} (of {total})',\n  ofCpusUsage:\n    '{nVcpus, number} vCPU{nVcpus, plural, one {} other {s}} (of {nCpus, number} CPU{nCpus, plural, one {} other {s}})',\n  noSrs: 'No storage',\n  srName: 'Name',\n  srPool: 'Pool',\n  srHost: 'Host',\n  srFormat: 'Type',\n  srSize: 'Size',\n  srUsage: 'Usage',\n  srUsed: 'used',\n  srFree: 'free',\n  srUsageStatePanel: 'Storage Usage',\n  srTopUsageStatePanel: 'Top 5 SR Usage (in %)',\n  notEnoughPermissionsError: 'Not enough permissions!',\n  vmsStates: '{running, number} running ({halted, number} halted)',\n  dashboardStatsButtonRemoveAll: 'Clear selection',\n  dashboardStatsButtonAddAllHost: 'Add all hosts',\n  dashboardStatsButtonAddAllVM: 'Add all VMs',\n  dashboardSendReport: 'Send report',\n  dashboardReport: 'Report',\n  dashboardSendReportMessage: 'This will send a usage report to your configured emails.',\n  dashboardSendReportInfo: 'The usage report and transport email plugins need to be loaded!',\n\n  // --- Stats board --\n  weekHeatmapData: '{value} {date, date, medium}',\n  weekHeatmapNoData: 'No data.',\n  weeklyHeatmap: 'Weekly Heatmap',\n  weeklyCharts: 'Weekly Charts',\n  weeklyChartsScaleInfo: 'Synchronize scale:',\n  statsDashboardGenericErrorTitle: 'Stats error',\n  statsDashboardGenericErrorMessage: 'There is no stats available for:',\n  noSelectedMetric: 'No selected metric',\n  statsDashboardSelectObjects: 'Select',\n  metricsLoading: 'Loading…',\n\n  // ----- Health -----\n  length: 'Length: {length}',\n  deleteBackups: 'Delete backup{nBackups, plural, one {} other {s}}',\n  deleteBackupsMessage:\n    'Are you sure you want to delete {nBackups, number} backup{nBackups, plural, one {} other {s}}?',\n  detachedBackups: 'Detached backups',\n  detachedVmSnapshots: 'Detached VM snapshots',\n  duplicatedMacAddresses: 'Duplicated MAC addresses',\n  localDefaultSrs: 'Local default SRs',\n  localDefaultSrsStatusTip:\n    \"It is usually recommended for a pool's default SR to be shared to avoid unexpected behaviors\",\n  missingJob: 'Missing job',\n  missingVm: 'Missing VM',\n  missingVmInJob: 'This VM does not belong to this job',\n  missingSchedule: 'Missing schedule',\n  unknownSchedule: 'Unknown schedule',\n  noDetachedBackups: 'No backups',\n  noDuplicatedMacAddresses: 'No duplicated MAC addresses',\n  noOldSnapshots: 'No snapshots older than 30 days with no enabled schedule',\n  reason: 'Reason',\n  oldSnapshots: 'Snapshots older than 30 days with no enabled schedule',\n  orphanedVdis: 'Orphan VDIs',\n  orphanVdisTip: 'VDIs and VDI snapshots that are not attached to a VM',\n  orphanedVms: 'Orphaned VMs snapshot',\n  noOrphanedObject: 'No orphans',\n  poolsWithNoDefaultSr: 'Pools with no default SR',\n  tooManySnapshots: 'Too many snapshots',\n  tooManySnapshotsTip: 'VMs with more than the recommended amount of snapshots',\n  noLocalDefaultSrs: 'No local default SRs',\n  noTooManySnapshotsObject: 'No VMs with too many snapshots',\n  numberOfSnapshots: 'Number of snapshots',\n  guestToolsNecessary: 'Guest tools must be installed to display stats',\n  guestToolStatus: 'Guest Tools status',\n  guestToolStatusTip: 'VMs with missing or outdated guest tools',\n  noGuestToolStatusObject: 'All running VMs have up to date guest tools',\n  guestToolStatusColumn: 'Status',\n  deleteOrphanedVdi: 'Delete orphaned snapshot VDI',\n  deleteSelectedOrphanedVdis: 'Delete selected orphaned snapshot VDIs',\n  vdisOnControlDomain: 'VDIs attached to Control Domain',\n  vifOnVmWithNetwork: 'VIF #{vifDevice, number} on {vm} ({network})',\n  vifs: 'VIFs',\n  vmNameLabel: 'Name',\n  vmNameDescription: 'Description',\n  vmContainer: 'Resident on',\n  snapshotOf: 'Snapshot of',\n  legacySnapshots: 'Legacy backups snapshots',\n  alarmMessage: 'Alarms',\n  noAlarms: 'No alarms',\n  alarmDate: 'Date',\n  alarmContent: 'Content',\n  alarmObject: 'Issue on',\n  alarmPool: 'Pool',\n  spaceLeftTooltip: '{used}% used ({free} left)',\n  unhealthyVdis: 'Unhealthy VDIs',\n  vdisToCoalesce: 'VDIs to coalesce',\n  vdisWithInvalidVhdParent: 'VDIs with invalid parent VHD',\n  srVdisToCoalesceWarning: 'This SR has {nVdis, number} VDI{nVdis, plural, one {} other {s}} to coalesce',\n\n  // ----- New VM -----\n  createVmModalTitle: 'Create VM',\n  createVmModalWarningMessage:\n    \"You're about to use a large amount of resources available on the resource set. Are you sure you want to continue?\",\n  copyHostBiosStrings: 'Copy host BIOS strings to VM',\n  enableVtpm: 'Enable VTPM',\n  newVmCreateNewVmOn: 'Create a new VM on {select}',\n  newVmCreateNewVmNoPermission: 'You have no permission to create a VM',\n  newVmInfoPanel: 'Info',\n  newVmNameLabel: 'Name',\n  newVmTemplateLabel: 'Template',\n  newVmDescriptionLabel: 'Description',\n  newVmPerfPanel: 'Performance',\n  newVmVcpusLabel: 'vCPUs',\n  newVmRamLabel: 'RAM',\n  newVmRamWarning: 'The memory is below the threshold ({threshold})',\n  newVmStaticMaxLabel: 'Static memory max',\n  newVmDynamicMinLabel: 'Dynamic memory min',\n  newVmDynamicMaxLabel: 'Dynamic memory max',\n  newVmInstallSettingsPanel: 'Install settings',\n  newVmIsoDvdLabel: 'ISO/DVD',\n  newVmNetworkLabel: 'Network',\n  newVmInstallNetworkPlaceHolder: 'e.g: http://httpredir.debian.org/debian',\n  newVmPvArgsLabel: 'PV Args',\n  newVmPxeLabel: 'PXE',\n  newVmInterfacesPanel: 'Interfaces',\n  newVmMacLabel: 'MAC',\n  newVmAddInterface: 'Add interface',\n  newVmDisksPanel: 'Disks',\n  newVmSrLabel: 'SR',\n  newVmSizeLabel: 'Size',\n  newVmAddDisk: 'Add disk',\n  newVmSummaryPanel: 'Summary',\n  newVmCreate: 'Create',\n  newVmReset: 'Reset',\n  newVmSelectTemplate: 'Select template',\n  newVmSshKey: 'SSH key',\n  noConfigDrive: 'No config drive',\n  newVmCustomConfig: 'Custom config',\n  availableTemplateVarsInfo: 'Click here to see the available template variables',\n  availableTemplateVarsTitle: 'Available template variables',\n  templateNameInfo: 'the VM\\'s name. It must not contain \"_\"',\n  templateIndexInfo: \"the VM's index, it will take 0 in case of single VM\",\n  templateEscape: 'Tip: escape any variable with a preceding backslash (\\\\)',\n  coreOsDefaultTemplateError: 'Error on getting the default coreOS cloud template',\n  newVmBootAfterCreate: 'Boot VM after creation',\n  newVmMacPlaceholder: 'Auto-generated if empty',\n  newVmCpuWeightLabel: 'CPU weight',\n  newVmDefaultCpuWeight: 'Default: {value, number}',\n  newVmCpuCapLabel: 'CPU cap',\n  newVmDefaultCpuCap: 'Default: {value, number}',\n  newVmCloudConfig: 'Cloud config',\n  newVmCreateVms: 'Create VMs',\n  newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms, number} VMs?',\n  newVmMultipleVms: 'Multiple VMs:',\n  newVmMultipleVmsPattern: 'Name pattern:',\n  newVmMultipleVmsPatternPlaceholder: 'e.g.: \\\\{name\\\\}_%',\n  newVmFirstIndex: 'First index:',\n  newVmNumberRecalculate: 'Recalculate VMs number',\n  newVmNameRefresh: 'Refresh VMs name',\n  newVmAffinityHost: 'Affinity host',\n  newVmAdvancedPanel: 'Advanced',\n  newVmShowAdvanced: 'Show advanced settings',\n  newVmHideAdvanced: 'Hide advanced settings',\n  newVmShare: 'Share this VM',\n  newVmSrsNotOnSameHost: 'The SRs must either be on the same host or shared',\n  newVmNetworkConfigLabel: 'Network config',\n  newVmNetworkConfigInfo: 'Network configuration is only compatible with the {noCloudDatasourceLink}.',\n  newVmNetworkConfigDocLink: 'See {networkConfigDocLink}.',\n  newVmNetworkConfigTooltip: 'Click here to get more information about network config',\n  newVmUserConfigLabel: 'User config',\n  newVmNoCloudDatasource: 'NoCloud datasource',\n  newVmNetworkConfigDoc: 'Network config documentation',\n  templateHasBiosStrings: 'The template already contains the BIOS strings',\n  secureBootLinkToDocumentationMessage: 'Click for more information about Guest UEFI Secure Boot.',\n  secureBootNotSetup: 'This pool has not yet been setup for Guest UEFI Secure Boot. Click for more information.',\n  seeVtpmDocumentation: 'See VTPM documentation',\n  vmBootFirmwareIsUefi: 'The boot firmware is UEFI',\n  destroyCloudConfigVdiAfterBoot: 'Destroy cloud config drive after first boot',\n  vtpmNotSupported: 'VTPM is only supported on pools running XCP-ng/XS 8.3 or later.',\n  warningVtpmRequired: 'This template requires a VTPM, if you proceed, the VM will likely not be able to boot.',\n\n  // ----- Self -----\n  resourceSets: 'Resource sets',\n  noResourceSets: 'No resource sets.',\n  resourceSetName: 'Resource set name',\n  resourceSetUsers: 'Users',\n  resourceSetPools: 'Pools',\n  resourceSetTemplates: 'Templates',\n  resourceSetSrs: 'SRs',\n  resourceSetNetworks: 'Networks',\n  recomputeResourceSets: 'Recompute all limits',\n  saveResourceSet: 'Save',\n  resetResourceSet: 'Reset',\n  editResourceSet: 'Edit',\n  defaultTags: 'Default tags',\n  deleteResourceSet: 'Delete',\n  deleteResourceSetWarning: 'Delete resource set',\n  deleteResourceSetQuestion: 'Are you sure you want to delete this resource set?',\n  resourceSetMissingObjects: 'Missing objects:',\n  unknownResourceSetValue: 'Unknown',\n  availableHosts: 'Available hosts',\n  excludedHosts: 'Excluded hosts',\n  noHostsAvailable: 'No hosts available.',\n  availableHostsDescription: 'VMs created from this resource set shall run on the following hosts.',\n  maxCpus: 'Maximum CPUs',\n  maxRam: 'Maximum RAM',\n  maxDiskSpace: 'Maximum disk space',\n  ipPool: 'IP pool',\n  quantity: 'Quantity',\n  usedResourceLabel: 'Used',\n  availableResourceLabel: 'Available',\n  resourceSetQuota: 'Used: {usage} (Total: {total})',\n  resourceSetNew: 'New',\n  shareVmsByDefault: 'Share VMs by default',\n  nVmsInResourceSet:\n    '{nVms, number} VM{nVms, plural, one {} other {s}} belong{nVms, plural, one {s} other {}} to this Resource Set',\n  unlimitedResourceSetUsage: 'Used: {usage}',\n\n  // ---- VM import ---\n  fileType: 'File type:',\n  firmware: 'Firmware',\n  fromUrl: 'From URL',\n  UrlImportSrsCompatible: 'URL import is only compatible with ISO SRs',\n  fromVmware: 'From VMware',\n  importVmsList: 'Drop OVA or XVA files here to import Virtual Machines.',\n  noSelectedVms: 'No selected VMs.',\n  noToolsInstalled: 'No tools installed',\n  url: 'URL:',\n  vmImportToPool: 'To Pool:',\n  vmImportToSr: 'To SR:',\n  vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',\n  warningVsanImport:\n    '<div>VM running from non file based datastore (like VSAN) will be migrated in a three steps process<ul><li>Stop the VM</li><li>Export the VM disks to a remote of Xen Orchestra</li><li>Load these disks in XCP-ng</li></ul>This process will be slower than migrating the VM to VMFS / NFS datastore and then migrating them to XCP-ng</div>',\n  workDirLabel: 'Remote used to store temporary disk files(VSAN migration)',\n  importVmsCleanList: 'Reset',\n  vmImportSuccess: 'VM import success',\n  vmImportFailed: 'VM import failed',\n  vdiImportSuccess: 'VDI import success',\n  vdiImportFailed: 'VDI import failed',\n  setVmFailed: 'Error on setting the VM: {vm}',\n  startVmImport: 'Import starting…',\n  startVdiImport: 'VDI import starting…',\n  startVmExport: 'Export starting…',\n  startVdiExport: 'VDI export starting…',\n  nCpus: 'Number of CPUs',\n  vmMemory: 'Memory',\n  diskInfo: 'Disk {position} ({capacity})',\n  diskDescription: 'Disk description',\n  noDisks: 'No disks.',\n  noNetworks: 'No networks.',\n  networkInfo: 'Network {name}',\n  noVmImportErrorDescription: 'No description available',\n  vmImportError: 'Error:',\n  vmImportFileType: '{type} file:',\n  vmImportConfigAlert: 'Please check and/or modify the VM configuration.',\n  toolsInstalled: 'The tools are installed',\n\n  // ---- Disk import ---\n  diskImportFailed: 'Disk import failed',\n  diskImportSuccess: 'Disk import success',\n  dropDisksFiles: 'Drop {types} files here to import disks.',\n  importToSr: 'To SR',\n  isoImportRequirement: 'To import ISO files, an ISO repository is required',\n\n  // ---- Tasks ---\n  poolTasks: 'Pool tasks',\n  xoTasks: 'XO tasks',\n  cancelTask: 'Cancel',\n  destroyTask: 'Destroy',\n  cancelTasks: 'Cancel selected tasks',\n  destroyTasks: 'Destroy selected tasks',\n  object: 'Object',\n  objects: 'Objects',\n  pool: 'Pool',\n  task: 'Task',\n  progress: 'Progress',\n  previousTasks: 'Previous tasks',\n  taskLastSeen: 'Last seen',\n\n  // ---- Backup views ---\n  backupSchedules: 'Schedules',\n  loneSnapshotsMessages: '{nLoneSnapshots} lone snapshot{nLoneSnapshots, plural, one {} other {s}} to delete!',\n  scheduleCron: 'Cron pattern',\n  scheduleLastRun: 'Click to display last run details',\n  scheduleName: 'Name',\n  scheduleCopyId: 'Copy ID {id}',\n  scheduleTimezone: 'Timezone',\n  scheduleExportRetention: 'Backup retention',\n  scheduleCopyRetention: 'Replication retention',\n  scheduleSnapshotRetention: 'Snapshot retention',\n  poolMetadataRetention: 'Pool retention',\n  xoMetadataRetention: 'XO retention',\n  getRemote: 'Get remote',\n  noBackups: 'There are no backups!',\n  restoreBackupsInfo: 'Click on a VM to display restore options',\n  remoteEnabled: 'Enabled',\n  remoteDisabled: 'Disabled',\n  enableRemote: 'Enable',\n  disableRemote: 'Disable',\n  remoteErrorMessage: 'The URL ({url}) is invalid (colon in path). Click this button to change the URL to {newUrl}.',\n  backupVmNameColumn: 'VM Name',\n  backupVmDescriptionColumn: 'VM Description',\n  firstBackupColumn: 'Oldest backup',\n  lastBackupColumn: 'Latest backup',\n  availableBackupsColumn: 'Available Backups',\n  backupRestoreErrorTitle: 'Missing parameters',\n  backupRestoreErrorMessage: 'Choose a SR and a backup',\n  backupisKey: 'key',\n  backupIsIncremental: 'incremental',\n  backupIsDifferencing: 'differencing',\n  vmsToBackup: 'VMs to backup',\n  refreshBackupList: 'Refresh backup list',\n  restoreVmBackups: 'Restore',\n  restoreVmBackupsTitle: 'Restore {vm}',\n  checkVmBackupsTitle: 'Restore health check {vm}',\n  restoreVmBackupsBulkTitle: 'Restore {nVms, number} VM{nVms, plural, one {} other {s}}',\n  restoreVmBackupsBulkMessage:\n    'Restore {nVms, number} VM{nVms, plural, one {} other {s}} from {nVms, plural, one {its} other {their}} {oldestOrLatest} backup.',\n  restoreMetadataBackupWarning:\n    'This operation will overwrite the host metadata. Only perform a metadata restore if it is a new server with nothing running on it.',\n  oldest: 'oldest',\n  latest: 'latest',\n  restoreVmBackupsStart: 'Start VM{nVms, plural, one {} other {s}} after restore',\n  restoreVmBackupsBulkErrorTitle: 'Multi-restore error',\n  restoreVmUseDifferentialRestore: 'Use differential restore',\n  restoreMetadataBackupTitle: 'Restore {item}',\n  bulkRestoreMetadataBackupTitle:\n    'Restore {nMetadataBackups, number} metadata backup{nMetadataBackups, plural, one {} other {s}}',\n  bulkRestoreMetadataBackupMessage:\n    'Restore {nMetadataBackups, number} metadata backup{nMetadataBackups, plural, one {} other {s}} from {nMetadataBackups, plural, one {its} other {their}} {oldestOrLatest} backup',\n  deleteMetadataBackupTitle: 'Delete {item} backup',\n  restoreVmBackupsBulkErrorMessage: 'You need to select a destination SR',\n  deleteVmBackups: 'Delete backups…',\n  deleteVmBackupsTitle: 'Delete {vm} backups',\n  deleteBackupsSelect: 'Select backups to delete:',\n  deleteVmBackupsSelectAll: 'All',\n  deleteVmBackupsBulkTitle: 'Delete backups',\n  deleteVmBackupsBulkMessage:\n    'Are you sure you want to delete all the backups from {nVms, number} VM{nVms, plural, one {} other {s}}?',\n  deleteVmBackupsBulkConfirmText: 'delete {nBackups} backup{nBackups, plural, one {} other {s}}',\n  bulkDeleteMetadataBackupsTitle: 'Delete metadata backups',\n  bulkDeleteMetadataBackupsMessage:\n    'Are you sure you want to delete all the backups from {nMetadataBackups, number} metadata backup{nMetadataBackups, plural, one {} other {s}}?',\n  bulkDeleteMetadataBackupsConfirmText:\n    'delete {nMetadataBackups} metadata backup{nMetadataBackups, plural, one {} other {s}}',\n  healthCheck: 'Health check',\n  healthCheckChooseSr: 'Choose SR used for VMs restoration',\n  healthCheckTagsInfo: 'If no tags are specified, all VMs in the backup will be tested.',\n  healthCheckAvailableEnterpriseUser: 'Only available to enterprise users',\n  remoteNotCompatibleWithSelectedProxy:\n    \"The backup will not be run on this remote because it's not compatible with the selected proxy\",\n  remoteLoadBackupsFailure: 'Loading backups failed',\n  remoteLoadBackupsFailureMessage: 'Failed to load backups from {name}.',\n  vmsTags: 'VMs tags',\n  tagNoBak: 'VMs with this tag will not be backed up {reason, select, null {} other {({reason})}}',\n  tagNoHealthCheck: 'VMs with this tag will not be tested by health check {reason, select, null {} other {({reason})}}',\n  tagNotifyOnSnapshot: 'An email will be sent when a VM with this tag is snapshotted',\n  nbdConnections: `NBD connections`,\n  speedLimitNoUnit: 'Speed limit',\n\n  // ----- Restore files view -----\n  restoreFiles: 'Restore backup files',\n  restoreFilesError: 'Invalid options',\n  restoreFilesExportFormat: 'Export format:',\n  restoreFilesFromBackup: 'Restore file from {name}',\n  restoreFilesSelectBackup: 'Select a backup…',\n  restoreFilesSelectDisk: 'Select a disk…',\n  restoreFilesSelectPartition: 'Select a partition…',\n  restoreFilesSelectFiles: 'Select a file…',\n  restoreFilesNoFilesSelected: 'No files selected',\n  restoreFilesSelectedFilesAndFolders: 'Selected files/folders ({files}):',\n  restoreFilesDiskError: 'Error while scanning disk',\n  restoreFilesSelectAllFiles: \"Select all this folder's files\",\n  restoreFilesSelectFolder: 'Select this folder',\n  restoreFilesTgz: 'tar+gzip (.tgz)',\n  restoreFilesUnselectAll: 'Unselect all files',\n  restoreFilesZip: 'ZIP (slow)',\n\n  // ----- Modals -----\n  bypassBackupHostModalMessage: 'There may be ongoing backups on the host. Are you sure you want to continue?',\n  bypassBackupPoolModalMessage: 'There may be ongoing backups on the pool. Are you sure you want to continue?',\n  bypassBlockedMigrationsModalTitle: 'Bypass blocked migrations',\n  bypassBlockedMigrationsModalMessage: 'This will allow migration on these VMs: {vms}',\n  emergencyShutdownHostModalTitle: 'Emergency shutdown Host',\n  emergencyShutdownHostModalMessage: 'Are you sure you want to shutdown {host}?',\n  emergencyShutdownHostsModalTitle: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}',\n  emergencyShutdownHostsModalMessage:\n    'Are you sure you want to shutdown {nHosts, number} Host{nHosts, plural, one {} other {s}}?',\n  stopHostModalTitle: 'Shutdown host',\n  stopHostModalMessage:\n    \"This will shutdown your host. Do you want to continue? If it's the pool master, your connection to the pool will be lost\",\n  forceStopHost: 'Force shutdown host',\n  forceStopHostMessage: 'This will shutdown your host without evacuating its VMs. Do you want to continue?',\n  addHostModalTitle: 'Add host',\n  addHostModalMessage: 'Are you sure you want to add {host} to {pool}?',\n  restartHostModalTitle: 'Restart host',\n  restartHostModalMessage: 'This will restart your host. Do you want to continue?',\n  restartHostsAgentsModalTitle:\n    'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',\n  restartHostsAgentsModalMessage:\n    'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',\n  restartHostsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}}',\n  restartHostsModalMessage: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}}?',\n  startVmsModalTitle: 'Start VM{vms, plural, one {} other {s}}',\n  cloneAndStartVM: 'Start a copy',\n  forceStartVm: 'Force start',\n  forceStartVmModalTitle: 'Forbidden operation',\n  blockedStartVmModalMessage: 'Start operation for this vm is blocked.',\n  blockedStartVmsModalMessage: 'Forbidden operation start for {nVms, number} vm{nVms, plural, one {} other {s}}.',\n  startVmsModalMessage: 'Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?',\n  failedVmsErrorMessage:\n    '{nVms, number} VM{nVms, plural, one {} other {s}} failed. Please check logs for more information',\n  failedVmsErrorTitle: 'Start failed',\n  failedDeleteErrorTitle: 'Delete failed',\n  stopHostsModalTitle: 'Stop Host{nHosts, plural, one {} other {s}}',\n  stopHostsModalMessage: 'Are you sure you want to stop {nHosts, number} Host{nHosts, plural, one {} other {s}}?',\n  stopVmsModalTitle: 'Stop VM{vms, plural, one {} other {s}}',\n  stopVmsModalMessage: 'Are you sure you want to stop {vms, number} VM{vms, plural, one {} other {s}}?',\n  restartVmModalTitle: 'Restart VM',\n  restartVmModalMessage: 'Are you sure you want to restart {name}?',\n  stopVmModalTitle: 'Stop VM',\n  stopVmModalMessage: 'Are you sure you want to stop {name}?',\n  blockedOperation: 'Blocked operation',\n  stopVmBlockedModalMessage: 'Stop operation for this VM is blocked. Would you like to stop it anyway?',\n  vmHasNoTools: 'No guest tools',\n  vmHasNoToolsMessage: \"The VM doesn't have Xen tools installed, which are required to properly stop or reboot it.\",\n  confirmForceShutdown: 'Would you like to force shutdown the VM?',\n  confirmForceReboot: 'Would you like to force reboot the VM?',\n  suspendVmsModalTitle: 'Suspend VM{vms, plural, one {} other {s}}',\n  suspendVmsModalMessage: 'Are you sure you want to suspend {vms, number} VM{vms, plural, one {} other {s}}?',\n  pauseVmsModalTitle: 'Pause VM{vms, plural, one {} other {s}}',\n  pauseVmsModalMessage: 'Are you sure you want to pause {vms, number} VM{vms, plural, one {} other {s}}?',\n  restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',\n  restartVmsModalMessage: 'Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?',\n  restartVmBlockedModalMessage: 'Restart operation for this VM is blocked. Would you like to restart it anyway?',\n  snapshotSaveMemory: 'save memory',\n  snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',\n  deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',\n  deleteVmsModalMessage:\n    'Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',\n  deleteVmsConfirmText: 'delete {nVms, number} vm{nVms, plural, one {} other {s}}',\n  deleteVmModalTitle: 'Delete VM',\n  deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',\n  deleteVmBlockedModalTitle: 'Blocked operation',\n  deleteVmBlockedModalMessage: 'Removing the VM is a blocked operation. Would you like to remove it anyway?',\n  forceVmMigrateModalTitle: 'Force migration',\n  forceVmMigrateModalMessage:\n    'The VM is incompatible with the CPU features of the destination host. Would you like to force it anyway?',\n  migrateVmModalTitle: 'Migrate VM',\n  migrateVmSelectHost: 'Select a destination host:',\n  migrateVmSelectMigrationNetwork: 'Select a migration network:',\n  migrateVmSelectNetworks: 'For each VIF, select a network:',\n  migrateVmsSelectSr: 'Select a destination SR:',\n  migrateVmsSelectSrIntraPool: 'Select a destination SR for local disks:',\n  migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',\n  migrateVmsSmartMapping: 'Smart mapping',\n  migrateVmVif: 'VIF',\n  migrateVmNetwork: 'Network',\n  migrateVmNoTargetHost: 'No target host',\n  migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',\n  migrateVmNoSr: 'SR required',\n  migrateVmNoSrMessage: 'A destination SR is required',\n  migrateVmNoDefaultSrError: 'No default SR',\n  migrateVmNotConnectedDefaultSrError: 'Default SR not connected to host',\n  chooseSrForEachVdisModalSelectSr: 'For each VDI, select an SR (optional)',\n  chooseSrForEachVdisModalMainSr: 'Select main SR…',\n  chooseSrForEachVdisModalVdiLabel: 'VDI',\n  chooseSrForEachVdisModalSrLabel: 'SR*',\n  deleteJobsModalTitle: 'Delete job{nJobs, plural, one {} other {s}}',\n  deleteJobsModalMessage: 'Are you sure you want to delete {nJobs, number} job{nJobs, plural, one {} other {s}}?',\n  deleteVbdsModalTitle: 'Delete VBD{nVbds, plural, one {} other {s}}',\n  deleteVbdsModalMessage: 'Are you sure you want to delete {nVbds, number} VBD{nVbds, plural, one {} other {s}}?',\n  deleteVdiModalTitle: 'Delete VDI',\n  deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',\n  deleteVdisModalTitle: 'Delete VDI{nVdis, plural, one {} other {s}}',\n  deleteVdisModalMessage:\n    'Are you sure you want to delete {nVdis, number} disk{nVdis, plural, one {} other {s}}? ALL DATA ON THESE DISKS WILL BE LOST',\n  deleteSchedulesModalTitle: 'Delete schedule{nSchedules, plural, one {} other {s}}',\n  deleteSchedulesModalMessage:\n    'Are you sure you want to delete {nSchedules, number} schedule{nSchedules, plural, one {} other {s}}?',\n  deleteRemotesModalTitle: 'Delete remote{nRemotes, plural, one {} other {s}}',\n  deleteRemotesModalMessage:\n    'Are you sure you want to delete {nRemotes, number} remote{nRemotes, plural, one {} other {s}}?',\n  revertVmModalTitle: 'Revert your VM',\n  shareVmInResourceSetModalTitle: 'Share your VM',\n  shareVmInResourceSetModalMessage:\n    'This VM will be shared with all the members of the self-service {self}. Are you sure?',\n  deleteVifsModalTitle: 'Delete VIF{nVifs, plural, one {} other {s}}',\n  deleteVifsModalMessage: 'Are you sure you want to delete {nVifs, number} VIF{nVifs, plural, one {} other {s}}?',\n  deleteSnapshotModalTitle: 'Delete snapshot',\n  deleteSnapshotModalMessage: 'Are you sure you want to delete this snapshot?',\n  deleteSnapshotsModalTitle: 'Delete snapshot{nVms, plural, one {} other {s}}',\n  deleteSnapshotsModalMessage:\n    'Are you sure you want to delete {nVms, number} snapshot{nVms, plural, one {} other {s}}?',\n  disconnectVbdsModalTitle: 'Disconnect VBD{nVbds, plural, one {} other {s}}',\n  disconnectVbdsModalMessage:\n    'Are you sure you want to disconnect {nVbds, number} VBD{nVbds, plural, one {} other {s}}?',\n  disableHost: 'Disable host',\n  disableHostModalMessage: 'Are you sure you want to disable {host}? This will prevent new VMs from starting.',\n  revertVmModalMessage:\n    'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.',\n  revertVmModalSnapshotBefore: 'Snapshot before',\n  importBackupModalSelectBackup: 'Select your backup…',\n  importBackupModalSelectSr: 'Select a destination SR…',\n  deleteOrphanedVdisModalTitle: 'Delete orphaned snapshot VDIs',\n  deleteOrphanedVdisModalMessage:\n    'Are you sure you want to delete {nVdis, number} orphaned snapshot VDI{nVdis, plural, one {} other {s}}?',\n  definitiveMessageModal: 'This operation is definitive.',\n  existingLunModalTitle: 'Previous LUN Usage',\n  existingLunModalText:\n    'This LUN has been previously used as storage by a XenServer host. All data will be lost if you choose to continue with the SR creation.',\n  alreadyRegisteredModal: 'Replace current registration?',\n  alreadyRegisteredModalText:\n    'Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?',\n  trialReadyModal: 'Ready for trial?',\n  trialReadyModalText:\n    'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!',\n  cancelTasksModalTitle: 'Cancel task{nTasks, plural, one {} other {s}}',\n  cancelTasksModalMessage: 'Are you sure you want to cancel {nTasks, number} task{nTasks, plural, one {} other {s}}?',\n  destroyTasksModalTitle: 'Destroy task{nTasks, plural, one {} other {s}}',\n  destroyTasksModalMessage: 'Are you sure you want to destroy {nTasks, number} task{nTasks, plural, one {} other {s}}?',\n  forgetHostFromSrModalTitle: 'Forget host',\n  forgetHostFromSrModalMessage:\n    'Are you sure you want to forget this host? This will disconnect the SR from the host by removing the link between them (PBD).',\n  forgetHostsFromSrModalTitle: 'Forget host{nPbds, plural, one {} other {s}}',\n  forgetHostsFromSrModalMessage:\n    'Are you sure you want to forget {nPbds, number} host{nPbds, plural, one {} other {s}}? This will disconnect the SR from these hosts by removing the links between the SR and the hosts (PBDs).',\n  forgetSrFromHostModalTitle: 'Forget SR',\n  forgetSrFromHostModalMessage:\n    'Are you sure you want to forget this SR? This will disconnect the SR from the host by removing the link between them (PBD).',\n  forgetSrsFromHostModalTitle: 'Forget SR{nPbds, plural, one {} other {s}}',\n  forgetSrsFromHostModalMessage:\n    'Are you sure you want to forget {nPbds, number} SR{nPbds, plural, one {} other {s}}? This will disconnect the SRs from the host by removing the links between the host and the SRs (PBDs).',\n  optionalEntry: '* optional',\n  vmWithDuplicatedMacAddressesMessage:\n    'This VM contains a duplicate MAC address or has the same MAC address as another running VM. Do you want to continue?',\n  vmsWithDuplicatedMacAddressesMessage:\n    '{nVms, number} VM{nVms, plural, one {} other {s}} contain{nVms, plural, one {s} other {}} duplicate MAC addresses or {nVms, plural, one {has} other {have}} the same MAC addresses as other running VMs. Do you want to continue?',\n  ignoreVdi: 'Ignore this VDI',\n  selectDestinationSr: 'Select a destination SR',\n\n  // ----- Servers -----\n  enableServerErrorTitle: 'Enable server',\n  enableServerErrorMessage: 'Unexpected response. Please check your server address.',\n  serverLabel: 'Label',\n  serverHost: 'Host',\n  serverUsername: 'Username',\n  serverPassword: 'Password',\n  serverReadOnly: 'Read Only',\n  serverUnauthorizedCertificates: 'Unauthorized Certificates',\n  serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates',\n  serverUnauthorizedCertificatesInfo:\n    \"Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured.\",\n  serverPlaceHolderUser: 'username',\n  serverPlaceHolderPassword: 'password',\n  serverPlaceHolderAddress: 'address[:port]',\n  serverPlaceHolderLabel: 'label',\n  serverConnect: 'Connect',\n  serverError: 'Error',\n  serverAddFailed: 'Adding server failed',\n  serverStatus: 'Status',\n  serverConnectionFailed: 'Connection failed. Click for more information.',\n  serverAuthFailed: 'Authentication error',\n  serverUnknownError: 'Unknown error',\n  serverSelfSignedCertError: 'Invalid self-signed certificate',\n  serverSelfSignedCertQuestion:\n    'Do you want to accept self-signed certificate for this server even though it would decrease security?',\n  serverEnable: 'Enable',\n  serverEnabled: 'Enabled',\n  serverDisabled: 'Disabled',\n  serverDisable: 'Disable server',\n  serverHttpProxy: ' HTTP proxy URL',\n  serverHttpProxyPlaceHolder: ' HTTP proxy URL',\n\n  // ----- Copy VM -----\n  copyVm: 'Copy VM',\n  copyVmName: 'Name',\n  copyVmNamePatternPlaceholder: 'e.g.: \"\\\\{name\\\\}_COPY\"',\n  copyVmSelectSr: 'Select SR',\n  copyVmsNoTargetSr: 'No target SR',\n  copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',\n  notSupportedZstdWarning: 'Zstd is not supported on {nVms, number} VM{nVms, plural, one {} other {s}}',\n  notSupportedZstdTooltip: 'Click to see the concerned VMs',\n  fastCloneMode: 'Fast clone',\n  fullCopyMode: 'Full copy',\n  copyTemplate: 'Copy template',\n\n  // ----- Detach host -----\n  detachHostModalTitle: 'Detach host',\n  detachHostModalMessage:\n    'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',\n  detachHost: 'Detach',\n\n  // ----- Advanced Live Telemetry -----\n  advancedLiveTelemetry: 'Advanced Live Telemetry',\n  pluginNetDataIsNecessary: 'Netdata plugin is necessary',\n  enableAdvancedLiveTelemetry: 'Enable Advanced Live Telemetry',\n  enableAdvancedLiveTelemetrySuccess: 'Advanced Live Telemetry successfully enabled',\n  xcpOnlyFeature: 'This feature is only XCP-ng compatible',\n\n  // ----- Forget host -----\n  forgetHostModalTitle: 'Forget host',\n  forgetHostModalMessage:\n    \"Are you sure you want to forget {host} from its pool? Be sure this host can't be back online, or use detach instead.\",\n  forgetHost: 'Forget',\n\n  // ----- Set pool master -----\n\n  setPoolMasterModalTitle: 'Designate a new master',\n  setPoolMasterModalMessage: 'This operation may take several minutes. Do you want to continue?',\n\n  // ----- Network -----\n  networkManagement: 'Management',\n  newNetworkCreate: 'Create network',\n  newNetworkInterface: 'Interface',\n  newNetworkName: 'Name',\n  newNetworkDescription: 'Description',\n  newNetworkVlan: 'VLAN',\n  newNetworkDefaultVlan: 'No VLAN if empty',\n  newNetworkMtu: 'MTU',\n  newNetworkDefaultMtu: 'Default: 1500',\n  newNetworkBondMode: 'Bond mode',\n  newNetworkInfo: 'Info',\n  newNetworkType: 'Type',\n  newNetworkPreferredCenter: 'Preferred center (optional)',\n  newNetworkEncapsulation: 'Encapsulation',\n  newNetworkEncrypted: 'Encrypted',\n  encryptionWarning: 'A pool can have 1 encrypted GRE network and 1 encrypted VxLAN network max',\n  preferredCenterTip: 'The host to try first to elect as center of the network',\n  newNetworkSdnControllerTip: 'Please see the requirements',\n  deleteNetwork: 'Delete network',\n  deleteNetworkConfirm: 'Are you sure you want to delete this network?',\n  networkInUse: 'This network is currently in use',\n  pillBonded: 'Bonded',\n  bondedNetwork: 'Bonded network',\n  privateNetwork: 'Private network',\n  addPool: 'Add pool',\n\n  // ----- Add host -----\n  hosts: 'Hosts',\n  addHostNoHost: 'No host',\n  addHostNoHostMessage: 'No host selected to be added',\n\n  // ----- About View -----\n  failedToFetchLatestMasterCommit: 'Failed to fetch latest master commit',\n  noProSupport: 'Professional support missing!',\n  productionUse: 'Want to use in production?',\n  getSupport: 'Get pro support with the Xen Orchestra Appliance at {website}',\n  bugTracker: 'Bug Tracker',\n  bugTrackerText: 'Issues? Report it!',\n  community: 'Community',\n  communityText: 'Join our community forum!',\n  freeTrial: 'Free Trial for Premium Edition!',\n  freeTrialNow: 'Request your trial now!',\n  issues: 'Any issue?',\n  issuesText: 'Problem? Contact us!',\n  documentation: 'Documentation',\n  documentationText: 'Read our official doc',\n  proSupportIncluded: 'Pro support included',\n  xoAccount: 'Access your XO Account',\n  openTicket: 'Report a problem',\n  openTicketText: 'Problem? Open a ticket!',\n  xoUpToDate: 'Your Xen Orchestra is up to date',\n  xoFromSourceNotUpToDate:\n    'You are not up to date with master. {nBehind} commit{nBehind, plural, one {} other {s}} behind {nAhead, plural, =0 {} other {and {nAhead, number} commit{nAhead, plural, one {} other {s}} ahead}}',\n\n  // ----- Upgrade Panel -----\n  upgradeNeeded: 'Upgrade needed',\n  upgradeNow: 'Upgrade now!',\n  or: 'Or',\n  tryIt: 'Try it for free!',\n  availableIn: 'This feature is available starting from {plan} Edition',\n  notAvailable: 'This feature is not available in your version, contact your administrator to know more.',\n\n  // ----- Updates View -----\n  registration: 'Registration',\n  settings: 'Settings',\n  proxySettings: 'Proxy settings',\n  proxySettingsHostPlaceHolder: 'Host (myproxy.example.org)',\n  proxySettingsPortPlaceHolder: 'Port (eg: 3128)',\n  proxySettingsUsernamePlaceHolder: 'Username',\n  proxySettingsPasswordPlaceHolder: 'Password',\n  updateRegistrationEmailPlaceHolder: 'Your email account',\n  updateRegistrationPasswordPlaceHolder: 'Your password',\n  updaterTroubleshootingLink: 'Troubleshooting documentation',\n  update: 'Update',\n  refresh: 'Refresh',\n  upgrade: 'Upgrade',\n  considerSubscribe:\n    'Please consider subscribing and trying it with all the features for free during 30 days on {link}.',\n  currentVersion: 'Current version:',\n  register: 'Register',\n  editRegistration: 'Edit registration',\n  trialRegistration: 'Please, take time to register in order to enjoy your trial.',\n  trialStartButton: 'Start trial',\n  trialAvailableUntil: 'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.',\n  trialConsumed: 'Your trial has been ended. Contact us or downgrade to Free version',\n  trialLocked: 'Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service.',\n  noUpdateInfo: 'No update information available',\n  waitingUpdateInfo: 'Update information may be available',\n  upToDate: 'Your XOA is up-to-date',\n  mustUpgrade: 'You need to update your XOA (new version is available)',\n  registerNeeded: 'Your XOA is not registered for updates',\n  updaterError: \"Can't fetch update information\",\n  promptUpgradeReloadTitle: 'Upgrade successful',\n  promptUpgradeReloadMessage:\n    'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',\n  upgradeWarningTitle: 'Upgrade warning',\n  upgradeWarningMessage:\n    'You have some backup jobs in progress. If you upgrade now, these jobs will be interrupted! Are you sure you want to continue?',\n  releaseChannels: 'Release channels',\n  unlistedChannel: 'unlisted channel',\n  unlistedChannelName: 'Unlisted channel name',\n  selectChannel: 'Select channel',\n  changeChannel: 'Change channel',\n  updaterCommunity: 'The Web updater, the release channels and the proxy settings are available in XOA.',\n  xoaBuild: 'XOA build:',\n\n  // ----- OS Disclaimer -----\n  disclaimerTitle: 'Xen Orchestra from the sources',\n  disclaimerText1: \"You are using XO from the sources! That's great for a personal/non-profit usage.\",\n  disclaimerText2: \"If you are a company, it's better to use it with our appliance + pro support included:\",\n  disclaimerText3: 'This version is not bundled with any support nor updates. Use it with caution.',\n  disclaimerText4: 'Why do I see this message?',\n  notRegisteredDisclaimerInfo: 'You are not registered. Your XOA may not be up to date.',\n  notRegisteredDisclaimerCreateAccount: 'Click here to create an account.',\n  notRegisteredDisclaimerRegister: 'Click here to register and update your XOA.',\n\n  // ----- PIF -----\n  connectPif: 'Connect PIF',\n  connectPifConfirm: 'Are you sure you want to connect this PIF?',\n  disconnectPif: 'Disconnect PIF',\n  disconnectPifConfirm: 'Are you sure you want to disconnect this PIF?',\n  deletePif: 'Delete PIF',\n  deletePifConfirm: 'Are you sure you want to delete this PIF?',\n  deletePifs: 'Delete PIFs',\n  deletePifsConfirm: 'Are you sure you want to delete {nPifs, number} PIF{nPifs, plural, one {} other {s}}?',\n  pifConnected: 'Connected',\n  pifDisconnected: 'Disconnected',\n  pifPhysicallyConnected: 'Physically connected',\n  pifPhysicallyDisconnected: 'Physically disconnected',\n\n  // ----- User -----\n  authToken: 'Token',\n  authTokens: 'Authentication tokens',\n  authTokenLastUse: 'Last use',\n  username: 'Username',\n  password: 'Password',\n  language: 'Language',\n  oldPasswordPlaceholder: 'Old password',\n  newPasswordPlaceholder: 'New password',\n  confirmPasswordPlaceholder: 'Confirm new password',\n  confirmationPasswordError: 'Confirmation password incorrect',\n  confirmationPasswordErrorBody: 'Password does not match the confirm password.',\n  pwdChangeSuccess: 'Password changed',\n  pwdChangeSuccessBody: 'Your password has been successfully changed.',\n  pwdChangeError: 'Incorrect password',\n  pwdChangeErrorBody: 'The old password provided is incorrect. Your password has not been changed.',\n  changePasswordOk: 'OK',\n  forgetTokens: 'Forget all authentication tokens',\n  forgetTokensExplained: 'This prevents authenticating with existing tokens but the one used by the current session',\n  forgetTokensSuccess: 'Successfully forgot authentication tokens',\n  forgetTokensError: 'Error while forgetting authentication tokens',\n  sshKeys: 'SSH keys',\n  newAuthToken: 'New token',\n  newSshKey: 'New SSH key',\n  deleteAuthTokens: 'Delete selected authentication tokens',\n  deleteSshKey: 'Delete',\n  deleteSshKeys: 'Delete selected SSH keys',\n  newAuthTokenModalTitle: 'New authentication token',\n  newSshKeyModalTitle: 'New SSH key',\n  sshKeyAlreadyExists: 'SSH key already exists!',\n  sshKeyErrorTitle: 'Invalid key',\n  sshKeyErrorMessage: 'An SSH key requires both a title and a key.',\n  title: 'Title',\n  key: 'Key',\n  deleteAuthTokenConfirm: 'Delete authentication token',\n  deleteAuthTokenConfirmMessage: 'Are you sure you want to delete the authentication token: {id}?',\n  deleteAuthTokensConfirm: 'Delete authentication token{nTokens, plural, one {} other {s}}',\n  deleteAuthTokensConfirmMessage:\n    'Are you sure you want to delete {nTokens, number} autentication token{nTokens, plural, one {} other {s}}?',\n  deleteSshKeyConfirm: 'Delete SSH key',\n  deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',\n  deleteSshKeysConfirm: 'Delete SSH key{nKeys, plural, one {} other {s}}',\n  deleteSshKeysConfirmMessage:\n    'Are you sure you want to delete {nKeys, number} SSH key{nKeys, plural, one {} other {s}}?',\n  addOtpConfirm: 'Add OTP authentication',\n  addOtpConfirmMessage:\n    'To enable OTP authentication, add it to your application and then enter the current password to validate.',\n  addOtpInvalidPassword: 'Password is invalid',\n  removeOtpConfirm: 'Remove OTP authentication',\n  removeOtpConfirmMessage: 'Are you sure you want to remove OTP authentication?',\n  OtpAuthentication: 'OTP authentication',\n  OtpCode: 'OTP code',\n\n  // ----- Usage -----\n  others: '{nOthers, number} other{nOthers, plural, one {} other {s}}',\n\n  // ----- Logs -----\n  logUser: 'User',\n  logMessage: 'Message',\n  logSuggestXcpNg: 'Use XCP-ng to get rid of restrictions',\n  logXapiError: 'This is a XenServer/XCP-ng error',\n  logError: 'Error',\n  logTitle: 'Logs',\n  logDisplayDetails: 'Display details',\n  logDownload: 'Download log',\n  logTime: 'Date',\n  logDelete: 'Delete log',\n  logsDelete: 'Delete logs',\n  logsJobId: 'Job ID',\n  logsJobName: 'Job name',\n  logsBackupTime: 'Backup time',\n  logsRestoreTime: 'Restore time',\n  copyLogToClipboard: 'Copy log to clipboard',\n  logsVmNotFound: 'VM not found!',\n  logsFailedRestoreError: 'Click to show error',\n  logsFailedRestoreTitle: 'Restore error',\n  logDeleteMultiple: 'Delete log{nLogs, plural, one {} other {s}}',\n  logDeleteMultipleMessage: 'Are you sure you want to delete {nLogs, number} log{nLogs, plural, one {} other {s}}?',\n  logIndicationToEnable: 'Click to enable',\n  logIndicationToDisable: 'Click to disable',\n  reportBug: 'Report a bug',\n  unhealthyVdiChainError: 'Job canceled to protect the VDI chain',\n  backupRestartVm: \"Restart VM's backup\",\n  backupForceRestartVm: \"Force restart VM's backup\",\n  backupRestartFailedVms: \"Restart failed VMs' backup\",\n  backupForceRestartFailedVms: \"Force restart failed VMs' backup\",\n  clickForMoreInformation: 'Click for more information',\n  goToThisJob: 'Click to go to this job',\n  goToCorrespondingLogs: 'Click to see corresponding logs',\n\n  // ----- IPs ------\n  ipPoolName: 'Name',\n  ipPoolIps: 'IPs',\n  ipPoolNetworks: 'Networks',\n  ipsNoIpPool: 'No IP pools',\n  ipsCreate: 'Create',\n  ipsVifs: 'VIFs',\n  ipsNotUsed: 'Not used',\n  ipPoolUnknownVif: 'unknown VIF',\n  ipPoolNameAlreadyExists: 'Name already exists',\n\n  // ----- Shortcuts -----\n  shortcutModalTitle: 'Keyboard shortcuts',\n  shortcut_XoApp: 'Global',\n  shortcut_XoApp_GO_TO_HOSTS: 'Go to hosts list',\n  shortcut_XoApp_GO_TO_POOLS: 'Go to pools list',\n  shortcut_XoApp_GO_TO_VMS: 'Go to VMs list',\n  shortcut_XoApp_GO_TO_SRS: 'Go to SRs list',\n  shortcut_XoApp_CREATE_VM: 'Create a new VM',\n  shortcut_XoApp_UNFOCUS: 'Unfocus field',\n  shortcut_XoApp_HELP: 'Show shortcuts key bindings',\n  shortcut_Home: 'Home',\n  shortcut_Home_SEARCH: 'Focus search bar',\n  shortcut_Home_NAV_DOWN: 'Next item',\n  shortcut_Home_NAV_UP: 'Previous item',\n  shortcut_Home_SELECT: 'Select item',\n  shortcut_Home_JUMP_INTO: 'Open',\n  shortcut_SortedTable: 'Supported tables',\n  shortcut_SortedTable_SEARCH: 'Focus the table search bar',\n  shortcut_SortedTable_NAV_DOWN: 'Next item',\n  shortcut_SortedTable_NAV_UP: 'Previous item',\n  shortcut_SortedTable_SELECT: 'Select item',\n  shortcut_SortedTable_ROW_ACTION: 'Action',\n\n  // ----- Settings/ACLs -----\n  settingsAclsButtonTooltipVM: 'VM',\n  settingsAclsButtonTooltiphost: 'Hosts',\n  settingsAclsButtonTooltippool: 'Pool',\n  settingsAclsButtonTooltipSR: 'SR',\n  settingsAclsButtonTooltipnetwork: 'Network',\n\n  // ----- Settings/Cloud configs -----\n  settingsCloudConfigTemplate: 'Template',\n  confirmDeleteCloudConfigsTitle: 'Delete cloud config{nCloudConfigs, plural, one {} other {s}}',\n  confirmDeleteCloudConfigsBody:\n    'Are you sure you want to delete {nCloudConfigs, number} cloud config{nCloudConfigs, plural, one {} other {s}}?',\n  confirmDeleteNetworkConfigsTitle: 'Delete network config{nNetworkConfigs, plural, one {} other {s}}',\n  confirmDeleteNetworkConfigsBody:\n    'Are you sure you want to delete {nNetworkConfigs, number} network config{nNetworkConfigs, plural, one {} other {s}}?',\n  deleteCloudConfig: 'Delete cloud config',\n  editCloudConfig: 'Edit cloud config',\n  deleteSelectedCloudConfigs: 'Delete selected cloud configs',\n  networkConfig: 'Network config',\n  cloudConfig: 'Cloud config',\n\n  // ----- Config -----\n  noConfigFile: 'No config file selected',\n  importTip: 'Try dropping a config file here or click to select a config file to upload.',\n  config: 'Config',\n  importConfig: 'Import',\n  importConfigEnterPassphrase: 'If the config is encrypted, please enter the passphrase:',\n  importConfigSuccess: 'Config file successfully imported',\n  importConfigError: 'Error while importing config file',\n  exportConfig: 'Export',\n  exportConfigEnterPassphrase: 'If you want to encrypt the exported config, please enter a passphrase:',\n  downloadConfig: 'Download current config',\n\n  // ----- SR -----\n  andNMore: 'and {n} more',\n  disabledVdiMigrateTooltip: \"Snapshots and base copies can't be migrated individually\",\n  srReconnectAllModalTitle: 'Reconnect all hosts',\n  srReconnectAllModalMessage: 'This will reconnect this SR to all its hosts.',\n  srDisconnectAllModalTitle: 'Disconnect all hosts',\n  srDisconnectAllModalMessage: 'This will disconnect this SR from all its hosts.',\n  srsDisconnectAllModalMessage:\n    'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).',\n  forgetNSrsModalMessage: 'Are you sure you want to forget {nSrs, number} SR{nSrs, plural, one {} other{s}}?',\n  srForgetModalWarning:\n    'You will lose all the metadata, meaning all the links between the VDIs (disks) and their respective VMs. This operation cannot be undone.',\n  srAllDisconnected: 'Disconnected',\n  srSomeConnected: 'Partially connected',\n  srAllConnected: 'Connected',\n  maintenanceSrModalBody:\n    'In order to put this SR in maintenance mode, the following VM{n, plural, one {} other {s}} will be shut down. Are you sure you want to continue?',\n  maintenanceMode: 'Maintenance mode',\n  migrateSelectedVdis: 'Migrate selected VDIs',\n  migrateVdiMessage:\n    'All the VDIs attached to a VM must either be on a shared SR or on the same host (local SR) for the VM to be able to start.',\n\n  // ----- XO cloud config -----\n  backedUpXoConfigs: 'Backed up XO Configs',\n  manageXoConfigCloudBackup: 'Manage XO Config Cloud Backup',\n  selectXoConfig: 'Select XO config',\n  xoConfigCloudBackup: 'XO Config Cloud Backup',\n  xoConfigCloudBackupTips:\n    'Your encrypted configuration is securely stored inside your Vates account and backed up once a day',\n  xoCloudConfigEnterPassphrase: 'Passphrase is required to encrypt backups',\n  xoCloudConfigRestoreEnterPassphrase: 'Enter the passphrase:',\n\n  // ----- XOSAN -----\n  xosanTitle: 'XOSAN',\n  xosanSuggestions: 'Suggestions',\n  xosanDisperseWarning: 'Warning: using disperse layout is not recommended right now. Please read {link}.',\n  xosanName: 'Name',\n  xosanHost: 'Host',\n  xosanHosts: 'Connected Hosts',\n  xosanPool: 'Pool',\n  xosanSize: 'Size',\n  xosanUsedSpace: 'Used space',\n  license: 'License',\n  xosanMultipleLicenses: 'This XOSAN has more than 1 license!',\n  xosanNeedPack: 'XOSAN pack needs to be installed and up to date on each host of the pool.',\n  xosanInstallIt: 'Install it now!',\n  xosanNeedRestart: 'Some hosts need their toolstack to be restarted before you can create an XOSAN',\n  xosanRestartAgents: 'Restart toolstacks',\n  xosanSrOnSameHostMessage: 'Select no more than 1 SR per host',\n  xosanLayout: 'Layout',\n  xosanRedundancy: 'Redundancy',\n  xosanCapacity: 'Capacity',\n  xosanAvailableSpace: 'Available space',\n  xosanDiskLossLegend: '* Can fail without data loss',\n  xosanCreate: 'Create',\n  xosanCommunity: 'XOSAN is available in XOA',\n  xosanNew: 'New',\n  xosanAdvanced: 'Advanced',\n  xosanRemoveSubvolumes: 'Remove subvolumes',\n  xosanAddSubvolume: 'Add subvolume…',\n  xosanWarning:\n    \"This version of XOSAN SR is from the first beta phase. You can keep using it, but to modify it you'll have to save your disks and re-create it.\",\n  xosanVlan: 'VLAN',\n  xosanNoSrs: 'No XOSAN found',\n  xosanPbdsDetached: 'Some SRs are detached from the XOSAN',\n  xosanBadStatus: 'Something is wrong with: {badStatuses}',\n  xosanRunning: 'Running',\n  xosanUpdatePacks: 'Update packs',\n  xosanPackUpdateChecking: 'Checking for updates',\n  xosanPackUpdateError:\n    'Error while checking XOSAN packs. Please make sure that the Cloud plugin is installed and loaded, and that the updater is reachable.',\n  xosanPackUpdateUnavailable: 'XOSAN resources are unavailable',\n  xosanPackUpdateUnregistered: 'Not registered for XOSAN resources',\n  xosanPackUpdateUpToDate: \"✓ This pool's XOSAN packs are up to date!\",\n  xosanPackUpdateVersion: 'Update pool with latest pack v{version}',\n  xosanDelete: 'Delete XOSAN',\n  xosanFixIssue: 'Fix',\n  xosanCreatingOn: 'Creating XOSAN on {pool}',\n  xosanState_configuringNetwork: 'Configuring network…',\n  xosanState_importingVm: 'Importing VM…',\n  xosanState_copyingVms: 'Copying VMs…',\n  xosanState_configuringVms: 'Configuring VMs…',\n  xosanState_configuringGluster: 'Configuring gluster…',\n  xosanState_creatingSr: 'Creating SR…',\n  xosanState_scanningSr: 'Scanning SR…',\n  xosanXcpngWarning:\n    'XOSAN cannot be installed on XCP-ng yet. Incoming XOSANv2 will be compatible with XCP-ng: {link}.',\n  // Pack download modal\n  xosanInstallCloudPlugin: 'Install XOA plugin first',\n  xosanLoadCloudPlugin: 'Load XOA plugin first',\n  xosanNoPackFound: 'No compatible XOSAN pack found for your XenServer versions.',\n  // SR tab XOSAN\n  xosanVmsNotRunning: 'Some XOSAN Virtual Machines are not running',\n  xosanVmsNotFound: 'Some XOSAN Virtual Machines could not be found',\n  xosanFilesNeedingHealing: 'Files needing healing',\n  xosanFilesNeedHealing: 'Some XOSAN Virtual Machines have files needing healing',\n  xosanHostNotInNetwork: 'Host {hostName} is not in XOSAN network',\n  xosanVm: 'VM controller',\n  xosanUnderlyingStorage: 'SR',\n  xosanReplace: 'Replace…',\n  xosanOnSameVm: 'On same VM',\n  xosanBrickName: 'Brick name',\n  xosanBrickUuid: 'Brick UUID',\n  xosanBrickSize: 'Brick size',\n  xosanMemorySize: 'Memory size',\n  xosanStatus: 'Status',\n  xosanArbiter: 'Arbiter',\n  xosanUsedInodes: 'Used Inodes',\n  xosanBlockSize: 'Block size',\n  xosanDevice: 'Device',\n  xosanFsName: 'FS name',\n  xosanMountOptions: 'Mount options',\n  xosanPath: 'Path',\n  xosanJob: 'Job',\n  xosanPid: 'PID',\n  xosanPort: 'Port',\n  xosanReplaceBrickErrorTitle: 'Missing values',\n  xosanReplaceBrickErrorMessage: 'You need to select a SR and a size',\n  xosanAddSubvolumeErrorTitle: 'Bad values',\n  xosanAddSubvolumeErrorMessage: 'You need to select {nSrs, number} and a size',\n  xosanSelectNSrs: 'Select {nSrs, number} SRs',\n  xosanRun: 'Run',\n  xosanRemove: 'Remove',\n  xosanVolume: 'Volume',\n  xosanVolumeOptions: 'Volume options',\n  xosanCouldNotFindVm: 'Could not find VM',\n  xosanUnderlyingStorageUsage: 'Using {usage}',\n  xosanCustomIpNetwork: 'Custom IP network (/24)',\n  xosanIssueHostNotInNetwork: 'Will configure the host xosan network device with a static IP address and plug it in.',\n  // ----- XOSTOR -----\n  approximateSrCapacity: 'Approximate SR capacity',\n  byDefaultManagementNetworkUsed: 'By default, the management network will be used',\n  cantFetchDisksFromNonXcpngHost: 'Unable to fetch physical disks from non-XCP-ng host',\n  createInterface: 'Create interface',\n  createXostoreConfirm:\n    'If packages need to be installed, the toolstack on those hosts will restart. Do you want to continue?',\n  diskAlreadyMounted: 'The disk is mounted on: {mountpoint}',\n  diskful: 'Diskful',\n  diskHasExistingPartition: 'The disk has existing partition',\n  diskIncompatibleXostor: 'Disk incompatible with XOSTOR',\n  diskIsReadOnly: 'The disk is Read-Only',\n  diskless: 'Diskless',\n  disks: 'Disks',\n  fieldRequired: '{field} is required',\n  fieldsMissing: 'Some fields are missing',\n  hostBoundToMultipleXostorLicenses: 'More than 1 XOSTOR license on {host}',\n  hostHasNoXostorLicense: 'No XOSTOR license on {host}',\n  hostsNotSameNumberOfDisks: 'Hosts do not have the same number of disks',\n  ignoreFileSystems: 'Ignore file systems',\n  ignoreFileSystemsInfo: 'Force LINSTOR group creation on existing filesystem',\n  interfaceName: 'Interface name',\n  interfaceNameRequired: 'Interface name is required if a network is provided',\n  interfaceNameReserved: 'This interface name is reserved',\n  isTapdevDisk: 'This is \"tapdev\" disk',\n  licenseBoundUnknownXostor: 'License attached to an unknown XOSTOR',\n  licenseNotBoundXostor: 'No XOSTOR attached',\n  licenseExpiredXostorWarning:\n    'License{nLicenseIds, plural, one {} other {s}} {licenseIds} ha{nLicenseIds, plural, one {s} other {ve}} expired on {host}',\n  manageXostorWarning: 'To manage this XOSTOR storage, you must resolve the following issues:',\n  networkNoPifs: 'The network does not have PIFs',\n  networks: 'Networks',\n  notXcpPool: 'Not an XCP-ng pool',\n  noXostorFound: 'No XOSTOR found',\n  numberOfHosts: 'Number of hosts',\n  objectDoesNotMeetXostorRequirements: '{object} does not meet XOSTOR requirements. Refer to the documentation.',\n  onlyShowXostorRequirements: 'Only show {type} that meet XOSTOR requirements',\n  poolAlreadyHasXostor: 'Pool already has a XOSTOR',\n  poolNotRecentEnough: 'Not recent enough. Current version: {version}',\n  pifsNoIp: 'Not all PIFs have an IP',\n  pifsNotAttached: 'Not all PIFs are attached',\n  pifsNotStatic: 'Not all PIFs are static',\n  replication: 'Replication',\n  replicationCountHigherThanHostsWithDisks: 'Replication count is higher than number of hosts with disks',\n  resourceList: 'Resource list',\n  rpuRequireVmsReboot: 'To fully apply the patches, some VMs will reboot. Are you sure you want to continue?',\n  selectDisks: 'Select disk(s)…',\n  selectedDiskTypeIncompatibleXostor: 'Only disks of type \"Disk\" and \"Raid\" are accepted. Selected disk type: {type}.',\n  setAsPreferred: 'Set as preferred',\n  storage: 'Storage',\n  summary: 'Summary',\n  tieBreaker: 'Tie breaker',\n  whiteSpaceNotAllowed: 'White space not allowed',\n  wrongNumberOfHosts: 'Wrong number of hosts',\n  xostor: 'XOSTOR',\n  xostorAvailableInXoa: 'XOSTOR is available in XOA',\n  xostorCreation: 'XOSTOR creation',\n  xostorDiskRequired: 'At least one disk is required',\n  xostorDisksDropdownLabel: '({nDisks, number} disk{nDisks, plural, one {} other {s}}) {hostname}',\n  xostorPackagesWillBeInstalled: '\"xcp-ng-release-linstor\" and \"xcp-ng-linstor\" will be installed on each host',\n  xostorReplicationWarning: 'If a disk dies, you will lose data',\n\n  // Hub\n  hubPage: 'Hub',\n  hubCommunity: 'Hub is available in XOA',\n  noDefaultSr: 'The selected pool has no default SR',\n  successfulInstall: 'VM installed successfully',\n  vmNoAvailable: 'No VMs available ',\n  create: 'Create',\n  hubResourceAlert: 'Resource alert',\n  os: 'OS',\n  version: 'Version',\n  size: 'Size',\n  totalDiskSize: 'Total disk size',\n  hideInstalledPool: 'Already installed templates are hidden',\n  hubImportNotificationTitle: 'XVA import',\n  hubTemplateDescriptionNotAvailable: 'No description available for this template',\n  recipeCreatedSuccessfully: 'Recipe created successfully',\n  recipeViewCreatedVms: 'View created VMs',\n  templatesLabel: 'Templates',\n  recipesLabel: 'Recipes',\n  network: 'Network',\n  recipeSelectK8sVersion: 'Select Kubernetes version',\n  recipeClusterNameLabel: 'Cluster name',\n  recipeNumberOfNodesLabel: 'Number of worker nodes',\n  recipeSshKeyLabel: 'SSH key',\n  recipeStaticIpAddresses: 'Static IP addresses',\n  recipeFaultTolerance: 'Control plane fault tolerance',\n  recipeNoneFaultTolerance: 'No fault tolerance (one control plane)',\n  recipeOneFaultTolerance: 'One fault tolerance (three control planes)',\n  recipeTwoFaultTolerance: 'Two fault tolerances (five control planes)',\n  recipeThreeFaultTolerance: 'Three fault tolerances (seven control planes)',\n  recipeHaControPlaneIpAddress: 'Control plane { i, number } node IP address/subnet mask',\n  recipeVip: 'VIP address',\n  recipeControlPlaneIpAddress: 'Control plane node IP address/subnet mask',\n  recipeWorkerIpAddress: 'Worker node { i, number } IP address/subnet mask',\n  recipeGatewayIpAddress: 'Gateway IP address',\n  recipeNameserverAddresses: 'Nameserver IP addresses',\n  recipeNameserverAddressesExample: '192.168.1.0,172.16.1.0',\n  recipeSearches: 'Search domains',\n  recipeSearchesExample: 'domain.com,search.org',\n\n  // Audit\n  auditActionEvent: 'Action/Event',\n  auditAlteredRecord: 'The record ({ id }) was altered ({ n, number } valid records)',\n  auditCheckIntegrity: 'Check integrity',\n  auditCopyFingerprintToClipboard: 'Copy fingerprint to clipboard',\n  auditGenerateNewFingerprint: 'Generate a new fingerprint',\n  auditMissingRecord: 'The record ({ id }) is missing ({ n, number } valid records)',\n  auditEnterFingerprint: 'Fingerprint',\n  auditEnterFingerprintInfo:\n    \"Enter the saved fingerprint to check the previous logs' integrity. If you don't have any, click OK.\",\n  auditRecord: 'Audit record',\n  auditIntegrityVerified: 'Integrity verified',\n  auditSaveFingerprintInfo: 'Keep this fingerprint to be able to check the integrity of the current records later.',\n  auditSaveFingerprintInErrorInfo:\n    'However, if you trust the current state of the records, keep this fingerprint to be able to check their integrity later.',\n  auditNewFingerprint: 'New fingerprint',\n  downloadAuditRecords: 'Download records',\n  displayAuditRecord: 'Display record',\n  noAuditRecordAvailable: 'No audit record available',\n  refreshAuditRecordsList: 'Refresh records list',\n  auditInactiveUserActionsRecord: 'User actions recording is currently inactive',\n  importAuditRecords: 'Import records',\n  importRecordsTip: 'Try dropping a .ndjson.gz or .ndjson file here or click to select a file.',\n  importAuditRecordsCleanList: 'Reset',\n  importAuditRecordsButton: 'Import',\n  importAuditRecordsTooltip: 'Import audit record from another XOA. Audit log database must be empty.',\n  importAuditRecordsSuccess: 'Audit records successfully imported',\n  importAuditRecordsSuccessWithProblems:\n    \"Audit records successfully imported, but {nInvalidRecords} invalid records were imported and at least {nMissingRecords} records were missing. Logs prior to the missing entries will not appear. The oldest visible log's ID is {lastLogId}\",\n  importAuditRecordsError: 'Error while importing audit records: {importError}',\n  noAuditRecordsFile: 'No audit records file selected',\n\n  // Licenses\n  allHostsMustBeBound: 'All hosts must be bound to a license',\n  boundSelectLicense: 'Bound (Plan (ID), expiration date, host - pool)',\n  bindXcpngLicenses: 'Bind XCP-ng licenses',\n  confirmBindingOnUnsupportedHost:\n    'You are about to bind {nLicenses, number} professional support license{nLicenses, plural, one {} other {s}} on older and unsupported XCP-ng version{nLicenses, plural, one {} other {s}}. Are you sure you want to continue?',\n  confirmRebindLicenseFromFullySupportedPool: 'The following pools will no longer be fully supported',\n  licenses: 'Licenses',\n  licensesBinding: 'Licenses binding',\n  notEnoughXcpngLicenses: 'Not enough XCP-ng licenses',\n  notBoundSelectLicense: 'Not bound (Plan (ID), expiration date)',\n  xcpngLicensesBindingAvancedView: \"To bind an XCP-ng license, go to the pool's Advanced tab.\",\n  xosanUnregisteredDisclaimer:\n    'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',\n  xosanSourcesDisclaimer:\n    'In order to create a XOSAN SR, you need to use the Xen Orchestra Appliance and buy a XOSAN license on {link}.',\n  registerNow: 'Register now!',\n  licensesUnregisteredDisclaimer: 'You need to register your appliance to manage your licenses.',\n  licenseProduct: 'Product',\n  licensePurchaser: 'Purchaser',\n  licenseExpires: 'Expires',\n  licensePurchaserYou: 'You',\n  productSupport: 'Support',\n  licenseNotBoundXosan: 'No XOSAN attached',\n  licenseNotBoundProxy: 'No proxy attached',\n  licenseBoundUnknownXosan: 'License attached to an unknown XOSAN',\n  licenseBoundUnknownProxy: 'License attached to an unknown proxy',\n  licensesManage: 'Manage the licenses',\n  newLicense: 'New license',\n  refreshLicenses: 'Refresh',\n  xosanLicenseRestricted: 'Limited size because XOSAN is in trial',\n  xosanAdminNoLicenseDisclaimer: 'You need a license on this SR to manage the XOSAN.',\n  xosanAdminExpiredLicenseDisclaimer:\n    'Your XOSAN license has expired. You can still use the SR but cannot administrate it anymore.',\n  xosanCheckLicenseError: 'Could not check the license on this XOSAN SR',\n  getLicensesError: 'Could not fetch licenses',\n  licenseHasExpired: 'License has expired.',\n  licenseBoundToOtherXoa: 'License bound to another XOA',\n  licenseBoundToThisXoa: 'This license is active on this XOA',\n  licenseExpiresDate: 'License expires on {date}.',\n  updateLicenseMessage: 'Update the license now!',\n  xosanUnknownSr: 'Unknown XOSAN SR.',\n  contactUs: 'Contact us!',\n  xosanNoLicense: 'No license.',\n  unlockNow: 'Unlock now!',\n  selectLicense: 'Select a license',\n  bindLicense: 'Bind license',\n  bindLicenses: 'Bind licenses',\n  expiresOn: 'expires on {date}',\n  xosanInstallXoaPlugin: 'Install XOA plugin first',\n  xosanLoadXoaPlugin: 'Load XOA plugin first',\n  bindXoaLicense: 'Activate license',\n  rebindXoaLicense: 'Move license to this XOA',\n  bindXoaLicenseConfirm: 'Are you sure you want to activate this license on your XOA? This action is not reversible!',\n  bindXoaLicenseConfirmText: 'activate {licenseType} license',\n  updateNeeded: 'Update needed',\n  starterLicense: 'Starter license',\n  enterpriseLicense: 'Enterprise license',\n  premiumLicense: 'Premium license',\n  trialLicenseInfo: 'You are currently in a {edition} trial period that will end on {date, date, medium}',\n  licenseNearlyExpired:\n    'Your current Xen Orchestra license is about to expire (less than {duration} to {date, date, medium}). Please reach out to your vendor.',\n  licenseExpired:\n    'Your current Xen Orchestra license has expired ({date, date, medium}). Please reach out to your vendor.',\n  proxyMultipleLicenses: 'This proxy has more than 1 license!',\n  proxyUnknownVm: 'Unknown proxy VM.',\n  xostorProSupportEnabled: 'XOSTOR Pro Support enabled',\n\n  // ----- plan -----\n  onlyAvailableToEnterprise: 'Only available to Enterprise users',\n\n  // ----- proxies -----\n  forgetProxyApplianceTitle: 'Forget prox{n, plural, one {y} other {ies}}',\n  forgetProxyApplianceMessage: 'Are you sure you want to forget {n, number} prox{n, plural, one {y} other {ies}}?',\n  forgetProxies: 'Forget proxy(ies)',\n  destroyProxyApplianceTitle: 'Destroy prox{n, plural, one {y} other {ies}}',\n  destroyProxyApplianceMessage: 'Are you sure you want to destroy {n, number} prox{n, plural, one {y} other {ies}}?',\n  destroyProxies: 'Destroy proxy(ies)',\n  deployProxy: 'Deploy a proxy',\n  redeployProxy: 'Redeploy proxy',\n  redeployProxyAction: 'Redeploy this proxy',\n  redeployProxyWarning: 'This action will destroy the old proxy VM',\n  registerProxy: 'Register a proxy',\n  noProxiesAvailable: 'No proxies available',\n  checkProxyHealth: 'Test your proxy',\n  updateProxyApplianceSettings: 'Update appliance settings',\n  urlNotFound: 'URL not found',\n  proxyAuthToken: 'Authentication token',\n  proxyConnectionFailedAfterRegistrationMessage: 'Unable to connect to this proxy. Do you want to forget it?',\n  proxyCopyUrl: 'Copy proxy URL',\n  proxyError: 'Proxy error',\n  proxyOptionalVmUuid: 'VM UUID is optional but recommended.',\n  proxyTestSuccess: 'Test passed for {name}',\n  proxyTestSuccessMessage: 'The proxy appears to work correctly',\n  proxyTestFailed: 'Test failed for {name}',\n  proxyTestFailedConnectionIssueMessage: 'Unable to connect to this proxy',\n  proxyLinkedRemotes: 'Click to see linked remotes',\n  proxyLinkedBackups: 'Click to see linked backups',\n  proxyNetworkDnsPlaceHolder: 'Default to: {dns}',\n  proxyNetworkNetmaskPlaceHolder: 'Default to: {netmask}',\n  proxySrPredicateInfo: 'The select only contains SRs connected to at least one HVM-capable host',\n  httpProxy: 'HTTP proxy',\n  httpProxyPlaceholder: 'protocol://username:password@address:port',\n  proxyUpgradesError: 'Unable to check upgrades availability',\n  proxyApplianceSettingsInfo: 'Leave the field empty and click on OK to remove the existing configuration',\n  proxyUpToDate: 'Your proxy is up-to-date',\n  proxyRunningBackupsMessage:\n    'The upgrade will interrupt {nJobs, number} running backup job{nJobs, plural, one {} other {s}}. Do you want to continue?',\n  proxiesNeedUpgrade: 'Some proxies need to be upgraded.',\n  upgradeNeededForProxies: 'Some proxies need to be upgraded. Click here to get more information.',\n  xoProxyConcreteGuide: 'XO Proxy: a concrete guide',\n  someProxiesHaveErrors:\n    '{n, number} prox{n, plural, one {y} other {ies}} ha{n, plural, one {s} other {ve}} error{n, plural, one {} other {s}}',\n\n  // ----- Utils -----\n  secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',\n  durationFormat:\n    '{days, plural, =0 {} one {# day } other {# days }}{hours, plural, =0 {} one {# hour } other {# hours }}{minutes, plural, =0 {} one {# minute } other {# minutes }}{seconds, plural, =0 {} one {# second} other {# seconds}}',\n\n  // ----- IPMI -----\n  currentBiosVersion: 'Current BIOS version: {version}',\n  downloadBiosUpdate: 'Download BIOS update ({version})',\n  highestCpuTemperature: '{n, number}x CPU{n, plural, one {} other {s}} (highest: {degres})',\n  highestFanSpeed: '{n, number}x fan{n, plural, one {} other {s}} (highest: {speed})',\n  inletTemperature: 'Inlet temperature',\n  ipmi: 'IPMI',\n  nFanStatus: '{n, number}x fan{n, plural, one {} other {s}} status: {status}',\n  nPsuStatus: '{n, number}x PSU{n, plural, one {} other {s}} status: {status}',\n  outletTemperature: 'Outlet temperature',\n  totalPower: 'Total power',\n}\nforEach(messages, function (message, id) {\n  if (typeof message === 'string') {\n    messages[id] = {\n      id,\n      defaultMessage: message,\n    }\n  } else if (!message.id) {\n    message.id = id\n  }\n})\n\nmodule.exports = messages\n","// Invoke a function and returns it result.\n// All parameters are forwarded.\n//\n// Why using `invoke()`?\n// - avoid tedious IIFE syntax\n// - avoid declaring variables in the common scope\n// - monkey-patching\n//\n// ```js\n// const sum = invoke(1, 2, (a, b) => a + b)\n//\n// eventEmitter.emit = invoke(eventEmitter.emit, emit => function (event) {\n//   if (event === 'foo') {\n//     throw new Error('event foo is disabled')\n//   }\n//\n//   return emit.apply(this, arguments)\n// })\n// ```\nexport default function invoke(fn) {\n  const n = arguments.length - 1\n  if (!n) {\n    return fn()\n  }\n\n  fn = arguments[n]\n  const args = new Array(n)\n  for (let i = 0; i < n; ++i) {\n    args[i] = arguments[i]\n  }\n\n  return fn.apply(undefined, args)\n}\n","import forEachRight from 'lodash/forEachRight'\nimport forEach from 'lodash/forEach'\nimport isIp from 'is-ip'\nimport some from 'lodash/some'\n\nexport { isIp }\nexport const isIpV4 = isIp.v4\nexport const isIpV6 = isIp.v6\n\n// Source: https://github.com/ezpaarse-project/ip-range-generator/blob/master/index.js\n\nconst ipv4 = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\\.(?!$)|$)){4}$/\n\nfunction ip2hex(ip) {\n  const parts = ip.split('.').map(str => parseInt(str, 10))\n  let n = 0\n\n  n += parts[3]\n  n += parts[2] * 256 // 2^8\n  n += parts[1] * 65536 // 2^16\n  n += parts[0] * 16777216 // 2^24\n\n  return n\n}\n\nfunction assertIpv4(str, msg) {\n  if (!ipv4.test(str)) {\n    throw new Error(msg)\n  }\n}\n\nfunction* range(ip1, ip2) {\n  assertIpv4(ip1, 'argument \"ip1\" must be a valid IPv4 address')\n  assertIpv4(ip2, 'argument \"ip2\" must be a valid IPv4 address')\n\n  let hex = ip2hex(ip1)\n  let hex2 = ip2hex(ip2)\n\n  if (hex > hex2) {\n    const tmp = hex\n    hex = hex2\n    hex2 = tmp\n  }\n\n  for (let i = hex; i <= hex2; i++) {\n    yield `${(i >> 24) & 0xff}.${(i >> 16) & 0xff}.${(i >> 8) & 0xff}.${i & 0xff}`\n  }\n}\n\n// -----------------------------------------------------------------------------\n\nexport const getNextIpV4 = ip => {\n  const splitIp = ip.split('.')\n  if (splitIp.length !== 4 || some(splitIp, value => value < 0 || value > 255)) {\n    return\n  }\n  let index\n  forEachRight(splitIp, (value, i) => {\n    if (value < 255) {\n      index = i\n      return false\n    }\n    splitIp[i] = 0\n  })\n  if (index === 0 && +splitIp[0] === 255) {\n    return 0\n  }\n  splitIp[index]++\n\n  return splitIp.join('.')\n}\n\nexport const formatIps = ips => {\n  if (!Array.isArray(ips)) {\n    throw new Error('ips must be an array')\n  }\n  if (ips.length === 0) {\n    return []\n  }\n  const sortedIps = ips.sort((ip1, ip2) => {\n    const splitIp1 = ip1.split('.')\n    const splitIp2 = ip2.split('.')\n    if (splitIp1.length !== 4) {\n      return 1\n    }\n    if (splitIp2.length !== 4) {\n      return -1\n    }\n    return (\n      splitIp1[3] -\n      splitIp2[3] +\n      (splitIp1[2] - splitIp2[2]) * 256 +\n      (splitIp1[1] - splitIp2[1]) * 256 * 256 +\n      (splitIp1[0] - splitIp2[0]) * 256 * 256 * 256\n    )\n  })\n  const range = { first: '', last: '' }\n  const formattedIps = []\n  let index = 0\n  forEach(sortedIps, ip => {\n    if (ip !== getNextIpV4(range.last)) {\n      if (range.first) {\n        formattedIps[index] = range.first === range.last ? range.first : { ...range }\n        index++\n      }\n      range.first = range.last = ip\n    } else {\n      range.last = ip\n    }\n  })\n  formattedIps[index] = range.first === range.last ? range.first : range\n\n  return formattedIps\n}\n\nexport const parseIpPattern = pattern => {\n  const ips = []\n  forEach(pattern.split(';'), rawIpRange => {\n    const ipRange = rawIpRange.split('-')\n    if (ipRange.length < 2) {\n      ips.push(ipRange[0])\n    } else if (!isIpV4(ipRange[0]) || !isIpV4(ipRange[1])) {\n      ips.push(rawIpRange)\n    } else {\n      ips.push(...range(ipRange[0], ipRange[1]))\n    }\n  })\n\n  return ips\n}\n","import PropTypes from 'prop-types'\nimport React from 'react'\n\nimport _ from 'intl'\nimport ActionButton from './action-button'\nimport Component from './base-component'\nimport Icon from 'icon'\nimport Tooltip from 'tooltip'\nimport { alert } from 'modal'\nimport { isAdmin } from 'selectors'\nimport isEmpty from 'lodash/isEmpty.js'\nimport { addSubscriptions, connectStore, resolveResourceSet } from './utils'\nimport { ejectCd, insertCd, rescanSrs, subscribeResourceSets } from './xo'\nimport { createGetObjectsOfType, createFinder, createGetObject, createSelector } from './selectors'\nimport { SelectResourceSetsVdi, SelectVdi as SelectAnyVdi } from './select-objects'\n\nconst vdiPredicate = vdi => !vdi.missing\n\n@addSubscriptions({\n  resourceSets: subscribeResourceSets,\n})\n@connectStore(() => {\n  const getCdDrive = createFinder(\n    createGetObjectsOfType('VBD').pick((_, { vm }) => vm.$VBDs),\n    [vbd => vbd.is_cd_drive]\n  )\n\n  const getIsoSrs = createGetObjectsOfType('SR').filter(\n    (_, { vm: { $pool } }) =>\n      sr =>\n        sr.$pool === $pool && sr.SR_type === 'iso'\n  )\n\n  const getMountedIso = createGetObject((state, props) => {\n    const cdDrive = getCdDrive(state, props)\n    if (cdDrive) {\n      return cdDrive.VDI\n    }\n  })\n\n  return {\n    cdDrive: getCdDrive,\n    isAdmin,\n    isoSrs: getIsoSrs,\n    mountedIso: getMountedIso,\n  }\n})\nexport default class IsoDevice extends Component {\n  static propTypes = {\n    vm: PropTypes.object.isRequired,\n  }\n\n  _getSrPredicate = createSelector(\n    () => this.props.vm.$pool,\n    () => this.props.vm.$container,\n    (vmPool, vmContainer) => sr => {\n      return (\n        vmPool === sr.$pool &&\n        (sr.shared || vmContainer === sr.$container) &&\n        (sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))\n      )\n    }\n  )\n\n  _getResolvedResourceSet = createSelector(\n    createFinder(\n      () => this.props.resourceSets,\n      createSelector(\n        () => this.props.vm.resourceSet,\n        id => resourceSet => resourceSet.id === id\n      )\n    ),\n    resolveResourceSet\n  )\n\n  _handleInsert = iso => {\n    const { vm } = this.props\n\n    if (iso) {\n      insertCd(vm, iso.id, true)\n    } else {\n      ejectCd(vm)\n    }\n  }\n\n  _handleEject = () => ejectCd(this.props.vm)\n\n  _rescanIsoSrs = () => rescanSrs(this.props.isoSrs)\n\n  _showWarning = () => alert(_('cdDriveNotInstalled'), _('cdDriveInstallation'))\n\n  render() {\n    const { cdDrive, isAdmin, isoSrs, mountedIso } = this.props\n    const resourceSet = this._getResolvedResourceSet()\n    const useResourceSet = !(isAdmin || resourceSet === undefined)\n    const SelectVdi = useResourceSet ? SelectResourceSetsVdi : SelectAnyVdi\n\n    return (\n      <div className='input-group'>\n        <SelectVdi\n          onChange={this._handleInsert}\n          predicate={vdiPredicate}\n          resourceSet={useResourceSet ? resourceSet : undefined}\n          srPredicate={this._getSrPredicate()}\n          value={mountedIso}\n        />\n        {!useResourceSet && (\n          <span className='input-group-btn'>\n            <ActionButton\n              disabled={isEmpty(isoSrs)}\n              handler={this._rescanIsoSrs}\n              icon='refresh'\n              tooltip={_('rescanIsoSrs')}\n            />\n          </span>\n        )}\n        <span className='input-group-btn'>\n          <ActionButton disabled={!mountedIso} handler={this._handleEject} icon='vm-eject' />\n        </span>\n        {mountedIso && !cdDrive.device && (\n          <Tooltip content={_('cdDriveNotInstalled')}>\n            <a className='text-warning btn btn-link' onClick={this._showWarning}>\n              <Icon icon='alarm' size='lg' />\n            </a>\n          </Tooltip>\n        )}\n      </div>\n    )\n  }\n}\n","import PropTypes from 'prop-types'\nimport React from 'react'\nimport uncontrollableInput from 'uncontrollable-input'\nimport { filter, map } from 'lodash'\n\nimport _ from '../intl'\nimport Button from '../button'\nimport Component from '../base-component'\nimport { EMPTY_ARRAY } from '../utils'\n\nimport GenericInput from './generic-input'\nimport { descriptionRender, forceDisplayOptionalAttr } from './helpers'\n\n@uncontrollableInput()\nexport default class ObjectInput extends Component {\n  static propTypes = {\n    depth: PropTypes.number,\n    disabled: PropTypes.bool,\n    label: PropTypes.any.isRequired,\n    required: PropTypes.bool,\n    schema: PropTypes.object.isRequired,\n    uiSchema: PropTypes.object,\n  }\n\n  state = {\n    use: this.props.required || forceDisplayOptionalAttr(this.props),\n  }\n\n  _onAddItem = () => {\n    const { props } = this\n    props.onChange((props.value || EMPTY_ARRAY).concat(undefined))\n  }\n\n  _onChangeItem = (value, name) => {\n    const key = Number(name)\n\n    const { props } = this\n    const newValue = (props.value || EMPTY_ARRAY).slice()\n    newValue[key] = value\n    props.onChange(newValue)\n  }\n\n  _onRemoveItem = key => {\n    const { props } = this\n    props.onChange(filter(props.value, (_, i) => i !== key))\n  }\n\n  render() {\n    const {\n      props: { depth = 0, disabled, label, required, schema, uiSchema, value = EMPTY_ARRAY },\n      state: { use },\n    } = this\n\n    const childDepth = depth + 2\n    const itemSchema = schema.items\n    const itemUiSchema = uiSchema && uiSchema.items\n\n    const itemLabel = itemSchema.title || _('item')\n\n    return (\n      <div style={{ paddingLeft: `${depth}em` }}>\n        <legend>{label}</legend>\n        {descriptionRender(schema.description)}\n        <hr />\n        {!required && (\n          <div className='checkbox'>\n            <label>\n              <input checked={use} disabled={disabled} onChange={this.linkState('use')} type='checkbox' />{' '}\n              {_('fillOptionalInformations')}\n            </label>\n          </div>\n        )}\n        {use && (\n          <div className='card-block'>\n            <ul style={{ paddingLeft: 0 }}>\n              {map(value, (value, key) => (\n                <li className='list-group-item clearfix' key={key}>\n                  <GenericInput\n                    depth={childDepth}\n                    disabled={disabled}\n                    label={itemLabel}\n                    name={key}\n                    onChange={this._onChangeItem}\n                    required\n                    schema={itemSchema}\n                    uiSchema={itemUiSchema}\n                    value={value}\n                  />\n                  <Button\n                    btnStyle='danger'\n                    className='pull-right'\n                    disabled={disabled}\n                    name={key}\n                    onClick={() => this._onRemoveItem(key)}\n                  >\n                    {_('remove')}\n                  </Button>\n                </li>\n              ))}\n            </ul>\n            <Button btnStyle='primary' className='pull-right mt-1 mr-1' disabled={disabled} onClick={this._onAddItem}>\n              {_('add')}\n            </Button>\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n","import React from 'react'\n\nimport uncontrollableInput from 'uncontrollable-input'\nimport Component from '../base-component'\nimport { Toggle } from '../form'\n\nimport { PrimitiveInputWrapper } from './helpers'\n\n// ===================================================================\n\n@uncontrollableInput()\nexport default class BooleanInput extends Component {\n  render() {\n    const { disabled, onChange, value, ...props } = this.props\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <div className='checkbox form-control'>\n          <Toggle disabled={disabled} onChange={onChange} value={value || false} />\n        </div>\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import _ from 'intl'\nimport uncontrollableInput from 'uncontrollable-input'\nimport Component from 'base-component'\nimport React from 'react'\nimport { createSelector } from 'reselect'\nimport { findIndex, map } from 'lodash'\n\nimport { PrimitiveInputWrapper } from './helpers'\n\n// ===================================================================\n\n@uncontrollableInput()\nexport default class EnumInput extends Component {\n  _getSelectedIndex = createSelector(\n    () => this.props.schema.enum,\n    () => {\n      const { schema, value = schema.default } = this.props\n      return value\n    },\n    (enumValues, value) => {\n      const index = findIndex(enumValues, current => current === value)\n      return index === -1 ? '' : index\n    }\n  )\n\n  _onChange = event => {\n    this.props.onChange(this.props.schema.enum[event.target.value])\n  }\n\n  render() {\n    const {\n      disabled,\n      schema: { enum: enumValues, enumNames = enumValues },\n      required,\n    } = this.props\n\n    return (\n      <PrimitiveInputWrapper {...this.props}>\n        <select\n          className='form-control'\n          disabled={disabled}\n          onChange={this._onChange}\n          required={required}\n          value={this._getSelectedIndex()}\n        >\n          {_('noSelectedValue', message => (\n            <option value=''>{message}</option>\n          ))}\n          {map(enumNames, (name, index) => (\n            <option value={index} key={index}>\n              {name}\n            </option>\n          ))}\n        </select>\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import PropTypes from 'prop-types'\nimport React, { Component } from 'react'\n\nimport getEventValue from '../get-event-value'\nimport uncontrollableInput from 'uncontrollable-input'\nimport { EMPTY_OBJECT } from '../utils'\n\nimport ArrayInput from './array-input'\nimport BooleanInput from './boolean-input'\nimport EnumInput from './enum-input'\nimport IntegerInput from './integer-input'\nimport NumberInput from './number-input'\nimport ObjectInput from './object-input'\nimport StringInput from './string-input'\n\nimport { getType } from './helpers'\n\n// ===================================================================\n\nconst InputByType = {\n  array: ArrayInput,\n  boolean: BooleanInput,\n  integer: IntegerInput,\n  number: NumberInput,\n  object: ObjectInput,\n  string: StringInput,\n}\n\n// ===================================================================\n\n@uncontrollableInput()\nexport default class GenericInput extends Component {\n  static propTypes = {\n    depth: PropTypes.number,\n    disabled: PropTypes.bool,\n    label: PropTypes.any.isRequired,\n    required: PropTypes.bool,\n    schema: PropTypes.object.isRequired,\n    uiSchema: PropTypes.object,\n  }\n\n  _onChange = event => {\n    const { name, onChange } = this.props\n    onChange && onChange(getEventValue(event), name)\n  }\n\n  render() {\n    const { schema, value, uiSchema = EMPTY_OBJECT, ...opts } = this.props\n\n    const props = {\n      ...opts,\n      onChange: this._onChange,\n      schema,\n      uiSchema,\n      value,\n    }\n\n    // Enum, special case.\n    if (schema.enum) {\n      return <EnumInput {...props} />\n    }\n\n    const type = getType(schema)\n    const Input = uiSchema.widget || InputByType[type.toLowerCase()]\n\n    if (!Input) {\n      throw new Error(`Unsupported type: ${type}.`)\n    }\n\n    return <Input {...props} {...uiSchema.config} />\n  }\n}\n","import React from 'react'\nimport includes from 'lodash/includes'\nimport marked from 'marked'\n\nimport { Col, Row } from 'grid'\n\n// ===================================================================\n\nexport const getType = schema => {\n  if (!schema) {\n    return\n  }\n\n  const type = schema.type\n\n  if (Array.isArray(type)) {\n    if (includes(type, 'integer')) {\n      return 'integer'\n    }\n    if (includes(type, 'number')) {\n      return 'number'\n    }\n\n    return 'string'\n  }\n\n  return type\n}\n\nexport const getXoType = schema => {\n  const type = schema && (schema['xo:type'] || schema.$type)\n\n  if (type) {\n    return type.toLowerCase()\n  }\n}\n\n// ===================================================================\n\nexport const descriptionRender = description => (\n  <span className='text-muted' dangerouslySetInnerHTML={{ __html: marked(description || '') }} />\n)\n\n// ===================================================================\n\nexport const PrimitiveInputWrapper = ({ label, required = false, schema, children }) => (\n  <Row>\n    <Col mediumSize={6}>\n      <div className='input-group'>\n        <span className='input-group-addon'>\n          {label}\n          {required && <span className='text-warning'>*</span>}\n        </span>\n        {children}\n      </div>\n    </Col>\n    <Col mediumSize={6}>{descriptionRender(schema.description)}</Col>\n  </Row>\n)\n\n// ===================================================================\n\nexport const forceDisplayOptionalAttr = ({ schema, value }) => {\n  if (!schema || !value) {\n    return false\n  }\n\n  // Array\n  if (schema.items && Array.isArray(value)) {\n    return true\n  }\n\n  // Object\n  for (const key in schema.properties) {\n    if (value[key]) {\n      return true\n    }\n  }\n\n  return false\n}\n","export default from './generic-input'\n","import React from 'react'\n\nimport uncontrollableInput from 'uncontrollable-input'\nimport Combobox from '../combobox'\nimport Component from '../base-component'\nimport getEventValue from '../get-event-value'\n\nimport { PrimitiveInputWrapper } from './helpers'\n\n// ===================================================================\n\n@uncontrollableInput()\nexport default class IntegerInput extends Component {\n  _onChange = event => {\n    const value = getEventValue(event)\n    this.props.onChange(value ? +value : undefined)\n  }\n\n  render() {\n    const { required, schema } = this.props\n    const {\n      disabled,\n      onChange, // eslint-disable-line no-unused-vars\n      placeholder = schema.default,\n      value,\n      ...props\n    } = this.props\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <Combobox\n          value={value === undefined ? '' : String(value)}\n          disabled={disabled}\n          max={schema.max}\n          min={schema.min}\n          onChange={this._onChange}\n          options={schema.defaults}\n          placeholder={placeholder}\n          required={required}\n          step={1}\n          type='number'\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\n\nimport uncontrollableInput from 'uncontrollable-input'\nimport Combobox from '../combobox'\nimport Component from '../base-component'\nimport getEventValue from '../get-event-value'\n\nimport { PrimitiveInputWrapper } from './helpers'\n\n// ===================================================================\n\n@uncontrollableInput()\nexport default class NumberInput extends Component {\n  _onChange = event => {\n    const value = getEventValue(event)\n    this.props.onChange(value ? +value : undefined)\n  }\n\n  render() {\n    const { required, schema } = this.props\n    const {\n      disabled,\n      onChange, // eslint-disable-line no-unused-vars\n      placeholder = schema.default,\n      value,\n      ...props\n    } = this.props\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <Combobox\n          value={value === undefined ? '' : String(value)}\n          disabled={disabled}\n          max={schema.max}\n          min={schema.min}\n          onChange={this._onChange}\n          options={schema.defaults}\n          placeholder={placeholder}\n          required={required}\n          step='any'\n          type='number'\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import PropTypes from 'prop-types'\nimport React from 'react'\nimport uncontrollableInput from 'uncontrollable-input'\nimport { createSelector } from 'reselect'\nimport { keyBy, map } from 'lodash'\n\nimport _ from '../intl'\nimport Component from '../base-component'\nimport getEventValue from '../get-event-value'\nimport { EMPTY_OBJECT } from '../utils'\n\nimport GenericInput from './generic-input'\nimport { descriptionRender, forceDisplayOptionalAttr } from './helpers'\n\n@uncontrollableInput()\nexport default class ObjectInput extends Component {\n  static propTypes = {\n    depth: PropTypes.number,\n    disabled: PropTypes.bool,\n    label: PropTypes.any.isRequired,\n    required: PropTypes.bool,\n    schema: PropTypes.object.isRequired,\n    uiSchema: PropTypes.object,\n  }\n\n  state = {\n    use: this.props.required || forceDisplayOptionalAttr(this.props),\n  }\n\n  _onChildChange = (value, key) => {\n    this.props.onChange({\n      ...this.props.value,\n      [key]: value,\n    })\n  }\n\n  _onUseChange = event => {\n    const use = getEventValue(event)\n    if (!use) {\n      this.props.onChange()\n    }\n    this.setState({ use })\n  }\n\n  _getRequiredProps = createSelector(\n    () => this.props.schema.required,\n    required => (required ? keyBy(required) : EMPTY_OBJECT)\n  )\n\n  render() {\n    const {\n      props: { depth = 0, disabled, label, required, schema, uiSchema, value = EMPTY_OBJECT },\n      state: { use },\n    } = this\n\n    const childDepth = depth + 2\n    const properties = (uiSchema != null && uiSchema.properties) || EMPTY_OBJECT\n    const requiredProps = this._getRequiredProps()\n\n    return (\n      <div style={{ paddingLeft: `${depth}em` }}>\n        <legend>{label}</legend>\n        {descriptionRender(schema.description)}\n        <hr />\n        {!required && (\n          <div className='checkbox'>\n            <label>\n              <input checked={use} disabled={disabled} onChange={this._onUseChange} type='checkbox' />{' '}\n              {_('fillOptionalInformations')}\n            </label>\n          </div>\n        )}\n        {use && (\n          <div className='card-block'>\n            {map(schema.properties, (childSchema, key) => (\n              <div className='pb-1' key={key}>\n                <GenericInput\n                  depth={childDepth}\n                  disabled={disabled}\n                  label={childSchema.title || key}\n                  name={key}\n                  onChange={this._onChildChange}\n                  required={Boolean(requiredProps[key])}\n                  schema={childSchema}\n                  uiSchema={properties[key]}\n                  value={value[key]}\n                />\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n","import PropTypes from 'prop-types'\nimport React from 'react'\nimport uncontrollableInput from 'uncontrollable-input'\n\nimport Combobox from '../combobox'\nimport Component from '../base-component'\nimport getEventValue from '../get-event-value'\n\nimport { PrimitiveInputWrapper } from './helpers'\n\n// ===================================================================\n\n@uncontrollableInput()\nexport default class StringInput extends Component {\n  static propTypes = {\n    password: PropTypes.bool,\n  }\n\n  // the value of this input  is undefined not '' when empty to make\n  // it homogenous with when the user has never touched this input\n  _onChange = event => {\n    const value = getEventValue(event)\n    this.props.onChange(value !== '' ? value : undefined)\n  }\n\n  render() {\n    const { required, schema } = this.props\n    const { disabled, password, placeholder = schema.default, value, ...props } = this.props\n    delete props.onChange\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <Combobox\n          value={value !== undefined ? value : ''}\n          disabled={disabled}\n          multiline={schema.$multiline}\n          onChange={this._onChange}\n          options={schema.defaults}\n          placeholder={placeholder || schema.default}\n          required={required}\n          type={password && 'password'}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import Link from 'react-router/lib/Link'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { routerShape } from 'react-router/lib/PropTypes'\n\nimport Component from './base-component'\n\n// ===================================================================\n\nexport { Link as default }\n\n// -------------------------------------------------------------------\n\nconst _IGNORED_TAGNAMES = {\n  A: true,\n  BUTTON: true,\n  INPUT: true,\n  SELECT: true,\n}\n\nexport class BlockLink extends Component {\n  static propTypes = {\n    className: PropTypes.string,\n    tagName: PropTypes.string,\n  }\n\n  static contextTypes = {\n    router: routerShape,\n  }\n\n  _style = { cursor: 'pointer' }\n  _onClickCapture = event => {\n    const { currentTarget } = event\n    let element = event.target\n    while (element !== currentTarget) {\n      if (_IGNORED_TAGNAMES[element.tagName]) {\n        return\n      }\n      element = element.parentNode\n    }\n    event.stopPropagation()\n    if (event.ctrlKey || event.button === 1) {\n      window.open(this.context.router.createHref(this.props.to))\n    } else {\n      this.context.router.push(this.props.to)\n    }\n  }\n\n  _addAuxClickListener = ref => {\n    // FIXME: when https://github.com/facebook/react/issues/8529 is fixed,\n    // remove and use onAuxClickCapture.\n    // In Chrome ^55 and Firefox ^53, middle-clicking triggers auxclick event\n    // instead of click\n    // Other browsers may trigger both events.\n    if (ref !== null) {\n      ref.addEventListener('auxclick', this._onClickCapture)\n      ref.addEventListener('mousedown', event => {\n        if (event.button === 1) {\n          event.preventDefault() // Prevent enabling auto-scroll\n        }\n      })\n    }\n  }\n\n  render() {\n    const { children, tagName = 'div', className } = this.props\n    const Component = tagName\n    return (\n      <Component\n        className={className}\n        ref={this._addAuxClickListener}\n        style={this._style}\n        onClickCapture={this._onClickCapture}\n      >\n        {children}\n      </Component>\n    )\n  }\n}\n","// Logs an error properly, correctly use the source map for the stack.\n//\n// This is achieved by throwing the error asynchronously.\nconst logError = (error, ...args) => {\n  setTimeout(() => {\n    if (args.length) {\n      console.error(...args)\n    }\n\n    throw error\n  }, 0)\n}\nexport { logError as default }\n","import PropTypes from 'prop-types'\nimport React, { Component, cloneElement } from 'react'\nimport { createSelector } from 'selectors'\nimport { identity, map } from 'lodash'\nimport { injectIntl } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport { Modal as ReactModal } from 'react-bootstrap-4/lib'\n\nimport _, { messages } from './intl'\nimport ActionButton from './action-button'\nimport Button from './button'\nimport decorate from './apply-decorators'\nimport getEventValue from './get-event-value'\nimport Icon from './icon'\nimport Tooltip from './tooltip'\nimport { generateId } from './reaclette-utils'\nimport { disable as disableShortcuts, enable as enableShortcuts } from './shortcuts'\n\n// -----------------------------------------------------------------------------\n\nlet instance\nconst modal = (content, onClose, props) => {\n  if (!instance) {\n    throw new Error('No modal instance.')\n  } else if (instance.state.showModal) {\n    throw new Error('Other modal still open.')\n  }\n  instance.setState({ content, onClose, showModal: true, props }, disableShortcuts)\n}\n\nconst _addRef = (component, ref) => {\n  if (typeof component === 'string' || Array.isArray(component)) {\n    return component\n  }\n\n  try {\n    return cloneElement(component, { ref })\n  } catch (_) {} // Stateless component.\n  return component\n}\n\n// -----------------------------------------------------------------------------\n\nclass GenericModal extends Component {\n  static propTypes = {\n    buttons: PropTypes.arrayOf(\n      PropTypes.shape({\n        btnStyle: PropTypes.string,\n        icon: PropTypes.string,\n        label: PropTypes.node.isRequired,\n        tooltip: PropTypes.node,\n        value: PropTypes.any,\n      })\n    ).isRequired,\n    children: PropTypes.node.isRequired,\n    icon: PropTypes.string,\n    title: PropTypes.node.isRequired,\n  }\n\n  _getBodyValue = () => {\n    const { body } = this.refs\n    if (body !== undefined) {\n      return body.getWrappedInstance === undefined ? body.value : body.getWrappedInstance().value\n    }\n  }\n\n  _resolve = (value = this._getBodyValue()) => {\n    this.props.resolve(value)\n    instance.close()\n  }\n\n  _reject = () => {\n    this.props.reject()\n    instance.close()\n  }\n\n  render() {\n    const { buttons, icon, title } = this.props\n\n    const body = _addRef(this.props.children, 'body')\n\n    return (\n      <div>\n        <ReactModal.Header closeButton>\n          <ReactModal.Title>\n            {icon ? (\n              <span>\n                <Icon icon={icon} /> {title}\n              </span>\n            ) : (\n              title\n            )}\n          </ReactModal.Title>\n        </ReactModal.Header>\n        <ReactModal.Body>{body}</ReactModal.Body>\n        <ReactModal.Footer>\n          {map(buttons, ({ label, tooltip, value, icon, ...props }, key) => {\n            const button = (\n              <Button onClick={() => this._resolve(value)} {...props}>\n                {icon !== undefined && <Icon icon={icon} fixedWidth />}\n                {label}\n              </Button>\n            )\n            return (\n              <span key={key}>{tooltip !== undefined ? <Tooltip content={tooltip}>{button}</Tooltip> : button} </span>\n            )\n          })}\n          {this.props.reject !== undefined && <Button onClick={this._reject}>{_('genericCancel')}</Button>}\n        </ReactModal.Footer>\n      </div>\n    )\n  }\n}\n\nexport const chooseAction = ({ body, buttons, icon, title }) => {\n  return new Promise((resolve, reject) => {\n    modal(\n      <GenericModal buttons={buttons} icon={icon} reject={reject} resolve={resolve} title={title}>\n        {body}\n      </GenericModal>,\n      reject\n    )\n  })\n}\n\n@injectIntl\nclass StrongConfirm extends Component {\n  static propTypes = {\n    body: PropTypes.node,\n    strongConfirm: PropTypes.object.isRequired,\n    icon: PropTypes.string,\n    reject: PropTypes.func,\n    resolve: PropTypes.func,\n    title: PropTypes.node.isRequired,\n  }\n\n  state = {\n    buttons: [{ btnStyle: 'danger', label: _('confirmOk'), disabled: true }],\n  }\n\n  _getStrongConfirmString = createSelector(\n    () => this.props.intl.formatMessage,\n    () => this.props.strongConfirm,\n    (format, { messageId, values }) => format(messages[messageId], values)\n  )\n\n  _onInputChange = event => {\n    const userInput = event.target.value\n    const strongConfirmString = this._getStrongConfirmString()\n    const confirmButton = this.state.buttons[0]\n\n    let disabled\n    if ((userInput.toLowerCase() === strongConfirmString.toLowerCase()) ^ (disabled = !confirmButton.disabled)) {\n      this.setState({\n        buttons: [{ ...confirmButton, disabled }],\n      })\n    }\n  }\n\n  _confirm = () => {\n    this.props.resolve()\n    instance.close()\n  }\n\n  _handleKeyDown = event => {\n    if (event.keyCode === 13 && !this.state.buttons[0].disabled) {\n      this._confirm()\n    }\n  }\n\n  _focusAndAddEventListener = ref => {\n    if (ref !== null) {\n      // When the modal is triggered by a react-bootstrap Dropdown, the Dropdown takes the focus back\n      // https://github.com/vatesfr/react-bootstrap/blob/bootstrap-4/src/Dropdown.js#L63-L85\n      // FIXME: remove the setTimeout workaround when react-bootstrap-4 is removed\n      // See https://github.com/react-bootstrap/react-bootstrap/issues/2553#issuecomment-324356126\n      setTimeout(() => {\n        ref.focus()\n      })\n      ref.addEventListener('keydown', this._handleKeyDown)\n      this.componentWillUnmount = () => ref.removeEventListener('keydown', this._handleKeyDown)\n    }\n  }\n\n  render() {\n    const {\n      body,\n      strongConfirm: { messageId, values },\n      icon,\n      reject,\n      resolve,\n      title,\n    } = this.props\n\n    return (\n      <GenericModal buttons={this.state.buttons} icon={icon} reject={reject} resolve={resolve} title={title}>\n        {body}\n        <hr />\n        <div>\n          {_('enterConfirmText')} <strong className='no-text-selection'>{_(messageId, values)}</strong>\n        </div>\n        <div>\n          <input className='form-control' ref={this._focusAndAddEventListener} onChange={this._onInputChange} />\n        </div>\n      </GenericModal>\n    )\n  }\n}\n\n// -----------------------------------------------------------------------------\n\nconst ALERT_BUTTONS = [{ label: _('alertOk'), value: 'ok' }]\n\nexport const alert = (title, body) =>\n  new Promise(resolve => {\n    modal(\n      <GenericModal buttons={ALERT_BUTTONS} resolve={resolve} title={title}>\n        {body}\n      </GenericModal>,\n      resolve\n    )\n  })\n\n// -----------------------------------------------------------------------------\n\nconst CONFIRM_BUTTONS = [{ btnStyle: 'primary', label: _('confirmOk') }]\n\nexport const confirm = ({ body, icon = 'alarm', title, strongConfirm }) =>\n  strongConfirm\n    ? new Promise((resolve, reject) => {\n        modal(\n          <StrongConfirm\n            body={body}\n            icon={icon}\n            reject={reject}\n            resolve={resolve}\n            strongConfirm={strongConfirm}\n            title={title}\n          />,\n          reject\n        )\n      })\n    : chooseAction({\n        body,\n        buttons: CONFIRM_BUTTONS,\n        icon,\n        title,\n      })\n\n// -----------------------------------------------------------------------------\n\nlet formModalState\nexport const form = ({ component, defaultValue, handler = identity, header, render, size }) =>\n  new Promise((resolve, reject) => {\n    formModalState.component = component\n    formModalState.handler = handler\n    formModalState.header = header\n    formModalState.reject = reject\n    formModalState.render = render\n    formModalState.resolve = resolve\n    formModalState.size = size\n    formModalState.value = defaultValue\n    disableShortcuts()\n\n    // the modal should be opened after its props have been set to avoid race conditions\n    formModalState.opened = true\n  })\n\nconst getInitialState = () => ({\n  component: undefined,\n  handler: undefined,\n  header: undefined,\n  isHandlerRunning: false,\n  opened: false,\n  reject: undefined,\n  render: undefined,\n  resolve: undefined,\n  size: undefined,\n  value: undefined,\n})\nexport const FormModal = decorate([\n  provideState({\n    initialState: getInitialState,\n    effects: {\n      initialize() {\n        if (formModalState !== undefined) {\n          throw new Error('FormModal is a singleton!')\n        }\n        formModalState = this.state\n      },\n      finalize: () => {\n        formModalState = undefined\n      },\n      onChange: (_, value) => () => ({\n        value: getEventValue(value),\n      }),\n      onCancel() {\n        const { state } = this\n        if (!state.isHandlerRunning) {\n          state.opened = false\n          state.reject()\n        }\n      },\n      async onSubmit({ close }) {\n        const { state } = this\n        state.isHandlerRunning = true\n\n        let result\n        try {\n          result = await state.handler(state.value)\n        } finally {\n          state.isHandlerRunning = false\n        }\n        state.opened = false\n        state.resolve(result)\n      },\n      reset: () => () => {\n        enableShortcuts()\n        return getInitialState()\n      },\n    },\n    computed: {\n      formId: generateId,\n    },\n  }),\n  injectState,\n  ({ state, effects }) => {\n    const Component = state.component\n\n    return (\n      <ReactModal\n        backdrop='static'\n        bsSize={state.size}\n        keyboard={false}\n        onExited={effects.reset}\n        onHide={effects.onCancel}\n        show={state.opened}\n      >\n        <ReactModal.Header closeButton>\n          <ReactModal.Title>{state.header}</ReactModal.Title>\n        </ReactModal.Header>\n\n        <ReactModal.Body>\n          <form id={state.formId}>\n            {/* It should be better to use a computed to avoid calling the render function on each render,\n            but reaclette(v0.4.0) not allow us to access to the effects from a computed */}\n            {Component !== undefined ? (\n              <Component onChange={effects.onChange} value={state.value} />\n            ) : (\n              state.render !== undefined &&\n              state.render({\n                onChange: effects.onChange,\n                value: state.value,\n              })\n            )}\n          </form>\n        </ReactModal.Body>\n\n        <ReactModal.Footer>\n          <ActionButton btnStyle='primary' form={state.formId} handler={effects.onSubmit} icon='save' size='large'>\n            {_('formOk')}\n          </ActionButton>{' '}\n          <ActionButton handler={effects.onCancel} icon='cancel' size='large'>\n            {_('formCancel')}\n          </ActionButton>\n        </ReactModal.Footer>\n      </ReactModal>\n    )\n  },\n])\n\n// -----------------------------------------------------------------------------\n\nexport default class Modal extends Component {\n  constructor() {\n    super()\n\n    this.state = { showModal: false }\n  }\n\n  static close() {\n    if (instance.state.showModal) {\n      instance.setState({ showModal: false })\n    }\n  }\n\n  componentDidMount() {\n    if (instance) {\n      throw new Error('Modal is a singleton!')\n    }\n    instance = this\n  }\n\n  componentWillUnmount() {\n    instance = undefined\n  }\n\n  close() {\n    this.setState({ showModal: false }, enableShortcuts)\n  }\n\n  _onHide = () => {\n    this.close()\n\n    const { onClose } = this.state\n    onClose && onClose()\n  }\n\n  render() {\n    const { content, showModal, props } = this.state\n    return (\n      <ReactModal show={showModal} onHide={this._onHide} {...props}>\n        {content}\n      </ReactModal>\n    )\n  }\n}\n","import classNames from 'classnames'\nimport React from 'react'\n\nimport Link from './link'\n\nexport const NavLink = ({ children, exact, to }) => (\n  <li className='nav-item' role='tab'>\n    <Link activeClassName='active' className='nav-link' onlyActiveOnIndex={exact} to={to}>\n      {children}\n    </Link>\n  </li>\n)\n\nexport const NavTabs = ({ children, className }) => (\n  <ul className={classNames(className, 'nav nav-tabs')} role='tablist'>\n    {children}\n  </ul>\n)\n","import PropTypes from 'prop-types'\nimport React from 'react'\nimport isEmpty from 'lodash/isEmpty.js'\n\n// This component returns :\n//  - A loading icon when the objects are not fetched\n//  - A default message if the objects are fetched and the collection is empty\n//  - The children if the objects are fetched and the collection is not empty\n//\n// ```js\n//  <NoObjects collection={collection} emptyMessage={message}>\n//    {children}\n// </NoObjects>\n// ````\nconst NoObjects = props => {\n  const { collection } = props\n\n  if (collection == null) {\n    return <img src='assets/loading.svg' alt='loading' />\n  }\n\n  if (isEmpty(collection)) {\n    return <p>{props.emptyMessage}</p>\n  }\n\n  const { children, component: Component, componentRef, ...otherProps } = props\n  return children !== undefined ? children(otherProps) : <Component ref={componentRef} {...otherProps} />\n}\n\nNoObjects.propTypes = {\n  children: PropTypes.func,\n  collection: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),\n  component: PropTypes.func,\n  componentRef: PropTypes.func,\n  emptyMessage: PropTypes.node.isRequired,\n}\nexport { NoObjects as default }\n","import _ from 'intl'\nimport ButtonLink from 'button-link'\nimport Icon from 'icon'\nimport React, { Component } from 'react'\nimport ReactNotify from 'react-notify'\nimport { connectStore } from 'utils'\nimport { isAdmin } from 'selectors'\n\nlet instance\n\nexport let error\nexport let info\nexport let success\n\n@connectStore({\n  isAdmin,\n})\nexport class Notification extends Component {\n  componentDidMount() {\n    if (instance) {\n      throw new Error('Notification is a singleton!')\n    }\n    instance = this\n  }\n\n  componentWillUnmount() {\n    instance = undefined\n  }\n\n  // This special component never have to rerender!\n  shouldComponentUpdate() {\n    return false\n  }\n\n  render() {\n    return (\n      <ReactNotify\n        ref={notification => {\n          if (!notification) {\n            return\n          }\n\n          error = (title, body, autoCloseTimeout = 6e3) =>\n            notification.error(\n              title,\n              this.props.isAdmin ? (\n                <div>\n                  <div>{body}</div>\n                  <ButtonLink btnStyle='danger' className='mt-1' size='small' to='/settings/logs'>\n                    <Icon icon='logs' /> {_('showLogs')}\n                  </ButtonLink>\n                </div>\n              ) : (\n                body\n              ),\n              autoCloseTimeout\n            )\n          info = (title, body, autoCloseTimeout = 3e3) => notification.info(title, body, autoCloseTimeout)\n          success = (title, body, autoCloseTimeout = 3e3) => notification.success(title, body, autoCloseTimeout)\n        }}\n      />\n    )\n  }\n}\n\nexport { info as default }\n\n/* Example:\n\nimport info, { success, error } from 'notification'\n\n<button onClick={() => info('Info', 'This is an info notification')}>\n  Info notification\n</button>\n\n<button onClick={() => success('Success', 'This is a success notification')}>\n  Success notification\n</button>\n\n<button onClick={() => error('Error', 'This is an error notification')}>\n  Error notification\n</button>\n*/\n","import authenticator from 'otplib/authenticator'\nimport crypto from 'crypto'\n\nauthenticator.options = { crypto }\n\nexport { authenticator as default }\n","import React from 'react'\nimport PropTypes from 'prop-types'\n\nconst PageItem = ({ active, children, disabled, onClick, value }) =>\n  active ? (\n    <li className='active page-item'>\n      <span className='page-link'>{children}</span>\n    </li>\n  ) : disabled ? (\n    <li className='disabled page-item'>\n      <span className='page-link'>{children}</span>\n    </li>\n  ) : (\n    <li className='page-item'>\n      <a className='page-link' href='#' onClick={onClick} data-value={value}>\n        {children}\n      </a>\n    </li>\n  )\n\nexport default class Pagination extends React.PureComponent {\n  static defaultProps = {\n    ellipsis: true,\n    maxButtons: 7,\n    next: true,\n    prev: true,\n  }\n\n  static propTypes = {\n    ariaLabel: PropTypes.string,\n    ellipsis: PropTypes.bool,\n    maxButtons: PropTypes.number,\n    next: PropTypes.bool,\n    onChange: PropTypes.func.isRequired,\n    pages: PropTypes.number.isRequired,\n    prev: PropTypes.bool,\n    value: PropTypes.number.isRequired,\n  }\n\n  _onClick(event) {\n    event.preventDefault()\n    this.props.onChange(+event.currentTarget.dataset.value)\n  }\n  _onClick = this._onClick.bind(this)\n\n  render() {\n    const { ariaLabel, ellipsis, maxButtons, next, pages, prev, value } = this.props\n    const onClick = this._onClick\n\n    let min, max\n    if (pages <= maxButtons) {\n      min = 1\n      max = pages\n    } else {\n      min = Math.max(1, Math.min(value - Math.floor(maxButtons / 2), pages - maxButtons + 1))\n      max = min + maxButtons - 1\n    }\n\n    const pageButtons = []\n    if (ellipsis && min !== 1) {\n      pageButtons.push(\n        <PageItem disabled key='firstEllipsis'>\n          …\n        </PageItem>\n      )\n    }\n    for (let page = min; page <= max; ++page) {\n      pageButtons.push(\n        <PageItem active={page === value} key={page} onClick={onClick} value={page}>\n          {page}\n        </PageItem>\n      )\n    }\n    if (ellipsis && max !== pages) {\n      pageButtons.push(\n        <PageItem disabled key='lastEllipsis'>\n          …\n        </PageItem>\n      )\n    }\n    return (\n      <nav aria-label={ariaLabel}>\n        <ul className='pagination'>\n          {prev && (\n            <PageItem aria-label='Previous' disabled={value === 1} onClick={onClick} value={value - 1}>\n              ‹\n            </PageItem>\n          )}\n          {pageButtons}\n          {next && (\n            <PageItem aria-label='Next' disabled={value === pages} onClick={onClick} value={value + 1}>\n              ›\n            </PageItem>\n          )}\n        </ul>\n      </nav>\n    )\n  }\n}\n","// this computed can be used to generate a random id for the lifetime of the\n// component\nexport const generateId = () => `i${Math.random().toString(36).slice(2)}`\n\n// TODO: remove these functions once the PR: https://github.com/JsCommunity/reaclette/pull/5 has been merged\n// It only supports native inputs\nexport const linkState =\n  (_, { target }) =>\n  () => ({\n    [target.name]:\n      target.nodeName.toLowerCase() === 'input' && target.type.toLowerCase() === 'checkbox'\n        ? target.checked\n        : target.value,\n  })\n\nexport const toggleState =\n  (_, { currentTarget: { name } }) =>\n  state => ({\n    [name]: !state[name],\n  })\n","import PropTypes from 'prop-types'\nimport React, { Component } from 'react'\nimport RFB from '@nraynaud/novnc/lib/rfb'\nimport URL from 'url-parse'\nimport { createBackoff } from 'jsonrpc-websocket-client'\nimport { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'\n\nconst PROTOCOL_ALIASES = {\n  'http:': 'ws:',\n  'https:': 'wss:',\n}\nconst fixProtocol = url => {\n  const protocol = PROTOCOL_ALIASES[url.protocol]\n  if (protocol) {\n    url.protocol = protocol\n  }\n}\n\nexport default class NoVnc extends Component {\n  static propTypes = {\n    onClipboardChange: PropTypes.func,\n    url: PropTypes.string.isRequired,\n  }\n\n  constructor(props) {\n    super(props)\n    this._rfb = null\n    this._retryGen = createBackoff(Infinity)\n\n    this._onUpdateState = (rfb, state) => {\n      if (state === 'normal') {\n        if (this._retryTimeout) {\n          clearTimeout(this._retryTimeout)\n          this._retryTimeout = undefined\n          this._retryGen = createBackoff(Infinity)\n        }\n      }\n\n      if (state !== 'disconnected' || this.refs.canvas == null) {\n        return\n      }\n\n      clearTimeout(this._retryTimeout)\n      this._retryTimeout = setTimeout(this._connect, this._retryGen.next().value)\n    }\n  }\n\n  sendCtrlAltDel() {\n    const rfb = this._rfb\n    if (rfb) {\n      rfb.sendCtrlAltDel()\n    }\n  }\n\n  setClipboard(text) {\n    const rfb = this._rfb\n    if (rfb) {\n      rfb.clipboardPasteFrom(text)\n    }\n  }\n\n  _clean() {\n    const rfb = this._rfb\n    if (rfb) {\n      this._rfb = null\n      rfb.disconnect()\n    }\n    enableShortcuts()\n  }\n\n  _connect = () => {\n    this._clean()\n\n    const { canvas } = this.refs\n    if (!canvas) {\n      return\n    }\n\n    const url = new URL(this.props.url)\n    fixProtocol(url)\n\n    const isSecure = url.protocol === 'wss:'\n\n    const { onClipboardChange } = this.props\n    const rfb = (this._rfb = new RFB({\n      encrypt: isSecure,\n      target: this.refs.canvas,\n      onClipboard:\n        onClipboardChange &&\n        ((_, text) => {\n          onClipboardChange(text)\n        }),\n      onUpdateState: this._onUpdateState,\n    }))\n\n    // remove leading slashes from the path\n    //\n    // a leading slash will be added by noVNC\n    const clippedPath = url.pathname.replace(/^\\/+/, '')\n\n    // a port is required\n    //\n    // if not available from the URL, use the default ones\n    const port = url.port || (isSecure ? 443 : 80)\n\n    rfb.connect(url.hostname, port, null, clippedPath)\n    disableShortcuts()\n  }\n\n  componentDidMount() {\n    this._connect()\n  }\n\n  componentWillUnmount() {\n    this._clean()\n  }\n\n  componentWillReceiveProps(props) {\n    const rfb = this._rfb\n    if (rfb && this.props.scale !== props.scale) {\n      rfb.get_display().set_scale(props.scale || 1)\n      rfb.get_mouse().set_scale(props.scale || 1)\n    }\n  }\n\n  _focus = () => {\n    const rfb = this._rfb\n    if (rfb) {\n      const { activeElement } = document\n      if (activeElement) {\n        activeElement.blur()\n      }\n\n      rfb.get_keyboard().grab()\n      rfb.get_mouse().grab()\n\n      disableShortcuts()\n    }\n  }\n\n  _unfocus = () => {\n    const rfb = this._rfb\n    if (rfb) {\n      rfb.get_keyboard().ungrab()\n      rfb.get_mouse().ungrab()\n\n      enableShortcuts()\n    }\n  }\n\n  render() {\n    return (\n      <canvas\n        className='center-block'\n        height='480'\n        onMouseEnter={this._focus}\n        onMouseLeave={this._unfocus}\n        ref='canvas'\n        width='640'\n      />\n    )\n  }\n}\n","import * as CM from 'complex-matcher'\nimport _ from 'intl'\nimport CopyToClipboard from 'react-copy-to-clipboard'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { get } from '@xen-orchestra/defined'\nimport { injectState, provideState } from 'reaclette'\nimport find from 'lodash/find.js'\nimport isEmpty from 'lodash/isEmpty.js'\n\nimport decorate from './apply-decorators'\nimport Icon from './icon'\nimport Link from './link'\nimport Tooltip from './tooltip'\nimport { addSubscriptions, connectStore, formatSize, NumericDate, ShortDate } from './utils'\nimport { createGetObject, createSelector } from './selectors'\nimport {\n  isSrWritable,\n  subscribeBackupNgJobs,\n  subscribeMetadataBackupJobs,\n  subscribeProxies,\n  subscribeRemotes,\n  subscribeSchedules,\n  subscribeUsers,\n  subscribeMirrorBackupJobs,\n} from './xo'\n\n// ===================================================================\n\nconst unknowItem = (uuid, type, placeholder) => (\n  <Tooltip content={_('copyUuid', { uuid })}>\n    <CopyToClipboard text={uuid}>\n      <span className='text-muted' style={{ cursor: 'pointer' }}>\n        {placeholder === undefined ? _('errorUnknownItem', { type }) : placeholder}\n      </span>\n    </CopyToClipboard>\n  </Tooltip>\n)\n\nconst LinkWrapper = ({ children, link, to, newTab }) =>\n  link ? (\n    <Link to={to} target={newTab && '_blank'}>\n      {children}\n    </Link>\n  ) : (\n    <span>{children}</span>\n  )\n\nLinkWrapper.propTypes = {\n  link: PropTypes.bool,\n  newTab: PropTypes.bool,\n  to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),\n}\n\n// ===================================================================\n\nexport const Pool = decorate([\n  connectStore(() => ({\n    pool: createGetObject(),\n  })),\n  ({ id, pool, link, newTab }) => {\n    if (pool === undefined) {\n      return unknowItem(id, 'pool')\n    }\n\n    return (\n      <LinkWrapper link={link} newTab={newTab} to={`/pools/${pool.id}`}>\n        <Icon icon='pool' /> {pool.name_label}\n      </LinkWrapper>\n    )\n  },\n])\n\nPool.propTypes = {\n  id: PropTypes.string.isRequired,\n  link: PropTypes.bool,\n  newTab: PropTypes.bool,\n}\n\nPool.defaultProps = {\n  link: false,\n  newTab: false,\n}\n\n// ===================================================================\n\nexport const Host = decorate([\n  connectStore(() => {\n    const getHost = createGetObject()\n    return {\n      host: getHost,\n      pool: createGetObject(\n        createSelector(\n          getHost,\n          (_, props) => props.pool,\n          (host, showPool) => showPool && get(() => host.$pool)\n        )\n      ),\n    }\n  }),\n  ({ id, host, pool, link, newTab, memoryFree }) => {\n    if (host === undefined) {\n      return unknowItem(id, 'host')\n    }\n\n    return (\n      <LinkWrapper link={link} newTab={newTab} to={`/hosts/${host.id}`}>\n        <Icon icon='host' /> {host.name_label}\n        {memoryFree && (\n          <span>\n            {' ('}\n            {_('memoryFree', {\n              memoryFree: formatSize(host.memory.size - host.memory.usage),\n            })}\n            )\n          </span>\n        )}\n        {pool !== undefined && <span>{` - ${pool.name_label}`}</span>}\n      </LinkWrapper>\n    )\n  },\n])\n\nHost.propTypes = {\n  id: PropTypes.string.isRequired,\n  link: PropTypes.bool,\n  memoryFree: PropTypes.bool,\n  newTab: PropTypes.bool,\n  pool: PropTypes.bool,\n}\n\nHost.defaultProps = {\n  link: false,\n  memoryFree: false,\n  newTab: false,\n  pool: true,\n}\n\n// ===================================================================\n\nexport const Vm = decorate([\n  connectStore(() => {\n    const getVm = createGetObject()\n    return {\n      vm: getVm,\n      container: createGetObject(createSelector(getVm, vm => get(() => vm.$container))),\n    }\n  }),\n  ({ id, vm, container, link, newTab, name }) => {\n    if (vm === undefined) {\n      return unknowItem(id, 'VM', name)\n    }\n\n    return (\n      <LinkWrapper link={link} newTab={newTab} to={`/vms/${vm.id}`}>\n        <Icon icon={isEmpty(vm.current_operations) ? `vm-${vm.power_state.toLowerCase()}` : 'vm-busy'} />{' '}\n        {vm.name_label}\n        {container !== undefined && ` (${container.name_label})`}\n      </LinkWrapper>\n    )\n  },\n])\n\nVm.propTypes = {\n  id: PropTypes.string.isRequired,\n  link: PropTypes.bool,\n  name: PropTypes.string,\n  newTab: PropTypes.bool,\n}\n\nVm.defaultProps = {\n  link: false,\n  newTab: false,\n}\n\n// ===================================================================\n\nexport const VmTemplate = decorate([\n  connectStore(() => {\n    const getObject = createGetObject()\n    const getPool = createGetObject(createSelector(getObject, vm => get(() => vm.$pool)))\n    return (state, props) => ({\n      // FIXME: props.self ugly workaround to get object as a self user\n      template: getObject(state, props, props.self),\n      pool: getPool(state, props),\n    })\n  }),\n  ({ id, template, pool }) => {\n    if (template === undefined) {\n      return unknowItem(id, 'template')\n    }\n\n    return (\n      <span>\n        <Icon icon='vm' /> {template.name_label}\n        {pool !== undefined && <span className='text-muted'>{` - ${pool.name_label}`}</span>}\n      </span>\n    )\n  },\n])\n\nVmTemplate.propTypes = {\n  id: PropTypes.string.isRequired,\n  self: PropTypes.bool,\n}\n\nVmTemplate.defaultProps = {\n  link: false,\n  newTab: false,\n  self: false,\n}\n\n// ===================================================================\n\nexport const Sr = decorate([\n  connectStore(() => {\n    const getSr = createGetObject()\n    const getContainer = createGetObject(\n      createSelector(\n        getSr,\n        (_, props) => props.container,\n        (sr, showContainer) => showContainer && get(() => sr.$container)\n      )\n    )\n    return (state, props) => ({\n      // FIXME: props.self ugly workaround to get object as a self user\n      sr: getSr(state, props, props.self),\n      container: getContainer(state, props),\n    })\n  }),\n  ({ id, sr, container, link, newTab, spaceLeft, self, name }) => {\n    if (sr === undefined) {\n      return unknowItem(id, 'SR', name)\n    }\n\n    return (\n      <LinkWrapper link={link} newTab={newTab} to={`/srs/${sr.id}`}>\n        <Icon icon='sr' /> {sr.name_label}\n        {!self && spaceLeft && isSrWritable(sr) && (\n          <span className={!link && 'text-muted'}>\n            {` (${formatSize(sr.size - sr.physical_usage)} free`}\n            {sr.allocationStrategy !== undefined && ` - ${sr.allocationStrategy}`})\n          </span>\n        )}\n        {!self && container !== undefined && <span className={!link && 'text-muted'}> - {container.name_label}</span>}\n      </LinkWrapper>\n    )\n  },\n])\n\nSr.propTypes = {\n  container: PropTypes.bool,\n  id: PropTypes.string.isRequired,\n  link: PropTypes.bool,\n  name: PropTypes.string,\n  newTab: PropTypes.bool,\n  self: PropTypes.bool,\n  spaceLeft: PropTypes.bool,\n}\n\nSr.defaultProps = {\n  container: true,\n  link: false,\n  newTab: false,\n  self: false,\n  spaceLeft: true,\n}\n\n// ===================================================================\n\nexport const Vdi = decorate([\n  connectStore(() => {\n    const getObject = createGetObject()\n    const getSr = createGetObject((state, props) => {\n      const vdi = getObject(state, props, props.self)\n      return vdi && vdi.$SR\n    })\n    // FIXME: props.self ugly workaround to get object as a self user\n    return (state, props) => ({\n      vdi: getObject(state, props, props.self),\n      sr: getSr(state, props),\n    })\n  }),\n  ({ id, showSize, showSr, sr, vdi }) => {\n    if (vdi === undefined) {\n      return unknowItem(id, 'VDI')\n    }\n\n    return (\n      <span>\n        <Icon icon='disk' /> {vdi.name_label}\n        {sr !== undefined && showSr && <span className='text-muted'> - {sr.name_label}</span>}\n        {showSize && <span className='text-muted'> ({formatSize(vdi.size)})</span>}\n      </span>\n    )\n  },\n])\n\nVdi.propTypes = {\n  id: PropTypes.string.isRequired,\n  self: PropTypes.bool,\n  showSize: PropTypes.bool,\n}\n\nVdi.defaultProps = {\n  self: false,\n  showSize: false,\n  showSr: false,\n}\n\n// ===================================================================\n\nexport const Pif = decorate([\n  connectStore(() => {\n    const getObject = createGetObject()\n    const getNetwork = createGetObject(createSelector(getObject, pif => get(() => pif.$network)))\n\n    // FIXME: props.self ugly workaround to get object as a self user\n    return (state, props) => ({\n      pif: getObject(state, props, props.self),\n      network: getNetwork(state, props),\n    })\n  }),\n  ({ id, showNetwork, pif, network }) => {\n    if (pif === undefined) {\n      return unknowItem(id, 'PIF')\n    }\n\n    const { carrier, device, deviceName, vlan } = pif\n    const showExtraInfo = deviceName || vlan !== -1 || (showNetwork && network !== undefined)\n\n    return (\n      <span>\n        <Icon icon='network' color={carrier ? 'text-success' : 'text-danger'} /> {device}\n        {showExtraInfo && (\n          <span>\n            {' '}\n            ({deviceName}\n            {vlan !== -1 && (\n              <span>\n                {' '}\n                -{' '}\n                {_('keyValue', {\n                  key: _('pifVlanLabel'),\n                  value: vlan,\n                })}\n              </span>\n            )}\n            {showNetwork && network !== undefined && <span> - {network.name_label}</span>})\n          </span>\n        )}\n      </span>\n    )\n  },\n])\n\nPif.propTypes = {\n  id: PropTypes.string.isRequired,\n  self: PropTypes.bool,\n  showNetwork: PropTypes.bool,\n}\n\nPif.defaultProps = {\n  self: false,\n  showNetwork: false,\n}\n\n// ===================================================================\n\nexport const Network = decorate([\n  connectStore(() => {\n    const getObject = createGetObject()\n    const getPool = createGetObject(createSelector(getObject, network => get(() => network.$pool)))\n\n    // FIXME: props.self ugly workaround to get object as a self user\n    return (state, props) => ({\n      network: getObject(state, props, props.self),\n      pool: getPool(state, props),\n    })\n  }),\n  ({ id, network, pool }) => {\n    if (network === undefined) {\n      return unknowItem(id, 'network')\n    }\n\n    return (\n      <span>\n        <Icon icon='network' /> {network.name_label}\n        {pool !== undefined && <span className='text-muted'>{` - ${pool.name_label}`}</span>}\n      </span>\n    )\n  },\n])\n\nNetwork.propTypes = {\n  id: PropTypes.string.isRequired,\n  self: PropTypes.bool,\n}\n\nNetwork.defaultProps = {\n  self: false,\n}\n\n// ===================================================================\n\nexport const Remote = decorate([\n  addSubscriptions(({ id }) => ({\n    remote: cb => subscribeRemotes(remotes => cb(find(remotes, { id }))),\n  })),\n  ({ id, remote, link, newTab }) => {\n    if (remote === undefined) {\n      return unknowItem(id, 'remote') // TODO: handle remotes not fetched yet\n    }\n\n    return (\n      <LinkWrapper link={link} newTab={newTab} to='/settings/remotes'>\n        <Icon icon='remote' /> {remote.name}\n      </LinkWrapper>\n    )\n  },\n])\n\nRemote.propTypes = {\n  id: PropTypes.string.isRequired,\n  link: PropTypes.bool,\n  newTab: PropTypes.bool,\n}\n\nRemote.defaultProps = {\n  link: false,\n  newTab: false,\n}\n\n// ===================================================================\n\nexport const Proxy = decorate([\n  addSubscriptions(({ id }) => ({\n    proxy: cb => subscribeProxies(proxies => cb(proxies.find(proxy => proxy.id === id))),\n  })),\n  ({ id, proxy, link, newTab }) =>\n    proxy !== undefined ? (\n      <LinkWrapper\n        link={link}\n        newTab={newTab}\n        to={{\n          pathname: '/proxies',\n          query: {\n            s: new CM.Property('id', new CM.String(id)).toString(),\n          },\n        }}\n      >\n        <Icon icon='proxy' /> {proxy.name || proxy.address}\n      </LinkWrapper>\n    ) : (\n      unknowItem(id, 'proxy')\n    ),\n])\n\nProxy.propTypes = {\n  id: PropTypes.string.isRequired,\n  link: PropTypes.bool,\n  newTab: PropTypes.bool,\n}\n\nProxy.defaultProps = {\n  link: false,\n  newTab: false,\n}\n\n// ===================================================================\n\nexport const BackupJob = decorate([\n  addSubscriptions(({ id }) => ({\n    job: cb => subscribeBackupNgJobs(jobs => cb(jobs.find(job => job.id === id))),\n  })),\n  ({ id, job, link, newTab }) => {\n    if (job === undefined) {\n      return unknowItem(id, 'job')\n    }\n\n    return (\n      <LinkWrapper\n        link={link}\n        newTab={newTab}\n        to={`/backup/overview?s=${encodeURIComponent(new CM.Property('id', new CM.String(id)).toString())}`}\n      >\n        {job.name}\n      </LinkWrapper>\n    )\n  },\n])\n\nBackupJob.propTypes = {\n  id: PropTypes.string.isRequired,\n  link: PropTypes.bool,\n  newTab: PropTypes.bool,\n}\n\nBackupJob.defaultProps = {\n  link: false,\n  newTab: false,\n}\n\n// ===================================================================\n\nexport const Schedule = decorate([\n  addSubscriptions(({ id }) => ({\n    schedule: cb => subscribeSchedules(schedules => cb(schedules.find(schedule => schedule.id === id))),\n    backupJobs: subscribeBackupNgJobs,\n    metadataBackupJobs: subscribeMetadataBackupJobs,\n    mirrorBackupJobs: subscribeMirrorBackupJobs,\n  })),\n  provideState({\n    initialState: () => ({}),\n    computed: {\n      job: (_, { backupJobs = [], metadataBackupJobs = [], mirrorBackupJobs = [], schedule }) =>\n        schedule && [...backupJobs, ...metadataBackupJobs, ...mirrorBackupJobs].find(job => job.id === schedule.jobId),\n    },\n  }),\n  injectState,\n  ({ id, schedule, showJob, showState, state: { job } }) => {\n    if (schedule === undefined) {\n      return unknowItem(id, 'schedule')\n    }\n\n    const isEnabled = schedule.enabled\n    const scheduleName = (schedule.name ?? '').trim()\n    const jobName = (job?.name ?? '').trim()\n\n    return (\n      <span>\n        {showState && (\n          <span className={`mr-1 tag tag-${isEnabled ? 'success' : 'danger'}`}>\n            {isEnabled ? _('stateEnabled') : _('stateDisabled')}\n          </span>\n        )}\n        <span>{scheduleName === '' ? <em>{_('unnamedSchedule')}</em> : scheduleName}</span>\n        {showJob && (\n          <span>\n            {' '}\n            ({job ? jobName === '' ? <em>{_('unnamedJob')}</em> : jobName : unknowItem(schedule.jobId, 'job')})\n          </span>\n        )}\n      </span>\n    )\n  },\n])\n\nSchedule.propTypes = {\n  id: PropTypes.string.isRequired,\n  showState: PropTypes.bool,\n  showJob: PropTypes.bool,\n}\n\nSchedule.defaultProps = {\n  showState: true,\n  showJob: true,\n}\n\n// ===================================================================\n\nexport const Vgpu = connectStore(() => ({\n  vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),\n}))(({ vgpu, vgpuType }) => (\n  <span>\n    <Icon icon='vgpu' /> {vgpuType.modelName}\n  </span>\n))\n\nVgpu.propTypes = {\n  vgpu: PropTypes.object.isRequired,\n}\n\n// ===================================================================\n\nexport const User = decorate([\n  addSubscriptions(({ id }) => ({\n    user: cb =>\n      subscribeUsers(users => {\n        const user = users.find(user => user.id === id)\n        cb(user === undefined ? null : user)\n      }),\n  })),\n  ({ defaultRender, id, link, newTab, user }) => {\n    if (user === undefined) {\n      return <Icon icon='loading' />\n    }\n    if (user === null) {\n      return defaultRender || unknowItem(id, 'user')\n    }\n    return (\n      <LinkWrapper\n        link={link}\n        newTab={newTab}\n        to={`/settings/users?s=${encodeURIComponent(new CM.Property('id', new CM.String(id)).toString())}`}\n      >\n        <Icon icon='user' /> {user.email}\n      </LinkWrapper>\n    )\n  },\n])\n\nUser.propTypes = {\n  defaultRender: PropTypes.node,\n  id: PropTypes.string.isRequired,\n  link: PropTypes.bool,\n  newTab: PropTypes.bool,\n}\n\nUser.defaultProps = {\n  link: false,\n  newTab: false,\n}\n\n// ===================================================================\n\nexport const Pci = decorate([\n  connectStore(() => ({\n    pci: createGetObject(),\n  })),\n  ({ pci }) => (\n    <span>\n      {pci.class_name} {pci.device_name} ({pci.pci_id})\n    </span>\n  ),\n])\n\n// ===================================================================\n\nconst xoItemToRender = {\n  // Subscription objects.\n  cloudConfig: template => (\n    <span>\n      <Icon icon='template' /> {template.name}\n    </span>\n  ),\n  group: group => (\n    <span>\n      <Icon icon='group' /> {group.name}\n    </span>\n  ),\n  remote: ({ value: { id } }) => <Remote id={id} />,\n  proxy: ({ id }) => <Proxy id={id} />,\n  role: role => <span>{role.name}</span>,\n  user: ({ id }) => <User id={id} />,\n  resourceSet: resourceSet => (\n    <span>\n      <strong>\n        <Icon icon='resource-set' /> {resourceSet.name}\n      </strong>\n    </span>\n  ),\n  sshKey: key => (\n    <span>\n      <Icon icon='ssh-key' /> {key.label}\n    </span>\n  ),\n  ipPool: ipPool => (\n    <span>\n      <Icon icon='ip' /> {ipPool.name}\n    </span>\n  ),\n  ipAddress: ({ label, used }) => {\n    if (used) {\n      return <strong className='text-warning'>{label}</strong>\n    }\n    return <span>{label}</span>\n  },\n  xoConfig: ({ createdAt }) => (\n    <span>\n      <Icon icon='xo-cloud-config' /> <ShortDate timestamp={createdAt} />\n    </span>\n  ),\n  // XO objects.\n  pool: props => <Pool {...props} />,\n\n  VDI: props => <Vdi {...props} showSr />,\n  'VDI-resourceSet': props => <Vdi {...props} self showSr />,\n\n  // Pool objects.\n  'VM-template': props => <VmTemplate {...props} />,\n  'VM-template-resourceSet': props => <VmTemplate {...props} self />,\n  host: props => <Host {...props} />,\n  network: props => <Network {...props} />,\n  'network-resourceSet': props => <Network {...props} self />,\n\n  // SR.\n  SR: props => <Sr {...props} />,\n  'SR-resourceSet': props => <Sr {...props} self />,\n\n  // VM.\n  VM: props => <Vm {...props} />,\n  'VM-snapshot': props => <Vm {...props} />,\n  'VM-controller': props => (\n    <span>\n      <Icon icon='host' /> <Vm {...props} />\n    </span>\n  ),\n\n  // PIF.\n  PIF: props => <Pif {...props} />,\n\n  // Tags.\n  tag: tag => (\n    <span>\n      <Icon icon='tag' /> {tag.value}\n    </span>\n  ),\n\n  // GPUs\n\n  vgpu: vgpu => <Vgpu vgpu={vgpu} />,\n\n  vgpuType: type => (\n    <span>\n      <Icon icon='gpu' /> {type.modelName} ({type.vendorName}) {type.maxResolutionX}x{type.maxResolutionY}\n    </span>\n  ),\n\n  gpuGroup: group => (\n    <span>{group.name_label.startsWith('Group of ') ? group.name_label.slice(9) : group.name_label}</span>\n  ),\n\n  backup: backup => (\n    <span>\n      <span className='tag tag-info' style={{ textTransform: 'capitalize' }}>\n        {backup.mode === 'delta' ? _('backupIsIncremental') : backup.mode}\n      </span>{' '}\n      {backup.isImmutable && (\n        <span className='tag tag-info'>\n          <Icon icon='lock' />\n        </span>\n      )}{' '}\n      <span className='tag tag-warning'>{backup.remote.name}</span>{' '}\n      {backup.differencingVhds > 0 && (\n        <span className='tag tag-info'>\n          {backup.differencingVhds} {_('backupIsDifferencing')}{' '}\n        </span>\n      )}\n      {backup.dynamicVhds > 0 && (\n        <span className='tag tag-info'>\n          {backup.dynamicVhds} {_('backupisKey')}{' '}\n        </span>\n      )}\n      {backup.withMemory && <span className='tag tag-info'>{_('withMemory')} </span>}\n      {backup.size !== undefined && <span className='tag tag-info'>{formatSize(backup.size)}</span>}{' '}\n      <NumericDate timestamp={backup.timestamp} />\n    </span>\n  ),\n\n  PCI: props => <Pci {...props} self />,\n\n  schedule: props => <Schedule {...props} />,\n\n  job: job => <spans>{job.name}</spans>,\n}\n\nconst renderXoItem = (item, { className, type: xoType, ...props } = {}) => {\n  const { id, label } = item\n  const type = xoType || item.type\n\n  if (item.removed) {\n    return (\n      <span key={id} className='text-danger'>\n        {' '}\n        <Icon icon='alarm' /> {id}\n      </span>\n    )\n  }\n\n  if (!type) {\n    if (process.env.NODE_ENV !== 'production' && !label) {\n      throw new Error(`an item must have at least either a type or a label`)\n    }\n    return (\n      <span key={id} className={className}>\n        {label}\n      </span>\n    )\n  }\n\n  const Component = xoItemToRender[type]\n\n  if (process.env.NODE_ENV !== 'production' && !Component) {\n    throw new Error(`no available component for type ${type}`)\n  }\n\n  if (Component) {\n    return (\n      <span key={id} className={className}>\n        <Component {...item} {...props} />\n      </span>\n    )\n  }\n}\n\nexport { renderXoItem as default }\n\nexport const getRenderXoItemOfType =\n  type =>\n  (item, options = {}) =>\n    renderXoItem(item, { ...options, type })\n\nconst GenericXoItem = connectStore(() => {\n  const getObject = createGetObject()\n\n  return (state, props) => ({\n    xoItem: getObject(state, props),\n  })\n})(({ xoItem, ...props }) => (xoItem ? renderXoItem(xoItem, props) : renderXoUnknownItem()))\n\nexport const renderXoItemFromId = (id, props) => <GenericXoItem {...props} id={id} />\n\nexport const renderXoUnknownItem = () => <span className='text-muted'>{_('errorNoSuchItem')}</span>\n","import _ from 'intl'\nimport * as xoaPlans from 'xoa-plans'\nimport decorate from 'apply-decorators'\nimport defined from '@xen-orchestra/defined'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport stripAnsi from 'strip-ansi'\nimport xoaUpdater from 'xoa-updater'\nimport { checkXoa, getApplianceInfo } from 'xo'\nimport { createBlobFromString } from 'utils'\nimport { createLogger } from '@xen-orchestra/log'\nimport { identity, omit } from 'lodash'\nimport { injectState, provideState } from 'reaclette'\nimport { post } from 'fetch'\nimport { timeout } from 'promise-toolbox'\n\nimport ActionButton from './action-button'\nimport ActionRowButton from './action-row-button'\n\nconst ADDITIONAL_FILES_FETCH_TIMEOUT = 5e3\n\nconst jsonStringify = json => JSON.stringify(json, null, 2)\nconst logger = createLogger('report-bug-button')\n\nconst GITHUB_URL = 'https://github.com/vatesfr/xen-orchestra/issues/new'\nconst GITHUB_BUG_TEMPLATE = 'bug_report.yml'\nconst XO_SUPPORT_URL = 'https://xen-orchestra.com/#!/member/support'\nconst SUPPORT_PANEL_URL = './api/support/create/ticket'\n\nconst ADDITIONAL_FILES = [\n  {\n    fetch: () => xoaUpdater.getLocalManifest().then(jsonStringify),\n    name: 'manifest.json',\n  },\n  {\n    fetch: () => checkXoa().then(stripAnsi),\n    name: 'xoaCheck.txt',\n  },\n  {\n    fetch: () => getApplianceInfo().then(jsonStringify),\n    name: 'xoaInfo.json',\n  },\n]\n\nconst reportInNewWindow = (url, { title, message, formatMessage = identity }) => {\n  const encodedTitle = encodeURIComponent(title == null ? '' : title)\n\n  let _url = `${url}?title=${encodedTitle}`\n\n  if (url === GITHUB_URL) {\n    const encodedErrorMessage = encodeURIComponent(jsonStringify(message.error))\n    const encodedLabels = encodeURIComponent('type: bug :bug:,status: triaging :triangular_flag_on_post:')\n\n    _url += `&template=${GITHUB_BUG_TEMPLATE}&labels=${encodedLabels}&error-message=${encodedErrorMessage}`\n  }\n\n  window.open(_url)\n}\n\nexport const reportOnSupportPanel = async ({ files = [], formatMessage = identity, message, title } = {}) => {\n  const { FormData, open } = window\n\n  const formData = new FormData()\n  if (title !== undefined) {\n    formData.append('title', title)\n  }\n  if (message !== undefined) {\n    formData.append('message', formatMessage(message))\n  }\n  files.forEach(({ content, name }) => {\n    formData.append('attachments', content, name)\n  })\n\n  await Promise.all(\n    ADDITIONAL_FILES.map(({ fetch, name }) =>\n      timeout.call(fetch(), ADDITIONAL_FILES_FETCH_TIMEOUT).then(\n        file => formData.append('attachments', createBlobFromString(file), name),\n        error => logger.warn(`cannot get ${name}`, { error })\n      )\n    )\n  )\n\n  try {\n    const res = await timeout.call(post(SUPPORT_PANEL_URL, formData), 1e4)\n    if (res.status !== 200) {\n      throw new Error(`not a valid response status (${res.status})`)\n    }\n    open(await res.text())\n  } catch (error) {\n    logger.warn('cannot get the new ticket URL', { error })\n    reportInNewWindow(XO_SUPPORT_URL, {\n      title: defined(title, 'Bug report'),\n      message,\n      formatMessage,\n    })\n  }\n}\n\nexport const reportBug =\n  xoaPlans.CURRENT === xoaPlans.SOURCES ? params => reportInNewWindow(GITHUB_URL, params) : reportOnSupportPanel\n\nconst REPORT_BUG_BUTTON_PROP_TYPES = {\n  files: PropTypes.arrayOf(\n    PropTypes.shape({\n      content: PropTypes.oneOfType([PropTypes.instanceOf(window.Blob), PropTypes.instanceOf(window.File)]).isRequired,\n      name: PropTypes.string.isRequired,\n    })\n  ),\n  formatMessage: PropTypes.func,\n  message: PropTypes.string,\n  rowButton: PropTypes.bool,\n  title: PropTypes.string,\n}\n\nconst ReportBugButton = decorate([\n  provideState({\n    effects: {\n      async reportBug() {\n        const { files, formatMessage, message, title } = this.props\n        await reportBug({\n          files,\n          formatMessage,\n          message,\n          title,\n        })\n      },\n    },\n    computed: {\n      Button: (state, { rowButton }) => (rowButton ? ActionRowButton : ActionButton),\n      buttonProps: (state, props) => omit(props, Object.keys(REPORT_BUG_BUTTON_PROP_TYPES)),\n    },\n  }),\n  injectState,\n  ({ state, effects }) => (\n    <state.Button {...state.buttonProps} handler={effects.reportBug} icon='bug' tooltip={_('reportBug')} />\n  ),\n])\n\nReportBugButton.propTypes = REPORT_BUG_BUTTON_PROP_TYPES\n\nexport { ReportBugButton as default }\n","import _, { messages } from 'intl'\nimport ChartistGraph from 'react-chartist'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { Container, Row, Col } from 'grid'\nimport { forEach, map } from 'lodash'\nimport { injectIntl } from 'react-intl'\n\nimport Component from './base-component'\nimport Icon from './icon'\nimport { createSelector } from './selectors'\nimport { formatSize } from './utils'\n\n// ===================================================================\n\nconst RESOURCES = ['disk', 'memory', 'cpus']\n\n// ===================================================================\n\n@injectIntl\nexport default class ResourceSetQuotas extends Component {\n  static propTypes = {\n    limits: PropTypes.object.isRequired,\n  }\n\n  _getQuotas = createSelector(\n    () => this.props.limits,\n    limits => {\n      const quotas = {}\n\n      forEach(RESOURCES, resource => {\n        if (limits[resource] != null) {\n          const { total, usage } = limits[resource]\n          quotas[resource] = {\n            total,\n            usage,\n          }\n        }\n      })\n\n      return quotas\n    }\n  )\n\n  render() {\n    const {\n      intl: { formatMessage },\n    } = this.props\n    const labels = [formatMessage(messages.availableResourceLabel), formatMessage(messages.usedResourceLabel)]\n    const { cpus, disk, memory } = this._getQuotas()\n    const quotas = [\n      {\n        header: (\n          <span>\n            <Icon icon='cpu' /> {_('cpuStatePanel')}\n          </span>\n        ),\n        validFormat: true,\n        quota: cpus,\n      },\n      {\n        header: (\n          <span>\n            <Icon icon='memory' /> {_('memoryStatePanel')}\n          </span>\n        ),\n        validFormat: false,\n        quota: memory,\n      },\n      {\n        header: (\n          <span>\n            <Icon icon='disk' /> {_('srUsageStatePanel')}\n          </span>\n        ),\n        validFormat: false,\n        quota: disk,\n      },\n    ]\n    return (\n      <Container>\n        <Row>\n          {map(quotas, ({ header, validFormat, quota }, key) => (\n            <Col key={key} mediumSize={4}>\n              <Card>\n                <CardHeader>{header}</CardHeader>\n                <CardBlock className='text-center'>\n                  {quota !== undefined ? (\n                    <div>\n                      {Number.isFinite(quota.total) ? (\n                        <ChartistGraph\n                          data={{\n                            labels,\n                            series: [quota.total - quota.usage, quota.usage],\n                          }}\n                          options={{\n                            donut: true,\n                            donutWidth: 40,\n                            showLabel: false,\n                          }}\n                          type='Pie'\n                        />\n                      ) : (\n                        <p className='text-xs-center display-1'>&infin;</p>\n                      )}\n                      <p className='text-xs-center'>\n                        {!Number.isFinite(quota.total)\n                          ? _('unlimitedResourceSetUsage', {\n                              usage: validFormat ? quota.usage?.toString() : formatSize(quota.usage),\n                            })\n                          : _('resourceSetQuota', {\n                              total: validFormat ? quota.total?.toString() : formatSize(quota.total),\n                              usage: validFormat ? quota.usage?.toString() : formatSize(quota.usage),\n                            })}\n                      </p>\n                    </div>\n                  ) : (\n                    <p className='text-xs-center display-1'>&infin;</p>\n                  )}\n                </CardBlock>\n              </Card>\n            </Col>\n          ))}\n        </Row>\n      </Container>\n    )\n  }\n}\n","import React from 'react'\nimport { connectStore } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\nimport PropTypes from 'prop-types'\nimport decorate from 'apply-decorators'\nimport { injectState, provideState } from 'reaclette'\nimport Copiable from 'copiable'\nimport { flatMap } from 'lodash'\nimport Link from 'link'\nimport store from 'store'\n\n/**\n * TODO : check user permissions on objects retrieved by refs, if using the component in non-admin pages\n */\nconst RichText = decorate([\n  connectStore({\n    vms: createGetObjectsOfType('VM'),\n    hosts: createGetObjectsOfType('host'),\n    pools: createGetObjectsOfType('pool'),\n    srs: createGetObjectsOfType('SR'),\n  }),\n  provideState({\n    computed: {\n      idToLink: (_, props) => {\n        const regex = /\\b(?:OpaqueRef:)?[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\\b/g\n        const parts = props.message.split(regex)\n        const ids = props.message.match(regex) || []\n        const { objects } = store.getState()\n\n        return flatMap(parts, (part, index) => {\n          // If on last part, return only the part without adding Copiable component\n          if (index === ids.length) {\n            return part\n          }\n\n          const id = ids[index]\n          let _object\n\n          for (const collection of [props.vms, props.hosts, props.pools, props.srs]) {\n            _object = id.startsWith('OpaqueRef:') ? objects.byRef.get(id) : collection[id]\n\n            if (_object !== undefined) break\n          }\n\n          if (_object !== undefined && ['VM', 'host', 'pool', 'SR'].includes(_object.type)) {\n            return [\n              part,\n              <Link key={index} to={`/${_object.type.toLowerCase()}s/${_object.uuid}`}>\n                {id}\n              </Link>,\n            ]\n          } else {\n            return [part, <Copiable key={index}>{id}</Copiable>]\n          }\n        })\n      },\n    },\n  }),\n  injectState,\n  ({ state: { idToLink }, copiable, message }) =>\n    copiable ? (\n      <Copiable tagName='pre' data={message}>\n        {idToLink}\n      </Copiable>\n    ) : (\n      <pre>{idToLink}</pre>\n    ),\n])\n\nRichText.propTypes = {\n  message: PropTypes.string,\n  copiable: PropTypes.bool,\n}\n\nRichText.defaultProps = {\n  copiable: false,\n}\n\nexport default RichText\n","import classNames from 'classnames'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { createSchedule } from '@xen-orchestra/cron'\nimport { FormattedDate, FormattedTime } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport { flatten, forEach, identity, map, sortedIndex } from 'lodash'\n\nimport _ from './intl'\nimport Button from './button'\nimport Component from './base-component'\nimport decorate from './apply-decorators'\nimport TimezonePicker from './timezone-picker'\nimport Tooltip from './tooltip'\nimport { Card, CardHeader, CardBlock } from './card'\nimport { Col, Row } from './grid'\nimport { getXoServerTimezone } from './xo'\nimport { Range, Toggle } from './form'\n\n// ===================================================================\n\nconst CLICKABLE = { cursor: 'pointer' }\nconst PREVIEW_SLIDER_STYLE = { width: '400px' }\n\n// ===================================================================\n\nconst UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']\n\nconst MIN_PREVIEWS = 5\nconst MAX_PREVIEWS = 20\n\nconst MONTHS = [\n  [0, 1, 2],\n  [3, 4, 5],\n  [6, 7, 8],\n  [9, 10, 11],\n]\n\nconst DAYS = (() => {\n  const days = []\n\n  for (let i = 0; i < 4; i++) {\n    days[i] = []\n\n    for (let j = 1; j < 8; j++) {\n      days[i].push(7 * i + j)\n    }\n  }\n\n  days.push([29, 30, 31])\n\n  return days\n})()\n\nconst WEEK_DAYS = [[0, 1, 2], [3, 4, 5], [6]]\n\nconst HOURS = (() => {\n  const hours = []\n\n  for (let i = 0; i < 4; i++) {\n    hours[i] = []\n\n    for (let j = 0; j < 6; j++) {\n      hours[i].push(6 * i + j)\n    }\n  }\n\n  return hours\n})()\n\nconst MINS = (() => {\n  const minutes = []\n\n  for (let i = 0; i < 6; i++) {\n    minutes[i] = []\n\n    for (let j = 0; j < 10; j++) {\n      minutes[i].push(10 * i + j)\n    }\n  }\n\n  return minutes\n})()\n\nconst PICKTIME_TO_ID = {\n  minute: 0,\n  hour: 1,\n  monthDay: 2,\n  month: 3,\n  weekDay: 4,\n}\n\nconst TIME_FORMAT = {\n  weekday: 'long',\n  year: 'numeric',\n  month: 'long',\n  day: 'numeric',\n  hour: 'numeric',\n  minute: 'numeric',\n}\n\n// ===================================================================\n\n// monthNum: [ 0 : 11 ]\nconst getMonthName = monthNum => <FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />\n\n// dayNum: [ 0 : 6 ]\nconst getDayName = dayNum => (\n  // January, 1970, 5th => Monday\n  <FormattedDate value={Date.UTC(1970, 0, 4 + dayNum)} weekday='long' timeZone='UTC' />\n)\n\n// ===================================================================\n\nexport class SchedulePreview extends Component {\n  static propTypes = {\n    cronPattern: PropTypes.string.isRequired,\n    timezone: PropTypes.string,\n  }\n\n  componentDidMount() {\n    getXoServerTimezone.then(serverTimezone => {\n      this.setState({\n        defaultTimezone: serverTimezone,\n      })\n    })\n  }\n\n  render() {\n    const { defaultTimezone, value } = this.state\n    const { cronPattern, timezone = defaultTimezone } = this.props\n    const dates = createSchedule(cronPattern, timezone).next(value)\n\n    return (\n      <div>\n        <div className='alert alert-info' role='alert'>\n          {_('cronPattern')} <strong>{cronPattern}</strong>\n        </div>\n        <div className='mb-1' style={PREVIEW_SLIDER_STYLE}>\n          <Range\n            min={MIN_PREVIEWS}\n            max={MAX_PREVIEWS}\n            onChange={this.linkState('value')}\n            value={value && +value}\n            required\n          />\n        </div>\n        <ul className='list-group'>\n          {map(dates, (date, id) => (\n            <li className='list-group-item' key={id}>\n              <FormattedTime value={date} {...TIME_FORMAT} />\n            </li>\n          ))}\n          <li className='list-group-item'>...</li>\n        </ul>\n      </div>\n    )\n  }\n}\n\n// ===================================================================\n\nclass ToggleTd extends Component {\n  static propTypes = {\n    children: PropTypes.any.isRequired,\n    onChange: PropTypes.func.isRequired,\n    tdId: PropTypes.number.isRequired,\n    value: PropTypes.bool.isRequired,\n  }\n\n  _onClick = () => {\n    const { props } = this\n    props.onChange(props.tdId, !props.value)\n  }\n\n  render() {\n    const { props } = this\n    return (\n      <td\n        className={classNames('text-xs-center', props.value && 'table-success')}\n        onClick={this._onClick}\n        style={CLICKABLE}\n      >\n        {props.children}\n      </td>\n    )\n  }\n}\n\n// ===================================================================\n\nconst TableSelect = decorate([\n  provideState({\n    effects: {\n      onChange:\n        (_, tdId, add) =>\n        (_, { value, onChange, options }) => {\n          let newValue = [...value]\n          const index = sortedIndex(newValue, tdId)\n          if (add) {\n            newValue[index] !== tdId && newValue.splice(index, 0, tdId)\n          } else if (newValue[index] === tdId) {\n            if (newValue.length > 1) {\n              newValue.splice(index, 1)\n            } else {\n              newValue = [options[0][0]]\n            }\n          }\n          onChange(newValue)\n        },\n      selectAll:\n        () =>\n        ({ optionsValues }, { onChange }) => {\n          onChange(optionsValues)\n        },\n    },\n    computed: {\n      optionsValues: (_, { options }) => flatten(options),\n    },\n  }),\n  injectState,\n  ({ state, effects, labelId, options, optionRenderer = identity, value }) => {\n    let k = 0\n    return (\n      <div>\n        <table className='table table-bordered table-sm'>\n          <tbody>\n            {map(options, (line, i) => (\n              <tr key={i}>\n                {map(line, tdOption => (\n                  <ToggleTd\n                    children={optionRenderer(tdOption)}\n                    tdId={tdOption}\n                    key={tdOption}\n                    onChange={effects.onChange}\n                    value={k < value.length && value[k] === tdOption && (++k, true)}\n                  />\n                ))}\n              </tr>\n            ))}\n          </tbody>\n        </table>\n        <Button className='pull-right' onClick={effects.selectAll}>\n          {_(`selectTableAll${labelId}`)}\n        </Button>\n      </div>\n    )\n  },\n])\n\nTableSelect.propTypes = {\n  labelId: PropTypes.string.isRequired,\n  options: PropTypes.array.isRequired,\n  optionRenderer: PropTypes.func,\n  onChange: PropTypes.func.isRequired,\n  value: PropTypes.array.isRequired,\n}\n\n// ===================================================================\n\nconst TimePicker = decorate([\n  provideState({\n    effects: {\n      onChange:\n        (_, value) =>\n        ({ optionsValues }, { onChange }) => {\n          if (Array.isArray(value)) {\n            value = value.length === optionsValues.length ? '*' : value.join(',')\n          } else {\n            value = `*/${value}`\n          }\n\n          onChange(value)\n        },\n    },\n    computed: {\n      maxStep: ({ optionsValues }) => Math.floor(optionsValues.length / 2),\n      optionsValues: (_, { options }) => flatten(options),\n\n      // '*' or '*/1' => all values\n      // '2,7' => [2,7]\n      // '*/2' => [min + 2 * 0, min + 2 * 1, ..., min + 2 * n <= max]\n      tableValue: ({ optionsValues, step }, { value }) =>\n        step === 1\n          ? optionsValues\n          : step !== undefined\n            ? optionsValues.filter((_, i) => i % step === 0)\n            : value.split(',').map(Number),\n\n      // '*' => 1\n      // '*/2' => 2\n      // otherwise => undefined\n      step: (_, { value }) => (value === '*' ? 1 : value.indexOf('/') === 1 ? +value.split('/')[1] : undefined),\n    },\n  }),\n  injectState,\n  ({ state, effects, ...props }) => (\n    <Card>\n      <CardHeader>\n        {_(`scheduling${props.labelId}`)}\n        {props.headerAddon}\n      </CardHeader>\n      <CardBlock>\n        <TableSelect\n          labelId={props.labelId}\n          onChange={effects.onChange}\n          optionRenderer={props.optionRenderer}\n          options={props.options}\n          value={state.tableValue}\n        />\n        <Range max={state.maxStep} min={1} onChange={effects.onChange} value={state.step} />\n      </CardBlock>\n    </Card>\n  ),\n])\n\nTimePicker.propTypes = {\n  headerAddon: PropTypes.node,\n  labelId: PropTypes.string.isRequired,\n  onChange: PropTypes.func.isRequired,\n  optionRenderer: PropTypes.func,\n  options: PropTypes.array.isRequired,\n  value: PropTypes.string.isRequired,\n}\n\nconst isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {\n  if (monthDayPattern === '*' && weekDayPattern === '*') {\n    return\n  }\n\n  return weekDayPattern !== '*'\n}\n\nclass DayPicker extends Component {\n  static propTypes = {\n    monthDayPattern: PropTypes.string.isRequired,\n    weekDayPattern: PropTypes.string.isRequired,\n  }\n\n  state = {\n    weekDayMode: isWeekDayMode(this.props),\n  }\n\n  componentWillReceiveProps(props) {\n    const weekDayMode = isWeekDayMode(props)\n\n    if (weekDayMode !== undefined) {\n      this.setState({ weekDayMode })\n    }\n  }\n\n  _setWeekDayMode = weekDayMode => {\n    this.props.onChange(['*', '*'])\n    this.setState({ weekDayMode })\n  }\n\n  _onChange = cron => {\n    this.props.onChange(this.state.weekDayMode ? ['*', cron] : [cron, '*'])\n  }\n\n  render() {\n    const { monthDayPattern, weekDayPattern } = this.props\n    const { weekDayMode } = this.state\n\n    const dayModeToggle = (\n      <Tooltip content={_(weekDayMode ? 'schedulingSetMonthDayMode' : 'schedulingSetWeekDayMode')}>\n        <span className='pull-right'>\n          <Toggle onChange={this._setWeekDayMode} iconSize={1} value={weekDayMode} />\n        </span>\n      </Tooltip>\n    )\n\n    return (\n      <TimePicker\n        headerAddon={dayModeToggle}\n        key={weekDayMode ? 'week' : 'month'}\n        labelId='Day'\n        onChange={this._onChange}\n        optionRenderer={weekDayMode ? getDayName : undefined}\n        options={weekDayMode ? WEEK_DAYS : DAYS}\n        value={weekDayMode ? weekDayPattern : monthDayPattern}\n      />\n    )\n  }\n}\n\n// ===================================================================\n\nexport default class Scheduler extends Component {\n  static propTypes = {\n    cronPattern: PropTypes.string,\n    onChange: PropTypes.func,\n    timezone: PropTypes.string,\n    value: PropTypes.shape({\n      cronPattern: PropTypes.string.isRequired,\n      timezone: PropTypes.string,\n    }),\n  }\n\n  constructor(props) {\n    super(props)\n\n    this._onCronChange = newCrons => {\n      const cronPattern = this._getCronPattern().split(' ')\n      forEach(newCrons, (cron, unit) => {\n        cronPattern[PICKTIME_TO_ID[unit]] = cron\n      })\n\n      this.props.onChange({\n        cronPattern: cronPattern.join(' '),\n        timezone: this._getTimezone(),\n      })\n    }\n\n    forEach(UNITS, unit => {\n      this[`_${unit}Change`] = cron => this._onCronChange({ [unit]: cron })\n    })\n    this._dayChange = ([monthDay, weekDay]) => this._onCronChange({ monthDay, weekDay })\n  }\n\n  _onTimezoneChange = timezone => {\n    this.props.onChange({\n      cronPattern: this._getCronPattern(),\n      timezone,\n    })\n  }\n\n  _getCronPattern = () => {\n    const { value, cronPattern = value.cronPattern } = this.props\n    return cronPattern\n  }\n\n  _getTimezone = () => {\n    const { value, timezone = value && value.timezone } = this.props\n    return timezone\n  }\n\n  render() {\n    const cronPatternArr = this._getCronPattern().split(' ')\n    const timezone = this._getTimezone()\n\n    return (\n      <div className='card-block'>\n        <em>\n          <Icon icon='info' /> {_('scheduleDstWarning')}\n        </em>\n        <Row>\n          <Col largeSize={6}>\n            <TimePicker\n              labelId='Month'\n              optionRenderer={getMonthName}\n              options={MONTHS}\n              onChange={this._monthChange}\n              value={cronPatternArr[PICKTIME_TO_ID.month]}\n            />\n          </Col>\n          <Col largeSize={6}>\n            <DayPicker\n              onChange={this._dayChange}\n              monthDayPattern={cronPatternArr[PICKTIME_TO_ID.monthDay]}\n              weekDayPattern={cronPatternArr[PICKTIME_TO_ID.weekDay]}\n            />\n          </Col>\n        </Row>\n        <Row>\n          <Col largeSize={6}>\n            <TimePicker\n              labelId='Hour'\n              options={HOURS}\n              onChange={this._hourChange}\n              value={cronPatternArr[PICKTIME_TO_ID.hour]}\n            />\n          </Col>\n          <Col largeSize={6}>\n            <TimePicker\n              labelId='Minute'\n              options={MINS}\n              onChange={this._minuteChange}\n              value={cronPatternArr[PICKTIME_TO_ID.minute]}\n            />\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <hr />\n            <TimezonePicker value={timezone} onChange={this._onTimezoneChange} />\n          </Col>\n        </Row>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport classNames from 'classnames'\nimport DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom\nimport DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom\nimport PropTypes from 'prop-types'\nimport React, { Component } from 'react'\nimport { Dropdown, MenuItem } from 'react-bootstrap-4/lib'\nimport { Input as DebouncedInput } from 'debounce-input-decorator'\nimport { isEmpty, map } from 'lodash'\n\nimport Button from './button'\nimport Icon from './icon'\nimport Tooltip from './tooltip'\n\nexport default class SearchBar extends Component {\n  static propTypes = {\n    filters: PropTypes.object,\n    onChange: PropTypes.func.isRequired,\n    value: PropTypes.string.isRequired,\n  }\n\n  _cleanFilter = () => this._setFilter('')\n\n  _setFilter = filterValue => {\n    const filter = this.refs.filter.getWrappedInstance()\n    filter.value = filterValue\n    filter.focus()\n    this.props.onChange(filterValue)\n  }\n\n  _onChange = event => {\n    this.props.onChange(event.target.value)\n  }\n\n  focus() {\n    this.refs.filter.getWrappedInstance().focus()\n  }\n\n  render() {\n    const { props } = this\n\n    return (\n      <div className={classNames('input-group', props.className)}>\n        {isEmpty(props.filters) ? (\n          <span className='input-group-addon'>\n            <Icon icon='search' />\n          </span>\n        ) : (\n          <span className='input-group-btn'>\n            <Dropdown id='filter'>\n              <DropdownToggle bsStyle='info'>\n                <Icon icon='search' />\n              </DropdownToggle>\n              <DropdownMenu>\n                {map(props.filters, (filter, label) => (\n                  <MenuItem key={label} onClick={() => this._setFilter(filter)}>\n                    {_(label)}\n                  </MenuItem>\n                ))}\n              </DropdownMenu>\n            </Dropdown>\n          </span>\n        )}\n        <DebouncedInput className='form-control' onChange={this._onChange} ref='filter' value={props.value} />\n        <Tooltip content={_('filterSyntaxLinkTooltip')}>\n          <a\n            className='input-group-addon'\n            href='https://docs.xen-orchestra.com/manage_infrastructure#live-filter-searchh'\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._cleanFilter}>\n            <Icon icon='clear-search' />\n          </Button>\n        </span>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { confirm } from 'modal'\nimport { createGetObject } from 'selectors'\nimport { injectState, provideState } from 'reaclette'\nimport { satisfies as versionSatisfies } from 'semver'\n\nimport { connectStore, noop } from './utils'\n\n// XAPI values should be lowercased\nconst VM_BOOT_FIRMWARES = ['bios', 'uefi']\n\nconst SelectBootFirmware = decorate([\n  connectStore({\n    host: createGetObject((_, props) => props.host),\n  }),\n  provideState({\n    effects: {\n      handleBootFirmwareChange(__, { target: { value } }) {\n        if (value !== '' && this.props.host !== undefined && versionSatisfies(this.props.host.version, '~8.0')) {\n          // Guest UEFI boot is provided in CH/XCP-ng 8.0 as an experimental feature.\n          // https://docs.citrix.com/en-us/citrix-hypervisor/8-0/downloads/citrix-hypervisor-8.0.pdf#page=10\n          confirm({\n            title: _('vmBootFirmware'),\n            body: _('vmBootFirmwareWarningMessage'),\n          }).then(() => this.props.onChange(value), noop)\n        } else {\n          this.props.onChange(value)\n        }\n      },\n    },\n  }),\n  injectState,\n  ({ effects, value }) => (\n    <select className='form-control' onChange={effects.handleBootFirmwareChange} value={value}>\n      <option value=''>{_('vmDefaultBootFirmwareLabel')}</option>\n      {VM_BOOT_FIRMWARES.map(val => (\n        <option key={val} value={val}>\n          {val}\n        </option>\n      ))}\n    </select>\n  ),\n])\n\nSelectBootFirmware.propTypes = {\n  host: PropTypes.string,\n  onChange: PropTypes.func.isRequired,\n  value: PropTypes.string.isRequired,\n}\n\nexport default SelectBootFirmware\n","import _ from 'intl'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { injectState, provideState } from 'reaclette'\nimport omit from 'lodash/omit.js'\n\nimport decorate from './apply-decorators'\nimport { Select } from './form'\n\nconst OPTIONS = [\n  {\n    label: _('stateDisabled'),\n    value: '',\n  },\n  {\n    label: _('chooseCompressionGzipOption'),\n    value: 'native',\n  },\n]\n\nconst OPTIONS_WITH_ZSTD = [\n  ...OPTIONS,\n  {\n    label: _('chooseCompressionZstdOption'),\n    value: 'zstd',\n  },\n]\n\nconst SELECT_COMPRESSION_PROP_TYPES = {\n  showZstd: PropTypes.bool,\n}\n\nconst SelectCompression = decorate([\n  provideState({\n    computed: {\n      options: (_, { showZstd }) => (showZstd ? OPTIONS_WITH_ZSTD : OPTIONS),\n      selectProps: (_, props) => omit(props, Object.keys(SELECT_COMPRESSION_PROP_TYPES)),\n    },\n  }),\n  injectState,\n  ({ onChange, state, value }) => (\n    <Select labelKey='label' options={state.options} required simpleValue {...state.selectProps} />\n  ),\n])\n\nSelectCompression.defaultProps = { showZstd: true }\nSelectCompression.propTypes = SELECT_COMPRESSION_PROP_TYPES\nexport { SelectCompression as default }\n","import _ from 'intl'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { injectState, provideState } from 'reaclette'\nimport omit from 'lodash/omit.js'\n\nimport decorate from './apply-decorators'\nimport Icon from './icon'\nimport Tooltip from './tooltip'\nimport { Select } from './form'\n\nconst PROP_TYPES = {\n  maxCores: PropTypes.number,\n  maxVcpus: PropTypes.number,\n  value: PropTypes.number,\n}\n\nconst SELECT_STYLE = {\n  display: 'inline-block',\n  fontSize: '1rem',\n  width: '20em',\n}\n\nconst LINE_ITEM_STYLE = {\n  alignItems: 'center',\n  display: 'flex',\n}\n\n// https://github.com/xcp-ng/xenadmin/blob/0160cd0119fae3b871eef656c23e2b76fcc04cb5/XenModel/XenAPI-Extensions/VM.cs#L62\nconst MAX_VM_SOCKETS = 16\n\n// This algorithm was inspired from: https://github.com/xcp-ng/xenadmin/blob/master/XenAdmin/Controls/ComboBoxes/CPUTopologyComboBox.cs#L116\nconst SelectCoresPerSocket = decorate([\n  provideState({\n    computed: {\n      isValidValue: (state, { maxVcpus, value }) =>\n        value == null || (maxVcpus % value === 0 && !state.valueExceedsCoresLimit && !state.valueExceedsSocketsLimit),\n      valueExceedsCoresLimit: (state, { maxCores, value }) => value > maxCores,\n      valueExceedsSocketsLimit: (state, { maxCores, maxVcpus, value }) => maxVcpus / value > MAX_VM_SOCKETS,\n      options: ({ isValidValue }, { maxCores, maxVcpus, value }) => {\n        const options = []\n\n        if (maxCores === undefined || maxVcpus === undefined) {\n          return options\n        }\n\n        const minCores = maxVcpus / MAX_VM_SOCKETS\n\n        // cores per socket must be a divisor of the max vCPUs and must not exceed the cores and sockets limit\n        // e.g: with maxCores = 4, maxSockets = 16 and maxVCPUS = 6\n        // 2 cores per socket is a valid value and 4 cores per socket isn't a valid value\n        for (let coresPerSocket = maxCores; coresPerSocket >= minCores; coresPerSocket--) {\n          if (maxVcpus % coresPerSocket === 0) {\n            options.push({\n              label: _('vmSocketsWithCoresPerSocket', {\n                nSockets: maxVcpus / coresPerSocket,\n                nCores: coresPerSocket,\n              }),\n              value: coresPerSocket,\n            })\n          }\n        }\n\n        if (!isValidValue) {\n          options.push({\n            label: _('vmCoresPerSocket', {\n              nCores: value,\n            }),\n            value,\n          })\n        }\n        return options\n      },\n      selectProps: (_, props) => omit(props, Object.keys(PROP_TYPES)),\n    },\n  }),\n  injectState,\n  ({ maxCores, state, value }) => (\n    <div style={LINE_ITEM_STYLE}>\n      <span style={SELECT_STYLE}>\n        <Select\n          options={state.options}\n          placeholder={_('vmChooseCoresPerSocket')}\n          simpleValue\n          value={value}\n          {...state.selectProps}\n        />\n      </span>\n      &nbsp;\n      {!state.isValidValue && (\n        <Tooltip\n          content={\n            state.valueExceedsCoresLimit\n              ? _('vmCoresPerSocketExceedsCoresLimit', { maxCores })\n              : state.valueExceedsSocketsLimit\n                ? _('vmCoresPerSocketExceedsSocketsLimit', {\n                    maxSockets: MAX_VM_SOCKETS,\n                  })\n                : _('vmCoresPerSocketNotDivisor')\n          }\n        >\n          <Icon icon='error' size='lg' />\n        </Tooltip>\n      )}\n    </div>\n  ),\n])\n\nSelectCoresPerSocket.propTypes = PROP_TYPES\n\nexport { SelectCoresPerSocket as default }\n","import _ from 'intl'\nimport React from 'react'\nimport { injectState, provideState } from 'reaclette'\n\nimport decorate from './apply-decorators'\nimport { Select } from './form'\n\nconst OPTIONS = [\n  {\n    label: 'xva',\n    value: 'xva',\n  },\n  {\n    label: 'ova',\n    value: 'ova',\n  },\n]\n\nconst SelectExportFormat = decorate([\n  provideState({\n    computed: {\n      options: () => OPTIONS,\n      selectProps: (_, props) => props,\n    },\n  }),\n  injectState,\n  ({ onChange, state, value }) => (\n    <Select labelKey='label' valueKey='value' options={state.options} required simpleValue {...state.selectProps} />\n  ),\n])\n\nexport { SelectExportFormat as default }\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport omit from 'lodash/omit.js'\n\nconst STYLE = {\n  marginBottom: 0,\n}\n\nexport default class SelectFiles extends Component {\n  static propTypes = {\n    multi: PropTypes.bool,\n    label: PropTypes.node,\n    onChange: PropTypes.func.isRequired,\n  }\n\n  _onChange = e => {\n    const { multi, onChange } = this.props\n    const { files } = e.target\n\n    onChange(multi ? files : files[0])\n  }\n\n  render() {\n    return (\n      <label className='btn btn-secondary btn-file hidden' style={STYLE}>\n        <Icon icon='file' /> {this.props.label || _('browseFiles')}\n        <input\n          {...omit(this.props, ['hidden', 'label', 'onChange', 'multi'])}\n          hidden\n          onChange={this._onChange}\n          type='file'\n        />\n      </label>\n    )\n  }\n}\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport React from 'react'\nimport { getLicenses } from 'xo'\nimport { injectIntl } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport map from 'lodash/map.js'\n\nimport { renderXoItemFromId } from './render-xo-item'\n\nconst LicenseOptions = ({ license, formatDate }) => {\n  /**\n   * license.productId can be:\n   * - xcpng-enterprise\n   * - xcpng-standard\n   * - xo-proxy\n   * - xostor\n   * - xostor.trial\n   */\n  const productId = license.productId.startsWith('xostor') ? license.productId : license.productId.split('-')[1]\n  return (\n    <option value={license.id}>\n      <span>\n        {productId.charAt(0).toUpperCase() + productId.slice(1)} ({license.id.slice(-4)}),{' '}\n        {license.expires !== undefined ? formatDate(license.expires) : '-'}\n        {license.boundObjectId !== undefined && <span>, {renderXoItemFromId(license.boundObjectId)}</span>}\n      </span>\n    </option>\n  )\n}\n\nconst SelectLicense = decorate([\n  injectIntl,\n  provideState({\n    computed: {\n      licenses: async (state, { productType }) => {\n        try {\n          const availableLicenses = {\n            bound: [],\n            notBound: [],\n          }\n          ;(await getLicenses({ productType })).forEach(license => {\n            if (license.expires === undefined || license.expires > Date.now()) {\n              availableLicenses[license.boundObjectId === undefined ? 'notBound' : 'bound'].push(license)\n            }\n          })\n          return availableLicenses\n        } catch (error) {\n          return { licenseError: error }\n        }\n      },\n    },\n  }),\n  injectState,\n  ({ state: { licenses }, intl: { formatDate }, onChange, showBoundLicenses }) =>\n    licenses?.licenseError !== undefined ? (\n      <span>\n        <em className='text-danger'>{_('getLicensesError')}</em>\n      </span>\n    ) : (\n      <select className='form-control' onChange={onChange}>\n        {_('selectLicense', message => (\n          <option key='none' value='none'>\n            {message}\n          </option>\n        ))}\n\n        {_('notBoundSelectLicense', i18nNotBound => (\n          <optgroup label={i18nNotBound}>\n            {map(licenses?.notBound, license => (\n              <LicenseOptions formatDate={formatDate} key={license.id} license={license} />\n            ))}\n          </optgroup>\n        ))}\n        {showBoundLicenses &&\n          _('boundSelectLicense', i18nBound => (\n            <optgroup label={i18nBound}>\n              {map(licenses?.bound, license => (\n                <LicenseOptions formatDate={formatDate} key={license.id} license={license} />\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    ),\n])\nexport default SelectLicense\n","import decorate from 'apply-decorators'\nimport React from 'react'\nimport PropTypes from 'prop-types'\nimport { injectState, provideState } from 'reaclette'\nimport { parse as parseRemote } from 'xo-remote-parser'\nimport {\n  filter,\n  flatten,\n  forEach,\n  groupBy,\n  includes,\n  isEmpty,\n  isInteger,\n  keyBy,\n  keys,\n  map,\n  mapValues,\n  pick,\n  sortBy,\n  toArray,\n} from 'lodash'\n\nimport _ from './intl'\nimport Button from './button'\nimport EphemeralInput from './ephemeral-input'\nimport Icon from './icon'\nimport renderXoItem from './render-xo-item'\nimport Select from './form/select'\nimport store from './store'\nimport Tooltip from './tooltip'\nimport uncontrollableInput from 'uncontrollable-input'\nimport {\n  createCollectionWrapper,\n  createFilter,\n  createGetObjectsOfType,\n  createGetTags,\n  createSelector,\n  createSort,\n  getObject,\n} from './selectors'\nimport { addSubscriptions, connectStore, resolveResourceSets } from './utils'\nimport {\n  isSrWritable,\n  subscribeBackupNgJobs,\n  subscribeCloudConfigs,\n  subscribeCloudXoConfigBackups,\n  subscribeCurrentUser,\n  subscribeGroups,\n  subscribeIpPools,\n  subscribeJobs,\n  subscribeMetadataBackupJobs,\n  subscribeMirrorBackupJobs,\n  subscribeNetworkConfigs,\n  subscribeProxies,\n  subscribeRemotes,\n  subscribeResourceSets,\n  subscribeRoles,\n  subscribeSchedules,\n  subscribeUsers,\n} from './xo'\nimport { toggleState } from './reaclette-utils'\n\n// ===================================================================\n\n// react-select's line-height is 1.4\n// https://github.com/JedWatson/react-select/blob/916ab0e62fc7394be8e24f22251c399a68de8b1c/less/multi.less#L33\n// while bootstrap button's line-height is 1.25\n// https://github.com/twbs/bootstrap/blob/959c4e527c6ef69623928db638267ba1c370479d/scss/_variables.scss#L342\nconst ADDON_BUTTON_STYLE = { lineHeight: '1.4' }\n\nconst getIds = value =>\n  value == null || typeof value === 'string' || isInteger(value)\n    ? value\n    : Array.isArray(value)\n      ? map(value, getIds)\n      : value.id\n\nconst getOption = (object, container) => ({\n  label: container ? `${getLabel(object)} ${getLabel(container)}` : getLabel(object),\n  value: object.id,\n  xoItem: object,\n})\n\nconst getLabel = object =>\n  object.name_label ||\n  object.name ||\n  object.email ||\n  (object.value && object.value.name) ||\n  object.value ||\n  object.label\n\nconst options = props => ({\n  defaultValue: props.multi ? [] : undefined,\n})\n\nconst getObjectsById = objects => keyBy(Array.isArray(objects) ? objects : flatten(toArray(objects)), 'id')\n\n// ===================================================================\n\n/*\n * WITHOUT xoContainers :\n *\n * xoObjects: [\n *  { type: 'myType', id: 'abc', label: 'First object' },\n *  { type: 'myType', id: 'def', label: 'Second object' }\n * ]\n *\n *\n * WITH xoContainers :\n *\n * xoContainers: [\n *  { type: 'containerType', id: 'ghi', label: 'First container' },\n *  { type: 'containerType', id: 'jkl', label: 'Second container' }\n * ]\n *\n * xoObjects: {\n *  ghi: [\n *    { type: 'objectType', id: 'mno', label: 'First object' }\n *    { type: 'objectType', id: 'pqr', label: 'Second object' }\n *  ],\n *  jkl: [\n *    { type: 'objectType', id: 'stu', label: 'Third object' }\n *  ]\n * }\n */\nclass GenericSelect extends React.Component {\n  static propTypes = {\n    allowMissingObjects: PropTypes.bool,\n    compareContainers: PropTypes.func,\n    compareOptions: PropTypes.func,\n    hasSelectAll: PropTypes.bool,\n    multi: PropTypes.bool,\n    onChange: PropTypes.func.isRequired,\n    xoContainers: PropTypes.array,\n    xoObjects: PropTypes.oneOfType([PropTypes.array, PropTypes.objectOf(PropTypes.array)]).isRequired,\n  }\n\n  _getSelectedIds = createSelector(() => this.props.value, createCollectionWrapper(getIds))\n\n  _getObjects = createSelector(\n    () => this.props.xoContainers !== undefined,\n    () => this.props.xoObjects,\n    this._getSelectedIds,\n    () => !this.props.allowMissingObjects,\n    (withContainers, objects, ids, removed) => {\n      const objectsById = getObjectsById(objects)\n      const missingObjects = []\n      const addIfMissing = id => {\n        if (id != null && !(id in objectsById)) {\n          missingObjects.push({\n            id,\n            label: id,\n            removed,\n            value: id,\n          })\n        }\n      }\n      if (Array.isArray(ids)) {\n        ids.forEach(addIfMissing)\n      } else {\n        addIfMissing(ids)\n      }\n\n      return isEmpty(missingObjects)\n        ? objects\n        : withContainers\n          ? {\n              ...objects,\n              missingObjects,\n            }\n          : [...objects, ...missingObjects]\n    }\n  )\n\n  _getObjectsById = createSelector(this._getObjects, getObjectsById)\n\n  _getOptions = createSelector(\n    () => this.props.xoContainers,\n    this._getObjects,\n    () => this.props.compareContainers,\n    () => this.props.compareOptions,\n    (containers, objects, compareContainers, compareOptions) => {\n      // createCollectionWrapper with a depth?\n      const { name } = this.constructor\n\n      let options\n      if (containers === undefined) {\n        if (__DEV__ && !Array.isArray(objects)) {\n          throw new Error(`${name}: without xoContainers, xoObjects must be an array`)\n        }\n\n        options = (compareOptions !== undefined ? objects.sort(compareOptions) : objects).map(getOption)\n      } else {\n        if (__DEV__ && Array.isArray(objects)) {\n          throw new Error(`${name}: with xoContainers, xoObjects must be an object`)\n        }\n\n        options = []\n        const _containers = compareContainers !== undefined ? containers.sort(compareContainers) : containers\n        forEach(_containers, container => {\n          options.push({\n            disabled: true,\n            xoItem: container,\n          })\n\n          const _objects =\n            compareOptions !== undefined ? objects[container.id].sort(compareOptions) : objects[container.id]\n          forEach(_objects, object => {\n            options.push(getOption(object, container))\n          })\n        })\n\n        // missing objects have \"missingObjects\" as container\n        const { missingObjects } = objects\n        if (missingObjects !== undefined) {\n          missingObjects.forEach(object => {\n            options.push(getOption(object))\n          })\n        }\n      }\n\n      options.map(option => {\n        if (option.xoItem.removed) {\n          option.disabled = true\n        }\n        return option\n      })\n      return options\n    }\n  )\n\n  _getSelectedObjects = (() => {\n    const helper = createSelector(\n      this._getObjectsById,\n      value => value,\n      (objectsById, value) =>\n        Array.isArray(value) ? map(value, value => objectsById[value.value]) : objectsById[value.value]\n    )\n    return value => (value == null ? value : helper(value))\n  })()\n\n  _onChange = value => {\n    this.props.onChange(this._getSelectedObjects(value))\n  }\n\n  _selectAll = () => {\n    this._onChange(filter(this._getOptions(), ({ disabled }) => !disabled))\n  }\n\n  // GroupBy: Display option with margin if not disabled and containers exists.\n  _renderOption = option => (\n    <span className={!option.disabled && this.props.xoContainers !== undefined ? 'ml-1' : undefined}>\n      {renderXoItem(option.xoItem, {\n        type:\n          this.props.resourceSet !== undefined && option.xoItem.type !== undefined\n            ? `${option.xoItem.type}-resourceSet`\n            : undefined,\n        memoryFree: option.xoItem.type === 'host' || undefined,\n        showNetwork: true,\n        ...(this.props.optionProps ?? {}),\n      })}\n    </span>\n  )\n\n  render() {\n    const { hasSelectAll, xoContainers, xoObjects, ...props } = this.props\n\n    const select = (\n      <Select\n        {...props}\n        onChange={this._onChange}\n        openOnFocus\n        optionRenderer={this._renderOption}\n        options={this._getOptions()}\n        value={this._getSelectedIds()}\n      />\n    )\n\n    if (!props.multi || !hasSelectAll) {\n      return select\n    }\n\n    // `hasSelectAll` should be provided by react-select after this pull request has been merged:\n    // https://github.com/JedWatson/react-select/pull/748\n    // TODO: remove once it has been merged upstream.\n    return (\n      <div className='input-group'>\n        {select}\n        <span className='input-group-btn'>\n          <Tooltip content={_('selectAll')}>\n            <Button onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>\n              <Icon icon='add' />\n            </Button>\n          </Tooltip>\n        </span>\n      </div>\n    )\n  }\n}\n\nconst makeStoreSelect = (createSelectors, defaultProps) =>\n  uncontrollableInput(options)(connectStore(createSelectors)(props => <GenericSelect {...defaultProps} {...props} />))\n\nconst makeSubscriptionSelect = (subscribe, { placeholder, placeholderMulti = placeholder, ...props } = {}) =>\n  uncontrollableInput(options)(\n    class extends React.PureComponent {\n      state = {}\n\n      _getFilteredXoContainers = createFilter(\n        () => this.state.xoContainers,\n        () => this.props.containerPredicate\n      )\n\n      _getFilteredXoObjects = createSelector(\n        () => this.state.xoObjects,\n        () => this.state.xoContainers && this._getFilteredXoContainers(),\n        () => this.props.predicate,\n        (xoObjects, xoContainers, predicate) => {\n          if (xoContainers == null) {\n            return filter(xoObjects, predicate)\n          } else {\n            // Filter xoObjects with `predicate`...\n            const filteredObjects = mapValues(xoObjects, xoObjectsGroup => filter(xoObjectsGroup, predicate))\n            // ...and keep only those whose xoContainer hasn't been filtered out\n            return pick(\n              filteredObjects,\n              map(xoContainers, container => container.id)\n            )\n          }\n        }\n      )\n\n      componentWillMount() {\n        this.componentWillUnmount = subscribe(::this.setState)\n      }\n\n      render() {\n        return (\n          <GenericSelect\n            {...props}\n            {...this.props}\n            placeholder={this.props.multi ? placeholderMulti : placeholder}\n            xoObjects={this._getFilteredXoObjects()}\n            xoContainers={this.state.xoContainers && this._getFilteredXoContainers()}\n          />\n        )\n      }\n    }\n  )\n\n// ===================================================================\n// XO objects.\n// ===================================================================\n\nconst getPredicate = (state, props) => props.predicate\n\n// ===================================================================\n\nexport const SelectHost = makeStoreSelect(\n  () => {\n    const getHostsByPool = createGetObjectsOfType('host').filter(getPredicate).sort().groupBy('$pool')\n\n    const getPools = createGetObjectsOfType('pool')\n      .pick(createSelector(getHostsByPool, hostsByPool => Object.keys(hostsByPool)))\n      .sort()\n\n    return {\n      xoObjects: getHostsByPool,\n      xoContainers: getPools,\n    }\n  },\n  { placeholder: _('selectHosts') }\n)\n\n// ===================================================================\n\nexport const SelectPool = makeStoreSelect(\n  () => ({\n    xoObjects: createGetObjectsOfType('pool').filter(getPredicate).sort(),\n  }),\n  { placeholder: _('selectPools') }\n)\n\n// ===================================================================\n\nexport const SelectSr = makeStoreSelect(\n  () => {\n    const getPools = createGetObjectsOfType('pool')\n    const getHosts = createGetObjectsOfType('host')\n\n    const getSrsByContainer = createGetObjectsOfType('SR')\n      .filter((_, { predicate }) => predicate || isSrWritable)\n      .sort()\n      .groupBy('$container')\n\n    const getContainerIds = createSelector(getSrsByContainer, srsByContainer => keys(srsByContainer))\n\n    const getContainers = createSelector(\n      getPools.pick(getContainerIds).sort(),\n      getHosts.pick(getContainerIds).sort(),\n      (pools, hosts) => pools.concat(hosts)\n    )\n\n    return {\n      xoObjects: getSrsByContainer,\n      xoContainers: getContainers,\n    }\n  },\n  { placeholder: _('selectSrs') }\n)\n\n// ===================================================================\n\nexport const SelectVm = makeStoreSelect(\n  () => {\n    const getVmsByContainer = createGetObjectsOfType('VM').filter(getPredicate).sort().groupBy('$container')\n\n    const getContainerIds = createSelector(getVmsByContainer, vmsByContainer => keys(vmsByContainer))\n\n    const getPools = createGetObjectsOfType('pool').pick(getContainerIds).sort()\n    const getHosts = createGetObjectsOfType('host').pick(getContainerIds).sort()\n\n    const getContainers = createSelector(getPools, getHosts, (pools, hosts) => pools.concat(hosts))\n\n    return {\n      xoObjects: getVmsByContainer,\n      xoContainers: getContainers,\n    }\n  },\n  { placeholder: _('selectVms') }\n)\n\n// ===================================================================\n\nexport const SelectVmSnapshot = makeStoreSelect(\n  () => {\n    const getSnapshotsByVms = createGetObjectsOfType('VM-snapshot').filter(getPredicate).sort().groupBy('$snapshot_of')\n\n    const getVms = createGetObjectsOfType('VM').pick(createSelector(getSnapshotsByVms, keys)).sort()\n\n    return {\n      xoObjects: getSnapshotsByVms,\n      xoContainers: getVms,\n    }\n  },\n  { placeholder: _('selectVmSnapshots') }\n)\n\n// ===================================================================\n\nexport const SelectHostVm = makeStoreSelect(\n  () => {\n    const getHosts = createGetObjectsOfType('host').filter(getPredicate).sort()\n    const getVms = createGetObjectsOfType('VM').filter(getPredicate).sort()\n\n    const getObjects = createSelector(getHosts, getVms, (hosts, vms) => hosts.concat(vms))\n\n    return {\n      xoObjects: getObjects,\n    }\n  },\n  { placeholder: _('selectHostsVms') }\n)\n\n// ===================================================================\n\nexport const SelectVmTemplate = makeStoreSelect(\n  () => {\n    const getVmTemplatesByPool = createGetObjectsOfType('VM-template').filter(getPredicate).sort().groupBy('$pool')\n    const getPools = createGetObjectsOfType('pool')\n      .pick(createSelector(getVmTemplatesByPool, vmTemplatesByPool => keys(vmTemplatesByPool)))\n      .sort()\n\n    return {\n      xoObjects: getVmTemplatesByPool,\n      xoContainers: getPools,\n    }\n  },\n  { placeholder: _('selectVmTemplates') }\n)\n\n// ===================================================================\n\nexport const SelectNetwork = makeStoreSelect(\n  () => {\n    const getNetworksByPool = createGetObjectsOfType('network').filter(getPredicate).sort().groupBy('$pool')\n    const getPools = createGetObjectsOfType('pool')\n      .pick(createSelector(getNetworksByPool, networksByPool => keys(networksByPool)))\n      .sort()\n\n    return {\n      xoObjects: getNetworksByPool,\n      xoContainers: getPools,\n    }\n  },\n  { placeholder: _('selectNetworks') }\n)\n\n// ===================================================================\n\nexport const SelectPif = makeStoreSelect(\n  () => {\n    const getPifsByHost = createGetObjectsOfType('PIF').filter(getPredicate).sort().groupBy('$host')\n    const getHosts = createGetObjectsOfType('host')\n      .pick(createSelector(getPifsByHost, networksByPool => keys(networksByPool)))\n      .sort()\n\n    return {\n      xoObjects: getPifsByHost,\n      xoContainers: getHosts,\n    }\n  },\n  { placeholder: _('selectPifs') }\n)\n\n// ===================================================================\n\nexport const GenericSelectTag = makeStoreSelect(\n  (_, props) => ({\n    xoObjects: createSelector(\n      createGetTags('objects' in props ? (_, props) => props.objects : undefined)\n        .filter(getPredicate)\n        .sort(),\n      tags => map(tags, tag => ({ id: tag, type: 'tag', value: tag }))\n    ),\n  }),\n  { allowMissingObjects: true, placeholder: _('selectTags') }\n)\n\nexport const SelectTag = decorate([\n  provideState({\n    initialState: () => ({\n      editing: false,\n    }),\n    effects: {\n      addTag:\n        (effects, newTag) =>\n        ({ value }, { multi, onChange }) => {\n          if (newTag === value || (multi && includes(value, newTag))) {\n            return\n          }\n          const _newTag = { id: newTag, type: 'tag', value: newTag }\n          onChange(multi ? [...map(value, tag => ({ id: tag, type: 'tag', value: tag })), _newTag] : _newTag)\n        },\n      closeEdition: () => ({ editing: false }),\n      toggleState,\n    },\n    computed: {\n      value: createCollectionWrapper((_, { value }) => getIds(value)),\n    },\n  }),\n  injectState,\n  ({ state, effects, resetState, allowCustomTag, ...props }) => (\n    <span>\n      {allowCustomTag ? (\n        state.editing ? (\n          <EphemeralInput closeEdition={effects.closeEdition} onChange={effects.addTag} type='text' />\n        ) : (\n          <Tooltip content={_('customTag')}>\n            <Button name='editing' onClick={effects.toggleState} size='small'>\n              <Icon icon='edit' />\n            </Button>\n          </Tooltip>\n        )\n      ) : null}\n      <GenericSelectTag {...props} />\n    </span>\n  ),\n])\n\nSelectTag.propTypes = {\n  allowCustomTag: PropTypes.bool,\n}\n\nSelectTag.defaultProps = {\n  allowCustomTag: true,\n}\n\n// ===================================================================\n\nexport const SelectHighLevelObject = makeStoreSelect(\n  () => {\n    const getHosts = createGetObjectsOfType('host').filter(getPredicate)\n    const getNetworks = createGetObjectsOfType('network').filter(getPredicate)\n    const getPools = createGetObjectsOfType('pool').filter(getPredicate)\n    const getSrs = createGetObjectsOfType('SR').filter(getPredicate)\n    const getVms = createGetObjectsOfType('VM').filter(getPredicate)\n\n    const getHighLevelObjects = createSelector(\n      getHosts,\n      getNetworks,\n      getPools,\n      getSrs,\n      getVms,\n      (hosts, networks, pools, srs, vms) =>\n        sortBy(Object.assign({}, hosts, networks, pools, srs, vms), ['type', 'name_label'])\n    )\n\n    return { xoObjects: getHighLevelObjects }\n  },\n  { placeholder: _('selectObjects') }\n)\n\n// ===================================================================\n\nexport const SelectVdi = makeStoreSelect(\n  () => {\n    const getSrs = createGetObjectsOfType('SR').filter((_, props) => props.srPredicate)\n    const getVdis = createGetObjectsOfType('VDI')\n      .filter(\n        createSelector(getSrs, getPredicate, (srs, predicate) =>\n          predicate ? vdi => srs[vdi.$SR] && predicate(vdi) : vdi => srs[vdi.$SR]\n        )\n      )\n      .sort()\n      .groupBy('$SR')\n\n    return {\n      xoObjects: getVdis,\n      xoContainers: getSrs.sort(),\n    }\n  },\n  { placeholder: _('selectVdis') }\n)\nSelectVdi.propTypes = {\n  srPredicate: PropTypes.func,\n}\n\n// ===================================================================\n\nexport const SelectVgpuType = makeStoreSelect(\n  () => {\n    const getVgpuTypes = createSelector(createGetObjectsOfType('vgpuType').filter(getPredicate), vgpuTypes => {\n      const gpuGroups = {}\n      forEach(vgpuTypes, vgpuType => {\n        forEach(vgpuType.gpuGroups, gpuGroup => {\n          if (gpuGroups[gpuGroup] === undefined) {\n            gpuGroups[gpuGroup] = []\n          }\n          gpuGroups[gpuGroup].push({\n            ...vgpuType,\n            gpuGroup,\n          })\n        })\n      })\n\n      return gpuGroups\n    })\n\n    const getGpuGroups = createGetObjectsOfType('gpuGroup').pick(createSelector(getVgpuTypes, keys)).sort()\n\n    return {\n      xoObjects: getVgpuTypes,\n      xoContainers: getGpuGroups,\n    }\n  },\n  { placeholder: _('selectVgpuType') }\n)\n\n// ===================================================================\n// Objects from subscriptions.\n// ===================================================================\n\nexport const SelectSubject = makeSubscriptionSelect(\n  subscriber => {\n    let subjects = {}\n\n    let usersLoaded, groupsLoaded\n    const set = newSubjects => {\n      subjects = newSubjects\n      /* We must wait for groups AND users options to be loaded,\n       * or a previously set value belonging to one type or another might be discarded\n       * by the internal <GenericSelect>\n       */\n      if (usersLoaded && groupsLoaded) {\n        subscriber({\n          xoObjects: subjects,\n        })\n      }\n    }\n\n    const unsubscribeGroups = subscribeGroups(groups => {\n      groupsLoaded = true\n      set([...filter(subjects, subject => subject.type === 'user'), ...groups])\n    })\n\n    const unsubscribeUsers = subscribeUsers(users => {\n      usersLoaded = true\n      set([...filter(subjects, subject => subject.type === 'group'), ...users])\n    })\n\n    return () => {\n      unsubscribeGroups()\n      unsubscribeUsers()\n    }\n  },\n  { placeholder: _('selectSubjects') }\n)\n\nexport const SelectUser = makeSubscriptionSelect(\n  subscriber => {\n    const unsubscribeUsers = subscribeUsers(users => {\n      subscriber({\n        xoObjects: users,\n      })\n    })\n\n    return unsubscribeUsers\n  },\n  { placeholder: _('selectUser') }\n)\n\n// ===================================================================\n\nexport const SelectRole = makeSubscriptionSelect(\n  subscriber => {\n    const unsubscribeRoles = subscribeRoles(roles => {\n      const xoObjects = map(sortBy(roles, 'name'), role => ({\n        ...role,\n        type: 'role',\n      }))\n      subscriber({ xoObjects })\n    })\n    return unsubscribeRoles\n  },\n  { placeholder: _('selectRole') }\n)\n\n// ===================================================================\n\nexport const SelectRemote = makeSubscriptionSelect(\n  subscriber => {\n    const unsubscribeRemotes = subscribeRemotes(remotes => {\n      const xoObjects = groupBy(\n        map(sortBy(remotes, 'name'), remote => ({\n          id: remote.id,\n          type: 'remote',\n          value: { ...remote, ...parseRemote(remote.url) },\n        })).filter(r => !r.value.invalid),\n        remote => remote.value.type\n      )\n\n      subscriber({\n        xoObjects,\n        xoContainers: map(xoObjects, (remote, type) => ({\n          id: type,\n          label: type,\n        })),\n      })\n    })\n\n    return unsubscribeRemotes\n  },\n  { placeholder: _('selectRemotes') }\n)\n\n// ===================================================================\n\nexport const SelectProxy = makeSubscriptionSelect(\n  subscriber =>\n    subscribeProxies(proxies => {\n      subscriber({\n        xoObjects: sortBy(proxies, 'name').map(proxy => ({\n          ...proxy,\n          type: 'proxy',\n        })),\n      })\n    }),\n  { placeholderMulti: _('selectProxies'), placeholder: _('selectProxy') }\n)\n\n// ===================================================================\n\nexport const SelectResourceSet = makeSubscriptionSelect(\n  subscriber => {\n    const unsubscribeResourceSets = subscribeResourceSets(resourceSets => {\n      const xoObjects = map(sortBy(resolveResourceSets(resourceSets), 'name'), resourceSet => ({\n        ...resourceSet,\n        type: 'resourceSet',\n      }))\n\n      subscriber({ xoObjects })\n    })\n\n    return unsubscribeResourceSets\n  },\n  { placeholder: _('selectResourceSets') }\n)\n\n// ===================================================================\n\nexport class SelectResourceSetsVmTemplate extends React.PureComponent {\n  get value() {\n    return this.refs.select.value\n  }\n\n  set value(value) {\n    this.refs.select.value = value\n  }\n\n  _getTemplates = createSelector(\n    () => this.props.resourceSet,\n    ({ objectsByType }) => {\n      const { predicate } = this.props\n      const templates = objectsByType['VM-template']\n      return sortBy(predicate ? filter(templates, predicate) : templates, 'name_label')\n    }\n  )\n\n  render() {\n    return (\n      <GenericSelect\n        ref='select'\n        placeholder={_('selectResourceSetsVmTemplate')}\n        {...this.props}\n        xoObjects={this._getTemplates()}\n      />\n    )\n  }\n}\n\n// ===================================================================\n\nexport class SelectResourceSetsSr extends React.PureComponent {\n  get value() {\n    return this.refs.select.value\n  }\n\n  set value(value) {\n    this.refs.select.value = value\n  }\n\n  _getSrs = createSort(\n    createFilter(\n      () => this.props.resourceSet.objectsByType.SR,\n      createSelector(\n        () => this.props.predicate,\n        predicate => predicate || (() => true)\n      )\n    ),\n    'name_label'\n  )\n\n  render() {\n    return (\n      <GenericSelect ref='select' placeholder={_('selectResourceSetsSr')} {...this.props} xoObjects={this._getSrs()} />\n    )\n  }\n}\n\n// ===================================================================\n\nexport class SelectResourceSetsVdi extends React.PureComponent {\n  get value() {\n    return this.refs.select.value\n  }\n\n  set value(value) {\n    this.refs.select.value = value\n  }\n\n  _getObject(id) {\n    return getObject(store.getState(), id, true)\n  }\n\n  _getSrs = createSelector(\n    () => this.props.resourceSet,\n    ({ objectsByType }) => {\n      const { srPredicate } = this.props\n      const srs = objectsByType.SR\n      return srPredicate ? filter(srs, srPredicate) : srs\n    }\n  )\n\n  _getVdis = createSelector(this._getSrs, srs =>\n    sortBy(map(flatten(map(srs, sr => sr.VDIs)), this._getObject), 'name_label')\n  )\n\n  render() {\n    return (\n      <GenericSelect\n        ref='select'\n        placeholder={_('selectResourceSetsVdi')}\n        {...this.props}\n        xoObjects={this._getVdis()}\n      />\n    )\n  }\n}\n\n// ===================================================================\n\nexport class SelectResourceSetsNetwork extends React.PureComponent {\n  get value() {\n    return this.refs.select.value\n  }\n\n  set value(value) {\n    this.refs.select.value = value\n  }\n\n  _getNetworks = createSort(\n    createFilter(\n      () => this.props.resourceSet.objectsByType.network,\n      createSelector(\n        () => this.props.predicate,\n        predicate => predicate || (() => true)\n      )\n    ),\n    'name_label'\n  )\n\n  render() {\n    return (\n      <GenericSelect\n        ref='select'\n        placeholder={_('selectResourceSetsNetwork')}\n        {...this.props}\n        xoObjects={this._getNetworks()}\n      />\n    )\n  }\n}\n\n// ===================================================================\n\n// Pass a function to @addSubscriptions to ensure subscribeIpPools and subscribeResourceSets\n// are correctly imported before they are called\n@addSubscriptions(() => ({\n  ipPools: subscribeIpPools,\n  resourceSets: subscribeResourceSets,\n}))\nexport class SelectResourceSetIp extends React.Component {\n  static propTypes = {\n    containerPredicate: PropTypes.func,\n    predicate: PropTypes.func,\n    resourceSetId: PropTypes.string.isRequired,\n  }\n\n  get value() {\n    return this.refs.select.value\n  }\n\n  set value(value) {\n    this.refs.select.value = value\n  }\n\n  _getResourceSetIpPools = createSelector(\n    () => this.props.ipPools,\n    () => this.props.resourceSets,\n    () => this.props.resourceSetId,\n    (allIpPools, allResourceSets, resourceSetId) => {\n      const { ipPools } = allResourceSets[resourceSetId]\n      return filter(allIpPools, ({ id }) => includes(ipPools, id))\n    }\n  )\n\n  _getIpPools = createSelector(\n    () => this.props.ipPools,\n    () => this.props.containerPredicate,\n    (ipPools, predicate) => (predicate ? filter(ipPools, predicate) : ipPools)\n  )\n\n  _getIps = createSelector(\n    this._getIpPools,\n    () => this.props.predicate,\n    () => this.props.ipPools,\n    (ipPools, predicate, resolvedIpPools) => {\n      return flatten(\n        map(ipPools, ipPool => {\n          const poolIps = map(ipPool.addresses, (address, ip) => ({\n            ...address,\n            id: ip,\n            label: ip,\n            type: 'ipAddress',\n            used: !isEmpty(address.vifs),\n          }))\n          return predicate ? filter(poolIps, predicate) : poolIps\n        })\n      )\n    }\n  )\n\n  render() {\n    return <GenericSelect ref='select' placeholder={_('selectIpPool')} {...this.props} xoObjects={this._getIps()} />\n  }\n}\n\n// ===================================================================\n\nexport class SelectSshKey extends React.PureComponent {\n  state = {}\n\n  get value() {\n    return this.refs.select.value\n  }\n\n  set value(value) {\n    this.refs.select.value = value\n  }\n\n  componentWillMount() {\n    this.componentWillUnmount = subscribeCurrentUser(user => {\n      this.setState({\n        sshKeys:\n          user &&\n          user.preferences &&\n          map(user.preferences.sshKeys, (key, id) => ({\n            id,\n            label: key.title,\n            type: 'sshKey',\n          })),\n      })\n    })\n  }\n\n  render() {\n    return (\n      <GenericSelect\n        ref='select'\n        placeholder={_('selectSshKey')}\n        {...this.props}\n        xoObjects={this.state.sshKeys || []}\n      />\n    )\n  }\n}\n\n// ===================================================================\n\nexport const SelectIp = makeSubscriptionSelect(\n  subscriber => {\n    const unsubscribeIpPools = subscribeIpPools(ipPools => {\n      const sortedIpPools = sortBy(ipPools, 'name')\n      const xoObjects = mapValues(groupBy(sortedIpPools, 'id'), ipPools =>\n        map(ipPools[0].addresses, (address, ip) => ({\n          ...address,\n          id: ip,\n          label: ip,\n          type: 'ipAddress',\n          used: !isEmpty(address.vifs),\n        }))\n      )\n      const xoContainers = map(sortedIpPools, ipPool => ({\n        ...ipPool,\n        type: 'ipPool',\n      }))\n      subscriber({ xoObjects, xoContainers })\n    })\n\n    return unsubscribeIpPools\n  },\n  { placeholder: _('selectIp') }\n)\n\n// ===================================================================\n\nexport const SelectIpPool = makeSubscriptionSelect(\n  subscriber => {\n    const unsubscribeIpPools = subscribeIpPools(ipPools => {\n      subscriber({\n        xoObjects: map(sortBy(ipPools, 'name'), ipPool => ({\n          ...ipPool,\n          type: 'ipPool',\n        })),\n      })\n    })\n\n    return unsubscribeIpPools\n  },\n  { placeholder: _('selectIpPool') }\n)\n\n// ===================================================================\n\nexport const SelectCloudConfig = makeSubscriptionSelect(\n  subscriber =>\n    subscribeCloudConfigs(cloudConfigs => {\n      subscriber({\n        xoObjects: map(sortBy(cloudConfigs, 'name'), cloudConfig => ({\n          ...cloudConfig,\n          type: 'cloudConfig',\n        })),\n      })\n    }),\n  { placeholder: _('selectCloudConfigs') }\n)\n\nexport const SelectNetworkConfig = makeSubscriptionSelect(\n  subscriber =>\n    subscribeNetworkConfigs(networkConfigs => {\n      subscriber({\n        xoObjects: map(sortBy(networkConfigs, 'name'), networkConfigs => ({\n          ...networkConfigs,\n          type: 'cloudConfig',\n        })),\n      })\n    }),\n  { placeholder: _('selectNetworkConfigs') }\n)\n\n// ===================================================================\n\nexport const SelectXoCloudConfig = makeSubscriptionSelect(\n  subscriber =>\n    subscribeCloudXoConfigBackups(configs => {\n      const xoObjects = groupBy(\n        map(configs, config => ({ ...config, type: 'xoConfig' }))\n          // from newest to oldest\n          .sort((a, b) => b.createdAt - a.createdAt),\n        'xoaId'\n      )\n      subscriber({\n        xoObjects,\n        xoContainers: map(xoObjects, (configs, id) => ({ ...configs, id, type: 'VM' })),\n      })\n    }),\n  { placeholder: _('selectXoConfig') }\n)\n\n// ===================================================================\n\nexport const SelectSchedule = makeSubscriptionSelect(\n  subscriber => {\n    let schedules, jobs, backupJobs, mirrorJobs, metadataJobs\n    const updateData = () => {\n      if (\n        schedules !== undefined &&\n        jobs !== undefined &&\n        backupJobs !== undefined &&\n        mirrorJobs !== undefined &&\n        metadataJobs !== undefined\n      ) {\n        // everything is loaded\n        subscriber({\n          xoObjects: groupBy(schedules, 'jobId'),\n          xoContainers: [...jobs, ...backupJobs, ...mirrorJobs, ...metadataJobs],\n        })\n      }\n    }\n    const unsubscribeSchedules = subscribeSchedules(_schedules => {\n      schedules = _schedules.map(schedule => ({ ...schedule, type: 'schedule' }))\n      updateData()\n    })\n\n    const unsubscribeJobs = subscribeJobs(_jobs => {\n      jobs = _jobs.map(_job => ({ ..._job, type: 'job' }))\n      updateData()\n    })\n    const unsubscribeBackupJobs = subscribeBackupNgJobs(_jobs => {\n      backupJobs = _jobs.map(_job => ({ ..._job, type: 'job' }))\n      updateData()\n    })\n    const unsubscribeMirrorBackupJobs = subscribeMirrorBackupJobs(_jobs => {\n      mirrorJobs = _jobs.map(_job => ({ ..._job, type: 'job' }))\n      updateData()\n    })\n    const unsubscribeMetadataJobs = subscribeMetadataBackupJobs(_jobs => {\n      metadataJobs = _jobs.map(_job => ({ ..._job, type: 'job' }))\n      updateData()\n    })\n\n    return () => {\n      unsubscribeSchedules()\n      unsubscribeJobs()\n      unsubscribeBackupJobs()\n      unsubscribeMirrorBackupJobs()\n      unsubscribeMetadataJobs()\n    }\n  },\n  { placeholder: _('selectSchedule') }\n)\n","import _ from 'intl'\nimport React from 'react'\nimport defined, { get } from '@xen-orchestra/defined'\nimport { injectState, provideState } from 'reaclette'\n\nimport decorate from './apply-decorators'\nimport Icon from './icon'\nimport renderXoItem from './render-xo-item'\nimport { connectStore } from './utils'\nimport { createGetObject } from './selectors'\nimport { editVm, editPool, isSrWritable } from './xo'\nimport { XoSelect } from './editable'\n\nexport const SelectSuspendSr = decorate([\n  connectStore({\n    suspendSr: createGetObject((_, { pool, vm }) => (vm || pool).suspendSr),\n  }),\n  provideState({\n    effects: {\n      onChange(_, value) {\n        const { isVm } = this.state\n        const method = isVm ? editVm : editPool\n        method(isVm ? this.props.vm : this.props.pool, {\n          suspendSr: defined(\n            get(() => value.id),\n            null\n          ),\n        })\n      },\n    },\n    computed: {\n      isVm: (state, props) => props.vm !== undefined,\n      predicate: (state, props) => sr =>\n        isSrWritable(sr) && (state.isVm ? props.vm.$pool === sr.$pool : props.pool.id === sr.$pool),\n    },\n  }),\n  injectState,\n  ({ effects: { onChange }, state: { predicate }, suspendSr }) => (\n    <span>\n      <XoSelect onChange={onChange} predicate={predicate} value={suspendSr} xoType='SR'>\n        {suspendSr !== undefined ? renderXoItem(suspendSr) : _('noValue')}\n      </XoSelect>{' '}\n      {suspendSr !== undefined && (\n        <a role='button' onClick={onChange}>\n          <Icon icon='remove' />\n        </a>\n      )}\n    </span>\n  ),\n])\n","import add from 'lodash/add'\nimport defined from '@xen-orchestra/defined'\nimport { check as checkPermissions } from 'xo-acl-resolver'\nimport { createSelector as create } from 'reselect'\nimport {\n  difference,\n  filter,\n  find,\n  forEach,\n  forOwn,\n  groupBy,\n  identity,\n  isArrayLike,\n  isEmpty,\n  keyBy,\n  keys,\n  map,\n  orderBy,\n  pick,\n  pickBy,\n  size,\n  slice,\n  some,\n} from 'lodash'\n\nimport invoke from './invoke'\nimport shallowEqual from './shallow-equal'\nimport { EMPTY_ARRAY, EMPTY_OBJECT, getDetachedBackupsOrSnapshots } from './utils'\n\n// ===================================================================\n\nexport {\n  // That's usually the name we want to import.\n  createSelector,\n  // But selectors.create is nice too :)\n  createSelector as create,\n} from 'reselect'\n\n// -------------------------------------------------------------------\n\n// Wraps a function which returns a collection to returns the previous\n// result if the collection has not really changed (ie still has the\n// same items).\n//\n// Use case: in connect, to avoid rerendering a component where the\n// objects are still the same.\nconst _createCollectionWrapper = selector => {\n  let cache, previous\n\n  return (...args) => {\n    const value = selector(...args)\n    if (value !== previous) {\n      previous = value\n\n      if (!shallowEqual(value, cache)) {\n        cache = value\n      }\n    }\n    return cache\n  }\n}\nexport { _createCollectionWrapper as createCollectionWrapper }\n\nconst _SELECTOR_PLACEHOLDER = Symbol('selector placeholder')\n\n// Experimental!\n//\n// Similar to reselect's createSelector() but inputs can be either\n// selectors or plain values.\n//\n// To pass a function as a plain value, simply wrap it with an array.\nconst _create2 = (...inputs) => {\n  const resultFn = inputs.pop()\n\n  if (inputs.length === 1 && Array.isArray(inputs[0])) {\n    inputs = inputs[0]\n  }\n\n  const n = inputs.length\n\n  const inputSelectors = []\n  for (let i = 0; i < n; ++i) {\n    const input = inputs[i]\n\n    if (typeof input === 'function') {\n      inputSelectors.push(input)\n      inputs[i] = _SELECTOR_PLACEHOLDER\n    } else if (Array.isArray(input) && input.length === 1) {\n      inputs[i] = input[0]\n    }\n  }\n\n  if (!inputSelectors.length) {\n    throw new Error('no input selectors')\n  }\n\n  return create(inputSelectors, function () {\n    const args = new Array(n)\n    for (let i = 0, j = 0; i < n; ++i) {\n      const input = inputs[i]\n      args[i] = input === _SELECTOR_PLACEHOLDER ? arguments[j++] : input\n    }\n\n    return resultFn.apply(this, args)\n  })\n}\n\n// ===================================================================\n// Generic selector creators.\n\nexport const createCounter = (collection, predicate) =>\n  _create2(collection, predicate, (collection, predicate) => {\n    if (!predicate) {\n      return size(collection)\n    }\n\n    let count = 0\n    forEach(collection, item => {\n      if (predicate(item)) {\n        ++count\n      }\n    })\n    return count\n  })\n\n// Creates an object selector from an object selector and a properties\n// selector.\n//\n// Should only be used with a reasonable number of properties.\nexport const createPicker = (object, props) =>\n  _create2(\n    object,\n    props,\n    _createCollectionWrapper((object, props) => {\n      const values = {}\n      forEach(props, prop => {\n        const value = object[prop]\n        if (value) {\n          values[prop] = value\n        }\n      })\n      return values\n    })\n  )\n\n// Special cases:\n// - predicate == null → no filtering\n// - predicate === false → everything is filtered out\nexport const createFilter = (collection, predicate) =>\n  _create2(\n    collection,\n    predicate,\n    _createCollectionWrapper((collection, predicate) =>\n      predicate === false\n        ? isArrayLike(collection)\n          ? EMPTY_ARRAY\n          : EMPTY_OBJECT\n        : predicate\n          ? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)\n          : collection\n    )\n  )\n\nexport const createFinder = (collection, predicate) => _create2(collection, predicate, find)\n\nexport const createGroupBy = (collection, getter) => _create2(collection, getter, groupBy)\n\nexport const createPager = (array, page, n = 25) =>\n  _create2(\n    array,\n    page,\n    n,\n    _createCollectionWrapper((array, page, n) => {\n      const start = (page - 1) * n\n      return slice(array, start, start + n)\n    })\n  )\n\nexport const createSort = (collection, getter = 'name_label', order = 'asc') =>\n  _create2(collection, getter, order, orderBy)\n\nexport const createSumBy = (itemsSelector, iterateeSelector) =>\n  _create2(itemsSelector, iterateeSelector, (items, iteratee) => map(items, iteratee).reduce(add, 0))\n\nexport const createTop = (collection, iteratee, n) =>\n  _create2(\n    collection,\n    iteratee,\n    n,\n    _createCollectionWrapper((objects, iteratee, n) => {\n      const results = orderBy(objects, iteratee, 'desc')\n      if (n < results.length) {\n        results.length = n\n      }\n      return results\n    })\n  )\n\n// ===================================================================\n// Root-ish selectors (no dependencies).\n\nexport const areObjectsFetched = state => state.objects.fetched\n\nconst _getId = (state, { routeParams, id }) => (routeParams ? routeParams.id : id)\n\nexport const getLang = state => state.lang\n\nexport const getStatus = state => state.status\n\nexport const getUser = state => state.user\n\nexport const getXoaState = state => state.xoaUpdaterState\n\nexport const getCheckPermissions = invoke(() => {\n  const getPredicate = create(\n    state => state.permissions,\n    state => state.objects,\n    (permissions, objects) => {\n      objects = objects.all\n      const getObject = id => objects[id] || EMPTY_OBJECT\n\n      return (id, permission) => checkPermissions(permissions, getObject, id, permission)\n    }\n  )\n\n  const isTrue = () => true\n  const isFalse = () => false\n\n  return state => {\n    const user = getUser(state)\n\n    if (!user) {\n      return isFalse\n    }\n\n    if (user.permission === 'admin') {\n      return isTrue\n    }\n\n    return getPredicate(state)\n  }\n})\n\nconst _getPermissionsPredicate = invoke(() => {\n  const getCache = create(identity, () => ({ __proto__: null }))\n\n  const getPredicate = create(\n    state => state.permissions,\n    state => state.objects,\n    (permissions, objects) => {\n      const cache = getCache(permissions)\n      objects = objects.all\n      const getObject = id => objects[id] || EMPTY_OBJECT\n\n      return id => {\n        if (typeof id !== 'string') {\n          id = id.id\n        }\n        let allowed = cache[id]\n        if (allowed === undefined) {\n          allowed = cache[id] = checkPermissions(permissions, getObject, id, 'view')\n        }\n        return allowed\n      }\n    }\n  )\n\n  return (state, props, useResourceSet) => {\n    const user = getUser(state)\n    if (!user) {\n      return false\n    }\n\n    if (user.permission === 'admin' || useResourceSet) {\n      return // No predicate means no filtering.\n    }\n\n    return getPredicate(state)\n  }\n})\n\nexport const isAdmin = (...args) => {\n  const user = getUser(...args)\n\n  return user && user.permission === 'admin'\n}\n\n// ===================================================================\n// Common selector creators.\n\n// Creates an object selector from an id selector.\nexport const createGetObject =\n  (idSelector = _getId) =>\n  (state, props, useResourceSet) => {\n    const object = state.objects.all[idSelector(state, props)]\n    if (!object) {\n      return\n    }\n\n    if (useResourceSet) {\n      return object\n    }\n\n    const predicate = _getPermissionsPredicate(state)\n\n    if (!predicate) {\n      if (predicate == null) {\n        return object // no filtering\n      }\n\n      // predicate is false.\n      return\n    }\n\n    if (predicate(object)) {\n      return object\n    }\n  }\n\n// Specialized createSort() configured for a given type.\nexport const createSortForType = invoke(() => {\n  const iterateesByType = {\n    message: message => message.time,\n    PIF: pif => pif.device,\n    patch: patch => patch.name,\n    pool: pool => pool.name_label,\n    tag: tag => tag,\n    VBD: vbd => vbd.position,\n    'VDI-snapshot': snapshot => snapshot.snapshot_time,\n    'VM-snapshot': snapshot => snapshot.snapshot_time,\n  }\n  const defaultIteratees = [object => object.$pool, object => object.name_label]\n  const getIteratees = type => iterateesByType[type] || defaultIteratees\n\n  const ordersByType = {\n    message: 'desc',\n    'VDI-snapshot': 'desc',\n    'VM-snapshot': 'desc',\n  }\n  const getOrders = type => ordersByType[type]\n\n  const autoSelector = (type, fn) =>\n    typeof type === 'function' ? (state, props) => fn(type(state, props)) : [fn(type)]\n\n  return (type, collection) => createSort(collection, autoSelector(type, getIteratees), autoSelector(type, getOrders))\n})\n\n// Add utility methods to a collection selector.\nconst _extendCollectionSelector = (selector, objectsType) => {\n  // Terminal methods.\n  const _addCount = selector => {\n    selector.count = predicate => createCounter(selector, predicate)\n    return selector\n  }\n  _addCount(selector)\n  const _addGroupBy = selector => {\n    selector.groupBy = getter => createGroupBy(selector, getter)\n    return selector\n  }\n  _addGroupBy(selector)\n  const _addFind = selector => {\n    selector.find = predicate => createFinder(selector, predicate)\n    return selector\n  }\n  _addFind(selector)\n\n  // groupBy can be chained.\n  const _addSort = selector => {\n    // TODO: maybe memoize when no idsSelector.\n    selector.sort = () => _addGroupBy(createSortForType(objectsType, selector))\n    return selector\n  }\n  _addSort(selector)\n\n  // count, groupBy and sort can be chained.\n  const _addFilter = selector => {\n    selector.filter = predicate => _addCount(_addGroupBy(_addSort(createFilter(selector, predicate))))\n    return selector\n  }\n  _addFilter(selector)\n\n  // filter, groupBy and sort can be chained.\n  selector.pick = idsSelector => _addFind(_addFilter(_addGroupBy(_addSort(createPicker(selector, idsSelector)))))\n\n  return selector\n}\n\n// Creates a collection selector which returns all objects of a given\n// type.\n//\n// The selector as the following methods:\n//\n// - count: returns a selector which returns the number of objects\n// - filter: returns a selector which returns the objects filtered by\n//           a predicate (count, groupBy and sort can be chained)\n// - find: returns a selector which returns the first object matching\n//         a predicate\n// - groupBy: returns a selector which returns the objects grouped by\n//            a value determined by a getter selector\n// - pick: returns a selector which returns only the objects with given\n//         ids (filter, find, groupBy and sort can be chained)\n// - sort: returns a selector which returns the objects appropriately\n//         sorted (groupBy can be chained)\nexport const createGetObjectsOfType = type => {\n  const getObjects =\n    typeof type === 'function'\n      ? (state, props) => state.objects.byType[type(state, props)] || EMPTY_OBJECT\n      : state => state.objects.byType[type] || EMPTY_OBJECT\n\n  return _extendCollectionSelector(createFilter(getObjects, _getPermissionsPredicate), type)\n}\n\nexport const createGetTags = collectionSelectors => {\n  if (!collectionSelectors) {\n    collectionSelectors = [\n      createGetObjectsOfType('host'),\n      createGetObjectsOfType('pool'),\n      createGetObjectsOfType('VM'),\n      createGetObjectsOfType('SR'),\n    ]\n  }\n\n  const getTags = create(collectionSelectors, (...collections) => {\n    const tags = {}\n\n    const addTag = tag => {\n      tags[tag] = null\n    }\n    const addItemTags = item => {\n      forEach(item.tags, addTag)\n    }\n    const addCollectionTags = collection => {\n      forEach(collection, addItemTags)\n    }\n    forEach(collections, addCollectionTags)\n\n    return keys(tags)\n  })\n\n  return _extendCollectionSelector(getTags, 'tag')\n}\n\nexport const createGetVmLastShutdownTime = (getVmId = (_, { vm }) => (vm != null ? vm.id : undefined)) =>\n  create(getVmId, createGetObjectsOfType('message'), (vmId, messages) => {\n    let max = null\n    forEach(messages, message => {\n      if (message.$object === vmId && message.name === 'VM_SHUTDOWN' && (max === null || message.time > max)) {\n        max = message.time\n      }\n    })\n    return max\n  })\n\nexport const createGetObjectMessages = objectSelector =>\n  createGetObjectsOfType('message')\n    .filter(\n      create(\n        (...args) => objectSelector(...args).id,\n        id => message => message.$object === id\n      )\n    )\n    .sort()\n\n// Example of use:\n// import store from 'store'\n// const object = getObject(store.getState(), objectId)\n// ...\nexport const getObject = createGetObject((_, id) => id)\n\nexport const createDoesHostNeedRestart = hostSelector => {\n  const patchRequiresReboot = createGetObjectsOfType('patch')\n    .pick(create(hostSelector, host => host.patches))\n    .find(\n      create(\n        hostSelector,\n        host =>\n          ({ guidance, time, upgrade }) =>\n            time > host.startTime &&\n            (upgrade || some(guidance, action => action === 'restartHost' || action === 'restartXapi'))\n      )\n    )\n\n  return create(\n    hostSelector,\n    (...args) => args,\n    (host, args) => host.rebootRequired || !!patchRequiresReboot(...args)\n  )\n}\n\nexport const createGetHostMetrics = hostSelector =>\n  create(\n    hostSelector,\n    _createCollectionWrapper(hosts => {\n      const metrics = {\n        count: 0,\n        cpus: 0,\n        memoryTotal: 0,\n        memoryUsage: 0,\n      }\n      forEach(hosts, host => {\n        metrics.count++\n        metrics.cpus += host.cpus.cores\n        metrics.memoryTotal += host.memory.size\n        metrics.memoryUsage += host.memory.usage\n      })\n      return metrics\n    })\n  )\n\nexport const createGetVmDisks = vmSelector =>\n  createGetObjectsOfType('VDI').pick(\n    create(\n      createGetObjectsOfType('VBD').pick((state, props) => vmSelector(state, props).$VBDs),\n      _createCollectionWrapper(vbds => map(vbds, vbd => (vbd.is_cd_drive ? undefined : vbd.VDI)))\n    )\n  )\n\nexport const getIsPoolAdmin = create(\n  create(createGetObjectsOfType('pool'), _createCollectionWrapper(Object.keys)),\n  getCheckPermissions,\n  (poolsIds, check) => some(poolsIds, poolId => check(poolId, 'administrate'))\n)\n\nexport const getLoneSnapshots = create(\n  create(\n    createFilter(createGetObjectsOfType('VM-snapshot'), [({ other }) => other['xo:backup:job'] !== undefined]),\n    backupSnapshots =>\n      map(backupSnapshots, snapshot => {\n        const other = snapshot.other\n        return {\n          ...snapshot,\n          jobId: other['xo:backup:job'],\n          vmId: other['xo:backup:vm'],\n          scheduleId: other['xo:backup:schedule'],\n        }\n      })\n  ),\n  _createCollectionWrapper((_, { jobs }) => (jobs === undefined ? undefined : keyBy(jobs, 'id'))),\n  _createCollectionWrapper((_, { schedules }) => (schedules === undefined ? undefined : keyBy(schedules, 'id'))),\n  createGetObjectsOfType('VM'),\n  _createCollectionWrapper((snapshots, jobs, schedules, vms) =>\n    getDetachedBackupsOrSnapshots(snapshots, { jobs, schedules, vms })\n  )\n)\n\nconst _getResolvedResourceSet = (resourceSet, networks, srs, vms) => {\n  if (resourceSet === undefined) {\n    return\n  }\n\n  const { objects, ...attrs } = resourceSet\n  const objectsByType = {}\n  const objectsFound = []\n\n  const resolve = (type, _objects) => {\n    const resolvedObjects = pick(_objects, objects)\n    if (!isEmpty(resolvedObjects)) {\n      objectsFound.push(...Object.keys(resolvedObjects))\n      objectsByType[type] = Object.values(resolvedObjects)\n    }\n  }\n  resolve('VM-template', vms)\n  resolve('SR', srs)\n  resolve('network', networks)\n\n  return {\n    ...attrs,\n    missingObjects: difference(objectsFound, objects),\n    objectsByType,\n  }\n}\n\nexport const getResolvedResourceSet = create(\n  (_, props) => props.resourceSet,\n  createGetObjectsOfType('network'),\n  createGetObjectsOfType('SR'),\n  createGetObjectsOfType('VM-template'),\n  _getResolvedResourceSet\n)\n\nexport const getResolvedResourceSets = create(\n  (_, props) => props.resourceSets,\n  createGetObjectsOfType('network'),\n  createGetObjectsOfType('SR'),\n  createGetObjectsOfType('VM-template'),\n  (resourceSets, networks, srs, vms) =>\n    map(resourceSets, resourceSet => _getResolvedResourceSet(resourceSet, networks, srs, vms))\n)\n\nexport const createGetHostState = getHost =>\n  create(\n    (state, props) => getHost(state, props).power_state,\n    (state, props) => getHost(state, props).enabled,\n    (state, props) => getHost(state, props).current_operations,\n    (powerState, enabled, operations) =>\n      powerState !== 'Running' ? powerState : !isEmpty(operations) ? 'Busy' : !enabled ? 'Disabled' : 'Running'\n  )\n\nconst taskPredicate = obj => !isEmpty(obj.current_operations)\nconst getLinkedObjectsByTaskRefOrId = create(\n  createGetObjectsOfType('pool').filter([taskPredicate]),\n  createGetObjectsOfType('host').filter([taskPredicate]),\n  createGetObjectsOfType('SR').filter([taskPredicate]),\n  createGetObjectsOfType('VDI').filter([taskPredicate]),\n  createGetObjectsOfType('VM').filter([taskPredicate]),\n  createGetObjectsOfType('network').filter([taskPredicate]),\n  getCheckPermissions,\n  (pools, hosts, srs, vdis, vms, networks, check) => {\n    const linkedObjectsByTaskRefOrId = {}\n    const resolveLinkedObjects = obj => {\n      if (!check(obj.id, 'view')) {\n        return\n      }\n\n      Object.keys(obj.current_operations).forEach(task => {\n        if (linkedObjectsByTaskRefOrId[task] === undefined) {\n          linkedObjectsByTaskRefOrId[task] = []\n        }\n        linkedObjectsByTaskRefOrId[task].push(obj)\n      })\n    }\n\n    forOwn(pools, resolveLinkedObjects)\n    forOwn(hosts, resolveLinkedObjects)\n    forOwn(srs, resolveLinkedObjects)\n    forOwn(vdis, resolveLinkedObjects)\n    forOwn(vms, resolveLinkedObjects)\n    forOwn(networks, resolveLinkedObjects)\n\n    return linkedObjectsByTaskRefOrId\n  }\n)\n\nexport const getResolvedPendingTasks = create(\n  createGetObjectsOfType('task').filter([task => task.status === 'pending']),\n  getLinkedObjectsByTaskRefOrId,\n  getCheckPermissions,\n  (tasks, linkedObjectsByTaskRefOrId, check) => {\n    const resolvedTasks = []\n    forEach(tasks, task => {\n      const objects = [\n        ...defined(linkedObjectsByTaskRefOrId[task.xapiRef], []),\n        // for VMs, the current_operations prop is\n        // { taskId → operation } map instead of { taskRef → operation } map\n        ...defined(linkedObjectsByTaskRefOrId[task.id], []),\n      ]\n\n      if (objects.length > 0 || check(task.$host, 'view')) {\n        resolvedTasks.push({\n          ...task,\n          objects,\n        })\n      }\n    })\n    return resolvedTasks\n  }\n)\n","import kindOf from 'kindof'\n\n// Tests that two collections (arrays, sets or objects) have strictly equals\n// values (items or properties)\nconst shallowEqual = (c1, c2) => {\n  if (c1 === c2) {\n    return true\n  }\n\n  const type = kindOf(c1)\n  if (type !== kindOf(c2)) {\n    return false\n  }\n\n  if (type === 'array') {\n    const { length } = c1\n    if (length !== c2.length) {\n      return false\n    }\n\n    for (let i = 0; i < length; ++i) {\n      if (c1[i] !== c2[i]) {\n        return false\n      }\n    }\n\n    return true\n  }\n\n  if (type === 'set') {\n    if (c1.size !== c2.size) {\n      return false\n    }\n\n    for (const v of c1) {\n      if (!c2.has(v)) {\n        return false\n      }\n    }\n\n    return true\n  }\n\n  if (type !== 'object') {\n    return false\n  }\n\n  let n = 0\n  // eslint-disable-next-line no-unused-vars\n  for (const _ in c2) {\n    ++n\n  }\n\n  for (const key in c1) {\n    if (c1[key] !== c2[key]) {\n      return false\n    }\n    --n\n  }\n\n  return !n\n}\nexport { shallowEqual as default }\n","import Component from 'base-component'\nimport React from 'react'\nimport { Shortcuts as ReactShortcuts } from 'react-shortcuts'\nimport { forEach, remove } from 'lodash'\n\nlet enabled = true\nconst instances = []\n\nconst updateInstances = () => {\n  forEach(instances, instance => instance.forceUpdate())\n}\n\nexport const enable = () => {\n  enabled = true\n  updateInstances()\n}\n\nexport const disable = () => {\n  enabled = false\n  updateInstances()\n}\n\nexport default class Shortcuts extends Component {\n  componentDidMount() {\n    instances.push(this)\n  }\n  componentWillUnmount() {\n    remove(instances, this)\n  }\n\n  _handler = (command, event) => {\n    // When an input is focused, shortcuts are disabled by default *except* for\n    // non-printable keys (Esc, Enter, ...) but we want to disable them as well\n    // https://github.com/avocode/react-shortcuts/issues/13#issuecomment-255868423\n    if (event.target.tagName === 'INPUT') {\n      return\n    }\n\n    this.props.handler(command, event)\n  }\n\n  render() {\n    return enabled ? <ReactShortcuts {...this.props} handler={this._handler} /> : null\n  }\n}\n","import PropTypes from 'prop-types'\nimport React, { cloneElement } from 'react'\n\nconst SINGLE_LINE_STYLE = { display: 'flex' }\nconst COL_STYLE = { marginTop: 'auto', marginBottom: 'auto' }\n\nconst SingleLineRow = ({ children, className }) => (\n  <div className={`${className || ''} row`} style={SINGLE_LINE_STYLE}>\n    {React.Children.map(\n      children,\n      child => child && cloneElement(child, { style: { ...child.props.style, ...COL_STYLE } })\n    )}\n  </div>\n)\n\nSingleLineRow.propTypes = {\n  className: PropTypes.string,\n}\n\nexport { SingleLineRow as default }\n","import { get, identity, isEmpty } from 'lodash'\n\nimport { EMPTY_OBJECT } from './../utils'\n\nexport const destructPattern = (pattern, valueTransform = identity) =>\n  pattern && {\n    not: !!pattern.__not,\n    values: valueTransform((pattern.__not || pattern).__or),\n  }\n\nexport const constructPattern = ({ not, values } = EMPTY_OBJECT, valueTransform = identity) => {\n  if (values == null || !values.length) {\n    return\n  }\n\n  const pattern = { __or: valueTransform(values) }\n  return not ? { __not: pattern } : pattern\n}\n\n// ===================================================================\n\nexport const destructSmartPattern = (pattern, valueTransform = identity) =>\n  pattern && {\n    values: valueTransform(pattern.__and !== undefined ? pattern.__and[0].__or : pattern.__or),\n    notValues: valueTransform(pattern.__and !== undefined ? pattern.__and[1].__not.__or : get(pattern, '__not.__or')),\n  }\n\nexport const constructSmartPattern = ({ values, notValues } = EMPTY_OBJECT, valueTransform = identity) => {\n  const valuesExist = !isEmpty(values)\n  const notValuesExist = !isEmpty(notValues)\n\n  if (!valuesExist && !notValuesExist) {\n    return\n  }\n\n  const valuesPattern = valuesExist && { __or: valueTransform(values) }\n  const notValuesPattern = notValuesExist && {\n    __not: { __or: valueTransform(notValues) },\n  }\n  return valuesPattern && notValuesPattern\n    ? { __and: [valuesPattern, notValuesPattern] }\n    : valuesPattern || notValuesPattern\n}\n\n// ===================================================================\n\nexport default from './preview'\n","import _ from 'intl'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { createSelector } from 'reselect'\nimport { filter, map } from 'lodash'\n\nimport Component from './../base-component'\nimport Icon from './../icon'\nimport Link from './../link'\nimport renderXoItem from './../render-xo-item'\nimport Tooltip from './../tooltip'\nimport { Card, CardBlock, CardHeader } from './../card'\nimport { smartModeToComplexMatcher } from '../smartModeToComplexMatcher'\n\nconst SAMPLE_SIZE_OF_MATCHING_VMS = 3\n\nexport default class SmartBackupPreview extends Component {\n  static propTypes = {\n    pattern: PropTypes.object.isRequired,\n    vms: PropTypes.object.isRequired,\n  }\n\n  // user pattern completed with support for `xo:no-bak` tag automatically\n  // ignored by xo-server\n  _getComplexMatcher = createSelector(() => this.props.pattern, smartModeToComplexMatcher)\n\n  _getMatchingVms = createSelector(\n    () => this.props.vms,\n    createSelector(this._getComplexMatcher, cm => cm.createPredicate()),\n    (vms, predicate) => filter(vms, predicate)\n  )\n\n  _getSampleOfMatchingVms = createSelector(this._getMatchingVms, vms => vms.slice(0, SAMPLE_SIZE_OF_MATCHING_VMS))\n\n  _getQueryString = createSelector(this._getComplexMatcher, cm => {\n    try {\n      return cm.toString()\n    } catch (error) {\n      console.error(error)\n      return ''\n    }\n  })\n\n  render() {\n    const nMatchingVms = this._getMatchingVms().length\n    const sampleOfMatchingVms = this._getSampleOfMatchingVms()\n    const queryString = this._getQueryString()\n\n    return (\n      <Card>\n        <CardHeader>{_('sampleOfMatchingVms')}</CardHeader>\n        <CardBlock>\n          {nMatchingVms === 0 ? (\n            <p className='text-xs-center'>{_('noMatchingVms')}</p>\n          ) : (\n            <div>\n              <ul className='list-group'>\n                {map(sampleOfMatchingVms, vm => (\n                  <li className='list-group-item' key={vm.id}>\n                    {renderXoItem(vm)}\n                  </li>\n                ))}\n              </ul>\n              <br />\n              <Tooltip content={_('redirectToMatchingVms')}>\n                <Link\n                  className='pull-right'\n                  target='_blank'\n                  to={{\n                    pathname: '/home',\n                    query: {\n                      t: 'VM',\n                      s: queryString,\n                    },\n                  }}\n                >\n                  {_('allMatchingVms', {\n                    icon: <Icon icon='preview' />,\n                    nMatchingVms,\n                  })}\n                </Link>\n              </Tooltip>\n            </div>\n          )}\n        </CardBlock>\n      </Card>\n    )\n  }\n}\n","import * as CM from 'complex-matcher'\nimport escapeRegExp from 'lodash/escapeRegExp.js'\n\n// compile a value-matcher like pattern (plus support for regexps) to a\n// complex-matcher pattern\nconst pseudoValueToComplexMatcher = pattern => {\n  if (typeof pattern === 'string') {\n    return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`)\n  }\n\n  if (Array.isArray(pattern)) {\n    return new CM.And(pattern.map(pseudoValueToComplexMatcher))\n  }\n\n  if (pattern instanceof RegExp) {\n    return new CM.RegExpNode(pattern)\n  }\n\n  if (pattern !== null && typeof pattern === 'object') {\n    const keys = Object.keys(pattern)\n    const { length } = keys\n\n    if (length === 1) {\n      const [key] = keys\n      if (key === '__and') {\n        return new CM.And(pattern.__and.map(pseudoValueToComplexMatcher))\n      }\n      if (key === '__or') {\n        return new CM.Or(pattern.__or.map(pseudoValueToComplexMatcher))\n      }\n      if (key === '__not') {\n        return new CM.Not(pseudoValueToComplexMatcher(pattern.__not))\n      }\n    }\n\n    const children = []\n    Object.keys(pattern).forEach(property => {\n      const subpattern = pattern[property]\n      if (subpattern !== undefined) {\n        children.push(new CM.Property(property, pseudoValueToComplexMatcher(subpattern)))\n      }\n    })\n    return children.length === 0 ? new CM.Null() : new CM.And(children)\n  }\n\n  throw new Error('could not transform this pattern')\n}\n\nexport const smartModeToComplexMatcher = pattern => {\n  // don't mutate param\n  pattern = JSON.parse(JSON.stringify(pattern))\n\n  // if the pattern does not match expected entries, simply don't change it\n  const { tags } = pattern\n  if (tags !== undefined) {\n    ;(tags.__not ?? tags.__and?.[1]?.__not)?.__or?.push(/^xo:no-bak(?:=.*)?$/)\n  }\n\n  return pseudoValueToComplexMatcher(pattern)\n}\n","module.exports = {\n    \"clickableColumn\": \"mc5df48f8c_clickableColumn\",\n    \"clickableRow\": \"mc5df48f8c_clickableRow\",\n    \"highlight\": \"mc5df48f8c_highlight\"\n};","import * as CM from 'complex-matcher'\nimport _ from 'intl'\nimport classNames from 'classnames'\nimport cookies from 'js-cookie'\nimport defined, { ifDef } from '@xen-orchestra/defined'\nimport DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom\nimport DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport Shortcuts from 'shortcuts'\nimport { Dropdown, DropdownButton, MenuItem } from 'react-bootstrap-4/lib'\nimport { Portal } from 'react-overlays'\nimport { Set } from 'immutable'\nimport { injectState, provideState } from 'reaclette'\nimport { withRouter } from 'react-router'\nimport { ceil, filter, findIndex, forEach, get as getProperty, groupBy, isEmpty, map, sortBy } from 'lodash'\n\nimport ActionRowButton from '../action-row-button'\nimport Button from '../button'\nimport ButtonGroup from '../button-group'\nimport Component from '../base-component'\nimport decorate from '../apply-decorators'\nimport Icon from '../icon'\nimport logError from '../log-error'\nimport Pagination from '../pagination'\nimport SingleLineRow from '../single-line-row'\nimport TableFilter from '../search-bar'\nimport UserError from '../user-error'\nimport { BlockLink } from '../link'\nimport { conditionalTooltip } from '../tooltip'\nimport { Container, Col } from '../grid'\nimport { error as _error } from '../notification'\nimport { generateId } from '../reaclette-utils'\nimport {\n  createCollectionWrapper,\n  createCounter,\n  createFilter,\n  createPager,\n  createSelector,\n  createSort,\n} from '../selectors'\nimport { ITEMS_PER_PAGE_OPTIONS } from '../xo'\n\nimport styles from './index.css'\n\nconst DEFAULT_ITEMS_PER_PAGE = 10\n\n// ===================================================================\n\nclass ColumnHead extends Component {\n  static propTypes = {\n    columnId: PropTypes.number.isRequired,\n    name: PropTypes.node,\n    sort: PropTypes.func,\n    sortIcon: PropTypes.string,\n    tooltip: PropTypes.node,\n  }\n\n  _sort = () => {\n    const { props } = this\n    props.sort(props.columnId)\n  }\n\n  render() {\n    const { name, sortIcon, textAlign, tooltip } = this.props\n\n    if (!this.props.sort) {\n      return conditionalTooltip(<th className={textAlign && `text-xs-${textAlign}`}>{name}</th>, tooltip)\n    }\n\n    const isSelected = sortIcon === 'asc' || sortIcon === 'desc'\n\n    return conditionalTooltip(\n      <th\n        className={classNames(\n          textAlign && `text-xs-${textAlign}`,\n          styles.clickableColumn,\n          isSelected && classNames('text-white', 'bg-info')\n        )}\n        onClick={this._sort}\n      >\n        {name}\n        <span className='pull-right'>\n          <Icon icon={sortIcon} />\n        </span>\n      </th>,\n      tooltip\n    )\n  }\n}\n\n// ===================================================================\n\nclass Checkbox extends Component {\n  static propTypes = {\n    indeterminate: PropTypes.bool.isRequired,\n  }\n\n  componentDidUpdate() {\n    const {\n      props: { indeterminate },\n      ref,\n    } = this\n    if (ref !== null) {\n      ref.indeterminate = indeterminate\n    }\n  }\n\n  _ref = ref => {\n    this.ref = ref\n    this.componentDidUpdate()\n  }\n\n  render() {\n    const { indeterminate, ...props } = this.props\n    props.ref = this._ref\n    props.type = 'checkbox'\n    return <input {...props} />\n  }\n}\n\n// ===================================================================\n\nconst actionsShape = PropTypes.arrayOf(\n  PropTypes.shape({\n    // groupedActions: the function will be called with an array of the selected items in parameters\n    // individualActions: the function will be called with the related item in parameters\n    collapsed: PropTypes.bool,\n    disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),\n    handler: PropTypes.func.isRequired,\n    icon: PropTypes.string.isRequired,\n    label: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,\n    level: PropTypes.oneOf(['primary', 'warning', 'danger']),\n    redirectOnSuccess: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),\n  })\n)\n\nconst Action = decorate([\n  provideState({\n    computed: {\n      disabled: ({ items }, { disabled, userData }) =>\n        typeof disabled === 'function' ? disabled(items, userData) : disabled,\n      handler:\n        ({ items }, { handler, userData }) =>\n        () =>\n          handler(items, userData),\n      icon: ({ items }, { icon, userData }) => (typeof icon === 'function' ? icon(items, userData) : icon),\n      items: (_, { items, grouped }) => (Array.isArray(items) || !grouped ? items : [items]),\n      label: ({ items }, { label, userData }) => (typeof label === 'function' ? label(items, userData) : label),\n      level: ({ items }, { level, userData }) => (typeof level === 'function' ? level(items, userData) : level),\n    },\n  }),\n  injectState,\n  ({ state, redirectOnSuccess, userData }) => (\n    <ActionRowButton\n      btnStyle={state.level}\n      disabled={state.disabled}\n      handler={state.handler}\n      icon={state.icon}\n      redirectOnSuccess={redirectOnSuccess}\n      tooltip={state.label}\n    />\n  ),\n])\n\nconst handleFnProps = (prop, items, userData) => (typeof prop === 'function' ? prop(items, userData) : prop)\n\nconst CollapsedActions = decorate([\n  withRouter,\n  provideState({\n    initialState: () => ({\n      runningActions: [],\n    }),\n    effects: {\n      async execute(state, { handler, label, redirectOnSuccess }) {\n        this.state.runningActions = [...this.state.runningActions, label]\n        try {\n          await handler()\n          ifDef(redirectOnSuccess, this.props.router.push)\n        } catch (error) {\n          // ignore when undefined because it usually means that the action has been canceled\n          if (error !== undefined) {\n            if (error instanceof UserError) {\n              _error(error.title, error.body)\n            } else {\n              logError(error)\n              _error(label, defined(error.message, String(error)))\n            }\n          }\n        } finally {\n          this.state.runningActions = this.state.runningActions.filter(action => action !== label)\n        }\n      },\n    },\n    computed: {\n      wrappedActions: ({ runningActions }, { actions }) =>\n        actions.map(action => {\n          action.isRunning = runningActions.includes(action.label)\n          return action\n        }),\n      dropdownId: generateId,\n      actions: ({ wrappedActions: actions }, { items, userData }) =>\n        actions.map(({ disabled, grouped, handler, icon, isRunning, label, level, redirectOnSuccess }) => {\n          const actionItems = Array.isArray(items) || !grouped ? items : [items]\n          return {\n            disabled: isRunning || handleFnProps(disabled, actionItems, userData),\n            handler: () => handler(actionItems, userData),\n            icon: isRunning ? 'loading' : handleFnProps(icon, actionItems, userData),\n            label: handleFnProps(label, actionItems, userData),\n            level: handleFnProps(level, actionItems, userData),\n            redirectOnSuccess: handleFnProps(redirectOnSuccess, actionItems, userData),\n          }\n        }),\n    },\n  }),\n  injectState,\n  ({ state, effects }) => (\n    <Dropdown id={state.dropdownId}>\n      <DropdownToggle bsSize='small' bsStyle='secondary' />\n      <DropdownMenu className='dropdown-menu-right'>\n        {state.actions.map((action, key) => (\n          <MenuItem\n            className={action.level !== undefined ? `text-${action.level}` : ''}\n            disabled={action.disabled}\n            key={key}\n            onClick={action.disabled ? undefined : () => effects.execute(action)}\n          >\n            <Icon icon={action.icon} /> {action.label}\n          </MenuItem>\n        ))}\n      </DropdownMenu>\n    </Dropdown>\n  ),\n])\n\nCollapsedActions.propTypes = {\n  actions: PropTypes.shape({\n    ...actionsShape,\n    grouped: PropTypes.bool,\n  }),\n  items: PropTypes.any,\n  userData: PropTypes.any,\n}\n\nconst LEVELS = [undefined, 'primary', 'warning', 'danger']\n// page number and sort info are optional for backward compatibility\nconst URL_STATE_RE = /^(?:(\\d+)(?:_(\\d+)(?:_(desc|asc))?)?-)?(.*)$/\n\nclass SortedTable extends Component {\n  static propTypes = {\n    defaultColumn: PropTypes.number,\n    defaultFilter: PropTypes.string,\n    collection: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,\n    columns: PropTypes.arrayOf(\n      PropTypes.shape({\n        default: PropTypes.bool,\n        name: PropTypes.node,\n        sortCriteria: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),\n        sortOrder: PropTypes.string,\n        textAlign: PropTypes.string,\n\n        // for the cell render, you can use component or itemRenderer or valuePath\n        //\n        // item and userData will be injected in the component as props\n        // component: <Component />\n        component: PropTypes.func,\n\n        // itemRenderer: (item, userData) => <span />\n        itemRenderer: PropTypes.func,\n\n        // the path to the value, it's also the sort criteria default value\n        // valuePath: 'a.b.c'\n        valuePath: PropTypes.string,\n      })\n    ).isRequired,\n    filterContainer: PropTypes.func,\n    filters: PropTypes.object,\n    actions: PropTypes.arrayOf(\n      PropTypes.shape({\n        // regroup individual actions and grouped actions\n        collapsed: PropTypes.bool,\n        disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),\n        handler: PropTypes.func.isRequired,\n        icon: PropTypes.string.isRequired,\n        individualDisabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),\n        individualHandler: PropTypes.func,\n        individualLabel: PropTypes.node,\n        label: PropTypes.node.isRequired,\n        level: PropTypes.oneOf(['primary', 'warning', 'danger']),\n        redirectOnSuccess: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),\n      })\n    ),\n    groupedActions: actionsShape,\n    individualActions: actionsShape,\n    itemsPerPageContainer: PropTypes.func,\n    onSelect: PropTypes.func,\n    paginationContainer: PropTypes.func,\n    rowAction: PropTypes.func,\n    rowLink: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),\n    rowTransform: PropTypes.func,\n    // DOM node selector like body or .my-class\n    // The shortcuts will be enabled when the node is focused\n    shortcutsTarget: PropTypes.string,\n    stateUrlParam: PropTypes.string.isRequired,\n\n    // @deprecated, use `data-${key}` instead\n    userData: PropTypes.any,\n  }\n\n  constructor(props, context) {\n    super(props, context)\n\n    this._getUserData =\n      'userData' in props\n        ? () => this.props.userData\n        : createCollectionWrapper(() => {\n            const { props } = this\n            const userData = {}\n            Object.keys(props).forEach(key => {\n              if (key.startsWith('data-')) {\n                userData[key.slice(5)] = props[key]\n              }\n            })\n            return isEmpty(userData) ? undefined : userData\n          })\n\n    const state = (this.state = {\n      all: false, // whether all items are selected (across pages)\n      itemsPerPage: +defined(cookies.get(`${props.location.pathname}-${props.stateUrlParam}`), DEFAULT_ITEMS_PER_PAGE),\n    })\n\n    this._getSelectedColumn = () => this.props.columns[this._getSelectedColumnId()]\n\n    let getAllItems = () => this.props.collection\n    if ('rowTransform' in props) {\n      getAllItems = createSelector(\n        getAllItems,\n        this._getUserData,\n        () => this.props.rowTransform,\n        (items, userData, rowTransform) => map(items, item => rowTransform(item, userData))\n      )\n    }\n    this._getTotalNumberOfItems = createCounter(getAllItems)\n\n    this._getItems = createSort(\n      createFilter(\n        getAllItems,\n        createSelector(this._getFilter, filter => {\n          try {\n            return CM.parse(filter).createPredicate()\n          } catch (_) {}\n        })\n      ),\n      createSelector(\n        () => this._getSelectedColumn().valuePath,\n        () => this._getSelectedColumn().sortCriteria,\n        this._getUserData,\n        (valuePath, sortCriteria = valuePath, userData) =>\n          typeof sortCriteria === 'function' ? object => sortCriteria(object, userData) : sortCriteria\n      ),\n      this._getSortOrder\n    )\n\n    this._getVisibleItems = createPager(this._getItems, this._getPage, () => this.state.itemsPerPage)\n\n    state.selectedItemsIds = new Set()\n\n    this._getSelectedItems = createSelector(\n      () => this.state.all,\n      () => this.state.selectedItemsIds,\n      this._getItems,\n      (all, selectedItemsIds, items) => (all ? items : filter(items, item => selectedItemsIds.has(item.id)))\n    )\n\n    this._hasGroupedActions = createSelector(this._getGroupedActions, actions => !isEmpty(actions))\n\n    this._getShortcutsHandler = createSelector(\n      this._getVisibleItems,\n      this._hasGroupedActions,\n      () => this.state.highlighted,\n      () => this.props.rowLink,\n      () => this.props.rowAction,\n      this._getUserData,\n      (visibleItems, hasGroupedActions, itemIndex, rowLink, rowAction, userData) => (command, event) => {\n        event.preventDefault()\n        const item = itemIndex !== undefined ? visibleItems[itemIndex] : undefined\n\n        switch (command) {\n          case 'SEARCH':\n            this.refs.filterInput.focus()\n            break\n          case 'NAV_DOWN':\n            if (hasGroupedActions || rowAction !== undefined || rowLink !== undefined) {\n              this.setState({\n                highlighted: (itemIndex + visibleItems.length + 1) % visibleItems.length || 0,\n              })\n            }\n            break\n          case 'NAV_UP':\n            if (hasGroupedActions || rowAction !== undefined || rowLink !== undefined) {\n              this.setState({\n                highlighted: (itemIndex + visibleItems.length - 1) % visibleItems.length || 0,\n              })\n            }\n            break\n          case 'SELECT':\n            if (itemIndex !== undefined && hasGroupedActions) {\n              this._selectItem(itemIndex)\n            }\n            break\n          case 'ROW_ACTION':\n            if (item !== undefined) {\n              if (rowLink !== undefined) {\n                this.props.router.push(typeof rowLink === 'function' ? rowLink(item, userData) : rowLink)\n              } else if (rowAction !== undefined) {\n                rowAction(item, userData)\n              }\n            }\n            break\n        }\n      }\n    )\n  }\n\n  componentDidMount() {\n    // Force one Portal refresh.\n    // Because Portal cannot see the container reference at first rendering.\n    if (this.props.paginationContainer) {\n      this.forceUpdate()\n    }\n  }\n\n  _sort = columnId => {\n    this._updateQueryString({\n      selectedColumn: columnId,\n      sortOrder:\n        this._getSelectedColumnId() === columnId\n          ? this._getSortOrder() === 'desc'\n            ? 'asc'\n            : 'desc'\n          : defined(this.props.columns[columnId].sortOrder, 'asc'),\n    })\n  }\n\n  componentDidUpdate() {\n    const { selectedItemsIds } = 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 = selectedItemsIds.intersect(map(this._getVisibleItems(), 'id'))\n      if (newSelectedItems.size < selectedItemsIds.size) {\n        this.setState({ selectedItemsIds: newSelectedItems })\n      }\n    }\n  }\n\n  _updateQueryString({\n    filter = this._getFilter(),\n    page = this._getPage(),\n    selectedColumn = this._getSelectedColumnId(),\n    sortOrder = this._getSortOrder(),\n  }) {\n    const { location, router } = this.props\n    router.replace({\n      ...location,\n      query: {\n        ...location.query,\n        [this.props.stateUrlParam]: `${page}_${selectedColumn}_${sortOrder}-${filter}`,\n      },\n    })\n  }\n\n  _setFilter = filter => {\n    this.setState({\n      highlighted: undefined,\n    })\n    this._updateQueryString({\n      filter,\n      page: 1,\n    })\n  }\n\n  _setPage(page) {\n    this._updateQueryString({ page })\n  }\n  _setPage = this._setPage.bind(this)\n\n  goTo(id) {\n    this._setPage(Math.floor(this._getItems().findIndex(item => item.id === id) / this.state.itemsPerPage) + 1)\n  }\n\n  _selectAllVisibleItems = event => {\n    const { checked } = event.target\n    const { onSelect } = this.props\n    if (onSelect !== undefined) {\n      onSelect(checked ? map(this._getVisibleItems(), 'id') : [])\n    }\n\n    this.setState({\n      all: false,\n      selectedItemsIds: checked\n        ? this.state.selectedItemsIds.union(map(this._getVisibleItems(), 'id'))\n        : this.state.selectedItemsIds.clear(),\n    })\n  }\n\n  // TODO: figure out why it's necessary\n  _toggleNestedCheckboxGuard = false\n\n  _toggleNestedCheckbox = event => {\n    const child = event.target.firstElementChild\n    if (child != null && child.tagName === 'INPUT') {\n      if (this._toggleNestedCheckboxGuard) {\n        return\n      }\n      this._toggleNestedCheckboxGuard = true\n      child.dispatchEvent(new window.MouseEvent('click', event.nativeEvent))\n      this._toggleNestedCheckboxGuard = false\n    }\n  }\n\n  _selectAll = () => {\n    const { onSelect } = this.props\n    if (onSelect !== undefined) {\n      onSelect(map(this._getItems(), 'id'))\n    }\n    this.setState({ all: true })\n  }\n\n  _selectItem(current, selected, range = false) {\n    const { onSelect } = this.props\n    const { all, selectedItemsIds } = this.state\n    const visibleItems = this._getVisibleItems()\n    const item = visibleItems[current]\n    let _selectedItemsIds\n\n    if (all) {\n      _selectedItemsIds = new Set().withMutations(selectedItemsIds => {\n        forEach(visibleItems, item => {\n          selectedItemsIds.add(item.id)\n        })\n        selectedItemsIds.delete(item.id)\n      })\n    } else {\n      const method = (selected === undefined ? !selectedItemsIds.has(item.id) : selected) ? 'add' : 'delete'\n\n      let previous\n      _selectedItemsIds =\n        range && (previous = this._previous) !== undefined\n          ? selectedItemsIds.withMutations(selectedItemsIds => {\n              let i = previous\n              let end = current\n              if (previous > current) {\n                i = current\n                end = previous\n              }\n              for (; i <= end; ++i) {\n                selectedItemsIds[method](visibleItems[i].id)\n              }\n            })\n          : selectedItemsIds[method](item.id)\n      this._previous = current\n    }\n\n    if (onSelect !== undefined) {\n      onSelect(_selectedItemsIds.toArray())\n    }\n\n    this.setState({\n      all: false,\n      selectedItemsIds: _selectedItemsIds,\n    })\n  }\n\n  _onSelectItemCheckbox = event => {\n    const { target } = event\n    this._selectItem(+target.name, target.checked, event.nativeEvent.shiftKey)\n  }\n\n  _getParsedQueryString = createSelector(\n    () => this.props.router.location.query[this.props.stateUrlParam],\n    urlState => {\n      if (urlState === undefined) {\n        return {}\n      }\n      const [, page, selectedColumnId, sortOrder, filter] = URL_STATE_RE.exec(urlState) || []\n      return {\n        filter,\n        page,\n        selectedColumnId,\n        sortOrder,\n      }\n    }\n  )\n\n  _getFilter = createSelector(\n    () => this._getParsedQueryString().filter,\n    () => this.props.filters,\n    () => this.props.defaultFilter,\n    (filter, filters, defaultFilter) => defined(filter, () => filters[defaultFilter], '')\n  )\n\n  _getNPages = createSelector(\n    () => this._getItems().length,\n    () => this.state.itemsPerPage,\n    (nItems, itemsPerPage) => ceil(nItems / itemsPerPage)\n  )\n\n  _getPage = createSelector(\n    () => this._getParsedQueryString().page,\n    this._getNPages,\n    (page = 1, lastPage) => Math.min(+page, lastPage)\n  )\n\n  _getSelectedColumnId = createSelector(\n    () => this._getParsedQueryString().selectedColumnId,\n    () => this.props.columns,\n    () => this.props.defaultColumn,\n    (columnIndex, columns, defaultColumnIndex) =>\n      columnIndex !== undefined && (columnIndex = +columnIndex) < columns.length\n        ? columnIndex\n        : defined(defaultColumnIndex, (columnIndex = findIndex(columns, 'default')) !== -1 ? columnIndex : 0)\n  )\n\n  _getSortOrder = createSelector(\n    () => this._getParsedQueryString().sortOrder,\n    this._getSelectedColumnId,\n    () => this.props.columns,\n    (sortOrder, selectedColumnIndex, columns) => defined(sortOrder, columns[selectedColumnIndex].sortOrder, 'asc')\n  )\n\n  _getGroupedActions = createSelector(\n    () => this.props.groupedActions,\n    () => this.props.actions,\n    (groupedActions, actions) =>\n      groupBy(\n        sortBy(\n          groupedActions !== undefined && actions !== undefined\n            ? groupedActions.concat(actions)\n            : groupedActions || actions,\n          action => LEVELS.indexOf(action.level)\n        ),\n        action => (action.collapsed ? 'secondary' : 'primary')\n      )\n  )\n\n  _getIndividualActions = createSelector(\n    () => this.props.individualActions,\n    () => this.props.actions,\n    (individualActions, actions) => {\n      const normalizedActions = map(actions, a => ({\n        collapsed: a.collapsed,\n        disabled: a.individualDisabled !== undefined ? a.individualDisabled : a.disabled,\n        grouped: a.individualHandler === undefined,\n        handler: a.individualHandler !== undefined ? a.individualHandler : a.handler,\n        icon: a.icon,\n        label: a.individualLabel !== undefined ? a.individualLabel : a.label,\n        level: a.level,\n        redirectOnSuccess: a.redirectOnSuccess,\n      }))\n\n      return groupBy(\n        sortBy(\n          individualActions !== undefined && actions !== undefined\n            ? individualActions.concat(normalizedActions)\n            : individualActions || normalizedActions,\n          action => LEVELS.indexOf(action.level)\n        ),\n        action => (action.collapsed ? 'secondary' : 'primary')\n      )\n    }\n  )\n\n  _renderItem = (item, i) => {\n    const { props, state } = this\n    const { actions, individualActions, onSelect, rowAction, rowLink } = props\n    const userData = this._getUserData()\n\n    const hasGroupedActions = this._hasGroupedActions()\n    const hasIndividualActions = !isEmpty(individualActions) || !isEmpty(actions)\n\n    const columns = map(props.columns, ({ component: Component, itemRenderer, valuePath, textAlign }, key) => (\n      <td className={textAlign && `text-xs-${textAlign}`} key={key}>\n        {Component !== undefined ? (\n          <Component item={item} userData={userData} />\n        ) : valuePath !== undefined ? (\n          getProperty(item, valuePath)\n        ) : (\n          itemRenderer(item, userData)\n        )}\n      </td>\n    ))\n\n    const { id = i } = item\n\n    const selectionColumn = (hasGroupedActions || onSelect !== undefined) && (\n      <td className='text-xs-center' onClick={this._toggleNestedCheckbox}>\n        <input\n          checked={state.all || state.selectedItemsIds.has(id)}\n          name={i} // position in visible items\n          onChange={this._onSelectItemCheckbox}\n          type='checkbox'\n        />\n      </td>\n    )\n\n    let actionsColumn\n    if (hasIndividualActions) {\n      const { primary, secondary } = this._getIndividualActions()\n      actionsColumn = (\n        <td>\n          <div className='pull-right'>\n            <ButtonGroup>\n              {map(primary, (props, key) => (\n                <Action {...props} items={item} key={key} userData={userData} />\n              ))}\n              {secondary !== undefined && <CollapsedActions actions={secondary} items={item} userData={userData} />}\n            </ButtonGroup>\n          </div>\n        </td>\n      )\n    }\n\n    return rowLink != null ? (\n      <BlockLink\n        className={state.highlighted === i ? styles.highlight : undefined}\n        key={id}\n        tagName='tr'\n        to={typeof rowLink === 'function' ? rowLink(item, userData) : rowLink}\n      >\n        {selectionColumn}\n        {columns}\n        {actionsColumn}\n      </BlockLink>\n    ) : (\n      <tr\n        className={classNames(rowAction && styles.clickableRow, state.highlighted === i && styles.highlight)}\n        key={id}\n        onClick={rowAction && (() => rowAction(item, userData))}\n      >\n        {selectionColumn}\n        {columns}\n        {actionsColumn}\n      </tr>\n    )\n  }\n\n  _setNItemsPerPage = itemsPerPage => {\n    const { location, stateUrlParam } = this.props\n    this.setState({ itemsPerPage })\n    cookies.set(`${location.pathname}-${stateUrlParam}`, itemsPerPage)\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._setPage(1)\n  }\n\n  render() {\n    const { props, state } = this\n    const {\n      actions,\n      filterContainer,\n      individualActions,\n      itemsPerPageContainer,\n      onSelect,\n      paginationContainer,\n      shortcutsTarget,\n      stateUrlParam,\n    } = props\n    const { all, itemsPerPage } = state\n    const groupedActions = this._getGroupedActions()\n\n    const nAllItems = this._getTotalNumberOfItems()\n    const nItems = this._getItems().length\n    const nSelectedItems = state.selectedItemsIds.size\n    const nVisibleItems = this._getVisibleItems().length\n\n    const hasGroupedActions = this._hasGroupedActions()\n    const hasIndividualActions = !isEmpty(individualActions) || !isEmpty(actions)\n\n    const nColumns = props.columns.length + (hasIndividualActions ? 2 : 1)\n\n    const displayPagination = paginationContainer === undefined && itemsPerPage < nAllItems\n\n    const paginationInstance = displayPagination && (\n      <Pagination pages={this._getNPages()} onChange={this._setPage} value={this._getPage()} />\n    )\n\n    const filterInstance = (\n      <TableFilter filters={props.filters} onChange={this._setFilter} ref='filterInput' value={this._getFilter()} />\n    )\n\n    const dropdownItemsPerPage = (\n      <DropdownButton bsStyle='info' id={stateUrlParam} title={itemsPerPage}>\n        {ITEMS_PER_PAGE_OPTIONS.map(nItems => (\n          <MenuItem key={nItems} onClick={() => this._setNItemsPerPage(nItems)}>\n            {nItems}\n          </MenuItem>\n        ))}\n      </DropdownButton>\n    )\n\n    const userData = this._getUserData()\n\n    return (\n      <div className={props.className}>\n        {shortcutsTarget !== undefined && (\n          <Shortcuts\n            handler={this._getShortcutsHandler()}\n            isolate\n            name='SortedTable'\n            targetNodeSelector={shortcutsTarget}\n          />\n        )}\n        <Container className='mb-1 p-0'>\n          <SingleLineRow>\n            <Col mediumSize={7}>\n              {displayPagination &&\n                (paginationContainer !== undefined ? (\n                  // Rebuild container function to refresh Portal component.\n                  <Portal container={() => paginationContainer()}>{paginationInstance}</Portal>\n                ) : (\n                  paginationInstance\n                ))}\n            </Col>\n            <Col mediumSize={4}>\n              {filterContainer ? <Portal container={() => filterContainer()}>{filterInstance}</Portal> : filterInstance}\n            </Col>\n            <Col mediumSize={1} style={{ justifyContent: 'end', display: 'flex' }}>\n              {itemsPerPageContainer !== undefined ? (\n                <Portal container={() => itemsPerPageContainer()}>{dropdownItemsPerPage}</Portal>\n              ) : (\n                dropdownItemsPerPage\n              )}\n            </Col>\n          </SingleLineRow>\n        </Container>\n        <table className='table'>\n          <thead className='thead-default'>\n            <tr>\n              <th colSpan={nColumns}>\n                {nItems === nAllItems\n                  ? _('sortedTableNumberOfItems', { nTotal: nItems })\n                  : _('sortedTableNumberOfFilteredItems', {\n                      nFiltered: nItems,\n                      nTotal: nAllItems,\n                    })}\n                {all ? (\n                  <span>\n                    {' '}\n                    - <span className='text-danger'>{_('sortedTableAllItemsSelected', { nItems })}</span>\n                  </span>\n                ) : (\n                  nSelectedItems !== 0 && (\n                    <span>\n                      {' '}\n                      -{' '}\n                      {_('sortedTableNumberOfSelectedItems', {\n                        nSelected: nSelectedItems,\n                      })}\n                      {nSelectedItems === nVisibleItems && nSelectedItems < nItems && (\n                        <Button btnStyle='info' className='ml-1' onClick={this._selectAll} size='small'>\n                          {_('sortedTableSelectAllItems')}\n                        </Button>\n                      )}\n                    </span>\n                  )\n                )}\n                {(nSelectedItems !== 0 || all) && (\n                  <div className='pull-right'>\n                    <ButtonGroup>\n                      {map(groupedActions.primary, (props, key) => (\n                        <Action {...props} key={key} items={this._getSelectedItems()} userData={userData} />\n                      ))}\n                      {groupedActions.secondary !== undefined && (\n                        <CollapsedActions\n                          actions={groupedActions.secondary}\n                          items={this._getSelectedItems()}\n                          userData={userData}\n                        />\n                      )}\n                    </ButtonGroup>\n                  </div>\n                )}\n              </th>\n            </tr>\n            <tr>\n              {(hasGroupedActions || onSelect !== undefined) && (\n                <th className='text-xs-center' onClick={this._toggleNestedCheckbox}>\n                  <Checkbox\n                    onChange={this._selectAllVisibleItems}\n                    checked={all || nSelectedItems !== 0}\n                    indeterminate={!all && nSelectedItems !== 0 && nSelectedItems !== nVisibleItems}\n                  />\n                </th>\n              )}\n              {map(props.columns, (column, key) => (\n                <ColumnHead\n                  textAlign={column.textAlign}\n                  columnId={key}\n                  key={key}\n                  name={column.name}\n                  sort={(column.sortCriteria !== undefined || column.valuePath !== undefined) && this._sort}\n                  sortIcon={this._getSelectedColumnId() === key ? this._getSortOrder() : 'sort'}\n                  tooltip={column.tooltip}\n                />\n              ))}\n              {hasIndividualActions && <th />}\n            </tr>\n          </thead>\n          <tbody>\n            {nVisibleItems !== 0 ? (\n              map(this._getVisibleItems(), this._renderItem)\n            ) : (\n              <tr>\n                <td className='text-info text-xs-center' colSpan={nColumns}>\n                  {_('sortedTableNoItems')}\n                </td>\n              </tr>\n            )}\n          </tbody>\n        </table>\n      </div>\n    )\n  }\n}\n\n// withRouter is needed to trigger a render on filtering/sorting items\nexport default withRouter(SortedTable, { withRef: true })\n","import _ from 'intl'\nimport ActionRowButton from 'action-row-button'\nimport BaseComponent from 'base-component'\nimport Button from 'button'\nimport ButtonGroup from 'button-group'\nimport Icon from 'icon'\nimport map from 'lodash/map'\nimport React, { Component } from 'react'\nimport Tooltip from 'tooltip'\nimport { connectStore } from 'utils'\nimport { createGetObject, createGetObjectsOfType, createSelector } from 'selectors'\nimport { connectPif, disconnectPif } from 'xo'\n\n@connectStore(() => {\n  const pif = createGetObject()\n  const host = createGetObject(createSelector(pif, pif => pif?.$host))\n  const disableUnplug = createSelector(\n    pif,\n    pif => pif?.attached && !pif?.isBondMaster && (pif?.management || pif?.disallowUnplug)\n  )\n\n  const bonds = createGetObjectsOfType('bond')\n  const bond = createSelector(pif, bonds, (pif, bonds) => Object.values(bonds).find(bond => bond.master === pif.id))\n\n  return { host, pif, disableUnplug, bond }\n})\nclass PifItem extends Component {\n  render() {\n    const { pif, host, disableUnplug, bond } = this.props\n\n    return (\n      <tr>\n        <td>{pif?.device ?? _('unknown')}</td>\n        <td>{host?.name_label ?? _('unknown')}</td>\n        <td>{pif?.ip ?? _('unknown')}</td>\n        <td>{pif?.mac ?? _('unknown')}</td>\n        <td>{bond?.mode ?? '-'}</td>\n        <td>\n          {pif?.carrier === undefined ? (\n            <span className='tag tag-warning'>{_('unknown')}</span>\n          ) : pif.carrier ? (\n            <span className='tag tag-success'>{_('poolNetworkPifAttached')}</span>\n          ) : (\n            <span className='tag tag-default'>{_('poolNetworkPifDetached')}</span>\n          )}\n        </td>\n        <td className='text-xs-right'>\n          {pif !== undefined && (\n            <ButtonGroup>\n              <ActionRowButton\n                disabled={disableUnplug}\n                handler={pif.attached ? disconnectPif : connectPif}\n                handlerParam={pif}\n                icon={pif.attached ? 'disconnect' : 'connect'}\n                tooltip={pif.attached ? _('disconnectPif') : _('connectPif')}\n              />\n            </ButtonGroup>\n          )}\n        </td>\n      </tr>\n    )\n  }\n}\n\nexport default class PifsColumn extends BaseComponent {\n  render() {\n    const { network, pifs } = this.props\n    const { showPifs } = this.state\n\n    return (\n      <div>\n        <Tooltip content={showPifs ? _('hidePifs') : _('showPifs')}>\n          <Button size='small' className='mb-1 pull-right' onClick={this.toggleState('showPifs')}>\n            <Icon icon={showPifs ? 'hidden' : 'shown'} />\n          </Button>\n        </Tooltip>\n        {showPifs && (\n          <table className='table'>\n            <thead className='thead-default'>\n              <tr>\n                <th>{_('pifDeviceLabel')}</th>\n                <th>{_('homeTypeHost')}</th>\n                <th>{_('pifAddressLabel')}</th>\n                <th>{_('pifMacLabel')}</th>\n                <th>{_('bondMode')}</th>\n                <th>{_('pifStatusLabel')}</th>\n                <th />\n              </tr>\n            </thead>\n            <tbody>\n              {map(network?.PIFs ?? pifs, (pifId, index) => (\n                <PifItem key={pifId ?? index} id={pifId} />\n              ))}\n            </tbody>\n          </table>\n        )}\n      </div>\n    )\n  }\n}\n","import PropTypes from 'prop-types'\nimport React from 'react'\nimport styled from 'styled-components'\nimport omit from 'lodash/omit.js'\n\nimport ActionButton from './action-button'\n\n// do not forward `state` to ActionButton\nconst Button = styled(p => <ActionButton {...omit(p, 'state')} />)`\n  background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]};\n  border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};\n  color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};\n`\n\nconst StateButton = ({\n  disabledHandler,\n  disabledHandlerParam,\n  disabledLabel,\n  disabledTooltip,\n\n  enabledLabel,\n  enabledTooltip,\n  enabledHandler,\n  enabledHandlerParam,\n\n  state,\n  ...props\n}) => (\n  <Button\n    handler={state ? enabledHandler : disabledHandler}\n    handlerParam={state ? enabledHandlerParam : disabledHandlerParam}\n    tooltip={state ? enabledTooltip : disabledTooltip}\n    {...props}\n    icon={state ? 'running' : 'halted'}\n    size='small'\n    state={state}\n  >\n    {state ? enabledLabel : disabledLabel}\n  </Button>\n)\n\nStateButton.propTypes = {\n  state: PropTypes.bool.isRequired,\n}\n\nexport { StateButton as default }\n","import PropTypes from 'prop-types'\nimport React from 'react'\nimport forOwn from 'lodash/forOwn.js'\n\nimport _ from './intl'\nimport { fetchHostStats, fetchSrStats, fetchVmStats } from './xo'\nimport { Select } from './form'\n\nexport const DEFAULT_GRANULARITY = {\n  granularity: 'seconds',\n  label: _('statLastTenMinutes'),\n  value: 'lastTenMinutes',\n}\n\nexport const INTERVAL_BY_GRANULARITY = {\n  seconds: 5,\n  minutes: 60,\n  hours: 3600,\n  days: 86400,\n}\n\nconst OPTIONS = [\n  DEFAULT_GRANULARITY,\n  {\n    granularity: 'minutes',\n    label: _('statLastTwoHours'),\n    value: 'lastTwoHours',\n  },\n  {\n    granularity: 'hours',\n    keep: 24,\n    label: _('statLastDay'),\n    value: 'lastDay',\n  },\n  {\n    granularity: 'hours',\n    label: _('statLastWeek'),\n    value: 'lastWeek',\n  },\n  {\n    granularity: 'days',\n    label: _('statLastYear'),\n    value: 'lastYear',\n  },\n]\n\nexport const SelectGranularity = ({ onChange, value, ...props }) => (\n  <Select {...props} onChange={onChange} options={OPTIONS} value={value} />\n)\n\nSelectGranularity.propTypes = {\n  onChange: PropTypes.func.isRequired,\n  value: PropTypes.object.isRequired,\n}\n\n// ===================================================================\n\nconst FETCH_FN_BY_TYPE = {\n  host: fetchHostStats,\n  sr: fetchSrStats,\n  vm: fetchVmStats,\n}\n\nconst keepNLastItems = (stats, n) =>\n  Array.isArray(stats) ? stats.splice(0, stats.length - n) : forOwn(stats, metrics => keepNLastItems(metrics, n))\n\nexport const fetchStats = async (objOrId, type, { granularity, keep }) => {\n  const stats = await FETCH_FN_BY_TYPE[type](objOrId, granularity)\n  if (keep !== undefined) {\n    keepNLastItems(stats, keep)\n  }\n  return stats\n}\n","const createAction = (() => {\n  const { defineProperty } = Object\n\n  return (type, payloadCreator) =>\n    defineProperty(\n      payloadCreator\n        ? (...args) => ({\n            type,\n            payload: payloadCreator(...args),\n          })\n        : (action =>\n            function () {\n              if (arguments.length) {\n                throw new Error('this action expects no payload!')\n              }\n\n              return action\n            })({ type }),\n      'toString',\n      { value: () => type }\n    )\n})()\n\n// ===================================================================\n\nexport const selectLang = createAction('SELECT_LANG', lang => lang)\n\n// ===================================================================\n\nexport const connected = createAction('CONNECTED')\nexport const disconnected = createAction('DISCONNECTED')\n\nexport const markObjectsFetched = createAction('OBJECTS_FETCHED')\nexport const updateObjects = createAction('UPDATE_OBJECTS', updates => updates)\nexport const updatePermissions = createAction('UPDATE_PERMISSIONS', permissions => permissions)\n\nexport const signedIn = createAction('SIGNED_IN', user => user)\nexport const signedOut = createAction('SIGNED_OUT')\n\nexport const setXoaUpdaterState = createAction('XOA_UPDATER_STATE', state => state)\nexport const setXoaTrialState = createAction('XOA_TRIAL_STATE', state => state)\nexport const setXoaUpdaterLog = createAction('XOA_UPDATER_LOG', log => log)\nexport const setXoaRegisterState = createAction('XOA_REGISTER_STATE', registration => registration)\nexport const setXoaConfiguration = createAction('XOA_CONFIGURATION', configuration => configuration)\nexport const setHomeVmIdsSelection = createAction('SET_HOME_VM_IDS_SELECTION', homeVmIdsSelection => homeVmIdsSelection)\nexport const markHubResourceAsInstalling = createAction('MARK_HUB_RESOURCE_AS_INSTALLING', id => id)\nexport const markHubResourceAsInstalled = createAction('MARK_HUB_RESOURCE_AS_INSTALLED', id => id)\nexport const markRecipeAsCreating = createAction('MARK_RECIPE_AS_CREATING', id => id)\nexport const markRecipeAsDone = createAction('MARK_RECIPE_AS_DONE', id => id)\n","import reduxThunk from 'redux-thunk'\nimport { applyMiddleware, combineReducers, createStore } from 'redux'\n\nimport { connectStore as connectXo } from '../xo'\n\nimport reducer from './reducer'\n\n// ===================================================================\n\nconst store = createStore(combineReducers(reducer), applyMiddleware(reduxThunk))\n\nconnectXo(store)\n\nif (process.env.XOA_PLAN < 5) {\n  require('xoa-updater').connectStore(store)\n}\n\nexport default store\n","import cookies from 'js-cookie'\nimport omit from 'lodash/omit.js'\n\nimport invoke from '../invoke'\n\nimport * as actions from './actions'\n\n// ===================================================================\n\nconst createAsyncHandler =\n  ({ error, next }) =>\n  (state, payload, action) => {\n    if (action.error) {\n      if (error) {\n        return error(state, payload, action)\n      }\n    } else {\n      if (next) {\n        return next(state, payload, action)\n      }\n    }\n\n    return state\n  }\n\n// Action handlers are reducers but bound to a specific action.\nconst combineActionHandlers = invoke(\n  Object.hasOwnProperty,\n  obj => {\n    for (const prop in obj) {\n      return prop\n    }\n  },\n  (has, firstProp) => (initialState, handlers) => {\n    let n = 0\n    for (const actionType in handlers) {\n      if (has.call(handlers, actionType)) {\n        if (actionType === 'undefined') {\n          throw new Error('invalid action type: undefined')\n        }\n\n        ++n\n\n        const handler = handlers[actionType]\n        if (typeof handler === 'object') {\n          handlers[actionType] = createAsyncHandler(handler)\n        }\n      }\n    }\n\n    if (!n) {\n      throw new Error('no action handlers declared')\n    }\n\n    // Optimization for this special case.\n    if (n === 1) {\n      const actionType = firstProp(handlers)\n      const handler = handlers[actionType]\n\n      return (state = initialState, action) =>\n        action.type === actionType ? handler(state, action.payload, action) : state\n    }\n\n    return (state = initialState, action) => {\n      const handler = handlers[action.type]\n\n      return handler ? handler(state, action.payload, action) : state\n    }\n  }\n)\n\n// ===================================================================\n\nexport default {\n  lang: combineActionHandlers(cookies.get('lang') || 'en', {\n    [actions.selectLang]: (_, lang) => {\n      cookies.set('lang', lang)\n\n      return lang\n    },\n  }),\n\n  permissions: combineActionHandlers(\n    {},\n    {\n      [actions.updatePermissions]: (_, permissions) => permissions,\n    }\n  ),\n\n  // These IDs are used temporarily to be preselected in backup/new/vms\n  homeVmIdsSelection: combineActionHandlers([], {\n    [actions.setHomeVmIdsSelection]: (_, homeVmIdsSelection) => homeVmIdsSelection,\n  }),\n\n  // whether a resource is currently being installed: `hubInstallingResources[<template id>]`\n  hubInstallingResources: combineActionHandlers(\n    {},\n    {\n      [actions.markHubResourceAsInstalling]: (prevHubInstallingResources, id) => ({\n        ...prevHubInstallingResources,\n        [id]: true,\n      }),\n      [actions.markHubResourceAsInstalled]: (prevHubInstallingResources, id) => omit(prevHubInstallingResources, id),\n    }\n  ),\n\n  // whether a resource is currently being created: `recipeCreatingResources[<recipe id>]`\n  recipeCreatingResources: combineActionHandlers(\n    {},\n    {\n      [actions.markRecipeAsCreating]: (prevRecipeCreatingResources, id) => ({\n        ...prevRecipeCreatingResources,\n        [id]: true,\n      }),\n      [actions.markRecipeAsDone]: (prevRecipeCreatedResources, id) => omit(prevRecipeCreatedResources, id),\n    }\n  ),\n\n  objects: combineActionHandlers(\n    {\n      all: {}, // Mutable for performance!\n      byRef: new Map(), // Mutable for performance!\n      byType: {},\n      fetched: false,\n    },\n    {\n      [actions.updateObjects]: ({ all, byRef, byType: prevByType, fetched }, updates) => {\n        const byType = { ...prevByType }\n        const get = type => {\n          const curr = byType[type]\n          const prev = prevByType[type]\n          return curr === prev ? (byType[type] = { ...prev }) : curr\n        }\n\n        for (const id in updates) {\n          const object = updates[id]\n          const previous = all[id]\n\n          if (object) {\n            const { type } = object\n\n            all[id] = object\n            byRef.set(object._xapiRef, object)\n            get(type)[id] = object\n\n            if (previous && previous.type !== type) {\n              delete get(previous.type)[id]\n            }\n          } else if (previous) {\n            delete all[id]\n            delete get(previous.type)[id]\n            byRef.delete(previous._xapiRef)\n          }\n        }\n\n        return { all, byRef, byType, fetched }\n      },\n      [actions.markObjectsFetched]: state => ({\n        ...state,\n        fetched: true,\n      }),\n    }\n  ),\n\n  user: combineActionHandlers(null, {\n    [actions.signedIn]: {\n      next: (_, user) => user,\n    },\n  }),\n\n  status: combineActionHandlers('disconnected', {\n    [actions.connected]: () => 'connected',\n    [actions.disconnected]: () => 'disconnected',\n  }),\n\n  xoaUpdaterState: combineActionHandlers('disconnected', {\n    [actions.setXoaUpdaterState]: (_, state) => state,\n  }),\n  xoaTrialState: combineActionHandlers(\n    {},\n    {\n      [actions.setXoaTrialState]: (_, state) => state,\n    }\n  ),\n  xoaUpdaterLog: combineActionHandlers([], {\n    [actions.setXoaUpdaterLog]: (_, log) => log,\n  }),\n  xoaRegisterState: combineActionHandlers(\n    { state: '?' },\n    {\n      [actions.setXoaRegisterState]: (_, registration) => registration,\n    }\n  ),\n  xoaConfiguration: combineActionHandlers(\n    { proxyHost: '', proxyPort: '', proxyUser: '' },\n    {\n      // defined values for controlled inputs\n      [actions.setXoaConfiguration]: (_, configuration) => {\n        delete configuration.password\n        return configuration\n      },\n    }\n  ),\n}\n","import React from 'react'\n\nimport _ from './intl'\nimport ActionButton from './action-button'\nimport Icon from './icon'\nimport Link from './link'\n\nconst STYLE = {\n  marginBottom: '1em',\n  marginLeft: '1em',\n}\n\nconst TabButton = ({ labelId, ...props }) => (\n  <ActionButton {...props} size='large' style={STYLE}>\n    {labelId !== undefined && <span className='hidden-md-down'>{_(labelId)}</span>}\n  </ActionButton>\n)\nexport { TabButton as default }\n\nexport const TabButtonLink = ({ labelId, icon, ...props }) => (\n  <Link {...props} className='btn btn-lg btn-primary' style={STYLE}>\n    {icon && (\n      <span>\n        <Icon icon={icon} />{' '}\n      </span>\n    )}\n    <span className='hidden-md-down'>{_(labelId)}</span>\n  </Link>\n)\n","import _ from 'intl'\nimport filter from 'lodash/filter'\nimport includes from 'lodash/includes'\nimport keyBy from 'lodash/keyBy'\nimport map from 'lodash/map'\nimport pFinally from 'promise-toolbox/finally'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport relativeLuminance from 'relative-luminance'\nimport { addSubscriptions, connectStore } from 'utils'\nimport { setTag, subscribeConfiguredTags } from 'xo'\nimport { Col, Container, Row } from 'grid'\nimport { isAdmin } from 'selectors'\n\nimport ActionButton from './action-button'\nimport Button from './button'\nimport Component from './base-component'\nimport getEventValue from './get-event-value'\nimport Icon from './icon'\nimport Tooltip from './tooltip'\nimport { confirm } from './modal'\nimport { SelectTag } from './select-objects'\n\nconst noop = Function.prototype\n\nconst DEFAULT_TAG_COLOR = '#2598d9'\n\nconst INPUT_STYLE = {\n  maxWidth: '8em',\n  margin: 'auto',\n}\n\nconst MARGIN_AUTO = {\n  margin: 'auto',\n}\n\nconst INHERIT_STYLE = {\n  display: 'inherit',\n  width: 'inherit',\n}\n\nconst ADD_TAG_STYLE = {\n  cursor: 'pointer',\n  display: 'inline-block',\n  fontSize: '0.8em',\n  marginLeft: '0.2em',\n  verticalAlign: 'middle',\n}\n\nconst Fragment = ({ children }) => <div style={INHERIT_STYLE}>{children}</div>\n\nclass AdvancedTagCreation extends Component {\n  state = {\n    tags: this.props.defaultTags.map(tag => ({ id: tag, value: tag })),\n    tagConfigurations: this.props.tagConfigurations ?? {},\n  }\n\n  get value() {\n    return { ...this.state, tags: this.state.tags.map(_ => _.value) }\n  }\n\n  onChangeTagConfiguration(tagId, key, value) {\n    this.setState({\n      tagConfigurations: {\n        ...this.state.tagConfigurations,\n        [tagId]: {\n          ...this.state.tagConfigurations[tagId],\n          [key]: value,\n        },\n      },\n    })\n  }\n\n  render() {\n    return (\n      <Container>\n        <Row className='d-flex'>\n          <Col>\n            <SelectTag multi onChange={this.linkState('tags')} value={this.state.tags} />\n          </Col>\n        </Row>\n        {this.props.isAdmin && (\n          <ul className='list-group'>\n            {this.state.tags.map(tag => {\n              const _onAddTagColor = () => this.onChangeTagConfiguration(tag.id, 'color', DEFAULT_TAG_COLOR)\n              const _onRemoveTagColor = () => this.onChangeTagConfiguration(tag.id, 'color', null)\n              const _onChangeTagColor = event => this.onChangeTagConfiguration(tag.id, 'color', getEventValue(event))\n\n              const tagConfiguration = this.state.tagConfigurations[tag.id]\n              return (\n                <li className='list-group-item' key={tag.id}>\n                  <Container>\n                    <Row className='d-flex'>\n                      <Col style={MARGIN_AUTO}>{tag.value}</Col>\n                      <Col className='d-flex justify-content-end'>\n                        {tagConfiguration?.color == null ? (\n                          <Button onClick={_onAddTagColor} size='small'>\n                            {_('addColor')}\n                          </Button>\n                        ) : (\n                          <Fragment>\n                            <input\n                              className='form-control mr-1'\n                              style={INPUT_STYLE}\n                              type='color'\n                              onChange={_onChangeTagColor}\n                              value={tagConfiguration.color}\n                            />\n                            <Button onClick={_onRemoveTagColor} size='small'>\n                              {_('removeColor')}\n                            </Button>\n                          </Fragment>\n                        )}\n                      </Col>\n                    </Row>\n                  </Container>\n                </li>\n              )\n            })}\n          </ul>\n        )}\n      </Container>\n    )\n  }\n}\n\n@addSubscriptions({\n  configuredTags: cb => subscribeConfiguredTags(tags => cb(keyBy(tags, 'id'))),\n})\n@connectStore({\n  isAdmin,\n})\nexport default class Tags extends Component {\n  static propTypes = {\n    labels: PropTypes.arrayOf(PropTypes.string).isRequired,\n    onAdd: PropTypes.func,\n    onChange: PropTypes.func,\n    onClick: PropTypes.func,\n    onDelete: PropTypes.func,\n  }\n\n  componentWillMount() {\n    this.setState({ editing: false })\n  }\n\n  _startEdit = () => {\n    this.setState({ editing: true })\n  }\n  _stopEdit = () => {\n    this.setState({ editing: false })\n  }\n\n  _addTag = newTag => {\n    const { labels, onAdd, onChange } = this.props\n\n    if (!includes(labels, newTag)) {\n      onAdd && onAdd(newTag)\n      onChange && onChange([...labels, newTag])\n    }\n  }\n  _deleteTag = tag => {\n    const { onChange, onDelete } = this.props\n\n    onDelete && onDelete(tag)\n    onChange && onChange(filter(this.props.labels, t => t !== tag))\n  }\n\n  _onKeyDown = event => {\n    const { keyCode, target } = event\n\n    if (keyCode === 13) {\n      if (target.value) {\n        this._addTag(target.value)\n        target.value = ''\n      }\n    } else if (keyCode === 27) {\n      this._stopEdit()\n    } else {\n      return\n    }\n\n    event.preventDefault()\n  }\n\n  _advancedTagCreation = () =>\n    confirm({\n      body: (\n        <AdvancedTagCreation\n          isAdmin={this.props.isAdmin}\n          tagConfigurations={this.props.configuredTags}\n          defaultTags={this.props.labels}\n        />\n      ),\n      icon: 'add',\n      title: _('advancedTagCreation'),\n    })\n      ::pFinally(this._stopEdit)\n      .then(({ tags, tagConfigurations }) =>\n        Promise.all(\n          tags.map(async tag => {\n            await this._addTag(tag)\n            const tagConfiguration = tagConfigurations[tag]\n            return this.props.isAdmin && tagConfiguration !== undefined ? setTag(tag, tagConfiguration) : noop()\n          })\n        )\n      )\n\n  _focus = () => {\n    this._focused = true\n  }\n\n  _closeEditionIfUnfocused = () => {\n    this._focused = false\n    setTimeout(() => {\n      !this._focused && this._stopEdit()\n    }, 10)\n  }\n\n  render() {\n    const { labels, onAdd, onChange, onClick, onDelete } = this.props\n\n    const deleteTag = (onDelete || onChange) && this._deleteTag\n\n    return (\n      <div style={{ color: '#999', display: 'inline-block' }}>\n        <div style={{ display: 'inline-block', verticalAlign: 'middle' }}>\n          <Icon icon='tags' />\n        </div>\n        <div style={{ display: 'inline-block', fontSize: '0.6em', verticalAlign: 'middle' }}>\n          {map(labels.sort(), (label, index) => (\n            <Tag label={label} onDelete={deleteTag} key={index} onClick={onClick} />\n          ))}\n        </div>\n        {(onAdd || onChange) && !this.state.editing ? (\n          <div onClick={this._startEdit} style={ADD_TAG_STYLE}>\n            <Icon icon='add-tag' />\n          </div>\n        ) : (\n          <div\n            style={{ display: 'inline-block', verticalAlign: 'middle' }}\n            className='form-inline'\n            onBlur={this._closeEditionIfUnfocused}\n            onFocus={this._focus}\n          >\n            <span className='input-group'>\n              <input autoFocus className='form-control' onKeyDown={this._onKeyDown} style={INPUT_STYLE} type='text' />\n              <span className='input-group-btn'>\n                <Tooltip content={_('advancedTagCreation')}>\n                  <ActionButton handler={this._advancedTagCreation} icon='add' />\n                </Tooltip>\n              </span>\n            </span>\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n\nconst TAG_TO_MESSAGE_ID = {\n  'xo:no-bak': 'tagNoBak',\n  'xo:notify-on-snapshot': 'tagNotifyOnSnapshot',\n  'xo:no-health-check': 'tagNoHealthCheck',\n}\n\n@addSubscriptions({\n  configuredTags: cb => subscribeConfiguredTags(tags => cb(keyBy(tags, 'id'))),\n})\nexport class Tag extends Component {\n  render() {\n    const { label, onDelete, onClick, configuredTags } = this.props\n    const color = configuredTags?.[label]?.color ?? DEFAULT_TAG_COLOR\n    const borderSize = '0.2em'\n    const padding = '0.2em'\n\n    const isLight =\n      relativeLuminance(\n        Array.from({ length: 3 }, (_, i) => {\n          const j = i * 2 + 1\n          return parseInt(color.slice(j, j + 2), 16)\n        })\n      ) > 0.5\n\n    const i = label.indexOf('=')\n    const isScoped = i !== -1\n\n    const scope = isScoped ? label.slice(0, i) : label\n    const reason = isScoped ? label.slice(i + 1) : null\n\n    const messageId = TAG_TO_MESSAGE_ID[scope]\n\n    return (\n      <div\n        style={{\n          background: color,\n          border: borderSize + ' solid ' + color,\n          borderRadius: '0.5em',\n          color: isLight ? '#000' : '#fff',\n          display: 'inline-block',\n          margin: '0.2em',\n\n          // prevent value background from breaking border radius\n          overflow: 'clip',\n        }}\n      >\n        {messageId && (\n          <div\n            style={{\n              cursor: 'help',\n              display: 'inline-block',\n              padding,\n            }}\n          >\n            <Tooltip content={_(messageId, { reason })}>\n              <Icon icon='info' />\n            </Tooltip>\n          </div>\n        )}\n        <div\n          onClick={onClick && (() => onClick(label))}\n          style={{\n            cursor: onClick && 'pointer',\n            display: 'inline-block',\n          }}\n        >\n          <div\n            style={{\n              display: 'inline-block',\n              padding,\n            }}\n          >\n            {scope}\n          </div>\n          {isScoped && (\n            <div\n              style={{\n                background: '#fff',\n                color: '#000',\n                display: 'inline-block',\n                padding,\n              }}\n            >\n              {reason || <i>N/A</i>}\n            </div>\n          )}\n        </div>\n        {onDelete && (\n          <div\n            onClick={onDelete && (() => onDelete(label))}\n            style={{\n              cursor: 'pointer',\n              display: 'inline-block',\n              padding,\n\n              // if isScoped, the display is a bit different\n              background: isScoped && '#fff',\n              color: isScoped && (isLight ? '#000' : color),\n            }}\n          >\n            <Icon icon='remove-tag' />\n          </div>\n        )}\n      </div>\n    )\n  }\n}\nTag.propTypes = {\n  label: PropTypes.string.isRequired,\n  onDelete: PropTypes.func,\n  onClick: PropTypes.func,\n}\n","export default {\n  failure: {\n    icon: 'halted',\n    label: 'taskFailed',\n  },\n  skipped: {\n    icon: 'skipped',\n    label: 'taskSkipped',\n  },\n  success: {\n    icon: 'running',\n    label: 'taskSuccess',\n  },\n  pending: {\n    icon: 'busy',\n    label: 'taskStarted',\n  },\n  interrupted: {\n    icon: 'halted',\n    label: 'taskInterrupted',\n  },\n  unknown: {\n    icon: 'unknown',\n    label: 'unknown',\n  },\n}\n","export default {\n  disabledStateBg: '#fff',\n  disabledStateColor: '#c0392b',\n  enabledStateBg: '#fff',\n  enabledStateColor: '#27ae60',\n}\n","//\n// This file has been generated by [index-modules](https://npmjs.com/index-modules)\n//\n\nconst properties = {}\n\nimport * as _base from \"./base.js\"\nif (\"default\" in _base) properties.base = { enumerable: true, get: () => _base.default }\nexport { _base as base }\n\nexport default Object.create(null, properties)\n","import ActionButton from 'action-button'\nimport map from 'lodash/map'\nimport moment from 'moment-timezone'\nimport PropTypes from 'prop-types'\nimport React from 'react'\n\nimport _ from './intl'\nimport Component from './base-component'\nimport { getXoServerTimezone } from './xo'\nimport { Select } from './form'\n\nconst SERVER_TIMEZONE_TAG = 'server'\nconst LOCAL_TIMEZONE = moment.tz.guess()\n\nexport default class TimezonePicker extends Component {\n  static propTypes = {\n    defaultValue: PropTypes.string,\n    onChange: PropTypes.func.isRequired,\n    required: PropTypes.bool,\n    value: PropTypes.string,\n  }\n\n  componentDidMount() {\n    getXoServerTimezone.then(serverTimezone => {\n      this.setState({\n        timezone: this.props.value || this.props.defaultValue || SERVER_TIMEZONE_TAG,\n        options: [\n          ...map(moment.tz.names(), value => ({ label: value, value })),\n          {\n            label: _('serverTimezoneOption', {\n              value: serverTimezone,\n            }),\n            value: SERVER_TIMEZONE_TAG,\n          },\n        ],\n      })\n    })\n  }\n\n  componentWillReceiveProps(props) {\n    if (props.value !== this.props.value) {\n      this.setState({ timezone: props.value || SERVER_TIMEZONE_TAG })\n    }\n  }\n\n  get value() {\n    return this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone\n  }\n\n  set value(value) {\n    this.setState({ timezone: value || SERVER_TIMEZONE_TAG })\n  }\n\n  _onChange = option => {\n    if (option && option.value === this.state.timezone) {\n      return\n    }\n\n    this.setState(\n      {\n        timezone: (option != null && option.value) || SERVER_TIMEZONE_TAG,\n      },\n      () => this.props.onChange(this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone)\n    )\n  }\n\n  _useLocalTime = () => {\n    this._onChange({ value: LOCAL_TIMEZONE })\n  }\n\n  render() {\n    const { timezone, options } = this.state\n\n    return (\n      <div>\n        <Select\n          className='mb-1'\n          onChange={this._onChange}\n          options={options}\n          placeholder={_('selectTimezone')}\n          required={this.props.required}\n          value={timezone}\n        />\n        <div className='pull-right'>\n          <ActionButton handler={this._useLocalTime} icon='time'>\n            {_('timezonePickerUseLocalTime')}\n          </ActionButton>\n        </div>\n      </div>\n    )\n  }\n}\n","// Source: https://github.com/wwayne/react-tooltip/blob/master/src/utils/getPosition.js\n\n/**\n * Calculate the position of tooltip\n *\n * @params\n * - `e` {Event} the event of current mouse\n * - `target` {Element} the currentTarget of the event\n * - `node` {DOM} the react-tooltip object\n * - `place` {String} top / right / bottom / left\n * - `effect` {String} float / solid\n * - `offset` {Object} the offset to default position\n *\n * @return {Object\n * - `isNewState` {Bool} required\n * - `newState` {Object}\n * - `position` {OBject} {left: {Number}, top: {Number}}\n */\nexport default function (e, target, node, place, effect, offset) {\n  const tipWidth = node.clientWidth\n  const tipHeight = node.clientHeight\n  const { mouseX, mouseY } = getCurrentOffset(e, target, effect)\n  const defaultOffset = getDefaultPosition(effect, target.clientWidth, target.clientHeight, tipWidth, tipHeight)\n  const { extraOffsetX, extraOffsetY } = calculateOffset(offset)\n\n  const windowWidth = window.innerWidth\n  const windowHeight = window.innerHeight\n\n  const { parentTop, parentLeft } = getParent(target)\n\n  // Get the edge offset of the tooltip\n  const getTipOffsetLeft = place => {\n    const offsetX = defaultOffset[place].l\n    return mouseX + offsetX + extraOffsetX\n  }\n  const getTipOffsetRight = place => {\n    const offsetX = defaultOffset[place].r\n    return mouseX + offsetX + extraOffsetX\n  }\n  const getTipOffsetTop = place => {\n    const offsetY = defaultOffset[place].t\n    return mouseY + offsetY + extraOffsetY\n  }\n  const getTipOffsetBottom = place => {\n    const offsetY = defaultOffset[place].b\n    return mouseY + offsetY + extraOffsetY\n  }\n\n  // Judge if the tooltip has over the window(screen)\n  const outsideVertical = () => {\n    let result = false\n    let newPlace\n    if (\n      getTipOffsetTop('left') < 0 &&\n      getTipOffsetBottom('left') <= windowHeight &&\n      getTipOffsetBottom('bottom') <= windowHeight\n    ) {\n      result = true\n      newPlace = 'bottom'\n    } else if (\n      getTipOffsetBottom('left') > windowHeight &&\n      getTipOffsetTop('left') >= 0 &&\n      getTipOffsetTop('top') >= 0\n    ) {\n      result = true\n      newPlace = 'top'\n    }\n    return { result, newPlace }\n  }\n  const outsideLeft = () => {\n    let { result, newPlace } = outsideVertical() // Deal with vertical as first priority\n    if (result && outsideHorizontal().result) {\n      return { result: false } // No need to change, if change to vertical will out of space\n    }\n    if (!result && getTipOffsetLeft('left') < 0 && getTipOffsetRight('right') <= windowWidth) {\n      result = true // If vertical ok, but let out of side and right won't out of side\n      newPlace = 'right'\n    }\n    return { result, newPlace }\n  }\n  const outsideRight = () => {\n    let { result, newPlace } = outsideVertical()\n    if (result && outsideHorizontal().result) {\n      return { result: false } // No need to change, if change to vertical will out of space\n    }\n    if (!result && getTipOffsetRight('right') > windowWidth && getTipOffsetLeft('left') >= 0) {\n      result = true\n      newPlace = 'left'\n    }\n    return { result, newPlace }\n  }\n\n  const outsideHorizontal = () => {\n    let result = false\n    let newPlace\n    if (\n      getTipOffsetLeft('top') < 0 &&\n      getTipOffsetRight('top') <= windowWidth &&\n      getTipOffsetRight('right') <= windowWidth\n    ) {\n      result = true\n      newPlace = 'right'\n    } else if (\n      getTipOffsetRight('top') > windowWidth &&\n      getTipOffsetLeft('top') >= 0 &&\n      getTipOffsetLeft('left') >= 0\n    ) {\n      result = true\n      newPlace = 'left'\n    }\n    return { result, newPlace }\n  }\n  const outsideTop = () => {\n    let { result, newPlace } = outsideHorizontal()\n    if (result && outsideVertical().result) {\n      return { result: false }\n    }\n    if (!result && getTipOffsetTop('top') < 0 && getTipOffsetBottom('bottom') <= windowHeight) {\n      result = true\n      newPlace = 'bottom'\n    }\n    return { result, newPlace }\n  }\n  const outsideBottom = () => {\n    let { result, newPlace } = outsideHorizontal()\n    if (result && outsideVertical().result) {\n      return { result: false }\n    }\n    if (!result && getTipOffsetBottom('bottom') > windowHeight && getTipOffsetTop('top') >= 0) {\n      result = true\n      newPlace = 'top'\n    }\n    return { result, newPlace }\n  }\n\n  // Return new state to change the placement to the reverse if possible\n  const outsideLeftResult = outsideLeft()\n  const outsideRightResult = outsideRight()\n  const outsideTopResult = outsideTop()\n  const outsideBottomResult = outsideBottom()\n\n  if (place === 'left' && outsideLeftResult.result) {\n    return {\n      isNewState: true,\n      newState: { place: outsideLeftResult.newPlace },\n    }\n  } else if (place === 'right' && outsideRightResult.result) {\n    return {\n      isNewState: true,\n      newState: { place: outsideRightResult.newPlace },\n    }\n  } else if (place === 'top' && outsideTopResult.result) {\n    return {\n      isNewState: true,\n      newState: { place: outsideTopResult.newPlace },\n    }\n  } else if (place === 'bottom' && outsideBottomResult.result) {\n    return {\n      isNewState: true,\n      newState: { place: outsideBottomResult.newPlace },\n    }\n  }\n\n  // Return tooltip offset position\n  return {\n    isNewState: false,\n    position: {\n      left: getTipOffsetLeft(place) - parentLeft,\n      top: getTipOffsetTop(place) - parentTop,\n    },\n  }\n}\n\n// Get current mouse offset\nconst getCurrentOffset = (e, currentTarget, effect) => {\n  const boundingClientRect = currentTarget.getBoundingClientRect()\n  const targetTop = boundingClientRect.top\n  const targetLeft = boundingClientRect.left\n  const targetWidth = currentTarget.clientWidth\n  const targetHeight = currentTarget.clientHeight\n\n  if (effect === 'float') {\n    return {\n      mouseX: e.clientX,\n      mouseY: e.clientY,\n    }\n  }\n  return {\n    mouseX: targetLeft + targetWidth / 2,\n    mouseY: targetTop + targetHeight / 2,\n  }\n}\n\n// List all possibility of tooltip final offset\n// This is useful in judging if it is necessary for tooltip to switch position when out of window\nconst getDefaultPosition = (effect, targetWidth, targetHeight, tipWidth, tipHeight) => {\n  let top\n  let right\n  let bottom\n  let left\n  const disToMouse = 3\n  const triangleHeight = 2\n  const cursorHeight = 12 // Optimize for float bottom only, cause the cursor will hide the tooltip\n\n  if (effect === 'float') {\n    top = {\n      l: -(tipWidth / 2),\n      r: tipWidth / 2,\n      t: -(tipHeight + disToMouse + triangleHeight),\n      b: -disToMouse,\n    }\n    bottom = {\n      l: -(tipWidth / 2),\n      r: tipWidth / 2,\n      t: disToMouse + cursorHeight,\n      b: tipHeight + disToMouse + triangleHeight + cursorHeight,\n    }\n    left = {\n      l: -(tipWidth + disToMouse + triangleHeight),\n      r: -disToMouse,\n      t: -(tipHeight / 2),\n      b: tipHeight / 2,\n    }\n    right = {\n      l: disToMouse,\n      r: tipWidth + disToMouse + triangleHeight,\n      t: -(tipHeight / 2),\n      b: tipHeight / 2,\n    }\n  } else if (effect === 'solid') {\n    top = {\n      l: -(tipWidth / 2),\n      r: tipWidth / 2,\n      t: -(targetHeight / 2 + tipHeight + triangleHeight),\n      b: -(targetHeight / 2),\n    }\n    bottom = {\n      l: -(tipWidth / 2),\n      r: tipWidth / 2,\n      t: targetHeight / 2,\n      b: targetHeight / 2 + tipHeight + triangleHeight,\n    }\n    left = {\n      l: -(tipWidth + targetWidth / 2 + triangleHeight),\n      r: -(targetWidth / 2),\n      t: -(tipHeight / 2),\n      b: tipHeight / 2,\n    }\n    right = {\n      l: targetWidth / 2,\n      r: tipWidth + targetWidth / 2 + triangleHeight,\n      t: -(tipHeight / 2),\n      b: tipHeight / 2,\n    }\n  }\n\n  return { top, bottom, left, right }\n}\n\n// Consider additional offset into position calculation\nconst calculateOffset = offset => {\n  let extraOffsetX = 0\n  let extraOffsetY = 0\n\n  if (Object.prototype.toString.apply(offset) === '[object String]') {\n    offset = JSON.parse(offset.toString().replace(/'/g, '\"'))\n  }\n  for (const key in offset) {\n    if (key === 'top') {\n      extraOffsetY -= parseInt(offset[key], 10)\n    } else if (key === 'bottom') {\n      extraOffsetY += parseInt(offset[key], 10)\n    } else if (key === 'left') {\n      extraOffsetX -= parseInt(offset[key], 10)\n    } else if (key === 'right') {\n      extraOffsetX += parseInt(offset[key], 10)\n    }\n  }\n\n  return { extraOffsetX, extraOffsetY }\n}\n\n// Get the offset of the parent elements\nconst getParent = currentTarget => {\n  let currentParent = currentTarget\n  while (currentParent) {\n    if (currentParent.style.transform.length > 0) break\n    currentParent = currentParent.parentElement\n  }\n\n  const parentTop = currentParent && currentParent.getBoundingClientRect().top\n  const parentLeft = currentParent && currentParent.getBoundingClientRect().left\n\n  return { parentTop, parentLeft }\n}\n","module.exports = {\n    \"tooltipEnabled\": \"mcaf272ffe_tooltipEnabled\",\n    \"tooltipDisabled\": \"mcaf272ffe_tooltipDisabled\"\n};","import classNames from 'classnames'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nimport Component from '../base-component'\nimport getPosition from './get-position'\n\nimport styles from './index.css'\n\n// ===================================================================\n\nlet instance\n\nexport class TooltipViewer extends Component {\n  constructor() {\n    super()\n\n    this.state.place = 'top'\n  }\n\n  componentDidMount() {\n    if (instance) {\n      throw new Error('Tooltip viewer is a singleton!')\n    }\n    instance = this\n  }\n\n  componentWillUnmount() {\n    instance = undefined\n  }\n\n  render() {\n    const { className, content, place, show, style } = this.state\n\n    return (\n      <div\n        className={classNames(\n          show && content !== undefined ? styles.tooltipEnabled : styles.tooltipDisabled,\n          className\n        )}\n        style={{\n          marginTop: (place === 'top' && '-10px') || (place === 'bottom' && '10px'),\n          marginLeft: (place === 'left' && '-10px') || (place === 'right' && '10px'),\n          ...style,\n        }}\n      >\n        {content}\n      </div>\n    )\n  }\n}\n\n// ===================================================================\n\nexport default class Tooltip extends Component {\n  static propTypes = {\n    children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),\n    className: PropTypes.string,\n    content: PropTypes.node,\n    style: PropTypes.object,\n    tagName: PropTypes.string,\n  }\n\n  _node = null\n\n  componentDidMount() {\n    this._updateListeners()\n  }\n\n  componentWillUnmount() {\n    this._removeListeners()\n  }\n\n  componentDidUpdate(prevProps) {\n    this._updateListeners()\n  }\n\n  _updateListeners() {\n    // eslint-disable-next-line react/no-find-dom-node\n    const node = ReactDOM.findDOMNode(this)\n\n    if (node !== this._node) {\n      this._removeListeners()\n\n      this._node = node\n      if (node !== null) {\n        // 2020-06-15: Use pointer events instead of mouse events to workaround\n        // Chrome not firing any mouse event on disabled inputs. Pointer events\n        // should be correctly fired on most browsers and are similar to mouse\n        // events on mouse-controlled devices.\n        // https://github.com/reach/reach-ui/issues/564#issuecomment-620502842\n        // https://caniuse.com/#feat=mdn-api_pointerevent\n        node.addEventListener('pointerenter', this._showTooltip)\n        node.addEventListener('pointerleave', this._hideTooltip)\n        node.addEventListener('pointermove', this._updateTooltip)\n      }\n    }\n  }\n\n  _removeListeners() {\n    this._hideTooltip()\n\n    const node = this._node\n    if (node !== null) {\n      node.removeEventListener('pointerenter', this._showTooltip)\n      node.removeEventListener('pointerleave', this._hideTooltip)\n      node.removeEventListener('pointermove', this._updateTooltip)\n    }\n  }\n\n  _showTooltip = () => {\n    const { props } = this\n\n    instance.setState({\n      className: props.className,\n      content: props.content,\n      show: true,\n      style: props.style,\n    })\n  }\n\n  _hideTooltip = () => {\n    if (instance !== undefined) {\n      instance.setState({ show: false })\n    }\n  }\n\n  _updateTooltip = event => {\n    if (instance === undefined) {\n      return\n    }\n\n    // eslint-disable-next-line react/no-find-dom-node\n    const node = ReactDOM.findDOMNode(instance)\n    const result = getPosition(event, event.currentTarget, node, instance.state.place, 'solid', {})\n\n    if (result.isNewState) {\n      return instance.setState(result.newState, () => this._updateTooltip(event))\n    }\n\n    const { position } = result\n    node.style.left = `${position.left}px`\n    node.style.top = `${position.top}px`\n  }\n\n  render() {\n    const { children } = this.props\n\n    if (!children) {\n      return <span />\n    }\n\n    if (typeof children === 'string') {\n      return <span>{children}</span>\n    }\n\n    return children\n  }\n}\n\n// ===================================================================\n\nexport const conditionalTooltip = (children, tooltip) =>\n  tooltip !== undefined && tooltip !== '' ? <Tooltip content={tooltip}>{children}</Tooltip> : children\n","import _ from 'intl'\nimport classNames from 'classnames'\nimport PropTypes from 'prop-types'\nimport React, { cloneElement } from 'react'\nimport { compact, sumBy } from 'lodash'\n\nimport Tooltip from '../tooltip'\n\nconst Usage = ({ total, children, link }) => {\n  const limit = total / 400\n  const othersProps = compact(\n    React.Children.map(children, child => {\n      const { value } = child.props\n      return value < limit && child.props\n    })\n  )\n  const othersTotal = sumBy(othersProps, 'value')\n  const nOthers = othersProps.length\n  const getLink = typeof link === 'function' ? link : () => link\n  return (\n    <span className='usage'>\n      {nOthers > 1 ? (\n        <span>\n          {React.Children.map(children, (child, index) => {\n            const { id, href, value } = child.props\n            return (\n              value > limit &&\n              cloneElement(child, {\n                href: href === undefined ? getLink(id) : href,\n                total,\n              })\n            )\n          })}\n          <Element\n            href={getLink(othersProps.map(_ => _.id))}\n            others\n            tooltip={_('others', { nOthers })}\n            total={total}\n            value={othersTotal}\n          />\n        </span>\n      ) : (\n        React.Children.map(children, (child, index) => {\n          const { id, href } = child.props\n          return cloneElement(child, {\n            href: href === undefined ? getLink(id) : href,\n            total,\n          })\n        })\n      )}\n    </span>\n  )\n}\nUsage.propTypes = {\n  link: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),\n  total: PropTypes.number.isRequired,\n}\nexport { Usage as default }\n\nconst Element = ({ highlight, href, others, tooltip, total, value }) => (\n  <Tooltip content={tooltip}>\n    <a\n      href={href}\n      target='_blank'\n      rel='noreferrer'\n      className={classNames('usage-element', highlight && 'usage-element-highlight', others && 'usage-element-others')}\n      style={{ width: (value / total) * 100 + '%' }}\n    />\n  </Tooltip>\n)\nElement.propTypes = {\n  highlight: PropTypes.bool,\n  href: PropTypes.string,\n  others: PropTypes.bool,\n  tooltip: PropTypes.node,\n  value: PropTypes.number.isRequired,\n}\nexport { Element as UsageElement }\n\nexport const Limits = ({ used, toBeUsed, limit }) => {\n  const available = limit - used\n\n  return (\n    <span className='limits'>\n      <span className='limits-used' style={{ width: ((used || 0) / limit) * 100 + '%' }} />\n      <span\n        className={toBeUsed > available ? 'limits-over-used' : 'limits-to-be-used'}\n        style={{\n          width: (Math.min(toBeUsed || 0, available) / limit) * 100 + '%',\n        }}\n      />\n    </span>\n  )\n}\nLimits.propTypes = {\n  used: PropTypes.number,\n  toBeUsed: PropTypes.number,\n  limit: PropTypes.number.isRequired,\n}\n","export default class UserError {\n  constructor(title, body) {\n    this.title = title\n    this.body = body\n  }\n}\n","import fromCallback from 'promise-toolbox/fromCallback'\nimport getStream from 'get-stream'\nimport humanFormat from 'human-format'\nimport React from 'react'\nimport ReadableStream from 'readable-stream'\nimport xml2js from 'xml2js'\nimport { connect } from 'react-redux'\nimport { createPredicate } from 'value-matcher'\nimport { FormattedDate } from 'react-intl'\nimport {\n  clone,\n  every,\n  forEach,\n  get,\n  isEmpty,\n  isFunction,\n  isPlainObject,\n  map,\n  mapValues,\n  omit,\n  pick,\n  sample,\n  some,\n} from 'lodash'\n\nimport _ from './intl'\nimport * as actions from './store/actions'\nimport invoke from './invoke'\nimport store from './store'\nimport { getObject, isAdmin } from './selectors'\nimport { satisfies as versionSatisfies } from 'semver'\n\nexport const EMPTY_ARRAY = Object.freeze([])\nexport const EMPTY_OBJECT = Object.freeze({})\n\nexport const VIRTUALIZATION_MODE_LABEL = {\n  hvm: 'hardwareVirtualizedMode',\n  pv: 'paraVirtualizedMode',\n  pvhvm: 'hvmModeWithPvDriversEnabled',\n  pv_in_pvh: 'pvInPvhMode',\n}\n\n// ===================================================================\n\nexport addSubscriptions from './add-subscriptions'\n\n// ===================================================================\n\nexport const getVirtualizationModeLabel = vm => {\n  const virtualizationMode = vm.virtualizationMode === 'hvm' && vm.pvDriversDetected ? 'pvhvm' : vm.virtualizationMode\n  const messageId = VIRTUALIZATION_MODE_LABEL[virtualizationMode]\n\n  return messageId === undefined ? virtualizationMode : _(messageId)\n}\n\n// ===================================================================\n\nexport const ensureArray = value => {\n  if (value === undefined) {\n    return []\n  }\n\n  return Array.isArray(value) ? value : [value]\n}\n\nexport const propsEqual = (o1, o2, props) => {\n  props = ensureArray(props)\n\n  for (const prop of props) {\n    if (o1[prop] !== o2[prop]) {\n      return false\n    }\n  }\n\n  return true\n}\n\n// ===================================================================\n\nconst _normalizeMapStateToProps = mapper => {\n  // accept a list of entries to extract from the state\n  if (Array.isArray(mapper)) {\n    return state => pick(state, mapper)\n  }\n\n  if (typeof mapper === 'function') {\n    const factoryOrMapper = (state, props) => {\n      const result = mapper(state, props)\n\n      // Properly handles factory pattern.\n      if (typeof result === 'function') {\n        mapper = result\n        return factoryOrMapper\n      }\n\n      if (isPlainObject(result)) {\n        if (isEmpty(result)) {\n          // Nothing can be determined, wait for it.\n          return result\n        }\n\n        if (every(result, isFunction)) {\n          indirection = (state, props) => mapValues(result, selector => selector(state, props))\n          return indirection(state, props)\n        }\n      }\n\n      indirection = mapper\n      return result\n    }\n\n    let indirection = factoryOrMapper\n    return (state, props) => indirection(state, props)\n  }\n\n  mapper = mapValues(mapper, _normalizeMapStateToProps)\n  return (state, props) => mapValues(mapper, fn => fn(state, props))\n}\n\nexport const connectStore = (mapStateToProps, opts = {}) => {\n  const connector = connect(_normalizeMapStateToProps(mapStateToProps), actions, undefined, opts)\n\n  return Component => {\n    const ConnectedComponent = connector(Component)\n\n    if (opts.withRef && 'value' in Component.prototype) {\n      Object.defineProperty(ConnectedComponent.prototype, 'value', {\n        configurable: true,\n        get() {\n          return this.getWrappedInstance().value\n        },\n        set(value) {\n          this.getWrappedInstance().value = value\n        },\n      })\n    }\n\n    return ConnectedComponent\n  }\n}\n\n// -------------------------------------------------------------------\n\nexport { default as Debug } from './debug'\n\n// -------------------------------------------------------------------\n\n// Returns the current XOA Plan or the Plan name if number given\n/**\n * @deprecated\n *\n * Use `getXoaPlan` from `xoa-plans` instead\n */\nexport const getXoaPlan = plan => {\n  switch (plan || +process.env.XOA_PLAN) {\n    case 1:\n      return 'Free'\n    case 2:\n      return 'Starter'\n    case 3:\n      return 'Enterprise'\n    case 4:\n      return 'Premium'\n    case 5:\n      return 'Community'\n  }\n  return 'Unknown'\n}\n\n// -------------------------------------------------------------------\n\nexport const mapPlus = (collection, cb) => {\n  const result = []\n  const push = ::result.push\n  forEach(collection, (value, index) => cb(value, push, index))\n  return result\n}\n\n// -------------------------------------------------------------------\n\nexport const noop = () => {}\n\n// -------------------------------------------------------------------\n\nexport const osFamily = invoke(\n  {\n    centos: ['centos'],\n    debian: ['debian'],\n    docker: ['coreos'],\n    fedora: ['fedora'],\n    freebsd: ['freebsd'],\n    gentoo: ['gentoo'],\n    'linux-mint': ['linux-mint'],\n    netbsd: ['netbsd'],\n    oracle: ['oracle'],\n    osx: ['osx'],\n    redhat: ['redhat', 'rhel'],\n    solaris: ['solaris'],\n    suse: ['sles', 'suse', 'opensuse', 'opensuse-leap', 'opensuse-microos'],\n    ubuntu: ['ubuntu'],\n    windows: ['windows'],\n  },\n  osByFamily => {\n    const osToFamily = Object.create(null)\n    forEach(osByFamily, (list, family) => {\n      forEach(list, os => {\n        osToFamily[os] = family\n      })\n    })\n\n    return osName => osName && osToFamily[osName.toLowerCase()]\n  }\n)\n\n// -------------------------------------------------------------------\n\nfunction safeHumanFormat(value, opts) {\n  try {\n    return humanFormat(value, opts)\n  } catch (error) {\n    console.error('humanFormat', value, opts, error)\n    return 'N/D'\n  }\n}\n\nexport const formatLogs = logs =>\n  Promise.all(\n    map(logs, ({ body, name }, id) => {\n      if (['BOND_STATUS_CHANGED', 'MULTIPATH_PERIODIC_ALERT'].includes(name)) {\n        // for these alarms, body is a string\n        // ex: \"body\": \"The status of the eth0+eth1 bond is: 1/2 up\"\n        return { name: body, id }\n      }\n      //  value can be: float, Infinity, -Infinity and NaN\n      const matches = /^value:\\s*(Infinity|NaN|-Infinity|[0-9.]+)\\s+config:\\s*([^]*)$/.exec(body)\n      if (matches === null) {\n        return\n      }\n\n      const [, value, xml] = matches\n      return fromCallback(xml2js.parseString, xml).then((result = {}) => {\n        const object = mapValues(result.variable, value => get(value, '[0].$.value'))\n        if (object.name === undefined) {\n          return\n        }\n\n        const { name, ...alarmAttributes } = object\n\n        return { name, value, alarmAttributes, id }\n      }, noop)\n    })\n  )\n\nexport const formatSize = bytes => (bytes != null ? safeHumanFormat(bytes, { scale: 'binary', unit: 'B' }) : 'N/D')\n\nexport const formatSizeShort = bytes => safeHumanFormat(bytes, { scale: 'binary', unit: 'B', maxDecimals: 'auto' })\n\nexport const formatSizeRaw = bytes => humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })\n\nexport const formatSpeed = (bytes, milliseconds) =>\n  safeHumanFormat((bytes * 1e3) / milliseconds, {\n    scale: 'binary',\n    unit: 'B/s',\n  })\n\nconst timeScale = new humanFormat.Scale({\n  ns: 1e-6,\n  µs: 1e-3,\n  ms: 1,\n  s: 1e3,\n  min: 60 * 1e3,\n  h: 3600 * 1e3,\n  d: 86400 * 1e3,\n  y: 2592000 * 1e3,\n})\nexport const formatTime = milliseconds => safeHumanFormat(milliseconds, { scale: timeScale, decimals: 0 })\n\nexport const parseSize = size => {\n  let bytes = humanFormat.parse.raw(size, { scale: 'binary' })\n  if (bytes.unit && bytes.unit !== 'B') {\n    bytes = humanFormat.parse.raw(size)\n\n    if (bytes.unit && bytes.unit !== 'B') {\n      throw new Error('invalid size: ' + size)\n    }\n  }\n  return Math.floor(bytes.value * bytes.factor)\n}\n\n// -------------------------------------------------------------------\n\nconst NotFound = () => <h1>{_('errorPageNotFound')}</h1>\n\n// Decorator to declare routes on a component.\n//\n// TODO: add support for function childRoutes (getChildRoutes).\nexport const routes = (indexRoute, childRoutes) => target => {\n  if (Array.isArray(indexRoute)) {\n    childRoutes = indexRoute\n    indexRoute = undefined\n  } else if (typeof indexRoute === 'function') {\n    indexRoute = {\n      component: indexRoute,\n    }\n  } else if (typeof indexRoute === 'string') {\n    indexRoute = {\n      onEnter: invoke(indexRoute, pathname => (state, replace) => {\n        const current = state.location.pathname\n        replace((current === '/' ? '' : current) + '/' + pathname)\n      }),\n    }\n  }\n\n  if (isPlainObject(childRoutes)) {\n    childRoutes = map(childRoutes, (component, path) => {\n      // The logic can be bypassed by passing a plain object.\n      if (isPlainObject(component)) {\n        return { ...component, path }\n      }\n\n      return { ...component.route, component, path }\n    })\n  }\n\n  if (childRoutes) {\n    childRoutes.push({ component: NotFound, path: '*' })\n  }\n\n  target.route = {\n    indexRoute,\n    childRoutes,\n  }\n\n  return target\n}\n\n// -------------------------------------------------------------------\n\n// Creates a new function which throws an error.\n//\n// ```js\n// promise.catch(throwFn('an error has occurred'))\n//\n// function foo (param = throwFn('param is required')) {}\n// ```\nexport const throwFn = error => () => {\n  throw typeof error === 'string' ? new Error(error) : error\n}\n\n// ===================================================================\n\nexport const resolveResourceSet = resourceSet => {\n  if (!resourceSet) {\n    return\n  }\n\n  const { objects, ipPools, ...attrs } = resourceSet\n  const resolvedObjects = {}\n  const resolvedSet = {\n    ...attrs,\n    missingObjects: [],\n    objectsByType: resolvedObjects,\n    ipPools,\n  }\n  const state = store.getState()\n\n  forEach(objects, id => {\n    const object = getObject(state, id, true) // true: useResourceSet to bypass permissions\n\n    // Error, missing resource.\n    if (!object) {\n      resolvedSet.missingObjects.push(id)\n      return\n    }\n\n    const { type } = object\n\n    if (!resolvedObjects[type]) {\n      resolvedObjects[type] = [object]\n    } else {\n      resolvedObjects[type].push(object)\n    }\n  })\n\n  return resolvedSet\n}\n\nexport const resolveResourceSets = resourceSets => map(resourceSets, resolveResourceSet)\n\n// ===================================================================\n\nexport const streamToString = getStream\n\n// ===================================================================\n\n/* global FileReader */\n\n// Creates a readable stream from a HTML file.\nexport const htmlFileToStream = file => {\n  const reader = new FileReader()\n  const stream = new ReadableStream()\n  let offset = 0\n\n  reader.onloadend = evt => {\n    stream.push(evt.target.result)\n  }\n  reader.onerror = error => {\n    stream.emit('error', error)\n  }\n\n  stream._read = function (size) {\n    if (offset >= file.size) {\n      stream.push(null)\n    } else {\n      reader.readAsBinaryString(file.slice(offset, offset + size))\n      offset += size\n    }\n  }\n\n  return stream\n}\n\n// ===================================================================\n\nexport const resolveId = value => (value != null && typeof value === 'object' && 'id' in value ? value.id : value)\n\nexport const resolveIds = params => {\n  for (const key in params) {\n    const param = params[key]\n    if (param != null && typeof param === 'object' && 'id' in param) {\n      params[key] = param.id\n    }\n  }\n  return params\n}\n\n// ===================================================================\n\nconst OPs = {\n  '<': a => a < 0,\n  '<=': a => a <= 0,\n  '===': a => a === 0,\n  '>': a => a > 0,\n  '>=': a => a >= 0,\n}\n\nconst makeNiceCompare = compare =>\n  function () {\n    const { length } = arguments\n    if (length === 2) {\n      return compare(arguments[0], arguments[1])\n    }\n\n    let i = 1\n    let v1 = arguments[0]\n    let op, v2\n    while (i < length) {\n      op = arguments[i++]\n      v2 = arguments[i++]\n      if (!OPs[op](compare(v1, v2))) {\n        return false\n      }\n      v1 = v2\n    }\n    return true\n  }\n\nexport const compareVersions = makeNiceCompare((v1, v2) => {\n  v1 = v1.split('.')\n  v2 = v2.split('.')\n\n  for (let i = 0; i < Math.max(v1.length, v2.length); i++) {\n    const n1 = +v1[i] || 0\n    const n2 = +v2[i] || 0\n\n    if (n1 < n2) return -1\n    if (n1 > n2) return 1\n  }\n\n  return 0\n})\n\nexport const isXosanPack = ({ name }) => name.startsWith('XOSAN')\n\n// ===================================================================\n\n// Generates a random human-readable string of length `length`\n// Useful to generate random default names intended for the UI user\nexport const generateReadableRandomString = (() => {\n  const CONSONANTS = 'bdfgklmnprtvz'.split('')\n  const VOWELS = 'aeiou'.split('')\n  return (length = 8) => {\n    const result = new Array(length)\n    for (let i = 0; i < length; ++i) {\n      result[i] = sample((i & 1) === 0 ? VOWELS : CONSONANTS)\n    }\n    return result.join('')\n  }\n})()\n\nexport const cowSet = (object, path, value, depth = 0) => {\n  if (depth >= path.length) {\n    return value\n  }\n\n  object = object != null ? clone(object) : {}\n  const prop = path[depth]\n  object[prop] = cowSet(object[prop], path, value, depth + 1)\n  return object\n}\n\n// Generates a function that returns a value between 0 and 1\n// This function returns an estimated progress value between 0 and 1\n// based on the elapsed time since the createFakeProgress call and\n// the given estimated duration d\n//\n// const getProgress = createFakeProgress(120)\n// setInterval(() => console.log(`Progress: ${getProgress() * 100} %`), 1000)\nexport const createFakeProgress = (() => {\n  const S = 0.95 // Progress value after d seconds\n  return d => {\n    const startTime = Date.now() / 1e3\n    return () => {\n      const x = Date.now() / 1e3 - startTime\n      return -Math.exp((x * Math.log(1 - S)) / d) + 1\n    }\n  }\n})()\n\nconst padNumber2 = n => (n < 10 ? '0' + n : String(n))\n\n/**\n * Format a Date object or a ECMAScript timestamp\n *\n * The format is very close to the date (YYYY-MM-DD) time (hh:mm) representation\n * of ISO 8601 expect the T separator is replaced by a whitespace for readability.\n */\nexport const NumericDate = ({ timestamp }) => {\n  const d = timestamp instanceof Date ? timestamp : new Date(timestamp)\n  return (\n    <span>\n      {d.getFullYear()}-{padNumber2(d.getMonth() + 1)}-{padNumber2(d.getDate())} {padNumber2(d.getHours())}:\n      {padNumber2(d.getMinutes())}\n    </span>\n  )\n}\n\nexport const ShortDate = ({ timestamp }) => (\n  <FormattedDate value={timestamp} month='short' day='numeric' year='numeric' />\n)\n\nexport const findLatestPack = (packs, hostsVersions) => {\n  const checkVersion = version =>\n    !version || every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))\n\n  let latestPack = { version: '0' }\n  forEach(packs, pack => {\n    if (\n      pack.type === 'iso' &&\n      compareVersions(pack.version, '>', latestPack.version) &&\n      checkVersion(pack.requirements && pack.requirements.xenserver)\n    ) {\n      latestPack = pack\n    }\n  })\n\n  if (latestPack.version === '0') {\n    // No compatible pack was found\n    return\n  }\n\n  return latestPack\n}\n\nexport const isLatestXosanPackInstalled = (latestXosanPack, hosts) =>\n  latestXosanPack !== undefined &&\n  every(hosts, host =>\n    some(host.supplementalPacks, ({ name, version }) => name === 'XOSAN' && version === latestXosanPack.version)\n  )\n\n// ===================================================================\n\nexport const getMemoryUsedMetric = ({ memory, memoryFree = memory }) =>\n  map(memory, (value, key) => value - memoryFree[key])\n\n// ===================================================================\n\nexport const generateRandomId = () => Math.random().toString(36).slice(2)\n\n// ===================================================================\n\n// it returns [nActivePaths, nPaths]\nexport const getIscsiPaths = pbd => {\n  const pathsInfo = pbd.otherConfig[`mpath-${pbd.device_config.SCSIid}`]\n  return pathsInfo !== undefined ? JSON.parse(pathsInfo) : []\n}\n\n// ===================================================================\n\nexport const createBlobFromString = str =>\n  new window.Blob([str], {\n    type: 'text/plain',\n  })\n\n// ===================================================================\n\n// Format a date in ISO 8601 in a safe way to be used in filenames\n// (even on Windows).\nexport const safeDateFormat = ms => new Date(ms).toISOString().replace(/:/g, '_')\n\n// ===================================================================\n\nexport const downloadLog = ({ log, date, type }) => {\n  const isJson = typeof log !== 'string'\n\n  const anchor = document.createElement('a')\n  anchor.href = window.URL.createObjectURL(createBlobFromString(isJson ? JSON.stringify(log, null, 2) : log))\n  anchor.download = `${safeDateFormat(date)} - ${type}.${isJson ? 'json' : 'log'}`\n  anchor.style.display = 'none'\n  document.body.appendChild(anchor)\n  anchor.click()\n  document.body.removeChild(anchor)\n}\n\n// ===================================================================\n\n// Creates compare function based on different criteria\n//\n// ```js\n// [{ name: 'bar', value: v2 }, { name: 'foo', value: v1 }].sort(\n//   createCompare([\n//     o => o.value === v1,\n//     'name'\n//   ])\n// )\n// ```\nexport const createCompare =\n  criterias =>\n  (...items) => {\n    let res = 0\n    // Array.find to stop when the result is != 0\n    criterias.find(fn => {\n      const [v1, v2] = items.map(item => {\n        const v = typeof fn === 'string' ? item[fn] : fn(item)\n        return v === true ? -1 : v === false ? 1 : v\n      })\n      return (res = v1 < v2 ? -1 : v1 > v2 ? 1 : 0)\n    })\n    return res\n  }\n\n// ===================================================================\n\nexport const createCompareContainers = poolId => createCompare([c => c.$pool === poolId, c => c.type === 'pool'])\n\n// ===================================================================\n\nexport const hasLicenseRestrictions = host => {\n  const licenseType = host.license_params.sku_type\n  return (\n    host.productBrand !== 'XCP-ng' &&\n    versionSatisfies(host.version, '>=7.3.0') &&\n    (licenseType === 'free' || licenseType === 'express')\n  )\n}\n\n// ===================================================================\n\nexport const adminOnly = Component =>\n  connectStore({\n    _isAdmin: isAdmin,\n  })(({ _isAdmin, ...props }) => (_isAdmin ? <Component {...props} /> : <NotFound />))\n\n// ===================================================================\n\nexport const TryXoa = ({ page }) => (\n  <a\n    href={`https://xen-orchestra.com/#/xoa?pk_campaign=xoa_source_upgrade&pk_kwd=${page}`}\n    target='_blank'\n    rel='noreferrer'\n  >\n    {_('tryXoa')}\n  </a>\n)\n\n// ===================================================================\n\nexport const getDetachedBackupsOrSnapshots = (backupsOrSnapshots, { jobs, schedules, vms }) => {\n  if (jobs === undefined || schedules === undefined) {\n    return []\n  }\n\n  const detachedBackupsOrSnapshots = []\n  forEach(backupsOrSnapshots, backupOrSnapshot => {\n    const { vmId, jobId, scheduleId } = backupOrSnapshot\n    const vm = vms[vmId]\n    const job = jobs[jobId]\n    const reason =\n      vm === undefined\n        ? 'missingVm'\n        : job === undefined\n          ? 'missingJob'\n          : schedules[scheduleId] === undefined\n            ? 'missingSchedule'\n            : !createPredicate(omit(job.vms, 'power_state'))(vm)\n              ? 'missingVmInJob'\n              : undefined\n\n    if (reason !== undefined) {\n      detachedBackupsOrSnapshots.push({\n        ...backupOrSnapshot,\n        reason,\n      })\n    }\n  })\n\n  return detachedBackupsOrSnapshots\n}\n","module.exports = {\n    \"done\": \"#089944\",\n    \"wizard\": \"mc12c16a24_wizard\",\n    \"section\": \"mc12c16a24_section\",\n    \"title\": \"mc12c16a24_title\",\n    \"success\": \"mc12c16a24_success\",\n    \"content\": \"mc12c16a24_content\",\n    \"active\": \"mc12c16a24_active\",\n    \"bullet\": \"mc12c16a24_bullet\"\n};","import classNames from 'classnames'\nimport every from 'lodash/every'\nimport PropTypes from 'prop-types'\nimport React, { Component, cloneElement } from 'react'\n\nimport _ from '../intl'\nimport Icon from '../icon'\n\nimport styles from './index.css'\n\nconst Wizard = ({ children }) => {\n  const allDone = every(React.Children.toArray(children), child => child.props.done || child.props.summary)\n\n  return (\n    <ul className={styles.wizard}>\n      {React.Children.map(children, (child, key) => child && cloneElement(child, { allDone, key }))}\n    </ul>\n  )\n}\nexport { Wizard as default }\n\nexport class Section extends Component {\n  static propTypes = {\n    icon: PropTypes.string.isRequired,\n    title: PropTypes.string.isRequired,\n  }\n\n  componentWillMount() {\n    this.setState({ isActive: false })\n  }\n\n  _onFocus = () => this.setState({ isActive: true })\n  _onBlur = () => this.setState({ isActive: false })\n\n  render() {\n    const { allDone, icon, title, done, children } = this.props\n    return (\n      <li\n        className={classNames(styles.section, styles.bullet, (done || allDone) && styles.success)}\n        onFocus={this._onFocus}\n        onBlur={this._onBlur}\n      >\n        {/* TITLE */}\n        <div className={classNames(styles.title, (done || allDone) && styles.success)}>\n          <h4>\n            {icon && <Icon icon={icon} />} {_(title)}\n          </h4>\n        </div>\n        {/* CONTENT */}\n        <div\n          className={classNames(\n            styles.content,\n            this.state.isActive && styles.active,\n            (done || allDone) && styles.success\n          )}\n        >\n          {children}\n        </div>\n      </li>\n    )\n  }\n}\n","import forEach from 'lodash/forEach'\n\nimport XoHighLevelObjectInput from './xo-highlevel-object-input'\nimport XoHostInput from './xo-host-input'\nimport XoPoolInput from './xo-pool-input'\nimport XoRemoteInput from './xo-remote-input'\nimport XoRoleInput from './xo-role-input'\nimport xoSnapshotInput from './xo-snapshot-input'\nimport XoSrInput from './xo-sr-input'\nimport XoSubjectInput from './xo-subject-input'\nimport XoTagInput from './xo-tag-input'\nimport XoVdiInput from './xo-vdi-input'\nimport XoVmInput from './xo-vm-input'\nimport { getType, getXoType } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nconst XO_TYPE_TO_COMPONENT = {\n  host: XoHostInput,\n  pool: XoPoolInput,\n  remote: XoRemoteInput,\n  role: XoRoleInput,\n  snapshot: xoSnapshotInput,\n  sr: XoSrInput,\n  subject: XoSubjectInput,\n  tag: XoTagInput,\n  vdi: XoVdiInput,\n  vm: XoVmInput,\n  xoobject: XoHighLevelObjectInput,\n}\n\n// ===================================================================\n\nconst buildStringInput = (uiSchema, key, xoType) => {\n  if (key === 'password') {\n    uiSchema.config = { password: true }\n  }\n\n  uiSchema.widget = XO_TYPE_TO_COMPONENT[xoType]\n}\n\n// ===================================================================\n\nconst _generateUiSchema = (schema, uiSchema, key) => {\n  const type = getType(schema)\n\n  if (type === 'object') {\n    const properties = (uiSchema.properties = {})\n\n    forEach(schema.properties, (schema, key) => {\n      const subUiSchema = (properties[key] = {})\n      _generateUiSchema(schema, subUiSchema, key)\n    })\n  } else if (type === 'array') {\n    const widget = XO_TYPE_TO_COMPONENT[getXoType(schema.items)]\n\n    if (widget) {\n      uiSchema.widget = widget\n      uiSchema.config = { multi: true }\n    } else {\n      const subUiSchema = (uiSchema.items = {})\n      _generateUiSchema(schema.items, subUiSchema, key)\n    }\n  } else if (type === 'string') {\n    buildStringInput(uiSchema, key, getXoType(schema))\n  }\n}\n\nexport const generateUiSchema = schema => {\n  const uiSchema = {}\n  _generateUiSchema(schema, uiSchema, '')\n  return uiSchema\n}\n","import map from 'lodash/map'\nimport { PureComponent } from 'react'\n\nimport getEventValue from '../get-event-value'\n\n// ===================================================================\n\nconst getId = value => (value != null && value.id) || value\n\nexport default class XoAbstractInput extends PureComponent {\n  _onChange = event => {\n    const value = getEventValue(event)\n    const { props } = this\n\n    return props.onChange(props.schema.type === 'array' ? map(value, getId) : getId(value))\n  }\n}\n","import React from 'react'\nimport { SelectHighLevelObject } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class HighLevelObjectInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectHighLevelObject\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\nimport { SelectHost } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class HostInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectHost\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\nimport { SelectPool } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class PoolInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectPool\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\nimport { SelectRemote } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class RemoteInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectRemote\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\nimport { SelectRole } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class RoleInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectRole\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\nimport { SelectVmSnapshot } from '../select-objects'\n\n// ===================================================================\n\nexport default class snapshotInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectVmSnapshot\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\nimport { SelectSr } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class SrInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectSr\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\nimport { SelectSubject } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class SubjectInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectSubject\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\nimport { SelectTag } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class TagInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectTag\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\nimport { SelectVdi } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class VdiInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectVdi\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","import React from 'react'\nimport { SelectVm } from 'select-objects'\n\nimport XoAbstractInput from './xo-abstract-input'\nimport { PrimitiveInputWrapper } from '../json-schema-input/helpers'\n\n// ===================================================================\n\nexport default class VmInput extends XoAbstractInput {\n  render() {\n    const { props } = this\n\n    return (\n      <PrimitiveInputWrapper {...props}>\n        <SelectVm\n          disabled={props.disabled}\n          hasSelectAll\n          multi={props.multi}\n          onChange={this._onChange}\n          ref='input'\n          required={props.required}\n          value={props.value}\n        />\n      </PrimitiveInputWrapper>\n    )\n  }\n}\n","module.exports = {\n    \"dashedLine\": \"mc3ce03460_dashedLine\"\n};","import ChartistGraph from 'react-chartist'\nimport ChartistLegend from 'chartist-plugin-legend'\nimport ChartistTooltip from 'chartist-plugin-tooltip'\nimport humanFormat from 'human-format'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { injectIntl } from 'react-intl'\nimport { messages } from 'intl'\nimport { find, flatten, floor, get, map, max, round, size, sortBy, sum, values } from 'lodash'\n\nimport { computeArraysSum } from '../xo-stats'\nimport { formatSize, formatSpeed, formatTime, getMemoryUsedMetric } from '../utils'\n\nimport styles from './index.css'\n\n// Number of labels on axis X.\nconst N_LABELS_X = 5\n\nconst LABEL_OFFSET_X = 40\nconst LABEL_OFFSET_Y = 85\n\n// ===================================================================\n\n// See xo-stats.js, data can be null.\n// Return the size of the first non-null object.\nconst getStatsLength = stats => size(find(stats, stats => stats != null))\n\n// ===================================================================\n\nconst makeOptions = ({ intl, nValues, endTimestamp, interval, valueTransform }) => ({\n  showPoint: true,\n  lineSmooth: false,\n  showArea: true,\n  height: 300,\n  low: 0,\n  axisX: {\n    labelInterpolationFnc: makeLabelInterpolationFnc(intl, nValues, endTimestamp, interval),\n    offset: LABEL_OFFSET_X,\n  },\n  axisY: {\n    labelInterpolationFnc: valueTransform,\n    offset: LABEL_OFFSET_Y,\n  },\n  plugins: [\n    ChartistLegend(),\n    ChartistTooltip({\n      valueTransform: value => valueTransform(+value), // '+value' because tooltip gives a string value...\n    }),\n  ],\n})\n\n// ===================================================================\n\nconst makeLabelInterpolationFnc = (intl, nValues, endTimestamp, interval) => {\n  const labelSpace = floor(nValues / N_LABELS_X)\n  let format\n\n  if (interval === 3600) {\n    format = {\n      minute: 'numeric',\n      hour: 'numeric',\n      weekday: 'short',\n    }\n  } else if (interval === 86400) {\n    format = {\n      day: 'numeric',\n      month: 'numeric',\n      year: 'numeric',\n    }\n  }\n\n  return (value, index) =>\n    index % labelSpace === 0 ? intl.formatTime((endTimestamp - (nValues - index - 1) * interval) * 1000, format) : null\n}\n\n// Supported series: xvds, vifs, pifs.\nconst buildSeries = ({ stats, label, addSumSeries }) => {\n  let series = []\n\n  for (const io in stats) {\n    const ioData = stats[io]\n    for (const letter in ioData) {\n      const data = ioData[letter]\n\n      // See xo-stats.js, data can be null.\n      if (data) {\n        series.push({\n          name: `${label}${letter} (${io})`,\n          data,\n        })\n      }\n    }\n\n    series = sortBy(series, 'name')\n\n    if (addSumSeries) {\n      series.push({\n        name: `All ${io}`,\n        data: computeArraysSum(values(ioData)),\n        className: styles.dashedLine,\n      })\n    }\n  }\n\n  return series\n}\n\nconst templateError = <div>No stats.</div>\n\n// ===================================================================\n\nexport const CpuLineChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const stats = data.stats.cpus\n  const length = getStatsLength(stats)\n\n  if (!length) {\n    return templateError\n  }\n\n  const series = map(stats, (data, id) => ({\n    name: `Cpu${id}`,\n    data,\n  }))\n\n  if (addSumSeries) {\n    series.push({\n      name: 'All Cpus',\n      data: computeArraysSum(stats),\n      className: styles.dashedLine,\n    })\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series,\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp: data.endTimestamp,\n          interval: data.interval,\n          valueTransform: value => `${floor(value)}%`,\n        }),\n        high: !addSumSeries ? 100 : stats.length * 100,\n        ...options,\n      }}\n    />\n  )\n})\nCpuLineChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.object.isRequired,\n  options: PropTypes.object,\n}\n\nexport const PoolCpuLineChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const firstHostData = data[0]\n  const length = getStatsLength(firstHostData.stats.cpus)\n\n  if (!length) {\n    return templateError\n  }\n\n  const series = map(data, ({ host, stats }) => ({\n    name: host,\n    data: computeArraysSum(stats.cpus),\n  }))\n\n  if (addSumSeries) {\n    series.push({\n      name: intl.formatMessage(messages.poolAllHosts),\n      data: computeArraysSum(map(series, 'data')),\n      className: styles.dashedLine,\n    })\n  }\n\n  const nbCpusByHost = map(data, ({ stats }) => stats.cpus.length)\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series,\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp: firstHostData.endTimestamp,\n          interval: firstHostData.interval,\n          valueTransform: value => `${floor(value)}%`,\n        }),\n        high: 100 * (addSumSeries ? sum(nbCpusByHost) : max(nbCpusByHost)),\n        ...options,\n      }}\n    />\n  )\n})\nPoolCpuLineChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.array.isRequired,\n  options: PropTypes.object,\n}\n\nexport const MemoryLineChart = injectIntl(({ data, options = {}, intl }) => {\n  const { memory } = data.stats\n  const memoryUsed = getMemoryUsedMetric(data.stats)\n\n  if (!memory || !memoryUsed) {\n    return templateError\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series: [\n          {\n            name: 'RAM',\n            data: memoryUsed,\n          },\n        ],\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: memoryUsed.length,\n          endTimestamp: data.endTimestamp,\n          interval: data.interval,\n          valueTransform: formatSize,\n        }),\n        high: memory[memory.length - 1],\n        ...options,\n      }}\n    />\n  )\n})\n\nMemoryLineChart.propTypes = {\n  data: PropTypes.object.isRequired,\n  options: PropTypes.object,\n}\n\nexport const PoolMemoryLineChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const firstHostData = data[0]\n  const { memory } = firstHostData.stats\n  const memoryUsed = getMemoryUsedMetric(firstHostData.stats)\n\n  if (!memory || !memoryUsed) {\n    return templateError\n  }\n\n  const series = map(data, ({ host, stats }) => ({\n    name: host,\n    data: getMemoryUsedMetric(stats),\n  }))\n\n  if (addSumSeries) {\n    series.push({\n      name: intl.formatMessage(messages.poolAllHosts),\n      data: computeArraysSum(map(data, ({ stats }) => getMemoryUsedMetric(stats))),\n      className: styles.dashedLine,\n    })\n  }\n\n  const currentMemoryByHost = map(data, ({ stats }) => stats.memory[stats.memory.length - 1])\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series,\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: memoryUsed.length,\n          endTimestamp: firstHostData.endTimestamp,\n          interval: firstHostData.interval,\n          valueTransform: formatSize,\n        }),\n        high: addSumSeries ? sum(currentMemoryByHost) : max(currentMemoryByHost),\n        ...options,\n      }}\n    />\n  )\n})\n\nPoolMemoryLineChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.array.isRequired,\n  options: PropTypes.object,\n}\n\nexport const XvdLineChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const stats = data.stats.xvds\n  const length = stats && getStatsLength(stats.r)\n\n  if (!length) {\n    return templateError\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series: buildSeries({ addSumSeries, stats, label: 'Xvd' }),\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp: data.endTimestamp,\n          interval: data.interval,\n          valueTransform: formatSize,\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nXvdLineChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.object.isRequired,\n  options: PropTypes.object,\n}\n\nexport const VifLineChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const stats = data.stats.vifs\n  const length = stats && getStatsLength(stats.rx)\n\n  if (!length) {\n    return templateError\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series: buildSeries({ addSumSeries, stats, label: 'Vif' }),\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp: data.endTimestamp,\n          interval: data.interval,\n          valueTransform: formatSize,\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nVifLineChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.object.isRequired,\n  options: PropTypes.object,\n}\n\nexport const PifLineChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const stats = data.stats.pifs\n  const length = stats && getStatsLength(stats.rx)\n\n  if (!length) {\n    return templateError\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series: buildSeries({ addSumSeries, stats, label: 'eth' }),\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp: data.endTimestamp,\n          interval: data.interval,\n          valueTransform: formatSize,\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nPifLineChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.array.isRequired,\n  options: PropTypes.object,\n}\n\nconst ios = ['rx', 'tx']\nexport const PoolPifLineChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const firstHostData = data[0]\n  const length = firstHostData.stats && getStatsLength(firstHostData.stats.pifs.rx)\n\n  if (!length) {\n    return templateError\n  }\n\n  const series = addSumSeries\n    ? map(ios, io => ({\n        name: `${intl.formatMessage(messages.poolAllHosts)} (${io})`,\n        data: computeArraysSum(map(data, ({ stats }) => computeArraysSum(stats.pifs[io]))),\n      }))\n    : flatten(\n        map(data, ({ stats, host }) =>\n          map(ios, io => ({\n            name: `${host} (${io})`,\n            data: computeArraysSum(stats.pifs[io]),\n          }))\n        )\n      )\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series,\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp: firstHostData.endTimestamp,\n          interval: firstHostData.interval,\n          valueTransform: formatSize,\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nPoolPifLineChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.array.isRequired,\n  options: PropTypes.object,\n}\n\nexport const LoadLineChart = injectIntl(({ data, options = {}, intl }) => {\n  const stats = data.stats.load\n  const { length } = stats || {}\n\n  if (!length) {\n    return templateError\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series: [\n          {\n            name: 'Load average',\n            data: stats,\n          },\n        ],\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp: data.endTimestamp,\n          interval: data.interval,\n          valueTransform: value => `${value.toPrecision(3)}`,\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nLoadLineChart.propTypes = {\n  data: PropTypes.object.isRequired,\n  options: PropTypes.object,\n}\n\nexport const PoolLoadLineChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const firstHostData = data[0]\n  const length = firstHostData.stats && firstHostData.stats.load.length\n\n  if (!length) {\n    return templateError\n  }\n\n  const series = map(data, ({ host, stats }) => ({\n    name: host,\n    data: stats.load,\n  }))\n\n  if (addSumSeries) {\n    series.push({\n      name: intl.formatMessage(messages.poolAllHosts),\n      data: computeArraysSum(map(data, 'stats.load')),\n      className: styles.dashedLine,\n    })\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series,\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp: firstHostData.endTimestamp,\n          interval: firstHostData.interval,\n          valueTransform: value => `${value.toPrecision(3)}`,\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nPoolLoadLineChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.array.isRequired,\n  options: PropTypes.object,\n}\n\nconst buildSrSeries = ({ stats, label, addSumSeries }) => {\n  const series = sortBy(\n    map(stats, (data, key) => ({\n      name: `${label} (${key})`,\n      data,\n    })),\n    'name'\n  )\n\n  if (addSumSeries) {\n    series.push({\n      name: `All ${label}`,\n      data: computeArraysSum(values(stats)),\n      className: styles.dashedLine,\n    })\n  }\n\n  return series\n}\n\nexport const IopsLineChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const {\n    endTimestamp,\n    interval,\n    stats: { iops },\n  } = data\n\n  const { length } = get(iops, 'r')\n\n  if (length === 0) {\n    return templateError\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series: buildSrSeries({ stats: iops, label: 'Iops', addSumSeries }),\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp,\n          interval,\n          valueTransform: value =>\n            humanFormat(value, {\n              decimals: 3,\n              unit: 'IOPS',\n            }),\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nIopsLineChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.array.isRequired,\n  options: PropTypes.object,\n}\n\nexport const IoThroughputChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const {\n    endTimestamp,\n    interval,\n    stats: { ioThroughput },\n  } = data\n\n  const { length } = get(ioThroughput, 'r') || []\n\n  if (length === 0) {\n    return templateError\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series: buildSrSeries({\n          stats: ioThroughput,\n          label: 'IO throughput',\n          addSumSeries,\n        }),\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp,\n          interval,\n          valueTransform: value => formatSpeed(value, 1e3),\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nIoThroughputChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.array.isRequired,\n  options: PropTypes.object,\n}\n\nexport const LatencyChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const {\n    endTimestamp,\n    interval,\n    stats: { latency },\n  } = data\n\n  const { length } = get(latency, 'r') || []\n\n  if (length === 0) {\n    return templateError\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series: buildSrSeries({\n          stats: latency,\n          label: 'Latency',\n          addSumSeries,\n        }),\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp,\n          interval,\n          valueTransform: value => formatTime(value),\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nLatencyChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.array.isRequired,\n  options: PropTypes.object,\n}\n\nexport const IowaitChart = injectIntl(({ addSumSeries, data, options = {}, intl }) => {\n  const {\n    endTimestamp,\n    interval,\n    stats: { iowait },\n  } = data\n\n  const { length } = iowait[Object.keys(iowait)[0]] || []\n\n  if (length === 0) {\n    return templateError\n  }\n\n  return (\n    <ChartistGraph\n      type='Line'\n      data={{\n        series: buildSrSeries({\n          stats: iowait,\n          label: 'IOwait',\n          addSumSeries,\n        }),\n      }}\n      options={{\n        ...makeOptions({\n          intl,\n          nValues: length,\n          endTimestamp,\n          interval,\n          valueTransform: value => `${round(value, 3)}%`,\n        }),\n        ...options,\n      }}\n    />\n  )\n})\n\nIowaitChart.propTypes = {\n  addSumSeries: PropTypes.bool,\n  data: PropTypes.array.isRequired,\n  options: PropTypes.object,\n}\n","import * as d3 from 'd3'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport forEach from 'lodash/forEach'\nimport keys from 'lodash/keys'\nimport map from 'lodash/map'\nimport times from 'lodash/times'\n\nimport Component from './base-component'\nimport { setStyles } from './d3-utils'\n\n// ===================================================================\n\nconst CHART_WIDTH = 2000\nconst CHART_HEIGHT = 800\n\nconst TICK_SIZE = CHART_WIDTH / 100\n\nconst N_TICKS = 4\n\nconst TOOLTIP_PADDING = 10\n\nconst DEFAULT_STROKE_WIDTH_FACTOR = 500\nconst HIGHLIGHT_STROKE_WIDTH_FACTOR = 200\n\nconst BRUSH_SELECTION_WIDTH = (2 * CHART_WIDTH) / 100\n\n// ===================================================================\n\nconst SVG_STYLE = {\n  display: 'block',\n  height: '100%',\n  left: 0,\n  position: 'absolute',\n  top: 0,\n  width: '100%',\n}\n\nconst SVG_CONTAINER_STYLE = {\n  'padding-bottom': '50%',\n  'vertical-align': 'middle',\n  overflow: 'hidden',\n  position: 'relative',\n  width: '100%',\n}\n\nconst SVG_CONTENT = {\n  'font-size': `${CHART_WIDTH / 100}px`,\n}\n\nconst COLUMN_TITLE_STYLE = {\n  'font-size': '100%',\n  'font-weight': 'bold',\n  'text-anchor': 'middle',\n}\n\nconst COLUMN_VALUES_STYLE = {\n  'font-size': '100%',\n}\n\nconst LINES_CONTAINER_STYLE = {\n  'stroke-opacity': 0.5,\n  'stroke-width': CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR,\n  fill: 'none',\n  stroke: 'red',\n}\n\nconst TOOLTIP_STYLE = {\n  fill: 'white',\n  'font-size': '125%',\n  'font-weight': 'bold',\n}\n\n// ===================================================================\n\nexport default class XoParallelChart extends Component {\n  static propTypes = {\n    dataSet: PropTypes.arrayOf(\n      PropTypes.shape({\n        data: PropTypes.object.isRequired,\n        label: PropTypes.string.isRequired,\n        objectId: PropTypes.string.isRequired,\n      })\n    ).isRequired,\n    labels: PropTypes.object.isRequired,\n    renderers: PropTypes.object,\n  }\n\n  _line = d3.line()\n\n  _color = d3.scaleOrdinal(d3.schemeCategory10)\n\n  _handleBrush = () => {\n    // 1. Get selected brushes.\n    const brushes = []\n    this._svg\n      .selectAll('.chartColumn')\n      .selectAll('.brush')\n      .each((_1, _2, [brush]) => {\n        if (d3.brushSelection(brush) != null) {\n          brushes.push(brush)\n        }\n      })\n\n    // 2. Change stroke of selected lines.\n    const lines = this._svg.select('.linesContainer').selectAll('path')\n\n    lines.each((elem, lineId, lines) => {\n      const { data } = elem\n\n      const res = brushes.every(brush => {\n        const selection = d3.brushSelection(brush)\n        const columnId = brush.__data__\n        const { invert } = this._y[columnId] // Range to domain.\n\n        return invert(selection[1]) <= data[columnId] && data[columnId] <= invert(selection[0])\n      })\n\n      const line = d3.select(lines[lineId])\n\n      if (!res) {\n        line.attr('stroke-opacity', 1.0).attr('stroke', '#e6e6e6')\n      } else {\n        line.attr('stroke-opacity', 0.5).attr('stroke', this._color(elem.label))\n      }\n    })\n  }\n\n  _brush = d3\n    .brushY()\n    // Brush area: (x0, y0), (x1, y1)\n    .extent([\n      [-BRUSH_SELECTION_WIDTH / 2, 0],\n      [BRUSH_SELECTION_WIDTH / 2, CHART_HEIGHT],\n    ])\n    .on('brush', this._handleBrush)\n    .on('end', this._handleBrush)\n\n  _highlight(elem, position) {\n    const svg = this._svg\n\n    // Reset tooltip.\n    svg.selectAll('.objectTooltip').remove()\n\n    // Reset all lines.\n    svg.selectAll('.chartLine').attr('stroke-width', CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR)\n\n    if (!position) {\n      return\n    }\n\n    // Set stroke on selected line.\n    svg.select('#chartLine-' + elem.objectId).attr('stroke-width', CHART_WIDTH / HIGHLIGHT_STROKE_WIDTH_FACTOR)\n\n    const { label } = elem\n\n    const tooltip = svg.append('g').attr('class', 'objectTooltip')\n\n    const bbox = tooltip\n      .append('text')\n      .text(label)\n      .attr('x', position[0])\n      .attr('y', position[1] - 30)\n      ::setStyles(TOOLTIP_STYLE)\n      .node()\n      .getBBox()\n\n    tooltip\n      .insert('rect', '*')\n      .attr('x', bbox.x - TOOLTIP_PADDING)\n      .attr('y', bbox.y - TOOLTIP_PADDING)\n      .attr('width', bbox.width + TOOLTIP_PADDING * 2)\n      .attr('height', bbox.height + TOOLTIP_PADDING * 2)\n      .style('fill', this._color(label))\n  }\n\n  _handleMouseOver = (elem, pathId, paths) => {\n    this._highlight(elem, d3.mouse(paths[pathId]))\n  }\n\n  _handleMouseOut = elem => {\n    this._highlight()\n  }\n\n  _draw(props = this.props) {\n    const svg = this._svg\n    const { labels, dataSet } = props\n\n    const columnsIds = keys(labels)\n    const spacing = (CHART_WIDTH - 200) / (columnsIds.length - 1)\n    const x = d3\n      .scaleOrdinal()\n      .domain(columnsIds)\n      .range(times(columnsIds.length, n => n * spacing))\n\n    // 1. Remove old nodes.\n    svg.selectAll('.chartColumn').remove()\n\n    svg.selectAll('.linesContainer').remove()\n\n    // 2. Build Ys.\n    const y = (this._y = {})\n    forEach(columnsIds, (columnId, index) => {\n      const max = d3.max(dataSet, elem => elem.data[columnId])\n\n      y[columnId] = d3.scaleLinear().domain([0, max]).range([CHART_HEIGHT, 0])\n    })\n\n    // 3. Build columns.\n    const columns = svg\n      .selectAll('.chartColumn')\n      .data(columnsIds)\n      .enter()\n      .append('g')\n      .attr('class', 'chartColumn')\n      .attr('transform', d => `translate(${x(d)})`)\n\n    // 4. Draw titles.\n    columns\n      .append('text')\n      .text(columnId => labels[columnId])\n      .attr('y', -50)\n      ::setStyles(COLUMN_TITLE_STYLE)\n\n    // 5. Draw axis.\n    columns\n      .append('g')\n      .each((columnId, axisId, axes) => {\n        const axis = d3.axisLeft().ticks(N_TICKS, ',f').tickSize(TICK_SIZE).scale(y[columnId])\n\n        const renderer = props.renderers[columnId]\n\n        // Add optional renderer like formatSize.\n        if (renderer) {\n          axis.tickFormat(renderer)\n        }\n\n        d3.select(axes[axisId]).call(axis)\n      })\n      ::setStyles(COLUMN_VALUES_STYLE)\n\n    // 6. Draw lines.\n    const path = elem => this._line(map(columnsIds.map(columnId => [x(columnId), y[columnId](elem.data[columnId])])))\n    svg\n      .append('g')\n      .attr('class', 'linesContainer')\n      ::setStyles(LINES_CONTAINER_STYLE)\n      .selectAll('path')\n      .data(dataSet)\n      .enter()\n      .append('path')\n      .attr('d', path)\n      .attr('class', 'chartLine')\n      .attr('id', elem => 'chartLine-' + elem.objectId)\n      .attr('stroke', elem => this._color(elem.label))\n      .attr('shape-rendering', 'optimizeQuality')\n      .attr('stroke-linecap', 'round')\n      .attr('stroke-linejoin', 'round')\n      .on('mouseover', this._handleMouseOver)\n      .on('mouseout', this._handleMouseOut)\n\n    // 7. Brushes.\n    columns\n      .append('g')\n      .attr('class', 'brush')\n      .each((_, brushId, brushes) => {\n        d3.select(brushes[brushId]).call(this._brush)\n      })\n  }\n\n  componentDidMount() {\n    this._svg = d3\n      .select(this.refs.chart)\n      .append('div')\n      ::setStyles(SVG_CONTAINER_STYLE)\n      .append('svg')\n      ::setStyles(SVG_STYLE)\n      .attr('preserveAspectRatio', 'xMinYMin meet')\n      .attr('viewBox', `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`)\n      .append('g')\n      .attr('transform', `translate(${100}, ${100})`)\n      ::setStyles(SVG_CONTENT)\n\n    this._draw()\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this._draw(nextProps)\n  }\n\n  render() {\n    return <div ref='chart' />\n  }\n}\n","import PropTypes from 'prop-types'\nimport React from 'react'\nimport { Sparklines, SparklinesLine } from 'react-sparklines'\n\nimport { computeArraysAvg, computeObjectsAvg } from './xo-stats'\nimport { getMemoryUsedMetric } from './utils'\n\nconst STYLE = {}\n\nconst WIDTH = 120\nconst HEIGHT = 20\nconst STROKE_WIDTH = 0.5\n\n// ===================================================================\n\nconst templateError = <div>No stats.</div>\n\n// ===================================================================\n\nexport const CpuSparkLines = ({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {\n  const { cpus } = data.stats\n\n  if (!cpus) {\n    return templateError\n  }\n\n  return (\n    <Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={width} height={height}>\n      <SparklinesLine\n        style={{\n          strokeWidth,\n          stroke: '#366e98',\n          fill: '#366e98',\n          fillOpacity: 0.5,\n        }}\n        color='#2598d9'\n      />\n    </Sparklines>\n  )\n}\n\nCpuSparkLines.propTypes = {\n  data: PropTypes.object.isRequired,\n}\n\nexport const MemorySparkLines = ({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {\n  const { memory } = data.stats\n  const memoryUsed = getMemoryUsedMetric(data.stats)\n  if (!memory || !memoryUsed) {\n    return templateError\n  }\n\n  return (\n    <Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={width} height={height}>\n      <SparklinesLine\n        style={{\n          strokeWidth,\n          stroke: '#990822',\n          fill: '#990822',\n          fillOpacity: 0.5,\n        }}\n        color='#cc0066'\n      />\n    </Sparklines>\n  )\n}\n\nMemorySparkLines.propTypes = {\n  data: PropTypes.object.isRequired,\n}\n\nexport const XvdSparkLines = ({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {\n  const { xvds } = data.stats\n\n  if (!xvds) {\n    return templateError\n  }\n\n  return (\n    <Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={width} height={height}>\n      <SparklinesLine\n        style={{\n          strokeWidth,\n          stroke: '#089944',\n          fill: '#089944',\n          fillOpacity: 0.5,\n        }}\n        color='#33cc33'\n      />\n    </Sparklines>\n  )\n}\n\nXvdSparkLines.propTypes = {\n  data: PropTypes.object.isRequired,\n}\n\nexport const NetworkSparkLines = ({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {\n  const { pifs, vifs: ifs = pifs } = data.stats\n\n  return ifs === undefined ? (\n    templateError\n  ) : (\n    <Sparklines style={STYLE} data={computeObjectsAvg(ifs)} min={0} width={width} height={height}>\n      <SparklinesLine\n        style={{\n          strokeWidth,\n          stroke: '#eca649',\n          fill: '#eca649',\n          fillOpacity: 0.5,\n        }}\n        color='#ffd633'\n      />\n    </Sparklines>\n  )\n}\n\nNetworkSparkLines.propTypes = {\n  data: PropTypes.object.isRequired,\n}\n\nexport const LoadSparkLines = ({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {\n  const { load } = data.stats\n\n  if (!load) {\n    return templateError\n  }\n\n  return (\n    <Sparklines style={STYLE} data={load} min={0} width={width} height={height}>\n      <SparklinesLine\n        style={{\n          strokeWidth,\n          stroke: '#33cc33',\n          fill: '#33cc33',\n          fillOpacity: 0.5,\n        }}\n        color='#33cc33'\n      />\n    </Sparklines>\n  )\n}\n\nLoadSparkLines.propTypes = {\n  data: PropTypes.object.isRequired,\n}\n","// ===================================================================\n// Tools to manipulate rrd stats\n// ===================================================================\n\nimport map from 'lodash/map'\nimport values from 'lodash/values'\nimport { mapPlus } from 'utils'\n\n// Returns a new array with arrays sums.\n// Example: computeArraysSum([[1, 2], [3, 4], [5, 0]) = [9, 6]\nconst _computeArraysSum = arrays => {\n  if (!arrays || !arrays.length || !arrays[0].length) {\n    return []\n  }\n\n  const n = arrays[0].length // N items in each array\n  const m = arrays.length // M arrays\n\n  const result = new Array(n)\n\n  for (let i = 0; i < n; i++) {\n    result[i] = 0\n\n    for (let j = 0; j < m; j++) {\n      result[i] += arrays[j][i]\n    }\n  }\n\n  return result\n}\n\n// Returns a new array with arrays avgs.\n// Example: computeArraysAvg([[1, 2], [3, 4], [5, 0]) = [4.5, 2]\nconst _computeArraysAvg = arrays => {\n  const sums = _computeArraysSum(arrays)\n\n  if (!arrays[0]) {\n    return []\n  }\n  const n = arrays && arrays[0].length\n  const m = arrays.length\n\n  for (let i = 0; i < n; i++) {\n    sums[i] /= m\n  }\n\n  return sums\n}\n\n// Arrays can be null.\n// See: https://github.com/vatesfr/xen-orchestra/issues/969\n//\n// It's a fix to avoid error like `Uncaught TypeError: Cannot read property 'length' of null`.\n// FIXME: Repair this bug in xo-server. (Warning: Can break the stats of xo-web v4.)\nconst removeUndefinedArrays = arrays =>\n  mapPlus(arrays, (array, push) => {\n    if (array != null) {\n      push(array)\n    }\n  })\n\nexport const computeArraysSum = arrays => _computeArraysSum(removeUndefinedArrays(arrays))\nexport const computeArraysAvg = arrays => _computeArraysAvg(removeUndefinedArrays(arrays))\n\n// More complex than computeArraysAvg.\n//\n// Take in parameter one object like:\n// { x: { a: [...], b: [...], c: [...] },\n//   y: { d: [...], e: [...], f: [...] } }\n// and returns the avgs between a, b, c, d, e and f.\n// Useful for vifs, pifs, xvds.\n//\n// Note: The parameter can be also an 3D array.\nexport const computeObjectsAvg = objects => {\n  return _computeArraysAvg(map(objects, object => computeArraysAvg(values(object))))\n}\n","module.exports = {\n    \"container\": \"mc4ba0a608_container\"\n};","import PropTypes from 'prop-types'\nimport React from 'react'\nimport * as d3 from 'd3'\nimport forEach from 'lodash/forEach'\nimport map from 'lodash/map'\n\nimport Component from '../base-component'\nimport _ from '../intl'\nimport { Toggle } from '../form'\nimport { setStyles } from '../d3-utils'\nimport { createGetObject, createSelector } from '../selectors'\nimport { connectStore, propsEqual } from '../utils'\n\nimport styles from './index.css'\n\n// ===================================================================\n\nconst X_AXIS_STYLE = {\n  'shape-rendering': 'crispEdges',\n  fill: 'none',\n  stroke: '#000',\n}\n\nconst X_AXIS_TEXT_STYLE = {\n  'font-size': '125%',\n  fill: 'black',\n  stroke: 'transparent',\n}\n\nconst LABEL_STYLE = {\n  'font-size': '125%',\n}\n\nconst MOUSE_AREA_STYLE = {\n  'pointer-events': 'all',\n  fill: 'none',\n}\n\nconst HOVER_LINE_STYLE = {\n  'stroke-width': '2px',\n  'stroke-dasharray': '5 5',\n  stroke: 'red',\n  fill: 'none',\n}\n\nconst HOVER_TEXT_STYLE = {\n  fill: 'black',\n}\n\nconst HORIZON_AREA_N_STEPS = 4\nconst HORIZON_AREA_MARGIN = 20\nconst HORIZON_AREA_PATH_STYLE = {\n  'fill-opacity': 0.25,\n  'stroke-opacity': 0.3,\n  fill: 'darkgreen',\n  stroke: 'transparent',\n}\n\n// ===================================================================\n\n@connectStore(() => {\n  const label = createSelector(\n    createGetObject((_, props) => props.objectId),\n    object => object.name_label\n  )\n\n  return { label }\n})\nclass XoWeekChart extends Component {\n  static propTypes = {\n    chartHeight: PropTypes.number,\n    chartWidth: PropTypes.number,\n    data: PropTypes.arrayOf(\n      PropTypes.shape({\n        date: PropTypes.number.isRequired,\n        value: PropTypes.number.isRequired,\n      })\n    ).isRequired,\n    maxValue: PropTypes.number,\n    objectId: PropTypes.string.isRequired,\n    onTooltipChange: PropTypes.func.isRequired,\n    tooltipX: PropTypes.number.isRequired,\n    valueRenderer: PropTypes.func,\n  }\n\n  static defaultProps = {\n    chartHeight: 70,\n    chartWidth: 300,\n    valueRenderer: value => value,\n  }\n\n  _x = d3.scaleTime()\n  _y = d3.scaleLinear()\n\n  _bisectDate = d3.bisector(elem => elem.date).left\n\n  _xAxis = d3.axisBottom().scale(this._x)\n\n  _line = d3\n    .line()\n    .x(elem => this._x(elem.date))\n    .y(elem => this._y(elem.value))\n\n  _drawHorizonArea(data, max = d3.max(data, elem => elem.value)) {\n    const intervalSize = max / HORIZON_AREA_N_STEPS\n    const splittedData = []\n\n    // Start.\n    let date = new Date(data[0].date)\n    for (let i = 0; i < HORIZON_AREA_N_STEPS; i++) {\n      splittedData[i] = [\n        {\n          date,\n          value: 0,\n        },\n      ]\n    }\n\n    // Middle.\n    forEach(data, elem => {\n      const date = new Date(elem.date)\n      for (let i = 0; i < HORIZON_AREA_N_STEPS; i++) {\n        splittedData[i].push({\n          date,\n          value: Math.min(Math.max(0, elem.value - intervalSize * i), intervalSize),\n        })\n      }\n    })\n\n    // End.\n    date = new Date(data[data.length - 1].date)\n    for (let i = 0; i < HORIZON_AREA_N_STEPS; i++) {\n      splittedData[i].push({\n        date,\n        value: 0,\n      })\n    }\n\n    this._x.domain(d3.extent(splittedData[0], elem => elem.date))\n    this._y.domain([0, max / HORIZON_AREA_N_STEPS])\n\n    const svg = this._svg\n\n    svg.select('.horizon-area').selectAll('path').remove()\n    forEach(splittedData, data => {\n      svg.select('.horizon-area').append('path').datum(data).attr('d', this._line)::setStyles(HORIZON_AREA_PATH_STYLE)\n    })\n  }\n\n  _draw(props = this.props) {\n    const svg = this._svg\n\n    // 1. Update dimensions.\n    const width = props.chartWidth\n    const horizonAreaWidth = width - HORIZON_AREA_MARGIN * 2\n\n    const horizonAreaHeight = props.chartHeight\n    const height = horizonAreaHeight + HORIZON_AREA_MARGIN\n\n    this._x.range([0, horizonAreaWidth])\n    this._y.range([horizonAreaHeight, 0])\n\n    svg\n      .attr('width', width)\n      .attr('height', height)\n      .select('.mouse-area')\n      .attr('width', horizonAreaWidth)\n      .attr('height', horizonAreaHeight)\n\n    svg.select('.hover-container').select('.hover-line').attr('y2', horizonAreaHeight)\n\n    // 2. Draw horizon area.\n    this._drawHorizonArea(props.data, props.maxValue)\n\n    // 3. Update x axis.\n    svg\n      .select('.x-axis')\n      .call(this._xAxis)\n      .attr('transform', `translate(0, ${props.chartHeight})`)\n      .selectAll('text')\n      ::setStyles(X_AXIS_TEXT_STYLE)\n\n    // 4. Update label.\n    svg.select('.label').attr('dx', 5).attr('dy', 20).text(props.label)\n  }\n\n  _handleMouseMove = () => {\n    this.props.onTooltipChange(d3.mouse(this.refs.chart)[0] - HORIZON_AREA_MARGIN)\n  }\n\n  // Update hover area position and text.\n  _updateTooltip(tooltipX) {\n    const date = this._x.invert(tooltipX)\n    const { data } = this.props\n    const index = this._bisectDate(data, date, 1)\n\n    const d0 = data[index - 1]\n    const d1 = data[index]\n\n    // Outside limits.\n    if (d1 === undefined) {\n      return\n    }\n\n    const elem = date - d0.date > d1.date - date ? d1 : d0\n    const x = this._x(elem.date)\n\n    const { props } = this\n    const hover = this._svg.select('.hover-container')\n\n    hover.select('.hover-line').attr('x1', x).attr('x2', x)\n\n    hover\n      .select('.hover-text')\n      .attr('dx', x + 5)\n      .attr('dy', props.chartHeight / 2)\n      .text(props.valueRenderer(elem.value))\n  }\n\n  componentDidMount() {\n    // Horizon area ----------------------------------------\n\n    const svg = (this._svg = d3\n      .select(this.refs.chart)\n      .append('svg')\n      .attr('transform', `translate(${HORIZON_AREA_MARGIN}, 0)`))\n    svg.append('g').attr('class', 'x-axis')::setStyles(X_AXIS_STYLE)\n\n    svg.append('g').attr('class', 'horizon-area')\n    svg.append('text').attr('class', 'label')::setStyles(LABEL_STYLE)\n\n    // Tooltip ---------------------------------------------\n    svg.append('rect').attr('class', 'mouse-area').on('mousemove', this._handleMouseMove)::setStyles(MOUSE_AREA_STYLE)\n\n    const hover = svg.append('g').attr('class', 'hover-container')::setStyles('pointer-events', 'none')\n    hover.append('line').attr('class', 'hover-line').attr('y1', 0)::setStyles(HOVER_LINE_STYLE)\n    hover.append('text').attr('class', 'hover-text')::setStyles(HOVER_TEXT_STYLE)\n\n    this._draw()\n  }\n\n  componentWillReceiveProps(nextProps) {\n    const { props } = this\n\n    if (!propsEqual(props, nextProps, ['chartHeight', 'chartWidth', 'data', 'maxValue'])) {\n      this._draw(nextProps)\n    }\n\n    if (props.tooltipX !== nextProps.tooltipX) {\n      this._updateTooltip(nextProps.tooltipX)\n    }\n  }\n\n  render() {\n    return <div ref='chart' />\n  }\n}\n\n// ===================================================================\n\nexport default class XoWeekCharts extends Component {\n  static propTypes = {\n    chartHeight: PropTypes.number,\n    series: PropTypes.arrayOf(\n      PropTypes.shape({\n        data: PropTypes.arrayOf(\n          PropTypes.shape({\n            date: PropTypes.number.isRequired,\n            value: PropTypes.number.isRequired,\n          })\n        ).isRequired,\n        objectId: PropTypes.string.isRequired,\n      })\n    ).isRequired,\n    valueRenderer: PropTypes.func,\n  }\n\n  _handleResize = () => {\n    const { container } = this.refs\n    this.setState({\n      chartsWidth: container && container.offsetWidth,\n    })\n  }\n\n  _handleTooltipChange = x => {\n    this.setState({ tooltipX: x })\n  }\n\n  _updateScale = (useScale, series = this.props.series) => {\n    let max\n\n    if (useScale) {\n      max = 0\n      forEach(series, series => {\n        forEach(series.data, elem => {\n          max = Math.max(elem.value, max)\n        })\n      })\n    }\n\n    this.setState({\n      maxValue: max,\n    })\n  }\n\n  componentDidMount() {\n    window.addEventListener('resize', this._handleResize)\n    this._handleResize()\n  }\n\n  componentWillMount() {\n    this.setState({\n      tooltipX: 0,\n    })\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener('resize', this._handleResize)\n  }\n\n  componentWillReceiveProps(nextProps) {\n    const { series } = nextProps\n\n    if (this.props.series !== series) {\n      this.setState({ tooltipX: 0 })\n      this._updateScale(this.state.maxValue !== undefined, series)\n    }\n  }\n\n  render() {\n    const {\n      props,\n      state: { chartsWidth, maxValue, tooltipX },\n    } = this\n\n    return (\n      <div>\n        <div>\n          <p className='mt-1'>\n            {_('weeklyChartsScaleInfo')} <Toggle iconSize={1} icon='scale' onChange={this._updateScale} />\n          </p>\n        </div>\n        <div ref='container' className={styles.container}>\n          {chartsWidth &&\n            map(props.series, (series, key) => (\n              <XoWeekChart\n                {...series}\n                chartWidth={chartsWidth}\n                key={key}\n                maxValue={maxValue}\n                onTooltipChange={this._handleTooltipChange}\n                tooltipX={tooltipX}\n                valueRenderer={props.valueRenderer}\n              />\n            ))}\n        </div>\n      </div>\n    )\n  }\n}\n","module.exports = {\n    \"cell\": \"mce63b6806_cell\",\n    \"table\": \"mce63b6806_table\"\n};","import PropTypes from 'prop-types'\nimport React from 'react'\nimport forEach from 'lodash/forEach'\nimport map from 'lodash/map'\nimport moment from 'moment'\nimport sortBy from 'lodash/sortBy'\nimport times from 'lodash/times'\nimport { extent, interpolateViridis, scaleSequential } from 'd3'\nimport { FormattedTime } from 'react-intl'\n\nimport _ from '../intl'\nimport Component from '../base-component'\nimport Tooltip from '../tooltip'\n\nimport styles from './index.css'\n\n// ===================================================================\n\nconst DAY_TIME_FORMAT = {\n  day: 'numeric',\n  month: 'numeric',\n}\n\n// ===================================================================\n\nconst computeColorGen = days => {\n  let min = Number.MAX_VALUE\n  let max = Number.MIN_VALUE\n\n  forEach(days, day => {\n    const [_min, _max] = extent(day.hours, value => value && value.value)\n\n    if (_min < min) {\n      min = _min\n    }\n\n    if (_max > max) {\n      max = _max\n    }\n  })\n\n  return scaleSequential(interpolateViridis).domain([max, min])\n}\n\nconst computeMissingDays = days => {\n  const correctedDays = days.slice()\n  const end = days.length - 1\n  const hours = new Array(24)\n\n  let a = moment(days[end].timestamp)\n  let b\n\n  for (let i = end; i > 0; i--) {\n    b = moment(days[i - 1].timestamp)\n\n    const diff = a.diff(b, 'days')\n\n    if (diff > 1) {\n      const missingDays = times(diff - 1, () => ({\n        hours,\n        timestamp: a.subtract(1, 'days').valueOf(),\n      })).reverse()\n\n      correctedDays.splice.apply(correctedDays, [i, 0].concat(missingDays))\n    }\n\n    a = b\n  }\n\n  return correctedDays\n}\n\n// ===================================================================\n\nexport default class XoWeekHeatmap extends Component {\n  static propTypes = {\n    cellRenderer: PropTypes.func,\n    data: PropTypes.arrayOf(\n      PropTypes.shape({\n        date: PropTypes.number.isRequired,\n        value: PropTypes.number.isRequired,\n      })\n    ).isRequired,\n  }\n\n  static defaultProps = {\n    cellRenderer: value => value,\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this._updateDays(nextProps.data)\n  }\n\n  componentWillMount() {\n    this._updateDays(this.props.data)\n  }\n\n  _updateDays(data) {\n    const days = {}\n\n    // 1. Compute average per day.\n    forEach(data, elem => {\n      const date = new Date(elem.date)\n      const dayId = moment(date).format('YYYYMMDD')\n      const hourId = date.getHours()\n\n      const { value } = elem\n\n      if (!days[dayId]) {\n        days[dayId] = {\n          hours: new Array(24),\n          timestamp: elem.date,\n        }\n      }\n\n      const { hours } = days[dayId]\n\n      if (!hours[hourId]) {\n        hours[hourId] = {\n          date,\n          nb: 1,\n          value,\n        }\n      } else {\n        const hour = hours[hourId]\n        hour.value = (hour.value * hour.nb + value) / (hour.nb + 1)\n        hour.nb++\n      }\n    })\n\n    // 2. Compute color gen.\n    const colorGen = computeColorGen(days)\n\n    // 3. Define color cells.\n    forEach(days, day => {\n      forEach(day.hours, hour => {\n        if (hour) {\n          hour.color = colorGen(hour.value)\n        }\n      })\n    })\n\n    this.setState({\n      days: computeMissingDays(sortBy(days, 'timestamp')),\n    })\n  }\n\n  render() {\n    return (\n      <table className={styles.table}>\n        <tbody>\n          <tr>\n            <th />\n            {times(24, hour => (\n              <th key={hour} className='text-xs-center'>\n                {hour}\n              </th>\n            ))}\n          </tr>\n          {map(this.state.days, (day, key) => (\n            <tr key={key}>\n              <th>\n                <FormattedTime value={day.timestamp} {...DAY_TIME_FORMAT} />\n              </th>\n              {map(day.hours, (hour, key) => (\n                <Tooltip\n                  content={\n                    hour\n                      ? _('weekHeatmapData', {\n                          date: hour.date,\n                          value: this.props.cellRenderer(hour.value),\n                        })\n                      : _('weekHeatmapNoData')\n                  }\n                  key={key}\n                >\n                  <td className={styles.cell} style={{ background: hour ? hour.color : '#ffffff' }} />\n                </Tooltip>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    )\n  }\n}\n","export default (string, cb) => {\n  const { length } = string\n  let i = 0\n  while (i < length) {\n    let j = string.indexOf('\\n', i)\n\n    // no final \\n\n    if (j === -1) {\n      j = length\n    }\n\n    // non empty line\n    if (j !== i) {\n      cb(JSON.parse(string.slice(i, j)))\n    }\n\n    i = j + 1\n  }\n}\n","import _ from 'intl'\nimport BaseComponent from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport { Col } from 'grid'\nimport { connectStore } from 'utils'\nimport { createCollectionWrapper, createGetObjectsOfType, createSelector } from 'selectors'\nimport { flatten, forEach, isEmpty, map, uniq } from 'lodash'\nimport { getPatchesDifference } from 'xo'\nimport { SelectHost } from 'select-objects'\n\n@connectStore(\n  () => ({\n    singleHosts: createSelector(\n      (_, { pool }) => pool && pool.id,\n      createGetObjectsOfType('host'),\n      createCollectionWrapper((poolId, hosts) => {\n        const visitedPools = {}\n        const singleHosts = {}\n        forEach(hosts, host => {\n          const { $pool } = host\n          if ($pool !== poolId) {\n            const previousHost = visitedPools[$pool]\n            if (previousHost !== undefined) {\n              delete singleHosts[previousHost]\n            } else {\n              const { id } = host\n              singleHosts[id] = true\n              visitedPools[$pool] = id\n            }\n          }\n        })\n        return singleHosts\n      })\n    ),\n  }),\n  { withRef: true }\n)\nexport default class AddHostsModal extends BaseComponent {\n  get value() {\n    const { nHostsMissingPatches, nPoolMissingPatches } = this.state\n    if (process.env.XOA_PLAN < 2 && (nHostsMissingPatches > 0 || nPoolMissingPatches > 0)) {\n      return {}\n    }\n\n    return { hosts: this.state.hosts }\n  }\n\n  _getHostPredicate = createSelector(\n    () => this.props.singleHosts,\n    singleHosts => host => singleHosts[host.id]\n  )\n\n  _onChangeHosts = async hosts => {\n    if (isEmpty(hosts)) {\n      this.setState({\n        hosts,\n        nHostsMissingPatches: undefined,\n        nPoolMissingPatches: undefined,\n      })\n      return\n    }\n\n    const { master } = this.props.pool\n    this.setState({\n      hosts,\n      nHostsMissingPatches: uniq(\n        flatten(await Promise.all(map(hosts, ({ id: hostId }) => getPatchesDifference(hostId, master))))\n      ).length,\n      nPoolMissingPatches: uniq(\n        flatten(await Promise.all(map(hosts, ({ id: hostId }) => getPatchesDifference(master, hostId))))\n      ).length,\n    })\n  }\n\n  render() {\n    const { hosts, nHostsMissingPatches, nPoolMissingPatches } = this.state\n    const canMulti = +process.env.XOA_PLAN > 3\n    return (\n      <div>\n        <SingleLineRow>\n          <Col size={6}>{_('hosts')}</Col>\n          <Col size={6}>\n            <SelectHost\n              multi={canMulti}\n              onChange={\n                canMulti ? this._onChangeHosts : host => this._onChangeHosts(host !== null ? [host] : undefined)\n              }\n              predicate={this._getHostPredicate()}\n              value={canMulti ? hosts : hosts !== undefined ? hosts[0] : undefined}\n            />\n          </Col>\n        </SingleLineRow>\n        <br />\n        {(nHostsMissingPatches > 0 || nPoolMissingPatches > 0) && (\n          <div>\n            {process.env.XOA_PLAN > 1 ? (\n              <div>\n                {nPoolMissingPatches > 0 && (\n                  <SingleLineRow>\n                    <Col>\n                      <span className='text-danger'>\n                        <Icon icon='error' />{' '}\n                        {_('missingPatchesPool', {\n                          nMissingPatches: nPoolMissingPatches,\n                        })}\n                      </span>\n                    </Col>\n                  </SingleLineRow>\n                )}\n                {nHostsMissingPatches > 0 && (\n                  <SingleLineRow>\n                    <Col>\n                      <span className='text-danger'>\n                        <Icon icon='error' />{' '}\n                        {_('missingPatchesHost', {\n                          nHosts: hosts.length,\n                          nMissingPatches: nHostsMissingPatches,\n                        })}\n                      </span>\n                    </Col>\n                  </SingleLineRow>\n                )}\n              </div>\n            ) : (\n              _('patchUpdateNoInstall', {\n                nHosts: hosts.length,\n              })\n            )}\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n","import _ from '../intl'\nimport authenticator from '../otp-authenticator.js'\nimport Copiable from '../copiable/index.js'\nimport qrcode from 'qrcode'\nimport PropTypes from 'prop-types'\nimport React, { PureComponent } from 'react'\nimport { Col, Row } from '../grid.js'\n\nconst RE = /\\s+/g\n\nexport class AddOtpModal extends PureComponent {\n  static propTypes = {\n    failedToken: PropTypes.string,\n    secret: PropTypes.string.isRequired,\n    user: PropTypes.object.isRequired,\n  }\n\n  state = {}\n  constructor(props, context) {\n    super(props, context)\n\n    qrcode.toDataURL(authenticator.keyuri(props.user.email, 'Xen Orchestra', props.secret), (err, url) => {\n      if (err) {\n        return console.warn('error while generating QR code', err)\n      }\n      this.state.qrcode = url\n    })\n  }\n\n  value = ''\n  _saveValue = event => {\n    this.value = event.target.value.replace(RE, '')\n  }\n\n  render() {\n    const { qrcode } = this.state\n    return (\n      <div>\n        <p>{_('addOtpConfirmMessage')}</p>\n        <Row>\n          <Col size={4}>\n            <strong>{_('OtpCode')}</strong>\n          </Col>\n          <Col size={8}>\n            <input className='form-control' inputMode='numeric' onChange={this._saveValue} />\n            {this.value === this.props.failedToken && <p className='text-warning'>{_('addOtpInvalidPassword')}</p>}\n          </Col>\n        </Row>\n        <hr />\n        {qrcode !== undefined && (\n          <div className='text-xs-center'>\n            <img src={qrcode} alt='qrcode' />\n          </div>\n        )}\n        <Copiable tagName='div' className='text-xs-center'>\n          {this.props.secret}\n        </Copiable>\n      </div>\n    )\n  }\n}\n","import keys from 'lodash/keys'\nimport PropTypes from 'prop-types'\nimport React from 'react'\n\nimport * as FormGrid from '../../form-grid'\nimport _ from '../../intl'\nimport Combobox from '../../combobox'\nimport Component from '../../base-component'\nimport { createSelector } from '../../selectors'\n\nexport default class SaveNewUserFilterModalBody extends Component {\n  static propTypes = {\n    type: PropTypes.string.isRequired,\n    user: PropTypes.object.isRequired,\n    value: PropTypes.string.isRequired,\n  }\n\n  get value() {\n    return this.state.name || ''\n  }\n\n  _getFilterOptions = createSelector(\n    tmp => (tmp = this.props.user) && (tmp = tmp.preferences) && (tmp = tmp.filters) && tmp[this.props.type],\n    keys\n  )\n\n  render() {\n    const { value } = this.props\n    const options = this._getFilterOptions()\n\n    return (\n      <div>\n        <FormGrid.Row>\n          <FormGrid.LabelCol>{_('filterName')}</FormGrid.LabelCol>\n          <FormGrid.InputCol>\n            <Combobox onChange={this.linkState('name')} options={options} value={this.state.name || ''} />\n          </FormGrid.InputCol>\n        </FormGrid.Row>\n        <FormGrid.Row>\n          <FormGrid.LabelCol>{_('filterValue')}</FormGrid.LabelCol>\n          <FormGrid.InputCol>\n            <input className='form-control' disabled type='text' value={value} />\n          </FormGrid.InputCol>\n        </FormGrid.Row>\n      </div>\n    )\n  }\n}\n","import Collapse from 'collapse'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { Container, Col } from 'grid'\nimport { isEmpty, map } from 'lodash'\nimport { isSrWritable } from 'xo'\nimport { Vdi } from 'render-xo-item'\n\nimport _ from '../../intl'\nimport SingleLineRow from '../../single-line-row'\nimport { SelectSr } from '../../select-objects'\n\nconst Collapsible = ({ collapsible, children, ...props }) =>\n  collapsible ? (\n    <Collapse {...props}>{children}</Collapse>\n  ) : (\n    <div>\n      <span>{props.buttonText}</span>\n      <br />\n      {children}\n    </div>\n  )\n\nCollapsible.propTypes = {\n  collapsible: PropTypes.bool.isRequired,\n  children: PropTypes.node.isRequired,\n}\n\nexport default class ChooseSrForEachVdisModal extends Component {\n  static propTypes = {\n    ignorableVdis: PropTypes.bool,\n    mainSrPredicate: PropTypes.func,\n    onChange: PropTypes.func.isRequired,\n    srPredicate: PropTypes.func,\n    value: PropTypes.objectOf(\n      PropTypes.shape({\n        mainSr: PropTypes.object,\n        mapVdisSrs: PropTypes.object,\n      })\n    ).isRequired,\n    vdis: PropTypes.object.isRequired,\n  }\n\n  _onChange = newValues => {\n    this.props.onChange({\n      ...this.props.value,\n      ...newValues,\n    })\n  }\n\n  _onChangeMainSr = mainSr => this._onChange({ mainSr })\n\n  render() {\n    const { props } = this\n    const {\n      ignorableVdis = false,\n      mainSrPredicate = isSrWritable,\n      placeholder,\n      required,\n      srPredicate = mainSrPredicate,\n      value: { mainSr, mapVdisSrs },\n      vdis,\n    } = props\n\n    return (\n      <div>\n        <SingleLineRow>\n          <Col size={6}>{_('selectDestinationSr')}</Col>\n          <Col size={6}>\n            <SelectSr\n              onChange={this._onChangeMainSr}\n              placeholder={placeholder !== undefined ? placeholder : _('chooseSrForEachVdisModalMainSr')}\n              predicate={mainSrPredicate}\n              required={required}\n              value={mainSr}\n            />\n          </Col>\n        </SingleLineRow>\n        {!required && <i>{_('optionalEntry')}</i>}\n        <br />\n        {!isEmpty(vdis) && (\n          <Collapsible buttonText={_('chooseSrForEachVdisModalSelectSr')} collapsible size='small'>\n            <br />\n            <Container>\n              <SingleLineRow>\n                <Col size={6}>\n                  <strong>{_('chooseSrForEachVdisModalVdiLabel')}</strong>\n                </Col>\n                <Col size={6}>\n                  <strong>{_('chooseSrForEachVdisModalSrLabel')}</strong>\n                </Col>\n              </SingleLineRow>\n              {map(vdis, vdi => (\n                <SingleLineRow key={vdi.uuid}>\n                  <Col size={ignorableVdis ? 5 : 6}>\n                    {vdi.name !== undefined ? vdi.name : <Vdi id={vdi.id} showSize />}\n                  </Col>\n                  <Col size={6}>\n                    <SelectSr\n                      onChange={sr =>\n                        this._onChange({\n                          mapVdisSrs: { ...mapVdisSrs, [vdi.uuid]: sr },\n                        })\n                      }\n                      predicate={srPredicate}\n                      value={mapVdisSrs !== undefined && mapVdisSrs[vdi.uuid]}\n                    />\n                  </Col>\n                  {ignorableVdis && (\n                    <Col size={1}>\n                      <Tooltip content={_('ignoreVdi')}>\n                        <a\n                          role='button'\n                          onClick={() =>\n                            this._onChange({\n                              mapVdisSrs: { ...mapVdisSrs, [vdi.uuid]: null },\n                            })\n                          }\n                        >\n                          <Icon icon='remove' />\n                        </a>\n                      </Tooltip>\n                    </Col>\n                  )}\n                </SingleLineRow>\n              ))}\n              <i>{_('optionalEntry')}</i>\n            </Container>\n          </Collapsible>\n        )}\n      </div>\n    )\n  }\n}\n","import _, { messages } from 'intl'\nimport Icon from 'icon'\nimport map from 'lodash/map'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { compileTemplate } from '@xen-orchestra/template'\nimport every from 'lodash/every.js'\nimport { injectIntl } from 'react-intl'\n\nimport BaseComponent from 'base-component'\nimport SingleLineRow from 'single-line-row'\nimport { Col } from 'grid'\nimport { SelectSr } from 'select-objects'\nimport { connectStore } from 'utils'\nimport { isSrWritable } from 'xo'\n\nimport SelectCompression from '../../select-compression'\nimport ZstdChecker from '../../zstd-checker'\nimport { getXoaPlan, STARTER } from '../../xoa-plans'\nimport { createGetObject, createGetObjectsOfType, createSelector } from '../../selectors'\n\nconst CAN_INTERPOOL_COPY = getXoaPlan().value > STARTER.value\n\n@connectStore(\n  () => {\n    const getVms = createGetObjectsOfType((_, props) => props.type).pick((_, props) => props.vms)\n    // To remove 'Zstd' option if it's not supported when copying one VM.\n    const getIsZstdSupported = createSelector(\n      createGetObject(createSelector(getVms, vms => (vms.length === 1 ? Object.values(vms)[0].$container : undefined))),\n      container => container === undefined || container.zstdSupported\n    )\n\n    return {\n      resolvedVms: getVms,\n      isZstdSupported: getIsZstdSupported,\n    }\n  },\n  { withRef: true }\n)\nclass CopyVmsModalBody extends BaseComponent {\n  get value() {\n    const { state } = this\n    if (state.copyMode === 'fullCopy' && state.sr == null) {\n      return {}\n    }\n    const { resolvedVms } = this.props\n    const { compression, copyMode, namePattern, sr } = state\n\n    const names = namePattern\n      ? map(\n          resolvedVms,\n          compileTemplate(namePattern, {\n            '{name}': vm => vm.name_label,\n            '{id}': vm => vm.id,\n          })\n        )\n      : map(resolvedVms, vm => vm.name_label)\n\n    return {\n      compression: compression === 'zstd' ? 'zstd' : compression === 'native',\n      copyMode,\n      names,\n      sr: sr == null ? undefined : sr.id,\n    }\n  }\n\n  componentWillMount() {\n    this.setState({\n      compression: '',\n      copyMode: 'fullCopy',\n      namePattern: '{name}_COPY',\n    })\n  }\n\n  getSrPredicate = createSelector(\n    () => this.props.resolvedVms,\n    vms => (CAN_INTERPOOL_COPY ? undefined : sr => isSrWritable(sr) && every(vms, { $poolId: sr.$pool }))\n  )\n\n  _onChangeSr = sr => this.setState({ sr })\n  _onChangeNamePattern = event => this.setState({ namePattern: event.target.value })\n\n  render() {\n    const {\n      intl: { formatMessage },\n      isZstdSupported,\n      vms,\n    } = this.props\n    const { compression, copyMode, namePattern, sr } = this.state\n\n    return (\n      <div>\n        <SingleLineRow>\n          <Col size={6}>{_('copyVmName')}</Col>\n          <Col size={6}>\n            <input\n              className='form-control'\n              onChange={this.linkState('namePattern')}\n              placeholder={formatMessage(messages.copyVmNamePatternPlaceholder)}\n              type='text'\n              value={namePattern}\n            />\n          </Col>\n        </SingleLineRow>\n        <div className='mt-1'>\n          <SingleLineRow>\n            <Col>\n              <label>\n                <input\n                  checked={copyMode === 'fullCopy'}\n                  name='copyMode'\n                  onChange={this.linkState('copyMode')}\n                  type='radio'\n                  value='fullCopy'\n                />\n                <span> {_('fullCopyMode')} </span>\n              </label>\n            </Col>\n          </SingleLineRow>\n          <SingleLineRow className='mt-1'>\n            <Col size={6}>{_('copyVmSelectSr')}</Col>\n            <Col size={6}>\n              <SelectSr\n                disabled={copyMode !== 'fullCopy'}\n                onChange={this.linkState('sr')}\n                predicate={this.getSrPredicate()}\n                value={sr}\n              />\n              {!CAN_INTERPOOL_COPY && (\n                <p className='text-muted'>\n                  <Icon icon='info' /> {_('cantInterPoolCopy')}\n                </p>\n              )}\n            </Col>\n          </SingleLineRow>\n          <SingleLineRow className='mt-1'>\n            <Col size={6}>{_('compression')}</Col>\n            <Col size={6}>\n              <SelectCompression\n                disabled={copyMode !== 'fullCopy'}\n                onChange={this.linkState('compression')}\n                showZstd={isZstdSupported}\n                value={compression}\n              />\n              {vms.length > 1 && compression === 'zstd' && <ZstdChecker vms={vms} />}\n            </Col>\n          </SingleLineRow>\n        </div>\n        <div>\n          <SingleLineRow className='mt-1'>\n            <Col>\n              <label>\n                <input\n                  checked={copyMode === 'fastClone'}\n                  name='copyMode'\n                  onChange={this.linkState('copyMode')}\n                  type='radio'\n                  value='fastClone'\n                />\n                <span> {_('fastCloneMode')} </span>\n              </label>\n            </Col>\n          </SingleLineRow>\n        </div>\n      </div>\n    )\n  }\n}\n\nCopyVmsModalBody.PropTypes = {\n  vms: PropTypes.arrayOf(PropTypes.string).isRequired,\n  type: PropTypes.oneOf(['VM', 'VM-snapshot', 'VM-template']),\n}\n\nCopyVmsModalBody.defaultProps = {\n  type: 'VM',\n}\n\nexport default injectIntl(CopyVmsModalBody, { withRef: true })\n","import _ from 'intl'\nimport Icon from 'icon'\nimport clamp from 'lodash/clamp'\nimport Component from 'base-component'\nimport React from 'react'\n\nexport default class EditVmNotesModalBody extends Component {\n  get value() {\n    return { notes: this.state.notes ?? this.props.vm.notes ?? '' }\n  }\n\n  render() {\n    return (\n      <div>\n        <textarea\n          autoFocus\n          rows={clamp(this.value.notes.split('\\n').length, 5, 20)}\n          onChange={this.linkState('notes')}\n          value={this.value.notes}\n          className='form-control'\n        />\n        {this.value.notes.length > 2048 && (\n          <em className='text-warning'>\n            <Icon icon='alarm' /> {_('vmNotesTooLong')}\n          </em>\n        )}\n        <em>\n          <Icon icon='info' />{' '}\n          <a href='https://commonmark.org/help/' target='_blank' rel='noreferrer'>\n            {_('supportsMarkdown')}\n          </a>\n        </em>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport React from 'react'\nimport { Password } from 'form'\n\nexport default class ExportConfigModalBody extends Component {\n  state = {\n    passphrase: '',\n  }\n\n  get value() {\n    const { passphrase } = this.state\n    return passphrase === '' ? undefined : passphrase\n  }\n\n  render() {\n    return (\n      <div>\n        <p>{_('exportConfigEnterPassphrase')}</p>\n        <Password\n          autoFocus\n          defaultVisible\n          enableGenerator\n          onChange={this.linkState('passphrase')}\n          value={this.state.passphrase}\n        />\n      </div>\n    )\n  }\n}\n","import BaseComponent from 'base-component'\nimport React from 'react'\n\nimport _ from '../../intl'\nimport { Container, Row, Col } from '../../grid'\nimport { Select } from '../../form'\n\nconst OPTIONS = [\n  {\n    label: _('qcow2'),\n    value: 'qcow2',\n  },\n  {\n    label: _('vhd'),\n    value: 'vhd',\n  },\n  {\n    label: _('vmdk'),\n    value: 'vmdk',\n  },\n  {\n    label: _('raw'),\n    value: 'raw',\n  },\n]\nexport default class ExportVdiModalBody extends BaseComponent {\n  state = {\n    format: 'vhd',\n  }\n\n  get value() {\n    return this.state.format\n  }\n\n  render() {\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={6}>\n            <strong>{_('format')}</strong>\n          </Col>\n          <Col mediumSize={6}>\n            <Select\n              labelKey='label'\n              options={OPTIONS}\n              required\n              simpleValue\n              onChange={this.linkState('format')}\n              value={this.state.format}\n            />\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import BaseComponent from 'base-component'\nimport PropTypes from 'prop-types'\nimport React from 'react'\n\nimport _ from '../../intl'\nimport SelectCompression from '../../select-compression'\nimport SelectExportFormat from '../../select-export-vm-format'\nimport { connectStore } from '../../utils'\nimport { Container, Row, Col } from '../../grid'\nimport { createGetObject, createSelector } from '../../selectors'\n\n@connectStore(\n  {\n    isZstdSupported: createSelector(\n      createGetObject((_, { vm }) => vm.$container),\n      container => container === undefined || container.zstdSupported\n    ),\n  },\n  { withRef: true }\n)\nexport default class ExportVmModalBody extends BaseComponent {\n  state = {\n    compression: '',\n    format: 'xva',\n  }\n\n  get value() {\n    const compression = this.state.compression === 'zstd' ? 'zstd' : this.state.compression === 'native'\n    const format = this.state.format\n    return { compression, format }\n  }\n\n  render() {\n    return (\n      <Container>\n        <Row>\n          <Col mediumSize={6}>\n            <strong> {_('exportType')}</strong>\n          </Col>\n          <Col mediumSize={6}>\n            <SelectExportFormat onChange={this.linkState('format')} value={this.state.format} />\n          </Col>\n        </Row>\n        {this.state.format === 'xva' && (\n          <Row>\n            <Col mediumSize={6}>\n              <strong>{_('compression')}</strong>\n            </Col>\n            <Col mediumSize={6}>\n              <SelectCompression\n                onChange={this.linkState('compression')}\n                showZstd={this.props.isZstdSupported}\n                value={this.state.compression}\n              />\n            </Col>\n          </Row>\n        )}\n      </Container>\n    )\n  }\n}\n\nExportVmModalBody.propTypes = {\n  vm: PropTypes.object.isRequired,\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport React from 'react'\nimport { Password } from 'form'\n\nexport default class ImportConfigModalBody extends Component {\n  state = {\n    passphrase: '',\n  }\n\n  get value() {\n    const { passphrase } = this.state\n    return passphrase === '' ? undefined : passphrase\n  }\n\n  render() {\n    return (\n      <div>\n        <p>{_('importConfigEnterPassphrase')}</p>\n        <Password autoFocus onChange={this.linkState('passphrase')} value={this.state.passphrase} />\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Dropzone from 'dropzone'\nimport React from 'react'\n\nexport default class ImportVdiModalBody extends Component {\n  get value() {\n    return this.state.file\n  }\n\n  render() {\n    const { file } = this.state\n    return (\n      <Dropzone\n        onDrop={this.linkState('file', '0')}\n        message={file === undefined ? _('selectVdiMessage') : file.name}\n        multiple={false}\n      />\n    )\n  }\n}\n","import authenticator from '../otp-authenticator.js'\nimport asap from 'asap'\nimport cookies from 'js-cookie'\nimport copy from 'copy-to-clipboard'\nimport fpSortBy from 'lodash/fp/sortBy'\nimport React from 'react'\nimport semver from 'semver'\nimport updater from 'xoa-updater'\nimport URL from 'url-parse'\nimport Xo from 'xo-lib'\nimport { createBackoff } from 'jsonrpc-websocket-client'\nimport { get as getDefined } from '@xen-orchestra/defined'\nimport { pFinally, reflect, retry, tap, tapCatch } from 'promise-toolbox'\nimport { SelectHost } from 'select-objects'\nimport { filter, forEach, get, includes, isEmpty, isEqual, map, once, size, sortBy, throttle } from 'lodash'\nimport {\n  forbiddenOperation,\n  incorrectState,\n  noHostsAvailable,\n  operationBlocked,\n  operationFailed,\n  vmBadPowerState,\n  vmLacksFeature,\n} from 'xo-common/api-errors'\n\nimport _ from '../intl'\nimport ActionButton from '../action-button'\nimport fetch, { post } from '../fetch'\nimport invoke from '../invoke'\nimport Icon from '../icon'\nimport logError from '../log-error'\nimport NewAuthTokenModal from './new-auth-token-modal'\nimport RegisterProxyModal from './register-proxy-modal'\nimport renderXoItem, { Host, renderXoItemFromId, Vm } from '../render-xo-item'\nimport store from 'store'\nimport WarmMigrationModal from './warm-migration-modal'\nimport { alert, chooseAction, confirm, form } from '../modal'\nimport { error, info, success } from '../notification'\nimport { getObject, isAdmin } from 'selectors'\nimport { getXoaPlan, SOURCES } from '../xoa-plans'\nimport { noop, resolveId, resolveIds } from '../utils'\nimport {\n  connected,\n  disconnected,\n  markObjectsFetched,\n  signedIn,\n  signedOut,\n  updateObjects,\n  updatePermissions,\n} from '../store/actions'\n\nimport parseNdJson from './_parseNdJson'\nimport RollingPoolRebootModal from './rolling-pool-reboot-modal'\n\nimport { NetworkCard } from '../../xo-app/xostor/new-xostor-form.js'\n\n// ===================================================================\n\nconst MAX_VMS = 30\n\n// ===================================================================\n\nexport const EXPIRES_SOON_DELAY = 30 * 24 * 60 * 60 * 1000 // 1 month\n\n// ===================================================================\n\nexport const ITEMS_PER_PAGE_OPTIONS = [10, 20, 50, 100]\nexport const VDIS_TO_COALESCE_LIMIT = 10\n\n// ===================================================================\n\nexport const XEN_DEFAULT_CPU_WEIGHT = 256\nexport const XEN_DEFAULT_CPU_CAP = 0\n\n// ===================================================================\n\nexport const XEN_VIDEORAM_VALUES = [1, 2, 4, 8, 16]\n\n// ===================================================================\n\nexport const isSrIso = sr => sr && sr.content_type === 'iso' && sr.size > 0\nexport const isSrWritable = sr => sr && sr.content_type !== 'iso' && sr.size > 0\nexport const isSrWritableOrIso = sr => sr && sr.size > 0\nexport const isSrShared = sr => sr && sr.shared\nexport const isVmRunning = vm => vm && vm.power_state === 'Running'\n\n// ===================================================================\n\nconst reload = () => {\n  // prevent automatic reconnection\n  xo.removeListener('closed', connect)\n\n  window.location.reload(true)\n}\n\nexport const signOut = () => {\n  cookies.remove('token')\n\n  reload()\n}\n\nexport const connect = () => {\n  xo.open(createBackoff()).catch(error => {\n    logError(error, 'failed to connect to xo-server')\n  })\n}\n\nconst xo = invoke(() => {\n  const token = cookies.get('token')\n  if (!token) {\n    reload()\n    throw new Error('no valid token')\n  }\n\n  const xo = new Xo({\n    credentials: { token },\n  })\n\n  xo.on('authenticationFailure', error => {\n    console.warn('authenticationFailure', error)\n\n    if (error.name !== 'ConnectionError') {\n      signOut(error)\n    }\n  })\n  xo.on('scheduledAttempt', ({ delay }) => {\n    console.warn('next attempt in %s ms', delay)\n  })\n\n  xo.on('closed', connect)\n\n  return xo\n})\nconnect()\n\nconst _signIn = new Promise(resolve => xo.once('authenticated', resolve))\n\n// eslint-disable-next-line n/no-unsupported-features/node-builtins\nconst _call = new URLSearchParams(window.location.search.slice(1)).has('debug')\n  ? async (method, params) => {\n      await _signIn\n      const now = Date.now()\n      return tap.call(\n        xo.call(method, params),\n        result => {\n          // eslint-disable-next-line no-console, n/no-unsupported-features/node-builtins\n          console.debug('API call (%d ms)', Date.now() - now, method, params, result)\n        },\n        error => {\n          console.error('API call (%d ms) error', Date.now() - now, method, params, error)\n        }\n      )\n    }\n  : (method, params) => _signIn.then(() => xo.call(method, params))\n\n// ===================================================================\n\nexport const connectStore = store => {\n  let updates = {}\n  const sendUpdates = throttle(() => {\n    store.dispatch(updateObjects(updates))\n    updates = {}\n  }, 5e2)\n\n  xo.on('open', () => store.dispatch(connected()))\n  xo.on('closed', () => {\n    store.dispatch(signedOut())\n    store.dispatch(disconnected())\n  })\n  xo.on('authenticated', () => {\n    store.dispatch(signedIn(xo.user))\n\n    _call('xo.getAllObjects', { ndjson: true })\n      .then(({ $getFrom }) => fetch('.' + $getFrom))\n      .then(response => response.text())\n      .then(data => {\n        const objects = Object.create(null)\n        parseNdJson(data, object => {\n          objects[object.id] = object\n        })\n        store.dispatch(updateObjects(objects))\n        store.dispatch(markObjectsFetched())\n      })\n  })\n  xo.on('notification', notification => {\n    if (notification.method !== 'all') {\n      return\n    }\n\n    Object.assign(updates, notification.params.items)\n    sendUpdates()\n  })\n  subscribePermissions(permissions => store.dispatch(updatePermissions(permissions)))\n\n  // work around to keep the user in Redux store up to date\n  //\n  // FIXME: store subscriptions data directly in Redux\n  subscribeCurrentUser(user => {\n    store.dispatch(signedIn(user))\n  })\n}\n\n// -------------------------------------------------------------------\n\nexport const resolveUrl = invoke(\n  xo._url, // FIXME: accessing private prop\n  baseUrl => to => new URL(to, baseUrl).toString()\n)\n\n// -------------------------------------------------------------------\n// Default subscription 5s\nconst createSubscription = (cb, { polling = 5e3 } = {}) => {\n  const delay = polling\n  const clearCacheDelay = 6e5 // 10m\n\n  // contains active and lazy subscribers\n  const subscribers = Object.create(null)\n  const hasSubscribers = () => Object.keys(subscribers).length !== 0\n\n  // only counts active subscribers\n  let nActiveSubscribers = 0\n\n  let cache\n  let nextId = 0\n  let timeout\n\n  let running = false\n\n  const clearCache = () => {\n    cache = undefined\n  }\n\n  const uninstall = () => {\n    clearTimeout(timeout)\n    timeout = setTimeout(clearCache, clearCacheDelay)\n  }\n\n  // will loop if n > 0 at the end\n  //\n  // will not do anything if already running\n  const run = () => {\n    clearTimeout(timeout)\n\n    if (running) {\n      return\n    }\n\n    running = true\n    _signIn\n      .then(() => cb(cache))\n      .then(\n        result => {\n          running = false\n\n          if (nActiveSubscribers === 0) {\n            return uninstall()\n          }\n\n          timeout = setTimeout(run, delay)\n\n          if (!isEqual(result, cache)) {\n            cache = result\n\n            forEach(subscribers, subscriber => {\n              // A subscriber might have disappeared during iteration.\n              //\n              // E.g.: if a subscriber triggers the subscription of another.\n              if (subscriber) {\n                subscriber(result)\n              }\n            })\n          }\n        },\n        error => {\n          running = false\n\n          if (nActiveSubscribers === 0) {\n            return uninstall()\n          }\n\n          console.error(error)\n        }\n      )\n  }\n\n  const subscribe = cb => {\n    const id = nextId++\n    subscribers[id] = cb\n\n    if (cache !== undefined) {\n      asap(() => cb(cache))\n    }\n\n    if (nActiveSubscribers++ === 0) {\n      run()\n    }\n\n    return once(() => {\n      delete subscribers[id]\n\n      if (--nActiveSubscribers === 0) {\n        uninstall()\n      }\n    })\n  }\n\n  subscribe.forceRefresh = () => {\n    if (hasSubscribers()) {\n      run()\n    }\n  }\n\n  subscribe.lazy = cb => {\n    const id = nextId++\n    subscribers[id] = cb\n\n    if (cache !== undefined) {\n      asap(() => cb(cache))\n    }\n\n    // trigger an initial run if necessary\n    if (nActiveSubscribers === 0) {\n      run()\n    }\n\n    return once(() => {\n      delete subscribers[id]\n\n      // schedule cache deletion if necessary\n      if (nActiveSubscribers === 0) {\n        uninstall()\n      }\n    })\n  }\n\n  return subscribe\n}\n\n// Subscriptions -----------------------------------------------------\n\nexport const subscribeCurrentUser = createSubscription(() => xo.refreshUser())\n\nexport const subscribeAcls = createSubscription(() => _call('acl.get'))\n\nexport const subscribeHvSupportedVersions = createSubscription(\n  async () => {\n    try {\n      return await _call('xoa.getHVSupportedVersions')\n    } catch (error) {\n      console.error(error)\n    }\n  },\n  { polling: 1e3 * 60 * 60 } // 1h\n)\n\nexport const subscribeJobs = createSubscription(() => _call('job.getAll'))\n\nexport const subscribeJobsLogs = createSubscription(() => _call('log.get', { namespace: 'jobs' }))\n\nexport const subscribeApiLogs = createSubscription(() => _call('log.get', { namespace: 'api' }))\n\nexport const subscribePermissions = createSubscription(() => _call('acl.getCurrentPermissions'))\n\nexport const subscribePlugins = createSubscription(() => _call('plugin.get'))\n\nexport const subscribeRemotes = createSubscription(() => _call('remote.getAll'))\n\nexport const subscribeRemotesInfo = createSubscription(() => _call('remote.getAllInfo'))\n\nexport const subscribeProxies = createSubscription(() => {\n  const _isAdmin = isAdmin(store.getState())\n\n  return _isAdmin ? _call('proxy.getAll') : undefined\n})\n\nexport const subscribeResourceSets = createSubscription(() => _call('resourceSet.getAll'))\n\nexport const subscribeSchedules = createSubscription(() => _call('schedule.getAll'))\n\nexport const subscribeServers = createSubscription(\n  invoke(fpSortBy('host'), sort => () => _call('server.getAll').then(sort))\n)\n\nexport const subscribeConfiguredTags = createSubscription(() => _call('tag.getAllConfigured'))\n\nexport const subscribeUsers = createSubscription(() =>\n  _call('user.getAll').then(users => {\n    forEach(users, user => {\n      user.type = 'user'\n    })\n\n    return sortBy(users, 'email')\n  })\n)\n\nexport const subscribeGroups = createSubscription(() =>\n  _call('group.getAll').then(groups => {\n    forEach(groups, group => {\n      group.type = 'group'\n    })\n\n    return sortBy(groups, 'name')\n  })\n)\n\nexport const subscribeRoles = createSubscription(invoke(sortBy('name'), sort => () => _call('role.getAll').then(sort)))\n\nexport const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))\n\nexport const subscribeResourceCatalog = createSubscription(() => _call('cloud.getResourceCatalog'))\n\nexport const subscribeHubResourceCatalog = createSubscription(() =>\n  _call('cloud.getResourceCatalog', { filters: { hub: true } })\n)\n\nconst getNotificationCookie = () => {\n  const notificationCookie = cookies.get(`notifications:${store.getState().user.id}`)\n  return notificationCookie === undefined ? {} : JSON.parse(notificationCookie)\n}\n\nconst setNotificationCookie = (id, changes) => {\n  const notifications = getNotificationCookie()\n  notifications[id] = { ...(notifications[id] || {}), ...changes }\n  forEach(notifications[id], (value, key) => {\n    if (value === null) {\n      delete notifications[id][key]\n    }\n  })\n  cookies.set(`notifications:${store.getState().user.id}`, JSON.stringify(notifications), { expires: 9999 })\n}\n\nexport const dismissNotification = id => {\n  setNotificationCookie(id, { read: true, date: Date.now() })\n  subscribeNotifications.forceRefresh()\n}\n\nexport const subscribeNotifications = createSubscription(async () => {\n  const { user, xoaUpdaterState } = store.getState()\n  if (+process.env.XOA_PLAN === 5 || xoaUpdaterState === 'disconnected' || xoaUpdaterState === 'error') {\n    return []\n  }\n\n  let notifications\n  try {\n    const now = Date.now()\n    notifications = (await updater._call('getMessages')).filter(({ expires }) => expires == null || expires > now)\n  } catch (err) {\n    return []\n  }\n  const notificationCookie = getNotificationCookie()\n  return map(\n    user != null && user.permission === 'admin' ? notifications : filter(notifications, { level: 'warning' }),\n    notification => ({\n      ...notification,\n      read: !!get(notificationCookie, `${notification.id}.read`),\n    })\n  )\n})\n\nconst checkSchedulerGranularitySubscriptions = {}\nexport const subscribeSchedulerGranularity = (host, cb) => {\n  if (checkSchedulerGranularitySubscriptions[host] === undefined) {\n    checkSchedulerGranularitySubscriptions[host] = createSubscription(() =>\n      _call('host.getSchedulerGranularity', { host })\n    )\n  }\n\n  return checkSchedulerGranularitySubscriptions[host](cb)\n}\nsubscribeSchedulerGranularity.forceRefresh = host => {\n  if (host === undefined) {\n    forEach(checkSchedulerGranularitySubscriptions, subscription => subscription.forceRefresh())\n    return\n  }\n\n  const subscription = checkSchedulerGranularitySubscriptions[host]\n  if (subscription !== undefined) {\n    subscription.forceRefresh()\n  }\n}\n\nconst checkSrCurrentStateSubscriptions = {}\nexport const subscribeCheckSrCurrentState = (pool, cb) => {\n  const poolId = resolveId(pool)\n\n  if (!checkSrCurrentStateSubscriptions[poolId]) {\n    checkSrCurrentStateSubscriptions[poolId] = createSubscription(() => _call('xosan.checkSrCurrentState', { poolId }))\n  }\n\n  return checkSrCurrentStateSubscriptions[poolId](cb)\n}\nsubscribeCheckSrCurrentState.forceRefresh = pool => {\n  if (pool === undefined) {\n    forEach(checkSrCurrentStateSubscriptions, subscription => subscription.forceRefresh())\n    return\n  }\n\n  const subscription = checkSrCurrentStateSubscriptions[resolveId(pool)]\n  if (subscription !== undefined) {\n    subscription.forceRefresh()\n  }\n}\n\nconst missingPatchesByHost = {}\nexport const subscribeHostMissingPatches = (host, cb) => {\n  const hostId = resolveId(host)\n\n  if (missingPatchesByHost[hostId] == null) {\n    missingPatchesByHost[hostId] = createSubscription(() => getHostMissingPatches(host))\n  }\n\n  return missingPatchesByHost[hostId](cb)\n}\nsubscribeHostMissingPatches.forceRefresh = host => {\n  if (host === undefined) {\n    forEach(missingPatchesByHost, subscription => subscription.forceRefresh())\n    return\n  }\n\n  const subscription = missingPatchesByHost[resolveId(host)]\n  if (subscription !== undefined) {\n    subscription.forceRefresh()\n  }\n}\n\nconst proxiesApplianceUpdaterState = {}\nexport const subscribeProxyApplianceUpdaterState = (proxyId, cb) => {\n  if (proxiesApplianceUpdaterState[proxyId] === undefined) {\n    proxiesApplianceUpdaterState[proxyId] = createSubscription(async () => {\n      try {\n        return await getProxyApplianceUpdaterState(proxyId)\n      } catch (error) {\n        console.error(error)\n        return { state: 'error' }\n      }\n    })\n  }\n  return proxiesApplianceUpdaterState[proxyId](cb)\n}\nsubscribeProxyApplianceUpdaterState.forceRefresh = proxyId => {\n  if (proxyId === undefined) {\n    forEach(proxiesApplianceUpdaterState, subscription => subscription.forceRefresh())\n    return\n  }\n\n  const subscription = proxiesApplianceUpdaterState[proxyId]\n  if (subscription !== undefined) {\n    subscription.forceRefresh()\n  }\n}\n\nconst volumeInfoBySr = {}\nexport const subscribeVolumeInfo = ({ sr, infoType }, cb) => {\n  sr = resolveId(sr)\n\n  if (volumeInfoBySr[sr] == null) {\n    volumeInfoBySr[sr] = {}\n  }\n\n  if (volumeInfoBySr[sr][infoType] == null) {\n    volumeInfoBySr[sr][infoType] = createSubscription(() => _call('xosan.getVolumeInfo', { sr, infoType }))\n  }\n\n  return volumeInfoBySr[sr][infoType](cb)\n}\nsubscribeVolumeInfo.forceRefresh = (() => {\n  const refreshSrVolumeInfo = volumeInfo => {\n    forEach(volumeInfo, subscription => subscription.forceRefresh())\n  }\n\n  return sr => {\n    if (sr === undefined) {\n      forEach(volumeInfoBySr, refreshSrVolumeInfo)\n    } else {\n      refreshSrVolumeInfo(volumeInfoBySr[sr])\n    }\n  }\n})()\n\nexport const subscribeSrsUnhealthyVdiChainsLength = createSubscription(() => {\n  const _isAdmin = isAdmin(store.getState())\n\n  return _isAdmin ? _call('sr.getAllUnhealthyVdiChainsLength') : undefined\n})\n\nconst unhealthyVdiChainsLengthSubscriptionsBySr = {}\nexport const createSrUnhealthyVdiChainsLengthSubscription = sr => {\n  sr = resolveId(sr)\n  let subscription = unhealthyVdiChainsLengthSubscriptionsBySr[sr]\n  if (subscription === undefined) {\n    subscription = createSubscription(() => _call('sr.getVdiChainsInfo', { sr }))\n    unhealthyVdiChainsLengthSubscriptionsBySr[sr] = subscription\n  }\n  return subscription\n}\n\nexport const subscribeUserAuthTokens = createSubscription(() => _call('user.getAuthenticationTokens'))\n\nexport const subscribeXoTasks = (() => {\n  let abortController\n  const cache = new Map()\n  const subscribers = new Set()\n\n  const basePath =\n    './rest/v0/tasks?fields=abortionRequestedAt,end,id,name,objectId,properties,start,status,updatedAt,href'\n\n  const clearCacheDelay = 6e5 // 10m\n  let clearCacheTimeout\n  function clearCache() {\n    cache.clear()\n  }\n\n  const notify = throttle(function () {\n    // The object needs to be different each time to ensure components refresh\n    const data = Array.from(cache.values())\n\n    for (const subscriber of subscribers) {\n      subscriber(data)\n    }\n  }, 100)\n\n  async function run() {\n    if (abortController !== undefined) {\n      return\n    }\n    abortController = new AbortController()\n    clearTimeout(clearCacheTimeout)\n\n    while (true) {\n      try {\n        // starts watching collection\n        const resWatch = await fetch(basePath + '&ndjson&watch', { signal: abortController.signal })\n\n        // fetches existing objects\n        const response = await fetch(basePath, { signal: abortController.signal })\n        const objects = await response.json()\n        cache.clear()\n        for (const object of objects) {\n          cache.set(object.id, object)\n        }\n        notify()\n\n        // handles events\n        let buf = ''\n        for await (const chunk of resWatch.body) {\n          buf += String.fromCharCode(...chunk)\n\n          let i\n          while ((i = buf.indexOf('\\n')) !== -1) {\n            const line = buf.slice(0, i)\n            buf = buf.slice(i + 1)\n            const [event, object] = JSON.parse(line)\n            if (event === 'remove') {\n              cache.delete(object.id)\n            } else {\n              cache.set(object.id, object)\n            }\n          }\n          notify()\n        }\n      } catch (error) {\n        if (error === 'abort') {\n          break\n        }\n\n        console.error('monitor XO tasks', error)\n      }\n\n      await new Promise(resolve => setTimeout(resolve, 10e3))\n    }\n\n    abortController = undefined\n    clearCacheTimeout = setTimeout(clearCache, clearCacheDelay)\n  }\n\n  return function subscribeXoTasks(cb) {\n    subscribers.add(cb)\n\n    asap(() => cb(cache))\n\n    run()\n\n    return once(function unsubscribeXoTasks() {\n      subscribers.delete(cb)\n      if (subscribers.size === 0) {\n        abortController.abort('abort')\n      }\n    })\n  }\n})()\n\nexport const subscribeCloudXoConfigBackups = createSubscription(\n  () => fetch('./rest/v0/cloud/xo-config/backups?fields=xoaId,createdAt,id,content_href').then(resp => resp.json()),\n  { polling: 6e4 }\n)\n\nexport const subscribeCloudXoConfig = createSubscription(() =>\n  fetch('./rest/v0/cloud/xo-config').then(resp => resp.json())\n)\n\nconst subscribeSrsXostorHealthCheck = {}\nexport const subscribeXostorHealthCheck = sr => {\n  const srId = resolveId(sr)\n\n  if (subscribeSrsXostorHealthCheck[srId] === undefined) {\n    subscribeSrsXostorHealthCheck[srId] = createSubscription(() => _call('xostor.healthCheck', { sr: srId }), {\n      polling: 6e4, // To avoid spamming the linstor controller\n    })\n  }\n\n  return subscribeSrsXostorHealthCheck[srId]\n}\n\nconst subscribeSrsXostorInterfaces = {}\nexport const subscribeXostorInterfaces = sr => {\n  const srId = resolveId(sr)\n\n  if (subscribeSrsXostorInterfaces[srId] === undefined) {\n    subscribeSrsXostorInterfaces[srId] = createSubscription(() => _call('xostor.getInterfaces', { sr: srId }), {\n      polling: 6e4, // To avoid spamming the linstor controller\n    })\n  }\n\n  return subscribeSrsXostorInterfaces[srId]\n}\nsubscribeXostorInterfaces.forceRefresh = sr => {\n  if (sr === undefined) {\n    forEach(subscribeSrsXostorInterfaces, subscription => subscription.forceRefresh())\n    return\n  }\n\n  const subscription = subscribeSrsXostorInterfaces[resolveId(sr)]\n  subscription?.forceRefresh()\n}\n\nconst subscribeHostsIpmiSensors = {}\nexport const subscribeIpmiSensors = host => {\n  const _isAdmin = isAdmin(store.getState())\n  const hostId = resolveId(host)\n\n  if (subscribeHostsIpmiSensors[hostId] === undefined) {\n    subscribeHostsIpmiSensors[hostId] = createSubscription(\n      async () =>\n        _isAdmin\n          ? await _call('host.getIpmiSensors', {\n              id: hostId,\n            })\n          : undefined,\n      {\n        polling: 6e4,\n      }\n    )\n  }\n\n  return subscribeHostsIpmiSensors[hostId]\n}\n\nconst subscribeHostsMdadmHealth = {}\nexport const subscribeMdadmHealth = host => {\n  const hostId = resolveId(host)\n\n  if (subscribeHostsMdadmHealth[hostId] === undefined) {\n    subscribeHostsMdadmHealth[hostId] = createSubscription(() => _call('host.getMdadmHealth', { id: hostId }))\n  }\n\n  return subscribeHostsMdadmHealth[hostId]\n}\n\nexport const getHostBiosInfo = host => _call('host.getBiosInfo', { id: resolveId(host) })\n\nconst subscribeVmSecurebootReadiness = {}\nexport const subscribeSecurebootReadiness = id => {\n  const vmId = resolveId(id)\n\n  if (subscribeVmSecurebootReadiness[vmId] === undefined) {\n    let forceRefresh = false\n    const subscription = createSubscription(\n      async () => {\n        try {\n          return await _call('vm.getSecurebootReadiness', { id: vmId, forceRefresh })\n        } catch (error) {\n          if (error.data?.code !== 'MESSAGE_METHOD_UNKNOWN') {\n            throw error\n          }\n        }\n      },\n      {\n        polling: 3e4,\n      }\n    )\n    const forceRefreshFn = subscription.forceRefresh\n    subscription.forceRefresh = async () => {\n      const _forceRefresh = forceRefresh\n      forceRefresh = true\n      await forceRefreshFn()\n      forceRefresh = _forceRefresh\n    }\n    subscribeVmSecurebootReadiness[vmId] = subscription\n  }\n  return subscribeVmSecurebootReadiness[vmId]\n}\nsubscribeSecurebootReadiness.forceRefresh = vm => {\n  if (vm === undefined) {\n    forEach(subscribeVmSecurebootReadiness, subscription => subscription.forceRefresh())\n    return\n  }\n\n  const subscription = subscribeVmSecurebootReadiness[resolveId(vm)]\n  subscription?.forceRefresh()\n}\n\nconst subscribePoolGuestSecurebootReadiness = {}\nexport const subscribeGetGuestSecurebootReadiness = pool => {\n  const poolId = resolveId(pool)\n\n  if (subscribePoolGuestSecurebootReadiness[poolId] === undefined) {\n    subscribePoolGuestSecurebootReadiness[poolId] = createSubscription(\n      async () => {\n        try {\n          return await _call('pool.getGuestSecureBootReadiness', { id: poolId })\n        } catch (error) {\n          if (error.data?.code !== 'MESSAGE_METHOD_UNKNOWN') {\n            throw error\n          }\n        }\n      },\n      {\n        polling: 3e4,\n      }\n    )\n  }\n\n  return subscribePoolGuestSecurebootReadiness[poolId]\n}\nsubscribePoolGuestSecurebootReadiness.forceRefresh = pool => {\n  if (pool === undefined) {\n    forEach(subscribePoolGuestSecurebootReadiness, subscription => subscription.forceRefresh())\n    return\n  }\n\n  const subscription = subscribePoolGuestSecurebootReadiness[resolveId(pool)]\n  subscription?.forceRefresh()\n}\n\n// System ============================================================\n\nexport const apiMethods = _call('system.getMethodsInfo')\n\nexport const serverVersion = _call('system.getServerVersion')\n\nexport const getXoServerTimezone = _call('system.getServerTimezone')\n\n// XO --------------------------------------------------------------------------\n\nimport ImportConfigModal from './import-config-modal' // eslint-disable-line import/first\nexport const importConfig = config =>\n  confirm({\n    title: _('importConfig'),\n    body: <ImportConfigModal />,\n    icon: 'import',\n  }).then(\n    passphrase =>\n      _call('xo.importConfig', { passphrase }).then(({ $sendTo }) =>\n        post($sendTo, config).then(response => {\n          if (response.status !== 200) {\n            throw new Error('config import failed')\n          }\n        })\n      ),\n    () => false\n  )\n\nimport ExportConfigModal from './export-config-modal' // eslint-disable-line import/first\nexport const exportConfig = () =>\n  confirm({\n    title: _('exportConfig'),\n    body: <ExportConfigModal />,\n    icon: 'export',\n  }).then(\n    passphrase =>\n      _call('xo.exportConfig', { passphrase }).then(({ $getFrom: url }) => {\n        window.open(`.${url}`)\n      }),\n    noop\n  )\n\n// Server ------------------------------------------------------------\n\nexport const addServer = (host, username, password, label, allowUnauthorized, httpProxy) =>\n  _call('server.add', {\n    allowUnauthorized,\n    host,\n    httpProxy: httpProxy || undefined,\n    label: label || undefined,\n    password,\n    username,\n  })::tap(subscribeServers.forceRefresh, () => error(_('serverError'), _('serverAddFailed')))\n\nexport const editServer = (server, props) =>\n  _call('server.set', { ...props, id: resolveId(server) })::tap(subscribeServers.forceRefresh)\n\nexport const enableServer = server =>\n  _call('server.enable', { id: resolveId(server) })\n    ::tapCatch(err => {\n      if (err.message === 'Invalid XML-RPC message') {\n        error(_('enableServerErrorTitle'), _('enableServerErrorMessage'))\n      }\n    })\n    ::pFinally(subscribeServers.forceRefresh)\n\nexport const disableServer = server =>\n  _call('server.disable', { id: resolveId(server) })::tap(subscribeServers.forceRefresh)\n\nexport const removeServer = server =>\n  _call('server.remove', { id: resolveId(server) })::tap(subscribeServers.forceRefresh)\n\n// Pool --------------------------------------------------------------\n\nexport const editPool = (pool, props) => _call('pool.set', { id: resolveId(pool), ...props })\n\nexport const getPatchesDifference = (source, target) =>\n  _call('pool.getPatchesDifference', {\n    source: resolveId(source),\n    target: resolveId(target),\n  })\n\nexport const addHostToPool = (pool, host) => {\n  if (host) {\n    return confirm({\n      icon: 'add',\n      title: _('addHostModalTitle'),\n      body: _('addHostModalMessage', {\n        pool: pool.name_label,\n        host: host.name_label,\n      }),\n    }).then(() =>\n      _call('pool.mergeInto', {\n        source: host.$pool,\n        target: pool.id,\n        force: true,\n      })\n    )\n  }\n}\n\nimport AddHostsModalBody from './add-hosts-modal' // eslint-disable-line import/first\nexport const addHostsToPool = pool =>\n  confirm({\n    icon: 'add',\n    title: _('addHostsLabel'),\n    body: <AddHostsModalBody pool={pool} />,\n  }).then(params => {\n    const { hosts } = params\n    if (isEmpty(hosts)) {\n      error(_('addHostNoHost'), _('addHostNoHostMessage'))\n      return\n    }\n\n    return _call('pool.mergeInto', {\n      sources: map(hosts, '$pool'),\n      target: pool.id,\n      force: true,\n    }).catch(error => {\n      if (error.code !== 'HOSTS_NOT_HOMOGENEOUS') {\n        throw error\n      }\n\n      error(_('addHostsErrorTitle', { nHosts: hosts.length }), _('addHostNotHomogeneousErrorMessage'))\n    })\n  })\n\nexport const enableAdvancedLiveTelemetry = async host => {\n  const isConfiguredToReceiveStreaming = await _call('netdata.isConfiguredToReceiveStreaming', { host: host.id })\n  if (!isConfiguredToReceiveStreaming) {\n    await _call('netdata.configureXoaToReceiveData')\n  }\n  await _call('netdata.configureHostToStreamHere', {\n    host: host.id,\n  })\n  success(_('advancedLiveTelemetry'), _('enableAdvancedLiveTelemetrySuccess'))\n}\n\nexport const isNetDataInstalledOnHost = async host => {\n  const isNetDataInstalledOnHost = await _call('netdata.isNetDataInstalledOnHost', { host: host.id })\n  if (!isNetDataInstalledOnHost) {\n    return false\n  }\n  const [hostApiKey, localApiKey] = await Promise.all([\n    _call('netdata.getHostApiKey', {\n      host: host.id,\n    }),\n    _call('netdata.getLocalApiKey'),\n  ])\n  return hostApiKey === localApiKey\n}\n\nexport const detachHost = host =>\n  confirm({\n    icon: 'host-eject',\n    title: _('detachHostModalTitle'),\n    body: _('detachHostModalMessage', {\n      host: <strong>{host.name_label}</strong>,\n    }),\n  }).then(() => _call('host.detach', { host: host.id }))\n\nexport const disableHost = host =>\n  confirm({\n    icon: 'host-disable',\n    title: _('disableHost'),\n    body: _('disableHostModalMessage', {\n      host: <strong>{host.name_label}</strong>,\n    }),\n  }).then(() => _call('host.disable', { host: resolveId(host) }))\n\nexport const forgetHost = host =>\n  confirm({\n    icon: 'host-forget',\n    title: _('forgetHostModalTitle'),\n    body: _('forgetHostModalMessage', {\n      host: <strong>{host.name_label}</strong>,\n    }),\n  }).then(() => _call('host.forget', { host: resolveId(host) }))\n\nexport const enableHost = host => _call('host.enable', { host: resolveId(host) })\n\nexport const enableHa = ({ pool, heartbeatSrs, configuration }) =>\n  _call('pool.enableHa', { pool: resolveId(pool), heartbeatSrs, configuration })\n\nexport const disableHa = pool => _call('pool.disableHa', { pool: resolveId(pool) })\n\nexport const setDefaultSr = sr => _call('pool.setDefaultSr', { sr: resolveId(sr) })\n\nexport const setPoolMaster = host =>\n  confirm({\n    title: _('setPoolMasterModalTitle'),\n    body: _('setPoolMasterModalMessage', {\n      host: <strong>{host.name_label}</strong>,\n    }),\n  }).then(() => _call('pool.setPoolMaster', { host: resolveId(host) }), noop)\n\nexport const rollingPoolReboot = async pool => {\n  const poolId = resolveId(pool)\n  await confirm({\n    body: <RollingPoolRebootModal pool={poolId} />,\n    title: _('rollingPoolReboot'),\n    icon: 'pool-rolling-reboot',\n  })\n  try {\n    return await _call('pool.rollingReboot', { pool: poolId })\n  } catch (error) {\n    if (!forbiddenOperation.is(error)) {\n      throw error\n    }\n    await confirm({\n      body: (\n        <p className='text-warning'>\n          <Icon icon='alarm' /> {_('bypassBackupPoolModalMessage')}\n        </p>\n      ),\n      title: _('rollingPoolReboot'),\n      icon: 'pool-rolling-reboot',\n    })\n    return _call('pool.rollingReboot', { pool: poolId, bypassBackupCheck: true })\n  }\n}\n\nexport const getPoolGuestSecureBootReadiness = async poolId => {\n  try {\n    return await _call('pool.getGuestSecureBootReadiness', { id: poolId })\n  } catch (error) {\n    if (error.data?.code !== 'MESSAGE_METHOD_UNKNOWN') {\n      throw error\n    }\n  }\n}\n// Host --------------------------------------------------------------\n\nexport const setSchedulerGranularity = (host, schedulerGranularity) =>\n  _call('host.setSchedulerGranularity', {\n    host,\n    schedulerGranularity,\n  })::tap(() => subscribeSchedulerGranularity.forceRefresh(host))\n\nexport const editHost = (host, props) => _call('host.set', { ...props, id: resolveId(host) })\n\nimport MultipathingModalBody from './multipathing-modal' // eslint-disable-line import/first\nexport const setHostsMultipathing = ({ host, hosts = [host], multipathing }) => {\n  const ids = resolveIds(hosts)\n  return confirm({\n    title: _(multipathing ? 'enableMultipathing' : 'disableMultipathing'),\n    body: <MultipathingModalBody hostIds={ids} />,\n  }).then(() => Promise.all(map(ids, id => editHost(id, { multipathing }))), noop)\n}\n\nexport const fetchHostStats = (host, granularity) => _call('host.stats', { host: resolveId(host), granularity })\n\nexport const setRemoteSyslogHost = (host, syslogDestination) =>\n  _call('host.setRemoteSyslogHost', {\n    id: resolveId(host),\n    syslogDestination,\n  })\n\nexport const setRemoteSyslogHosts = (hosts, syslogDestination) =>\n  Promise.all(map(hosts, host => setRemoteSyslogHost(host, syslogDestination)))\n\nexport const restartHost = async (\n  host,\n  force = false,\n  suspendResidentVms = false,\n  bypassBlockedSuspend = false,\n  bypassCurrentVmCheck = false\n) => {\n  await confirm({\n    title: _('restartHostModalTitle'),\n    body: _('restartHostModalMessage'),\n  })\n  return _restartHost({ host, force, suspendResidentVms, bypassBlockedSuspend, bypassCurrentVmCheck })\n}\n\nconst _restartHost = async ({ host, ...opts }) => {\n  opts = { ...opts, id: resolveId(host) }\n\n  try {\n    await _call('host.restart', opts)\n  } catch (error) {\n    if (cantSuspend(error)) {\n      await confirm({\n        body: (\n          <p>\n            <Icon icon='alarm' /> {_('forceSmartRebootHost', { nVms: error.data.actual.length })}\n          </p>\n        ),\n        title: _('restartHostModalTitle'),\n      })\n      return _restartHost({ ...opts, host, bypassBlockedSuspend: true })\n    }\n\n    if (xoaOnHost(error)) {\n      await confirm({\n        body: (\n          <p>\n            <Icon icon='alarm' /> {_('smartRebootBypassCurrentVmCheck')}\n          </p>\n        ),\n        title: _('restartHostModalTitle'),\n      })\n      return _restartHost({ ...opts, host, bypassCurrentVmCheck: true })\n    }\n\n    if (backupIsRunning(error, host.$poolId)) {\n      await confirm({\n        body: (\n          <p className='text-warning'>\n            <Icon icon='alarm' /> {_('bypassBackupHostModalMessage')}\n          </p>\n        ),\n        title: _('restartHostModalTitle'),\n      })\n      return _restartHost({ ...opts, host, bypassBackupCheck: true })\n    }\n\n    if (masterNeedsUpdate(error)) {\n      const state = store.getState()\n      const master = getObject(state, getObject(state, host.$pool).master)\n      await chooseAction({\n        body: (\n          <p>\n            <Icon icon='alarm' />{' '}\n            {_('slaveHostMoreUpToDateThanMasterAfterRestart', { master: <Host id={master.id} link /> })}\n          </p>\n        ),\n        buttons: [{ label: _('restartAnyway'), btnStyle: 'danger' }],\n        icon: 'alarm',\n        title: _('restartHostModalTitle'),\n      })\n\n      return _restartHost({ ...opts, host, bypassVersionCheck: true })\n    }\n\n    if (noHostsAvailableErrCheck(error)) {\n      alert(_('noHostsAvailableErrorTitle'), _('noHostsAvailableErrorMessage'))\n    }\n    throw error\n  }\n}\n\n// ---- Restart Host errors\nconst cantSuspend = err =>\n  err !== undefined &&\n  incorrectState.is(err, {\n    object: 'suspendBlocked',\n  })\nconst xoaOnHost = err =>\n  err !== undefined &&\n  operationFailed.is(err, {\n    code: 'xoaOnHost',\n  })\nconst backupIsRunning = (err, poolId) =>\n  err !== undefined &&\n  (forbiddenOperation.is(err, {\n    reason: `A backup may run on the pool: ${poolId}`,\n  }) ||\n    forbiddenOperation.is(err, {\n      reason: `A backup is running on the pool: ${poolId}`,\n    }))\nconst masterNeedsUpdate = err =>\n  err !== undefined &&\n  incorrectState.is(err, {\n    property: 'rebootRequired',\n  })\n\nconst noHostsAvailableErrCheck = err => err !== undefined && noHostsAvailable.is(err)\n\nexport const restartHosts = (hosts, force = false) => {\n  const nHosts = size(hosts)\n  return confirm({\n    title: _('restartHostsModalTitle', { nHosts }),\n    body: _('restartHostsModalMessage', { nHosts }),\n  }).then(\n    () =>\n      Promise.all(map(hosts, host => _call('host.restart', { id: resolveId(host), force })::reflect())).then(\n        results => {\n          const nbErrors = filter(results, result => !result.isFulfilled()).length\n          if (nbErrors) {\n            return alert(\n              _('failHostBulkRestartTitle'),\n              _('failHostBulkRestartMessage', {\n                failedHosts: nbErrors,\n                totalHosts: results.length,\n              })\n            )\n          }\n        }\n      ),\n    noop\n  )\n}\n\nexport const restartHostAgent = async host => {\n  try {\n    await _call('host.restart_agent', { id: resolveId(host) })\n  } catch (error) {\n    if (forbiddenOperation.is(error)) {\n      await confirm({\n        body: (\n          <p className='text-warning'>\n            <Icon icon='alarm' /> {_('bypassBackupHostModalMessage')}\n          </p>\n        ),\n        title: _('restartHostAgent'),\n      })\n      return _call('host.restart_agent', { id: resolveId(host), bypassBackupCheck: true })\n    }\n    throw error\n  }\n}\n\nexport const restartHostsAgents = hosts => {\n  const nHosts = size(hosts)\n  return confirm({\n    title: _('restartHostsAgentsModalTitle', { nHosts }),\n    body: _('restartHostsAgentsModalMessage', { nHosts }),\n  }).then(() => Promise.all(map(hosts, restartHostAgent)), noop)\n}\n\nexport const startHost = host => _call('host.start', { id: resolveId(host) })\n\nexport const stopHost = async host => {\n  await confirm({\n    body: _('stopHostModalMessage'),\n    title: _('stopHostModalTitle'),\n  })\n\n  let ignoreBackup = false\n  return _call('host.stop', { id: resolveId(host) })\n    .catch(async err => {\n      if (\n        forbiddenOperation.is(err, {\n          reason: `A backup may run on the pool: ${host.$poolId}`,\n        }) ||\n        forbiddenOperation.is(error, {\n          reason: `A backup is running on the pool: ${host.$poolId}`,\n        })\n      ) {\n        ignoreBackup = true\n        await confirm({\n          body: (\n            <p className='text-warning'>\n              <Icon icon='alarm' /> {_('bypassBackupHostModalMessage')}\n            </p>\n          ),\n          title: _('stopHostModalTitle'),\n        })\n        return _call('host.stop', { id: resolveId(host), bypassBackupCheck: ignoreBackup })\n      }\n      throw err\n    })\n    .catch(async err => {\n      if (noHostsAvailable.is(err)) {\n        await confirm({\n          body: _('forceStopHostMessage'),\n          title: _('forceStopHost'),\n        })\n        // Retry with bypassEvacuate.\n        return _call('host.stop', { id: resolveId(host), bypassEvacuate: true, bypassBackupCheck: ignoreBackup })\n      }\n      throw err\n    })\n}\n\nexport const stopHosts = hosts => {\n  const nHosts = size(hosts)\n  return confirm({\n    title: _('stopHostsModalTitle', { nHosts }),\n    body: _('stopHostsModalMessage', { nHosts }),\n  }).then(() => Promise.all(map(hosts, host => _call('host.stop', { id: resolveId(host) }))), noop)\n}\n\nexport const toggleMaintenanceMode = async host => {\n  if (host.enabled) {\n    try {\n      await confirm({\n        title: _('maintenanceHostModalTitle'),\n        body: _('maintenanceHostModalMessage'),\n      })\n    } catch (error) {\n      return\n    }\n  }\n  return _call('host.setMaintenanceMode', { id: resolveId(host), maintenance: host.enabled }).catch(async err => {\n    if (err.message === 'operation blocked') {\n      const residentVmsIds = host.residentVms\n      const vms = residentVmsIds\n        .map(vmId => getObject(store.getState(), vmId))\n        .filter(\n          vm =>\n            vm.type === 'VM' &&\n            vm.power_state === 'Running' &&\n            vm?.blockedOperations &&\n            Object.keys(vm.blockedOperations).length > 0\n        )\n      const vmsToForceMigrate = vms.map(vm => vm.id)\n      confirm({\n        title: _('bypassBlockedMigrationsModalTitle'),\n        body: _('bypassBlockedMigrationsModalMessage', {\n          vms: (\n            <ul>\n              {vms.map(vm => (\n                <li key={vm.id}>\n                  <Vm id={vm.id} />\n                </li>\n              ))}\n            </ul>\n          ),\n        }),\n      }).then(() =>\n        _call('host.setMaintenanceMode', { id: resolveId(host), maintenance: host.enabled, vmsToForceMigrate })\n      )\n      return\n    }\n    throw err\n  })\n}\n\nexport const getHostMissingPatches = async host => {\n  const hostId = resolveId(host)\n  host = getObject(store.getState(), hostId)\n\n  if (host.power_state !== 'Running') {\n    return []\n  }\n  if (host.productBrand !== 'XCP-ng') {\n    const patches = await _call('pool.listMissingPatches', { host: hostId })\n    // Hide paid patches to XS-free users\n    return host.license_params.sku_type !== 'free' ? patches : filter(patches, { paid: false })\n  }\n  try {\n    return await _call('pool.listMissingPatches', { host: hostId })\n  } catch (_) {\n    return null\n  }\n}\n\nexport const emergencyShutdownHost = host =>\n  confirm({\n    title: _('emergencyShutdownHostModalTitle'),\n    body: _('emergencyShutdownHostModalMessage', {\n      host: <strong>{host.name_label}</strong>,\n    }),\n  }).then(() => _call('host.emergencyShutdownHost', { host: resolveId(host) }))\n\nexport const emergencyShutdownHosts = hosts => {\n  const nHosts = size(hosts)\n  return confirm({\n    title: _('emergencyShutdownHostsModalTitle', { nHosts }),\n    body: _('emergencyShutdownHostsModalMessage', { nHosts }),\n  }).then(() => Promise.all(map(hosts, host => emergencyShutdownHost(host))), noop)\n}\n\nexport const isHostTimeConsistentWithXoaTime = host => {\n  if (host.power_state !== 'Running') {\n    return true\n  }\n  return _call('host.isHostServerTimeConsistent', { host: resolveId(host) })\n}\n\nexport const isHyperThreadingEnabledHost = host =>\n  _call('host.isHyperThreadingEnabled', {\n    id: resolveId(host),\n  })\n\nexport const getSmartctlHealth = host => _call('host.getSmartctlHealth', { id: resolveId(host) })\n\nexport const getSmartctlInformation = (host, deviceNames) =>\n  _call('host.getSmartctlInformation', { id: resolveId(host), deviceNames })\n\nexport const installCertificateOnHost = (id, props) => _call('host.installCertificate', { id, ...props })\n\nexport const setControlDomainMemory = (id, memory) => _call('host.setControlDomainMemory', { id, memory })\n\nexport const isPubKeyTooShort = host => {\n  // this check is only relevant for old hosts, and cannot be done on offline hosts\n  if (host.productBrand !== 'XCP-ng' || semver.satisfies(host.version, '>=8.3.0') || host.power_state === 'Halted') {\n    return Promise.resolve(false)\n  }\n  return _call('host.isPubKeyTooShort', { id: host.id })\n}\n\n// for XCP-ng now\nexport const installAllPatchesOnHost = ({ host }) =>\n  confirm({\n    body: _('installAllPatchesOnHostContent'),\n    title: _('installAllPatchesTitle'),\n  }).then(() =>\n    _call('pool.installPatches', { hosts: [resolveId(host)] })::tap(() =>\n      subscribeHostMissingPatches.forceRefresh(host)\n    )\n  )\n\nexport const installPatches = (patches, pool) =>\n  confirm({\n    body: _('installPatchesContent', { nPatches: patches.length }),\n    title: _('installPatchesTitle', { nPatches: patches.length }),\n  }).then(() =>\n    _call('pool.installPatches', {\n      pool: resolveId(pool),\n      patches: resolveIds(patches),\n    })::tap(() => subscribeHostMissingPatches.forceRefresh())\n  )\n\nimport InstallPoolPatchesModalBody from './install-pool-patches-modal' // eslint-disable-line import/first\nexport const installAllPatchesOnPool = ({ pool }) => {\n  const poolId = resolveId(pool)\n  return confirm({\n    body: <InstallPoolPatchesModalBody pool={poolId} />,\n    title: _('installPoolPatches'),\n    icon: 'host-patch-update',\n  }).then(\n    () => _call('pool.installPatches', { pool: poolId })::tap(() => subscribeHostMissingPatches.forceRefresh()),\n    noop\n  )\n}\n\nimport RollingPoolUpdateModal from './rolling-pool-updates-modal' // eslint-disable-line import/first\nexport const rollingPoolUpdate = async poolId => {\n  await confirm({\n    body: <RollingPoolUpdateModal pool={poolId} />,\n    title: _('rollingPoolUpdate'),\n    icon: 'pool-rolling-update',\n  })\n\n  const rpu = async ({ bypassBackupCheck = false, rebootVm = false } = {}) => {\n    try {\n      await _call('pool.rollingUpdate', { pool: poolId, bypassBackupCheck, rebootVm })\n      subscribeHostMissingPatches.forceRefresh()\n    } catch (err) {\n      if (forbiddenOperation.is(err)) {\n        await confirm({\n          body: (\n            <p className='text-warning'>\n              <Icon icon='alarm' /> {_('bypassBackupPoolModalMessage')}\n            </p>\n          ),\n          title: _('rollingPoolUpdate'),\n          icon: 'pool-rolling-update',\n        })\n        await rpu({ bypassBackupCheck: true, rebootVm })\n      }\n      if (incorrectState.is(err, { property: 'guidance' })) {\n        await confirm({\n          body: (\n            <p className='text-warning'>\n              <Icon icon='alarm' /> {_('rpuRequireVmsReboot')}\n            </p>\n          ),\n          title: _('rollingPoolUpdate'),\n          icon: 'pool-rolling-update',\n        })\n        await rpu({ bypassBackupCheck, rebootVm: true })\n      }\n    }\n  }\n\n  await rpu()\n}\n\nexport const installSupplementalPack = (host, file) => {\n  info(_('supplementalPackInstallStartedTitle'), _('supplementalPackInstallStartedMessage'))\n\n  return _call('host.installSupplementalPack', { host: resolveId(host) }).then(({ $sendTo }) =>\n    post($sendTo, file)\n      .then(res => {\n        if (res.status !== 200) {\n          throw new Error('installing supplemental pack failed')\n        }\n\n        success(_('supplementalPackInstallSuccessTitle'), _('supplementalPackInstallSuccessMessage'))\n      })\n      .catch(err => {\n        error(_('supplementalPackInstallErrorTitle'), _('supplementalPackInstallErrorMessage'))\n        throw err\n      })\n  )\n}\n\nexport const installSupplementalPackOnAllHosts = (pool, file) => {\n  info(_('supplementalPackInstallStartedTitle'), _('supplementalPackInstallStartedMessage'))\n\n  return _call('pool.installSupplementalPack', { pool: resolveId(pool) }).then(({ $sendTo }) =>\n    post($sendTo, file)\n      .then(res => {\n        if (res.status !== 200) {\n          throw new Error('installing supplemental pack failed')\n        }\n\n        success(_('supplementalPackInstallSuccessTitle'), _('supplementalPackInstallSuccessMessage'))\n      })\n      .catch(err => {\n        error(_('supplementalPackInstallErrorTitle'), _('supplementalPackInstallErrorMessage'))\n        throw err\n      })\n  )\n}\n\nexport const hidePcis = async (pcis, hide) => {\n  await confirm({\n    body: _('applyChangeOnPcis', { nPcis: pcis.length }),\n    // hide `true` means that we will disable dom0's PCI access, so we will \"enable\" the possibility of passthrough this PCI\n    title: _(hide ? 'pcisEnable' : 'pcisDisable', { nPcis: pcis.length }),\n  })\n  try {\n    await _call('pci.disableDom0Access', { pcis: resolveIds(pcis), disable: hide })\n  } catch (error) {\n    if (!noHostsAvailable.is(error)) {\n      throw error\n    }\n    try {\n      await confirm({\n        body: _('hostEvacuationFailed'),\n        title: _('confirmForceRebootHost'),\n      })\n    } catch (_) {\n      throw error // throw original error if user doesn't want to force\n    }\n    await _call('pci.disableDom0Access', { pcis: resolveIds(pcis), disable: hide, forceReboot: true })\n  }\n}\n\nexport const isPciHidden = async pci => (await _call('pci.getDom0AccessStatus', { id: resolveId(pci) })) === 'disabled'\n\n//  ATM, unknown date for the availability on XS, since they are doing rolling release\n// FIXME: When XS release methods to do PCI passthrough, update this check\nexport const isPciPassthroughAvailable = host =>\n  host.productBrand === 'XCP-ng' && semver.satisfies(host.version, '>=8.3.0')\n\nexport const vmAttachPcis = (vm, pcis) => _call('vm.attachPcis', { id: resolveId(vm), pcis: resolveIds(pcis) })\n\nexport const vmDetachPcis = (vm, pciIds) => _call('vm.detachPcis', { id: resolveId(vm), pciIds })\n\nexport const vmSetUefiMode = (vm, mode) =>\n  _call('vm.set', { id: resolveId(vm), uefiMode: mode })::tap(() => subscribeSecurebootReadiness.forceRefresh(vm))\n\n// Containers --------------------------------------------------------\n\nexport const pauseContainer = (vm, container) => _call('docker.pause', { vm: resolveId(vm), container })\n\nexport const restartContainer = (vm, container) => _call('docker.restart', { vm: resolveId(vm), container })\n\nexport const startContainer = (vm, container) => _call('docker.start', { vm: resolveId(vm), container })\n\nexport const stopContainer = (vm, container) => _call('docker.stop', { vm: resolveId(vm), container })\n\nexport const unpauseContainer = (vm, container) => _call('docker.unpause', { vm: resolveId(vm), container })\n\n// VM ----------------------------------------------------------------\n\nconst chooseActionToUnblockForbiddenStartVm = props =>\n  chooseAction({\n    icon: 'alarm',\n    buttons: [\n      { label: _('cloneAndStartVM'), value: 'clone', btnStyle: 'success' },\n      { label: _('forceStartVm'), value: 'force', btnStyle: 'danger' },\n    ],\n    ...props,\n  })\n\nconst cloneAndStartVm = async (vm, host) => _call('vm.start', { id: await cloneVm(vm), host: resolveId(host) })\n\nconst _startVm = (id, host, { force = false, bypassMacAddressesCheck = force } = {}) =>\n  _call('vm.start', { id, bypassMacAddressesCheck, force, host })\n\nexport const startVm = async (vm, host) => {\n  if (host === true) {\n    host = await confirm({\n      icon: 'vm-start',\n      title: _('startVmOnLabel'),\n      body: <SelectHost predicate={({ $pool }) => $pool === vm.$pool} />,\n    })\n    if (host === undefined) {\n      error(_('startVmOnMissingHostTitle'), _('startVmOnMissingHostMessage'))\n      return\n    }\n  }\n\n  const id = resolveId(vm)\n  const hostId = resolveId(host)\n  return _startVm(id, hostId).catch(async reason => {\n    const isDuplicatedMacAddressError = reason.data !== undefined && reason.data.code === 'DUPLICATED_MAC_ADDRESS'\n    if (!isDuplicatedMacAddressError && !forbiddenOperation.is(reason)) {\n      throw reason\n    }\n\n    if (isDuplicatedMacAddressError) {\n      // Retry without checking MAC addresses\n      await confirm({\n        title: _('forceStartVm'),\n        body: _('vmWithDuplicatedMacAddressesMessage'),\n      })\n      try {\n        await _startVm(id, hostId, { bypassMacAddressesCheck: true })\n      } catch (error) {\n        if (!forbiddenOperation.is(error)) {\n          throw error\n        }\n        reason = error\n      }\n    }\n\n    if (forbiddenOperation.is(reason)) {\n      // Clone or retry with force\n      const choice = await chooseActionToUnblockForbiddenStartVm({\n        body: _('blockedStartVmModalMessage'),\n        title: _('forceStartVmModalTitle'),\n      })\n\n      if (choice === 'clone') {\n        return cloneAndStartVm(vm, host)\n      }\n\n      return _startVm(id, hostId, { force: true })\n    }\n  })\n}\n\nexport const startVms = vms =>\n  confirm({\n    title: _('startVmsModalTitle', { vms: vms.length }),\n    body: _('startVmsModalMessage', { vms: vms.length }),\n  }).then(async () => {\n    const vmsWithduplicatedMacAddresses = []\n    const forbiddenStart = []\n    let nErrors = 0\n\n    await Promise.all(\n      map(vms, id =>\n        _startVm(id).catch(reason => {\n          if (reason.data !== undefined && reason.data.code === 'DUPLICATED_MAC_ADDRESS') {\n            vmsWithduplicatedMacAddresses.push(id)\n          } else if (forbiddenOperation.is(reason)) {\n            forbiddenStart.push(id)\n          } else {\n            nErrors++\n          }\n        })\n      )\n    )\n\n    if (forbiddenStart.length === 0 && vmsWithduplicatedMacAddresses.length === 0) {\n      if (nErrors === 0) {\n        return\n      }\n\n      return error(_('failedVmsErrorTitle'), _('failedVmsErrorMessage', { nVms: nErrors }))\n    }\n\n    if (vmsWithduplicatedMacAddresses.length > 0) {\n      // Retry without checking MAC addresses\n      await confirm({\n        title: _('forceStartVm'),\n        body: _('vmsWithDuplicatedMacAddressesMessage', { nVms: vmsWithduplicatedMacAddresses.length }),\n      })\n      await Promise.all(\n        map(vmsWithduplicatedMacAddresses, id =>\n          _startVm(id, undefined, { bypassMacAddressesCheck: true }).catch(reason => {\n            if (forbiddenOperation.is(reason)) {\n              forbiddenStart.push(id)\n            } else {\n              nErrors++\n            }\n          })\n        )\n      )\n    }\n\n    if (forbiddenStart.length > 0) {\n      // Clone or retry with force\n      const choice = await chooseActionToUnblockForbiddenStartVm({\n        body: _('blockedStartVmsModalMessage', { nVms: forbiddenStart.length }),\n        title: _('forceStartVmModalTitle'),\n      }).catch(noop)\n\n      if (nErrors !== 0) {\n        error(_('failedVmsErrorTitle'), _('failedVmsErrorMessage', { nVms: nErrors }))\n      }\n\n      if (choice === 'clone') {\n        return Promise.all(map(forbiddenStart, async id => cloneAndStartVm(getObject(store.getState(), id))))\n      }\n\n      if (choice === 'force') {\n        return Promise.all(map(forbiddenStart, id => _startVm(id, undefined, { force: true })))\n      }\n    }\n  }, noop)\n\nexport const stopVm = (vm, hardShutdown = false) => stopOrRestartVm(vm, 'stop', hardShutdown)\n\nexport const stopVms = (vms, force = false) =>\n  confirm({\n    title: _('stopVmsModalTitle', { vms: vms.length }),\n    body: _('stopVmsModalMessage', { vms: vms.length }),\n  }).then(() => Promise.all(map(vms, vm => _call('vm.stop', { id: resolveId(vm), force }))), noop)\n\nexport const suspendVm = vm => _call('vm.suspend', { id: resolveId(vm) })\n\nexport const suspendVms = vms =>\n  confirm({\n    title: _('suspendVmsModalTitle', { vms: vms.length }),\n    body: _('suspendVmsModalMessage', { vms: vms.length }),\n  }).then(() => Promise.all(map(vms, vm => _call('vm.suspend', { id: resolveId(vm) }))), noop)\n\nexport const pauseVm = vm => _call('vm.pause', { id: resolveId(vm) })\n\nexport const pauseVms = vms =>\n  confirm({\n    title: _('pauseVmsModalTitle', { vms: vms.length }),\n    body: _('pauseVmsModalMessage', { vms: vms.length }),\n  }).then(() => Promise.all(map(vms, pauseVm)), noop)\n\nexport const recoveryStartVm = vm => _call('vm.recoveryStart', { id: resolveId(vm) })\n\nconst stopOrRestartVm = async (vm, method, force = false) => {\n  let bypassBlockedOperation = false\n  const id = resolveId(vm)\n\n  if (method !== 'stop' && method !== 'restart') {\n    throw new Error(`invalid ${method}`)\n  }\n  const isStopOperation = method === 'stop'\n\n  await confirm({\n    title: _(isStopOperation ? 'stopVmModalTitle' : 'restartVmModalTitle'),\n    body: _(isStopOperation ? 'stopVmModalMessage' : 'restartVmModalMessage', { name: vm.name_label }),\n  })\n\n  return retry(() => _call(`vm.${isStopOperation ? 'stop' : 'restart'}`, { id, force, bypassBlockedOperation }), {\n    when: err => operationBlocked.is(err) || (vmLacksFeature.is(err) && !force),\n    async onRetry(err) {\n      if (operationBlocked.is(err)) {\n        await confirm({\n          title: _('blockedOperation'),\n          body: _(isStopOperation ? 'stopVmBlockedModalMessage' : 'restartVmBlockedModalMessage'),\n        })\n        bypassBlockedOperation = true\n      }\n      if (vmLacksFeature.is(err) && !force) {\n        await confirm({\n          title: _('vmHasNoTools'),\n          body: (\n            <div>\n              <p>{_('vmHasNoToolsMessage')}</p>\n              <p>\n                <strong>{_(isStopOperation ? 'confirmForceShutdown' : 'confirmForceReboot')}</strong>\n              </p>\n            </div>\n          ),\n        })\n        force = true\n      }\n    },\n    delay: 0,\n  })\n}\n\nexport const restartVm = (vm, hardRestart = false) => stopOrRestartVm(vm, 'restart', hardRestart)\n\nexport const restartVms = (vms, force = false) =>\n  confirm({\n    title: _('restartVmsModalTitle', { vms: vms.length }),\n    body: _('restartVmsModalMessage', { vms: vms.length }),\n  }).then(() => Promise.all(map(vms, vmId => _call('vm.restart', { id: resolveId(vmId), force }))), noop)\n\nexport const cloneVm = ({ id, name_label: nameLabel }, fullCopy = false, name) =>\n  _call('vm.clone', {\n    id,\n    name: name === undefined ? `${nameLabel}_clone` : name,\n    full_copy: fullCopy,\n  })::tap(subscribeResourceSets.forceRefresh)\n\nimport CopyVmsModalBody from './copy-vms-modal' // eslint-disable-line import/first\nexport const copyVms = (vms, type) => {\n  const _vms = resolveIds(vms)\n  return confirm({\n    title: type === 'VM-template' ? _('copyTemplate') : _('copyVm'),\n    body: <CopyVmsModalBody vms={_vms} type={type} />,\n  }).then(({ compress, copyMode, names, sr }) => {\n    if (copyMode === 'fastClone') {\n      return Promise.all(_vms.map((vm, index) => cloneVm({ id: vm }, false, names[index])))\n    }\n\n    if (sr !== undefined) {\n      return Promise.all(_vms.map((vm, index) => _call('vm.copy', { vm, sr, compress, name: names[index] })))\n    }\n    error(_('copyVmsNoTargetSr'), _('copyVmsNoTargetSrMessage'))\n  }, noop)\n}\n\nexport const copyVm = async vm => {\n  const result = await copyVms([vm], vm.type)\n  return getDefined(() => result[0])\n}\n\nexport const convertVmToTemplate = vm =>\n  confirm({\n    title: 'Convert to template',\n    body: (\n      <div>\n        <p>Are you sure you want to convert this VM into a template?</p>\n        <p>This operation is definitive.</p>\n      </div>\n    ),\n  }).then(() => _call('vm.convertToTemplate', { id: resolveId(vm) }), noop)\n\nexport const copyToTemplate = async vm => {\n  await confirm({\n    title: _('copyToTemplate'),\n    body: <p>{_('copyToTemplateMessage')}</p>,\n  })\n  await _call('vm.copyToTemplate', { id: resolveId(vm) })\n}\n\nexport const changeVirtualizationMode = vm =>\n  confirm({\n    title: _('vmVirtualizationModeModalTitle'),\n    body: _('vmVirtualizationModeModalBody'),\n  }).then(() =>\n    editVm(vm, {\n      virtualizationMode: vm.virtualizationMode === 'hvm' ? 'pv' : 'hvm',\n    })\n  )\n\nimport EditVmNotesModalBody from './edit-vm-notes-modal' // eslint-disable-line import/first\nexport const editVmNotes = async vm => {\n  const { notes } = await confirm({\n    icon: 'edit',\n    title: _('editVmNotes'),\n    body: <EditVmNotesModalBody vm={vm} />,\n  })\n\n  // Remove notes if `''` is passed\n  await _call('vm.set', { id: resolveId(vm), notes: notes || null })\n}\n\nexport const createKubernetesCluster = params => _call('xoa.recipe.createKubernetesCluster', params)\n\nexport const deleteTemplates = templates =>\n  confirm({\n    title: _('templateDeleteModalTitle', { templates: templates.length }),\n    body: _('templateDeleteModalBody', { templates: templates.length }),\n  }).then(async () => {\n    const defaultTemplates = []\n    const protectedTemplates = []\n    let nErrors = 0\n    const isDefaultTemplateError = error =>\n      incorrectState.is(error, {\n        expected: false,\n        property: 'isDefaultTemplate',\n      })\n\n    await Promise.all(\n      map(resolveIds(templates), id =>\n        _call('vm.delete', { id }).catch(reason => {\n          if (isDefaultTemplateError(reason)) {\n            defaultTemplates.push(id)\n          } else if (forbiddenOperation.is(reason)) {\n            protectedTemplates.push(id)\n          } else {\n            nErrors++\n          }\n        })\n      )\n    )\n\n    const nProtectedTemplates = protectedTemplates.length\n    let forceBlockedOperation = false\n\n    if (nProtectedTemplates !== 0) {\n      await confirm({\n        title: _('deleteProtectedTemplatesTitle', { nProtectedTemplates }),\n        body: _('deleteProtectedTemplatesMessage', { nProtectedTemplates }),\n      })\n      forceBlockedOperation = true\n      await Promise.all(\n        protectedTemplates.map(id =>\n          _call('vm.delete', {\n            id,\n            forceBlockedOperation,\n          }).catch(reason => {\n            if (isDefaultTemplateError(reason)) {\n              defaultTemplates.push(id)\n            } else {\n              nErrors++\n            }\n          })\n        )\n      )\n    }\n\n    const nDefaultTemplates = defaultTemplates.length\n    if (nDefaultTemplates !== 0) {\n      await confirm({\n        title: _('deleteDefaultTemplatesTitle', { nDefaultTemplates }),\n        body: _('deleteDefaultTemplatesMessage', { nDefaultTemplates }),\n      })\n      await Promise.all(\n        defaultTemplates.map(id =>\n          _call('vm.delete', {\n            id,\n            forceDeleteDefaultTemplate: true,\n            forceBlockedOperation,\n          }).catch(() => {\n            nErrors++\n          })\n        )\n      )\n    }\n\n    if (nErrors !== 0) {\n      error(\n        _('failedToDeleteTemplatesTitle', { nTemplates: nErrors }),\n        _('failedToDeleteTemplatesMessage', { nTemplates: nErrors })\n      )\n    }\n  }, noop)\n\nexport const snapshotVm = async (vm, name, saveMemory, description) => {\n  if (saveMemory) {\n    try {\n      await confirm({\n        title: _('newSnapshotWithMemory'),\n        body: _('newSnapshotWithMemoryConfirm'),\n        icon: 'memory',\n      })\n    } catch (error) {\n      return\n    }\n  }\n  return _call('vm.snapshot', {\n    id: resolveId(vm),\n    name,\n    description,\n    saveMemory,\n  })::tap(subscribeResourceSets.forceRefresh)\n}\n\nimport SnapshotVmModalBody from './snapshot-vm-modal' // eslint-disable-line import/first\nexport const snapshotVms = vms =>\n  confirm({\n    icon: 'memory',\n    title: _('snapshotVmsModalTitle', { vms: vms.length }),\n    body: <SnapshotVmModalBody vms={vms} />,\n  })\n    .then(\n      ({ names, saveMemory, descriptions }) =>\n        Promise.all(map(vms, vm => snapshotVm(vm, names[vm], saveMemory, descriptions[vm]))),\n      noop\n    )\n    .catch(e => error(_('snapshotError'), e.message))\n\nexport const deleteSnapshot = vm =>\n  confirm({\n    title: _('deleteSnapshotModalTitle'),\n    body: _('deleteSnapshotModalMessage'),\n  }).then(() => _call('vm.delete', { id: resolveId(vm) }), noop)\n\nexport const deleteSnapshots = vms =>\n  confirm({\n    title: _('deleteSnapshotsModalTitle', { nVms: vms.length }),\n    body: _('deleteSnapshotsModalMessage', { nVms: vms.length }),\n  }).then(() => Promise.all(map(vms, vm => _call('vm.delete', { id: resolveId(vm) }))), noop)\n\n// checkpoint snapshot is in a Suspended state\nexport const isCheckpointSnapshot = ({ power_state }) => power_state === 'Suspended'\n\nimport MigrateVmModalBody from './migrate-vm-modal' // eslint-disable-line import/first\nexport const migrateVm = async (vm, host) => {\n  let params\n  try {\n    params = await confirm({\n      title: _('migrateVmModalTitle'),\n      body: <MigrateVmModalBody vm={vm} host={host} />,\n    })\n  } catch (error) {\n    return\n  }\n\n  const { sr, srRequired, targetHost } = params\n\n  if (!targetHost) {\n    return error(_('migrateVmNoTargetHost'), _('migrateVmNoTargetHostMessage'))\n  }\n\n  if (srRequired && sr === undefined) {\n    return error(_('migrateVmNoSr'), _('migrateVmNoSrMessage'))\n  }\n  delete params.srRequired\n\n  try {\n    await _call('vm.migrate', { vm: vm.id, ...params })\n  } catch (error) {\n    // https://developer-docs.citrix.com/projects/citrix-hypervisor-management-api/en/latest/api-ref-autogen-errors/#vmincompatiblewiththishost\n    if (error != null && error.data !== undefined && error.data.code === 'VM_INCOMPATIBLE_WITH_THIS_HOST') {\n      // Retry with force.\n      try {\n        await confirm({\n          body: _('forceVmMigrateModalMessage'),\n          title: _('forceVmMigrateModalTitle'),\n        })\n      } catch (error) {\n        return\n      }\n      return _call('vm.migrate', { vm: vm.id, force: true, ...params })\n    }\n    throw error\n  }\n}\n\nimport MigrateVmsModalBody from './migrate-vms-modal' // eslint-disable-line import/first\nexport const migrateVms = vms =>\n  confirm({\n    title: _('migrateVmModalTitle'),\n    body: <MigrateVmsModalBody vms={resolveIds(vms)} />,\n  }).then(params => {\n    if (isEmpty(params.vms)) {\n      return\n    }\n    if (!params.targetHost) {\n      return error(_('migrateVmNoTargetHost'), _('migrateVmNoTargetHostMessage'))\n    }\n\n    if (params.srRequired && params.sr === undefined) {\n      return error(_('migrateVmNoTargetHost'), _('migrateVmNoTargetHostMessage'))\n    }\n    delete params.srRequired\n\n    const { mapVmsMapVdisSrs, mapVmsMapVifsNetworks, migrationNetwork, sr, targetHost, vms } = params\n    Promise.all(\n      map(vms, ({ id }) =>\n        _call('vm.migrate', {\n          mapVdisSrs: mapVmsMapVdisSrs[id],\n          mapVifsNetworks: mapVmsMapVifsNetworks[id],\n          migrationNetwork,\n          sr,\n          targetHost,\n          vm: id,\n        })\n      )\n    )\n  }, noop)\n\nexport const createVm = async args => {\n  try {\n    return await _call('vm.create', args)\n  } catch (err) {\n    handlePoolDoesNotSupportVtpmError(err)\n    throw err\n  }\n}\n\nexport const createVms = async (args, nameLabels, cloudConfigs) => {\n  await confirm({\n    title: _('newVmCreateVms'),\n    body: _('newVmCreateVmsConfirm', { nbVms: nameLabels.length }),\n  })\n  try {\n    return await Promise.all(\n      map(\n        nameLabels,\n        (\n          name_label, // eslint-disable-line camelcase\n          i\n        ) =>\n          _call('vm.create', {\n            ...args,\n            name_label,\n            cloudConfig: get(cloudConfigs, i),\n          })\n      )\n    )\n  } catch (error) {\n    handlePoolDoesNotSupportVtpmError(error)\n    throw error\n  }\n}\n\nexport const getCloudInitConfig = template => _call('vm.getCloudInitConfig', { template })\n\nexport const pureDeleteVm = (vm, props) =>\n  _call('vm.delete', { id: resolveId(vm), ...props })::tap(subscribeResourceSets.forceRefresh)\n\nexport const deleteVm = (vm, retryWithForce = true) =>\n  confirm({\n    title: _('deleteVmModalTitle'),\n    body: _('deleteVmModalMessage'),\n  })\n    .then(() => pureDeleteVm(vm), noop)\n    .catch(error => {\n      if (retryWithForce && forbiddenOperation.is(error)) {\n        return confirm({\n          title: _('deleteVmBlockedModalTitle'),\n          body: _('deleteVmBlockedModalMessage'),\n        }).then(() => pureDeleteVm(vm, { force: true }), noop)\n      }\n\n      throw error\n    })\n\nexport const deleteVms = async vms => {\n  if (vms.length === 1) {\n    return deleteVm(vms[0])\n  }\n  try {\n    await confirm({\n      title: _('deleteVmsModalTitle', { vms: vms.length }),\n      body: _('deleteVmsModalMessage', { vms: vms.length }),\n      strongConfirm: vms.length > 1 && {\n        messageId: 'deleteVmsConfirmText',\n        values: { nVms: vms.length },\n      },\n    })\n  } catch (err) {\n    return\n  }\n\n  let nErrors = 0\n  await Promise.all(\n    map(vms, vmId =>\n      _call('vm.delete', { id: resolveId(vmId) }).catch(() => {\n        nErrors++\n      })\n    )\n  )\n\n  if (nErrors > 0) {\n    error(_('failedDeleteErrorTitle'), _('failedVmsErrorMessage', { nVms: nErrors }))\n  }\n}\n\nexport const coalesceLeafVm = async vm => {\n  if (vm.power_state !== 'Halted' && vm.power_state !== 'Suspended') {\n    await confirm({\n      title: _('coalesceLeaf'),\n      body: _('coalesceLeafSuspendVm'),\n    })\n  }\n  await _call('vm.coalesceLeaf', { id: resolveId(vm) })\n\n  success(_('coalesceLeaf'), _('coalesceLeafSuccess'))\n}\n\nexport const importBackup = ({ remote, file, sr }) => _call('vm.importBackup', resolveIds({ remote, file, sr }))\n\nexport const importDeltaBackup = ({ remote, file, sr, mapVdisSrs }) =>\n  _call(\n    'vm.importDeltaBackup',\n    resolveIds({\n      remote,\n      filePath: file,\n      sr,\n      mapVdisSrs: resolveIds(mapVdisSrs),\n    })\n  )\n\nimport RevertSnapshotModalBody from './revert-snapshot-modal' // eslint-disable-line import/first\nexport const revertSnapshot = snapshot =>\n  confirm({\n    title: _('revertVmModalTitle'),\n    body: <RevertSnapshotModalBody />,\n  }).then(async snapshotBefore => {\n    if (snapshotBefore) {\n      await _call('vm.snapshot', { id: snapshot.$snapshot_of })\n    }\n    await _call('vm.revert', { snapshot: snapshot.id })::tap(subscribeResourceSets.forceRefresh)\n    success(_('vmRevertSuccessfulTitle'), _('vmRevertSuccessfulMessage'))\n  }, noop)\n\nexport const editVm = async (vm, props) => {\n  if (props.hasVendorDevice) {\n    try {\n      await confirm({\n        title: _('windowsToolsModalTitle'),\n        body: (\n          <div>\n            <p>{_('windowsToolsModalMessage')}</p>\n            <p className='text-warning'>\n              <Icon icon='alarm' />\n              &nbsp;\n              {_('windowsToolsModalWarning')}\n            </p>\n          </div>\n        ),\n      })\n    } catch (err) {\n      if (err !== undefined) {\n        throw err\n      }\n      return\n    }\n  }\n  await _call('vm.set', { ...props, id: resolveId(vm) })\n    .catch(async err => {\n      if (vmBadPowerState.is(err, { actual: 'running' }) || err.message === 'Cannot change memory on running VM') {\n        try {\n          const force = await chooseAction({\n            body: <p>{_('vmEditAndRestartModalMessage')}</p>,\n            buttons: [\n              { label: _('rebootVmLabel'), value: false, btnStyle: 'success' },\n              { label: _('forceRebootVmLabel'), value: true, btnStyle: 'danger' },\n            ],\n            icon: 'vm-reboot',\n            title: _('vmEditAndRestartModalTitle'),\n          })\n          await _call('vm.setAndRestart', { ...props, id: resolveId(vm), force })\n        } catch (err) {\n          if (err !== undefined) {\n            error(_('setAndRestartVmFailed', { vm: renderXoItemFromId(resolveId(vm)) }), err.message)\n            throw err\n          }\n        }\n      } else {\n        error(_('setVmFailed', { vm: renderXoItemFromId(resolveId(vm)) }), err.message)\n      }\n    })\n    ::tap(() => {\n      subscribeResourceSets.forceRefresh()\n      if (props.secureBoot !== undefined) {\n        subscribeSecurebootReadiness.forceRefresh(vm)\n      }\n    })\n}\n\nexport const fetchVmStats = (vm, granularity) => _call('vm.stats', { id: resolveId(vm), granularity })\n\nexport const getVmsHaValues = () => _call('vm.getHaValues')\n\nexport const importVm = async (file, type = 'xva', data = undefined, sr, url = undefined) => {\n  const { name } = file\n\n  info(_('startVmImport'), name)\n  // eslint-disable-next-line no-undef\n  const formData = new FormData()\n  if (data !== undefined && data.tables !== undefined) {\n    for (const k in data.tables) {\n      const tables = await data.tables[k]\n      delete data.tables[k]\n      for (const l in tables) {\n        // eslint-disable-next-line no-undef\n        const blob = new Blob([tables[l]])\n        formData.append(l, blob, k)\n      }\n    }\n  }\n  const result = await _call('vm.import', { type, data, sr: resolveId(sr), url })\n  if (url !== undefined) {\n    // If imported from URL, result is the ID of the created VM\n    success(_('vmImportSuccess'), name)\n    return [result]\n  }\n  formData.append('file', file)\n  const res = await post(result.$sendTo, formData)\n  const json = await res.json()\n  if (res.status !== 200) {\n    error(_('vmImportFailed'), name)\n    throw json.error\n  }\n  success(_('vmImportSuccess'), name)\n  return json.result\n}\n\nimport ImportVdiModalBody from './import-vdi-modal' // eslint-disable-line import/first\nexport const importVdi = async vdi => {\n  const file = await confirm({\n    body: <ImportVdiModalBody />,\n    icon: 'import',\n    title: _('importVdi'),\n  })\n\n  if (file === undefined) {\n    error(_('importVdi'), _('importVdiNoFile'))\n    return\n  }\n\n  const { name } = file\n  info(_('startVdiImport'), name)\n\n  return _call('disk.importContent', { id: resolveId(vdi) }).then(({ $sendTo }) =>\n    post($sendTo, file)\n      .then(res => {\n        if (res.status !== 200) {\n          throw res.status\n        }\n        success(_('vdiImportSuccess'), name)\n        return res.json().then(body => body.result)\n      })\n      .catch(err => {\n        error(_('vdiImportFailed'), err)\n      })\n  )\n}\n\nexport const importVms = (vms, sr) =>\n  Promise.all(\n    map(vms, ({ file, type, data }) =>\n      importVm(file, type, data, sr).catch(error => {\n        console.warn('importVms', file.name, error)\n      })\n    )\n  ).then(ids => ids.filter(_ => _ !== undefined))\n\nconst importDisk = async ({ description, file, name, type, vmdkData, url = undefined }, sr) => {\n  // eslint-disable-next-line no-undef\n  const formData = new FormData()\n  if (vmdkData !== undefined) {\n    for (const l of ['grainLogicalAddressList', 'grainFileOffsetList']) {\n      const table = await vmdkData[l]\n      delete vmdkData[l]\n      // eslint-disable-next-line no-undef\n      const blob = new Blob([table])\n      formData.append(l, blob, file.name)\n    }\n  }\n  const res = await _call('disk.import', {\n    description,\n    name,\n    sr: resolveId(sr),\n    type,\n    vmdkData,\n    url,\n  })\n\n  if (url !== undefined) {\n    success(_('vdiImportSuccess'), name)\n    return res\n  }\n\n  formData.append('file', file)\n  const result = await post(res.$sendTo, formData)\n  const text = await result.text()\n  let body\n  try {\n    body = JSON.parse(text)\n  } catch (error) {\n    throw new Error(`Body is not a JSON, original message is : ${text}`)\n  }\n  if (result.status !== 200) {\n    throw new Error(body.error.message)\n  }\n  await body.result\n}\n\nexport const importDisks = (disks, sr) =>\n  Promise.all(\n    map(disks, disk =>\n      importDisk(disk, sr).catch(err => {\n        error(_('diskImportFailed'), err.message)\n        throw err\n      })\n    )\n  )\n\nexport const getBlockdevices = host => _call('host.getBlockdevices', { id: resolveId(host) })\n\nimport ExportVmModalBody from './export-vm-modal' // eslint-disable-line import/first\nexport const exportVm = async vm => {\n  const { compression, format } = await confirm({\n    body: <ExportVmModalBody vm={vm} />,\n    icon: 'export',\n    title: _('exportVmLabel'),\n  })\n  const id = resolveId(vm)\n  const { $getFrom: url } = await _call('vm.export', { vm: id, compress: compression, format })\n  const fullUrl = window.location.origin + url\n  const copytoClipboard = () => copy(fullUrl)\n  const _info = () => info(_('startVmExport'), id)\n\n  await confirm({\n    body: (\n      <div>\n        <a href={fullUrl} target='_blank' rel='noreferrer' onClick={_info}>\n          {_('downloadVm')}\n        </a>{' '}\n        <ActionButton handler={copytoClipboard} icon='clipboard' tooltip={_('copyExportedUrl')} size='small' />\n        <br />\n        <Icon icon='info' /> <em>{_('vmExportUrlValidity')}</em>\n      </div>\n    ),\n    icon: 'download',\n    title: _('downloadVm'),\n  })\n  _info()\n  window.open(`.${url}`)\n}\n\nimport ExportVdiModalBody from './export-vdi-modal' // eslint-disable-line import/first\nexport const exportVdi = async vdi => {\n  const format = await confirm({\n    body: <ExportVdiModalBody />,\n    icon: 'export',\n    title: _('exportVdi'),\n  })\n\n  info(_('startVdiExport'), vdi.id)\n\n  if (format === 'raw') {\n    return window.open(`./rest/v0/vdis/${resolveId(vdi)}.raw`)\n  }\n\n  return _call('disk.exportContent', { id: resolveId(vdi), format }).then(({ $getFrom: url }) => {\n    window.open(`.${url}`)\n  })\n}\n\nexport const insertCd = (vm, cd, force = false) =>\n  _call('vm.insertCd', {\n    id: resolveId(vm),\n    cd_id: resolveId(cd),\n    force,\n  })\n\nexport const ejectCd = vm => _call('vm.ejectCd', { id: resolveId(vm) })\n\nexport const setVmBootOrder = (vm, order) =>\n  _call('vm.setBootOrder', {\n    vm: resolveId(vm),\n    order,\n  })\n\nexport const attachDiskToVm = (vdi, vm, { bootable, mode, position }) =>\n  _call('vm.attachDisk', {\n    bootable,\n    mode,\n    position: (position && String(position)) || undefined,\n    vdi: resolveId(vdi),\n    vm: resolveId(vm),\n  })\n\nexport const createVgpu = (vm, { gpuGroup, vgpuType }) => _call('vm.createVgpu', resolveIds({ vm, gpuGroup, vgpuType }))\n\nexport const deleteVgpu = vgpu => _call('vm.deleteVgpu', resolveIds({ vgpu }))\n\nexport const shareVm = async (vm, resourceSet) =>\n  confirm({\n    title: _('shareVmInResourceSetModalTitle'),\n    body: _('shareVmInResourceSetModalMessage', {\n      self: renderXoItem({\n        ...(await getResourceSet(resourceSet)),\n        type: 'resourceSet',\n      }),\n    }),\n  }).then(() => editVm(vm, { share: true }), noop)\n\nexport const vmWarmMigration = async vm => {\n  const { sr, deleteSourceVm, startDestinationVm } = await confirm({\n    body: <WarmMigrationModal />,\n    title: _('vmWarmMigration'),\n    icon: 'vm-warm-migration',\n  })\n  return _call('vm.warmMigration', {\n    deleteSourceVm,\n    sr: resolveId(sr),\n    startDestinationVm,\n    vm: resolveId(vm),\n  })\n}\n\n// DISK ---------------------------------------------------------------\n\nexport const createDisk = (name, size, sr, { vm, bootable, mode, position }) =>\n  _call('disk.create', {\n    bootable,\n    mode,\n    name,\n    position,\n    size,\n    sr: resolveId(sr),\n    vm: resolveId(vm),\n  })\n\n// VDI ---------------------------------------------------------------\n\nexport const editVdi = (vdi, props) => _call('vdi.set', { ...props, id: resolveId(vdi) })\n\nexport const deleteVdi = vdi =>\n  confirm({\n    title: _('deleteVdiModalTitle'),\n    body: _('deleteVdiModalMessage'),\n  }).then(() => _call('vdi.delete', { id: resolveId(vdi) }), noop)\n\nexport const deleteVdis = vdis =>\n  confirm({\n    title: _('deleteVdisModalTitle', { nVdis: vdis.length }),\n    body: _('deleteVdisModalMessage', { nVdis: vdis.length }),\n  }).then(() => Promise.all(map(vdis, vdi => _call('vdi.delete', { id: resolveId(vdi) }))), noop)\n\nexport const deleteOrphanedVdis = vdis =>\n  confirm({\n    title: _('deleteOrphanedVdisModalTitle'),\n    body: (\n      <div>\n        <p>{_('deleteOrphanedVdisModalMessage', { nVdis: vdis.length })}</p>\n        <p>{_('definitiveMessageModal')}</p>\n      </div>\n    ),\n  }).then(() => Promise.all(map(resolveIds(vdis), id => _call('vdi.delete', { id }))), noop)\n\nexport const migrateVdi = (vdi, sr, resourceSet) =>\n  _call('vdi.migrate', {\n    id: resolveId(vdi),\n    resourceSet,\n    sr_id: resolveId(sr),\n  })\n\nexport const setCbt = (vdi, cbt) =>\n  _call('vdi.set', { id: resolveId(vdi), cbt }).catch(err => error(_('setCbtError'), err.message || String(err)))\n\n// VBD ---------------------------------------------------------------\n\nexport const connectVbd = vbd => _call('vbd.connect', { id: resolveId(vbd) })\n\nexport const disconnectVbd = vbd => _call('vbd.disconnect', { id: resolveId(vbd) })\n\nexport const disconnectVbds = vbds =>\n  confirm({\n    title: _('disconnectVbdsModalTitle', { nVbds: vbds.length }),\n    body: _('disconnectVbdsModalMessage', { nVbds: vbds.length }),\n  }).then(() => Promise.all(map(vbds, vbd => _call('vbd.disconnect', { id: resolveId(vbd) }))), noop)\n\nexport const deleteVbd = vbd => _call('vbd.delete', { id: resolveId(vbd) })\n\nexport const deleteVbds = vbds =>\n  confirm({\n    title: _('deleteVbdsModalTitle', { nVbds: vbds.length }),\n    body: _('deleteVbdsModalMessage', { nVbds: vbds.length }),\n  }).then(() => Promise.all(map(vbds, vbd => _call('vbd.delete', { id: resolveId(vbd) }))), noop)\n\nexport const editVbd = (vbd, props) => _call('vbd.set', { ...props, id: resolveId(vbd) })\n\nexport const setBootableVbd = (vbd, bootable) => _call('vbd.setBootable', { vbd: resolveId(vbd), bootable })\n\n// VIF ---------------------------------------------------------------\n\nexport const createVmInterface = (vm, network, mac) => _call('vm.createInterface', resolveIds({ vm, network, mac }))\n\nexport const connectVif = vif => _call('vif.connect', { id: resolveId(vif) })\n\nexport const disconnectVif = vif => _call('vif.disconnect', { id: resolveId(vif) })\n\nexport const deleteVif = vif => _call('vif.delete', { id: resolveId(vif) })\n\nexport const deleteVifs = vifs =>\n  confirm({\n    title: _('deleteVifsModalTitle', { nVifs: vifs.length }),\n    body: _('deleteVifsModalMessage', { nVifs: vifs.length }),\n  }).then(() => Promise.all(map(vifs, vif => _call('vif.delete', { id: resolveId(vif) }))), noop)\n\nexport const setVif = (\n  vif,\n  { allowedIpv4Addresses, allowedIpv6Addresses, lockingMode, mac, network, rateLimit, resourceSet, txChecksumming }\n) =>\n  _call('vif.set', {\n    allowedIpv4Addresses,\n    allowedIpv6Addresses,\n    id: resolveId(vif),\n    lockingMode,\n    mac,\n    network: resolveId(network),\n    rateLimit,\n    resourceSet,\n    txChecksumming,\n  })\n\nexport const getLockingModeValues = () => _call('vif.getLockingModeValues')\n\nexport const addAclRule = ({ allow, protocol = undefined, port = undefined, ipRange = '', direction, vif }) =>\n  _call('sdnController.addRule', {\n    allow,\n    protocol,\n    port,\n    ipRange,\n    direction,\n    vifId: resolveId(vif),\n  })\n\nexport const deleteAclRule = ({ protocol = undefined, port = undefined, ipRange = '', direction, vif }) =>\n  _call('sdnController.deleteRule', {\n    protocol,\n    port,\n    ipRange,\n    direction,\n    vifId: resolveId(vif),\n  })\n\n// VTPM -----------------------------------------------------------\nconst handlePoolDoesNotSupportVtpmError = err => {\n  if (\n    incorrectState.is(err, {\n      property: 'restrictions.restrict_vtpm',\n      expected: 'false',\n    })\n  ) {\n    console.error(err)\n    throw new Error('This pool does not support VTPM')\n  }\n}\n\nexport const createVtpm = async vm => {\n  try {\n    return await _call('vtpm.create', { id: resolveId(vm) })\n  } catch (err) {\n    handlePoolDoesNotSupportVtpmError(err)\n    throw err\n  }\n}\nexport const deleteVtpm = vtpm => _call('vtpm.destroy', { id: resolveId(vtpm) })\n\nexport const editPusb = (pusb, props) => _call('pusb.set', { id: resolveId(pusb), ...props })\n\nexport const createVusb = (vm, usbGroup) => _call('vusb.create', { vm: resolveId(vm), usbGroup: resolveId(usbGroup) })\n\nexport const unplugVusb = vusb => _call('vusb.unplug', { id: resolveId(vusb) })\n\nexport const deleteVusb = vusb => _call('vusb.destroy', { id: resolveId(vusb) })\n\n// Network -----------------------------------------------------------\n\nexport const editNetwork = (network, props) => _call('network.set', { ...props, id: resolveId(network) })\n\nexport const getBondModes = () => _call('network.getBondModes')\nexport const createNetwork = params => _call('network.create', params)\nexport const createBondedNetwork = params => _call('network.createBonded', params)\nexport const createPrivateNetwork = ({ preferredCenter, ...params }) =>\n  _call('sdnController.createPrivateNetwork', {\n    ...params,\n    preferredCenterId: preferredCenter !== null ? resolveId(preferredCenter) : undefined,\n  })\n\nexport const deleteNetwork = network =>\n  confirm({\n    title: _('deleteNetwork'),\n    body: _('deleteNetworkConfirm'),\n  }).then(() => _call('network.delete', { network: resolveId(network) }), noop)\n\n// PIF ---------------------------------------------------------------\n\nexport const connectPif = pif =>\n  confirm({\n    title: _('connectPif'),\n    body: _('connectPifConfirm'),\n  }).then(() => _call('pif.connect', { pif: resolveId(pif) }), noop)\n\nexport const disconnectPif = pif =>\n  confirm({\n    title: _('disconnectPif'),\n    body: _('disconnectPifConfirm'),\n  }).then(() => _call('pif.disconnect', { pif: resolveId(pif) }), noop)\n\nexport const deletePif = pif =>\n  confirm({\n    title: _('deletePif'),\n    body: _('deletePifConfirm'),\n  }).then(() => _call('pif.delete', { pif: resolveId(pif) }), noop)\n\nexport const deletePifs = pifs =>\n  confirm({\n    title: _('deletePifs'),\n    body: _('deletePifsConfirm', { nPifs: pifs.length }),\n  }).then(() => Promise.all(map(pifs, pif => _call('pif.delete', { pif: resolveId(pif) }))), noop)\n\nexport const reconfigurePifIp = (pif, { mode, ip, ipv6, ipv6Mode, netmask, gateway, dns }) =>\n  _call('pif.reconfigureIp', {\n    pif: resolveId(pif),\n    mode,\n    ip,\n    ipv6,\n    ipv6Mode,\n    netmask,\n    gateway,\n    dns,\n  })\n\nexport const getIpv4ConfigModes = () => _call('pif.getIpv4ConfigurationModes')\n\nexport const getIpv6ConfigModes = () => _call('pif.getIpv6ConfigurationModes')\n\nexport const editPif = (pif, { vlan }) => _call('pif.editPif', { pif: resolveId(pif), vlan })\n\nexport const scanHostPifs = hostId => _call('host.scanPifs', { host: hostId })\n\n// SR ----------------------------------------------------------------\n\nexport const deleteSr = sr =>\n  confirm({\n    title: 'Delete SR',\n    body: (\n      <div>\n        <p>Are you sure you want to remove this SR?</p>\n        <p>This operation is definitive, and ALL DISKS WILL BE LOST FOREVER.</p>\n      </div>\n    ),\n  }).then(() => _call('sr.destroy', { id: resolveId(sr) }), noop)\n\nexport const fetchSrStats = (sr, granularity) => _call('sr.stats', { id: resolveId(sr), granularity })\n\nexport const forgetSr = sr => forgetSrs([sr])\n\nexport const forgetSrs = srs =>\n  confirm({\n    title: _('nSrsForget', { nSrs: srs.length }),\n    body: (\n      <p className='text-warning font-weight-bold'>\n        {_('forgetNSrsModalMessage', { nSrs: srs.length })} {_('srForgetModalWarning')}\n      </p>\n    ),\n    strongConfirm: {\n      messageId: 'nSrsForget',\n      values: { nSrs: srs.length },\n    },\n  }).then(() => Promise.all(map(resolveIds(srs), id => _call('sr.forget', { id }))), noop)\n\nexport const reconnectAllHostsSr = sr =>\n  confirm({\n    title: _('srReconnectAllModalTitle'),\n    body: _('srReconnectAllModalMessage'),\n  }).then(() => _call('sr.connectAllPbds', { id: resolveId(sr) }), noop)\nexport const reconnectAllHostsSrs = srs =>\n  confirm({\n    title: _('srReconnectAllModalTitle'),\n    body: _('srReconnectAllModalMessage'),\n  }).then(() => Promise.all(resolveIds(srs), id => _call('sr.connectAllPbds', { id })), noop)\n\nexport const disconnectAllHostsSr = sr =>\n  confirm({\n    title: _('srDisconnectAllModalTitle'),\n    body: _('srDisconnectAllModalMessage'),\n  }).then(() => _call('sr.disconnectAllPbds', { id: resolveId(sr) }), noop)\nexport const disconnectAllHostsSrs = srs =>\n  confirm({\n    title: _('srDisconnectAllModalTitle'),\n    body: _('srsDisconnectAllModalMessage'),\n  }).then(() => Promise.all(resolveIds(srs), id => _call('sr.disconnectAllPbds', { id })), noop)\n\nexport const editSr = (sr, { nameDescription, nameLabel }) =>\n  _call('sr.set', {\n    id: resolveId(sr),\n    name_description: nameDescription,\n    name_label: nameLabel,\n  })\n\nexport const rescanSr = sr => _call('sr.scan', { id: resolveId(sr) })\nexport const rescanSrs = srs => Promise.all(map(resolveIds(srs), id => _call('sr.scan', { id })))\n\nexport const toggleSrMaintenanceMode = sr => {\n  const id = resolveId(sr)\n  const method = sr.inMaintenanceMode ? 'disableMaintenanceMode' : 'enableMaintenanceMode'\n\n  return _call(`sr.${method}`, { id }).catch(async err => {\n    if (\n      incorrectState.is(err, {\n        property: 'vmsToShutdown',\n      })\n    ) {\n      const vmIds = err.data.expected\n      const nVms = vmIds.length\n      await confirm({\n        title: _('maintenanceMode'),\n        body: (\n          <div>\n            {_('maintenanceSrModalBody', { n: nVms })}\n            <ul>\n              {vmIds.slice(0, MAX_VMS).map(id => (\n                <li key={id}>\n                  <Vm id={id} />\n                </li>\n              ))}\n            </ul>\n            {nVms > MAX_VMS && _('andNMore', { n: nVms - MAX_VMS })}\n          </div>\n        ),\n      })\n      return _call(`sr.${method}`, { id, vmsToShutdown: vmIds })\n    } else {\n      throw err\n    }\n  })\n}\n\nexport const reclaimSrSpace = async sr => {\n  await confirm({\n    icon: 'sr-reclaim-space',\n    title: _('srReclaimSpace'),\n    body: _('srReclaimSpaceConfirm'),\n  })\n\n  try {\n    await _call('sr.reclaimSpace', { id: resolveId(sr) })\n    success(_('srReclaimSpace'))\n  } catch (err) {\n    if (err?.data?.message?.includes('Operation not supported')) {\n      throw new Error('Space reclaim not supported. Only supported on block based/LVM based SRs.')\n    }\n    throw err\n  }\n}\n\n// PBDs --------------------------------------------------------------\n\nexport const connectPbd = pbd => _call('pbd.connect', { id: resolveId(pbd) })\n\nexport const disconnectPbd = pbd => _call('pbd.disconnect', { id: resolveId(pbd) })\n\nexport const deletePbd = pbd => _call('pbd.delete', { id: resolveId(pbd) })\n\nexport const deletePbds = pbds => Promise.all(map(pbds, deletePbd))\n\n// Messages ----------------------------------------------------------\n\nexport const deleteMessage = message => _call('message.delete', { id: resolveId(message) })\n\nexport const deleteMessages = logs =>\n  confirm({\n    title: _('logDeleteMultiple', { nLogs: logs.length }),\n    body: _('logDeleteMultipleMessage', { nLogs: logs.length }),\n  }).then(() => Promise.all(map(logs, deleteMessage)), noop)\n\n// Tags --------------------------------------------------------------\n\nexport const addTag = (object, tag) => _call('tag.add', { id: resolveId(object), tag })\n\nexport const setTag = (id, params) => _call('tag.set', { id, ...params })::tap(subscribeConfiguredTags.forceRefresh)\n\nexport const removeTag = (object, tag) => _call('tag.remove', { id: resolveId(object), tag })\n\n// Custom fields ------------------------------------------------------------------------\n\nexport const addCustomField = (id, name, value) => _call('customField.add', { id, name, value })\n\nexport const removeCustomField = (id, name) => _call('customField.remove', { id, name })\n\nexport const setCustomField = (id, name, value) => _call('customField.set', { id, name, value })\n\n// Tasks --------------------------------------------------------------\n\nexport const cancelTask = task => _call('task.cancel', { id: resolveId(task) })\n\nexport const cancelTasks = tasks =>\n  confirm({\n    title: _('cancelTasksModalTitle', { nTasks: tasks.length }),\n    body: _('cancelTasksModalMessage', { nTasks: tasks.length }),\n  }).then(() => Promise.all(map(tasks, task => _call('task.cancel', { id: resolveId(task) }))), noop)\n\nexport const destroyTask = task => _call('task.destroy', { id: resolveId(task) })\n\nexport const destroyTasks = tasks =>\n  confirm({\n    title: _('destroyTasksModalTitle', { nTasks: tasks.length }),\n    body: _('destroyTasksModalMessage', { nTasks: tasks.length }),\n  }).then(() => Promise.all(map(tasks, task => _call('task.destroy', { id: resolveId(task) }))), noop)\n\n// XO Tasks --------------------------------------------------------------\n\nexport const abortXoTask = async task => {\n  const response = await fetch(`./rest/v0/tasks/${task.id}/actions/abort`, { method: 'POST' })\n  if (!response.ok) {\n    throw new Error(await response.text())\n  }\n}\n\nexport const deleteXoTaskLog = async task => {\n  const response = await fetch(`./rest/v0/tasks/${task.id}`, { method: 'DELETE' })\n  if (!response.ok) {\n    throw new Error(await response.text())\n  }\n}\n\n// Jobs -------------------------------------------------------------\n\nexport const createJob = job => _call('job.create', { job })::tap(subscribeJobs.forceRefresh)\n\nexport const deleteJob = job => _call('job.delete', { id: resolveId(job) })::tap(subscribeJobs.forceRefresh)\n\nexport const deleteJobs = jobs =>\n  confirm({\n    title: _('deleteJobsModalTitle', { nJobs: jobs.length }),\n    body: _('deleteJobsModalMessage', { nJobs: jobs.length }),\n  }).then(\n    () => Promise.all(map(jobs, job => _call('job.delete', { id: resolveId(job) })))::tap(subscribeJobs.forceRefresh),\n    noop\n  )\n\nexport const editJob = job => _call('job.set', { job })::tap(subscribeJobs.forceRefresh)\n\nexport const getJob = id => _call('job.get', { id })\n\nexport const runJob = job => {\n  info(_('runJob'), _('runJobVerbose'))\n  return _call('job.runSequence', { idSequence: [resolveId(job)] })\n}\n\nexport const cancelJob = ({ id, name, runId }) =>\n  confirm({\n    title: _('cancelJob'),\n    body: _('cancelJobConfirm', {\n      id: id.slice(0, 5),\n      name: <strong>{name}</strong>,\n    }),\n  }).then(() => _call('job.cancel', { runId }))\n\n// Backup/Schedule ---------------------------------------------------------\n\nexport const createSchedule = (jobId, { cron, enabled, name = undefined, timezone = undefined }) =>\n  _call('schedule.create', { jobId, cron, enabled, name, timezone })::tap(subscribeSchedules.forceRefresh)\n\nexport const deleteBackupSchedule = async schedule => {\n  await confirm({\n    title: _('deleteBackupSchedule'),\n    body: _('deleteBackupScheduleQuestion'),\n  })\n  await _call('schedule.delete', { id: schedule.id })\n  await _call('job.delete', { id: schedule.jobId })\n\n  subscribeSchedules.forceRefresh()\n  subscribeJobs.forceRefresh()\n}\n\nexport const deleteSchedule = schedule =>\n  _call('schedule.delete', { id: resolveId(schedule) })::tap(subscribeSchedules.forceRefresh)\n\nexport const deleteSchedules = schedules =>\n  confirm({\n    title: _('deleteSchedulesModalTitle', { nSchedules: schedules.length }),\n    body: _('deleteSchedulesModalMessage', { nSchedules: schedules.length }),\n  }).then(() =>\n    Promise.all(\n      map(schedules, schedule =>\n        _call('schedule.delete', { id: resolveId(schedule) })::tap(subscribeSchedules.forceRefresh)\n      )\n    )\n  )\n\nexport const disableSchedule = id => editSchedule({ id, enabled: false })\n\nexport const editSchedule = ({ id, jobId, cron, enabled, name, timezone }) =>\n  _call('schedule.set', { id, jobId, cron, enabled, name, timezone })::tap(subscribeSchedules.forceRefresh)\n\nexport const enableSchedule = id => editSchedule({ id, enabled: true })\n\nexport const getSchedule = id => _call('schedule.get', { id })\n\n// Backup NG ---------------------------------------------------------\n\nexport const subscribeBackupNgJobs = createSubscription(() => _call('backupNg.getAllJobs'))\n\nexport const subscribeBackupNgLogs = createSubscription(async () => {\n  const { $getFrom } = await _call('backupNg.getAllLogs', { ndjson: true })\n  const response = await fetch(`.${$getFrom}`)\n  const data = await response.text()\n\n  const logs = { __proto__: null }\n  parseNdJson(data, log => {\n    logs[log.id] = log\n  })\n  return logs\n})\n\nexport const subscribeMetadataBackupJobs = createSubscription(() => _call('metadataBackup.getAllJobs'))\n\nexport const createBackupNgJob = props => _call('backupNg.createJob', props)::tap(subscribeBackupNgJobs.forceRefresh)\n\nexport const getSuggestedExcludedTags = () => _call('backupNg.getSuggestedExcludedTags')\n\nexport const deleteBackupJobs = async ({ backupIds = [], metadataBackupIds = [], mirrorBackupIds = [] }) => {\n  const nJobs = backupIds.length + metadataBackupIds.length + mirrorBackupIds.length\n  if (nJobs === 0) {\n    return\n  }\n  const vars = { nJobs }\n  try {\n    await confirm({\n      title: _('confirmDeleteBackupJobsTitle', vars),\n      body: <p>{_('confirmDeleteBackupJobsBody', vars)}</p>,\n    })\n  } catch (_) {\n    return\n  }\n\n  const promises = []\n  if (backupIds.length !== 0) {\n    promises.push(\n      Promise.all(backupIds.map(id => _call('backupNg.deleteJob', { id: resolveId(id) })))::tap(\n        subscribeBackupNgJobs.forceRefresh\n      )\n    )\n  }\n  if (metadataBackupIds.length !== 0) {\n    promises.push(\n      Promise.all(metadataBackupIds.map(id => _call('metadataBackup.deleteJob', { id: resolveId(id) })))::tap(\n        subscribeMetadataBackupJobs.forceRefresh\n      )\n    )\n  }\n  if (mirrorBackupIds.length !== 0) {\n    promises.push(\n      Promise.all(mirrorBackupIds.map(id => _call('mirrorBackup.deleteJob', { id: resolveId(id) })))::tap(\n        subscribeMirrorBackupJobs.forceRefresh\n      )\n    )\n  }\n\n  return Promise.all(promises)::tap(subscribeSchedules.forceRefresh)\n}\n\nexport const editBackupNgJob = props => _call('backupNg.editJob', props)::tap(subscribeBackupNgJobs.forceRefresh)\n\nexport const getBackupNgJob = id => _call('backupNg.getJob', { id })\n\nexport const runBackupNgJob = ({ force, ...params }) => {\n  if (force) {\n    params.settings = {\n      '': {\n        bypassVdiChainsCheck: true,\n      },\n    }\n  }\n  return _call('backupNg.runJob', params)\n}\n\nexport const listVmBackups = remotes => _call('backupNg.listVmBackups', { remotes: resolveIds(remotes) })\n\nexport const restoreBackup = (\n  backup,\n  sr,\n  { generateNewMacAddresses = false, mapVdisSrs = {}, startOnRestore = false, useDifferentialRestore = false } = {}\n) => {\n  const promise = _call('backupNg.importVmBackup', {\n    id: resolveId(backup),\n    settings: { mapVdisSrs: resolveIds(mapVdisSrs), newMacAddresses: generateNewMacAddresses, useDifferentialRestore },\n    sr: resolveId(sr),\n  })\n\n  if (startOnRestore) {\n    return promise.then(startVm)\n  }\n\n  return promise\n}\n\nexport const checkBackup = (backup, sr, { mapVdisSrs = {} } = {}) => {\n  return _call('backupNg.checkBackup', {\n    id: resolveId(backup),\n    settings: { mapVdisSrs: resolveIds(mapVdisSrs) },\n    sr: resolveId(sr),\n  })\n}\n\nexport const deleteBackup = backup => _call('backupNg.deleteVmBackup', { id: resolveId(backup) })\n\nexport const deleteBackups = async backups =>\n  _call('backupNg.deleteVmBackups', { ids: backups.map(backup => resolveId(backup)) })\n\nexport const createMetadataBackupJob = props =>\n  _call('metadataBackup.createJob', props)\n    ::tap(subscribeMetadataBackupJobs.forceRefresh)\n    ::tap(subscribeSchedules.forceRefresh)\n\nexport const editMetadataBackupJob = props =>\n  _call('metadataBackup.editJob', props)\n    ::tap(subscribeMetadataBackupJobs.forceRefresh)\n    ::tap(subscribeSchedules.forceRefresh)\n\nexport const runMetadataBackupJob = params => _call('metadataBackup.runJob', params)\n\nexport const listMetadataBackups = remotes => _call('metadataBackup.list', { remotes: resolveIds(remotes) })\n\nexport const restoreMetadataBackup = data =>\n  _call('metadataBackup.restore', {\n    id: resolveId(data.backup),\n    pool: data.pool,\n  })::tap(subscribeBackupNgLogs.forceRefresh)\n\nexport const deleteMetadataBackup = backup =>\n  _call('metadataBackup.delete', {\n    id: resolveId(backup),\n  })\n\nexport const deleteMetadataBackups = async (backups = []) => {\n  // delete sequentially from newest to oldest\n  backups = backups.slice().sort((b1, b2) => b2.timestamp - b1.timestamp)\n  for (let i = 0, n = backups.length; i < n; ++i) {\n    await deleteMetadataBackup(backups[i])\n  }\n}\n\n// Mirror backup ---------------------------------------------------------\n\nexport const subscribeMirrorBackupJobs = createSubscription(() => _call('mirrorBackup.getAllJobs'))\n\nexport const createMirrorBackupJob = props =>\n  _call('mirrorBackup.createJob', props)::tap(subscribeMirrorBackupJobs.forceRefresh)\n\nexport const runMirrorBackupJob = props => _call('mirrorBackup.runJob', props)\n\nexport const editMirrorBackupJob = props => _call('mirrorBackup.editJob', props)\n\n// Plugins -----------------------------------------------------------\n\nexport const loadPlugin = async id =>\n  _call('plugin.load', { id })::tap(subscribePlugins.forceRefresh, err =>\n    error(_('pluginError'), (err && err.message) || _('unknownPluginError'))\n  )\n\nexport const unloadPlugin = id =>\n  _call('plugin.unload', { id })::tap(subscribePlugins.forceRefresh, err =>\n    error(_('pluginError'), (err && err.message) || _('unknownPluginError'))\n  )\n\nexport const enablePluginAutoload = id => _call('plugin.enableAutoload', { id })::tap(subscribePlugins.forceRefresh)\n\nexport const disablePluginAutoload = id => _call('plugin.disableAutoload', { id })::tap(subscribePlugins.forceRefresh)\n\nexport const configurePlugin = (id, configuration) =>\n  _call('plugin.configure', { id, configuration })::tap(\n    () => {\n      info(_('pluginConfigurationSuccess'), _('pluginConfigurationChanges'))\n      subscribePlugins.forceRefresh()\n    },\n    err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))\n  )\n\nexport const getPlugin = async id => {\n  const { user } = store.getState()\n  if (user != null && user.permission === 'admin') {\n    return (await _call('plugin.get')).find(plugin => plugin.id === id)\n  }\n}\n\nexport const purgePluginConfiguration = async id => {\n  await confirm({\n    title: _('purgePluginConfiguration'),\n    body: _('purgePluginConfigurationQuestion'),\n  })\n  await _call('plugin.purgeConfiguration', { id })\n\n  subscribePlugins.forceRefresh()\n}\n\nexport const testPlugin = (id, data) => _call('plugin.test', { id, data })\n\nexport const sendUsageReport = () => _call('plugin.usageReport.send')\n\n// Resource set ------------------------------------------------------\n\nexport const createResourceSet = (name, { shareByDefault, subjects, objects, tags, limits } = {}) =>\n  _call('resourceSet.create', { name, shareByDefault, subjects, objects, tags, limits })::tap(\n    subscribeResourceSets.forceRefresh\n  )\n\nexport const editResourceSet = (id, { name, shareByDefault, subjects, objects, tags, limits, ipPools } = {}) =>\n  _call('resourceSet.set', {\n    id,\n    name,\n    shareByDefault,\n    subjects,\n    objects,\n    tags,\n    limits,\n    ipPools,\n  })::tap(subscribeResourceSets.forceRefresh)\n\nexport const deleteResourceSet = async id => {\n  await confirm({\n    title: _('deleteResourceSetWarning'),\n    body: _('deleteResourceSetQuestion'),\n  })\n  await _call('resourceSet.delete', { id: resolveId(id) })\n\n  subscribeResourceSets.forceRefresh()\n}\n\nexport const recomputeResourceSetsLimits = () =>\n  _call('resourceSet.recomputeAllLimits')::tap(subscribeResourceSets.forceRefresh)\n\nexport const getResourceSet = id => _call('resourceSet.get', { id: resolveId(id) })\n\n// Remote ------------------------------------------------------------\n\nexport const getRemotes = () => _call('remote.getAll')\n\nexport const getRemote = remote =>\n  _call('remote.get', resolveIds({ id: remote }))::tap(null, err => error(_('getRemote'), err.message || String(err)))\n\nexport const createRemote = (name, url, options, proxy) =>\n  _call('remote.create', {\n    name,\n    options,\n    proxy: resolveId(proxy),\n    url,\n  })::tap(remote => {\n    testRemote(remote).catch(noop)\n  })\n\nexport const deleteRemote = remote =>\n  _call('remote.delete', { id: resolveId(remote) })::tap(subscribeRemotes.forceRefresh)\n\nexport const deleteRemotes = remotes =>\n  confirm({\n    title: _('deleteRemotesModalTitle', { nRemotes: remotes.length }),\n    body: _('deleteRemotesModalMessage', { nRemotes: remotes.length }),\n  }).then(\n    () =>\n      Promise.all(map(remotes, remote => _call('remote.delete', { id: resolveId(remote) })))::tap(\n        subscribeRemotes.forceRefresh\n      ),\n    noop\n  )\n\nexport const enableRemote = remote =>\n  _call('remote.set', { id: resolveId(remote), enabled: true })::tap(() => testRemote(remote).catch(noop))\n\nexport const disableRemote = remote =>\n  _call('remote.set', { id: resolveId(remote), enabled: false })::tap(subscribeRemotes.forceRefresh)\n\nexport const editRemote = (remote, { name, options, proxy, url }) =>\n  _call('remote.set', {\n    id: remote.id,\n    name,\n    options,\n    proxy: resolveId(proxy),\n    url,\n  })::tap(() => {\n    subscribeRemotes.forceRefresh()\n    if (remote.enabled) {\n      testRemote(remote).catch(noop)\n    }\n  })\n\nexport const testRemote = remote =>\n  _call('remote.test', resolveIds({ id: remote }))\n    ::tap(null, err => error(_('testRemote'), err.message || String(err)))\n    ::pFinally(subscribeRemotes.forceRefresh)\n\n// File restore  ----------------------------------------------------\n\nexport const listPartitions = (remote, disk) => _call('backupNg.listPartitions', resolveIds({ remote, disk }))\n\nexport const listFiles = (remote, disk, path, partition) =>\n  _call('backupNg.listFiles', resolveIds({ remote, disk, path, partition }))\n\nexport const fetchFiles = (remote, disk, partition, paths, format) =>\n  _call('backupNg.fetchFiles', resolveIds({ remote, disk, format, partition, paths })).then(({ $getFrom: url }) => {\n    window.open(`.${url}`)\n  })\n\n// -------------------------------------------------------------------\n\nexport const probeSrNfs = (host, server, nfsVersion) => _call('sr.probeNfs', { host, nfsVersion, server })\n\nexport const probeSrNfsExists = (host, server, serverPath, nfsVersion) =>\n  _call('sr.probeNfsExists', { host, nfsVersion, server, serverPath })\n\nexport const probeSrIscsiIqns = (host, target, port = undefined, chapUser = undefined, chapPassword) => {\n  const params = { host, target }\n  port && (params.port = port)\n  chapUser && (params.chapUser = chapUser)\n  chapPassword && (params.chapPassword = chapPassword)\n  return _call('sr.probeIscsiIqns', params)\n}\n\nexport const probeSrIscsiLuns = (host, target, targetIqn, chapUser = undefined, chapPassword) => {\n  const params = { host, target, targetIqn }\n  chapUser && (params.chapUser = chapUser)\n  chapPassword && (params.chapPassword = chapPassword)\n  return _call('sr.probeIscsiLuns', params)\n}\n\nexport const probeSrIscsiExists = (\n  host,\n  target,\n  targetIqn,\n  scsiId,\n  port = undefined,\n  chapUser = undefined,\n  chapPassword\n) => {\n  const params = { host, target, targetIqn, scsiId }\n  port && (params.port = port)\n  chapUser && (params.chapUser = chapUser)\n  chapPassword && (params.chapPassword = chapPassword)\n  return _call('sr.probeIscsiExists', params)\n}\n\nexport const probeSrHba = host => _call('sr.probeHba', { host })\n\nexport const probeSrHbaExists = (host, scsiId) => _call('sr.probeHbaExists', { host, scsiId })\n\nexport const probeZfs = host => _call('sr.probeZfs', { host: resolveId(host) })\n\nexport const createSrNfs = (\n  host,\n  nameLabel,\n  nameDescription,\n  server,\n  serverPath,\n  nfsVersion = undefined,\n  nfsOptions,\n  srUuid\n) => {\n  const params = { host, nameLabel, nameDescription, server, serverPath }\n  nfsVersion && (params.nfsVersion = nfsVersion)\n  nfsOptions && (params.nfsOptions = nfsOptions)\n  srUuid && (params.srUuid = srUuid)\n  return _call('sr.createNfs', params)\n}\n\nexport const createSrSmb = (host, nameLabel, nameDescription, server, user, password, srUuid) => {\n  const params = { host, nameLabel, nameDescription, server, user, password }\n  if (srUuid !== undefined) {\n    params.srUuid = srUuid\n  }\n  return _call('sr.createSmb', params)\n}\n\nexport const createSrIscsi = (\n  host,\n  nameLabel,\n  nameDescription,\n  target,\n  targetIqn,\n  scsiId,\n  port = undefined,\n  chapUser = undefined,\n  chapPassword = undefined,\n  srUuid\n) => {\n  const params = { host, nameLabel, nameDescription, target, targetIqn, scsiId }\n  port && (params.port = port)\n  chapUser && (params.chapUser = chapUser)\n  chapPassword && (params.chapPassword = chapPassword)\n  srUuid && (params.srUuid = srUuid)\n  return _call('sr.createIscsi', params)\n}\n\nexport const createSrHba = (host, nameLabel, nameDescription, scsiId, srUuid) => {\n  const params = { host, nameLabel, nameDescription, scsiId }\n  srUuid && (params.srUuid = srUuid)\n  return _call('sr.createHba', params)\n}\n\nexport const createSrIso = (\n  host,\n  nameLabel,\n  nameDescription,\n  path,\n  type,\n  user = undefined,\n  password = undefined,\n  nfsVersion = undefined,\n  nfsOptions = undefined,\n  srUuid\n) => {\n  const params = { host, nameLabel, nameDescription, path, type, srUuid }\n  user && (params.user = user)\n  password && (params.password = password)\n  nfsVersion && (params.nfsVersion = nfsVersion)\n  nfsOptions && (params.nfsOptions = nfsOptions)\n  srUuid && (params.srUuid = srUuid)\n  return _call('sr.createIso', params)\n}\n\nexport const createSrLvm = (host, nameLabel, nameDescription, device) =>\n  _call('sr.createLvm', { host, nameLabel, nameDescription, device })\n\nexport const createSrExt = (host, nameLabel, nameDescription, device) =>\n  _call('sr.createExt', { host, nameLabel, nameDescription, device })\n\nexport const createSrZfs = (host, nameLabel, nameDescription, location) =>\n  _call('sr.createZfs', {\n    host: resolveId(host),\n    nameDescription,\n    nameLabel,\n    location,\n  })\n\n// Job logs ----------------------------------------------------------\n\nexport const deleteJobsLogs = async ids => {\n  const { length } = ids\n  if (length === 0) {\n    return\n  }\n  if (length !== 1) {\n    const vars = { nLogs: length }\n    try {\n      await confirm({\n        title: _('logDeleteMultiple', vars),\n        body: <p>{_('logDeleteMultipleMessage', vars)}</p>,\n      })\n    } catch (_) {\n      return\n    }\n  }\n\n  return _call('log.delete', {\n    namespace: 'jobs',\n    id: ids.map(resolveId),\n  })::tap(subscribeJobsLogs.forceRefresh)\n}\n\n// Logs\n\nexport const deleteApiLog = log =>\n  _call('log.delete', { namespace: 'api', id: resolveId(log) })::tap(subscribeApiLogs.forceRefresh)\n\nexport const deleteApiLogs = logs =>\n  confirm({\n    title: _('logDeleteMultiple', { nLogs: logs.length }),\n    body: _('logDeleteMultipleMessage', { nLogs: logs.length }),\n  }).then(() => Promise.all(map(logs, deleteApiLog)), noop)\n\n// Acls, users, groups ----------------------------------------------------------\n\nexport const addAcl = ({ subject, object, action }) =>\n  _call('acl.add', resolveIds({ subject, object, action }))::tap(subscribeAcls.forceRefresh, err =>\n    error('Add ACL', err.message || String(err))\n  )\n\nexport const removeAcl = ({ subject, object, action }) =>\n  _call('acl.remove', resolveIds({ subject, object, action }))::tap(subscribeAcls.forceRefresh, err =>\n    error('Remove ACL', err.message || String(err))\n  )\n\nexport const removeAcls = acls =>\n  confirm({\n    title: _('deleteAclsModalTitle', { nAcls: acls.length }),\n    body: <p>{_('deleteAclsModalMessage', { nAcls: acls.length })}</p>,\n  }).then(\n    () =>\n      Promise.all(\n        map(acls, ({ subject, object, action }) => _call('acl.remove', resolveIds({ subject, object, action })))\n      )::tap(subscribeAcls.forceRefresh, err => error('Remove ACLs', err.message || String(err))),\n    noop\n  )\n\nexport const editAcl = (\n  { subject, object, action },\n  { subject: newSubject = subject, object: newObject = object, action: newAction = action }\n) =>\n  _call('acl.remove', resolveIds({ subject, object, action }))\n    .then(() =>\n      _call(\n        'acl.add',\n        resolveIds({\n          subject: newSubject,\n          object: newObject,\n          action: newAction,\n        })\n      )\n    )\n    ::tap(subscribeAcls.forceRefresh, err => error('Edit ACL', err.message || String(err)))\n\nexport const createGroup = name =>\n  _call('group.create', { name })::tap(subscribeGroups.forceRefresh, err =>\n    error(_('createGroup'), err.message || String(err))\n  )\n\nexport const setGroupName = (group, name) =>\n  _call('group.set', resolveIds({ group, name }))::tap(subscribeGroups.forceRefresh)\n\nexport const deleteGroup = group =>\n  confirm({\n    title: _('deleteGroup'),\n    body: <p>{_('deleteGroupConfirm')}</p>,\n  }).then(\n    () =>\n      _call('group.delete', resolveIds({ id: group }))::tap(subscribeGroups.forceRefresh, err =>\n        error(_('deleteGroup'), err.message || String(err))\n      ),\n    noop\n  )\n\nexport const deleteGroups = groups =>\n  confirm({\n    title: _('deleteGroupsModalTitle', { nGroups: groups.length }),\n    body: <p>{_('deleteGroupsModalMessage', { nGroups: groups.length })}</p>,\n  }).then(\n    () =>\n      Promise.all(groups.map(({ id }) => _call('group.delete', { id })))::tap(subscribeGroups.forceRefresh, err =>\n        error(_('deleteGroup'), err.message || String(err))\n      ),\n    noop\n  )\n\nexport const removeUserFromGroup = (user, group) =>\n  _call('group.removeUser', resolveIds({ id: group, userId: user }))::tap(subscribeGroups.forceRefresh, err =>\n    error(_('removeUserFromGroup'), err.message || String(err))\n  )\n\nexport const addUserToGroup = (user, group) =>\n  _call('group.addUser', resolveIds({ id: group, userId: user }))::tap(subscribeGroups.forceRefresh, err =>\n    error('Add User', err.message || String(err))\n  )\n\nexport const createUser = (email, password, permission) =>\n  _call('user.create', {\n    email,\n    password,\n    permission,\n  })::tap(subscribeUsers.forceRefresh, err => error('Create user', err.message || String(err)))\n\nexport const deleteUser = user =>\n  confirm({\n    title: _('deleteUser'),\n    body: <p>{_('deleteUserConfirm')}</p>,\n  }).then(() =>\n    _call('user.delete', {\n      id: resolveId(user),\n    })::tap(subscribeUsers.forceRefresh, err => error(_('deleteUser'), err.message || String(err)))\n  )\n\nexport const deleteUsers = users =>\n  confirm({\n    title: _('deleteUsersModalTitle', { nUsers: users.length }),\n    body: <p>{_('deleteUsersModalMessage', { nUsers: users.length })}</p>,\n  }).then(\n    () =>\n      Promise.all(map(resolveIds(users), id => _call('user.delete', { id })))::tap(subscribeUsers.forceRefresh, err =>\n        error(_('deleteUser'), err.message || String(err))\n      ),\n    noop\n  )\n\nexport const editUser = (user, { email, password, permission }) =>\n  _call('user.set', { id: resolveId(user), email, password, permission })::tap(subscribeUsers.forceRefresh)\n\nexport const removeUserAuthProvider = ({ userId, authProviderId }) => {\n  _call('user.removeAuthProvider', { id: userId, authProvider: authProviderId })\n    ::tap(subscribeUsers.forceRefresh)\n    .catch(e => {\n      error('user.removeAuthProvider', e.message)\n    })\n}\n\nconst _signOutFromEverywhereElse = () =>\n  _call('token.deleteOwn', {\n    pattern: {\n      id: {\n        __not: cookies.get('token'),\n      },\n    },\n  })\n\nexport const signOutFromEverywhereElse = () =>\n  _signOutFromEverywhereElse().then(\n    () => success(_('forgetTokens'), _('forgetTokensSuccess')),\n    () => error(_('forgetTokens'), _('forgetTokensError'))\n  )\n\nexport const changePassword = (oldPassword, newPassword) =>\n  _call('user.changePassword', {\n    oldPassword,\n    newPassword,\n  })\n    .then(_signOutFromEverywhereElse)\n    .then(\n      () => success(_('pwdChangeSuccess'), _('pwdChangeSuccessBody')),\n      () => error(_('pwdChangeError'), _('pwdChangeErrorBody'))\n    )\n\nconst _setUserPreferences = (preferences, userId) =>\n  _call('user.set', {\n    id: userId ?? xo.user.id,\n    preferences,\n  })::tap(subscribeCurrentUser.forceRefresh)\n\nimport NewSshKeyModalBody from './new-ssh-key-modal' // eslint-disable-line import/first\nexport const addSshKey = async key => {\n  const { preferences } = xo.user\n  const otherKeys = (preferences && preferences.sshKeys) || []\n\n  if (key === undefined) {\n    try {\n      key = await confirm({\n        icon: 'ssh-key',\n        title: _('newSshKeyModalTitle'),\n        body: <NewSshKeyModalBody />,\n      })\n    } catch (err) {\n      return\n    }\n\n    if (!key.title || !key.key) {\n      error(_('sshKeyErrorTitle'), _('sshKeyErrorMessage'))\n      return\n    }\n  }\n\n  if (otherKeys.some(otherKey => otherKey.key === key.key)) {\n    error(_('sshKeyErrorTitle'), _('sshKeyAlreadyExists'))\n    return\n  }\n\n  return _setUserPreferences({\n    sshKeys: [...otherKeys, key],\n  })\n}\n\nexport const deleteSshKey = key =>\n  confirm({\n    title: _('deleteSshKeyConfirm'),\n    body: _('deleteSshKeyConfirmMessage', {\n      title: <strong>{key.title}</strong>,\n    }),\n  }).then(() => {\n    const { preferences } = xo.user\n    return _setUserPreferences({\n      sshKeys: filter(preferences && preferences.sshKeys, k => k.key !== resolveId(key)),\n    })\n  }, noop)\n\n// eslint-disable-next-line import/first\nimport { AddOtpModal } from './add-otp-modal.js'\nexport const addOtp = async secret => {\n  try {\n    let token\n    do {\n      token = await confirm({\n        title: _('addOtpConfirm'),\n        body: <AddOtpModal failedToken={token} secret={secret} user={xo.user} />,\n      })\n    } while (!(await authenticator.check(token, secret)))\n  } catch (error) {\n    if (error === undefined) {\n      // action canceled\n      return\n    }\n    throw error\n  }\n\n  return _setUserPreferences({\n    otp: secret,\n  })\n}\n\nexport const removeOtp = user =>\n  confirm({\n    title: _('removeOtpConfirm'),\n    body: _('removeOtpConfirmMessage'),\n  }).then(\n    () =>\n      _setUserPreferences(\n        {\n          otp: null,\n        },\n        resolveId(user)\n      ),\n    noop\n  )\n\nexport const deleteSshKeys = keys =>\n  confirm({\n    title: _('deleteSshKeysConfirm', { nKeys: keys.length }),\n    body: _('deleteSshKeysConfirmMessage', {\n      nKeys: keys.length,\n    }),\n  }).then(() => {\n    const { preferences } = xo.user\n    const keyIds = resolveIds(keys)\n    return _setUserPreferences({\n      sshKeys: filter(preferences && preferences.sshKeys, sshKey => !includes(keyIds, sshKey.key)),\n    })\n  }, noop)\n\nexport const addAuthToken = async () => {\n  const { description, expiration } = await confirm({\n    body: <NewAuthTokenModal />,\n    icon: 'user',\n    title: _('newAuthTokenModalTitle'),\n  })\n  const expires = new Date(expiration).setHours(23, 59, 59)\n  return _call('token.create', {\n    description,\n    expiresIn: Number.isNaN(expires) ? undefined : expires - new Date().getTime(),\n  })::tap(subscribeUserAuthTokens.forceRefresh)\n}\n\nexport const deleteAuthToken = async ({ id }) => {\n  await confirm({\n    body: _('deleteAuthTokenConfirmMessage', {\n      id,\n    }),\n    icon: 'user',\n    title: _('deleteAuthTokenConfirm'),\n  })\n  return _call('token.deleteOwn', { tokens: [id] })::tap(subscribeUserAuthTokens.forceRefresh)\n}\n\nexport const deleteAuthTokens = async tokens => {\n  await confirm({\n    body: _('deleteAuthTokensConfirmMessage', {\n      nTokens: tokens.length,\n    }),\n    icon: 'user',\n    title: _('deleteAuthTokensConfirm', { nTokens: tokens.length }),\n  })\n  return _call('token.deleteOwn', { tokens: tokens.map(token => token.id) })::tap(subscribeUserAuthTokens.forceRefresh)\n}\n\nexport const editAuthToken = ({ description, id }) =>\n  _call('token.set', {\n    description,\n    id,\n  })::tap(subscribeUserAuthTokens.forceRefresh)\n\nexport const editXsCredentials = xsCredentials =>\n  _setUserPreferences({\n    xsCredentials,\n  })\n\n// User filters --------------------------------------------------\n\nimport AddUserFilterModalBody from './add-user-filter-modal' // eslint-disable-line import/first\nexport const addCustomFilter = (type, value) => {\n  const { user } = xo\n  return confirm({\n    title: _('saveNewFilterTitle'),\n    body: <AddUserFilterModalBody user={user} type={type} value={value} />,\n  }).then(name => {\n    if (name.length === 0) {\n      return error(_('saveNewUserFilterErrorTitle'), _('saveNewUserFilterErrorBody'))\n    }\n\n    const { preferences } = user\n    const filters = (preferences && preferences.filters) || {}\n\n    return _setUserPreferences({\n      filters: {\n        ...filters,\n        [type]: {\n          ...filters[type],\n          [name]: value,\n        },\n      },\n    })\n  })\n}\n\nexport const removeCustomFilter = (type, name) =>\n  confirm({\n    title: _('removeUserFilterTitle'),\n    body: <p>{_('removeUserFilterBody')}</p>,\n  }).then(() => {\n    const { user } = xo\n    const { filters } = user.preferences\n\n    return _setUserPreferences({\n      filters: {\n        ...filters,\n        [type]: {\n          ...filters[type],\n          [name]: undefined,\n        },\n      },\n    })\n  })\n\nexport const editCustomFilter = (type, name, { newName = name, newValue }) => {\n  const { filters } = xo.user.preferences\n  return _setUserPreferences({\n    filters: {\n      ...filters,\n      [type]: {\n        ...filters[type],\n        [name]: undefined,\n        [newName]: newValue || filters[type][name],\n      },\n    },\n  })\n}\n\nexport const setDefaultHomeFilter = (type, name) => {\n  const { user } = xo\n  const { preferences } = user\n  const defaultFilters = (preferences && preferences.defaultHomeFilters) || {}\n\n  return _setUserPreferences({\n    defaultHomeFilters: {\n      ...defaultFilters,\n      [type]: name === null ? undefined : name,\n    },\n  })\n}\n\n// IP pools --------------------------------------------------------------------\n\nexport const createIpPool = ({ name, ips, networks }) => {\n  const addresses = {}\n  forEach(ips, ip => {\n    addresses[ip] = {}\n  })\n  return _call('ipPool.create', {\n    name,\n    addresses,\n    networks: resolveIds(networks),\n  })::tap(subscribeIpPools.forceRefresh)\n}\n\nexport const deleteIpPool = ipPool =>\n  _call('ipPool.delete', { id: resolveId(ipPool) })::tap(subscribeIpPools.forceRefresh)\n\nexport const setIpPool = (ipPool, { name, addresses, networks }) =>\n  _call('ipPool.set', {\n    id: resolveId(ipPool),\n    name,\n    addresses,\n    networks: resolveIds(networks),\n  })::tap(subscribeIpPools.forceRefresh)\n\n// Cloud configs --------------------------------------------------------------------\n\nexport const subscribeCloudConfigs = createSubscription(() => _call('cloudConfig.getAll'))\n\nexport const createCloudConfig = props => _call('cloudConfig.create', props)::tap(subscribeCloudConfigs.forceRefresh)\n\nexport const deleteCloudConfigs = ids => {\n  const { length } = ids\n  if (length === 0) {\n    return\n  }\n\n  const vars = { nCloudConfigs: length }\n  return confirm({\n    title: _('confirmDeleteCloudConfigsTitle', vars),\n    body: <p>{_('confirmDeleteCloudConfigsBody', vars)}</p>,\n  }).then(\n    () =>\n      Promise.all(ids.map(id => _call('cloudConfig.delete', { id: resolveId(id) })))::tap(\n        subscribeCloudConfigs.forceRefresh\n      ),\n    noop\n  )\n}\n\nexport const editCloudConfig = (cloudConfig, props) =>\n  _call('cloudConfig.update', { ...props, id: resolveId(cloudConfig) })::tap(subscribeCloudConfigs.forceRefresh)\n\nexport const subscribeNetworkConfigs = createSubscription(() => _call('cloudConfig.getAllNetworkConfigs'))\n\nexport const createNetworkConfig = props =>\n  _call('cloudConfig.createNetworkConfig', props)::tap(subscribeNetworkConfigs.forceRefresh)\n\nexport const deleteNetworkConfigs = ids => {\n  const { length } = ids\n  if (length === 0) {\n    return\n  }\n\n  const vars = { nNetworkConfigs: length }\n  return confirm({\n    title: _('confirmDeleteNetworkConfigsTitle', vars),\n    body: <p>{_('confirmDeleteNetworkConfigsBody', vars)}</p>,\n  }).then(\n    () =>\n      Promise.all(ids.map(id => _call('cloudConfig.delete', { id: resolveId(id) })))::tap(\n        subscribeNetworkConfigs.forceRefresh\n      ),\n    noop\n  )\n}\n\nexport const editNetworkConfig = (networkConfig, props) =>\n  _call('cloudConfig.update', { ...props, id: resolveId(networkConfig) })::tap(subscribeNetworkConfigs.forceRefresh)\n\n// XO SAN ----------------------------------------------------------------------\n\nexport const getVolumeInfo = (xosanSr, infoType) => _call('xosan.getVolumeInfo', { sr: xosanSr, infoType })\n\nexport const createXosanSR = ({\n  template,\n  pif,\n  vlan,\n  srs,\n  glusterType,\n  redundancy,\n  brickSize,\n  memorySize,\n  ipRange,\n}) => {\n  const promise = _call('xosan.createSR', {\n    template,\n    pif: pif.id,\n    vlan: String(vlan),\n    srs: resolveIds(srs),\n    glusterType,\n    redundancy: Number.parseInt(redundancy),\n    brickSize,\n    memorySize,\n    ipRange,\n  })\n\n  // Force refresh in parallel to get the creation progress sooner\n  subscribeCheckSrCurrentState.forceRefresh()\n\n  return promise\n}\n\nexport const addXosanBricks = (xosansr, lvmsrs, brickSize) => _call('xosan.addBricks', { xosansr, lvmsrs, brickSize })\n\nexport const replaceXosanBrick = (xosansr, previousBrick, newLvmSr, brickSize, onSameVM = false) =>\n  _call('xosan.replaceBrick', resolveIds({ xosansr, previousBrick, newLvmSr, brickSize, onSameVM }))\n\nexport const removeXosanBricks = (xosansr, bricks) => _call('xosan.removeBricks', { xosansr, bricks })\n\nexport const computeXosanPossibleOptions = (lvmSrs, brickSize) =>\n  _call('xosan.computeXosanPossibleOptions', { lvmSrs, brickSize })\n\nexport const registerXosan = () =>\n  _call('cloud.registerResource', { namespace: 'xosan' })::tap(subscribeResourceCatalog.forceRefresh)\n\nexport const fixHostNotInXosanNetwork = (xosanSr, host) => _call('xosan.fixHostNotInNetwork', { xosanSr, host })\n\n// XOSAN packs -----------------------------------------------------------------\n\nexport const getResourceCatalog = ({ filters } = {}) => _call('cloud.getResourceCatalog', { filters })\n\nexport const getAllResourceCatalog = () => _call('cloud.getAllResourceCatalog')\n\nconst downloadAndInstallXosanPack = (pack, pool, { version }) =>\n  _call('xosan.downloadAndInstallXosanPack', {\n    id: resolveId(pack),\n    version,\n    pool: resolveId(pool),\n  })\n\nexport const downloadAndInstallResource = ({ namespace, id, version, sr, templateOnly }) =>\n  _call('cloud.downloadAndInstallResource', {\n    namespace,\n    id,\n    version,\n    sr: resolveId(sr),\n    templateOnly,\n  })\n\nimport UpdateXosanPacksModal from './update-xosan-packs-modal' // eslint-disable-line import/first\nexport const updateXosanPacks = pool =>\n  confirm({\n    title: _('xosanUpdatePacks'),\n    icon: 'host-patch-update',\n    body: <UpdateXosanPacksModal pool={pool} />,\n  }).then(pack => {\n    if (pack === undefined) {\n      return\n    }\n\n    return downloadAndInstallXosanPack(pack, pool, { version: pack.version })\n  })\n\n// XOSTOR   --------------------------------------------------------------------\n\nexport const createXostorSr = params => _call('xostor.create', params)\n\nexport const destroyXostorInterfaces = async (sr, ifaceNames) => {\n  const srId = resolveId(sr)\n  await Promise.all(ifaceNames.map(ifaceName => _call('xostor.destroyInterface', { sr: srId, name: ifaceName })))::tap(\n    () => subscribeXostorInterfaces.forceRefresh(sr)\n  )\n}\n\nexport const createXostorInterface = async sr => {\n  const { interfaceName, networkId } = await form({\n    render: props => <NetworkCard {...props} insideModalForm sr={sr} />,\n    defaultValue: {\n      interfaceName: '',\n      networkId: undefined,\n    },\n    header: (\n      <span>\n        <Icon icon='add' /> {_('createInterface')}\n      </span>\n    ),\n  })\n\n  await _call('xostor.createInterface', { sr: resolveId(sr), network: networkId, name: interfaceName })::tap(() =>\n    subscribeXostorInterfaces.forceRefresh(sr)\n  )\n}\nexport const setXostor = (sr, params) => _call('xostor.set', { sr: resolveId(sr), ...params })\n\n// Licenses --------------------------------------------------------------------\n\nexport const getLicenses = ({ productType } = {}) => _call('xoa.licenses.getAll', { productType })\n\nexport const getLicense = (productId, boundObjectId) => _call('xoa.licenses.get', { productId, boundObjectId })\n\nexport const unlockXosan = (licenseId, srId) => _call('xosan.unlock', { licenseId, sr: srId })\n\nexport const bindLicense = (licenseId, boundObjectId) => _call('xoa.licenses.bind', { licenseId, boundObjectId })\n\nexport const rebindObjectLicense = (boundObjectId, licenseId, productId) =>\n  _call('xoa.licenses.rebindObject', { boundObjectId, licenseId, productId })\n\nexport const bindXcpngLicense = (licenseId, boundObjectId) =>\n  bindLicense(licenseId, boundObjectId)::tap(subscribeXcpngLicenses.forceRefresh)\n\nexport const rebindLicense = (licenseType, licenseId, oldBoundObjectId, newBoundObjectId) =>\n  _call('xoa.licenses.rebind', { licenseId, oldBoundObjectId, newBoundObjectId })::tap(() => {\n    if (licenseType === 'xcpng-standard' || licenseType === 'xcpng-enterprise') {\n      return subscribeXcpngLicenses.forceRefresh()\n    }\n  })\n\nexport const selfBindLicense = ({ id, plan, oldXoaId }) =>\n  confirm({\n    title: _('bindXoaLicense'),\n    body: _('bindXoaLicenseConfirm'),\n    strongConfirm: {\n      messageId: 'bindXoaLicenseConfirmText',\n      values: { licenseType: plan },\n    },\n    icon: 'unlock',\n  })\n    .then(() => _call('xoa.licenses.bindToSelf', { licenseId: id, oldXoaId }), noop)\n    ::tap(subscribeSelfLicenses.forceRefresh)\n\nexport const subscribeSelfLicenses = createSubscription(() =>\n  _call('xoa.licenses.getSelf').then(licenses => {\n    if (!Array.isArray(licenses)) {\n      if (licenses?.state === 'register-needed') {\n        return []\n      }\n\n      throw new Error(licenses?.message || 'Could not fetch licenses')\n    }\n\n    return licenses\n  })\n)\n\nconst createLicenseSubscription = productType =>\n  createSubscription(() =>\n    getXoaPlan() !== SOURCES && store.getState().user.permission === 'admin'\n      ? _call('xoa.licenses.getAll', { productType })\n      : undefined\n  )\n\nexport const subscribeXcpngLicenses = createLicenseSubscription('xcpng')\n\nexport const subscribeXostorLicenses = createLicenseSubscription('xostor')\n\n// Support --------------------------------------------------------------------\n\nexport const clearXoaCheckCache = () => _call('xoa.clearCheckCache')\n\nexport const checkXoa = () => _call('xoa.check')\n\nexport const closeTunnel = () => _call('xoa.supportTunnel.close')::tap(subscribeTunnelState.forceRefresh)\n\nexport const openTunnel = () =>\n  _call('xoa.supportTunnel.open')::tap(() => {\n    subscribeTunnelState.forceRefresh()\n    // After 1s, we most likely got the tunnel ID\n    // and we don't want to wait another 5s to show it to the user.\n    setTimeout(subscribeTunnelState.forceRefresh, 1000)\n  })\n\nexport const subscribeTunnelState = createSubscription(() => _call('xoa.supportTunnel.getState'))\n\nexport const getApplianceInfo = () => _call('xoa.getApplianceInfo')\n\nexport const getApiApplianceInfo = () => fetch('./rest/v0/appliance').then(resp => resp.json())\n\nexport const restartXoServer = async () => {\n  await confirm({\n    icon: 'restart',\n    title: _('restartXoServer'),\n    body: _('restartXoServerConfirm'),\n    strongConfirm: {\n      messageId: 'restartXoServer',\n    },\n  })\n\n  await _call('xoa.restartXoServer')\n}\n\n// Proxy --------------------------------------------------------------------\n\nexport const getAllProxies = () => _call('proxy.getAll')\n\nexport const createProxyTrialLicense = () => _call('xoa.licenses.createProxyTrial')\n\nexport const deployProxyAppliance = (license, sr, { network, proxy, ...props } = {}) =>\n  _call('proxy.deploy', {\n    license: resolveId(license),\n    network: resolveId(network),\n    proxy: resolveId(proxy),\n    sr: resolveId(sr),\n    ...props,\n  })::tap(subscribeProxies.forceRefresh)\n\nexport const registerProxy = async () => {\n  const getStringOrUndefined = string => (string.trim() === '' ? undefined : string)\n\n  const { address, authenticationToken, name, vmUuid } = await confirm({\n    body: <RegisterProxyModal />,\n    icon: 'connect',\n    title: _('registerProxy'),\n  })\n\n  const proxyId = await registerProxyApplicance({\n    address: getStringOrUndefined(address),\n    authenticationToken: getStringOrUndefined(authenticationToken),\n    name: getStringOrUndefined(name),\n    vmUuid: getStringOrUndefined(vmUuid),\n  })\n  const _isProxyWorking = await isProxyWorking(proxyId).catch(err => {\n    console.error('isProxyWorking error:', err)\n    return false\n  })\n  if (!_isProxyWorking) {\n    await confirm({\n      body: _('proxyConnectionFailedAfterRegistrationMessage'),\n      title: _('proxyError'),\n    })\n    await forgetProxyAppliances([proxyId])\n  }\n}\n\nexport const registerProxyApplicance = proxyInfo =>\n  _call('proxy.register', proxyInfo)::tap(subscribeProxies.forceRefresh)\n\nexport const editProxyAppliance = (proxy, { vm, ...props }) =>\n  _call('proxy.update', {\n    id: resolveId(proxy),\n    vm: resolveId(vm),\n    ...props,\n  })::tap(subscribeProxies.forceRefresh)\n\nconst _forgetProxyAppliance = proxy => _call('proxy.unregister', { id: resolveId(proxy) })\nexport const forgetProxyAppliances = proxies =>\n  confirm({\n    title: _('forgetProxyApplianceTitle', { n: proxies.length }),\n    body: _('forgetProxyApplianceMessage', { n: proxies.length }),\n  }).then(() => Promise.all(map(proxies, _forgetProxyAppliance))::tap(subscribeProxies.forceRefresh))\n\nconst _destroyProxyAppliance = proxy => _call('proxy.destroy', { id: resolveId(proxy) })\nexport const destroyProxyAppliances = proxies =>\n  confirm({\n    title: _('destroyProxyApplianceTitle', { n: proxies.length }),\n    body: _('destroyProxyApplianceMessage', { n: proxies.length }),\n  }).then(() => Promise.all(map(proxies, _destroyProxyAppliance))::tap(subscribeProxies.forceRefresh))\n\nexport const upgradeProxyAppliance = (proxy, props) =>\n  _call('proxy.upgradeAppliance', { id: resolveId(proxy), ...props })\n\nexport const openTunnelOnProxy = async proxy => {\n  const result = await _call('proxy.openSupportTunnel', { id: resolveId(proxy) }).catch(err => err.message)\n  await alert(_('supportTunnel'), <pre>{result}</pre>)\n}\n\nexport const getProxyApplianceUpdaterState = id => _call('proxy.getApplianceUpdaterState', { id })\n\nexport const updateProxyApplianceSettings = (id, props) => _call('proxy.updateApplianceSettings', { id, ...props })\n\nconst PROXY_HEALTH_CHECK_COMMON_ERRORS_CODE = new Set([\n  'ECONNREFUSED',\n  'ECONNRESET',\n  'EHOSTUNREACH',\n  'ENOTFOUND',\n  'ETIMEDOUT',\n])\n\nexport const checkProxyHealth = async proxy => {\n  const result = await _call('proxy.checkHealth', {\n    id: resolveId(proxy),\n  })\n  return result.success\n    ? success(\n        <span>\n          <Icon icon='success' /> {_('proxyTestSuccess', { name: proxy.name })}\n        </span>,\n        _('proxyTestSuccessMessage')\n      )\n    : error(\n        <span>\n          <Icon icon='error' /> {_('proxyTestFailed', { name: proxy.name })}\n        </span>,\n        <span>\n          {PROXY_HEALTH_CHECK_COMMON_ERRORS_CODE.has(result.error.code)\n            ? _('proxyTestFailedConnectionIssueMessage')\n            : result.error.message}\n        </span>\n      )\n}\n\nexport const isProxyWorking = async proxy => (await _call('proxy.checkHealth', { id: resolveId(proxy) })).success\n\n// Audit plugin ---------------------------------------------------------\n\nconst METHOD_NOT_FOUND_CODE = -32601\nexport const fetchAuditRecords = async () => {\n  try {\n    const { $getFrom } = await _call('audit.getRecords', { ndjson: true })\n    const response = await fetch(`.${$getFrom}`)\n    const data = await response.text()\n\n    const records = []\n    parseNdJson(data, record => {\n      records.push(record)\n    })\n    return records\n  } catch (error) {\n    if (error.code === METHOD_NOT_FOUND_CODE) {\n      return []\n    }\n    throw error\n  }\n}\n\nexport const exportAuditRecords = () =>\n  _call('audit.exportRecords').then(({ $getFrom: url }) => {\n    window.open(`.${url}`)\n  })\n\nexport const importAuditRecords = async recordsFile => {\n  const { $sendTo } = await _call('audit.importRecords', { zipped: recordsFile.type === 'application/gzip' })\n  const response = await post($sendTo, recordsFile)\n  const text = await response.text()\n\n  if (response.status !== 200) {\n    throw new Error(text)\n  }\n\n  try {\n    return JSON.parse(text)\n  } catch (error) {\n    throw new Error(`Body is not a JSON, original message is : ${text}`)\n  }\n}\n\nexport const checkAuditRecordsIntegrity = (oldest, newest) => _call('audit.checkIntegrity', { oldest, newest })\n\nexport const generateAuditFingerprint = oldest => _call('audit.generateFingerprint', { oldest })\n\n// LDAP ------------------------------------------------------------------------\n\nexport const synchronizeLdapGroups = () =>\n  confirm({\n    title: _('syncLdapGroups'),\n    body: _('syncLdapGroupsWarning'),\n    icon: 'refresh',\n  }).then(() => _call('ldap.synchronizeGroups')::tap(subscribeGroups.forceRefresh), noop)\n\n// Netbox plugin ---------------------------------------------------------------\n\nexport const synchronizeNetbox = pools =>\n  confirm({\n    title: _('syncNetbox'),\n    body: _('syncNetboxWarning'),\n    icon: 'refresh',\n  }).then(() => _call('netbox.synchronize', { pools: resolveIds(pools) }))\n\n// ESXi import ---------------------------------------------------------------\n\nexport const esxiListVms = (host, user, password, sslVerify) =>\n  _call('esxi.listVms', { host, user, password, sslVerify })\n\nexport const importVmsFromEsxi = params => _call('vm.importMultipleFromEsxi', params)\n\n// GitHub API ---------------------------------------------------------------\nconst _callGithubApi = async (endpoint = '') => {\n  const url = new URL('https://api.github.com/repos/vatesfr/xen-orchestra')\n  url.pathname += endpoint\n  const resp = await fetch(url.toString())\n  const json = await resp.json()\n  if (resp.ok) {\n    return json\n  } else {\n    throw new Error(json.message)\n  }\n}\n\nexport const getMasterCommit = () => _callGithubApi('/commits/master')\n\nexport const compareCommits = (base, head) => _callGithubApi(`/compare/${base}...${head}`)\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport Tooltip from 'tooltip'\nimport { Container, Col } from 'grid'\nimport { connectStore } from 'utils'\nimport {\n  createCollectionWrapper,\n  createCounter,\n  createGetObject,\n  createGetObjectsOfType,\n  createSelector,\n} from 'selectors'\nimport forEach from 'lodash/forEach.js'\nimport { SelectSr } from 'select-objects'\nimport { Vm } from 'render-xo-item'\nimport { ejectCd, isSrWritable, setDefaultSr } from 'xo'\n\n@connectStore(\n  () => {\n    const getPool = createGetObject((_, props) => props.pool)\n    return {\n      pool: getPool,\n      poolMaster: createGetObject(createSelector(getPool, ({ master }) => master)),\n      vbds: createGetObjectsOfType('VBD').filter(\n        createSelector(\n          getPool,\n          ({ id }) =>\n            vbd =>\n              vbd.$pool === id\n        )\n      ),\n    }\n  },\n  { withRef: true }\n)\nexport default class InstallPoolPatchesModalBody extends Component {\n  _getVmsWithCds = createSelector(\n    () => this.props.vbds,\n    createCollectionWrapper(vbds => {\n      const vmIds = []\n      forEach(vbds, vbd => {\n        if (vbd.is_cd_drive && vbd.VDI !== undefined && vbd.attached && !vmIds.includes(vbd.VM)) {\n          vmIds.push(vbd.VM)\n        }\n      })\n      return vmIds\n    })\n  )\n\n  _getNVmsWithCds = createCounter(this._getVmsWithCds)\n\n  _ejectCds = () => Promise.all(this._getVmsWithCds().map(ejectCd))\n\n  _getTooltip = createSelector(this._getVmsWithCds, vmIds =>\n    vmIds.map(vmId => (\n      <p className='m-0' key={vmId}>\n        <Vm id={vmId} />\n      </p>\n    ))\n  )\n\n  render() {\n    const { pool, poolMaster } = this.props\n    const needDefaultSr = poolMaster.productBrand !== 'XCP-ng' && pool.default_SR === undefined\n    const someCdsInserted = this._getNVmsWithCds() > 0\n\n    return (\n      <Container>\n        {!needDefaultSr && !someCdsInserted && (\n          <SingleLineRow>\n            <Col>{_('confirmPoolPatch')}</Col>\n          </SingleLineRow>\n        )}\n        {needDefaultSr && [\n          <SingleLineRow className='mt-1' key='message'>\n            <Col>\n              <Icon icon='alarm' /> {_('poolNeedsDefaultSr')}\n            </Col>\n          </SingleLineRow>,\n          <SingleLineRow className='mt-1' key='select'>\n            <Col size={6}>{_('setDefaultSr')}</Col>\n            <Col size={5}>\n              <div className='input-group'>\n                <SelectSr\n                  onChange={this.linkState('sr')}\n                  predicate={sr => sr.$pool === pool.id && isSrWritable(sr)}\n                  value={this.state.sr}\n                />\n                <span className='input-group-btn'>\n                  <ActionButton handler={setDefaultSr} handlerParam={this.state.sr} icon='save'>\n                    {_('formOk')}\n                  </ActionButton>\n                </span>\n              </div>\n            </Col>\n          </SingleLineRow>,\n        ]}\n        {someCdsInserted && (\n          <SingleLineRow className='mt-1'>\n            <Tooltip content={this._getTooltip()}>\n              <Col size={6}>\n                <Icon icon='alarm' /> {_('vmsHaveCds', { nVms: this._getNVmsWithCds() })}\n              </Col>\n            </Tooltip>\n            <Col size={6}>\n              <ActionButton icon='vm-eject' handler={this._ejectCds}>\n                {_('ejectCds')}\n              </ActionButton>\n            </Col>\n          </SingleLineRow>\n        )}\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport { Container, Col } from 'grid'\nimport { createCompare, createCompareContainers } from 'utils'\nimport { createSelector } from 'selectors'\nimport { SelectResourceSetsSr, SelectSr as SelectAnySr } from 'select-objects'\n\nimport { isSrIso, isSrShared, isSrWritable } from '../'\n\nconst compareSrs = createCompare([isSrShared])\n\nexport default class MigrateVdiModalBody extends Component {\n  static propTypes = {\n    pool: PropTypes.string.isRequired,\n    resourceSet: PropTypes.object,\n    warningBeforeMigrate: PropTypes.func.isRequired,\n  }\n\n  get value() {\n    return this.state\n  }\n\n  _getCompareContainers = createSelector(() => this.props.pool, createCompareContainers)\n\n  _getWarningBeforeMigrate = createSelector(\n    () => this.props.warningBeforeMigrate,\n    () => this.state.sr,\n    (warningBeforeMigrate, sr) => warningBeforeMigrate(sr)\n  )\n\n  _getSrPredicate = createSelector(\n    () => this.props.pool,\n    () => this.props.isoSr,\n    (pool, isoSr) => sr => (isoSr ? isSrIso(sr) : isSrWritable(sr)) && sr.$pool === pool\n  )\n\n  render() {\n    const { resourceSet } = this.props\n    const warningBeforeMigrate = this._getWarningBeforeMigrate()\n    const SelectSr = resourceSet !== undefined ? SelectResourceSetsSr : SelectAnySr\n    return (\n      <Container>\n        <SingleLineRow>\n          <Col size={6}>{_('vdiMigrateSelectSr')}</Col>\n          <Col size={6}>\n            <SelectSr\n              compareContainers={this._getCompareContainers()}\n              compareOptions={compareSrs}\n              onChange={this.linkState('sr')}\n              predicate={this._getSrPredicate()}\n              required\n              resourceSet={resourceSet}\n              value={this.state.sr}\n            />\n          </Col>\n        </SingleLineRow>\n        {warningBeforeMigrate !== null && (\n          <SingleLineRow>\n            <Col>{warningBeforeMigrate}</Col>\n          </SingleLineRow>\n        )}\n      </Container>\n    )\n  }\n}\n","module.exports = {\n    \"listTitle\": \"mc9c3d73b2_listTitle\",\n    \"listItem\": \"mc9c3d73b2_listItem\",\n    \"block\": \"mc9c3d73b2_block\",\n    \"groupBlock\": \"mc9c3d73b2_groupBlock\"\n};","import BaseComponent from 'base-component'\nimport every from 'lodash/every'\nimport find from 'lodash/find'\nimport forEach from 'lodash/forEach'\nimport isEmpty from 'lodash/isEmpty'\nimport map from 'lodash/map'\nimport React from 'react'\nimport store from 'store'\n\nimport _ from '../../intl'\nimport ChooseSrForEachVdisModal from '../choose-sr-for-each-vdis-modal'\nimport invoke from '../../invoke'\nimport SingleLineRow from '../../single-line-row'\nimport { Col } from '../../grid'\nimport { connectStore, mapPlus, resolveId, resolveIds } from '../../utils'\nimport { getDefaultNetworkForVif, getDefaultMigrationNetwork } from '../utils'\nimport { SelectHost, SelectNetwork } from '../../select-objects'\nimport { createGetObjectsOfType, createPicker, createSelector, getObject } from '../../selectors'\n\nimport { isSrShared, isSrWritable } from '../'\n\nimport styles from './index.css'\n\n@connectStore(\n  () => {\n    const getVm = (_, props) => props.vm\n\n    const getVbds = createGetObjectsOfType('VBD')\n      .pick((state, props) => getVm(state, props).$VBDs)\n      .sort()\n\n    const getVdis = createGetObjectsOfType('VDI').pick(\n      createSelector(getVbds, vbds =>\n        mapPlus(vbds, (vbd, push) => {\n          if (!vbd.is_cd_drive && vbd.VDI) {\n            push(vbd.VDI)\n          }\n        })\n      )\n    )\n\n    const getVifs = createGetObjectsOfType('VIF')\n      .pick(createSelector(getVm, vm => vm.VIFs))\n      .sort()\n\n    const getPifs = createGetObjectsOfType('PIF')\n    const getNetworks = createGetObjectsOfType('network')\n    const getPools = createGetObjectsOfType('pool')\n\n    return {\n      networks: getNetworks,\n      pifs: getPifs,\n      pools: getPools,\n      vbds: getVbds,\n      vdis: getVdis,\n      vifs: getVifs,\n    }\n  },\n  { withRef: true }\n)\nexport default class MigrateVmModalBody extends BaseComponent {\n  constructor(props) {\n    super(props)\n\n    this.state = {\n      mapVifsNetworks: {},\n      targetSrs: {},\n      host: props.host || undefined,\n      intraPool: props.host ? props.vm.$pool === props.host.$pool : undefined,\n    }\n\n    this._getHostPredicate = createSelector(\n      () => this.props.vm,\n      ({ $container }) =>\n        host =>\n          host.id !== $container\n    )\n\n    this._getSrPredicate = createSelector(\n      () => this.state.host,\n      host => (host ? sr => isSrWritable(sr) && (sr.$container === host.id || sr.$container === host.$pool) : false)\n    )\n\n    this._getTargetNetworkPredicate = createSelector(\n      () => this.props.networks,\n      () => this.state.host.$poolId,\n      (networks, poolId) => {\n        const _networks = {}\n        forEach(networks, network => {\n          if (network.$poolId === poolId) {\n            _networks[network.id] = true\n          }\n        })\n        return isEmpty(_networks) ? false : network => _networks[network.id]\n      }\n    )\n\n    this._getMigrationNetworkPredicate = createSelector(\n      createPicker(\n        () => this.props.pifs,\n        () => this.state.host.$PIFs\n      ),\n      pifs => {\n        if (!pifs) {\n          return false\n        }\n\n        const networks = {}\n        forEach(pifs, pif => {\n          pif.ip && (networks[pif.$network] = true)\n        })\n\n        return network => networks[network.id]\n      }\n    )\n  }\n\n  get value() {\n    return {\n      mapVdisSrs: resolveIds(this.state.targetSrs.mapVdisSrs),\n      mapVifsNetworks: this.state.mapVifsNetworks,\n      migrationNetwork: this.state.migrationNetworkId,\n      sr: resolveId(this.state.targetSrs.mainSr),\n      targetHost: this.state.host && this.state.host.id,\n      srRequired: !this.state.doNotMigrateVdis,\n    }\n  }\n\n  _getObject(id) {\n    return getObject(store.getState(), id)\n  }\n\n  _selectHost = host => {\n    // No host selected\n    if (!host) {\n      this.setState({\n        host: undefined,\n        intraPool: undefined,\n      })\n      return\n    }\n\n    const { pifs, pools, vbds, vm } = this.props\n    const intraPool = vm.$pool === host.$pool\n    // Intra-pool\n    if (intraPool) {\n      let doNotMigrateVdis\n      if (vm.$container === host.id) {\n        doNotMigrateVdis = true\n      } else {\n        const _doNotMigrateVdi = {}\n        forEach(vbds, vbd => {\n          if (vbd.VDI != null) {\n            _doNotMigrateVdi[vbd.VDI] = isSrShared(this._getObject(this._getObject(vbd.VDI).$SR))\n          }\n        })\n        doNotMigrateVdis = every(_doNotMigrateVdi)\n      }\n\n      this.setState({\n        doNotMigrateVdis,\n        host,\n        intraPool,\n        mapVifsNetworks: undefined,\n        migrationNetworkId: getDefaultMigrationNetwork(intraPool, host, pools, pifs),\n        targetSrs: {},\n      })\n      return\n    }\n\n    // Inter-pool\n    const { networks, vifs } = this.props\n\n    const defaultNetwork = invoke(() => {\n      // First PIF with an IP.\n      const pifId = find(host.$PIFs, pif => pifs[pif].ip)\n      const pif = pifId && pifs[pifId]\n\n      return pif && pif.$network\n    })\n\n    const defaultNetworksForVif = {}\n    forEach(vifs, vif => {\n      defaultNetworksForVif[vif.id] = getDefaultNetworkForVif(vif, host, pifs, networks) || defaultNetwork\n    })\n\n    this.setState({\n      doNotMigrateVdis: false,\n      host,\n      intraPool,\n      mapVifsNetworks: defaultNetworksForVif,\n      migrationNetworkId: getDefaultMigrationNetwork(intraPool, host, pools, pifs),\n      targetSrs: { mainSr: pools[host.$pool].default_SR },\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  _selectMigrationNetwork = migrationNetwork =>\n    this.setState({\n      migrationNetworkId: migrationNetwork == null ? undefined : migrationNetwork.id,\n    })\n\n  render() {\n    const { vdis, vifs, networks } = this.props\n    const { doNotMigrateVdis, host, intraPool, mapVifsNetworks, migrationNetworkId, targetSrs } = this.state\n    return (\n      <div>\n        <div className={styles.block}>\n          <SingleLineRow>\n            <Col size={6}>{_('migrateVmSelectHost')}</Col>\n            <Col size={6}>\n              <SelectHost\n                compareContainers={this.compareContainers}\n                onChange={this._selectHost}\n                predicate={this._getHostPredicate()}\n                required\n                value={host}\n              />\n            </Col>\n          </SingleLineRow>\n        </div>\n        {host && (\n          <div className={styles.groupBlock}>\n            <SingleLineRow>\n              <Col size={12}>\n                <ChooseSrForEachVdisModal\n                  mainSrPredicate={this._getSrPredicate()}\n                  onChange={this.linkState('targetSrs')}\n                  required={!doNotMigrateVdis}\n                  value={targetSrs}\n                  vdis={vdis}\n                />\n              </Col>\n            </SingleLineRow>\n          </div>\n        )}\n        {host !== undefined && (\n          <div className={styles.groupBlock}>\n            <SingleLineRow>\n              <Col size={6}>{_('migrateVmSelectMigrationNetwork')}</Col>\n              <Col size={6}>\n                <SelectNetwork\n                  onChange={this._selectMigrationNetwork}\n                  predicate={this._getMigrationNetworkPredicate()}\n                  required={!intraPool}\n                  value={migrationNetworkId}\n                />\n              </Col>\n            </SingleLineRow>\n            {intraPool && <i>{_('optionalEntry')}</i>}\n          </div>\n        )}\n        {intraPool !== undefined && !intraPool && (\n          <div>\n            <div className={styles.groupBlock}>\n              <SingleLineRow>\n                <Col>{_('migrateVmSelectNetworks')}</Col>\n              </SingleLineRow>\n              <br />\n              <SingleLineRow>\n                <Col size={6}>\n                  <span className={styles.listTitle}>{_('migrateVmVif')}</span>\n                </Col>\n                <Col size={6}>\n                  <span className={styles.listTitle}>{_('migrateVmNetwork')}</span>\n                </Col>\n              </SingleLineRow>\n              {map(vifs, vif => (\n                <div className={styles.listItem} key={vif.id}>\n                  <SingleLineRow>\n                    <Col size={6}>\n                      {vif.MAC} ({networks[vif.$network].name_label})\n                    </Col>\n                    <Col size={6}>\n                      <SelectNetwork\n                        onChange={network =>\n                          this.setState({\n                            mapVifsNetworks: {\n                              ...mapVifsNetworks,\n                              [vif.id]: network.id,\n                            },\n                          })\n                        }\n                        predicate={this._getTargetNetworkPredicate()}\n                        value={mapVifsNetworks[vif.id]}\n                      />\n                    </Col>\n                  </SingleLineRow>\n                </div>\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n","import BaseComponent from 'base-component'\nimport every from 'lodash/every'\nimport flatten from 'lodash/flatten'\nimport forEach from 'lodash/forEach'\nimport filter from 'lodash/filter'\nimport find from 'lodash/find'\nimport isEmpty from 'lodash/isEmpty'\nimport map from 'lodash/map'\nimport React from 'react'\nimport some from 'lodash/some'\nimport store from 'store'\n\nimport _ from '../../intl'\nimport Icon from 'icon'\nimport invoke from '../../invoke'\nimport SingleLineRow from '../../single-line-row'\nimport Tooltip from '../../tooltip'\nimport { Col } from '../../grid'\nimport { getDefaultNetworkForVif, getDefaultMigrationNetwork } from '../utils'\nimport { SelectHost, SelectNetwork, SelectSr } from '../../select-objects'\nimport { connectStore, createCompare } from '../../utils'\nimport { createGetObjectsOfType, createPicker, createSelector, getObject } from '../../selectors'\nimport { isSrShared } from 'xo'\n\nimport { isSrWritable } from '../'\n\nconst LINE_STYLE = { paddingBottom: '1em' }\n\n@connectStore(\n  () => {\n    const getNetworks = createGetObjectsOfType('network')\n    const getPifs = createGetObjectsOfType('PIF')\n    const getPools = createGetObjectsOfType('pool')\n\n    const getVms = createGetObjectsOfType('VM').pick((_, props) => props.vms)\n\n    const getVbdsByVm = createGetObjectsOfType('VBD')\n      .pick(createSelector(getVms, vms => flatten(map(vms, vm => vm.$VBDs))))\n      .groupBy('VM')\n\n    const getVifsByVM = createGetObjectsOfType('VIF')\n      .pick(createSelector(getVms, vms => flatten(map(vms, vm => vm.VIFs))))\n      .groupBy('$VM')\n\n    return {\n      networks: getNetworks,\n      pifs: getPifs,\n      pools: getPools,\n      vbdsByVm: getVbdsByVm,\n      vifsByVm: getVifsByVM,\n      vms: getVms,\n    }\n  },\n  { withRef: true }\n)\nexport default class MigrateVmsModalBody extends BaseComponent {\n  constructor(props) {\n    super(props)\n\n    this._getHostPredicate = createSelector(\n      () => this.props.vms,\n      vms => host => some(vms, vm => host.id !== vm.$container)\n    )\n\n    this._getSrPredicate = createSelector(\n      () => this.state.host,\n      host => (host ? sr => isSrWritable(sr) && (sr.$container === host.id || sr.$container === host.$pool) : false)\n    )\n\n    this._getTargetNetworkPredicate = createSelector(\n      () => this.props.networks,\n      () => this.state.host.$poolId,\n      (networks, poolId) => {\n        const _networks = {}\n        forEach(networks, network => {\n          if (network.$poolId === poolId) {\n            _networks[network.id] = true\n          }\n        })\n        return isEmpty(_networks) ? false : network => _networks[network.id]\n      }\n    )\n\n    this._getMigrationNetworkPredicate = createSelector(\n      createPicker(\n        () => this.props.pifs,\n        () => this.state.host.$PIFs\n      ),\n      pifs => {\n        if (!pifs) {\n          return false\n        }\n\n        const networks = {}\n        forEach(pifs, pif => {\n          pif.ip && (networks[pif.$network] = true)\n        })\n\n        return network => networks[network.id]\n      }\n    )\n  }\n\n  get value() {\n    const { host } = this.state\n    const vms = filter(this.props.vms, vm => vm.$container !== host.id)\n    if (!host || isEmpty(vms)) {\n      return { vms }\n    }\n    const { networks, pifs, vbdsByVm, vifsByVm } = this.props\n    const {\n      doNotMigrateVdi,\n      doNotMigrateVmVdis,\n      migrationNetworkId,\n      noVdisMigration,\n      networkId,\n      smartVifMapping,\n      srId,\n    } = this.state\n\n    // Map VM --> ( Map VDI --> SR )\n    // - Intra-pool: a VDI will only be migrated to the selected SR if the VDI was on a local SR or if an SR was explicitly selected.\n    // - Inter-pool: all VDIs will be migrated to the selected SR.\n    const mapVmsMapVdisSrs = {}\n    forEach(vbdsByVm, (vbds, vm) => {\n      const mapVdisSrs = {}\n      forEach(vbds, vbd => {\n        const vdi = vbd.VDI\n        if (!vbd.is_cd_drive && vdi) {\n          if (!doNotMigrateVmVdis[vm] && !doNotMigrateVdi[vdi]) {\n            mapVdisSrs[vdi] = srId\n          }\n        }\n      })\n      if (!isEmpty(mapVdisSrs)) {\n        mapVmsMapVdisSrs[vm] = mapVdisSrs\n      }\n    })\n\n    const defaultNetwork =\n      smartVifMapping &&\n      invoke(() => {\n        // First PIF with an IP.\n        const pifId = find(host.$PIFs, pif => pifs[pif].ip)\n        const pif = pifId && pifs[pifId]\n\n        return pif && pif.$network\n      })\n\n    // Map VM --> ( Map VIF --> network )\n    const mapVmsMapVifsNetworks = {}\n    forEach(vms, vm => {\n      if (vm.$pool === host.$pool) {\n        return\n      }\n      const mapVifsNetworks = {}\n      forEach(vifsByVm[vm.id], vif => {\n        mapVifsNetworks[vif.id] = smartVifMapping\n          ? getDefaultNetworkForVif(vif, host, pifs, networks) || defaultNetwork\n          : networkId\n      })\n      mapVmsMapVifsNetworks[vm.id] = mapVifsNetworks\n    })\n\n    return {\n      mapVmsMapVdisSrs,\n      mapVmsMapVifsNetworks,\n      migrationNetwork: migrationNetworkId,\n      sr: srId,\n      srRequired: !noVdisMigration,\n      targetHost: host.id,\n      vms,\n    }\n  }\n\n  _getObject(id) {\n    return getObject(store.getState(), id)\n  }\n\n  _selectHost = host => {\n    if (!host) {\n      this.setState({ targetHost: undefined })\n      return\n    }\n    const { pools, pifs } = this.props\n    const intraPool = every(this.props.vms, vm => vm.$pool === host.$pool)\n    const defaultMigrationNetworkId = getDefaultMigrationNetwork(intraPool, host, pools, pifs)\n    const defaultSrId = pools[host.$pool].default_SR\n    const defaultSrConnectedToHost = some(host.$PBDs, pbd => this._getObject(pbd).SR === defaultSrId)\n\n    const doNotMigrateVmVdis = {}\n    const doNotMigrateVdi = {}\n    let noVdisMigration = false\n    if (intraPool) {\n      forEach(this.props.vbdsByVm, (vbds, vm) => {\n        if (this._getObject(vm).$container === host.id) {\n          doNotMigrateVmVdis[vm] = true\n          return\n        }\n        const _doNotMigrateVdi = {}\n        forEach(vbds, vbd => {\n          if (vbd.VDI != null) {\n            doNotMigrateVdi[vbd.VDI] = _doNotMigrateVdi[vbd.VDI] = isSrShared(\n              this._getObject(this._getObject(vbd.VDI).$SR)\n            )\n          }\n        })\n        doNotMigrateVmVdis[vm] = every(_doNotMigrateVdi)\n      })\n      noVdisMigration = every(doNotMigrateVmVdis)\n    }\n    this.setState({\n      defaultSrConnectedToHost,\n      defaultSrId,\n      host,\n      intraPool,\n      doNotMigrateVdi,\n      doNotMigrateVmVdis,\n      migrationNetworkId: defaultMigrationNetworkId,\n      networkId: defaultMigrationNetworkId,\n      noVdisMigration,\n      smartVifMapping: true,\n      srId: !noVdisMigration && defaultSrConnectedToHost ? defaultSrId : undefined,\n    })\n  }\n\n  getCompareContainers = createSelector(\n    () => this.props.vms,\n    vms => createCompare([pool => some(vms, vm => vm.$pool === pool.id)])\n  )\n\n  _selectMigrationNetwork = migrationNetwork =>\n    this.setState({ migrationNetworkId: migrationNetwork == null ? undefined : migrationNetwork.id })\n  _selectNetwork = network => this.setState({ networkId: network.id })\n  _selectSr = sr => this.setState({ srId: sr.id })\n  _toggleSmartVifMapping = () => this.setState({ smartVifMapping: !this.state.smartVifMapping })\n\n  render() {\n    const {\n      defaultSrConnectedToHost,\n      defaultSrId,\n      host,\n      intraPool,\n      migrationNetworkId,\n      networkId,\n      noVdisMigration,\n      smartVifMapping,\n      srId,\n    } = this.state\n    return (\n      <div>\n        <div style={LINE_STYLE}>\n          <SingleLineRow>\n            <Col size={6}>{_('migrateVmSelectHost')}</Col>\n            <Col size={6}>\n              <SelectHost\n                compareContainers={this.getCompareContainers()}\n                onChange={this._selectHost}\n                predicate={this._getHostPredicate()}\n                value={host}\n              />\n            </Col>\n          </SingleLineRow>\n        </div>\n        {host !== undefined && [\n          <div key='sr' style={LINE_STYLE}>\n            <SingleLineRow>\n              <Col size={6}>\n                {_('selectDestinationSr')}{' '}\n                {(defaultSrId === undefined || !defaultSrConnectedToHost) && (\n                  <Tooltip\n                    content={\n                      defaultSrId !== undefined\n                        ? _('migrateVmNotConnectedDefaultSrError')\n                        : _('migrateVmNoDefaultSrError')\n                    }\n                  >\n                    <Icon\n                      icon={defaultSrId !== undefined ? 'alarm' : 'info'}\n                      className={defaultSrId !== undefined ? 'text-warning' : 'text-info'}\n                      size='lg'\n                    />\n                  </Tooltip>\n                )}\n              </Col>\n              <Col size={6}>\n                <SelectSr\n                  onChange={this._selectSr}\n                  predicate={this._getSrPredicate()}\n                  required={!noVdisMigration}\n                  value={srId}\n                />\n              </Col>\n            </SingleLineRow>\n            {noVdisMigration && <i>{_('optionalEntry')}</i>}\n          </div>,\n          <div style={LINE_STYLE} key='network'>\n            <SingleLineRow>\n              <Col size={6}>{_('migrateVmSelectMigrationNetwork')}</Col>\n              <Col size={6}>\n                <SelectNetwork\n                  onChange={this._selectMigrationNetwork}\n                  predicate={this._getMigrationNetworkPredicate()}\n                  required={!intraPool}\n                  value={migrationNetworkId}\n                />\n              </Col>\n            </SingleLineRow>\n            {intraPool && <i>{_('optionalEntry')}</i>}\n          </div>,\n        ]}\n        {host && !intraPool && (\n          <div key='network' style={LINE_STYLE}>\n            <SingleLineRow>\n              <Col size={6}>{_('migrateVmsSelectNetwork')}</Col>\n              <Col size={6}>\n                <SelectNetwork\n                  disabled={smartVifMapping}\n                  onChange={this._selectNetwork}\n                  predicate={this._getTargetNetworkPredicate()}\n                  value={networkId}\n                />\n              </Col>\n            </SingleLineRow>\n            <SingleLineRow>\n              <Col size={6} offset={6}>\n                <input type='checkbox' onChange={this._toggleSmartVifMapping} checked={smartVifMapping} />{' '}\n                {_('migrateVmsSmartMapping')}\n              </Col>\n            </SingleLineRow>\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n","import PropTypes from 'prop-types'\nimport React, { Component } from 'react'\n\nimport _ from '../../intl'\nimport Collapse from '../../collapse'\nimport Icon from '../../icon'\nimport { connectStore } from '../../utils'\nimport { createGetObjectsOfType, createSelector } from '../../selectors'\nimport { Sr } from '../../render-xo-item'\n\n@connectStore(\n  {\n    srIds: createSelector(\n      createGetObjectsOfType('PBD').filter(\n        (_, { hostIds }) =>\n          pbd =>\n            hostIds.includes(pbd.host)\n      ),\n      pbds => {\n        const srIds = new Set([])\n        for (const id in pbds) {\n          srIds.add(pbds[id].SR)\n        }\n        return [...srIds]\n      }\n    ),\n  },\n  { withRef: true }\n)\nexport default class MultipathingModal extends Component {\n  static propTypes = {\n    hostIds: PropTypes.arrayOf(PropTypes.string).isRequired,\n  }\n\n  render() {\n    const { hostIds, srIds } = this.props\n    return (\n      <div>\n        {_('hostMultipathingWarning', {\n          nHosts: hostIds.length,\n        })}\n        <br />\n        <span className='text-info'>\n          <Icon icon='info' /> {_('hostMultipathingRequiredState')}\n        </span>\n        <Collapse buttonText={_('hostMultipathingSrs')} size='small' className='mt-1'>\n          {srIds.map(srId => (\n            <div key={srId}>\n              <Sr id={srId} link newTab />\n            </div>\n          ))}\n        </Collapse>\n      </div>\n    )\n  }\n}\n","import BaseComponent from 'base-component'\nimport React from 'react'\n\nimport _ from '../../intl'\nimport SingleLineRow from '../../single-line-row'\nimport { Col } from '../../grid'\n\nexport default class NewAuthTokenModal extends BaseComponent {\n  get value() {\n    return this.state\n  }\n\n  render() {\n    const { description, expiration } = this.state\n\n    return (\n      <div>\n        <div className='pb-1'>\n          <SingleLineRow>\n            <Col size={4}>{_('expiration')}</Col>\n            <Col size={8}>\n              <input\n                className='form-control'\n                min={new Date().toISOString().split('T')[0]}\n                onChange={this.linkState('expiration')}\n                type='date'\n                value={expiration ?? ''}\n              />\n            </Col>\n          </SingleLineRow>\n        </div>\n        <div className='pb-1'>\n          <SingleLineRow>\n            <Col size={4}>{_('description')}</Col>\n            <Col size={8}>\n              <textarea\n                className='form-control'\n                onChange={this.linkState('description')}\n                rows={10}\n                value={description ?? ''}\n              />\n            </Col>\n          </SingleLineRow>\n        </div>\n      </div>\n    )\n  }\n}\n","import BaseComponent from 'base-component'\nimport React from 'react'\n\nimport _ from '../../intl'\nimport SingleLineRow from '../../single-line-row'\nimport { Col } from '../../grid'\nimport getEventValue from '../../get-event-value'\n\nexport default class NewSshKeyModalBody extends BaseComponent {\n  get value() {\n    return this.state\n  }\n\n  _onKeyChange = event => {\n    const key = getEventValue(event)\n    const splitKey = key.split(' ')\n    if (!this.state.title && splitKey.length === 3) {\n      this.setState({ title: splitKey[2].split('\\n')[0] })\n    }\n    this.setState({ key })\n  }\n\n  render() {\n    const { key, title } = this.state\n\n    return (\n      <div>\n        <div className='pb-1'>\n          <SingleLineRow>\n            <Col size={4}>{_('title')}</Col>\n            <Col size={8}>\n              <input className='form-control' onChange={this.linkState('title')} type='text' value={title || ''} />\n            </Col>\n          </SingleLineRow>\n        </div>\n        <div className='pb-1'>\n          <SingleLineRow>\n            <Col size={4}>{_('key')}</Col>\n            <Col size={8}>\n              <textarea\n                className='form-control text-monospace'\n                onChange={this._onKeyChange}\n                rows={10}\n                value={key || ''}\n              />\n            </Col>\n          </SingleLineRow>\n        </div>\n      </div>\n    )\n  }\n}\n","import React from 'react'\nimport SelectLicense from 'select-license'\n\nimport BaseComponent from '../../base-component'\nimport { Host } from '../../render-xo-item'\n\nexport default class PoolBindLicenseModal extends BaseComponent {\n  licenseByHost = {}\n\n  get value() {\n    return this.licenseByHost\n  }\n\n  onSelectLicense = hostId => event => (this.licenseByHost[hostId] = event.target.value)\n\n  render() {\n    const { hosts } = this.props\n    return (\n      <div>\n        {hosts.map(({ id }) => (\n          <div key={id}>\n            <Host id={id} link newTab />\n            <SelectLicense productType='xcpng' showBoundLicenses onChange={this.onSelectLicense(id)} />\n          </div>\n        ))}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport { Col, Container } from 'grid'\nimport { Input as DebounceInput } from 'debounce-input-decorator'\n\nexport default class RegisterProxyModal extends Component {\n  state = {\n    address: '',\n    authenticationToken: '',\n    name: '',\n    vmUuid: '',\n  }\n  get value() {\n    return this.state\n  }\n\n  render() {\n    const { address, authenticationToken, name, vmUuid } = this.state\n    return (\n      <Container>\n        <a href='https://xen-orchestra.com/blog/xo-proxy-a-concrete-guide/' target='_blank' rel='noreferrer'>\n          <Icon icon='info' /> {_('xoProxyConcreteGuide')}\n        </a>\n        <SingleLineRow className='mt-1'>\n          <Col size={6}>{_('proxyAuthToken')}</Col>\n          <Col size={6}>\n            <DebounceInput\n              className='form-control'\n              onChange={this.linkState('authenticationToken')}\n              value={authenticationToken}\n            />\n          </Col>\n        </SingleLineRow>\n        <SingleLineRow className='mt-1'>\n          <Col size={6}>{_('name')}</Col>\n          <Col size={6}>\n            <DebounceInput className='form-control' onChange={this.linkState('name')} value={name} />\n          </Col>\n        </SingleLineRow>\n        <SingleLineRow className='mt-1'>\n          <Col size={6}>{_('address')}</Col>\n          <Col size={6}>\n            <DebounceInput\n              className='form-control'\n              onChange={this.linkState('address')}\n              placeholder='192.168.2.20[:4343]'\n              value={address}\n            />\n          </Col>\n        </SingleLineRow>\n        <SingleLineRow className='mt-1'>\n          <Col size={6}>{_('vmUuid')}</Col>\n          <Col size={6}>\n            <DebounceInput className='form-control' onChange={this.linkState('vmUuid')} value={vmUuid} />\n          </Col>\n        </SingleLineRow>\n        <SingleLineRow className='mt-1'>\n          <Col className='text-info'>\n            <Icon icon='info' /> {_('proxyOptionalVmUuid')}\n          </Col>\n        </SingleLineRow>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport BaseComponent from 'base-component'\nimport React from 'react'\n\nexport default class RevertSnapshotModalBody extends BaseComponent {\n  state = { snapshotBefore: true }\n\n  get value() {\n    return this.state.snapshotBefore\n  }\n\n  render() {\n    return (\n      <div>\n        <div>{_('revertVmModalMessage')}</div>\n        <br />\n        <label>\n          <input type='checkbox' onChange={this.linkState('snapshotBefore')} checked={this.state.snapshotBefore} />{' '}\n          {_('revertVmModalSnapshotBefore')}\n        </label>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport BaseComponent from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport { connectStore } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\n\nimport { subscribePlugins } from '../'\n\n@addSubscriptions(() => ({\n  plugins: subscribePlugins,\n}))\n@connectStore(\n  {\n    pools: createGetObjectsOfType('pool'),\n  },\n  { withRef: true }\n)\nexport default class RollingPoolRebootModal extends BaseComponent {\n  render() {\n    const pool = this.props.pools[this.props.pool]\n    const loadBalancerPlugin =\n      this.props.plugins !== undefined && this.props.plugins.find(plugin => plugin.name === 'load-balancer')\n\n    return (\n      <div>\n        <p>{_('rollingPoolRebootMessage')}</p>\n        {pool.HA_enabled && (\n          <p>\n            <em className='text-warning'>\n              <Icon icon='alarm' /> {_('rollingPoolRebootHaWarning')}\n            </em>\n          </p>\n        )}\n        {loadBalancerPlugin !== undefined && loadBalancerPlugin.loaded && (\n          <p>\n            <em className='text-warning'>\n              <Icon icon='alarm' /> {_('rollingPoolRebootLoadBalancerWarning')}\n            </em>\n          </p>\n        )}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport BaseComponent from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport { connectStore } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\n\nimport { subscribePlugins } from '../'\n\n@addSubscriptions(() => ({\n  plugins: subscribePlugins,\n}))\n@connectStore(\n  {\n    pools: createGetObjectsOfType('pool'),\n  },\n  { withRef: true }\n)\nexport default class RollingPoolUpdateModal extends BaseComponent {\n  render() {\n    const pool = this.props.pools[this.props.pool]\n    const loadBalancerPlugin =\n      this.props.plugins !== undefined && this.props.plugins.find(plugin => plugin.name === 'load-balancer')\n\n    return (\n      <div>\n        <p>{_('rollingPoolUpdateMessage')}</p>\n        {pool.HA_enabled && (\n          <p>\n            <em className='text-warning'>\n              <Icon icon='alarm' /> {_('rollingPoolUpdateHaWarning')}\n            </em>\n          </p>\n        )}\n        {loadBalancerPlugin !== undefined && loadBalancerPlugin.loaded && (\n          <p>\n            <em className='text-warning'>\n              <Icon icon='alarm' /> {_('rollingPoolUpdateLoadBalancerWarning')}\n            </em>\n          </p>\n        )}\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport React from 'react'\nimport BaseComponent from 'base-component'\nimport { compileTemplate } from '@xen-orchestra/template'\nimport { connectStore } from 'utils'\nimport { Container, Col, Row } from 'grid'\nimport { createGetObjectsOfType } from 'selectors'\nimport forEach from 'lodash/forEach.js'\n\nconst RULES = {\n  '{date}': () => new Date().toISOString(),\n  '{description}': vm => vm.name_description,\n  '{name}': vm => vm.name_label,\n}\n\n@connectStore(\n  {\n    vms: createGetObjectsOfType('VM').pick((_, props) => props.vms),\n  },\n  { withRef: true }\n)\nexport default class SnapshotVmModalBody extends BaseComponent {\n  state = {\n    descriptionPattern: '{description}',\n    namePattern: '{name}_{date}',\n  }\n\n  get value() {\n    const { descriptionPattern, namePattern, saveMemory } = this.state\n    if (namePattern === '' && descriptionPattern === '') {\n      return { names: {}, descriptions: {}, saveMemory }\n    }\n\n    const generateName = compileTemplate(namePattern, RULES)\n    const generateDescription = compileTemplate(descriptionPattern, RULES)\n    const names = {}\n    const descriptions = {}\n\n    forEach(this.props.vms, (vm, id) => {\n      if (namePattern !== '') {\n        names[id] = generateName(vm)\n      }\n      if (descriptionPattern !== '') {\n        descriptions[id] = generateDescription(vm)\n      }\n    })\n\n    return {\n      descriptions,\n      names,\n      saveMemory,\n    }\n  }\n\n  render() {\n    return (\n      <Container>\n        <Row className='mb-1'>\n          <Col size={6}>{_('snapshotVmsName')}</Col>\n          <Col size={6}>\n            <input\n              className='form-control'\n              onChange={this.linkState('namePattern')}\n              type='text'\n              value={this.state.namePattern}\n            />\n          </Col>\n        </Row>\n        <Row>\n          <Col size={6}>{_('snapshotVmsDescription')}</Col>\n          <Col size={6}>\n            <input\n              className='form-control'\n              onChange={this.linkState('descriptionPattern')}\n              type='text'\n              value={this.state.descriptionPattern}\n            />\n          </Col>\n        </Row>\n        <Row>\n          <Col>\n            <label>\n              <input type='checkbox' onChange={this.linkState('saveMemory')} checked={this.state.saveMemory} />{' '}\n              {_('snapshotSaveMemory')}\n            </label>\n          </Col>\n        </Row>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport React from 'react'\nimport Component from 'base-component'\nimport { createGetObjectsOfType, createSelector } from 'selectors'\nimport map from 'lodash/map.js'\nimport { subscribeResourceCatalog } from 'xo'\nimport { isLatestXosanPackInstalled, connectStore, findLatestPack } from 'utils'\n\n@connectStore(\n  {\n    hosts: createGetObjectsOfType('host').filter(\n      (_, { pool }) =>\n        host =>\n          host.$pool === pool.id\n    ),\n  },\n  { withRef: true }\n)\nexport default class UpdateXosanPacksModal extends Component {\n  componentDidMount() {\n    this.componentWillUnmount = subscribeResourceCatalog(catalog => this.setState({ catalog }))\n  }\n\n  get value() {\n    return this._getStatus().pack\n  }\n\n  _getStatus = createSelector(\n    () => this.state.catalog,\n    () => this.props.hosts,\n    (catalog, hosts) => {\n      if (catalog === undefined) {\n        return { status: 'error' }\n      }\n\n      if (catalog._namespaces.xosan === undefined) {\n        return { status: 'unavailable' }\n      }\n\n      if (!catalog._namespaces.xosan.registered) {\n        return { status: 'unregistered' }\n      }\n\n      const pack = findLatestPack(catalog.xosan, map(hosts, 'version'))\n\n      if (pack === undefined) {\n        return { status: 'noPack' }\n      }\n\n      if (isLatestXosanPackInstalled(pack, hosts)) {\n        return { status: 'upToDate' }\n      }\n\n      return { status: 'packFound', pack }\n    }\n  )\n\n  render() {\n    const { status, pack } = this._getStatus()\n    switch (status) {\n      case 'checking':\n        return <em>{_('xosanPackUpdateChecking')}</em>\n      case 'error':\n        return <em>{_('xosanPackUpdateError')}</em>\n      case 'unavailable':\n        return <em>{_('xosanPackUpdateUnavailable')}</em>\n      case 'unregistered':\n        return <em>{_('xosanPackUpdateUnregistered')}</em>\n      case 'noPack':\n        return <em>{_('xosanNoPackFound')}</em>\n      case 'upToDate':\n        return <em>{_('xosanPackUpdateUpToDate')}</em>\n      case 'packFound':\n        return (\n          <div>\n            {_('xosanPackUpdateVersion', {\n              version: pack.version,\n            })}\n          </div>\n        )\n    }\n  }\n}\n","import defined from '@xen-orchestra/defined'\nimport semver from 'semver'\nimport { find, forEach, includes, map } from 'lodash'\n\nexport const getDefaultNetworkForVif = (vif, destHost, pifs, networks) => {\n  const originNetwork = networks[vif.$network]\n  const originVlans = map(originNetwork.PIFs, pifId => pifs[pifId].vlan)\n\n  let destNetworkId = pifs[destHost.$PIFs[0]].$network\n\n  forEach(destHost.$PIFs, pifId => {\n    const { $network, vlan } = pifs[pifId]\n\n    if (networks[$network].name_label === originNetwork.name_label) {\n      destNetworkId = $network\n\n      return false\n    }\n\n    if (vlan !== -1 && includes(originVlans, vlan)) {\n      destNetworkId = $network\n    }\n  })\n\n  return destNetworkId\n}\n\nexport const getDefaultMigrationNetwork = (intraPool, destHost, pools, pifs) => {\n  const migrationNetwork = pools[destHost.$pool].otherConfig['xo:migrationNetwork']\n  let defaultPif\n  return defined(\n    find(pifs, pif => {\n      if (pif.$host === destHost.id) {\n        if (migrationNetwork !== undefined && pif.ip !== '' && pif.$network === migrationNetwork) {\n          return true\n        }\n        if (pif.management) {\n          defaultPif = pif\n        }\n      }\n    }),\n    intraPool ? {} : defaultPif\n  ).$network\n}\n\nexport const isXsHostWithCdnPatches = ({ version, productBrand }) =>\n  productBrand === 'XenServer' && semver.gte(version, '8.4.0')\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport { Col, Container } from 'grid'\nimport { SelectSr } from 'select-objects'\nimport { Toggle } from 'form'\n\nexport default class WarmMigrationModal extends Component {\n  state = {\n    deleteSourceVm: false,\n    sr: undefined,\n    startDestinationVm: false,\n  }\n  get value() {\n    return this.state\n  }\n\n  render() {\n    const { deleteSourceVm, sr, startDestinationVm } = this.state\n    return (\n      <Container>\n        <div className='text-info'>\n          <Icon icon='info' /> <i>{_('vmWarmMigrationProcessInfo')}</i>\n        </div>\n        <SingleLineRow className='mt-1'>\n          <Col size={6}>{_('destinationSR')}</Col>\n          <Col size={6}>\n            <SelectSr onChange={this.linkState('sr')} value={sr} />\n          </Col>\n        </SingleLineRow>\n        <SingleLineRow className='mt-1'>\n          <Col size={6}>{_('deleteSourceVm')}</Col>\n          <Col size={6}>\n            <Toggle onChange={this.toggleState('deleteSourceVm')} value={deleteSourceVm} />\n          </Col>\n        </SingleLineRow>\n        <SingleLineRow className='mt-1'>\n          <Col size={6}>{_('startMigratedVm')}</Col>\n          <Col size={6}>\n            <Toggle onChange={this.toggleState('startDestinationVm')} value={startDestinationVm} />\n          </Col>\n        </SingleLineRow>\n      </Container>\n    )\n  }\n}\n","export const FREE = {\n  value: 1,\n  name: 'Free',\n}\nexport const STARTER = {\n  value: 2,\n  name: 'Starter',\n}\nexport const ENTERPRISE = {\n  value: 3,\n  name: 'Enterprise',\n}\nexport const PREMIUM = {\n  value: 4,\n  name: 'Premium',\n}\nexport const SOURCES = {\n  value: 5,\n  name: 'Community',\n}\nconst UNKNOWN = {\n  value: 0,\n  name: 'Unknown',\n}\n\nexport const productId2Plan = {\n  starter: 2,\n  enterprise: 3,\n  premium: 4,\n  'sb-premium': 4,\n}\n\nexport const getXoaPlan = (plan = +process.env.XOA_PLAN) => {\n  switch (+plan) {\n    case 1:\n      return FREE\n    case 2:\n      return STARTER\n    case 3:\n      return ENTERPRISE\n    case 4:\n      return PREMIUM\n    case 5:\n      return SOURCES\n    default:\n      return UNKNOWN\n  }\n}\n\nexport const CURRENT = getXoaPlan()\n","import forEach from 'lodash/forEach'\nimport fromEvents from 'promise-toolbox/fromEvents'\nimport makeError from 'make-error'\nimport map from 'lodash/map'\nimport { AbortedConnection, ConnectionError, JsonRpcWebSocketClient as Client } from 'jsonrpc-websocket-client'\nimport { EventEmitter } from 'events'\nimport {\n  setXoaConfiguration,\n  setXoaRegisterState,\n  setXoaTrialState,\n  setXoaUpdaterLog,\n  setXoaUpdaterState,\n} from 'store/actions'\nimport { ENTERPRISE, getXoaPlan, PREMIUM, STARTER } from './xoa-plans'\n\n// ===================================================================\n\nconst states = ['disconnected', 'updating', 'upgrading', 'upToDate', 'upgradeNeeded', 'registerNeeded', 'error']\n\n// ===================================================================\n\nexport function isTrialRunning(trial) {\n  return trial && trial.end && Date.now() < trial.end\n}\n\nexport function exposeTrial(trial) {\n  // We won't suggest trial if any trial is running now, or if premium was enjoyed in any past trial\n  return !(trial && (isTrialRunning(trial) || trial.plan === 'premium'))\n}\n\nexport function blockXoaAccess(xoaState) {\n  let block = xoaState.state === 'untrustedTrial'\n  if (process.env.XOA_PLAN <= 1 || process.env.XOA_PLAN >= 5) {\n    block = block || xoaState.state === 'ERROR'\n  }\n  return block\n}\n\nexport function getLicenseNearExpiration(licenses, trial) {\n  const plan = getXoaPlan()\n  // user does not have a licence for free or source version\n  // also don't crash the app if the licence is in unexpected state\n  if (![PREMIUM, ENTERPRISE, STARTER].includes(plan)) {\n    return\n  }\n  // user under trial are not expected to have licences\n  if (isTrialRunning(trial)) {\n    return\n  }\n  // got a unlimited license bound to this XO nothing can expires\n  if (licenses.find(({ expires }) => expires === undefined) !== undefined) {\n    return\n  }\n\n  const SLOTS = [\n    {\n      strCode: 'licenseNearlyExpired',\n      textDuration: '3 months',\n      duration: -90 * 24 * 3600 * 1000,\n      popupClass: 'alert-info',\n    },\n    {\n      strCode: 'licenseNearlyExpired',\n      textDuration: '2 months',\n      duration: -60 * 24 * 3600 * 1000,\n      popupClass: 'alert-info',\n    },\n    {\n      strCode: 'licenseNearlyExpired',\n      textDuration: '1 month',\n      duration: -30 * 24 * 3600 * 1000,\n      popupClass: 'alert-danger',\n    },\n    {\n      strCode: 'licenseNearlyExpired',\n      textDuration: '1 week',\n      duration: -7 * 24 * 3600 * 1000,\n      popupClass: 'alert-danger',\n    },\n    {\n      strCode: 'licenseExpired',\n      code: 'EXPIRED',\n      duration: 0,\n      popupClass: 'alert-danger',\n    },\n    {\n      strCode: 'licenseExpired',\n      code: 'EXPIRED',\n      duration: 90 * 24 * 3600 * 1000,\n      blocked: true,\n      popupClass: 'alert-danger',\n    },\n  ]\n  if (licenses.length === 0) {\n    return {\n      ...SLOTS.pop(),\n      license: {\n        expires: 0,\n      },\n    }\n  }\n\n  licenses.sort(({ expires: expires1 }, { expires: expires2 }) => expires2 - expires1)\n  const newestLicense = licenses[0]\n\n  const candidates = SLOTS.filter(({ duration }) => newestLicense.expires + duration < Date.now())\n\n  if (candidates.length === 0) {\n    // no license near expiration\n    return\n  }\n\n  // only show the most recent expire slot\n  candidates.sort(({ duration: duration1 }, { duration: duration2 }) => {\n    return duration2 - duration1\n  })\n  return {\n    ...candidates[0],\n    license: newestLicense,\n  }\n}\n\n// ===================================================================\n\nexport const NotRegistered = makeError('NotRegistered')\n\nclass XoaUpdater extends EventEmitter {\n  constructor() {\n    super()\n    this._waiting = false\n    this._log = []\n    this._lastRun = 0\n    this._lowState = null\n    this.state('disconnected')\n    this.registerError = ''\n    this._configuration = {}\n  }\n\n  state(state) {\n    this._state = state\n    this.emit(state, this._lowState && this._lowState.source)\n  }\n\n  async update() {\n    if (this._waiting) {\n      return\n    }\n    this._waiting = true\n    this.state('updating')\n    this._update(false)\n  }\n\n  async upgrade() {\n    if (this._waiting) {\n      return\n    }\n    this._waiting = true\n    this.state('upgrading')\n    await this._update(true)\n  }\n\n  _upgradeSuccessful() {\n    this.emit('upgradeSuccessful', this._lowState && this._lowState.source)\n  }\n\n  async _open() {\n    const openFailure = error => {\n      switch (true) {\n        case error instanceof AbortedConnection:\n          this.log('error', 'AbortedConnection')\n          break\n        case error instanceof ConnectionError:\n          this.log('error', 'ConnectionError')\n          break\n        default:\n          this.log('error', error)\n      }\n      delete this._client\n      this.state('disconnected')\n      throw error\n    }\n\n    const handleOpen = c => {\n      const middle = new EventEmitter()\n      const handleError = error => {\n        this.log('error', error.message)\n        this._lowState = error\n        this.state('error')\n        this._waiting = false\n        this.emit('error', error)\n      }\n\n      c.on('notification', n => middle.emit(n.method, n.params))\n      c.on('closed', () => middle.emit('disconnected'))\n\n      middle.on('print', ({ content }) => {\n        Array.isArray(content) || (content = [content])\n        content.forEach(elem => this.log('info', elem))\n        this.emit('print', content)\n      })\n      middle.on('end', end => {\n        this._lowState = end\n        const { state } = end\n        if (state.endsWith('-upgrade-needed')) {\n          this.state('upgradeNeeded')\n        } else {\n          switch (state) {\n            case 'xoa-up-to-date':\n            case 'xoa-upgraded':\n            case 'updater-upgraded':\n            case 'installer-upgraded':\n              this.state('upToDate')\n              break\n            case 'register-needed':\n              this.state('registerNeeded')\n              break\n            default:\n              this.state('error')\n          }\n        }\n        this.log(end.level, end.message)\n        this._lastRun = Date.now()\n        this._waiting = false\n        this.emit('end', end)\n        if (state === 'register-needed') {\n          this.isRegistered()\n        } else if (state === 'updater-upgraded' || state === 'installer-upgraded') {\n          this.update()\n        } else if (state === 'xoa-upgraded') {\n          this._upgradeSuccessful()\n        }\n        this.xoaState()\n      })\n      middle.on('warning', warning => {\n        this.log('warning', warning.message)\n        this.emit('warning', warning)\n      })\n      middle.on('server-error', handleError)\n      middle.on('disconnected', () => {\n        this._lowState = null\n        this.state('disconnected')\n        this._waiting = false\n        this.log('warning', 'Lost connection with xoa-updater')\n        middle.emit('reconnect_failed') // No reconnecting attempts implemented so far\n      })\n      middle.on('reconnect_failed', () => {\n        this._waiting = false\n        middle.removeAllListeners()\n        this._client.removeAllListeners()\n        if (this._client.status !== 'closed') {\n          this._client.close()\n        }\n        delete this._client\n        const message = 'xoa-updater could not be reached'\n        this._xoaStateError({ message })\n        this.log('error', message)\n        this.emit('disconnected')\n      })\n\n      this.update()\n      this.isRegistered()\n      this.getConfiguration()\n      return c\n    }\n\n    if (!this._client) {\n      try {\n        this._client = new Client('./api/updater')\n        await this._client.open()\n        handleOpen(this._client)\n      } catch (error) {\n        openFailure(error)\n      }\n    }\n    const c = this._client\n    if (c.status === 'open') {\n      return c\n    } else {\n      return fromEvents(c, ['open'], ['closed', 'error']).then(() => c)\n    }\n  }\n\n  async isRegistered() {\n    try {\n      const token = await this._call('isRegistered')\n      if (token.registrationToken === undefined) {\n        throw new NotRegistered('Your Xen Orchestra Appliance is not registered')\n      } else {\n        this.registerState = 'registered'\n        this.token = token\n        return token\n      }\n    } catch (error) {\n      delete this.token\n      if (error instanceof NotRegistered) {\n        this.registerState = 'unregistered'\n      } else {\n        this.registerError = error.message\n        this.registerState = 'error'\n      }\n    } finally {\n      this.emit('registerState', {\n        state: this.registerState,\n        email: (this.token && this.token.registrationEmail) || '',\n        error: this.registerError,\n      })\n    }\n  }\n\n  getLocalManifest() {\n    return this._call('getLocalManifest')\n  }\n\n  async register(email, password, renew = false) {\n    try {\n      const token = await this._call('register', { email, password, renew })\n      this.registerState = 'registered'\n      this.registerError = ''\n      this.token = token\n      return token\n    } catch (error) {\n      if (!renew) {\n        delete this.token\n      }\n      if (error.code && error.code === 1) {\n        this.registerError = 'Authentication failed'\n      } else {\n        this.registerError = error.message\n        this.registerState = 'error'\n      }\n    } finally {\n      this.emit('registerState', {\n        state: this.registerState,\n        email: (this.token && this.token.registrationEmail) || '',\n        error: this.registerError,\n      })\n      if (this.registerState === 'registered') {\n        this.update()\n      }\n    }\n  }\n\n  async requestTrial() {\n    const state = await this.xoaState()\n    if (!state.state === 'ERROR') {\n      throw new Error(state.message)\n    }\n    if (isTrialRunning(state.trial)) {\n      throw new Error('You are already under trial')\n    }\n    try {\n      return await this._call('requestTrial', { trialPlan: 'premium' })\n    } finally {\n      await this.xoaState()\n    }\n  }\n\n  async xoaState() {\n    try {\n      const state = await this._call('xoaState')\n      this._xoaState = state\n      return state\n    } catch (error) {\n      return this._xoaStateError(error)\n    } finally {\n      this.emit('trialState', Object.assign({}, this._xoaState))\n    }\n  }\n\n  _xoaStateError(error) {\n    const message = error.message || String(error)\n    this._xoaState = {\n      state: 'ERROR',\n      message,\n    }\n    return this._xoaState\n  }\n\n  async _update(upgrade = false) {\n    try {\n      const c = await this._open()\n      this.log('info', 'Start ' + (upgrade ? 'upgrading' : 'updating' + '...'))\n      c.notify('update', { upgrade })\n    } catch (error) {\n      this._waiting = false\n    }\n  }\n\n  async start() {\n    if (this.isStarted()) {\n      return\n    }\n    await this.xoaState()\n    await this.isRegistered()\n    this._interval = setInterval(() => this.run(), 60 * 60 * 1000)\n    this.run()\n  }\n\n  stop() {\n    if (this._interval) {\n      clearInterval(this._interval)\n      delete this._interval\n    }\n    if (this._client) {\n      this._client.removeAllListeners()\n      if (this._client.status !== 'closed') {\n        this._client.close()\n      }\n      delete this._client\n    }\n    this.state('disconnected')\n  }\n\n  run() {\n    if (Date.now() - this._lastRun >= 24 * 60 * 60 * 1000) {\n      this.update()\n    }\n  }\n\n  isStarted() {\n    return this._interval\n  }\n\n  log(level, message) {\n    message = (message != null && message.message) || String(message)\n    const date = new Date()\n    this._log.push({\n      date: date.toLocaleString(),\n      level,\n      message,\n    })\n    while (this._log.length > 10) {\n      this._log.shift()\n    }\n    this.emit(\n      'log',\n      map(this._log, item => Object.assign({}, item))\n    )\n  }\n\n  async getConfiguration() {\n    try {\n      this._configuration = await this._call('getConfiguration')\n      return this._configuration\n    } catch (error) {\n      this._configuration = {}\n    } finally {\n      this.emit('configuration', Object.assign({}, this._configuration))\n    }\n  }\n\n  getReleaseChannels() {\n    return this._call('getReleaseChannels').catch(error => {\n      console.error('getReleaseChannels', error)\n      return {}\n    })\n  }\n\n  async _call(...args) {\n    const c = await this._open()\n    try {\n      return await c.call(...args)\n    } catch (error) {\n      this.log('error', error)\n      throw error\n    }\n  }\n\n  async configure(config) {\n    try {\n      this._configuration = await this._call('configure', config)\n      this.update()\n      return this._configuration\n    } catch (error) {\n      this._configuration = {}\n    } finally {\n      this.emit('configuration', Object.assign({}, this._configuration))\n    }\n  }\n}\n\nconst xoaUpdater = new XoaUpdater()\n\nexport default xoaUpdater\n\nexport const connectStore = store => {\n  forEach(states, state => xoaUpdater.on(state, () => store.dispatch(setXoaUpdaterState(state))))\n  xoaUpdater.on('trialState', state => store.dispatch(setXoaTrialState(state)))\n  xoaUpdater.on('log', log => store.dispatch(setXoaUpdaterLog(log)))\n  xoaUpdater.on('registerState', registration => store.dispatch(setXoaRegisterState(registration)))\n  xoaUpdater.on('configuration', configuration => store.dispatch(setXoaConfiguration(configuration)))\n}\n","import PropTypes from 'prop-types'\nimport React from 'react'\n\nimport _ from './intl'\nimport Icon from './icon'\nimport Link from './link'\nimport { Card, CardHeader, CardBlock } from './card'\nimport { connectStore, getXoaPlan } from './utils'\nimport { isAdmin } from 'selectors'\n\nconst Upgrade = connectStore({\n  isAdmin,\n})(({ available, children, isAdmin, place, required = available }) =>\n  process.env.XOA_PLAN < required ? (\n    <Card>\n      <CardHeader>{_('upgradeNeeded')}</CardHeader>\n      {isAdmin ? (\n        <CardBlock className='text-xs-center'>\n          <p>{_('availableIn', { plan: getXoaPlan(required) })}</p>\n          <p>\n            <a\n              href={`https://xen-orchestra.com/#!/pricing?pk_campaign=xoa_${getXoaPlan()}_upgrade&pk_kwd=${place}`}\n              target='_blank'\n              rel='noreferrer'\n              className='btn btn-primary btn-lg'\n            >\n              <Icon icon='plan-upgrade' /> {_('upgradeNow')}\n            </a>{' '}\n            {_('or')}\n            &nbsp;\n            <Link className='btn btn-success btn-lg' to='/xoa/update'>\n              <Icon icon='plan-trial' /> {_('tryIt')}\n            </Link>\n          </p>\n        </CardBlock>\n      ) : (\n        <CardBlock className='text-xs-center'>\n          <p>{_('notAvailable')}</p>\n        </CardBlock>\n      )}\n    </Card>\n  ) : (\n    children\n  )\n)\n\nUpgrade.propTypes = {\n  available: PropTypes.number,\n  place: PropTypes.string.isRequired,\n  required: PropTypes.number,\n}\n\nexport { Upgrade as default }\n","import PropTypes from 'prop-types'\nimport React from 'react'\n\nimport _ from './intl'\nimport Component from './base-component'\nimport Icon from './icon'\nimport Link from './link'\nimport Tooltip from './tooltip'\nimport { connectStore } from './utils'\nimport { createCollectionWrapper, createGetObjectsOfType, createSelector } from './selectors'\nimport { smartModeToComplexMatcher } from './smartModeToComplexMatcher'\n\n@connectStore({\n  containers: createSelector(createGetObjectsOfType('pool'), createGetObjectsOfType('host'), (pools, hosts) => ({\n    ...pools,\n    ...hosts,\n  })),\n  vms: createGetObjectsOfType('VM').pick((_, props) => props.vms),\n})\nexport default class ZstdChecker extends Component {\n  static propTypes = {\n    vms: PropTypes.arrayOf(PropTypes.string).isRequired,\n  }\n\n  _getVmsWithoutZstd = createSelector(\n    () => this.props.vms,\n    () => this.props.containers,\n    createCollectionWrapper((vms, containers) => {\n      const vmIds = []\n      for (const id in vms) {\n        const container = containers[vms[id].$container]\n        if (container !== undefined && !container.zstdSupported) {\n          vmIds.push(id)\n        }\n      }\n      return vmIds\n    })\n  )\n\n  _getVmsWithoutZstdLink = createSelector(this._getVmsWithoutZstd, vms => ({\n    pathname: '/home',\n    query: {\n      t: 'VM',\n      s: smartModeToComplexMatcher({\n        id: {\n          __or: vms,\n        },\n      }).toString(),\n    },\n  }))\n\n  render() {\n    const nVmsWithoutZstd = this._getVmsWithoutZstd().length\n    return nVmsWithoutZstd > 0 ? (\n      <Tooltip content={_('notSupportedZstdTooltip')}>\n        <Link className='text-warning' target='_blank' to={this._getVmsWithoutZstdLink()}>\n          <Icon icon='alarm' />{' '}\n          {_('notSupportedZstdWarning', {\n            nVms: nVmsWithoutZstd,\n          })}\n        </Link>\n      </Tooltip>\n    ) : null\n  }\n}\n","import _ from 'intl'\nimport mapValues from 'lodash/mapValues'\n\nconst keymap = {\n  XoApp: {\n    GO_TO_HOSTS: 'g h',\n    GO_TO_POOLS: 'g p',\n    GO_TO_VMS: 'g v',\n    GO_TO_SRS: 'g s',\n    CREATE_VM: 'c v',\n    UNFOCUS: 'esc',\n    HELP: ['?', 'h'],\n  },\n  Home: {\n    SEARCH: '/',\n    NAV_DOWN: 'j',\n    NAV_UP: 'k',\n    SELECT: 'x',\n    JUMP_INTO: 'enter',\n  },\n  SortedTable: {\n    SEARCH: '/',\n    NAV_DOWN: 'j',\n    NAV_UP: 'k',\n    SELECT: 'x',\n    ROW_ACTION: 'enter',\n  },\n}\nexport { keymap as default }\n\nexport const help = mapValues(keymap, (shortcuts, contextLabel) => ({\n  name: _(`shortcut_${contextLabel}`),\n  shortcuts: mapValues(shortcuts, (shortcut, label) => ({\n    keys: shortcuts[label],\n    message: _(`shortcut_${contextLabel}_${label}`),\n  })),\n}))\n","import logError from 'log-error'\nimport React from 'react'\n\n// Avoid global breakage if a component fails to render.\n//\n// Inspired by https://gist.github.com/Aldredcz/4d63b0a9049b00f54439f8780be7f0d8\nReact.createElement = (createElement => {\n  const errorComponent = (\n    <p\n      className='text-danger'\n      style={{\n        fontWeight: 'bold',\n      }}\n    >\n      an error has occurred\n    </p>\n  )\n\n  const wrapRender = render => {\n    function patchedRender() {\n      try {\n        return render.apply(this, arguments)\n      } catch (error) {\n        logError(error)\n\n        return errorComponent\n      }\n    }\n    patchedRender.originalRender = render\n    return patchedRender\n  }\n\n  return function (Component) {\n    if (typeof Component === 'function') {\n      const patched = Component._patched\n      if (patched) {\n        arguments[0] = patched\n      } else {\n        const { prototype } = Component\n        let render\n        if (prototype && typeof (render = prototype.render) === 'function') {\n          prototype.render = wrapRender(render)\n          Component._patched = Component // itself\n        } else {\n          arguments[0] = Component._patched = Object.assign(wrapRender(Component), Component)\n        }\n      }\n    }\n\n    return createElement.apply(this, arguments)\n  }\n})(React.createElement)\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport Link from 'link'\nimport Page from '../page'\nimport React from 'react'\nimport { getUser } from 'selectors'\nimport { compareCommits, getMasterCommit, serverVersion } from 'xo'\nimport { Container, Row, Col } from 'grid'\nimport { connectStore, getXoaPlan } from 'utils'\n\nconst COMMIT_ID = process.env.GIT_HEAD\n\nconst HEADER = (\n  <Container>\n    <Row>\n      <Col mediumSize={12}>\n        <h2>\n          <Icon icon='menu-about' /> {_('aboutXoaPlan', { xoaPlan: getXoaPlan() })}\n        </h2>\n      </Col>\n    </Row>\n  </Container>\n)\n\n@connectStore(() => ({\n  user: getUser,\n}))\nexport default class About extends Component {\n  async componentWillMount() {\n    serverVersion.then(serverVersion => {\n      this.setState({ serverVersion })\n    })\n\n    if (process.env.XOA_PLAN > 4 && COMMIT_ID !== '') {\n      try {\n        const commit = await getMasterCommit()\n        const isOnLatest = commit.sha === COMMIT_ID\n        const diff = {\n          nAhead: 0,\n          nBehind: 0,\n        }\n        if (!isOnLatest) {\n          try {\n            const { ahead_by, behind_by } = await compareCommits(commit.sha, COMMIT_ID)\n            diff.nAhead = ahead_by\n            diff.nBehind = behind_by\n          } catch (err) {\n            console.error(err)\n            diff.nBehind = 'unknown'\n          }\n        }\n\n        this.setState({\n          commit: {\n            isOnLatest,\n            master: commit,\n            diffWithMaster: diff,\n            fetched: true,\n          },\n        })\n      } catch (err) {\n        console.error(err)\n        this.setState({\n          commit: {\n            fetched: false,\n          },\n        })\n      }\n    }\n  }\n  render() {\n    const { user } = this.props\n    const { commit } = this.state\n    const isAdmin = user && user.permission === 'admin'\n\n    return (\n      <Page header={HEADER} title='aboutPage' formatTitle>\n        <Container className='text-xs-center'>\n          {isAdmin && [\n            process.env.XOA_PLAN > 4 && COMMIT_ID !== '' && (\n              <Col>\n                <Row key='0'>\n                  <Col mediumSize={6}>\n                    <Icon icon='git' size={4} />\n                    <h4>\n                      Xen Orchestra, commit{' '}\n                      <a\n                        href={'https://github.com/vatesfr/xen-orchestra/commit/' + COMMIT_ID}\n                        target='_blank'\n                        rel='noreferrer'\n                      >\n                        {COMMIT_ID.slice(0, 5)}\n                      </a>\n                    </h4>\n                  </Col>\n                  <Col mediumSize={6} className={commit?.fetched === false ? 'text-warning' : ''}>\n                    <Icon icon='git' size={4} />\n                    <h4>\n                      {commit === undefined ? (\n                        _('statusLoading')\n                      ) : commit.fetched ? (\n                        <span>\n                          Master, commit{' '}\n                          <a href={commit.master.html_url} target='_blank' rel='noreferrer'>\n                            {commit.master.sha.slice(0, 5)}\n                          </a>\n                        </span>\n                      ) : (\n                        _('failedToFetchLatestMasterCommit')\n                      )}\n                    </h4>\n                  </Col>\n                </Row>\n                {commit?.fetched && (\n                  <Row className={`mt-1 ${commit.isOnLatest ? '' : 'text-warning '}`}>\n                    <h4>\n                      {commit.isOnLatest ? (\n                        <span>\n                          {_('xoUpToDate')} <Icon icon='check' color='text-success' />\n                        </span>\n                      ) : (\n                        <span>\n                          {_('xoFromSourceNotUpToDate', {\n                            nBehind: commit.diffWithMaster.nBehind,\n                            nAhead: commit.diffWithMaster.nAhead,\n                          })}{' '}\n                          <Icon icon='alarm' color='text-warning' />\n                        </span>\n                      )}\n                    </h4>\n                  </Row>\n                )}\n              </Col>\n            ),\n          ]}\n          {process.env.XOA_PLAN > 4 ? (\n            <div>\n              <Row>\n                <Col>\n                  <h2 className='text-info'>{_('productionUse')}</h2>\n                  <h4 className='text-info'>\n                    {_('getSupport', {\n                      website: (\n                        <a\n                          href='https://vates.tech/pricing-and-support/?pk_campaign=xoa_source_upgrade&pk_kwd=about'\n                          target='_blank'\n                          rel='noreferrer'\n                        >\n                          https://xen-orchestra.com\n                        </a>\n                      ),\n                    })}\n                  </h4>\n                </Col>\n              </Row>\n              <Row>\n                <Col mediumSize={6}>\n                  <a href='https://github.com/vatesfr/xen-orchestra/issues/new/choose' target='_blank' rel='noreferrer'>\n                    <Icon icon='bug' size={4} />\n                    <h4>{_('bugTracker')}</h4>\n                  </a>\n                  <p className='text-muted'>{_('bugTrackerText')}</p>\n                </Col>\n                <Col mediumSize={6}>\n                  <a href='https://xcp-ng.org/forum/category/12/xen-orchestra' target='_blank' rel='noreferrer'>\n                    <Icon icon='group' size={4} />\n                    <h4>{_('community')}</h4>\n                  </a>\n                  <p className='text-muted'>{_('communityText')}</p>\n                </Col>\n              </Row>\n            </div>\n          ) : +process.env.XOA_PLAN === 1 ? (\n            <div>\n              <Row>\n                <Col>\n                  <Link to='/xoa/update'>\n                    <h2>{_('freeTrial')}</h2>\n                    {_('freeTrialNow')}\n                  </Link>\n                </Col>\n              </Row>\n              <Row>\n                <Col mediumSize={6}>\n                  <a href='https://xen-orchestra.com/' target='_blank' rel='noreferrer'>\n                    <Icon icon='help' size={4} />\n                    <h4>{_('issues')}</h4>\n                  </a>\n                  <p className='text-muted'>{_('issuesText')}</p>\n                </Col>\n                <Col mediumSize={6}>\n                  <a href='https://xen-orchestra.com/docs' target='_blank' rel='noreferrer'>\n                    <Icon icon='user' size={4} />\n                    <h4>{_('documentation')}</h4>\n                  </a>\n                  <p className='text-muted'>{_('documentationText')}</p>\n                </Col>\n              </Row>\n            </div>\n          ) : (\n            <div>\n              <Row>\n                <Col>\n                  <h2 className='text-success'>{_('proSupportIncluded')}</h2>\n                  <a href='https://xen-orchestra.com/#!/member/products' target='_blank' rel='noreferrer'>\n                    {_('xoAccount')}\n                  </a>\n                </Col>\n              </Row>\n              <Row>\n                <Col mediumSize={6}>\n                  <a href='https://xen-orchestra.com/#!/member/support' target='_blank' rel='noreferrer'>\n                    <Icon icon='help' size={4} />\n                    <h4>{_('openTicket')}</h4>\n                  </a>\n                  <p className='text-muted'>{_('openTicketText')}</p>\n                </Col>\n                <Col mediumSize={6}>\n                  <a href='https://xen-orchestra.com/docs' target='_blank' rel='noreferrer'>\n                    <Icon icon='user' size={4} />\n                    <h4>{_('documentation')}</h4>\n                  </a>\n                  <p className='text-muted'>{_('documentationText')}</p>\n                </Col>\n              </Row>\n            </div>\n          )}\n        </Container>\n      </Page>\n    )\n  }\n}\n","import pickBy from 'lodash/pickBy.js'\n\nconst DEFAULTS = {\n  __proto__: null,\n\n  backupReportTpl: 'mjml',\n  cbtDestroySnapshotData: false,\n  checkpointSnapshot: false,\n  compression: '',\n  concurrency: 0,\n  fullInterval: 0,\n  hideSuccessfulItems: false,\n  nbdConcurrency: 1,\n  offlineBackup: false,\n  offlineSnapshot: false,\n  preferNbd: false,\n  reportWhen: 'failure',\n  timeout: 0,\n}\n\nconst MODES = {\n  __proto__: null,\n\n  compression: 'full',\n  fullInterval: 'delta',\n  offlineBackup: 'full',\n}\n\nconst getSettingsWithNonDefaultValue = (mode, settings) =>\n  pickBy(settings, (value, key) => {\n    const settingMode = MODES[key]\n\n    return (settingMode === undefined || settingMode === mode) && value !== undefined && value !== DEFAULTS[key]\n  })\n\nexport { getSettingsWithNonDefaultValue as default }\n","import _ from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport decorate from 'apply-decorators'\nimport defined from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport React from 'react'\nimport { injectState, provideState } from 'reaclette'\nimport { find, groupBy, keyBy } from 'lodash'\nimport {\n  subscribeBackupNgJobs,\n  subscribeJobs,\n  subscribeMetadataBackupJobs,\n  subscribeMirrorBackupJobs,\n  subscribeSchedules,\n} from 'xo'\n\nimport Metadata from './new/metadata'\nimport New from './new'\nimport NewMirrorBackup from './new/mirror'\nimport NewSequence from './new/sequence'\n\nexport default decorate([\n  addSubscriptions({\n    jobs: subscribeJobs,\n    backupJobs: subscribeBackupNgJobs,\n    metadataJobs: subscribeMetadataBackupJobs,\n    mirrorBackupJobs: subscribeMirrorBackupJobs,\n    schedulesByJob: cb =>\n      subscribeSchedules(schedules => {\n        cb(groupBy(schedules, 'jobId'))\n      }),\n  }),\n  provideState({\n    computed: {\n      job: (_, { backupJobs, jobs, metadataJobs, mirrorBackupJobs, routeParams: { id } }) =>\n        defined(\n          find(jobs, { id }),\n          find(backupJobs, { id }),\n          find(metadataJobs, { id }),\n          find(mirrorBackupJobs, { id })\n        ),\n      schedules: (_, { schedulesByJob, routeParams: { id } }) => schedulesByJob && keyBy(schedulesByJob[id], 'id'),\n      loading: (_, props) =>\n        props.jobs === undefined ||\n        props.backupJobs === undefined ||\n        props.metadataJobs === undefined ||\n        props.mirrorBackupJobs === undefined ||\n        props.schedulesByJob === undefined,\n    },\n  }),\n  injectState,\n  ({ state: { job, loading, schedules } }) =>\n    loading ? (\n      _('statusLoading')\n    ) : job === undefined ? (\n      <span className='text-danger'>\n        <Icon icon='error' /> {_('editJobNotFound')}\n      </span>\n    ) : job.type === 'backup' ? (\n      <New job={job} schedules={schedules} />\n    ) : job.type === 'mirrorBackup' ? (\n      <NewMirrorBackup job={job} schedules={schedules} />\n    ) : job.type === 'metadataBackup' ? (\n      <Metadata job={job} schedules={schedules} />\n    ) : job.type === 'call' && job.method === 'schedule.runSequence' ? (\n      <NewSequence job={job} schedule={schedules[Object.keys(schedules)[0]]} />\n    ) : (\n      'Unknown job type'\n    ),\n])\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport Component from 'base-component'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport Upgrade from 'xoa-upgrade'\nimport { addSubscriptions, noop, NumericDate } from 'utils'\nimport { confirm } from 'modal'\nimport { error } from 'notification'\nimport { deleteBackups, fetchFiles, listVmBackups, subscribeBackupNgJobs, subscribeRemotes } from 'xo'\nimport { filter, find, flatMap, forEach, keyBy, map, orderBy, reduce, toArray } from 'lodash'\n\nimport DeleteBackupsModalBody from '../restore/delete-backups-modal-body'\nimport RestoreFileModalBody from './restore-file-modal'\n\n// -----------------------------------------------------------------------------\n\nconst BACKUPS_COLUMNS = [\n  {\n    name: _('backupVmNameColumn'),\n    itemRenderer: ({ last }) => last.vm.name_label,\n    sortCriteria: 'last.vm.name_label',\n  },\n  {\n    name: _('backupVmDescriptionColumn'),\n    itemRenderer: ({ last }) => last.vm.name_description,\n    sortCriteria: 'last.vm.name_description',\n  },\n  {\n    name: _('firstBackupColumn'),\n    itemRenderer: ({ first }) => <NumericDate timestamp={first.timestamp} />,\n    sortCriteria: 'first.timestamp',\n    sortOrder: 'desc',\n  },\n  {\n    name: _('lastBackupColumn'),\n    itemRenderer: ({ last }) => <NumericDate timestamp={last.timestamp} />,\n    sortCriteria: 'last.timestamp',\n    default: true,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('availableBackupsColumn'),\n    itemRenderer: ({ count }) => count,\n    sortCriteria: 'count',\n  },\n]\n\n// -----------------------------------------------------------------------------\n\n@addSubscriptions({\n  jobs: subscribeBackupNgJobs,\n  remotes: subscribeRemotes,\n})\nexport default class Restore extends Component {\n  state = {\n    backupDataByVm: {},\n  }\n\n  componentWillReceiveProps(props) {\n    if (props.remotes !== this.props.remotes || props.jobs !== this.props.jobs) {\n      this._refreshBackupList(props.remotes, props.jobs)\n    }\n  }\n\n  _refreshBackupList = async (_remotes = this.props.remotes, jobs = this.props.jobs) => {\n    const remotes = keyBy(\n      filter(_remotes, remote => remote.enabled),\n      'id'\n    )\n    const backupsByRemote = await listVmBackups(toArray(remotes))\n\n    const backupDataByVm = {}\n    forEach(backupsByRemote, (backups, remoteId) => {\n      const remote = remotes[remoteId]\n      forEach(backups, (vmBackups, vmId) => {\n        vmBackups = filter(vmBackups, { mode: 'delta' })\n        if (vmBackups.length === 0) {\n          return\n        }\n        if (backupDataByVm[vmId] === undefined) {\n          backupDataByVm[vmId] = { backups: [] }\n        }\n\n        backupDataByVm[vmId].backups.push(\n          ...map(vmBackups, bkp => {\n            const job = find(jobs, { id: bkp.jobId })\n            return { ...bkp, remote, jobName: job && job.name }\n          })\n        )\n      })\n    })\n    let first, last\n    forEach(backupDataByVm, (data, vmId) => {\n      first = { timestamp: Infinity }\n      last = { timestamp: 0 }\n      let count = 0 // Number since there's only 1 mode in file restore\n      forEach(data.backups, backup => {\n        if (backup.timestamp > last.timestamp) {\n          last = backup\n        }\n        if (backup.timestamp < first.timestamp) {\n          first = backup\n        }\n        count++\n      })\n\n      Object.assign(data, { first, last, count, id: vmId })\n    })\n\n    forEach(backupDataByVm, ({ backups }, vmId) => {\n      backupDataByVm[vmId].backups = orderBy(backups, 'timestamp', 'desc')\n    })\n\n    this.setState({ backupDataByVm })\n  }\n\n  // Actions -------------------------------------------------------------------\n\n  _restore = ({ backups, last }) =>\n    confirm({\n      title: _('restoreFilesFromBackup', { name: last.vm.name_label }),\n      body: <RestoreFileModalBody vmName={last.vm.name_label} backups={backups} />,\n    }).then(({ remote, disk, format, partition, paths }) => {\n      if (remote === undefined || disk === undefined || paths.length === 0) {\n        return error(_('restoreFiles'), _('restoreFilesError'))\n      }\n      return fetchFiles(remote, disk, partition, paths, format)\n    }, noop)\n\n  _delete = data =>\n    confirm({\n      title: _('deleteVmBackupsTitle', { vm: data.last.vm.name_label }),\n      body: <DeleteBackupsModalBody backups={data.backups} />,\n      icon: 'delete',\n    })\n      .then(deleteBackups, noop)\n      .then(() => this._refreshBackupList())\n\n  _bulkDelete = datas =>\n    confirm({\n      title: _('deleteVmBackupsBulkTitle'),\n      body: <p>{_('deleteVmBackupsBulkMessage', { nVms: datas.length })}</p>,\n      icon: 'delete',\n      strongConfirm: {\n        messageId: 'deleteVmBackupsBulkConfirmText',\n        values: {\n          nBackups: reduce(datas, (sum, data) => sum + data.backups.length, 0),\n        },\n      },\n    })\n      .then(() => deleteBackups(flatMap(datas, 'backups')), noop)\n      .then(() => this._refreshBackupList())\n\n  // ---------------------------------------------------------------------------\n\n  _actions = [\n    {\n      handler: this._bulkDelete,\n      icon: 'delete',\n      individualHandler: this._delete,\n      label: _('deleteVmBackups'),\n      level: 'danger',\n    },\n  ]\n\n  _individualActions = [\n    {\n      handler: this._restore,\n      icon: 'restore',\n      label: _('restoreVmBackups'),\n      level: 'primary',\n    },\n  ]\n\n  render() {\n    return (\n      <Upgrade place='restoreBackup' available={4}>\n        <div>\n          <div className='mb-1'>\n            <ActionButton btnStyle='primary' handler={this._refreshBackupList} icon='refresh'>\n              {_('refreshBackupList')}\n            </ActionButton>\n          </div>\n          <SortedTable\n            actions={this._actions}\n            collection={this.state.backupDataByVm}\n            columns={BACKUPS_COLUMNS}\n            individualActions={this._individualActions}\n            stateUrlParam='s'\n          />\n        </div>\n      </Upgrade>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport ButtonGroup from 'button-group'\nimport Component from 'base-component'\nimport defined from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport React from 'react'\nimport Select from 'form/select'\nimport Tooltip from 'tooltip'\nimport { dirname } from 'path'\nimport { Container, Col, Row } from 'grid'\nimport { createSelector } from 'reselect'\nimport { formatSize } from 'utils'\nimport { filter, find, forEach, includes, isEmpty, map } from 'lodash'\nimport { getRenderXoItemOfType } from 'render-xo-item'\nimport { listPartitions, listFiles } from 'xo'\n\n// -----------------------------------------------------------------------------\n\nconst PARTITION_TYPE_NAMES = {\n  0x07: 'NTFS',\n  0x0c: 'FAT',\n  0x83: 'LINUX',\n}\n\nconst BACKUP_RENDERER = getRenderXoItemOfType('backup')\n\nconst diskOptionRenderer = disk => (\n  <span>\n    <Icon icon='disk' /> {disk.name}\n  </span>\n)\n\nconst partitionOptionRenderer = partition => (\n  <span>\n    {partition.name} {defined(PARTITION_TYPE_NAMES[partition.type], partition.type)}{' '}\n    {partition.size && `(${formatSize(+partition.size)})`}\n  </span>\n)\n\nconst fileOptionRenderer = ({ isFile, name }) => (\n  <span>\n    <Icon icon={isFile ? 'file' : 'folder'} /> {name}\n  </span>\n)\n\nconst ensureTrailingSlash = path => path + (path.endsWith('/') ? '' : '/')\n\n// -----------------------------------------------------------------------------\n\nconst formatFilesOptions = (rawFiles, path) => {\n  const files =\n    path !== '/'\n      ? [\n          {\n            id: '..',\n            isFile: false,\n            name: '..',\n            path: ensureTrailingSlash(dirname(path)),\n          },\n        ]\n      : []\n\n  return files.concat(\n    map(rawFiles, (_, name) => ({\n      id: `${path}${name}`,\n      isFile: !name.endsWith('/'),\n      name,\n      path: `${path}${name}`,\n    }))\n  )\n}\n\n// -----------------------------------------------------------------------------\n\nexport default class RestoreFileModalBody extends Component {\n  state = {\n    format: 'tgz',\n    selectedFiles: [],\n  }\n\n  get value() {\n    const { disk, format, partition, selectedFiles, backup } = this.state\n    const redundantFiles = this._getRedundantFiles()\n\n    return {\n      disk,\n      format,\n      partition,\n      paths: map(\n        selectedFiles.filter(({ path }) => !redundantFiles[path]),\n        'path'\n      ),\n      remote: backup.remote.id,\n    }\n  }\n\n  _listFiles = () => {\n    const { backup, disk, partition, path } = this.state\n    this.setState({ scanningFiles: true })\n\n    return listFiles(backup.remote.id, disk, path, partition).then(\n      rawFiles =>\n        this.setState({\n          files: formatFilesOptions(rawFiles, path),\n          scanningFiles: false,\n          listFilesError: false,\n        }),\n      error => {\n        this.setState({\n          scanningFiles: false,\n          listFilesError: true,\n        })\n        throw error\n      }\n    )\n  }\n\n  _getSelectableFiles = createSelector(\n    () => this.state.files,\n    () => this.state.selectedFiles,\n    (available, selected) => filter(available, file => !includes(selected, file))\n  )\n\n  _onBackupChange = backup => {\n    this.setState({\n      backup,\n      disk: undefined,\n      partition: undefined,\n      partitions: undefined,\n      file: undefined,\n      selectedFiles: [],\n      scanDiskError: false,\n      listFilesError: false,\n    })\n  }\n\n  _onDiskChange = disk => {\n    this.setState({\n      partition: undefined,\n      partitions: undefined,\n      file: undefined,\n      selectedFiles: [],\n      scanDiskError: false,\n      listFilesError: false,\n    })\n\n    if (!disk) {\n      return\n    }\n\n    listPartitions(this.state.backup.remote.id, disk).then(\n      partitions => {\n        if (isEmpty(partitions)) {\n          this.setState(\n            {\n              disk,\n              path: '/',\n            },\n            this._listFiles\n          )\n\n          return\n        }\n\n        this.setState({\n          disk,\n          partitions,\n        })\n      },\n      error => {\n        this.setState({\n          disk,\n          scanDiskError: true,\n        })\n        throw error\n      }\n    )\n  }\n\n  _onPartitionChange = partition => {\n    this.setState(\n      {\n        partition,\n        path: '/',\n        file: undefined,\n        selectedFiles: [],\n      },\n      partition && this._listFiles\n    )\n  }\n\n  _onFileChange = file => {\n    if (file == null) {\n      return\n    }\n\n    // Ugly workaround to keep the ReactSelect open after selecting a folder\n    // FIXME: Remove once something better is implemented in react-select:\n    // https://github.com/JedWatson/react-select/issues/1989\n    const select = document.activeElement\n    select.blur()\n    select.focus()\n\n    if (file.isFile) {\n      const { selectedFiles } = this.state\n      if (!includes(selectedFiles, file)) {\n        this.setState({\n          selectedFiles: selectedFiles.concat(file),\n        })\n      }\n    } else {\n      this.setState(\n        {\n          path: file.path,\n        },\n        this._listFiles\n      )\n    }\n  }\n\n  _selectFolder = () => {\n    const { selectedFiles, path } = this.state\n    this.setState({\n      selectedFiles: selectedFiles.concat({ id: path, path }),\n    })\n  }\n\n  _getFolderAlreadySelected = createSelector(\n    () => this.state.path,\n    () => this.state.selectedFiles,\n    (path, selectedFiles) => find(selectedFiles, { path }) !== undefined\n  )\n\n  _unselectFile = file => {\n    this.setState({\n      selectedFiles: filter(this.state.selectedFiles, ({ id }) => id !== file.id),\n    })\n  }\n\n  _unselectAllFiles = () => {\n    this.setState({\n      selectedFiles: [],\n    })\n  }\n\n  _selectAllFolderFiles = () => {\n    this.setState({\n      selectedFiles: this.state.selectedFiles.concat(filter(this._getSelectableFiles(), 'isFile')),\n    })\n  }\n\n  _getRedundantFiles = createSelector(\n    () => this.state.selectedFiles,\n    files => {\n      const redundantFiles = {}\n      forEach(files, file => {\n        redundantFiles[file.path] =\n          find(files, f => !f.isFile && f !== file && file.path.startsWith(f.path)) !== undefined\n      })\n      return redundantFiles\n    }\n  )\n\n  _linkState = ({ target }) => {\n    this.setState({ [target.name]: target.value })\n  }\n\n  // ---------------------------------------------------------------------------\n\n  render() {\n    const { backups } = this.props\n    const {\n      backup,\n      disk,\n      format,\n      partition,\n      partitions,\n      path,\n      scanDiskError,\n      listFilesError,\n      scanningFiles,\n      selectedFiles,\n    } = this.state\n    const noPartitions = isEmpty(partitions)\n    const redundantFiles = this._getRedundantFiles()\n\n    return (\n      <div>\n        <Select\n          labelKey='name'\n          onChange={this._onBackupChange}\n          optionRenderer={BACKUP_RENDERER}\n          options={backups}\n          placeholder={_('restoreFilesSelectBackup')}\n          value={backup}\n          valueKey='id'\n        />\n        {backup && [\n          <br />,\n          <Select\n            labelKey='name'\n            onChange={this._onDiskChange}\n            optionRenderer={diskOptionRenderer}\n            options={backup.disks}\n            placeholder={_('restoreFilesSelectDisk')}\n            value={disk}\n            valueKey='id'\n          />,\n        ]}\n        {scanDiskError && (\n          <span>\n            <Icon icon='error' /> {_('restoreFilesDiskError')}\n          </span>\n        )}\n        {disk &&\n          !scanDiskError &&\n          !noPartitions && [\n            <br />,\n            <Select\n              labelKey='name'\n              onChange={this._onPartitionChange}\n              optionRenderer={partitionOptionRenderer}\n              options={partitions}\n              placeholder={_('restoreFilesSelectPartition')}\n              value={partition}\n              valueKey='id'\n            />,\n          ]}\n        {(partition || (disk && !scanDiskError && noPartitions)) && [\n          <br />,\n          <Container>\n            <Row>\n              <Col size={9}>\n                <pre>\n                  {path} {scanningFiles && <Icon icon='loading' />}\n                  {listFilesError && <Icon icon='error' />}\n                </pre>\n              </Col>\n              <Col size={3}>\n                <span className='pull-right'>\n                  <ButtonGroup>\n                    <Tooltip content={_('restoreFilesSelectFolder')}>\n                      <ActionButton\n                        disabled={this._getFolderAlreadySelected()}\n                        handler={this._selectFolder}\n                        icon='folder'\n                        size='small'\n                      />\n                    </Tooltip>\n                    <Tooltip content={_('restoreFilesSelectAllFiles')}>\n                      <ActionButton handler={this._selectAllFolderFiles} icon='files' size='small' />\n                    </Tooltip>\n                  </ButtonGroup>\n                </span>\n              </Col>\n            </Row>\n          </Container>,\n          <Select\n            labelKey='name'\n            onChange={this._onFileChange}\n            optionRenderer={fileOptionRenderer}\n            options={this._getSelectableFiles()}\n            placeholder={_('restoreFilesSelectFiles')}\n            value={null}\n            valueKey='id'\n          />,\n          <br />,\n          selectedFiles.length ? (\n            <Container>\n              <Row>\n                <Col className='pl-0 pb-1' size={10}>\n                  <em>\n                    {_('restoreFilesSelectedFilesAndFolders', {\n                      files: selectedFiles.length,\n                    })}\n                  </em>\n                </Col>\n                <Col size={2} className='text-xs-right'>\n                  <ActionButton\n                    handler={this._unselectAllFiles}\n                    icon='remove'\n                    size='small'\n                    tooltip={_('restoreFilesUnselectAll')}\n                  />\n                </Col>\n              </Row>\n              {map(selectedFiles, file => (\n                <Row key={file.id}>\n                  <Col size={10}>\n                    <pre\n                      style={{\n                        textDecoration: redundantFiles[file.path] && 'line-through',\n                      }}\n                    >\n                      <Icon icon={file.isFile ? 'file' : 'folder'} /> {file.path}\n                    </pre>\n                  </Col>\n                  <Col size={2} className='text-xs-right'>\n                    <ActionButton handler={this._unselectFile} handlerParam={file} icon='remove' size='small' />\n                  </Col>\n                </Row>\n              ))}\n            </Container>\n          ) : (\n            <em>{_('restoreFilesNoFilesSelected')}</em>\n          ),\n        ]}\n        <Container className='mt-1'>\n          <Row>\n            <Col>{_('restoreFilesExportFormat')}</Col>\n          </Row>\n          <Row>\n            <Col size={6}>\n              <label>\n                <input checked={format === 'tgz'} name='format' onChange={this._linkState} type='radio' value='tgz' />{' '}\n                {_('restoreFilesTgz')}\n              </label>\n            </Col>\n            <Col size={6}>\n              <label>\n                <input checked={format === 'zip'} name='format' onChange={this._linkState} type='radio' value='zip' />{' '}\n                {_('restoreFilesZip')}\n              </label>\n            </Col>\n          </Row>\n        </Container>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport decorate from 'apply-decorators'\nimport defined, { get } from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport Link from 'link'\nimport NoObjects from 'no-objects'\nimport React from 'react'\nimport renderXoItem, { BackupJob, Vm } from 'render-xo-item'\nimport SortedTable from 'sorted-table'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { Container, Row, Col } from 'grid'\nimport { confirm } from 'modal'\nimport { createGetObjectsOfType, getLoneSnapshots } from 'selectors'\nimport { flatMapDepth, keyBy, map, toArray } from 'lodash'\nimport { FormattedRelative, FormattedTime } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport { addSubscriptions, connectStore, getDetachedBackupsOrSnapshots, noop, NumericDate } from 'utils'\nimport {\n  deleteBackups,\n  deleteSnapshot,\n  deleteSnapshots,\n  getRemotes,\n  listVmBackups,\n  subscribeBackupNgJobs,\n  subscribeMirrorBackupJobs,\n  subscribeSchedules,\n} from 'xo'\n\nconst DETACHED_BACKUP_COLUMNS = [\n  {\n    name: _('date'),\n    itemRenderer: backup => <NumericDate timestamp={backup.timestamp} />,\n    sortCriteria: 'timestamp',\n    sortOrder: 'desc',\n  },\n  {\n    name: _('vm'),\n    itemRenderer: ({ vm, vmId }) => <Vm id={vmId} link name={vm.name_label} />,\n    sortCriteria: ({ vm, vmId }, { vms }) => defined(vms[vmId], vm).name_label,\n  },\n  {\n    name: _('job'),\n    itemRenderer: ({ jobId }) => <BackupJob id={jobId} link />,\n    sortCriteria: ({ jobId }, { jobs }) => get(() => jobs[jobId].name),\n  },\n  {\n    name: _('jobModes'),\n    valuePath: 'mode',\n  },\n  {\n    name: _('reason'),\n    itemRenderer: ({ reason }) => _(reason),\n    sortCriteria: 'reason',\n  },\n]\n\nconst DETACHED_BACKUP_ACTIONS = [\n  {\n    handler: (backups, { fetchBackupList }) => {\n      const nBackups = backups.length\n      return confirm({\n        title: _('deleteBackups', { nBackups }),\n        body: _('deleteBackupsMessage', { nBackups }),\n        icon: 'delete',\n      })\n        .then(() => deleteBackups(backups), noop)\n        .then(fetchBackupList)\n    },\n    icon: 'delete',\n    label: backups => _('deleteBackups', { nBackups: backups.length }),\n    level: 'danger',\n  },\n]\n\nconst SNAPSHOT_COLUMNS = [\n  {\n    name: _('snapshotDate'),\n    itemRenderer: snapshot => (\n      <span>\n        <FormattedTime\n          day='numeric'\n          hour='numeric'\n          minute='numeric'\n          month='long'\n          value={snapshot.snapshot_time * 1000}\n          year='numeric'\n        />{' '}\n        (<FormattedRelative value={snapshot.snapshot_time * 1000} />)\n      </span>\n    ),\n    sortCriteria: 'snapshot_time',\n    sortOrder: 'desc',\n  },\n  {\n    name: _('vmNameLabel'),\n    itemRenderer: renderXoItem,\n    sortCriteria: 'name_label',\n  },\n  {\n    name: _('vmNameDescription'),\n    itemRenderer: snapshot => snapshot.name_description,\n    sortCriteria: 'name_description',\n  },\n  {\n    name: _('snapshotOf'),\n    itemRenderer: (snapshot, { vms }) => {\n      const vm = vms[snapshot.$snapshot_of]\n      return vm && <Link to={`/vms/${vm.id}`}>{renderXoItem(vm)}</Link>\n    },\n    sortCriteria: (snapshot, { vms }) => {\n      const vm = vms[snapshot.$snapshot_of]\n      return vm && vm.name_label\n    },\n  },\n  {\n    name: _('reason'),\n    itemRenderer: ({ reason }) => _(reason),\n    sortCriteria: 'reason',\n  },\n]\n\nconst ACTIONS = [\n  {\n    label: _('deleteSnapshots'),\n    individualLabel: _('deleteSnapshot'),\n    icon: 'delete',\n    level: 'danger',\n    handler: deleteSnapshots,\n    individualHandler: deleteSnapshot,\n  },\n]\n\nconst Health = decorate([\n  addSubscriptions({\n    // used by getLoneSnapshots\n    schedules: cb =>\n      subscribeSchedules(schedules => {\n        cb(keyBy(schedules, 'id'))\n      }),\n    jobs: cb => {\n      let backupJobs, mirrorJobs\n      subscribeBackupNgJobs(jobs => {\n        backupJobs = jobs\n        if (mirrorJobs !== undefined) {\n          // both are loaded\n          cb(keyBy([...backupJobs, ...mirrorJobs], 'id'))\n        }\n      })\n      subscribeMirrorBackupJobs(jobs => {\n        mirrorJobs = jobs\n        if (backupJobs !== undefined) {\n          // both are loaded\n          cb(keyBy([...backupJobs, ...mirrorJobs], 'id'))\n        }\n      })\n    },\n  }),\n  connectStore({\n    loneSnapshots: getLoneSnapshots,\n    legacySnapshots: createGetObjectsOfType('VM-snapshot').filter([\n      (() => {\n        const RE = /^(?:XO_DELTA_EXPORT:|XO_DELTA_BASE_VM_SNAPSHOT_|rollingSnapshot_)/\n        return (\n          { name_label } // eslint-disable-line camelcase\n        ) => RE.test(name_label)\n      })(),\n    ]),\n    vms: createGetObjectsOfType('VM'),\n  }),\n  provideState({\n    initialState: () => ({\n      backupsByRemote: undefined,\n    }),\n    effects: {\n      initialize({ fetchBackupList }) {\n        return fetchBackupList()\n      },\n      async fetchBackupList() {\n        this.state.backupsByRemote = await listVmBackups(toArray(await getRemotes()))\n      },\n    },\n    computed: {\n      detachedBackups: ({ backupsByRemote }, { jobs, vms, schedules }) => {\n        if (jobs === undefined || schedules === undefined) {\n          return []\n        }\n\n        return getDetachedBackupsOrSnapshots(\n          flatMapDepth(\n            backupsByRemote,\n            backupsByVm => map(backupsByVm, (vmBackups, vmId) => vmBackups.map(backup => ({ ...backup, vmId }))),\n            2\n          ),\n          { jobs, schedules, vms }\n        )\n      },\n    },\n  }),\n  injectState,\n  ({ effects: { fetchBackupList }, jobs, legacySnapshots, loneSnapshots, state: { detachedBackups }, vms }) => (\n    <Container>\n      <Row className='detached-backups'>\n        <Col>\n          <Card>\n            <CardHeader>\n              <Icon icon='backup' /> {_('detachedBackups')}\n            </CardHeader>\n            <CardBlock>\n              <div className='mb-1'>\n                <ActionButton btnStyle='primary' handler={fetchBackupList} icon='refresh'>\n                  {_('refreshBackupList')}\n                </ActionButton>\n              </div>\n              <NoObjects\n                actions={DETACHED_BACKUP_ACTIONS}\n                collection={detachedBackups}\n                columns={DETACHED_BACKUP_COLUMNS}\n                component={SortedTable}\n                data-fetchBackupList={fetchBackupList}\n                data-jobs={jobs}\n                data-vms={vms}\n                emptyMessage={_('noDetachedBackups')}\n                shortcutsTarget='.detached-backups'\n                stateUrlParam='s_detached_backups'\n              />\n            </CardBlock>\n          </Card>\n        </Col>\n      </Row>\n      <Row className='lone-snapshots'>\n        <Col>\n          <Card>\n            <CardHeader>\n              <Icon icon='vm' /> {_('detachedVmSnapshots')}\n            </CardHeader>\n            <CardBlock>\n              <NoObjects\n                actions={ACTIONS}\n                collection={loneSnapshots}\n                columns={SNAPSHOT_COLUMNS}\n                component={SortedTable}\n                data-vms={vms}\n                emptyMessage={_('noSnapshots')}\n                shortcutsTarget='.lone-snapshots'\n                stateUrlParam='s_vm_snapshots'\n              />\n            </CardBlock>\n          </Card>\n        </Col>\n      </Row>\n      <Row className='legacy-snapshots'>\n        <Col>\n          <Card>\n            <CardHeader>\n              <Icon icon='vm' /> {_('legacySnapshots')}\n            </CardHeader>\n            <CardBlock>\n              <NoObjects\n                actions={ACTIONS}\n                collection={legacySnapshots}\n                columns={SNAPSHOT_COLUMNS}\n                component={SortedTable}\n                data-vms={vms}\n                emptyMessage={_('noSnapshots')}\n                shortcutsTarget='.legacy-snapshots'\n                stateUrlParam='s_legacy_vm_snapshots'\n              />\n            </CardBlock>\n          </Card>\n        </Col>\n      </Row>\n    </Container>\n  ),\n])\n\nexport default Health\n","import _ from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport ButtonLink from 'button-link'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { adminOnly, connectStore, routes } from 'utils'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { Container, Row, Col } from 'grid'\nimport { createCounter, getLoneSnapshots } from 'selectors'\nimport { NavLink, NavTabs } from 'nav'\nimport { subscribeBackupNgJobs, subscribeSchedules } from 'xo'\n\nimport Edit from './edit'\nimport FileRestore from './file-restore'\nimport Health from './health'\nimport NewVmBackup, { NewMetadataBackup, NewMirrorBackup, NewSequence } from './new'\nimport Overview from './overview'\nimport Restore, { RestoreMetadata } from './restore'\nimport Sequences from './sequences'\n\nimport Page from '../page'\n\nconst HealthNavTab = decorate([\n  addSubscriptions({\n    // used by getLoneSnapshots\n    schedules: subscribeSchedules,\n    jobs: subscribeBackupNgJobs,\n  }),\n  connectStore({\n    nLoneSnapshots: createCounter(getLoneSnapshots),\n  }),\n  ({ nLoneSnapshots }) => (\n    <NavLink to='/backup/health'>\n      <Icon icon='menu-dashboard-health' /> {_('overviewHealthDashboardPage')}{' '}\n      {nLoneSnapshots > 0 && (\n        <Tooltip content={_('loneSnapshotsMessages', { nLoneSnapshots })}>\n          <span className='tag tag-pill tag-warning'>{nLoneSnapshots}</span>\n        </Tooltip>\n      )}\n    </NavLink>\n  ),\n])\n\nconst HEADER = (\n  <Container>\n    <Row>\n      <Col mediumSize={3}>\n        <h2>\n          <Icon icon='backup' /> {_('backupPage')}\n        </h2>\n      </Col>\n      <Col mediumSize={9}>\n        <NavTabs className='pull-right'>\n          <NavLink to='/backup/overview'>\n            <Icon icon='menu-backup-overview' /> {_('backupOverviewPage')}\n          </NavLink>\n          <NavLink to='/backup/sequences'>\n            <Icon icon='menu-backup-sequence' /> {_('sequences')}\n          </NavLink>\n          <NavLink to='/backup/new'>\n            <Icon icon='menu-backup-new' /> {_('backupNewPage')}\n          </NavLink>\n          <NavLink to='/backup/restore'>\n            <Icon icon='menu-backup-restore' /> {_('backupRestorePage')}\n          </NavLink>\n          <NavLink to='/backup/file-restore'>\n            <Icon icon='menu-backup-file-restore' /> {_('backupFileRestorePage')}\n          </NavLink>\n          <HealthNavTab />\n        </NavTabs>\n      </Col>\n    </Row>\n  </Container>\n)\n\nconst ChooseBackupType = () => (\n  <Container>\n    <Row>\n      <Col>\n        <Card>\n          <CardHeader>{_('backupType')}</CardHeader>\n          <CardBlock className='text-md-center'>\n            <p>\n              <ButtonLink to='backup/new/vms'>\n                <Icon icon='backup' /> {_('backupVms')}\n              </ButtonLink>{' '}\n              <ButtonLink to='backup/new/mirror'>\n                <Icon icon='mirror-backup' /> {_('mirrorBackupVms')}\n              </ButtonLink>\n            </p>\n            <p>\n              <ButtonLink to='backup/new/metadata'>\n                <Icon icon='database' /> {_('backupMetadata')}\n              </ButtonLink>{' '}\n              <ButtonLink to='backup/new/sequence'>\n                <Icon icon='menu-backup-sequence' /> {_('sequence')}\n              </ButtonLink>\n            </p>\n          </CardBlock>\n        </Card>\n      </Col>\n    </Row>\n  </Container>\n)\n\nexport default routes('overview', {\n  ':id/edit': Edit,\n  new: ChooseBackupType,\n  'new/vms': NewVmBackup,\n  'new/mirror': NewMirrorBackup,\n  'new/metadata': NewMetadataBackup,\n  'new/sequence': NewSequence,\n  overview: Overview,\n  sequences: Sequences,\n  restore: Restore,\n  'restore/metadata': RestoreMetadata,\n  'file-restore': FileRestore,\n  health: Health,\n})(\n  adminOnly(({ children }) => (\n    <Page header={HEADER} title='backupPage' formatTitle>\n      {children}\n    </Page>\n  ))\n)\n","import _ from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { generateId } from 'reaclette-utils'\nimport { injectState, provideState } from 'reaclette'\nimport keyBy from 'lodash/keyBy.js'\nimport { SelectProxy } from 'select-objects'\nimport { subscribeRemotes } from 'xo'\n\nimport { FormGroup } from '../utils'\n\nexport const RemoteProxy = decorate([\n  provideState({\n    effects: {\n      onChange(_, proxy) {\n        this.props.onChange(proxy !== null ? proxy.id : null)\n      },\n    },\n    computed: {\n      inputId: generateId,\n    },\n  }),\n  injectState,\n  ({ effects, value, onChange, state }) => (\n    <FormGroup>\n      <label htmlFor={state.inputId}>\n        <strong>{_('proxy')}</strong>\n      </label>\n      <SelectProxy id={state.inputId} onChange={effects.onChange} value={value} />\n    </FormGroup>\n  ),\n])\nRemoteProxy.propTypes = {\n  value: PropTypes.string,\n  onChange: PropTypes.func.isRequired,\n}\n\nexport const RemoteProxyWarning = decorate([\n  addSubscriptions({\n    remotes: cb =>\n      subscribeRemotes(remotes => {\n        cb(keyBy(remotes, 'id'))\n      }),\n  }),\n  provideState({\n    computed: {\n      showWarning: (_, { id, proxyId, remotes = {} }) => {\n        const remote = remotes[id]\n        if (proxyId === null) {\n          proxyId = undefined\n        }\n        return remote !== undefined && remote.proxy !== proxyId\n      },\n    },\n  }),\n  injectState,\n  ({ state }) =>\n    state.showWarning ? (\n      <Tooltip content={_('remoteNotCompatibleWithSelectedProxy')}>\n        <Icon icon='alarm' color='text-danger' />\n      </Tooltip>\n    ) : null,\n])\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport Link from 'link'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport Select from 'form/select'\nimport Tooltip from 'tooltip'\nimport { generateId } from 'reaclette-utils'\nimport { injectState, provideState } from 'reaclette'\n\nimport { FormGroup } from './../utils'\n\nexport const REPORT_WHEN_LABELS = {\n  always: 'reportWhenAlways',\n  // skipped and failure\n  failure: 'reportWhenSkippedFailure',\n  // only failure\n  error: 'reportWhenFailure',\n  never: 'reportWhenNever',\n}\n\nconst REPORT_WHEN_FILTER_OPTIONS = Object.entries(REPORT_WHEN_LABELS).map(([value, label]) => ({ label, value }))\n\nconst getOptionRenderer = ({ label }) => <span>{_(label)}</span>\n\nconst ReportWhen = decorate([\n  provideState({\n    computed: {\n      idInput: generateId,\n    },\n  }),\n  injectState,\n  ({ state, onChange, value, ...props }) => (\n    <FormGroup>\n      <label htmlFor={state.idInput}>\n        <strong>{_('reportWhen')}</strong>\n      </label>{' '}\n      <Tooltip content={_('pluginsWarning')}>\n        <Link className='btn btn-primary btn-sm' target='_blank' to='/settings/plugins'>\n          <Icon icon='menu-settings-plugins' /> <strong>{_('pluginsSettings')}</strong>\n        </Link>\n      </Tooltip>\n      <Select\n        id={state.idInput}\n        onChange={onChange}\n        optionRenderer={getOptionRenderer}\n        options={REPORT_WHEN_FILTER_OPTIONS}\n        value={value}\n        {...props}\n      />\n    </FormGroup>\n  ),\n])\n\nReportWhen.propTypes = {\n  onChange: PropTypes.func.isRequired,\n  value: PropTypes.string,\n}\n\nexport { ReportWhen as default }\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport moment from 'moment-timezone'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport UserError from 'user-error'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { form } from 'modal'\nimport { generateRandomId } from 'utils'\nimport { get } from '@xen-orchestra/defined'\nimport { injectState, provideState } from 'reaclette'\n\nimport { FormFeedback } from '../../utils'\n\nimport NewSchedule from './new'\n\nconst DEFAULT_SCHEDULE = {\n  cron: '0 0 * * *',\n  timezone: moment.tz.guess(),\n}\n\nconst setDefaultRetentions = (schedule, retentions) => {\n  retentions.forEach(({ defaultValue, valuePath }) => {\n    if (schedule[valuePath] === undefined) {\n      schedule[valuePath] = defaultValue\n    }\n  })\n  return schedule\n}\n\nexport const areRetentionsMissing = (value, retentions) =>\n  retentions.length !== 0 && !retentions.some(({ valuePath }) => value[valuePath] > 0)\n\nconst COLUMNS = [\n  {\n    valuePath: 'name',\n    name: _('scheduleName'),\n    default: true,\n  },\n  {\n    itemRenderer: (schedule, { toggleScheduleState }) => (\n      <StateButton\n        disabledLabel={_('stateDisabled')}\n        disabledTooltip={_('logIndicationToEnable')}\n        enabledLabel={_('stateEnabled')}\n        enabledTooltip={_('logIndicationToDisable')}\n        handler={toggleScheduleState}\n        handlerParam={schedule.id}\n        state={schedule.enabled}\n      />\n    ),\n    sortCriteria: 'enabled',\n    name: _('state'),\n  },\n  {\n    valuePath: 'cron',\n    name: _('scheduleCron'),\n  },\n  {\n    valuePath: 'timezone',\n    name: _('scheduleTimezone'),\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    handler: (schedule, { showModal }) => showModal(schedule),\n    icon: 'edit',\n    label: _('scheduleEdit'),\n    level: 'primary',\n  },\n  {\n    handler: ({ id }, { deleteSchedule }) => deleteSchedule(id),\n    icon: 'delete',\n    label: _('scheduleDelete'),\n    level: 'danger',\n  },\n]\n\nconst Schedules = decorate([\n  provideState({\n    effects: {\n      deleteSchedule: (_, id) => (state, props) => {\n        const schedules = { ...props.schedules }\n        delete schedules[id]\n        props.handlerSchedules(schedules)\n\n        const settings = { ...props.settings }\n        delete settings[id]\n        props.handlerSettings(settings)\n      },\n      showModal:\n        (effects, { id = generateRandomId(), name, cron, timezone } = DEFAULT_SCHEDULE) =>\n        async (state, props) => {\n          const schedule = get(() => props.schedules[id])\n          const setting = get(() => props.settings[id])\n\n          const {\n            cron: newCron,\n            name: newName,\n            timezone: newTimezone,\n            ...newSetting\n          } = await form({\n            defaultValue: setDefaultRetentions({ cron, name, timezone, ...setting }, state.retentions),\n            render: formProps => (\n              <NewSchedule retentions={state.retentions} withHealthCheck={props.withHealthCheck} {...formProps} />\n            ),\n            header: (\n              <span>\n                <Icon icon='schedule' /> {_('schedule')}\n              </span>\n            ),\n            size: 'large',\n            handler: value => {\n              if (areRetentionsMissing(value, state.retentions)) {\n                throw new UserError(_('newScheduleError'), _('retentionNeeded'))\n              }\n              return value\n            },\n          })\n\n          props.handlerSchedules({\n            ...props.schedules,\n            [id]: {\n              ...schedule,\n              cron: newCron,\n              id,\n              name: newName,\n              timezone: newTimezone,\n            },\n          })\n          props.handlerSettings({\n            ...props.settings,\n            [id]: {\n              ...setting,\n              ...newSetting,\n            },\n          })\n        },\n      toggleScheduleState:\n        (_, id) =>\n        (state, { handlerSchedules, schedules }) => {\n          const schedule = schedules[id]\n          handlerSchedules({\n            ...schedules,\n            [id]: {\n              ...schedule,\n              enabled: !schedule.enabled,\n            },\n          })\n        },\n    },\n    computed: {\n      columns: (_, { retentions }) => [...COLUMNS, ...retentions.map(({ defaultValue, ...props }) => props)],\n      rowTransform:\n        (_, { settings = {}, retentions }) =>\n        schedule => {\n          schedule = { ...schedule, ...settings[schedule.id] }\n          return setDefaultRetentions(schedule, retentions)\n        },\n    },\n  }),\n  injectState,\n  ({ state, effects, schedules, missingSchedules, missingRetentions }) => (\n    <FormFeedback\n      component={Card}\n      error={missingSchedules || missingRetentions}\n      message={missingSchedules ? _('missingSchedules') : _('missingRetentions')}\n    >\n      <CardHeader>\n        {_('backupSchedules')}*\n        <ActionButton\n          btnStyle='primary'\n          className='pull-right'\n          handler={effects.showModal}\n          icon='add'\n          tooltip={_('scheduleAdd')}\n        />\n      </CardHeader>\n      <CardBlock>\n        <SortedTable\n          collection={schedules}\n          columns={state.columns}\n          data-deleteSchedule={effects.deleteSchedule}\n          data-showModal={effects.showModal}\n          data-toggleScheduleState={effects.toggleScheduleState}\n          individualActions={INDIVIDUAL_ACTIONS}\n          rowTransform={state.rowTransform}\n          stateUrlParam='s_schedules'\n        />\n      </CardBlock>\n    </FormFeedback>\n  ),\n])\n\nSchedules.propTypes = {\n  handlerSchedules: PropTypes.func.isRequired,\n  handlerSettings: PropTypes.func.isRequired,\n  missingRetentions: PropTypes.bool,\n  missingSchedules: PropTypes.bool,\n  retentions: PropTypes.arrayOf(\n    PropTypes.shape({\n      defaultValue: PropTypes.number,\n      name: PropTypes.node.isRequired,\n      valuePath: PropTypes.string.isRequired,\n    })\n  ).isRequired,\n  schedules: PropTypes.object,\n  settings: PropTypes.object,\n}\n\nexport { Schedules as default }\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport Scheduler, { SchedulePreview } from 'scheduling'\nimport { generateId } from 'reaclette-utils'\nimport { injectState, provideState } from 'reaclette'\nimport { Number } from 'form'\n\nimport { FormGroup, Input } from '../../utils'\n\nimport { areRetentionsMissing } from '.'\n\nimport ScheduleHealthCheck from '../healthCheck/ScheduleHealthCheck'\n\nexport default decorate([\n  provideState({\n    effects: {\n      setSchedule:\n        (_, params) =>\n        (_, { value, onChange }) => {\n          onChange({\n            ...value,\n            ...params,\n          })\n        },\n      setCronTimezone:\n        ({ setSchedule }, { cronPattern: cron, timezone }) =>\n        () => {\n          setSchedule({\n            cron,\n            timezone,\n          })\n        },\n      setName:\n        ({ setSchedule }, { target: { value } }) =>\n        () => {\n          setSchedule({\n            name: value.trim() === '' ? null : value,\n          })\n        },\n      setRetention({ setSchedule }, value, { name }) {\n        setSchedule({\n          [name]: value,\n        })\n      },\n      toggleHealthCheck:\n        ({ setSchedule }, { target: { checked } }) =>\n        state =>\n          setSchedule({\n            healthCheckVmsWithTags: checked ? [] : undefined,\n            healthCheckSr: checked ? state.healthCheckSr : undefined,\n          }),\n      setHealthCheckSr: ({ setSchedule }, sr) => setSchedule({ healthCheckSr: sr.id }),\n      setHealthCheckTags: ({ setSchedule }, tags) =>\n        setSchedule({\n          healthCheckVmsWithTags: tags,\n        }),\n    },\n    computed: {\n      idInputName: generateId,\n\n      missingRetentions: (_, { value, retentions }) => areRetentionsMissing(value, retentions),\n    },\n  }),\n  injectState,\n  ({ effects, state, retentions, value: schedule, withHealthCheck = false }) => (\n    <div>\n      {state.missingRetentions && (\n        <div className='text-danger text-md-center'>\n          <Icon icon='alarm' /> {_('retentionNeeded')}\n        </div>\n      )}\n      <FormGroup>\n        <label htmlFor={state.idInputName}>\n          <strong>{_('formName')}</strong>\n        </label>\n        <Input id={state.idInputName} onChange={effects.setName} value={schedule.name} />\n      </FormGroup>\n      {retentions.map(({ name, valuePath }) => (\n        <FormGroup key={valuePath}>\n          <label>\n            <strong>{name}</strong>\n          </label>\n          <Number data-name={valuePath} min='0' onChange={effects.setRetention} required value={schedule[valuePath]} />\n        </FormGroup>\n      ))}\n      {withHealthCheck && (\n        <ScheduleHealthCheck\n          setHealthCheckSr={effects.setHealthCheckSr}\n          setHealthCheckTags={effects.setHealthCheckTags}\n          schedule={schedule}\n          toggleHealthCheck={effects.toggleHealthCheck}\n        />\n      )}\n      <Scheduler onChange={effects.setCronTimezone} cronPattern={schedule.cron} timezone={schedule.timezone} />\n      <SchedulePreview cronPattern={schedule.cron} timezone={schedule.timezone} />\n    </div>\n  ),\n])\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { CURRENT, PREMIUM } from 'xoa-plans'\nimport { generateId } from 'reaclette-utils'\nimport { injectState, provideState } from 'reaclette'\nimport { Select } from 'form'\n\nimport { FormGroup } from '../utils'\n\nconst OPTIONS = [\n  {\n    label: _('normal'),\n    value: '',\n  },\n  {\n    disabled: CURRENT.value < PREMIUM.value,\n    label: _('withMemory'),\n    value: 'checkpointSnapshot',\n  },\n  {\n    label: _('offline'),\n    value: 'offlineSnapshot',\n  },\n]\n\nconst SelectSnapshotMode = decorate([\n  provideState({\n    effects: {\n      setMode(_, value) {\n        this.props.setGlobalSettings({\n          offlineSnapshot: value === 'offlineSnapshot',\n          checkpointSnapshot: value === 'checkpointSnapshot',\n        })\n      },\n    },\n    computed: {\n      idSelect: generateId,\n      value: (_, { checkpointSnapshot, offlineSnapshot }) =>\n        checkpointSnapshot ? 'checkpointSnapshot' : offlineSnapshot ? 'offlineSnapshot' : '',\n    },\n  }),\n  injectState,\n  ({ state, effects, ...props }) => (\n    <FormGroup>\n      <label htmlFor={state.idSelect}>\n        <strong>{_('snapshotMode')}</strong>\n      </label>{' '}\n      <Select\n        {...props}\n        id={state.idSelect}\n        onChange={effects.setMode}\n        options={OPTIONS}\n        required\n        simpleValue\n        value={state.value}\n      />\n    </FormGroup>\n  ),\n])\n\nSelectSnapshotMode.propTypes = {\n  checkpointSnapshot: PropTypes.bool,\n  offlineSnapshot: PropTypes.bool,\n  setGlobalSettings: PropTypes.func.isRequired,\n}\n\nexport { SelectSnapshotMode as default }\n","import _ from 'intl'\nimport Icon from 'icon'\nimport React from 'react'\nimport Tags from 'tags'\nimport { conditionalTooltip } from 'tooltip'\nimport { getXoaPlan, ENTERPRISE } from 'xoa-plans'\nimport { SelectSr } from 'select-objects'\n\nimport { FormGroup } from '../../utils'\n\nconst ScheduleHealthCheck = ({ schedule, toggleHealthCheck, setHealthCheckTags, setHealthCheckSr }) => (\n  <FormGroup>\n    <label>\n      <strong>\n        <a\n          className='text-info'\n          rel='noreferrer'\n          href='https://docs.xen-orchestra.com/backups#backup-health-check'\n          target='_blank'\n        >\n          <Icon icon='info' />\n        </a>{' '}\n        {_('healthCheck')}\n      </strong>{' '}\n      {conditionalTooltip(\n        <input\n          type='checkbox'\n          checked={schedule.healthCheckVmsWithTags !== undefined}\n          disabled={getXoaPlan().value < ENTERPRISE.value}\n          onChange={toggleHealthCheck}\n          name='healthCheck'\n        />,\n        getXoaPlan().value < ENTERPRISE.value ? _('healthCheckAvailableEnterpriseUser') : undefined\n      )}\n    </label>\n    {schedule.healthCheckVmsWithTags !== undefined && (\n      <div className='mb-2'>\n        <strong>{_('vmsTags')}</strong>\n        <br />\n        <em>\n          <Icon icon='info' /> {_('healthCheckTagsInfo')}\n        </em>\n        <p className='h2'>\n          <Tags labels={schedule.healthCheckVmsWithTags} onChange={setHealthCheckTags} />\n        </p>\n        <strong>{_('healthCheckChooseSr')}</strong>\n        <SelectSr\n          onChange={setHealthCheckSr}\n          placeholder={_('healthCheckChooseSr')}\n          required\n          value={schedule.healthCheckSr}\n        />\n      </div>\n    )}\n  </FormGroup>\n)\n\nexport default ScheduleHealthCheck\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport SelectCompression from 'select-compression'\nimport decorate from 'apply-decorators'\nimport defined, { get } from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport Link from 'link'\nimport moment from 'moment-timezone'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport Upgrade from 'xoa-upgrade'\nimport UserError from 'user-error'\nimport ZstdChecker from 'zstd-checker'\nimport { addSubscriptions, connectStore, generateRandomId, resolveId, resolveIds } from 'utils'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { constructSmartPattern, destructSmartPattern } from 'smart-backup'\nimport { Container, Col, Row } from 'grid'\nimport { createGetObjectsOfType } from 'selectors'\nimport { form } from 'modal'\nimport { generateId, linkState } from 'reaclette-utils'\nimport { injectIntl } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport { Map } from 'immutable'\nimport { Number, Toggle } from 'form'\nimport { renderXoItemFromId, Remote } from 'render-xo-item'\nimport { SelectRemote, SelectSr, SelectVm } from 'select-objects'\nimport {\n  createBackupNgJob,\n  createSchedule,\n  deleteSchedule,\n  editBackupNgJob,\n  editSchedule,\n  getSuggestedExcludedTags,\n  isSrWritable,\n  subscribeRemotes,\n} from 'xo'\nimport { flatten, includes, isEmpty, map, mapValues, omit, some } from 'lodash'\n\nimport NewSchedule from './new-schedule'\nimport ReportWhen from './_reportWhen'\nimport Schedules from './schedules'\nimport SmartBackup from './smart-backup'\nimport SelectSnapshotMode from './_selectSnapshotMode'\nimport { RemoteProxy, RemoteProxyWarning } from './_remoteProxy'\n\nimport getSettingsWithNonDefaultValue from '../_getSettingsWithNonDefaultValue'\nimport { canDeltaBackup, constructPattern, destructPattern, FormFeedback, FormGroup, Input, Li, Ul } from './../utils'\n\nexport NewMetadataBackup from './metadata'\nexport NewMirrorBackup from './mirror'\nexport NewSequence from './sequence'\n\n// ===================================================================\n\nconst DEFAULT_RETENTION = 1\nconst DEFAULT_TIMEZONE = moment.tz.guess()\nconst DEFAULT_SCHEDULE = {\n  copyRetention: DEFAULT_RETENTION,\n  exportRetention: DEFAULT_RETENTION,\n  snapshotRetention: DEFAULT_RETENTION,\n  cron: '0 0 * * *',\n  timezone: DEFAULT_TIMEZONE,\n}\nconst RETENTION_LIMIT = 50\n\nexport const ReportRecipients = decorate([\n  provideState({\n    initialState: () => ({\n      recipient: '',\n    }),\n    effects: {\n      linkState,\n      add() {\n        this.props.add(this.state.recipient)\n        this.resetState()\n      },\n      onKeyDown({ add }, event) {\n        if (event.key === 'Enter') {\n          event.preventDefault()\n          add()\n        }\n      },\n    },\n    computed: {\n      inputId: generateId,\n      disabledAddButton: ({ recipient }) => recipient === '',\n    },\n  }),\n  injectIntl,\n  injectState,\n  ({ effects, intl: { formatMessage }, recipients, remove, state }) => (\n    <div>\n      <FormGroup>\n        <label htmlFor={state.inputId}>\n          <strong>{_('reportRecipients')}</strong>\n        </label>\n        <div className='input-group'>\n          <Input\n            id={state.inputId}\n            name='recipient'\n            onChange={effects.linkState}\n            onKeyDown={effects.onKeyDown}\n            placeholder={formatMessage(messages.emailPlaceholderExample)}\n            type='email'\n            value={state.recipient}\n          />\n          <span className='input-group-btn'>\n            <ActionButton btnStyle='primary' disabled={state.disabledAddButton} handler={effects.add} icon='add' />\n          </span>\n        </div>\n      </FormGroup>\n      <Ul className='mb-1'>\n        {map(recipients, (recipient, key) => (\n          <Li key={recipient}>\n            {recipient}{' '}\n            <ActionButton\n              btnStyle='danger'\n              className='pull-right'\n              handler={remove}\n              handlerParam={key}\n              icon='delete'\n              size='small'\n            />\n          </Li>\n        ))}\n      </Ul>\n    </div>\n  ),\n])\n\nconst SR_BACKEND_FAILURE_LINK = 'https://docs.xen-orchestra.com/backup_troubleshooting#sr_backend_failure_44'\n\nexport const BACKUP_NG_DOC_LINK = 'https://docs.xen-orchestra.com/backup'\n\nconst ThinProvisionedTip = ({ label }) => (\n  <Tooltip content={_(label)}>\n    <a className='text-info' href={SR_BACKEND_FAILURE_LINK} rel='noopener noreferrer' target='_blank'>\n      <Icon icon='info' />\n    </a>\n  </Tooltip>\n)\n\nconst normalizeTagValues = values => resolveIds(values).map(value => [value])\n\nconst normalizeSettings = ({ copyMode, exportMode, offlineBackupActive, settings, snapshotMode }) =>\n  settings.map(setting =>\n    defined(setting.copyRetention, setting.exportRetention, setting.snapshotRetention) !== undefined\n      ? {\n          ...setting,\n          cbtDestroySnapshotData: undefined,\n          copyRetention: copyMode ? setting.copyRetention : undefined,\n          exportRetention: exportMode ? setting.exportRetention : undefined,\n          snapshotRetention: snapshotMode && !offlineBackupActive ? setting.snapshotRetention : undefined,\n          preferNbd: undefined,\n        }\n      : setting\n  )\n\nconst destructVmsPattern = pattern =>\n  pattern.id === undefined\n    ? {\n        tags: destructSmartPattern(pattern.tags, flatten),\n      }\n    : {\n        vms: destructPattern(pattern),\n      }\n\n// isRetentionLow returns the expected result when the 'fullInterval' is undefined.\nconst isRetentionLow = (fullInterval, retention) => retention < RETENTION_LIMIT || fullInterval < RETENTION_LIMIT\n\nconst checkRetentions = (schedule, { copyMode, exportMode, snapshotMode }) =>\n  (!copyMode && !exportMode && !snapshotMode) ||\n  (copyMode && schedule.copyRetention > 0) ||\n  (exportMode && schedule.exportRetention > 0) ||\n  (snapshotMode && schedule.snapshotRetention > 0)\n\nconst createDoesRetentionExist = name => {\n  const predicate = setting => setting[name] > 0\n  return ({ propSettings, settings = propSettings }) => settings.some(predicate)\n}\n\nconst getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection, suggestedExcludedTags }) => {\n  setHomeVmIdsSelection([]) // Clear preselected vmIds\n  return {\n    _displayAdvancedSettings: undefined,\n    _proxyId: undefined,\n    _vmsPattern: undefined,\n    backupMode: false,\n    cbtDestroySnapshotData: false,\n    compression: undefined,\n    crMode: false,\n    deltaMode: false,\n    drMode: false,\n    name: '',\n    nbdConcurrency: 1,\n    nRetriesVmBackupFailures: 0,\n    preferNbd: false,\n    remotes: [],\n    schedules: {},\n    settings: undefined,\n    showErrors: false,\n    smartMode: false,\n    snapshotMode: false,\n    srs: [],\n    tags: { notValues: suggestedExcludedTags },\n    vms: preSelectedVmIds,\n  }\n}\n\nexport const DeleteOldBackupsFirst = ({ handler, handlerParam, value }) => (\n  <ActionButton\n    handler={handler}\n    handlerParam={handlerParam}\n    icon={value ? 'toggle-on' : 'toggle-off'}\n    iconColor={value ? 'text-success' : undefined}\n    size='small'\n    tooltip={_('deleteOldBackupsFirstMessage')}\n  >\n    {_('deleteOldBackupsFirst')}\n  </ActionButton>\n)\n\nconst New = decorate([\n  New => props => (\n    <Upgrade place='newBackup' required={2}>\n      <New {...props} />\n    </Upgrade>\n  ),\n  connectStore(() => ({\n    hostsById: createGetObjectsOfType('host'),\n    poolsById: createGetObjectsOfType('pool'),\n    srsById: createGetObjectsOfType('SR'),\n    preSelectedVmIds: state => state.homeVmIdsSelection,\n  })),\n  injectIntl,\n  provideState({\n    initialState: getInitialState,\n    effects: {\n      initialize: function ({ updateParams }) {\n        if (this.state.edition) {\n          updateParams()\n        }\n      },\n      createJob: () => async state => {\n        if (state.isJobInvalid) {\n          return {\n            ...state,\n            showErrors: true,\n          }\n        }\n\n        let schedules, settings\n        if (!isEmpty(state.schedules)) {\n          schedules = mapValues(state.schedules, ({ id, ...schedule }) => schedule)\n          settings = normalizeSettings({\n            offlineBackupActive: state.offlineBackupActive,\n            settings: state.settings,\n            exportMode: state.exportMode,\n            copyMode: state.copyMode,\n            snapshotMode: state.snapshotMode,\n          }).toObject()\n        } else {\n          const id = generateId()\n          schedules = {\n            [id]: DEFAULT_SCHEDULE,\n          }\n          settings = {\n            '': state.settings?.get(''),\n            [id]: {\n              copyRetention: state.copyMode ? DEFAULT_RETENTION : undefined,\n              exportRetention: state.exportMode ? DEFAULT_RETENTION : undefined,\n              snapshotRetention: state.snapshotMode && !state.offlineBackupActive ? DEFAULT_RETENTION : undefined,\n            },\n          }\n        }\n\n        if (settings[''] === undefined) {\n          settings[''] = { __proto__: null }\n        }\n\n        if (!state.backupMode && !state.deltaMode) {\n          delete settings[''].longTermRetention\n        }\n\n        if (settings['']?.maxExportRate <= 0) {\n          settings[''].maxExportRate = undefined\n        }\n\n        settings[''].timezone = DEFAULT_TIMEZONE\n\n        await createBackupNgJob({\n          name: state.name,\n          mode: state.isDelta ? 'delta' : 'full',\n          compression: state.compression,\n          proxy: state.proxyId === null ? undefined : state.proxyId,\n          schedules,\n          settings,\n          remotes: state.deltaMode || state.backupMode ? constructPattern(state.remotes) : undefined,\n          srs: state.crMode || state.drMode ? constructPattern(state.srs) : undefined,\n          vms: state.smartMode ? state.vmsSmartPattern : constructPattern(state.vms),\n        })\n      },\n      editJob: () => async (state, props) => {\n        if (state.isJobInvalid) {\n          return {\n            ...state,\n            showErrors: true,\n          }\n        }\n\n        await Promise.all(\n          map(props.schedules, oldSchedule => {\n            const id = oldSchedule.id\n            const newSchedule = state.schedules[id]\n\n            if (newSchedule === undefined) {\n              return deleteSchedule(id)\n            }\n\n            if (\n              newSchedule.cron !== oldSchedule.cron ||\n              newSchedule.name !== oldSchedule.name ||\n              newSchedule.timezone !== oldSchedule.timezone ||\n              newSchedule.enabled !== oldSchedule.enabled\n            ) {\n              return editSchedule({\n                id,\n                cron: newSchedule.cron,\n                name: newSchedule.name,\n                timezone: newSchedule.timezone,\n                enabled: newSchedule.enabled,\n              })\n            }\n          })\n        )\n\n        let settings = state.settings\n        await Promise.all(\n          map(state.schedules, async schedule => {\n            const tmpId = schedule.id\n            if (props.schedules[tmpId] === undefined) {\n              const { id } = await createSchedule(props.job.id, {\n                cron: schedule.cron,\n                name: schedule.name,\n                timezone: schedule.timezone,\n                enabled: schedule.enabled,\n              })\n\n              settings = settings.withMutations(settings => {\n                settings.set(id, settings.get(tmpId))\n                settings.delete(tmpId)\n              })\n            }\n          })\n        )\n\n        const normalizedSettings = normalizeSettings({\n          offlineBackupActive: state.offlineBackupActive,\n          settings: settings || state.propSettings,\n          exportMode: state.exportMode,\n          copyMode: state.copyMode,\n          snapshotMode: state.snapshotMode,\n        }).toObject()\n\n        if (normalizedSettings[''] === undefined) {\n          normalizedSettings[''] = { __proto__: null }\n        }\n\n        if (!state.backupMode && !state.deltaMode) {\n          delete normalizedSettings[''].longTermRetention\n        }\n\n        if (normalizedSettings['']?.maxExportRate <= 0) {\n          normalizedSettings[''].maxExportRate = undefined\n        }\n\n        normalizedSettings[''].timezone = DEFAULT_TIMEZONE\n\n        await editBackupNgJob({\n          id: props.job.id,\n          name: state.name,\n          mode: state.isDelta ? 'delta' : 'full',\n          compression: state.compression,\n          proxy: state.proxyId,\n          settings: normalizedSettings,\n          remotes: state.deltaMode || state.backupMode ? constructPattern(state.remotes) : constructPattern([]),\n          srs: state.crMode || state.drMode ? constructPattern(state.srs) : constructPattern([]),\n          vms: state.smartMode ? state.vmsSmartPattern : constructPattern(state.vms),\n        })\n      },\n      toggleMode:\n        (_, { mode }) =>\n        state => ({\n          ...state,\n          [mode]: !state[mode],\n        }),\n      setCheckboxValue:\n        (_, { target: { checked, name } }) =>\n        state => ({\n          ...state,\n          [name]: checked,\n        }),\n      toggleScheduleState: (_, id) => state => ({\n        ...state,\n        schedules: {\n          ...state.schedules,\n          [id]: {\n            ...state.schedules[id],\n            enabled: !state.schedules[id].enabled,\n          },\n        },\n      }),\n      setName:\n        (_, { target: { value } }) =>\n        state => ({\n          ...state,\n          name: value,\n        }),\n      setTargetDeleteFirst:\n        (_, id) =>\n        ({ propSettings, settings = propSettings }) => ({\n          settings: settings.set(id, {\n            deleteFirst: !settings.getIn([id, 'deleteFirst']),\n          }),\n        }),\n      addRemote: (_, remote) => state => {\n        return {\n          ...state,\n          remotes: [...state.remotes, resolveId(remote)],\n        }\n      },\n      deleteRemote: (_, key) => state => {\n        const remotes = [...state.remotes]\n        remotes.splice(key, 1)\n        return {\n          ...state,\n          remotes,\n        }\n      },\n      addSr: (_, sr) => state => ({\n        ...state,\n        srs: [...state.srs, resolveId(sr)],\n      }),\n      deleteSr: (_, key) => state => {\n        const srs = [...state.srs]\n        srs.splice(key, 1)\n        return {\n          ...state,\n          srs,\n        }\n      },\n      setVms: (_, vms) => state => ({ ...state, vms }),\n      updateParams:\n        () =>\n        (_, { job, schedules }) => {\n          const remotes = job.remotes !== undefined ? destructPattern(job.remotes) : []\n          const srs = job.srs !== undefined ? destructPattern(job.srs) : []\n\n          return {\n            name: job.name,\n            smartMode: job.vms.id === undefined,\n            snapshotMode: some(job.settings, ({ snapshotRetention }) => snapshotRetention > 0),\n            backupMode: job.mode === 'full' && !isEmpty(remotes),\n            deltaMode: job.mode === 'delta' && !isEmpty(remotes),\n            drMode: job.mode === 'full' && !isEmpty(srs),\n            crMode: job.mode === 'delta' && !isEmpty(srs),\n            remotes,\n            srs,\n            schedules,\n            ...destructVmsPattern(job.vms),\n          }\n        },\n      showScheduleModal:\n        ({ saveSchedule }, storedSchedule = DEFAULT_SCHEDULE) =>\n        async (\n          { copyMode, exportMode, deltaMode, isDelta, propSettings, settings = propSettings, snapshotMode },\n          { intl: { formatMessage } }\n        ) => {\n          const modes = { copyMode, isDelta, exportMode, snapshotMode }\n          const schedule = await form({\n            defaultValue: storedSchedule,\n            render: props => (\n              <NewSchedule\n                missingRetentions={!checkRetentions(props.value, modes)}\n                modes={modes}\n                showRetentionWarning={\n                  deltaMode &&\n                  !isRetentionLow(\n                    defined(props.value.fullInterval, settings.getIn(['', 'fullInterval'])),\n                    props.value.exportRetention\n                  )\n                }\n                {...props}\n              />\n            ),\n            header: (\n              <span>\n                <Icon icon='schedule' /> {_('schedule')}\n              </span>\n            ),\n            size: 'large',\n            handler: value => {\n              if (!checkRetentions(value, modes)) {\n                throw new UserError(_('newScheduleError'), _('retentionNeeded'))\n              }\n              return value\n            },\n          })\n\n          saveSchedule({\n            ...schedule,\n            id: storedSchedule.id || generateRandomId(),\n          })\n        },\n      deleteSchedule:\n        (_, schedule) =>\n        ({ schedules: oldSchedules, propSettings, settings = propSettings }) => {\n          const id = resolveId(schedule)\n          const schedules = { ...oldSchedules }\n          delete schedules[id]\n          return {\n            schedules,\n            settings: settings.delete(id),\n          }\n        },\n      saveSchedule:\n        (\n          _,\n          {\n            copyRetention,\n            cron,\n            enabled = true,\n            exportRetention,\n            fullInterval,\n            healthCheckSr,\n            healthCheckVmsWithTags,\n            id,\n            name,\n            snapshotRetention,\n            timezone,\n          }\n        ) =>\n        ({ propSettings, schedules, settings = propSettings }) => ({\n          schedules: {\n            ...schedules,\n            [id]: {\n              ...schedules[id],\n              cron,\n              enabled,\n              id,\n              name,\n              timezone,\n            },\n          },\n          settings: settings.set(id, {\n            copyRetention,\n            exportRetention,\n            fullInterval,\n            healthCheckSr,\n            healthCheckVmsWithTags,\n            snapshotRetention,\n          }),\n        }),\n      onVmsPatternChange: (_, _vmsPattern) => ({\n        _vmsPattern,\n      }),\n      setTagValues: (_, values) => state => ({\n        ...state,\n        tags: {\n          ...state.tags,\n          values,\n        },\n      }),\n      setTagNotValues: (_, notValues) => state => ({\n        ...state,\n        tags: {\n          ...state.tags,\n          notValues,\n        },\n      }),\n      resetJob:\n        ({ updateParams }) =>\n        (state, { job }) => {\n          if (job !== undefined) {\n            updateParams()\n          }\n\n          return getInitialState()\n        },\n      setCompression: (_, compression) => ({ compression }),\n      setProxy(_, id) {\n        this.state._proxyId = id\n      },\n      toggleDisplayAdvancedSettings:\n        () =>\n        ({ displayAdvancedSettings }) => ({\n          _displayAdvancedSettings: !displayAdvancedSettings,\n        }),\n      setGlobalSettings:\n        (_, globalSettings) =>\n        ({ propSettings, settings = propSettings }) => ({\n          settings: settings.update('', setting => ({\n            ...setting,\n            ...globalSettings,\n          })),\n        }),\n      addReportRecipient({ setGlobalSettings }, value) {\n        const { propSettings, settings = propSettings } = this.state\n        const reportRecipients = defined(settings.getIn(['', 'reportRecipients']), [])\n        if (!reportRecipients.includes(value)) {\n          setGlobalSettings({\n            reportRecipients: (reportRecipients.push(value), reportRecipients),\n          })\n        }\n      },\n      deleteReportRecipient({ setGlobalSettings }, key) {\n        const { propSettings, settings = propSettings } = this.state\n        const reportRecipients = settings.getIn(['', 'reportRecipients'])\n        setGlobalSettings({\n          reportRecipients: (reportRecipients.splice(key, 1), reportRecipients),\n        })\n      },\n      setLongTermRetention({ setGlobalSettings }, retention, granularity) {\n        const { propSettings, settings = propSettings } = this.state\n        const longTermRetention = settings.getIn(['', 'longTermRetention']) ?? {}\n\n        if (retention > 0) {\n          longTermRetention[granularity] = { retention, settings: {} } // settings will be used for advanced configuration in the future\n        } else {\n          delete longTermRetention[granularity]\n        }\n\n        setGlobalSettings({ longTermRetention: isEmpty(longTermRetention) ? undefined : longTermRetention })\n      },\n      setReportWhen:\n        ({ setGlobalSettings }, { value }) =>\n        () => {\n          setGlobalSettings({\n            reportWhen: value,\n          })\n        },\n      setConcurrency:\n        ({ setGlobalSettings }, concurrency) =>\n        () => {\n          setGlobalSettings({\n            concurrency,\n          })\n        },\n      setTimeout:\n        ({ setGlobalSettings }, value) =>\n        () => {\n          setGlobalSettings({\n            timeout: value && value * 3600e3,\n          })\n        },\n      setFullInterval({ setGlobalSettings }, fullInterval) {\n        setGlobalSettings({\n          fullInterval,\n        })\n      },\n      setMaxExportRate({ setGlobalSettings }, rate) {\n        setGlobalSettings({\n          maxExportRate: rate !== undefined ? rate * (1024 * 1024) : undefined,\n        })\n      },\n      setOfflineBackup:\n        ({ setGlobalSettings }, { target: { checked: offlineBackup } }) =>\n        () => {\n          setGlobalSettings({\n            offlineBackup,\n          })\n        },\n      setPreferNbd:\n        ({ setGlobalSettings }, preferNbd) =>\n        () => {\n          setGlobalSettings({\n            preferNbd,\n          })\n        },\n      setCbtDestroySnapshotData:\n        ({ setGlobalSettings }, cbtDestroySnapshotData) =>\n        () => {\n          setGlobalSettings({\n            cbtDestroySnapshotData,\n          })\n        },\n      setNbdConcurrency({ setGlobalSettings }, nbdConcurrency) {\n        setGlobalSettings({\n          nbdConcurrency,\n        })\n      },\n      setNRetriesVmBackupFailures({ setGlobalSettings }, nRetries) {\n        setGlobalSettings({\n          nRetriesVmBackupFailures: nRetries,\n        })\n      },\n      setBackupReportTpl({ setGlobalSettings }, compactBackupTpl) {\n        setGlobalSettings({\n          backupReportTpl: compactBackupTpl ? 'compactMjml' : 'mjml',\n        })\n      },\n      setHideSuccessfulItems({ setGlobalSettings }, hideSuccessfulItems) {\n        setGlobalSettings({\n          hideSuccessfulItems,\n        })\n      },\n      setMergeBackupsSynchronously({ setGlobalSettings }, mergeBackupsSynchronously) {\n        setGlobalSettings({\n          mergeBackupsSynchronously,\n        })\n      },\n    },\n    computed: {\n      compressionId: generateId,\n      formId: generateId,\n      inputConcurrencyId: generateId,\n      inputCbtDestroySnapshotData: generateId,\n      inputFullIntervalId: generateId,\n      inputMaxExportRate: generateId,\n      inputPreferNbd: generateId,\n      inputNbdConcurrency: generateId,\n      inputNRetriesVmBackupFailures: generateId,\n      inputBackupReportTplId: generateId,\n      inputHideSuccessfulItemsId: generateId,\n      inputMergeBackupsSynchronously: generateId,\n      inputTimeoutId: generateId,\n      inputLongTermRetentionDaily: generateId,\n      inputLongTermRetentionWeekly: generateId,\n      inputLongTermRetentionMonthly: generateId,\n      inputLongTermRetentionYearly: generateId,\n\n      // In order to keep the user preference, the offline backup is kept in the DB\n      // and it's considered active only when the full mode is enabled\n      offlineBackupActive: ({ isFull, propSettings, settings = propSettings }) =>\n        isFull && Boolean(settings.getIn(['', 'offlineBackup'])),\n      vmsPattern: ({ _vmsPattern }, { job }) =>\n        defined(_vmsPattern, () => (job.vms.id !== undefined ? undefined : job.vms), {\n          type: 'VM',\n        }),\n      edition: (_, { job }) => job !== undefined,\n      isJobInvalid: state =>\n        state.missingName ||\n        state.missingVms ||\n        state.missingBackupMode ||\n        state.missingSchedules ||\n        state.missingRemotes ||\n        state.missingSrs ||\n        state.missingExportRetention ||\n        state.missingCopyRetention ||\n        state.missingSnapshotRetention,\n      missingName: state => state.name.trim() === '',\n      missingVms: state => isEmpty(state.vms) && !state.smartMode,\n      missingBackupMode: state => !state.isDelta && !state.isFull && !state.snapshotMode,\n      missingRemotes: state => (state.backupMode || state.deltaMode) && isEmpty(state.remotes),\n      missingSrs: state => (state.drMode || state.crMode) && isEmpty(state.srs),\n      missingSchedules: (state, { job }) => job !== undefined && isEmpty(state.schedules),\n      missingExportRetention: (state, { job }) => job !== undefined && state.exportMode && !state.exportRetentionExists,\n      missingCopyRetention: (state, { job }) => job !== undefined && state.copyMode && !state.copyRetentionExists,\n      missingSnapshotRetention: (state, { job }) =>\n        job !== undefined && state.snapshotMode && !state.snapshotRetentionExists,\n      exportMode: state => state.backupMode || state.deltaMode,\n      copyMode: state => state.drMode || state.crMode,\n      exportRetentionExists: createDoesRetentionExist('exportRetention'),\n      copyRetentionExists: createDoesRetentionExist('copyRetention'),\n      snapshotRetentionExists: createDoesRetentionExist('snapshotRetention'),\n      isDelta: state => state.deltaMode || state.crMode,\n      isFull: state => state.backupMode || state.drMode,\n      vmsSmartPattern: ({ tags, vmsPattern }) => ({\n        ...vmsPattern,\n        tags: constructSmartPattern(tags, normalizeTagValues),\n      }),\n      vmPredicate:\n        ({ isDelta }, { hostsById, poolsById }) =>\n        ({ $container }) =>\n          !isDelta ||\n          canDeltaBackup(\n            get(() => hostsById[$container].version) || get(() => hostsById[poolsById[$container].master].version)\n          ),\n      selectedVmIds: state => resolveIds(state.vms),\n      showRetentionWarning: ({ deltaMode, propSettings, settings = propSettings, schedules }) => {\n        if (!deltaMode) {\n          return false\n        }\n\n        const globalFullInterval = settings.getIn(['', 'fullInterval'])\n        return some(\n          Object.keys(schedules),\n          key =>\n            !isRetentionLow(\n              defined(settings.getIn([key, 'fullInterval']), globalFullInterval),\n              settings.getIn([key, 'exportRetention'])\n            )\n        )\n      },\n      srPredicate:\n        ({ srs }) =>\n        sr =>\n          isSrWritable(sr) && !includes(srs, sr.id),\n      remotePredicate:\n        ({ proxyId, remotes }) =>\n        remote => {\n          if (proxyId === null) {\n            proxyId = undefined\n          }\n          return !remotes.includes(remote.id) && remote.value.proxy === proxyId\n        },\n      propSettings: (_, { job }) =>\n        Map(get(() => job.settings)).map(setting =>\n          defined(setting.copyRetention, setting.exportRetention, setting.snapshotRetention)\n            ? {\n                ...setting,\n                copyRetention: defined(setting.copyRetention, DEFAULT_RETENTION),\n                exportRetention: defined(setting.exportRetention, DEFAULT_RETENTION),\n                snapshotRetention: defined(setting.snapshotRetention, DEFAULT_RETENTION),\n              }\n            : setting\n        ),\n      proxyId: (s, p) => defined(s._proxyId, () => p.job.proxy),\n      displayAdvancedSettings: (state, props) =>\n        defined(\n          state._displayAdvancedSettings,\n          !isEmpty(\n            getSettingsWithNonDefaultValue(state.isFull ? 'full' : 'delta', {\n              compression: get(() => props.job.compression),\n              ...get(() => omit(props.job.settings[''], ['reportRecipients', 'reportWhen'])),\n            })\n          )\n        ),\n    },\n  }),\n  injectState,\n  ({ state, effects, remotes, srsById, job = {}, intl }) => {\n    const { formatMessage } = intl\n    const { propSettings, settings = propSettings } = state\n    const compression = defined(state.compression, job.compression, '')\n    const {\n      cbtDestroySnapshotData,\n      checkpointSnapshot,\n      concurrency,\n      fullInterval,\n      longTermRetention = {},\n      maxExportRate,\n      nbdConcurrency = 1,\n      nRetriesVmBackupFailures = 0,\n      offlineBackup,\n      offlineSnapshot,\n      preferNbd,\n      reportRecipients,\n      reportWhen = 'failure',\n      backupReportTpl = 'mjml',\n      hideSuccessfulItems,\n      mergeBackupsSynchronously,\n      timeout,\n    } = settings.get('') || {}\n\n    return (\n      <form id={state.formId}>\n        <Container>\n          <Row>\n            <Col mediumSize={6}>\n              <Card>\n                <CardHeader>{_('backupName')}*</CardHeader>\n                <CardBlock>\n                  <FormFeedback\n                    component={Input}\n                    message={_('missingBackupName')}\n                    onChange={effects.setName}\n                    error={state.showErrors ? state.missingName : undefined}\n                    value={state.name}\n                  />\n                </CardBlock>\n              </Card>\n              <FormFeedback\n                component={Card}\n                error={state.showErrors ? state.missingBackupMode : undefined}\n                message={_('missingBackupMode')}\n              >\n                <CardBlock>\n                  <div className='text-xs-center'>\n                    <ActionButton\n                      active={state.snapshotMode}\n                      data-mode='snapshotMode'\n                      disabled={state.offlineBackupActive}\n                      handler={effects.toggleMode}\n                      icon='rolling-snapshot'\n                    >\n                      {_('rollingSnapshot')}\n                    </ActionButton>{' '}\n                    <ActionButton\n                      active={state.backupMode}\n                      data-mode='backupMode'\n                      disabled={state.isDelta}\n                      handler={effects.toggleMode}\n                      icon='backup'\n                    >\n                      {_('backup')}\n                    </ActionButton>{' '}\n                    <ActionButton\n                      active={state.deltaMode}\n                      data-mode='deltaMode'\n                      disabled={state.isFull || (!state.deltaMode && process.env.XOA_PLAN < 3)}\n                      handler={effects.toggleMode}\n                      icon='delta-backup'\n                    >\n                      {_('deltaBackup')}\n                    </ActionButton>{' '}\n                    <ActionButton\n                      active={state.drMode}\n                      data-mode='drMode'\n                      disabled={state.isDelta || (!state.drMode && process.env.XOA_PLAN < 3)}\n                      handler={effects.toggleMode}\n                      icon='disaster-recovery'\n                    >\n                      {_('disasterRecovery')}\n                    </ActionButton>{' '}\n                    {process.env.XOA_PLAN < 3 && (\n                      <Tooltip content={_('dbAndDrRequireEnterprisePlan')}>\n                        <Icon icon='info' />\n                      </Tooltip>\n                    )}{' '}\n                    <ActionButton\n                      active={state.crMode}\n                      data-mode='crMode'\n                      disabled={state.isFull || (!state.crMode && process.env.XOA_PLAN < 4)}\n                      handler={effects.toggleMode}\n                      icon='continuous-replication'\n                    >\n                      {_('continuousReplication')}\n                    </ActionButton>{' '}\n                    {process.env.XOA_PLAN < 4 && (\n                      <Tooltip content={_('crRequiresPremiumPlan')}>\n                        <Icon icon='info' />\n                      </Tooltip>\n                    )}\n                    <br />\n                    <a className='text-muted' href={BACKUP_NG_DOC_LINK} rel='noopener noreferrer' target='_blank'>\n                      <Icon icon='info' /> {_('backupNgLinkToDocumentationMessage')}\n                    </a>\n                  </div>\n                </CardBlock>\n              </FormFeedback>\n              <br />\n              {(state.backupMode || state.deltaMode) && (\n                <Card>\n                  <CardHeader>\n                    {_(state.backupMode ? 'backup' : 'deltaBackup')}\n                    <Link className='btn btn-primary pull-right' target='_blank' to='/settings/remotes'>\n                      <Icon icon='settings' /> <strong>{_('remotesSettings')}</strong>\n                    </Link>\n                  </CardHeader>\n                  <CardBlock>\n                    {isEmpty(remotes) ? (\n                      <span className='text-warning'>\n                        <Icon icon='alarm' /> {_('createRemoteMessage')}\n                      </span>\n                    ) : (\n                      <FormGroup>\n                        <label>\n                          <strong>{_('backupTargetRemotes')}</strong>\n                        </label>\n                        <FormFeedback\n                          component={SelectRemote}\n                          message={_('missingRemotes')}\n                          onChange={effects.addRemote}\n                          predicate={state.remotePredicate}\n                          error={state.showErrors ? state.missingRemotes : undefined}\n                          value={null}\n                        />\n                        <br />\n                        <Ul>\n                          {map(state.remotes, (id, key) => (\n                            <Li key={id}>\n                              <Remote id={id} /> <RemoteProxyWarning id={id} proxyId={state.proxyId} />\n                              <div className='pull-right'>\n                                <DeleteOldBackupsFirst\n                                  handler={effects.setTargetDeleteFirst}\n                                  handlerParam={id}\n                                  value={settings.getIn([id, 'deleteFirst'])}\n                                />{' '}\n                                <ActionButton\n                                  btnStyle='danger'\n                                  handler={effects.deleteRemote}\n                                  handlerParam={key}\n                                  icon='delete'\n                                  size='small'\n                                />\n                              </div>\n                            </Li>\n                          ))}\n                        </Ul>\n                      </FormGroup>\n                    )}\n                  </CardBlock>\n                </Card>\n              )}\n              {(state.drMode || state.crMode) && (\n                <Card>\n                  <CardHeader>{_(state.drMode ? 'disasterRecovery' : 'continuousReplication')}</CardHeader>\n                  <CardBlock>\n                    <FormGroup>\n                      <label>\n                        <strong>{_('backupTargetSrs')}</strong>\n                      </label>\n                      <FormFeedback\n                        component={SelectSr}\n                        message={_('missingSrs')}\n                        onChange={effects.addSr}\n                        predicate={state.srPredicate}\n                        error={state.showErrors ? state.missingSrs : undefined}\n                        value={null}\n                      />\n                      <br />\n                      <Ul>\n                        {map(state.srs, (id, key) => (\n                          <Li key={id}>\n                            {renderXoItemFromId(id)}{' '}\n                            {!isEmpty(srsById) && state.crMode && get(() => srsById[id].SR_type) === 'lvm' && (\n                              <ThinProvisionedTip label='crOnThickProvisionedSrWarning' />\n                            )}\n                            <div className='pull-right'>\n                              <DeleteOldBackupsFirst\n                                handler={effects.setTargetDeleteFirst}\n                                handlerParam={id}\n                                value={settings.getIn([id, 'deleteFirst'])}\n                              />{' '}\n                              <ActionButton\n                                btnStyle='danger'\n                                icon='delete'\n                                size='small'\n                                handler={effects.deleteSr}\n                                handlerParam={key}\n                              />\n                            </div>\n                          </Li>\n                        ))}\n                      </Ul>\n                    </FormGroup>\n                  </CardBlock>\n                </Card>\n              )}\n              <Card>\n                <CardHeader>\n                  {_('newBackupSettings')}\n                  <ActionButton\n                    className='pull-right'\n                    data-mode='_displayAdvancedSettings'\n                    handler={effects.toggleDisplayAdvancedSettings}\n                    icon={state.displayAdvancedSettings ? 'toggle-on' : 'toggle-off'}\n                    iconColor={state.displayAdvancedSettings ? 'text-success' : undefined}\n                    size='small'\n                  >\n                    {_('newBackupAdvancedSettings')}\n                  </ActionButton>\n                </CardHeader>\n                <CardBlock>\n                  <RemoteProxy onChange={effects.setProxy} value={state.proxyId} />\n                  <ReportWhen\n                    onChange={effects.setReportWhen}\n                    required\n                    //  Handle improper value introduced by:\n                    //  https://github.com/vatesfr/xen-orchestra/commit/753ee994f2948bbaca9d3161eaab82329a682773#diff-9c044ab8a42ed6576ea927a64c1ec3ebR105\n                    value={reportWhen === 'Never' ? 'never' : reportWhen}\n                  />\n                  <ReportRecipients\n                    add={effects.addReportRecipient}\n                    recipients={reportRecipients}\n                    remove={effects.deleteReportRecipient}\n                  />\n                  {state.displayAdvancedSettings && (\n                    <div>\n                      <FormGroup>\n                        <label htmlFor={state.inputConcurrencyId}>\n                          <strong>{_('concurrency')}</strong>\n                        </label>\n                        <Number\n                          id={state.inputConcurrencyId}\n                          min={1}\n                          onChange={effects.setConcurrency}\n                          value={concurrency}\n                        />\n                      </FormGroup>\n                      <FormGroup>\n                        <label htmlFor={state.inputNRetriesVmBackupFailures}>\n                          <strong>{_('nRetriesVmBackupFailures')}</strong>\n                        </label>\n                        <Number\n                          id={state.inputNRetriesVmBackupFailures}\n                          min={0}\n                          onChange={effects.setNRetriesVmBackupFailures}\n                          value={nRetriesVmBackupFailures}\n                        />\n                      </FormGroup>\n                      <FormGroup>\n                        <label htmlFor={state.inputTimeoutId}>\n                          <strong>{_('timeout')}</strong>\n                        </label>{' '}\n                        <Tooltip content={_('timeoutInfo')}>\n                          <Icon icon='info' />\n                        </Tooltip>\n                        <Number\n                          id={state.inputTimeoutId}\n                          onChange={effects.setTimeout}\n                          value={timeout && timeout / 3600e3}\n                          placeholder={formatMessage(messages.timeoutUnit)}\n                        />\n                      </FormGroup>\n                      {state.isDelta && (\n                        <FormGroup>\n                          <label htmlFor={state.inputFullIntervalId}>\n                            <strong>{_('fullBackupInterval')}</strong>\n                          </label>{' '}\n                          {state.showRetentionWarning && (\n                            <Tooltip content={_('deltaChainRetentionWarning')}>\n                              <Icon icon='error' />\n                            </Tooltip>\n                          )}{' '}\n                          <Tooltip content={_('clickForMoreInformation')}>\n                            <a\n                              className='text-info'\n                              href='https://xen-orchestra.com/docs/incremental_backups.html#key-backup-interval'\n                              rel='noopener noreferrer'\n                              target='_blank'\n                            >\n                              <Icon icon='info' />\n                            </a>\n                          </Tooltip>\n                          <Number\n                            id={state.inputFullIntervalId}\n                            onChange={effects.setFullInterval}\n                            value={fullInterval}\n                          />\n                        </FormGroup>\n                      )}\n                      {state.isDelta && (\n                        <div>\n                          <FormGroup>\n                            <label htmlFor={state.inputPreferNbd}>\n                              <strong>{_('preferNbd')}</strong>{' '}\n                              <Tooltip content={_('preferNbdInformation')}>\n                                <Icon icon='info' />\n                              </Tooltip>\n                            </label>\n                            <Toggle\n                              className='pull-right'\n                              id={state.inputPreferNbd}\n                              name='preferNbd'\n                              value={preferNbd}\n                              onChange={effects.setPreferNbd}\n                            />\n                          </FormGroup>\n                          <FormGroup>\n                            <label htmlFor={state.inputCbtDestroySnapshotData}>\n                              <strong>{_('cbtDestroySnapshotData')}</strong>{' '}\n                              <Tooltip content={_('cbtDestroySnapshotDataInformation')}>\n                                <Icon icon='info' />\n                              </Tooltip>\n                            </label>\n                            <Tooltip\n                              content={\n                                !preferNbd || state.snapshotMode\n                                  ? _('cbtDestroySnapshotDataDisabledInformation')\n                                  : undefined\n                              }\n                            >\n                              <Toggle\n                                className='pull-right'\n                                id={state.cbtDestroySnapshotData}\n                                name='cbtDestroySnapshotData'\n                                value={preferNbd && cbtDestroySnapshotData && !state.snapshotMode}\n                                disabled={!preferNbd || state.snapshotMode}\n                                onChange={effects.setCbtDestroySnapshotData}\n                              />\n                            </Tooltip>\n                          </FormGroup>\n                        </div>\n                      )}\n                      {state.isDelta && (\n                        <FormGroup>\n                          <label htmlFor={state.inputNbdConcurrency}>\n                            <strong>{_('nbdConcurrency')}</strong>\n                          </label>\n                          <Number\n                            id={state.inputNbdConcurrency}\n                            min={1}\n                            onChange={effects.setNbdConcurrency}\n                            value={nbdConcurrency}\n                            disabled={!state.inputPreferNbd}\n                          />\n                        </FormGroup>\n                      )}\n                      <FormGroup>\n                        <label htmlFor={state.inputMaxExportRate}>\n                          <strong>{_('speedLimit')}</strong>\n                        </label>\n                        <Number\n                          id={state.inputMaxExportRate}\n                          min={0}\n                          onChange={effects.setMaxExportRate}\n                          value={maxExportRate / (1024 * 1024)}\n                        />\n                      </FormGroup>\n                      {state.isFull && (\n                        <div>\n                          <FormGroup>\n                            <label htmlFor={state.compressionId}>\n                              <strong>{_('compression')}</strong>\n                            </label>\n                            <SelectCompression\n                              id={state.compressionId}\n                              onChange={effects.setCompression}\n                              value={compression}\n                            />\n                          </FormGroup>\n                          <FormGroup>\n                            <label>\n                              <strong>{_('offlineBackup')}</strong>{' '}\n                              <Tooltip content={_('offlineBackupInfo')}>\n                                <Icon icon='info' />\n                              </Tooltip>{' '}\n                              <input\n                                checked={offlineBackup}\n                                disabled={offlineSnapshot || checkpointSnapshot}\n                                onChange={effects.setOfflineBackup}\n                                type='checkbox'\n                              />\n                            </label>\n                          </FormGroup>\n                        </div>\n                      )}\n                      <SelectSnapshotMode\n                        checkpointSnapshot={checkpointSnapshot}\n                        disabled={state.offlineBackupActive}\n                        offlineSnapshot={offlineSnapshot}\n                        setGlobalSettings={effects.setGlobalSettings}\n                      />\n                      <FormGroup>\n                        <label htmlFor={state.inputBackupReportTplId}>\n                          <strong>{_('shorterBackupReports')}</strong>\n                        </label>\n                        <Toggle\n                          className='pull-right'\n                          id={state.inputBackupReportTplId}\n                          value={backupReportTpl === 'compactMjml'}\n                          onChange={effects.setBackupReportTpl}\n                        />\n                      </FormGroup>\n                      <FormGroup>\n                        <label htmlFor={state.inputHideSuccessfulItemsId}>\n                          <strong>{_('hideSuccessfulItems')}</strong>\n                        </label>\n                        <Toggle\n                          className='pull-right'\n                          id={state.inputHideSuccessfulItemsId}\n                          value={hideSuccessfulItems}\n                          onChange={effects.setHideSuccessfulItems}\n                        />\n                      </FormGroup>\n                      <FormGroup>\n                        <label htmlFor={state.inputMergeBackupsSynchronously}>\n                          <strong>{_('mergeBackupsSynchronously')}</strong>\n                        </label>{' '}\n                        <Tooltip content={_('mergeBackupsSynchronouslyTooltip')}>\n                          <Icon icon='info' />\n                        </Tooltip>\n                        <Toggle\n                          className='pull-right'\n                          id={state.inputMergeBackupsSynchronously}\n                          name='mergeBackupsSynchronously'\n                          value={mergeBackupsSynchronously}\n                          onChange={effects.setMergeBackupsSynchronously}\n                        />\n                      </FormGroup>\n                    </div>\n                  )}\n                </CardBlock>\n              </Card>\n            </Col>\n            <Col mediumSize={6}>\n              <Card>\n                <CardHeader>\n                  {_('vmsToBackup')}* <ThinProvisionedTip label='vmsOnThinProvisionedSrTip' />\n                  <ActionButton\n                    className='pull-right'\n                    data-mode='smartMode'\n                    handler={effects.toggleMode}\n                    icon={state.smartMode ? 'toggle-on' : 'toggle-off'}\n                    iconColor={state.smartMode ? 'text-success' : undefined}\n                    size='small'\n                  >\n                    {_('smartBackupModeTitle')}\n                  </ActionButton>\n                </CardHeader>\n                <CardBlock>\n                  {state.isDelta && (\n                    <span className='text-muted'>\n                      <Icon icon='info' /> {_('deltaBackupOnOutdatedXenServerWarning')}\n                    </span>\n                  )}\n\n                  {state.smartMode ? (\n                    <Upgrade place='newBackup' required={3}>\n                      <SmartBackup\n                        deltaMode={state.isDelta}\n                        onChange={effects.onVmsPatternChange}\n                        pattern={state.vmsPattern}\n                      />\n                    </Upgrade>\n                  ) : (\n                    <div>\n                      <FormFeedback\n                        component={SelectVm}\n                        error={state.showErrors ? state.missingVms : undefined}\n                        message={_('missingVms')}\n                        multi\n                        onChange={effects.setVms}\n                        predicate={state.vmPredicate}\n                        value={state.vms}\n                      />\n                      {compression === 'zstd' && <ZstdChecker vms={state.selectedVmIds} />}\n                    </div>\n                  )}\n                </CardBlock>\n              </Card>\n              <Schedules />\n              {(state.backupMode || state.deltaMode) && (\n                <Card>\n                  <CardHeader>{_('longTermRetention')}</CardHeader>\n                  <CardBlock>\n                    <FormGroup>\n                      <label htmlFor={state.inputLongTermRetentionDaily}>\n                        <strong>{_('numberOfDailyBackupsKept')}</strong>\n                      </label>\n                      <Number\n                        id={state.inputLongTermRetentionDaily}\n                        onChange={value => effects.setLongTermRetention(value, 'daily')}\n                        value={longTermRetention.daily?.retention}\n                      />\n                    </FormGroup>\n                    <FormGroup>\n                      <label htmlFor={state.inputLongTermRetentionWeekly}>\n                        <strong>{_('numberOfWeeklyBackupsKept')}</strong>\n                      </label>\n                      <Number\n                        id={state.inputLongTermRetentionWeekly}\n                        onChange={value => effects.setLongTermRetention(value, 'weekly')}\n                        value={longTermRetention.weekly?.retention}\n                      />\n                    </FormGroup>\n                    <FormGroup>\n                      <label htmlFor={state.inputLongTermRetentionMonthly}>\n                        <strong>{_('numberOfMonthlyBackupsKept')}</strong>\n                      </label>\n                      <Number\n                        id={state.inputLongTermRetentionMonthly}\n                        onChange={value => effects.setLongTermRetention(value, 'monthly')}\n                        value={longTermRetention.monthly?.retention}\n                      />\n                    </FormGroup>\n                    <FormGroup>\n                      <label htmlFor={state.inputLongTermRetentionYearly}>\n                        <strong>{_('numberOfYearlyBackupsKept')}</strong>\n                      </label>\n                      <Number\n                        id={state.inputLongTermRetentionYearly}\n                        onChange={value => effects.setLongTermRetention(value, 'yearly')}\n                        value={longTermRetention.yearly?.retention}\n                      />\n                    </FormGroup>\n                  </CardBlock>\n                </Card>\n              )}\n            </Col>\n          </Row>\n          <Row>\n            <Card>\n              <CardBlock>\n                {state.edition ? (\n                  // editing a backup is done from /backup/overview\n                  // using GO_BACK on success allows the user to be redirected\n                  // to the overview with all the filters/page intact\n\n                  <ActionButton\n                    btnStyle='primary'\n                    form={state.formId}\n                    handler={effects.editJob}\n                    icon='save'\n                    redirectOnSuccess={state.isJobInvalid ? undefined : ActionButton.GO_BACK}\n                    size='large'\n                  >\n                    {_('formSave')}\n                  </ActionButton>\n                ) : (\n                  // creating a new backup can be initiated from the nav menu,\n                  // the user can be anywhere in xo-web\n                  // we force a redirection to /backup to ensure a consistent\n                  // browsing experience\n\n                  <ActionButton\n                    btnStyle='primary'\n                    form={state.formId}\n                    handler={effects.createJob}\n                    icon='save'\n                    redirectOnSuccess={state.isJobInvalid ? undefined : '/backup'}\n                    size='large'\n                  >\n                    {_('formCreate')}\n                  </ActionButton>\n                )}\n                <ActionButton handler={effects.resetJob} icon='undo' className='pull-right' size='large'>\n                  {_('formReset')}\n                </ActionButton>\n              </CardBlock>\n            </Card>\n          </Row>\n        </Container>\n      </form>\n    )\n  },\n])\n\nexport default decorate([\n  addSubscriptions({\n    remotes: subscribeRemotes,\n  }),\n  provideState({\n    computed: {\n      loading: (state, props) => state.suggestedExcludedTags === undefined || props.remotes === undefined,\n      suggestedExcludedTags: () => getSuggestedExcludedTags(),\n    },\n  }),\n  injectState,\n  ({ state: { loading, suggestedExcludedTags }, ...props }) =>\n    loading ? _('statusLoading') : <New suggestedExcludedTags={suggestedExcludedTags} {...props} />,\n])\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport addSubscriptions from 'add-subscriptions'\nimport decorate from 'apply-decorators'\nimport defined from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport Link from 'link'\nimport React from 'react'\nimport Upgrade from 'xoa-upgrade'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { Container, Col, Row } from 'grid'\nimport { generateId, linkState } from 'reaclette-utils'\nimport { injectState, provideState } from 'reaclette'\nimport { every, isEmpty, mapValues, map } from 'lodash'\nimport { Remote } from 'render-xo-item'\nimport { SelectPool, SelectRemote } from 'select-objects'\nimport { Toggle } from 'form'\nimport {\n  createMetadataBackupJob,\n  createSchedule,\n  deleteSchedule,\n  editMetadataBackupJob,\n  editSchedule,\n  subscribeRemotes,\n} from 'xo'\nimport { ReportRecipients } from '..'\n\nimport { constructPattern, destructPattern, FormFeedback, FormGroup, Input, Li, Ul } from '../../utils'\n\nimport ReportWhen from '../_reportWhen'\nimport Schedules from '../_schedules'\nimport { RemoteProxy, RemoteProxyWarning } from '../_remoteProxy'\n\n// A retention can be:\n// - number: set by user\n// - undefined: will be replaced by the default value in the display(table + modal) and on submitting the form\nconst DEFAULT_RETENTION = 1\n\nconst RETENTION_POOL_METADATA = {\n  defaultValue: DEFAULT_RETENTION,\n  name: _('poolMetadataRetention'),\n  valuePath: 'retentionPoolMetadata',\n}\nconst RETENTION_XO_METADATA = {\n  defaultValue: DEFAULT_RETENTION,\n  name: _('xoMetadataRetention'),\n  valuePath: 'retentionXoMetadata',\n}\n\nconst GLOBAL_SETTING_KEY = ''\n\nconst setSettingsDefaultRetentions = (settings, { modePoolMetadata, modeXoMetadata }) =>\n  mapValues(settings, (setting, key) =>\n    key !== GLOBAL_SETTING_KEY\n      ? {\n          retentionPoolMetadata: modePoolMetadata\n            ? defined(setting.retentionPoolMetadata, DEFAULT_RETENTION)\n            : undefined,\n          retentionXoMetadata: modeXoMetadata ? defined(setting.retentionXoMetadata, DEFAULT_RETENTION) : undefined,\n        }\n      : setting\n  )\n\nconst getInitialState = () => ({\n  _modePoolMetadata: undefined,\n  _modeXoMetadata: undefined,\n  _name: undefined,\n  _pools: undefined,\n  _proxyId: undefined,\n  _remotes: undefined,\n  _schedules: undefined,\n  _settings: undefined,\n  showErrors: false,\n})\n\nexport default decorate([\n  New => props => (\n    <Upgrade place='newMetadataBackup' required={3}>\n      <New {...props} />\n    </Upgrade>\n  ),\n  addSubscriptions({\n    remotes: subscribeRemotes,\n  }),\n  provideState({\n    initialState: getInitialState,\n    effects: {\n      createJob: () => async state => {\n        if (state.isJobInvalid) {\n          return { showErrors: true }\n        }\n\n        const { modePoolMetadata, modeXoMetadata, name, pools, proxyId, remotes, schedules, settings } = state\n        await createMetadataBackupJob({\n          name,\n          pools: modePoolMetadata ? constructPattern(pools) : undefined,\n          proxy: proxyId === null ? undefined : state.proxyId,\n          remotes: constructPattern(remotes),\n          schedules: mapValues(schedules, ({ id, ...schedule }) => schedule),\n          settings: setSettingsDefaultRetentions(settings, {\n            modePoolMetadata,\n            modeXoMetadata,\n          }),\n          xoMetadata: modeXoMetadata,\n        })\n      },\n      editJob: () => async (state, props) => {\n        if (state.isJobInvalid) {\n          return { showErrors: true }\n        }\n\n        const settings = { ...state.settings }\n        await Promise.all([\n          ...map(props.schedules, ({ id }) => {\n            const schedule = state.schedules[id]\n            if (schedule === undefined) {\n              return deleteSchedule(id)\n            }\n\n            return editSchedule({\n              id,\n              cron: schedule.cron,\n              name: schedule.name,\n              timezone: schedule.timezone,\n              enabled: schedule.enabled,\n            })\n          }),\n          ...map(state.schedules, async schedule => {\n            if (props.schedules[schedule.id] === undefined) {\n              const { id } = await createSchedule(props.job.id, {\n                cron: schedule.cron,\n                name: schedule.name,\n                timezone: schedule.timezone,\n                enabled: schedule.enabled,\n              })\n              settings[id] = settings[schedule.id]\n              delete settings[schedule.id]\n            }\n          }),\n        ])\n\n        const { modePoolMetadata, modeXoMetadata, name, pools, proxyId, remotes } = state\n        await editMetadataBackupJob({\n          id: props.job.id,\n          name,\n          pools: modePoolMetadata ? constructPattern(pools) : null,\n          proxy: proxyId,\n          remotes: constructPattern(remotes),\n          settings: setSettingsDefaultRetentions(settings, {\n            modePoolMetadata,\n            modeXoMetadata,\n          }),\n          xoMetadata: modeXoMetadata,\n        })\n      },\n\n      linkState,\n      reset: () => getInitialState,\n      setProxy(_, id) {\n        this.state._proxyId = id\n      },\n      setPools: (_, _pools) => () => ({\n        _pools,\n      }),\n      setSchedules: (_, _schedules) => () => ({\n        _schedules,\n      }),\n      setSettings: (_, _settings) => () => ({\n        _settings,\n      }),\n      setGlobalSettings:\n        ({ setSettings }, name, value) =>\n        ({ settings = {} }) => {\n          setSettings({\n            ...settings,\n            [GLOBAL_SETTING_KEY]: {\n              ...settings[GLOBAL_SETTING_KEY],\n              [name]: value,\n            },\n          })\n        },\n      setReportWhen({ setGlobalSettings }, { value }) {\n        setGlobalSettings('reportWhen', value)\n      },\n      addReportRecipient({ setGlobalSettings }, value) {\n        const { reportRecipients = [] } = this.state.settings?.[GLOBAL_SETTING_KEY] ?? {}\n        if (!reportRecipients.includes(value)) {\n          reportRecipients.push(value)\n          setGlobalSettings('reportRecipients', reportRecipients)\n        }\n      },\n      removeReportRecipient({ setGlobalSettings }, key) {\n        const { reportRecipients } = this.state.settings[GLOBAL_SETTING_KEY]\n        reportRecipients.splice(key, 1)\n        setGlobalSettings('reportRecipients', reportRecipients)\n      },\n      setBackupReportTpl({ setGlobalSettings }, compactBackupTpl) {\n        setGlobalSettings('backupReportTpl', compactBackupTpl ? 'compactMjml' : 'mjml')\n      },\n      setHideSuccessfulItems({ setGlobalSettings }, hideSuccessfulItems) {\n        setGlobalSettings('hideSuccessfulItems', hideSuccessfulItems)\n      },\n      toggleMode:\n        (_, { mode }) =>\n        state => ({\n          [`_${mode}`]: !state[mode],\n        }),\n      addRemote:\n        (_, { id }) =>\n        state => ({\n          _remotes: [...state.remotes, id],\n        }),\n      deleteRemote: (_, key) => state => {\n        const _remotes = [...state.remotes]\n        _remotes.splice(key, 1)\n        return {\n          _remotes,\n        }\n      },\n    },\n    computed: {\n      idForm: generateId,\n      inputBackupReportTplId: generateId,\n      inputHideSuccessfulItemsId: generateId,\n\n      modePoolMetadata: ({ _modePoolMetadata }, { job }) =>\n        defined(_modePoolMetadata, () => !isEmpty(destructPattern(job.pools))),\n      modeXoMetadata: ({ _modeXoMetadata }, { job }) => defined(_modeXoMetadata, () => job.xoMetadata),\n      name: (state, { job }) => defined(state._name, () => job.name, ''),\n      pools: ({ _pools }, { job }) => defined(_pools, () => destructPattern(job.pools)),\n      retentions: ({ modePoolMetadata, modeXoMetadata }) => {\n        const retentions = []\n        if (modePoolMetadata) {\n          retentions.push(RETENTION_POOL_METADATA)\n        }\n        if (modeXoMetadata) {\n          retentions.push(RETENTION_XO_METADATA)\n        }\n        return retentions\n      },\n      schedules: ({ _schedules }, { schedules }) => defined(_schedules, schedules),\n      settings: ({ _settings }, { job }) =>\n        // it replaces null retentions introduced by the commit\n        // https://github.com/vatesfr/xen-orchestra/commit/fea5117ed83b58d3a57715b32d63d46e3004a094#diff-c02703199db2a4c217943cf8e02b91deR40\n        defined(_settings, () =>\n          mapValues(job.settings, setting => {\n            const newSetting = { ...setting }\n            if (newSetting.retentionPoolMetadata === null) {\n              newSetting.retentionPoolMetadata = 0\n            }\n            if (newSetting.retentionXoMetadata === null) {\n              newSetting.retentionXoMetadata = 0\n            }\n            return newSetting\n          })\n        ),\n      remotes: ({ _remotes }, { job }) => defined(_remotes, () => destructPattern(job.remotes), []),\n      remotesPredicate:\n        ({ proxyId, remotes }) =>\n        remote => {\n          if (proxyId === null) {\n            proxyId = undefined\n          }\n          return !remotes.includes(remote.id) && remote.value.proxy === proxyId\n        },\n\n      isJobInvalid: state =>\n        state.missingModes ||\n        state.missingPools ||\n        state.missingRemotes ||\n        state.missingRetentionPoolMetadata ||\n        state.missingRetentionXoMetadata ||\n        state.missingSchedules,\n\n      missingModes: state => !state.modeXoMetadata && !state.modePoolMetadata,\n      missingPools: state => state.modePoolMetadata && isEmpty(state.pools),\n      missingRemotes: state => isEmpty(state.remotes),\n      missingRetentionPoolMetadata: state =>\n        state.modePoolMetadata && every(state.settings, ({ retentionPoolMetadata }) => retentionPoolMetadata === 0),\n      missingRetentionXoMetadata: state =>\n        state.modeXoMetadata && every(state.settings, ({ retentionXoMetadata }) => retentionXoMetadata === 0),\n      missingSchedules: state => isEmpty(state.schedules),\n      proxyId: (state, props) => defined(state._proxyId, () => props.job.proxy),\n    },\n  }),\n  injectState,\n  ({ state, effects, job, remotes }) => {\n    const [submitHandler, submitTitle] =\n      job === undefined ? [effects.createJob, 'formCreate'] : [effects.editJob, 'formSave']\n    const {\n      missingModes,\n      missingPools,\n      missingRemotes,\n      missingRetentionPoolMetadata,\n      missingRetentionXoMetadata,\n      missingSchedules,\n    } = state.showErrors ? state : {}\n\n    const {\n      reportWhen = 'failure',\n      reportRecipients = [],\n      backupReportTpl = 'mjml',\n      hideSuccessfulItems,\n    } = defined(() => state.settings[GLOBAL_SETTING_KEY], {})\n\n    return (\n      <form id={state.idForm}>\n        <Container>\n          <Row>\n            <Col mediumSize={6}>\n              <Card>\n                <CardHeader>{_('backupName')}*</CardHeader>\n                <CardBlock>\n                  <Input onChange={effects.linkState} name='_name' value={state.name} />\n                </CardBlock>\n              </Card>\n              <FormFeedback component={Card} error={missingModes} message={_('missingBackupMode')}>\n                <CardBlock>\n                  <div className='text-xs-center'>\n                    <ActionButton\n                      active={state.modePoolMetadata}\n                      data-mode='modePoolMetadata'\n                      handler={effects.toggleMode}\n                      icon='pool'\n                    >\n                      {_('poolMetadata')}\n                    </ActionButton>{' '}\n                    <ActionButton\n                      active={state.modeXoMetadata}\n                      data-mode='modeXoMetadata'\n                      handler={effects.toggleMode}\n                      icon='file'\n                    >\n                      {_('xoConfig')}\n                    </ActionButton>{' '}\n                  </div>\n                </CardBlock>\n              </FormFeedback>\n              <Card>\n                <CardHeader>\n                  {_('remotes')}\n                  <Link className='btn btn-primary pull-right' target='_blank' to='/settings/remotes'>\n                    <Icon icon='settings' /> <strong>{_('remotesSettings')}</strong>\n                  </Link>\n                </CardHeader>\n                <CardBlock>\n                  {isEmpty(remotes) ? (\n                    <span className='text-warning'>\n                      <Icon icon='alarm' /> {_('createRemoteMessage')}\n                    </span>\n                  ) : (\n                    <FormGroup>\n                      <label>\n                        <strong>{_('backupTargetRemotes')}</strong>\n                      </label>\n                      <FormFeedback\n                        component={SelectRemote}\n                        message={_('missingRemotes')}\n                        onChange={effects.addRemote}\n                        predicate={state.remotesPredicate}\n                        error={missingRemotes}\n                        value={null}\n                      />\n                      <br />\n                      <Ul>\n                        {state.remotes.map((id, key) => (\n                          <Li key={id}>\n                            <Remote id={id} /> <RemoteProxyWarning id={id} proxyId={state.proxyId} />\n                            <div className='pull-right'>\n                              <ActionButton\n                                btnStyle='danger'\n                                handler={effects.deleteRemote}\n                                handlerParam={key}\n                                icon='delete'\n                                size='small'\n                              />\n                            </div>\n                          </Li>\n                        ))}\n                      </Ul>\n                    </FormGroup>\n                  )}\n                </CardBlock>\n              </Card>\n              <Card>\n                <CardHeader>{_('newBackupSettings')}</CardHeader>\n                <CardBlock>\n                  <RemoteProxy onChange={effects.setProxy} value={state.proxyId} />\n                  <ReportWhen onChange={effects.setReportWhen} required value={reportWhen} />\n                  <ReportRecipients\n                    recipients={reportRecipients}\n                    add={effects.addReportRecipient}\n                    remove={effects.removeReportRecipient}\n                  />\n                  <FormGroup>\n                    <label htmlFor={state.inputBackupReportTplId}>\n                      <strong>{_('shorterBackupReports')}</strong>\n                    </label>\n                    <Toggle\n                      className='pull-right'\n                      id={state.inputBackupReportTplId}\n                      value={backupReportTpl === 'compactMjml'}\n                      onChange={effects.setBackupReportTpl}\n                    />\n                  </FormGroup>\n                  <FormGroup>\n                    <label htmlFor={state.inputHideSuccessfulItemsId}>\n                      <strong>{_('hideSuccessfulItems')}</strong>\n                    </label>\n                    <Toggle\n                      className='pull-right'\n                      id={state.inputHideSuccessfulItemsId}\n                      value={hideSuccessfulItems}\n                      onChange={effects.setHideSuccessfulItems}\n                    />\n                  </FormGroup>\n                </CardBlock>\n              </Card>\n            </Col>\n            <Col mediumSize={6}>\n              {state.modePoolMetadata && (\n                <Card>\n                  <CardHeader>{_('pools')}*</CardHeader>\n                  <CardBlock>\n                    <FormFeedback\n                      component={SelectPool}\n                      message={_('missingPools')}\n                      multi\n                      onChange={effects.setPools}\n                      error={missingPools}\n                      value={state.pools}\n                    />\n                  </CardBlock>\n                </Card>\n              )}\n              <Schedules\n                handlerSchedules={effects.setSchedules}\n                handlerSettings={effects.setSettings}\n                missingRetentions={missingRetentionPoolMetadata || missingRetentionXoMetadata}\n                missingSchedules={missingSchedules}\n                retentions={state.retentions}\n                schedules={state.schedules}\n                settings={state.settings}\n              />\n            </Col>\n          </Row>\n          <Row>\n            <Col>\n              <Card>\n                <CardBlock>\n                  <ActionButton\n                    btnStyle='primary'\n                    form={state.idForm}\n                    handler={submitHandler}\n                    icon='save'\n                    redirectOnSuccess={state.isJobInvalid ? undefined : '/backup'}\n                    size='large'\n                  >\n                    {_(submitTitle)}\n                  </ActionButton>\n                  <ActionButton handler={effects.reset} icon='undo' className='pull-right' size='large'>\n                    {_('formReset')}\n                  </ActionButton>\n                </CardBlock>\n              </Card>\n            </Col>\n          </Row>\n        </Container>\n      </form>\n    )\n  },\n])\n","import _, { messages } from 'intl'\nimport ActionButton from 'action-button'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport Link from 'link'\nimport Upgrade from 'xoa-upgrade'\nimport React from 'react'\nimport Tooltip from 'tooltip'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { Container, Col, Row } from 'grid'\nimport {\n  createMirrorBackupJob,\n  createSchedule,\n  deleteSchedule,\n  editSchedule,\n  editMirrorBackupJob,\n  listVmBackups,\n} from 'xo'\nimport { every, forEach, isEmpty, last, map, mapValues, sortBy } from 'lodash'\nimport { generateId, linkState } from 'reaclette-utils'\nimport { injectIntl } from 'react-intl'\nimport { injectState, provideState } from 'reaclette'\nimport { Number, Select, Toggle } from 'form'\nimport { Remote } from 'render-xo-item'\nimport { resolveId } from 'utils'\nimport { SelectRemote } from 'select-objects'\n\nimport { BACKUP_NG_DOC_LINK, DeleteOldBackupsFirst, ReportRecipients } from '..'\n\nimport ReportWhen from '../_reportWhen'\nimport Schedules from '../_schedules'\nimport { RemoteProxy, RemoteProxyWarning } from '../_remoteProxy'\n\nimport { destructPattern, FormFeedback, FormGroup, Input, Li, Ul, constructPattern } from '../../utils'\n\nconst MIRROR_ALL_BACKUPS_STYLE = {\n  formGroup: { display: 'flex', justifyContent: 'space-between' },\n  label: { marginTop: 'auto', marginBottom: 'auto' },\n}\n\nconst DEFAULT_RETENTIONS = [\n  {\n    defaultValue: 1,\n    name: _('scheduleExportRetention'),\n    valuePath: 'exportRetention',\n  },\n]\n\nconst getInitialState = ({ job = {}, schedules = {} }) => {\n  const targetRemoteIds = job.remotes !== undefined ? destructPattern(job.remotes) : []\n  const { '': { reportWhen, reportRecipients, ...advancedSettings } = {}, ...settings } = job.settings ?? {}\n\n  return {\n    edition: !isEmpty(job),\n    retentions: DEFAULT_RETENTIONS,\n    showErrors: false,\n    displayAdvancedSettings: advancedSettings !== undefined && !isEmpty(advancedSettings),\n\n    mode: (job.mode === 'delta' ? 'incremental' : job.mode) ?? '',\n    name: job.name ?? '',\n    schedules: schedules ?? {},\n    settings: settings ?? {},\n\n    sourceRemote: job.sourceRemote ?? {},\n    targetRemoteIds,\n\n    advancedSettings: advancedSettings ?? {},\n    proxyId: job.proxy ?? undefined,\n    reportWhen: reportWhen ?? 'failure',\n    reportRecipient: '',\n    reportRecipients: reportRecipients ?? [],\n\n    mirrorAll: job.filter === undefined,\n    vmsToMirror: job.filter?.vm.uuid.__or,\n  }\n}\n\nconst normalize = state => {\n  const {\n    name,\n    proxyId,\n    sourceRemote,\n    targetRemoteIds,\n    settings,\n    advancedSettings,\n    reportWhen,\n    reportRecipients,\n    vmsToMirror,\n    mirrorAll,\n  } = state\n  let { schedules, mode } = state\n\n  schedules = mapValues(schedules, ({ id, ...schedule }) => schedule)\n  settings[''] = {\n    ...advancedSettings,\n    reportWhen: reportWhen.value ?? reportWhen,\n    reportRecipients: reportRecipients.length !== 0 ? reportRecipients : undefined,\n  }\n  return {\n    name,\n    mode: state.isIncremental ? 'delta' : mode,\n    proxy: proxyId,\n    sourceRemote: resolveId(sourceRemote),\n    remotes: constructPattern(targetRemoteIds),\n    schedules,\n    settings,\n    filter: !mirrorAll ? { vm: { uuid: { __or: vmsToMirror } } } : state.edition ? null : undefined,\n  }\n}\n\nconst NewMirrorBackup = decorate([\n  injectIntl,\n  provideState({\n    initialState: getInitialState,\n    effects: {\n      addTargetRemoteId: (_, obj) => state => ({\n        targetRemoteIds: [...state.targetRemoteIds, resolveId(obj)],\n      }),\n      deleteTargetRemoteId: (_, id) => state => ({\n        targetRemoteIds: state.targetRemoteIds.filter(remoteId => remoteId !== id),\n      }),\n      setTargetDeleteFirst: (_, id) => state => ({\n        settings: {\n          ...state.settings,\n          [id]: {\n            deleteFirst: !(state.settings[id] ?? false),\n          },\n        },\n      }),\n      toggleAdvancedSettings: () => state => ({\n        displayAdvancedSettings: !state.displayAdvancedSettings,\n      }),\n      setProxy: (_, id) => () => ({\n        proxyId: id,\n      }),\n      setReportWhen: (_, reportWhen) => () => ({ reportWhen }),\n      addReportRecipient: (_, recipient) => state => ({\n        reportRecipients: !state.reportRecipients.includes(recipient)\n          ? [...state.reportRecipients, recipient]\n          : state.reportRecipients,\n      }),\n      removeReportRecipient: (_, key) => state => {\n        state.reportRecipients.splice(key, 1)\n        return {\n          reportRecipients: [...state.reportRecipients],\n        }\n      },\n      setConcurrency: ({ setAdvancedSettings }, concurrency) => setAdvancedSettings({ concurrency }),\n      setTimeout: ({ setAdvancedSettings }, timeout) =>\n        setAdvancedSettings({ timeout: timeout !== undefined ? timeout * 3600e3 : undefined }),\n      setMaxExportRate: ({ setAdvancedSettings }, rate) =>\n        setAdvancedSettings({ maxExportRate: rate !== undefined ? rate * (1024 * 1024) : undefined }),\n      setNRetriesVmBackupFailures: ({ setAdvancedSettings }, nRetriesVmBackupFailures) =>\n        setAdvancedSettings({ nRetriesVmBackupFailures }),\n      setBackupReportTpl: ({ setAdvancedSettings }, compactBackupTpl) =>\n        setAdvancedSettings({ backupReportTpl: compactBackupTpl ? 'compactMjml' : 'mjml' }),\n      setHideSuccessfulItems({ setAdvancedSettings }, hideSuccessfulItems) {\n        setAdvancedSettings({\n          hideSuccessfulItems,\n        })\n      },\n      setSourceRemote: (_, obj) => () => ({\n        sourceRemote: obj === null ? {} : obj.value,\n        vmsToMirror: undefined,\n      }),\n      setSchedules: (_, schedules) => () => ({\n        schedules,\n      }),\n      setSettings: (_, settings) => () => ({\n        settings,\n      }),\n      setAdvancedSettings: (_, obj) => state => ({\n        advancedSettings: {\n          ...state.advancedSettings,\n          ...obj,\n        },\n      }),\n      createMirrorBackup: () => async state => {\n        if (state.isBackupInvalid) {\n          return {\n            ...state,\n            showErrors: true,\n          }\n        }\n\n        await createMirrorBackupJob(normalize(state))\n      },\n      editMirrorBackup: () => async (state, props) => {\n        if (state.isBackupInvalid) {\n          return {\n            ...state,\n            showErrors: true,\n          }\n        }\n\n        const settings = { ...state.settings }\n        await Promise.all([\n          ...map(props.schedules, ({ id }) => {\n            const schedule = state.schedules[id]\n            if (schedule === undefined) {\n              return deleteSchedule(id)\n            }\n\n            return editSchedule({\n              id,\n              cron: schedule.cron,\n              name: schedule.name,\n              timezone: schedule.timezone,\n              enabled: schedule.enabled,\n            })\n          }),\n          ...map(state.schedules, async schedule => {\n            if (props.schedules[schedule.id] === undefined) {\n              const newSchedule = await createSchedule(props.job.id, {\n                cron: schedule.cron,\n                name: schedule.name,\n                timezone: schedule.timezone,\n                enabled: schedule.enabled,\n              })\n              settings[newSchedule.id] = settings[schedule.id]\n              delete settings[schedule.id]\n            }\n          }),\n        ])\n\n        const { schedules, ...jobProps } = normalize({ ...state, settings, isIncremental: state.isIncremental })\n\n        await editMirrorBackupJob({\n          id: props.job.id,\n          ...jobProps,\n        })\n      },\n      resetMirrorBackup: () => (_, props) => getInitialState(props),\n      linkState,\n      toggleMode: (_, { mode }) => ({ mode, vmsToMirror: undefined }),\n      toggleMirrorAll: (_, value) => ({ mirrorAll: value }),\n      onChangeVmBackups: (_, vmBackups) => () => ({\n        vmsToMirror: vmBackups.map(({ value: vmUuid }) => vmUuid),\n      }),\n    },\n    computed: {\n      vmBackupOptions: async state => {\n        const sourceRemoteId = resolveId(state.sourceRemote)\n        const mode = state.mode === 'incremental' ? 'delta' : state.mode\n        if (sourceRemoteId === undefined || isEmpty(sourceRemoteId) || mode === '') {\n          return\n        }\n\n        const vmBackups = (await listVmBackups([sourceRemoteId]))[sourceRemoteId]\n\n        const options = []\n        forEach(vmBackups, (backups, vmUuid) => {\n          const lastBackupInfo = last(\n            sortBy(\n              backups.filter(backup => backup.mode === mode),\n              'timestamp'\n            )\n          )\n          if (lastBackupInfo === undefined) {\n            return\n          }\n\n          options.push({\n            label: lastBackupInfo.vm.name_label,\n            value: vmUuid,\n          })\n        })\n\n        return options\n      },\n\n      formId: generateId,\n      inputConcurrencyId: generateId,\n      inputTimeoutId: generateId,\n      inputMaxExportRateId: generateId,\n      inputNRetriesVmBackupFailures: generateId,\n      inputBackupReportTplId: generateId,\n      inputHideSuccessfulItemsId: generateId,\n      inputMirrorAllId: generateId,\n      isBackupInvalid: state =>\n        state.isMissingName ||\n        state.isMissingBackupMode ||\n        state.isMissingSchedules ||\n        state.isMissingRetention ||\n        state.isMissingVmsToMirror,\n      isFull: state => state.mode === 'full',\n      isIncremental: state => state.mode === 'incremental',\n      isMissingBackupMode: state => state.mode === '',\n      isMissingName: state => state.name.trim() === '',\n      isMissingSchedules: state => isEmpty(state.schedules),\n      isMissingRetention: state =>\n        state.isMissingSchedules || every(state.settings, ({ exportRetention }) => exportRetention < 1),\n      isMissingSourceRemote: state => isEmpty(state.sourceRemote),\n      isMissingTargetRemotes: state => state.targetRemoteIds.length === 0,\n      isMissingVmsToMirror: state =>\n        !state.mirrorAll && (state.vmsToMirror === undefined || state.vmsToMirror.length === 0),\n      remoteProxyPredicate:\n        ({ proxyId }) =>\n        remote => {\n          if (proxyId === null) {\n            proxyId = undefined\n          }\n          return remote.value.proxy === proxyId\n        },\n      targetRemotesPredicate:\n        ({ targetRemoteIds, remoteProxyPredicate }) =>\n        remote =>\n          !targetRemoteIds.includes(remote.id) && remoteProxyPredicate(remote),\n    },\n  }),\n  injectState,\n  ({ state, effects, intl: { formatMessage } }) => {\n    const {\n      concurrency,\n      timeout,\n      maxExportRate,\n      backupReportTpl = 'mjml',\n      hideSuccessfulItems,\n      nRetriesVmBackupFailures = 0,\n    } = state.advancedSettings\n    return (\n      <form id={state.formId}>\n        <Container>\n          <Row>\n            <Col mediumSize={6}>\n              <Card>\n                <CardHeader>{_('backupName')}*</CardHeader>\n                <CardBlock>\n                  <FormFeedback\n                    component={Input}\n                    error={state.showErrors ? state.isMissingName : undefined}\n                    message={_('missingBackupName')}\n                    name='name'\n                    onChange={effects.linkState}\n                    value={state.name}\n                  />\n                </CardBlock>\n              </Card>\n              <FormFeedback\n                component={Card}\n                error={state.showErrors ? state.isMissingBackupMode : undefined}\n                message={_('missingBackupMode')}\n              >\n                <CardBlock>\n                  <div className='text-xs-center'>\n                    <ActionButton\n                      active={state.isIncremental}\n                      data-mode='incremental'\n                      handler={effects.toggleMode}\n                      icon='delta-backup'\n                    >\n                      {_('mirrorIncrementalBackup')}\n                    </ActionButton>{' '}\n                    <ActionButton\n                      active={state.isFull}\n                      data-mode='full'\n                      handler={effects.toggleMode}\n                      icon='mirror-backup'\n                    >\n                      {_('mirrorFullBackup')}\n                    </ActionButton>{' '}\n                    <br />\n                    <a className='text-muted' href={BACKUP_NG_DOC_LINK} rel='noopener noreferrer' target='_blank'>\n                      <Icon icon='info' /> {_('backupNgLinkToDocumentationMessage')}\n                    </a>\n                  </div>\n                </CardBlock>\n              </FormFeedback>\n              <Card>\n                <CardHeader>\n                  {_('newBackupSettings')}\n                  <ActionButton\n                    className='pull-right'\n                    handler={effects.toggleAdvancedSettings}\n                    icon={state.displayAdvancedSettings ? 'toggle-on' : 'toggle-off'}\n                    iconColor={state.displayAdvancedSettings ? 'text-success' : undefined}\n                    size='small'\n                  >\n                    {_('newBackupAdvancedSettings')}\n                  </ActionButton>\n                </CardHeader>\n                <CardBlock>\n                  <RemoteProxy onChange={effects.setProxy} value={state.proxyId} />\n                  <ReportWhen value={state.reportWhen} required onChange={effects.setReportWhen} />\n                  <ReportRecipients\n                    recipients={state.reportRecipients}\n                    add={effects.addReportRecipient}\n                    remove={effects.removeReportRecipient}\n                  />\n                  {state.displayAdvancedSettings && (\n                    <div>\n                      <FormGroup>\n                        <label htmlFor={state.inputConcurrencyId}>\n                          <strong>{_('concurrency')}</strong>\n                        </label>\n                        <Number\n                          id={state.inputConcurrencyId}\n                          min={1}\n                          onChange={effects.setConcurrency}\n                          value={concurrency}\n                        />\n                      </FormGroup>\n                      <FormGroup>\n                        <label htmlFor={state.inputNRetriesVmBackupFailures}>\n                          <strong>{_('nRetriesVmBackupFailures')}</strong>\n                        </label>\n                        <Number\n                          id={state.inputNRetriesVmBackupFailures}\n                          min={0}\n                          onChange={effects.setNRetriesVmBackupFailures}\n                          value={nRetriesVmBackupFailures}\n                        />\n                      </FormGroup>\n                      <FormGroup>\n                        <label htmlFor={state.inputTimeoutId}>\n                          <strong>{_('timeout')}</strong>\n                        </label>{' '}\n                        <Tooltip content={_('timeoutInfo')}>\n                          <Icon icon='info' />\n                        </Tooltip>\n                        <Number\n                          id={state.inputTimeoutId}\n                          onChange={effects.setTimeout}\n                          value={timeout ? timeout / 3600e3 : undefined}\n                          placeholder={formatMessage(messages.timeoutUnit)}\n                        />\n                      </FormGroup>\n                      <FormGroup>\n                        <label htmlFor={state.inputMaxExportRateId}>\n                          <strong>{_('speedLimit')}</strong>\n                        </label>\n                        <Number\n                          id={state.inputMaxExportRateId}\n                          min={0}\n                          onChange={effects.setMaxExportRate}\n                          value={maxExportRate / (1024 * 1024)}\n                        />\n                      </FormGroup>\n                      <FormGroup>\n                        <label htmlFor={state.inputBackupReportTplId}>\n                          <strong>{_('shorterBackupReports')}</strong>\n                        </label>\n                        <Toggle\n                          className='pull-right'\n                          id={state.inputBackupReportTplId}\n                          value={backupReportTpl === 'compactMjml'}\n                          onChange={effects.setBackupReportTpl}\n                        />\n                      </FormGroup>\n                      <FormGroup>\n                        <label htmlFor={state.inputHideSuccessfulItemsId}>\n                          <strong>{_('hideSuccessfulItems')}</strong>\n                        </label>\n                        <Toggle\n                          className='pull-right'\n                          id={state.inputHideSuccessfulItemsId}\n                          value={hideSuccessfulItems}\n                          onChange={effects.setHideSuccessfulItems}\n                        />\n                      </FormGroup>\n                    </div>\n                  )}\n                </CardBlock>\n              </Card>\n            </Col>\n            <Col mediumSize={6}>\n              <Card>\n                <CardHeader>\n                  {_(\n                    state.isMissingBackupMode\n                      ? 'mirrorBackupVms'\n                      : state.isFull\n                        ? 'mirrorFullBackup'\n                        : 'mirrorIncrementalBackup'\n                  )}\n                  <Link className='btn btn-primary pull-right' target='_blank' to='/settings/remotes'>\n                    <Icon icon='settings' /> <strong>{_('remotesSettings')}</strong>\n                  </Link>{' '}\n                </CardHeader>\n                <CardBlock>\n                  <FormGroup>\n                    <label>\n                      <strong>{_('sourceRemote')}</strong>\n                    </label>\n                    <FormFeedback\n                      component={SelectRemote}\n                      error={state.showErrors ? state.isMissingSourceRemote : undefined}\n                      message={_('missingRemote')}\n                      onChange={effects.setSourceRemote}\n                      predicate={state.remoteProxyPredicate}\n                      value={state.sourceRemote}\n                    />\n                  </FormGroup>\n\n                  <FormGroup style={MIRROR_ALL_BACKUPS_STYLE.formGroup}>\n                    <label style={MIRROR_ALL_BACKUPS_STYLE.label} htmlFor={state.inputMirrorAllId}>\n                      <strong>{_('mirrorAllVmBackups', { mode: state.mode })}</strong>\n                    </label>\n                    <Toggle id={state.inputMirrorAllId} onChange={effects.toggleMirrorAll} value={state.mirrorAll} />\n                  </FormGroup>\n                  {!state.mirrorAll && (\n                    <FormGroup>\n                      <label>\n                        <strong>{_('vms')}</strong>\n                      </label>\n\n                      <Tooltip\n                        content={state.vmBackupOptions === undefined ? _('backupModeSourceRemoteRequired') : undefined}\n                      >\n                        <FormFeedback\n                          component={Select}\n                          disabled={state.vmBackupOptions === undefined}\n                          error={state.showErrors ? state.isMissingVmsToMirror : undefined}\n                          message={_('missingVms')}\n                          multi\n                          onChange={effects.onChangeVmBackups}\n                          options={state.vmBackupOptions}\n                          placeholder={_('selectVms')}\n                          value={state.vmsToMirror}\n                          required={!state.mirrorAll}\n                        />\n                      </Tooltip>\n                    </FormGroup>\n                  )}\n                  <FormGroup>\n                    <label>\n                      <strong>{_('targetRemotes')}</strong>\n                    </label>\n                    <FormFeedback\n                      component={SelectRemote}\n                      error={state.showErrors ? state.isMissingTargetRemotes : undefined}\n                      message={_('missingRemotes')}\n                      onChange={effects.addTargetRemoteId}\n                      predicate={state.targetRemotesPredicate}\n                      value={null}\n                    />\n                    <br />\n                    <Ul>\n                      {state.targetRemoteIds.map(id => (\n                        <Li key={id}>\n                          <Remote id={id} /> <RemoteProxyWarning id={id} proxyId={state.proxyId} />\n                          <div className='pull-right'>\n                            <DeleteOldBackupsFirst\n                              handler={effects.setTargetDeleteFirst}\n                              handlerParam={id}\n                              value={state.settings[id]?.deleteFirst ?? false}\n                            />{' '}\n                            <ActionButton\n                              handler={effects.deleteTargetRemoteId}\n                              handlerParam={id}\n                              icon='delete'\n                              size='small'\n                              btnStyle='danger'\n                            />\n                          </div>\n                        </Li>\n                      ))}\n                    </Ul>\n                  </FormGroup>\n                </CardBlock>\n              </Card>\n              <Schedules\n                handlerSchedules={effects.setSchedules}\n                handlerSettings={effects.setSettings}\n                missingRetentions={state.showErrors ? state.isMissingRetention : undefined}\n                missingSchedules={state.showErrors ? state.isMissingSchedules : undefined}\n                retentions={state.retentions}\n                schedules={state.schedules}\n                settings={state.settings}\n                withHealthCheck\n              />\n            </Col>\n          </Row>\n          <Row>\n            <Card>\n              <CardBlock>\n                {state.edition ? (\n                  <ActionButton\n                    btnStyle='primary'\n                    form={state.formId}\n                    redirectOnSuccess={state.isBackupInvalid ? undefined : ActionButton.GO_BACK}\n                    handler={effects.editMirrorBackup}\n                    icon='save'\n                    size='large'\n                  >\n                    {_('formSave')}\n                  </ActionButton>\n                ) : (\n                  <ActionButton\n                    btnStyle='primary'\n                    form={state.formId}\n                    redirectOnSuccess={state.isBackupInvalid ? undefined : '/backup'}\n                    handler={effects.createMirrorBackup}\n                    icon='save'\n                    size='large'\n                  >\n                    {_('create')}\n                  </ActionButton>\n                )}\n                <ActionButton handler={effects.resetMirrorBackup} icon='undo' className='pull-right' size='large'>\n                  {_('formReset')}\n                </ActionButton>\n              </CardBlock>\n            </Card>\n          </Row>\n        </Container>\n      </form>\n    )\n  },\n])\n\nexport default props => (\n  <Upgrade available={3}>\n    <NewMirrorBackup {...props} />\n  </Upgrade>\n)\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport Scheduler, { SchedulePreview } from 'scheduling'\nimport Tooltip from 'tooltip'\nimport { Card, CardBlock } from 'card'\nimport { generateId } from 'reaclette-utils'\nimport { injectState, provideState } from 'reaclette'\nimport { Number } from 'form'\n\nimport ScheduleHealthCheck from './healthCheck/ScheduleHealthCheck'\n\nimport { FormGroup, Input } from './../utils'\n\nconst New = decorate([\n  provideState({\n    computed: {\n      forceFullBackup: (_, { value }) => value.fullInterval === 1,\n      formId: generateId,\n      idInputName: generateId,\n    },\n    effects: {\n      setSchedule:\n        (_, params) =>\n        (_, { value, onChange }) => {\n          onChange({\n            ...value,\n            ...params,\n          })\n        },\n      setExportRetention:\n        ({ setSchedule }, exportRetention) =>\n        () => {\n          setSchedule({\n            exportRetention,\n          })\n        },\n      setCopyRetention:\n        ({ setSchedule }, copyRetention) =>\n        () => {\n          setSchedule({\n            copyRetention,\n          })\n        },\n      setSnapshotRetention:\n        ({ setSchedule }, snapshotRetention) =>\n        () => {\n          setSchedule({\n            snapshotRetention,\n          })\n        },\n      setCronTimezone:\n        ({ setSchedule }, { cronPattern: cron, timezone }) =>\n        () => {\n          setSchedule({\n            cron,\n            timezone,\n          })\n        },\n      setName:\n        ({ setSchedule }, { target: { value } }) =>\n        () => {\n          setSchedule({\n            name: value.trim() === '' ? null : value,\n          })\n        },\n      setHealthCheckTags({ setSchedule }, tags) {\n        setSchedule({\n          healthCheckVmsWithTags: tags,\n        })\n      },\n      toggleForceFullBackup({ setSchedule }) {\n        setSchedule({\n          fullInterval: this.state.forceFullBackup ? undefined : 1,\n        })\n      },\n      toggleHealthCheck({ setSchedule }, { target: { checked } }) {\n        setSchedule({\n          healthCheckVmsWithTags: checked ? [] : undefined,\n          healthCheckSr: checked ? this.state.healthCheckSr : undefined,\n        })\n      },\n      setHealthCheckSr({ setSchedule }, sr) {\n        setSchedule({\n          healthCheckSr: sr.id,\n        })\n      },\n    },\n  }),\n  injectState,\n  ({ effects, missingRetentions, modes, showRetentionWarning, state, value: schedule }) => (\n    <Card>\n      <CardBlock>\n        {missingRetentions && (\n          <div className='text-danger text-md-center'>\n            <Icon icon='alarm' /> {_('retentionNeeded')}\n          </div>\n        )}\n        <FormGroup>\n          <label htmlFor={state.idInputName}>\n            <strong>{_('formName')}</strong>\n          </label>\n          <Input id={state.idInputName} onChange={effects.setName} value={schedule.name} />\n        </FormGroup>\n        {modes.exportMode && (\n          <FormGroup>\n            <label>\n              <strong>{_('scheduleExportRetention')}</strong>\n            </label>{' '}\n            {showRetentionWarning && (\n              <Tooltip content={_('deltaChainRetentionWarning')}>\n                <Icon icon='error' />\n              </Tooltip>\n            )}\n            <Number min='0' onChange={effects.setExportRetention} value={schedule.exportRetention} required />\n          </FormGroup>\n        )}\n        {modes.copyMode && (\n          <FormGroup>\n            <label>\n              <strong>{_('scheduleCopyRetention')}</strong>\n            </label>\n            <Number min='0' onChange={effects.setCopyRetention} value={schedule.copyRetention} required />\n          </FormGroup>\n        )}\n        {modes.snapshotMode && (\n          <FormGroup>\n            <label>\n              <strong>{_('snapshotRetention')}</strong>\n            </label>\n            <Number min='0' onChange={effects.setSnapshotRetention} value={schedule.snapshotRetention} required />\n          </FormGroup>\n        )}\n        <ScheduleHealthCheck\n          schedule={schedule}\n          toggleHealthCheck={effects.toggleHealthCheck}\n          setHealthCheckSr={effects.setHealthCheckSr}\n          setHealthCheckTags={effects.setHealthCheckTags}\n        />\n        {modes.isDelta && (\n          <FormGroup>\n            <label>\n              <strong>{_('forceFullBackup')}</strong>{' '}\n              <input checked={state.forceFullBackup} onChange={effects.toggleForceFullBackup} type='checkbox' />\n            </label>\n          </FormGroup>\n        )}\n        <Scheduler onChange={effects.setCronTimezone} cronPattern={schedule.cron} timezone={schedule.timezone} />\n        <SchedulePreview cronPattern={schedule.cron} timezone={schedule.timezone} />\n      </CardBlock>\n    </Card>\n  ),\n])\n\nNew.propTypes = {\n  missingRetentions: PropTypes.bool,\n  modes: PropTypes.object.isRequired,\n  onChange: PropTypes.func.isRequired,\n  value: PropTypes.object.isRequired,\n}\n\nexport { New as default }\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport decorate from 'apply-decorators'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { injectState, provideState } from 'reaclette'\nimport { isEmpty, find } from 'lodash'\n\nimport { FormFeedback } from './../utils'\n\n// ===================================================================\n\nconst FEEDBACK_ERRORS = [\n  'missingSchedules',\n  'missingCopyRetention',\n  'missingExportRetention',\n  'missingSnapshotRetention',\n]\n\nexport default decorate([\n  injectState,\n  provideState({\n    computed: {\n      error: state => find(FEEDBACK_ERRORS, error => state[error]),\n      individualActions: ({ disabledEdition }, { effects: { deleteSchedule, showScheduleModal } }) => [\n        {\n          disabled: disabledEdition,\n          handler: showScheduleModal,\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      rowTransform:\n        ({ propSettings, settings = propSettings }) =>\n        schedule => ({\n          ...schedule,\n          ...settings.get(schedule.id),\n        }),\n      schedulesColumns: (state, { effects: { toggleScheduleState } }) => {\n        const columns = [\n          {\n            itemRenderer: _ => _.name,\n            sortCriteria: 'name',\n            name: _('scheduleName'),\n            default: true,\n          },\n          {\n            itemRenderer: schedule => (\n              <StateButton\n                disabledLabel={_('stateDisabled')}\n                disabledTooltip={_('logIndicationToEnable')}\n                enabledLabel={_('stateEnabled')}\n                enabledTooltip={_('logIndicationToDisable')}\n                handler={toggleScheduleState}\n                handlerParam={schedule.id}\n                state={schedule.enabled}\n              />\n            ),\n            sortCriteria: 'enabled',\n            name: _('state'),\n          },\n          {\n            itemRenderer: _ => _.cron,\n            sortCriteria: 'cron',\n            name: _('scheduleCron'),\n          },\n          {\n            itemRenderer: _ => _.timezone,\n            sortCriteria: 'timezone',\n            name: _('scheduleTimezone'),\n          },\n        ]\n\n        if (state.isDelta) {\n          columns.push({\n            itemRenderer: schedule => (schedule.fullInterval === 1 ? _('stateEnabled') : _('stateDisabled')),\n            sortCriteria: 'fullInterval',\n            name: _('forceFullBackup'),\n          })\n        }\n\n        if (state.exportMode) {\n          columns.push({\n            itemRenderer: _ => _.exportRetention,\n            sortCriteria: _ => _.exportRetention,\n            name: _('scheduleExportRetention'),\n          })\n        }\n\n        if (state.copyMode) {\n          columns.push({\n            itemRenderer: _ => _.copyRetention,\n            sortCriteria: _ => _.copyRetention,\n            name: _('scheduleCopyRetention'),\n          })\n        }\n\n        if (state.snapshotMode) {\n          columns.push({\n            itemRenderer: _ => _.snapshotRetention,\n            sortCriteria: _ => _.snapshotRetention,\n            name: _('scheduleSnapshotRetention'),\n          })\n        }\n        return columns\n      },\n    },\n  }),\n  injectState,\n  ({ effects, state }) => (\n    <div>\n      <FormFeedback\n        component={Card}\n        error={state.showErrors ? state.error !== undefined : undefined}\n        message={state.error !== undefined && _(state.error)}\n      >\n        <CardHeader>\n          {_('backupSchedules')}*\n          <ActionButton\n            btnStyle='primary'\n            className='pull-right'\n            handler={effects.showScheduleModal}\n            icon='add'\n            tooltip={_('scheduleAdd')}\n          />\n        </CardHeader>\n        <CardBlock>\n          {isEmpty(state.schedules) ? (\n            <p className='text-md-center'>{_('noSchedules')}</p>\n          ) : (\n            <SortedTable\n              collection={state.schedules}\n              columns={state.schedulesColumns}\n              individualActions={state.individualActions}\n              rowTransform={state.rowTransform}\n              stateUrlParam='s'\n            />\n          )}\n        </CardBlock>\n      </FormFeedback>\n    </div>\n  ),\n])\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport moment from 'moment-timezone'\nimport React from 'react'\nimport Scheduler from 'scheduling'\nimport Upgrade from 'xoa-upgrade'\nimport { Card, CardBlock, CardHeader } from 'card'\nimport { Container, Col, Row } from 'grid'\nimport { createJob, editJob, createSchedule, editSchedule } from 'xo'\nimport { generateId } from 'reaclette-utils'\nimport { injectState, provideState } from 'reaclette'\nimport { SelectSchedule } from 'select-objects'\n\nimport { Input } from '../../utils'\n\nconst NewSequence = decorate([\n  provideState({\n    initialState: props => ({\n      name: props.job?.name ?? '',\n      sequenceSchedule:\n        props.schedule !== undefined\n          ? { cronPattern: props.schedule.cron, timezone: props.schedule.timezone }\n          : {\n              cronPattern: '0 0 * * *',\n              timezone: moment.tz.guess(),\n            },\n      schedules: props.job?.paramsVector?.items?.[0]?.values?.[0]?.schedules?.map(scheduleId => ({\n        id: scheduleId,\n        key: generateId(),\n      })) ?? [{ key: generateId() }, { key: generateId() }],\n    }),\n    effects: {\n      addSchedule: () => state => ({ schedules: [...state.schedules, { key: generateId() }] }),\n      removeSchedule: (_, scheduleKey) => state => ({\n        schedules: state.schedules.filter(schedule => schedule.key !== scheduleKey),\n      }),\n      selectSchedule: (_, scheduleKey, schedule) => state => ({\n        schedules: state.schedules.map(s => (s.key !== scheduleKey ? s : { key: s.key, ...schedule })),\n      }),\n      onChangeName: (_, event) => ({ name: event.target.value }),\n      onChangeSequenceSchedule: (_, sequenceSchedule) => ({ sequenceSchedule }),\n      save: () => async (state, props) => {\n        const jobFormData = {\n          name: state.name,\n          paramsVector: {\n            type: 'crossProduct',\n            items: [\n              {\n                type: 'set',\n                values: [{ schedules: state.schedules.map(schedule => schedule.id) }],\n              },\n            ],\n          },\n        }\n\n        const scheduleFormData = {\n          cron: state.sequenceSchedule.cronPattern,\n          timezone: state.sequenceSchedule.timezone,\n        }\n\n        if (props.job !== undefined) {\n          await editJob({\n            id: props.job.id,\n            ...jobFormData,\n          })\n\n          await editSchedule({\n            id: props.schedule.id,\n            jobId: props.job.id,\n            enabled: true,\n            ...scheduleFormData,\n          })\n        } else {\n          const jobId = await createJob({\n            type: 'call',\n            key: 'genericTask',\n            method: 'schedule.runSequence',\n            ...jobFormData,\n          })\n\n          await createSchedule(jobId, {\n            enabled: true,\n            ...scheduleFormData,\n          })\n        }\n      },\n    },\n  }),\n  injectState,\n  ({ job, state, effects }) => (\n    <form id='sequence-form'>\n      <Container>\n        <Row>\n          <Col mediumSize={12}>\n            <Card>\n              <CardHeader>\n                <Icon icon='menu-backup-sequence' /> {_('sequence')}\n              </CardHeader>\n              <CardBlock>\n                <Row>\n                  <Col>\n                    <label className='w-100'>\n                      <strong>{_('name')}</strong>\n                      <Input\n                        autoFocus\n                        className='w-100'\n                        message={_('missingBackupName')}\n                        onChange={effects.onChangeName}\n                        value={state.name}\n                        required\n                      />\n                    </label>\n                  </Col>\n                </Row>\n                <Row className='mt-1'>\n                  <Col>\n                    <ol className='pl-1'>\n                      {state.schedules.map(schedule => (\n                        <li key={schedule.key} className='mb-1'>\n                          <div style={{ display: 'flex', gap: '1rem' }}>\n                            <SelectSchedule\n                              id={schedule.id}\n                              onChange={s => effects.selectSchedule(schedule.key, s)}\n                              value={schedule}\n                              required\n                              optionProps={{ showState: false }}\n                            />\n                            <ActionButton\n                              size='small'\n                              handler={effects.removeSchedule}\n                              handlerParam={schedule.key}\n                              icon='remove'\n                              disabled={state.schedules.length < 2}\n                            />\n                          </div>\n                        </li>\n                      ))}\n                    </ol>\n                    <ActionButton icon='add' handler={effects.addSchedule}>\n                      {_('add')}\n                    </ActionButton>\n                  </Col>\n                </Row>\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Col mediumSize={12}>\n            <Card>\n              <CardHeader>\n                <Icon icon='schedule' /> {_('scheduleSequence')}\n              </CardHeader>\n              <CardBlock>\n                <Scheduler\n                  onChange={effects.onChangeSequenceSchedule}\n                  cronPattern={state.sequenceSchedule.cronPattern}\n                  timezone={state.sequenceSchedule.timezone}\n                />\n              </CardBlock>\n            </Card>\n          </Col>\n        </Row>\n        <Row>\n          <Card>\n            <CardBlock>\n              <ActionButton\n                btnStyle='primary'\n                form='sequence-form'\n                handler={effects.save}\n                icon='save'\n                redirectOnSuccess='/backup/sequences'\n                size='large'\n              >\n                {_('formSave')}\n              </ActionButton>\n            </CardBlock>\n          </Card>\n        </Row>\n      </Container>\n    </form>\n  ),\n])\n\nexport default props => (\n  <Upgrade available={3}>\n    <NewSequence {...props} />\n  </Upgrade>\n)\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport defined, { get } from '@xen-orchestra/defined'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SmartBackupPreview, { constructSmartPattern, destructSmartPattern } from 'smart-backup'\nimport Tooltip from 'tooltip'\nimport { connectStore, resolveIds } from 'utils'\nimport { createGetObjectsOfType } from 'selectors'\nimport { injectState, provideState } from 'reaclette'\nimport { Select } from 'form'\nimport { SelectPool, SelectTag } from 'select-objects'\n\nimport { canDeltaBackup, FormGroup } from './../utils'\n\nconst VMS_STATUSES_OPTIONS = [\n  { value: 'All', label: _('vmStateAll') },\n  { value: 'Running', label: _('vmStateRunning') },\n  { value: 'Halted', label: _('vmStateHalted') },\n]\n\nconst SmartBackup = decorate([\n  connectStore({\n    hosts: createGetObjectsOfType('host'),\n    vms: createGetObjectsOfType('VM'),\n  }),\n  provideState({\n    effects: {\n      setPattern:\n        (_, value) =>\n        (_, { pattern, onChange }) => {\n          onChange({\n            ...pattern,\n            ...value,\n          })\n        },\n      setPowerState({ setPattern }, powerState) {\n        setPattern({\n          power_state: powerState === 'All' ? undefined : powerState,\n        })\n      },\n      setPoolPattern:\n        ({ setPattern }, { values, notValues }) =>\n        ({ pools }) => {\n          setPattern({\n            $pool: constructSmartPattern(\n              {\n                values: values || pools.values,\n                notValues: notValues || pools.notValues,\n              },\n              resolveIds\n            ),\n          })\n        },\n      setPoolValues({ setPoolPattern }, values) {\n        setPoolPattern({ values })\n      },\n      setPoolNotValues({ setPoolPattern }, notValues) {\n        setPoolPattern({ notValues })\n      },\n    },\n    computed: {\n      poolPredicate:\n        (_, { deltaMode, hosts }) =>\n        pool =>\n          !deltaMode || canDeltaBackup(get(() => hosts[pool.master].version)),\n      pools: (_, { pattern }) => (pattern.$pool !== undefined ? destructSmartPattern(pattern.$pool) : {}),\n    },\n  }),\n  injectState,\n  ({ state, effects, vms, pattern }) => (\n    <div>\n      <FormGroup>\n        <label>\n          <strong>{_('editBackupSmartStatusTitle')}</strong>\n        </label>\n        <Select\n          options={VMS_STATUSES_OPTIONS}\n          onChange={effects.setPowerState}\n          value={defined(pattern.power_state, 'All')}\n          simpleValue\n          required\n        />\n      </FormGroup>\n      <h3>{_('editBackupSmartPools')}</h3>\n      <hr />\n      <FormGroup>\n        <label>\n          <strong>{_('editBackupSmartResidentOn')}</strong>\n        </label>\n        <SelectPool multi onChange={effects.setPoolValues} predicate={state.poolPredicate} value={state.pools.values} />\n      </FormGroup>\n      <FormGroup>\n        <label>\n          <strong>{_('editBackupSmartNotResidentOn')}</strong>\n        </label>\n        <SelectPool multi onChange={effects.setPoolNotValues} value={state.pools.notValues} />\n      </FormGroup>\n      <h3>\n        {_('editBackupSmartTags')}\n        <Tooltip content={_('editBackupSmartTagsInfo')}>\n          <Icon icon='info' />\n        </Tooltip>{' '}\n      </h3>\n      <hr />\n      <FormGroup>\n        <label>\n          <strong>{_('editBackupSmartTagsTitle')}</strong>\n        </label>{' '}\n        <SelectTag multi onChange={effects.setTagValues} value={get(() => state.tags.values)} />\n      </FormGroup>\n      <FormGroup>\n        <label>\n          <strong>{_('editBackupSmartExcludedTagsTitle')}</strong>\n        </label>{' '}\n        <Tooltip content={_('backupReplicatedVmsInfo')}>\n          <Icon icon='info' />\n        </Tooltip>{' '}\n        <SelectTag multi onChange={effects.setTagNotValues} value={get(() => state.tags.notValues)} />\n      </FormGroup>\n      <SmartBackupPreview vms={vms} pattern={state.vmsSmartPattern} />\n    </div>\n  ),\n])\n\nSmartBackup.propTypes = {\n  onChange: PropTypes.func.isRequired,\n  pattern: PropTypes.object.isRequired,\n}\n\nexport default SmartBackup\n","import _ from 'intl'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport React from 'react'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { injectState, provideState } from 'reaclette'\n\nimport JobsTable from './tab-jobs'\nimport LogsTable from '../../logs/backup-ng'\n\nconst Overview = decorate([\n  provideState({\n    initialState: () => ({\n      scrollIntoJobs: undefined,\n      scrollIntoLogs: undefined,\n    }),\n    effects: {\n      handleJobsRef(_, ref) {\n        if (ref !== null) {\n          this.state.scrollIntoJobs = ref.scrollIntoView.bind(ref)\n        }\n      },\n      handleLogsRef(_, ref) {\n        if (ref !== null) {\n          this.state.scrollIntoLogs = ref.scrollIntoView.bind(ref)\n        }\n      },\n    },\n  }),\n  injectState,\n  ({ effects, state: { scrollIntoJobs, scrollIntoLogs } }) => (\n    <div>\n      <div className='mb-1'>\n        <Card>\n          <CardHeader>\n            <Icon icon='backup' /> {_('backupJobs')}\n          </CardHeader>\n          <CardBlock>\n            <div ref={effects.handleJobsRef}>\n              <JobsTable scrollIntoLogs={scrollIntoLogs} />\n            </div>\n          </CardBlock>\n        </Card>\n        <div ref={effects.handleLogsRef}>\n          <LogsTable scrollIntoJobs={scrollIntoJobs} />\n        </div>\n      </div>\n    </div>\n  ),\n])\n\nexport default Overview\n","import * as CM from 'complex-matcher'\nimport _ from 'intl'\nimport ActionButton from 'action-button'\nimport addSubscriptions from 'add-subscriptions'\nimport Button from 'button'\nimport Copiable from 'copiable'\nimport CopyToClipboard from 'react-copy-to-clipboard'\nimport decorate from 'apply-decorators'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport StateButton from 'state-button'\nimport Tooltip from 'tooltip'\nimport { confirm } from 'modal'\nimport { connectStore, formatSpeed } from 'utils'\nimport { createFilter, createGetObjectsOfType, createSelector } from 'selectors'\nimport { createPredicate } from 'value-matcher'\nimport { get } from '@xen-orchestra/defined'\nimport { groupBy, isEmpty, map, some } from 'lodash'\nimport { injectState, provideState } from 'reaclette'\nimport { Proxy } from 'render-xo-item'\nimport { smartModeToComplexMatcher } from 'smartModeToComplexMatcher'\nimport { withRouter } from 'react-router'\nimport {\n  cancelJob,\n  deleteBackupJobs,\n  disableSchedule,\n  enableSchedule,\n  runBackupNgJob,\n  runMetadataBackupJob,\n  runMirrorBackupJob,\n  subscribeBackupNgJobs,\n  subscribeBackupNgLogs,\n  subscribeMetadataBackupJobs,\n  subscribeMirrorBackupJobs,\n  subscribeSchedules,\n} from 'xo'\n\nimport getSettingsWithNonDefaultValue from '../_getSettingsWithNonDefaultValue'\nimport { destructPattern } from '../utils'\nimport { REPORT_WHEN_LABELS } from '../new/_reportWhen'\nimport { LogStatus } from '../../logs/backup-ng'\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 isMirrorBackupType = item => item?.type === 'mirrorBackup'\n\nconst isBackupType = item => item?.type === 'backup'\n\nconst MODES = [\n  {\n    label: 'mirrorFullBackup',\n    test: job => isMirrorBackupType(job) && job.mode === 'full',\n  },\n  {\n    label: 'mirrorIncrementalBackup',\n    test: job => isMirrorBackupType(job) && job.mode === 'delta',\n  },\n  {\n    label: 'rollingSnapshot',\n    test: job => isBackupType(job) && some(job.settings, ({ snapshotRetention }) => snapshotRetention > 0),\n  },\n  {\n    label: 'backup',\n    test: job => isBackupType(job) && job.mode === 'full' && !isEmpty(get(() => destructPattern(job.remotes))),\n  },\n  {\n    label: 'deltaBackup',\n    test: job => isBackupType(job) && job.mode === 'delta' && !isEmpty(get(() => destructPattern(job.remotes))),\n  },\n  {\n    label: 'continuousReplication',\n    test: job => isBackupType(job) && job.mode === 'delta' && !isEmpty(get(() => destructPattern(job.srs))),\n  },\n  {\n    label: 'disasterRecovery',\n    test: job => isBackupType(job) && job.mode === 'full' && !isEmpty(get(() => destructPattern(job.srs))),\n  },\n  {\n    label: 'poolMetadata',\n    test: job => !isEmpty(destructPattern(job.pools)),\n  },\n  {\n    label: 'xoConfig',\n    test: job => job.xoMetadata,\n  },\n]\n\nconst _deleteBackupJobs = items => {\n  const { backup: backupIds, metadataBackup: metadataBackupIds, mirrorBackup: mirrorBackupIds } = groupBy(items, 'type')\n  return deleteBackupJobs({ backupIds, metadataBackupIds, mirrorBackupIds })\n}\n\nconst _runBackupJob = ({ id, name, nVms, schedule, type }) =>\n  confirm({\n    title: _('runJob'),\n    body: (\n      <span>\n        {_('runBackupNgJobConfirm', {\n          id: id.slice(0, 5),\n          name: <strong>{name}</strong>,\n        })}{' '}\n        {type === 'backup' &&\n          _('runBackupJobWarningNVms', {\n            nVms,\n          })}\n      </span>\n    ),\n  }).then(() => {\n    const method =\n      type === 'backup' ? runBackupNgJob : isMirrorBackupType({ type }) ? runMirrorBackupJob : runMetadataBackupJob\n    return method({ id, schedule })\n  })\n\nconst CURSOR_POINTER_STYLE = { cursor: 'pointer' }\nconst GoToLogs = decorate([\n  withRouter,\n  provideState({\n    effects: {\n      goTo() {\n        const { jobId, location, router, scheduleId, scrollIntoLogs } = this.props\n        const search = jobId !== undefined ? ['jobId', jobId] : ['scheduleId', scheduleId]\n        router.replace({\n          ...location,\n          query: {\n            ...location.query,\n            s_logs: new CM.Property(search[0], new CM.String(search[1])).toString(),\n          },\n        })\n        scrollIntoLogs()\n      },\n    },\n  }),\n  injectState,\n  ({ effects, children }) => (\n    <Tooltip content={_('goToCorrespondingLogs')}>\n      <span onClick={effects.goTo} style={CURSOR_POINTER_STYLE}>\n        {children}\n      </span>\n    </Tooltip>\n  ),\n])\n\nGoToLogs.propTypes = {\n  jobId: PropTypes.string,\n  scheduleId: PropTypes.string,\n  scrollIntoLogs: PropTypes.func.isRequired,\n}\n\nconst SchedulePreviewBody = decorate([\n  addSubscriptions(({ schedule }) => ({\n    lastRunLog: cb =>\n      subscribeBackupNgLogs(logs => {\n        let lastRunLog\n        for (const runId in logs) {\n          const log = logs[runId]\n          if (log.scheduleId === schedule.id) {\n            if (log.status === 'pending') {\n              lastRunLog = log\n              break\n            }\n            if (lastRunLog === undefined || (lastRunLog.end || lastRunLog.start) < (log.end || log.start)) {\n              lastRunLog = log\n            }\n          }\n        }\n        cb(lastRunLog)\n      }),\n  })),\n  connectStore(() => ({\n    nVms: createGetObjectsOfType('VM')\n      .filter(\n        createSelector(\n          (_, props) => props.job.id,\n          (_, props) => props.job.vms,\n          (jobId, pattern) => {\n            const isMatchingVm = createPredicate(pattern)\n            return vm => isMatchingVm(vm) && !('start' in vm.blockedOperations && vm.other['xo:backup:job'] === jobId)\n          }\n        )\n      )\n      .count(),\n  })),\n  ({ job, schedule, scrollIntoLogs, lastRunLog, nVms }) => (\n    <Ul>\n      <Li>\n        <GoToLogs scheduleId={schedule.id} scrollIntoLogs={scrollIntoLogs}>\n          {schedule.name ? _.keyValue(_('scheduleName'), schedule.name) : _.keyValue(_('scheduleCron'), schedule.cron)}\n        </GoToLogs>{' '}\n        <Tooltip content={_('scheduleCopyId', { id: schedule.id.slice(4, 8) })}>\n          <CopyToClipboard text={schedule.id}>\n            <Button size='small'>\n              <Icon icon='clipboard' />\n            </Button>\n          </CopyToClipboard>\n        </Tooltip>\n      </Li>\n      <Li>\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          style={{ marginRight: '0.5em' }}\n        />\n        {job.runId !== undefined ? (\n          <Tooltip content={_('temporarilyDisabled')}>\n            <ActionButton\n              btnStyle='danger'\n              // 2020-01-29 Job cancellation will be disabled until we find a way to make it work.\n              // See https://github.com/vatesfr/xen-orchestra/issues/4657\n              disabled\n              handler={cancelJob}\n              handlerParam={job}\n              icon='cancel'\n              key='cancel'\n              size='small'\n              tooltip={_('formCancel')}\n            />\n          </Tooltip>\n        ) : (\n          <ActionButton\n            btnStyle='primary'\n            data-id={job.id}\n            data-name={job.name}\n            data-nVms={nVms}\n            data-schedule={schedule.id}\n            data-type={job.type}\n            handler={_runBackupJob}\n            icon='run-schedule'\n            key='run'\n            size='small'\n            tooltip={_('runBackupJob')}\n          />\n        )}{' '}\n        {lastRunLog !== undefined && <LogStatus log={lastRunLog} tooltip={_('scheduleLastRun')} />}\n      </Li>\n    </Ul>\n  ),\n])\n\n@addSubscriptions({\n  jobs: subscribeBackupNgJobs,\n  metadataJobs: subscribeMetadataBackupJobs,\n  mirrorBackupJobs: subscribeMirrorBackupJobs,\n  schedulesByJob: cb =>\n    subscribeSchedules(schedules => {\n      cb(groupBy(schedules, 'jobId'))\n    }),\n})\nclass JobsTable extends React.Component {\n  static contextTypes = {\n    router: PropTypes.object,\n  }\n\n  static propTypes = {\n    predicate: PropTypes.func,\n    main: PropTypes.bool,\n  }\n\n  static defaultProps = {\n    main: true,\n  }\n\n  static tableProps = {\n    columns: [\n      {\n        itemRenderer: ({ id }, { scrollIntoLogs }) => (\n          <Copiable data={id} tagName='p'>\n            <GoToLogs jobId={id} scrollIntoLogs={scrollIntoLogs}>\n              {id.slice(4, 8)}\n            </GoToLogs>\n          </Copiable>\n        ),\n        name: _('jobId'),\n      },\n      {\n        valuePath: 'name',\n        name: _('jobName'),\n        default: true,\n      },\n      {\n        itemRenderer: job => (\n          <Ul>\n            {MODES.filter(({ test }) => test(job)).map(({ label }) => (\n              <Li key={label}>{_(label)}</Li>\n            ))}\n          </Ul>\n        ),\n        sortCriteria: 'mode',\n        name: _('jobModes'),\n      },\n      {\n        itemRenderer: (job, { schedulesByJob, scrollIntoLogs }) =>\n          map(\n            get(() => schedulesByJob[job.id]),\n            schedule => (\n              <SchedulePreviewBody job={job} key={schedule.id} schedule={schedule} scrollIntoLogs={scrollIntoLogs} />\n            )\n          ),\n        name: _('jobSchedules'),\n      },\n      {\n        itemRenderer: job => {\n          const {\n            backupReportTpl,\n            cbtDestroySnapshotData,\n            checkpointSnapshot,\n            compression,\n            concurrency,\n            fullInterval,\n            hideSuccessfulItems,\n            maxExportRate,\n            nbdConcurrency,\n            nRetriesVmBackupFailures,\n            offlineBackup,\n            offlineSnapshot,\n            preferNbd,\n            proxyId,\n            reportWhen,\n            timeout,\n          } = getSettingsWithNonDefaultValue(job.mode, {\n            compression: job.compression,\n            proxyId: job.proxy,\n            ...job.settings?.[''],\n          })\n\n          return (\n            <Ul>\n              {proxyId !== undefined && <Li>{_.keyValue(_('proxy'), <Proxy id={proxyId} key={proxyId} />)}</Li>}\n              {reportWhen in REPORT_WHEN_LABELS && (\n                <Li>{_.keyValue(_('reportWhen'), _(REPORT_WHEN_LABELS[reportWhen]))}</Li>\n              )}\n              {backupReportTpl !== undefined && (\n                <Li>\n                  {_.keyValue(\n                    _('shorterBackupReports'),\n                    _(backupReportTpl === 'compactMjml' ? 'stateEnabled' : 'stateDisabled')\n                  )}\n                </Li>\n              )}\n              {hideSuccessfulItems !== undefined && (\n                <Li>\n                  {_.keyValue(_('hideSuccessfulItems'), _(hideSuccessfulItems ? 'stateEnabled' : 'stateDisabled'))}\n                </Li>\n              )}\n              {concurrency !== undefined && <Li>{_.keyValue(_('concurrency'), concurrency)}</Li>}\n              {preferNbd && <Li>{_.keyValue(_('nbdConnections'), nbdConcurrency ?? 1)}</Li>}\n              {preferNbd && cbtDestroySnapshotData !== undefined && (\n                <Li>\n                  {_.keyValue(\n                    _('cbtDestroySnapshotData'),\n                    _(cbtDestroySnapshotData ? 'stateEnabled' : 'stateDisabled')\n                  )}\n                </Li>\n              )}\n              {timeout !== undefined && <Li>{_.keyValue(_('timeout'), timeout / 3600e3)} hours</Li>}\n              {fullInterval !== undefined && <Li>{_.keyValue(_('fullBackupInterval'), fullInterval)}</Li>}\n              {maxExportRate > 0 && <Li>{_.keyValue(_('speedLimitNoUnit'), formatSpeed(maxExportRate, 1000))}</Li>}\n              {offlineBackup !== undefined && (\n                <Li>{_.keyValue(_('offlineBackup'), _(offlineBackup ? 'stateEnabled' : 'stateDisabled'))}</Li>\n              )}\n              {offlineSnapshot !== undefined && (\n                <Li>{_.keyValue(_('offlineSnapshot'), _(offlineSnapshot ? 'stateEnabled' : 'stateDisabled'))}</Li>\n              )}\n              {checkpointSnapshot !== undefined && (\n                <Li>{_.keyValue(_('checkpointSnapshot'), _(checkpointSnapshot ? 'stateEnabled' : 'stateDisabled'))}</Li>\n              )}\n              {compression !== undefined && (\n                <Li>{_.keyValue(_('compression'), compression === 'native' ? 'GZIP' : compression)}</Li>\n              )}\n              {nRetriesVmBackupFailures > 0 && (\n                <Li>{_.keyValue(_('nRetriesVmBackupFailures'), nRetriesVmBackupFailures)}</Li>\n              )}\n            </Ul>\n          )\n        },\n        name: _('formNotes'),\n      },\n    ],\n    individualActions: [\n      {\n        handler: (job, { goTo }) =>\n          goTo({\n            pathname: '/home',\n            query: { t: 'VM', s: smartModeToComplexMatcher(job.vms).toString() },\n          }),\n        disabled: job => job.type !== 'backup',\n        label: _('redirectToMatchingVms'),\n        icon: 'preview',\n      },\n      {\n        handler: (job, { goTo, goToNewTab, main }) => (main ? goTo : goToNewTab)(`/backup/${job.id}/edit`),\n        label: _('formEdit'),\n        icon: 'edit',\n        level: 'primary',\n      },\n    ],\n  }\n\n  _getActions = createSelector(\n    () => this.props.main,\n    main =>\n      main\n        ? [\n            {\n              handler: _deleteBackupJobs,\n              label: _('deleteBackupSchedule'),\n              icon: 'delete',\n              level: 'danger',\n            },\n          ]\n        : undefined\n  )\n\n  _goTo = path => {\n    this.context.router.push(path)\n  }\n\n  _goToNewTab = path => {\n    window.open(this.context.router.createHref(path))\n  }\n\n  _getCollection = createFilter(\n    createSelector(\n      () => this.props.jobs,\n      () => this.props.metadataJobs,\n      () => this.props.mirrorBackupJobs,\n      (jobs = [], metadataJobs = [], mirrorJobs = []) => [...jobs, ...metadataJobs, ...mirrorJobs]\n    ),\n    () => this.props.predicate\n  )\n\n  render() {\n    return (\n      <SortedTable\n        {...JobsTable.tableProps}\n        actions={this._getActions()}\n        collection={this._getCollection()}\n        data-goTo={this._goTo}\n        data-goToNewTab={this._goToNewTab}\n        data-main={this.props.main}\n        data-schedulesByJob={this.props.schedulesByJob}\n        data-scrollIntoLogs={this.props.scrollIntoLogs}\n        stateUrlParam='s'\n      />\n    )\n  }\n}\n\nexport default JobsTable\n","import _ from 'intl'\nimport classNames from 'classnames'\nimport Component from 'base-component'\nimport React from 'react'\nimport { forEach, map, orderBy } from 'lodash'\nimport { createFilter, createSelector } from 'selectors'\nimport { Toggle } from 'form'\nimport { getRenderXoItemOfType } from 'render-xo-item'\n\nconst BACKUP_RENDERER = getRenderXoItemOfType('backup')\n\nconst _escapeDot = id => id.replace('.', '\\0')\n\nexport default class DeleteBackupsModalBody extends Component {\n  get value() {\n    return this._getSelectedBackups()\n  }\n\n  _selectAll = () => {\n    const selected = this._getSelectedBackups().length === 0\n\n    const state = {}\n    forEach(this.props.backups, backup => {\n      state[_escapeDot(backup.id)] = selected\n    })\n\n    this.setState(state)\n  }\n\n  _getSelectedBackups = createFilter(\n    () => this.props.backups,\n    createSelector(\n      () => this.state,\n      state => backup => state[_escapeDot(backup.id)]\n    )\n  )\n\n  _getAllSelected = createSelector(\n    () => this.props.backups,\n    this._getSelectedBackups,\n    (backups, selectedBackups) => backups.length === selectedBackups.length\n  )\n\n  _getBackups = createSelector(\n    () => this.props.backups,\n    backups => orderBy(backups, 'timestamp', 'desc')\n  )\n\n  render() {\n    return (\n      <div>\n        <div>{_('deleteBackupsSelect')}</div>\n        <div className='list-group'>\n          {map(this._getBackups(), backup => (\n            <button\n              className={classNames(\n                'list-group-item',\n                'list-group-item-action',\n                this.state[_escapeDot(backup.id)] && 'active'\n              )}\n              data-id={backup.id}\n              key={backup.id}\n              onClick={this.toggleState(_escapeDot(backup.id))}\n              type='button'\n            >\n              {BACKUP_RENDERER(backup)}\n            </button>\n          ))}\n        </div>\n        <div>\n          <Toggle iconSize={1} onChange={this._selectAll} value={this._getAllSelected()} />{' '}\n          {_('deleteVmBackupsSelectAll')}\n        </div>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport { Container, Col } from 'grid'\nimport { NumericDate } from 'utils'\nimport { Select } from 'form'\n\nexport default class DeleteMetadataBackupModalBody extends Component {\n  static propTypes = {\n    backups: PropTypes.array,\n  }\n\n  get value() {\n    return this.state.backups\n  }\n\n  _optionRenderer = ({ timestamp }) => <NumericDate timestamp={timestamp} />\n\n  render() {\n    return (\n      <Container>\n        <SingleLineRow>\n          <Col size={6}>{_('deleteBackupsSelect')}</Col>\n          <Col size={6}>\n            <Select\n              labelKey='timestamp'\n              multi\n              onChange={this.linkState('backups')}\n              optionRenderer={this._optionRenderer}\n              options={this.props.backups}\n              required\n              value={this.state.backups}\n              valueKey='id'\n            />\n          </Col>\n        </SingleLineRow>\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport ButtonLink from 'button-link'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport { addSubscriptions, formatSize, noop, NumericDate } from 'utils'\nimport { confirm } from 'modal'\nimport { error } from 'notification'\nimport { cloneDeep, filter, find, flatMap, forEach, map, reduce, orderBy } from 'lodash'\nimport { checkBackup, deleteBackups, listVmBackups, restoreBackup, subscribeBackupNgJobs, subscribeRemotes } from 'xo'\n\nimport RestoreBackupsModalBody, { RestoreBackupsBulkModalBody } from './restore-backups-modal-body'\nimport DeleteBackupsModalBody from './delete-backups-modal-body'\n\nimport Logs from '../../logs/restore'\n\nexport RestoreMetadata from './metadata'\n\n// -----------------------------------------------------------------------------\n\nconst BACKUPS_COLUMNS = [\n  {\n    name: _('backupVmNameColumn'),\n    itemRenderer: ({ last }) => last.vm.name_label,\n    sortCriteria: 'last.vm.name_label',\n  },\n  {\n    name: _('backupVmDescriptionColumn'),\n    itemRenderer: ({ last }) => last.vm.name_description,\n    sortCriteria: 'last.vm.name_description',\n  },\n  {\n    name: _('firstBackupColumn'),\n    itemRenderer: ({ first }) => <NumericDate timestamp={first.timestamp} />,\n    sortCriteria: 'first.timestamp',\n    sortOrder: 'desc',\n  },\n  {\n    name: _('lastBackupColumn'),\n    itemRenderer: ({ last }) => <NumericDate timestamp={last.timestamp} />,\n    sortCriteria: 'last.timestamp',\n    default: true,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('labelSize'),\n    itemRenderer: ({ size }) => size !== undefined && size !== 0 && formatSize(size),\n    sortCriteria: 'size',\n  },\n  {\n    name: _('availableBackupsColumn'),\n    itemRenderer: ({ count }) =>\n      map(count, (n, mode) => (\n        <span key={mode}>\n          <span style={{ textTransform: 'capitalize' }}>{mode}</span>{' '}\n          <span className='tag tag-pill tag-primary'>{n}</span>\n          <br />\n        </span>\n      )),\n  },\n]\n\n// -----------------------------------------------------------------------------\n\n@addSubscriptions({\n  jobs: subscribeBackupNgJobs,\n  remotes: subscribeRemotes,\n})\nexport default class Restore extends Component {\n  state = {\n    backupDataByVm: {},\n  }\n\n  componentWillReceiveProps(props) {\n    if (props.remotes !== this.props.remotes || props.jobs !== this.props.jobs) {\n      this._refreshBackupList(props.remotes, props.jobs)\n    }\n  }\n\n  _sortBackupList = backupDataByVm => {\n    let first, last\n    forEach(backupDataByVm, (data, vmId) => {\n      first = { timestamp: Infinity }\n      last = { timestamp: 0 }\n      const count = {}\n      let size = 0\n      forEach(data.backups, backup => {\n        if (backup.timestamp > last.timestamp) {\n          last = backup\n        }\n        if (backup.timestamp < first.timestamp) {\n          first = backup\n        }\n        count[backup.mode] = (count[backup.mode] || 0) + 1\n\n        if (backup.size !== undefined) {\n          size += backup.size\n        }\n      })\n\n      Object.assign(data, { first, last, count, id: vmId, size })\n    })\n\n    forEach(backupDataByVm, ({ backups }, vmId) => {\n      backupDataByVm[vmId].backups = orderBy(backups, 'timestamp', 'desc')\n    })\n\n    return backupDataByVm\n  }\n\n  _refreshBackupListOnRemote = async (remote, jobs) => {\n    const remoteId = remote.id\n    const backupsByRemote = await listVmBackups([remoteId])\n    const { backupDataByVm } = this.state\n    const remoteBackupDataByVm = {}\n    forEach(backupsByRemote[remoteId], (vmBackups, vmId) => {\n      if (vmBackups.length === 0) {\n        return\n      }\n\n      const backupData = backupDataByVm[vmId]\n      remoteBackupDataByVm[vmId] = backupData === undefined ? { backups: [] } : cloneDeep(backupData)\n\n      remoteBackupDataByVm[vmId].backups.push(\n        ...map(vmBackups, bkp => {\n          const job = find(jobs, { id: bkp.jobId })\n          return {\n            ...bkp,\n            remote,\n            jobName: job && job.name,\n          }\n        })\n      )\n    })\n\n    this.setState(({ backupDataByVm }) => ({\n      backupDataByVm: {\n        ...backupDataByVm,\n        ...this._sortBackupList(remoteBackupDataByVm),\n      },\n    }))\n  }\n\n  _refreshBackupList = (remotes = this.props.remotes, jobs = this.props.jobs) =>\n    new Promise((resolve, reject) => {\n      this.setState({ backupDataByVm: {} }, () =>\n        Promise.all(\n          map(filter(remotes, { enabled: true }), remote =>\n            this._refreshBackupListOnRemote(remote, jobs).catch(() =>\n              error(_('remoteLoadBackupsFailure'), _('remoteLoadBackupsFailureMessage', { name: remote.name }))\n            )\n          )\n        ).then(resolve, reject)\n      )\n    })\n  // Actions -------------------------------------------------------------------\n\n  _restore = data =>\n    confirm({\n      title: _('restoreVmBackupsTitle', { vm: data.last.vm.name_label }),\n      body: <RestoreBackupsModalBody data={data} />,\n      icon: 'restore',\n    })\n      .then(({ backup, generateNewMacAddresses, targetSrs: { mainSr, mapVdisSrs }, start, useDifferentialRestore }) => {\n        if (backup == null || mainSr == null) {\n          error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))\n          return\n        }\n        return restoreBackup(backup, mainSr, {\n          generateNewMacAddresses,\n          mapVdisSrs,\n          startOnRestore: start,\n          useDifferentialRestore,\n        })\n      }, noop)\n      .then(() => this._refreshBackupList())\n\n  _restoreHealthCheck = data =>\n    confirm({\n      title: _('checkVmBackupsTitle', { vm: data.last.vm.name_label }),\n      body: (\n        <RestoreBackupsModalBody\n          data={data}\n          showGenerateNewMacAddress={false}\n          showStartAfterBackup={false}\n          backupHealthCheck\n        />\n      ),\n      icon: 'restore',\n    })\n      .then(({ backup, targetSrs: { mainSr, mapVdisSrs } }) => {\n        if (backup == null || mainSr == null) {\n          error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))\n          return\n        }\n\n        return checkBackup(backup, mainSr, {\n          mapVdisSrs,\n        })\n      }, noop)\n      .then(() => this._refreshBackupList())\n\n  _delete = data =>\n    confirm({\n      title: _('deleteVmBackupsTitle', { vm: data.last.vm.name_label }),\n      body: <DeleteBackupsModalBody backups={data.backups} />,\n      icon: 'delete',\n    })\n      .then(deleteBackups, noop)\n      .then(() => this._refreshBackupList())\n\n  _bulkRestore = datas =>\n    confirm({\n      title: _('restoreVmBackupsBulkTitle', { nVms: datas.length }),\n      body: <RestoreBackupsBulkModalBody datas={datas} />,\n      icon: 'restore',\n    })\n      .then(({ sr, generateNewMacAddresses, latest, start }) => {\n        if (sr == null) {\n          error(_('restoreVmBackupsBulkErrorTitle', 'restoreVmBackupsBulkErrorMessage'))\n          return\n        }\n\n        const prop = latest ? 'last' : 'first'\n        return Promise.all(\n          map(datas, data => restoreBackup(data[prop], sr, { generateNewMacAddresses, startOnRestore: start }))\n        )\n      }, noop)\n      .then(() => this._refreshBackupList())\n\n  _bulkDelete = datas =>\n    confirm({\n      title: _('deleteVmBackupsBulkTitle'),\n      body: <p>{_('deleteVmBackupsBulkMessage', { nVms: datas.length })}</p>,\n      icon: 'delete',\n      strongConfirm: {\n        messageId: 'deleteVmBackupsBulkConfirmText',\n        values: {\n          nBackups: reduce(datas, (sum, data) => sum + data.backups.length, 0),\n        },\n      },\n    })\n      .then(() => deleteBackups(flatMap(datas, 'backups')), noop)\n      .then(() => this._refreshBackupList())\n\n  // ---------------------------------------------------------------------------\n\n  _actions = [\n    {\n      handler: this._bulkRestore,\n      icon: 'restore',\n      individualHandler: this._restore,\n      label: _('restoreVmBackups'),\n      level: 'primary',\n    },\n    {\n      icon: 'check',\n      individualHandler: this._restoreHealthCheck,\n      label: _('checkBackup'),\n      level: 'secondary',\n    },\n    {\n      handler: this._bulkDelete,\n      icon: 'delete',\n      individualHandler: this._delete,\n      label: _('deleteVmBackups'),\n      level: 'danger',\n    },\n  ]\n\n  render() {\n    return (\n      <div>\n        <div className='mb-1'>\n          <ActionButton btnStyle='primary' handler={this._refreshBackupList} icon='refresh'>\n            {_('refreshBackupList')}\n          </ActionButton>{' '}\n          <ButtonLink to='backup/restore/metadata'>\n            <Icon icon='database' /> {_('metadata')}\n          </ButtonLink>\n        </div>\n        <SortedTable\n          actions={this._actions}\n          collection={this.state.backupDataByVm}\n          columns={BACKUPS_COLUMNS}\n          stateUrlParam='s'\n        />\n        <br />\n        <Logs />\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport addSubscriptions from 'add-subscriptions'\nimport ButtonLink from 'button-link'\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 Upgrade from 'xoa-upgrade'\nimport { confirm } from 'modal'\nimport { error } from 'notification'\nimport { filter, flatMap, forOwn, reduce } from 'lodash'\nimport { injectState, provideState } from 'reaclette'\nimport { noop, NumericDate, resolveId } from 'utils'\nimport { deleteMetadataBackups, listMetadataBackups, restoreMetadataBackup, subscribeRemotes } from 'xo'\n\nimport Logs from '../../logs/restore-metadata'\n\nimport DeleteMetadataBackupModalBody from './delete-metadata-backups-modal-body'\nimport RestoreMetadataBackupModalBody, {\n  RestoreMetadataBackupsBulkModalBody,\n} from './restore-metadata-backups-modal-body'\n\n// Actions -------------------------------------------------------------------\n\nconst restore = entry =>\n  confirm({\n    title: _('restoreMetadataBackupTitle', {\n      item: `${entry.type} (${entry.label})`,\n    }),\n    body: <RestoreMetadataBackupModalBody backups={entry.backups} type={entry.type} />,\n    icon: 'restore',\n  }).then(data => {\n    if (data === undefined) {\n      error(_('backupRestoreErrorTitle'), _('chooseBackup'))\n      return\n    }\n    return restoreMetadataBackup({ backup: resolveId(data.backup), pool: resolveId(data.pool) })\n  }, noop)\n\nconst bulkRestore = entries => {\n  const nMetadataBackups = entries.length\n  return confirm({\n    title: _('bulkRestoreMetadataBackupTitle', { nMetadataBackups }),\n    body: <RestoreMetadataBackupsBulkModalBody nMetadataBackups={nMetadataBackups} poolMetadataBackups={entries} />,\n    icon: 'restore',\n  }).then(\n    data =>\n      Promise.all(\n        entries.map(({ first, last, id }) =>\n          restoreMetadataBackup({ backup: data.latest ? last : first, pool: resolveId(data[id]) })\n        )\n      ),\n    noop\n  )\n}\n\nconst delete_ = entry =>\n  confirm({\n    title: _('deleteMetadataBackupTitle', {\n      item: `${entry.type} (${entry.label})`,\n    }),\n    body: <DeleteMetadataBackupModalBody backups={entry.backups} />,\n    icon: 'delete',\n  }).then(deleteMetadataBackups, noop)\n\nconst bulkDelete = entries => {\n  confirm({\n    title: _('bulkDeleteMetadataBackupsTitle'),\n    body: (\n      <p>\n        {_('bulkDeleteMetadataBackupsMessage', {\n          nMetadataBackups: entries.length,\n        })}\n      </p>\n    ),\n    icon: 'delete',\n    strongConfirm: {\n      messageId: 'bulkDeleteMetadataBackupsConfirmText',\n      values: {\n        nMetadataBackups: reduce(entries, (sum, entry) => sum + entry.backups.length, 0),\n      },\n    },\n  }).then(() => deleteMetadataBackups(flatMap(entries, ({ backups }) => backups)), noop)\n}\n\n// ---------------------------------------------------------------------------\n\nconst ACTIONS = [\n  {\n    handler: bulkRestore,\n    icon: 'restore',\n    individualHandler: restore,\n    label: _('restore'),\n    level: 'primary',\n  },\n  {\n    handler: bulkDelete,\n    icon: 'delete',\n    individualHandler: delete_,\n    label: _('delete'),\n    level: 'danger',\n  },\n]\n\nconst COLUMNS = [\n  {\n    name: _('type'),\n    valuePath: 'type',\n  },\n  {\n    name: _('item'),\n    itemRenderer: ({ item }) => item,\n    sortCriteria: 'id',\n  },\n  {\n    name: _('firstBackupColumn'),\n    itemRenderer: ({ first }) => <NumericDate timestamp={first.timestamp} />,\n    sortCriteria: 'first',\n    sortOrder: 'desc',\n  },\n  {\n    name: _('lastBackupColumn'),\n    itemRenderer: ({ last }) => <NumericDate timestamp={last.timestamp} />,\n    sortCriteria: 'last',\n    default: true,\n    sortOrder: 'desc',\n  },\n  {\n    name: _('availableBackupsColumn'),\n    valuePath: 'available',\n  },\n]\n\nexport default decorate([\n  addSubscriptions({\n    remotes: cb =>\n      subscribeRemotes(remotes => {\n        cb(filter(remotes, { enabled: true }))\n      }),\n  }),\n  provideState({\n    computed: {\n      // {\n      //   [jobId | poolId]: {\n      //     available: Number,\n      //     backups: Array,\n      //     first: Number,\n      //     id: jobId | poolId, // required by the SortedTable\n      //     item: Node | String,\n      //     label: String,\n      //     last: Number,\n      //     type: 'XO' | 'pool',\n      //   }\n      // }\n      async backups(_, { remotes = [] }) {\n        if (remotes.length === 0) {\n          // NoObjects displays a spinner when collection is undefined\n          return {}\n        }\n\n        const { xo: xoType, pool: poolType } = await listMetadataBackups(remotes)\n\n        const collection = {}\n        forOwn(xoType, entries =>\n          entries.forEach(entry => {\n            const { jobName, jobId } = entry\n            let backup = collection[jobId]\n            if (backup === undefined) {\n              backup = collection[jobId] = {\n                backups: [],\n                id: jobId,\n                item: `Xen Orchestra (${jobName})`,\n                label: jobName,\n                type: 'XO',\n              }\n            }\n\n            backup.backups.push(entry)\n          })\n        )\n        forOwn(poolType, entriesByPool =>\n          forOwn(entriesByPool, (poolEntry, poolId) => {\n            let backup = collection[poolId]\n            if (backup === undefined) {\n              const { pool, poolMaster } = poolEntry[0]\n              const label = pool.name_label || poolMaster.name_label\n              backup = collection[poolId] = {\n                backups: [],\n                id: poolId,\n                item: (\n                  <Copiable data={poolId} tagName='p'>\n                    {label || poolId}\n                  </Copiable>\n                ),\n                label,\n                type: 'pool',\n              }\n            }\n            poolEntry.forEach(entry => {\n              backup.backups.push(entry)\n            })\n          })\n        )\n\n        forOwn(collection, entry => {\n          const backups = entry.backups\n          const size = backups.length\n\n          backups.sort((a, b) => a.timestamp - b.timestamp)\n          entry.first = backups[0]\n          entry.last = backups[size - 1]\n          entry.available = size\n        })\n\n        return collection\n      },\n    },\n  }),\n  injectState,\n  ({ state, effects }) => (\n    <Upgrade place='restoreMetadataBackup' available={3}>\n      <div>\n        <div className='mb-1'>\n          <ButtonLink to='backup/restore'>\n            <Icon icon='backup' /> {_('vms')}\n          </ButtonLink>\n        </div>\n        <NoObjects\n          actions={ACTIONS}\n          collection={state.backups}\n          columns={COLUMNS}\n          component={SortedTable}\n          emptyMessage={_('noBackups')}\n          stateUrlParam='s_metadata'\n        />\n        <br />\n        <Logs />\n      </div>\n    </Upgrade>\n  ),\n])\n","import _ from 'intl'\nimport Icon from 'icon'\nimport React from 'react'\nimport ChooseSrForEachVdisModal from 'xo/choose-sr-for-each-vdis-modal'\nimport Component from 'base-component'\nimport StateButton from 'state-button'\nimport { createSelector } from 'selectors'\nimport { getRenderXoItemOfType } from 'render-xo-item'\nimport { Select, Toggle } from 'form'\nimport { SelectSr } from 'select-objects'\n\nconst BACKUP_RENDERER = getRenderXoItemOfType('backup')\n\nexport default class RestoreBackupsModalBody extends Component {\n  state = {\n    generateNewMacAddresses: false,\n    targetSrs: { mainSr: undefined, mapVdisSrs: undefined },\n    useDifferentialRestore: false,\n  }\n\n  get value() {\n    return this.state\n  }\n\n  _getDisks = createSelector(\n    () => this.state.backup,\n    () => this.state.targetSrs.mapVdisSrs,\n    (backup, mapVdisSrs) =>\n      backup !== undefined && backup.mode === 'delta'\n        ? backup.disks.reduce(\n            (vdis, vdi) =>\n              mapVdisSrs !== undefined && mapVdisSrs[vdi.uuid] === null ? vdis : { ...vdis, [vdi.uuid]: vdi },\n            {}\n          )\n        : {}\n  )\n\n  render() {\n    return (\n      <div>\n        {this.props.backupHealthCheck && (\n          <a\n            className='text-info'\n            rel='noreferrer'\n            href='https://docs.xen-orchestra.com/backups#backup-health-check'\n            target='_blank'\n          >\n            <Icon icon='info' />\n          </a>\n        )}\n        <div className='mb-1'>\n          <Select\n            optionRenderer={BACKUP_RENDERER}\n            options={this.props.data.backups}\n            onChange={this.linkState('backup')}\n            placeholder={_('importBackupModalSelectBackup')}\n          />\n        </div>\n        {this.state.backup != null && (\n          <div>\n            <div className='mb-1'>\n              <ChooseSrForEachVdisModal\n                ignorableVdis\n                onChange={this.linkState('targetSrs')}\n                placeholder={_('importBackupModalSelectSr')}\n                required\n                value={this.state.targetSrs}\n                vdis={this._getDisks()}\n              />\n            </div>\n            {this.props.showStartAfterBackup && (\n              <div>\n                <Toggle iconSize={1} onChange={this.linkState('start')} /> {_('restoreVmBackupsStart', { nVms: 1 })}\n              </div>\n            )}\n            {this.props.showGenerateNewMacAddress && (\n              <div>\n                <Toggle\n                  iconSize={1}\n                  value={this.state.generateNewMacAddresses}\n                  onChange={this.toggleState('generateNewMacAddresses')}\n                />{' '}\n                {_('generateNewMacAddress')}\n              </div>\n            )}\n\n            {this.state.backup.mode === 'delta' && (\n              <div>\n                <Toggle\n                  iconSize={1}\n                  value={this.state.useDifferentialRestore}\n                  onChange={this.toggleState('useDifferentialRestore')}\n                />{' '}\n                {_('restoreVmUseDifferentialRestore')}\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n    )\n  }\n}\nRestoreBackupsModalBody.defaultProps = {\n  showGenerateNewMacAddress: true,\n  showStartAfterBackup: true,\n  backupHealthCheck: false,\n}\n\nexport class RestoreBackupsBulkModalBody extends Component {\n  state = { generateNewMacAddresses: false, latest: true }\n\n  get value() {\n    return this.state\n  }\n  render() {\n    const { datas } = this.props\n    return (\n      <div>\n        <div className='mb-1'>\n          {_('restoreVmBackupsBulkMessage', {\n            nVms: datas.length,\n            oldestOrLatest: (\n              <StateButton\n                disabledLabel={_('oldest')}\n                disabledHandler={() => this.setState({ latest: true })}\n                enabledLabel={_('latest')}\n                enabledHandler={() => this.setState({ latest: false })}\n                state={this.state.latest}\n              />\n            ),\n          })}\n        </div>\n        <div className='mb-1'>\n          <SelectSr onChange={this.linkState('sr')} placeholder={_('importBackupModalSelectSr')} />\n        </div>\n        <div>\n          <Toggle iconSize={1} onChange={this.linkState('start')} />{' '}\n          {_('restoreVmBackupsStart', { nVms: datas.length })}\n        </div>\n        <div>\n          <Toggle\n            iconSize={1}\n            value={this.state.generateNewMacAddresses}\n            onChange={this.toggleState('generateNewMacAddresses')}\n          />{' '}\n          {_('generateNewMacAddress')}\n        </div>\n      </div>\n    )\n  }\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport SingleLineRow from 'single-line-row'\nimport StateButton from 'state-button'\nimport { Container, Col, Row } from 'grid'\nimport { NumericDate } from 'utils'\nimport { Select } from 'form'\nimport { SelectPool } from 'select-objects'\n\nconst restorationWarning = (\n  <p className='text-warning mt-1'>\n    <Icon icon='alarm' /> {_('restoreMetadataBackupWarning')}\n  </p>\n)\n\nexport default class RestoreMetadataBackupModalBody extends Component {\n  static propTypes = {\n    backups: PropTypes.array,\n    type: PropTypes.string,\n  }\n\n  get value() {\n    return this.state\n  }\n\n  _optionRenderer = ({ timestamp }) => <NumericDate timestamp={timestamp} />\n\n  render() {\n    return (\n      <Container>\n        <Row>\n          <Col size={6}>{_('chooseBackup')}</Col>\n          <Col size={6}>\n            <Select\n              labelKey='timestamp'\n              onChange={this.linkState('backup')}\n              optionRenderer={this._optionRenderer}\n              options={this.props.backups}\n              required\n              value={this.state.backup}\n              valueKey='id'\n            />\n          </Col>\n        </Row>\n        {this.props.type !== 'XO' && [\n          <Row className='mt-1' key='select'>\n            <SelectPool onChange={this.linkState('pool')} required value={this.state.pool} />\n          </Row>,\n          <SingleLineRow key='message'>{restorationWarning}</SingleLineRow>,\n        ]}\n      </Container>\n    )\n  }\n}\n\nexport class RestoreMetadataBackupsBulkModalBody extends Component {\n  static propTypes = {\n    nMetadataBackups: PropTypes.number,\n    poolMetadataBackups: PropTypes.array,\n  }\n\n  state = { latest: true }\n\n  get value() {\n    return this.state\n  }\n\n  render() {\n    return (\n      <Container>\n        <Row>\n          {_('bulkRestoreMetadataBackupMessage', {\n            nMetadataBackups: this.props.nMetadataBackups,\n            oldestOrLatest: (\n              <StateButton\n                disabledLabel={_('oldest')}\n                enabledLabel={_('latest')}\n                handler={this.toggleState('latest')}\n                state={this.state.latest}\n              />\n            ),\n          })}\n        </Row>\n        {this.props.poolMetadataBackups.map(value => (\n          <Container key={value.id} className='mt-1'>\n            <SingleLineRow>\n              <Col size={6}>{value.first.jobName}</Col>\n              <Col size={6}>\n                <SelectPool onChange={this.linkState(value.id)} required value={this.state[value.id]} />\n              </Col>\n            </SingleLineRow>\n          </Container>\n        ))}\n        {restorationWarning}\n      </Container>\n    )\n  }\n}\n","import _ from 'intl'\nimport ActionButton from 'action-button'\nimport addSubscriptions from 'add-subscriptions'\nimport Copiable from 'copiable'\nimport decorate from 'apply-decorators'\nimport filter from 'lodash/filter.js'\nimport groupBy from 'lodash/groupBy.js'\nimport Icon from 'icon'\nimport React from 'react'\nimport SortedTable from 'sorted-table'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { deleteJobs, subscribeJobs, subscribeSchedules, runJob } from 'xo'\nimport { noop } from 'utils'\nimport { Schedule } from 'render-xo-item'\n\nconst COLUMNS = [\n  {\n    itemRenderer: ({ id }) => (\n      <Copiable data={id} tagName='p'>\n        {id.slice(4, 8)}\n      </Copiable>\n    ),\n    name: _('jobId'),\n  },\n  {\n    valuePath: 'name',\n    name: _('jobName'),\n    default: true,\n  },\n  {\n    name: _('sequence'),\n    itemRenderer: sequenceJob => {\n      const scheduleIds = sequenceJob.paramsVector?.items?.[0]?.values?.[0]?.schedules\n      if (scheduleIds === undefined) {\n        return null\n      }\n\n      return (\n        <ol>\n          {scheduleIds.map((scheduleId, i) => (\n            <li key={`${i}-${scheduleId}`}>\n              <Schedule id={scheduleId} showState={false} />\n            </li>\n          ))}\n        </ol>\n      )\n    },\n  },\n  {\n    name: _('schedule'),\n    itemRenderer: (sequenceJob, { schedulesByJob }) => {\n      const schedules = schedulesByJob?.[sequenceJob.id]\n      if (schedules === undefined || schedules.length === 0) {\n        return null\n      }\n\n      return schedules[0].cron\n    },\n  },\n]\n\nconst ACTIONS = [\n  {\n    handler: jobs => deleteJobs(jobs),\n    label: _('delete'),\n    icon: 'delete',\n    level: 'danger',\n  },\n]\n\nconst INDIVIDUAL_ACTIONS = [\n  {\n    handler: job => runJob(job),\n    label: _('scheduleRun'),\n    icon: 'run-schedule',\n    level: 'primary',\n  },\n  {\n    handler: (job, { router }) => router.push(`/backup/${job.id}/edit`),\n    label: _('formEdit'),\n    icon: 'edit',\n    level: 'primary',\n  },\n]\n\nconst Sequences = decorate([\n  addSubscriptions({\n    sequenceJobs: cb => subscribeJobs(jobs => cb(filter(jobs, { type: 'call', method: 'schedule.runSequence' }))),\n    schedulesByJob: cb => subscribeSchedules(schedules => cb(groupBy(schedules, 'jobId'))),\n  }),\n  ({ sequenceJobs, schedulesByJob, router }) => (\n    <div>\n      <div className='mb-1'>\n        <Card>\n          <CardHeader>\n            <Icon icon='menu-backup-sequence' /> {_('sequences')}\n            <ActionButton\n              btnStyle='primary'\n              className='pull-right'\n              icon='add'\n              handler={noop}\n              redirectOnSuccess='/backup/new/sequence'\n            >\n              {_('new')}\n            </ActionButton>\n          </CardHeader>\n          <CardBlock>\n            <SortedTable\n              columns={COLUMNS}\n              collection={sequenceJobs}\n              actions={ACTIONS}\n              individualActions={INDIVIDUAL_ACTIONS}\n              data-router={router}\n              data-schedulesByJob={schedulesByJob}\n            />\n          </CardBlock>\n        </Card>\n      </div>\n    </div>\n  ),\n])\n\nexport default Sequences\n","import classNames from 'classnames'\nimport Icon from 'icon'\nimport PropTypes from 'prop-types'\nimport React from 'react'\nimport { resolveId, resolveIds } from 'utils'\n\nexport const FormGroup = props => <div {...props} className={classNames(props.className, 'form-group')} />\nexport const Input = props => <input {...props} className={classNames(props.className, 'form-control')} />\nexport const Ul = props => <ul {...props} className={classNames(props.className, 'list-group')} />\nexport const Li = props => <li {...props} className={classNames(props.className, 'list-group-item')} />\n\nexport const destructPattern = pattern => pattern && (pattern.id.__or || [pattern.id])\nexport const constructPattern = values =>\n  values.length === 1\n    ? {\n        id: resolveId(values[0]),\n      }\n    : {\n        id: {\n          __or: resolveIds(values),\n        },\n      }\n\nexport const FormFeedback = ({ component: Component, error, message, ...props }) => (\n  <div>\n    <Component\n      {...props}\n      style={\n        error === undefined\n          ? undefined\n          : {\n              borderColor: error ? 'red' : 'green',\n              ...props.style,\n            }\n      }\n    />\n    {error && (\n      <span className='text-danger'>\n        <Icon icon='alarm' /> {message}\n      </span>\n    )}\n  </div>\n)\n\nFormFeedback.propTypes = {\n  component: PropTypes.func.isRequired,\n  error: PropTypes.bool,\n  message: PropTypes.node.isRequired,\n}\n\nexport const canDeltaBackup = version => {\n  if (version === undefined) {\n    return false\n  }\n  const [major, minor] = version.split('.')\n  return +major > 6 || (+major === 6 && +minor >= 5)\n}\n","import _ from 'intl'\nimport Component from 'base-component'\nimport copy from 'copy-to-clipboard'\nimport decorate from 'apply-decorators'\nimport { get as getDefined } from '@xen-orchestra/defined'\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 { Host, Network, Pool, Sr, Vm } from 'render-xo-item'\nimport { SelectPool } from 'select-objects'\nimport { Container, Row, Col } from 'grid'\nimport { Card, CardHeader, CardBlock } from 'card'\nimport { FormattedRelative, FormattedTime } from 'react-intl'\nimport { countBy, filter, flatten, forEach, includes, isEmpty, keyBy, map, pick } from 'lodash'\nimport { addSubscriptions, connectStore, formatLogs, formatSize, noop, resolveIds } from 'utils'\nimport {\n  deleteMessage,\n  deleteMessages,\n  deleteOrphanedVdis,\n  deleteVbd,\n  deleteVbds,\n  deleteVdi,\n  deleteVm,\n  deleteVms,\n  isSrWritable,\n  subscribeSchedules,\n} from 'xo'\nimport {\n  areObjectsFetched,\n  createCollectionWrapper,\n  createFilter,\n  createGetObject,\n  createGetObjectsOfType,\n  createSelector,\n  createSort,\n} from 'selectors'\n\nimport UnhealthyVdis from './unhealthyVdis'\n\nconst SrColContainer = connectStore(() => ({\n  container: createGetObject(),\n}))(\n  ({ container }) =>\n    container !== undefined && <Link to={`${container.type}s/${container.id}`}>{container.name_label}</Link>\n)\n\nconst VmColContainer = connectStore(() => ({\n  container: createGetObject(),\n}))(({ container }) => <span>{container.name_label}</span>)\n\nconst AlarmColObject = connectStore(() => ({\n  object: createGetObject(),\n}))(({ object }) => {\n  if (!object) {\n    return null\n  }\n\n  switch (object.type) {\n    case 'VM':\n      return <Link to={`vms/${object.id}`}>{object.name_label}</Link>\n    case 'VM-controller':\n      return <Link to=