const pveOnlineHelpInfo = {
   "ceph_rados_block_devices" : {
      "link" : "/pve-docs/chapter-pvesm.html#ceph_rados_block_devices",
      "title" : "Ceph RADOS Block Devices (RBD)"
   },
   "chapter_ha_manager" : {
      "link" : "/pve-docs/chapter-ha-manager.html#chapter_ha_manager",
      "title" : "High Availability"
   },
   "chapter_lvm" : {
      "link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm",
      "title" : "Logical Volume Manager (LVM)"
   },
   "chapter_pct" : {
      "link" : "/pve-docs/chapter-pct.html#chapter_pct",
      "title" : "Proxmox Container Toolkit"
   },
   "chapter_pve_firewall" : {
      "link" : "/pve-docs/chapter-pve-firewall.html#chapter_pve_firewall",
      "title" : "Proxmox VE Firewall"
   },
   "chapter_pveceph" : {
      "link" : "/pve-docs/chapter-pveceph.html#chapter_pveceph",
      "title" : "Deploy Hyper-Converged Ceph Cluster"
   },
   "chapter_pvecm" : {
      "link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm",
      "title" : "Cluster Manager"
   },
   "chapter_pvesdn" : {
      "link" : "/pve-docs/chapter-pvesdn.html#chapter_pvesdn",
      "title" : "Software Defined Network"
   },
   "chapter_pvesr" : {
      "link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr",
      "title" : "Storage Replication"
   },
   "chapter_storage" : {
      "link" : "/pve-docs/chapter-pvesm.html#chapter_storage",
      "title" : "Proxmox VE Storage"
   },
   "chapter_system_administration" : {
      "link" : "/pve-docs/chapter-sysadmin.html#chapter_system_administration",
      "title" : "Host System Administration"
   },
   "chapter_user_management" : {
      "link" : "/pve-docs/chapter-pveum.html#chapter_user_management",
      "title" : "User Management"
   },
   "chapter_virtual_machines" : {
      "link" : "/pve-docs/chapter-qm.html#chapter_virtual_machines",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "chapter_vzdump" : {
      "link" : "/pve-docs/chapter-vzdump.html#chapter_vzdump",
      "title" : "Backup and Restore"
   },
   "chapter_zfs" : {
      "link" : "/pve-docs/chapter-sysadmin.html#chapter_zfs",
      "title" : "ZFS on Linux"
   },
   "datacenter_configuration_file" : {
      "link" : "/pve-docs/pve-admin-guide.html#datacenter_configuration_file",
      "title" : "Datacenter Configuration"
   },
   "external_metric_server" : {
      "link" : "/pve-docs/chapter-sysadmin.html#external_metric_server",
      "title" : "External Metric Server"
   },
   "getting_help" : {
      "link" : "/pve-docs/pve-admin-guide.html#getting_help",
      "title" : "Getting Help"
   },
   "gui_my_settings" : {
      "link" : "/pve-docs/chapter-pve-gui.html#gui_my_settings",
      "subtitle" : "My Settings",
      "title" : "Graphical User Interface"
   },
   "ha_manager_crs" : {
      "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_crs",
      "subtitle" : "Cluster Resource Scheduling",
      "title" : "High Availability"
   },
   "ha_manager_fencing" : {
      "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing",
      "subtitle" : "Fencing",
      "title" : "High Availability"
   },
   "ha_manager_groups" : {
      "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_groups",
      "subtitle" : "Groups",
      "title" : "High Availability"
   },
   "ha_manager_resource_config" : {
      "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resource_config",
      "subtitle" : "Resources",
      "title" : "High Availability"
   },
   "ha_manager_resources" : {
      "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resources",
      "subtitle" : "Resources",
      "title" : "High Availability"
   },
   "ha_manager_shutdown_policy" : {
      "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_shutdown_policy",
      "subtitle" : "Shutdown Policy",
      "title" : "High Availability"
   },
   "markdown_basics" : {
      "link" : "/pve-docs/pve-admin-guide.html#markdown_basics",
      "title" : "Markdown Primer"
   },
   "metric_server_graphite" : {
      "link" : "/pve-docs/chapter-sysadmin.html#metric_server_graphite",
      "subtitle" : "Graphite server configuration",
      "title" : "External Metric Server"
   },
   "metric_server_influxdb" : {
      "link" : "/pve-docs/chapter-sysadmin.html#metric_server_influxdb",
      "subtitle" : "Influxdb plugin configuration",
      "title" : "External Metric Server"
   },
   "pct_configuration" : {
      "link" : "/pve-docs/chapter-pct.html#pct_configuration",
      "subtitle" : "Configuration",
      "title" : "Proxmox Container Toolkit"
   },
   "pct_container_images" : {
      "link" : "/pve-docs/chapter-pct.html#pct_container_images",
      "subtitle" : "Container Images",
      "title" : "Proxmox Container Toolkit"
   },
   "pct_container_network" : {
      "link" : "/pve-docs/chapter-pct.html#pct_container_network",
      "subtitle" : "Network",
      "title" : "Proxmox Container Toolkit"
   },
   "pct_container_storage" : {
      "link" : "/pve-docs/chapter-pct.html#pct_container_storage",
      "subtitle" : "Container Storage",
      "title" : "Proxmox Container Toolkit"
   },
   "pct_cpu" : {
      "link" : "/pve-docs/chapter-pct.html#pct_cpu",
      "subtitle" : "CPU",
      "title" : "Proxmox Container Toolkit"
   },
   "pct_general" : {
      "link" : "/pve-docs/chapter-pct.html#pct_general",
      "subtitle" : "General Settings",
      "title" : "Proxmox Container Toolkit"
   },
   "pct_memory" : {
      "link" : "/pve-docs/chapter-pct.html#pct_memory",
      "subtitle" : "Memory",
      "title" : "Proxmox Container Toolkit"
   },
   "pct_migration" : {
      "link" : "/pve-docs/chapter-pct.html#pct_migration",
      "subtitle" : "Migration",
      "title" : "Proxmox Container Toolkit"
   },
   "pct_options" : {
      "link" : "/pve-docs/chapter-pct.html#pct_options",
      "subtitle" : "Options",
      "title" : "Proxmox Container Toolkit"
   },
   "pct_startup_and_shutdown" : {
      "link" : "/pve-docs/chapter-pct.html#pct_startup_and_shutdown",
      "subtitle" : "Automatic Start and Shutdown of Containers",
      "title" : "Proxmox Container Toolkit"
   },
   "proxmox_node_management" : {
      "link" : "/pve-docs/chapter-sysadmin.html#proxmox_node_management",
      "title" : "Proxmox Node Management"
   },
   "pve_admin_guide" : {
      "link" : "/pve-docs/pve-admin-guide.html",
      "title" : "Proxmox VE Administration Guide"
   },
   "pve_ceph_install" : {
      "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_install",
      "subtitle" : "CLI Installation of Ceph Packages",
      "title" : "Deploy Hyper-Converged Ceph Cluster"
   },
   "pve_ceph_osds" : {
      "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds",
      "subtitle" : "Ceph OSDs",
      "title" : "Deploy Hyper-Converged Ceph Cluster"
   },
   "pve_ceph_pools" : {
      "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools",
      "subtitle" : "Ceph Pools",
      "title" : "Deploy Hyper-Converged Ceph Cluster"
   },
   "pve_documentation_index" : {
      "link" : "/pve-docs/index.html",
      "title" : "Proxmox VE Documentation Index"
   },
   "pve_firewall_cluster_wide_setup" : {
      "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_cluster_wide_setup",
      "subtitle" : "Cluster Wide Setup",
      "title" : "Proxmox VE Firewall"
   },
   "pve_firewall_host_specific_configuration" : {
      "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_host_specific_configuration",
      "subtitle" : "Host Specific Configuration",
      "title" : "Proxmox VE Firewall"
   },
   "pve_firewall_ip_aliases" : {
      "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_aliases",
      "subtitle" : "IP Aliases",
      "title" : "Proxmox VE Firewall"
   },
   "pve_firewall_ip_sets" : {
      "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_sets",
      "subtitle" : "IP Sets",
      "title" : "Proxmox VE Firewall"
   },
   "pve_firewall_security_groups" : {
      "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_security_groups",
      "subtitle" : "Security Groups",
      "title" : "Proxmox VE Firewall"
   },
   "pve_firewall_vm_container_configuration" : {
      "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_vm_container_configuration",
      "subtitle" : "VM/Container Configuration",
      "title" : "Proxmox VE Firewall"
   },
   "pve_service_daemons" : {
      "link" : "/pve-docs/index.html#_service_daemons",
      "title" : "Service Daemons"
   },
   "pveceph_fs" : {
      "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs",
      "subtitle" : "CephFS",
      "title" : "Deploy Hyper-Converged Ceph Cluster"
   },
   "pveceph_fs_create" : {
      "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create",
      "subtitle" : "Create CephFS",
      "title" : "Deploy Hyper-Converged Ceph Cluster"
   },
   "pvecm_create_cluster" : {
      "link" : "/pve-docs/chapter-pvecm.html#pvecm_create_cluster",
      "subtitle" : "Create a Cluster",
      "title" : "Cluster Manager"
   },
   "pvecm_join_node_to_cluster" : {
      "link" : "/pve-docs/chapter-pvecm.html#pvecm_join_node_to_cluster",
      "subtitle" : "Adding Nodes to the Cluster",
      "title" : "Cluster Manager"
   },
   "pvesdn_config_controllers" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_controllers",
      "subtitle" : "Controllers",
      "title" : "Software Defined Network"
   },
   "pvesdn_config_vnet" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_vnet",
      "subtitle" : "VNets",
      "title" : "Software Defined Network"
   },
   "pvesdn_config_zone" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_zone",
      "subtitle" : "Zones",
      "title" : "Software Defined Network"
   },
   "pvesdn_controller_plugin_evpn" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_controller_plugin_evpn",
      "subtitle" : "EVPN Controller",
      "title" : "Software Defined Network"
   },
   "pvesdn_dns_plugin_powerdns" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_dns_plugin_powerdns",
      "subtitle" : "PowerDNS Plugin",
      "title" : "Software Defined Network"
   },
   "pvesdn_ipam_plugin_netbox" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_netbox",
      "subtitle" : "NetBox IPAM Plugin",
      "title" : "Software Defined Network"
   },
   "pvesdn_ipam_plugin_phpipam" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_phpipam",
      "subtitle" : "phpIPAM Plugin",
      "title" : "Software Defined Network"
   },
   "pvesdn_ipam_plugin_pveipam" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_pveipam",
      "subtitle" : "Proxmox VE IPAM Plugin",
      "title" : "Software Defined Network"
   },
   "pvesdn_zone_plugin_evpn" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_evpn",
      "subtitle" : "EVPN Zones",
      "title" : "Software Defined Network"
   },
   "pvesdn_zone_plugin_qinq" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_qinq",
      "subtitle" : "QinQ Zones",
      "title" : "Software Defined Network"
   },
   "pvesdn_zone_plugin_simple" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_simple",
      "subtitle" : "Simple Zones",
      "title" : "Software Defined Network"
   },
   "pvesdn_zone_plugin_vlan" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vlan",
      "subtitle" : "VLAN Zones",
      "title" : "Software Defined Network"
   },
   "pvesdn_zone_plugin_vxlan" : {
      "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vxlan",
      "subtitle" : "VXLAN Zones",
      "title" : "Software Defined Network"
   },
   "pvesr_schedule_time_format" : {
      "link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format",
      "subtitle" : "Schedule Format",
      "title" : "Storage Replication"
   },
   "pveum_authentication_realms" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_authentication_realms",
      "subtitle" : "Authentication Realms",
      "title" : "User Management"
   },
   "pveum_configure_u2f" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_configure_u2f",
      "subtitle" : "Server Side U2F Configuration",
      "title" : "User Management"
   },
   "pveum_configure_webauthn" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_configure_webauthn",
      "subtitle" : "Server Side Webauthn Configuration",
      "title" : "User Management"
   },
   "pveum_groups" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_groups",
      "subtitle" : "Groups",
      "title" : "User Management"
   },
   "pveum_ldap_sync" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_ldap_sync",
      "subtitle" : "Syncing LDAP-Based Realms",
      "title" : "User Management"
   },
   "pveum_permission_management" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_permission_management",
      "subtitle" : "Permission Management",
      "title" : "User Management"
   },
   "pveum_pools" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_pools",
      "subtitle" : "Pools",
      "title" : "User Management"
   },
   "pveum_roles" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_roles",
      "subtitle" : "Roles",
      "title" : "User Management"
   },
   "pveum_tokens" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_tokens",
      "subtitle" : "API Tokens",
      "title" : "User Management"
   },
   "pveum_users" : {
      "link" : "/pve-docs/chapter-pveum.html#pveum_users",
      "subtitle" : "Users",
      "title" : "User Management"
   },
   "qm_bios_and_uefi" : {
      "link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi",
      "subtitle" : "BIOS and UEFI",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_bootorder" : {
      "link" : "/pve-docs/chapter-qm.html#qm_bootorder",
      "subtitle" : "Device Boot Order",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_cloud_init" : {
      "link" : "/pve-docs/chapter-qm.html#qm_cloud_init",
      "title" : "Cloud-Init Support"
   },
   "qm_copy_and_clone" : {
      "link" : "/pve-docs/chapter-qm.html#qm_copy_and_clone",
      "subtitle" : "Copies and Clones",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_cpu" : {
      "link" : "/pve-docs/chapter-qm.html#qm_cpu",
      "subtitle" : "CPU",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_display" : {
      "link" : "/pve-docs/chapter-qm.html#qm_display",
      "subtitle" : "Display",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_general_settings" : {
      "link" : "/pve-docs/chapter-qm.html#qm_general_settings",
      "subtitle" : "General Settings",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_hard_disk" : {
      "link" : "/pve-docs/chapter-qm.html#qm_hard_disk",
      "subtitle" : "Hard Disk",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_memory" : {
      "link" : "/pve-docs/chapter-qm.html#qm_memory",
      "subtitle" : "Memory",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_migration" : {
      "link" : "/pve-docs/chapter-qm.html#qm_migration",
      "subtitle" : "Migration",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_network_device" : {
      "link" : "/pve-docs/chapter-qm.html#qm_network_device",
      "subtitle" : "Network Device",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_options" : {
      "link" : "/pve-docs/chapter-qm.html#qm_options",
      "subtitle" : "Options",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_os_settings" : {
      "link" : "/pve-docs/chapter-qm.html#qm_os_settings",
      "subtitle" : "OS Settings",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_pci_passthrough_vm_config" : {
      "link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough_vm_config",
      "subtitle" : "VM Configuration",
      "title" : "PCI(e) Passthrough"
   },
   "qm_qemu_agent" : {
      "link" : "/pve-docs/chapter-qm.html#qm_qemu_agent",
      "subtitle" : "QEMU Guest Agent",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_spice_enhancements" : {
      "link" : "/pve-docs/chapter-qm.html#qm_spice_enhancements",
      "subtitle" : "SPICE Enhancements",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_startup_and_shutdown" : {
      "link" : "/pve-docs/chapter-qm.html#qm_startup_and_shutdown",
      "subtitle" : "Automatic Start and Shutdown of Virtual Machines",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_system_settings" : {
      "link" : "/pve-docs/chapter-qm.html#qm_system_settings",
      "subtitle" : "System Settings",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_usb_passthrough" : {
      "link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough",
      "subtitle" : "USB Passthrough",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_virtio_rng" : {
      "link" : "/pve-docs/chapter-qm.html#qm_virtio_rng",
      "subtitle" : "VirtIO RNG",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "qm_virtual_machines_settings" : {
      "link" : "/pve-docs/chapter-qm.html#qm_virtual_machines_settings",
      "subtitle" : "Virtual Machines Settings",
      "title" : "QEMU/KVM Virtual Machines"
   },
   "storage_btrfs" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_btrfs",
      "title" : "BTRFS Backend"
   },
   "storage_cephfs" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_cephfs",
      "title" : "Ceph Filesystem (CephFS)"
   },
   "storage_cifs" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_cifs",
      "title" : "CIFS Backend"
   },
   "storage_directory" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_directory",
      "title" : "Directory Backend"
   },
   "storage_glusterfs" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_glusterfs",
      "title" : "GlusterFS Backend"
   },
   "storage_lvm" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_lvm",
      "title" : "LVM Backend"
   },
   "storage_lvmthin" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_lvmthin",
      "title" : "LVM thin Backend"
   },
   "storage_nfs" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_nfs",
      "title" : "NFS Backend"
   },
   "storage_open_iscsi" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_open_iscsi",
      "title" : "Open-iSCSI initiator"
   },
   "storage_pbs" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_pbs",
      "title" : "Proxmox Backup Server"
   },
   "storage_pbs_encryption" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_pbs_encryption",
      "subtitle" : "Encryption",
      "title" : "Proxmox Backup Server"
   },
   "storage_zfspool" : {
      "link" : "/pve-docs/chapter-pvesm.html#storage_zfspool",
      "title" : "Local ZFS Pool Backend"
   },
   "sysadmin_certificate_management" : {
      "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certificate_management",
      "title" : "Certificate Management"
   },
   "sysadmin_certs_acme_plugins" : {
      "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certs_acme_plugins",
      "subtitle" : "ACME Plugins",
      "title" : "Certificate Management"
   },
   "sysadmin_network_configuration" : {
      "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration",
      "title" : "Network Configuration"
   },
   "sysadmin_package_repositories" : {
      "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_package_repositories",
      "title" : "Package Repositories"
   },
   "user-realms-ldap" : {
      "link" : "/pve-docs/chapter-pveum.html#user-realms-ldap",
      "subtitle" : "LDAP",
      "title" : "User Management"
   },
   "user_mgmt" : {
      "link" : "/pve-docs/chapter-pveum.html#user_mgmt",
      "title" : "User Management"
   },
   "vzdump_retention" : {
      "link" : "/pve-docs/chapter-vzdump.html#vzdump_retention",
      "subtitle" : "Backup Retention",
      "title" : "Backup and Restore"
   }
};
// Some configuration values are complex strings - so we need parsers/generators for them.
Ext.define('PVE.Parser', {
 statics: {
    // this class only contains static functions
    printACME: function(value) {
	if (Ext.isArray(value.domains)) {
	    value.domains = value.domains.join(';');
	}
	return PVE.Parser.printPropertyString(value);
    },
    parseACME: function(value) {
	if (!value) {
	    return {};
	}
	let res = {};
	try {
	    value.split(',').forEach(property => {
		let [k, v] = property.split('=', 2);
		if (Ext.isDefined(v)) {
		    res[k] = v;
		} else {
		    throw `Failed to parse key-value pair: ${property}`;
		}
	    });
	} catch (err) {
	    console.warn(err);
	    return undefined;
	}
	if (res.domains !== undefined) {
	    res.domains = res.domains.split(/;/);
	}
	return res;
    },
    parseBoolean: function(value, default_value) {
	if (!Ext.isDefined(value)) {
	    return default_value;
	}
	value = value.toLowerCase();
	return value === '1' ||
	       value === 'on' ||
	       value === 'yes' ||
	       value === 'true';
    },
    parsePropertyString: function(value, defaultKey) {
	let res = {};
	if (typeof value !== 'string' || value === '') {
	    return res;
	}
	try {
	    value.split(',').forEach(property => {
		let [k, v] = property.split('=', 2);
		if (Ext.isDefined(v)) {
		    res[k] = v;
		} else if (Ext.isDefined(defaultKey)) {
		    if (Ext.isDefined(res[defaultKey])) {
			throw 'defaultKey may be only defined once in propertyString';
		    }
		    res[defaultKey] = k; // k ist the value in this case
		} else {
		    throw `Failed to parse key-value pair: ${property}`;
		}
	    });
	} catch (err) {
	    console.warn(err);
	    return undefined;
	}
	return res;
    },
    printPropertyString: function(data, defaultKey) {
	var stringparts = [],
	    gotDefaultKeyVal = false,
	    defaultKeyVal;
	Ext.Object.each(data, function(key, value) {
	    if (defaultKey !== undefined && key === defaultKey) {
		gotDefaultKeyVal = true;
		defaultKeyVal = value;
	    } else if (value !== '') {
		stringparts.push(key + '=' + value);
	    }
	});
	stringparts = stringparts.sort();
	if (gotDefaultKeyVal) {
	    stringparts.unshift(defaultKeyVal);
	}
	return stringparts.join(',');
    },
    parseQemuNetwork: function(key, value) {
	if (!(key && value)) {
	    return undefined;
	}
	let res = {},
	    errors = false;
	Ext.Array.each(value.split(','), function(p) {
	    if (!p || p.match(/^\s*$/)) {
		return undefined; // continue
	    }
	    let match_res;
	    if ((match_res = p.match(/^(ne2k_pci|e1000|e1000-82540em|e1000-82544gc|e1000-82545em|vmxnet3|rtl8139|pcnet|virtio|ne2k_isa|i82551|i82557b|i82559er)(=([0-9a-f]{2}(:[0-9a-f]{2}){5}))?$/i)) !== null) {
		res.model = match_res[1].toLowerCase();
		if (match_res[3]) {
		    res.macaddr = match_res[3];
		}
	    } else if ((match_res = p.match(/^bridge=(\S+)$/)) !== null) {
		res.bridge = match_res[1];
	    } else if ((match_res = p.match(/^rate=(\d+(\.\d+)?)$/)) !== null) {
		res.rate = match_res[1];
	    } else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) {
		res.tag = match_res[1];
	    } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
		res.firewall = match_res[1];
	    } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
		res.disconnect = match_res[1];
	    } else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) {
		res.queues = match_res[1];
	    } else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) {
		res.trunks = match_res[1];
	    } else if ((match_res = p.match(/^mtu=(\d+)$/)) !== null) {
		res.mtu = match_res[1];
	    } else {
		errors = true;
		return false; // break
	    }
	    return undefined; // continue
	});
	if (errors || !res.model) {
	    return undefined;
	}
	return res;
    },
    printQemuNetwork: function(net) {
	var netstr = net.model;
	if (net.macaddr) {
	    netstr += "=" + net.macaddr;
	}
	if (net.bridge) {
	    netstr += ",bridge=" + net.bridge;
	    if (net.tag) {
		netstr += ",tag=" + net.tag;
	    }
	    if (net.firewall) {
		netstr += ",firewall=" + net.firewall;
	    }
	}
	if (net.rate) {
	    netstr += ",rate=" + net.rate;
	}
	if (net.queues) {
	    netstr += ",queues=" + net.queues;
	}
	if (net.disconnect) {
	    netstr += ",link_down=" + net.disconnect;
	}
	if (net.trunks) {
	    netstr += ",trunks=" + net.trunks;
	}
	if (net.mtu) {
	    netstr += ",mtu=" + net.mtu;
	}
	return netstr;
    },
    parseQemuDrive: function(key, value) {
	if (!(key && value)) {
	    return undefined;
	}
	const [, bus, index] = key.match(/^([a-z]+)(\d+)$/);
	if (!bus) {
	    return undefined;
	}
	let res = {
	    'interface': bus,
	    index,
	};
	var errors = false;
	Ext.Array.each(value.split(','), function(p) {
	    if (!p || p.match(/^\s*$/)) {
		return undefined; // continue
	    }
	    let match = p.match(/^([a-z_]+)=(\S+)$/);
	    if (!match) {
		if (!p.match(/[=]/)) {
		    res.file = p;
		    return undefined; // continue
		}
		errors = true;
		return false; // break
	    }
	    let [, k, v] = match;
	    if (k === 'volume') {
		k = 'file';
	    }
	    if (Ext.isDefined(res[k])) {
		errors = true;
		return false; // break
	    }
	    if (k === 'cache' && v === 'off') {
		v = 'none';
	    }
	    res[k] = v;
	    return undefined; // continue
	});
	if (errors || !res.file) {
	    return undefined;
	}
	return res;
    },
    printQemuDrive: function(drive) {
	var drivestr = drive.file;
	Ext.Object.each(drive, function(key, value) {
	    if (!Ext.isDefined(value) || key === 'file' ||
		key === 'index' || key === 'interface') {
		return; // continue
	    }
	    drivestr += ',' + key + '=' + value;
	});
	return drivestr;
    },
    parseIPConfig: function(key, value) {
	if (!(key && value)) {
	    return undefined; // continue
	}
	let res = {};
	try {
	    value.split(',').forEach(p => {
		if (!p || p.match(/^\s*$/)) {
		    return; // continue
		}
		const match = p.match(/^(ip|gw|ip6|gw6)=(\S+)$/);
		if (!match) {
		    throw `could not parse as IP config: ${p}`;
		}
		let [, k, v] = match;
		res[k] = v;
	    });
	} catch (err) {
	    console.warn(err);
	    return undefined; // continue
	}
	return res;
    },
    printIPConfig: function(cfg) {
	return Object.entries(cfg)
	    .filter(([k, v]) => v && k.match(/^(ip|gw|ip6|gw6)$/))
	    .map(([k, v]) => `${k}=${v}`)
	    .join(',');
    },
    parseLxcNetwork: function(value) {
	if (!value) {
	    return undefined;
	}
	let data = {};
	value.split(',').forEach(p => {
	    if (!p || p.match(/^\s*$/)) {
		return; // continue
	    }
	    let match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/);
	    if (match_res) {
		data[match_res[1]] = match_res[2];
	    } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
		data.firewall = PVE.Parser.parseBoolean(match_res[1]);
	    } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
		data.link_down = PVE.Parser.parseBoolean(match_res[1]);
	    } else if (!p.match(/^type=\S+$/)) {
		console.warn(`could not parse LXC network string ${p}`);
	    }
	});
	return data;
    },
    printLxcNetwork: function(config) {
	let knownKeys = {
	    bridge: 1,
	    firewall: 1,
	    gw6: 1,
	    gw: 1,
	    hwaddr: 1,
	    ip6: 1,
	    ip: 1,
	    mtu: 1,
	    name: 1,
	    rate: 1,
	    tag: 1,
	    link_down: 1,
	};
	return Object.entries(config)
	    .filter(([k, v]) => v !== undefined && v !== '' && knownKeys[k])
	    .map(([k, v]) => `${k}=${v}`)
	    .join(',');
    },
    parseLxcMountPoint: function(value) {
	if (!value) {
	    return undefined;
	}
	let res = {};
	let errors = false;
	Ext.Array.each(value.split(','), function(p) {
	    if (!p || p.match(/^\s*$/)) {
		return undefined; // continue
	    }
	    let match = p.match(/^([a-z_]+)=(.+)$/);
	    if (!match) {
		if (!p.match(/[=]/)) {
		    res.file = p;
		    return undefined; // continue
		}
		errors = true;
		return false; // break
	    }
	    let [, k, v] = match;
	    if (k === 'volume') {
		k = 'file';
	    }
	    if (Ext.isDefined(res[k])) {
		errors = true;
		return false; // break
	    }
	    res[k] = v;
	    return undefined;
	});
	if (errors || !res.file) {
	    return undefined;
	}
	const match = res.file.match(/^([a-z][a-z0-9\-_.]*[a-z0-9]):/i);
	if (match) {
	    res.storage = match[1];
	    res.type = 'volume';
	} else if (res.file.match(/^\/dev\//)) {
	    res.type = 'device';
	} else {
	    res.type = 'bind';
	}
	return res;
    },
    printLxcMountPoint: function(mp) {
	let drivestr = mp.file;
	for (const [key, value] of Object.entries(mp)) {
	    if (!Ext.isDefined(value) || key === 'file' || key === 'type' || key === 'storage') {
		continue;
	    }
	    drivestr += `,${key}=${value}`;
	}
	return drivestr;
    },
    parseStartup: function(value) {
	if (value === undefined) {
	    return undefined;
	}
	let res = {};
	try {
	    value.split(',').forEach(p => {
		if (!p || p.match(/^\s*$/)) {
		    return; // continue
		}
		let match_res;
		if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) {
		    res.order = match_res[2];
		} else if ((match_res = p.match(/^up=(\d+)$/)) !== null) {
		    res.up = match_res[1];
		} else if ((match_res = p.match(/^down=(\d+)$/)) !== null) {
		    res.down = match_res[1];
		} else {
		    throw `could not parse startup config ${p}`;
		}
	    });
	} catch (err) {
	    console.warn(err);
	    return undefined;
	}
	return res;
    },
    printStartup: function(startup) {
	let arr = [];
	if (startup.order !== undefined && startup.order !== '') {
	    arr.push('order=' + startup.order);
	}
	if (startup.up !== undefined && startup.up !== '') {
	    arr.push('up=' + startup.up);
	}
	if (startup.down !== undefined && startup.down !== '') {
	    arr.push('down=' + startup.down);
	}
	return arr.join(',');
    },
    parseQemuSmbios1: function(value) {
	let res = value.split(',').reduce((acc, currentValue) => {
	    const [k, v] = currentValue.split(/[=](.+)/);
	    acc[k] = v;
	    return acc;
	}, {});
	if (PVE.Parser.parseBoolean(res.base64, false)) {
	    for (const [k, v] of Object.entries(res)) {
		if (k !== 'uuid') {
		    res[k] = Ext.util.Base64.decode(v);
		}
	    }
	}
	return res;
    },
    printQemuSmbios1: function(data) {
	let base64 = false;
	let datastr = Object.entries(data)
	    .map(([key, value]) => {
		if (value === '') {
		    return undefined;
		}
		if (key !== 'uuid') {
		    base64 = true; // smbios values can be arbitrary, so encode and mark config as such
		    value = Ext.util.Base64.encode(value);
		}
		return `${key}=${value}`;
	    })
	    .filter(v => v !== undefined)
	    .join(',');
	if (base64) {
	    datastr += ',base64=1';
	}
	return datastr;
    },
    parseTfaConfig: function(value) {
	let res = {};
	value.split(',').forEach(p => {
	    const [k, v] = p.split('=', 2);
	    res[k] = v;
	});
	return res;
    },
    parseTfaType: function(value) {
	let match;
	if (!value || !value.length) {
	    return undefined;
	} else if (value === 'x!oath') {
	    return 'totp';
	} else if ((match = value.match(/^x!(.+)$/)) !== null) {
	    return match[1];
	} else {
	    return 1;
	}
    },
    parseQemuCpu: function(value) {
	if (!value) {
	    return {};
	}
	let res = {};
	let errors = false;
	Ext.Array.each(value.split(','), function(p) {
	    if (!p || p.match(/^\s*$/)) {
		return undefined; // continue
	    }
	    if (!p.match(/[=]/)) {
		if (Ext.isDefined(res.cpu)) {
		    errors = true;
		    return false; // break
		}
		res.cputype = p;
		return undefined; // continue
	    }
	    let match = p.match(/^([a-z_]+)=(\S+)$/);
	    if (!match || Ext.isDefined(res[match[1]])) {
		errors = true;
		return false; // break
	    }
	    let [, k, v] = match;
	    res[k] = v;
	    return undefined;
	});
	if (errors || !res.cputype) {
	    return undefined;
	}
	return res;
    },
    printQemuCpu: function(cpu) {
	let cpustr = cpu.cputype;
	let optstr = '';
	Ext.Object.each(cpu, function(key, value) {
	    if (!Ext.isDefined(value) || key === 'cputype') {
		return; // continue
	    }
	    optstr += ',' + key + '=' + value;
	});
	if (!cpustr) {
	    if (optstr) {
		return 'kvm64' + optstr;
	    } else {
		return undefined;
	    }
	}
	return cpustr + optstr;
    },
    parseSSHKey: function(key) {
	//                |--- options can have quotes--|     type    key        comment
	let keyre = /^(?:((?:[^\s"]|"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/;
	let typere = /^(?:(?:sk-)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)(?:@(?:[a-z0-9_-]+\.)+[a-z]{2,})?)$/;
	let m = key.match(keyre);
	if (!m || m.length < 3 || !m[2]) { // [2] is always either type or key
	    return null;
	}
	if (m[1] && m[1].match(typere)) {
	    return {
		type: m[1],
		key: m[2],
		comment: m[3],
	    };
	}
	if (m[2].match(typere)) {
	    return {
		options: m[1],
		type: m[2],
		key: m[3],
		comment: m[4],
	    };
	}
	return null;
    },
    parseACMEPluginData: function(data) {
	let res = {};
	let extradata = [];
	data.split('\n').forEach((line) => {
	    // capture everything after the first = as value
	    let [key, value] = line.split(/[=](.+)/);
	    if (value !== undefined) {
		res[key] = value;
	    } else {
		extradata.push(line);
	    }
	});
	return [res, extradata];
    },
},
});
/* This state provider keeps part of the state inside the browser history.
 *
 * We compress (shorten) url using dictionary based compression, i.e., we use
 * column separated list instead of url encoded hash:
 *  #v\d*       version/format
 *  :=          indicates string values
 *  :\d+        lookup value in dictionary hash
 *  #v1:=value1:5:=value2:=value3:...
*/
Ext.define('PVE.StateProvider', {
    extend: 'Ext.state.LocalStorageProvider',
    // private
    setHV: function(name, newvalue, fireEvents) {
	let me = this;
	let changes = false;
	let oldtext = Ext.encode(me.UIState[name]);
	let newtext = Ext.encode(newvalue);
	if (newtext !== oldtext) {
	    changes = true;
	    me.UIState[name] = newvalue;
	    if (fireEvents) {
		me.fireEvent("statechange", me, name, { value: newvalue });
	    }
	}
	return changes;
    },
    // private
    hslist: [
	// order is important for notifications
	// [ name, default ]
	['view', 'server'],
	['rid', 'root'],
	['ltab', 'tasks'],
	['nodetab', ''],
	['storagetab', ''],
	['sdntab', ''],
	['pooltab', ''],
	['kvmtab', ''],
	['lxctab', ''],
	['dctab', ''],
    ],
    hprefix: 'v1',
    compDict: {
        tfa: 54,
	sdn: 53,
	cloudinit: 52,
	replication: 51,
	system: 50,
	monitor: 49,
	'ha-fencing': 48,
	'ha-groups': 47,
	'ha-resources': 46,
	'ceph-log': 45,
	'ceph-crushmap': 44,
	'ceph-pools': 43,
	'ceph-osdtree': 42,
	'ceph-disklist': 41,
	'ceph-monlist': 40,
	'ceph-config': 39,
	ceph: 38,
	'firewall-fwlog': 37,
	'firewall-options': 36,
	'firewall-ipset': 35,
	'firewall-aliases': 34,
	'firewall-sg': 33,
	firewall: 32,
	apt: 31,
	members: 30,
	snapshot: 29,
	ha: 28,
	support: 27,
	pools: 26,
	syslog: 25,
	ubc: 24,
	initlog: 23,
	openvz: 22,
	backup: 21,
	resources: 20,
	content: 19,
	root: 18,
	domains: 17,
	roles: 16,
	groups: 15,
	users: 14,
	time: 13,
	dns: 12,
	network: 11,
	services: 10,
	options: 9,
	console: 8,
	hardware: 7,
	permissions: 6,
	summary: 5,
	tasks: 4,
	clog: 3,
	storage: 2,
	folder: 1,
	server: 0,
    },
    decodeHToken: function(token) {
	let me = this;
	let state = {};
	if (!token) {
	    me.hslist.forEach(([k, v]) => { state[k] = v; });
	    return state;
	}
	let [prefix, ...items] = token.split(':');
	if (prefix !== me.hprefix) {
	    return me.decodeHToken();
	}
	Ext.Array.each(me.hslist, function(rec) {
	    let value = items.shift();
	    if (value) {
		if (value[0] === '=') {
		    value = decodeURIComponent(value.slice(1));
		}
		for (const [key, hash] of Object.entries(me.compDict)) {
		    if (String(value) === String(hash)) {
			value = key;
			break;
		    }
		}
	    }
	    state[rec[0]] = value;
	});
	return state;
    },
    encodeHToken: function(state) {
	let me = this;
	let ctoken = me.hprefix;
	Ext.Array.each(me.hslist, function(rec) {
	    let value = state[rec[0]];
	    if (!Ext.isDefined(value)) {
		value = rec[1];
	    }
	    value = encodeURIComponent(value);
	    if (!value) {
		ctoken += ':';
	    } else if (Ext.isDefined(me.compDict[value])) {
		ctoken += ":" + me.compDict[value];
	    } else {
		ctoken += ":=" + value;
	    }
	});
	return ctoken;
    },
    constructor: function(config) {
	let me = this;
	me.callParent([config]);
	me.UIState = me.decodeHToken(); // set default
	let history_change_cb = function(token) {
	    if (!token) {
		Ext.History.back();
		return;
	    }
	    let newstate = me.decodeHToken(token);
	    Ext.Array.each(me.hslist, function(rec) {
		if (typeof newstate[rec[0]] === "undefined") {
		    return;
		}
		me.setHV(rec[0], newstate[rec[0]], true);
	    });
	};
	let start_token = Ext.History.getToken();
	if (start_token) {
	    history_change_cb(start_token);
	} else {
	    let htext = me.encodeHToken(me.UIState);
	    Ext.History.add(htext);
	}
	Ext.History.on('change', history_change_cb);
    },
    get: function(name, defaultValue) {
	let me = this;
	let data;
	if (typeof me.UIState[name] !== "undefined") {
	    data = { value: me.UIState[name] };
	} else {
	    data = me.callParent(arguments);
	    if (!data && name === 'GuiCap') {
		data = {
		    vms: {},
		    storage: {},
		    access: {},
		    nodes: {},
		    dc: {},
		    sdn: {},
		};
	    }
	}
	return data;
    },
    clear: function(name) {
	let me = this;
	if (typeof me.UIState[name] !== "undefined") {
	    me.UIState[name] = null;
	}
	me.callParent(arguments);
    },
    set: function(name, value, fireevent) {
        let me = this;
	if (typeof me.UIState[name] !== "undefined") {
	    var newvalue = value ? value.value : null;
	    if (me.setHV(name, newvalue, fireevent)) {
		let htext = me.encodeHToken(me.UIState);
		Ext.History.add(htext);
	    }
	} else {
	    me.callParent(arguments);
	}
    },
});
Ext.ns('PVE');
console.log("Starting Proxmox VE Manager");
Ext.Ajax.defaultHeaders = {
    'Accept': 'application/json',
};
Ext.define('PVE.Utils', {
 utilities: {
    // this singleton contains miscellaneous utilities
    toolkit: undefined, // (extjs|touch), set inside Toolkit.js
    bus_match: /^(ide|sata|virtio|scsi)(\d+)$/,
    log_severity_hash: {
	0: "panic",
	1: "alert",
	2: "critical",
	3: "error",
	4: "warning",
	5: "notice",
	6: "info",
	7: "debug",
    },
    support_level_hash: {
	'c': gettext('Community'),
	'b': gettext('Basic'),
	's': gettext('Standard'),
	'p': gettext('Premium'),
    },
    noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit '
      +''
      +'www.proxmox.com to get a list of available options.',
    kvm_ostypes: {
	'Linux': [
	    { desc: '6.x - 2.6 Kernel', val: 'l26' },
	    { desc: '2.4 Kernel', val: 'l24' },
	],
	'Microsoft Windows': [
	    { desc: '11/2022', val: 'win11' },
	    { desc: '10/2016/2019', val: 'win10' },
	    { desc: '8.x/2012/2012r2', val: 'win8' },
	    { desc: '7/2008r2', val: 'win7' },
	    { desc: 'Vista/2008', val: 'w2k8' },
	    { desc: 'XP/2003', val: 'wxp' },
	    { desc: '2000', val: 'w2k' },
	],
	'Solaris Kernel': [
	    { desc: '-', val: 'solaris' },
	],
	'Other': [
	    { desc: '-', val: 'other' },
	],
    },
    is_windows: function(ostype) {
	for (let entry of PVE.Utils.kvm_ostypes['Microsoft Windows']) {
	    if (entry.val === ostype) {
		return true;
	    }
	}
	return false;
    },
    get_health_icon: function(state, circle) {
	if (circle === undefined) {
	    circle = false;
	}
	if (state === undefined) {
	    state = 'uknown';
	}
	var icon = 'faded fa-question';
	switch (state) {
	    case 'good':
		icon = 'good fa-check';
		break;
	    case 'upgrade':
		icon = 'warning fa-upload';
		break;
	    case 'old':
		icon = 'warning fa-refresh';
		break;
	    case 'warning':
		icon = 'warning fa-exclamation';
		break;
	    case 'critical':
		icon = 'critical fa-times';
		break;
	    default: break;
	}
	if (circle) {
	    icon += '-circle';
	}
	return icon;
    },
    parse_ceph_version: function(service) {
	if (service.ceph_version_short) {
	    return service.ceph_version_short;
	}
	if (service.ceph_version) {
	    var match = service.ceph_version.match(/version (\d+(\.\d+)*)/);
	    if (match) {
		return match[1];
	    }
	}
	return undefined;
    },
    compare_ceph_versions: function(a, b) {
	let avers = [];
	let bvers = [];
	if (a === b) {
	    return 0;
	}
	if (Ext.isArray(a)) {
	    avers = a.slice(); // copy array
	} else {
	    avers = a.toString().split('.');
	}
	if (Ext.isArray(b)) {
	    bvers = b.slice(); // copy array
	} else {
	    bvers = b.toString().split('.');
	}
	for (;;) {
	    let av = avers.shift();
	    let bv = bvers.shift();
	    if (av === undefined && bv === undefined) {
		return 0;
	    } else if (av === undefined) {
		return -1;
	    } else if (bv === undefined) {
		return 1;
	    } else {
		let diff = parseInt(av, 10) - parseInt(bv, 10);
		if (diff !== 0) return diff;
		// else we need to look at the next parts
	    }
	}
    },
    get_ceph_icon_html: function(health, fw) {
	var state = PVE.Utils.map_ceph_health[health];
	var cls = PVE.Utils.get_health_icon(state);
	if (fw) {
	    cls += ' fa-fw';
	}
	return " ";
    },
    map_ceph_health: {
	'HEALTH_OK': 'good',
	'HEALTH_UPGRADE': 'upgrade',
	'HEALTH_OLD': 'old',
	'HEALTH_WARN': 'warning',
	'HEALTH_ERR': 'critical',
    },
    render_sdn_pending: function(rec, value, key, index) {
	if (rec.data.state === undefined || rec.data.state === null) {
	    return value;
	}
	if (rec.data.state === 'deleted') {
	    if (value === undefined) {
		return ' ';
	    } else {
		return '
'+ value +'
';
	    }
	} else if (rec.data.pending[key] !== undefined && rec.data.pending[key] !== null) {
	    if (rec.data.pending[key] === 'deleted') {
		return ' ';
	    } else {
		return rec.data.pending[key];
	    }
	}
	return value;
    },
    render_sdn_pending_state: function(rec, value) {
	if (value === undefined || value === null) {
	    return ' ';
	}
	let icon = ``;
	if (value === 'deleted') {
	    return '' + icon + value + '';
	}
	let tip = gettext('Pending Changes') + ': 
';
	for (const [key, keyvalue] of Object.entries(rec.data.pending)) {
	    if ((rec.data[key] !== undefined && rec.data.pending[key] !== rec.data[key]) ||
		rec.data[key] === undefined
	    ) {
		tip += `${key}: ${keyvalue} 
`;
	    }
	}
	return ''+ icon + value + '';
    },
    render_ceph_health: function(healthObj) {
	var state = {
	    iconCls: PVE.Utils.get_health_icon(),
	    text: '',
	};
	if (!healthObj || !healthObj.status) {
	    return state;
	}
	var health = PVE.Utils.map_ceph_health[healthObj.status];
	state.iconCls = PVE.Utils.get_health_icon(health, true);
	state.text = healthObj.status;
	return state;
    },
    render_zfs_health: function(value) {
	if (typeof value === 'undefined') {
	    return "";
	}
	var iconCls = 'question-circle';
	switch (value) {
	    case 'AVAIL':
	    case 'ONLINE':
		iconCls = 'check-circle good';
		break;
	    case 'REMOVED':
	    case 'DEGRADED':
		iconCls = 'exclamation-circle warning';
		break;
	    case 'UNAVAIL':
	    case 'FAULTED':
	    case 'OFFLINE':
		iconCls = 'times-circle critical';
		break;
	    default: //unknown
	}
	return ' ' + value;
    },
    render_pbs_fingerprint: fp => fp.substring(0, 23),
    render_backup_encryption: function(v, meta, record) {
	if (!v) {
	    return gettext('No');
	}
	let tip = '';
	if (v.match(/^[a-fA-F0-9]{2}:/)) { // fingerprint
	    tip = `Key fingerprint ${PVE.Utils.render_pbs_fingerprint(v)}`;
	}
	let icon = ``;
	return `${icon} ${gettext('Encrypted')}`;
    },
    render_backup_verification: function(v, meta, record) {
	let i = (cls, txt) => ` ${txt}`;
	if (v === undefined || v === null) {
	    return i('question-circle-o warning', gettext('None'));
	}
	let tip = "";
	let txt = gettext('Failed');
	let iconCls = 'times critical';
	if (v.state === 'ok') {
	    txt = gettext('OK');
	    iconCls = 'check good';
	    let now = Date.now() / 1000;
	    let task = Proxmox.Utils.parse_task_upid(v.upid);
	    let verify_time = Proxmox.Utils.render_timestamp(task.starttime);
	    tip = `Last verify task started on ${verify_time}`;
	    if (now - v.starttime > 30 * 24 * 60 * 60) {
		tip = `Last verify task over 30 days ago: ${verify_time}`;
		iconCls = 'check warning';
	    }
	}
	return ` ${i(iconCls, txt)} `;
    },
    render_backup_status: function(value, meta, record) {
	if (typeof value === 'undefined') {
	    return "";
	}
	let iconCls = 'check-circle good';
	let text = gettext('Yes');
	if (!PVE.Parser.parseBoolean(value.toString())) {
	    iconCls = 'times-circle critical';
	    text = gettext('No');
	    let reason = record.get('reason');
	    if (typeof reason !== 'undefined') {
		if (reason in PVE.Utils.backup_reasons_table) {
		    reason = PVE.Utils.backup_reasons_table[record.get('reason')];
		}
		text = `${text} - ${reason}`;
	    }
	}
	return ` ${text}`;
    },
    render_backup_days_of_week: function(val) {
	var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
	var selected = [];
	var cur = -1;
	val.split(',').forEach(function(day) {
	    cur++;
	    var dow = (dows.indexOf(day)+6)%7;
	    if (cur === dow) {
		if (selected.length === 0 || selected[selected.length-1] === 0) {
		    selected.push(1);
		} else {
		    selected[selected.length-1]++;
		}
	    } else {
		while (cur < dow) {
		    cur++;
		    selected.push(0);
		}
		selected.push(1);
	    }
	});
	cur = -1;
	var days = [];
	selected.forEach(function(item) {
	    cur++;
	    if (item > 2) {
		days.push(Ext.Date.dayNames[cur+1] + '-' + Ext.Date.dayNames[(cur+item)%7]);
		cur += item-1;
	    } else if (item === 2) {
		days.push(Ext.Date.dayNames[cur+1]);
		days.push(Ext.Date.dayNames[(cur+2)%7]);
		cur++;
	    } else if (item === 1) {
		days.push(Ext.Date.dayNames[(cur+1)%7]);
	    }
	});
	return days.join(', ');
    },
    render_backup_selection: function(value, metaData, record) {
	let allExceptText = gettext('All except {0}');
	let allText = '-- ' + gettext('All') + ' --';
	if (record.data.all) {
	    if (record.data.exclude) {
		return Ext.String.format(allExceptText, record.data.exclude);
	    }
	    return allText;
	}
	if (record.data.vmid) {
	    return record.data.vmid;
	}
	if (record.data.pool) {
	    return "Pool '"+ record.data.pool + "'";
	}
	return "-";
    },
    backup_reasons_table: {
	'backup=yes': gettext('Enabled'),
	'backup=no': gettext('Disabled'),
	'enabled': gettext('Enabled'),
	'disabled': gettext('Disabled'),
	'not a volume': gettext('Not a volume'),
	'efidisk but no OMVF BIOS': gettext('EFI Disk without OMVF BIOS'),
    },
    renderNotFound: what => Ext.String.format(gettext("No {0} found"), what),
    get_kvm_osinfo: function(value) {
	var info = { base: 'Other' }; // default
	if (value) {
	    Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) {
		Ext.each(PVE.Utils.kvm_ostypes[k], function(e) {
		    if (e.val === value) {
			info = { desc: e.desc, base: k };
		    }
		});
	    });
	}
	return info;
    },
    render_kvm_ostype: function(value) {
	var osinfo = PVE.Utils.get_kvm_osinfo(value);
	if (osinfo.desc && osinfo.desc !== '-') {
	    return osinfo.base + ' ' + osinfo.desc;
	} else {
	    return osinfo.base;
	}
    },
    render_hotplug_features: function(value) {
	var fa = [];
	if (!value || value === '0') {
	    return gettext('Disabled');
	}
	if (value === '1') {
	    value = 'disk,network,usb';
	}
	Ext.each(value.split(','), function(el) {
	    if (el === 'disk') {
		fa.push(gettext('Disk'));
	    } else if (el === 'network') {
		fa.push(gettext('Network'));
	    } else if (el === 'usb') {
		fa.push('USB');
	    } else if (el === 'memory') {
		fa.push(gettext('Memory'));
	    } else if (el === 'cpu') {
		fa.push(gettext('CPU'));
	    } else {
		fa.push(el);
	    }
	});
	return fa.join(', ');
    },
    render_localtime: function(value) {
	if (value === '__default__') {
	    return Proxmox.Utils.defaultText + ' (' + gettext('Enabled for Windows') + ')';
	}
	return Proxmox.Utils.format_boolean(value);
    },
    render_qga_features: function(config) {
	if (!config) {
	    return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')';
	}
	let qga = PVE.Parser.parsePropertyString(config, 'enabled');
	if (!PVE.Parser.parseBoolean(qga.enabled)) {
	    return Proxmox.Utils.disabledText;
	}
	delete qga.enabled;
	let agentstring = Proxmox.Utils.enabledText;
	for (const [key, value] of Object.entries(qga)) {
	    let displayText = Proxmox.Utils.disabledText;
	    if (key === 'type') {
		let map = {
		    isa: "ISA",
		    virtio: "VirtIO",
		};
		displayText = map[value] || Proxmox.Utils.unknownText;
	    } else if (PVE.Parser.parseBoolean(value)) {
		displayText = Proxmox.Utils.enabledText;
	    }
	    agentstring += `, ${key}: ${displayText}`;
	}
	return agentstring;
    },
    render_qemu_machine: function(value) {
	return value || Proxmox.Utils.defaultText + ' (i440fx)';
    },
    render_qemu_bios: function(value) {
	if (!value) {
	    return Proxmox.Utils.defaultText + ' (SeaBIOS)';
	} else if (value === 'seabios') {
	    return "SeaBIOS";
	} else if (value === 'ovmf') {
	    return "OVMF (UEFI)";
	} else {
	    return value;
	}
    },
    render_dc_ha_opts: function(value) {
	if (!value) {
	    return Proxmox.Utils.defaultText;
	} else {
	    return PVE.Parser.printPropertyString(value);
	}
    },
    render_as_property_string: v => !v ? Proxmox.Utils.defaultText : PVE.Parser.printPropertyString(v),
    render_scsihw: function(value) {
	if (!value || value === '__default__') {
	    return Proxmox.Utils.defaultText + ' (LSI 53C895A)';
	} else if (value === 'lsi') {
	    return 'LSI 53C895A';
	} else if (value === 'lsi53c810') {
	    return 'LSI 53C810';
	} else if (value === 'megasas') {
	    return 'MegaRAID SAS 8708EM2';
	} else if (value === 'virtio-scsi-pci') {
	    return 'VirtIO SCSI';
	} else if (value === 'virtio-scsi-single') {
	    return 'VirtIO SCSI single';
	} else if (value === 'pvscsi') {
	    return 'VMware PVSCSI';
	} else {
	    return value;
	}
    },
    render_spice_enhancements: function(values) {
	let props = PVE.Parser.parsePropertyString(values);
	if (Ext.Object.isEmpty(props)) {
	    return Proxmox.Utils.noneText;
	}
	let output = [];
	if (PVE.Parser.parseBoolean(props.foldersharing)) {
	    output.push('Folder Sharing: ' + gettext('Enabled'));
	}
	if (props.videostreaming === 'all' || props.videostreaming === 'filter') {
	    output.push('Video Streaming: ' + props.videostreaming);
	}
	return output.join(', ');
    },
    // fixme: auto-generate this
    // for now, please keep in sync with PVE::Tools::kvmkeymaps
    kvm_keymaps: {
	'__default__': Proxmox.Utils.defaultText,
	//ar: 'Arabic',
	da: 'Danish',
	de: 'German',
	'de-ch': 'German (Swiss)',
	'en-gb': 'English (UK)',
	'en-us': 'English (USA)',
	es: 'Spanish',
	//et: 'Estonia',
	fi: 'Finnish',
	//fo: 'Faroe Islands',
	fr: 'French',
	'fr-be': 'French (Belgium)',
	'fr-ca': 'French (Canada)',
	'fr-ch': 'French (Swiss)',
	//hr: 'Croatia',
	hu: 'Hungarian',
	is: 'Icelandic',
	it: 'Italian',
	ja: 'Japanese',
	lt: 'Lithuanian',
	//lv: 'Latvian',
	mk: 'Macedonian',
	nl: 'Dutch',
	//'nl-be': 'Dutch (Belgium)',
	no: 'Norwegian',
	pl: 'Polish',
	pt: 'Portuguese',
	'pt-br': 'Portuguese (Brazil)',
	//ru: 'Russian',
	sl: 'Slovenian',
	sv: 'Swedish',
	//th: 'Thai',
	tr: 'Turkish',
    },
    kvm_vga_drivers: {
	'__default__': Proxmox.Utils.defaultText,
	std: gettext('Standard VGA'),
	vmware: gettext('VMware compatible'),
	qxl: 'SPICE',
	qxl2: 'SPICE dual monitor',
	qxl3: 'SPICE three monitors',
	qxl4: 'SPICE four monitors',
	serial0: gettext('Serial terminal') + ' 0',
	serial1: gettext('Serial terminal') + ' 1',
	serial2: gettext('Serial terminal') + ' 2',
	serial3: gettext('Serial terminal') + ' 3',
	virtio: 'VirtIO-GPU',
	'virtio-gl': 'VirGL GPU',
	none: Proxmox.Utils.noneText,
    },
    render_kvm_language: function(value) {
	if (!value || value === '__default__') {
	    return Proxmox.Utils.defaultText;
	}
	let text = PVE.Utils.kvm_keymaps[value];
	return text ? `${text} (${value})` : value;
    },
    console_map: {
	'__default__': Proxmox.Utils.defaultText + ' (xterm.js)',
	'vv': 'SPICE (remote-viewer)',
	'html5': 'HTML5 (noVNC)',
	'xtermjs': 'xterm.js',
    },
    render_console_viewer: function(value) {
	value = value || '__default__';
	return PVE.Utils.console_map[value] || value;
    },
    render_kvm_vga_driver: function(value) {
	if (!value) {
	    return Proxmox.Utils.defaultText;
	}
	let vga = PVE.Parser.parsePropertyString(value, 'type');
	let text = PVE.Utils.kvm_vga_drivers[vga.type];
	if (!vga.type) {
	    text = Proxmox.Utils.defaultText;
	}
	return text ? `${text} (${value})` : value;
    },
    render_kvm_startup: function(value) {
	var startup = PVE.Parser.parseStartup(value);
	var res = 'order=';
	if (startup.order === undefined) {
	    res += 'any';
	} else {
	    res += startup.order;
	}
	if (startup.up !== undefined) {
	    res += ',up=' + startup.up;
	}
	if (startup.down !== undefined) {
	    res += ',down=' + startup.down;
	}
	return res;
    },
    extractFormActionError: function(action) {
	var msg;
	switch (action.failureType) {
	case Ext.form.action.Action.CLIENT_INVALID:
	    msg = gettext('Form fields may not be submitted with invalid values');
	    break;
	case Ext.form.action.Action.CONNECT_FAILURE:
	    msg = gettext('Connection error');
	    var resp = action.response;
	    if (resp.status && resp.statusText) {
		msg += " " + resp.status + ": " + resp.statusText;
	    }
	    break;
	case Ext.form.action.Action.LOAD_FAILURE:
	case Ext.form.action.Action.SERVER_INVALID:
	    msg = Proxmox.Utils.extractRequestError(action.result, true);
	    break;
	}
	return msg;
    },
    contentTypes: {
	'images': gettext('Disk image'),
	'backup': gettext('VZDump backup file'),
	'vztmpl': gettext('Container template'),
	'iso': gettext('ISO image'),
	'rootdir': gettext('Container'),
	'snippets': gettext('Snippets'),
    },
    volume_is_qemu_backup: function(volid, format) {
	return format === 'pbs-vm' || volid.match(':backup/vzdump-qemu-');
    },
    volume_is_lxc_backup: function(volid, format) {
	return format === 'pbs-ct' || volid.match(':backup/vzdump-(lxc|openvz)-');
    },
    authSchema: {
	ad: {
	    name: gettext('Active Directory Server'),
	    ipanel: 'pveAuthADPanel',
	    syncipanel: 'pveAuthLDAPSyncPanel',
	    add: true,
	    tfa: true,
	    pwchange: true,
	},
	ldap: {
	    name: gettext('LDAP Server'),
	    ipanel: 'pveAuthLDAPPanel',
	    syncipanel: 'pveAuthLDAPSyncPanel',
	    add: true,
	    tfa: true,
	    pwchange: true,
	},
	openid: {
	    name: gettext('OpenID Connect Server'),
	    ipanel: 'pveAuthOpenIDPanel',
	    add: true,
	    tfa: false,
	    pwchange: false,
	    iconCls: 'pmx-itype-icon-openid-logo',
	},
	pam: {
	    name: 'Linux PAM',
	    ipanel: 'pveAuthBasePanel',
	    add: false,
	    tfa: true,
	    pwchange: true,
	},
	pve: {
	    name: 'Proxmox VE authentication server',
	    ipanel: 'pveAuthBasePanel',
	    add: false,
	    tfa: true,
	    pwchange: true,
	},
    },
    storageSchema: {
	dir: {
	    name: Proxmox.Utils.directoryText,
	    ipanel: 'DirInputPanel',
	    faIcon: 'folder',
	    backups: true,
	},
	lvm: {
	    name: 'LVM',
	    ipanel: 'LVMInputPanel',
	    faIcon: 'folder',
	    backups: false,
	},
	lvmthin: {
	    name: 'LVM-Thin',
	    ipanel: 'LvmThinInputPanel',
	    faIcon: 'folder',
	    backups: false,
	},
	btrfs: {
	    name: 'BTRFS',
	    ipanel: 'BTRFSInputPanel',
	    faIcon: 'folder',
	    backups: true,
	},
	nfs: {
	    name: 'NFS',
	    ipanel: 'NFSInputPanel',
	    faIcon: 'building',
	    backups: true,
	},
	cifs: {
	    name: 'SMB/CIFS',
	    ipanel: 'CIFSInputPanel',
	    faIcon: 'building',
	    backups: true,
	},
	glusterfs: {
	    name: 'GlusterFS',
	    ipanel: 'GlusterFsInputPanel',
	    faIcon: 'building',
	    backups: true,
	},
	iscsi: {
	    name: 'iSCSI',
	    ipanel: 'IScsiInputPanel',
	    faIcon: 'building',
	    backups: false,
	},
	cephfs: {
	    name: 'CephFS',
	    ipanel: 'CephFSInputPanel',
	    faIcon: 'building',
	    backups: true,
	},
	pvecephfs: {
	    name: 'CephFS (PVE)',
	    ipanel: 'CephFSInputPanel',
	    hideAdd: true,
	    faIcon: 'building',
	    backups: true,
	},
	rbd: {
	    name: 'RBD',
	    ipanel: 'RBDInputPanel',
	    faIcon: 'building',
	    backups: false,
	},
	pveceph: {
	    name: 'RBD (PVE)',
	    ipanel: 'RBDInputPanel',
	    hideAdd: true,
	    faIcon: 'building',
	    backups: false,
	},
	zfs: {
	    name: 'ZFS over iSCSI',
	    ipanel: 'ZFSInputPanel',
	    faIcon: 'building',
	    backups: false,
	},
	zfspool: {
	    name: 'ZFS',
	    ipanel: 'ZFSPoolInputPanel',
	    faIcon: 'folder',
	    backups: false,
	},
	pbs: {
	    name: 'Proxmox Backup Server',
	    ipanel: 'PBSInputPanel',
	    faIcon: 'floppy-o',
	    backups: true,
	},
	drbd: {
	    name: 'DRBD',
	    hideAdd: true,
	    backups: false,
	},
    },
    sdnvnetSchema: {
	vnet: {
	    name: 'vnet',
	    faIcon: 'folder',
	},
    },
    sdnzoneSchema: {
	zone: {
	     name: 'zone',
	     hideAdd: true,
	},
	simple: {
	    name: 'Simple',
	    ipanel: 'SimpleInputPanel',
	    faIcon: 'th',
	},
	vlan: {
	    name: 'VLAN',
	    ipanel: 'VlanInputPanel',
	    faIcon: 'th',
	},
	qinq: {
	    name: 'QinQ',
	    ipanel: 'QinQInputPanel',
	    faIcon: 'th',
	},
	vxlan: {
	    name: 'VXLAN',
	    ipanel: 'VxlanInputPanel',
	    faIcon: 'th',
	},
	evpn: {
	    name: 'EVPN',
	    ipanel: 'EvpnInputPanel',
	    faIcon: 'th',
	},
    },
    sdncontrollerSchema: {
	controller: {
	     name: 'controller',
	     hideAdd: true,
	},
	evpn: {
	    name: 'evpn',
	    ipanel: 'EvpnInputPanel',
	    faIcon: 'crosshairs',
	},
	bgp: {
	    name: 'bgp',
	    ipanel: 'BgpInputPanel',
	    faIcon: 'crosshairs',
	},
    },
    sdnipamSchema: {
	ipam: {
	     name: 'ipam',
	     hideAdd: true,
	},
	pve: {
	    name: 'PVE',
	    ipanel: 'PVEIpamInputPanel',
	    faIcon: 'th',
	    hideAdd: true,
	},
	netbox: {
	    name: 'Netbox',
	    ipanel: 'NetboxInputPanel',
	    faIcon: 'th',
	},
	phpipam: {
	    name: 'PhpIpam',
	    ipanel: 'PhpIpamInputPanel',
	    faIcon: 'th',
	},
    },
    sdndnsSchema: {
	dns: {
	     name: 'dns',
	     hideAdd: true,
	},
	powerdns: {
	    name: 'powerdns',
	    ipanel: 'PowerdnsInputPanel',
	    faIcon: 'th',
	},
    },
    format_sdnvnet_type: function(value, md, record) {
	var schema = PVE.Utils.sdnvnetSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },
    format_sdnzone_type: function(value, md, record) {
	var schema = PVE.Utils.sdnzoneSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },
    format_sdncontroller_type: function(value, md, record) {
	var schema = PVE.Utils.sdncontrollerSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },
    format_sdnipam_type: function(value, md, record) {
	var schema = PVE.Utils.sdnipamSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },
    format_sdndns_type: function(value, md, record) {
	var schema = PVE.Utils.sdndnsSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },
    format_storage_type: function(value, md, record) {
	if (value === 'rbd') {
	    value = !record || record.get('monhost') ? 'rbd' : 'pveceph';
	} else if (value === 'cephfs') {
	    value = !record || record.get('monhost') ? 'cephfs' : 'pvecephfs';
	}
	let schema = PVE.Utils.storageSchema[value];
	return schema?.name ?? value;
    },
    format_ha: function(value) {
	var text = Proxmox.Utils.noneText;
	if (value.managed) {
	    text = value.state || Proxmox.Utils.noneText;
	    text += ', ' + Proxmox.Utils.groupText + ': ';
	    text += value.group || Proxmox.Utils.noneText;
	}
	return text;
    },
    format_content_types: function(value) {
	return value.split(',').sort().map(function(ct) {
	    return PVE.Utils.contentTypes[ct] || ct;
	}).join(', ');
    },
    render_storage_content: function(value, metaData, record) {
	var data = record.data;
	if (Ext.isNumber(data.channel) &&
	    Ext.isNumber(data.id) &&
	    Ext.isNumber(data.lun)) {
	    return "CH " +
		Ext.String.leftPad(data.channel, 2, '0') +
		" ID " + data.id + " LUN " + data.lun;
	}
	return data.volid.replace(/^.*?:(.*?\/)?/, '');
    },
    render_serverity: function(value) {
	return PVE.Utils.log_severity_hash[value] || value;
    },
    calculate_hostcpu: function(data) {
	if (!(data.uptime && Ext.isNumeric(data.cpu))) {
	    return -1;
	}
	if (data.type !== 'qemu' && data.type !== 'lxc') {
	    return -1;
	}
	var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node);
	var node = PVE.data.ResourceStore.getAt(index);
	if (!Ext.isDefined(node) || node === null) {
	    return -1;
	}
	var maxcpu = node.data.maxcpu || 1;
	if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
	    return -1;
	}
	return (data.cpu/maxcpu) * data.maxcpu;
    },
    render_hostcpu: function(value, metaData, record, rowIndex, colIndex, store) {
	if (!(record.data.uptime && Ext.isNumeric(record.data.cpu))) {
	    return '';
	}
	if (record.data.type !== 'qemu' && record.data.type !== 'lxc') {
	    return '';
	}
	var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node);
	var node = PVE.data.ResourceStore.getAt(index);
	if (!Ext.isDefined(node) || node === null) {
	    return '';
	}
	var maxcpu = node.data.maxcpu || 1;
	if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
	    return '';
	}
	var per = (record.data.cpu/maxcpu) * record.data.maxcpu * 100;
	return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU');
    },
    render_bandwidth: function(value) {
	if (!Ext.isNumeric(value)) {
	    return '';
	}
	return Proxmox.Utils.format_size(value) + '/s';
    },
    render_timestamp_human_readable: function(value) {
	return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s');
    },
    // render a timestamp or pending
    render_next_event: function(value) {
	if (!value) {
	    return '-';
	}
	let now = new Date(), next = new Date(value * 1000);
	if (next < now) {
	    return gettext('pending');
	}
	return Proxmox.Utils.render_timestamp(value);
    },
    calculate_mem_usage: function(data) {
	if (!Ext.isNumeric(data.mem) ||
	    data.maxmem === 0 ||
	    data.uptime < 1) {
	    return -1;
	}
	return data.mem / data.maxmem;
    },
    calculate_hostmem_usage: function(data) {
	if (data.type !== 'qemu' && data.type !== 'lxc') {
	    return -1;
	}
        var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node);
	var node = PVE.data.ResourceStore.getAt(index);
        if (!Ext.isDefined(node) || node === null) {
	    return -1;
        }
	var maxmem = node.data.maxmem || 0;
	if (!Ext.isNumeric(data.mem) ||
	    maxmem === 0 ||
	    data.uptime < 1) {
	    return -1;
	}
	return data.mem / maxmem;
    },
    render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
	if (!Ext.isNumeric(value) || value === -1) {
	    return '';
	}
	if (value > 1) {
	    // we got no percentage but bytes
	    var mem = value;
	    var maxmem = record.data.maxmem;
	    if (!record.data.uptime ||
		maxmem === 0 ||
		!Ext.isNumeric(mem)) {
		return '';
	    }
	    return (mem*100/maxmem).toFixed(1) + " %";
	}
	return (value*100).toFixed(1) + " %";
    },
    render_hostmem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
	if (!Ext.isNumeric(record.data.mem) || value === -1) {
	    return '';
	}
	if (record.data.type !== 'qemu' && record.data.type !== 'lxc') {
	    return '';
	}
	var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node);
	var node = PVE.data.ResourceStore.getAt(index);
	var maxmem = node.data.maxmem || 0;
	if (record.data.mem > 1) {
	    // we got no percentage but bytes
	    var mem = record.data.mem;
	    if (!record.data.uptime ||
		maxmem === 0 ||
		!Ext.isNumeric(mem)) {
		return '';
	    }
	    return ((mem*100)/maxmem).toFixed(1) + " %";
	}
	return (value*100).toFixed(1) + " %";
    },
    render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) {
	var mem = value;
	var maxmem = record.data.maxmem;
	if (!record.data.uptime) {
	    return '';
	}
	if (!(Ext.isNumeric(mem) && maxmem)) {
	    return '';
	}
	return Proxmox.Utils.render_size(value);
    },
    calculate_disk_usage: function(data) {
	if (!Ext.isNumeric(data.disk) ||
	    ((data.type === 'qemu' || data.type === 'lxc') && data.uptime === 0) ||
	    data.maxdisk === 0
	) {
	    return -1;
	}
	return data.disk / data.maxdisk;
    },
    render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
	if (!Ext.isNumeric(value) || value === -1) {
	    return '';
	}
	return (value * 100).toFixed(1) + " %";
    },
    render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) {
	var disk = value;
	var maxdisk = record.data.maxdisk;
	var type = record.data.type;
	if (!Ext.isNumeric(disk) ||
	    maxdisk === 0 ||
	    ((type === 'qemu' || type === 'lxc') && record.data.uptime === 0)
	) {
	    return '';
	}
	return Proxmox.Utils.render_size(value);
    },
    get_object_icon_class: function(type, record) {
	var status = '';
	var objType = type;
	if (type === 'type') {
	    // for folder view
	    objType = record.groupbyid;
	} else if (record.template) {
	    // templates
	    objType = 'template';
	    status = type;
	} else {
	    // everything else
	    status = record.status + ' ha-' + record.hastate;
	}
	if (record.lock) {
	    status += ' locked lock-' + record.lock;
	}
	var defaults = PVE.tree.ResourceTree.typeDefaults[objType];
	if (defaults && defaults.iconCls) {
	    var retVal = defaults.iconCls + ' ' + status;
	    return retVal;
	}
	return '';
    },
    render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) {
	var cls = PVE.Utils.get_object_icon_class(value, record.data);
	var fa = ' ';
	return fa + value;
    },
    render_support_level: function(value, metaData, record) {
	return PVE.Utils.support_level_hash[value] || '-';
    },
    render_upid: function(value, metaData, record) {
	var type = record.data.type;
	var id = record.data.id;
	return Proxmox.Utils.format_task_description(type, id);
    },
    render_optional_url: function(value) {
	if (value && value.match(/^https?:\/\//)) {
	    return '' + value + '';
	}
	return value;
    },
    render_san: function(value) {
	var names = [];
	if (Ext.isArray(value)) {
	    value.forEach(function(val) {
		if (!Ext.isNumber(val)) {
		    names.push(val);
		}
	    });
	    return names.join('
');
	}
	return value;
    },
    render_full_name: function(firstname, metaData, record) {
	var first = firstname || '';
	var last = record.data.lastname || '';
	return Ext.htmlEncode(first + " " + last);
    },
    // expecting the following format:
    // [v2:10.10.10.1:6802/2008,v1:10.10.10.1:6803/2008]
    render_ceph_osd_addr: function(value) {
	value = value.trim();
	if (value.startsWith('[') && value.endsWith(']')) {
	    value = value.slice(1, -1); // remove []
	}
	value = value.replaceAll(',', '\n'); // split IPs in lines
	let retVal = '';
	for (const i of value.matchAll(/^(v[0-9]):(.*):([0-9]*)\/([0-9]*)$/gm)) {
	    retVal += `${i[1]}: ${i[2]}:${i[3]}
`;
	}
	return retVal.length < 1 ? value : retVal;
    },
    windowHostname: function() {
	return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match,
            function(m, addr, offset, original) { return addr; });
    },
    openDefaultConsoleWindow: function(consoles, consoleType, vmid, nodename, vmname, cmd) {
	var dv = PVE.Utils.defaultViewer(consoles, consoleType);
	PVE.Utils.openConsoleWindow(dv, consoleType, vmid, nodename, vmname, cmd);
    },
    openConsoleWindow: function(viewer, consoleType, vmid, nodename, vmname, cmd) {
	if (vmid === undefined && (consoleType === 'kvm' || consoleType === 'lxc')) {
	    throw "missing vmid";
	}
	if (!nodename) {
	    throw "no nodename specified";
	}
	if (viewer === 'html5') {
	    PVE.Utils.openVNCViewer(consoleType, vmid, nodename, vmname, cmd);
	} else if (viewer === 'xtermjs') {
	    Proxmox.Utils.openXtermJsViewer(consoleType, vmid, nodename, vmname, cmd);
	} else if (viewer === 'vv') {
	    let url = '/nodes/' + nodename + '/spiceshell';
	    let params = {
		proxy: PVE.Utils.windowHostname(),
	    };
	    if (consoleType === 'kvm') {
		url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy';
	    } else if (consoleType === 'lxc') {
		url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy';
	    } else if (consoleType === 'upgrade') {
		params.cmd = 'upgrade';
	    } else if (consoleType === 'cmd') {
		params.cmd = cmd;
	    } else if (consoleType !== 'shell') {
		throw `unknown spice viewer type '${consoleType}'`;
	    }
	    PVE.Utils.openSpiceViewer(url, params);
	} else {
	    throw `unknown viewer type '${viewer}'`;
	}
    },
    defaultViewer: function(consoles, type) {
	var allowSpice, allowXtermjs;
	if (consoles === true) {
	    allowSpice = true;
	    allowXtermjs = true;
	} else if (typeof consoles === 'object') {
	    allowSpice = consoles.spice;
	    allowXtermjs = !!consoles.xtermjs;
	}
	let dv = PVE.UIOptions.options.console || (type === 'kvm' ? 'vv' : 'xtermjs');
	if (dv === 'vv' && !allowSpice) {
	    dv = allowXtermjs ? 'xtermjs' : 'html5';
	} else if (dv === 'xtermjs' && !allowXtermjs) {
	    dv = allowSpice ? 'vv' : 'html5';
	}
	return dv;
    },
    openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) {
	let scaling = 'off';
	if (Proxmox.Utils.toolkit !== 'touch') {
	    var sp = Ext.state.Manager.getProvider();
	    scaling = sp.get('novnc-scaling', 'off');
	}
	var url = Ext.Object.toQueryString({
	    console: vmtype, // kvm, lxc, upgrade or shell
	    novnc: 1,
	    vmid: vmid,
	    vmname: vmname,
	    node: nodename,
	    resize: scaling,
	    cmd: cmd,
	});
	var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427");
	if (nw) {
	    nw.focus();
	}
    },
    openSpiceViewer: function(url, params) {
	var downloadWithName = function(uri, name) {
	    var link = Ext.DomHelper.append(document.body, {
		tag: 'a',
		href: uri,
		css: 'display:none;visibility:hidden;height:0px;',
	    });
	    // Note: we need to tell Android and Chrome the correct file name extension
	    // but we do not set 'download' tag for other environments, because
	    // It can have strange side effects (additional user prompt on firefox)
	    if (navigator.userAgent.match(/Android|Chrome/i)) {
		link.download = name;
	    }
	    if (link.fireEvent) {
		link.fireEvent('onclick');
	    } else {
		let evt = document.createEvent("MouseEvents");
		evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
		link.dispatchEvent(evt);
	    }
	};
	Proxmox.Utils.API2Request({
	    url: url,
	    params: params,
	    method: 'POST',
	    failure: function(response, opts) {
		Ext.Msg.alert('Error', response.htmlStatus);
	    },
	    success: function(response, opts) {
		let cfg = response.result.data;
		let raw = Object.entries(cfg).reduce((acc, [k, v]) => acc + `${k}=${v}\n`, "[virt-viewer]\n");
		let spiceDownload = 'data:application/x-virt-viewer;charset=UTF-8,' + encodeURIComponent(raw);
		downloadWithName(spiceDownload, "pve-spice.vv");
	    },
	});
    },
    openTreeConsole: function(tree, record, item, index, e) {
	e.stopEvent();
	let nodename = record.data.node;
	let vmid = record.data.vmid;
	let vmname = record.data.name;
	if (record.data.type === 'qemu' && !record.data.template) {
	    Proxmox.Utils.API2Request({
		url: `/nodes/${nodename}/qemu/${vmid}/status/current`,
		failure: response => Ext.Msg.alert('Error', response.htmlStatus),
		success: function(response, opts) {
		    let conf = response.result.data;
		    let consoles = {
			spice: !!conf.spice,
			xtermjs: !!conf.serial,
		    };
		    PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
		},
	    });
	} else if (record.data.type === 'lxc' && !record.data.template) {
	    PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
	}
    },
    // test automation helper
    call_menu_handler: function(menu, text) {
	let item = menu.query('menuitem').find(el => el.text === text);
	if (item && item.handler) {
	    item.handler();
	}
    },
    createCmdMenu: function(v, record, item, index, event) {
	event.stopEvent();
	if (!(v instanceof Ext.tree.View)) {
	    v.select(record);
	}
	let menu;
	let type = record.data.type;
	if (record.data.template) {
	    if (type === 'qemu' || type === 'lxc') {
		menu = Ext.create('PVE.menu.TemplateMenu', {
		    pveSelNode: record,
		});
	    }
	} else if (type === 'qemu' || type === 'lxc' || type === 'node') {
	    menu = Ext.create('PVE.' + type + '.CmdMenu', {
		pveSelNode: record,
		nodename: record.data.node,
	    });
	} else {
	    return undefined;
	}
	menu.showAt(event.getXY());
	return menu;
    },
    // helper for deleting field which are set to there default values
    delete_if_default: function(values, fieldname, default_val, create) {
	if (values[fieldname] === '' || values[fieldname] === default_val) {
	    if (!create) {
		if (values.delete) {
		    if (Ext.isArray(values.delete)) {
			values.delete.push(fieldname);
		    } else {
			values.delete += ',' + fieldname;
		    }
		} else {
		    values.delete = fieldname;
		}
	    }
	    delete values[fieldname];
	}
    },
    loadSSHKeyFromFile: function(file, callback) {
	// ssh-keygen produces ~ 740 bytes for a 4096 bit RSA key,  current max is 16 kbit, so assume:
	// 740 * 8 for max. 32kbit (5920 bytes), round upwards to 8192 bytes, leaves lots of comment space
	PVE.Utils.loadFile(file, callback, 8192);
    },
    loadFile: function(file, callback, maxSize) {
	maxSize = maxSize || 32 * 1024;
	if (file.size > maxSize) {
	    Ext.Msg.alert(gettext('Error'), `${gettext("Invalid file size")}: ${file.size} > ${maxSize}`);
	    return;
	}
	let reader = new FileReader();
	reader.onload = evt => callback(evt.target.result);
	reader.readAsText(file);
    },
    loadTextFromFile: function(file, callback, maxBytes) {
	let maxSize = maxBytes || 8192;
	if (file.size > maxSize) {
	    Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
	    return;
	}
	let reader = new FileReader();
	reader.onload = evt => callback(evt.target.result);
	reader.readAsText(file);
    },
    diskControllerMaxIDs: {
	ide: 4,
	sata: 6,
	scsi: 31,
	virtio: 16,
	unused: 256,
    },
    // types is either undefined (all busses), an array of busses, or a single bus
    forEachBus: function(types, func) {
	let busses = Object.keys(PVE.Utils.diskControllerMaxIDs);
	if (Ext.isArray(types)) {
	    busses = types;
	} else if (Ext.isDefined(types)) {
	    busses = [types];
	}
	// check if we only have valid busses
	for (let i = 0; i < busses.length; i++) {
	    if (!PVE.Utils.diskControllerMaxIDs[busses[i]]) {
		throw "invalid bus: '" + busses[i] + "'";
	    }
	}
	for (let i = 0; i < busses.length; i++) {
	    let count = PVE.Utils.diskControllerMaxIDs[busses[i]];
	    for (let j = 0; j < count; j++) {
		let cont = func(busses[i], j);
		if (!cont && cont !== undefined) {
		    return;
		}
	    }
	}
    },
    mp_counts: {
	mp: 256,
	unused: 256,
    },
    forEachMP: function(func, includeUnused) {
	for (let i = 0; i < PVE.Utils.mp_counts.mp; i++) {
	    let cont = func('mp', i);
	    if (!cont && cont !== undefined) {
		return;
	    }
	}
	if (!includeUnused) {
	    return;
	}
	for (let i = 0; i < PVE.Utils.mp_counts.unused; i++) {
	    let cont = func('unused', i);
	    if (!cont && cont !== undefined) {
		return;
	    }
	}
    },
    hardware_counts: {
	net: 32,
	usb: 14,
	usb_old: 5,
	hostpci: 16,
	audio: 1,
	efidisk: 1,
	serial: 4,
	rng: 1,
	tpmstate: 1,
    },
    // we can have usb6 and up only for specific machine/ostypes
    get_max_usb_count: function(ostype, machine) {
	if (!ostype) {
	    return PVE.Utils.hardware_counts.usb_old;
	}
	let match = /-(\d+).(\d+)/.exec(machine ?? '');
	if (!match || PVE.Utils.qemu_min_version([match[1], match[2]], [7, 1])) {
	    if (ostype === 'l26') {
		return PVE.Utils.hardware_counts.usb;
	    }
	    let os_match = /^win(\d+)$/.exec(ostype);
	    if (os_match && os_match[1] > 7) {
		return PVE.Utils.hardware_counts.usb;
	    }
	}
	return PVE.Utils.hardware_counts.usb_old;
    },
    // parameters are expected to be arrays, e.g. [7,1], [4,0,1]
    // returns true if toCheck is equal or greater than minVersion
    qemu_min_version: function(toCheck, minVersion) {
	let i;
	for (i = 0; i < toCheck.length && i < minVersion.length; i++) {
	    if (toCheck[i] < minVersion[i]) {
		return false;
	    }
	}
	if (minVersion.length > toCheck.length) {
	    for (; i < minVersion.length; i++) {
		if (minVersion[i] !== 0) {
		    return false;
		}
	    }
	}
	return true;
    },
    cleanEmptyObjectKeys: function(obj) {
	for (const propName of Object.keys(obj)) {
	    if (obj[propName] === null || obj[propName] === undefined) {
		delete obj[propName];
	    }
	}
    },
    acmedomain_count: 5,
    add_domain_to_acme: function(acme, domain) {
	if (acme.domains === undefined) {
	    acme.domains = [domain];
	} else {
	    acme.domains.push(domain);
	    acme.domains = acme.domains.filter((value, index, self) => self.indexOf(value) === index);
	}
	return acme;
    },
    remove_domain_from_acme: function(acme, domain) {
	if (acme.domains !== undefined) {
	    acme.domains = acme
		.domains
		.filter((value, index, self) => self.indexOf(value) === index && value !== domain);
	}
	return acme;
    },
    handleStoreErrorOrMask: function(view, store, regex, callback) {
	view.mon(store, 'load', function(proxy, response, success, operation) {
	    if (success) {
		Proxmox.Utils.setErrorMask(view, false);
		return;
	    }
	    let msg;
	    if (operation.error.statusText) {
		if (operation.error.statusText.match(regex)) {
		    callback(view, operation.error);
		    return;
		} else {
		    msg = operation.error.statusText + ' (' + operation.error.status + ')';
		}
	    } else {
		msg = gettext('Connection error');
	    }
	    Proxmox.Utils.setErrorMask(view, msg);
	});
    },
    showCephInstallOrMask: function(container, msg, nodename, callback) {
	if (msg.match(/not (installed|initialized)/i)) {
	    if (Proxmox.UserName === 'root@pam') {
		container.el.mask();
		if (!container.down('pveCephInstallWindow')) {
		    var isInstalled = !!msg.match(/not initialized/i);
		    var win = Ext.create('PVE.ceph.Install', {
			nodename: nodename,
		    });
		    win.getViewModel().set('isInstalled', isInstalled);
		    container.add(win);
		    win.on('close', () => {
			container.el.unmask();
		    });
		    win.show();
		    callback(win);
		}
	    } else {
		container.mask(Ext.String.format(gettext('{0} not installed.') +
		    ' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']);
	    }
	    return true;
	} else {
	    return false;
	}
    },
    monitor_ceph_installed: function(view, rstore, nodename, maskOwnerCt) {
	PVE.Utils.handleStoreErrorOrMask(
	    view,
	    rstore,
	    /not (installed|initialized)/i,
	    (_, error) => {
		nodename = nodename || 'localhost';
		let maskTarget = maskOwnerCt ? view.ownerCt : view;
		rstore.stopUpdate();
		PVE.Utils.showCephInstallOrMask(maskTarget, error.statusText, nodename, win => {
		    view.mon(win, 'cephInstallWindowClosed', () => rstore.startUpdate());
		});
	    },
	);
    },
    propertyStringSet: function(target, source, name, value) {
	if (source) {
	    if (value === undefined) {
		target[name] = source;
	    } else {
		target[name] = value;
	    }
	} else {
	    delete target[name];
	}
    },
    forEachCorosyncLink: function(nodeinfo, cb) {
	let re = /(?:ring|link)(\d+)_addr/;
	Ext.iterate(nodeinfo, (prop, val) => {
	    let match = re.exec(prop);
	    if (match) {
		cb(Number(match[1]), val);
	    }
	});
    },
    cpu_vendor_map: {
	'default': 'QEMU',
	'AuthenticAMD': 'AMD',
	'GenuineIntel': 'Intel',
    },
    cpu_vendor_order: {
	"AMD": 1,
	"Intel": 2,
	"QEMU": 3,
	"Host": 4,
	"_default_": 5, // includes custom models
    },
    verify_ip64_address_list: function(value, with_suffix) {
	for (let addr of value.split(/[ ,;]+/)) {
	    if (addr === '') {
		continue;
	    }
	    if (with_suffix) {
		let parts = addr.split('%');
		addr = parts[0];
		if (parts.length > 2) {
		    return false;
		}
		if (parts.length > 1 && !addr.startsWith('fe80:')) {
		    return false;
		}
	    }
	    if (!Proxmox.Utils.IP64_match.test(addr)) {
		return false;
	    }
	}
	return true;
    },
    sortByPreviousUsage: function(vmconfig, controllerList) {
	if (!controllerList) {
	    controllerList = ['ide', 'virtio', 'scsi', 'sata'];
	}
	let usedControllers = {};
	for (const type of Object.keys(PVE.Utils.diskControllerMaxIDs)) {
	    usedControllers[type] = 0;
	}
	for (const property of Object.keys(vmconfig)) {
	    if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) {
		const foundController = property.match(PVE.Utils.bus_match)[1];
		usedControllers[foundController]++;
	    }
	}
	let sortPriority = PVE.qemu.OSDefaults.getDefaults(vmconfig.ostype).busPriority;
	let sortedList = Ext.clone(controllerList);
	sortedList.sort(function(a, b) {
	    if (usedControllers[b] === usedControllers[a]) {
		return sortPriority[b] - sortPriority[a];
	    }
	    return usedControllers[b] - usedControllers[a];
	});
	return sortedList;
    },
    nextFreeDisk: function(controllers, config) {
	for (const controller of controllers) {
	    for (let i = 0; i < PVE.Utils.diskControllerMaxIDs[controller]; i++) {
		let confid = controller + i.toString();
		if (!Ext.isDefined(config[confid])) {
		    return {
			controller,
			id: i,
			confid,
		    };
		}
	    }
	}
	return undefined;
    },
    nextFreeMP: function(type, config) {
	for (let i = 0; i < PVE.Utils.mp_counts[type]; i++) {
	    let confid = `${type}${i}`;
	    if (!Ext.isDefined(config[confid])) {
		return {
		    type,
		    id: i,
		    confid,
		};
	    }
	}
	return undefined;
    },
    escapeNotesTemplate: function(value) {
	let replace = {
	    '\\': '\\\\',
	    '\n': '\\n',
	};
	return value.replace(/(\\|[\n])/g, match => replace[match]);
    },
    unEscapeNotesTemplate: function(value) {
	let replace = {
	    '\\\\': '\\',
	    '\\n': '\n',
	};
	return value.replace(/(\\\\|\\n)/g, match => replace[match]);
    },
    notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'],
    renderTags: function(tagstext, overrides) {
	let text = '';
	if (tagstext) {
	    let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t);
	    if (PVE.UIOptions.shouldSortTags()) {
		tags = tags.sort((a, b) => {
		    let alc = a.toLowerCase();
		    let blc = b.toLowerCase();
		    return alc < blc ? -1 : blc < alc ? 1 : a.localeCompare(b);
		});
	    }
	    text += ' ';
	    tags.forEach((tag) => {
		text += Proxmox.Utils.getTagElement(tag, overrides);
	    });
	}
	return text;
    },
    tagCharRegex: /^[a-z0-9+_.-]+$/i,
    verificationStateOrder: {
	'failed': 0,
	'none': 1,
	'ok': 2,
	'__default__': 3,
    },
},
    singleton: true,
    constructor: function() {
	var me = this;
	Ext.apply(me, me.utilities);
	Proxmox.Utils.override_task_descriptions({
	    acmedeactivate: ['ACME Account', gettext('Deactivate')],
	    acmenewcert: ['SRV', gettext('Order Certificate')],
	    acmerefresh: ['ACME Account', gettext('Refresh')],
	    acmeregister: ['ACME Account', gettext('Register')],
	    acmerenew: ['SRV', gettext('Renew Certificate')],
	    acmerevoke: ['SRV', gettext('Revoke Certificate')],
	    acmeupdate: ['ACME Account', gettext('Update')],
	    'auth-realm-sync': [gettext('Realm'), gettext('Sync')],
	    'auth-realm-sync-test': [gettext('Realm'), gettext('Sync Preview')],
	    cephcreatemds: ['Ceph Metadata Server', gettext('Create')],
	    cephcreatemgr: ['Ceph Manager', gettext('Create')],
	    cephcreatemon: ['Ceph Monitor', gettext('Create')],
	    cephcreateosd: ['Ceph OSD', gettext('Create')],
	    cephcreatepool: ['Ceph Pool', gettext('Create')],
	    cephdestroymds: ['Ceph Metadata Server', gettext('Destroy')],
	    cephdestroymgr: ['Ceph Manager', gettext('Destroy')],
	    cephdestroymon: ['Ceph Monitor', gettext('Destroy')],
	    cephdestroyosd: ['Ceph OSD', gettext('Destroy')],
	    cephdestroypool: ['Ceph Pool', gettext('Destroy')],
	    cephdestroyfs: ['CephFS', gettext('Destroy')],
	    cephfscreate: ['CephFS', gettext('Create')],
	    cephsetpool: ['Ceph Pool', gettext('Edit')],
	    cephsetflags: ['', gettext('Change global Ceph flags')],
	    clustercreate: ['', gettext('Create Cluster')],
	    clusterjoin: ['', gettext('Join Cluster')],
	    dircreate: [gettext('Directory Storage'), gettext('Create')],
	    dirremove: [gettext('Directory'), gettext('Remove')],
	    download: [gettext('File'), gettext('Download')],
	    hamigrate: ['HA', gettext('Migrate')],
	    hashutdown: ['HA', gettext('Shutdown')],
	    hastart: ['HA', gettext('Start')],
	    hastop: ['HA', gettext('Stop')],
	    imgcopy: ['', gettext('Copy data')],
	    imgdel: ['', gettext('Erase data')],
	    lvmcreate: [gettext('LVM Storage'), gettext('Create')],
	    lvmremove: ['Volume Group', gettext('Remove')],
	    lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
	    lvmthinremove: ['Thinpool', gettext('Remove')],
	    migrateall: ['', gettext('Migrate all VMs and Containers')],
	    'move_volume': ['CT', gettext('Move Volume')],
	    'pbs-download': ['VM/CT', gettext('File Restore Download')],
	    pull_file: ['CT', gettext('Pull file')],
	    push_file: ['CT', gettext('Push file')],
	    qmclone: ['VM', gettext('Clone')],
	    qmconfig: ['VM', gettext('Configure')],
	    qmcreate: ['VM', gettext('Create')],
	    qmdelsnapshot: ['VM', gettext('Delete Snapshot')],
	    qmdestroy: ['VM', gettext('Destroy')],
	    qmigrate: ['VM', gettext('Migrate')],
	    qmmove: ['VM', gettext('Move disk')],
	    qmpause: ['VM', gettext('Pause')],
	    qmreboot: ['VM', gettext('Reboot')],
	    qmreset: ['VM', gettext('Reset')],
	    qmrestore: ['VM', gettext('Restore')],
	    qmresume: ['VM', gettext('Resume')],
	    qmrollback: ['VM', gettext('Rollback')],
	    qmshutdown: ['VM', gettext('Shutdown')],
	    qmsnapshot: ['VM', gettext('Snapshot')],
	    qmstart: ['VM', gettext('Start')],
	    qmstop: ['VM', gettext('Stop')],
	    qmsuspend: ['VM', gettext('Hibernate')],
	    qmtemplate: ['VM', gettext('Convert to template')],
	    spiceproxy: ['VM/CT', gettext('Console') + ' (Spice)'],
	    spiceshell: ['', gettext('Shell') + ' (Spice)'],
	    startall: ['', gettext('Start all VMs and Containers')],
	    stopall: ['', gettext('Stop all VMs and Containers')],
	    unknownimgdel: ['', gettext('Destroy image from unknown guest')],
	    wipedisk: ['Device', gettext('Wipe Disk')],
	    vncproxy: ['VM/CT', gettext('Console')],
	    vncshell: ['', gettext('Shell')],
	    vzclone: ['CT', gettext('Clone')],
	    vzcreate: ['CT', gettext('Create')],
	    vzdelsnapshot: ['CT', gettext('Delete Snapshot')],
	    vzdestroy: ['CT', gettext('Destroy')],
	    vzdump: (type, id) => id ? `VM/CT ${id} - ${gettext('Backup')}` : gettext('Backup Job'),
	    vzmigrate: ['CT', gettext('Migrate')],
	    vzmount: ['CT', gettext('Mount')],
	    vzreboot: ['CT', gettext('Reboot')],
	    vzrestore: ['CT', gettext('Restore')],
	    vzresume: ['CT', gettext('Resume')],
	    vzrollback: ['CT', gettext('Rollback')],
	    vzshutdown: ['CT', gettext('Shutdown')],
	    vzsnapshot: ['CT', gettext('Snapshot')],
	    vzstart: ['CT', gettext('Start')],
	    vzstop: ['CT', gettext('Stop')],
	    vzsuspend: ['CT', gettext('Suspend')],
	    vztemplate: ['CT', gettext('Convert to template')],
	    vzumount: ['CT', gettext('Unmount')],
	    zfscreate: [gettext('ZFS Storage'), gettext('Create')],
	    zfsremove: ['ZFS Pool', gettext('Remove')],
	});
    },
});
Ext.define('PVE.UIOptions', {
    singleton: true,
    options: {
	'allowed-tags': [],
    },
    update: function() {
	Proxmox.Utils.API2Request({
	    url: '/cluster/options',
	    method: 'GET',
	    success: function(response) {
		for (const option of ['allowed-tags', 'console', 'tag-style']) {
		    PVE.UIOptions.options[option] = response?.result?.data?.[option];
		}
		PVE.UIOptions.updateTagList(PVE.UIOptions.options['allowed-tags']);
		PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']);
		PVE.UIOptions.fireUIConfigChanged();
	    },
	});
    },
    tagList: [],
    updateTagList: function(tags) {
	PVE.UIOptions.tagList = [...new Set([...tags])].sort();
    },
    parseTagOverrides: function(overrides) {
	let colors = {};
	(overrides || "").split(';').forEach(color => {
	    if (!color) {
		return;
	    }
	    let [tag, color_hex, font_hex] = color.split(':');
	    let r = parseInt(color_hex.slice(0, 2), 16);
	    let g = parseInt(color_hex.slice(2, 4), 16);
	    let b = parseInt(color_hex.slice(4, 6), 16);
	    colors[tag] = [r, g, b];
	    if (font_hex) {
		colors[tag].push(parseInt(font_hex.slice(0, 2), 16));
		colors[tag].push(parseInt(font_hex.slice(2, 4), 16));
		colors[tag].push(parseInt(font_hex.slice(4, 6), 16));
	    }
	});
	return colors;
    },
    tagOverrides: {},
    updateTagOverrides: function(colors) {
	let sp = Ext.state.Manager.getProvider();
	let color_state = sp.get('colors', '');
	let browser_colors = PVE.UIOptions.parseTagOverrides(color_state);
	PVE.UIOptions.tagOverrides = Ext.apply({}, browser_colors, colors);
    },
    updateTagSettings: function(style) {
	let overrides = style?.['color-map'];
	PVE.UIOptions.updateTagOverrides(PVE.UIOptions.parseTagOverrides(overrides ?? ""));
	let shape = style?.shape ?? 'circle';
	if (shape === '__default__') {
	    style = 'circle';
	}
	Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${shape}`);
    },
    tagTreeStyles: {
	'__default__': `${Proxmox.Utils.defaultText} (${gettext('Circle')})`,
	'full': gettext('Full'),
	'circle': gettext('Circle'),
	'dense': gettext('Dense'),
	'none': Proxmox.Utils.NoneText,
    },
    tagOrderOptions: {
	'__default__': `${Proxmox.Utils.defaultText} (${gettext('Alphabetical')})`,
	'config': gettext('Configuration'),
	'alphabetical': gettext('Alphabetical'),
    },
    shouldSortTags: function() {
	return !(PVE.UIOptions.options['tag-style']?.ordering === 'config');
    },
    getTreeSortingValue: function(key) {
	let localStorage = Ext.state.Manager.getProvider();
	let browserValues = localStorage.get('pve-tree-sorting');
	let defaults = {
	    'sort-field': 'vmid',
	    'group-templates': true,
	    'group-guest-types': true,
	};
	return browserValues?.[key] ?? defaults[key];
    },
    fireUIConfigChanged: function() {
	PVE.data.ResourceStore.refresh();
	Ext.GlobalEvents.fireEvent('loadedUiOptions');
    },
});
// ExtJS related things
Proxmox.Utils.toolkit = 'extjs';
// custom PVE specific VTypes
Ext.apply(Ext.form.field.VTypes, {
    QemuStartDate: function(v) {
	return (/^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/).test(v);
    },
    QemuStartDateText: gettext('Format') + ': "now" or "2006-06-17T16:01:21" or "2006-06-17"',
    IP64AddressList: v => PVE.Utils.verify_ip64_address_list(v, false),
    IP64AddressWithSuffixList: v => PVE.Utils.verify_ip64_address_list(v, true),
    IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2',
    IP64AddressListMask: /[A-Fa-f0-9,:.; ]/,
    PciIdText: gettext('Example') + ': 0x8086',
    PciId: v => /^0x[0-9a-fA-F]{4}$/.test(v),
});
Ext.define('PVE.form.field.Display', {
    override: 'Ext.form.field.Display',
    setSubmitValue: function(value) {
	// do nothing, this is only to allow generalized  bindings for the:
	// `me.isCreate ? 'textfield' : 'displayfield'` cases we have.
    },
});
Ext.define('PVE.noVncConsole', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveNoVncConsole',
    nodename: undefined,
    vmid: undefined,
    cmd: undefined,
    consoleType: undefined, // lxc, kvm, shell, cmd
    xtermjs: false,
    layout: 'fit',
    border: false,
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.consoleType) {
	    throw "no console type specified";
	}
	if (!me.vmid && me.consoleType !== 'shell' && me.consoleType !== 'cmd') {
	    throw "no VM ID specified";
	}
	// always use same iframe, to avoid running several noVnc clients
	// at same time (to avoid performance problems)
	var box = Ext.create('Ext.ux.IFrame', { itemid: "vncconsole" });
	var type = me.xtermjs ? 'xtermjs' : 'novnc';
	Ext.apply(me, {
	    items: box,
	    listeners: {
		activate: function() {
		    let sp = Ext.state.Manager.getProvider();
		    if (Ext.isFunction(me.beforeLoad)) {
			me.beforeLoad();
		    }
		    let queryDict = {
			console: me.consoleType, // kvm, lxc, upgrade or shell
			vmid: me.vmid,
			node: me.nodename,
			cmd: me.cmd,
			'cmd-opts': me.cmdOpts,
			resize: sp.get('novnc-scaling', 'scale'),
		    };
		    queryDict[type] = 1;
		    PVE.Utils.cleanEmptyObjectKeys(queryDict);
		    var url = '/?' + Ext.Object.toQueryString(queryDict);
		    box.load(url);
		},
	    },
	});
	me.callParent();
	me.on('afterrender', function() {
	    me.focus();
	});
    },
    reload: function() {
	// reload IFrame content to forcibly reconnect VNC/xterm.js to VM
	var box = this.down('[itemid=vncconsole]');
	box.getWin().location.reload();
    },
});
Ext.define('PVE.button.ConsoleButton', {
    extend: 'Ext.button.Split',
    alias: 'widget.pveConsoleButton',
    consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade', 'cmd'
    cmd: undefined,
    consoleName: undefined,
    iconCls: 'fa fa-terminal',
    enableSpice: true,
    enableXtermjs: true,
    nodename: undefined,
    vmid: 0,
    text: gettext('Console'),
    setEnableSpice: function(enable) {
	var me = this;
	me.enableSpice = enable;
	me.down('#spicemenu').setDisabled(!enable);
    },
    setEnableXtermJS: function(enable) {
	var me = this;
	me.enableXtermjs = enable;
	me.down('#xtermjs').setDisabled(!enable);
    },
    handler: function() { // main, general, handler
	let me = this;
	PVE.Utils.openDefaultConsoleWindow(
	    {
		spice: me.enableSpice,
		xtermjs: me.enableXtermjs,
	    },
	    me.consoleType,
	    me.vmid,
	    me.nodename,
	    me.consoleName,
	    me.cmd,
	);
    },
    openConsole: function(types) { // used by split-menu buttons
	let me = this;
	PVE.Utils.openConsoleWindow(
	    types,
	    me.consoleType,
	    me.vmid,
	    me.nodename,
	    me.consoleName,
	    me.cmd,
	);
    },
    menu: [
	{
	    xtype: 'menuitem',
	    text: 'noVNC',
	    iconCls: 'pve-itype-icon-novnc',
	    type: 'html5',
	    handler: function(button) {
		let view = this.up('button');
		view.openConsole(button.type);
	    },
	},
	{
	    xterm: 'menuitem',
	    itemId: 'spicemenu',
	    text: 'SPICE',
	    type: 'vv',
	    iconCls: 'pve-itype-icon-virt-viewer',
	    handler: function(button) {
		let view = this.up('button');
		view.openConsole(button.type);
	    },
	},
	{
	    text: 'xterm.js',
	    itemId: 'xtermjs',
	    iconCls: 'pve-itype-icon-xtermjs',
	    type: 'xtermjs',
	    handler: function(button) {
		let view = this.up('button');
		view.openConsole(button.type);
	    },
	},
    ],
    initComponent: function() {
        let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	me.callParent();
    },
});
Ext.define('PVE.button.PendingRevert', {
    extend: 'Proxmox.button.Button',
    alias: 'widget.pvePendingRevertButton',
    text: gettext('Revert'),
    disabled: true,
    config: {
	pendingGrid: null,
	apiurl: undefined,
    },
    handler: function() {
	if (!this.pendingGrid) {
	    this.pendingGrid = this.up('proxmoxPendingObjectGrid');
	    if (!this.pendingGrid) throw "revert button requires a pendingGrid";
	}
	let view = this.pendingGrid;
	let rec = view.getSelectionModel().getSelection()[0];
	if (!rec) return;
	let rowdef = view.rows[rec.data.key] || {};
	let keys = rowdef.multiKey || [rec.data.key];
	Proxmox.Utils.API2Request({
	    url: this.apiurl || view.editorConfig.url,
	    waitMsgTarget: view,
	    selModel: view.getSelectionModel(),
	    method: 'PUT',
	    params: {
		'revert': keys.join(','),
	    },
	    callback: () => view.reload(),
	    failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
	});
    },
});
/* Button features:
 * - observe selection changes to enable/disable the button using enableFn()
 * - pop up confirmation dialog using confirmMsg()
 *
 *   does this for the button and every menu item
 */
Ext.define('PVE.button.Split', {
    extend: 'Ext.button.Split',
    alias: 'widget.pveSplitButton',
    // the selection model to observe
    selModel: undefined,
    // if 'false' handler will not be called (button disabled)
    enableFn: function(record) {
	// do nothing
    },
    // function(record) or text
    confirmMsg: false,
    // take special care in confirm box (select no as default).
    dangerous: false,
    handlerWrapper: function(button, event) {
	var me = this;
	var rec, msg;
	if (me.selModel) {
	    rec = me.selModel.getSelection()[0];
	    if (!rec || me.enableFn(rec) === false) {
		return;
	    }
	}
	if (me.confirmMsg) {
	    msg = me.confirmMsg;
	    // confirMsg can be boolean or function
	    if (Ext.isFunction(me.confirmMsg)) {
		msg = me.confirmMsg(rec);
	    }
	    Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
	    Ext.Msg.show({
		title: gettext('Confirm'),
		icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
		msg: msg,
		buttons: Ext.Msg.YESNO,
		callback: function(btn) {
		    if (btn !== 'yes') {
			return;
		    }
		    me.realHandler(button, event, rec);
		},
	    });
	} else {
	    me.realHandler(button, event, rec);
	}
    },
    initComponent: function() {
        var me = this;
	if (me.handler) {
	    me.realHandler = me.handler;
	    me.handler = me.handlerWrapper;
	}
	if (me.menu && me.menu.items) {
	    me.menu.items.forEach(function(item) {
		if (item.handler) {
		    item.realHandler = item.handler;
		    item.handler = me.handlerWrapper;
		}
		if (item.selModel) {
		    me.mon(item.selModel, "selectionchange", function() {
			var rec = item.selModel.getSelection()[0];
			if (!rec || item.enableFn(rec) === false) {
			    item.setDisabled(true);
			} else {
			    item.setDisabled(false);
			}
		    });
		}
	    });
	}
	me.callParent();
	if (me.selModel) {
	    me.mon(me.selModel, "selectionchange", function() {
		var rec = me.selModel.getSelection()[0];
		if (!rec || me.enableFn(rec) === false) {
		    me.setDisabled(true);
		} else {
		    me.setDisabled(false);
		}
	    });
	}
    },
});
Ext.define('PVE.controller.StorageEdit', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.storageEdit',
    control: {
	'field[name=content]': {
	    change: function(field, value) {
		const hasImages = Ext.Array.contains(value, 'images');
		const prealloc = field.up('form').getForm().findField('preallocation');
		if (prealloc) {
		    prealloc.setDisabled(!hasImages);
		}
		var hasBackups = Ext.Array.contains(value, 'backup');
		var maxfiles = this.lookupReference('maxfiles');
		if (!maxfiles) {
		    return;
		}
		if (!hasBackups) {
		// clear values which will never be submitted
		    maxfiles.reset();
		}
		maxfiles.setDisabled(!hasBackups);
	    },
	},
    },
});
Ext.define('PVE.data.PermPathStore', {
    extend: 'Ext.data.Store',
    alias: 'store.pvePermPath',
    fields: ['value'],
    autoLoad: false,
    data: [
	{ 'value': '/' },
	{ 'value': '/access' },
	{ 'value': '/access/groups' },
	{ 'value': '/access/realm' },
	{ 'value': '/nodes' },
	{ 'value': '/pool' },
	{ 'value': '/sdn/zones' },
	{ 'value': '/storage' },
	{ 'value': '/vms' },
    ],
    constructor: function(config) {
	var me = this;
	config = config || {};
	me.callParent([config]);
	let donePaths = {};
	me.suspendEvents();
	PVE.data.ResourceStore.each(function(record) {
	    let path;
	    switch (record.get('type')) {
		case 'node': path = '/nodes/' + record.get('text');
		    break;
		case 'qemu': path = '/vms/' + record.get('vmid');
		    break;
		case 'lxc': path = '/vms/' + record.get('vmid');
		    break;
		case 'sdn': path = '/sdn/zones/' + record.get('sdn');
		    break;
		case 'storage': path = '/storage/' + record.get('storage');
		    break;
		case 'pool': path = '/pool/' + record.get('pool');
		    break;
	    }
	    if (path !== undefined && !donePaths[path]) {
		me.add({ value: path });
		donePaths[path] = 1;
	    }
	});
	me.resumeEvents();
	me.fireEvent('refresh', me);
	me.fireEvent('datachanged', me);
	me.sort({
	    property: 'value',
	    direction: 'ASC',
	});
    },
});
Ext.define('PVE.data.ResourceStore', {
    extend: 'Proxmox.data.UpdateStore',
    singleton: true,
    findVMID: function(vmid) {
	let me = this;
	return me.findExact('vmid', parseInt(vmid, 10)) >= 0;
    },
    // returns the cached data from all nodes
    getNodes: function() {
	let me = this;
	let nodes = [];
	me.each(function(record) {
	    if (record.get('type') === "node") {
		nodes.push(record.getData());
	    }
	});
	return nodes;
    },
    storageIsShared: function(storage_path) {
	let me = this;
	let index = me.findExact('id', storage_path);
	if (index >= 0) {
	    return me.getAt(index).data.shared;
	} else {
	    return undefined;
	}
    },
    guestNode: function(vmid) {
	let me = this;
	let index = me.findExact('vmid', parseInt(vmid, 10));
	return me.getAt(index).data.node;
    },
    guestName: function(vmid) {
	let me = this;
	let index = me.findExact('vmid', parseInt(vmid, 10));
	if (index < 0) {
	    return '-';
	}
	let rec = me.getAt(index).data;
	if ('name' in rec) {
	    return rec.name;
	}
	return '';
    },
    refresh: function() {
	let me = this;
	// can only refresh if we're loaded at least once and are not currently loading
	if (!me.isLoading() && me.isLoaded()) {
	    let records = (me.getData().getSource() || me.getData()).getRange();
	    me.fireEvent('load', me, records);
	}
    },
    constructor: function(config) {
	let me = this;
	config = config || {};
	let field_defaults = {
	    type: {
		header: gettext('Type'),
		type: 'string',
		renderer: PVE.Utils.render_resource_type,
		sortable: true,
		hideable: false,
		width: 100,
	    },
	    id: {
		header: 'ID',
		type: 'string',
		hidden: true,
		sortable: true,
		width: 80,
	    },
	    running: {
		header: gettext('Online'),
		type: 'boolean',
		renderer: Proxmox.Utils.format_boolean,
		hidden: true,
		convert: function(value, record) {
		    var info = record.data;
		    return Ext.isNumeric(info.uptime) && info.uptime > 0;
		},
	    },
	    text: {
		header: gettext('Description'),
		type: 'string',
		sortable: true,
		width: 200,
		convert: function(value, record) {
		    if (value) {
			return value;
		    }
		    let info = record.data, text;
		    if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
			text = String(info.vmid);
			if (info.name) {
			    text += " (" + info.name + ')';
			}
		    } else { // node, pool, storage
			text = info[info.type] || info.id;
			if (info.node && info.type !== 'node') {
			    text += " (" + info.node + ")";
			}
		    }
		    return text;
		},
	    },
	    vmid: {
		header: 'VMID',
		type: 'integer',
		hidden: true,
		sortable: true,
		width: 80,
	    },
	    name: {
		header: gettext('Name'),
		hidden: true,
		sortable: true,
		type: 'string',
	    },
	    disk: {
		header: gettext('Disk usage'),
		type: 'integer',
		renderer: PVE.Utils.render_disk_usage,
		sortable: true,
		width: 100,
		hidden: true,
	    },
	    diskuse: {
		header: gettext('Disk usage') + " %",
		type: 'number',
		sortable: true,
		renderer: PVE.Utils.render_disk_usage_percent,
		width: 100,
		calculate: PVE.Utils.calculate_disk_usage,
		sortType: 'asFloat',
	    },
	    maxdisk: {
		header: gettext('Disk size'),
		type: 'integer',
		renderer: Proxmox.Utils.render_size,
		sortable: true,
		hidden: true,
		width: 100,
	    },
	    mem: {
		header: gettext('Memory usage'),
		type: 'integer',
		renderer: PVE.Utils.render_mem_usage,
		sortable: true,
		hidden: true,
		width: 100,
	    },
	    memuse: {
		header: gettext('Memory usage') + " %",
		type: 'number',
		renderer: PVE.Utils.render_mem_usage_percent,
		calculate: PVE.Utils.calculate_mem_usage,
		sortType: 'asFloat',
		sortable: true,
		width: 100,
	    },
	    maxmem: {
		header: gettext('Memory size'),
		type: 'integer',
		renderer: Proxmox.Utils.render_size,
		hidden: true,
		sortable: true,
		width: 100,
	    },
	    cpu: {
		header: gettext('CPU usage'),
		type: 'float',
		renderer: Proxmox.Utils.render_cpu,
		sortable: true,
		width: 100,
	    },
	    maxcpu: {
		header: gettext('maxcpu'),
		type: 'integer',
		hidden: true,
		sortable: true,
		width: 60,
	    },
	    diskread: {
		header: gettext('Total Disk Read'),
		type: 'integer',
		hidden: true,
		sortable: true,
		renderer: Proxmox.Utils.format_size,
		width: 100,
	    },
	    diskwrite: {
		header: gettext('Total Disk Write'),
		type: 'integer',
		hidden: true,
		sortable: true,
		renderer: Proxmox.Utils.format_size,
		width: 100,
	    },
	    netin: {
		header: gettext('Total NetIn'),
		type: 'integer',
		hidden: true,
		sortable: true,
		renderer: Proxmox.Utils.format_size,
		width: 100,
	    },
	    netout: {
		header: gettext('Total NetOut'),
		type: 'integer',
		hidden: true,
		sortable: true,
		renderer: Proxmox.Utils.format_size,
		width: 100,
	    },
	    template: {
		header: gettext('Template'),
		type: 'integer',
		hidden: true,
		sortable: true,
		width: 60,
	    },
	    uptime: {
		header: gettext('Uptime'),
		type: 'integer',
		renderer: Proxmox.Utils.render_uptime,
		sortable: true,
		width: 110,
	    },
	    node: {
		header: gettext('Node'),
		type: 'string',
		hidden: true,
		sortable: true,
		width: 110,
	    },
	    storage: {
		header: gettext('Storage'),
		type: 'string',
		hidden: true,
		sortable: true,
		width: 110,
	    },
	    pool: {
		header: gettext('Pool'),
		type: 'string',
		hidden: true,
		sortable: true,
		width: 110,
	    },
	    hastate: {
		header: gettext('HA State'),
		type: 'string',
		defaultValue: 'unmanaged',
		hidden: true,
		sortable: true,
	    },
	    status: {
		header: gettext('Status'),
		type: 'string',
		hidden: true,
		sortable: true,
		width: 110,
	    },
	    lock: {
		header: gettext('Lock'),
		type: 'string',
		hidden: true,
		sortable: true,
		width: 110,
	    },
	    hostcpu: {
		header: gettext('Host CPU usage'),
		type: 'float',
		renderer: PVE.Utils.render_hostcpu,
		calculate: PVE.Utils.calculate_hostcpu,
		sortType: 'asFloat',
		sortable: true,
		width: 100,
	    },
	    hostmemuse: {
		header: gettext('Host Memory usage') + " %",
		type: 'number',
		renderer: PVE.Utils.render_hostmem_usage_percent,
		calculate: PVE.Utils.calculate_hostmem_usage,
		sortType: 'asFloat',
		sortable: true,
		width: 100,
	    },
	    tags: {
		header: gettext('Tags'),
		renderer: (value) => PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides),
		type: 'string',
		sortable: true,
		flex: 1,
	    },
	    // note: flex only last column to keep info closer together
	};
	let fields = [];
	let fieldNames = [];
	Ext.Object.each(field_defaults, function(key, value) {
	    var field = { name: key, type: value.type };
	    if (Ext.isDefined(value.convert)) {
		field.convert = value.convert;
	    }
	    if (Ext.isDefined(value.calculate)) {
		field.calculate = value.calculate;
	    }
	    if (Ext.isDefined(value.defaultValue)) {
		field.defaultValue = value.defaultValue;
	    }
	    fields.push(field);
	    fieldNames.push(key);
	});
	Ext.define('PVEResources', {
	    extend: "Ext.data.Model",
	    fields: fields,
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/cluster/resources',
	    },
	});
	Ext.define('PVETree', {
	    extend: "Ext.data.Model",
	    fields: fields,
	    proxy: { type: 'memory' },
	});
	Ext.apply(config, {
	    storeid: 'PVEResources',
	    model: 'PVEResources',
	    defaultColumns: function() {
		let res = [];
		Ext.Object.each(field_defaults, function(field, info) {
		    let fieldInfo = Ext.apply({ dataIndex: field }, info);
		    res.push(fieldInfo);
		});
		return res;
	    },
	    fieldNames: fieldNames,
	});
	me.callParent([config]);
    },
});
Ext.define('pve-rrd-node', {
    extend: 'Ext.data.Model',
    fields: [
	{
	    name: 'cpu',
	    // percentage
	    convert: function(value) {
		return value*100;
	    },
	},
	{
	    name: 'iowait',
	    // percentage
	    convert: function(value) {
		return value*100;
	    },
	},
	'loadavg',
	'maxcpu',
	'memtotal',
	'memused',
	'netin',
	'netout',
	'roottotal',
	'rootused',
	'swaptotal',
	'swapused',
	{ type: 'date', dateFormat: 'timestamp', name: 'time' },
    ],
});
Ext.define('pve-rrd-guest', {
    extend: 'Ext.data.Model',
    fields: [
	{
	    name: 'cpu',
	    // percentage
	    convert: function(value) {
		return value*100;
	    },
	},
	'maxcpu',
	'netin',
	'netout',
	'mem',
	'maxmem',
	'disk',
	'maxdisk',
	'diskread',
	'diskwrite',
	{ type: 'date', dateFormat: 'timestamp', name: 'time' },
    ],
});
Ext.define('pve-rrd-storage', {
    extend: 'Ext.data.Model',
    fields: [
	'used',
	'total',
	{ type: 'date', dateFormat: 'timestamp', name: 'time' },
    ],
});
Ext.define('pve-acme-challenges', {
    extend: 'Ext.data.Model',
    fields: ['id', 'type', 'schema'],
    proxy: {
	type: 'proxmox',
	    url: "/api2/json/cluster/acme/challenge-schema",
    },
    idProperty: 'id',
});
Ext.define('PVE.form.ACMEApiSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveACMEApiSelector',
    fieldLabel: gettext('DNS API'),
    displayField: 'name',
    valueField: 'id',
    store: {
	model: 'pve-acme-challenges',
	autoLoad: true,
    },
    triggerAction: 'all',
    queryMode: 'local',
    allowBlank: false,
    editable: true,
    forceSelection: true,
    anyMatch: true,
    selectOnFocus: true,
    getSchema: function() {
	let me = this;
	let val = me.getValue();
	if (val) {
	    let record = me.getStore().findRecord('id', val, 0, false, true, true);
	    if (record) {
		return record.data.schema;
	    }
	}
	return {};
    },
});
Ext.define('PVE.form.ACMEAccountSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveACMEAccountSelector',
    displayField: 'name',
    valueField: 'name',
    store: {
	model: 'pve-acme-accounts',
	autoLoad: true,
    },
    triggerAction: 'all',
    queryMode: 'local',
    allowBlank: false,
    editable: false,
    forceSelection: true,
    isEmpty: function() {
	return this.getStore().getData().length === 0;
    },
});
Ext.define('PVE.form.ACMEPluginSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveACMEPluginSelector',
    fieldLabel: gettext('Plugin'),
    displayField: 'plugin',
    valueField: 'plugin',
    store: {
	model: 'pve-acme-plugins',
	autoLoad: true,
	filters: item => item.data.type === 'dns',
    },
    triggerAction: 'all',
    queryMode: 'local',
    allowBlank: false,
    editable: false,
});
Ext.define('PVE.form.AgentFeatureSelector', {
    extend: 'Proxmox.panel.InputPanel',
    alias: ['widget.pveAgentFeatureSelector'],
    viewModel: {},
    items: [
	{
	    xtype: 'proxmoxcheckbox',
	    boxLabel: Ext.String.format(gettext('Use {0}'), 'QEMU Guest Agent'),
	    name: 'enabled',
	    reference: 'enabled',
	    uncheckedValue: 0,
	},
	{
	    xtype: 'proxmoxcheckbox',
	    boxLabel: gettext('Run guest-trim after a disk move or VM migration'),
	    name: 'fstrim_cloned_disks',
	    bind: {
		disabled: '{!enabled.checked}',
	    },
	    disabled: true,
	},
	{
	    xtype: 'displayfield',
	    userCls: 'pmx-hint',
	    value: gettext('Make sure the QEMU Guest Agent is installed in the VM'),
	    bind: {
		hidden: '{!enabled.checked}',
	    },
	},
    ],
    advancedItems: [
	{
	    xtype: 'proxmoxKVComboBox',
	    name: 'type',
	    value: '__default__',
	    deleteEmpty: false,
	    fieldLabel: 'Type',
	    comboItems: [
		['__default__', Proxmox.Utils.defaultText + " (VirtIO)"],
		['virtio', 'VirtIO'],
		['isa', 'ISA'],
	    ],
	},
    ],
    onGetValues: function(values) {
	var agentstr = PVE.Parser.printPropertyString(values, 'enabled');
	return { agent: agentstr };
    },
    setValues: function(values) {
	let res = PVE.Parser.parsePropertyString(values.agent, 'enabled');
	this.callParent([res]);
    },
});
Ext.define('PVE.form.BackupModeSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveBackupModeSelector'],
    comboItems: [
                ['snapshot', gettext('Snapshot')],
                ['suspend', gettext('Suspend')],
                ['stop', gettext('Stop')],
    ],
});
Ext.define('PVE.form.SizeField', {
    extend: 'Ext.form.FieldContainer',
    alias: 'widget.pveSizeField',
    mixins: ['Proxmox.Mixin.CBind'],
    viewModel: {
	data: {
	    unit: 'MiB',
	    unitPostfix: '',
	},
	formulas: {
	    unitlabel: (get) => get('unit') + get('unitPostfix'),
	},
    },
    emptyText: '',
    layout: 'hbox',
    defaults: {
	hideLabel: true,
    },
    units: {
	'B': 1,
	'KiB': 1024,
	'MiB': 1024*1024,
	'GiB': 1024*1024*1024,
	'TiB': 1024*1024*1024*1024,
	'KB': 1000,
	'MB': 1000*1000,
	'GB': 1000*1000*1000,
	'TB': 1000*1000*1000*1000,
    },
    // display unit (TODO: make (optionally) selectable)
    unit: 'MiB',
    unitPostfix: '',
    // use this if the backend saves values in another unit tha bytes, e.g.,
    // for KiB set it to 'KiB'
    backendUnit: undefined,
    // allow setting 0 and using it as a submit value
    allowZero: false,
    emptyValue: null,
    items: [
	{
	    xtype: 'numberfield',
	    cbind: {
		name: '{name}',
		emptyText: '{emptyText}',
		allowZero: '{allowZero}',
		emptyValue: '{emptyValue}',
	    },
	    minValue: 0,
	    step: 1,
	    submitLocaleSeparator: false,
	    fieldStyle: 'text-align: right',
	    flex: 1,
	    enableKeyEvents: true,
	    setValue: function(v) {
		if (!this._transformed) {
		    let fieldContainer = this.up('fieldcontainer');
		    let vm = fieldContainer.getViewModel();
		    let unit = vm.get('unit');
		    v /= fieldContainer.units[unit];
		    v *= fieldContainer.backendFactor;
		    this._transformed = true;
		}
		if (Number(v) === 0 && !this.allowZero) {
		    v = undefined;
		}
		return Ext.form.field.Text.prototype.setValue.call(this, v);
	    },
	    getSubmitValue: function() {
		let v = this.processRawValue(this.getRawValue());
		v = v.replace(this.decimalSeparator, '.');
		if (v === undefined || v === '') {
		    return this.emptyValue;
		}
		if (Number(v) === 0) {
		    return this.allowZero ? 0 : null;
		}
		let fieldContainer = this.up('fieldcontainer');
		let vm = fieldContainer.getViewModel();
		let unit = vm.get('unit');
		v = parseFloat(v) * fieldContainer.units[unit];
		v /= fieldContainer.backendFactor;
		return String(Math.floor(v));
	    },
	    listeners: {
		// our setValue gets only called if we have a value, avoid
		// transformation of the first user-entered value
		keydown: function() { this._transformed = true; },
	    },
	},
	{
	    xtype: 'displayfield',
	    name: 'unit',
	    submitValue: false,
	    padding: '0 0 0 10',
	    bind: {
		value: '{unitlabel}',
	    },
	    listeners: {
		change: (f, v) => {
		    f.originalValue = v;
		},
	    },
	    width: 40,
	},
    ],
    initComponent: function() {
	let me = this;
	me.unit = me.unit || 'MiB';
	if (!(me.unit in me.units)) {
	    throw "unknown unit: " + me.unit;
	}
	me.backendFactor = 1;
	if (me.backendUnit !== undefined) {
	    if (!(me.unit in me.units)) {
		throw "unknown backend unit: " + me.backendUnit;
	    }
	    me.backendFactor = me.units[me.backendUnit];
	}
	me.callParent(arguments);
	me.getViewModel().set('unit', me.unit);
	me.getViewModel().set('unitPostfix', me.unitPostfix);
    },
});
Ext.define('PVE.form.BandwidthField', {
    extend: 'PVE.form.SizeField',
    alias: 'widget.pveBandwidthField',
    unitPostfix: '/s',
});
Ext.define('PVE.form.BridgeSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.PVE.form.BridgeSelector'],
    bridgeType: 'any_bridge', // bridge, OVSBridge or any_bridge
    store: {
	fields: ['iface', 'active', 'type'],
	filterOnLoad: true,
	sorters: [
	    {
		property: 'iface',
		direction: 'ASC',
	    },
	],
    },
    valueField: 'iface',
    displayField: 'iface',
    listConfig: {
	columns: [
	    {
		header: gettext('Bridge'),
		dataIndex: 'iface',
		hideable: false,
		width: 100,
	    },
	    {
		header: gettext('Active'),
		width: 60,
		dataIndex: 'active',
		renderer: Proxmox.Utils.format_boolean,
	    },
	    {
		header: gettext('Comment'),
		dataIndex: 'comments',
		renderer: Ext.String.htmlEncode,
		flex: 1,
	    },
	],
    },
    setNodename: function(nodename) {
	var me = this;
	if (!nodename || me.nodename === nodename) {
	    return;
	}
	me.nodename = nodename;
	me.store.setProxy({
	    type: 'proxmox',
	    url: '/api2/json/nodes/' + me.nodename + '/network?type=' +
		me.bridgeType,
	});
	me.store.load();
    },
    initComponent: function() {
	var me = this;
	var nodename = me.nodename;
	me.nodename = undefined;
        me.callParent();
	me.setNodename(nodename);
    },
});
Ext.define('PVE.form.BusTypeSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: 'widget.pveBusSelector',
    withVirtIO: true,
    withUnused: false,
    initComponent: function() {
	var me = this;
	me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']];
	if (me.withVirtIO) {
	    me.comboItems.push(['virtio', 'VirtIO Block']);
	}
	me.comboItems.push(['scsi', 'SCSI']);
	if (me.withUnused) {
	    me.comboItems.push(['unused', 'Unused']);
	}
	me.callParent();
    },
});
Ext.define('PVE.data.CPUModel', {
    extend: 'Ext.data.Model',
    fields: [
	{ name: 'name' },
	{ name: 'vendor' },
	{ name: 'custom' },
	{ name: 'displayname' },
    ],
});
Ext.define('PVE.form.CPUModelSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.CPUModelSelector'],
    valueField: 'name',
    displayField: 'displayname',
    emptyText: Proxmox.Utils.defaultText + ' (kvm64)',
    allowBlank: true,
    editable: true,
    anyMatch: true,
    forceSelection: true,
    autoSelect: false,
    deleteEmpty: true,
    listConfig: {
	columns: [
	    {
		header: gettext('Model'),
		dataIndex: 'displayname',
		hideable: false,
		sortable: true,
		flex: 3,
	    },
	    {
		header: gettext('Vendor'),
		dataIndex: 'vendor',
		hideable: false,
		sortable: true,
		flex: 2,
	    },
	],
	width: 360,
    },
    store: {
	autoLoad: true,
	model: 'PVE.data.CPUModel',
	proxy: {
	    type: 'proxmox',
	    url: '/api2/json/nodes/localhost/capabilities/qemu/cpu',
	},
	sorters: [
	    {
		sorterFn: function(recordA, recordB) {
		    let a = recordA.data;
		    let b = recordB.data;
		    let vendorOrder = PVE.Utils.cpu_vendor_order;
		    let orderA = vendorOrder[a.vendor] || vendorOrder._default_;
		    let orderB = vendorOrder[b.vendor] || vendorOrder._default_;
		    if (orderA > orderB) {
			return 1;
		    } else if (orderA < orderB) {
			return -1;
		    }
		    // Within same vendor, sort alphabetically
		    return a.name.localeCompare(b.name);
		},
		direction: 'ASC',
	    },
	],
	listeners: {
	    load: function(store, records, success) {
		if (success) {
		    records.forEach(rec => {
			rec.data.displayname = rec.data.name.replace(/^custom-/, '');
			let vendor = rec.data.vendor;
			if (rec.data.name === 'host') {
			    vendor = 'Host';
			}
			// We receive vendor names as given to QEMU as CPUID
			vendor = PVE.Utils.cpu_vendor_map[vendor] || vendor;
			if (rec.data.custom) {
			    vendor = gettext('Custom') + ` (${vendor})`;
			}
			rec.data.vendor = vendor;
		    });
		    store.sort();
		}
	    },
	},
    },
});
Ext.define('PVE.form.CacheTypeSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.CacheTypeSelector'],
    comboItems: [
	['__default__', Proxmox.Utils.defaultText + " (" + gettext('No cache') + ")"],
	['directsync', 'Direct sync'],
	['writethrough', 'Write through'],
	['writeback', 'Write back'],
	['unsafe', 'Write back (' + gettext('unsafe') + ')'],
	['none', gettext('No cache')],
    ],
});
Ext.define('PVE.form.CalendarEvent', {
    extend: 'Ext.form.field.ComboBox',
    xtype: 'pveCalendarEvent',
    editable: true,
    emptyText: gettext('Editable'), // FIXME: better way to convey that to confused users?
    valueField: 'value',
    queryMode: 'local',
    matchFieldWidth: false,
    listConfig: {
	maxWidth: 450,
    },
    store: {
	field: ['value', 'text'],
	data: [
	    { value: '*/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) },
	    { value: '*/2:00', text: gettext("Every two hours") },
	    { value: '21:00', text: gettext("Every day") + " 21:00" },
	    { value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30" },
	    { value: 'mon..fri 00:00', text: gettext("Monday to Friday") + " 00:00" },
	    { value: 'mon..fri */1:00', text: gettext("Monday to Friday") + ': ' + gettext("hourly") },
	    {
		value: 'mon..fri 7..18:00/15',
		text: gettext("Monday to Friday") + ', '
		    + Ext.String.format(gettext('{0} to {1}'), '07:00', '18:45') + ': '
		    + Ext.String.format(gettext("Every {0} minutes"), 15),
	    },
	    { value: 'sun 01:00', text: gettext("Sunday") + " 01:00" },
	    { value: 'monthly', text: gettext("Every first day of the Month") + " 00:00" },
	    { value: 'sat *-1..7 15:00', text: gettext("First Saturday each month") + " 15:00" },
	    { value: 'yearly', text: gettext("First day of the year") + " 00:00" },
	],
    },
    tpl: [
	'',
    ],
    displayTpl: [
	'',
	    '{value}',
	'',
    ],
});
Ext.define('PVE.form.CephPoolSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveCephPoolSelector',
    allowBlank: false,
    valueField: 'pool_name',
    displayField: 'pool_name',
    editable: false,
    queryMode: 'local',
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no nodename given";
	}
	let onlyRBDPools = ({ data }) =>
	    !data?.application_metadata || !!data?.application_metadata?.rbd;
	var store = Ext.create('Ext.data.Store', {
	    fields: ['name'],
	    sorters: 'name',
	    filters: [
		onlyRBDPools,
	    ],
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/nodes/' + me.nodename + '/ceph/pool',
	    },
	});
	Ext.apply(me, {
	    store: store,
	});
        me.callParent();
	store.load({
	    callback: function(rec, op, success) {
		let filteredRec = rec.filter(onlyRBDPools);
		if (success && filteredRec.length > 0) {
		    me.select(filteredRec[0]);
		}
	    },
	});
    },
});
Ext.define('PVE.form.CephFSSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveCephFSSelector',
    allowBlank: false,
    valueField: 'name',
    displayField: 'name',
    editable: false,
    queryMode: 'local',
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no nodename given";
	}
	var store = Ext.create('Ext.data.Store', {
	    fields: ['name'],
	    sorters: 'name',
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/nodes/' + me.nodename + '/ceph/fs',
	    },
	});
	Ext.apply(me, {
	    store: store,
	});
        me.callParent();
	store.load({
	    callback: function(rec, op, success) {
		if (success && rec.length > 0) {
		    me.select(rec[0]);
		}
	    },
	});
    },
});
Ext.define('PVE.form.ComboBoxSetStoreNode', {
    extend: 'Proxmox.form.ComboGrid',
    config: {
	apiBaseUrl: '/api2/json/nodes/',
	apiSuffix: '',
    },
    showNodeSelector: false,
    setNodeName: function(value) {
	let me = this;
	value ||= Proxmox.NodeName;
	me.getStore().getProxy().setUrl(`${me.apiBaseUrl}${value}${me.apiSuffix}`);
	me.clearValue();
    },
    nodeChange: function(_field, value) {
	let me = this;
	// disable autoSelect if there is already a selection or we have the picker open
	if (me.getValue() || me.isExpanded) {
	    let autoSelect = me.autoSelect;
	    me.autoSelect = false;
	    me.store.on('afterload', function() {
		me.autoSelect = autoSelect;
	    }, { single: true });
	}
	me.setNodeName(value);
	me.fireEvent('nodechanged', value);
    },
    tbarMouseDown: function() {
	this.topBarMousePress = true;
    },
    tbarMouseUp: function() {
	let me = this;
	delete this.topBarMousePress;
	if (me.focusLeft) {
	    me.focus();
	    delete me.focusLeft;
	}
    },
    // conditionally prevent the focusLeave handler to continue, preventing collapsing of the picker
    onFocusLeave: function() {
	let me = this;
	me.focusLeft = true;
	if (!me.topBarMousePress) {
	    me.callParent(arguments);
	}
	return undefined;
    },
    initComponent: function() {
	let me = this;
	if (me.showNodeSelector && PVE.data.ResourceStore.getNodes().length > 1) {
	    me.errorHeight = 140;
	    Ext.apply(me.listConfig ?? {}, {
		tbar: {
		    xtype: 'toolbar',
		    minHeight: 40,
		    listeners: {
			mousedown: me.tbarMouseDown,
			mouseup: me.tbarMouseUp,
			element: 'el',
			scope: me,
		    },
		    items: [
			{
			    xtype: "pveStorageScanNodeSelector",
			    autoSelect: false,
			    fieldLabel: gettext('Node to scan'),
			    listeners: {
				change: (field, value) => me.nodeChange(field, value),
			    },
			},
		    ],
		},
		emptyText: me.listConfig?.emptyText ?? gettext('Nothing found'),
	    });
	}
	me.callParent();
    },
});
Ext.define('PVE.form.CompressionSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveCompressionSelector'],
    comboItems: [
                ['0', Proxmox.Utils.noneText],
                ['lzo', 'LZO (' + gettext('fast') + ')'],
                ['gzip', 'GZIP (' + gettext('good') + ')'],
                ['zstd', 'ZSTD (' + gettext('fast and good') + ')'],
    ],
});
Ext.define('PVE.form.ContentTypeSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveContentTypeSelector'],
    cts: undefined,
    initComponent: function() {
	var me = this;
	me.comboItems = [];
	if (me.cts === undefined) {
	    me.cts = ['images', 'iso', 'vztmpl', 'backup', 'rootdir', 'snippets'];
	}
	Ext.Array.each(me.cts, function(ct) {
	    me.comboItems.push([ct, PVE.Utils.format_content_types(ct)]);
	});
	me.callParent();
    },
});
Ext.define('PVE.form.ControllerSelector', {
    extend: 'Ext.form.FieldContainer',
    alias: 'widget.pveControllerSelector',
    withVirtIO: true,
    withUnused: false,
    vmconfig: {}, // used to check for existing devices
    setToFree: function(controllers, busField, deviceIDField) {
	let me = this;
	let freeId = PVE.Utils.nextFreeDisk(controllers, me.vmconfig);
	if (freeId !== undefined) {
	    busField?.setValue(freeId.controller);
	    deviceIDField.setValue(freeId.id);
	}
    },
    updateVMConfig: function(vmconfig) {
	let me = this;
	me.vmconfig = Ext.apply({}, vmconfig);
	me.down('field[name=deviceid]').validate();
    },
    setVMConfig: function(vmconfig, autoSelect) {
	let me = this;
	me.vmconfig = Ext.apply({}, vmconfig);
	let bussel = me.down('field[name=controller]');
	let deviceid = me.down('field[name=deviceid]');
	let clist;
	if (autoSelect === 'cdrom') {
	    if (!Ext.isDefined(me.vmconfig.ide2)) {
		bussel.setValue('ide');
		deviceid.setValue(2);
		return;
	    }
	    clist = ['ide', 'scsi', 'sata'];
	} else {
	    // in most cases we want to add a disk to the same controller we previously used
	    clist = PVE.Utils.sortByPreviousUsage(me.vmconfig);
	}
	me.setToFree(clist, bussel, deviceid);
	deviceid.validate();
    },
    getConfId: function() {
	let me = this;
	let controller = me.getComponent('controller').getValue() || 'ide';
	let id = me.getComponent('deviceid').getValue() || 0;
	return `${controller}${id}`;
    },
    initComponent: function() {
	let me = this;
	Ext.apply(me, {
	    fieldLabel: gettext('Bus/Device'),
	    layout: 'hbox',
	    defaults: {
                hideLabel: true,
	    },
	    items: [
		{
		    xtype: 'pveBusSelector',
		    name: 'controller',
		    itemId: 'controller',
		    value: PVE.qemu.OSDefaults.generic.busType,
		    withVirtIO: me.withVirtIO,
		    withUnused: me.withUnused,
		    allowBlank: false,
		    flex: 2,
		    listeners: {
			change: function(t, value) {
			    if (!value) {
				return;
			    }
			    let field = me.down('field[name=deviceid]');
			    me.setToFree([value], undefined, field);
			    field.setMaxValue(PVE.Utils.diskControllerMaxIDs[value] - 1);
			    field.validate();
			},
		    },
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'deviceid',
		    itemId: 'deviceid',
		    minValue: 0,
		    maxValue: PVE.Utils.diskControllerMaxIDs.ide - 1,
		    value: '0',
		    flex: 1,
		    allowBlank: false,
		    validator: function(value) {
			if (!me.rendered) {
			    return undefined;
			}
			let controller = me.down('field[name=controller]').getValue();
			let confid = controller + value;
			if (Ext.isDefined(me.vmconfig[confid])) {
			    return "This device is already in use.";
			}
			return true;
		    },
		},
	    ],
	});
	me.callParent();
	if (me.selectFree) {
	    me.setVMConfig(me.vmconfig);
	}
    },
});
Ext.define('PVE.form.DayOfWeekSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveDayOfWeekSelector'],
    comboItems: [],
    initComponent: function() {
	var me = this;
	me.comboItems = [
	    ['mon', Ext.util.Format.htmlDecode(Ext.Date.dayNames[1])],
	    ['tue', Ext.util.Format.htmlDecode(Ext.Date.dayNames[2])],
	    ['wed', Ext.util.Format.htmlDecode(Ext.Date.dayNames[3])],
	    ['thu', Ext.util.Format.htmlDecode(Ext.Date.dayNames[4])],
	    ['fri', Ext.util.Format.htmlDecode(Ext.Date.dayNames[5])],
	    ['sat', Ext.util.Format.htmlDecode(Ext.Date.dayNames[6])],
	    ['sun', Ext.util.Format.htmlDecode(Ext.Date.dayNames[0])],
	];
	this.callParent();
    },
});
Ext.define('PVE.form.DiskFormatSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: 'widget.pveDiskFormatSelector',
    comboItems: [
	['raw', gettext('Raw disk image') + ' (raw)'],
	['qcow2', gettext('QEMU image format') + ' (qcow2)'],
	['vmdk', gettext('VMware image format') + ' (vmdk)'],
    ],
});
Ext.define('PVE.form.DiskStorageSelector', {
    extend: 'Ext.container.Container',
    alias: 'widget.pveDiskStorageSelector',
    layout: 'fit',
    defaults: {
	margin: '0 0 5 0',
    },
    // the fieldLabel for the storageselector
    storageLabel: gettext('Storage'),
    // the content to show (e.g., images or rootdir)
    storageContent: undefined,
    // if true, selects the first available storage
    autoSelect: false,
    allowBlank: false,
    emptyText: '',
    // hides the selection field
    // this is always hidden on creation,
    // and only shown when the storage needs a selection and
    // hideSelection is not true
    hideSelection: undefined,
    // hides the size field (e.g, for the efi disk dialog)
    hideSize: false,
    // hides the format field (e.g. for TPM state)
    hideFormat: false,
    // sets the initial size value
    // string because else we get a type confusion
    defaultSize: '32',
    changeStorage: function(f, value) {
	var me = this;
	var formatsel = me.getComponent('diskformat');
	var hdfilesel = me.getComponent('hdimage');
	var hdsizesel = me.getComponent('disksize');
	// initial store load, and reset/deletion of the storage
	if (!value) {
	    hdfilesel.setDisabled(true);
	    hdfilesel.setVisible(false);
	    formatsel.setDisabled(true);
	    return;
	}
	var rec = f.store.getById(value);
	// if the storage is not defined, or valid,
	// we cannot know what to enable/disable
	if (!rec) {
	    return;
	}
	let validFormats = {};
	let selectFormat = 'raw';
	if (rec.data.format) {
	    validFormats = rec.data.format[0]; // 0 is the formats, 1 the default in the backend
	    delete validFormats.subvol; // we never need subvol in the gui
	    if (validFormats.qcow2) {
		selectFormat = 'qcow2';
	    } else if (validFormats.raw) {
		selectFormat = 'raw';
	    } else {
		selectFormat = rec.data.format[1];
	    }
	}
	var select = !!rec.data.select_existing && !me.hideSelection;
	formatsel.setDisabled(me.hideFormat || Ext.Object.getSize(validFormats) <= 1);
	formatsel.setValue(selectFormat);
	hdfilesel.setDisabled(!select);
	hdfilesel.setVisible(select);
	if (select) {
	    hdfilesel.setStorage(value);
	}
	hdsizesel.setDisabled(select || me.hideSize);
	hdsizesel.setVisible(!select && !me.hideSize);
    },
    setNodename: function(nodename) {
	var me = this;
	var hdstorage = me.getComponent('hdstorage');
	var hdfilesel = me.getComponent('hdimage');
	hdstorage.setNodename(nodename);
	hdfilesel.setNodename(nodename);
    },
    setDisabled: function(value) {
	var me = this;
	var hdstorage = me.getComponent('hdstorage');
	// reset on disable
	if (value) {
	    hdstorage.setValue();
	}
	hdstorage.setDisabled(value);
	// disabling does not always fire this event and we do not need
	// the value of the validity
	hdstorage.fireEvent('validitychange');
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: 'pveStorageSelector',
		itemId: 'hdstorage',
		name: 'hdstorage',
		reference: 'hdstorage',
		fieldLabel: me.storageLabel,
		nodename: me.nodename,
		storageContent: me.storageContent,
		disabled: me.disabled,
		autoSelect: me.autoSelect,
		allowBlank: me.allowBlank,
		emptyText: me.emptyText,
		listeners: {
		    change: {
			fn: me.changeStorage,
			scope: me,
		    },
		},
	    },
	    {
		xtype: 'pveFileSelector',
		name: 'hdimage',
		reference: 'hdimage',
		itemId: 'hdimage',
		fieldLabel: gettext('Disk image'),
		nodename: me.nodename,
		disabled: true,
		hidden: true,
	    },
	    {
		xtype: 'numberfield',
		itemId: 'disksize',
		reference: 'disksize',
		name: 'disksize',
		fieldLabel: gettext('Disk size') + ' (GiB)',
		hidden: me.hideSize,
		disabled: me.hideSize,
		minValue: 0.001,
		maxValue: 128*1024,
		decimalPrecision: 3,
		value: me.defaultSize,
		allowBlank: false,
	    },
	    {
		xtype: 'pveDiskFormatSelector',
		itemId: 'diskformat',
		reference: 'diskformat',
		name: 'diskformat',
		fieldLabel: gettext('Format'),
		nodename: me.nodename,
		disabled: true,
		hidden: me.hideFormat || me.storageContent === 'rootdir',
		value: 'qcow2',
		allowBlank: false,
	    },
	];
	// use it to disable the children but not ourself
	me.disabled = false;
	me.callParent();
    },
});
Ext.define('PVE.form.EmailNotificationSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveEmailNotificationSelector'],
    comboItems: [
	['always', gettext('Notify always')],
	['failure', gettext('On failure only')],
    ],
});
Ext.define('PVE.form.FileSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: 'widget.pveFileSelector',
    editable: true,
    anyMatch: true,
    forceSelection: true,
    listeners: {
	afterrender: function() {
	    var me = this;
	    if (!me.disabled) {
		me.setStorage(me.storage, me.nodename);
	    }
	},
    },
    setStorage: function(storage, nodename) {
	var me = this;
	var change = false;
	if (storage && me.storage !== storage) {
	    me.storage = storage;
	    change = true;
	}
	if (nodename && me.nodename !== nodename) {
	    me.nodename = nodename;
	    change = true;
	}
	if (!(me.storage && me.nodename && change)) {
	    return;
	}
	var url = '/api2/json/nodes/' + me.nodename + '/storage/' + me.storage + '/content';
	if (me.storageContent) {
	    url += '?content=' + me.storageContent;
	}
	me.store.setProxy({
	    type: 'proxmox',
	    url: url,
	});
	me.store.removeAll();
	me.store.load();
    },
    setNodename: function(nodename) {
	this.setStorage(undefined, nodename);
    },
    store: {
	model: 'pve-storage-content',
    },
    allowBlank: false,
    autoSelect: false,
    valueField: 'volid',
    displayField: 'text',
    listConfig: {
	width: 600,
	columns: [
	    {
		header: gettext('Name'),
		dataIndex: 'text',
		hideable: false,
		flex: 1,
	    },
	    {
		header: gettext('Format'),
		width: 60,
		dataIndex: 'format',
	    },
	    {
		header: gettext('Size'),
		width: 100,
		dataIndex: 'size',
		renderer: Proxmox.Utils.format_size,
	    },
	],
    },
});
Ext.define('PVE.form.FirewallPolicySelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveFirewallPolicySelector'],
    comboItems: [
	    ['ACCEPT', 'ACCEPT'],
	    ['REJECT', 'REJECT'],
	    ['DROP', 'DROP'],
	],
});
/*
 *  This is a global search field it loads the /cluster/resources on focus and displays the
 *  result in a floating grid. Filtering and sorting is done in the customFilter function
 *
 *  Accepts key up/down and enter for input, and it opens to CTRL+SHIFT+F and CTRL+SPACE
 */
Ext.define('PVE.form.GlobalSearchField', {
    extend: 'Ext.form.field.Text',
    alias: 'widget.pveGlobalSearchField',
    emptyText: gettext('Search'),
    enableKeyEvents: true,
    selectOnFocus: true,
    padding: '0 5 0 5',
    grid: {
	xtype: 'gridpanel',
	userCls: 'proxmox-tags-full',
	focusOnToFront: false,
	floating: true,
	emptyText: Proxmox.Utils.noneText,
	width: 600,
	height: 400,
	scrollable: {
	    xtype: 'scroller',
	    y: true,
	    x: true,
	},
	store: {
	    model: 'PVEResources',
	    proxy: {
		type: 'proxmox',
		url: '/api2/extjs/cluster/resources',
	    },
	},
	plugins: {
	    ptype: 'bufferedrenderer',
	    trailingBufferZone: 20,
	    leadingBufferZone: 20,
	},
	hideMe: function() {
	    var me = this;
	    if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
		return;
	    }
	    me.hasFocus = false;
	    if (!me.textfield.hasFocus) {
		me.hide();
	    }
	},
	setFocus: function() {
	    var me = this;
	    me.hasFocus = true;
	},
	listeners: {
	    rowclick: function(grid, record) {
		var me = this;
		me.textfield.selectAndHide(record.id);
	    },
	    itemcontextmenu: function(v, record, item, index, event) {
		var me = this;
		me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
	    },
	    focusleave: 'hideMe',
	    focusenter: 'setFocus',
	},
	columns: [
	    {
		text: gettext('Type'),
		dataIndex: 'type',
		width: 100,
		renderer: PVE.Utils.render_resource_type,
	    },
	    {
		text: gettext('Description'),
		flex: 1,
		dataIndex: 'text',
		renderer: function(value, mD, rec) {
		    let overrides = PVE.UIOptions.tagOverrides;
		    let tags = PVE.Utils.renderTags(rec.data.tags, overrides);
		    return `${value}${tags}`;
		},
	    },
	    {
		text: gettext('Node'),
		dataIndex: 'node',
	    },
	    {
		text: gettext('Pool'),
		dataIndex: 'pool',
	    },
	],
    },
    customFilter: function(item) {
	let me = this;
	if (me.filterVal === '') {
	    item.data.relevance = 0;
	    return true;
	}
	// different types have different fields to search, e.g., a node will never have a pool
	const fieldMap = {
	    'pool': ['type', 'pool', 'text'],
	    'node': ['type', 'node', 'text'],
	    'storage': ['type', 'pool', 'node', 'storage'],
	    'default': ['name', 'type', 'node', 'pool', 'vmid'],
	};
	let fields = fieldMap[item.data.type] || fieldMap.default;
	let fieldArr = fields.map(field => item.data[field]?.toString().toLowerCase());
	if (item.data.tags) {
	    let tags = item.data.tags.split(/[;, ]/);
	    fieldArr.push(...tags);
	}
	let filterWords = me.filterVal.split(/\s+/);
	// all text is case insensitive and each split-out word is searched for separately.
	// a row gets 1 point for every partial match, and and additional point for every exact match
	let match = 0;
	for (let fieldValue of fieldArr) {
	    if (fieldValue === undefined || fieldValue === "") {
		continue;
	    }
	    for (let filterWord of filterWords) {
		if (fieldValue.indexOf(filterWord) !== -1) {
		    match++; // partial match
		    if (fieldValue === filterWord) {
			match++; // exact match is worth more
		    }
		}
	    }
	}
	item.data.relevance = match; // set the row's virtual 'relevance' value for ordering
	return match > 0;
    },
    updateFilter: function(field, newValue, oldValue) {
	let me = this;
	// parse input and filter store, show grid
	me.grid.store.filterVal = newValue.toLowerCase().trim();
	me.grid.store.clearFilter(true);
	me.grid.store.filterBy(me.customFilter);
	me.grid.getSelectionModel().select(0);
    },
    selectAndHide: function(id) {
	var me = this;
	me.tree.selectById(id);
	me.grid.hide();
	me.setValue('');
	me.blur();
    },
    onKey: function(field, e) {
	var me = this;
	var key = e.getKey();
	switch (key) {
	    case Ext.event.Event.ENTER:
		// go to first entry if there is one
		if (me.grid.store.getCount() > 0) {
		    me.selectAndHide(me.grid.getSelection()[0].data.id);
		}
		break;
	    case Ext.event.Event.UP:
		me.grid.getSelectionModel().selectPrevious();
		break;
	    case Ext.event.Event.DOWN:
		me.grid.getSelectionModel().selectNext();
		break;
	    case Ext.event.Event.ESC:
		me.grid.hide();
		me.blur();
		break;
	}
    },
    loadValues: function(field) {
	let me = this;
	me.hasFocus = true;
	me.grid.textfield = me;
	me.grid.store.load();
	me.grid.showBy(me, 'tl-bl');
    },
    hideGrid: function() {
	let me = this;
	me.hasFocus = false;
	if (!me.grid.hasFocus) {
	    me.grid.hide();
	}
    },
    listeners: {
	change: {
	    fn: 'updateFilter',
	    buffer: 250,
	},
	specialkey: 'onKey',
	focusenter: 'loadValues',
	focusleave: {
	    fn: 'hideGrid',
	    delay: 100,
	},
    },
    toggleFocus: function() {
	let me = this;
	if (!me.hasFocus) {
	    me.focus();
	} else {
	    me.blur();
	}
    },
    initComponent: function() {
	let me = this;
	if (!me.tree) {
	    throw "no tree given";
	}
	me.grid = Ext.create(me.grid);
	me.callParent();
	// bind CTRL + SHIFT + F and CTRL + SPACE to open/close the search
	me.keymap = new Ext.KeyMap({
	    target: Ext.get(document),
	    binding: [{
		key: 'F',
		ctrl: true,
		shift: true,
		fn: me.toggleFocus,
		scope: me,
	    }, {
		key: ' ',
		ctrl: true,
		fn: me.toggleFocus,
		scope: me,
	    }],
	});
	// always select first item and sort by relevance after load
	me.mon(me.grid.store, 'load', function() {
	    me.grid.getSelectionModel().select(0);
	    me.grid.store.sort({
		property: 'relevance',
		direction: 'DESC',
	    });
	});
    },
});
Ext.define('pve-groups', {
    extend: 'Ext.data.Model',
    fields: ['groupid', 'comment', 'users'],
    proxy: {
	type: 'proxmox',
	url: "/api2/json/access/groups",
    },
    idProperty: 'groupid',
});
Ext.define('PVE.form.GroupSelector', {
    extend: 'Proxmox.form.ComboGrid',
    xtype: 'pveGroupSelector',
    allowBlank: false,
    autoSelect: false,
    valueField: 'groupid',
    displayField: 'groupid',
    listConfig: {
	columns: [
	    {
		header: gettext('Group'),
		sortable: true,
		dataIndex: 'groupid',
		flex: 1,
	    },
	    {
		header: gettext('Comment'),
		sortable: false,
		dataIndex: 'comment',
		renderer: Ext.String.htmlEncode,
		flex: 1,
	    },
	    {
		header: gettext('Users'),
		sortable: false,
		dataIndex: 'users',
		renderer: Ext.String.htmlEncode,
		flex: 1,
	    },
	],
    },
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-groups',
	    sorters: [{
		property: 'groupid',
	    }],
	});
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
	store.load();
    },
});
Ext.define('PVE.form.GuestIDSelector', {
    extend: 'Ext.form.field.Number',
    alias: 'widget.pveGuestIDSelector',
    allowBlank: false,
    minValue: 100,
    maxValue: 999999999,
    validateExists: undefined,
    loadNextFreeID: false,
    guestType: undefined,
    validator: function(value) {
	var me = this;
	if (!Ext.isNumeric(value) ||
	    value < me.minValue ||
	    value > me.maxValue) {
	    // check is done by ExtJS
	    return true;
	}
	if (me.validateExists === true && !me.exists) {
	    return me.unknownID;
	}
	if (me.validateExists === false && me.exists) {
	    return me.inUseID;
	}
	return true;
    },
    initComponent: function() {
	var me = this;
	var label = '{0} ID';
	var unknownID = gettext('This {0} ID does not exist');
	var inUseID = gettext('This {0} ID is already in use');
	var type = 'CT/VM';
	if (me.guestType === 'lxc') {
	    type = 'CT';
	} else if (me.guestType === 'qemu') {
	    type = 'VM';
	}
	me.label = Ext.String.format(label, type);
	me.unknownID = Ext.String.format(unknownID, type);
	me.inUseID = Ext.String.format(inUseID, type);
	Ext.apply(me, {
	    fieldLabel: me.label,
	    listeners: {
		'change': function(field, newValue, oldValue) {
		    if (!Ext.isDefined(me.validateExists)) {
			return;
		    }
		    Proxmox.Utils.API2Request({
			params: { vmid: newValue },
			url: '/cluster/nextid',
			method: 'GET',
			success: function(response, opts) {
			    me.exists = false;
			    me.validate();
			},
			failure: function(response, opts) {
			    me.exists = true;
			    me.validate();
			},
		    });
		},
	    },
	});
        me.callParent();
	if (me.loadNextFreeID) {
	    Proxmox.Utils.API2Request({
		url: '/cluster/nextid',
		method: 'GET',
		success: function(response, opts) {
		    me.setRawValue(response.result.data);
		},
	    });
	}
    },
});
Ext.define('PVE.form.hashAlgorithmSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveHashAlgorithmSelector'],
    config: {
	deleteEmpty: false,
    },
    comboItems: [
	['__default__', 'None'],
	['md5', 'MD5'],
	['sha1', 'SHA-1'],
	['sha224', 'SHA-224'],
	['sha256', 'SHA-256'],
	['sha384', 'SHA-384'],
	['sha512', 'SHA-512'],
    ],
});
Ext.define('PVE.form.HotplugFeatureSelector', {
    extend: 'Ext.form.CheckboxGroup',
    alias: 'widget.pveHotplugFeatureSelector',
    columns: 1,
    vertical: true,
    defaults: {
	name: 'hotplugCbGroup',
	submitValue: false,
    },
    items: [
	{
	    boxLabel: gettext('Disk'),
	    inputValue: 'disk',
	    checked: true,
	},
	{
	    boxLabel: gettext('Network'),
	    inputValue: 'network',
	    checked: true,
	},
	{
	    boxLabel: 'USB',
	    inputValue: 'usb',
	    checked: true,
	},
	{
	    boxLabel: gettext('Memory'),
	    inputValue: 'memory',
	},
	{
	    boxLabel: gettext('CPU'),
	    inputValue: 'cpu',
	},
    ],
    setValue: function(value) {
	var me = this;
	var newVal = [];
	if (value === '1') {
	    newVal = ['disk', 'network', 'usb'];
	} else if (value !== '0') {
	    newVal = value.split(',');
	}
	me.callParent([{ hotplugCbGroup: newVal }]);
    },
    // override framework function to
    // assemble the hotplug value
    getSubmitData: function() {
	var me = this,
	boxes = me.getBoxes(),
	data = [];
	Ext.Array.forEach(boxes, function(box) {
	    if (box.getValue()) {
		data.push(box.inputValue);
	    }
	});
	/* because above is hotplug an array */
	if (data.length === 0) {
	    return { 'hotplug': '0' };
	} else {
	    return { 'hotplug': data.join(',') };
	}
    },
});
Ext.define('PVE.form.IPProtocolSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveIPProtocolSelector'],
    valueField: 'p',
    displayField: 'p',
    listConfig: {
	columns: [
	    {
		header: gettext('Protocol'),
		dataIndex: 'p',
		hideable: false,
		sortable: false,
		width: 100,
	    },
	    {
		header: gettext('Number'),
		dataIndex: 'n',
		hideable: false,
		sortable: false,
		width: 50,
	    },
	    {
		header: gettext('Description'),
		dataIndex: 'd',
		hideable: false,
		sortable: false,
		flex: 1,
	    },
	],
    },
    store: {
	    fields: ['p', 'd', 'n'],
	    data: [
		{ p: 'tcp', n: 6, d: 'Transmission Control Protocol' },
		{ p: 'udp', n: 17, d: 'User Datagram Protocol' },
		{ p: 'icmp', n: 1, d: 'Internet Control Message Protocol' },
		{ p: 'igmp', n: 2, d: 'Internet Group Management' },
		{ p: 'ggp', n: 3, d: 'gateway-gateway protocol' },
		{ p: 'ipencap', n: 4, d: 'IP encapsulated in IP' },
		{ p: 'st', n: 5, d: 'ST datagram mode' },
		{ p: 'egp', n: 8, d: 'exterior gateway protocol' },
		{ p: 'igp', n: 9, d: 'any private interior gateway (Cisco)' },
		{ p: 'pup', n: 12, d: 'PARC universal packet protocol' },
		{ p: 'hmp', n: 20, d: 'host monitoring protocol' },
		{ p: 'xns-idp', n: 22, d: 'Xerox NS IDP' },
		{ p: 'rdp', n: 27, d: '"reliable datagram" protocol' },
		{ p: 'iso-tp4', n: 29, d: 'ISO Transport Protocol class 4 [RFC905]' },
		{ p: 'dccp', n: 33, d: 'Datagram Congestion Control Prot. [RFC4340]' },
		{ p: 'xtp', n: 36, d: 'Xpress Transfer Protocol' },
		{ p: 'ddp', n: 37, d: 'Datagram Delivery Protocol' },
		{ p: 'idpr-cmtp', n: 38, d: 'IDPR Control Message Transport' },
		{ p: 'ipv6', n: 41, d: 'Internet Protocol, version 6' },
		{ p: 'ipv6-route', n: 43, d: 'Routing Header for IPv6' },
		{ p: 'ipv6-frag', n: 44, d: 'Fragment Header for IPv6' },
		{ p: 'idrp', n: 45, d: 'Inter-Domain Routing Protocol' },
		{ p: 'rsvp', n: 46, d: 'Reservation Protocol' },
		{ p: 'gre', n: 47, d: 'General Routing Encapsulation' },
		{ p: 'esp', n: 50, d: 'Encap Security Payload [RFC2406]' },
		{ p: 'ah', n: 51, d: 'Authentication Header [RFC2402]' },
		{ p: 'skip', n: 57, d: 'SKIP' },
		{ p: 'ipv6-icmp', n: 58, d: 'ICMP for IPv6' },
		{ p: 'ipv6-nonxt', n: 59, d: 'No Next Header for IPv6' },
		{ p: 'ipv6-opts', n: 60, d: 'Destination Options for IPv6' },
		{ p: 'vmtp', n: 81, d: 'Versatile Message Transport' },
		{ p: 'eigrp', n: 88, d: 'Enhanced Interior Routing Protocol (Cisco)' },
		{ p: 'ospf', n: 89, d: 'Open Shortest Path First IGP' },
		{ p: 'ax.25', n: 93, d: 'AX.25 frames' },
		{ p: 'ipip', n: 94, d: 'IP-within-IP Encapsulation Protocol' },
		{ p: 'etherip', n: 97, d: 'Ethernet-within-IP Encapsulation [RFC3378]' },
		{ p: 'encap', n: 98, d: 'Yet Another IP encapsulation [RFC1241]' },
		{ p: 'pim', n: 103, d: 'Protocol Independent Multicast' },
		{ p: 'ipcomp', n: 108, d: 'IP Payload Compression Protocol' },
		{ p: 'vrrp', n: 112, d: 'Virtual Router Redundancy Protocol [RFC5798]' },
		{ p: 'l2tp', n: 115, d: 'Layer Two Tunneling Protocol [RFC2661]' },
		{ p: 'isis', n: 124, d: 'IS-IS over IPv4' },
		{ p: 'sctp', n: 132, d: 'Stream Control Transmission Protocol' },
		{ p: 'fc', n: 133, d: 'Fibre Channel' },
		{ p: 'mobility-header', n: 135, d: 'Mobility Support for IPv6 [RFC3775]' },
		{ p: 'udplite', n: 136, d: 'UDP-Lite [RFC3828]' },
		{ p: 'mpls-in-ip', n: 137, d: 'MPLS-in-IP [RFC4023]' },
		{ p: 'hip', n: 139, d: 'Host Identity Protocol' },
		{ p: 'shim6', n: 140, d: 'Shim6 Protocol [RFC5533]' },
		{ p: 'wesp', n: 141, d: 'Wrapped Encapsulating Security Payload' },
		{ p: 'rohc', n: 142, d: 'Robust Header Compression' },
	    ],
	},
});
Ext.define('PVE.form.IPRefSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveIPRefSelector'],
    base_url: undefined,
    preferredValue: '', // hack: else Form sets dirty flag?
    ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias']
    valueField: 'ref',
    displayField: 'ref',
    notFoundIsValid: true,
    initComponent: function() {
	var me = this;
	if (!me.base_url) {
	    throw "no base_url specified";
	}
	var url = "/api2/json" + me.base_url;
	if (me.ref_type) {
	    url += "?type=" + me.ref_type;
	}
	var store = Ext.create('Ext.data.Store', {
	    autoLoad: true,
	    fields: ['type', 'name', 'ref', 'comment'],
	    idProperty: 'ref',
	    proxy: {
		type: 'proxmox',
		url: url,
	    },
	    sorters: {
		property: 'ref',
		direction: 'ASC',
	    },
	});
	var disable_query_for_ips = function(f, value) {
	    if (value === null ||
		value.match(/^\d/)) { // IP address starts with \d
		f.queryDelay = 9999999999; // hack: disable with long delay
	    } else {
		f.queryDelay = 10;
	    }
	};
	var columns = [];
	if (!me.ref_type) {
	    columns.push({
		header: gettext('Type'),
		dataIndex: 'type',
		hideable: false,
		width: 60,
	    });
	}
	columns.push(
	    {
		header: gettext('Name'),
		dataIndex: 'ref',
		hideable: false,
		width: 140,
	    },
	    {
		header: gettext('Comment'),
		dataIndex: 'comment',
		renderer: Ext.String.htmlEncode,
		flex: 1,
	    },
	);
	Ext.apply(me, {
	    store: store,
            listConfig: { columns: columns },
	});
	me.on('change', disable_query_for_ips);
        me.callParent();
    },
});
Ext.define('PVE.form.MDevSelector', {
    extend: 'Proxmox.form.ComboGrid',
    xtype: 'pveMDevSelector',
    store: {
	fields: ['type', 'available', 'description'],
	filterOnLoad: true,
	sorters: [
	    {
		property: 'type',
		direction: 'ASC',
	    },
	],
    },
    autoSelect: false,
    valueField: 'type',
    displayField: 'type',
    listConfig: {
	width: 550,
	columns: [
	    {
		header: gettext('Type'),
		dataIndex: 'type',
		renderer: function(value, md, rec) {
		    if (rec.data.name !== undefined) {
			return `${rec.data.name} (${value})`;
		    }
		    return value;
		},
		flex: 1,
	    },
	    {
		header: gettext('Avail'),
		dataIndex: 'available',
		width: 60,
	    },
	    {
		header: gettext('Description'),
		dataIndex: 'description',
		flex: 1,
		cellWrap: true,
		renderer: function(value) {
		    if (!value) {
			return '';
		    }
		    return value.split('\n').join('
');
		},
	    },
	],
    },
    setPciID: function(pciid, force) {
	var me = this;
	if (!force && (!pciid || me.pciid === pciid)) {
	    return;
	}
	me.pciid = pciid;
	me.updateProxy();
    },
    setNodename: function(nodename) {
	var me = this;
	if (!nodename || me.nodename === nodename) {
	    return;
	}
	me.nodename = nodename;
	me.updateProxy();
    },
    updateProxy: function() {
	var me = this;
	me.store.setProxy({
	    type: 'proxmox',
	    url: '/api2/json/nodes/' + me.nodename + '/hardware/pci/' + me.pciid + '/mdev',
	});
	me.store.load();
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw 'no node name specified';
	}
        me.callParent();
	if (me.pciid) {
	    me.setPciID(me.pciid, true);
	}
    },
});
Ext.define('PVE.form.MemoryField', {
    extend: 'Ext.form.field.Number',
    alias: 'widget.pveMemoryField',
    allowBlank: false,
    hotplug: false,
    minValue: 32,
    maxValue: 4178944,
    step: 32,
    value: '512', // qm backend default
    allowDecimals: false,
    allowExponential: false,
    computeUpDown: function(value) {
	var me = this;
	if (!me.hotplug) {
	    return { up: value + me.step, down: value - me.step };
	}
	var dimm_size = 512;
	var prev_dimm_size = 0;
	var min_size = 1024;
	var current_size = min_size;
	var value_up = min_size;
	var value_down = min_size;
	var value_start = min_size;
	var i, j;
	for (j = 0; j < 9; j++) {
	    for (i = 0; i < 32; i++) {
		if (value >= current_size && value < current_size + dimm_size) {
		    value_start = current_size;
		    value_up = current_size + dimm_size;
		    value_down = current_size - (i === 0 ? prev_dimm_size : dimm_size);
		}
		current_size += dimm_size;
	    }
	    prev_dimm_size = dimm_size;
	    dimm_size = dimm_size*2;
	}
	return { up: value_up, down: value_down, start: value_start };
    },
    onSpinUp: function() {
	var me = this;
	if (!me.readOnly) {
	    var res = me.computeUpDown(me.getValue());
	    me.setValue(Ext.Number.constrain(res.up, me.minValue, me.maxValue));
	}
    },
    onSpinDown: function() {
	var me = this;
	if (!me.readOnly) {
	    var res = me.computeUpDown(me.getValue());
	    me.setValue(Ext.Number.constrain(res.down, me.minValue, me.maxValue));
	}
    },
    initComponent: function() {
        var me = this;
	if (me.hotplug) {
	    me.minValue = 1024;
	    me.on('blur', function(field) {
		var value = me.getValue();
		var res = me.computeUpDown(value);
		if (value === res.start || value === res.up || value === res.down) {
		    return;
		}
		field.setValue(res.up);
	    });
	}
        me.callParent();
    },
});
Ext.define('PVE.form.NetworkCardSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: 'widget.pveNetworkCardSelector',
    comboItems: [
	['e1000', 'Intel E1000'],
	['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'],
	['rtl8139', 'Realtek RTL8139'],
	['vmxnet3', 'VMware vmxnet3'],
    ],
});
Ext.define('PVE.form.NodeSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveNodeSelector'],
    // invalidate nodes which are offline
    onlineValidator: false,
    selectCurNode: false,
    // do not allow those nodes (array)
    disallowedNodes: undefined,
    // only allow those nodes (array)
    allowedNodes: undefined,
    // set default value to empty array, else it inits it with
    // null and after the store load it is an empty array,
    // triggering dirtychange
    value: [],
    valueField: 'node',
    displayField: 'node',
    store: {
	    fields: ['node', 'cpu', 'maxcpu', 'mem', 'maxmem', 'uptime'],
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/nodes',
	    },
	    sorters: [
		{
		    property: 'node',
		    direction: 'ASC',
		},
		{
		    property: 'mem',
		    direction: 'DESC',
		},
	    ],
	},
    listConfig: {
	columns: [
	    {
		header: gettext('Node'),
		dataIndex: 'node',
		sortable: true,
		hideable: false,
		flex: 1,
	    },
	    {
		header: gettext('Memory usage') + " %",
		renderer: PVE.Utils.render_mem_usage_percent,
		sortable: true,
		width: 100,
		dataIndex: 'mem',
	    },
	    {
		header: gettext('CPU usage'),
		renderer: Proxmox.Utils.render_cpu,
		sortable: true,
		width: 100,
		dataIndex: 'cpu',
	    },
	],
    },
    validator: function(value) {
	let me = this;
	if (!me.onlineValidator || (me.allowBlank && !value)) {
	    return true;
	}
	let offline = [], notAllowed = [];
	Ext.Array.each(value.split(/\s*,\s*/), function(node) {
	    let rec = me.store.findRecord(me.valueField, node, 0, false, true, true);
	    if (!(rec && rec.data) || rec.data.status !== 'online') {
		offline.push(node);
	    } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) {
		notAllowed.push(node);
	    }
	});
	if (value && notAllowed.length !== 0) {
	    return "Node " + notAllowed.join(', ') + " is not allowed for this action!";
	}
	if (value && offline.length !== 0) {
	    return "Node " + offline.join(', ') + " seems to be offline!";
	}
	return true;
    },
    initComponent: function() {
	var me = this;
	if (me.selectCurNode && PVE.curSelectedNode && PVE.curSelectedNode.data.node) {
	    me.preferredValue = PVE.curSelectedNode.data.node;
	}
	me.callParent();
	me.getStore().load();
	me.getStore().addFilter(new Ext.util.Filter({ // filter out disallowed nodes
	    filterFn: (item) => !(me.disallowedNodes && me.disallowedNodes.includes(item.data.node)),
	}));
	me.mon(me.getStore(), 'load', () => me.isValid());
    },
});
Ext.define('PVE.form.PCISelector', {
    extend: 'Proxmox.form.ComboGrid',
    xtype: 'pvePCISelector',
    store: {
	fields: ['id', 'vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev'],
	filterOnLoad: true,
	sorters: [
	    {
		property: 'id',
		direction: 'ASC',
	    },
	],
    },
    autoSelect: false,
    valueField: 'id',
    displayField: 'id',
    // can contain a load callback for the store
    // useful to determine the state of the IOMMU
    onLoadCallBack: undefined,
    listConfig: {
	width: 800,
	columns: [
	    {
		header: 'ID',
		dataIndex: 'id',
		width: 100,
	    },
	    {
		header: gettext('IOMMU Group'),
		dataIndex: 'iommugroup',
		renderer: v => v === -1 ? '-' : v,
		width: 75,
	    },
	    {
		header: gettext('Vendor'),
		dataIndex: 'vendor_name',
		flex: 2,
	    },
	    {
		header: gettext('Device'),
		dataIndex: 'device_name',
		flex: 6,
	    },
	    {
		header: gettext('Mediated Devices'),
		dataIndex: 'mdev',
		flex: 1,
		renderer: function(val) {
		    return Proxmox.Utils.format_boolean(!!val);
		},
	    },
	],
    },
    setNodename: function(nodename) {
	var me = this;
	if (!nodename || me.nodename === nodename) {
	    return;
	}
	me.nodename = nodename;
	me.store.setProxy({
	    type: 'proxmox',
	    url: '/api2/json/nodes/' + me.nodename + '/hardware/pci',
	});
	me.store.load();
    },
    initComponent: function() {
	var me = this;
	var nodename = me.nodename;
	me.nodename = undefined;
        me.callParent();
	if (me.onLoadCallBack !== undefined) {
	    me.mon(me.getStore(), 'load', me.onLoadCallBack);
	}
	me.setNodename(nodename);
    },
});
Ext.define('PVE.form.PermPathSelector', {
    extend: 'Ext.form.field.ComboBox',
    xtype: 'pvePermPathSelector',
    valueField: 'value',
    displayField: 'value',
    typeAhead: true,
    queryMode: 'local',
    store: {
	type: 'pvePermPath',
    },
});
Ext.define('PVE.form.PoolSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pvePoolSelector'],
    allowBlank: false,
    valueField: 'poolid',
    displayField: 'poolid',
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-pools',
	    sorters: 'poolid',
	});
	Ext.apply(me, {
	    store: store,
	    autoSelect: false,
            listConfig: {
		columns: [
		    {
			header: gettext('Pool'),
			sortable: true,
			dataIndex: 'poolid',
			flex: 1,
		    },
		    {
			header: gettext('Comment'),
			sortable: false,
			dataIndex: 'comment',
			renderer: Ext.String.htmlEncode,
			flex: 1,
		    },
		],
	    },
	});
        me.callParent();
	store.load();
    },
}, function() {
    Ext.define('pve-pools', {
	extend: 'Ext.data.Model',
	fields: ['poolid', 'comment'],
	proxy: {
            type: 'proxmox',
	    url: "/api2/json/pools",
	},
	idProperty: 'poolid',
    });
});
Ext.define('PVE.form.preallocationSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pvePreallocationSelector'],
    comboItems: [
	['__default__', Proxmox.Utils.defaultText],
	['off', 'Off'],
	['metadata', 'Metadata'],
	['falloc', 'Full (posix_fallocate)'],
	['full', 'Full'],
    ],
});
Ext.define('PVE.form.PrivilegesSelector', {
    extend: 'Proxmox.form.KVComboBox',
    xtype: 'pvePrivilegesSelector',
    multiSelect: true,
    initComponent: function() {
	let me = this;
	me.callParent();
	Proxmox.Utils.API2Request({
	    url: '/access/roles/Administrator',
	    method: 'GET',
	    success: function(response, options) {
		let data = Object.keys(response.result.data).map(key => [key, key]);
		me.store.setData(data);
		me.store.sort({
		    property: 'key',
		    direction: 'ASC',
		});
	    },
	    failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
	});
    },
});
Ext.define('PVE.form.QemuBiosSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveQemuBiosSelector'],
    initComponent: function() {
	var me = this;
        me.comboItems = [
	    ['__default__', PVE.Utils.render_qemu_bios('')],
	    ['seabios', PVE.Utils.render_qemu_bios('seabios')],
	    ['ovmf', PVE.Utils.render_qemu_bios('ovmf')],
	];
	me.callParent();
    },
});
Ext.define('PVE.form.SDNControllerSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveSDNControllerSelector'],
    allowBlank: false,
    valueField: 'controller',
    displayField: 'controller',
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-sdn-controller',
            sorters: {
                property: 'controller',
                direction: 'ASC',
            },
	});
	Ext.apply(me, {
	    store: store,
	    autoSelect: false,
            listConfig: {
		columns: [
		    {
			header: gettext('Controller'),
			sortable: true,
			dataIndex: 'controller',
			flex: 1,
		    },
		],
	    },
	});
        me.callParent();
	store.load();
    },
}, function() {
    Ext.define('pve-sdn-controller', {
	extend: 'Ext.data.Model',
	fields: ['controller'],
	proxy: {
            type: 'proxmox',
	    url: "/api2/json/cluster/sdn/controllers",
	},
	idProperty: 'controller',
    });
});
Ext.define('PVE.form.SDNZoneSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveSDNZoneSelector'],
    allowBlank: false,
    valueField: 'zone',
    displayField: 'zone',
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-sdn-zone',
            sorters: {
                property: 'zone',
                direction: 'ASC',
            },
	});
	Ext.apply(me, {
	    store: store,
	    autoSelect: false,
            listConfig: {
		columns: [
		    {
			header: gettext('Zone'),
			sortable: true,
			dataIndex: 'zone',
			flex: 1,
		    },
		],
	    },
	});
        me.callParent();
	store.load();
    },
}, function() {
    Ext.define('pve-sdn-zone', {
	extend: 'Ext.data.Model',
	fields: ['zone'],
	proxy: {
            type: 'proxmox',
	    url: "/api2/json/cluster/sdn/zones",
	},
	idProperty: 'zone',
    });
});
Ext.define('PVE.form.SDNVnetSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveSDNVnetSelector'],
    allowBlank: false,
    valueField: 'vnet',
    displayField: 'vnet',
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-sdn-vnet',
            sorters: {
                property: 'vnet',
                direction: 'ASC',
            },
	});
	Ext.apply(me, {
	    store: store,
	    autoSelect: false,
            listConfig: {
		columns: [
		    {
			header: gettext('Vnet'),
			sortable: true,
			dataIndex: 'vnet',
			flex: 1,
		    },
		    {
			header: gettext('Alias'),
			flex: 1,
			dataIndex: 'alias',
		    },
		    {
			header: gettext('Tag'),
			flex: 1,
			dataIndex: 'tag',
		    },
		],
	    },
	});
        me.callParent();
	store.load();
    },
}, function() {
    Ext.define('pve-sdn-vnet', {
	extend: 'Ext.data.Model',
	fields: [
	    'alias',
	    'tag',
	    'type',
	    'vnet',
	    'zone',
	],
	proxy: {
            type: 'proxmox',
	    url: "/api2/json/cluster/sdn/vnets",
	},
	idProperty: 'vnet',
    });
});
Ext.define('PVE.form.SDNIpamSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveSDNIpamSelector'],
    allowBlank: false,
    valueField: 'ipam',
    displayField: 'ipam',
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-sdn-ipam',
            sorters: {
                property: 'ipam',
                direction: 'ASC',
            },
	});
	Ext.apply(me, {
	    store: store,
	    autoSelect: false,
            listConfig: {
		columns: [
		    {
			header: gettext('Ipam'),
			sortable: true,
			dataIndex: 'ipam',
			flex: 1,
		    },
		],
	    },
	});
        me.callParent();
	store.load();
    },
}, function() {
    Ext.define('pve-sdn-ipam', {
	extend: 'Ext.data.Model',
	fields: ['ipam'],
	proxy: {
            type: 'proxmox',
	    url: "/api2/json/cluster/sdn/ipams",
	},
	idProperty: 'ipam',
    });
});
Ext.define('PVE.form.SDNDnsSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveSDNDnsSelector'],
    allowBlank: false,
    valueField: 'dns',
    displayField: 'dns',
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-sdn-dns',
            sorters: {
                property: 'dns',
                direction: 'ASC',
            },
	});
	Ext.apply(me, {
	    store: store,
	    autoSelect: false,
            listConfig: {
		columns: [
		    {
			header: gettext('dns'),
			sortable: true,
			dataIndex: 'dns',
			flex: 1,
		    },
		],
	    },
	});
        me.callParent();
	store.load();
    },
}, function() {
    Ext.define('pve-sdn-dns', {
	extend: 'Ext.data.Model',
	fields: ['dns'],
	proxy: {
            type: 'proxmox',
	    url: "/api2/json/cluster/sdn/dns",
	},
	idProperty: 'dns',
    });
});
Ext.define('PVE.form.ScsiHwSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveScsiHwSelector'],
    comboItems: [
	['__default__', PVE.Utils.render_scsihw('')],
	['lsi', PVE.Utils.render_scsihw('lsi')],
	['lsi53c810', PVE.Utils.render_scsihw('lsi53c810')],
	['megasas', PVE.Utils.render_scsihw('megasas')],
	['virtio-scsi-pci', PVE.Utils.render_scsihw('virtio-scsi-pci')],
	['virtio-scsi-single', PVE.Utils.render_scsihw('virtio-scsi-single')],
	['pvscsi', PVE.Utils.render_scsihw('pvscsi')],
    ],
});
Ext.define('PVE.form.SecurityGroupsSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveSecurityGroupsSelector'],
    valueField: 'group',
    displayField: 'group',
    initComponent: function() {
	var me = this;
	var store = Ext.create('Ext.data.Store', {
	    autoLoad: true,
	    fields: ['group', 'comment'],
	    idProperty: 'group',
	    proxy: {
		type: 'proxmox',
		url: "/api2/json/cluster/firewall/groups",
	    },
	    sorters: {
		property: 'group',
		direction: 'ASC',
	    },
	});
	Ext.apply(me, {
	    store: store,
            listConfig: {
		columns: [
		    {
			header: gettext('Security Group'),
			dataIndex: 'group',
			hideable: false,
			width: 100,
		    },
		    {
			header: gettext('Comment'),
			dataIndex: 'comment',
			renderer: Ext.String.htmlEncode,
			flex: 1,
		    },
		],
	    },
	});
        me.callParent();
    },
});
Ext.define('PVE.form.SnapshotSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.PVE.form.SnapshotSelector'],
    valueField: 'name',
    displayField: 'name',
    loadStore: function(nodename, vmid) {
	var me = this;
	if (!nodename) {
	    return;
	}
	me.nodename = nodename;
        if (!vmid) {
	    return;
        }
	me.vmid = vmid;
	me.store.setProxy({
	    type: 'proxmox',
	    url: '/api2/json/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid +'/snapshot',
	});
	me.store.load();
    },
    initComponent: function() {
	var me = this;
        if (!me.nodename) {
            throw "no node name specified";
        }
        if (!me.vmid) {
            throw "no VM ID specified";
        }
	if (!me.guestType) {
	    throw "no guest type specified";
	}
	var store = Ext.create('Ext.data.Store', {
	    fields: ['name'],
	    filterOnLoad: true,
	});
	Ext.apply(me, {
	    store: store,
            listConfig: {
		columns: [
		    {
			header: gettext('Snapshot'),
			dataIndex: 'name',
			hideable: false,
			flex: 1,
		    },
		],
	    },
	});
        me.callParent();
	me.loadStore(me.nodename, me.vmid);
    },
});
Ext.define('PVE.form.SpiceEnhancementSelector', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveSpiceEnhancementSelector',
    viewModel: {},
    items: [
	{
	    xtype: 'proxmoxcheckbox',
	    itemId: 'foldersharing',
	    name: 'foldersharing',
	    reference: 'foldersharing',
	    fieldLabel: 'Folder Sharing',
	    uncheckedValue: 0,
	},
	{
	    xtype: 'proxmoxKVComboBox',
	    itemId: 'videostreaming',
	    name: 'videostreaming',
	    value: 'off',
	    fieldLabel: 'Video Streaming',
	    comboItems: [
		['off', 'off'],
		['all', 'all'],
		['filter', 'filter'],
	    ],
	},
	{
	    xtype: 'displayfield',
	    itemId: 'spicehint',
	    userCls: 'pmx-hint',
	    value: gettext('To use these features set the display to SPICE in the hardware settings of the VM.'),
	    hidden: true,
	},
	{
	    xtype: 'displayfield',
	    itemId: 'spicefolderhint',
	    userCls: 'pmx-hint',
	    value: gettext('Make sure the SPICE WebDav daemon is installed in the VM.'),
	    bind: {
		hidden: '{!foldersharing.checked}',
	    },
	},
    ],
    onGetValues: function(values) {
	var ret = {};
	if (values.videostreaming !== "off") {
	    ret.videostreaming = values.videostreaming;
	}
	if (values.foldersharing) {
	    ret.foldersharing = 1;
	}
	if (Ext.Object.isEmpty(ret)) {
	    return { 'delete': 'spice_enhancements' };
	}
	var enhancements = PVE.Parser.printPropertyString(ret);
	return { spice_enhancements: enhancements };
    },
    setValues: function(values) {
	var vga = PVE.Parser.parsePropertyString(values.vga, 'type');
	if (!/^qxl\d?$/.test(vga.type)) {
	    this.down('#spicehint').setVisible(true);
	}
	if (values.spice_enhancements) {
	    var enhancements = PVE.Parser.parsePropertyString(values.spice_enhancements);
	    enhancements.foldersharing = PVE.Parser.parseBoolean(enhancements.foldersharing, 0);
	    this.callParent([enhancements]);
	}
    },
});
Ext.define('PVE.form.StorageScanNodeSelector', {
    extend: 'PVE.form.NodeSelector',
    xtype: 'pveStorageScanNodeSelector',
    name: 'storageScanNode',
    itemId: 'pveStorageScanNodeSelector',
    fieldLabel: gettext('Scan node'),
    allowBlank: true,
    disallowedNodes: undefined,
    autoSelect: false,
    submitValue: false,
    value: null,
    autoEl: {
	tag: 'div',
	'data-qtip': gettext('Scan for available storages on the selected node'),
    },
    triggers: {
	clear: {
	    handler: function() {
		let me = this;
		me.setValue(null);
	    },
	},
    },
    emptyText: Proxmox.NodeName,
    setValue: function(value) {
	let me = this;
	me.callParent([value]);
	me.triggers.clear.setVisible(!!value);
    },
});
Ext.define('PVE.form.StorageSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: 'widget.pveStorageSelector',
    mixins: ['Proxmox.Mixin.CBind'],
    cbindData: {
	clusterView: false,
    },
    allowBlank: false,
    valueField: 'storage',
    displayField: 'storage',
    listConfig: {
	cbind: {
	    clusterView: '{clusterView}',
	},
	width: 450,
	columns: [
	    {
		header: gettext('Name'),
		dataIndex: 'storage',
		hideable: false,
		flex: 1,
	    },
	    {
		header: gettext('Type'),
		width: 75,
		dataIndex: 'type',
	    },
	    {
		header: gettext('Avail'),
		width: 90,
		dataIndex: 'avail',
		renderer: Proxmox.Utils.format_size,
		cbind: {
		    hidden: '{clusterView}',
		},
	    },
	    {
		header: gettext('Capacity'),
		width: 90,
		dataIndex: 'total',
		renderer: Proxmox.Utils.format_size,
		cbind: {
		    hidden: '{clusterView}',
		},
	    },
	    {
		header: gettext('Nodes'),
		width: 120,
		dataIndex: 'nodes',
		renderer: (value) => value ? value : '-- ' + gettext('All') + ' --',
		cbind: {
		    hidden: '{!clusterView}',
		},
	    },
	    {
		header: gettext('Shared'),
		width: 70,
		dataIndex: 'shared',
		renderer: Proxmox.Utils.format_boolean,
		cbind: {
		    hidden: '{!clusterView}',
		},
	    },
	],
    },
    reloadStorageList: function() {
	let me = this;
	if (me.clusterView) {
	    me.getStore().setProxy({
		type: 'proxmox',
		url: `/api2/json/storage`,
	    });
	    // filter here, back-end does not support it currently
	    let filters = [(storage) => !storage.data.disable];
	    if (me.storageContent) {
		filters.push(
		    (storage) => storage.data.content.split(',').includes(me.storageContent),
		);
	    }
	    if (me.nodename) {
		filters.push(
		    (storage) => !storage.data.nodes || storage.data.nodes.includes(me.nodename),
		);
	    }
	    me.getStore().clearFilter();
	    me.getStore().setFilters(filters);
	} else {
	    if (!me.nodename) {
		return;
	    }
	    let params = {
		format: 1,
	    };
	    if (me.storageContent) {
		params.content = me.storageContent;
	    }
	    if (me.targetNode) {
		params.target = me.targetNode;
		params.enabled = 1; // skip disabled storages
	    }
	    me.store.setProxy({
		type: 'proxmox',
		url: `/api2/json/nodes/${me.nodename}/storage`,
		extraParams: params,
	    });
	}
	me.store.load(() => me.validate());
    },
    setTargetNode: function(targetNode) {
	var me = this;
	if (!targetNode || me.targetNode === targetNode) {
	    return;
	}
	if (me.clusterView) {
	    throw "setting targetNode with clusterView is not implemented";
	}
	me.targetNode = targetNode;
	me.reloadStorageList();
    },
    setNodename: function(nodename) {
	var me = this;
	nodename = nodename || '';
	if (me.nodename === nodename) {
	    return;
	}
	me.nodename = nodename;
	me.reloadStorageList();
    },
    initComponent: function() {
	var me = this;
	let nodename = me.nodename;
	me.nodename = undefined;
	var store = Ext.create('Ext.data.Store', {
	    model: 'pve-storage-status',
	    sorters: {
		property: 'storage',
		direction: 'ASC',
	    },
	});
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
	me.setNodename(nodename);
    },
}, function() {
    Ext.define('pve-storage-status', {
	extend: 'Ext.data.Model',
	fields: ['storage', 'active', 'type', 'avail', 'total', 'nodes', 'shared'],
	idProperty: 'storage',
    });
});
Ext.define('PVE.form.TFASelector', {
    extend: 'Ext.container.Container',
    xtype: 'pveTFASelector',
    mixins: ['Proxmox.Mixin.CBind'],
    deleteEmpty: true,
    viewModel: {
	data: {
	    type: '__default__',
	    step: null,
	    digits: null,
	    id: null,
	    key: null,
	    url: null,
	},
	formulas: {
	    isOath: (get) => get('type') === 'oath',
	    isYubico: (get) => get('type') === 'yubico',
	    tfavalue: {
		get: function(get) {
		    let val = {
			type: get('type'),
		    };
		    if (get('isOath')) {
			let step = get('step');
			let digits = get('digits');
			if (step) {
			    val.step = step;
			}
			if (digits) {
			    val.digits = digits;
			}
		    } else if (get('isYubico')) {
			let id = get('id');
			let key = get('key');
			let url = get('url');
			val.id = id;
			val.key = key;
			if (url) {
			    val.url = url;
			}
		    } else if (val.type === '__default__') {
			return "";
		    }
		    return PVE.Parser.printPropertyString(val);
		},
		set: function(value) {
		    let val = PVE.Parser.parseTfaConfig(value);
		    this.set(val);
		    this.notify();
		    // we need to reset the original values, so that
		    // we can reliably track the state of the form
		    let form = this.getView().up('form');
		    if (form.trackResetOnLoad) {
			let fields = this.getView().query('field[name!="tfa"]');
			fields.forEach((field) => field.resetOriginalValue());
		    }
		},
	    },
	},
    },
    items: [
	{
	    xtype: 'proxmoxtextfield',
	    name: 'tfa',
	    hidden: true,
	    submitValue: true,
	    cbind: {
		deleteEmpty: '{deleteEmpty}',
	    },
	    bind: {
		value: "{tfavalue}",
	    },
	},
	{
	    xtype: 'proxmoxKVComboBox',
	    value: '__default__',
	    deleteEmpty: false,
	    submitValue: false,
	    fieldLabel: gettext('Require TFA'),
	    comboItems: [
		['__default__', Proxmox.Utils.noneText],
		['oath', 'OATH/TOTP'],
		['yubico', 'Yubico'],
	    ],
	    bind: {
		value: "{type}",
	    },
	},
	{
	    xtype: 'proxmoxintegerfield',
	    hidden: true,
	    minValue: 10,
	    submitValue: false,
	    emptyText: Proxmox.Utils.defaultText + ' (30)',
	    fieldLabel: gettext('Time Step'),
	    bind: {
		value: "{step}",
		hidden: "{!isOath}",
		disabled: "{!isOath}",
	    },
	},
	{
	    xtype: 'proxmoxintegerfield',
	    hidden: true,
	    submitValue: false,
	    fieldLabel: gettext('Secret Length'),
	    minValue: 6,
	    maxValue: 8,
	    emptyText: Proxmox.Utils.defaultText + ' (6)',
	    bind: {
		value: "{digits}",
		hidden: "{!isOath}",
		disabled: "{!isOath}",
	    },
	},
	{
	    xtype: 'textfield',
	    hidden: true,
	    submitValue: false,
	    allowBlank: false,
	    fieldLabel: 'Yubico API Id',
	    bind: {
		value: "{id}",
		hidden: "{!isYubico}",
		disabled: "{!isYubico}",
	    },
	},
	{
	    xtype: 'textfield',
	    hidden: true,
	    submitValue: false,
	    allowBlank: false,
	    fieldLabel: 'Yubico API Key',
	    bind: {
		value: "{key}",
		hidden: "{!isYubico}",
		disabled: "{!isYubico}",
	    },
	},
	{
	    xtype: 'textfield',
	    hidden: true,
	    submitValue: false,
	    fieldLabel: 'Yubico URL',
	    bind: {
		value: "{url}",
		hidden: "{!isYubico}",
		disabled: "{!isYubico}",
	    },
	},
    ],
});
Ext.define('PVE.form.TokenSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveTokenSelector'],
    allowBlank: false,
    autoSelect: false,
    displayField: 'id',
    editable: true,
    anyMatch: true,
    forceSelection: true,
    store: {
	model: 'pve-tokens',
	autoLoad: true,
	proxy: {
	    type: 'proxmox',
	    url: 'api2/json/access/users',
	    extraParams: { 'full': 1 },
	},
	sorters: 'id',
	listeners: {
	    load: function(store, records, success) {
		let tokens = [];
		for (const { data: user } of records) {
		    if (!user.tokens || user.tokens.length === 0) {
			continue;
		    }
		    for (const token of user.tokens) {
			tokens.push({
			    id: `${user.userid}!${token.tokenid}`,
			    comment: token.comment,
			});
		    }
		}
		store.loadData(tokens);
	    },
	},
    },
    listConfig: {
	columns: [
	    {
		header: gettext('API Token'),
		sortable: true,
		dataIndex: 'id',
		renderer: Ext.String.htmlEncode,
		flex: 1,
	    },
	    {
		header: gettext('Comment'),
		sortable: false,
		dataIndex: 'comment',
		renderer: Ext.String.htmlEncode,
		flex: 1,
	    },
	],
    },
}, function() {
    Ext.define('pve-tokens', {
	extend: 'Ext.data.Model',
	fields: [
	    'id', 'userid', 'tokenid', 'comment',
	    { type: 'boolean', name: 'privsep' },
	    { type: 'date', dateFormat: 'timestamp', name: 'expire' },
	],
	idProperty: 'id',
    });
});
Ext.define('PVE.form.USBSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveUSBSelector'],
    allowBlank: false,
    autoSelect: false,
    anyMatch: true,
    displayField: 'product_and_id',
    valueField: 'usbid',
    editable: true,
    validator: function(value) {
	var me = this;
	if (!value) {
	    return true; // handled later by allowEmpty in the getErrors call chain
	}
	value = me.getValue(); // as the valueField is not the displayfield
	if (me.type === 'device') {
	    return (/^[a-f0-9]{4}:[a-f0-9]{4}$/i).test(value);
	} else if (me.type === 'port') {
	    return (/^[0-9]+-[0-9]+(\.[0-9]+)*$/).test(value);
	}
	return gettext("Invalid Value");
    },
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no nodename specified";
	}
	if (me.type !== 'device' && me.type !== 'port') {
	    throw "no valid type specified";
	}
	let store = new Ext.data.Store({
	    model: `pve-usb-${me.type}`,
	    proxy: {
		type: 'proxmox',
		url: `/api2/json/nodes/${nodename}/hardware/usb`,
	    },
	    filters: [
		({ data }) => !!data.usbpath && !!data.prodid && String(data.class) !== "9",
	    ],
	});
	let emptyText = '';
	if (me.type === 'device') {
	    emptyText = gettext('Passthrough a specific device');
	} else {
	    emptyText = gettext('Passthrough a full port');
	}
	Ext.apply(me, {
	    store: store,
	    emptyText: emptyText,
	    listConfig: {
		width: 520,
		columns: [
		    {
			header: me.type === 'device'?gettext('Device'):gettext('Port'),
			sortable: true,
			dataIndex: 'usbid',
			width: 80,
		    },
		    {
			header: gettext('Manufacturer'),
			sortable: true,
			dataIndex: 'manufacturer',
			width: 150,
		    },
		    {
			header: gettext('Product'),
			sortable: true,
			dataIndex: 'product',
			flex: 1,
		    },
		    {
			header: gettext('Speed'),
			width: 75,
			sortable: true,
			dataIndex: 'speed',
			renderer: function(value) {
			    let speed2Class = {
				"10000": "USB 3.1",
				"5000": "USB 3.0",
				"480": "USB 2.0",
				"12": "USB 1.x",
				"1.5": "USB 1.x",
			    };
			    return speed2Class[value] || value + " Mbps";
			},
		    },
		],
	    },
	});
	me.callParent();
	store.load();
    },
}, function() {
    Ext.define('pve-usb-device', {
	extend: 'Ext.data.Model',
	fields: [
	    {
		name: 'usbid',
		convert: function(val, data) {
		    if (val) {
			return val;
		    }
		    return data.get('vendid') + ':' + data.get('prodid');
		},
	    },
	    'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath',
	    { name: 'port', type: 'number' },
	    { name: 'level', type: 'number' },
	    { name: 'class', type: 'number' },
	    { name: 'devnum', type: 'number' },
	    { name: 'busnum', type: 'number' },
	    {
		name: 'product_and_id',
		type: 'string',
		convert: (v, rec) => {
		    let res = rec.data.product || gettext('Unkown');
		    res += " (" + rec.data.usbid + ")";
		    return res;
		},
	    },
	],
    });
    Ext.define('pve-usb-port', {
	extend: 'Ext.data.Model',
	fields: [
	    {
		name: 'usbid',
		convert: function(val, data) {
		    if (val) {
			return val;
		    }
		    return data.get('busnum') + '-' + data.get('usbpath');
		},
	    },
	    'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath',
	    { name: 'port', type: 'number' },
	    { name: 'level', type: 'number' },
	    { name: 'class', type: 'number' },
	    { name: 'devnum', type: 'number' },
	    { name: 'busnum', type: 'number' },
	    {
		name: 'product_and_id',
		type: 'string',
		convert: (v, rec) => {
		    let res = rec.data.product || gettext('Unplugged');
		    res += " (" + rec.data.usbid + ")";
		    return res;
		},
	    },
	],
    });
});
Ext.define('pmx-users', {
    extend: 'Ext.data.Model',
    fields: [
	'userid', 'firstname', 'lastname', 'email', 'comment',
	{ type: 'boolean', name: 'enable' },
	{ type: 'date', dateFormat: 'timestamp', name: 'expire' },
    ],
    proxy: {
	type: 'proxmox',
	url: "/api2/json/access/users",
    },
    idProperty: 'userid',
});
Ext.define('PVE.form.VlanField', {
    extend: 'Ext.form.field.Number',
    alias: ['widget.pveVlanField'],
    deleteEmpty: false,
    emptyText: 'no VLAN',
    fieldLabel: gettext('VLAN Tag'),
    allowBlank: true,
    getSubmitData: function() {
        var me = this,
            data = null,
            val;
        if (!me.disabled && me.submitValue) {
            val = me.getSubmitValue();
            if (val) {
                data = {};
                data[me.getName()] = val;
            } else if (me.deleteEmpty) {
		data = {};
                data.delete = me.getName();
	    }
        }
        return data;
    },
    initComponent: function() {
	var me = this;
	Ext.apply(me, {
	    minValue: 1,
	    maxValue: 4094,
	});
	me.callParent();
    },
});
Ext.define('PVE.form.VMCPUFlagSelector', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.vmcpuflagselector',
    mixins: {
	field: 'Ext.form.field.Field',
    },
    disableSelection: true,
    columnLines: false,
    selectable: false,
    hideHeaders: true,
    scrollable: 'y',
    height: 200,
    unkownFlags: [],
    store: {
	type: 'store',
	fields: ['flag', { name: 'state', defaultValue: '=' }, 'desc'],
	data: [
	    // FIXME: let qemu-server host this and autogenerate or get from API call??
	    { flag: 'md-clear', desc: 'Required to let the guest OS know if MDS is mitigated correctly' },
	    { flag: 'pcid', desc: 'Meltdown fix cost reduction on Westmere, Sandy-, and IvyBridge Intel CPUs' },
	    { flag: 'spec-ctrl', desc: 'Allows improved Spectre mitigation with Intel CPUs' },
	    { flag: 'ssbd', desc: 'Protection for "Speculative Store Bypass" for Intel models' },
	    { flag: 'ibpb', desc: 'Allows improved Spectre mitigation with AMD CPUs' },
	    { flag: 'virt-ssbd', desc: 'Basis for "Speculative Store Bypass" protection for AMD models' },
	    { flag: 'amd-ssbd', desc: 'Improves Spectre mitigation performance with AMD CPUs, best used with "virt-ssbd"' },
	    { flag: 'amd-no-ssb', desc: 'Notifies guest OS that host is not vulnerable for Spectre on AMD CPUs' },
	    { flag: 'pdpe1gb', desc: 'Allow guest OS to use 1GB size pages, if host HW supports it' },
	    { flag: 'hv-tlbflush', desc: 'Improve performance in overcommitted Windows guests. May lead to guest bluescreens on old CPUs.' },
	    { flag: 'hv-evmcs', desc: 'Improve performance for nested virtualization. Only supported on Intel CPUs.' },
	    { flag: 'aes', desc: 'Activate AES instruction set for HW acceleration.' },
	],
	listeners: {
	    update: function() {
		this.commitChanges();
	    },
	},
    },
    getValue: function() {
	var me = this;
	var store = me.getStore();
	var flags = '';
	// ExtJS does not has a nice getAllRecords interface for stores :/
	store.queryBy(Ext.returnTrue).each(function(rec) {
	    var s = rec.get('state');
	    if (s && s !== '=') {
		var f = rec.get('flag');
		if (flags === '') {
		    flags = s + f;
		} else {
		    flags += ';' + s + f;
		}
	    }
	});
	flags += me.unkownFlags.join(';');
	return flags;
    },
    setValue: function(value) {
	var me = this;
	var store = me.getStore();
	me.value = value || '';
	me.unkownFlags = [];
	me.getStore().queryBy(Ext.returnTrue).each(function(rec) {
	    rec.set('state', '=');
	});
	var flags = value ? value.split(';') : [];
	flags.forEach(function(flag) {
	    var sign = flag.substr(0, 1);
	    flag = flag.substr(1);
	    var rec = store.findRecord('flag', flag, 0, false, true, true);
	    if (rec !== null) {
		rec.set('state', sign);
	    } else {
		me.unkownFlags.push(flag);
	    }
	});
	store.reload();
	var res = me.mixins.field.setValue.call(me, value);
	return res;
    },
    columns: [
	{
	    dataIndex: 'state',
	    renderer: function(v) {
		switch (v) {
		    case '=': return 'Default';
		    case '-': return 'Off';
		    case '+': return 'On';
		    default: return 'Unknown';
		}
	    },
	    width: 65,
	},
	{
	    xtype: 'widgetcolumn',
	    dataIndex: 'state',
	    width: 95,
	    onWidgetAttach: function(column, widget, record) {
		var val = record.get('state') || '=';
		widget.down('[inputValue=' + val + ']').setValue(true);
		// TODO: disable if selected CPU model and flag are incompatible
	    },
	    widget: {
		xtype: 'radiogroup',
		hideLabel: true,
		layout: 'hbox',
		validateOnChange: false,
		value: '=',
		listeners: {
		    change: function(f, value) {
			var v = Object.values(value)[0];
			f.getWidgetRecord().set('state', v);
			var view = this.up('grid');
			view.dirty = view.getValue() !== view.originalValue;
			view.checkDirty();
			//view.checkChange();
		    },
		},
		items: [
		    {
			boxLabel: '-',
			boxLabelAlign: 'before',
			inputValue: '-',
			isFormField: false,
		    },
		    {
			checked: true,
			inputValue: '=',
			isFormField: false,
		    },
		    {
			boxLabel: '+',
			inputValue: '+',
			isFormField: false,
		    },
		],
	    },
	},
	{
	    dataIndex: 'flag',
	    width: 100,
	},
	{
	    dataIndex: 'desc',
	    cellWrap: true,
	    flex: 1,
	},
    ],
    initComponent: function() {
	var me = this;
	// static class store, thus gets not recreated, so ensure defaults are set!
	me.getStore().data.forEach(function(v) {
	    v.state = '=';
	});
	me.value = me.originalValue = '';
	me.callParent(arguments);
    },
});
/* filter is a javascript builtin, but extjs calls it also filter */
Ext.define('PVE.form.VMSelector', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.vmselector',
    mixins: {
	field: 'Ext.form.field.Field',
    },
    allowBlank: true,
    selectAll: false,
    isFormField: true,
    plugins: 'gridfilters',
    store: {
	model: 'PVEResources',
	sorters: 'vmid',
    },
    columnsDeclaration: [
	{
	    header: 'ID',
	    dataIndex: 'vmid',
	    width: 80,
	    filter: {
		type: 'number',
	    },
	},
	{
	    header: gettext('Node'),
	    dataIndex: 'node',
	},
	{
	    header: gettext('Status'),
	    dataIndex: 'status',
	    filter: {
		type: 'list',
	    },
	},
	{
	    header: gettext('Name'),
	    dataIndex: 'name',
	    flex: 1,
	    filter: {
		type: 'string',
	    },
	},
	{
	    header: gettext('Pool'),
	    dataIndex: 'pool',
	    filter: {
		type: 'list',
	    },
	},
	{
	    header: gettext('Type'),
	    dataIndex: 'type',
	    width: 120,
	    renderer: function(value) {
		if (value === 'qemu') {
		    return gettext('Virtual Machine');
		} else if (value === 'lxc') {
		    return gettext('LXC Container');
		}
		return '';
	    },
	    filter: {
		type: 'list',
		store: {
		    data: [
			{ id: 'qemu', text: gettext('Virtual Machine') },
			{ id: 'lxc', text: gettext('LXC Container') },
		    ],
		    un: function() {
			// Due to EXTJS-18711. we have to do a static list via a store but to avoid
			// creating an object, we have to have an empty pseudo un function
		    },
		},
	    },
	},
	{
	    header: 'HA ' + gettext('Status'),
	    dataIndex: 'hastate',
	    flex: 1,
	    filter: {
		type: 'list',
	    },
	},
    ],
    // should be a list of 'dataIndex' values, if 'undefined' all declared columns will be included
    columnSelection: undefined,
    selModel: {
	selType: 'checkboxmodel',
	mode: 'SIMPLE',
    },
    checkChangeEvents: [
	'selectionchange',
	'change',
    ],
    listeners: {
	selectionchange: function() {
	    // to trigger validity and error checks
	    this.checkChange();
	},
    },
    getValue: function() {
	var me = this;
	if (me.savedValue !== undefined) {
	    return me.savedValue;
	}
	var sm = me.getSelectionModel();
	var selection = sm.getSelection();
	var values = [];
	var store = me.getStore();
	selection.forEach(function(item) {
	    // only add if not filtered
	    if (store.findExact('vmid', item.data.vmid) !== -1) {
		values.push(item.data.vmid);
	    }
	});
	return values;
    },
    setValueSelection: function(value) {
	let me = this;
	let store = me.getStore();
	let notFound = [];
	let selection = value.map(item => {
	    let found = store.findRecord('vmid', item, 0, false, true, true);
	    if (!found) {
		notFound.push(item);
	    }
	    return found;
	}).filter(r => r);
	for (const vmid of notFound) {
	    let rec = store.add({
		vmid,
		node: 'unknown',
	    });
	    selection.push(rec[0]);
	}
	let sm = me.getSelectionModel();
	if (selection.length) {
	    sm.select(selection);
	} else {
	    sm.deselectAll();
	}
	// to correctly trigger invalid class
	me.getErrors();
    },
    setValue: function(value) {
	let me = this;
	if (!Ext.isArray(value)) {
	    value = value.split(',');
	}
	let store = me.getStore();
	if (!store.isLoaded()) {
	    me.savedValue = value;
	    store.on('load', function() {
		me.setValueSelection(value);
		delete me.savedValue;
	    }, { single: true });
	} else {
	    me.setValueSelection(value);
	}
	return me.mixins.field.setValue.call(me, value);
    },
    getErrors: function(value) {
	let me = this;
	if (!me.isDisabled() && me.allowBlank === false &&
	    me.getSelectionModel().getCount() === 0) {
	    me.addBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
	    return [gettext('No VM selected')];
	}
	me.removeBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
	return [];
    },
    setDisabled: function(disabled) {
	let me = this;
	let res = me.callParent([disabled]);
	me.getErrors();
	return res;
    },
    initComponent: function() {
	let me = this;
	let columns = me.columnsDeclaration.filter((column) =>
	    me.columnSelection ? me.columnSelection.indexOf(column.dataIndex) !== -1 : true,
	).map((x) => x);
	me.columns = columns;
	me.callParent();
	me.getStore().load({ params: { type: 'vm' } });
	if (me.nodename) {
	    me.getStore().addFilter({
		property: 'node',
		exactMatch: true,
		value: me.nodename,
	    });
	}
	// only show the relevant guests by default
	if (me.action) {
	    var statusfilter = '';
	    switch (me.action) {
		case 'startall':
		    statusfilter = 'stopped';
		    break;
		case 'stopall':
		    statusfilter = 'running';
		    break;
	    }
	    if (statusfilter !== '') {
		me.getStore().addFilter([{
		    property: 'template',
		    value: 0,
		}, {
		    id: 'x-gridfilter-status',
		    operator: 'in',
		    property: 'status',
		    value: [statusfilter],
		}]);
	    }
	}
	if (me.selectAll) {
	    me.mon(me.getStore(), 'load', function() {
		me.getSelectionModel().selectAll(false);
	    });
	}
    },
});
Ext.define('PVE.form.VMComboSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: 'widget.vmComboSelector',
    valueField: 'vmid',
    displayField: 'vmid',
    autoSelect: false,
    editable: true,
    anyMatch: true,
    forceSelection: true,
    store: {
	model: 'PVEResources',
	autoLoad: true,
	sorters: 'vmid',
	filters: [{
	    property: 'type',
	    value: /lxc|qemu/,
	}],
    },
    listConfig: {
	width: 600,
	plugins: 'gridfilters',
	columns: [
	    {
		header: 'ID',
		dataIndex: 'vmid',
		width: 80,
		filter: {
		    type: 'number',
		},
	    },
	    {
		header: gettext('Name'),
		dataIndex: 'name',
		flex: 1,
		filter: {
		    type: 'string',
		},
	    },
	    {
		header: gettext('Node'),
		dataIndex: 'node',
	    },
	    {
		header: gettext('Status'),
		dataIndex: 'status',
		filter: {
		    type: 'list',
		},
	    },
	    {
		header: gettext('Pool'),
		dataIndex: 'pool',
		hidden: true,
		filter: {
		    type: 'list',
		},
	    },
	    {
		header: gettext('Type'),
		dataIndex: 'type',
		width: 120,
		renderer: function(value) {
		    if (value === 'qemu') {
			return gettext('Virtual Machine');
		    } else if (value === 'lxc') {
			return gettext('LXC Container');
		    }
		    return '';
		},
		filter: {
		    type: 'list',
		    store: {
			data: [
			    { id: 'qemu', text: gettext('Virtual Machine') },
			    { id: 'lxc', text: gettext('LXC Container') },
			],
			un: function() { /* due to EXTJS-18711 */ },
		    },
		},
	    },
	    {
		header: 'HA ' + gettext('Status'),
		dataIndex: 'hastate',
		hidden: true,
		flex: 1,
		filter: {
		    type: 'list',
		},
	    },
	],
    },
});
Ext.define('PVE.form.VNCKeyboardSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.VNCKeyboardSelector'],
    comboItems: Object.entries(PVE.Utils.kvm_keymaps),
});
/*
 * Top left combobox, used to select a view of the underneath RessourceTree
 */
Ext.define('PVE.form.ViewSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: ['widget.pveViewSelector'],
    editable: false,
    allowBlank: false,
    forceSelection: true,
    autoSelect: false,
    valueField: 'key',
    displayField: 'value',
    hideLabel: true,
    queryMode: 'local',
    initComponent: function() {
	let me = this;
	let default_views = {
	    server: {
		text: gettext('Server View'),
		groups: ['node'],
	    },
	    folder: {
		text: gettext('Folder View'),
		groups: ['type'],
	    },
	    pool: {
		text: gettext('Pool View'),
		groups: ['pool'],
		// Pool View only lists VMs and Containers
		filterfn: ({ data }) => data.type === 'qemu' || data.type === 'lxc' || data.type === 'pool',
	    },
	};
	let groupdef = Object.entries(default_views).map(([name, config]) => [name, config.text]);
	let store = Ext.create('Ext.data.Store', {
	    model: 'KeyValue',
	    proxy: {
		type: 'memory',
		reader: 'array',
	    },
	    data: groupdef,
	    autoload: true,
	});
	Ext.apply(me, {
	    store: store,
	    value: groupdef[0][0],
	    getViewFilter: function() {
		let view = me.getValue();
		return Ext.apply({ id: view }, default_views[view] || default_views.server);
	    },
	    getState: function() {
		return { value: me.getValue() };
	    },
	    applyState: function(state, doSelect) {
		let view = me.getValue();
		if (state && state.value && view !== state.value) {
		    let record = store.findRecord('key', state.value, 0, false, true, true);
		    if (record) {
			me.setValue(state.value, true);
			if (doSelect) {
			    me.fireEvent('select', me, [record]);
			}
		    }
		}
	    },
	    stateEvents: ['select'],
	    stateful: true,
	    stateId: 'pveview',
	    id: 'view',
	});
	me.callParent();
	let statechange = function(sp, key, value) {
	    if (key === me.id) {
		me.applyState(value, true);
	    }
	};
	let sp = Ext.state.Manager.getProvider();
	me.mon(sp, 'statechange', statechange, me);
    },
});
Ext.define('PVE.form.iScsiProviderSelector', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveiScsiProviderSelector'],
    comboItems: [
	['comstar', 'Comstar'],
	['istgt', 'istgt'],
	['iet', 'IET'],
	['LIO', 'LIO'],
    ],
});
Ext.define('PVE.form.ColorPicker', {
    extend: 'Ext.form.FieldContainer',
    alias: 'widget.pveColorPicker',
    defaultBindProperty: 'value',
    config: {
	value: null,
    },
    height: 24,
    layout: {
	type: 'hbox',
	align: 'stretch',
    },
    getValue: function() {
	return this.realvalue.slice(1);
    },
    setValue: function(value) {
	let me = this;
	me.setColor(value);
	if (value && value.length === 6) {
	    me.picker.value = value[0] !== '#' ? `#${value}` : value;
	}
    },
    setColor: function(value) {
	let me = this;
	let oldValue = me.realvalue;
	me.realvalue = value;
	let color = value.length === 6 ? `#${value}` : undefined;
	me.down('#picker').setStyle('background-color', color);
	me.down('#text').setValue(value ?? "");
	me.fireEvent('change', me, me.realvalue, oldValue);
    },
    initComponent: function() {
	let me = this;
	me.picker = document.createElement('input');
	me.picker.type = 'color';
	me.picker.style = `opacity: 0; border: 0px; width: 100%; height: ${me.height}px`;
	me.picker.value = `${me.value}`;
	me.items = [
	    {
		xtype: 'textfield',
		itemId: 'text',
		minLength: !me.allowBlank ? 6 : undefined,
		maxLength: 6,
		enforceMaxLength: true,
		allowBlank: me.allowBlank,
		emptyText: me.allowBlank ? gettext('Automatic') : undefined,
		maskRe: /[a-f0-9]/i,
		regex: /^[a-f0-9]{6}$/i,
		flex: 1,
		listeners: {
		    change: function(field, value) {
			me.setValue(value);
		    },
		},
	    },
	    {
		xtype: 'box',
		style: {
		    'margin-left': '1px',
		    border: '1px solid #cfcfcf',
		},
		itemId: 'picker',
		width: 24,
		contentEl: me.picker,
	    },
	];
	me.callParent();
	me.picker.oninput = function() {
	    me.setColor(me.picker.value.slice(1));
	};
    },
});
Ext.define('PVE.form.TagColorGrid', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.pveTagColorGrid',
    mixins: [
	'Ext.form.field.Field',
    ],
    allowBlank: true,
    selectAll: false,
    isFormField: true,
    deleteEmpty: false,
    selModel: 'checkboxmodel',
    config: {
	deleteEmpty: false,
    },
    emptyText: gettext('No Overrides'),
    viewConfig: {
	deferEmptyText: false,
    },
    setValue: function(value) {
	let me = this;
	let colors;
	if (Ext.isObject(value)) {
	    colors = value.colors;
	} else {
	    colors = value;
	}
	if (!colors) {
	    me.getStore().removeAll();
	    me.checkChange();
	    return me;
	}
	let entries = (colors.split(';') || []).map((entry) => {
	    let [tag, bg, fg] = entry.split(':');
	    fg = fg || "";
	    return {
		tag,
		color: bg,
		text: fg,
	    };
	});
	me.getStore().setData(entries);
	me.checkChange();
	return me;
    },
    getValue: function() {
	let me = this;
	let values = [];
	me.getStore().each((rec) => {
	    if (rec.data.tag) {
		let val = `${rec.data.tag}:${rec.data.color}`;
		if (rec.data.text) {
		    val += `:${rec.data.text}`;
		}
		values.push(val);
	    }
	});
	return values.join(';');
    },
    getErrors: function(value) {
	let me = this;
	let emptyTag = false;
	let notValidColor = false;
	let colorRegex = new RegExp(/^[0-9a-f]{6}$/i);
	me.getStore().each((rec) => {
	    if (!rec.data.tag) {
		emptyTag = true;
	    }
	    if (!rec.data.color?.match(colorRegex)) {
		notValidColor = true;
	    }
	    if (rec.data.text && !rec.data.text?.match(colorRegex)) {
		notValidColor = true;
	    }
	});
	let errors = [];
	if (emptyTag) {
	    errors.push(gettext('Tag must not be empty.'));
	}
	if (notValidColor) {
	    errors.push(gettext('Not a valid color.'));
	}
	return errors;
    },
    // override framework function to implement deleteEmpty behaviour
    getSubmitData: function() {
	let me = this,
	    data = null,
	    val;
	if (!me.disabled && me.submitValue) {
	    val = me.getValue();
	    if (val !== null && val !== '') {
		data = {};
		data[me.getName()] = val;
	    } else if (me.getDeleteEmpty()) {
		data = {};
		data.delete = me.getName();
	    }
	}
	return data;
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	addLine: function() {
	    let me = this;
	    me.getView().getStore().add({
		tag: '',
		color: '',
		text: '',
	    });
	},
	removeSelection: function() {
	    let me = this;
	    let view = me.getView();
	    let selection = view.getSelection();
	    if (selection === undefined) {
		return;
	    }
	    selection.forEach((sel) => {
		view.getStore().remove(sel);
	    });
	    view.checkChange();
	},
	tagChange: function(field, newValue, oldValue) {
	    let me = this;
	    let rec = field.getWidgetRecord();
	    if (!rec) {
		return;
	    }
	    if (newValue && newValue !== oldValue) {
		let newrgb = Proxmox.Utils.stringToRGB(newValue);
		let newvalue = Proxmox.Utils.rgbToHex(newrgb);
		if (!rec.get('color')) {
		    rec.set('color', newvalue);
		} else if (oldValue) {
		    let oldrgb = Proxmox.Utils.stringToRGB(oldValue);
		    let oldvalue = Proxmox.Utils.rgbToHex(oldrgb);
		    if (rec.get('color') === oldvalue) {
			rec.set('color', newvalue);
		    }
		}
	    }
	    me.fieldChange(field, newValue, oldValue);
	},
	backgroundChange: function(field, newValue, oldValue) {
	    let me = this;
	    let rec = field.getWidgetRecord();
	    if (!rec) {
		return;
	    }
	    if (newValue && newValue !== oldValue) {
		let newrgb = Proxmox.Utils.hexToRGB(newValue);
		let newcls = Proxmox.Utils.getTextContrastClass(newrgb);
		let hexvalue = newcls === 'dark' ? '000000' : 'FFFFFF';
		if (!rec.get('text')) {
		    rec.set('text', hexvalue);
		} else if (oldValue) {
		    let oldrgb = Proxmox.Utils.hexToRGB(oldValue);
		    let oldcls = Proxmox.Utils.getTextContrastClass(oldrgb);
		    let oldvalue = oldcls === 'dark' ? '000000' : 'FFFFFF';
		    if (rec.get('text') === oldvalue) {
			rec.set('text', hexvalue);
		    }
		}
	    }
	    me.fieldChange(field, newValue, oldValue);
	},
	fieldChange: function(field, newValue, oldValue) {
	    let me = this;
	    let view = me.getView();
	    let rec = field.getWidgetRecord();
	    if (!rec) {
		return;
	    }
	    let column = field.getWidgetColumn();
	    rec.set(column.dataIndex, newValue);
	    view.checkChange();
	},
    },
    tbar: [
	{
	    text: gettext('Add'),
	    handler: 'addLine',
	},
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Remove'),
	    handler: 'removeSelection',
	    disabled: true,
	},
    ],
    columns: [
	{
	    header: 'Tag',
	    dataIndex: 'tag',
	    xtype: 'widgetcolumn',
	    onWidgetAttach: function(col, widget, rec) {
		widget.getStore().setData(PVE.UIOptions.tagList.map(v => ({ tag: v })));
	    },
	    widget: {
		xtype: 'combobox',
		isFormField: false,
		maskRe: PVE.Utils.tagCharRegex,
		allowBlank: false,
		queryMode: 'local',
		displayField: 'tag',
		valueField: 'tag',
		store: {},
		listeners: {
		    change: 'tagChange',
		},
	    },
	    flex: 1,
	},
	{
	    header: gettext('Background'),
	    xtype: 'widgetcolumn',
	    flex: 1,
	    dataIndex: 'color',
	    widget: {
		xtype: 'pveColorPicker',
		isFormField: false,
		listeners: {
		    change: 'backgroundChange',
		},
	    },
	},
	{
	    header: gettext('Text'),
	    xtype: 'widgetcolumn',
	    flex: 1,
	    dataIndex: 'text',
	    widget: {
		xtype: 'pveColorPicker',
		allowBlank: true,
		isFormField: false,
		listeners: {
		    change: 'fieldChange',
		},
	    },
	},
    ],
    store: {
	listeners: {
	    update: function() {
		this.commitChanges();
	    },
	},
    },
    initComponent: function() {
	let me = this;
	me.callParent();
	me.initField();
    },
});
Ext.define('PVE.form.ListField', {
    extend: 'Ext.container.Container',
    alias: 'widget.pveListField',
    mixins: [
	'Ext.form.field.Field',
    ],
    // override for column header
    fieldTitle: gettext('Item'),
    // will be applied to the textfields
    maskRe: undefined,
    allowBlank: true,
    selectAll: false,
    isFormField: true,
    deleteEmpty: false,
    config: {
	deleteEmpty: false,
    },
    setValue: function(list) {
	let me = this;
	list = Ext.isArray(list) ? list : (list ?? '').split(';').filter(t => t !== '');
	let store = me.lookup('grid').getStore();
	if (list.length > 0) {
	    store.setData(list.map(item => ({ item })));
	} else {
	    store.removeAll();
	}
	me.checkChange();
	return me;
    },
    getValue: function() {
	let me = this;
	let values = [];
	me.lookup('grid').getStore().each((rec) => {
	    if (rec.data.item) {
		values.push(rec.data.item);
	    }
	});
	return values.join(';');
    },
    getErrors: function(value) {
	let me = this;
	let empty = false;
	me.lookup('grid').getStore().each((rec) => {
	    if (!rec.data.item) {
		empty = true;
	    }
	});
	if (empty) {
	    return [gettext('Tag must not be empty.')];
	}
	return [];
    },
    // override framework function to implement deleteEmpty behaviour
    getSubmitData: function() {
	let me = this,
	    data = null,
	    val;
	if (!me.disabled && me.submitValue) {
	    val = me.getValue();
	    if (val !== null && val !== '') {
		data = {};
		data[me.getName()] = val;
	    } else if (me.getDeleteEmpty()) {
		data = {};
		data.delete = me.getName();
	    }
	}
	return data;
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	addLine: function() {
	    let me = this;
	    me.lookup('grid').getStore().add({
		item: '',
	    });
	},
	removeSelection: function(field) {
	    let me = this;
	    let view = me.getView();
	    let grid = me.lookup('grid');
	    let record = field.getWidgetRecord();
	    if (record === undefined) {
		// this is sometimes called before a record/column is initialized
		return;
	    }
	    grid.getStore().remove(record);
	    view.checkChange();
	    view.validate();
	},
	itemChange: function(field, newValue) {
	    let rec = field.getWidgetRecord();
	    if (!rec) {
		return;
	    }
	    let column = field.getWidgetColumn();
	    rec.set(column.dataIndex, newValue);
	    let list = field.up('pveListField');
	    list.checkChange();
	    list.validate();
	},
	control: {
	    'grid button': {
		click: 'removeSelection',
	    },
	},
    },
    items: [
	{
	    xtype: 'grid',
	    reference: 'grid',
	    viewConfig: {
		deferEmptyText: false,
	    },
	    store: {
		listeners: {
		    update: function() {
			this.commitChanges();
		    },
		},
	    },
	},
	{
	    xtype: 'button',
	    text: gettext('Add'),
	    iconCls: 'fa fa-plus-circle',
	    handler: 'addLine',
	},
    ],
    initComponent: function() {
	let me = this;
	for (const [key, value] of Object.entries(me.gridConfig ?? {})) {
	    me.items[0][key] = value;
	}
	me.items[0].columns = [
	    {
		header: me.fieldTtitle,
		dataIndex: 'item',
		xtype: 'widgetcolumn',
		widget: {
		    xtype: 'textfield',
		    isFormField: false,
		    maskRe: me.maskRe,
		    allowBlank: false,
		    queryMode: 'local',
		    listeners: {
			change: 'itemChange',
		    },
		},
		flex: 1,
	    },
	    {
		xtype: 'widgetcolumn',
		width: 40,
		widget: {
		    xtype: 'button',
		    iconCls: 'fa fa-trash-o',
		},
	    },
	];
	me.callParent();
	me.initField();
    },
});
Ext.define('Proxmox.form.Tag', {
    extend: 'Ext.Component',
    alias: 'widget.pveTag',
    mode: 'editable',
    tag: '',
    cls: 'pve-edit-tag',
    tpl: [
	'',
	'{tag}',
	'',
    ],
    // contains tags not to show in the picker and not allowing to set
    filter: [],
    updateFilter: function(tags) {
	this.filter = tags;
    },
    onClick: function(event) {
	let me = this;
	if (event.target.tagName === 'I' && !event.target.classList.contains('handle')) {
	    if (me.mode === 'editable') {
		me.destroy();
		return;
	    }
	} else if (event.target.tagName !== 'SPAN' || me.mode !== 'editable') {
	    return;
	}
	me.selectText();
    },
    selectText: function(collapseToEnd) {
	let me = this;
	let tagEl = me.tagEl();
	tagEl.contentEditable = true;
	let range = document.createRange();
	range.selectNodeContents(tagEl);
	if (collapseToEnd) {
	    range.collapse(false);
	}
	let sel = window.getSelection();
	sel.removeAllRanges();
	sel.addRange(range);
	me.showPicker();
    },
    showPicker: function() {
	let me = this;
	if (!me.picker) {
	    me.picker = Ext.widget({
		xtype: 'boundlist',
		minWidth: 70,
		scrollable: true,
		floating: true,
		hidden: true,
		userCls: 'proxmox-tags-full',
		displayField: 'tag',
		itemTpl: [
		    '{[Proxmox.Utils.getTagElement(values.tag, PVE.UIOptions.tagOverrides)]}',
		],
		store: [],
		listeners: {
		    select: function(picker, rec) {
			me.tagEl().innerHTML = rec.data.tag;
			me.setTag(rec.data.tag, true);
			me.selectText(true);
			me.setColor(rec.data.tag);
			me.picker.hide();
		    },
		},
	    });
	}
	me.picker.getStore()?.clearFilter();
	let taglist = PVE.UIOptions.tagList.filter(v => !me.filter.includes(v)).map(v => ({ tag: v }));
	if (taglist.length < 1) {
	    return;
	}
	me.picker.getStore().setData(taglist);
	me.picker.showBy(me, 'tl-bl');
	me.picker.setMaxHeight(200);
    },
    setMode: function(mode) {
	let me = this;
	let tagEl = me.tagEl();
	if (tagEl) {
	    tagEl.contentEditable = mode === 'editable';
	}
	me.removeCls(me.mode);
	me.addCls(mode);
	me.mode = mode;
	if (me.mode !== 'editable') {
	    me.picker?.hide();
	}
    },
    onKeyPress: function(event) {
	let me = this;
	let key = event.browserEvent.key;
	switch (key) {
	    case 'Enter':
	    case 'Escape':
		me.fireEvent('keypress', key);
		break;
	    case 'ArrowLeft':
	    case 'ArrowRight':
	    case 'Backspace':
	    case 'Delete':
		return;
	    default:
		if (key.match(PVE.Utils.tagCharRegex)) {
		    return;
		}
		me.setTag(me.tagEl().innerHTML);
	}
	event.browserEvent.preventDefault();
	event.browserEvent.stopPropagation();
    },
    // for pasting text
    beforeInput: function(event) {
	let me = this;
	me.updateLayout();
	let tag = event.event.data ?? event.event.dataTransfer?.getData('text/plain');
	if (!tag) {
	    return;
	}
	if (tag.match(PVE.Utils.tagCharRegex) === null) {
	    event.event.preventDefault();
	    event.event.stopPropagation();
	}
    },
    onInput: function(event) {
	let me = this;
	me.picker.getStore().filter({
	    property: 'tag',
	    value: me.tagEl().innerHTML,
	    anyMatch: true,
	});
	me.setTag(me.tagEl().innerHTML);
    },
    lostFocus: function(list, event) {
	let me = this;
	me.picker?.hide();
	window.getSelection().removeAllRanges();
    },
    setColor: function(tag) {
	let me = this;
	let rgb = PVE.UIOptions.tagOverrides[tag] ?? Proxmox.Utils.stringToRGB(tag);
	let cls = Proxmox.Utils.getTextContrastClass(rgb);
	let color = Proxmox.Utils.rgbToCss(rgb);
	me.setUserCls(`proxmox-tag-${cls}`);
	me.setStyle('background-color', color);
	if (rgb.length > 3) {
	    let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]);
	    me.setStyle('color', fgcolor);
	} else {
	    me.setStyle('color');
	}
    },
    setTag: function(tag) {
	let me = this;
	let oldtag = me.tag;
	me.tag = tag;
	clearTimeout(me.colorTimeout);
	me.colorTimeout = setTimeout(() => me.setColor(tag), 200);
	me.updateLayout();
	if (oldtag !== tag) {
	    me.fireEvent('change', me, tag, oldtag);
	}
    },
    tagEl: function() {
	return this.el?.dom?.getElementsByTagName('span')?.[0];
    },
    listeners: {
	click: 'onClick',
	focusleave: 'lostFocus',
	keydown: 'onKeyPress',
	beforeInput: 'beforeInput',
	input: 'onInput',
	element: 'el',
	scope: 'this',
    },
    initComponent: function() {
	let me = this;
	me.data = {
	    tag: me.tag,
	};
	me.setTag(me.tag);
	me.setColor(me.tag);
	me.setMode(me.mode ?? 'normal');
	me.callParent();
    },
    destroy: function() {
	let me = this;
	if (me.picker) {
	    Ext.destroy(me.picker);
	}
	clearTimeout(me.colorTimeout);
	me.callParent();
    },
});
Ext.define('PVE.panel.TagEditContainer', {
    extend: 'Ext.container.Container',
    alias: 'widget.pveTagEditContainer',
    layout: {
	type: 'hbox',
	align: 'middle',
    },
    // set to false to hide the 'no tags' field and the edit button
    canEdit: true,
    controller: {
	xclass: 'Ext.app.ViewController',
	loadTags: function(tagstring = '', force = false) {
	    let me = this;
	    let view = me.getView();
	    if (me.oldTags === tagstring && !force) {
		return;
	    }
	    view.suspendLayout = true;
	    me.forEachTag((tag) => {
		view.remove(tag);
	    });
	    me.getViewModel().set('tagCount', 0);
	    let newtags = tagstring.split(/[;, ]/).filter((t) => !!t) || [];
	    newtags.forEach((tag) => {
		me.addTag(tag);
	    });
	    view.suspendLayout = false;
	    view.updateLayout();
	    if (!force) {
		me.oldTags = tagstring;
	    }
	    me.tagsChanged();
	},
	onRender: function(v) {
	    let me = this;
	    let view = me.getView();
	    view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags());
	    view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), {
		getDragData: function(e) {
		    let source = e.getTarget('.handle');
		    if (!source) {
			return undefined;
		    }
		    let sourceId = source.parentNode.id;
		    let cmp = Ext.getCmp(sourceId);
		    let ddel = document.createElement('div');
		    ddel.classList.add('proxmox-tags-full');
		    ddel.innerHTML = Proxmox.Utils.getTagElement(cmp.tag, PVE.UIOptions.tagOverrides);
		    let repairXY = Ext.fly(source).getXY();
		    cmp.setDisabled(true);
		    ddel.id = Ext.id();
		    return {
			ddel,
			repairXY,
			sourceId,
		    };
		},
		onMouseUp: function(target, e, id) {
		    let cmp = Ext.getCmp(this.dragData.sourceId);
		    if (cmp && !cmp.isDestroyed) {
			cmp.setDisabled(false);
		    }
		},
		getRepairXY: function() {
		    return this.dragData.repairXY;
		},
		beforeInvalidDrop: function(target, e, id) {
		    let cmp = Ext.getCmp(this.dragData.sourceId);
		    if (cmp && !cmp.isDestroyed) {
			cmp.setDisabled(false);
		    }
		},
	    });
	    view.dropzone = Ext.create('Ext.dd.DropZone', v.getEl(), {
		getTargetFromEvent: function(e) {
		    return e.getTarget('.proxmox-tag-dark,.proxmox-tag-light');
		},
		getIndicator: function() {
		    if (!view.indicator) {
			view.indicator = Ext.create('Ext.Component', {
			    floating: true,
			    html: '',
			    hidden: true,
			    shadow: false,
			});
		    }
		    return view.indicator;
		},
		onContainerOver: function() {
		    this.getIndicator().setVisible(false);
		},
		notifyOut: function() {
		    this.getIndicator().setVisible(false);
		},
		onNodeOver: function(target, dd, e, data) {
		    let indicator = this.getIndicator();
		    indicator.setVisible(true);
		    indicator.alignTo(Ext.getCmp(target.id), 't50-bl', [-1, -2]);
		    return this.dropAllowed;
		},
		onNodeDrop: function(target, dd, e, data) {
		    this.getIndicator().setVisible(false);
		    let sourceCmp = Ext.getCmp(data.sourceId);
		    if (!sourceCmp) {
			return;
		    }
		    sourceCmp.setDisabled(false);
		    let targetCmp = Ext.getCmp(target.id);
		    view.remove(sourceCmp, { destroy: false });
		    view.insert(view.items.indexOf(targetCmp), sourceCmp);
		    me.tagsChanged();
		},
	    });
	},
	forEachTag: function(func) {
	    let me = this;
	    let view = me.getView();
	    view.items.each((field) => {
		if (field.getXType() === 'pveTag') {
		    func(field);
		}
		return true;
	    });
	},
	toggleEdit: function(cancel) {
	    let me = this;
	    let vm = me.getViewModel();
	    let view = me.getView();
	    let editMode = !vm.get('editMode');
	    vm.set('editMode', editMode);
	    // get a current tag list for editing
	    if (editMode) {
		PVE.UIOptions.update();
	    }
	    me.forEachTag((tag) => {
		tag.setMode(editMode ? 'editable' : 'normal');
	    });
	    if (!vm.get('editMode')) {
		let tags = [];
		if (cancel) {
		    me.loadTags(me.oldTags, true);
		} else {
		    let toRemove = [];
		    me.forEachTag((cmp) => {
			if (cmp.isVisible() && cmp.tag) {
			    tags.push(cmp.tag);
			} else {
			    toRemove.push(cmp);
			}
		    });
		    toRemove.forEach(cmp => view.remove(cmp));
		    tags = tags.join(',');
		    if (me.oldTags !== tags) {
			me.oldTags = tags;
			me.loadTags(tags, true);
			me.getView().fireEvent('change', tags);
		    }
		}
	    }
	    me.getView().updateLayout();
	},
	tagsChanged: function() {
	    let me = this;
	    let tags = [];
	    me.forEachTag(cmp => {
		if (cmp.tag) {
		    tags.push(cmp.tag);
		}
	    });
	    me.getViewModel().set('isDirty', me.oldTags !== tags.join(','));
	    me.forEachTag(cmp => {
		cmp.updateFilter(tags);
	    });
	},
	addTag: function(tag, isNew) {
	    let me = this;
	    let view = me.getView();
	    let vm = me.getViewModel();
	    let index = view.items.length - 5;
	    if (PVE.UIOptions.shouldSortTags() && !isNew) {
		index = view.items.findIndexBy(tagField => {
		    if (tagField.reference === 'noTagsField') {
			return false;
		    }
		    if (tagField.xtype !== 'pveTag') {
			return true;
		    }
		    let a = tagField.tag.toLowerCase();
		    let b = tag.toLowerCase();
		    return a > b ? true : a < b ? false : tagField.tag.localeCompare(tag) > 0;
		}, 1);
	    }
	    let tagField = view.insert(index, {
		xtype: 'pveTag',
		tag,
		mode: vm.get('editMode') ? 'editable' : 'normal',
		listeners: {
		    change: 'tagsChanged',
		    destroy: function() {
			vm.set('tagCount', vm.get('tagCount') - 1);
			me.tagsChanged();
		    },
		    keypress: function(key) {
			if (key === 'Enter') {
			    me.editClick();
			} else if (key === 'Escape') {
			    me.cancelClick();
			}
		    },
		},
	    });
	    if (isNew) {
		me.tagsChanged();
		tagField.selectText();
	    }
	    vm.set('tagCount', vm.get('tagCount') + 1);
	},
	addTagClick: function(event) {
	    let me = this;
	    me.lookup('noTagsField').setVisible(false);
	    me.addTag('', true);
	},
	cancelClick: function() {
	    this.toggleEdit(true);
	},
	editClick: function() {
	    this.toggleEdit(false);
	},
	init: function(view) {
	    let me = this;
	    if (view.tags) {
		me.loadTags(view.tags);
	    }
	    me.getViewModel().set('canEdit', view.canEdit);
	    me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
		view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags());
		me.loadTags(me.oldTags, true); // refresh tag colors and order
	    });
	},
    },
    viewModel: {
	data: {
	    tagCount: 0,
	    editMode: false,
	    canEdit: true,
	    isDirty: false,
	},
	formulas: {
	    hideNoTags: function(get) {
		return get('tagCount') !== 0 || !get('canEdit');
	    },
	    hideEditBtn: function(get) {
		return get('editMode') || !get('canEdit');
	    },
	},
    },
    loadTags: function() {
	return this.getController().loadTags(...arguments);
    },
    items: [
	{
	    xtype: 'box',
	    reference: 'noTagsField',
	    bind: {
		hidden: '{hideNoTags}',
	    },
	    html: gettext('No Tags'),
	    style: {
		opacity: 0.5,
	    },
	},
	{
	    xtype: 'button',
	    iconCls: 'fa fa-plus',
	    tooltip: gettext('Add Tag'),
	    bind: {
		hidden: '{!editMode}',
	    },
	    hidden: true,
	    margin: '0 8 0 5',
	    ui: 'default-toolbar',
	    handler: 'addTagClick',
	},
	{
	    xtype: 'tbseparator',
	    ui: 'horizontal',
	    bind: {
		hidden: '{!editMode}',
	    },
	    hidden: true,
	},
	{
	    xtype: 'button',
	    iconCls: 'fa fa-times',
	    tooltip: gettext('Cancel Edit'),
	    bind: {
		hidden: '{!editMode}',
	    },
	    hidden: true,
	    margin: '0 5 0 0',
	    ui: 'default-toolbar',
	    handler: 'cancelClick',
	},
	{
	    xtype: 'button',
	    iconCls: 'fa fa-check',
	    tooltip: gettext('Finish Edit'),
	    bind: {
		hidden: '{!editMode}',
		disabled: '{!isDirty}',
	    },
	    hidden: true,
	    handler: 'editClick',
	},
	{
	    xtype: 'box',
	    cls: 'pve-tag-inline-button',
	    html: ``,
	    bind: {
		hidden: '{hideEditBtn}',
	    },
	    listeners: {
		click: 'editClick',
		element: 'el',
	    },
	},
    ],
    listeners: {
	render: 'onRender',
    },
    destroy: function() {
	let me = this;
	Ext.destroy(me.dragzone);
	Ext.destroy(me.dropzone);
	Ext.destroy(me.indicator);
	me.callParent();
    },
});
Ext.define('PVE.grid.BackupView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveBackupView'],
    onlineHelp: 'chapter_vzdump',
    stateful: true,
    stateId: 'grid-guest-backup',
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = me.pveSelNode.data.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	var vmtype = me.pveSelNode.data.type;
	if (!vmtype) {
	    throw "no VM type specified";
	}
	var vmtypeFilter;
	if (vmtype === 'lxc' || vmtype === 'openvz') {
	    vmtypeFilter = function(item) {
		return PVE.Utils.volume_is_lxc_backup(item.data.volid, item.data.format);
	    };
	} else if (vmtype === 'qemu') {
	    vmtypeFilter = function(item) {
		return PVE.Utils.volume_is_qemu_backup(item.data.volid, item.data.format);
	    };
	} else {
	    throw "unsupported VM type '" + vmtype + "'";
	}
	var searchFilter = {
	    property: 'volid',
	    value: '',
	    anyMatch: true,
	    caseSensitive: false,
	};
	var vmidFilter = {
	    property: 'vmid',
	    value: vmid,
	    exactMatch: true,
	};
	me.store = Ext.create('Ext.data.Store', {
	    model: 'pve-storage-content',
	    sorters: [
		{
		    property: 'vmid',
		    direction: 'ASC',
		},
		{
		    property: 'vdate',
		    direction: 'DESC',
		},
	    ],
	    filters: [
	        vmtypeFilter,
		searchFilter,
		vmidFilter,
		],
	});
	let updateFilter = function() {
	    me.store.filter([
		vmtypeFilter,
		searchFilter,
		vmidFilter,
	    ]);
	};
	const reload = Ext.Function.createBuffered((options) => {
	    if (me.store) {
		me.store.load(options);
	    }
	}, 100);
	let isPBS = false;
	var setStorage = function(storage) {
	    var url = '/api2/json/nodes/' + nodename + '/storage/' + storage + '/content';
	    url += '?content=backup';
	    me.store.setProxy({
		type: 'proxmox',
		url: url,
	    });
	    Proxmox.Utils.monStoreErrors(me.view, me.store, true);
	    reload();
	};
	let file_restore_btn;
	var storagesel = Ext.create('PVE.form.StorageSelector', {
	    nodename: nodename,
	    fieldLabel: gettext('Storage'),
	    labelAlign: 'right',
	    storageContent: 'backup',
	    allowBlank: false,
	    listeners: {
		change: function(f, value) {
		    let storage = f.getStore().findRecord('storage', value, 0, false, true, true);
		    if (storage) {
			isPBS = storage.data.type === 'pbs';
			me.getColumns().forEach((column) => {
			    let id = column.dataIndex;
			    if (id === 'verification' || id === 'encrypted') {
				column.setHidden(!isPBS);
			    }
			});
		    } else {
			isPBS = false;
		    }
		    setStorage(value);
		    if (file_restore_btn) {
			file_restore_btn.setHidden(!isPBS);
		    }
		},
	    },
	});
	var storagefilter = Ext.create('Ext.form.field.Text', {
	    fieldLabel: gettext('Search'),
	    labelWidth: 50,
	    labelAlign: 'right',
	    enableKeyEvents: true,
	    value: searchFilter.value,
	    listeners: {
		buffer: 500,
		keyup: function(field) {
		    me.store.clearFilter(true);
		    searchFilter.value = field.getValue();
		    updateFilter();
		},
	    },
	});
	var vmidfilterCB = Ext.create('Ext.form.field.Checkbox', {
	    boxLabel: gettext('Filter VMID'),
	    value: '1',
	    listeners: {
		change: function(cb, value) {
		    vmidFilter.value = value ? vmid : '';
		    vmidFilter.exactMatch = !!value;
		    updateFilter();
		},
	    },
	});
	var sm = Ext.create('Ext.selection.RowModel', {});
	var backup_btn = Ext.create('Ext.button.Button', {
	    text: gettext('Backup now'),
	    handler: function() {
		var win = Ext.create('PVE.window.Backup', {
		    nodename: nodename,
		    vmid: vmid,
		    vmtype: vmtype,
		    storage: storagesel.getValue(),
		    listeners: {
			close: function() {
			    reload();
			},
		    },
		});
		win.show();
	    },
	});
	var restore_btn = Ext.create('Proxmox.button.Button', {
	    text: gettext('Restore'),
	    disabled: true,
	    selModel: sm,
	    enableFn: function(rec) {
		return !!rec;
	    },
	    handler: function(b, e, rec) {
		let win = Ext.create('PVE.window.Restore', {
		    nodename: nodename,
		    vmid: vmid,
		    volid: rec.data.volid,
		    volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
		    vmtype: vmtype,
		    isPBS: isPBS,
		});
		win.show();
		win.on('destroy', reload);
	    },
	});
	let delete_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    dangerous: true,
	    delay: 5,
	    enableFn: rec => !rec?.data?.protected,
	    confirmMsg: ({ data }) => {
		let msg = Ext.String.format(
		    gettext('Are you sure you want to remove entry {0}'), `'${data.volid}'`);
		return msg + " " + gettext('This will permanently erase all data.');
	    },
	    getUrl: ({ data }) => `/nodes/${nodename}/storage/${storagesel.getValue()}/content/${data.volid}`,
	    callback: () => reload(),
	});
	let config_btn = Ext.create('Proxmox.button.Button', {
	    text: gettext('Show Configuration'),
	    disabled: true,
	    selModel: sm,
	    enableFn: rec => !!rec,
	    handler: function(b, e, rec) {
		let storage = storagesel.getValue();
		if (!storage) {
		    return;
		}
		Ext.create('PVE.window.BackupConfig', {
		    volume: rec.data.volid,
		    pveSelNode: me.pveSelNode,
		    autoShow: true,
		});
	    },
	});
	// declared above so that the storage selector can change this buttons hidden state
	file_restore_btn = Ext.create('Proxmox.button.Button', {
	    text: gettext('File Restore'),
	    disabled: true,
	    selModel: sm,
	    enableFn: rec => !!rec && isPBS,
	    hidden: !isPBS,
	    handler: function(b, e, rec) {
		let storage = storagesel.getValue();
		let isVMArchive = PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format);
		Ext.create('Proxmox.window.FileBrowser', {
		    title: gettext('File Restore') + " - " + rec.data.text,
		    listURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/list`,
		    downloadURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/download`,
		    extraParams: {
			volume: rec.data.volid,
		    },
		    archive: isVMArchive ? 'all' : undefined,
		    autoShow: true,
		});
	    },
	});
	Ext.apply(me, {
	    selModel: sm,
	    tbar: {
		overflowHandler: 'scroller',
		items: [
		    backup_btn,
		    '-',
		    restore_btn,
		    file_restore_btn,
		    config_btn,
		    {
			xtype: 'proxmoxButton',
			text: gettext('Edit Notes'),
			disabled: true,
			handler: function() {
			    let volid = sm.getSelection()[0].data.volid;
			    var storage = storagesel.getValue();
			    Ext.create('Proxmox.window.Edit', {
				autoLoad: true,
				width: 600,
				height: 400,
				resizable: true,
				title: gettext('Notes'),
				url: `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`,
				layout: 'fit',
				items: [
				    {
					xtype: 'textarea',
					layout: 'fit',
					name: 'notes',
					height: '100%',
				    },
				],
				listeners: {
				    destroy: () => reload(),
				},
			    }).show();
			},
		    },
		    {
			xtype: 'proxmoxButton',
			text: gettext('Change Protection'),
			disabled: true,
			handler: function(button, event, record) {
			    let volid = record.data.volid, storage = storagesel.getValue();
			    let url = `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`;
			    Proxmox.Utils.API2Request({
				url: url,
				method: 'PUT',
				waitMsgTarget: me,
				params: {
				    'protected': record.data.protected ? 0 : 1,
				},
				failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
				success: () => {
				    reload({
					callback: () => sm.fireEvent('selectionchange', sm, [record]),
				    });
				},
			    });
			},
		    },
		    '-',
		    delete_btn,
		    '->',
		    storagesel,
		    '-',
		    vmidfilterCB,
		    storagefilter,
		],
	    },
	    columns: [
		{
		    header: gettext('Name'),
		    flex: 2,
		    sortable: true,
		    renderer: PVE.Utils.render_storage_content,
		    dataIndex: 'volid',
		},
		{
		    header: gettext('Notes'),
		    dataIndex: 'notes',
		    flex: 1,
		    renderer: Ext.htmlEncode,
		},
		{
		    header: ``,
		    tooltip: gettext('Protected'),
		    width: 30,
		    renderer: v => v ? `` : '',
		    sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0),
		    dataIndex: 'protected',
		},
		{
		    header: gettext('Date'),
		    width: 150,
		    dataIndex: 'vdate',
		},
		{
		    header: gettext('Format'),
		    width: 100,
		    dataIndex: 'format',
		},
		{
		    header: gettext('Size'),
		    width: 100,
		    renderer: Proxmox.Utils.format_size,
		    dataIndex: 'size',
		},
		{
		    header: gettext('VMID'),
		    dataIndex: 'vmid',
		    hidden: true,
		},
		{
		    header: gettext('Encrypted'),
		    dataIndex: 'encrypted',
		    renderer: PVE.Utils.render_backup_encryption,
		},
		{
		    header: gettext('Verify State'),
		    dataIndex: 'verification',
		    renderer: PVE.Utils.render_backup_verification,
		},
	    ],
	});
	me.callParent();
    },
});
Ext.define('PVE.FirewallAliasEdit', {
    extend: 'Proxmox.window.Edit',
    base_url: undefined,
    alias_name: undefined,
    width: 400,
    initComponent: function() {
	let me = this;
	me.isCreate = me.alias_name === undefined;
	if (me.isCreate) {
	    me.url = '/api2/extjs' + me.base_url;
	    me.method = 'POST';
	} else {
	    me.url = '/api2/extjs' + me.base_url + '/' + me.alias_name;
	    me.method = 'PUT';
	}
	let ipanel = Ext.create('Proxmox.panel.InputPanel', {
	    isCreate: me.isCreate,
	    items: [
		{
		    xtype: 'textfield',
		    name: me.isCreate ? 'name' : 'rename',
		    fieldLabel: gettext('Name'),
		    allowBlank: false,
		},
		{
		    xtype: 'textfield',
		    name: 'cidr',
		    fieldLabel: gettext('IP/CIDR'),
		    allowBlank: false,
		},
		{
		    xtype: 'textfield',
		    name: 'comment',
		    fieldLabel: gettext('Comment'),
		},
	    ],
	});
	Ext.apply(me, {
	    subject: gettext('Alias'),
	    isAdd: true,
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    let values = response.result.data;
		    values.rename = values.name;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('pve-fw-aliases', {
    extend: 'Ext.data.Model',
    fields: ['name', 'cidr', 'comment', 'digest'],
    idProperty: 'name',
});
Ext.define('PVE.FirewallAliases', {
    extend: 'Ext.grid.Panel',
    alias: ['widget.pveFirewallAliases'],
    onlineHelp: 'pve_firewall_ip_aliases',
    stateful: true,
    stateId: 'grid-firewall-aliases',
    base_url: undefined,
    title: gettext('Alias'),
    initComponent: function() {
	let me = this;
	if (!me.base_url) {
	    throw "missing base_url configuration";
	}
	let store = new Ext.data.Store({
	    model: 'pve-fw-aliases',
	    proxy: {
		type: 'proxmox',
		url: "/api2/json" + me.base_url,
	    },
	    sorters: {
		property: 'name',
		direction: 'ASC',
	    },
	});
	let sm = Ext.create('Ext.selection.RowModel', {});
	let reload = function() {
	    let oldrec = sm.getSelection()[0];
	    store.load(function(records, operation, success) {
		if (oldrec) {
		    var rec = store.findRecord('name', oldrec.data.name, 0, false, true, true);
		    if (rec) {
			sm.select(rec);
		    }
		}
	    });
	};
	let run_editor = function() {
	    let rec = me.getSelectionModel().getSelection()[0];
	    if (!rec) {
		return;
	    }
	    let win = Ext.create('PVE.FirewallAliasEdit', {
		base_url: me.base_url,
		alias_name: rec.data.name,
	    });
	    win.show();
	    win.on('destroy', reload);
	};
	me.editBtn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	me.addBtn = Ext.create('Ext.Button', {
	    text: gettext('Add'),
	    handler: function() {
		var win = Ext.create('PVE.FirewallAliasEdit', {
		    base_url: me.base_url,
		});
		win.on('destroy', reload);
		win.show();
	    },
	});
	me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: me.base_url + '/',
	    callback: reload,
	});
	Ext.apply(me, {
	    store: store,
	    tbar: [me.addBtn, me.removeBtn, me.editBtn],
	    selModel: sm,
	    columns: [
		{
		    header: gettext('Name'),
		    dataIndex: 'name',
		    flex: 1,
		},
		{
		    header: gettext('IP/CIDR'),
		    dataIndex: 'cidr',
		    flex: 1,
		},
		{
		    header: gettext('Comment'),
		    dataIndex: 'comment',
		    renderer: Ext.String.htmlEncode,
		    flex: 3,
		},
	    ],
	    listeners: {
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
	me.on('activate', reload);
    },
});
Ext.define('PVE.FirewallOptions', {
    extend: 'Proxmox.grid.ObjectGrid',
    alias: ['widget.pveFirewallOptions'],
    fwtype: undefined, // 'dc', 'node' or 'vm'
    base_url: undefined,
    initComponent: function() {
	var me = this;
	if (!me.base_url) {
	    throw "missing base_url configuration";
	}
	if (me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm') {
	    if (me.fwtype === 'node') {
		me.cwidth1 = 250;
	    }
	} else {
	    throw "unknown firewall option type";
	}
	me.rows = {};
	var add_boolean_row = function(name, text, defaultValue) {
	    me.add_boolean_row(name, text, { defaultValue: defaultValue });
	};
	var add_integer_row = function(name, text, minValue, labelWidth) {
	    me.add_integer_row(name, text, {
		minValue: minValue,
		deleteEmpty: true,
		labelWidth: labelWidth,
		renderer: function(value) {
		    if (value === undefined) {
			return Proxmox.Utils.defaultText;
		    }
		    return value;
		},
	    });
	};
	var add_log_row = function(name, labelWidth) {
	    me.rows[name] = {
		header: name,
		required: true,
		defaultValue: 'nolog',
		editor: {
		    xtype: 'proxmoxWindowEdit',
		    subject: name,
		    fieldDefaults: { labelWidth: labelWidth || 100 },
		    items: {
			xtype: 'pveFirewallLogLevels',
			name: name,
			fieldLabel: name,
		    },
		},
	    };
	};
	if (me.fwtype === 'node') {
	    me.rows.enable = {
		required: true,
		defaultValue: 1,
		header: gettext('Firewall'),
		renderer: Proxmox.Utils.format_boolean,
		editor: {
		    xtype: 'pveFirewallEnableEdit',
		    defaultValue: 1,
		},
	    };
	    add_boolean_row('nosmurfs', gettext('SMURFS filter'), 1);
	    add_boolean_row('tcpflags', gettext('TCP flags filter'), 0);
	    add_boolean_row('ndp', 'NDP', 1);
	    add_integer_row('nf_conntrack_max', 'nf_conntrack_max', 32768, 120);
	    add_integer_row('nf_conntrack_tcp_timeout_established',
			    'nf_conntrack_tcp_timeout_established', 7875, 250);
	    add_log_row('log_level_in');
	    add_log_row('log_level_out');
	    add_log_row('tcp_flags_log_level', 120);
	    add_log_row('smurf_log_level');
	} else if (me.fwtype === 'vm') {
	    me.rows.enable = {
		required: true,
		defaultValue: 0,
		header: gettext('Firewall'),
		renderer: Proxmox.Utils.format_boolean,
		editor: {
		    xtype: 'pveFirewallEnableEdit',
		    defaultValue: 0,
		},
	    };
	    add_boolean_row('dhcp', 'DHCP', 1);
	    add_boolean_row('ndp', 'NDP', 1);
	    add_boolean_row('radv', gettext('Router Advertisement'), 0);
	    add_boolean_row('macfilter', gettext('MAC filter'), 1);
	    add_boolean_row('ipfilter', gettext('IP filter'), 0);
	    add_log_row('log_level_in');
	    add_log_row('log_level_out');
	} else if (me.fwtype === 'dc') {
	    add_boolean_row('enable', gettext('Firewall'), 0);
	    add_boolean_row('ebtables', 'ebtables', 1);
	    me.rows.log_ratelimit = {
		header: gettext('Log rate limit'),
		required: true,
		defaultValue: gettext('Default') + ' (enable=1,rate1/second,burst=5)',
		editor: {
		    xtype: 'pveFirewallLograteEdit',
		    defaultValue: 'enable=1',
		},
	    };
	}
	if (me.fwtype === 'dc' || me.fwtype === 'vm') {
	    me.rows.policy_in = {
		header: gettext('Input Policy'),
		required: true,
		defaultValue: 'DROP',
		editor: {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Input Policy'),
		    items: {
			xtype: 'pveFirewallPolicySelector',
			name: 'policy_in',
			value: 'DROP',
			fieldLabel: gettext('Input Policy'),
		    },
		},
	    };
	    me.rows.policy_out = {
		header: gettext('Output Policy'),
		required: true,
		defaultValue: 'ACCEPT',
		editor: {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Output Policy'),
		    items: {
			xtype: 'pveFirewallPolicySelector',
			name: 'policy_out',
			value: 'ACCEPT',
			fieldLabel: gettext('Output Policy'),
		    },
		},
	    };
	}
	var edit_btn = new Ext.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    handler: function() { me.run_editor(); },
	});
	var set_button_status = function() {
	    var sm = me.getSelectionModel();
	    var rec = sm.getSelection()[0];
	    if (!rec) {
		edit_btn.disable();
		return;
	    }
	    var rowdef = me.rows[rec.data.key];
	    edit_btn.setDisabled(!rowdef.editor);
	};
	Ext.apply(me, {
	    url: "/api2/json" + me.base_url,
	    tbar: [edit_btn],
	    editorConfig: {
		url: '/api2/extjs/' + me.base_url,
	    },
	    listeners: {
		itemdblclick: me.run_editor,
		selectionchange: set_button_status,
	    },
	});
	me.callParent();
	me.on('activate', me.rstore.startUpdate);
	me.on('destroy', me.rstore.stopUpdate);
	me.on('deactivate', me.rstore.stopUpdate);
    },
});
Ext.define('PVE.FirewallLogLevels', {
    extend: 'Proxmox.form.KVComboBox',
    alias: ['widget.pveFirewallLogLevels'],
    name: 'log',
    fieldLabel: gettext('Log level'),
    value: 'nolog',
    comboItems: [['nolog', 'nolog'], ['emerg', 'emerg'], ['alert', 'alert'],
	['crit', 'crit'], ['err', 'err'], ['warning', 'warning'],
	['notice', 'notice'], ['info', 'info'], ['debug', 'debug']],
});
Ext.define('PVE.form.FWMacroSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: 'widget.pveFWMacroSelector',
    allowBlank: true,
    autoSelect: false,
    valueField: 'macro',
    displayField: 'macro',
    listConfig: {
	columns: [
	    {
		header: gettext('Macro'),
		dataIndex: 'macro',
		hideable: false,
		width: 100,
	    },
	    {
		header: gettext('Description'),
		renderer: Ext.String.htmlEncode,
		flex: 1,
		dataIndex: 'descr',
	    },
	],
    },
    initComponent: function() {
	var me = this;
	var store = Ext.create('Ext.data.Store', {
	    autoLoad: true,
	    fields: ['macro', 'descr'],
	    idProperty: 'macro',
	    proxy: {
		type: 'proxmox',
		url: "/api2/json/cluster/firewall/macros",
	    },
	    sorters: {
		property: 'macro',
		direction: 'ASC',
	    },
	});
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.form.ICMPTypeSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: 'widget.pveICMPTypeSelector',
    allowBlank: true,
    autoSelect: false,
    valueField: 'name',
    displayField: 'name',
    listConfig: {
	columns: [
	    {
		header: gettext('Type'),
		dataIndex: 'type',
		hideable: false,
		sortable: false,
		width: 50,
	    },
	    {
		header: gettext('Name'),
		dataIndex: 'name',
		hideable: false,
		sortable: false,
		flex: 1,
	    },
	],
    },
    setName: function(value) {
	this.name = value;
    },
});
let ICMP_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', {
    field: ['type', 'name'],
    data: [
	{ type: 'any', name: 'any' },
	{ type: '0', name: 'echo-reply' },
	{ type: '3', name: 'destination-unreachable' },
	{ type: '3/0', name: 'network-unreachable' },
	{ type: '3/1', name: 'host-unreachable' },
	{ type: '3/2', name: 'protocol-unreachable' },
	{ type: '3/3', name: 'port-unreachable' },
	{ type: '3/4', name: 'fragmentation-needed' },
	{ type: '3/5', name: 'source-route-failed' },
	{ type: '3/6', name: 'network-unknown' },
	{ type: '3/7', name: 'host-unknown' },
	{ type: '3/9', name: 'network-prohibited' },
	{ type: '3/10', name: 'host-prohibited' },
	{ type: '3/11', name: 'TOS-network-unreachable' },
	{ type: '3/12', name: 'TOS-host-unreachable' },
	{ type: '3/13', name: 'communication-prohibited' },
	{ type: '3/14', name: 'host-precedence-violation' },
	{ type: '3/15', name: 'precedence-cutoff' },
	{ type: '4', name: 'source-quench' },
	{ type: '5', name: 'redirect' },
	{ type: '5/0', name: 'network-redirect' },
	{ type: '5/1', name: 'host-redirect' },
	{ type: '5/2', name: 'TOS-network-redirect' },
	{ type: '5/3', name: 'TOS-host-redirect' },
	{ type: '8', name: 'echo-request' },
	{ type: '9', name: 'router-advertisement' },
	{ type: '10', name: 'router-solicitation' },
	{ type: '11', name: 'time-exceeded' },
	{ type: '11/0', name: 'ttl-zero-during-transit' },
	{ type: '11/1', name: 'ttl-zero-during-reassembly' },
	{ type: '12', name: 'parameter-problem' },
	{ type: '12/0', name: 'ip-header-bad' },
	{ type: '12/1', name: 'required-option-missing' },
	{ type: '13', name: 'timestamp-request' },
	{ type: '14', name: 'timestamp-reply' },
	{ type: '17', name: 'address-mask-request' },
	{ type: '18', name: 'address-mask-reply' },
    ],
});
let ICMPV6_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', {
    field: ['type', 'name'],
    data: [
	{ type: '1', name: 'destination-unreachable' },
	{ type: '1/0', name: 'no-route' },
	{ type: '1/1', name: 'communication-prohibited' },
	{ type: '1/2', name: 'beyond-scope' },
	{ type: '1/3', name: 'address-unreachable' },
	{ type: '1/4', name: 'port-unreachable' },
	{ type: '1/5', name: 'failed-policy' },
	{ type: '1/6', name: 'reject-route' },
	{ type: '2', name: 'packet-too-big' },
	{ type: '3', name: 'time-exceeded' },
	{ type: '3/0', name: 'ttl-zero-during-transit' },
	{ type: '3/1', name: 'ttl-zero-during-reassembly' },
	{ type: '4', name: 'parameter-problem' },
	{ type: '4/0', name: 'bad-header' },
	{ type: '4/1', name: 'unknown-header-type' },
	{ type: '4/2', name: 'unknown-option' },
	{ type: '128', name: 'echo-request' },
	{ type: '129', name: 'echo-reply' },
	{ type: '133', name: 'router-solicitation' },
	{ type: '134', name: 'router-advertisement' },
	{ type: '135', name: 'neighbour-solicitation' },
	{ type: '136', name: 'neighbour-advertisement' },
	{ type: '137', name: 'redirect' },
    ],
});
Ext.define('PVE.FirewallRulePanel', {
    extend: 'Proxmox.panel.InputPanel',
    allow_iface: false,
    list_refs_url: undefined,
    onGetValues: function(values) {
	var me = this;
	// hack: editable ComboGrid returns nothing when empty, so we need to set ''
	// Also, disabled text fields return nothing, so we need to set ''
	Ext.Array.each(['source', 'dest', 'macro', 'proto', 'sport', 'dport', 'icmp-type', 'log'], function(key) {
	    if (values[key] === undefined) {
		values[key] = '';
	    }
	});
	delete values.modified_marker;
	return values;
    },
    initComponent: function() {
	var me = this;
	if (!me.list_refs_url) {
	    throw "no list_refs_url specified";
	}
	me.column1 = [
	    {
		// hack: we use this field to mark the form 'dirty' when the
		// record has errors- so that the user can safe the unmodified
		// form again.
		xtype: 'hiddenfield',
		name: 'modified_marker',
		value: '',
	    },
	    {
		xtype: 'proxmoxKVComboBox',
		name: 'type',
		value: 'in',
		comboItems: [['in', 'in'], ['out', 'out']],
		fieldLabel: gettext('Direction'),
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxKVComboBox',
		name: 'action',
		value: 'ACCEPT',
		comboItems: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']],
		fieldLabel: gettext('Action'),
		allowBlank: false,
	    },
        ];
	if (me.allow_iface) {
	    me.column1.push({
		xtype: 'proxmoxtextfield',
		name: 'iface',
		deleteEmpty: !me.isCreate,
		value: '',
		fieldLabel: gettext('Interface'),
	    });
	} else {
	    me.column1.push({
		xtype: 'displayfield',
		fieldLabel: '',
		value: '',
	    });
	}
	me.column1.push(
	    {
		xtype: 'displayfield',
		fieldLabel: '',
		height: 7,
		value: '',
	    },
	    {
		xtype: 'pveIPRefSelector',
		name: 'source',
		autoSelect: false,
		editable: true,
		base_url: me.list_refs_url,
		value: '',
		fieldLabel: gettext('Source'),
		maxLength: 512,
		maxLengthText: gettext('Too long, consider using IP sets.'),
	    },
	    {
		xtype: 'pveIPRefSelector',
		name: 'dest',
		autoSelect: false,
		editable: true,
		base_url: me.list_refs_url,
		value: '',
		fieldLabel: gettext('Destination'),
		maxLength: 512,
		maxLengthText: gettext('Too long, consider using IP sets.'),
	    },
	);
	me.column2 = [
	    {
		xtype: 'proxmoxcheckbox',
		name: 'enable',
		checked: false,
		uncheckedValue: 0,
		fieldLabel: gettext('Enable'),
	    },
	    {
		xtype: 'pveFWMacroSelector',
		name: 'macro',
		fieldLabel: gettext('Macro'),
		editable: true,
		allowBlank: true,
		listeners: {
		    change: function(f, value) {
                        if (value === null) {
			    me.down('field[name=proto]').setDisabled(false);
			    me.down('field[name=sport]').setDisabled(false);
			    me.down('field[name=dport]').setDisabled(false);
                        } else {
			    me.down('field[name=proto]').setDisabled(true);
			    me.down('field[name=proto]').setValue('');
			    me.down('field[name=sport]').setDisabled(true);
			    me.down('field[name=sport]').setValue('');
			    me.down('field[name=dport]').setDisabled(true);
			    me.down('field[name=dport]').setValue('');
                       }
                    },
                },
	    },
	    {
		xtype: 'pveIPProtocolSelector',
		name: 'proto',
		autoSelect: false,
		editable: true,
		value: '',
		fieldLabel: gettext('Protocol'),
		listeners: {
		    change: function(f, value) {
			if (value === 'icmp' || value === 'icmpv6' || value === 'ipv6-icmp') {
			    me.down('field[name=dport]').setHidden(true);
			    me.down('field[name=dport]').setDisabled(true);
			    if (value === 'icmp') {
				me.down('#icmpv4-type').setHidden(false);
				me.down('#icmpv4-type').setDisabled(false);
				me.down('#icmpv6-type').setHidden(true);
				me.down('#icmpv6-type').setDisabled(true);
			    } else {
				me.down('#icmpv6-type').setHidden(false);
				me.down('#icmpv6-type').setDisabled(false);
				me.down('#icmpv4-type').setHidden(true);
				me.down('#icmpv4-type').setDisabled(true);
			    }
			} else {
			    me.down('#icmpv4-type').setHidden(true);
			    me.down('#icmpv4-type').setDisabled(true);
			    me.down('#icmpv6-type').setHidden(true);
			    me.down('#icmpv6-type').setDisabled(true);
			    me.down('field[name=dport]').setHidden(false);
			    me.down('field[name=dport]').setDisabled(false);
			}
		    },
		},
	    },
	    {
		xtype: 'displayfield',
		fieldLabel: '',
		height: 7,
		value: '',
	    },
	    {
		xtype: 'textfield',
		name: 'sport',
		value: '',
		fieldLabel: gettext('Source port'),
	    },
	    {
		xtype: 'textfield',
		name: 'dport',
		value: '',
		fieldLabel: gettext('Dest. port'),
	    },
	    {
		xtype: 'pveICMPTypeSelector',
		name: 'icmp-type',
		id: 'icmpv4-type',
		autoSelect: false,
		editable: true,
		hidden: true,
		disabled: true,
		value: '',
		fieldLabel: gettext('ICMP type'),
		store: ICMP_TYPE_NAMES_STORE,
	    },
	    {
		xtype: 'pveICMPTypeSelector',
		name: 'icmp-type',
		id: 'icmpv6-type',
		autoSelect: false,
		editable: true,
		hidden: true,
		disabled: true,
		value: '',
		fieldLabel: gettext('ICMP type'),
		store: ICMPV6_TYPE_NAMES_STORE,
	    },
	];
	me.advancedColumn1 = [
	    {
		xtype: 'pveFirewallLogLevels',
	    },
	];
	me.columnB = [
	    {
		xtype: 'textfield',
		name: 'comment',
		value: '',
		fieldLabel: gettext('Comment'),
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.FirewallRuleEdit', {
    extend: 'Proxmox.window.Edit',
    base_url: undefined,
    list_refs_url: undefined,
    allow_iface: false,
    initComponent: function() {
	var me = this;
	if (!me.base_url) {
	    throw "no base_url specified";
	}
	if (!me.list_refs_url) {
	    throw "no list_refs_url specified";
	}
	me.isCreate = me.rule_pos === undefined;
	if (me.isCreate) {
            me.url = '/api2/extjs' + me.base_url;
            me.method = 'POST';
        } else {
            me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
            me.method = 'PUT';
        }
	var ipanel = Ext.create('PVE.FirewallRulePanel', {
	    isCreate: me.isCreate,
	    list_refs_url: me.list_refs_url,
	    allow_iface: me.allow_iface,
	    rule_pos: me.rule_pos,
	});
	Ext.apply(me, {
            subject: gettext('Rule'),
	    isAdd: true,
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    ipanel.setValues(values);
		    // set icmp-type again after protocol has been set
		    if (values["icmp-type"] !== undefined) {
			ipanel.setValues({ "icmp-type": values["icmp-type"] });
		    }
		    if (values.errors) {
			var field = me.query('[isFormField][name=modified_marker]')[0];
			field.setValue(1);
			Ext.Function.defer(function() {
			    var form = ipanel.up('form').getForm();
			    form.markInvalid(values.errors);
			}, 100);
		    }
		},
	    });
	} else if (me.rec) {
	    ipanel.setValues(me.rec.data);
	}
    },
});
Ext.define('PVE.FirewallGroupRuleEdit', {
    extend: 'Proxmox.window.Edit',
    base_url: undefined,
    allow_iface: false,
    initComponent: function() {
	var me = this;
	me.isCreate = me.rule_pos === undefined;
	if (me.isCreate) {
            me.url = '/api2/extjs' + me.base_url;
            me.method = 'POST';
        } else {
            me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
            me.method = 'PUT';
        }
	var column1 = [
	    {
		xtype: 'hiddenfield',
		name: 'type',
		value: 'group',
	    },
	    {
		xtype: 'pveSecurityGroupsSelector',
		name: 'action',
		value: '',
		fieldLabel: gettext('Security Group'),
		allowBlank: false,
	    },
	];
	if (me.allow_iface) {
	    column1.push({
		xtype: 'proxmoxtextfield',
		name: 'iface',
		deleteEmpty: !me.isCreate,
		value: '',
		fieldLabel: gettext('Interface'),
	    });
	}
	var ipanel = Ext.create('Proxmox.panel.InputPanel', {
	    isCreate: me.isCreate,
	    column1: column1,
	    column2: [
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'enable',
		    checked: false,
		    uncheckedValue: 0,
		    fieldLabel: gettext('Enable'),
		},
	    ],
	    columnB: [
		{
		    xtype: 'textfield',
		    name: 'comment',
		    value: '',
		    fieldLabel: gettext('Comment'),
		},
	    ],
	});
	Ext.apply(me, {
            subject: gettext('Rule'),
	    isAdd: true,
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.FirewallRules', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.pveFirewallRules',
    onlineHelp: 'chapter_pve_firewall',
    stateful: true,
    stateId: 'grid-firewall-rules',
    base_url: undefined,
    list_refs_url: undefined,
    addBtn: undefined,
    removeBtn: undefined,
    editBtn: undefined,
    groupBtn: undefined,
    tbar_prefix: undefined,
    allow_groups: true,
    allow_iface: false,
    setBaseUrl: function(url) {
        var me = this;
	me.base_url = url;
	if (url === undefined) {
	    me.addBtn.setDisabled(true);
	    if (me.groupBtn) {
		me.groupBtn.setDisabled(true);
	    }
	    me.store.removeAll();
	} else {
	    me.addBtn.setDisabled(false);
	    me.removeBtn.baseurl = url + '/';
	    if (me.groupBtn) {
		me.groupBtn.setDisabled(false);
	    }
	    me.store.setProxy({
		type: 'proxmox',
		url: '/api2/json' + url,
	    });
	    me.store.load();
	}
    },
    moveRule: function(from, to) {
        var me = this;
	if (!me.base_url) {
	    return;
	}
	Proxmox.Utils.API2Request({
	    url: me.base_url + "/" + from,
	    method: 'PUT',
	    params: { moveto: to },
	    waitMsgTarget: me,
	    failure: function(response, options) {
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
	    },
	    callback: function() {
		me.store.load();
	    },
	});
    },
    updateRule: function(rule) {
        var me = this;
	if (!me.base_url) {
	    return;
	}
	rule.enable = rule.enable ? 1 : 0;
	var pos = rule.pos;
	delete rule.pos;
	delete rule.errors;
	Proxmox.Utils.API2Request({
	    url: me.base_url + '/' + pos.toString(),
	    method: 'PUT',
	    params: rule,
	    waitMsgTarget: me,
	    failure: function(response, options) {
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
	    },
	    callback: function() {
		me.store.load();
	    },
	});
    },
    initComponent: function() {
        var me = this;
	if (!me.list_refs_url) {
	    throw "no list_refs_url specified";
	}
	var store = Ext.create('Ext.data.Store', {
	    model: 'pve-fw-rule',
	});
	var reload = function() {
	    store.load();
	};
	var sm = Ext.create('Ext.selection.RowModel', {});
	var run_editor = function() {
	    var rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    var type = rec.data.type;
	    var editor;
	    if (type === 'in' || type === 'out') {
		editor = 'PVE.FirewallRuleEdit';
	    } else if (type === 'group') {
		editor = 'PVE.FirewallGroupRuleEdit';
	    } else {
		return;
	    }
	    var win = Ext.create(editor, {
		digest: rec.data.digest,
		allow_iface: me.allow_iface,
		base_url: me.base_url,
		list_refs_url: me.list_refs_url,
		rule_pos: rec.data.pos,
	    });
	    win.show();
	    win.on('destroy', reload);
	};
	me.editBtn = Ext.create('Proxmox.button.Button', {
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	me.addBtn = Ext.create('Ext.Button', {
	    text: gettext('Add'),
	    disabled: true,
	    handler: function() {
		var win = Ext.create('PVE.FirewallRuleEdit', {
		    allow_iface: me.allow_iface,
		    base_url: me.base_url,
		    list_refs_url: me.list_refs_url,
		});
		win.on('destroy', reload);
		win.show();
	    },
	});
	var run_copy_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    let type = rec.data.type;
	    if (!(type === 'in' || type === 'out')) {
		return;
	    }
	    let win = Ext.create('PVE.FirewallRuleEdit', {
		allow_iface: me.allow_iface,
		base_url: me.base_url,
		list_refs_url: me.list_refs_url,
		rec: rec,
	    });
	    win.show();
	    win.on('destroy', reload);
	};
	me.copyBtn = Ext.create('Proxmox.button.Button', {
	    text: gettext('Copy'),
	    selModel: sm,
	    enableFn: ({ data }) => data.type === 'in' || data.type === 'out',
	    disabled: true,
	    handler: run_copy_editor,
	});
	if (me.allow_groups) {
	    me.groupBtn = Ext.create('Ext.Button', {
		text: gettext('Insert') + ': ' +
		    gettext('Security Group'),
		disabled: true,
		handler: function() {
		    var win = Ext.create('PVE.FirewallGroupRuleEdit', {
			allow_iface: me.allow_iface,
			base_url: me.base_url,
		    });
		    win.on('destroy', reload);
		    win.show();
		},
	    });
	}
	me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: me.base_url + '/',
	    confirmMsg: false,
	    getRecordName: function(rec) {
		var rule = rec.data;
		return rule.pos.toString() +
		    '?digest=' + encodeURIComponent(rule.digest);
	    },
	    callback: function() {
		me.store.load();
	    },
	});
	let tbar = me.tbar_prefix ? [me.tbar_prefix] : [];
	tbar.push(me.addBtn, me.copyBtn);
	if (me.groupBtn) {
	    tbar.push(me.groupBtn);
	}
	tbar.push(me.removeBtn, me.editBtn);
	let render_errors = function(name, value, metaData, record) {
	    let errors = record.data.errors;
	    if (errors && errors[name]) {
		metaData.tdCls = 'proxmox-invalid-row';
		let html = '' + Ext.htmlEncode(errors[name]) + '
';
		metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html + '"';
	    }
	    return value;
	};
	let columns = [
	    {
		// similar to xtype: 'rownumberer',
		dataIndex: 'pos',
		resizable: false,
		minWidth: 65,
		maxWidth: 83,
		flex: 1,
		sortable: false,
		hideable: false,
		menuDisabled: true,
		renderer: function(value, metaData, record, rowIdx, colIdx) {
		    metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
		    let dragHandle = "";
		    if (value >= 0) {
			return dragHandle + value;
		    }
		    return dragHandle;
		},
	    },
	    {
		xtype: 'checkcolumn',
		header: gettext('On'),
		dataIndex: 'enable',
		listeners: {
		    checkchange: function(column, recordIndex, checked) {
			var record = me.getStore().getData().items[recordIndex];
			record.commit();
			var data = {};
			Ext.Array.forEach(record.getFields(), function(field) {
			    data[field.name] = record.get(field.name);
			});
			if (!me.allow_iface || !data.iface) {
			    delete data.iface;
			}
			me.updateRule(data);
		    },
		},
		width: 40,
	    },
	    {
		header: gettext('Type'),
		dataIndex: 'type',
		renderer: function(value, metaData, record) {
		    return render_errors('type', value, metaData, record);
		},
		minWidth: 60,
		maxWidth: 80,
		flex: 2,
	    },
	    {
		header: gettext('Action'),
		dataIndex: 'action',
		renderer: function(value, metaData, record) {
		    return render_errors('action', value, metaData, record);
		},
		minWidth: 80,
		maxWidth: 200,
		flex: 2,
	    },
	    {
		header: gettext('Macro'),
		dataIndex: 'macro',
		renderer: function(value, metaData, record) {
		    return render_errors('macro', value, metaData, record);
		},
		minWidth: 80,
		flex: 2,
	    },
	];
	if (me.allow_iface) {
	    columns.push({
		header: gettext('Interface'),
		dataIndex: 'iface',
		renderer: function(value, metaData, record) {
		    return render_errors('iface', value, metaData, record);
		},
		minWidth: 80,
		flex: 2,
	    });
	}
	columns.push(
	    {
		header: gettext('Protocol'),
		dataIndex: 'proto',
		renderer: function(value, metaData, record) {
		    return render_errors('proto', value, metaData, record);
		},
		width: 75,
	    },
	    {
		header: gettext('Source'),
		dataIndex: 'source',
		renderer: function(value, metaData, record) {
		    return render_errors('source', value, metaData, record);
		},
		minWidth: 100,
		flex: 2,
	    },
	    {
		header: gettext('S.Port'),
		dataIndex: 'sport',
		renderer: function(value, metaData, record) {
		    return render_errors('sport', value, metaData, record);
		},
		width: 75,
	    },
	    {
		header: gettext('Destination'),
		dataIndex: 'dest',
		renderer: function(value, metaData, record) {
		    return render_errors('dest', value, metaData, record);
		},
		minWidth: 100,
		flex: 2,
	    },
	    {
		header: gettext('D.Port'),
		dataIndex: 'dport',
		renderer: function(value, metaData, record) {
		    return render_errors('dport', value, metaData, record);
		},
		width: 75,
	    },
	    {
		header: gettext('Log level'),
		dataIndex: 'log',
		renderer: function(value, metaData, record) {
		    return render_errors('log', value, metaData, record);
		},
		width: 100,
	    },
	    {
		header: gettext('Comment'),
		dataIndex: 'comment',
		flex: 10,
		minWidth: 75,
		renderer: function(value, metaData, record) {
		    let comment = render_errors('comment', Ext.util.Format.htmlEncode(value), metaData, record) || '';
		    if (comment.length * 12 > metaData.column.cellWidth) {
			comment = `${comment}`;
		    }
		    return comment;
		},
	    },
	);
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: tbar,
	    viewConfig: {
		plugins: [
		    {
			ptype: 'gridviewdragdrop',
			dragGroup: 'FWRuleDDGroup',
			dropGroup: 'FWRuleDDGroup',
		    },
		],
		listeners: {
		    beforedrop: function(node, data, dropRec, dropPosition) {
			if (!dropRec) {
			    return false; // empty view
			}
			let moveto = dropRec.get('pos');
			if (dropPosition === 'after') {
			    moveto++;
			}
			let pos = data.records[0].get('pos');
			me.moveRule(pos, moveto);
			return 0;
                    },
		    itemdblclick: run_editor,
		},
	    },
	    sortableColumns: false,
	    columns: columns,
	});
	me.callParent();
	if (me.base_url) {
	    me.setBaseUrl(me.base_url); // load
	}
    },
}, function() {
    Ext.define('pve-fw-rule', {
	extend: 'Ext.data.Model',
	fields: [
	    { name: 'enable', type: 'boolean' },
	    'type',
	    'action',
	    'macro',
	    'source',
	    'dest',
	    'proto',
	    'iface',
	    'dport',
	    'sport',
	    'comment',
	    'pos',
	    'digest',
	    'errors',
	],
	idProperty: 'pos',
    });
});
Ext.define('PVE.pool.AddVM', {
    extend: 'Proxmox.window.Edit',
    width: 600,
    height: 400,
    isAdd: true,
    isCreate: true,
    initComponent: function() {
	var me = this;
	if (!me.pool) {
	    throw "no pool specified";
	}
	me.url = "/pools/" + me.pool;
	me.method = 'PUT';
	var vmsField = Ext.create('Ext.form.field.Text', {
	    name: 'vms',
	    hidden: true,
	    allowBlank: false,
	});
	var vmStore = Ext.create('Ext.data.Store', {
	    model: 'PVEResources',
	    sorters: [
		{
		    property: 'vmid',
		    direction: 'ASC',
		},
	    ],
	    filters: [
		function(item) {
		    return (item.data.type === 'lxc' || item.data.type === 'qemu') && item.data.pool === '';
		},
	    ],
	});
	var vmGrid = Ext.create('widget.grid', {
	    store: vmStore,
	    border: true,
	    height: 300,
	    scrollable: true,
	    selModel: {
		selType: 'checkboxmodel',
		mode: 'SIMPLE',
		listeners: {
		    selectionchange: function(model, selected, opts) {
			var selectedVms = [];
			selected.forEach(function(vm) {
			    selectedVms.push(vm.data.vmid);
			});
			vmsField.setValue(selectedVms);
		    },
		},
	    },
	    columns: [
		{
		    header: 'ID',
		    dataIndex: 'vmid',
		    width: 60,
		},
		{
		    header: gettext('Node'),
		    dataIndex: 'node',
		},
		{
		    header: gettext('Status'),
		    dataIndex: 'uptime',
		    renderer: function(value) {
			if (value) {
			    return Proxmox.Utils.runningText;
			} else {
			    return Proxmox.Utils.stoppedText;
			}
		    },
		},
		{
		    header: gettext('Name'),
		    dataIndex: 'name',
		    flex: 1,
		},
		{
		    header: gettext('Type'),
		    dataIndex: 'type',
		},
	    ],
	});
	Ext.apply(me, {
	    subject: gettext('Virtual Machine'),
	    items: [vmsField, vmGrid],
	});
	me.callParent();
	vmStore.load();
    },
});
Ext.define('PVE.pool.AddStorage', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	if (!me.pool) {
	    throw "no pool specified";
	}
	me.isCreate = true;
	me.isAdd = true;
	me.url = "/pools/" + me.pool;
	me.method = 'PUT';
	Ext.apply(me, {
	    subject: gettext('Storage'),
	    width: 350,
	    items: [
		{
		    xtype: 'pveStorageSelector',
		    name: 'storage',
		    nodename: 'localhost',
		    autoSelect: false,
		    value: '',
		    fieldLabel: gettext("Storage"),
		},
	    ],
	});
	me.callParent();
    },
});
Ext.define('PVE.grid.PoolMembers', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pvePoolMembers'],
    // fixme: dynamic status update ?
    stateful: true,
    stateId: 'grid-pool-members',
    initComponent: function() {
	var me = this;
	if (!me.pool) {
	    throw "no pool specified";
	}
	var store = Ext.create('Ext.data.Store', {
	    model: 'PVEResources',
	    sorters: [
		{
		    property: 'type',
		    direction: 'ASC',
		},
	    ],
	    proxy: {
		type: 'proxmox',
		root: 'data.members',
		url: "/api2/json/pools/" + me.pool,
	    },
	});
	var coldef = PVE.data.ResourceStore.defaultColumns().filter((c) =>
	    c.dataIndex !== 'tags' && c.dataIndex !== 'lock',
	);
	var reload = function() {
	    store.load();
	};
	var sm = Ext.create('Ext.selection.RowModel', {});
	var remove_btn = new Proxmox.button.Button({
	    text: gettext('Remove'),
	    disabled: true,
	    selModel: sm,
	    confirmMsg: function(rec) {
		return Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
					 "'" + rec.data.id + "'");
	    },
	    handler: function(btn, event, rec) {
		var params = { 'delete': 1 };
		if (rec.data.type === 'storage') {
		    params.storage = rec.data.storage;
		} else if (rec.data.type === 'qemu' || rec.data.type === 'lxc' || rec.data.type === 'openvz') {
		    params.vms = rec.data.vmid;
		} else {
		    throw "unknown resource type";
		}
		Proxmox.Utils.API2Request({
		    url: '/pools/' + me.pool,
		    method: 'PUT',
		    params: params,
		    waitMsgTarget: me,
		    callback: function() {
			reload();
		    },
		    failure: function(response, opts) {
			Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		    },
		});
	    },
	});
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: new Ext.menu.Menu({
			items: [
			    {
				text: gettext('Virtual Machine'),
				iconCls: 'pve-itype-icon-qemu',
				handler: function() {
				    var win = Ext.create('PVE.pool.AddVM', { pool: me.pool });
				    win.on('destroy', reload);
				    win.show();
				},
			    },
			    {
				text: gettext('Storage'),
				iconCls: 'pve-itype-icon-storage',
				handler: function() {
				    var win = Ext.create('PVE.pool.AddStorage', { pool: me.pool });
				    win.on('destroy', reload);
				    win.show();
				},
			    },
			],
		    }),
		},
		remove_btn,
	    ],
	    viewConfig: {
		stripeRows: true,
            },
            columns: coldef,
	    listeners: {
		itemcontextmenu: PVE.Utils.createCmdMenu,
		itemdblclick: function(v, record) {
		    var ws = me.up('pveStdWorkspace');
		    ws.selectById(record.data.id);
		},
		activate: reload,
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.window.ReplicaEdit', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveReplicaEdit',
    subject: gettext('Replication Job'),
    url: '/cluster/replication',
    method: 'POST',
    initComponent: function() {
	var me = this;
	var vmid = me.pveSelNode.data.vmid;
	var nodename = me.pveSelNode.data.node;
	var items = [];
	items.push({
	    xtype: me.isCreate && !vmid?'pveGuestIDSelector':'displayfield',
	    name: 'guest',
	    fieldLabel: 'CT/VM ID',
	    value: vmid || '',
	});
	items.push(
	    {
		xtype: me.isCreate ? 'pveNodeSelector':'displayfield',
		name: 'target',
		disallowedNodes: [nodename],
		allowBlank: false,
		onlineValidator: true,
		fieldLabel: gettext("Target"),
	    },
	    {
		xtype: 'pveCalendarEvent',
		fieldLabel: gettext('Schedule'),
		emptyText: '*/15 - ' + Ext.String.format(gettext('Every {0} minutes'), 15),
		name: 'schedule',
	    },
	    {
		xtype: 'numberfield',
		fieldLabel: gettext('Rate limit') + ' (MB/s)',
		step: 1,
		minValue: 1,
		emptyText: gettext('unlimited'),
		name: 'rate',
	    },
	    {
		xtype: 'textfield',
		fieldLabel: gettext('Comment'),
		name: 'comment',
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'enabled',
		defaultValue: 'on',
		checked: true,
		fieldLabel: gettext('Enabled'),
	    },
	);
	me.items = [
	    {
		xtype: 'inputpanel',
		itemId: 'ipanel',
		onlineHelp: 'pvesr_schedule_time_format',
		onGetValues: function(values) {
		    let win = this.up('window');
		    values.disable = values.enabled ? 0 : 1;
		    delete values.enabled;
		    PVE.Utils.delete_if_default(values, 'rate', '', win.isCreate);
		    PVE.Utils.delete_if_default(values, 'disable', 0, win.isCreate);
		    PVE.Utils.delete_if_default(values, 'schedule', '*/15', win.isCreate);
		    PVE.Utils.delete_if_default(values, 'comment', '', win.isCreate);
		    if (win.isCreate) {
			values.type = 'local';
			let vm = vmid || values.guest;
			let id = -1;
			if (win.highestids[vm] !== undefined) {
			    id = win.highestids[vm];
			}
			id++;
			values.id = vm + '-' + id.toString();
			delete values.guest;
		    }
		    return values;
		},
		items: items,
	    },
	];
	me.callParent();
	if (me.isCreate) {
	    me.load({
		success: function(response) {
		    var jobs = response.result.data;
		    var highestids = {};
		    Ext.Array.forEach(jobs, function(job) {
			var match = /^([0-9]+)-([0-9]+)$/.exec(job.id);
			if (match) {
			    let jobVMID = parseInt(match[1], 10);
			    let id = parseInt(match[2], 10);
			    if (highestids[jobVMID] === undefined || highestids[jobVMID] < id) {
				highestids[jobVMID] = id;
			    }
			}
		    });
		    me.highestids = highestids;
		},
	    });
	} else {
	    me.load({
		success: function(response, options) {
		    response.result.data.enabled = !response.result.data.disable;
		    me.setValues(response.result.data);
		    me.digest = response.result.data.digest;
		},
	    });
	}
    },
});
/* callback is a function and string */
Ext.define('PVE.grid.ReplicaView', {
    extend: 'Ext.grid.Panel',
    xtype: 'pveReplicaView',
    onlineHelp: 'chapter_pvesr',
    stateful: true,
    stateId: 'grid-pve-replication-status',
    controller: {
	xclass: 'Ext.app.ViewController',
	addJob: function(button, event, rec) {
	    let me = this;
	    let view = me.getView();
	    Ext.create('PVE.window.ReplicaEdit', {
		isCreate: true,
		method: 'POST',
		pveSelNode: view.pveSelNode,
		listeners: {
		    destroy: () => me.reload(),
		},
		autoShow: true,
	    });
	},
	editJob: function(button, event, { data }) {
	    let me = this;
	    let view = me.getView();
	    Ext.create('PVE.window.ReplicaEdit', {
		url: `/cluster/replication/${data.id}`,
		method: 'PUT',
		pveSelNode: view.pveSelNode,
		listeners: {
		    destroy: () => me.reload(),
		},
		autoShow: true,
	    });
	},
	scheduleJobNow: function(button, event, rec) {
	    let me = this;
	    let view = me.getView();
	    Proxmox.Utils.API2Request({
		url: `/api2/extjs/nodes/${view.nodename}/replication/${rec.data.id}/schedule_now`,
		method: 'POST',
		waitMsgTarget: view,
		callback: () => me.reload(),
		failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
	    });
	},
	showLog: function(button, event, rec) {
	    let me = this;
	    let view = this.getView();
	    let logView = Ext.create('Proxmox.panel.LogView', {
		border: false,
		url: `/api2/extjs/nodes/${view.nodename}/replication/${rec.data.id}/log`,
	    });
	    let task = Ext.TaskManager.newTask({
		run: () => logView.requestUpdate(),
		interval: 1000,
	    });
	    let win = Ext.create('Ext.window.Window', {
		items: [logView],
		layout: 'fit',
		width: 800,
		height: 400,
		modal: true,
		title: gettext("Replication Log"),
		listeners: {
		    destroy: function() {
			task.stop();
			me.reload();
		    },
		},
	    });
	    task.start();
	    win.show();
	},
	reload: function() {
	    this.getView().rstore.load();
	},
	dblClick: function(grid, record, item) {
	    this.editJob(undefined, undefined, record);
	},
	// currently replication is for cluster only, so disable the whole component for non-cluster
	checkPrerequisites: function() {
	    let view = this.getView();
	    if (PVE.data.ResourceStore.getNodes().length < 2) {
		view.mask(gettext("Replication needs at least two nodes"), ['pve-static-mask']);
	    }
	},
	control: {
	    '#': {
		itemdblclick: 'dblClick',
		afterlayout: 'checkPrerequisites',
	    },
	},
    },
    tbar: [
	{
	    text: gettext('Add'),
	    itemId: 'addButton',
	    handler: 'addJob',
	},
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Edit'),
	    itemId: 'editButton',
	    handler: 'editJob',
	    disabled: true,
	},
	{
	    xtype: 'proxmoxStdRemoveButton',
	    itemId: 'removeButton',
	    baseurl: '/api2/extjs/cluster/replication/',
	    dangerous: true,
	    callback: 'reload',
	},
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Log'),
	    itemId: 'logButton',
	    handler: 'showLog',
	    disabled: true,
	},
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Schedule now'),
	    itemId: 'scheduleNowButton',
	    handler: 'scheduleJobNow',
	    disabled: true,
	},
    ],
    initComponent: function() {
	var me = this;
	var mode = '';
	var url = '/cluster/replication';
	me.nodename = me.pveSelNode.data.node;
	me.vmid = me.pveSelNode.data.vmid;
	me.columns = [
	    {
		header: gettext('Enabled'),
		width: 80,
		dataIndex: 'enabled',
		align: 'center',
		// TODO: switch to Proxmox.Utils.renderEnabledIcon once available
		renderer: enabled => ``,
		sortable: true,
	    },
	    {
		text: 'ID',
		dataIndex: 'id',
		width: 60,
		hidden: true,
	    },
	    {
		text: gettext('Guest'),
		dataIndex: 'guest',
		width: 75,
	    },
	    {
		text: gettext('Job'),
		dataIndex: 'jobnum',
		width: 60,
	    },
	    {
		text: gettext('Target'),
		dataIndex: 'target',
	    },
	];
	if (!me.nodename) {
	    mode = 'dc';
	    me.stateId = 'grid-pve-replication-dc';
	} else if (!me.vmid) {
	    mode = 'node';
	    url = `/nodes/${me.nodename}/replication`;
	} else {
	    mode = 'vm';
	    url = `/nodes/${me.nodename}/replication?guest=${me.vmid}`;
	}
	if (mode !== 'dc') {
	    me.columns.push(
		{
		    text: gettext('Status'),
		    dataIndex: 'state',
		    minWidth: 160,
		    flex: 1,
		    renderer: function(value, metadata, record) {
			if (record.data.pid) {
			    metadata.tdCls = 'x-grid-row-loading';
			    return '';
			}
			let icons = [], states = [];
			if (record.data.remove_job) {
			    icons.push('');
			    states.push(gettext("Removal Scheduled"));
			}
			if (record.data.error) {
			    icons.push('');
			    states.push(record.data.error);
			}
			if (icons.length === 0) {
			    icons.push('');
			    states.push(gettext('OK'));
			}
			return icons.join(',') + ' ' + states.join(',');
		    },
		},
		{
		    text: gettext('Last Sync'),
		    dataIndex: 'last_sync',
		    width: 150,
		    renderer: function(value, metadata, record) {
			if (!value) {
			    return '-';
			}
			if (record.data.pid) {
			    return gettext('syncing');
			}
			return Proxmox.Utils.render_timestamp(value);
		    },
		},
		{
		    text: gettext('Duration'),
		    dataIndex: 'duration',
		    width: 60,
		    renderer: Proxmox.Utils.render_duration,
		},
		{
		    text: gettext('Next Sync'),
		    dataIndex: 'next_sync',
		    width: 150,
		    renderer: function(value) {
			if (!value) {
			    return '-';
			}
			let now = new Date(), next = new Date(value * 1000);
			if (next < now) {
			    return gettext('pending');
			}
			return Proxmox.Utils.render_timestamp(value);
		    },
		},
	    );
	}
	me.columns.push(
	    {
		text: gettext('Schedule'),
		width: 75,
		dataIndex: 'schedule',
	    },
	    {
		text: gettext('Rate limit'),
		dataIndex: 'rate',
		renderer: function(value) {
		    if (!value) {
			return gettext('unlimited');
		    }
		    return value.toString() + ' MB/s';
		},
		hidden: true,
	    },
	    {
		text: gettext('Comment'),
		dataIndex: 'comment',
		renderer: Ext.htmlEncode,
	    },
	);
	me.rstore = Ext.create('Proxmox.data.UpdateStore', {
	    storeid: 'pve-replica-' + me.nodename + me.vmid,
	    model: mode === 'dc'? 'pve-replication' : 'pve-replication-state',
	    interval: 3000,
	    proxy: {
		type: 'proxmox',
		url: "/api2/json" + url,
	    },
	});
	me.store = Ext.create('Proxmox.data.DiffStore', {
	    rstore: me.rstore,
	    sorters: [
		{
		    property: 'guest',
		},
		{
		    property: 'jobnum',
		},
	    ],
	});
	me.callParent();
	// we cannot access the log and scheduleNow button
	// in the datacenter, because
	// we do not know where/if the jobs runs
	if (mode === 'dc') {
	    me.down('#logButton').setHidden(true);
	    me.down('#scheduleNowButton').setHidden(true);
	}
	// if we set the warning mask, we do not want to load
	// or set the mask on store errors
	if (PVE.data.ResourceStore.getNodes().length < 2) {
	    return;
	}
	Proxmox.Utils.monStoreErrors(me, me.rstore);
	me.on('destroy', me.rstore.stopUpdate);
	me.rstore.startUpdate();
    },
}, function() {
    Ext.define('pve-replication', {
	extend: 'Ext.data.Model',
	fields: [
	    'id', 'target', 'comment', 'rate', 'type',
	    { name: 'guest', type: 'integer' },
	    { name: 'jobnum', type: 'integer' },
	    { name: 'schedule', defaultValue: '*/15' },
	    { name: 'disable', defaultValue: '' },
	    { name: 'enabled', calculate: function(data) { return !data.disable; } },
	],
    });
    Ext.define('pve-replication-state', {
	extend: 'pve-replication',
	fields: [
	    'last_sync', 'next_sync', 'error', 'duration', 'state',
	    'fail_count', 'remove_job', 'pid',
	],
    });
});
Ext.define('PVE.grid.ResourceGrid', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveResourceGrid'],
    border: false,
    defaultSorter: {
	property: 'type',
	direction: 'ASC',
    },
    userCls: 'proxmox-tags-full',
    initComponent: function() {
	let me = this;
	let rstore = PVE.data.ResourceStore;
	let store = Ext.create('Ext.data.Store', {
	    model: 'PVEResources',
	    sorters: me.defaultSorter,
	    proxy: {
		type: 'memory',
	    },
	});
	let textfilter = '';
	let textfilterMatch = function(item) {
	    for (const field of ['name', 'storage', 'node', 'type', 'text']) {
		let v = item.data[field];
		if (v && v.toLowerCase().indexOf(textfilter) >= 0) {
		    return true;
		}
	    }
	    return false;
	};
	let updateGrid = function() {
	    var filterfn = me.viewFilter ? me.viewFilter.filterfn : null;
	    store.suspendEvents();
	    let nodeidx = {};
	    let gather_child_nodes;
	    gather_child_nodes = function(node) {
		if (!node || !node.childNodes) {
		    return;
		}
		for (let child of node.childNodes) {
		    let orgNode = rstore.data.get(child.data.id);
		    if (orgNode) {
			if ((!filterfn || filterfn(child)) && (!textfilter || textfilterMatch(child))) {
			    nodeidx[child.data.id] = orgNode;
			}
		    }
		    gather_child_nodes(child);
		}
	    };
	    gather_child_nodes(me.pveSelNode);
	    // remove vanished items
	    let rmlist = [];
	    store.each(olditem => {
		if (!nodeidx[olditem.data.id]) {
		    rmlist.push(olditem);
		}
	    });
	    if (rmlist.length) {
		store.remove(rmlist);
	    }
	    // add new items
	    let addlist = [];
	    for (const [_key, item] of Object.entries(nodeidx)) {
		// getById() use find(), which is slow (ExtJS4 DP5)
		let olditem = store.data.get(item.data.id);
		if (!olditem) {
		    addlist.push(item);
		    continue;
		}
		let changes = false;
		for (let field of PVE.data.ResourceStore.fieldNames) {
		    if (field !== 'id' && item.data[field] !== olditem.data[field]) {
			changes = true;
			olditem.beginEdit();
			olditem.set(field, item.data[field]);
		    }
		}
		if (changes) {
		    olditem.endEdit(true);
		    olditem.commit(true);
		}
	    }
	    if (addlist.length) {
		store.add(addlist);
	    }
	    store.sort();
	    store.resumeEvents();
	    store.fireEvent('refresh', store);
	};
	Ext.apply(me, {
	    store: store,
	    stateful: true,
	    stateId: 'grid-resource',
	    tbar: [
		'->',
		gettext('Search') + ':', ' ',
		{
		    xtype: 'textfield',
		    width: 200,
		    value: textfilter,
		    enableKeyEvents: true,
		    listeners: {
			buffer: 500,
			keyup: function(field, e) {
			    textfilter = field.getValue().toLowerCase();
			    updateGrid();
			},
		    },
		},
	    ],
	    viewConfig: {
		stripeRows: true,
            },
	    listeners: {
		itemcontextmenu: PVE.Utils.createCmdMenu,
		itemdblclick: function(v, record) {
		    var ws = me.up('pveStdWorkspace');
		    ws.selectById(record.data.id);
		},
		afterrender: function() {
		    updateGrid();
		},
	    },
            columns: rstore.defaultColumns(),
	});
	me.callParent();
	me.mon(rstore, 'load', () => updateGrid());
    },
});
/*
 * Base class for all the multitab config panels
 *
 * How to use this:
 *
 * You create a subclass of this, and then define your wanted tabs
 * as items like this:
 *
 * items: [{
 *  title: "myTitle",
 *  xytpe: "somextype",
 *  iconCls: 'fa fa-icon',
 *  groups: ['somegroup'],
 *  expandedOnInit: true,
 *  itemId: 'someId'
 * }]
 *
 * this has to be in the declarative syntax, else we
 * cannot save them for later
 * (so no Ext.create or Ext.apply of an item in the subclass)
 *
 * the groups array expects the itemids of the items
 * which are the parents, which have to come before they
 * are used
 *
 * if you want following the tree:
 *
 * Option1
 * Option2
 *   -> SubOption1
 *	-> SubSubOption1
 *
 * the suboption1 group array has to look like this:
 * groups: ['itemid-of-option2']
 *
 * and of subsuboption1:
 * groups: ['itemid-of-option2', 'itemid-of-suboption1']
 *
 * setting the expandedOnInit determines if the item/group is expanded
 * initially (false by default)
 */
Ext.define('PVE.panel.Config', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pvePanelConfig',
    showSearch: true, // add a resource grid with a search button as first tab
    viewFilter: undefined, // a filter to pass to that resource grid
    tbarSpacing: true, // if true, adds a spacer after the title in tbar
    dockedItems: [{
	// this is needed for the overflow handler
	xtype: 'toolbar',
	overflowHandler: 'scroller',
	dock: 'left',
	style: {
	    padding: 0,
	    margin: 0,
	},
	cls: 'pve-toolbar-bg',
	items: {
	    xtype: 'treelist',
	    itemId: 'menu',
	    ui: 'pve-nav',
	    expanderOnly: true,
	    expanderFirst: false,
	    animation: false,
	    singleExpand: false,
	    listeners: {
		selectionchange: function(treeList, selection) {
		    if (!selection) {
			return;
		    }
		    let view = this.up('panel');
		    view.suspendLayout = true;
		    view.activateCard(selection.data.id);
		    view.suspendLayout = false;
		    view.updateLayout();
		},
		itemclick: function(treelist, info) {
		    var olditem = treelist.getSelection();
		    var newitem = info.node;
		    // when clicking on the expand arrow, we don't select items, but still want the original behaviour
		    if (info.select === false) {
			return;
		    }
		    // click on a different, open item then leave it open, else toggle the clicked item
		    if (olditem.data.id !== newitem.data.id &&
			newitem.data.expanded === true) {
			info.toggle = false;
		    } else {
			info.toggle = true;
		    }
		},
	    },
	},
    },
    {
	xtype: 'toolbar',
	itemId: 'toolbar',
	dock: 'top',
	height: 36,
	overflowHandler: 'scroller',
    }],
    firstItem: '',
    layout: 'card',
    border: 0,
    // used for automated test
    selectById: function(cardid) {
	var me = this;
	var root = me.store.getRoot();
	var selection = root.findChild('id', cardid, true);
	if (selection) {
	    selection.expand();
	    var menu = me.down('#menu');
	    menu.setSelection(selection);
	    return cardid;
	}
	return '';
    },
    activateCard: function(cardid) {
	var me = this;
	if (me.savedItems[cardid]) {
	    var curcard = me.getLayout().getActiveItem();
	    var newcard = me.add(me.savedItems[cardid]);
	    me.helpButton.setOnlineHelp(newcard.onlineHelp || me.onlineHelp);
	    if (curcard) {
		me.setActiveItem(cardid);
		me.remove(curcard, true);
		// trigger state change
		var ncard = cardid;
		// Note: '' is alias for first tab.
		// First tab can be 'search' or something else
		if (cardid === me.firstItem) {
		    ncard = '';
		}
		if (me.hstateid) {
		   me.sp.set(me.hstateid, { value: ncard });
		}
	    }
	}
    },
    initComponent: function() {
        var me = this;
	var stateid = me.hstateid;
	me.sp = Ext.state.Manager.getProvider();
	var activeTab; // leaving this undefined means items[0] will be the default tab
	if (stateid) {
	    let state = me.sp.get(stateid);
	    if (state && state.value) {
		// if this tab does not exist, it chooses the first
		activeTab = state.value;
	    }
	}
	// get title
	var title = me.title || me.pveSelNode.data.text;
	me.title = undefined;
	// create toolbar
	var tbar = me.tbar || [];
	me.tbar = undefined;
	if (!me.onlineHelp) {
	    // use the onlineHelp property indirection to enforce checking reference validity
	    let typeToOnlineHelp = {
		'type/lxc': { onlineHelp: 'chapter_pct' },
		'type/node': { onlineHelp: 'chapter_system_administration' },
		'type/pool': { onlineHelp: 'pveum_pools' },
		'type/qemu': { onlineHelp: 'chapter_virtual_machines' },
		'type/sdn': { onlineHelp: 'chapter_pvesdn' },
		'type/storage': { onlineHelp: 'chapter_storage' },
	    };
	    me.onlineHelp = typeToOnlineHelp[me.pveSelNode.data.id]?.onlineHelp;
	}
	if (me.tbarSpacing) {
	    tbar.unshift('->');
	}
	tbar.unshift({
	    xtype: 'tbtext',
	    text: title,
	    baseCls: 'x-panel-header-text',
	});
	me.helpButton = Ext.create('Proxmox.button.Help', {
	    hidden: false,
	    listenToGlobalEvent: false,
	    onlineHelp: me.onlineHelp || undefined,
	});
	tbar.push(me.helpButton);
	me.dockedItems[1].items = tbar;
	// include search tab
	me.items = me.items || [];
	if (me.showSearch) {
	    me.items.unshift({
		xtype: 'pveResourceGrid',
		itemId: 'search',
		title: gettext('Search'),
		iconCls: 'fa fa-search',
		pveSelNode: me.pveSelNode,
	    });
	}
	me.savedItems = {};
	if (me.items[0]) {
	    me.firstItem = me.items[0].itemId;
	}
	me.store = Ext.create('Ext.data.TreeStore', {
	    root: {
		expanded: true,
	    },
	});
	var root = me.store.getRoot();
	me.insertNodes(me.items);
	delete me.items;
	me.defaults = me.defaults || {};
	Ext.apply(me.defaults, {
	    pveSelNode: me.pveSelNode,
	    viewFilter: me.viewFilter,
	    workspace: me.workspace,
	    border: 0,
	});
	me.callParent();
	var menu = me.down('#menu');
	var selection = root.findChild('id', activeTab, true) || root.firstChild;
	var node = selection;
	while (node !== root) {
	    node.expand();
	    node = node.parentNode;
	}
	menu.setStore(me.store);
	menu.setSelection(selection);
	// on a state change,
	// select the new item
	var statechange = function(sp, key, state) {
	    // it the state change is for this panel
	    if (stateid && key === stateid && state) {
		// get active item
		var acard = me.getLayout().getActiveItem().itemId;
		// get the itemid of the new value
		var ncard = state.value || me.firstItem;
		if (ncard && acard !== ncard) {
		    // select the chosen item
		    menu.setSelection(root.findChild('id', ncard, true) || root.firstChild);
		}
	    }
	};
	if (stateid) {
	    me.mon(me.sp, 'statechange', statechange);
	}
    },
    insertNodes: function(items) {
	var me = this;
	var root = me.store.getRoot();
	items.forEach(function(item) {
	    var treeitem = Ext.create('Ext.data.TreeModel', {
		id: item.itemId,
		text: item.title,
		iconCls: item.iconCls,
		leaf: true,
		expanded: item.expandedOnInit,
	    });
	    item.header = false;
	    if (me.savedItems[item.itemId] !== undefined) {
		throw "itemId already exists, please use another";
	    }
	    me.savedItems[item.itemId] = item;
	    var group;
	    var curnode = root;
	    // get/create the group items
	    while (Ext.isArray(item.groups) && item.groups.length > 0) {
		group = item.groups.shift();
		var child = curnode.findChild('id', group);
		if (child === null) {
		    // did not find the group item
		    // so add it where we are
		    break;
		}
		curnode = child;
	    }
	    // insert the item
	    // lets see if it already exists
	    var node = curnode.findChild('id', item.itemId);
	    if (node === null) {
		curnode.appendChild(treeitem);
	    } else {
		// should not happen!
		throw "id already exists";
	    }
	});
    },
});
/*
 * Input panel for prune settings with a keep-all option intended to be used as
 * part of an edit/create window.
 */
Ext.define('PVE.panel.BackupJobPrune', {
    extend: 'Proxmox.panel.PruneInputPanel',
    xtype: 'pveBackupJobPrunePanel',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'vzdump_retention',
    onGetValues: function(formValues) {
	if (this.needMask) { // isMasked() may not yet be true if not rendered once
	    return {};
	} else if (this.isCreate && !this.rendered) {
	    return this.keepAllDefaultForCreate ? { 'prune-backups': 'keep-all=1' } : {};
	}
	let options = { 'delete': [] };
	if ('max-protected-backups' in formValues) {
	    options['max-protected-backups'] = formValues['max-protected-backups'];
	} else if (this.hasMaxProtected) {
	    options.delete.push('max-protected-backups');
	}
	delete formValues['max-protected-backups'];
	delete formValues.delete;
	let retention = PVE.Parser.printPropertyString(formValues);
	if (retention === '') {
	    options.delete.push('prune-backups');
	} else {
	    options['prune-backups'] = retention;
	}
	if (!this.isCreate) {
	    // always delete old 'maxfiles' on edit, we map it to keep-last on window load
	    options.delete.push('maxfiles');
	} else {
	    delete options.delete;
	}
	return options;
    },
    updateComponents: function() {
	let me = this;
	let keepAll = me.down('proxmoxcheckbox[name=keep-all]').getValue();
	let anyValue = false;
	me.query('pmxPruneKeepField').forEach(field => {
	    anyValue = anyValue || field.getValue() !== null;
	    field.setDisabled(keepAll);
	});
	me.down('component[name=no-keeps-hint]').setHidden(anyValue || keepAll);
    },
    listeners: {
	afterrender: function(panel) {
	    if (panel.needMask) {
		panel.down('component[name=no-keeps-hint]').setHtml('');
		panel.mask(
		    gettext('Backup content type not available for this storage.'),
		);
	    } else if (panel.isCreate && panel.keepAllDefaultForCreate) {
		panel.down('proxmoxcheckbox[name=keep-all]').setValue(true);
	    }
	    panel.down('component[name=pbs-hint]').setHidden(!panel.showPBSHint);
	    let maxProtected = panel.down('proxmoxintegerfield[name=max-protected-backups]');
	    maxProtected.setDisabled(!panel.hasMaxProtected);
	    maxProtected.setHidden(!panel.hasMaxProtected);
	    panel.query('pmxPruneKeepField').forEach(field => {
		field.on('change', panel.updateComponents, panel);
	    });
	    panel.updateComponents();
	},
    },
    columnT: {
	xtype: 'proxmoxcheckbox',
	name: 'keep-all',
	boxLabel: gettext('Keep all backups'),
	listeners: {
	    change: function(field, newValue) {
		let panel = field.up('pveBackupJobPrunePanel');
		panel.updateComponents();
	    },
	},
    },
    columnB: [
	{
	    xtype: 'component',
	    userCls: 'pmx-hint',
	    name: 'no-keeps-hint',
	    hidden: true,
	    padding: '5 1',
	    cbind: {
		html: '{fallbackHintHtml}',
	    },
	},
	{
	    xtype: 'component',
	    userCls: 'pmx-hint',
	    name: 'pbs-hint',
	    hidden: true,
	    padding: '5 1',
	    html: gettext("It's preferred to configure backup retention directly on the Proxmox Backup Server."),
	},
	{
	    xtype: 'proxmoxintegerfield',
	    name: 'max-protected-backups',
	    fieldLabel: gettext('Maximum Protected'),
	    minValue: -1,
	    hidden: true,
	    disabled: true,
	    emptyText: 'unlimited with Datastore.Allocate privilege, 5 otherwise',
	    deleteEmpty: true,
	    autoEl: {
		tag: 'div',
		'data-qtip': Ext.String.format(gettext('Use {0} for unlimited'), -1),
	    },
	},
    ],
});
Ext.define('PVE.widget.HealthWidget', {
    extend: 'Ext.Component',
    alias: 'widget.pveHealthWidget',
    data: {
	iconCls: PVE.Utils.get_health_icon(undefined, true),
	text: '',
	title: '',
    },
    style: {
	'text-align': 'center',
    },
    tpl: [
	'{title}
',
	'',
	'
',
	'{text}',
    ],
    updateHealth: function(data) {
	var me = this;
	me.update(Ext.apply(me.data, data));
    },
    initComponent: function() {
	var me = this;
	if (me.title) {
	    me.config.data.title = me.title;
	}
	me.callParent();
    },
});
Ext.define('pve-fw-ipsets', {
    extend: 'Ext.data.Model',
    fields: ['name', 'comment', 'digest'],
    idProperty: 'name',
});
Ext.define('PVE.IPSetList', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.pveIPSetList',
    stateful: true,
    stateId: 'grid-firewall-ipsetlist',
    ipset_panel: undefined,
    base_url: undefined,
    addBtn: undefined,
    removeBtn: undefined,
    editBtn: undefined,
    initComponent: function() {
        var me = this;
	if (typeof me.ipset_panel === 'undefined') {
	    throw "no rule panel specified";
	}
	if (typeof me.ipset_panel === 'undefined') {
	    throw "no base_url specified";
	}
	var store = new Ext.data.Store({
	    model: 'pve-fw-ipsets',
	    proxy: {
		type: 'proxmox',
		url: "/api2/json" + me.base_url,
	    },
	    sorters: {
		property: 'name',
		direction: 'ASC',
	    },
	});
	var sm = Ext.create('Ext.selection.RowModel', {});
	var reload = function() {
	    var oldrec = sm.getSelection()[0];
	    store.load(function(records, operation, success) {
		if (oldrec) {
		    var rec = store.findRecord('name', oldrec.data.name, 0, false, true, true);
		    if (rec) {
			sm.select(rec);
		    }
		}
	    });
	};
	var run_editor = function() {
	    var rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    var win = Ext.create('Proxmox.window.Edit', {
		subject: "IPSet '" + rec.data.name + "'",
		url: me.base_url,
		method: 'POST',
		digest: rec.data.digest,
		items: [
		    {
			xtype: 'hiddenfield',
			name: 'rename',
			value: rec.data.name,
		    },
		    {
			xtype: 'textfield',
			name: 'name',
			value: rec.data.name,
			fieldLabel: gettext('Name'),
			allowBlank: false,
		    },
		    {
			xtype: 'textfield',
			name: 'comment',
			value: rec.data.comment,
			fieldLabel: gettext('Comment'),
		    },
		],
	    });
	    win.show();
	    win.on('destroy', reload);
	};
	me.editBtn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	me.addBtn = new Proxmox.button.Button({
	    text: gettext('Create'),
	    handler: function() {
		sm.deselectAll();
		var win = Ext.create('Proxmox.window.Edit', {
		    subject: 'IPSet',
		    url: me.base_url,
		    method: 'POST',
		    items: [
			{
			    xtype: 'textfield',
			    name: 'name',
			    value: '',
			    fieldLabel: gettext('Name'),
			    allowBlank: false,
			},
			{
			    xtype: 'textfield',
			    name: 'comment',
			    value: '',
			    fieldLabel: gettext('Comment'),
			},
		    ],
		});
		win.show();
		win.on('destroy', reload);
	    },
	});
	me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: me.base_url + '/',
	    callback: reload,
	});
	Ext.apply(me, {
	    store: store,
	    tbar: ['IPSet:', me.addBtn, me.removeBtn, me.editBtn],
	    selModel: sm,
	    columns: [
		{ header: 'IPSet', dataIndex: 'name', width: '100' },
		{ header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 },
	    ],
	    listeners: {
		itemdblclick: run_editor,
		select: function(_, rec) {
		    var url = me.base_url + '/' + rec.data.name;
		    me.ipset_panel.setBaseUrl(url);
		},
		deselect: function() {
		    me.ipset_panel.setBaseUrl(undefined);
		},
		show: reload,
	    },
	});
	me.callParent();
	store.load();
    },
});
Ext.define('PVE.IPSetCidrEdit', {
    extend: 'Proxmox.window.Edit',
    cidr: undefined,
    initComponent: function() {
	var me = this;
	me.isCreate = me.cidr === undefined;
	if (me.isCreate) {
            me.url = '/api2/extjs' + me.base_url;
            me.method = 'POST';
        } else {
            me.url = '/api2/extjs' + me.base_url + '/' + me.cidr;
            me.method = 'PUT';
        }
	var column1 = [];
	if (me.isCreate) {
	    if (!me.list_refs_url) {
		throw "no alias_base_url specified";
	    }
	    column1.push({
		xtype: 'pveIPRefSelector',
		name: 'cidr',
		ref_type: 'alias',
		autoSelect: false,
		editable: true,
		base_url: me.list_refs_url,
		value: '',
		fieldLabel: gettext('IP/CIDR'),
	    });
	} else {
	    column1.push({
		xtype: 'displayfield',
		name: 'cidr',
		value: '',
		fieldLabel: gettext('IP/CIDR'),
	    });
	}
	var ipanel = Ext.create('Proxmox.panel.InputPanel', {
	    isCreate: me.isCreate,
	    column1: column1,
	    column2: [
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'nomatch',
		    checked: false,
		    uncheckedValue: 0,
		    fieldLabel: 'nomatch',
		},
	    ],
	    columnB: [
		{
		    xtype: 'textfield',
		    name: 'comment',
		    value: '',
		    fieldLabel: gettext('Comment'),
		},
	    ],
	});
	Ext.apply(me, {
	    subject: gettext('IP/CIDR'),
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.IPSetGrid', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.pveIPSetGrid',
    stateful: true,
    stateId: 'grid-firewall-ipsets',
    base_url: undefined,
    list_refs_url: undefined,
    addBtn: undefined,
    removeBtn: undefined,
    editBtn: undefined,
    setBaseUrl: function(url) {
        var me = this;
	me.base_url = url;
	if (url === undefined) {
	    me.addBtn.setDisabled(true);
	    me.store.removeAll();
	} else {
	    me.addBtn.setDisabled(false);
	    me.removeBtn.baseurl = url + '/';
	    me.store.setProxy({
		type: 'proxmox',
		url: '/api2/json' + url,
	    });
	    me.store.load();
	}
    },
    initComponent: function() {
        var me = this;
	if (!me.list_refs_url) {
	    throw "no1 list_refs_url specified";
	}
	var store = new Ext.data.Store({
	    model: 'pve-ipset',
	});
	var reload = function() {
	    store.load();
	};
	var sm = Ext.create('Ext.selection.RowModel', {});
	var run_editor = function() {
	    var rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    var win = Ext.create('PVE.IPSetCidrEdit', {
		base_url: me.base_url,
		cidr: rec.data.cidr,
	    });
	    win.show();
	    win.on('destroy', reload);
	};
	me.editBtn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	me.addBtn = new Proxmox.button.Button({
	    text: gettext('Add'),
	    disabled: true,
	    handler: function() {
		if (!me.base_url) {
		    return;
		}
		var win = Ext.create('PVE.IPSetCidrEdit', {
		    base_url: me.base_url,
		    list_refs_url: me.list_refs_url,
		});
		win.show();
		win.on('destroy', reload);
	    },
	});
	me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: me.base_url + '/',
	    callback: reload,
	});
	var render_errors = function(value, metaData, record) {
	    var errors = record.data.errors;
	    if (errors) {
		var msg = errors.cidr || errors.nomatch;
		if (msg) {
		    metaData.tdCls = 'proxmox-invalid-row';
		    var html = '' + Ext.htmlEncode(msg) + '
';
		    metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' +
			html.replace(/"/g, '"') + '"';
		}
	    }
	    return value;
	};
	Ext.apply(me, {
	    tbar: ['IP/CIDR:', me.addBtn, me.removeBtn, me.editBtn],
	    store: store,
	    selModel: sm,
	    listeners: {
		itemdblclick: run_editor,
	    },
	    columns: [
		{
		    xtype: 'rownumberer',
		},
		{
		    header: gettext('IP/CIDR'),
		    dataIndex: 'cidr',
		    width: 150,
		    renderer: function(value, metaData, record) {
			value = render_errors(value, metaData, record);
			if (record.data.nomatch) {
			    return '! ' + value;
			}
			return value;
		    },
		},
		{
		    header: gettext('Comment'),
		    dataIndex: 'comment',
		    flex: 1,
		    renderer: function(value) {
			return Ext.util.Format.htmlEncode(value);
		    },
		},
	    ],
	});
	me.callParent();
	if (me.base_url) {
	    me.setBaseUrl(me.base_url); // load
	}
    },
}, function() {
    Ext.define('pve-ipset', {
	extend: 'Ext.data.Model',
	fields: [{ name: 'nomatch', type: 'boolean' },
		  'cidr', 'comment', 'errors'],
	idProperty: 'cidr',
    });
});
Ext.define('PVE.IPSet', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveIPSet',
    title: 'IPSet',
    onlineHelp: 'pve_firewall_ip_sets',
    list_refs_url: undefined,
    initComponent: function() {
	var me = this;
	if (!me.list_refs_url) {
	    throw "no list_refs_url specified";
	}
	var ipset_panel = Ext.createWidget('pveIPSetGrid', {
	    region: 'center',
	    list_refs_url: me.list_refs_url,
	    border: false,
	});
	var ipset_list = Ext.createWidget('pveIPSetList', {
	    region: 'west',
	    ipset_panel: ipset_panel,
	    base_url: me.base_url,
	    width: '50%',
	    border: false,
	    split: true,
	});
	Ext.apply(me, {
            layout: 'border',
            items: [ipset_list, ipset_panel],
	    listeners: {
		show: function() {
		    ipset_list.fireEvent('show', ipset_list);
		},
	    },
	});
	me.callParent();
    },
});
/*
 * This is a running chart widget you add time datapoints to it, and we only
 * show the last x of it used for ceph performance charts
 */
Ext.define('PVE.widget.RunningChart', {
    extend: 'Ext.container.Container',
    alias: 'widget.pveRunningChart',
    layout: {
	type: 'hbox',
	align: 'center',
    },
    items: [
	{
	    width: 80,
	    xtype: 'box',
	    itemId: 'title',
	    data: {
		title: '',
	    },
	    tpl: '{title}:
',
	},
	{
	    flex: 1,
	    xtype: 'cartesian',
	    height: '100%',
	    itemId: 'chart',
	    border: false,
	    axes: [
		{
		    type: 'numeric',
		    position: 'left',
		    hidden: true,
		    minimum: 0,
		},
		{
		    type: 'numeric',
		    position: 'bottom',
		    hidden: true,
		},
	    ],
	    store: {
		trackRemoved: false,
		data: {},
	    },
	    sprites: [{
		id: 'valueSprite',
		type: 'text',
		text: '0 B/s',
		textAlign: 'end',
		textBaseline: 'middle',
		fontSize: 14,
	    }],
	    series: [{
		type: 'line',
		xField: 'time',
		yField: 'val',
		fill: 'true',
		colors: ['#cfcfcf'],
		tooltip: {
		    trackMouse: true,
		    renderer: function(tooltip, record, ctx) {
			if (!record || !record.data) return;
			const view = this.getChart();
			const date = new Date(record.data.time);
			const value = view.up().renderer(record.data.val);
			const line1 = `${view.up().title}: ${value}`;
			const line2 = Ext.Date.format(date, 'H:i:s');
			tooltip.setHtml(`${line1}
${line2}`);
		    },
		},
		style: {
		    lineWidth: 1.5,
		    opacity: 0.60,
		},
		marker: {
		    opacity: 0,
		    scaling: 0.01,
		    fx: {
			duration: 200,
			easing: 'easeOut',
		    },
		},
		highlightCfg: {
		    opacity: 1,
		    scaling: 1.5,
		},
	    }],
	},
    ],
    // the renderer for the tooltip and last value, default just the value
    renderer: Ext.identityFn,
    // show the last x seconds default is 5 minutes
    timeFrame: 5*60,
    checkThemeColors: function() {
	let me = this;
	let rootStyle = getComputedStyle(document.documentElement);
	// get color
	let background = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff";
	let text = rootStyle.getPropertyValue("--pwt-text-color").trim() || "#000000";
	// set the colors
	me.chart.setBackground(background);
	me.chart.valuesprite.setAttributes({ fillStyle: text }, true);
	me.chart.redraw();
    },
    addDataPoint: function(value, time) {
	let view = this.chart;
	let panel = view.up();
	let now = new Date().getTime();
	let begin = new Date(now - 1000 * panel.timeFrame).getTime();
	view.store.add({
	    time: time || now,
	    val: value || 0,
	});
	// delete all old records when we have 20 times more datapoints
	// than seconds in our timeframe (so even a subsecond graph does
	// not trigger this often)
	//
	// records in the store do not take much space, but like this,
	// we prevent a memory leak when someone has the site open for a long time
	// with minimal graphical glitches
	if (view.store.count() > panel.timeFrame * 20) {
	    var oldData = view.store.getData().createFiltered(function(item) {
		return item.data.time < begin;
	    });
	    view.store.remove(oldData.getRange());
	}
	view.timeaxis.setMinimum(begin);
	view.timeaxis.setMaximum(now);
	view.valuesprite.setText(panel.renderer(value || 0).toString());
	view.valuesprite.setAttributes({
	    x: view.getWidth() - 15,
	    y: view.getHeight()/2,
	}, true);
	view.redraw();
    },
    setTitle: function(title) {
	this.title = title;
	let titlebox = this.getComponent('title');
	titlebox.update({ title: title });
    },
    initComponent: function() {
	var me = this;
	me.callParent();
	if (me.title) {
	    me.getComponent('title').update({ title: me.title });
	}
	me.chart = me.getComponent('chart');
	me.chart.timeaxis = me.chart.getAxes()[1];
	me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite');
	if (me.color) {
	    me.chart.series[0].setStyle({
		fill: me.color,
		stroke: me.color,
	    });
	}
	me.checkThemeColors();
	// switch colors on media query changes
	me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
	me.themeListener = (e) => { me.checkThemeColors(); };
	me.mediaQueryList.addEventListener("change", me.themeListener);
    },
    doDestroy: function() {
	let me = this;
	me.mediaQueryList.removeEventListener("change", me.themeListener);
	me.callParent();
    },
});
/*
 * This class describes the bottom panel
 */
Ext.define('PVE.panel.StatusPanel', {
    extend: 'Ext.tab.Panel',
    alias: 'widget.pveStatusPanel',
    //title: "Logs",
    //tabPosition: 'bottom',
    initComponent: function() {
        var me = this;
	var stateid = 'ltab';
	var sp = Ext.state.Manager.getProvider();
	var state = sp.get(stateid);
	if (state && state.value) {
	    me.activeTab = state.value;
	}
	Ext.apply(me, {
	    listeners: {
		tabchange: function() {
		    var atab = me.getActiveTab().itemId;
		    let tabstate = { value: atab };
		    sp.set(stateid, tabstate);
		},
	    },
	    items: [
		{
		    itemId: 'tasks',
		    title: gettext('Tasks'),
		    xtype: 'pveClusterTasks',
		},
		{
		    itemId: 'clog',
		    title: gettext('Cluster log'),
		    xtype: 'pveClusterLog',
		},
	    ],
	});
	me.callParent();
	me.items.get(0).fireEvent('show', me.items.get(0));
	var statechange = function(_, key, newstate) {
	    if (key === stateid) {
		var atab = me.getActiveTab().itemId;
		let ntab = newstate.value;
		if (newstate && ntab && atab !== ntab) {
		    me.setActiveTab(ntab);
		}
	    }
	};
	sp.on('statechange', statechange);
	me.on('destroy', function() {
	    sp.un('statechange', statechange);
	});
    },
});
Ext.define('PVE.panel.GuestStatusView', {
    extend: 'Proxmox.panel.StatusView',
    alias: 'widget.pveGuestStatusView',
    mixins: ['Proxmox.Mixin.CBind'],
    cbindData: function(initialConfig) {
	var me = this;
	return {
	    isQemu: me.pveSelNode.data.type === 'qemu',
	    isLxc: me.pveSelNode.data.type === 'lxc',
	};
    },
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    defaults: {
	xtype: 'pmxInfoWidget',
	padding: '2 25',
    },
    items: [
	{
	    xtype: 'box',
	    height: 20,
	},
	{
	    itemId: 'status',
	    title: gettext('Status'),
	    iconCls: 'fa fa-info fa-fw',
	    printBar: false,
	    multiField: true,
	    renderer: function(record) {
		var me = this;
		var text = record.data.status;
		var qmpstatus = record.data.qmpstatus;
		if (qmpstatus && qmpstatus !== record.data.status) {
		    text += ' (' + qmpstatus + ')';
		}
		return text;
	    },
	},
	{
	    itemId: 'hamanaged',
	    iconCls: 'fa fa-heartbeat fa-fw',
	    title: gettext('HA State'),
	    printBar: false,
	    textField: 'ha',
	    renderer: PVE.Utils.format_ha,
	},
	{
	    itemId: 'node',
	    iconCls: 'fa fa-building fa-fw',
	    title: gettext('Node'),
	    cbind: {
		text: '{pveSelNode.data.node}',
	    },
	    printBar: false,
	},
	{
	    xtype: 'box',
	    height: 15,
	},
	{
	    itemId: 'cpu',
	    iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
	    title: gettext('CPU usage'),
	    valueField: 'cpu',
	    maxField: 'cpus',
	    renderer: Proxmox.Utils.render_cpu_usage,
	    // in this specific api call
	    // we already have the correct value for the usage
	    calculate: Ext.identityFn,
	},
	{
	    itemId: 'memory',
	    iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
	    title: gettext('Memory usage'),
	    valueField: 'mem',
	    maxField: 'maxmem',
	},
	{
	    itemId: 'swap',
	    iconCls: 'fa fa-refresh fa-fw',
	    title: gettext('SWAP usage'),
	    valueField: 'swap',
	    maxField: 'maxswap',
	    cbind: {
		hidden: '{isQemu}',
		disabled: '{isQemu}',
	    },
	},
	{
	    itemId: 'rootfs',
	    iconCls: 'fa fa-hdd-o fa-fw',
	    title: gettext('Bootdisk size'),
	    valueField: 'disk',
	    maxField: 'maxdisk',
	    printBar: false,
	    renderer: function(used, max) {
		var me = this;
		me.setPrintBar(used > 0);
		if (used === 0) {
		    return Proxmox.Utils.render_size(max);
		} else {
		    return Proxmox.Utils.render_size_usage(used, max);
		}
	    },
	},
	{
	    xtype: 'box',
	    height: 15,
	},
	{
	    itemId: 'ips',
	    xtype: 'pveAgentIPView',
	    cbind: {
		rstore: '{rstore}',
		pveSelNode: '{pveSelNode}',
		hidden: '{isLxc}',
		disabled: '{isLxc}',
	    },
	},
    ],
    updateTitle: function() {
	var me = this;
	var uptime = me.getRecordValue('uptime');
	var text = "";
	if (Number(uptime) > 0) {
	    text = " (" + gettext('Uptime') + ': ' + Proxmox.Utils.format_duration_long(uptime)
		+ ')';
	}
	me.setTitle(me.getRecordValue('name') + text);
    },
});
Ext.define('PVE.guest.Summary', {
    extend: 'Ext.panel.Panel',
    xtype: 'pveGuestSummary',
    scrollable: true,
    bodyPadding: 5,
    initComponent: function() {
        var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = me.pveSelNode.data.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	if (!me.workspace) {
	    throw "no workspace specified";
	}
	if (!me.statusStore) {
	    throw "no status storage specified";
	}
	var type = me.pveSelNode.data.type;
	var template = !!me.pveSelNode.data.template;
	var rstore = me.statusStore;
	var items = [
	    {
		xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
		flex: 1,
		padding: template ? '5' : '0 5 0 0',
		itemId: 'gueststatus',
		pveSelNode: me.pveSelNode,
		rstore: rstore,
	    },
	    {
		xtype: 'pmxNotesView',
		flex: 1,
		padding: template ? '5' : '0 0 0 5',
		itemId: 'notesview',
		pveSelNode: me.pveSelNode,
	    },
	];
	var rrdstore;
	if (!template) {
	    // in non-template mode put the two panels always together
	    items = [
		{
		    xtype: 'container',
		    height: 300,
		    layout: {
			type: 'hbox',
			align: 'stretch',
		    },
		    items: items,
		},
	    ];
	    rrdstore = Ext.create('Proxmox.data.RRDStore', {
		rrdurl: `/api2/json/nodes/${nodename}/${type}/${vmid}/rrddata`,
		model: 'pve-rrd-guest',
	    });
	    items.push(
		{
		    xtype: 'proxmoxRRDChart',
		    title: gettext('CPU usage'),
		    pveSelNode: me.pveSelNode,
		    fields: ['cpu'],
		    fieldTitles: [gettext('CPU usage')],
		    unit: 'percent',
		    store: rrdstore,
		},
		{
		    xtype: 'proxmoxRRDChart',
		    title: gettext('Memory usage'),
		    pveSelNode: me.pveSelNode,
		    fields: ['maxmem', 'mem'],
		    fieldTitles: [gettext('Total'), gettext('RAM usage')],
		    unit: 'bytes',
		    powerOfTwo: true,
		    store: rrdstore,
		},
		{
		    xtype: 'proxmoxRRDChart',
		    title: gettext('Network traffic'),
		    pveSelNode: me.pveSelNode,
		    fields: ['netin', 'netout'],
		    store: rrdstore,
		},
		{
		    xtype: 'proxmoxRRDChart',
		    title: gettext('Disk IO'),
		    pveSelNode: me.pveSelNode,
		    fields: ['diskread', 'diskwrite'],
		    store: rrdstore,
		},
	    );
	}
	Ext.apply(me, {
	    tbar: ['->', { xtype: 'proxmoxRRDTypeSelector' }],
	    items: [
		{
		    xtype: 'container',
		    itemId: 'itemcontainer',
		    layout: {
			type: 'column',
		    },
		    minWidth: 700,
		    defaults: {
			minHeight: 330,
			padding: 5,
		    },
		    items: items,
		    listeners: {
			resize: function(container) {
			    Proxmox.Utils.updateColumns(container);
			},
		    },
		},
	    ],
	});
	me.callParent();
	if (!template) {
	    rrdstore.startUpdate();
	    me.on('destroy', rrdstore.stopUpdate);
	}
	let sp = Ext.state.Manager.getProvider();
	me.mon(sp, 'statechange', function(provider, key, value) {
	    if (key !== 'summarycolumns') {
		return;
	    }
	    Proxmox.Utils.updateColumns(me.getComponent('itemcontainer'));
	});
    },
});
Ext.define('PVE.panel.TemplateStatusView', {
    extend: 'Proxmox.panel.StatusView',
    alias: 'widget.pveTemplateStatusView',
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    defaults: {
	xtype: 'pmxInfoWidget',
	printBar: false,
	padding: '2 25',
    },
    items: [
	{
	    xtype: 'box',
	    height: 20,
	},
	{
	    itemId: 'hamanaged',
	    iconCls: 'fa fa-heartbeat fa-fw',
	    title: gettext('HA State'),
	    printBar: false,
	    textField: 'ha',
	    renderer: PVE.Utils.format_ha,
	},
	{
	    itemId: 'node',
	    iconCls: 'fa fa-fw fa-building',
	    title: gettext('Node'),
	},
	{
	    xtype: 'box',
	    height: 20,
	},
	{
	    itemId: 'cpus',
	    iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
	    title: gettext('Processors'),
	    textField: 'cpus',
	},
	{
	    itemId: 'memory',
	    iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
	    title: gettext('Memory'),
	    textField: 'maxmem',
	    renderer: Proxmox.Utils.render_size,
	},
	{
	    itemId: 'swap',
	    iconCls: 'fa fa-refresh fa-fw',
	    title: gettext('Swap'),
	    textField: 'maxswap',
	    renderer: Proxmox.Utils.render_size,
	},
	{
	    itemId: 'disk',
	    iconCls: 'fa fa-hdd-o fa-fw',
	    title: gettext('Bootdisk size'),
	    textField: 'maxdisk',
	    renderer: Proxmox.Utils.render_size,
	},
	{
	    xtype: 'box',
	    height: 20,
	},
    ],
    initComponent: function() {
	var me = this;
	var name = me.pveSelNode.data.name;
	if (!name) {
	    throw "no name specified";
	}
	me.title = name;
	me.callParent();
	if (me.pveSelNode.data.type !== 'lxc') {
	    me.remove(me.getComponent('swap'));
	}
	me.getComponent('node').updateValue(me.pveSelNode.data.node);
    },
});
Ext.define('PVE.panel.MultiDiskPanel', {
    extend: 'Ext.panel.Panel',
    setNodename: function(nodename) {
	this.items.each((panel) => panel.setNodename(nodename));
    },
    border: false,
    bodyBorder: false,
    layout: 'card',
    controller: {
	xclass: 'Ext.app.ViewController',
	vmconfig: {},
	onAdd: function() {
	    let me = this;
	    me.lookup('addButton').setDisabled(true);
	    me.addDisk();
	    let count = me.lookup('grid').getStore().getCount() + 1; // +1 is from ide2
	    me.lookup('addButton').setDisabled(count >= me.maxCount);
	},
	getNextFreeDisk: function(vmconfig) {
	    throw "implement in subclass";
	},
	addPanel: function(itemId, vmconfig, nextFreeDisk) {
	    throw "implement in subclass";
	},
	// define in subclass
	diskSorter: undefined,
	addDisk: function() {
	    let me = this;
	    let grid = me.lookup('grid');
	    let store = grid.getStore();
	    // get free disk id
	    let vmconfig = me.getVMConfig(true);
	    let nextFreeDisk = me.getNextFreeDisk(vmconfig);
	    if (!nextFreeDisk) {
		return;
	    }
	    // add store entry + panel
	    let itemId = 'disk-card-' + ++Ext.idSeed;
	    let rec = store.add({
		name: nextFreeDisk.confid,
		itemId,
	    })[0];
	    let panel = me.addPanel(itemId, vmconfig, nextFreeDisk);
	    panel.updateVMConfig(vmconfig);
	    // we need to setup a validitychange handler, so that we can show
	    // that a disk has invalid fields
	    let fields = panel.query('field');
	    fields.forEach((el) => el.on('validitychange', () => {
		let valid = fields.every((field) => field.isValid());
		rec.set('valid', valid);
		me.checkValidity();
	    }));
	    store.sort(me.diskSorter);
	    // select if the panel added is the only one
	    if (store.getCount() === 1) {
		grid.getSelectionModel().select(0, false);
	    }
	},
	getBaseVMConfig: function() {
	    throw "implement in subclass";
	},
	getVMConfig: function(all) {
	    let me = this;
	    let vmconfig = me.getBaseVMConfig();
	    me.lookup('grid').getStore().each((rec) => {
		if (all || rec.get('valid')) {
		    vmconfig[rec.get('name')] = rec.get('itemId');
		}
	    });
	    return vmconfig;
	},
	checkValidity: function() {
	    let me = this;
	    let valid = me.lookup('grid').getStore().findExact('valid', false) === -1;
	    me.lookup('validationfield').setValue(valid);
	},
	updateVMConfig: function() {
	    let me = this;
	    let view = me.getView();
	    let grid = me.lookup('grid');
	    let store = grid.getStore();
	    let vmconfig = me.getVMConfig();
	    let valid = true;
	    store.each((rec) => {
		let itemId = rec.get('itemId');
		let name = rec.get('name');
		let panel = view.getComponent(itemId);
		if (!panel) {
		    throw "unexpected missing panel";
		}
		// copy config for each panel and remote its own id
		let panel_vmconfig = Ext.apply({}, vmconfig);
		if (panel_vmconfig[name] === itemId) {
		    delete panel_vmconfig[name];
		}
		if (!rec.get('valid')) {
		    valid = false;
		}
		panel.updateVMConfig(panel_vmconfig);
	    });
	    me.lookup('validationfield').setValue(valid);
	    return vmconfig;
	},
	onChange: function(panel, newVal) {
	    let me = this;
	    let store = me.lookup('grid').getStore();
	    let el = store.findRecord('itemId', panel.itemId, 0, false, true, true);
	    if (el.get('name') === newVal) {
		// do not update if there was no change
		return;
	    }
	    el.set('name', newVal);
	    el.commit();
	    store.sort(me.diskSorter);
	    // so that it happens after the layouting
	    setTimeout(function() {
		me.updateVMConfig();
	    }, 10);
	},
	onRemove: function(tableview, rowIndex, colIndex, item, event, record) {
	    let me = this;
	    let grid = me.lookup('grid');
	    let store = grid.getStore();
	    let removed_idx = store.indexOf(record);
	    let selection = grid.getSelection()[0];
	    let selected_idx = store.indexOf(selection);
	    if (selected_idx === removed_idx) {
		let newidx = store.getCount() > removed_idx + 1 ? removed_idx + 1: removed_idx - 1;
		grid.getSelectionModel().select(newidx, false);
	    }
	    store.remove(record);
	    me.getView().remove(record.get('itemId'));
	    me.lookup('addButton').setDisabled(false);
	    me.updateVMConfig();
	    me.checkValidity();
	},
	onSelectionChange: function(grid, selection) {
	    let me = this;
	    if (!selection || selection.length < 1) {
		return;
	    }
	    me.getView().setActiveItem(selection[0].data.itemId);
	},
	control: {
	    'inputpanel': {
		diskidchange: 'onChange',
	    },
	    'grid[reference=grid]': {
		selectionchange: 'onSelectionChange',
	    },
	},
	init: function(view) {
	    let me = this;
	    me.onAdd();
	    me.lookup('grid').getSelectionModel().select(0, false);
	},
    },
    dockedItems: [
	{
	    xtype: 'container',
	    layout: {
		type: 'vbox',
		align: 'stretch',
	    },
	    dock: 'left',
	    border: false,
	    width: 130,
	    items: [
		{
		    xtype: 'grid',
		    hideHeaders: true,
		    reference: 'grid',
		    flex: 1,
		    emptyText: gettext('No Disks'),
		    margin: '0 0 5 0',
		    store: {
			fields: ['name', 'itemId', 'valid'],
			data: [],
		    },
		    columns: [
			{
			    dataIndex: 'name',
			    renderer: function(val, md, rec) {
				let warn = '';
				if (!rec.get('valid')) {
				    warn = ' ';
				}
				return val + warn;
			    },
			    flex: 1,
			},
			{
			    xtype: 'actioncolumn',
			    width: 30,
			    align: 'center',
			    menuDisabled: true,
			    items: [
				{
				    iconCls: 'x-fa fa-trash critical',
				    tooltip: 'Delete',
				    handler: 'onRemove',
				    isActionDisabled: 'deleteDisabled',
				},
			    ],
			},
		    ],
		},
		{
		    xtype: 'button',
		    reference: 'addButton',
		    text: gettext('Add'),
		    iconCls: 'fa fa-plus-circle',
		    handler: 'onAdd',
		},
		{
		    // dummy field to control wizard validation
		    xtype: 'textfield',
		    hidden: true,
		    reference: 'validationfield',
		    submitValue: false,
		    value: true,
		    validator: (val) => !!val,
		},
	    ],
	},
    ],
});
/*
 * Left Treepanel, containing all the resources we manage in this datacenter: server nodes, server storages, VMs and Containers
 */
Ext.define('PVE.tree.ResourceTree', {
    extend: 'Ext.tree.TreePanel',
    alias: ['widget.pveResourceTree'],
    userCls: 'proxmox-tags-circle',
    statics: {
	typeDefaults: {
	    node: {
		iconCls: 'fa fa-building',
		text: gettext('Nodes'),
	    },
	    pool: {
		iconCls: 'fa fa-tags',
		text: gettext('Resource Pool'),
	    },
	    storage: {
		iconCls: 'fa fa-database',
		text: gettext('Storage'),
	    },
	    sdn: {
		iconCls: 'fa fa-th',
		text: gettext('SDN'),
	    },
	    qemu: {
		iconCls: 'fa fa-desktop',
		text: gettext('Virtual Machine'),
	    },
	    lxc: {
		//iconCls: 'x-tree-node-lxc',
		iconCls: 'fa fa-cube',
		text: gettext('LXC Container'),
	    },
	    template: {
		iconCls: 'fa fa-file-o',
	    },
	},
    },
    useArrows: true,
    // private
    nodeSortFn: function(node1, node2) {
	let me = this;
	let n1 = node1.data, n2 = node2.data;
	if (!n1.groupbyid === !n2.groupbyid) {
	    let n1IsGuest = n1.type === 'qemu' || n1.type === 'lxc';
	    let n2IsGuest = n2.type === 'qemu' || n2.type === 'lxc';
	    if (me['group-guest-types'] || !n1IsGuest || !n2IsGuest) {
		// first sort (group) by type
		if (n1.type > n2.type) {
		    return 1;
		} else if (n1.type < n2.type) {
		    return -1;
		}
	    }
	    // then sort (group) by ID
	    if (n1IsGuest) {
		if (me['group-templates'] && (!n1.template !== !n2.template)) {
		    return n1.template ? 1 : -1; // sort templates after regular VMs
		}
		if (me['sort-field'] === 'vmid') {
		    if (n1.vmid > n2.vmid) { // prefer VMID as metric for guests
			return 1;
		    } else if (n1.vmid < n2.vmid) {
			return -1;
		    }
		} else {
		    return n1.name.localeCompare(n2.name);
		}
	    }
	    // same types but not a guest
	    return n1.id > n2.id ? 1 : n1.id < n2.id ? -1 : 0;
	} else if (n1.groupbyid) {
	    return -1;
	} else if (n2.groupbyid) {
	    return 1;
	}
	return 0; // should not happen
    },
    // private: fast binary search
    findInsertIndex: function(node, child, start, end) {
	let me = this;
	let diff = end - start;
	if (diff <= 0) {
	    return start;
	}
	let mid = start + (diff >> 1);
	let res = me.nodeSortFn(child, node.childNodes[mid]);
	if (res <= 0) {
	    return me.findInsertIndex(node, child, start, mid);
	} else {
	    return me.findInsertIndex(node, child, mid + 1, end);
	}
    },
    setIconCls: function(info) {
	let cls = PVE.Utils.get_object_icon_class(info.type, info);
	if (cls !== '') {
	    info.iconCls = cls;
	}
    },
    // add additional elements to text. Currently only the usage indicator for storages
    setText: function(info) {
	let me = this;
	let status = '';
	if (info.type === 'storage') {
	    let usage = info.disk / info.maxdisk;
	    if (usage >= 0.0 && usage <= 1.0) {
		let barHeight = (usage * 100).toFixed(0);
		let remainingHeight = (100 - barHeight).toFixed(0);
		status = '';
		status += `
`;
		status += `
`;
		status += '
{{${v}}}`).join(', '),
		    ),
		},
		{
		    xtype: 'label',
		    name: 'pruneLabel',
		    text: gettext('Storage Retention Configuration') + ':',
		    hidden: true,
		},
		{
		    layout: 'hbox',
		    border: false,
		    defaults: {
			border: false,
			layout: 'anchor',
			flex: 1,
		    },
		    items: [
			{
			    padding: '0 10 0 0',
			    defaults: {
				labelWidth: 110,
			    },
			    items: [
				pruneSettings[0],
				pruneSettings[2],
				pruneSettings[4],
			    ],
			},
			{
			    padding: '0 0 0 10',
			    defaults: {
				labelWidth: 110,
			    },
			    items: [
				pruneSettings[1],
				pruneSettings[3],
				pruneSettings[5],
			    ],
			},
		    ],
		},
	    ],
	});
	var submitBtn = Ext.create('Ext.Button', {
	    text: gettext('Backup'),
	    handler: function() {
		var storage = storagesel.getValue();
		let values = me.formPanel.getValues();
		var params = {
		    storage: storage,
		    vmid: me.vmid,
		    mode: values.mode,
		    remove: values.remove,
		};
		if (values.mailto) {
		    params.mailto = values.mailto;
		}
		if (values.compress) {
		    params.compress = values.compress;
		}
		if (values.protected) {
		    params.protected = values.protected;
		}
		if (values['notes-template']) {
		    params['notes-template'] = PVE.Utils.escapeNotesTemplate(
			values['notes-template']);
		}
		Proxmox.Utils.API2Request({
		    url: '/nodes/' + me.nodename + '/vzdump',
		    params: params,
		    method: 'POST',
		    failure: function(response, opts) {
			Ext.Msg.alert('Error', response.htmlStatus);
		    },
		    success: function(response, options) {
			// close later so we reload the grid
			// after the task has completed
			me.hide();
			var upid = response.result.data;
			var win = Ext.create('Proxmox.window.TaskViewer', {
			    upid: upid,
			    listeners: {
				close: function() {
				    me.close();
				},
			    },
			});
			win.show();
		    },
		});
	    },
	});
	var helpBtn = Ext.create('Proxmox.button.Help', {
	    onlineHelp: 'chapter_vzdump',
	    listenToGlobalEvent: false,
	    hidden: false,
	});
	var title = gettext('Backup') + " " +
	    (me.vmtype === 'lxc' ? "CT" : "VM") +
	    " " + me.vmid;
	Ext.apply(me, {
	    title: title,
	    modal: true,
	    layout: 'auto',
	    border: false,
	    width: 600,
	    items: [me.formPanel],
	    buttons: [helpBtn, '->', submitBtn],
	    listeners: {
		afterrender: function() {
		    /// cleared within the storage selector's change listener
		    me.setLoading(gettext('Please wait...'));
		    storagesel.setValue(me.storage);
		},
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.window.BackupConfig', {
    extend: 'Ext.window.Window',
    title: gettext('Configuration'),
    width: 600,
    height: 400,
    layout: 'fit',
    modal: true,
    items: {
	xtype: 'component',
	itemId: 'configtext',
	autoScroll: true,
	style: {
	    'white-space': 'pre',
	    'font-family': 'monospace',
	    padding: '5px',
	},
    },
    initComponent: function() {
	var me = this;
	if (!me.volume) {
	    throw "no volume specified";
	}
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	me.callParent();
	Proxmox.Utils.API2Request({
	    url: "/nodes/" + nodename + "/vzdump/extractconfig",
	    method: 'GET',
	    params: {
		volume: me.volume,
	    },
	    failure: function(response, opts) {
		me.close();
		Ext.Msg.alert('Error', response.htmlStatus);
	    },
	    success: function(response, options) {
		me.show();
		me.down('#configtext').update(Ext.htmlEncode(response.result.data));
	    },
	});
    },
});
Ext.define('PVE.window.BulkAction', {
    extend: 'Ext.window.Window',
    resizable: true,
    width: 800,
    height: 600,
    modal: true,
    layout: {
	type: 'fit',
    },
    border: false,
    // the action to set, currently there are: `startall`, `migrateall`, `stopall`
    action: undefined,
    submit: function(params) {
	let me = this;
	Proxmox.Utils.API2Request({
	    params: params,
	    url: `/nodes/${me.nodename}/${me.action}`,
	    waitMsgTarget: me,
	    method: 'POST',
	    failure: response => Ext.Msg.alert('Error', response.htmlStatus),
	    success: function({ result }, options) {
		Ext.create('Proxmox.window.TaskViewer', {
		    autoShow: true,
		    upid: result.data,
		    listeners: {
			destroy: () => me.close(),
		    },
		});
		me.hide();
	    },
	});
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.action) {
	    throw "no action specified";
	}
	if (!me.btnText) {
	    throw "no button text specified";
	}
	if (!me.title) {
	    throw "no title specified";
	}
	let items = [];
	if (me.action === 'migrateall') {
	    items.push(
		{
		    xtype: 'pveNodeSelector',
		    name: 'target',
		    disallowedNodes: [me.nodename],
		    fieldLabel: gettext('Target node'),
		    allowBlank: false,
		    onlineValidator: true,
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'maxworkers',
		    minValue: 1,
		    maxValue: 100,
		    value: 1,
		    fieldLabel: gettext('Parallel jobs'),
		    allowBlank: false,
		},
		{
		    xtype: 'fieldcontainer',
		    fieldLabel: gettext('Allow local disk migration'),
		    layout: 'hbox',
		    items: [{
			xtype: 'proxmoxcheckbox',
			name: 'with-local-disks',
			checked: true,
			uncheckedValue: 0,
			listeners: {
			    change: (cb, val) => me.down('#localdiskwarning').setVisible(val),
			},
		    },
		    {
			itemId: 'localdiskwarning',
			xtype: 'displayfield',
			flex: 1,
			padding: '0 0 0 10',
			userCls: 'pmx-hint',
			value: 'Note: Migration with local disks might take long.',
		    }],
		},
		{
		    itemId: 'lxcwarning',
		    xtype: 'displayfield',
		    userCls: 'pmx-hint',
		    value: 'Warning: Running CTs will be migrated in Restart Mode.',
		    hidden: true, // only visible if running container chosen
		},
	    );
	} else if (me.action === 'startall') {
	    items.push({
		xtype: 'hiddenfield',
		name: 'force',
		value: 1,
	    });
	} else if (me.action === 'stopall') {
	    items.push(
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'force-stop',
		    fieldLabel: gettext('Force Stop'),
		    boxLabel: gettext('Force stop guest if shutdown times out.'),
		    checked: true,
		    uncheckedValue: 0,
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'timeout',
		    fieldLabel: gettext('Timeout (s)'),
		    emptyText: '180',
		    minValue: 0,
		    maxValue: 7200,
		    allowBlank: true,
		},
	    );
	}
	items.push({
	    xtype: 'vmselector',
	    itemId: 'vms',
	    name: 'vms',
	    flex: 1,
	    height: 300,
	    selectAll: true,
	    allowBlank: false,
	    nodename: me.nodename,
	    action: me.action,
	    listeners: {
		selectionchange: function(vmselector, records) {
		    if (me.action === 'migrateall') {
			let showWarning = records.some(
			    item => item.data.type === 'lxc' && item.data.status === 'running',
			);
			me.down('#lxcwarning').setVisible(showWarning);
		    }
		},
	    },
	});
	me.formPanel = Ext.create('Ext.form.Panel', {
	    bodyPadding: 10,
	    border: false,
	    layout: {
		type: 'vbox',
		align: 'stretch',
	    },
	    fieldDefaults: {
		labelWidth: me.action === 'migrateall' ? 300 : 120,
		anchor: '100%',
	    },
	    items: items,
	});
	let form = me.formPanel.getForm();
	let submitBtn = Ext.create('Ext.Button', {
	    text: me.btnText,
	    handler: function() {
		form.isValid();
		me.submit(form.getValues());
	    },
	});
	Ext.apply(me, {
	    items: [me.formPanel],
	    buttons: [submitBtn],
	});
	me.callParent();
	form.on('validitychange', function() {
	    let valid = form.isValid();
	    submitBtn.setDisabled(!valid);
	});
	form.isValid();
    },
});
Ext.define('PVE.ceph.Install', {
    extend: 'Ext.window.Window',
    xtype: 'pveCephInstallWindow',
    mixins: ['Proxmox.Mixin.CBind'],
    width: 220,
    header: false,
    resizable: false,
    draggable: false,
    modal: true,
    nodename: undefined,
    shadow: false,
    border: false,
    bodyBorder: false,
    closable: false,
    cls: 'install-mask',
    bodyCls: 'install-mask',
    layout: {
        align: 'stretch',
        pack: 'center',
	type: 'vbox',
    },
    viewModel: {
	data: {
	      isInstalled: false,
	},
	formulas: {
	    buttonText: function(get) {
		if (get('isInstalled')) {
		    return gettext('Configure Ceph');
		} else {
		    return gettext('Install Ceph');
		}
	    },
	    windowText: function(get) {
		if (get('isInstalled')) {
		    return `
		    ${Ext.String.format(gettext('{0} is not initialized.'), 'Ceph')}
		    ${gettext('You need to create an initial config once.')}
`;
		} else {
		    return '' +
		    Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + '
' +
		    gettext('Would you like to install it now?') + '
';
		}
	    },
	},
    },
    items: [
	{
	    bind: {
		html: '{windowText}',
	    },
	    border: false,
	    padding: 5,
	    bodyCls: 'install-mask',
	},
	{
	    xtype: 'button',
	    bind: {
		text: '{buttonText}',
	    },
	    viewModel: {},
	    cbind: {
		nodename: '{nodename}',
	    },
	    handler: function() {
		let view = this.up('pveCephInstallWindow');
		let wizzard = Ext.create('PVE.ceph.CephInstallWizard', {
		    nodename: view.nodename,
		});
		wizzard.getViewModel().set('isInstalled', this.getViewModel().get('isInstalled'));
		wizzard.show();
		view.mon(wizzard, 'beforeClose', function() {
		    view.fireEvent("cephInstallWindowClosed");
		    view.close();
		});
	    },
	},
    ],
});
Ext.define('PVE.window.Clone', {
    extend: 'Ext.window.Window',
    resizable: false,
    isTemplate: false,
    onlineHelp: 'qm_copy_and_clone',
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    'panel[reference=cloneform]': {
		validitychange: 'disableSubmit',
	    },
	},
	disableSubmit: function(form) {
	    this.lookupReference('submitBtn').setDisabled(!form.isValid());
	},
    },
    statics: {
	// display a snapshot selector only if needed
	wrap: function(nodename, vmid, isTemplate, guestType) {
	    Proxmox.Utils.API2Request({
		url: '/nodes/' + nodename + '/' + guestType + '/' + vmid +'/snapshot',
		failure: function(response, opts) {
		    Ext.Msg.alert('Error', response.htmlStatus);
		},
		success: function(response, opts) {
		    var snapshotList = response.result.data;
		    var hasSnapshots = !(snapshotList.length === 1 &&
			snapshotList[0].name === 'current');
		    Ext.create('PVE.window.Clone', {
			nodename: nodename,
			guestType: guestType,
			vmid: vmid,
			isTemplate: isTemplate,
			hasSnapshots: hasSnapshots,
		    }).show();
		},
	    });
	},
    },
    create_clone: function(values) {
	var me = this;
	var params = { newid: values.newvmid };
	if (values.snapname && values.snapname !== 'current') {
	    params.snapname = values.snapname;
	}
	if (values.pool) {
	    params.pool = values.pool;
	}
	if (values.name) {
	    if (me.guestType === 'lxc') {
		params.hostname = values.name;
	    } else {
		params.name = values.name;
	    }
	}
	if (values.target) {
	    params.target = values.target;
	}
	if (values.clonemode === 'copy') {
	    params.full = 1;
	    if (values.hdstorage) {
		params.storage = values.hdstorage;
		if (values.diskformat && me.guestType !== 'lxc') {
		    params.format = values.diskformat;
		}
	    }
	}
	Proxmox.Utils.API2Request({
	    params: params,
	    url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/clone',
	    waitMsgTarget: me,
	    method: 'POST',
	    failure: function(response, opts) {
		Ext.Msg.alert('Error', response.htmlStatus);
	    },
	    success: function(response, options) {
		me.close();
	    },
	});
    },
    // disable the Storage selector when clone mode is linked clone
    updateVisibility: function() {
	var me = this;
	var clonemode = me.lookupReference('clonemodesel').getValue();
	var disksel = me.lookup('diskselector');
	disksel.setDisabled(clonemode === 'clone');
    },
    // add to the list of valid nodes each node where
    // all the VM disks are available
    verifyFeature: function() {
	var me = this;
	var snapname = me.lookupReference('snapshotsel').getValue();
	var clonemode = me.lookupReference('clonemodesel').getValue();
	var params = { feature: clonemode };
	if (snapname !== 'current') {
	    params.snapname = snapname;
	}
	Proxmox.Utils.API2Request({
	    waitMsgTarget: me,
	    url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/feature',
	    params: params,
	    method: 'GET',
	    failure: function(response, opts) {
		me.lookupReference('submitBtn').setDisabled(true);
		Ext.Msg.alert('Error', response.htmlStatus);
	    },
	    success: function(response, options) {
		var res = response.result.data;
		me.lookupReference('targetsel').allowedNodes = res.nodes;
		me.lookupReference('targetsel').validate();
	    },
	});
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.vmid) {
	    throw "no VM ID specified";
	}
	if (!me.snapname) {
	    me.snapname = 'current';
	}
	if (!me.guestType) {
	    throw "no Guest Type specified";
	}
	var titletext = me.guestType === 'lxc' ? 'CT' : 'VM';
	if (me.isTemplate) {
	    titletext += ' Template';
	}
	me.title = "Clone " + titletext + " " + me.vmid;
	var col1 = [];
	var col2 = [];
	col1.push({
	    xtype: 'pveNodeSelector',
	    name: 'target',
	    reference: 'targetsel',
	    fieldLabel: gettext('Target node'),
	    selectCurNode: true,
	    allowBlank: false,
	    onlineValidator: true,
	    listeners: {
		change: function(f, value) {
		    me.lookupReference('hdstorage').setTargetNode(value);
		},
	    },
	});
	var modelist = [['copy', gettext('Full Clone')]];
	if (me.isTemplate) {
	    modelist.push(['clone', gettext('Linked Clone')]);
	}
	col1.push({
	    xtype: 'pveGuestIDSelector',
	    name: 'newvmid',
	    guestType: me.guestType,
	    value: '',
	    loadNextFreeID: true,
	    validateExists: false,
	},
	{
	    xtype: 'textfield',
	    name: 'name',
	    allowBlank: true,
	    fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name'),
	},
	{
	    xtype: 'pvePoolSelector',
	    fieldLabel: gettext('Resource Pool'),
	    name: 'pool',
	    value: '',
	    allowBlank: true,
	},
	);
	col2.push({
	    xtype: 'proxmoxKVComboBox',
	    fieldLabel: gettext('Mode'),
	    name: 'clonemode',
	    reference: 'clonemodesel',
	    allowBlank: false,
	    hidden: !me.isTemplate,
	    value: me.isTemplate ? 'clone' : 'copy',
	    comboItems: modelist,
	    listeners: {
		change: function(t, value) {
		    me.updateVisibility();
		    me.verifyFeature();
		},
	    },
	},
	{
	    xtype: 'PVE.form.SnapshotSelector',
	    name: 'snapname',
	    reference: 'snapshotsel',
	    fieldLabel: gettext('Snapshot'),
	    nodename: me.nodename,
	    guestType: me.guestType,
	    vmid: me.vmid,
	    hidden: !!(me.isTemplate || !me.hasSnapshots),
	    disabled: false,
	    allowBlank: false,
	    value: me.snapname,
	    listeners: {
		change: function(f, value) {
		    me.verifyFeature();
		},
	    },
	},
	{
	    xtype: 'pveDiskStorageSelector',
	    reference: 'diskselector',
	    nodename: me.nodename,
	    autoSelect: false,
	    hideSize: true,
	    hideSelection: true,
	    storageLabel: gettext('Target Storage'),
	    allowBlank: true,
	    storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir',
	    emptyText: gettext('Same as source'),
	    disabled: !!me.isTemplate, // because default mode is clone for templates
	});
	var formPanel = Ext.create('Ext.form.Panel', {
	    bodyPadding: 10,
	    reference: 'cloneform',
	    border: false,
	    layout: 'hbox',
	    defaultType: 'container',
	    fieldDefaults: {
		labelWidth: 100,
		anchor: '100%',
	    },
	    items: [
		{
		    flex: 1,
		    padding: '0 10 0 0',
		    layout: 'anchor',
		    items: col1,
		},
		{
		    flex: 1,
		    padding: '0 0 0 10',
		    layout: 'anchor',
		    items: col2,
		},
	    ],
	});
	Ext.apply(me, {
	    modal: true,
	    width: 600,
	    height: 250,
	    border: false,
	    layout: 'fit',
	    buttons: [{
		xtype: 'proxmoxHelpButton',
		listenToGlobalEvent: false,
		hidden: false,
		onlineHelp: me.onlineHelp,
	    },
	    '->',
	    {
		reference: 'submitBtn',
		text: gettext('Clone'),
		disabled: true,
		handler: function() {
		    var cloneForm = me.lookupReference('cloneform');
		    if (cloneForm.isValid()) {
			me.create_clone(cloneForm.getValues());
		    }
		},
	    }],
	    items: [formPanel],
	});
	me.callParent();
	me.verifyFeature();
    },
});
Ext.define('PVE.FirewallEnableEdit', {
    extend: 'Proxmox.window.Edit',
    alias: ['widget.pveFirewallEnableEdit'],
    mixins: ['Proxmox.Mixin.CBind'],
    subject: gettext('Firewall'),
    cbindData: {
	defaultValue: 0,
    },
    width: 350,
    items: [
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'enable',
	    uncheckedValue: 0,
	    cbind: {
		defaultValue: '{defaultValue}',
		checked: '{defaultValue}',
	    },
	    deleteDefaultValue: false,
	    fieldLabel: gettext('Firewall'),
	},
	{
	    xtype: 'displayfield',
	    name: 'warning',
	    userCls: 'pmx-hint',
	    value: gettext('Warning: Firewall still disabled at datacenter level!'),
	    hidden: true,
	},
    ],
    beforeShow: function() {
	var me = this;
	Proxmox.Utils.API2Request({
	    url: '/api2/extjs/cluster/firewall/options',
	    method: 'GET',
	    failure: function(response, opts) {
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
	    },
	    success: function(response, opts) {
		if (!response.result.data.enable) {
		    me.down('displayfield[name=warning]').setVisible(true);
		}
	    },
	});
    },
});
Ext.define('PVE.FirewallLograteInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveFirewallLograteInputPanel',
    viewModel: {},
    items: [
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'enable',
	    reference: 'enable',
	    fieldLabel: gettext('Enable'),
	    value: true,
	},
	{
	    layout: 'hbox',
	    border: false,
	    items: [
		{
		    xtype: 'numberfield',
		    name: 'rate',
		    fieldLabel: gettext('Log rate limit'),
		    minValue: 1,
		    maxValue: 99,
		    allowBlank: false,
		    flex: 2,
		    value: 1,
		},
		{
		    xtype: 'box',
		    html: '/
',
		},
		{
		    xtype: 'proxmoxKVComboBox',
		    name: 'unit',
		    comboItems: [
			['second', 'second'],
			['minute', 'minute'],
			['hour', 'hour'],
			['day', 'day'],
		    ],
		    allowBlank: false,
		    flex: 1,
		    value: 'second',
		},
	    ],
	},
	{
	    xtype: 'numberfield',
	    name: 'burst',
	    fieldLabel: gettext('Log burst limit'),
	    minValue: 1,
	    maxValue: 99,
	    value: 5,
	},
    ],
    onGetValues: function(values) {
	let me = this;
	let cfg = {
	    enable: values.enable !== undefined ? 1 : 0,
	    rate: values.rate + '/' + values.unit,
	    burst: values.burst,
	};
	let properties = PVE.Parser.printPropertyString(cfg, undefined);
	if (properties === '') {
	    return { 'delete': 'log_ratelimit' };
	}
	return { log_ratelimit: properties };
    },
    setValues: function(values) {
	let me = this;
	let properties = {};
	if (values.log_ratelimit !== undefined) {
	    properties = PVE.Parser.parsePropertyString(values.log_ratelimit, 'enable');
	    if (properties.rate) {
		var matches = properties.rate.match(/^(\d+)\/(second|minute|hour|day)$/);
		if (matches) {
		    properties.rate = matches[1];
		    properties.unit = matches[2];
		}
	    }
	}
	me.callParent([properties]);
    },
});
Ext.define('PVE.FirewallLograteEdit', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveFirewallLograteEdit',
    subject: gettext('Log rate limit'),
    items: [{
	xtype: 'pveFirewallLograteInputPanel',
    }],
    autoLoad: true,
});
/*global u2f*/
Ext.define('PVE.window.LoginWindow', {
    extend: 'Ext.window.Window',
    viewModel: {
	data: {
	    openid: false,
	},
	formulas: {
	    button_text: function(get) {
		if (get("openid") === true) {
		    return gettext("Login (OpenID redirect)");
		} else {
		    return gettext("Login");
		}
	    },
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	onLogon: async function() {
	    var me = this;
	    var form = this.lookupReference('loginForm');
	    var unField = this.lookupReference('usernameField');
	    var saveunField = this.lookupReference('saveunField');
	    var view = this.getView();
	    if (!form.isValid()) {
		return;
	    }
	    let creds = form.getValues();
	    if (this.getViewModel().data.openid === true) {
		const redirectURL = location.origin;
		Proxmox.Utils.API2Request({
		    url: '/api2/extjs/access/openid/auth-url',
		    params: {
			realm: creds.realm,
			"redirect-url": redirectURL,
		    },
		    method: 'POST',
		    success: function(resp, opts) {
			window.location = resp.result.data;
		    },
		    failure: function(resp, opts) {
			Proxmox.Utils.authClear();
			form.unmask();
			Ext.MessageBox.alert(
			    gettext('Error'),
			    gettext('OpenID redirect failed.') + `
${resp.htmlStatus}`,
			);
		    },
		});
		return;
	    }
	    view.el.mask(gettext('Please wait...'), 'x-mask-loading');
	    // set or clear username
	    var sp = Ext.state.Manager.getProvider();
	    if (saveunField.getValue() === true) {
		sp.set(unField.getStateId(), unField.getValue());
	    } else {
		sp.clear(unField.getStateId());
	    }
	    sp.set(saveunField.getStateId(), saveunField.getValue());
	    try {
		// Request updated authentication mechanism:
		creds['new-format'] = 1;
		let resp = await Proxmox.Async.api2({
		    url: '/api2/extjs/access/ticket',
		    params: creds,
		    method: 'POST',
		});
		let data = resp.result.data;
		if (data.ticket.startsWith("PVE:!tfa!")) {
		    // Store first factor login information first:
		    data.LoggedOut = true;
		    Proxmox.Utils.setAuthData(data);
		    data = await me.performTFAChallenge(data);
		    // Fill in what we copy over from the 1st factor:
		    data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
		    data.username = Proxmox.UserName;
		    me.success(data);
		} else if (Ext.isDefined(data.NeedTFA)) {
		    // Store first factor login information first:
		    data.LoggedOut = true;
		    Proxmox.Utils.setAuthData(data);
		    if (Ext.isDefined(data.U2FChallenge)) {
			me.perform_u2f(data);
		    } else {
			me.perform_otp();
		    }
		} else {
		    me.success(data);
		}
	    } catch (error) {
		me.failure(error);
	    }
	},
	/* START NEW TFA CODE (pbs copy) */
	performTFAChallenge: async function(data) {
	    let me = this;
	    let userid = data.username;
	    let ticket = data.ticket;
	    let challenge = JSON.parse(decodeURIComponent(
	        ticket.split(':')[1].slice("!tfa!".length),
	    ));
	    let resp = await new Promise((resolve, reject) => {
		Ext.create('Proxmox.window.TfaLoginWindow', {
		    userid,
		    ticket,
		    challenge,
		    onResolve: value => resolve(value),
		    onReject: reject,
		}).show();
	    });
	    return resp.result.data;
	},
	/* END NEW TFA CODE (pbs copy) */
	failure: function(resp) {
	    var me = this;
	    var view = me.getView();
	    view.el.unmask();
	    var handler = function() {
		var uf = me.lookupReference('usernameField');
		uf.focus(true, true);
	    };
	    let emsg = gettext("Login failed. Please try again");
	    if (resp.failureType === "connect") {
		emsg = gettext("Connection failure. Network error or Proxmox VE services not running?");
	    }
	    Ext.MessageBox.alert(gettext('Error'), emsg, handler);
	},
	success: function(data) {
	    var me = this;
	    var view = me.getView();
	    var handler = view.handler || Ext.emptyFn;
	    handler.call(me, data);
	    view.close();
	},
	perform_otp: function() {
	    var me = this;
	    var win = Ext.create('PVE.window.TFALoginWindow', {
		onLogin: function(value) {
		    me.finish_tfa(value);
		},
		onCancel: function() {
		    Proxmox.LoggedOut = false;
		    Proxmox.Utils.authClear();
		    me.getView().show();
		},
	    });
	    win.show();
	},
	perform_u2f: function(data) {
	    var me = this;
	    // Show the message:
	    var msg = Ext.Msg.show({
		title: 'U2F: '+gettext('Verification'),
		message: gettext('Please press the button on your U2F Device'),
		buttons: [],
	    });
	    var chlg = data.U2FChallenge;
	    var key = {
		version: chlg.version,
		keyHandle: chlg.keyHandle,
	    };
	    u2f.sign(chlg.appId, chlg.challenge, [key], function(res) {
		msg.close();
		if (res.errorCode) {
		    Proxmox.Utils.authClear();
		    Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode));
		    return;
		}
		delete res.errorCode;
		me.finish_tfa(JSON.stringify(res));
	    });
	},
	finish_tfa: function(res) {
	    var me = this;
	    var view = me.getView();
	    view.el.mask(gettext('Please wait...'), 'x-mask-loading');
	    Proxmox.Utils.API2Request({
		url: '/api2/extjs/access/tfa',
		params: {
		    response: res,
		},
		method: 'POST',
		timeout: 5000, // it'll delay both success & failure
		success: function(resp, opts) {
		    view.el.unmask();
		    // Fill in what we copy over from the 1st factor:
		    var data = resp.result.data;
		    data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
		    data.username = Proxmox.UserName;
		    // Finish logging in:
		    me.success(data);
		},
		failure: function(resp, opts) {
		    Proxmox.Utils.authClear();
		    me.failure(resp);
		},
	    });
	},
	control: {
	    'field[name=username]': {
		specialkey: function(f, e) {
		    if (e.getKey() === e.ENTER) {
			var pf = this.lookupReference('passwordField');
			if (!pf.getValue()) {
			    pf.focus(false);
			}
		    }
		},
	    },
	    'field[name=lang]': {
		change: function(f, value) {
		    var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
		    Ext.util.Cookies.set('PVELangCookie', value, dt);
		    this.getView().mask(gettext('Please wait...'), 'x-mask-loading');
		    window.location.reload();
		},
	    },
	    'field[name=realm]': {
		change: function(f, value) {
		    let record = f.store.getById(value);
		    if (record === undefined) return;
		    let data = record.data;
		    this.getViewModel().set("openid", data.type === "openid");
		},
	    },
	   'button[reference=loginButton]': {
		click: 'onLogon',
	    },
	    '#': {
		show: function() {
		    var me = this;
		    var sp = Ext.state.Manager.getProvider();
		    var checkboxField = this.lookupReference('saveunField');
		    var unField = this.lookupReference('usernameField');
		    var checked = sp.get(checkboxField.getStateId());
		    checkboxField.setValue(checked);
		    if (checked === true) {
			var username = sp.get(unField.getStateId());
			unField.setValue(username);
			var pwField = this.lookupReference('passwordField');
			pwField.focus();
		    }
		    let auth = Proxmox.Utils.getOpenIDRedirectionAuthorization();
		    if (auth !== undefined) {
			Proxmox.Utils.authClear();
			let loginForm = this.lookupReference('loginForm');
			loginForm.mask(gettext('OpenID login - please wait...'), 'x-mask-loading');
			const redirectURL = location.origin;
			Proxmox.Utils.API2Request({
			    url: '/api2/extjs/access/openid/login',
			    params: {
				state: auth.state,
				code: auth.code,
				"redirect-url": redirectURL,
			    },
			    method: 'POST',
			    failure: function(response) {
				loginForm.unmask();
				let error = response.htmlStatus;
				Ext.MessageBox.alert(
				    gettext('Error'),
				    gettext('OpenID login failed, please try again') + `
${error}`,
				    () => { window.location = redirectURL; },
				);
			    },
			    success: function(response, options) {
				loginForm.unmask();
				let data = response.result.data;
				history.replaceState(null, '', redirectURL);
				me.success(data);
			    },
			});
		    }
		},
	    },
	},
    },
    width: 400,
    modal: true,
    border: false,
    draggable: true,
    closable: false,
    resizable: false,
    layout: 'auto',
    title: gettext('Proxmox VE Login'),
    defaultFocus: 'usernameField',
    defaultButton: 'loginButton',
    items: [{
	xtype: 'form',
	layout: 'form',
	url: '/api2/extjs/access/ticket',
	reference: 'loginForm',
	fieldDefaults: {
	    labelAlign: 'right',
	    allowBlank: false,
	},
	items: [
	    {
		xtype: 'textfield',
		fieldLabel: gettext('User name'),
		name: 'username',
		itemId: 'usernameField',
		reference: 'usernameField',
		stateId: 'login-username',
		bind: {
		    visible: "{!openid}",
		    disabled: "{openid}",
		},
	    },
	    {
		xtype: 'textfield',
		inputType: 'password',
		fieldLabel: gettext('Password'),
		name: 'password',
		reference: 'passwordField',
		bind: {
		    visible: "{!openid}",
		    disabled: "{openid}",
		},
	    },
	    {
		xtype: 'pmxRealmComboBox',
		name: 'realm',
	    },
	    {
		xtype: 'proxmoxLanguageSelector',
		fieldLabel: gettext('Language'),
		value: Ext.util.Cookies.get('PVELangCookie') || Proxmox.defaultLang || 'en',
		name: 'lang',
		reference: 'langField',
		submitValue: false,
	    },
	],
	buttons: [
	    {
		xtype: 'checkbox',
		fieldLabel: gettext('Save User name'),
		name: 'saveusername',
		reference: 'saveunField',
		stateId: 'login-saveusername',
		labelWidth: 250,
		labelAlign: 'right',
		submitValue: false,
		bind: {
		    visible: "{!openid}",
		},
	    },
	    {
		bind: {
		    text: "{button_text}",
		},
		reference: 'loginButton',
	    },
	],
    }],
 });
Ext.define('PVE.window.Migrate', {
    extend: 'Ext.window.Window',
    vmtype: undefined,
    nodename: undefined,
    vmid: undefined,
    maxHeight: 450,
    viewModel: {
	data: {
	    vmid: undefined,
	    nodename: undefined,
	    vmtype: undefined,
	    running: false,
	    qemu: {
		onlineHelp: 'qm_migration',
		commonName: 'VM',
	    },
	    lxc: {
		onlineHelp: 'pct_migration',
		commonName: 'CT',
	    },
	    migration: {
		possible: true,
		preconditions: [],
		'with-local-disks': 0,
		mode: undefined,
		allowedNodes: undefined,
		overwriteLocalResourceCheck: false,
		hasLocalResources: false,
	    },
	},
	formulas: {
	    setMigrationMode: function(get) {
		if (get('running')) {
		    if (get('vmtype') === 'qemu') {
			return gettext('Online');
		    } else {
			return gettext('Restart Mode');
		    }
		} else {
		    return gettext('Offline');
		}
	    },
	    setStorageselectorHidden: function(get) {
		    if (get('migration.with-local-disks') && get('running')) {
			return false;
		    } else {
			return true;
		    }
	    },
	    setLocalResourceCheckboxHidden: function(get) {
		if (get('running') || !get('migration.hasLocalResources') ||
		    Proxmox.UserName !== 'root@pam') {
		    return true;
		} else {
		    return false;
		}
	    },
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    'panel[reference=formPanel]': {
		validityChange: function(panel, isValid) {
		    this.getViewModel().set('migration.possible', isValid);
		    this.checkMigratePreconditions();
		},
	    },
	},
	init: function(view) {
	    var me = this,
		vm = view.getViewModel();
	    if (!view.nodename) {
		throw "missing custom view config: nodename";
	    }
	    vm.set('nodename', view.nodename);
	    if (!view.vmid) {
		throw "missing custom view config: vmid";
	    }
	    vm.set('vmid', view.vmid);
	    if (!view.vmtype) {
		throw "missing custom view config: vmtype";
	    }
	    vm.set('vmtype', view.vmtype);
	    view.setTitle(
		Ext.String.format('{0} {1} {2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid),
	    );
	    me.lookup('proxmoxHelpButton').setHelpConfig({
		onlineHelp: vm.get(view.vmtype).onlineHelp,
	    });
	    me.lookup('formPanel').isValid();
	},
	onTargetChange: function(nodeSelector) {
	    // Always display the storages of the currently seleceted migration target
	    this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value);
	    this.checkMigratePreconditions();
	},
	startMigration: function() {
	    var me = this,
		view = me.getView(),
		vm = me.getViewModel();
	    var values = me.lookup('formPanel').getValues();
	    var params = {
		target: values.target,
	    };
	    if (vm.get('migration.mode')) {
		params[vm.get('migration.mode')] = 1;
	    }
	    if (vm.get('migration.with-local-disks')) {
		params['with-local-disks'] = 1;
	    }
	    //offline migration to a different storage currently might fail at a late stage
	    //(i.e. after some disks have been moved), so don't expose it yet in the GUI
	    if (vm.get('migration.with-local-disks') && vm.get('running') && values.targetstorage) {
		params.targetstorage = values.targetstorage;
	    }
	    if (vm.get('migration.overwriteLocalResourceCheck')) {
		params.force = 1;
	    }
	    Proxmox.Utils.API2Request({
		params: params,
		url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
		waitMsgTarget: view,
		method: 'POST',
		failure: function(response, opts) {
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		},
		success: function(response, options) {
		    var upid = response.result.data;
		    var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target);
		    Ext.create('Proxmox.window.TaskViewer', {
			upid: upid,
			extraTitle: extraTitle,
		    }).show();
		    view.close();
		},
	    });
	},
	checkMigratePreconditions: function(resetMigrationPossible) {
	    var me = this,
		vm = me.getViewModel();
	    var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'),
			0, false, false, true);
	    if (vmrec && vmrec.data && vmrec.data.running) {
		vm.set('running', true);
	    }
	    if (vm.get('vmtype') === 'qemu') {
		me.checkQemuPreconditions(resetMigrationPossible);
	    } else {
		me.checkLxcPreconditions(resetMigrationPossible);
	    }
	    me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
	    // Only allow nodes where the local storage is available in case of offline migration
	    // where storage migration is not possible
	    me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes');
	    me.lookup('formPanel').isValid();
	},
	checkQemuPreconditions: async function(resetMigrationPossible) {
	    let me = this,
		vm = me.getViewModel(),
		migrateStats;
	    if (vm.get('running')) {
		vm.set('migration.mode', 'online');
	    }
	    try {
		if (me.fetchingNodeMigrateInfo && me.fetchingNodeMigrateInfo === vm.get('nodename')) {
		    return;
		}
		me.fetchingNodeMigrateInfo = vm.get('nodename');
		let { result } = await Proxmox.Async.api2({
		    url: `/nodes/${vm.get('nodename')}/${vm.get('vmtype')}/${vm.get('vmid')}/migrate`,
		    method: 'GET',
		});
		migrateStats = result.data;
		me.fetchingNodeMigrateInfo = false;
	    } catch (error) {
		Ext.Msg.alert(gettext('Error'), error.htmlStatus);
		return;
	    }
	    if (migrateStats.running) {
		vm.set('running', true);
	    }
	    // Get migration object from viewmodel to prevent to many bind callbacks
	    let migration = vm.get('migration');
	    if (resetMigrationPossible) {
		migration.possible = true;
	    }
	    migration.preconditions = [];
	    if (migrateStats.allowed_nodes) {
		migration.allowedNodes = migrateStats.allowed_nodes;
		let target = me.lookup('pveNodeSelector').value;
		if (target.length && !migrateStats.allowed_nodes.includes(target)) {
		    let disallowed = migrateStats.not_allowed_nodes[target];
		    let missingStorages = disallowed.unavailable_storages.join(', ');
		    migration.possible = false;
		    migration.preconditions.push({
			text: 'Storage (' + missingStorages + ') not available on selected target. ' +
			  'Start VM to use live storage migration or select other target node',
			severity: 'error',
		    });
		}
	    }
	    if (migrateStats.local_resources.length) {
		migration.hasLocalResources = true;
		if (!migration.overwriteLocalResourceCheck || vm.get('running')) {
		    migration.possible = false;
		    migration.preconditions.push({
			text: Ext.String.format('Can\'t migrate VM with local resources: {0}',
			migrateStats.local_resources.join(', ')),
			severity: 'error',
		    });
		} else {
		    migration.preconditions.push({
			text: Ext.String.format('Migrate VM with local resources: {0}. ' +
			'This might fail if resources aren\'t available on the target node.',
			migrateStats.local_resources.join(', ')),
			severity: 'warning',
		    });
		}
	    }
	    if (migrateStats.local_disks.length) {
		migrateStats.local_disks.forEach(function(disk) {
		    if (disk.cdrom && disk.cdrom === 1) {
			if (!disk.volid.includes('vm-' + vm.get('vmid') + '-cloudinit')) {
			    migration.possible = false;
			    migration.preconditions.push({
				text: "Can't migrate VM with local CD/DVD",
				severity: 'error',
			    });
			}
		    } else {
			let size = disk.size ? '(' + Proxmox.Utils.render_size(disk.size) + ')' : '';
			migration['with-local-disks'] = 1;
			migration.preconditions.push({
			    text: Ext.String.format('Migration with local disk might take long: {0} {1}', disk.volid, size),
			    severity: 'warning',
			});
		    }
		});
	    }
	    vm.set('migration', migration);
	},
	checkLxcPreconditions: function(resetMigrationPossible) {
	    let vm = this.getViewModel();
	    if (vm.get('running')) {
		vm.set('migration.mode', 'restart');
	    }
	},
    },
    width: 600,
    modal: true,
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    border: false,
    items: [
	{
	    xtype: 'form',
	    reference: 'formPanel',
	    bodyPadding: 10,
	    border: false,
	    layout: 'hbox',
	    items: [
		{
		    xtype: 'container',
		    flex: 1,
		    items: [{
			xtype: 'displayfield',
			name: 'source',
			fieldLabel: gettext('Source node'),
			bind: {
			    value: '{nodename}',
			},
		    },
		    {
			xtype: 'displayfield',
			reference: 'migrationMode',
			fieldLabel: gettext('Mode'),
			bind: {
			    value: '{setMigrationMode}',
			},
		    }],
		},
		{
		    xtype: 'container',
		    flex: 1,
		    items: [{
			xtype: 'pveNodeSelector',
			reference: 'pveNodeSelector',
			name: 'target',
			fieldLabel: gettext('Target node'),
			allowBlank: false,
			disallowedNodes: undefined,
			onlineValidator: true,
			listeners: {
			    change: 'onTargetChange',
			},
		    },
		    {
			    xtype: 'pveStorageSelector',
			    reference: 'pveDiskStorageSelector',
			    name: 'targetstorage',
			    fieldLabel: gettext('Target storage'),
			    storageContent: 'images',
			    allowBlank: true,
			    autoSelect: false,
			    emptyText: gettext('Current layout'),
			    bind: {
				hidden: '{setStorageselectorHidden}',
			    },
		    },
		    {
			xtype: 'proxmoxcheckbox',
			name: 'overwriteLocalResourceCheck',
			fieldLabel: gettext('Force'),
			autoEl: {
			    tag: 'div',
			    'data-qtip': 'Overwrite local resources unavailable check',
			},
			bind: {
			    hidden: '{setLocalResourceCheckboxHidden}',
			    value: '{migration.overwriteLocalResourceCheck}',
			},
			listeners: {
			    change: {
				fn: 'checkMigratePreconditions',
				extraArg: true,
			    },
			},
		}],
		},
	    ],
	},
	{
	    xtype: 'gridpanel',
	    reference: 'preconditionGrid',
	    selectable: false,
	    flex: 1,
	    columns: [{
		text: '',
		dataIndex: 'severity',
		renderer: function(v) {
		    switch (v) {
			case 'warning':
			    return ' ';
			case 'error':
			    return '';
			default:
			    return v;
		    }
		},
		width: 35,
	    },
	    {
		text: 'Info',
		dataIndex: 'text',
		cellWrap: true,
		flex: 1,
	    }],
	    bind: {
		hidden: '{!migration.preconditions.length}',
		store: {
		    fields: ['severity', 'text'],
		    data: '{migration.preconditions}',
		    sorters: 'text',
		},
	    },
	},
    ],
    buttons: [
	{
	    xtype: 'proxmoxHelpButton',
	    reference: 'proxmoxHelpButton',
	    onlineHelp: 'pct_migration',
	    listenToGlobalEvent: false,
	    hidden: false,
	},
	'->',
	{
	    xtype: 'button',
	    reference: 'submitButton',
	    text: gettext('Migrate'),
	    handler: 'startMigration',
	    bind: {
		disabled: '{!migration.possible}',
	    },
	},
    ],
});
Ext.define('pve-prune-list', {
    extend: 'Ext.data.Model',
    fields: [
	'type',
	'vmid',
	{
	    name: 'ctime',
	    type: 'date',
	    dateFormat: 'timestamp',
	},
    ],
});
Ext.define('PVE.PruneInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pvePruneInputPanel',
    mixins: ['Proxmox.Mixin.CBind'],
    onGetValues: function(values) {
	let me = this;
	// the API expects a single prune-backups property string
	let pruneBackups = PVE.Parser.printPropertyString(values);
	values = {
	    'prune-backups': pruneBackups,
	    'type': me.backup_type,
	    'vmid': me.backup_id,
	};
	return values;
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	init: function(view) {
	    if (!view.url) {
		throw "no url specified";
	    }
	    if (!view.backup_type) {
		throw "no backup_type specified";
	    }
	    if (!view.backup_id) {
		throw "no backup_id specified";
	    }
	    this.reload(); // initial load
	},
	reload: function() {
	    let view = this.getView();
	    // helper to allow showing why a backup is kept
	    let addKeepReasons = function(backups, params) {
		const rules = [
		    'keep-last',
		    'keep-hourly',
		    'keep-daily',
		    'keep-weekly',
		    'keep-monthly',
		    'keep-yearly',
		    'keep-all', // when all keep options are not set
		];
		let counter = {};
		backups.sort((a, b) => b.ctime - a.ctime);
		let ruleIndex = -1;
		let nextRule = function() {
		    let rule;
		    do {
			ruleIndex++;
			rule = rules[ruleIndex];
		    } while (!params[rule] && rule !== 'keep-all');
		    counter[rule] = 0;
		    return rule;
		};
		let rule = nextRule();
		for (let backup of backups) {
		    if (backup.mark === 'keep') {
			counter[rule]++;
			if (rule !== 'keep-all') {
			    backup.keepReason = rule + ': ' + counter[rule];
			    if (counter[rule] >= params[rule]) {
				rule = nextRule();
			    }
			} else {
			    backup.keepReason = rule;
			}
		    }
		}
	    };
	    let params = view.getValues();
	    let keepParams = PVE.Parser.parsePropertyString(params["prune-backups"]);
	    Proxmox.Utils.API2Request({
		url: view.url,
		method: "GET",
		params: params,
		callback: function() {
		    // for easy breakpoint setting
		},
		failure: function(response, opts) {
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		},
		success: function(response, options) {
		    var data = response.result.data;
		    addKeepReasons(data, keepParams);
		    view.pruneStore.setData(data);
		},
	    });
	},
	control: {
	    field: { change: 'reload' },
	},
    },
    column1: [
	{
	    xtype: 'pmxPruneKeepField',
	    name: 'keep-last',
	    fieldLabel: gettext('keep-last'),
	},
	{
	    xtype: 'pmxPruneKeepField',
	    name: 'keep-hourly',
	    fieldLabel: gettext('keep-hourly'),
	},
	{
	    xtype: 'pmxPruneKeepField',
	    name: 'keep-daily',
	    fieldLabel: gettext('keep-daily'),
	},
	{
	    xtype: 'pmxPruneKeepField',
	    name: 'keep-weekly',
	    fieldLabel: gettext('keep-weekly'),
	},
	{
	    xtype: 'pmxPruneKeepField',
	    name: 'keep-monthly',
	    fieldLabel: gettext('keep-monthly'),
	},
	{
	    xtype: 'pmxPruneKeepField',
	    name: 'keep-yearly',
	    fieldLabel: gettext('keep-yearly'),
	},
    ],
    initComponent: function() {
        var me = this;
	me.pruneStore = Ext.create('Ext.data.Store', {
	    model: 'pve-prune-list',
	    sorters: { property: 'ctime', direction: 'DESC' },
	});
	me.column2 = [
	    {
		xtype: 'grid',
		height: 200,
		store: me.pruneStore,
		columns: [
		    {
			header: gettext('Backup Time'),
			sortable: true,
			dataIndex: 'ctime',
			renderer: function(value, metaData, record) {
			    let text = Ext.Date.format(value, 'Y-m-d H:i:s');
			    if (record.data.mark === 'remove') {
				return ''+ text +'
';
			    } else {
				return text;
			    }
			},
			flex: 1,
		    },
		    {
			text: 'Keep (reason)',
			dataIndex: 'mark',
			renderer: function(value, metaData, record) {
			    if (record.data.mark === 'keep') {
				return 'true (' + record.data.keepReason + ')';
			    } else if (record.data.mark === 'protected') {
				return 'true (protected)';
			    } else if (record.data.mark === 'renamed') {
				return 'true (renamed)';
			    } else {
				return 'false';
			    }
			},
			flex: 1,
		    },
		],
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.window.Prune', {
    extend: 'Proxmox.window.Edit',
    method: 'DELETE',
    submitText: gettext("Prune"),
    fieldDefaults: { labelWidth: 130 },
    isCreate: true,
    initComponent: function() {
        var me = this;
	if (!me.nodename) {
	    throw "no nodename specified";
	}
	if (!me.storage) {
	    throw "no storage specified";
	}
	if (!me.backup_type) {
	    throw "no backup_type specified";
	}
	if (me.backup_type !== 'qemu' && me.backup_type !== 'lxc') {
	    throw "unknown backup type: " + me.backup_type;
	}
	if (!me.backup_id) {
	    throw "no backup_id specified";
	}
	let title = Ext.String.format(
	    gettext("Prune Backups for '{0}' on Storage '{1}'"),
	    me.backup_type + '/' + me.backup_id,
	    me.storage,
	);
	Ext.apply(me, {
	    url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups",
	    title: title,
	    items: [
		{
		    xtype: 'pvePruneInputPanel',
		    url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups",
		    backup_type: me.backup_type,
		    backup_id: me.backup_id,
		    storage: me.storage,
		},
	    ],
	});
	me.callParent();
    },
});
Ext.define('PVE.window.Restore', {
    extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit?
    resizable: false,
    width: 500,
    modal: true,
    layout: 'auto',
    border: false,
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    '#liveRestore': {
		change: function(el, newVal) {
		    let liveWarning = this.lookupReference('liveWarning');
		    liveWarning.setHidden(!newVal);
		    let start = this.lookupReference('start');
		    start.setDisabled(newVal);
		},
	    },
	    'form': {
		validitychange: function(f, valid) {
		    this.lookupReference('doRestoreBtn').setDisabled(!valid);
		},
	    },
	},
	doRestore: function() {
	    let me = this;
	    let view = me.getView();
	    let values = view.down('form').getForm().getValues();
	    let params = {
		vmid: view.vmid || values.vmid,
		force: view.vmid ? 1 : 0,
	    };
	    if (values.unique) {
		params.unique = 1;
	    }
	    if (values.start && !values['live-restore']) {
		params.start = 1;
	    }
	    if (values['live-restore']) {
		params['live-restore'] = 1;
	    }
	    if (values.storage) {
		params.storage = values.storage;
	    }
	    ['bwlimit', 'cores', 'name', 'memory', 'sockets'].forEach(opt => {
		if ((values[opt] ?? '') !== '') {
		    params[opt] = values[opt];
		}
	    });
	    if (params.name && view.vmtype === 'lxc') {
		params.hostname = params.name;
		delete params.name;
	    }
	    let confirmMsg;
	    if (view.vmtype === 'lxc') {
		params.ostemplate = view.volid;
		params.restore = 1;
		if (values.unprivileged !== 'keep') {
		    params.unprivileged = values.unprivileged;
		}
		confirmMsg = Proxmox.Utils.format_task_description('vzrestore', params.vmid);
	    } else if (view.vmtype === 'qemu') {
		params.archive = view.volid;
		confirmMsg = Proxmox.Utils.format_task_description('qmrestore', params.vmid);
	    } else {
		throw 'unknown VM type';
	    }
	    let executeRestore = () => {
		Proxmox.Utils.API2Request({
		    url: `/nodes/${view.nodename}/${view.vmtype}`,
		    params: params,
		    method: 'POST',
		    waitMsgTarget: view,
		    failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
		    success: function(response, options) {
			Ext.create('Proxmox.window.TaskViewer', {
			    autoShow: true,
			    upid: response.result.data,
			});
			view.close();
		    },
		});
	    };
	    if (view.vmid) {
		confirmMsg += `. ${Ext.String.format(
		    gettext('This will permanently erase current {0} data.'),
		    view.vmtype === 'lxc' ? 'CT' : 'VM',
		)}`;
		if (view.vmtype === 'lxc') {
		    confirmMsg += `
${gettext('Mount point volumes are also erased.')}`;
		}
		Ext.Msg.confirm(gettext('Confirm'), confirmMsg, function(btn) {
		    if (btn === 'yes') {
			executeRestore();
		    }
		});
	    } else {
		executeRestore();
	    }
	},
	afterRender: function() {
	    let view = this.getView();
	    Proxmox.Utils.API2Request({
		url: `/nodes/${view.nodename}/vzdump/extractconfig`,
		method: 'GET',
		waitMsgTarget: view,
		params: {
		    volume: view.volid,
		},
		failure: response => Ext.Msg.alert('Error', response.htmlStatus),
		success: function(response, options) {
		    let allStoragesAvailable = true;
		    response.result.data.split('\n').forEach(line => {
			let [_, key, value] = line.match(/^([^:]+):\s*(\S+)\s*$/) ?? [];
			if (!key) {
			    return;
			}
			if (key === '#qmdump#map') {
			    let match = value.match(/^(\S+):(\S+):(\S*):(\S*):$/) ?? [];
			    // if a /dev/XYZ disk was backed up, ther is no storage hint
			    allStoragesAvailable &&= !!match[3] && !!PVE.data.ResourceStore.getById(
				`storage/${view.nodename}/${match[3]}`);
			} else if (key === 'name' || key === 'hostname') {
			    view.lookupReference('nameField').setEmptyText(value);
			} else if (key === 'memory' || key === 'cores' || key === 'sockets') {
			    view.lookupReference(`${key}Field`).setEmptyText(value);
			}
		    });
		    if (!allStoragesAvailable) {
			let storagesel = view.down('pveStorageSelector[name=storage]');
			storagesel.allowBlank = false;
			storagesel.setEmptyText('');
		    }
		},
	    });
	},
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.volid) {
	    throw "no volume ID specified";
	}
	if (!me.vmtype) {
	    throw "no vmtype specified";
	}
	let storagesel = Ext.create('PVE.form.StorageSelector', {
	    nodename: me.nodename,
	    name: 'storage',
	    value: '',
	    fieldLabel: gettext('Storage'),
	    storageContent: me.vmtype === 'lxc' ? 'rootdir' : 'images',
	    // when restoring a container without specifying a storage, the backend defaults
	    // to 'local', which is unintuitive and 'rootdir' might not even be allowed on it
	    allowBlank: me.vmtype !== 'lxc',
	    emptyText: me.vmtype === 'lxc' ? '' : gettext('From backup configuration'),
	    autoSelect: me.vmtype === 'lxc',
	});
	let items = [
	    {
		xtype: 'displayfield',
		value: me.volidText || me.volid,
		fieldLabel: gettext('Source'),
	    },
	    storagesel,
	    {
		xtype: 'pmxDisplayEditField',
		name: 'vmid',
		fieldLabel: me.vmtype === 'lxc' ? 'CT' : 'VM',
		value: me.vmid,
		editable: !me.vmid,
		editConfig: {
		    xtype: 'pveGuestIDSelector',
		    guestType: me.vmtype,
		    loadNextFreeID: true,
		    validateExists: false,
		},
	    },
	    {
		xtype: 'pveBandwidthField',
		name: 'bwlimit',
		backendUnit: 'KiB',
		allowZero: true,
		fieldLabel: gettext('Bandwidth Limit'),
		emptyText: gettext('Defaults to target storage restore limit'),
		autoEl: {
		    tag: 'div',
		    'data-qtip': gettext("Use '0' to disable all bandwidth limits."),
		},
	    },
	    {
		xtype: 'fieldcontainer',
		layout: 'hbox',
		items: [{
		    xtype: 'proxmoxcheckbox',
		    name: 'unique',
		    fieldLabel: gettext('Unique'),
		    flex: 1,
		    autoEl: {
			tag: 'div',
			'data-qtip': gettext('Autogenerate unique properties, e.g., MAC addresses'),
		    },
		    checked: false,
		},
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'start',
		    reference: 'start',
		    flex: 1,
		    fieldLabel: gettext('Start after restore'),
		    labelWidth: 105,
		    checked: false,
		}],
	    },
	];
	if (me.vmtype === 'lxc') {
	    items.push(
		{
		    xtype: 'radiogroup',
		    fieldLabel: gettext('Privilege Level'),
		    reference: 'noVNCScalingGroup',
		    height: '15px', // renders faster with value assigned
		    layout: {
			type: 'hbox',
			algin: 'stretch',
		    },
		    autoEl: {
			tag: 'div',
			'data-qtip':
			    gettext('Choose if you want to keep or override the privilege level of the restored Container.'),
		    },
		    items: [
			{
			    xtype: 'radiofield',
			    name: 'unprivileged',
			    inputValue: 'keep',
			    boxLabel: gettext('From Backup'),
			    flex: 1,
			    checked: true,
			},
			{
			    xtype: 'radiofield',
			    name: 'unprivileged',
			    inputValue: '1',
			    boxLabel: gettext('Unprivileged'),
			    flex: 1,
			},
			{
			    xtype: 'radiofield',
			    name: 'unprivileged',
			    inputValue: '0',
			    boxLabel: gettext('Privileged'),
			    flex: 1,
			    //margin: '0 0 0 10',
			},
		    ],
		},
	    );
	} else if (me.vmtype === 'qemu') {
	    items.push({
		xtype: 'proxmoxcheckbox',
		name: 'live-restore',
		itemId: 'liveRestore',
		flex: 1,
		fieldLabel: gettext('Live restore'),
		checked: false,
		hidden: !me.isPBS,
	    },
	    {
		xtype: 'displayfield',
		reference: 'liveWarning',
		// TODO: Remove once more tested/stable?
		value: gettext('Note: If anything goes wrong during the live-restore, new data written by the VM may be lost.'),
		userCls: 'pmx-hint',
		hidden: true,
	    });
	}
	items.push({
	    xtype: 'fieldset',
	    title: `${gettext('Override Settings')}:`,
	    layout: 'hbox',
	    defaults: {
		border: false,
		layout: 'anchor',
		flex: 1,
	    },
	    items: [
		{
		    padding: '0 10 0 0',
		    items: [{
			xtype: 'textfield',
			fieldLabel: me.vmtype === 'lxc' ? gettext('Hostname') : gettext('Name'),
			name: 'name',
			reference: 'nameField',
			allowBlank: true,
		    }, {
			xtype: 'proxmoxintegerfield',
			fieldLabel: gettext('Cores'),
			name: 'cores',
			reference: 'coresField',
			minValue: 1,
			maxValue: 128,
			allowBlank: true,
		    }],
		},
		{
		    padding: '0 0 0 10',
		    items: [
		    {
			xtype: 'pveMemoryField',
			fieldLabel: gettext('Memory'),
			name: 'memory',
			reference: 'memoryField',
			value: '',
			allowBlank: true,
		    },
		    {
			xtype: 'proxmoxintegerfield',
			fieldLabel: gettext('Sockets'),
			name: 'sockets',
			reference: 'socketsField',
			minValue: 1,
			maxValue: 4,
			allowBlank: true,
			hidden: me.vmtype !== 'qemu',
			disabled: me.vmtype !== 'qemu',
		    }],
		},
	    ],
	});
	let title = gettext('Restore') + ": " + (me.vmtype === 'lxc' ? 'CT' : 'VM');
	if (me.vmid) {
	    title = `${gettext('Overwrite')} ${title} ${me.vmid}`;
	}
	Ext.apply(me, {
	    title: title,
	    items: [
		{
		    xtype: 'form',
		    bodyPadding: 10,
		    border: false,
		    fieldDefaults: {
			labelWidth: 100,
			anchor: '100%',
		    },
		    items: items,
		},
	    ],
	    buttons: [
		{
		    text: gettext('Restore'),
		    reference: 'doRestoreBtn',
		    handler: 'doRestore',
		},
	    ],
	});
	me.callParent();
    },
});
/*
 * SafeDestroy window with additional checkboxes for removing guests
 */
Ext.define('PVE.window.SafeDestroyGuest', {
    extend: 'Proxmox.window.SafeDestroy',
    alias: 'widget.pveSafeDestroyGuest',
    additionalItems: [
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'purge',
	    reference: 'purgeCheckbox',
	    boxLabel: gettext('Purge from job configurations'),
	    checked: false,
	    autoEl: {
		tag: 'div',
		'data-qtip': gettext('Remove from replication, HA and backup jobs'),
	    },
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'destroyUnreferenced',
	    reference: 'destroyUnreferencedCheckbox',
	    boxLabel: gettext('Destroy unreferenced disks owned by guest'),
	    checked: false,
	    autoEl: {
		tag: 'div',
		'data-qtip': gettext('Scan all enabled storages for unreferenced disks and delete them.'),
	    },
	},
    ],
    note: gettext('Referenced disks will always be destroyed.'),
    getParams: function() {
	let me = this;
	const purgeCheckbox = me.lookupReference('purgeCheckbox');
	me.params.purge = purgeCheckbox.checked ? 1 : 0;
	const destroyUnreferencedCheckbox = me.lookupReference('destroyUnreferencedCheckbox');
	me.params["destroy-unreferenced-disks"] = destroyUnreferencedCheckbox.checked ? 1 : 0;
	return me.callParent();
    },
});
/*
 * SafeDestroy window with additional checkboxes for removing a storage on the disk level.
 */
Ext.define('PVE.window.SafeDestroyStorage', {
    extend: 'Proxmox.window.SafeDestroy',
    alias: 'widget.pveSafeDestroyStorage',
    showProgress: true,
    additionalItems: [
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'wipeDisks',
	    reference: 'wipeDisksCheckbox',
	    boxLabel: gettext('Cleanup Disks'),
	    checked: true,
	    autoEl: {
		tag: 'div',
		'data-qtip': gettext('Wipe labels and other left-overs'),
	    },
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'cleanupConfig',
	    reference: 'cleanupConfigCheckbox',
	    boxLabel: gettext('Cleanup Storage Configuration'),
	    checked: true,
	},
    ],
    getParams: function() {
	let me = this;
	me.params['cleanup-disks'] = me.lookupReference('wipeDisksCheckbox').checked ? 1 : 0;
	me.params['cleanup-config'] = me.lookupReference('cleanupConfigCheckbox').checked ? 1 : 0;
	return me.callParent();
    },
});
Ext.define('PVE.window.Settings', {
    extend: 'Ext.window.Window',
    width: '800px',
    title: gettext('My Settings'),
    iconCls: 'fa fa-gear',
    modal: true,
    bodyPadding: 10,
    resizable: false,
    buttons: [
	{
	    xtype: 'proxmoxHelpButton',
	    onlineHelp: 'gui_my_settings',
	    hidden: false,
	},
	'->',
	{
	    text: gettext('Close'),
	    handler: function() {
		this.up('window').close();
	    },
	},
    ],
    layout: 'hbox',
    controller: {
	xclass: 'Ext.app.ViewController',
	init: function(view) {
	    var me = this;
	    var sp = Ext.state.Manager.getProvider();
	    var username = sp.get('login-username') || Proxmox.Utils.noneText;
	    me.lookupReference('savedUserName').setValue(Ext.String.htmlEncode(username));
	    var vncMode = sp.get('novnc-scaling') || 'auto';
	    me.lookupReference('noVNCScalingGroup').setValue({ noVNCScalingField: vncMode });
	    let summarycolumns = sp.get('summarycolumns', 'auto');
	    me.lookup('summarycolumns').setValue(summarycolumns);
	    me.lookup('guestNotesCollapse').setValue(sp.get('guest-notes-collapse', 'never'));
	    var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
	    settings.forEach(function(setting) {
		var val = localStorage.getItem('pve-xterm-' + setting);
		if (val !== undefined && val !== null) {
		    var field = me.lookup(setting);
		    field.setValue(val);
		    field.resetOriginalValue();
		}
	    });
	},
	set_button_status: function() {
	    let me = this;
	    let form = me.lookup('xtermform');
	    let valid = form.isValid(), dirty = form.isDirty();
	    let hasValues = Object.values(form.getValues()).some(v => !!v);
	    me.lookup('xtermsave').setDisabled(!dirty || !valid);
	    me.lookup('xtermreset').setDisabled(!hasValues);
	},
	control: {
	    '#xtermjs form': {
		dirtychange: 'set_button_status',
		validitychange: 'set_button_status',
	    },
	    '#xtermjs button': {
		click: function(button) {
		    var me = this;
		    var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
		    settings.forEach(function(setting) {
			var field = me.lookup(setting);
			if (button.reference === 'xtermsave') {
			    var value = field.getValue();
			    if (value) {
				localStorage.setItem('pve-xterm-' + setting, value);
			    } else {
				localStorage.removeItem('pve-xterm-' + setting);
			    }
			} else if (button.reference === 'xtermreset') {
			    field.setValue(undefined);
			    localStorage.removeItem('pve-xterm-' + setting);
			}
			field.resetOriginalValue();
		    });
		    me.set_button_status();
		},
	    },
	    'button[name=reset]': {
		click: function() {
		    let blacklist = ['GuiCap', 'login-username', 'dash-storages'];
		    let sp = Ext.state.Manager.getProvider();
		    for (const state of Object.keys(sp.state)) {
			if (!blacklist.includes(state)) {
			    sp.clear(state);
			}
		    }
		    window.location.reload();
		},
	    },
	    'button[name=clear-username]': {
		click: function() {
		    let me = this;
		    me.lookupReference('savedUserName').setValue(Proxmox.Utils.noneText);
		    Ext.state.Manager.getProvider().clear('login-username');
		},
	    },
	    'grid[reference=dashboard-storages]': {
		selectionchange: function(grid, selected) {
		    var me = this;
		    var sp = Ext.state.Manager.getProvider();
		    // saves the selected storageids as "id1,id2,id3,..." or clears the variable
		    if (selected.length > 0) {
			sp.set('dash-storages', Ext.Array.pluck(selected, 'id').join(','));
		    } else {
			sp.clear('dash-storages');
		    }
		},
		afterrender: function(grid) {
		    let store = grid.getStore();
		    let storages = Ext.state.Manager.getProvider().get('dash-storages') || '';
		    let items = [];
		    storages.split(',').forEach(storage => {
			if (storage !== '') { // we have to get the records to be able to select them
			    let item = store.getById(storage);
			    if (item) {
				items.push(item);
			    }
			}
		    });
		    grid.suspendEvent('selectionchange');
		    grid.getSelectionModel().select(items);
		    grid.resumeEvent('selectionchange');
		},
	    },
	    'field[reference=summarycolumns]': {
		change: (el, newValue) => Ext.state.Manager.getProvider().set('summarycolumns', newValue),
	    },
	    'field[reference=guestNotesCollapse]': {
		change: (e, v) => Ext.state.Manager.getProvider().set('guest-notes-collapse', v),
	    },
	},
    },
    items: [{
	xtype: 'fieldset',
	flex: 1,
	title: gettext('Webinterface Settings'),
	margin: '5',
	layout: {
	    type: 'vbox',
	    align: 'left',
	},
	defaults: {
	    width: '100%',
	    margin: '0 0 10 0',
	},
	items: [
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Dashboard Storages'),
		labelAlign: 'left',
		labelWidth: '50%',
	    },
	    {
		xtype: 'grid',
		maxHeight: 150,
		reference: 'dashboard-storages',
		selModel: {
		    selType: 'checkboxmodel',
		},
		columns: [{
		    header: gettext('Name'),
		    dataIndex: 'storage',
		    flex: 1,
		}, {
		    header: gettext('Node'),
		    dataIndex: 'node',
		    flex: 1,
		}],
		store: {
		    type: 'diff',
		    field: ['type', 'storage', 'id', 'node'],
		    rstore: PVE.data.ResourceStore,
		    filters: [{
			property: 'type',
			value: 'storage',
		    }],
		    sorters: ['node', 'storage'],
		},
	    },
	    {
		xtype: 'box',
		autoEl: { tag: 'hr' },
	    },
	    {
		xtype: 'container',
		layout: 'hbox',
		items: [
		    {
			xtype: 'displayfield',
			fieldLabel: gettext('Saved User Name') + ':',
			labelWidth: 150,
			stateId: 'login-username',
			reference: 'savedUserName',
			flex: 1,
			value: '',
		    },
		    {
			xtype: 'button',
			cls: 'x-btn-default-toolbar-small proxmox-inline-button',
			text: gettext('Reset'),
			name: 'clear-username',
		    },
		],
	    },
	    {
		xtype: 'box',
		autoEl: { tag: 'hr' },
	    },
	    {
		xtype: 'container',
		layout: 'hbox',
		items: [
		    {
			xtype: 'displayfield',
			fieldLabel: gettext('Layout') + ':',
			flex: 1,
		    },
		    {
			xtype: 'button',
			cls: 'x-btn-default-toolbar-small proxmox-inline-button',
			text: gettext('Reset'),
			tooltip: gettext('Reset all layout changes (for example, column widths)'),
			name: 'reset',
		    },
		],
	    },
	    {
		xtype: 'box',
		autoEl: { tag: 'hr' },
	    },
	    {
		xtype: 'proxmoxKVComboBox',
		fieldLabel: gettext('Summary columns') + ':',
		labelWidth: 150,
		stateId: 'summarycolumns',
		reference: 'summarycolumns',
		comboItems: [
		    ['auto', 'auto'],
		    ['1', '1'],
		    ['2', '2'],
		    ['3', '3'],
		],
	    },
	    {
		xtype: 'proxmoxKVComboBox',
		fieldLabel: gettext('Guest Notes') + ':',
		labelWidth: 150,
		stateId: 'guest-notes-collapse',
		reference: 'guestNotesCollapse',
		comboItems: [
		    ['never', 'Show by default'],
		    ['always', 'Collapse by default'],
		    ['auto', 'auto (Collapse if empty)'],
		],
	    },
	],
    },
    {
	xtype: 'container',
	layout: 'vbox',
	flex: 1,
	margin: '5',
	defaults: {
	    width: '100%',
	    // right margin ensures that the right border of the fieldsets
	    // is shown
	    margin: '0 2 10 0',
	},
	items: [
	    {
		xtype: 'fieldset',
		itemId: 'xtermjs',
		title: gettext('xterm.js Settings'),
		items: [{
		    xtype: 'form',
		    reference: 'xtermform',
		    border: false,
		    layout: {
			type: 'vbox',
			algin: 'left',
		    },
		    defaults: {
			width: '100%',
			margin: '0 0 10 0',
		    },
		    items: [
			{
			    xtype: 'textfield',
			    name: 'fontFamily',
			    reference: 'fontFamily',
			    emptyText: Proxmox.Utils.defaultText,
			    fieldLabel: gettext('Font-Family'),
			},
			{
			    xtype: 'proxmoxintegerfield',
			    emptyText: Proxmox.Utils.defaultText,
			    name: 'fontSize',
			    reference: 'fontSize',
			    minValue: 1,
			    fieldLabel: gettext('Font-Size'),
			},
			{
			    xtype: 'numberfield',
			    name: 'letterSpacing',
			    reference: 'letterSpacing',
			    emptyText: Proxmox.Utils.defaultText,
			    fieldLabel: gettext('Letter Spacing'),
			},
			{
			    xtype: 'numberfield',
			    name: 'lineHeight',
			    minValue: 0.1,
			    reference: 'lineHeight',
			    emptyText: Proxmox.Utils.defaultText,
			    fieldLabel: gettext('Line Height'),
			},
			{
			    xtype: 'container',
			    layout: {
				type: 'hbox',
				pack: 'end',
			    },
			    defaults: {
				margin: '0 0 0 5',
			    },
			    items: [
				{
				    xtype: 'button',
				    reference: 'xtermreset',
				    disabled: true,
				    text: gettext('Reset'),
				},
				{
				    xtype: 'button',
				    reference: 'xtermsave',
				    disabled: true,
				    text: gettext('Save'),
				},
			    ],
			},
		    ],
		}],
	    }, {
		xtype: 'fieldset',
		title: gettext('noVNC Settings'),
		items: [
		    {
			xtype: 'radiogroup',
			fieldLabel: gettext('Scaling mode'),
			reference: 'noVNCScalingGroup',
			height: '15px', // renders faster with value assigned
			layout: {
			    type: 'hbox',
			},
			items: [
			    {
				xtype: 'radiofield',
				name: 'noVNCScalingField',
				inputValue: 'auto',
				boxLabel: 'Auto',
			    },
			    {
				xtype: 'radiofield',
				name: 'noVNCScalingField',
				inputValue: 'scale',
				boxLabel: 'Local Scaling',
				margin: '0 0 0 10',
			    }, {
				xtype: 'radiofield',
				name: 'noVNCScalingField',
				inputValue: 'off',
				boxLabel: 'Off',
				margin: '0 0 0 10',
			    },
			],
			listeners: {
			    change: function(el, { noVNCScalingField }) {
				let provider = Ext.state.Manager.getProvider();
				if (noVNCScalingField === 'auto') {
				    provider.clear('novnc-scaling');
				} else {
				    provider.set('novnc-scaling', noVNCScalingField);
				}
			    },
			},
		    },
		],
	    },
	],
    }],
});
Ext.define('PVE.window.Snapshot', {
    extend: 'Proxmox.window.Edit',
    viewModel: {
	data: {
	    type: undefined,
	    isCreate: undefined,
	    running: false,
	    guestAgentEnabled: false,
	},
	formulas: {
	    runningWithoutGuestAgent: (get) => get('type') === 'qemu' && get('running') && !get('guestAgentEnabled'),
	    shouldWarnAboutFS: (get) => get('isCreate') && get('runningWithoutGuestAgent') && get('!vmstate.checked'),
	},
    },
    onGetValues: function(values) {
	let me = this;
	if (me.type === 'lxc') {
	    delete values.vmstate;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	var vm = me.getViewModel();
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.vmid) {
	    throw "no VM ID specified";
	}
	if (!me.type) {
	    throw "no type specified";
	}
	vm.set('type', me.type);
	vm.set('running', me.running);
	vm.set('isCreate', me.isCreate);
	if (me.type === 'qemu' && me.isCreate) {
	    Proxmox.Utils.API2Request({
		url: `/nodes/${me.nodename}/${me.type}/${me.vmid}/config`,
		params: { 'current': '1' },
		method: 'GET',
		success: function(response, options) {
		    let res = response.result.data;
		    let enabled = PVE.Parser.parsePropertyString(res.agent, 'enabled');
		    vm.set('guestAgentEnabled', !!PVE.Parser.parseBoolean(enabled.enabled));
		},
	    });
	}
	me.items = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'snapname',
		value: me.snapname,
		fieldLabel: gettext('Name'),
		vtype: 'ConfigId',
		allowBlank: false,
	    },
	    {
		xtype: 'displayfield',
		hidden: me.isCreate,
		disabled: me.isCreate,
		name: 'snaptime',
		renderer: PVE.Utils.render_timestamp_human_readable,
		fieldLabel: gettext('Timestamp'),
	    },
	    {
		xtype: 'proxmoxcheckbox',
		hidden: me.type !== 'qemu' || !me.isCreate || !me.running,
		disabled: me.type !== 'qemu' || !me.isCreate || !me.running,
		name: 'vmstate',
		reference: 'vmstate',
		uncheckedValue: 0,
		defaultValue: 0,
		checked: 1,
		fieldLabel: gettext('Include RAM'),
	    },
	    {
		xtype: 'textareafield',
		grow: true,
		editable: !me.viewonly,
		name: 'description',
		fieldLabel: gettext('Description'),
	    },
	    {
		xtype: 'displayfield',
		userCls: 'pmx-hint',
		name: 'fswarning',
		hidden: true,
		value: gettext('It is recommended to either include the RAM or use the QEMU Guest Agent when taking a snapshot of a running VM to avoid inconsistencies.'),
		bind: {
		    hidden: '{!shouldWarnAboutFS}',
		},
	    },
	    {
		title: gettext('Settings'),
		hidden: me.isCreate,
		xtype: 'grid',
		itemId: 'summary',
		border: true,
		height: 200,
		store: {
		    model: 'KeyValue',
		    sorters: [
			{
			    property: 'key',
			    direction: 'ASC',
			},
		    ],
		},
		columns: [
		    {
			header: gettext('Key'),
			width: 150,
			dataIndex: 'key',
		    },
		    {
			header: gettext('Value'),
			flex: 1,
			dataIndex: 'value',
		    },
		],
	    },
	];
	me.url = `/nodes/${me.nodename}/${me.type}/${me.vmid}/snapshot`;
	let subject;
	if (me.isCreate) {
	    subject = (me.type === 'qemu' ? 'VM' : 'CT') + me.vmid + ' ' + gettext('Snapshot');
	    me.method = 'POST';
	    me.showTaskViewer = true;
	} else {
	    subject = `${gettext('Snapshot')} ${me.snapname}`;
	    me.url += `/${me.snapname}/config`;
	}
	Ext.apply(me, {
	    subject: subject,
	    width: me.isCreate ? 450 : 620,
	    height: me.isCreate ? undefined : 420,
	});
	me.callParent();
	if (!me.snapname) {
	    return;
	}
	me.load({
	    success: function(response) {
		let kvarray = [];
		Ext.Object.each(response.result.data, function(key, value) {
		    if (key === 'description' || key === 'snaptime') {
			return;
		    }
		    kvarray.push({ key: key, value: value });
		});
		let summarystore = me.down('#summary').getStore();
		summarystore.suspendEvents();
		summarystore.add(kvarray);
		summarystore.sort();
		summarystore.resumeEvents();
		summarystore.fireEvent('refresh', summarystore);
		me.setValues(response.result.data);
	    },
	});
    },
});
Ext.define('PVE.panel.StartupInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    onlineHelp: 'qm_startup_and_shutdown',
    onGetValues: function(values) {
	var me = this;
	var res = PVE.Parser.printStartup(values);
	if (res === undefined || res === '') {
	    return { 'delete': 'startup' };
	}
	return { startup: res };
    },
    setStartup: function(value) {
	var me = this;
	var startup = PVE.Parser.parseStartup(value);
	if (startup) {
	    me.setValues(startup);
	}
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: 'textfield',
		name: 'order',
		defaultValue: '',
		emptyText: 'any',
		fieldLabel: gettext('Start/Shutdown order'),
	    },
	    {
		xtype: 'textfield',
		name: 'up',
		defaultValue: '',
		emptyText: 'default',
		fieldLabel: gettext('Startup delay'),
	    },
	    {
		xtype: 'textfield',
		name: 'down',
		defaultValue: '',
		emptyText: 'default',
		fieldLabel: gettext('Shutdown timeout'),
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.window.StartupEdit', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveWindowStartupEdit',
    onlineHelp: undefined,
    initComponent: function() {
	let me = this;
	let ipanelConfig = me.onlineHelp ? { onlineHelp: me.onlineHelp } : {};
	let ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig);
	Ext.applyIf(me, {
	    subject: gettext('Start/Shutdown order'),
	    fieldDefaults: {
		labelWidth: 120,
	    },
	    items: [ipanel],
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		me.vmconfig = response.result.data;
		ipanel.setStartup(me.vmconfig.startup);
	    },
	});
    },
});
Ext.define('PVE.window.DownloadUrlToStorage', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveStorageDownloadUrl',
    mixins: ['Proxmox.Mixin.CBind'],
    isCreate: true,
    method: 'POST',
    showTaskViewer: true,
    title: gettext('Download from URL'),
    submitText: gettext('Download'),
    cbindData: function(initialConfig) {
	var me = this;
	return {
	    nodename: me.nodename,
	    storage: me.storage,
	    content: me.content,
	};
    },
    cbind: {
	url: '/nodes/{nodename}/storage/{storage}/download-url',
    },
    viewModel: {
	data: {
	    size: '-',
	    mimetype: '-',
	    enableQuery: true,
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	urlChange: function(field) {
	    this.resetMetaInfo();
	    this.setQueryEnabled();
	},
	setQueryEnabled: function() {
	    this.getViewModel().set('enableQuery', true);
	},
	resetMetaInfo: function() {
	    let vm = this.getViewModel();
	    vm.set('size', '-');
	    vm.set('mimetype', '-');
	},
	urlCheck: function(field) {
	    let me = this;
	    let view = me.getView();
	    const queryParam = view.getValues();
	    me.getViewModel().set('enableQuery', false);
	    me.resetMetaInfo();
	    let urlField = view.down('[name=url]');
	    Proxmox.Utils.API2Request({
		url: `/nodes/${view.nodename}/query-url-metadata`,
		method: 'GET',
		params: {
		    url: queryParam.url,
		    'verify-certificates': queryParam['verify-certificates'],
		},
		waitMsgTarget: view,
		failure: res => {
		    urlField.setValidation(res.result.message);
		    urlField.validate();
		    Ext.MessageBox.alert(gettext('Error'), res.htmlStatus);
		    // re-enable so one can directly requery, e.g., if it was just a network hiccup
		    me.setQueryEnabled();
		},
		success: function(res, opt) {
		    urlField.setValidation();
		    urlField.validate();
		    let data = res.result.data;
		    view.setValues({
			filename: data.filename || "",
			size: (data.size && Proxmox.Utils.format_size(data.size)) || gettext("Unknown"),
			mimetype: data.mimetype || gettext("Unknown"),
		    });
		},
	    });
	},
	hashChange: function(field) {
	    let checksum = Ext.getCmp('downloadUrlChecksum');
	    if (field.getValue() === '__default__') {
		checksum.setDisabled(true);
		checksum.setValue("");
		checksum.allowBlank = true;
	    } else {
		checksum.setDisabled(false);
		checksum.allowBlank = false;
	    }
	},
    },
    items: [
	{
	    xtype: 'inputpanel',
	    border: false,
	    onGetValues: function(values) {
		if (typeof values.checksum === 'string') {
		    values.checksum = values.checksum.trim();
		}
		return values;
	    },
	    columnT: [
		{
		    xtype: 'fieldcontainer',
		    layout: 'hbox',
		    fieldLabel: gettext('URL'),
		    items: [
			{
			    xtype: 'textfield',
			    name: 'url',
			    emptyText: gettext("Enter URL to download"),
			    allowBlank: false,
			    flex: 1,
			    listeners: {
				change: 'urlChange',
			    },
			},
			{
			    xtype: 'button',
			    name: 'check',
			    text: gettext('Query URL'),
			    margin: '0 0 0 5',
			    bind: {
				disabled: '{!enableQuery}',
			    },
			    listeners: {
				click: 'urlCheck',
			    },
			},
		    ],
		},
		{
		    xtype: 'textfield',
		    name: 'filename',
		    allowBlank: false,
		    fieldLabel: gettext('File name'),
		    emptyText: gettext("Please (re-)query URL to get meta information"),
		},
	    ],
	    column1: [
		{
		    xtype: 'displayfield',
		    name: 'size',
		    fieldLabel: gettext('File size'),
		    bind: {
			value: '{size}',
		    },
		},
	    ],
	    column2: [
		{
		    xtype: 'displayfield',
		    name: 'mimetype',
		    fieldLabel: gettext('MIME type'),
		    bind: {
			value: '{mimetype}',
		    },
		},
	    ],
	    advancedColumn1: [
		{
		    xtype: 'pveHashAlgorithmSelector',
		    name: 'checksum-algorithm',
		    fieldLabel: gettext('Hash algorithm'),
		    allowBlank: true,
		    hasNoneOption: true,
		    value: '__default__',
		    listeners: {
			change: 'hashChange',
		    },
		},
		{
		    xtype: 'textfield',
		    name: 'checksum',
		    fieldLabel: gettext('Checksum'),
		    allowBlank: true,
		    disabled: true,
		    emptyText: gettext('none'),
		    id: 'downloadUrlChecksum',
		},
	    ],
	    advancedColumn2: [
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'verify-certificates',
		    fieldLabel: gettext('Verify certificates'),
		    uncheckedValue: 0,
		    checked: true,
		    listeners: {
			change: 'setQueryEnabled',
		    },
		},
	    ],
	},
	{
	    xtype: 'hiddenfield',
	    name: 'content',
	    cbind: {
		value: '{content}',
	    },
	},
    ],
    initComponent: function() {
        var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.storage) {
	    throw "no storage ID specified";
	}
        me.callParent();
    },
});
Ext.define('PVE.window.UploadToStorage', {
    extend: 'Ext.window.Window',
    alias: 'widget.pveStorageUpload',
    mixins: ['Proxmox.Mixin.CBind'],
    resizable: false,
    modal: true,
    title: gettext('Upload'),
    acceptedExtensions: {
	iso: ['.img', '.iso'],
	vztmpl: ['.tar.gz', '.tar.xz', '.tar.zst'],
    },
    cbindData: function(initialConfig) {
	const me = this;
	const ext = me.acceptedExtensions[me.content] || [];
	me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
	return {
	    extensions: ext.join(', '),
	    filenameRegex: RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'),
	};
    },
    viewModel: {
	data: {
	    size: '-',
	    mimetype: '-',
	    filename: '',
	},
    },
    controller: {
	submit: function(button) {
	    const view = this.getView();
	    const form = this.lookup('formPanel').getForm();
	    const abortBtn = this.lookup('abortBtn');
	    const pbar = this.lookup('progressBar');
	    const updateProgress = function(per, bytes) {
		let text = (per * 100).toFixed(2) + '%';
		if (bytes) {
		    text += " (" + Proxmox.Utils.format_size(bytes) + ')';
		}
		pbar.updateProgress(per, text);
	    };
	    const fd = new FormData();
	    button.setDisabled(true);
	    abortBtn.setDisabled(false);
	    fd.append("content", view.content);
	    const fileField = form.findField('file');
	    const file = fileField.fileInputEl.dom.files[0];
	    fileField.setDisabled(true);
	    const filenameField = form.findField('filename');
	    const filename = filenameField.getValue();
	    filenameField.setDisabled(true);
	    const algorithmField = form.findField('checksum-algorithm');
	    algorithmField.setDisabled(true);
	    if (algorithmField.getValue() !== '__default__') {
		fd.append("checksum-algorithm", algorithmField.getValue());
		const checksumField = form.findField('checksum');
		fd.append("checksum", checksumField.getValue()?.trim());
		checksumField.setDisabled(true);
	    }
	    fd.append("filename", file, filename);
	    pbar.setVisible(true);
	    updateProgress(0);
	    const xhr = new XMLHttpRequest();
	    view.xhr = xhr;
	    xhr.addEventListener("load", function(e) {
		if (xhr.status === 200) {
		    view.hide();
		    const result = JSON.parse(xhr.response);
		    const upid = result.data;
		    Ext.create('Proxmox.window.TaskViewer', {
			autoShow: true,
			upid: upid,
			taskDone: view.taskDone,
			listeners: {
			    destroy: function() {
				view.close();
			    },
			},
		    });
		    return;
		}
		const err = Ext.htmlEncode(xhr.statusText);
		let msg = `${gettext('Error')} ${xhr.status.toString()}: ${err}`;
		if (xhr.responseText !== "") {
		    const result = Ext.decode(xhr.responseText);
		    result.message = msg;
		    msg = Proxmox.Utils.extractRequestError(result, true);
		}
		Ext.Msg.alert(gettext('Error'), msg, btn => view.close());
	    }, false);
	    xhr.addEventListener("error", function(e) {
		const err = e.target.status.toString();
		const msg = `Error '${err}' occurred while receiving the document.`;
		Ext.Msg.alert(gettext('Error'), msg, btn => view.close());
	    });
	    xhr.upload.addEventListener("progress", function(evt) {
		if (evt.lengthComputable) {
		    const percentComplete = evt.loaded / evt.total;
		    updateProgress(percentComplete, evt.loaded);
		}
	    }, false);
	    xhr.open("POST", `/api2/json${view.url}`, true);
	    xhr.send(fd);
	},
	validitychange: function(f, valid) {
	    const submitBtn = this.lookup('submitBtn');
	    submitBtn.setDisabled(!valid);
	},
	fileChange: function(input) {
	    const vm = this.getViewModel();
	    const name = input.value.replace(/^.*(\/|\\)/, '');
	    const fileInput = input.fileInputEl.dom;
	    vm.set('filename', name);
	    vm.set('size', (fileInput.files[0] && Proxmox.Utils.format_size(fileInput.files[0].size)) || '-');
	    vm.set('mimetype', (fileInput.files[0] && fileInput.files[0].type) || '-');
	},
	hashChange: function(field, value) {
	    const checksum = this.lookup('downloadUrlChecksum');
	    if (value === '__default__') {
		checksum.setDisabled(true);
		checksum.setValue("");
	    } else {
		checksum.setDisabled(false);
	    }
	},
    },
    items: [
	{
	    xtype: 'form',
	    reference: 'formPanel',
	    method: 'POST',
	    waitMsgTarget: true,
	    bodyPadding: 10,
	    border: false,
	    width: 400,
	    fieldDefaults: {
		labelWidth: 100,
		anchor: '100%',
            },
	    items: [
		{
		    xtype: 'filefield',
		    name: 'file',
		    buttonText: gettext('Select File'),
		    allowBlank: false,
		    fieldLabel: gettext('File'),
		    cbind: {
			accept: '{extensions}',
		    },
		    listeners: {
			change: 'fileChange',
		    },
		},
		{
		    xtype: 'textfield',
		    name: 'filename',
		    allowBlank: false,
		    fieldLabel: gettext('File name'),
		    bind: {
			value: '{filename}',
		    },
		    cbind: {
			regex: '{filenameRegex}',
		    },
		    regexText: gettext('Wrong file extension'),
		},
		{
		    xtype: 'displayfield',
		    name: 'size',
		    fieldLabel: gettext('File size'),
		    bind: {
			value: '{size}',
		    },
		},
		{
		    xtype: 'displayfield',
		    name: 'mimetype',
		    fieldLabel: gettext('MIME type'),
		    bind: {
			value: '{mimetype}',
		    },
		},
		{
		    xtype: 'pveHashAlgorithmSelector',
		    name: 'checksum-algorithm',
		    fieldLabel: gettext('Hash algorithm'),
		    allowBlank: true,
		    hasNoneOption: true,
		    value: '__default__',
		    listeners: {
			change: 'hashChange',
		    },
		},
		{
		    xtype: 'textfield',
		    name: 'checksum',
		    fieldLabel: gettext('Checksum'),
		    allowBlank: false,
		    disabled: true,
		    emptyText: gettext('none'),
		    reference: 'downloadUrlChecksum',
		},
		{
		    xtype: 'progressbar',
		    text: 'Ready',
		    hidden: true,
		    reference: 'progressBar',
		},
		{
		    xtype: 'hiddenfield',
		    name: 'content',
		    cbind: {
			value: '{content}',
		    },
		},
	    ],
	   listeners: {
		validitychange: 'validitychange',
	   },
	},
    ],
    buttons: [
	{
	    xtype: 'button',
	    text: gettext('Abort'),
	    reference: 'abortBtn',
	    disabled: true,
	    handler: function() {
		const me = this;
		me.up('pveStorageUpload').close();
	    },
	},
	{
	    text: gettext('Upload'),
	    reference: 'submitBtn',
	    disabled: true,
	    handler: 'submit',
	},
    ],
    listeners: {
	close: function() {
	    const me = this;
	    if (me.xhr) {
		me.xhr.abort();
		delete me.xhr;
	    }
	},
    },
    initComponent: function() {
        const me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.storage) {
	    throw "no storage ID specified";
	}
	if (!me.acceptedExtensions[me.content]) {
	    throw "content type not supported";
	}
        me.callParent();
    },
});
Ext.define('PVE.window.ScheduleSimulator', {
    extend: 'Ext.window.Window',
    title: gettext('Job Schedule Simulator'),
    viewModel: {
	data: {
	    simulatedOnce: false,
	},
	formulas: {
	    gridEmptyText: get => get('simulatedOnce') ? Proxmox.Utils.NoneText : gettext('No simulation done'),
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	close: function() {
	    this.getView().close();
	},
	simulate: function() {
	    let me = this;
	    let schedule = me.lookup('schedule').getValue();
	    if (!schedule) {
		return;
	    }
	    let iterations = me.lookup('iterations').getValue() || 10;
	    Proxmox.Utils.API2Request({
		url: '/cluster/jobs/schedule-analyze',
		method: 'GET',
		params: {
		    schedule,
		    iterations,
		},
		failure: response => {
		    me.getViewModel().set('simulatedOnce', true);
		    me.lookup('grid').getStore().setData([]);
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		},
		success: function(response) {
		    let schedules = response.result.data;
		    me.lookup('grid').getStore().setData(schedules);
		    me.getViewModel().set('simulatedOnce', true);
		},
	    });
	},
	scheduleChanged: function(field, value) {
	    this.lookup('simulateBtn').setDisabled(!value);
	},
	renderDate: function(value) {
	    let date = new Date(value*1000);
	    return date.toLocaleDateString();
	},
	renderTime: function(value) {
	    let date = new Date(value*1000);
	    return date.toLocaleTimeString();
	},
	init: function(view) {
	    let me = this;
	    if (view.schedule) {
		me.lookup('schedule').setValue(view.schedule);
	    }
	},
    },
    bodyPadding: 10,
    modal: true,
    resizable: false,
    width: 600,
    layout: 'fit',
    items: [
	{
	    xtype: 'inputpanel',
	    column1: [
		{
		    xtype: 'pveCalendarEvent',
		    reference: 'schedule',
		    fieldLabel: gettext('Schedule'),
		    listeners: {
			change: 'scheduleChanged',
		    },
		},
		{
		    xtype: 'proxmoxintegerfield',
		    reference: 'iterations',
		    fieldLabel: gettext('Iterations'),
		    minValue: 1,
		    maxValue: 100,
		    value: 10,
		},
		{
		    xtype: 'container',
		    layout: 'hbox',
		    items: [
			{
			    xtype: 'box',
			    flex: 1,
			},
			{
			    xtype: 'button',
			    reference: 'simulateBtn',
			    text: gettext('Simulate'),
			    handler: 'simulate',
			    disabled: true,
			},
		    ],
		},
	    ],
	    column2: [
		{
		    xtype: 'grid',
		    reference: 'grid',
		    bind: {
			emptyText: '{gridEmptyText}',
		    },
		    scrollable: true,
		    height: 300,
		    columns: [
			{
			    text: gettext('Date'),
			    renderer: 'renderDate',
			    dataIndex: 'timestamp',
			    flex: 1,
			},
			{
			    text: gettext('Time'),
			    renderer: 'renderTime',
			    dataIndex: 'timestamp',
			    align: 'right',
			    flex: 1,
			},
		    ],
		    store: {
			fields: ['timestamp'],
			data: [],
			sorter: 'timestamp',
		    },
		},
	    ],
	},
    ],
    buttons: [
	{
	    text: gettext('Done'),
	    handler: 'close',
	},
    ],
});
Ext.define('PVE.window.Wizard', {
    extend: 'Ext.window.Window',
    activeTitle: '', // used for automated testing
    width: 720,
    height: 510,
    modal: true,
    border: false,
    draggable: true,
    closable: true,
    resizable: false,
    layout: 'border',
    getValues: function(dirtyOnly) {
	let me = this;
	let values = {};
	me.down('form').getForm().getFields().each(field => {
	    if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) {
		Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
	    }
	});
	me.query('inputpanel').forEach(panel => {
	    Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly));
	});
        return values;
    },
    initComponent: function() {
	var me = this;
	var tabs = me.items || [];
	delete me.items;
	/*
	 * Items may have the following functions:
	 * validator(): per tab custom validation
	 * onSubmit(): submit handler
	 * onGetValues(): overwrite getValues results
	 */
	Ext.Array.each(tabs, function(tab) {
	    tab.disabled = true;
	});
	tabs[0].disabled = false;
	let maxidx = 0, curidx = 0;
	let check_card = function(card) {
	    let fields = card.query('field, fieldcontainer');
	    if (card.isXType('fieldcontainer')) {
		fields.unshift(card);
	    }
	    let valid = true;
	    for (const field of fields) {
		// Note: not all fielcontainer have isValid()
		if (Ext.isFunction(field.isValid) && !field.isValid()) {
		    valid = false;
		}
	    }
	    if (Ext.isFunction(card.validator)) {
		return card.validator();
	    }
	    return valid;
	};
	let disableTab = function(card) {
	    let tp = me.down('#wizcontent');
	    for (let idx = tp.items.indexOf(card); idx < tp.items.getCount(); idx++) {
		let tab = tp.items.getAt(idx);
		if (tab) {
		    tab.disable();
		}
	    }
	};
	let tabchange = function(tp, newcard, oldcard) {
	    if (newcard.onSubmit) {
		me.down('#next').setVisible(false);
		me.down('#submit').setVisible(true);
	    } else {
		me.down('#next').setVisible(true);
		me.down('#submit').setVisible(false);
	    }
	    let valid = check_card(newcard);
	    me.down('#next').setDisabled(!valid);
	    me.down('#submit').setDisabled(!valid);
	    me.down('#back').setDisabled(tp.items.indexOf(newcard) === 0);
	    let idx = tp.items.indexOf(newcard);
	    if (idx > maxidx) {
		maxidx = idx;
	    }
	    curidx = idx;
	    let ntab = tp.items.getAt(idx + 1);
	    if (valid && ntab && !newcard.onSubmit) {
		ntab.enable();
	    }
	};
	if (me.subject && !me.title) {
	    me.title = Proxmox.Utils.dialog_title(me.subject, true, false);
	}
	let sp = Ext.state.Manager.getProvider();
	let advancedOn = sp.get('proxmox-advanced-cb');
	Ext.apply(me, {
	    items: [
		{
		    xtype: 'form',
		    region: 'center',
		    layout: 'fit',
		    border: false,
		    margins: '5 5 0 5',
		    fieldDefaults: {
			labelWidth: 100,
			anchor: '100%',
		    },
		    items: [{
			itemId: 'wizcontent',
			xtype: 'tabpanel',
			activeItem: 0,
			bodyPadding: 0,
			listeners: {
			    afterrender: function(tp) {
				tabchange(tp, this.getActiveTab());
			    },
			    tabchange: function(tp, newcard, oldcard) {
				tabchange(tp, newcard, oldcard);
			    },
			},
			defaults: {
			    padding: 10,
			},
			items: tabs,
		    }],
		},
	    ],
	    fbar: [
		{
		    xtype: 'proxmoxHelpButton',
		    itemId: 'help',
		},
		'->',
		{
		    xtype: 'proxmoxcheckbox',
		    boxLabelAlign: 'before',
		    boxLabel: gettext('Advanced'),
		    value: advancedOn,
		    listeners: {
			change: function(_, value) {
			    let tp = me.down('#wizcontent');
			    tp.query('inputpanel').forEach(function(ip) {
				ip.setAdvancedVisible(value);
			    });
			    sp.set('proxmox-advanced-cb', value);
			},
		    },
		},
		{
		    text: gettext('Back'),
		    disabled: true,
		    itemId: 'back',
		    minWidth: 60,
		    handler: function() {
			let tp = me.down('#wizcontent');
			let prev = tp.items.indexOf(tp.getActiveTab()) - 1;
			if (prev < 0) {
			    return;
			}
			let ntab = tp.items.getAt(prev);
			if (ntab) {
			    tp.setActiveTab(ntab);
			}
		    },
		},
		{
		    text: gettext('Next'),
		    disabled: true,
		    itemId: 'next',
		    minWidth: 60,
		    handler: function() {
			let tp = me.down('#wizcontent');
			let activeTab = tp.getActiveTab();
			if (!check_card(activeTab)) {
			    return;
			}
			let next = tp.items.indexOf(activeTab) + 1;
			let ntab = tp.items.getAt(next);
			if (ntab) {
			    ntab.enable();
			    tp.setActiveTab(ntab);
			}
		    },
		},
		{
		    text: gettext('Finish'),
		    minWidth: 60,
		    hidden: true,
		    itemId: 'submit',
		    handler: function() {
			let tp = me.down('#wizcontent');
			tp.getActiveTab().onSubmit();
		    },
		},
	    ],
	});
	me.callParent();
	Ext.Array.each(me.query('inputpanel'), function(panel) {
	    panel.setAdvancedVisible(advancedOn);
	});
	Ext.Array.each(me.query('field'), function(field) {
	    let validcheck = function() {
		let tp = me.down('#wizcontent');
		// check validity for current to last enabled tab, as local change may affect validity of a later one
		for (let i = curidx; i <= maxidx && i < tp.items.getCount(); i++) {
		    let tab = tp.items.getAt(i);
		    let valid = check_card(tab);
		    // only set the buttons on the current panel
		    if (i === curidx) {
			me.down('#next').setDisabled(!valid);
			me.down('#submit').setDisabled(!valid);
		    }
		    // if a panel is invalid, then disable all following, else enable the next tab
		    let nextTab = tp.items.getAt(i + 1);
		    if (!valid) {
			disableTab(nextTab);
			return;
		    } else if (nextTab && !tab.onSubmit) {
			nextTab.enable();
		    }
		}
	    };
	    field.on('change', validcheck);
	    field.on('validitychange', validcheck);
	});
    },
});
Ext.define('PVE.window.GuestDiskReassign', {
    extend: 'Proxmox.window.Edit',
    mixins: ['Proxmox.Mixin.CBind'],
    resizable: false,
    modal: true,
    width: 350,
    border: false,
    layout: 'fit',
    showReset: false,
    showProgress: true,
    method: 'POST',
    viewModel: {
	data: {
	    mpType: '',
	},
	formulas: {
	    mpMaxCount: get => get('mpType') === 'mp'
		? PVE.Utils.mp_counts.mps - 1
		: PVE.Utils.mp_counts.unused - 1,
	},
    },
    cbindData: function() {
	let me = this;
	return {
	    vmid: me.vmid,
	    disk: me.disk,
	    isQemu: me.type === 'qemu',
	    nodename: me.nodename,
	    url: () => {
		let endpoint = me.type === 'qemu' ? 'move_disk' : 'move_volume';
		return `/nodes/${me.nodename}/${me.type}/${me.vmid}/${endpoint}`;
	    },
	};
    },
    cbind: {
	title: get => get('isQemu') ? gettext('Reassign Disk') : gettext('Reassign Volume'),
	submitText: get => get('title'),
	qemu: '{isQemu}',
	url: '{url}',
    },
    getValues: function() {
	let me = this;
	let values = me.formPanel.getForm().getValues();
	let params = {
	    vmid: me.vmid,
	    'target-vmid': values.targetVmid,
	};
	params[me.qemu ? 'disk' : 'volume'] = me.disk;
	if (me.qemu) {
	    params['target-disk'] = `${values.controller}${values.deviceid}`;
	} else {
	    params['target-volume'] = `${values.mpType}${values.mpId}`;
	}
	return params;
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	initViewModel: function(model) {
	    let view = this.getView();
	    let mpTypeValue = view.disk.match(/^unused\d+/) ? 'unused' : 'mp';
	    model.set('mpType', mpTypeValue);
	},
	onMpTypeChange: function(value) {
	    let view = this.getView();
	    view.getViewModel().set('mpType', value.getValue());
	    view.lookup('mpIdSelector').validate();
	},
	onTargetVMChange: function(f, vmid) {
	    let me = this;
	    let view = me.getView();
	    let diskSelector = view.lookup('diskSelector');
	    if (!vmid) {
		diskSelector.setVMConfig(null);
		me.VMConfig = null;
		return;
	    }
	    let url = `/nodes/${view.nodename}/${view.type}/${vmid}/config`;
	    Proxmox.Utils.API2Request({
		url: url,
		method: 'GET',
		failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
		success: function({ result }, options) {
		    if (view.qemu) {
			diskSelector.setVMConfig(result.data);
			diskSelector.setDisabled(false);
		    } else {
			let mpIdSelector = view.lookup('mpIdSelector');
			let mpType = view.lookup('mpType');
			view.VMConfig = result.data;
			mpIdSelector.setValue(
			    PVE.Utils.nextFreeMP(
				view.getViewModel().get('mpType'),
				view.VMConfig,
			    ).id,
			);
			mpType.setDisabled(false);
			mpIdSelector.setDisabled(false);
			mpIdSelector.validate();
		    }
		},
	    });
	},
    },
    defaultFocus: 'sourceDisk',
    items: [
	{
	    xtype: 'displayfield',
	    name: 'sourceDisk',
	    fieldLabel: gettext('Source'),
	    cbind: {
		name: get => get('isQemu') ? 'disk' : 'volume',
		value: '{disk}',
	    },
	    allowBlank: false,
	},
	{
	    xtype: 'vmComboSelector',
	    name: 'targetVmid',
	    allowBlank: false,
	    fieldLabel: gettext('Target Guest'),
	    store: {
		model: 'PVEResources',
		autoLoad: true,
		sorters: 'vmid',
		cbind: {}, // for nested cbinds
		filters: [
		    {
			property: 'type',
			cbind: { value: '{type}' },
		    },
		    {
			property: 'node',
			cbind: { value: '{nodename}' },
		    },
		    // FIXME: remove, artificial restriction that doesn't gains us anything..
		    {
			property: 'vmid',
			operator: '!=',
			cbind: { value: '{vmid}' },
		    },
		    {
			property: 'template',
			value: 0,
		    },
		],
	    },
	    listeners: { change: 'onTargetVMChange' },
	},
	{
	    xtype: 'pveControllerSelector',
	    reference: 'diskSelector',
	    withUnused: true,
	    disabled: true,
	    cbind: {
		hidden: '{!isQemu}',
	    },
	},
	{
	    xtype: 'container',
	    layout: 'hbox',
	    cbind: {
		hidden: '{isQemu}',
		disabled: '{isQemu}',
	    },
	    items: [
		{
		    xtype: 'pmxDisplayEditField',
		    cbind: {
			editable: get => !get('disk').match(/^unused\d+/),
			value: get => get('disk').match(/^unused\d+/) ? 'unused' : 'mp',
		    },
		    disabled: true,
		    name: 'mpType',
		    reference: 'mpType',
		    fieldLabel: gettext('Add as'),
		    submitValue: true,
		    flex: 4,
		    editConfig: {
			xtype: 'proxmoxKVComboBox',
			name: 'mpTypeCombo',
			deleteEmpty: false,
			cbind: {
			    hidden: '{isQemu}',
			},
			comboItems: [
			    ['mp', gettext('Mount Point')],
			    ['unused', gettext('Unused')],
			],
			listeners: { change: 'onMpTypeChange' },
		    },
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'mpId',
		    reference: 'mpIdSelector',
		    minValue: 0,
		    flex: 1,
		    allowBlank: false,
		    validateOnChange: true,
		    disabled: true,
		    bind: {
			maxValue: '{mpMaxCount}',
		    },
		    validator: function(value) {
			let view = this.up('window');
			let type = view.getViewModel().get('mpType');
			if (Ext.isDefined(view.VMConfig[`${type}${value}`])) {
			    return "Mount point is already in use.";
			}
			return true;
		    },
		},
	    ],
	},
    ],
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.vmid) {
	    throw "no VM ID specified";
	}
	if (!me.type) {
	    throw "no type specified";
	}
	me.callParent();
    },
});
Ext.define('PVE.window.TreeSettingsEdit', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveTreeSettingsEdit',
    title: gettext('Tree Settings'),
    isCreate: false,
    url: '#', // ignored as submit() gets overriden here, but the parent class requires it
    width: 450,
    fieldDefaults: {
	labelWidth: 150,
    },
    items: [
	{
	    xtype: 'inputpanel',
	    items: [
		{
		    xtype: 'proxmoxKVComboBox',
		    name: 'sort-field',
		    fieldLabel: gettext('Sort Key'),
		    comboItems: [
			['__default__', `${Proxmox.Utils.defaultText} (VMID)`],
			['vmid', 'VMID'],
			['name', gettext('Name')],
		    ],
		    defaultValue: '__default__',
		    value: '__default__',
		    deleteEmpty: false,
		},
		{
		    xtype: 'proxmoxKVComboBox',
		    name: 'group-templates',
		    fieldLabel: gettext('Group Templates'),
		    comboItems: [
			['__default__', `${Proxmox.Utils.defaultText} (${gettext("Yes")})`],
			[1, gettext('Yes')],
			[0, gettext('No')],
		    ],
		    defaultValue: '__default__',
		    value: '__default__',
		    deleteEmpty: false,
		},
		{
		    xtype: 'proxmoxKVComboBox',
		    name: 'group-guest-types',
		    fieldLabel: gettext('Group Guest Types'),
		    comboItems: [
			['__default__', `${Proxmox.Utils.defaultText} (${gettext("Yes")})`],
			[1, gettext('Yes')],
			[0, gettext('No')],
		    ],
		    defaultValue: '__default__',
		    value: '__default__',
		    deleteEmpty: false,
		},
		{
		    xtype: 'displayfield',
		    userCls: 'pmx-hint',
		    value: gettext('Settings are saved in the local storage of the browser'),
		},
	    ],
	},
    ],
    submit: function() {
	let me = this;
	let localStorage = Ext.state.Manager.getProvider();
	localStorage.set('pve-tree-sorting', me.down('inputpanel').getValues() || null);
	me.apiCallDone();
	me.close();
    },
    initComponent: function() {
	let me = this;
	me.callParent();
	let localStorage = Ext.state.Manager.getProvider();
	me.down('inputpanel').setValues(localStorage.get('pve-tree-sorting'));
    },
});
Ext.define('PVE.ha.FencingView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveFencingView'],
    onlineHelp: 'ha_manager_fencing',
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-ha-fencing',
	    data: [],
	});
	Ext.apply(me, {
	    store: store,
	    stateful: false,
	    viewConfig: {
		trackOver: false,
		deferEmptyText: false,
		emptyText: 'Use watchdog based fencing.',
	    },
	    columns: [
		{
		    header: 'Node',
		    width: 100,
		    sortable: true,
		    dataIndex: 'node',
		},
		{
		    header: gettext('Command'),
		    flex: 1,
		    dataIndex: 'command',
		},
	    ],
	});
	me.callParent();
    },
}, function() {
    Ext.define('pve-ha-fencing', {
	extend: 'Ext.data.Model',
	fields: [
	    'node', 'command', 'digest',
	],
    });
});
Ext.define('PVE.ha.GroupInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    onlineHelp: 'ha_manager_groups',
    groupId: undefined,
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = 'group';
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	let update_nodefield, update_node_selection;
	let sm = Ext.create('Ext.selection.CheckboxModel', {
	    mode: 'SIMPLE',
	    listeners: {
		selectionchange: function(model, selected) {
		    update_nodefield(selected);
		},
	    },
	});
	let store = Ext.create('Ext.data.Store', {
	    fields: ['node', 'mem', 'cpu', 'priority'],
	    data: PVE.data.ResourceStore.getNodes(), // use already cached data to avoid an API call
	    proxy: {
		type: 'memory',
		reader: { type: 'json' },
	    },
	    sorters: [
		{
		    property: 'node',
		    direction: 'ASC',
		},
	    ],
	});
	var nodegrid = Ext.createWidget('grid', {
	    store: store,
	    border: true,
	    height: 300,
	    selModel: sm,
	    columns: [
		{
		    header: gettext('Node'),
		    flex: 1,
		    dataIndex: 'node',
		},
		{
		    header: gettext('Memory usage') + " %",
		    renderer: PVE.Utils.render_mem_usage_percent,
		    sortable: true,
		    width: 150,
		    dataIndex: 'mem',
		},
		{
		    header: gettext('CPU usage'),
		    renderer: Proxmox.Utils.render_cpu,
		    sortable: true,
		    width: 150,
		    dataIndex: 'cpu',
		},
		{
		    header: 'Priority',
		    xtype: 'widgetcolumn',
		    dataIndex: 'priority',
		    sortable: true,
		    stopSelection: true,
		    widget: {
			xtype: 'proxmoxintegerfield',
			minValue: 0,
			maxValue: 1000,
			isFormField: false,
			listeners: {
			    change: function(numberfield, value, old_value) {
				let record = numberfield.getWidgetRecord();
				record.set('priority', value);
				update_nodefield(sm.getSelection());
				record.commit();
			    },
			},
		    },
		},
	    ],
	});
	let nodefield = Ext.create('Ext.form.field.Hidden', {
	    name: 'nodes',
	    value: '',
	    listeners: {
		change: function(field, value) {
		    update_node_selection(value);
		},
	    },
	    isValid: function() {
		let value = this.getValue();
		return value && value.length !== 0;
	    },
	});
	update_node_selection = function(string) {
	    sm.deselectAll(true);
	    string.split(',').forEach(function(e, idx, array) {
		let [node, priority] = e.split(':');
		store.each(function(record) {
		    if (record.get('node') === node) {
			sm.select(record, true);
			record.set('priority', priority);
			record.commit();
		    }
		});
	    });
	    nodegrid.reconfigure(store);
	};
	update_nodefield = function(selected) {
	    let nodes = selected
		.map(({ data }) => data.node + (data.priority ? `:${data.priority}` : ''))
		.join(',');
	    // nodefield change listener calls us again, which results in a
	    // endless recursion, suspend the event temporary to avoid this
	    nodefield.suspendEvent('change');
	    nodefield.setValue(nodes);
	    nodefield.resumeEvent('change');
	};
	me.column1 = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'group',
		value: me.groupId || '',
		fieldLabel: 'ID',
		vtype: 'StorageId',
		allowBlank: false,
	    },
	    nodefield,
	];
	me.column2 = [
	    {
		xtype: 'proxmoxcheckbox',
		name: 'restricted',
		uncheckedValue: 0,
		fieldLabel: 'restricted',
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'nofailback',
		uncheckedValue: 0,
		fieldLabel: 'nofailback',
	    },
	];
	me.columnB = [
	    {
		xtype: 'textfield',
		name: 'comment',
		fieldLabel: gettext('Comment'),
	    },
	    nodegrid,
	];
	me.callParent();
    },
});
Ext.define('PVE.ha.GroupEdit', {
    extend: 'Proxmox.window.Edit',
    groupId: undefined,
    initComponent: function() {
	var me = this;
	me.isCreate = !me.groupId;
	if (me.isCreate) {
            me.url = '/api2/extjs/cluster/ha/groups';
            me.method = 'POST';
        } else {
            me.url = '/api2/extjs/cluster/ha/groups/' + me.groupId;
            me.method = 'PUT';
        }
	var ipanel = Ext.create('PVE.ha.GroupInputPanel', {
	    isCreate: me.isCreate,
	    groupId: me.groupId,
	});
	Ext.apply(me, {
            subject: gettext('HA Group'),
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.ha.GroupSelector', {
    extend: 'Proxmox.form.ComboGrid',
    alias: ['widget.pveHAGroupSelector'],
    value: [],
    autoSelect: false,
    valueField: 'group',
    displayField: 'group',
    listConfig: {
	columns: [
	    {
		header: gettext('Group'),
		width: 100,
		sortable: true,
		dataIndex: 'group',
	    },
	    {
		header: gettext('Nodes'),
		width: 100,
		sortable: false,
		dataIndex: 'nodes',
	    },
	    {
		header: gettext('Comment'),
		flex: 1,
		dataIndex: 'comment',
		renderer: Ext.String.htmlEncode,
	    },
	],
    },
    store: {
	    model: 'pve-ha-groups',
	    sorters: {
		property: 'group',
		direction: 'ASC',
	    },
    },
    initComponent: function() {
	var me = this;
	me.callParent();
	me.getStore().load();
    },
}, function() {
    Ext.define('pve-ha-groups', {
	extend: 'Ext.data.Model',
	fields: [
	    'group', 'type', 'digest', 'nodes', 'comment',
	    {
		name: 'restricted',
		type: 'boolean',
	    },
	    {
		name: 'nofailback',
		type: 'boolean',
	    },
	],
	proxy: {
            type: 'proxmox',
	    url: "/api2/json/cluster/ha/groups",
	},
	idProperty: 'group',
    });
});
Ext.define('PVE.ha.GroupsView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveHAGroupsView'],
    onlineHelp: 'ha_manager_groups',
    stateful: true,
    stateId: 'grid-ha-groups',
    initComponent: function() {
	var me = this;
	var caps = Ext.state.Manager.get('GuiCap');
	var store = new Ext.data.Store({
	    model: 'pve-ha-groups',
	    sorters: {
		property: 'group',
		direction: 'ASC',
	    },
	});
	var reload = function() {
	    store.load();
	};
	var sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
            Ext.create('PVE.ha.GroupEdit', {
                groupId: rec.data.group,
		listeners: {
		    destroy: () => store.load(),
		},
		autoShow: true,
            });
	};
	let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/cluster/ha/groups/',
	    callback: () => store.load(),
	});
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    tbar: [
		{
		    text: gettext('Create'),
		    disabled: !caps.nodes['Sys.Console'],
		    handler: function() {
			Ext.create('PVE.ha.GroupEdit', {
			    listeners: {
				destroy: () => store.load(),
			    },
			    autoShow: true,
			});
		    },
		},
		edit_btn,
		remove_btn,
	    ],
	    columns: [
		{
		    header: gettext('Group'),
		    width: 150,
		    sortable: true,
		    dataIndex: 'group',
		},
		{
		    header: 'restricted',
		    width: 100,
		    sortable: true,
		    renderer: Proxmox.Utils.format_boolean,
		    dataIndex: 'restricted',
		},
		{
		    header: 'nofailback',
		    width: 100,
		    sortable: true,
		    renderer: Proxmox.Utils.format_boolean,
		    dataIndex: 'nofailback',
		},
		{
		    header: gettext('Nodes'),
		    flex: 1,
		    sortable: false,
		    dataIndex: 'nodes',
		},
		{
		    header: gettext('Comment'),
		    flex: 1,
		    renderer: Ext.String.htmlEncode,
		    dataIndex: 'comment',
		},
	    ],
	    listeners: {
		activate: reload,
		beforeselect: (grid, record, index, eOpts) => caps.nodes['Sys.Console'],
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.ha.VMResourceInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    onlineHelp: 'ha_manager_resource_config',
    vmid: undefined,
    onGetValues: function(values) {
	var me = this;
	if (values.vmid) {
	    values.sid = values.vmid;
	}
	delete values.vmid;
	PVE.Utils.delete_if_default(values, 'group', '', me.isCreate);
	PVE.Utils.delete_if_default(values, 'max_restart', '1', me.isCreate);
	PVE.Utils.delete_if_default(values, 'max_relocate', '1', me.isCreate);
	return values;
    },
    initComponent: function() {
	var me = this;
	var MIN_QUORUM_VOTES = 3;
	var disabledHint = Ext.createWidget({
	    xtype: 'displayfield', // won't get submitted by default
	    userCls: 'pmx-hint',
	    value: 'Disabling the resource will stop the guest system. ' +
	    'See the online help for details.',
	    hidden: true,
	});
	var fewVotesHint = Ext.createWidget({
	    itemId: 'fewVotesHint',
	    xtype: 'displayfield',
	    userCls: 'pmx-hint',
	    value: 'At least three quorum votes are recommended for reliable HA.',
	    hidden: true,
	});
	Proxmox.Utils.API2Request({
	    url: '/cluster/config/nodes',
	    method: 'GET',
	    failure: function(response) {
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
	    },
	    success: function(response) {
		var nodes = response.result.data;
		var votes = 0;
		Ext.Array.forEach(nodes, function(node) {
		    var vote = parseInt(node.quorum_votes, 10); // parse as base 10
		    votes += vote || 0; // parseInt might return NaN, which is false
		});
		if (votes < MIN_QUORUM_VOTES) {
		    fewVotesHint.setVisible(true);
		}
	    },
	});
	var vmidStore = me.vmid ? {} : {
	    model: 'PVEResources',
	    autoLoad: true,
	    sorters: 'vmid',
	    filters: [
		{
		    property: 'type',
		    value: /lxc|qemu/,
		},
		{
		    property: 'hastate',
		    value: /unmanaged/,
		},
	    ],
	};
	// value is a string above, but a number below
	me.column1 = [
	    {
		xtype: me.vmid ? 'displayfield' : 'vmComboSelector',
		submitValue: me.isCreate,
		name: 'vmid',
		fieldLabel: me.vmid && me.guestType === 'ct' ? 'CT' : 'VM',
		value: me.vmid,
		store: vmidStore,
		validateExists: true,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'max_restart',
		fieldLabel: gettext('Max. Restart'),
		value: 1,
		minValue: 0,
		maxValue: 10,
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'max_relocate',
		fieldLabel: gettext('Max. Relocate'),
		value: 1,
		minValue: 0,
		maxValue: 10,
		allowBlank: false,
	    },
	];
	me.column2 = [
	    {
		xtype: 'pveHAGroupSelector',
		name: 'group',
		fieldLabel: gettext('Group'),
	    },
	    {
		xtype: 'proxmoxKVComboBox',
		name: 'state',
		value: 'started',
		fieldLabel: gettext('Request State'),
		comboItems: [
		    ['started', 'started'],
		    ['stopped', 'stopped'],
		    ['ignored', 'ignored'],
		    ['disabled', 'disabled'],
		],
		listeners: {
		    'change': function(field, newValue) {
			if (newValue === 'disabled') {
			    disabledHint.setVisible(true);
			} else if (disabledHint.isVisible()) {
				disabledHint.setVisible(false);
			    }
		    },
		},
	    },
	    disabledHint,
	];
	me.columnB = [
	    {
		xtype: 'textfield',
		name: 'comment',
		fieldLabel: gettext('Comment'),
	    },
	    fewVotesHint,
	];
	me.callParent();
    },
});
Ext.define('PVE.ha.VMResourceEdit', {
    extend: 'Proxmox.window.Edit',
    vmid: undefined,
    guestType: undefined,
    isCreate: undefined,
    initComponent: function() {
	var me = this;
	if (me.isCreate === undefined) {
	    me.isCreate = !me.vmid;
	}
	if (me.isCreate) {
            me.url = '/api2/extjs/cluster/ha/resources';
            me.method = 'POST';
        } else {
            me.url = '/api2/extjs/cluster/ha/resources/' + me.vmid;
            me.method = 'PUT';
        }
	var ipanel = Ext.create('PVE.ha.VMResourceInputPanel', {
	    isCreate: me.isCreate,
	    vmid: me.vmid,
	    guestType: me.guestType,
	});
	Ext.apply(me, {
	    subject: gettext('Resource') + ': ' + gettext('Container') +
	    '/' + gettext('Virtual Machine'),
	    isAdd: true,
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    var regex = /^(\S+):(\S+)$/;
		    var res = regex.exec(values.sid);
		    if (res[1] !== 'vm' && res[1] !== 'ct') {
			throw "got unexpected resource type";
		    }
		    values.vmid = res[2];
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.ha.ResourcesView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveHAResourcesView'],
    onlineHelp: 'ha_manager_resources',
    stateful: true,
    stateId: 'grid-ha-resources',
    initComponent: function() {
	let me = this;
	if (!me.rstore) {
	    throw "no store given";
	}
	Proxmox.Utils.monStoreErrors(me, me.rstore);
	let store = Ext.create('Proxmox.data.DiffStore', {
	    rstore: me.rstore,
	    filters: {
		property: 'type',
		value: 'service',
	    },
	});
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    let sid = rec.data.sid;
	    let res = sid.match(/^(\S+):(\S+)$/);
	    if (!res || (res[1] !== 'vm' && res[1] !== 'ct')) {
		console.warn(`unknown HA service ID type ${sid}`);
		return;
	    }
	    let [, guestType, vmid] = res;
	    Ext.create('PVE.ha.VMResourceEdit', {
		guestType: guestType,
		vmid: vmid,
		listeners: {
		    destroy: () => me.rstore.load(),
		},
		autoShow: true,
            });
	};
	let caps = Ext.state.Manager.get('GuiCap');
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    tbar: [
		{
		    text: gettext('Add'),
		    disabled: !caps.nodes['Sys.Console'],
		    handler: function() {
			Ext.create('PVE.ha.VMResourceEdit', {
			    listeners: {
				destroy: () => me.rstore.load(),
			    },
			    autoShow: true,
			});
		    },
		},
		{
		    xtype: 'proxmoxButton',
		    text: gettext('Edit'),
		    disabled: true,
		    selModel: sm,
		    handler: run_editor,
		},
		{
		    xtype: 'proxmoxStdRemoveButton',
		    selModel: sm,
		    getUrl: function(rec) {
			return `/cluster/ha/resources/${rec.get('sid')}`;
		    },
		    callback: () => me.rstore.load(),
		},
	    ],
	    columns: [
		{
		    header: 'ID',
		    width: 100,
		    sortable: true,
		    dataIndex: 'sid',
		},
		{
		    header: gettext('State'),
		    width: 100,
		    sortable: true,
		    dataIndex: 'state',
		},
		{
		    header: gettext('Node'),
		    width: 100,
		    sortable: true,
		    dataIndex: 'node',
		},
		{
		    header: gettext('Request State'),
		    width: 100,
		    hidden: true,
		    sortable: true,
		    renderer: v => v || 'started',
		    dataIndex: 'request_state',
		},
		{
		    header: gettext('CRM State'),
		    width: 100,
		    hidden: true,
		    sortable: true,
		    dataIndex: 'crm_state',
		},
		{
		    header: gettext('Name'),
		    width: 100,
		    sortable: true,
		    dataIndex: 'vname',
		},
		{
		    header: gettext('Max. Restart'),
		    width: 100,
		    sortable: true,
		    renderer: (v) => v === undefined ? '1' : v,
		    dataIndex: 'max_restart',
		},
		{
		    header: gettext('Max. Relocate'),
		    width: 100,
		    sortable: true,
		    renderer: (v) => v === undefined ? '1' : v,
		    dataIndex: 'max_relocate',
		},
		{
		    header: gettext('Group'),
		    width: 200,
		    sortable: true,
		    renderer: function(value, metaData, { data }) {
			if (data.errors && data.errors.group) {
			    metaData.tdCls = 'proxmox-invalid-row';
			    let html = `${Ext.htmlEncode(data.errors.group)}
`;
			    metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html + '"';
			}
			return value;
		    },
		    dataIndex: 'group',
		},
		{
		    header: gettext('Description'),
		    flex: 1,
		    renderer: Ext.String.htmlEncode,
		    dataIndex: 'comment',
		},
	    ],
	    listeners: {
		beforeselect: (grid, record, index, eOpts) => caps.nodes['Sys.Console'],
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.ha.Status', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveHAStatus',
    onlineHelp: 'chapter_ha_manager',
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    initComponent: function() {
	var me = this;
	me.rstore = Ext.create('Proxmox.data.ObjectStore', {
	    interval: me.interval,
	    model: 'pve-ha-status',
	    storeid: 'pve-store-' + ++Ext.idSeed,
	    groupField: 'type',
	    proxy: {
                type: 'proxmox',
		url: '/api2/json/cluster/ha/status/current',
	    },
	});
	me.items = [{
	    xtype: 'pveHAStatusView',
	    title: gettext('Status'),
	    rstore: me.rstore,
	    border: 0,
	    collapsible: true,
	    padding: '0 0 20 0',
	}, {
	    xtype: 'pveHAResourcesView',
	    flex: 1,
	    collapsible: true,
	    title: gettext('Resources'),
	    border: 0,
	    rstore: me.rstore,
	}];
	me.callParent();
	me.on('activate', me.rstore.startUpdate);
    },
});
Ext.define('PVE.ha.StatusView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveHAStatusView'],
    onlineHelp: 'chapter_ha_manager',
    sortPriority: {
	quorum: 1,
	master: 2,
	lrm: 3,
	service: 4,
    },
    initComponent: function() {
	var me = this;
	if (!me.rstore) {
	    throw "no rstore given";
	}
	Proxmox.Utils.monStoreErrors(me, me.rstore);
	var store = Ext.create('Proxmox.data.DiffStore', {
	    rstore: me.rstore,
	    sortAfterUpdate: true,
	    sorters: [{
		sorterFn: function(rec1, rec2) {
		    var p1 = me.sortPriority[rec1.data.type];
		    var p2 = me.sortPriority[rec2.data.type];
		    return p1 !== p2 ? p1 > p2 ? 1 : -1 : 0;
		},
	    }],
	    filters: {
		property: 'type',
		value: 'service',
		operator: '!=',
	    },
	});
	Ext.apply(me, {
	    store: store,
	    stateful: false,
	    viewConfig: {
		trackOver: false,
	    },
	    columns: [
		{
		    header: gettext('Type'),
		    width: 80,
		    dataIndex: 'type',
		},
		{
		    header: gettext('Status'),
		    width: 80,
		    flex: 1,
		    dataIndex: 'status',
		},
	    ],
	});
	me.callParent();
	me.on('activate', me.rstore.startUpdate);
	me.on('destroy', me.rstore.stopUpdate);
    },
}, function() {
    Ext.define('pve-ha-status', {
	extend: 'Ext.data.Model',
	fields: [
	    'id', 'type', 'node', 'status', 'sid',
	    'state', 'group', 'comment',
	    'max_restart', 'max_relocate', 'type',
	    'crm_state', 'request_state',
	    {
		name: 'vname',
		convert: function(value, record) {
		    let sid = record.data.sid;
		    if (!sid) return '';
		    let res = sid.match(/^(\S+):(\S+)$/);
		    if (res[1] !== 'vm' && res[1] !== 'ct') {
			return '-';
		    }
		    let vmid = res[2];
		    return PVE.data.ResourceStore.guestName(vmid);
		},
	    },
	],
	idProperty: 'id',
    });
});
Ext.define('PVE.dc.ACLAdd', {
    extend: 'Proxmox.window.Edit',
    alias: ['widget.pveACLAdd'],
    url: '/access/acl',
    method: 'PUT',
    isAdd: true,
    isCreate: true,
    width: 400,
    initComponent: function() {
        let me = this;
	let items = [
	    {
		xtype: me.path ? 'hiddenfield' : 'pvePermPathSelector',
		name: 'path',
		value: me.path,
		allowBlank: false,
		fieldLabel: gettext('Path'),
	    },
	];
	if (me.aclType === 'group') {
	    me.subject = gettext("Group Permission");
	    items.push({
		xtype: 'pveGroupSelector',
		name: 'groups',
		fieldLabel: gettext('Group'),
	    });
	} else if (me.aclType === 'user') {
	    me.subject = gettext("User Permission");
	    items.push({
		xtype: 'pmxUserSelector',
		name: 'users',
		fieldLabel: gettext('User'),
	    });
	} else if (me.aclType === 'token') {
	    me.subject = gettext("API Token Permission");
	    items.push({
		xtype: 'pveTokenSelector',
		name: 'tokens',
		fieldLabel: gettext('API Token'),
	    });
	} else {
	    throw "unknown ACL type";
	}
	items.push({
	    xtype: 'pmxRoleSelector',
	    name: 'roles',
	    value: 'NoAccess',
	    fieldLabel: gettext('Role'),
	});
	if (!me.path) {
	    items.push({
		xtype: 'proxmoxcheckbox',
		name: 'propagate',
		checked: true,
		uncheckedValue: 0,
		fieldLabel: gettext('Propagate'),
	    });
	}
	let ipanel = Ext.create('Proxmox.panel.InputPanel', {
	    items: items,
	    onlineHelp: 'pveum_permission_management',
	});
	Ext.apply(me, {
	    items: [ipanel],
	});
	me.callParent();
    },
});
Ext.define('PVE.dc.ACLView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveACLView'],
    onlineHelp: 'chapter_user_management',
    stateful: true,
    stateId: 'grid-acls',
    // use fixed path
    path: undefined,
    initComponent: function() {
	let me = this;
	let store = Ext.create('Ext.data.Store', {
	    model: 'pve-acl',
	    proxy: {
                type: 'proxmox',
		url: "/api2/json/access/acl",
	    },
	    sorters: {
		property: 'path',
		direction: 'ASC',
	    },
	});
	if (me.path) {
	    store.addFilter(Ext.create('Ext.util.Filter', {
		filterFn: item => item.data.path === me.path,
	    }));
	}
	let render_ugid = function(ugid, metaData, record) {
	    if (record.data.type === 'group') {
		return '@' + ugid;
	    }
	    return Ext.String.htmlEncode(ugid);
	};
	let columns = [
	    {
		header: gettext('User') + '/' + gettext('Group') + '/' + gettext('API Token'),
		flex: 1,
		sortable: true,
		renderer: render_ugid,
		dataIndex: 'ugid',
	    },
	    {
		header: gettext('Role'),
		flex: 1,
		sortable: true,
		dataIndex: 'roleid',
	    },
	];
	if (!me.path) {
	    columns.unshift({
		header: gettext('Path'),
		flex: 1,
		sortable: true,
		dataIndex: 'path',
	    });
	    columns.push({
		header: gettext('Propagate'),
		width: 80,
		sortable: true,
		dataIndex: 'propagate',
	    });
	}
	let sm = Ext.create('Ext.selection.RowModel', {});
	let remove_btn = new Proxmox.button.Button({
	    text: gettext('Remove'),
	    disabled: true,
	    selModel: sm,
	    confirmMsg: gettext('Are you sure you want to remove this entry'),
	    handler: function(btn, event, rec) {
		var params = {
		    'delete': 1,
		    path: rec.data.path,
		    roles: rec.data.roleid,
		};
		if (rec.data.type === 'group') {
		    params.groups = rec.data.ugid;
		} else if (rec.data.type === 'user') {
		    params.users = rec.data.ugid;
		} else if (rec.data.type === 'token') {
		    params.tokens = rec.data.ugid;
		} else {
		    throw 'unknown data type';
		}
		Proxmox.Utils.API2Request({
		    url: '/access/acl',
		    params: params,
		    method: 'PUT',
		    waitMsgTarget: me,
		    callback: () => store.load(),
		    failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
		});
	    },
	});
	Proxmox.Utils.monStoreErrors(me, store);
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: {
			xtype: 'menu',
			items: [
			    {
				text: gettext('Group Permission'),
				iconCls: 'fa fa-fw fa-group',
				handler: function() {
				    var win = Ext.create('PVE.dc.ACLAdd', {
					aclType: 'group',
					path: me.path,
				    });
				    win.on('destroy', () => store.load());
				    win.show();
				},
			    },
			    {
				text: gettext('User Permission'),
				iconCls: 'fa fa-fw fa-user',
				handler: function() {
				    var win = Ext.create('PVE.dc.ACLAdd', {
					aclType: 'user',
					path: me.path,
				    });
				    win.on('destroy', () => store.load());
				    win.show();
				},
			    },
			    {
				text: gettext('API Token Permission'),
				iconCls: 'fa fa-fw fa-user-o',
				handler: function() {
				    let win = Ext.create('PVE.dc.ACLAdd', {
					aclType: 'token',
					path: me.path,
				    });
				    win.on('destroy', () => store.load());
				    win.show();
				},
			    },
			],
		    },
		},
		remove_btn,
	    ],
	    viewConfig: {
		trackOver: false,
	    },
	    columns: columns,
	    listeners: {
		activate: () => store.load(),
	    },
	});
	me.callParent();
    },
}, function() {
    Ext.define('pve-acl', {
	extend: 'Ext.data.Model',
	fields: [
	    'path', 'type', 'ugid', 'roleid',
	    {
		name: 'propagate',
		type: 'boolean',
	    },
	],
    });
});
Ext.define('pve-acme-accounts', {
    extend: 'Ext.data.Model',
    fields: ['name'],
    proxy: {
	type: 'proxmox',
	    url: "/api2/json/cluster/acme/account",
    },
    idProperty: 'name',
});
Ext.define('pve-acme-plugins', {
    extend: 'Ext.data.Model',
    fields: ['type', 'plugin', 'api'],
    proxy: {
	type: 'proxmox',
	url: "/api2/json/cluster/acme/plugins",
    },
    idProperty: 'plugin',
});
Ext.define('PVE.dc.ACMEAccountView', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.pveACMEAccountView',
    title: gettext('Accounts'),
    controller: {
	xclass: 'Ext.app.ViewController',
	addAccount: function() {
	    let me = this;
	    let view = me.getView();
	    let defaultExists = view.getStore().findExact('name', 'default') !== -1;
	    Ext.create('PVE.node.ACMEAccountCreate', {
		defaultExists,
		taskDone: function() {
		    me.reload();
		},
	    }).show();
	},
	viewAccount: function() {
	    let me = this;
	    let view = me.getView();
	    let selection = view.getSelection();
	    if (selection.length < 1) return;
	    Ext.create('PVE.node.ACMEAccountView', {
		accountname: selection[0].data.name,
	    }).show();
	},
	reload: function() {
	    let me = this;
	    let view = me.getView();
	    view.getStore().rstore.load();
	},
	showTaskAndReload: function(options, success, response) {
	    let me = this;
	    if (!success) return;
	    let upid = response.result.data;
	    Ext.create('Proxmox.window.TaskProgress', {
		upid,
		taskDone: function() {
		    me.reload();
		},
	    }).show();
	},
    },
    minHeight: 150,
    emptyText: gettext('No Accounts configured'),
    columns: [
	{
	    dataIndex: 'name',
	    text: gettext('Name'),
	    renderer: Ext.String.htmlEncode,
	    flex: 1,
	},
    ],
    tbar: [
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Add'),
	    selModel: false,
	    handler: 'addAccount',
	},
	{
	    xtype: 'proxmoxButton',
	    text: gettext('View'),
	    handler: 'viewAccount',
	    disabled: true,
	},
	{
	    xtype: 'proxmoxStdRemoveButton',
	    baseurl: '/cluster/acme/account',
	    callback: 'showTaskAndReload',
	},
    ],
    listeners: {
	itemdblclick: 'viewAccount',
    },
    store: {
	type: 'diff',
	autoDestroy: true,
	autoDestroyRstore: true,
	rstore: {
	    type: 'update',
	    storeid: 'pve-acme-accounts',
	    model: 'pve-acme-accounts',
	    autoStart: true,
	},
	sorters: 'name',
    },
});
Ext.define('PVE.dc.ACMEPluginView', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.pveACMEPluginView',
    title: gettext('Challenge Plugins'),
    controller: {
	xclass: 'Ext.app.ViewController',
	addPlugin: function() {
	    let me = this;
	    Ext.create('PVE.dc.ACMEPluginEditor', {
		isCreate: true,
		apiCallDone: function() {
		    me.reload();
		},
	    }).show();
	},
	editPlugin: function() {
	    let me = this;
	    let view = me.getView();
	    let selection = view.getSelection();
	    if (selection.length < 1) return;
	    let plugin = selection[0].data.plugin;
	    Ext.create('PVE.dc.ACMEPluginEditor', {
		url: `/cluster/acme/plugins/${plugin}`,
		apiCallDone: function() {
		    me.reload();
		},
	    }).show();
	},
	reload: function() {
	    let me = this;
	    let view = me.getView();
	    view.getStore().rstore.load();
	},
    },
    minHeight: 150,
    emptyText: gettext('No Plugins configured'),
    columns: [
	{
	    dataIndex: 'plugin',
	    text: gettext('Plugin'),
	    renderer: Ext.String.htmlEncode,
	    flex: 1,
	},
	{
	    dataIndex: 'api',
	    text: 'API',
	    renderer: Ext.String.htmlEncode,
	    flex: 1,
	},
    ],
    tbar: [
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Add'),
	    handler: 'addPlugin',
	    selModel: false,
	},
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Edit'),
	    handler: 'editPlugin',
	    disabled: true,
	},
	{
	    xtype: 'proxmoxStdRemoveButton',
	    baseurl: '/cluster/acme/plugins',
	    callback: 'reload',
	},
    ],
    listeners: {
	itemdblclick: 'editPlugin',
    },
    store: {
	type: 'diff',
	autoDestroy: true,
	autoDestroyRstore: true,
	rstore: {
	    type: 'update',
	    storeid: 'pve-acme-plugins',
	    model: 'pve-acme-plugins',
	    autoStart: true,
	    filters: item => !!item.data.api,
	},
	sorters: 'plugin',
    },
});
Ext.define('PVE.dc.ACMEClusterView', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveACMEClusterView',
    onlineHelp: 'sysadmin_certificate_management',
    items: [
	{
	    region: 'north',
	    border: false,
	    xtype: 'pveACMEAccountView',
	},
	{
	    region: 'center',
	    border: false,
	    xtype: 'pveACMEPluginView',
	},
    ],
});
Ext.define('PVE.dc.ACMEPluginEditor', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveACMEPluginEditor',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'sysadmin_certs_acme_plugins',
    isAdd: true,
    isCreate: false,
    width: 550,
    url: '/cluster/acme/plugins/',
    subject: 'ACME DNS Plugin',
    items: [
	{
	    xtype: 'inputpanel',
	    // we dynamically create fields from the given schema
	    // things we have to do here:
	    // * save which fields we created to remove them again
	    // * split the data from the generic 'data' field into the boxes
	    // * on deletion collect those values again
	    // * save the original values of the data field
	    createdFields: {},
	    createdInitially: false,
	    originalValues: {},
	    createSchemaFields: function(schema) {
		let me = this;
		// we know where to add because we define it right below
		let container = me.down('container');
		let datafield = me.down('field[name=data]');
		let hintfield = me.down('field[name=hint]');
		if (!me.createdInitially) {
		    [me.originalValues] = PVE.Parser.parseACMEPluginData(datafield.getValue());
		}
		// collect values from custom fields and add it to 'data'',
		// then remove the custom fields
		let data = [];
		for (const [name, field] of Object.entries(me.createdFields)) {
		    let value = field.getValue();
		    if (value !== undefined && value !== null && value !== '') {
			data.push(`${name}=${value}`);
		    }
		    container.remove(field);
		}
		let datavalue = datafield.getValue();
		if (datavalue !== undefined && datavalue !== null && datavalue !== '') {
		    data.push(datavalue);
		}
		datafield.setValue(data.join('\n'));
		me.createdFields = {};
		if (typeof schema.fields !== 'object') {
		    schema.fields = {};
		}
		// create custom fields according to schema
		let gotSchemaField = false;
		let cmp = (a, b) => a[0].localeCompare(b[0]);
		for (const [name, definition] of Object.entries(schema.fields).sort(cmp)) {
		    let xtype;
		    switch (definition.type) {
			case 'string':
			    xtype = 'proxmoxtextfield';
			    break;
			case 'integer':
			    xtype = 'proxmoxintegerfield';
			    break;
			case 'number':
			    xtype = 'numberfield';
			    break;
			default:
			    console.warn(`unknown type '${definition.type}'`);
			    xtype = 'proxmoxtextfield';
			    break;
		    }
		    let label = name;
		    if (typeof definition.name === "string") {
			label = definition.name;
		    }
		    let field = Ext.create({
			xtype,
			name: `custom_${name}`,
			fieldLabel: label,
			width: '100%',
			labelWidth: 150,
			labelSeparator: '=',
			emptyText: definition.default || '',
			autoEl: definition.description ? {
			    tag: 'div',
			    'data-qtip': definition.description,
			} : undefined,
		    });
		    me.createdFields[name] = field;
		    container.add(field);
		    gotSchemaField = true;
		}
		datafield.setHidden(gotSchemaField); // prefer schema-fields
		if (schema.description) {
		    hintfield.setValue(schema.description);
		    hintfield.setHidden(false);
		} else {
		    hintfield.setValue('');
		    hintfield.setHidden(true);
		}
		// parse data from field and set it to the custom ones
		let extradata = [];
		[data, extradata] = PVE.Parser.parseACMEPluginData(datafield.getValue());
		for (const [key, value] of Object.entries(data)) {
		    if (me.createdFields[key]) {
			me.createdFields[key].setValue(value);
			me.createdFields[key].originalValue = me.originalValues[key];
		    } else {
			extradata.push(`${key}=${value}`);
		    }
		}
		datafield.setValue(extradata.join('\n'));
		if (!me.createdInitially) {
		    datafield.resetOriginalValue();
		    me.createdInitially = true; // save that we initally set that
		}
	    },
	    onGetValues: function(values) {
		let me = this;
		let win = me.up('pveACMEPluginEditor');
		if (win.isCreate) {
		    values.id = values.plugin;
		    values.type = 'dns'; // the only one for now
		}
		delete values.plugin;
		PVE.Utils.delete_if_default(values, 'validation-delay', '30', win.isCreate);
		let data = '';
		for (const [name, field] of Object.entries(me.createdFields)) {
		    let value = field.getValue();
		    if (value !== null && value !== undefined && value !== '') {
			data += `${name}=${value}\n`;
		    }
		    delete values[`custom_${name}`];
		}
		values.data = Ext.util.Base64.encode(data + values.data);
		return values;
	    },
	    items: [
		{
		    xtype: 'pmxDisplayEditField',
		    cbind: {
			editable: (get) => get('isCreate'),
			submitValue: (get) => get('isCreate'),
		    },
		    editConfig: {
			flex: 1,
			xtype: 'proxmoxtextfield',
			allowBlank: false,
		    },
		    name: 'plugin',
		    labelWidth: 150,
		    fieldLabel: gettext('Plugin ID'),
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'validation-delay',
		    labelWidth: 150,
		    fieldLabel: gettext('Validation Delay'),
		    emptyText: 30,
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		    minValue: 0,
		    maxValue: 48*60*60,
		},
		{
		    xtype: 'pveACMEApiSelector',
		    name: 'api',
		    labelWidth: 150,
		    listeners: {
			change: function(selector) {
			    let schema = selector.getSchema();
			    selector.up('inputpanel').createSchemaFields(schema);
			},
		    },
		},
		{
		    xtype: 'textarea',
		    fieldLabel: gettext('API Data'),
		    labelWidth: 150,
		    name: 'data',
		},
		{
		    xtype: 'displayfield',
		    fieldLabel: gettext('Hint'),
		    labelWidth: 150,
		    name: 'hint',
		    hidden: true,
		},
	    ],
	},
    ],
    initComponent: function() {
	var me = this;
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, opts) {
		    me.setValues(response.result.data);
		},
	    });
	} else {
	    me.method = 'POST';
	}
    },
});
Ext.define('PVE.panel.AuthBase', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveAuthBasePanel',
    type: '',
    onGetValues: function(values) {
	let me = this;
	if (!values.port) {
	    if (!me.isCreate) {
		Proxmox.Utils.assemble_field_data(values, { 'delete': 'port' });
	    }
	    delete values.port;
	}
	if (me.isCreate) {
	    values.type = me.type;
	}
	return values;
    },
    initComponent: function() {
	let me = this;
	let options = PVE.Utils.authSchema[me.type];
	if (!me.column1) { me.column1 = []; }
	if (!me.column2) { me.column2 = []; }
	if (!me.columnB) { me.columnB = []; }
	// first field is name
	me.column1.unshift({
	    xtype: me.isCreate ? 'textfield' : 'displayfield',
	    name: 'realm',
	    fieldLabel: gettext('Realm'),
	    value: me.realm,
	    allowBlank: false,
	});
	// last field is default'
	me.column1.push({
	    xtype: 'proxmoxcheckbox',
	    fieldLabel: gettext('Default'),
	    name: 'default',
	    uncheckedValue: 0,
	});
	if (options.tfa) {
	    // last field of column2is tfa
	    me.column2.push({
		xtype: 'pveTFASelector',
		deleteEmpty: !me.isCreate,
	    });
	}
	me.columnB.push({
	    xtype: 'textfield',
	    name: 'comment',
	    fieldLabel: gettext('Comment'),
	});
	me.callParent();
    },
});
Ext.define('PVE.dc.AuthEditBase', {
    extend: 'Proxmox.window.Edit',
    onlineHelp: 'pveum_authentication_realms',
    isAdd: true,
    fieldDefaults: {
	labelWidth: 120,
    },
    initComponent: function() {
	var me = this;
	me.isCreate = !me.realm;
	if (me.isCreate) {
	    me.url = '/api2/extjs/access/domains';
	    me.method = 'POST';
	} else {
	    me.url = '/api2/extjs/access/domains/' + me.realm;
	    me.method = 'PUT';
	}
	let authConfig = PVE.Utils.authSchema[me.authType];
	if (!authConfig) {
	    throw 'unknown auth type';
	} else if (!authConfig.add && me.isCreate) {
	    throw 'trying to add non addable realm';
	}
	me.subject = authConfig.name;
	let items;
	let bodyPadding;
	if (authConfig.syncipanel) {
	    bodyPadding = 0;
	    items = {
		xtype: 'tabpanel',
		region: 'center',
		layout: 'fit',
		bodyPadding: 10,
		items: [
		    {
			title: gettext('General'),
			realm: me.realm,
			xtype: authConfig.ipanel,
			isCreate: me.isCreate,
			type: me.authType,
		    },
		    {
			title: gettext('Sync Options'),
			realm: me.realm,
			xtype: authConfig.syncipanel,
			isCreate: me.isCreate,
			type: me.authType,
		    },
		],
	    };
	} else {
	    items = [{
		realm: me.realm,
		xtype: authConfig.ipanel,
		isCreate: me.isCreate,
		type: me.authType,
	    }];
	}
	Ext.apply(me, {
	    items,
	    bodyPadding,
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var data = response.result.data || {};
		    // just to be sure (should not happen)
		    if (data.type !== me.authType) {
			me.close();
			throw "got wrong auth type";
		    }
		    me.setValues(data);
		},
	    });
	}
    },
});
Ext.define('PVE.panel.ADInputPanel', {
    extend: 'PVE.panel.AuthBase',
    xtype: 'pveAuthADPanel',
    initComponent: function() {
	let me = this;
	if (me.type !== 'ad') {
	    throw 'invalid type';
	}
	me.column1 = [
	    {
		xtype: 'textfield',
		name: 'domain',
		fieldLabel: gettext('Domain'),
		emptyText: 'company.net',
		allowBlank: false,
	    },
	];
	me.column2 = [
	    {
		xtype: 'textfield',
		fieldLabel: gettext('Server'),
		name: 'server1',
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxtextfield',
		fieldLabel: gettext('Fallback Server'),
		deleteEmpty: !me.isCreate,
		name: 'server2',
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'port',
		fieldLabel: gettext('Port'),
		minValue: 1,
		maxValue: 65535,
		emptyText: gettext('Default'),
		submitEmptyText: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: 'SSL',
		name: 'secure',
		uncheckedValue: 0,
		listeners: {
		    change: function(field, newValue) {
			let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]');
			if (newValue === true) {
			    verifyCheckbox.enable();
			} else {
			    verifyCheckbox.disable();
			    verifyCheckbox.setValue(0);
			}
		    },
		},
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Verify Certificate'),
		name: 'verify',
		unceckedValue: 0,
		disabled: true,
		checked: false,
		autoEl: {
		    tag: 'div',
		    'data-qtip': gettext('Verify SSL certificate of the server'),
		},
	    },
	];
	me.callParent();
    },
    onGetValues: function(values) {
	let me = this;
	if (!values.verify) {
	    if (!me.isCreate) {
		Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' });
	    }
	    delete values.verify;
	}
	return me.callParent([values]);
    },
});
Ext.define('PVE.panel.LDAPInputPanel', {
    extend: 'PVE.panel.AuthBase',
    xtype: 'pveAuthLDAPPanel',
    initComponent: function() {
	let me = this;
	if (me.type !== 'ldap') {
	    throw 'invalid type';
	}
	me.column1 = [
	    {
		xtype: 'textfield',
		name: 'base_dn',
		fieldLabel: gettext('Base Domain Name'),
		emptyText: 'CN=Users,DC=Company,DC=net',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'user_attr',
		emptyText: 'uid / sAMAccountName',
		fieldLabel: gettext('User Attribute Name'),
		allowBlank: false,
	    },
	];
	me.column2 = [
	    {
		xtype: 'textfield',
		fieldLabel: gettext('Server'),
		name: 'server1',
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxtextfield',
		fieldLabel: gettext('Fallback Server'),
		deleteEmpty: !me.isCreate,
		name: 'server2',
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'port',
		fieldLabel: gettext('Port'),
		minValue: 1,
		maxValue: 65535,
		emptyText: gettext('Default'),
		submitEmptyText: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: 'SSL',
		name: 'secure',
		uncheckedValue: 0,
		listeners: {
		    change: function(field, newValue) {
			let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]');
			if (newValue === true) {
			    verifyCheckbox.enable();
			} else {
			    verifyCheckbox.disable();
			    verifyCheckbox.setValue(0);
			}
		    },
		},
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Verify Certificate'),
		name: 'verify',
		unceckedValue: 0,
		disabled: true,
		checked: false,
		autoEl: {
		    tag: 'div',
		    'data-qtip': gettext('Verify SSL certificate of the server'),
		},
	    },
	];
	me.callParent();
    },
    onGetValues: function(values) {
	let me = this;
	if (!values.verify) {
	    if (!me.isCreate) {
		Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' });
	    }
	    delete values.verify;
	}
	return me.callParent([values]);
    },
});
Ext.define('PVE.panel.LDAPSyncInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveAuthLDAPSyncPanel',
    editableAttributes: ['email'],
    editableDefaults: ['scope', 'enable-new'],
    default_opts: {},
    sync_attributes: {},
    // (de)construct the sync-attributes from the list above,
    // not touching all others
    onGetValues: function(values) {
	let me = this;
	me.editableDefaults.forEach((attr) => {
	    if (values[attr]) {
		me.default_opts[attr] = values[attr];
		delete values[attr];
	    } else {
		delete me.default_opts[attr];
	    }
	});
	let vanished_opts = [];
	['acl', 'entry', 'properties'].forEach((prop) => {
	    if (values[`remove-vanished-${prop}`]) {
		vanished_opts.push(prop);
	    }
	    delete values[`remove-vanished-${prop}`];
	});
	me.default_opts['remove-vanished'] = vanished_opts.join(';');
	values['sync-defaults-options'] = PVE.Parser.printPropertyString(me.default_opts);
	me.editableAttributes.forEach((attr) => {
	    if (values[attr]) {
		me.sync_attributes[attr] = values[attr];
		delete values[attr];
	    } else {
		delete me.sync_attributes[attr];
	    }
	});
	values.sync_attributes = PVE.Parser.printPropertyString(me.sync_attributes);
	PVE.Utils.delete_if_default(values, 'sync-defaults-options');
	PVE.Utils.delete_if_default(values, 'sync_attributes');
	// Force values.delete to be an array
	if (typeof values.delete === 'string') {
	   values.delete = values.delete.split(',');
	}
	if (me.isCreate) {
	    delete values.delete; // on create we cannot delete values
	}
	return values;
    },
    setValues: function(values) {
	let me = this;
	if (values.sync_attributes) {
	    me.sync_attributes = PVE.Parser.parsePropertyString(values.sync_attributes);
	    delete values.sync_attributes;
	    me.editableAttributes.forEach((attr) => {
		if (me.sync_attributes[attr]) {
		    values[attr] = me.sync_attributes[attr];
		}
	    });
	}
	if (values['sync-defaults-options']) {
	    me.default_opts = PVE.Parser.parsePropertyString(values['sync-defaults-options']);
	    delete values.default_opts;
	    me.editableDefaults.forEach((attr) => {
		if (me.default_opts[attr]) {
		    values[attr] = me.default_opts[attr];
		}
	    });
	    if (me.default_opts['remove-vanished']) {
		let opts = me.default_opts['remove-vanished'].split(';');
		for (const opt of opts) {
		    values[`remove-vanished-${opt}`] = 1;
		}
	    }
	}
	return me.callParent([values]);
    },
    column1: [
	{
	    xtype: 'proxmoxtextfield',
	    name: 'bind_dn',
	    deleteEmpty: true,
	    emptyText: Proxmox.Utils.noneText,
	    fieldLabel: gettext('Bind User'),
	},
	{
	    xtype: 'proxmoxtextfield',
	    inputType: 'password',
	    name: 'password',
	    emptyText: gettext('Unchanged'),
	    fieldLabel: gettext('Bind Password'),
	},
	{
	    xtype: 'proxmoxtextfield',
	    name: 'email',
	    fieldLabel: gettext('E-Mail attribute'),
	},
	{
	    xtype: 'proxmoxtextfield',
	    name: 'group_name_attr',
	    deleteEmpty: true,
	    fieldLabel: gettext('Groupname attr.'),
	},
	{
	    xtype: 'displayfield',
	    value: gettext('Default Sync Options'),
	},
	{
	    xtype: 'proxmoxKVComboBox',
	    name: 'scope',
	    emptyText: Proxmox.Utils.NoneText,
	    fieldLabel: gettext('Scope'),
	    value: '__default__',
	    deleteEmpty: false,
	    comboItems: [
		['__default__', Proxmox.Utils.NoneText],
		['users', gettext('Users')],
		['groups', gettext('Groups')],
		['both', gettext('Users and Groups')],
	    ],
	},
    ],
    column2: [
	{
	    xtype: 'proxmoxtextfield',
	    name: 'user_classes',
	    fieldLabel: gettext('User classes'),
	    deleteEmpty: true,
	    emptyText: 'inetorgperson, posixaccount, person, user',
	},
	{
	    xtype: 'proxmoxtextfield',
	    name: 'group_classes',
	    fieldLabel: gettext('Group classes'),
	    deleteEmpty: true,
	    emptyText: 'groupOfNames, group, univentionGroup, ipausergroup',
	},
	{
	    xtype: 'proxmoxtextfield',
	    name: 'filter',
	    fieldLabel: gettext('User Filter'),
	    deleteEmpty: true,
	},
	{
	    xtype: 'proxmoxtextfield',
	    name: 'group_filter',
	    fieldLabel: gettext('Group Filter'),
	    deleteEmpty: true,
	},
	{
	    // fake for spacing
	    xtype: 'displayfield',
	    value: ' ',
	},
	{
	    xtype: 'proxmoxKVComboBox',
	    value: '__default__',
	    deleteEmpty: false,
	    comboItems: [
		[
		    '__default__',
		    Ext.String.format(
			gettext("{0} ({1})"),
			Proxmox.Utils.yesText,
			Proxmox.Utils.defaultText,
		    ),
		],
		['1', Proxmox.Utils.yesText],
		['0', Proxmox.Utils.noText],
	    ],
	    name: 'enable-new',
	    fieldLabel: gettext('Enable new users'),
	},
    ],
    columnB: [
	{
	    xtype: 'fieldset',
	    title: gettext('Remove Vanished Options'),
	    items: [
		{
		    xtype: 'proxmoxcheckbox',
		    fieldLabel: gettext('ACL'),
		    name: 'remove-vanished-acl',
		    boxLabel: gettext('Remove ACLs of vanished users and groups.'),
		},
		{
		    xtype: 'proxmoxcheckbox',
		    fieldLabel: gettext('Entry'),
		    name: 'remove-vanished-entry',
		    boxLabel: gettext('Remove vanished user and group entries.'),
		},
		{
		    xtype: 'proxmoxcheckbox',
		    fieldLabel: gettext('Properties'),
		    name: 'remove-vanished-properties',
		    boxLabel: gettext('Remove vanished properties from synced users.'),
		},
	    ],
	},
    ],
});
Ext.define('PVE.panel.OpenIDInputPanel', {
    extend: 'PVE.panel.AuthBase',
    xtype: 'pveAuthOpenIDPanel',
    mixins: ['Proxmox.Mixin.CBind'],
    onGetValues: function(values) {
	let me = this;
	if (!values.verify) {
	    if (!me.isCreate) {
		Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' });
	    }
	    delete values.verify;
	}
	return me.callParent([values]);
    },
    columnT: [
	{
	    xtype: 'textfield',
	    name: 'issuer-url',
	    fieldLabel: gettext('Issuer URL'),
	    allowBlank: false,
	},
    ],
    column1: [
	{
	    xtype: 'proxmoxtextfield',
	    fieldLabel: gettext('Client ID'),
	    name: 'client-id',
	    allowBlank: false,
	},
	{
	    xtype: 'proxmoxtextfield',
	    fieldLabel: gettext('Client Key'),
	    cbind: {
		deleteEmpty: '{!isCreate}',
	    },
	    name: 'client-key',
	},
    ],
    column2: [
	{
	    xtype: 'proxmoxcheckbox',
	    fieldLabel: gettext('Autocreate Users'),
	    name: 'autocreate',
	    value: 0,
	    cbind: {
		deleteEmpty: '{!isCreate}',
	    },
	},
	{
	    xtype: 'pmxDisplayEditField',
	    name: 'username-claim',
	    fieldLabel: gettext('Username Claim'),
	    editConfig: {
		xtype: 'proxmoxKVComboBox',
		editable: true,
		comboItems: [
		    ['__default__', Proxmox.Utils.defaultText],
		    ['subject', 'subject'],
		    ['username', 'username'],
		    ['email', 'email'],
		],
	    },
	    cbind: {
		value: get => get('isCreate') ? '__default__' : Proxmox.Utils.defaultText,
		deleteEmpty: '{!isCreate}',
		editable: '{isCreate}',
	    },
	},
	{
	    xtype: 'proxmoxtextfield',
	    name: 'scopes',
	    fieldLabel: gettext('Scopes'),
	    emptyText: `${Proxmox.Utils.defaultText} (email profile)`,
	    submitEmpty: false,
	    cbind: {
		deleteEmpty: '{!isCreate}',
	    },
	},
	{
	    xtype: 'proxmoxKVComboBox',
	    name: 'prompt',
	    fieldLabel: gettext('Prompt'),
	    editable: true,
	    emptyText: gettext('Auth-Provider Default'),
	    comboItems: [
		['__default__', gettext('Auth-Provider Default')],
		['none', 'none'],
		['login', 'login'],
		['consent', 'consent'],
		['select_account', 'select_account'],
	    ],
	    cbind: {
		deleteEmpty: '{!isCreate}',
	    },
	},
    ],
    advancedColumnB: [
	{
	    xtype: 'proxmoxtextfield',
	    name: 'acr-values',
	    fieldLabel: gettext('ACR Values'),
	    submitEmpty: false,
	    cbind: {
		deleteEmpty: '{!isCreate}',
	    },
	},
    ],
    initComponent: function() {
	let me = this;
	if (me.type !== 'openid') {
	    throw 'invalid type';
	}
	me.callParent();
    },
});
Ext.define('PVE.dc.AuthView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveAuthView'],
    onlineHelp: 'pveum_authentication_realms',
    stateful: true,
    stateId: 'grid-authrealms',
    viewConfig: {
	trackOver: false,
    },
    columns: [
	{
	    header: gettext('Realm'),
	    width: 100,
	    sortable: true,
	    dataIndex: 'realm',
	},
	{
	    header: gettext('Type'),
	    width: 100,
	    sortable: true,
	    dataIndex: 'type',
	},
	{
	    header: gettext('TFA'),
	    width: 100,
	    sortable: true,
	    dataIndex: 'tfa',
	},
	{
	    header: gettext('Comment'),
	    sortable: false,
	    dataIndex: 'comment',
	    renderer: Ext.String.htmlEncode,
	    flex: 1,
	},
    ],
    store: {
	model: 'pmx-domains',
	sorters: {
	    property: 'realm',
	    direction: 'ASC',
	},
    },
    openEditWindow: function(authType, realm) {
	let me = this;
	Ext.create('PVE.dc.AuthEditBase', {
	    authType,
	    realm,
	    listeners: {
		destroy: () => me.reload(),
	    },
	}).show();
    },
    reload: function() {
	let me = this;
	me.getStore().load();
    },
    run_editor: function() {
	let me = this;
	let rec = me.getSelection()[0];
	if (!rec) {
	    return;
	}
	me.openEditWindow(rec.data.type, rec.data.realm);
    },
    open_sync_window: function() {
	let me = this;
	let rec = me.getSelection()[0];
	if (!rec) {
	    return;
	}
	Ext.create('PVE.dc.SyncWindow', {
	    realm: rec.data.realm,
	    listeners: {
		destroy: () => me.reload(),
	    },
	}).show();
    },
    initComponent: function() {
	var me = this;
	let items = [];
	for (const [authType, config] of Object.entries(PVE.Utils.authSchema)) {
	    if (!config.add) { continue; }
	    items.push({
		text: config.name,
		iconCls: 'fa fa-fw ' + (config.iconCls || 'fa-address-book-o'),
		handler: () => me.openEditWindow(authType),
	    });
	}
	Ext.apply(me, {
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: {
			items: items,
		    },
		},
		{
		    xtype: 'proxmoxButton',
		    text: gettext('Edit'),
		    disabled: true,
		    handler: () => me.run_editor(),
		},
		{
		    xtype: 'proxmoxStdRemoveButton',
		    baseurl: '/access/domains/',
		    enableFn: (rec) => PVE.Utils.authSchema[rec.data.type].add,
		    callback: () => me.reload(),
		},
		'-',
		{
		    xtype: 'proxmoxButton',
		    text: gettext('Sync'),
		    disabled: true,
		    enableFn: (rec) => Boolean(PVE.Utils.authSchema[rec.data.type].syncipanel),
		    handler: () => me.open_sync_window(),
		},
	    ],
	    listeners: {
		activate: () => me.reload(),
		itemdblclick: () => me.run_editor(),
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.dc.BackupDiskTree', {
    extend: 'Ext.tree.Panel',
    alias: 'widget.pveBackupDiskTree',
    folderSort: true,
    rootVisible: false,
    store: {
	sorters: 'id',
	data: {},
    },
    tools: [
	{
	    type: 'expand',
	    tooltip: gettext('Expand All'),
	    callback: panel => panel.expandAll(),
	},
	{
	    type: 'collapse',
	    tooltip: gettext('Collapse All'),
	    callback: panel => panel.collapseAll(),
	},
    ],
    columns: [
	{
	    xtype: 'treecolumn',
	    text: gettext('Guest Image'),
	    renderer: function(value, meta, record) {
		if (record.data.type) {
		    // guest level
		    let ret = value;
		    if (record.data.name) {
			ret += " (" + record.data.name + ")";
		    }
		    return ret;
		} else {
		    // extJS needs unique IDs but we only want to show the volumes key from "vmid:key"
		    return value.split(':')[1] + " - " + record.data.name;
		}
	    },
	    dataIndex: 'id',
	    flex: 6,
	},
	{
	    text: gettext('Type'),
	    dataIndex: 'type',
	    flex: 1,
	},
	{
	    text: gettext('Backup Job'),
	    renderer: PVE.Utils.render_backup_status,
	    dataIndex: 'included',
	    flex: 3,
	},
    ],
    reload: function() {
	let me = this;
	let sm = me.getSelectionModel();
	Proxmox.Utils.API2Request({
	    url: `/cluster/backup/${me.jobid}/included_volumes`,
	    waitMsgTarget: me,
	    method: 'GET',
	    failure: function(response, opts) {
		Proxmox.Utils.setErrorMask(me, response.htmlStatus);
	    },
	    success: function(response, opts) {
		sm.deselectAll();
		me.setRootNode(response.result.data);
		me.expandAll();
	    },
	});
    },
    initComponent: function() {
	var me = this;
	if (!me.jobid) {
	    throw "no job id specified";
	}
	var sm = Ext.create('Ext.selection.TreeModel', {});
	Ext.apply(me, {
	    selModel: sm,
	    fields: ['id', 'type',
		{
		    type: 'string',
		    name: 'iconCls',
		    calculate: function(data) {
			var txt = 'fa x-fa-tree fa-';
			if (data.leaf && !data.type) {
			    return txt + 'hdd-o';
			} else if (data.type === 'qemu') {
			    return txt + 'desktop';
			} else if (data.type === 'lxc') {
			    return txt + 'cube';
			} else {
			    return txt + 'question-circle';
			}
		    },
		},
	    ],
	    header: {
		items: [{
		    xtype: 'textfield',
		    fieldLabel: gettext('Search'),
		    labelWidth: 50,
		    emptyText: 'Name, VMID, Type',
		    width: 200,
		    padding: '0 5 0 0',
		    enableKeyEvents: true,
		    listeners: {
			buffer: 500,
			keyup: function(field) {
			    let searchValue = field.getValue().toLowerCase();
			    me.store.clearFilter(true);
			    me.store.filterBy(function(record) {
				let data = {};
				if (record.data.depth === 0) {
				    return true;
				} else if (record.data.depth === 1) {
				    data = record.data;
				} else if (record.data.depth === 2) {
				    data = record.parentNode.data;
				}
				for (const property of ['name', 'id', 'type']) {
				    if (!data[property]) {
					continue;
				    }
				    let v = data[property].toString();
				    if (v !== undefined) {
					v = v.toLowerCase();
					if (v.includes(searchValue)) {
					    return true;
					}
				    }
				}
				return false;
			    });
			},
		    },
		}],
	    },
	});
	me.callParent();
	me.reload();
    },
});
Ext.define('PVE.dc.BackupInfo', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveBackupInfo',
    viewModel: {
	data: {
	    retentionType: 'none',
	},
	formulas: {
	    hasRetention: (get) => get('retentionType') !== 'none',
	    retentionKeepAll: (get) => get('retentionType') === 'all',
	},
    },
    padding: '5 0 5 10',
    column1: [
	{
	    xtype: 'displayfield',
	    name: 'node',
	    fieldLabel: gettext('Node'),
	    renderer: value => value || `-- ${gettext('All')} --`,
	},
	{
	    xtype: 'displayfield',
	    name: 'storage',
	    fieldLabel: gettext('Storage'),
	},
	{
	    xtype: 'displayfield',
	    name: 'schedule',
	    fieldLabel: gettext('Schedule'),
	},
	{
	    xtype: 'displayfield',
	    name: 'next-run',
	    fieldLabel: gettext('Next Run'),
	    renderer: PVE.Utils.render_next_event,
	},
	{
	    xtype: 'displayfield',
	    name: 'selMode',
	    fieldLabel: gettext('Selection mode'),
	},
    ],
    column2: [
	{
	    xtype: 'displayfield',
	    name: 'mailnotification',
	    fieldLabel: gettext('Notification'),
	    renderer: function(value) {
		let mailto = this.up('pveBackupInfo')?.record?.mailto || 'root@localhost';
		let when = gettext('Always');
		if (value === 'failure') {
		    when = gettext('On failure only');
		}
		return `${when} (${mailto})`;
	    },
	},
	{
	    xtype: 'displayfield',
	    name: 'compress',
	    fieldLabel: gettext('Compression'),
	},
	{
	    xtype: 'displayfield',
	    name: 'mode',
	    fieldLabel: gettext('Mode'),
	    renderer: function(value) {
		const modeToDisplay = {
		    snapshot: gettext('Snapshot'),
		    stop: gettext('Stop'),
		    suspend: gettext('Snapshot'),
		};
		return modeToDisplay[value] ?? gettext('Unknown');
	    },
	},
	{
	    xtype: 'displayfield',
	    name: 'enabled',
	    fieldLabel: gettext('Enabled'),
	    renderer: v => PVE.Parser.parseBoolean(v.toString()) ? gettext('Yes') : gettext('No'),
	},
	{
	    xtype: 'displayfield',
	    name: 'pool',
	    fieldLabel: gettext('Pool to backup'),
	},
    ],
    columnB: [
	{
	    xtype: 'displayfield',
	    name: 'comment',
	    fieldLabel: gettext('Comment'),
	},
	{
	    xtype: 'fieldset',
	    title: gettext('Retention Configuration'),
	    layout: 'hbox',
	    collapsible: true,
	    defaults: {
		border: false,
		layout: 'anchor',
		flex: 1,
	    },
	    bind: {
		hidden: '{!hasRetention}',
	    },
	    items: [
		{
		    padding: '0 10 0 0',
		    defaults: {
			labelWidth: 110,
		    },
		    items: [{
			xtype: 'displayfield',
			name: 'keep-all',
			fieldLabel: gettext('Keep All'),
			renderer: Proxmox.Utils.format_boolean,
			bind: {
			    hidden: '{!retentionKeepAll}',
			},
		    }].concat(
			[
			    ['keep-last', gettext('Keep Last')],
			    ['keep-hourly', gettext('Keep Hourly')],
			].map(
			    name => ({
				xtype: 'displayfield',
				name: name[0],
				fieldLabel: name[1],
				bind: {
				    hidden: '{!hasRetention || retentionKeepAll}',
				},
			    }),
			),
		    ),
		},
		{
		    padding: '0 0 0 10',
		    defaults: {
			labelWidth: 110,
		    },
		    items: [
			['keep-daily', gettext('Keep Daily')],
			['keep-weekly', gettext('Keep Weekly')],
		    ].map(
			name => ({
			    xtype: 'displayfield',
			    name: name[0],
			    fieldLabel: name[1],
			    bind: {
				hidden: '{!hasRetention || retentionKeepAll}',
			    },
			}),
		    ),
		},
		{
		    padding: '0 0 0 10',
		    defaults: {
			labelWidth: 110,
		    },
		    items: [
			['keep-monthly', gettext('Keep Monthly')],
			['keep-yearly', gettext('Keep Yearly')],
		    ].map(
			name => ({
			    xtype: 'displayfield',
			    name: name[0],
			    fieldLabel: name[1],
			    bind: {
				hidden: '{!hasRetention || retentionKeepAll}',
			    },
			}),
		    ),
		},
	    ],
	},
    ],
    setValues: function(values) {
	var me = this;
	let vm = me.getViewModel();
        Ext.iterate(values, function(fieldId, val) {
	    let field = me.query('[isFormField][name=' + fieldId + ']')[0];
	    if (field) {
		field.setValue(val);
            }
	});
	if (values['prune-backups'] || values.maxfiles !== undefined) {
	    let keepValues;
	    if (values['prune-backups']) {
		keepValues = values['prune-backups'];
	    } else if (values.maxfiles > 0) {
		keepValues = { 'keep-last': values.maxfiles };
	    } else {
		keepValues = { 'keep-all': 1 };
	    }
	    vm.set('retentionType', keepValues['keep-all'] ? 'all' : 'other');
	    // set values of all keep-X fields
	    ['all', 'last', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'].forEach(time => {
		let name = `keep-${time}`;
		me.query(`[isFormField][name=${name}]`)[0]?.setValue(keepValues[name]);
	    });
	} else {
	    vm.set('retentionType', 'none');
	}
	// selection Mode depends on the presence/absence of several keys
	let selModeField = me.query('[isFormField][name=selMode]')[0];
	let selMode = 'none';
	if (values.vmid) {
	    selMode = gettext('Include selected VMs');
	}
	if (values.all) {
	    selMode = gettext('All');
	}
	if (values.exclude) {
	     selMode = gettext('Exclude selected VMs');
	}
	if (values.pool) {
	    selMode = gettext('Pool based');
	}
	selModeField.setValue(selMode);
	if (!values.pool) {
	    let poolField = me.query('[isFormField][name=pool]')[0];
	    poolField.setVisible(0);
	}
    },
    initComponent: function() {
	var me = this;
	if (!me.record) {
	    throw "no data provided";
	}
	me.callParent();
	me.setValues(me.record);
    },
});
Ext.define('PVE.dc.BackedGuests', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveBackedGuests',
    stateful: true,
    stateId: 'grid-dc-backed-guests',
    textfilter: '',
    columns: [
	{
	    header: gettext('Type'),
	    dataIndex: "type",
	    renderer: PVE.Utils.render_resource_type,
	    flex: 1,
	    sortable: true,
	},
	{
	    header: gettext('VMID'),
	    dataIndex: 'vmid',
	    flex: 1,
	    sortable: true,
	},
	{
	    header: gettext('Name'),
	    dataIndex: 'name',
	    flex: 2,
	    sortable: true,
	},
    ],
    viewConfig: {
	stripeRows: true,
	trackOver: false,
    },
    initComponent: function() {
	let me = this;
	me.store.clearFilter(true);
	Ext.apply(me, {
	    tbar: [
	        '->',
		gettext('Search') + ':',
		' ',
		{
		    xtype: 'textfield',
		    width: 200,
		    emptyText: 'Name, VMID, Type',
		    enableKeyEvents: true,
		    listeners: {
			buffer: 500,
			keyup: function(field) {
			    let searchValue = field.getValue().toLowerCase();
			    me.store.clearFilter(true);
			    me.store.filterBy(function(record) {
				let data = record.data;
				for (const property of ['name', 'vmid', 'type']) {
				    if (data[property] === null) {
					continue;
				    }
				    let v = data[property].toString();
				    if (v !== undefined) {
					if (v.toLowerCase().includes(searchValue)) {
					    return true;
					}
				    }
				}
				return false;
			    });
			},
		    },
		},
	    ],
	});
	me.callParent();
    },
});
Ext.define('PVE.dc.BackupEdit', {
    extend: 'Proxmox.window.Edit',
    alias: ['widget.pveDcBackupEdit'],
    mixins: ['Proxmox.Mixin.CBind'],
    defaultFocus: undefined,
    subject: gettext("Backup Job"),
    bodyPadding: 0,
    url: '/api2/extjs/cluster/backup',
    method: 'POST',
    isCreate: true,
    cbindData: function() {
	let me = this;
	if (me.jobid) {
	    me.isCreate = false;
	    me.method = 'PUT';
	    me.url += `/${me.jobid}`;
	}
	return {};
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	onGetValues: function(values) {
	    let me = this;
	    let isCreate = me.getView().isCreate;
	    if (!values.node) {
		if (!isCreate) {
		    Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
		}
		delete values.node;
	    }
	    if (!values.id && isCreate) {
		values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
	    }
	    let selMode = values.selMode;
	    delete values.selMode;
	    if (selMode === 'all') {
		values.all = 1;
		values.exclude = '';
		delete values.vmid;
	    } else if (selMode === 'exclude') {
		values.all = 1;
		values.exclude = values.vmid;
		delete values.vmid;
	    } else if (selMode === 'pool') {
		delete values.vmid;
	    }
	    if (selMode !== 'pool') {
		delete values.pool;
	    }
	    return values;
	},
	nodeChange: function(f, value) {
	    let me = this;
	    me.lookup('storageSelector').setNodename(value);
	    let vmgrid = me.lookup('vmgrid');
	    let store = vmgrid.getStore();
	    store.clearFilter();
	    store.filterBy(function(rec) {
		return !value || rec.get('node') === value;
	    });
	    let mode = me.lookup('modeSelector').getValue();
	    if (mode === 'all') {
		vmgrid.selModel.selectAll(true);
	    }
	    if (mode === 'pool') {
		me.selectPoolMembers();
	    }
	},
	storageChange: function(f, v) {
	    let me = this;
	    let rec = f.getStore().findRecord('storage', v, 0, false, true, true);
	    let compressionSelector = me.lookup('compressionSelector');
	    if (rec?.data?.type === 'pbs') {
		compressionSelector.setValue('zstd');
		compressionSelector.setDisabled(true);
	    } else if (!compressionSelector.getEditable()) {
		compressionSelector.setDisabled(false);
	    }
	},
	selectPoolMembers: function() {
	    let me = this;
	    let vmgrid = me.lookup('vmgrid');
	    let poolid = me.lookup('poolSelector').getValue();
	    vmgrid.getSelectionModel().deselectAll(true);
	    if (!poolid) {
		return;
	    }
	    vmgrid.getStore().filter([
		{
		    id: 'poolFilter',
		    property: 'pool',
		    value: poolid,
		},
	    ]);
	    vmgrid.selModel.selectAll(true);
	},
	modeChange: function(f, value, oldValue) {
	    let me = this;
	    let vmgrid = me.lookup('vmgrid');
	    vmgrid.getStore().removeFilter('poolFilter');
	    if (oldValue === 'all' && value !== 'all') {
		vmgrid.getSelectionModel().deselectAll(true);
	    }
	    if (value === 'all') {
		vmgrid.getSelectionModel().selectAll(true);
	    }
	    if (value === 'pool') {
		me.selectPoolMembers();
	    }
	},
	init: function(view) {
	    let me = this;
	    if (view.isCreate) {
		me.lookup('modeSelector').setValue('include');
	    } else {
		view.load({
		    success: function(response, _options) {
			let data = response.result.data;
			if (data.exclude) {
			    data.vmid = data.exclude;
			    data.selMode = 'exclude';
			} else if (data.all) {
			    data.vmid = '';
			    data.selMode = 'all';
			} else if (data.pool) {
			    data.selMode = 'pool';
			    data.selPool = data.pool;
			} else {
			    data.selMode = 'include';
			}
			me.getViewModel().set('selMode', data.selMode);
			if (data['prune-backups']) {
			    Object.assign(data, data['prune-backups']);
			    delete data['prune-backups'];
			} else if (data.maxfiles !== undefined) {
			    if (data.maxfiles > 0) {
				data['keep-last'] = data.maxfiles;
			    } else {
				data['keep-all'] = 1;
			    }
			    delete data.maxfiles;
			}
			if (data['notes-template']) {
			    data['notes-template'] =
				PVE.Utils.unEscapeNotesTemplate(data['notes-template']);
			}
			view.setValues(data);
		    },
		});
	    }
	},
    },
    viewModel: {
	data: {
	    selMode: 'include',
	},
	formulas: {
	    poolMode: (get) => get('selMode') === 'pool',
	    disableVMSelection: (get) => get('selMode') !== 'include' && get('selMode') !== 'exclude',
	},
    },
    items: [
	{
	    xtype: 'tabpanel',
	    region: 'center',
	    layout: 'fit',
	    bodyPadding: 10,
	    items: [
		{
		    xtype: 'container',
		    title: gettext('General'),
		    region: 'center',
		    layout: {
			type: 'vbox',
			align: 'stretch',
		    },
		    items: [
			{
			    xtype: 'inputpanel',
			    onlineHelp: 'chapter_vzdump',
			    column1: [
				{
				    xtype: 'pveNodeSelector',
				    name: 'node',
				    fieldLabel: gettext('Node'),
				    allowBlank: true,
				    editable: true,
				    autoSelect: false,
				    emptyText: '-- ' + gettext('All') + ' --',
				    listeners: {
					change: 'nodeChange',
				    },
				},
				{
				    xtype: 'pveStorageSelector',
				    reference: 'storageSelector',
				    fieldLabel: gettext('Storage'),
				    clusterView: true,
				    storageContent: 'backup',
				    allowBlank: false,
				    name: 'storage',
				    listeners: {
					change: 'storageChange',
				    },
				},
				{
				    xtype: 'pveCalendarEvent',
				    fieldLabel: gettext('Schedule'),
				    allowBlank: false,
				    name: 'schedule',
				},
				{
				    xtype: 'proxmoxKVComboBox',
				    reference: 'modeSelector',
				    comboItems: [
					['include', gettext('Include selected VMs')],
					['all', gettext('All')],
					['exclude', gettext('Exclude selected VMs')],
					['pool', gettext('Pool based')],
				    ],
				    fieldLabel: gettext('Selection mode'),
				    name: 'selMode',
				    value: '',
				    bind: {
					value: '{selMode}',
				    },
				    listeners: {
					change: 'modeChange',
				    },
				},
				{
				    xtype: 'pvePoolSelector',
				    reference: 'poolSelector',
				    fieldLabel: gettext('Pool to backup'),
				    hidden: true,
				    allowBlank: false,
				    name: 'pool',
				    listeners: {
					change: 'selectPoolMembers',
				    },
				    bind: {
					hidden: '{!poolMode}',
					disabled: '{!poolMode}',
				    },
				},
			    ],
			    column2: [
				{
				    xtype: 'textfield',
				    fieldLabel: gettext('Send email to'),
				    name: 'mailto',
				},
				{
				    xtype: 'pveEmailNotificationSelector',
				    fieldLabel: gettext('Email'),
				    name: 'mailnotification',
				    cbind: {
					value: (get) => get('isCreate') ? 'always' : '',
					deleteEmpty: '{!isCreate}',
				    },
				},
				{
				    xtype: 'pveCompressionSelector',
				    reference: 'compressionSelector',
				    fieldLabel: gettext('Compression'),
				    name: 'compress',
				    cbind: {
					deleteEmpty: '{!isCreate}',
				    },
				    value: 'zstd',
				},
				{
				    xtype: 'pveBackupModeSelector',
				    fieldLabel: gettext('Mode'),
				    value: 'snapshot',
				    name: 'mode',
				},
				{
				    xtype: 'proxmoxcheckbox',
				    fieldLabel: gettext('Enable'),
				    name: 'enabled',
				    uncheckedValue: 0,
				    defaultValue: 1,
				    checked: true,
				},
			    ],
			    columnB: [
				{
				    xtype: 'proxmoxtextfield',
				    name: 'comment',
				    fieldLabel: gettext('Job Comment'),
				    cbind: {
					deleteEmpty: '{!isCreate}',
				    },
				    autoEl: {
					tag: 'div',
					'data-qtip': gettext('Description of the job'),
				    },
				},
				{
				    xtype: 'vmselector',
				    reference: 'vmgrid',
				    height: 300,
				    name: 'vmid',
				    disabled: true,
				    allowBlank: false,
				    columnSelection: ['vmid', 'node', 'status', 'name', 'type'],
				    bind: {
					disabled: '{disableVMSelection}',
				    },
				},
			    ],
			    advancedColumn1: [
				{
				    xtype: 'proxmoxcheckbox',
				    fieldLabel: gettext('Repeat missed'),
				    name: 'repeat-missed',
				    uncheckedValue: 0,
				    defaultValue: 0,
				    cbind: {
					deleteDefaultValue: '{!isCreate}',
				    },
				},
			    ],
			    onGetValues: function(values) {
				return this.up('window').getController().onGetValues(values);
			    },
			},
		    ],
		},
		{
		    xtype: 'pveBackupJobPrunePanel',
		    title: gettext('Retention'),
		    cbind: {
			isCreate: '{isCreate}',
		    },
		    keepAllDefaultForCreate: false,
		    showPBSHint: false,
		    fallbackHintHtml: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
		},
		{
		    xtype: 'inputpanel',
		    title: gettext('Note Template'),
		    region: 'center',
		    layout: {
			type: 'vbox',
			align: 'stretch',
		    },
		    onGetValues: function(values) {
			if (values['notes-template']) {
			    values['notes-template'] =
				PVE.Utils.escapeNotesTemplate(values['notes-template']);
			}
			return values;
		    },
		    items: [
			{
			    xtype: 'textarea',
			    name: 'notes-template',
			    fieldLabel: gettext('Backup Notes'),
			    height: 100,
			    maxLength: 512,
			    cbind: {
				deleteEmpty: '{!isCreate}',
				value: (get) => get('isCreate') ? '{{guestname}}' : undefined,
			    },
			},
			{
			    xtype: 'box',
			    style: {
				margin: '8px 0px',
				'line-height': '1.5em',
			    },
			    html: gettext('The notes are added to each backup created by this job.')
				+ '
'
				+ Ext.String.format(
				    gettext('Possible template variables are: {0}'),
				    PVE.Utils.notesTemplateVars.map(v => `{{${v}}}`).join(', '),
				),
			},
		    ],
		},
	    ],
	},
    ],
});
Ext.define('PVE.dc.BackupView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveDcBackupView'],
    onlineHelp: 'chapter_vzdump',
    allText: '-- ' + gettext('All') + ' --',
    initComponent: function() {
	let me = this;
	let store = new Ext.data.Store({
	    model: 'pve-cluster-backup',
	    proxy: {
		type: 'proxmox',
		url: "/api2/json/cluster/backup",
	    },
	});
	let not_backed_store = new Ext.data.Store({
	    sorters: 'vmid',
	    proxy: {
		type: 'proxmox',
		url: 'api2/json/cluster/backup-info/not-backed-up',
	    },
	});
	let noBackupJobInfoButton;
	let reload = function() {
	    store.load();
	    not_backed_store.load({
		callback: records => noBackupJobInfoButton.setVisible(records.length > 0),
	    });
	};
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    let win = Ext.create('PVE.dc.BackupEdit', {
		jobid: rec.data.id,
	    });
	    win.on('destroy', reload);
	    win.show();
	};
	let run_detail = function() {
	    let record = sm.getSelection()[0];
	    if (!record) {
		return;
	    }
	    Ext.create('Ext.window.Window', {
		modal: true,
		width: 800,
		height: Ext.getBody().getViewSize().height > 1000 ? 800 : 600, // factor out as common infra?
		resizable: true,
		layout: 'fit',
		title: gettext('Backup Details'),
		items: [
		    {
			xtype: 'panel',
			region: 'center',
			layout: {
			    type: 'vbox',
			    align: 'stretch',
			},
			items: [
			    {
				xtype: 'pveBackupInfo',
				flex: 0,
				layout: 'fit',
				record: record.data,
			    },
			    {
				xtype: 'pveBackupDiskTree',
				title: gettext('Included disks'),
				flex: 1,
				jobid: record.data.id,
			    },
			],
		    },
		],
	    }).show();
	};
	let run_backup_now = function(job) {
	    job = Ext.clone(job);
	    let jobNode = job.node;
	    // Remove properties related to scheduling
	    delete job.enabled;
	    delete job.starttime;
	    delete job.dow;
	    delete job.id;
	    delete job.schedule;
	    delete job.type;
	    delete job.node;
	    delete job.comment;
	    delete job['next-run'];
	    delete job['repeat-missed'];
	    job.all = job.all === true ? 1 : 0;
	    ['performance', 'prune-backups'].forEach(key => {
		if (job[key]) {
		    job[key] = PVE.Parser.printPropertyString(job[key]);
		}
	    });
	    let allNodes = PVE.data.ResourceStore.getNodes();
	    let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node);
	    let errors = [];
	    if (jobNode !== undefined) {
		if (!nodes.includes(jobNode)) {
		    Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
		    return;
		}
		nodes = [jobNode];
	    } else {
		let unkownNodes = allNodes.filter(node => node.status !== 'online');
		if (unkownNodes.length > 0) {errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));}
	    }
	    let jobTotalCount = nodes.length, jobsStarted = 0;
	    Ext.Msg.show({
		title: gettext('Please wait...'),
		closable: false,
		progress: true,
		progressText: '0/' + jobTotalCount,
	    });
	    let postRequest = function() {
		jobsStarted++;
		Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount);
		if (jobsStarted === jobTotalCount) {
		    Ext.Msg.hide();
		    if (errors.length > 0) {
			Ext.Msg.alert('Error', 'Some errors have been encountered:
' + errors.join('
'));
		    }
		}
	    };
	    nodes.forEach(node => Proxmox.Utils.API2Request({
		url: '/nodes/' + node + '/vzdump',
		method: 'POST',
		params: job,
		failure: function(response, opts) {
		    errors.push(node + ': ' + response.htmlStatus);
		    postRequest();
		},
		success: postRequest,
	    }));
	};
	var edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	var run_btn = new Proxmox.button.Button({
	    text: gettext('Run now'),
	    disabled: true,
	    selModel: sm,
	    handler: function() {
		var rec = sm.getSelection()[0];
		if (!rec) {
		    return;
		}
		Ext.Msg.show({
		    title: gettext('Confirm'),
		    icon: Ext.Msg.QUESTION,
		    msg: gettext('Start the selected backup job now?'),
		    buttons: Ext.Msg.YESNO,
		    callback: function(btn) {
			if (btn !== 'yes') {
			    return;
			}
			run_backup_now(rec.data);
		    },
		});
	    },
	});
	var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/cluster/backup',
	    callback: function() {
		reload();
	    },
	});
	var detail_btn = new Proxmox.button.Button({
	    text: gettext('Job Detail'),
	    disabled: true,
	    tooltip: gettext('Show job details and which guests and volumes are affected by the backup job'),
	    selModel: sm,
	    handler: run_detail,
	});
	noBackupJobInfoButton = new Proxmox.button.Button({
	    text: `${gettext('Show')}: ${gettext('Guests Without Backup Job')}`,
	    tooltip: gettext('Some guests are not covered by any backup job.'),
	    iconCls: 'fa fa-fw fa-exclamation-circle',
	    hidden: true,
	    handler: () => {
		Ext.create('Ext.window.Window', {
		    autoShow: true,
		    modal: true,
		    width: 600,
		    height: 500,
		    resizable: true,
		    layout: 'fit',
		    title: gettext('Guests Without Backup Job'),
		    items: [
			{
			    xtype: 'panel',
			    region: 'center',
			    layout: {
				type: 'vbox',
				align: 'stretch',
			    },
			    items: [
				{
				    xtype: 'pveBackedGuests',
				    flex: 1,
				    layout: 'fit',
				    store: not_backed_store,
				},
			    ],
			},
		    ],
		});
	    },
	});
	Proxmox.Utils.monStoreErrors(me, store);
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    stateful: true,
	    stateId: 'grid-dc-backup',
	    viewConfig: {
		trackOver: false,
	    },
	    dockedItems: [{
		xtype: 'toolbar',
		overflowHandler: 'scroller',
		dock: 'top',
		items: [
		    {
			text: gettext('Add'),
			handler: function() {
			    var win = Ext.create('PVE.dc.BackupEdit', {});
			    win.on('destroy', reload);
			    win.show();
			},
		    },
		    '-',
		    remove_btn,
		    edit_btn,
		    detail_btn,
		    '-',
		    run_btn,
		    '->',
		    noBackupJobInfoButton,
		    '-',
		    {
			xtype: 'proxmoxButton',
			selModel: null,
			text: gettext('Schedule Simulator'),
			handler: () => {
			    let record = sm.getSelection()[0];
			    let schedule;
			    if (record) {
				schedule = record.data.schedule;
			    }
			    Ext.create('PVE.window.ScheduleSimulator', {
				autoShow: true,
				schedule,
			    });
			},
		    },
		],
	    }],
	    columns: [
		{
		    header: gettext('Enabled'),
		    width: 80,
		    dataIndex: 'enabled',
		    align: 'center',
		    // TODO: switch to Proxmox.Utils.renderEnabledIcon once available
		    renderer: enabled => ``,
		    sortable: true,
		},
		{
		    header: gettext('ID'),
		    dataIndex: 'id',
		    hidden: true,
		},
		{
		    header: gettext('Node'),
		    width: 100,
		    sortable: true,
		    dataIndex: 'node',
		    renderer: function(value) {
			if (value) {
			    return value;
			}
			return me.allText;
		    },
		},
		{
		    header: gettext('Schedule'),
		    width: 150,
		    dataIndex: 'schedule',
		},
		{
		    text: gettext('Next Run'),
		    dataIndex: 'next-run',
		    width: 150,
		    renderer: PVE.Utils.render_next_event,
		},
		{
		    header: gettext('Storage'),
		    width: 100,
		    sortable: true,
		    dataIndex: 'storage',
		},
		{
		    header: gettext('Comment'),
		    dataIndex: 'comment',
		    renderer: Ext.htmlEncode,
		    sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''),
		    flex: 1,
		},
		{
		    header: gettext('Retention'),
		    dataIndex: 'prune-backups',
		    renderer: v => v ? PVE.Parser.printPropertyString(v) : gettext('Fallback from storage config'),
		    flex: 2,
		},
		{
		    header: gettext('Selection'),
		    flex: 4,
		    sortable: false,
		    dataIndex: 'vmid',
		    renderer: PVE.Utils.render_backup_selection,
		},
	    ],
	    listeners: {
		activate: reload,
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
    },
}, function() {
    Ext.define('pve-cluster-backup', {
	extend: 'Ext.data.Model',
	fields: [
	    'id',
	    'compress',
	    'dow',
	    'exclude',
	    'mailto',
	    'mode',
	    'node',
	    'pool',
	    'prune-backups',
	    'starttime',
	    'storage',
	    'vmid',
	    { name: 'enabled', type: 'boolean' },
	    { name: 'all', type: 'boolean' },
	],
    });
});
Ext.define('pve-cluster-nodes', {
    extend: 'Ext.data.Model',
    fields: [
	'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr',
	{ type: 'integer', name: 'quorum_votes' },
    ],
    proxy: {
        type: 'proxmox',
	url: "/api2/json/cluster/config/nodes",
    },
    idProperty: 'nodeid',
});
Ext.define('pve-cluster-info', {
    extend: 'Ext.data.Model',
    proxy: {
        type: 'proxmox',
	url: "/api2/json/cluster/config/join",
    },
});
Ext.define('PVE.ClusterAdministration', {
    extend: 'Ext.panel.Panel',
    xtype: 'pveClusterAdministration',
    title: gettext('Cluster Administration'),
    onlineHelp: 'chapter_pvecm',
    border: false,
    defaults: { border: false },
    viewModel: {
	parent: null,
	data: {
	    totem: {},
	    nodelist: [],
	    preferred_node: {
		name: '',
		fp: '',
		addr: '',
	    },
	    isInCluster: false,
	    nodecount: 0,
	},
    },
    items: [
	{
	    xtype: 'panel',
	    title: gettext('Cluster Information'),
	    controller: {
		xclass: 'Ext.app.ViewController',
		init: function(view) {
		    view.store = Ext.create('Proxmox.data.UpdateStore', {
			autoStart: true,
			interval: 15 * 1000,
			storeid: 'pve-cluster-info',
			model: 'pve-cluster-info',
		    });
		    view.store.on('load', this.onLoad, this);
		    view.on('destroy', view.store.stopUpdate);
		},
		onLoad: function(store, records, success, operation) {
		    let vm = this.getViewModel();
		    let data = records?.[0]?.data;
		    if (!success || !data || !data.nodelist?.length) {
			let error = operation.getError();
			if (error) {
			    let msg = Proxmox.Utils.getResponseErrorMessage(error);
			    if (error.status !== 424 && !msg.match(/node is not in a cluster/i)) {
				// an actual error, not just the "not in a cluster one", so show it!
				Proxmox.Utils.setErrorMask(this.getView(), msg);
			    }
			}
			vm.set('totem', {});
			vm.set('isInCluster', false);
			vm.set('nodelist', []);
			vm.set('preferred_node', {
			    name: '',
			    addr: '',
			    fp: '',
			});
			return;
		    }
		    vm.set('totem', data.totem);
		    vm.set('isInCluster', !!data.totem.cluster_name);
		    vm.set('nodelist', data.nodelist);
		    let nodeinfo = data.nodelist.find(el => el.name === data.preferred_node);
		    let links = {};
		    let ring_addr = [];
		    PVE.Utils.forEachCorosyncLink(nodeinfo, (num, link) => {
			links[num] = link;
			ring_addr.push(link);
		    });
		    vm.set('preferred_node', {
			name: data.preferred_node,
			addr: nodeinfo.pve_addr,
			peerLinks: links,
			ring_addr: ring_addr,
			fp: nodeinfo.pve_fp,
		    });
		},
		onCreate: function() {
		    let view = this.getView();
		    view.store.stopUpdate();
		    Ext.create('PVE.ClusterCreateWindow', {
			autoShow: true,
			listeners: {
			    destroy: function() {
				view.store.startUpdate();
			    },
			},
		    });
		},
		onClusterInfo: function() {
		    let vm = this.getViewModel();
		    Ext.create('PVE.ClusterInfoWindow', {
			autoShow: true,
			joinInfo: {
			    ipAddress: vm.get('preferred_node.addr'),
			    fingerprint: vm.get('preferred_node.fp'),
			    peerLinks: vm.get('preferred_node.peerLinks'),
			    ring_addr: vm.get('preferred_node.ring_addr'),
			    totem: vm.get('totem'),
			},
		    });
		},
		onJoin: function() {
		    let view = this.getView();
		    view.store.stopUpdate();
		    Ext.create('PVE.ClusterJoinNodeWindow', {
			autoShow: true,
			listeners: {
			    destroy: function() {
				view.store.startUpdate();
			    },
			},
		    });
		},
	    },
	    tbar: [
		{
		    text: gettext('Create Cluster'),
		    reference: 'createButton',
		    handler: 'onCreate',
		    bind: {
			disabled: '{isInCluster}',
		    },
		},
		{
		    text: gettext('Join Information'),
		    reference: 'addButton',
		    handler: 'onClusterInfo',
		    bind: {
			disabled: '{!isInCluster}',
		    },
		},
		{
		    text: gettext('Join Cluster'),
		    reference: 'joinButton',
		    handler: 'onJoin',
		    bind: {
			disabled: '{isInCluster}',
		    },
		},
	    ],
	    layout: 'hbox',
	    bodyPadding: 5,
	    items: [
		{
		    xtype: 'displayfield',
		    fieldLabel: gettext('Cluster Name'),
		    bind: {
			value: '{totem.cluster_name}',
			hidden: '{!isInCluster}',
		    },
		    flex: 1,
		},
		{
		    xtype: 'displayfield',
		    fieldLabel: gettext('Config Version'),
		    bind: {
			value: '{totem.config_version}',
			hidden: '{!isInCluster}',
		    },
		    flex: 1,
		},
		{
		    xtype: 'displayfield',
		    fieldLabel: gettext('Number of Nodes'),
		    labelWidth: 120,
		    bind: {
			value: '{nodecount}',
			hidden: '{!isInCluster}',
		    },
		    flex: 1,
		},
		{
		    xtype: 'displayfield',
		    value: gettext('Standalone node - no cluster defined'),
		    bind: {
			hidden: '{isInCluster}',
		    },
		    flex: 1,
		},
	    ],
	},
	{
	    xtype: 'grid',
	    title: gettext('Cluster Nodes'),
	    autoScroll: true,
	    enableColumnHide: false,
	    controller: {
		xclass: 'Ext.app.ViewController',
		init: function(view) {
		    view.rstore = Ext.create('Proxmox.data.UpdateStore', {
			autoLoad: true,
			xtype: 'update',
			interval: 5 * 1000,
			autoStart: true,
			storeid: 'pve-cluster-nodes',
			model: 'pve-cluster-nodes',
		    });
		    view.setStore(Ext.create('Proxmox.data.DiffStore', {
			rstore: view.rstore,
			sorters: {
			    property: 'nodeid',
			    direction: 'ASC',
			},
		    }));
		    Proxmox.Utils.monStoreErrors(view, view.rstore);
		    view.rstore.on('load', this.onLoad, this);
		    view.on('destroy', view.rstore.stopUpdate);
		},
		onLoad: function(store, records, success) {
		    let view = this.getView();
		    let vm = this.getViewModel();
		    if (!success || !records || !records.length) {
			vm.set('nodecount', 0);
			return;
		    }
		    vm.set('nodecount', records.length);
		    // show/hide columns according to used links
		    let linkIndex = view.columns.length;
		    Ext.each(view.columns, (col, i) => {
			if (col.linkNumber !== undefined) {
			    col.setHidden(true);
			    // save offset at which link columns start, so we can address them directly below
			    if (i < linkIndex) {
				linkIndex = i;
			    }
			}
		    });
		    PVE.Utils.forEachCorosyncLink(records[0].data,
			(linknum, val) => {
			    if (linknum > 7) {
				return;
			    }
			    view.columns[linkIndex + linknum].setHidden(false);
			},
		    );
		},
	    },
	    columns: {
		items: [
		    {
			header: gettext('Nodename'),
			hidden: false,
			dataIndex: 'name',
		    },
		    {
			header: gettext('ID'),
			minWidth: 100,
			width: 100,
			flex: 0,
			hidden: false,
			dataIndex: 'nodeid',
		    },
		    {
			header: gettext('Votes'),
			minWidth: 100,
			width: 100,
			flex: 0,
			hidden: false,
			dataIndex: 'quorum_votes',
		    },
		    {
			header: Ext.String.format(gettext('Link {0}'), 0),
			dataIndex: 'ring0_addr',
			linkNumber: 0,
		    },
		    {
			header: Ext.String.format(gettext('Link {0}'), 1),
			dataIndex: 'ring1_addr',
			linkNumber: 1,
		    },
		    {
			header: Ext.String.format(gettext('Link {0}'), 2),
			dataIndex: 'ring2_addr',
			linkNumber: 2,
		    },
		    {
			header: Ext.String.format(gettext('Link {0}'), 3),
			dataIndex: 'ring3_addr',
			linkNumber: 3,
		    },
		    {
			header: Ext.String.format(gettext('Link {0}'), 4),
			dataIndex: 'ring4_addr',
			linkNumber: 4,
		    },
		    {
			header: Ext.String.format(gettext('Link {0}'), 5),
			dataIndex: 'ring5_addr',
			linkNumber: 5,
		    },
		    {
			header: Ext.String.format(gettext('Link {0}'), 6),
			dataIndex: 'ring6_addr',
			linkNumber: 6,
		    },
		    {
			header: Ext.String.format(gettext('Link {0}'), 7),
			dataIndex: 'ring7_addr',
			linkNumber: 7,
		    },
		],
		defaults: {
		    flex: 1,
		    hidden: true,
		    minWidth: 150,
		},
	    },
	},
    ],
});
Ext.define('PVE.ClusterCreateWindow', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveClusterCreateWindow',
    title: gettext('Create Cluster'),
    width: 600,
    method: 'POST',
    url: '/cluster/config',
    isCreate: true,
    subject: gettext('Cluster'),
    showTaskViewer: true,
    onlineHelp: 'pvecm_create_cluster',
    items: {
	xtype: 'inputpanel',
	items: [{
	    xtype: 'textfield',
	    fieldLabel: gettext('Cluster Name'),
	    allowBlank: false,
	    maxLength: 15,
	    name: 'clustername',
	},
	{
	    xtype: 'fieldcontainer',
	    fieldLabel: gettext("Cluster Network"),
	    items: [
		{
		    xtype: 'pveCorosyncLinkEditor',
		    infoText: gettext("Multiple links are used as failover, lower numbers have higher priority."),
		    name: 'links',
		},
	    ],
	}],
    },
});
Ext.define('PVE.ClusterInfoWindow', {
    extend: 'Ext.window.Window',
    xtype: 'pveClusterInfoWindow',
    mixins: ['Proxmox.Mixin.CBind'],
    width: 800,
    modal: true,
    resizable: false,
    title: gettext('Cluster Join Information'),
    joinInfo: {
	ipAddress: undefined,
	fingerprint: undefined,
	totem: {},
    },
    items: [
	{
	    xtype: 'component',
	    border: false,
	    padding: '10 10 10 10',
	    html: gettext("Copy the Join Information here and use it on the node you want to add."),
	},
	{
	    xtype: 'container',
	    layout: 'form',
	    border: false,
	    padding: '0 10 10 10',
	    items: [
		{
		    xtype: 'textfield',
		    fieldLabel: gettext('IP Address'),
		    cbind: {
			value: '{joinInfo.ipAddress}',
		    },
		    editable: false,
		},
		{
		    xtype: 'textfield',
		    fieldLabel: gettext('Fingerprint'),
		    cbind: {
			value: '{joinInfo.fingerprint}',
		    },
		    editable: false,
		},
		{
		    xtype: 'textarea',
		    inputId: 'pveSerializedClusterInfo',
		    fieldLabel: gettext('Join Information'),
		    grow: true,
		    cbind: {
			joinInfo: '{joinInfo}',
		    },
		    editable: false,
		    listeners: {
			afterrender: function(field) {
			    if (!field.joinInfo) {
				return;
			    }
			    var jsons = Ext.JSON.encode(field.joinInfo);
			    var base64s = Ext.util.Base64.encode(jsons);
			    field.setValue(base64s);
			},
		    },
		},
	    ],
	},
    ],
    dockedItems: [{
	dock: 'bottom',
	xtype: 'toolbar',
	items: [{
	    xtype: 'button',
	    handler: function(b) {
		var el = document.getElementById('pveSerializedClusterInfo');
		el.select();
		document.execCommand("copy");
	    },
	    text: gettext('Copy Information'),
	}],
    }],
});
Ext.define('PVE.ClusterJoinNodeWindow', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveClusterJoinNodeWindow',
    title: gettext('Cluster Join'),
    width: 800,
    method: 'POST',
    url: '/cluster/config/join',
    defaultFocus: 'textarea[name=serializedinfo]',
    isCreate: true,
    bind: {
	submitText: '{submittxt}',
    },
    showTaskViewer: true,
    onlineHelp: 'pvecm_join_node_to_cluster',
    viewModel: {
	parent: null,
	data: {
	    info: {
		fp: '',
		ip: '',
		clusterName: '',
	    },
	    hasAssistedInfo: false,
	},
	formulas: {
	    submittxt: function(get) {
		let cn = get('info.clusterName');
		if (cn) {
		    return Ext.String.format(gettext('Join {0}'), `'${cn}'`);
		}
		return gettext('Join');
	    },
	    showClusterFields: (get) => {
		let manualMode = !get('assistedEntry.checked');
		return get('hasAssistedInfo') || manualMode;
	    },
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    '#': {
		close: function() {
		    delete PVE.Utils.silenceAuthFailures;
		},
	    },
	    'proxmoxcheckbox[name=assistedEntry]': {
		change: 'onInputTypeChange',
	    },
	    'textarea[name=serializedinfo]': {
		change: 'recomputeSerializedInfo',
		enable: 'resetField',
	    },
	    'textfield': {
		disable: 'resetField',
	    },
	},
	resetField: function(field) {
	    field.reset();
	},
	onInputTypeChange: function(field, assistedInput) {
	    let linkEditor = this.lookup('linkEditor');
	    // this also clears all links
	    linkEditor.setAllowNumberEdit(!assistedInput);
	    if (!assistedInput) {
		linkEditor.setInfoText();
		linkEditor.setDefaultLinks();
	    }
	},
	recomputeSerializedInfo: function(field, value) {
	    let vm = this.getViewModel();
	    let assistedEntryBox = this.lookup('assistedEntry');
	    if (!assistedEntryBox.getValue()) {
		// not in assisted entry mode, nothing to do
		vm.set('hasAssistedInfo', false);
		return;
	    }
	    let linkEditor = this.lookup('linkEditor');
	    let jsons = Ext.util.Base64.decode(value);
	    let joinInfo = Ext.JSON.decode(jsons, true);
	    let info = {
		fp: '',
		ip: '',
		clusterName: '',
	    };
	    if (!(joinInfo && joinInfo.totem)) {
		field.valid = false;
		linkEditor.setLinks([]);
		linkEditor.setInfoText();
		vm.set('hasAssistedInfo', false);
	    } else {
		let interfaces = joinInfo.totem.interface;
		let links = Object.values(interfaces).map(iface => {
		    let linkNumber = iface.linknumber;
		    let peerLink;
		    if (joinInfo.peerLinks) {
			peerLink = joinInfo.peerLinks[linkNumber];
		    }
		    return {
			number: linkNumber,
			value: '',
			text: peerLink ? Ext.String.format(gettext("peer's link address: {0}"), peerLink) : '',
			allowBlank: false,
		    };
		});
		linkEditor.setInfoText();
		if (links.length === 1 && joinInfo.ring_addr !== undefined &&
		    joinInfo.ring_addr[0] === joinInfo.ipAddress
		) {
		    links[0].allowBlank = true;
		    links[0].emptyText = gettext("IP resolved by node's hostname");
		}
		linkEditor.setLinks(links);
		info = {
		    ip: joinInfo.ipAddress,
		    fp: joinInfo.fingerprint,
		    clusterName: joinInfo.totem.cluster_name,
		};
		field.valid = true;
		vm.set('hasAssistedInfo', true);
	    }
	    vm.set('info', info);
	},
    },
    submit: function() {
	// joining may produce temporarily auth failures, ignore as long the task runs
	PVE.Utils.silenceAuthFailures = true;
	this.callParent();
    },
    taskDone: function(success) {
	delete PVE.Utils.silenceAuthFailures;
	if (success) {
	    // reload always (if user wasn't faster), but wait a bit for pveproxy
	    Ext.defer(function() {
		window.location.reload(true);
	    }, 5000);
	    let txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!');
	    // ensure user cannot do harm
	    Ext.getBody().mask(txt, ['pve-static-mask']);
	    // TaskView may hide above mask, so tell him directly
	    Ext.Msg.show({
		title: gettext('Join Task Finished'),
		icon: Ext.Msg.INFO,
		msg: txt,
	    });
	}
    },
    items: [{
	xtype: 'proxmoxcheckbox',
	reference: 'assistedEntry',
	name: 'assistedEntry',
	itemId: 'assistedEntry',
	submitValue: false,
	value: true,
	autoEl: {
	    tag: 'div',
	    'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering'),
	},
	boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.'),
    },
    {
	xtype: 'textarea',
	name: 'serializedinfo',
	submitValue: false,
	allowBlank: false,
	fieldLabel: gettext('Information'),
	emptyText: gettext('Paste encoded Cluster Information here'),
	validator: function(val) {
	    return val === '' || this.valid || gettext('Does not seem like a valid encoded Cluster Information!');
	},
	bind: {
	    disabled: '{!assistedEntry.checked}',
	    hidden: '{!assistedEntry.checked}',
	},
	value: '',
    },
    {
	xtype: 'panel',
	width: 776,
	layout: {
	    type: 'hbox',
	    align: 'center',
	},
	bind: {
	    hidden: '{!showClusterFields}',
	},
	items: [
	    {
		xtype: 'textfield',
		flex: 1,
		margin: '0 5px 0 0',
		fieldLabel: gettext('Peer Address'),
		allowBlank: false,
		bind: {
		    value: '{info.ip}',
		    readOnly: '{assistedEntry.checked}',
		},
		name: 'hostname',
	    },
	    {
		xtype: 'textfield',
		flex: 1,
		margin: '0 0 10px 5px',
		inputType: 'password',
		emptyText: gettext("Peer's root password"),
		fieldLabel: gettext('Password'),
		allowBlank: false,
		name: 'password',
	    },
	],
    },
    {
	xtype: 'textfield',
	fieldLabel: gettext('Fingerprint'),
	allowBlank: false,
	bind: {
	    value: '{info.fp}',
	    readOnly: '{assistedEntry.checked}',
	    hidden: '{!showClusterFields}',
	},
	name: 'fingerprint',
    },
    {
	xtype: 'fieldcontainer',
	fieldLabel: gettext("Cluster Network"),
	bind: {
	    hidden: '{!showClusterFields}',
	},
	items: [
	    {
		xtype: 'pveCorosyncLinkEditor',
		itemId: 'linkEditor',
		reference: 'linkEditor',
		allowNumberEdit: false,
	    },
	],
    }],
});
/*
 * Datacenter config panel, located in the center of the ViewPort after the Datacenter view is selected
 */
Ext.define('PVE.dc.Config', {
    extend: 'PVE.panel.Config',
    alias: 'widget.PVE.dc.Config',
    onlineHelp: 'pve_admin_guide',
    initComponent: function() {
        var me = this;
	var caps = Ext.state.Manager.get('GuiCap');
	me.items = [];
	Ext.apply(me, {
	    title: gettext("Datacenter"),
	    hstateid: 'dctab',
	});
	if (caps.dc['Sys.Audit']) {
	    me.items.push({
		title: gettext('Summary'),
		xtype: 'pveDcSummary',
		iconCls: 'fa fa-book',
		itemId: 'summary',
	    },
	    {
		xtype: 'pmxNotesView',
		title: gettext('Notes'),
		iconCls: 'fa fa-sticky-note-o',
		itemId: 'notes',
	    },
	    {
		title: gettext('Cluster'),
		xtype: 'pveClusterAdministration',
		iconCls: 'fa fa-server',
		itemId: 'cluster',
	    },
	    {
		title: 'Ceph',
		itemId: 'ceph',
		iconCls: 'fa fa-ceph',
		xtype: 'pveNodeCephStatus',
	    },
	    {
		xtype: 'pveDcOptionView',
		title: gettext('Options'),
		iconCls: 'fa fa-gear',
		itemId: 'options',
	    });
	}
	if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) {
	    me.items.push({
		xtype: 'pveStorageView',
		title: gettext('Storage'),
		iconCls: 'fa fa-database',
		itemId: 'storage',
	    });
	}
	if (caps.dc['Sys.Audit']) {
	    me.items.push({
		xtype: 'pveDcBackupView',
		iconCls: 'fa fa-floppy-o',
		title: gettext('Backup'),
		itemId: 'backup',
	    },
	    {
		xtype: 'pveReplicaView',
		iconCls: 'fa fa-retweet',
		title: gettext('Replication'),
		itemId: 'replication',
	    },
	    {
		xtype: 'pveACLView',
		title: gettext('Permissions'),
		iconCls: 'fa fa-unlock',
		itemId: 'permissions',
		expandedOnInit: true,
	    });
	}
	me.items.push({
	    xtype: 'pveUserView',
	    groups: ['permissions'],
	    iconCls: 'fa fa-user',
	    title: gettext('Users'),
	    itemId: 'users',
	});
	me.items.push({
	    xtype: 'pveTokenView',
	    groups: ['permissions'],
	    iconCls: 'fa fa-user-o',
	    title: gettext('API Tokens'),
	    itemId: 'apitokens',
	});
	me.items.push({
	    xtype: 'pmxTfaView',
	    title: gettext('Two Factor'),
	    groups: ['permissions'],
	    iconCls: 'fa fa-key',
	    itemId: 'tfa',
	    yubicoEnabled: true,
	    issuerName: `Proxmox VE - ${PVE.ClusterName || Proxmox.NodeName}`,
	});
	if (caps.dc['Sys.Audit']) {
	    me.items.push({
		xtype: 'pveGroupView',
		title: gettext('Groups'),
		iconCls: 'fa fa-users',
		groups: ['permissions'],
		itemId: 'groups',
	    },
	    {
		xtype: 'pvePoolView',
		title: gettext('Pools'),
		iconCls: 'fa fa-tags',
		groups: ['permissions'],
		itemId: 'pools',
	    },
	    {
		xtype: 'pveRoleView',
		title: gettext('Roles'),
		iconCls: 'fa fa-male',
		groups: ['permissions'],
		itemId: 'roles',
	    },
	    {
		xtype: 'pveAuthView',
		title: gettext('Realms'),
		groups: ['permissions'],
		iconCls: 'fa fa-address-book-o',
		itemId: 'domains',
	    },
	    {
		xtype: 'pveHAStatus',
		title: 'HA',
		iconCls: 'fa fa-heartbeat',
		itemId: 'ha',
	    },
	    {
		title: gettext('Groups'),
		groups: ['ha'],
		xtype: 'pveHAGroupsView',
		iconCls: 'fa fa-object-group',
		itemId: 'ha-groups',
	    },
	    {
		title: gettext('Fencing'),
		groups: ['ha'],
		iconCls: 'fa fa-bolt',
		xtype: 'pveFencingView',
		itemId: 'ha-fencing',
	    });
	    // always show on initial load, will be hiddea later if the SDN API calls don't exist,
	    // else it won't be shown at first if the user initially loads with DC selected
	    if (PVE.SDNInfo || PVE.SDNInfo === undefined) {
		me.items.push({
		    xtype: 'pveSDNStatus',
		    title: gettext('SDN'),
		    iconCls: 'fa fa-sdn',
		    hidden: true,
		    itemId: 'sdn',
		    expandedOnInit: true,
		},
		{
		    xtype: 'pveSDNZoneView',
		    groups: ['sdn'],
		    title: gettext('Zones'),
		    hidden: true,
		    iconCls: 'fa fa-th',
		    itemId: 'sdnzone',
		},
		{
		    xtype: 'pveSDNVnet',
		    groups: ['sdn'],
		    title: gettext('Vnets'),
		    hidden: true,
		    iconCls: 'fa fa-network-wired',
		    itemId: 'sdnvnet',
		},
		{
		    xtype: 'pveSDNOptions',
		    groups: ['sdn'],
		    title: gettext('Options'),
		    hidden: true,
		    iconCls: 'fa fa-gear',
		    itemId: 'sdnoptions',
		});
	    }
	    if (Proxmox.UserName === 'root@pam') {
		me.items.push({
		    xtype: 'pveACMEClusterView',
		    title: 'ACME',
		    iconCls: 'fa fa-certificate',
		    itemId: 'acme',
		});
	    }
	    me.items.push({
		xtype: 'pveFirewallRules',
		title: gettext('Firewall'),
		allow_iface: true,
		base_url: '/cluster/firewall/rules',
		list_refs_url: '/cluster/firewall/refs',
		iconCls: 'fa fa-shield',
		itemId: 'firewall',
	    },
	    {
		xtype: 'pveFirewallOptions',
		title: gettext('Options'),
		groups: ['firewall'],
		iconCls: 'fa fa-gear',
		base_url: '/cluster/firewall/options',
		onlineHelp: 'pve_firewall_cluster_wide_setup',
		fwtype: 'dc',
		itemId: 'firewall-options',
	    },
	    {
		xtype: 'pveSecurityGroups',
		title: gettext('Security Group'),
		groups: ['firewall'],
		iconCls: 'fa fa-group',
		itemId: 'firewall-sg',
	    },
	    {
		xtype: 'pveFirewallAliases',
		title: gettext('Alias'),
		groups: ['firewall'],
		iconCls: 'fa fa-external-link',
		base_url: '/cluster/firewall/aliases',
		itemId: 'firewall-aliases',
	    },
	    {
		xtype: 'pveIPSet',
		title: 'IPSet',
		groups: ['firewall'],
		iconCls: 'fa fa-list-ol',
		base_url: '/cluster/firewall/ipset',
		list_refs_url: '/cluster/firewall/refs',
		itemId: 'firewall-ipset',
	    },
	    {
		xtype: 'pveMetricServerView',
		title: gettext('Metric Server'),
		iconCls: 'fa fa-bar-chart',
		itemId: 'metricservers',
		onlineHelp: 'external_metric_server',
	    },
	    {
		xtype: 'pveDcSupport',
		title: gettext('Support'),
		itemId: 'support',
		iconCls: 'fa fa-comments-o',
	    });
	}
	me.callParent();
   },
});
Ext.define('PVE.form.CorosyncLinkEditorController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.pveCorosyncLinkEditorController',
    addLinkIfEmpty: function() {
	let view = this.getView();
	if (view.items || view.items.length === 0) {
	    this.addLink();
	}
    },
    addEmptyLink: function() {
	this.addLink(); // discard parameters to allow being called from 'handler'
    },
    addLink: function(link) {
	let me = this;
	let view = me.getView();
	let vm = view.getViewModel();
	let linkCount = vm.get('linkCount');
	if (linkCount >= vm.get('maxLinkCount')) {
	    return;
	}
	link = link || {};
	if (link.number === undefined) {
	    link.number = me.getNextFreeNumber();
	}
	if (link.value === undefined) {
	    link.value = me.getNextFreeNetwork();
	}
	let linkSelector = Ext.create('PVE.form.CorosyncLinkSelector', {
	    maxLinkNumber: vm.get('maxLinkCount') - 1,
	    allowNumberEdit: vm.get('allowNumberEdit'),
	    allowBlankNetwork: link.allowBlank,
	    initNumber: link.number,
	    initNetwork: link.value,
	    text: link.text,
	    emptyText: link.emptyText,
	    // needs to be set here, because we need to update the viewmodel
	    removeBtnHandler: function() {
		let curLinkCount = vm.get('linkCount');
		if (curLinkCount <= 1) {
		    return;
		}
		vm.set('linkCount', curLinkCount - 1);
		// 'this' is the linkSelector here
		view.remove(this);
		me.updateDeleteButtonState();
	    },
	});
	view.add(linkSelector);
	linkCount++;
	vm.set('linkCount', linkCount);
	me.updateDeleteButtonState();
    },
    // ExtJS trips on binding this for some reason, so do it manually
    updateDeleteButtonState: function() {
	let view = this.getView();
	let vm = view.getViewModel();
	let disabled = vm.get('linkCount') <= 1;
	let deleteButtons = view.query('button[cls=removeLinkBtn]');
	Ext.Array.each(deleteButtons, btn => {
	    btn.setDisabled(disabled);
	});
    },
    getNextFreeNetwork: function() {
	let view = this.getView();
	let vm = view.getViewModel();
	let networksInUse = view.query('proxmoxNetworkSelector').map(selector => selector.value);
	for (const network of vm.get('networks')) {
	    if (!networksInUse.includes(network)) {
		return network;
	    }
	}
	return undefined; // default to empty field, user has to set up link manually
    },
    getNextFreeNumber: function() {
	let view = this.getView();
	let vm = view.getViewModel();
	let numbersInUse = view.query('numberfield').map(field => field.value);
	for (let i = 0; i < vm.get('maxLinkCount'); i++) {
	    if (!numbersInUse.includes(i)) {
		return i;
	    }
	}
	// all numbers in use, this should never happen since add button is disabled automatically
	return 0;
    },
});
Ext.define('PVE.form.CorosyncLinkSelector', {
    extend: 'Ext.panel.Panel',
    xtype: 'pveCorosyncLinkSelector',
    mixins: ['Proxmox.Mixin.CBind'],
    cbindData: [],
    // config
    maxLinkNumber: 7,
    allowNumberEdit: true,
    allowBlankNetwork: false,
    removeBtnHandler: undefined,
    emptyText: '',
    // values
    initNumber: 0,
    initNetwork: '',
    text: '',
    layout: 'hbox',
    bodyPadding: 5,
    border: 0,
    items: [
	{
	    xtype: 'displayfield',
	    fieldLabel: 'Link',
	    cbind: {
		hidden: '{allowNumberEdit}',
		value: '{initNumber}',
	    },
	    width: 45,
	    labelWidth: 30,
	    allowBlank: false,
	},
	{
	    xtype: 'numberfield',
	    fieldLabel: 'Link',
	    cbind: {
		maxValue: '{maxLinkNumber}',
		hidden: '{!allowNumberEdit}',
		value: '{initNumber}',
	    },
	    width: 80,
	    labelWidth: 30,
	    minValue: 0,
	    submitValue: false, // see getSubmitValue of network selector
	    allowBlank: false,
	},
	{
	    xtype: 'proxmoxNetworkSelector',
	    cbind: {
		allowBlank: '{allowBlankNetwork}',
		value: '{initNetwork}',
		emptyText: '{emptyText}',
	    },
	    autoSelect: false,
	    valueField: 'address',
	    displayField: 'address',
	    width: 220,
	    margin: '0 5px 0 5px',
	    getSubmitValue: function() {
		let me = this;
		// link number is encoded into key, so we need to set field name before value retrieval
		let linkNumber = me.prev('numberfield').getValue(); // always the correct one
		me.name = 'link' + linkNumber;
		return me.getValue();
	    },
	},
	{
	    xtype: 'button',
	    iconCls: 'fa fa-trash-o',
	    cls: 'removeLinkBtn',
	    cbind: {
		hidden: '{!allowNumberEdit}',
	    },
	    handler: function() {
		let me = this;
		let parent = me.up('pveCorosyncLinkSelector');
		if (parent.removeBtnHandler !== undefined) {
		    parent.removeBtnHandler();
		}
	    },
	},
	{
	    xtype: 'label',
	    margin: '-1px 0 0 5px',
	    // for muted effect
	    cls: 'x-form-item-label-default',
	    cbind: {
		text: '{text}',
	    },
	},
    ],
    initComponent: function() {
	let me = this;
	me.callParent();
	let numSelect = me.down('numberfield');
	let netSelect = me.down('proxmoxNetworkSelector');
	numSelect.validator = me.createNoDuplicatesValidator(
		'numberfield',
		gettext("Duplicate link number not allowed."),
	);
	netSelect.validator = me.createNoDuplicatesValidator(
		'proxmoxNetworkSelector',
		gettext("Duplicate link address not allowed."),
	);
    },
    createNoDuplicatesValidator: function(queryString, errorMsg) { // linkSelector generator
	let view = this; // eslint-disable-line consistent-this
	/** @this is the field itself, as the validator this is called from scopes it that way */
	return function(val) {
	    let me = this;
	    let form = view.up('form');
	    let linkEditor = view.up('pveCorosyncLinkEditor');
	    if (!form.validating) {
		// avoid recursion/double validation by setting temporary states
		me.validating = true;
		form.validating = true;
		// validate all other fields as well, to always mark both
		// parties involved in a 'duplicate' error
		form.isValid();
		form.validating = false;
		me.validating = false;
	    } else if (me.validating) {
		// we'll be validated by the original call in the other if-branch, avoid double work
		return true;
	    }
	    if (val === undefined || (val instanceof String && val.length === 0)) {
		return true; // let this be caught by allowBlank, if at all
	    }
	    let allFields = linkEditor.query(queryString);
	    for (const field of allFields) {
		if (field !== me && String(field.getValue()) === String(val)) {
		    return errorMsg;
		}
	    }
	    return true;
	};
    },
});
Ext.define('PVE.form.CorosyncLinkEditor', {
    extend: 'Ext.panel.Panel',
    xtype: 'pveCorosyncLinkEditor',
    controller: 'pveCorosyncLinkEditorController',
    // only initial config, use setter otherwise
    allowNumberEdit: true,
    viewModel: {
	data: {
	    linkCount: 0,
	    maxLinkCount: 8,
	    networks: null,
	    allowNumberEdit: true,
	    infoText: '',
	},
	formulas: {
	    addDisabled: function(get) {
		return !get('allowNumberEdit') ||
		    get('linkCount') >= get('maxLinkCount');
	    },
	    dockHidden: function(get) {
		return !(get('allowNumberEdit') || get('infoText'));
	    },
	},
    },
    dockedItems: [{
	xtype: 'toolbar',
	dock: 'bottom',
	defaultButtonUI: 'default',
	border: false,
	padding: '6 0 6 0',
	bind: {
	    hidden: '{dockHidden}',
	},
	items: [
	    {
		xtype: 'button',
		text: gettext('Add'),
		bind: {
		    disabled: '{addDisabled}',
		    hidden: '{!allowNumberEdit}',
		},
		handler: 'addEmptyLink',
	    },
	    {
		xtype: 'label',
		bind: {
		    text: '{infoText}',
		},
	    },
	],
    }],
    setInfoText: function(text) {
	let me = this;
	let vm = me.getViewModel();
	vm.set('infoText', text || '');
    },
    setLinks: function(links) {
	let me = this;
	let controller = me.getController();
	let vm = me.getViewModel();
	me.removeAll();
	vm.set('linkCount', 0);
	Ext.Array.each(links, link => controller.addLink(link));
    },
    setDefaultLinks: function() {
	let me = this;
	let controller = me.getController();
	let vm = me.getViewModel();
	me.removeAll();
	vm.set('linkCount', 0);
	controller.addLink();
    },
    // clears all links
    setAllowNumberEdit: function(allow) {
	let me = this;
	let vm = me.getViewModel();
	vm.set('allowNumberEdit', allow);
	me.removeAll();
	vm.set('linkCount', 0);
    },
    items: [{
	// No links is never a valid scenario, but can occur during a slow load
	xtype: 'hiddenfield',
	submitValue: false,
	isValid: function() {
	    let me = this;
	    let vm = me.up('pveCorosyncLinkEditor').getViewModel();
	    return vm.get('linkCount') > 0;
	},
    }],
    initComponent: function() {
	let me = this;
	let vm = me.getViewModel();
	let controller = me.getController();
	vm.set('allowNumberEdit', me.allowNumberEdit);
	vm.set('infoText', me.infoText || '');
	me.callParent();
	// Request local node networks to pre-populate first link.
	Proxmox.Utils.API2Request({
	    url: '/nodes/localhost/network',
	    method: 'GET',
	    waitMsgTarget: me,
	    success: response => {
		let data = response.result.data;
		if (data.length > 0) {
		    data.sort((a, b) => a.iface.localeCompare(b.iface));
		    let addresses = [];
		    for (let net of data) {
			if (net.address) {
			    addresses.push(net.address);
			}
			if (net.address6) {
			    addresses.push(net.address6);
			}
		    }
		    vm.set('networks', addresses);
		}
		// Always have at least one link, but account for delay in API,
		// someone might have called 'setLinks' in the meantime -
		// except if 'allowNumberEdit' is false, in which case we're
		// probably waiting for the user to input the join info
		if (vm.get('allowNumberEdit')) {
		    controller.addLinkIfEmpty();
		}
	    },
	    failure: () => {
		if (vm.get('allowNumberEdit')) {
		    controller.addLinkIfEmpty();
		}
	    },
	});
    },
});
Ext.define('PVE.dc.GroupEdit', {
    extend: 'Proxmox.window.Edit',
    alias: ['widget.pveDcGroupEdit'],
    initComponent: function() {
        var me = this;
        me.isCreate = !me.groupid;
        var url;
        var method;
        if (me.isCreate) {
            url = '/api2/extjs/access/groups';
            method = 'POST';
        } else {
            url = '/api2/extjs/access/groups/' + me.groupid;
            method = 'PUT';
        }
        Ext.applyIf(me, {
            subject: gettext('Group'),
            url: url,
            method: method,
            items: [
                {
		    xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
		    fieldLabel: gettext('Name'),
		    name: 'groupid',
		    value: me.groupid,
		    allowBlank: false,
		},
                {
		    xtype: 'textfield',
		    fieldLabel: gettext('Comment'),
		    name: 'comment',
		    allowBlank: true,
		},
            ],
        });
        me.callParent();
        if (!me.isCreate) {
            me.load();
        }
    },
});
Ext.define('PVE.dc.GroupView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveGroupView'],
    onlineHelp: 'pveum_groups',
    stateful: true,
    stateId: 'grid-groups',
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-groups',
	    sorters: {
		property: 'groupid',
		direction: 'ASC',
	    },
	});
        var reload = function() {
            store.load();
        };
	var sm = Ext.create('Ext.selection.RowModel', {});
	var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    callback: function() {
		reload();
	    },
	    baseurl: '/access/groups/',
	});
	var run_editor = function() {
	    var rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
            var win = Ext.create('PVE.dc.GroupEdit', {
                groupid: rec.data.groupid,
            });
            win.on('destroy', reload);
            win.show();
	};
	var edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	var tbar = [
            {
		text: gettext('Create'),
		handler: function() {
		    var win = Ext.create('PVE.dc.GroupEdit', {});
		    win.on('destroy', reload);
		    win.show();
		},
            },
	    edit_btn, remove_btn,
        ];
	Proxmox.Utils.monStoreErrors(me, store);
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: tbar,
	    viewConfig: {
		trackOver: false,
	    },
	    columns: [
		{
		    header: gettext('Name'),
		    width: 200,
		    sortable: true,
		    dataIndex: 'groupid',
		},
		{
		    header: gettext('Comment'),
		    sortable: false,
		    renderer: Ext.String.htmlEncode,
		    dataIndex: 'comment',
		    flex: 1,
		},
		{
		    header: gettext('Users'),
		    sortable: false,
		    dataIndex: 'users',
		    renderer: Ext.String.htmlEncode,
		    flex: 1,
		},
	    ],
	    listeners: {
		activate: reload,
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.dc.Guests', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveDcGuests',
    title: gettext('Guests'),
    height: 250,
    layout: {
	type: 'table',
	columns: 2,
	tableAttrs: {
	    style: {
		width: '100%',
	    },
	},
    },
    bodyPadding: '0 20 20 20',
    defaults: {
	xtype: 'box',
	padding: '0 50 0 50',
	style: {
	    'text-align': 'center',
	    'line-height': '1.5em',
	    'font-size': '14px',
	},
    },
    items: [
	{
	    itemId: 'qemu',
	    data: {
		running: 0,
		paused: 0,
		stopped: 0,
		template: 0,
	    },
	    cls: 'centered-flex-column',
	    tpl: [
		'' + gettext("Virtual Machines") + '
',
		'',
		    '
',
			' ',
			gettext('Running'),
		    '
',
		    '
{running}
',
		'
',
			'
',
			    ' ',
			    gettext('Paused'),
			'
',
			'
{paused}
',
		    '
',
		    '
',
			' ',
			gettext('Stopped'),
		    '
',
		    '
{stopped}
',
		'
',
			'
',
			    ' ',
			    gettext('Templates'),
			'
',
			'
{template}
',
		    '
' + gettext("LXC Container") + '
',
		'',
		    '
',
			' ',
			gettext('Running'),
		    '
',
		    '
{running}
',
		'
',
			'
',
			    ' ',
			    gettext('Paused'),
			'
',
			'
{paused}
',
		    '
',
		    '
',
			' ',
			gettext('Stopped'),
		    '
',
		    '
{stopped}
',
		'
',
			'
',
			    ' ',
			    gettext('Templates'),
			'
',
			'
{template}
',
		    '
',
			' ',
			gettext('Error'),
		    '
',
		    '{num}
',
		'',
	    ],
	},
    ],
    updateValues: function(qemu, lxc, error) {
	let me = this;
	let lazyUpdate = (query, newData) => {
	    let el = me.getComponent(query);
            let currentData = el.data;
	    let keys = Object.keys(newData);
	    if (keys.length === Object.keys(currentData).length) {
		if (keys.every(k => newData[k] === currentData[k])) {
		    return; // all stayed the same here, return early to avoid bogus regeneration
		}
	    }
	    el.update(newData);
	};
	lazyUpdate('qemu', qemu);
	lazyUpdate('lxc', lxc);
	lazyUpdate('error', { num: error });
    },
});
Ext.define('PVE.dc.Health', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveDcHealth',
    title: gettext('Health'),
    bodyPadding: 10,
    height: 250,
    layout: {
	type: 'hbox',
	align: 'stretch',
    },
    defaults: {
	flex: 1,
	xtype: 'box',
	style: {
	    'text-align': 'center',
	},
    },
    nodeList: [],
    nodeIndex: 0,
    updateStatus: function(store, records, success) {
	let me = this;
	if (!success) {
	    return;
	}
	let cluster = {
	    iconCls: PVE.Utils.get_health_icon('good', true),
	    text: gettext("Standalone node - no cluster defined"),
	};
	let nodes = {
	    online: 0,
	    offline: 0,
	};
	let numNodes = 1; // by default we have one node
	for (const { data } of records) {
	    if (data.type === 'node') {
		nodes[data.online === 1 ? 'online':'offline']++;
	    } else if (data.type === 'cluster') {
		cluster.text = `${gettext("Cluster")}: ${data.name}, ${gettext("Quorate")}: `;
		cluster.text += Proxmox.Utils.format_boolean(data.quorate);
		if (data.quorate !== 1) {
		    cluster.iconCls = PVE.Utils.get_health_icon('critical', true);
		}
		numNodes = data.nodes;
	    }
	}
	if (numNodes !== nodes.online + nodes.offline) {
	    nodes.offline = numNodes - nodes.online;
	}
	me.getComponent('clusterstatus').updateHealth(cluster);
	me.getComponent('nodestatus').update(nodes);
    },
    updateCeph: function(store, records, success) {
	let me = this;
	let cephstatus = me.getComponent('ceph');
	if (!success || records.length < 1) {
	    if (cephstatus.isVisible()) {
		return; // if ceph status is already visible don't stop to update
	    }
	    // try all nodes until we either get a successful api call, or we tried all nodes
	    if (++me.nodeIndex >= me.nodeList.length) {
		me.cephstore.stopUpdate();
	    } else {
		store.getProxy().setUrl(`/api2/json/nodes/${me.nodeList[me.nodeIndex].node}/ceph/status`);
	    }
	    return;
	}
	let state = PVE.Utils.render_ceph_health(records[0].data.health || {});
	cephstatus.updateHealth(state);
	cephstatus.setVisible(true);
    },
    listeners: {
	destroy: function() {
	    let me = this;
	    me.cephstore.stopUpdate();
	},
    },
    items: [
	{
	    itemId: 'clusterstatus',
	    xtype: 'pveHealthWidget',
	    title: gettext('Status'),
	},
	{
	    itemId: 'nodestatus',
	    data: {
		online: 0,
		offline: 0,
	    },
	    tpl: [
		'' + gettext('Nodes') + '
',
		'',
		'
',
		' ',
		gettext('Online'),
		'
',
		'
{online}
',
		'
',
		'
',
		' ',
		gettext('Offline'),
		'
',
		'
{offline}
',
		'
${tags}
`;
					    },
					    flex: 1,
					},
				    ],
				},
				store: {
				    data: Object.entries(PVE.UIOptions.tagTreeStyles).map(v => ({
					value: v[0],
					display: v[1],
				    })),
				},
				deleteDefault: true,
				defaultValue: '__default__',
				deleteEmpty: true,
			    },
			    {
				name: 'ordering',
				xtype: 'proxmoxKVComboBox',
				fieldLabel: gettext('Ordering'),
				comboItems: Object.entries(PVE.UIOptions.tagOrderOptions),
				defaultValue: '__default__',
				value: '__default__',
				deleteEmpty: true,
			    },
			    {
				name: 'case-sensitive',
				xtype: 'proxmoxcheckbox',
				fieldLabel: gettext('Case-Sensitive'),
				boxLabel: gettext('Applies to new edits'),
				value: 0,
			    },
			    {
				xtype: 'displayfield',
				fieldLabel: gettext('Color Overrides'),
			    },
			    {
				name: 'colors',
				xtype: 'pveTagColorGrid',
				deleteEmpty: true,
				height: 300,
			    },
			],
		    },
		],
	    },
	};
	me.rows['user-tag-access'] = {
	    required: true,
	    renderer: (value) => {
		if (value === undefined) {
		    return Ext.String.format(gettext('Mode: {0}'), 'free');
		}
		let mode = value?.['user-allow'] ?? 'free';
		let list = value?.['user-allow-list']?.join(',') ?? '';
		let modeTxt = Ext.String.format(gettext('Mode: {0}'), mode);
		let overrides = PVE.UIOptions.tagOverrides;
		let tags = PVE.Utils.renderTags(list, overrides);
		let listTxt = tags !== '' ? `, ${gettext('Pre-defined:')} ${tags}` : '';
		return `${modeTxt}${listTxt}`;
	    },
	    header: gettext('User Tag Access'),
	    editor: {
		xtype: 'pveUserTagAccessEdit',
	    },
	};
	me.rows['registered-tags'] = {
	    required: true,
	    renderer: (value) => {
		if (value === undefined) {
		    return gettext('No Registered Tags');
		}
		let overrides = PVE.UIOptions.tagOverrides;
		return PVE.Utils.renderTags(value.join(','), overrides);
	    },
	    header: gettext('Registered Tags'),
	    editor: {
		xtype: 'pveRegisteredTagEdit',
	    },
	};
	me.selModel = Ext.create('Ext.selection.RowModel', {});
	Ext.apply(me, {
	    tbar: [{
		text: gettext('Edit'),
		xtype: 'proxmoxButton',
		disabled: true,
		handler: function() { me.run_editor(); },
		selModel: me.selModel,
	    }],
	    url: "/api2/json/cluster/options",
	    editorConfig: {
		url: "/api2/extjs/cluster/options",
	    },
	    interval: 5000,
	    cwidth1: 200,
	    listeners: {
		itemdblclick: me.run_editor,
	    },
	});
	me.callParent();
	// set the new value for the default console
	me.mon(me.rstore, 'load', function(store, records, success) {
	    if (!success) {
		return;
	    }
	    var rec = store.getById('console');
	    PVE.UIOptions.options.console = rec.data.value;
	    if (rec.data.value === '__default__') {
		delete PVE.UIOptions.options.console;
	    }
	    PVE.UIOptions.options['tag-style'] = store.getById('tag-style')?.data?.value;
	    PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']);
	    PVE.UIOptions.fireUIConfigChanged();
	});
	me.on('activate', me.rstore.startUpdate);
	me.on('destroy', me.rstore.stopUpdate);
	me.on('deactivate', me.rstore.stopUpdate);
    },
});
Ext.define('pve-permissions', {
    extend: 'Ext.data.TreeModel',
    fields: [
	'text', 'type',
	{
	    type: 'boolean', name: 'propagate',
	},
    ],
});
Ext.define('PVE.dc.PermissionGridPanel', {
    extend: 'Ext.tree.Panel',
    alias: 'widget.pveUserPermissionGrid',
    onlineHelp: 'chapter_user_management',
    scrollable: true,
    layout: 'fit',
    rootVisible: false,
    animate: false,
    sortableColumns: false,
    columns: [
	{
	    xtype: 'treecolumn',
	    header: gettext('Path') + '/' + gettext('Permission'),
	    dataIndex: 'text',
	    flex: 6,
	},
	{
	    header: gettext('Propagate'),
	    dataIndex: 'propagate',
	    flex: 1,
	    renderer: function(value) {
		if (Ext.isDefined(value)) {
		    return Proxmox.Utils.format_boolean(value);
		}
		return '';
	    },
	},
    ],
    initComponent: function() {
	let me = this;
	Proxmox.Utils.API2Request({
	    url: '/access/permissions?userid=' + me.userid,
	    method: 'GET',
	    failure: function(response, opts) {
		Proxmox.Utils.setErrorMask(me, response.htmlStatus);
		me.load_task.delay(me.load_delay);
	    },
	    success: function(response, opts) {
		Proxmox.Utils.setErrorMask(me, false);
		let result = Ext.decode(response.responseText);
		let data = result.data || {};
		let root = {
		    name: '__root',
		    expanded: true,
		    children: [],
		};
		let idhash = {
		    '/': {
			children: [],
			text: '/',
			type: 'path',
		    },
		};
		Ext.Object.each(data, function(path, perms) {
		    let path_item = {
			text: path,
			type: 'path',
			children: [],
		    };
		    Ext.Object.each(perms, function(perm, propagate) {
			let perm_item = {
			    text: perm,
			    type: 'perm',
			    propagate: propagate === 1,
			    iconCls: 'fa fa-fw fa-unlock',
			    leaf: true,
			};
			path_item.children.push(perm_item);
			path_item.expandable = true;
		    });
		    idhash[path] = path_item;
		});
		Ext.Object.each(idhash, function(path, item) {
		    let parent_item = idhash['/'];
		    if (path === '/') {
			parent_item = root;
			item.expanded = true;
		    } else {
			let split_path = path.split('/');
			while (split_path.pop()) {
			    let parent_path = split_path.join('/');
			    if (idhash[parent_path]) {
				parent_item = idhash[parent_path];
				break;
			    }
			}
		    }
		    parent_item.children.push(item);
		});
		me.setRootNode(root);
	    },
	});
	me.callParent();
	me.store.sorters.add(new Ext.util.Sorter({
	    sorterFn: function(rec1, rec2) {
		let v1 = rec1.data.text,
		    v2 = rec2.data.text;
		if (rec1.data.type !== rec2.data.type) {
		    v2 = rec1.data.type;
		    v1 = rec2.data.type;
		}
		if (v1 > v2) {
		    return 1;
		} else if (v1 < v2) {
		    return -1;
		}
		return 0;
	    },
	}));
    },
});
Ext.define('PVE.dc.PermissionView', {
    extend: 'Ext.window.Window',
    alias: 'widget.userShowPermissionWindow',
    mixins: ['Proxmox.Mixin.CBind'],
    scrollable: true,
    width: 800,
    height: 600,
    layout: 'fit',
    cbind: {
	title: (get) => Ext.String.htmlEncode(get('userid')) +
	    ` - ${gettext('Granted Permissions')}`,
    },
    items: [{
	xtype: 'pveUserPermissionGrid',
	cbind: {
	    userid: '{userid}',
	},
    }],
});
Ext.define('PVE.dc.PoolEdit', {
    extend: 'Proxmox.window.Edit',
    alias: ['widget.pveDcPoolEdit'],
    mixins: ['Proxmox.Mixin.CBind'],
    subject: gettext('Pool'),
    cbindData: {
	poolid: '',
	isCreate: (cfg) => !cfg.poolid,
    },
    cbind: {
	autoLoad: get => !get('isCreate'),
	url: get => `/api2/extjs/pools/${get('poolid')}`,
	method: get => get('isCreate') ? 'POST' : 'PUT',
    },
    items: [
	{
	    xtype: 'pmxDisplayEditField',
	    fieldLabel: gettext('Name'),
	    cbind: {
		editable: '{isCreate}',
		value: '{poolid}',
	    },
	    name: 'poolid',
	    allowBlank: false,
	},
	{
	    xtype: 'textfield',
	    fieldLabel: gettext('Comment'),
	    name: 'comment',
	    allowBlank: true,
	},
    ],
});
Ext.define('PVE.dc.PoolView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pvePoolView'],
    onlineHelp: 'pveum_pools',
    stateful: true,
    stateId: 'grid-pools',
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-pools',
	    sorters: {
		property: 'poolid',
		direction: 'ASC',
	    },
	});
        var reload = function() {
            store.load();
        };
	var sm = Ext.create('Ext.selection.RowModel', {});
	var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/pools/',
	    callback: function() {
		reload();
	    },
	});
	var run_editor = function() {
	    var rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
            var win = Ext.create('PVE.dc.PoolEdit', {
                poolid: rec.data.poolid,
            });
            win.on('destroy', reload);
            win.show();
	};
	var edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	var tbar = [
            {
		text: gettext('Create'),
		handler: function() {
		    var win = Ext.create('PVE.dc.PoolEdit', {});
		    win.on('destroy', reload);
		    win.show();
		},
            },
	    edit_btn, remove_btn,
        ];
	Proxmox.Utils.monStoreErrors(me, store);
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: tbar,
	    viewConfig: {
		trackOver: false,
	    },
	    columns: [
		{
		    header: gettext('Name'),
		    width: 200,
		    sortable: true,
		    dataIndex: 'poolid',
		},
		{
		    header: gettext('Comment'),
		    sortable: false,
		    renderer: Ext.String.htmlEncode,
		    dataIndex: 'comment',
		    flex: 1,
		},
	    ],
	    listeners: {
		activate: reload,
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.dc.RoleEdit', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveDcRoleEdit',
    width: 400,
    initComponent: function() {
	var me = this;
	me.isCreate = !me.roleid;
	var url;
	var method;
	if (me.isCreate) {
	    url = '/api2/extjs/access/roles';
	    method = 'POST';
	} else {
	    url = '/api2/extjs/access/roles/' + me.roleid;
	    method = 'PUT';
	}
	Ext.applyIf(me, {
	    subject: gettext('Role'),
	    url: url,
	    method: method,
	    items: [
		{
		    xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
		    name: 'roleid',
		    value: me.roleid,
		    allowBlank: false,
		    fieldLabel: gettext('Name'),
		},
		{
		    xtype: 'pvePrivilegesSelector',
		    name: 'privs',
		    value: me.privs,
		    allowBlank: false,
		    fieldLabel: gettext('Privileges'),
		},
	    ],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response) {
		    var data = response.result.data;
		    var keys = Ext.Object.getKeys(data);
		    me.setValues({
			privs: keys,
			roleid: me.roleid,
		    });
		},
	    });
	}
    },
});
Ext.define('PVE.dc.RoleView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveRoleView'],
    onlineHelp: 'pveum_roles',
    stateful: true,
    stateId: 'grid-roles',
    initComponent: function() {
	let me = this;
	let store = new Ext.data.Store({
	    model: 'pmx-roles',
	    sorters: {
		property: 'roleid',
		direction: 'ASC',
	    },
	});
	Proxmox.Utils.monStoreErrors(me, store);
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    if (rec.data.special) {
		return;
	    }
	    Ext.create('PVE.dc.RoleEdit', {
		roleid: rec.data.roleid,
		privs: rec.data.privs,
		listeners: {
		    destroy: () => store.load(),
		},
		autoShow: true,
	    });
	};
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    columns: [
		{
		    header: gettext('Built-In'),
		    width: 65,
		    sortable: true,
		    dataIndex: 'special',
		    renderer: Proxmox.Utils.format_boolean,
		},
		{
		    header: gettext('Name'),
		    width: 150,
		    sortable: true,
		    dataIndex: 'roleid',
		},
		{
		    itemid: 'privs',
		    header: gettext('Privileges'),
		    sortable: false,
		    renderer: (value, metaData) => {
			if (!value) {
			    return '-';
			}
			metaData.style = 'white-space:normal;'; // allow word wrap
			return value.replace(/,/g, ' ');
		    },
		    variableRowHeight: true,
		    dataIndex: 'privs',
		    flex: 1,
		},
	    ],
	    listeners: {
		activate: function() {
		    store.load();
		},
		itemdblclick: run_editor,
	    },
	    tbar: [
		{
		    text: gettext('Create'),
		    handler: function() {
			Ext.create('PVE.dc.RoleEdit', {
			    listeners: {
				destroy: () => store.load(),
			    },
			    autoShow: true,
			});
		    },
		},
		{
		    xtype: 'proxmoxButton',
		    text: gettext('Edit'),
		    disabled: true,
		    selModel: sm,
		    handler: run_editor,
		    enableFn: (rec) => !rec.data.special,
		},
		{
		    xtype: 'proxmoxStdRemoveButton',
		    selModel: sm,
		    callback: () => store.load(),
		    baseurl: '/access/roles/',
		    enableFn: (rec) => !rec.data.special,
		},
	    ],
	});
	me.callParent();
    },
});
Ext.define('pve-security-groups', {
    extend: 'Ext.data.Model',
    fields: ['group', 'comment', 'digest'],
    idProperty: 'group',
});
Ext.define('PVE.SecurityGroupEdit', {
    extend: 'Proxmox.window.Edit',
    base_url: "/cluster/firewall/groups",
    allow_iface: false,
    initComponent: function() {
	var me = this;
	me.isCreate = me.group_name === undefined;
	var subject;
        me.url = '/api2/extjs' + me.base_url;
        me.method = 'POST';
	var items = [
	    {
		xtype: 'textfield',
		name: 'group',
		value: me.group_name || '',
		fieldLabel: gettext('Name'),
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'comment',
		value: me.group_comment || '',
		fieldLabel: gettext('Comment'),
	    },
	];
	if (me.isCreate) {
	    subject = gettext('Security Group');
        } else {
	    subject = gettext('Security Group') + " '" + me.group_name + "'";
	    items.push({
		xtype: 'hiddenfield',
		name: 'rename',
		value: me.group_name,
	    });
        }
	var ipanel = Ext.create('Proxmox.panel.InputPanel', {
	// InputPanel does not have a 'create' property, does it need a 'isCreate'
	    isCreate: me.isCreate,
	    items: items,
	});
	Ext.apply(me, {
            subject: subject,
	    items: [ipanel],
	});
	me.callParent();
    },
});
Ext.define('PVE.SecurityGroupList', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.pveSecurityGroupList',
    stateful: true,
    stateId: 'grid-securitygroups',
    rulePanel: undefined,
    addBtn: undefined,
    removeBtn: undefined,
    editBtn: undefined,
    base_url: "/cluster/firewall/groups",
    initComponent: function() {
	let me = this;
	if (!me.base_url) {
	    throw "no base_url specified";
	}
	let store = new Ext.data.Store({
	    model: 'pve-security-groups',
	    proxy: {
		type: 'proxmox',
		url: '/api2/json' + me.base_url,
	    },
	    sorters: {
		property: 'group',
		direction: 'ASC',
	    },
	});
	let sm = Ext.create('Ext.selection.RowModel', {});
	let reload = function() {
	    let oldrec = sm.getSelection()[0];
	    store.load((records, operation, success) => {
		if (oldrec) {
		    let rec = store.findRecord('group', oldrec.data.group, 0, false, true, true);
		    if (rec) {
			sm.select(rec);
		    }
		}
	    });
	};
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    Ext.create('PVE.SecurityGroupEdit', {
		digest: rec.data.digest,
		group_name: rec.data.group,
		group_comment: rec.data.comment,
		listeners: {
		    destroy: () => reload(),
		},
		autoShow: true,
	    });
	};
	me.editBtn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	me.addBtn = new Proxmox.button.Button({
	    text: gettext('Create'),
	    handler: function() {
		sm.deselectAll();
		var win = Ext.create('PVE.SecurityGroupEdit', {});
		win.show();
		win.on('destroy', reload);
	    },
	});
	me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: me.base_url + '/',
	    enableFn: function(rec) {
		return rec && me.base_url;
	    },
	    callback: () => reload(),
	});
	Ext.apply(me, {
	    store: store,
	    tbar: ['' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn],
	    selModel: sm,
	    columns: [
		{
		    header: gettext('Group'),
		    dataIndex: 'group',
		    width: '100',
		},
		{
		    header: gettext('Comment'),
		    dataIndex: 'comment',
		    renderer: Ext.String.htmlEncode,
		    flex: 1,
		},
	    ],
	    listeners: {
		itemdblclick: run_editor,
		select: function(_sm, rec) {
		    if (!me.rulePanel) {
			me.rulePanel = me.up('panel').down('pveFirewallRules');
		    }
		    me.rulePanel.setBaseUrl(`/cluster/firewall/groups/${rec.data.group}`);
		},
		deselect: function() {
		    if (!me.rulePanel) {
			me.rulePanel = me.up('panel').down('pveFirewallRules');
		    }
		    me.rulePanel.setBaseUrl(undefined);
		},
		show: reload,
	    },
	});
	me.callParent();
	store.load();
    },
});
Ext.define('PVE.SecurityGroups', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveSecurityGroups',
    title: 'Security Groups',
    onlineHelp: 'pve_firewall_security_groups',
    layout: 'border',
    items: [
	{
	    xtype: 'pveFirewallRules',
	    region: 'center',
	    allow_groups: false,
	    list_refs_url: '/cluster/firewall/refs',
	    tbar_prefix: '' + gettext('Rules') + ':',
	    border: false,
	},
	{
	    xtype: 'pveSecurityGroupList',
	    region: 'west',
	    width: '25%',
	    border: false,
	    split: true,
	},
    ],
    listeners: {
	show: function() {
	    let sglist = this.down('pveSecurityGroupList');
	    sglist.fireEvent('show', sglist);
	},
    },
});
Ext.define('PVE.dc.StorageView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveStorageView'],
    onlineHelp: 'chapter_storage',
    stateful: true,
    stateId: 'grid-dc-storage',
    createStorageEditWindow: function(type, sid) {
	let schema = PVE.Utils.storageSchema[type];
	if (!schema || !schema.ipanel) {
	    throw "no editor registered for storage type: " + type;
	}
	Ext.create('PVE.storage.BaseEdit', {
	    paneltype: 'PVE.storage.' + schema.ipanel,
	    type: type,
	    storageId: sid,
	    canDoBackups: schema.backups,
	    autoShow: true,
	    listeners: {
		destroy: this.reloadStore,
	    },
	});
    },
    initComponent: function() {
	let me = this;
	let store = new Ext.data.Store({
	    model: 'pve-storage',
	    proxy: {
                type: 'proxmox',
		url: "/api2/json/storage",
	    },
	    sorters: {
		property: 'storage',
		direction: 'ASC',
	    },
	});
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    let { type, storage } = rec.data;
	    me.createStorageEditWindow(type, storage);
	};
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/storage/',
	    callback: () => store.load(),
	});
	// else we cannot dynamically generate the add menu handlers
	let addHandleGenerator = function(type) {
	    return function() { me.createStorageEditWindow(type); };
	};
	let addMenuItems = [];
	for (const [type, storage] of Object.entries(PVE.Utils.storageSchema)) {
	    if (storage.hideAdd) {
		continue;
	    }
	    addMenuItems.push({
		text: PVE.Utils.format_storage_type(type),
		iconCls: 'fa fa-fw fa-' + storage.faIcon,
		handler: addHandleGenerator(type),
	    });
	}
	Ext.apply(me, {
	    store: store,
	    reloadStore: () => store.load(),
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: new Ext.menu.Menu({
			items: addMenuItems,
		    }),
		},
		remove_btn,
		edit_btn,
	    ],
	    columns: [
		{
		    header: 'ID',
		    flex: 2,
		    sortable: true,
		    dataIndex: 'storage',
		},
		{
		    header: gettext('Type'),
		    flex: 1,
		    sortable: true,
		    dataIndex: 'type',
		    renderer: PVE.Utils.format_storage_type,
		},
		{
		    header: gettext('Content'),
		    flex: 3,
		    sortable: true,
		    dataIndex: 'content',
		    renderer: PVE.Utils.format_content_types,
		},
		{
		    header: gettext('Path') + '/' + gettext('Target'),
		    flex: 2,
		    sortable: true,
		    dataIndex: 'path',
		    renderer: function(value, metaData, record) {
			if (record.data.target) {
			    return record.data.target;
			}
			return value;
		    },
		},
		{
		    header: gettext('Shared'),
		    flex: 1,
		    sortable: true,
		    dataIndex: 'shared',
		    renderer: Proxmox.Utils.format_boolean,
		},
		{
		    header: gettext('Enabled'),
		    flex: 1,
		    sortable: true,
		    dataIndex: 'disable',
		    renderer: Proxmox.Utils.format_neg_boolean,
		},
		{
		    header: gettext('Bandwidth Limit'),
		    flex: 2,
		    sortable: true,
		    dataIndex: 'bwlimit',
		},
	    ],
	    listeners: {
		activate: () => store.load(),
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
    },
}, function() {
    Ext.define('pve-storage', {
	extend: 'Ext.data.Model',
	fields: [
	    'path', 'type', 'content', 'server', 'portal', 'target', 'export', 'storage',
	    { name: 'shared', type: 'boolean' },
	    { name: 'disable', type: 'boolean' },
	],
	idProperty: 'storage',
    });
});
Ext.define('PVE.dc.Summary', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveDcSummary',
    scrollable: true,
    bodyPadding: 5,
    layout: 'column',
    defaults: {
	padding: 5,
	columnWidth: 1,
    },
    items: [
	{
	    itemId: 'dcHealth',
	    xtype: 'pveDcHealth',
	},
	{
	    itemId: 'dcGuests',
	    xtype: 'pveDcGuests',
	},
	{
	    title: gettext('Resources'),
	    xtype: 'panel',
	    minHeight: 250,
	    bodyPadding: 5,
	    layout: 'hbox',
	    defaults: {
		xtype: 'proxmoxGauge',
		flex: 1,
	    },
	    items: [
		{
		    title: gettext('CPU'),
		    itemId: 'cpu',
		},
		{
		    title: gettext('Memory'),
		    itemId: 'memory',
		},
		{
		    title: gettext('Storage'),
		    itemId: 'storage',
		},
	    ],
	},
	{
	    itemId: 'nodeview',
	    xtype: 'pveDcNodeView',
	    height: 250,
	},
	{
	    title: gettext('Subscriptions'),
	    height: 220,
	    items: [
		{
		    itemId: 'subscriptions',
		    xtype: 'pveHealthWidget',
		    userCls: 'pointer',
		    listeners: {
			element: 'el',
			click: function() {
			    if (this.component.userCls === 'pointer') {
				window.open('https://www.proxmox.com/en/proxmox-ve/pricing', '_blank');
			    }
			},
		    },
		},
	    ],
	},
    ],
    listeners: {
	resize: function(panel) {
	    Proxmox.Utils.updateColumns(panel);
	},
    },
    initComponent: function() {
        var me = this;
	var rstore = Ext.create('Proxmox.data.UpdateStore', {
	    interval: 3000,
	    storeid: 'pve-cluster-status',
	    model: 'pve-dc-nodes',
	    proxy: {
                type: 'proxmox',
                url: "/api2/json/cluster/status",
	    },
	});
	var gridstore = Ext.create('Proxmox.data.DiffStore', {
	    rstore: rstore,
	    filters: {
		property: 'type',
		value: 'node',
	    },
	    sorters: {
		property: 'id',
		direction: 'ASC',
	    },
	});
	me.callParent();
	me.getComponent('nodeview').setStore(gridstore);
	var gueststatus = me.getComponent('dcGuests');
	var cpustat = me.down('#cpu');
	var memorystat = me.down('#memory');
	var storagestat = me.down('#storage');
	var sp = Ext.state.Manager.getProvider();
	me.mon(PVE.data.ResourceStore, 'load', function(curstore, results) {
	    me.suspendLayout = true;
	    let cpu = 0, maxcpu = 0;
	    let memory = 0, maxmem = 0;
	    let used = 0, total = 0;
	    let countedStorage = {}, usableStorages = {};
	    let storages = sp.get('dash-storages') || '';
	    storages.split(',').filter(v => v !== '').forEach(storage => {
		usableStorages[storage] = true;
	    });
	    let qemu = {
		running: 0,
		paused: 0,
		stopped: 0,
		template: 0,
	    };
	    let lxc = {
		running: 0,
		paused: 0,
		stopped: 0,
		template: 0,
	    };
	    let error = 0;
	    for (const { data } of results) {
		switch (data.type) {
		    case 'node':
			cpu += data.cpu * data.maxcpu;
			maxcpu += data.maxcpu || 0;
			memory += data.mem || 0;
			maxmem += data.maxmem || 0;
			if (gridstore.getById(data.id)) {
			    let griditem = gridstore.getById(data.id);
			    griditem.set('cpuusage', data.cpu);
			    let max = data.maxmem || 1;
			    let val = data.mem || 0;
			    griditem.set('memoryusage', val / max);
			    griditem.set('uptime', data.uptime);
			    griditem.commit(); // else the store marks the field as dirty
			}
			break;
		    case 'storage': {
			let sid = !data.shared || data.storage === 'local' ? data.id : data.storage;
			if (!Ext.Object.isEmpty(usableStorages)) {
			    if (usableStorages[data.id] !== true) {
				break;
			    }
			    sid = data.id;
			} else if (countedStorage[sid]) {
			    break;
			}
			used += data.disk;
			total += data.maxdisk;
			countedStorage[sid] = true;
			break;
		    }
		    case 'qemu':
			qemu[data.template ? 'template' : data.status]++;
			if (data.hastate === 'error') {
			    error++;
			}
			break;
		    case 'lxc':
			lxc[data.template ? 'template' : data.status]++;
			if (data.hastate === 'error') {
			    error++;
			}
			break;
		    default: break;
		}
	    }
	    let text = Ext.String.format(gettext('of {0} CPU(s)'), maxcpu);
	    cpustat.updateValue(cpu/maxcpu, text);
	    text = Ext.String.format(gettext('{0} of {1}'), Proxmox.Utils.render_size(memory), Proxmox.Utils.render_size(maxmem));
	    memorystat.updateValue(memory/maxmem, text);
	    text = Ext.String.format(gettext('{0} of {1}'), Proxmox.Utils.render_size(used), Proxmox.Utils.render_size(total));
	    storagestat.updateValue(used/total, text);
	    gueststatus.updateValues(qemu, lxc, error);
	    me.suspendLayout = false;
	    me.updateLayout(true);
	});
	let dcHealth = me.getComponent('dcHealth');
	me.mon(rstore, 'load', dcHealth.updateStatus, dcHealth);
	let subs = me.down('#subscriptions');
	me.mon(rstore, 'load', function(store, records, success) {
	    var level;
	    var mixed = false;
	    for (let i = 0; i < records.length; i++) {
		let node = records[i];
		if (node.get('type') !== 'node' || node.get('status') === 'offline') {
		    continue;
		}
		let curlevel = node.get('level');
		if (curlevel === '') { // no subscription beats all, set it and break the loop
		    level = '';
		    break;
		}
		if (level === undefined) { // save level
		    level = curlevel;
		} else if (level !== curlevel) { // detect different levels
		    mixed = true;
		}
	    }
	    let data = {
		title: Proxmox.Utils.unknownText,
		text: Proxmox.Utils.unknownText,
		iconCls: PVE.Utils.get_health_icon(undefined, true),
	    };
	    if (level === '') {
		data = {
		    title: gettext('No Subscription'),
		    iconCls: PVE.Utils.get_health_icon('critical', true),
		    text: gettext('You have at least one node without subscription.'),
		};
		subs.setUserCls('pointer');
	    } else if (mixed) {
		data = {
		    title: gettext('Mixed Subscriptions'),
		    iconCls: PVE.Utils.get_health_icon('warning', true),
		    text: gettext('Warning: Your subscription levels are not the same.'),
		};
		subs.setUserCls('pointer');
	    } else if (level) {
		data = {
		    title: PVE.Utils.render_support_level(level),
		    iconCls: PVE.Utils.get_health_icon('good', true),
		    text: gettext('Your subscription status is valid.'),
		};
		subs.setUserCls('');
	    }
	    subs.setData(data);
	});
	me.on('destroy', function() {
	    rstore.stopUpdate();
	});
	me.mon(sp, 'statechange', function(provider, key, value) {
	    if (key !== 'summarycolumns') {
		return;
	    }
	    Proxmox.Utils.updateColumns(me);
	});
	rstore.startUpdate();
    },
});
Ext.define('PVE.dc.Support', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveDcSupport',
    pveGuidePath: '/pve-docs/index.html',
    onlineHelp: 'getting_help',
    invalidHtml: 'No valid subscription
' + PVE.Utils.noSubKeyHtml,
    communityHtml: 'Please use the public community forum for any questions.',
    activeHtml: 'Please use our support portal for any questions. You can also use the public community forum to get additional information.',
    bugzillaHtml: 'Bug Tracking
Our bug tracking system is available here.',
    docuHtml: function() {
	var me = this;
	var guideUrl = window.location.origin + me.pveGuidePath;
	var text = Ext.String.format('Documentation
'
	+ 'The official Proxmox VE Administration Guide'
	+ ' is included with this installation and can be browsed at '
	+ '{0}', guideUrl);
	return text;
    },
    updateActive: function(data) {
	var me = this;
	var html = '' + data.productname + '
' + me.activeHtml;
	html += '
' + me.docuHtml();
	html += '
' + me.bugzillaHtml;
	me.update(html);
    },
    updateCommunity: function(data) {
	var me = this;
	var html = '' + data.productname + '
' + me.communityHtml;
	html += '
' + me.docuHtml();
	html += '
' + me.bugzillaHtml;
	me.update(html);
    },
    updateInactive: function(data) {
	var me = this;
	me.update(me.invalidHtml);
    },
    initComponent: function() {
        let me = this;
	let reload = function() {
	    Proxmox.Utils.API2Request({
		url: '/nodes/localhost/subscription',
		method: 'GET',
		waitMsgTarget: me,
		failure: function(response, opts) {
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		    me.update(`${gettext('Unable to load subscription status')}: ${response.htmlStatus}`);
		},
		success: function(response, opts) {
		    let data = response.result.data;
		    if (data?.status.toLowerCase() === 'active') {
			if (data.level === 'c') {
			    me.updateCommunity(data);
			} else {
			    me.updateActive(data);
			}
		    } else {
			me.updateInactive(data);
		    }
		},
	    });
	};
	Ext.apply(me, {
	    autoScroll: true,
	    bodyStyle: 'padding:10px',
	    listeners: {
		activate: reload,
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.dc.SyncWindow', {
    extend: 'Ext.window.Window',
    title: gettext('Realm Sync'),
    width: 600,
    bodyPadding: 10,
    modal: true,
    resizable: false,
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    'form': {
		validitychange: function(field, valid) {
		    let me = this;
		    me.lookup('preview_btn').setDisabled(!valid);
		    me.lookup('sync_btn').setDisabled(!valid);
		},
	    },
	    'button': {
		click: function(btn) {
		    if (btn.reference === 'help_btn') return;
		    this.sync_realm(btn.reference === 'preview_btn');
		},
	    },
	},
	sync_realm: function(is_preview) {
	    let me = this;
	    let view = me.getView();
	    let ipanel = me.lookup('ipanel');
	    let params = ipanel.getValues();
	    let vanished_opts = [];
	    ['acl', 'entry', 'properties'].forEach((prop) => {
		if (params[`remove-vanished-${prop}`]) {
		    vanished_opts.push(prop);
		}
		delete params[`remove-vanished-${prop}`];
	    });
	    if (vanished_opts.length > 0) {
		params['remove-vanished'] = vanished_opts.join(';');
	    } else {
		params['remove-vanished'] = 'none';
	    }
	    params['dry-run'] = is_preview ? 1 : 0;
	    Proxmox.Utils.API2Request({
		url: `/access/domains/${view.realm}/sync`,
		waitMsgTarget: view,
		method: 'POST',
		params,
		failure: function(response) {
		    view.show();
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		},
		success: function(response) {
		    view.hide();
		    Ext.create('Proxmox.window.TaskViewer', {
			upid: response.result.data,
			listeners: {
			    destroy: function() {
				if (is_preview) {
				    view.show();
				} else {
				    view.close();
				}
			    },
			},
		    }).show();
		},
	    });
	},
    },
    items: [
	{
	    xtype: 'form',
	    reference: 'form',
	    border: false,
	    fieldDefaults: {
		labelWidth: 100,
		anchor: '100%',
	    },
	    items: [{
		xtype: 'inputpanel',
		reference: 'ipanel',
		column1: [
		    {
			xtype: 'proxmoxKVComboBox',
			name: 'scope',
			fieldLabel: gettext('Scope'),
			value: '',
			emptyText: gettext('No default available'),
			deleteEmpty: false,
			allowBlank: false,
			comboItems: [
			    ['users', gettext('Users')],
			    ['groups', gettext('Groups')],
			    ['both', gettext('Users and Groups')],
			],
		    },
		],
		column2: [
		    {
			xtype: 'proxmoxKVComboBox',
			value: '1',
			deleteEmpty: false,
			allowBlank: false,
			comboItems: [
			    ['1', Proxmox.Utils.yesText],
			    ['0', Proxmox.Utils.noText],
			],
			name: 'enable-new',
			fieldLabel: gettext('Enable new'),
		    },
		],
		columnB: [
		    {
			xtype: 'fieldset',
			title: gettext('Remove Vanished Options'),
			items: [
			    {
				xtype: 'proxmoxcheckbox',
				fieldLabel: gettext('ACL'),
				name: 'remove-vanished-acl',
				boxLabel: gettext('Remove ACLs of vanished users and groups.'),
			    },
			    {
				xtype: 'proxmoxcheckbox',
				fieldLabel: gettext('Entry'),
				name: 'remove-vanished-entry',
				boxLabel: gettext('Remove vanished user and group entries.'),
			    },
			    {
				xtype: 'proxmoxcheckbox',
				fieldLabel: gettext('Properties'),
				name: 'remove-vanished-properties',
				boxLabel: gettext('Remove vanished properties from synced users.'),
			    },
			],
		    },
		    {
			xtype: 'displayfield',
			reference: 'defaulthint',
			value: gettext('Default sync options can be set by editing the realm.'),
			userCls: 'pmx-hint',
			hidden: true,
		    },
		],
	    }],
	},
    ],
    buttons: [
	{
	    xtype: 'proxmoxHelpButton',
	    reference: 'help_btn',
	    onlineHelp: 'pveum_ldap_sync',
	    hidden: false,
	},
	'->',
	{
	    text: gettext('Preview'),
	    reference: 'preview_btn',
	},
	{
	    text: gettext('Sync'),
	    reference: 'sync_btn',
	},
    ],
    initComponent: function() {
	let me = this;
	if (!me.realm) {
	    throw "no realm defined";
	}
	me.callParent();
	Proxmox.Utils.API2Request({
	    url: `/access/domains/${me.realm}`,
	    waitMsgTarget: me,
	    method: 'GET',
	    failure: function(response) {
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		me.close();
	    },
	    success: function(response) {
		let default_options = response.result.data['sync-defaults-options'];
		if (default_options) {
		    let options = PVE.Parser.parsePropertyString(default_options);
		    if (options['remove-vanished']) {
			let opts = options['remove-vanished'].split(';');
			for (const opt of opts) {
			    options[`remove-vanished-${opt}`] = 1;
			}
		    }
		    let ipanel = me.lookup('ipanel');
		    ipanel.setValues(options);
		} else {
		    me.lookup('defaulthint').setVisible(true);
		}
		// check validity for button state
		me.lookup('form').isValid();
	    },
	});
    },
});
/* This class defines the "Tasks" tab of the bottom status panel
 * Tasks are jobs with a start, end and log output
 */
Ext.define('PVE.dc.Tasks', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveClusterTasks'],
    initComponent: function() {
	let me = this;
	let taskstore = Ext.create('Proxmox.data.UpdateStore', {
	    storeid: 'pve-cluster-tasks',
	    model: 'proxmox-tasks',
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/cluster/tasks',
	    },
	});
	let store = Ext.create('Proxmox.data.DiffStore', {
	    rstore: taskstore,
	    sortAfterUpdate: true,
	    appendAtStart: true,
	    sorters: [
		{
		    property: 'pid',
		    direction: 'DESC',
		},
		{
		    property: 'starttime',
		    direction: 'DESC',
		},
	    ],
	});
	let run_task_viewer = function() {
	    var sm = me.getSelectionModel();
	    var rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    var win = Ext.create('Proxmox.window.TaskViewer', {
		upid: rec.data.upid,
		endtime: rec.data.endtime,
	    });
	    win.show();
	};
	Ext.apply(me, {
	    store: store,
	    stateful: false,
	    viewConfig: {
		trackOver: false,
		stripeRows: true, // does not work with getRowClass()
		getRowClass: function(record, index) {
		    let taskState = record.get('status');
		    if (taskState) {
			let parsed = Proxmox.Utils.parse_task_status(taskState);
			if (parsed === 'warning') {
			    return "proxmox-warning-row";
			} else if (parsed !== 'ok') {
			    return "proxmox-invalid-row";
			}
		    }
		    return '';
		},
	    },
	    sortableColumns: false,
	    columns: [
		{
		    header: gettext("Start Time"),
		    dataIndex: 'starttime',
		    width: 150,
		    renderer: function(value) {
			return Ext.Date.format(value, "M d H:i:s");
		    },
		},
		{
		    header: gettext("End Time"),
		    dataIndex: 'endtime',
		    width: 150,
		    renderer: function(value, metaData, record) {
			if (record.data.pid) {
			    if (record.data.type === "vncproxy" ||
				record.data.type === "vncshell" ||
				record.data.type === "spiceproxy") {
				metaData.tdCls = "x-grid-row-console";
			    } else {
				metaData.tdCls = "x-grid-row-loading";
			    }
			    return "";
			}
			return Ext.Date.format(value, "M d H:i:s");
		    },
		},
		{
		    header: gettext("Node"),
		    dataIndex: 'node',
		    width: 100,
		},
		{
		    header: gettext("User name"),
		    dataIndex: 'user',
		    renderer: Ext.String.htmlEncode,
		    width: 150,
		},
		{
		    header: gettext("Description"),
		    dataIndex: 'upid',
		    flex: 1,
		    renderer: Proxmox.Utils.render_upid,
		},
		{
		    header: gettext("Status"),
		    dataIndex: 'status',
		    width: 200,
		    renderer: function(value, metaData, record) {
			if (record.data.pid) {
			    if (record.data.type !== "vncproxy") {
				metaData.tdCls = "x-grid-row-loading";
			    }
			    return "";
			}
			return Proxmox.Utils.format_task_status(value);
		    },
		},
	    ],
	    listeners: {
		itemdblclick: run_task_viewer,
		show: () => taskstore.startUpdate(),
		destroy: () => taskstore.stopUpdate(),
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.dc.TokenEdit', {
    extend: 'Proxmox.window.Edit',
    alias: ['widget.pveDcTokenEdit'],
    mixins: ['Proxmox.Mixin.CBind'],
    subject: gettext('Token'),
    onlineHelp: 'pveum_tokens',
    isAdd: true,
    isCreate: false,
    fixedUser: false,
    method: 'POST',
    url: '/api2/extjs/access/users/',
    defaultFocus: 'field[disabled=false][hidden=false][name=tokenid]',
    items: {
	xtype: 'inputpanel',
	onGetValues: function(values) {
	    let me = this;
	    let win = me.up('pveDcTokenEdit');
	    win.url = '/api2/extjs/access/users/';
	    let uid = encodeURIComponent(values.userid);
	    let tid = encodeURIComponent(values.tokenid);
	    delete values.userid;
	    delete values.tokenid;
	    win.url += `${uid}/token/${tid}`;
	    return values;
	},
	column1: [
	    {
		xtype: 'pmxDisplayEditField',
		cbind: {
		    editable: (get) => get('isCreate') && !get('fixedUser'),
		},
		submitValue: true,
		editConfig: {
		    xtype: 'pmxUserSelector',
		    allowBlank: false,
		},
		name: 'userid',
		value: Proxmox.UserName,
		renderer: Ext.String.htmlEncode,
		fieldLabel: gettext('User'),
	    },
	    {
		xtype: 'pmxDisplayEditField',
		cbind: {
		    editable: '{isCreate}',
		},
		name: 'tokenid',
		fieldLabel: gettext('Token ID'),
		submitValue: true,
		minLength: 2,
		allowBlank: false,
	    },
	],
	column2: [
	    {
		xtype: 'proxmoxcheckbox',
		name: 'privsep',
		checked: true,
		uncheckedValue: 0,
		fieldLabel: gettext('Privilege Separation'),
	    },
	    {
		xtype: 'pmxExpireDate',
		name: 'expire',
	    },
	],
	columnB: [
	    {
		xtype: 'textfield',
		name: 'comment',
		fieldLabel: gettext('Comment'),
	    },
	],
    },
    initComponent: function() {
	let me = this;
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    me.setValues(response.result.data);
		},
	    });
	}
    },
    apiCallDone: function(success, response, options) {
	let res = response.result.data;
	if (!success || !res.value) {
	    return;
	}
	Ext.create('PVE.dc.TokenShow', {
	    autoShow: true,
	    tokenid: res['full-tokenid'],
	    secret: res.value,
	});
    },
});
Ext.define('PVE.dc.TokenShow', {
    extend: 'Ext.window.Window',
    alias: ['widget.pveTokenShow'],
    mixins: ['Proxmox.Mixin.CBind'],
    width: 600,
    modal: true,
    resizable: false,
    title: gettext('Token Secret'),
    items: [
	{
	    xtype: 'container',
	    layout: 'form',
	    bodyPadding: 10,
	    border: false,
	    fieldDefaults: {
		labelWidth: 100,
		anchor: '100%',
            },
	    padding: '0 10 10 10',
	    items: [
		{
		    xtype: 'textfield',
		    fieldLabel: gettext('Token ID'),
		    cbind: {
			value: '{tokenid}',
		    },
		    editable: false,
		},
		{
		    xtype: 'textfield',
		    fieldLabel: gettext('Secret'),
		    inputId: 'token-secret-value',
		    cbind: {
			value: '{secret}',
		    },
		    editable: false,
		},
	    ],
	},
	{
	    xtype: 'component',
	    border: false,
	    padding: '10 10 10 10',
	    userCls: 'pmx-hint',
	    html: gettext('Please record the API token secret - it will only be displayed now'),
	},
    ],
    buttons: [
	{
	    handler: function(b) {
		document.getElementById('token-secret-value').select();
		document.execCommand("copy");
	    },
	    text: gettext('Copy Secret Value'),
	    iconCls: 'fa fa-clipboard',
	},
    ],
});
Ext.define('PVE.dc.TokenView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveTokenView'],
    onlineHelp: 'chapter_user_management',
    stateful: true,
    stateId: 'grid-tokens',
    // use fixed user
    fixedUser: undefined,
    initComponent: function() {
	let me = this;
	let caps = Ext.state.Manager.get('GuiCap');
	let store = new Ext.data.Store({
            id: "tokens",
	    model: 'pve-tokens',
	    sorters: 'id',
	});
	let reload = function() {
	    if (me.fixedUser) {
		Proxmox.Utils.API2Request({
		    url: `/access/users/${encodeURIComponent(me.fixedUser)}/token`,
		    method: 'GET',
		    failure: function(response, opts) {
			Proxmox.Utils.setErrorMask(me, response.htmlStatus);
			me.load_task.delay(me.load_delay);
		    },
		    success: function(response, opts) {
			Proxmox.Utils.setErrorMask(me, false);
			let result = Ext.decode(response.responseText);
			let data = result.data || [];
			let records = [];
			Ext.Array.each(data, function(token) {
			    let r = {};
			    r.id = me.fixedUser + '!' + token.tokenid;
			    r.userid = me.fixedUser;
			    r.tokenid = token.tokenid;
			    r.comment = token.comment;
			    r.expire = token.expire;
			    r.privsep = token.privsep === 1;
			    records.push(r);
			});
			store.loadData(records);
		    },
		});
		return;
	    }
	    Proxmox.Utils.API2Request({
		url: '/access/users/?full=1',
		method: 'GET',
		failure: function(response, opts) {
		    Proxmox.Utils.setErrorMask(me, response.htmlStatus);
		    me.load_task.delay(me.load_delay);
		},
		success: function(response, opts) {
		    Proxmox.Utils.setErrorMask(me, false);
		    let result = Ext.decode(response.responseText);
		    let data = result.data || [];
		    let records = [];
		    Ext.Array.each(data, function(user) {
			let tokens = user.tokens || [];
			Ext.Array.each(tokens, function(token) {
			    let r = {};
			    r.id = user.userid + '!' + token.tokenid;
			    r.userid = user.userid;
			    r.tokenid = token.tokenid;
			    r.comment = token.comment;
			    r.expire = token.expire;
			    r.privsep = token.privsep === 1;
			    records.push(r);
			});
		    });
		    store.loadData(records);
		},
	    });
	};
	let sm = Ext.create('Ext.selection.RowModel', {});
	let urlFromRecord = (rec) => {
	    let uid = encodeURIComponent(rec.data.userid);
	    let tid = encodeURIComponent(rec.data.tokenid);
	    return `/access/users/${uid}/token/${tid}`;
	};
	let run_editor = function(rec) {
	    if (!caps.access['User.Modify']) {
		return;
	    }
	    let win = Ext.create('PVE.dc.TokenEdit', {
		method: 'PUT',
		url: urlFromRecord(rec),
	    });
	    win.setValues(rec.data);
	    win.on('destroy', reload);
	    win.show();
	};
        let tbar = [
            {
		text: gettext('Add'),
		disabled: !caps.access['User.Modify'],
		handler: function(btn, e, rec) {
		    let data = {};
		    if (me.fixedUser) {
			data.userid = me.fixedUser;
			data.fixedUser = true;
		    } else if (rec && rec.data) {
			data.userid = rec.data.userid;
		    }
		    let win = Ext.create('PVE.dc.TokenEdit', {
			isCreate: true,
			fixedUser: me.fixedUser,
		    });
		    win.setValues(data);
		    win.on('destroy', reload);
		    win.show();
		},
            },
	    {
		xtype: 'proxmoxButton',
		text: gettext('Edit'),
		disabled: true,
		enableFn: (rec) => !!caps.access['User.Modify'],
		selModel: sm,
		handler: (btn, e, rec) => run_editor(rec),
	    },
	    {
		xtype: 'proxmoxStdRemoveButton',
		selModel: sm,
		enableFn: (rec) => !!caps.access['User.Modify'],
		callback: reload,
		getUrl: urlFromRecord,
	    },
	    '-',
	    {
		xtype: 'proxmoxButton',
		text: gettext('Show Permissions'),
		disabled: true,
		selModel: sm,
		handler: function(btn, event, rec) {
		    Ext.create('PVE.dc.PermissionView', {
			autoShow: true,
			userid: rec.data.id,
		    });
		},
	    },
        ];
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: tbar,
	    viewConfig: {
		trackOver: false,
	    },
	    columns: [
		{
		    header: gettext('User name'),
		    dataIndex: 'userid',
		    renderer: (uid) => {
			let realmIndex = uid.lastIndexOf('@');
			let user = Ext.String.htmlEncode(uid.substr(0, realmIndex));
			let realm = Ext.String.htmlEncode(uid.substr(realmIndex));
			return `${user} ${realm}`;
		    },
		    hidden: !!me.fixedUser,
		    flex: 2,
		},
		{
		    header: gettext('Token Name'),
		    dataIndex: 'tokenid',
		    hideable: false,
		    flex: 1,
		},
		{
		    header: gettext('Expire'),
		    dataIndex: 'expire',
		    hideable: false,
		    renderer: Proxmox.Utils.format_expire,
		    flex: 1,
		},
		{
		    header: gettext('Comment'),
		    dataIndex: 'comment',
		    renderer: Ext.String.htmlEncode,
		    flex: 3,
		},
		{
		    header: gettext('Privilege Separation'),
		    dataIndex: 'privsep',
		    hideable: false,
		    renderer: Proxmox.Utils.format_boolean,
		    flex: 1,
		},
	    ],
	    listeners: {
		activate: reload,
		itemdblclick: (view, rec) => run_editor(rec),
	    },
	});
	if (me.fixedUser) {
	    reload();
	}
	me.callParent();
    },
});
Ext.define('PVE.window.TokenView', {
    extend: 'Ext.window.Window',
    mixins: ['Proxmox.Mixin.CBind'],
    modal: true,
    subject: gettext('API Tokens'),
    scrollable: true,
    layout: 'fit',
    width: 800,
    height: 400,
    cbind: {
	title: gettext('API Tokens') + ' - {userid}',
    },
    items: [{
	xtype: 'pveTokenView',
	cbind: {
	    fixedUser: '{userid}',
	},
    }],
});
Ext.define('PVE.dc.UserEdit', {
    extend: 'Proxmox.window.Edit',
    alias: ['widget.pveDcUserEdit'],
    isAdd: true,
    initComponent: function() {
	let me = this;
	me.isCreate = !me.userid;
	let url = '/api2/extjs/access/users';
	let method = 'POST';
	if (!me.isCreate) {
	    url += '/' + encodeURIComponent(me.userid);
	    method = 'PUT';
	}
	let verifypw, pwfield;
	let validate_pw = function() {
	    if (verifypw.getValue() !== pwfield.getValue()) {
		return gettext("Passwords do not match");
	    }
	    return true;
	};
	verifypw = Ext.createWidget('textfield', {
	    inputType: 'password',
	    fieldLabel: gettext('Confirm password'),
	    name: 'verifypassword',
	    submitValue: false,
	    disabled: true,
	    hidden: true,
	    validator: validate_pw,
	});
	pwfield = Ext.createWidget('textfield', {
	    inputType: 'password',
	    fieldLabel: gettext('Password'),
	    minLength: 5,
	    name: 'password',
	    disabled: true,
	    hidden: true,
	    validator: validate_pw,
	});
	let column1 = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'userid',
		fieldLabel: gettext('User name'),
		value: me.userid,
		renderer: Ext.String.htmlEncode,
		allowBlank: false,
		submitValue: !!me.isCreate,
	    },
	    pwfield,
	    verifypw,
	    {
		xtype: 'pveGroupSelector',
		name: 'groups',
		multiSelect: true,
		allowBlank: true,
		fieldLabel: gettext('Group'),
	    },
	    {
		xtype: 'pmxExpireDate',
		name: 'expire',
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Enabled'),
		name: 'enable',
		uncheckedValue: 0,
		defaultValue: 1,
		checked: true,
	    },
	];
	let column2 = [
	    {
		xtype: 'textfield',
		name: 'firstname',
		fieldLabel: gettext('First Name'),
	    },
	    {
		xtype: 'textfield',
		name: 'lastname',
		fieldLabel: gettext('Last Name'),
	    },
	    {
		xtype: 'textfield',
		name: 'email',
		fieldLabel: gettext('E-Mail'),
		vtype: 'proxmoxMail',
	    },
	];
	if (me.isCreate) {
	    column1.splice(1, 0, {
		xtype: 'pmxRealmComboBox',
		name: 'realm',
		fieldLabel: gettext('Realm'),
		allowBlank: false,
		matchFieldWidth: false,
		listConfig: { width: 300 },
		listeners: {
		    change: function(combo, realm) {
			me.realm = realm;
			pwfield.setVisible(realm === 'pve');
			pwfield.setDisabled(realm !== 'pve');
			verifypw.setVisible(realm === 'pve');
			verifypw.setDisabled(realm !== 'pve');
		    },
		},
		submitValue: false,
	    });
	}
	var ipanel = Ext.create('Proxmox.panel.InputPanel', {
	    column1: column1,
	    column2: column2,
	    columnB: [
		{
		    xtype: 'textfield',
		    name: 'comment',
		    fieldLabel: gettext('Comment'),
		},
	    ],
	    advancedItems: [
		{
		    xtype: 'textfield',
		    name: 'keys',
		    fieldLabel: gettext('Key IDs'),
		},
	    ],
	    onGetValues: function(values) {
		if (me.realm) {
		    values.userid = values.userid + '@' + me.realm;
		}
		if (!values.password) {
		    delete values.password;
		}
		return values;
	    },
	});
	Ext.applyIf(me, {
	    subject: gettext('User'),
	    url: url,
	    method: method,
	    fieldDefaults: {
		labelWidth: 110, // some translation are quite long (e.g., Spanish)
	    },
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var data = response.result.data;
		    me.setValues(data);
		    if (data.keys) {
			if (data.keys === 'x!oath' || data.keys === 'x!u2f') {
			    me.down('[name="keys"]').setDisabled(1);
			}
		    }
		},
	    });
	}
    },
});
Ext.define('PVE.dc.UserView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveUserView'],
    onlineHelp: 'pveum_users',
    stateful: true,
    stateId: 'grid-users',
    initComponent: function() {
	var me = this;
	var caps = Ext.state.Manager.get('GuiCap');
	var store = new Ext.data.Store({
            id: "users",
	    model: 'pmx-users',
	    sorters: {
		property: 'userid',
		direction: 'ASC',
	    },
	});
	let reload = () => store.load();
	let sm = Ext.create('Ext.selection.RowModel', {});
	let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/access/users/',
	    dangerous: true,
	    enableFn: rec => caps.access['User.Modify'] && rec.data.userid !== 'root@pam',
	    callback: () => reload(),
	});
	let run_editor = function() {
	    var rec = sm.getSelection()[0];
	    if (!rec || !caps.access['User.Modify']) {
		return;
	    }
	    Ext.create('PVE.dc.UserEdit', {
		userid: rec.data.userid,
		autoShow: true,
		listeners: {
		    destroy: () => reload(),
		},
	    });
	};
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    enableFn: function(rec) {
		return !!caps.access['User.Modify'];
	    },
	    selModel: sm,
	    handler: run_editor,
	});
	let pwchange_btn = new Proxmox.button.Button({
	    text: gettext('Password'),
	    disabled: true,
	    selModel: sm,
	    enableFn: function(record) {
		let type = record.data['realm-type'];
		if (type) {
		    if (PVE.Utils.authSchema[type]) {
			return !!PVE.Utils.authSchema[type].pwchange;
		    }
		}
		return false;
	    },
	    handler: function(btn, event, rec) {
		Ext.create('Proxmox.window.PasswordEdit', {
		    userid: rec.data.userid,
		    autoShow: true,
		    listeners: {
			destroy: () => reload(),
		    },
		});
	    },
	});
	var perm_btn = new Proxmox.button.Button({
	    text: gettext('Permissions'),
	    disabled: true,
	    selModel: sm,
	    handler: function(btn, event, rec) {
		Ext.create('PVE.dc.PermissionView', {
		    userid: rec.data.userid,
		    autoShow: true,
		    listeners: {
			destroy: () => reload(),
		    },
		});
	    },
	});
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: [
		{
		    text: gettext('Add'),
		    disabled: !caps.access['User.Modify'],
		    handler: function() {
			Ext.create('PVE.dc.UserEdit', {
			    autoShow: true,
			    listeners: {
				destroy: () => reload(),
			    },
			});
		    },
		},
		'-',
		edit_btn,
		remove_btn,
		'-',
		pwchange_btn,
		'-',
		perm_btn,
	    ],
	    viewConfig: {
		trackOver: false,
	    },
	    columns: [
		{
		    header: gettext('User name'),
		    width: 200,
		    sortable: true,
		    renderer: Proxmox.Utils.render_username,
		    dataIndex: 'userid',
		},
		{
		    header: gettext('Realm'),
		    width: 100,
		    sortable: true,
		    renderer: Proxmox.Utils.render_realm,
		    dataIndex: 'userid',
		},
		{
		    header: gettext('Enabled'),
		    width: 80,
		    sortable: true,
		    renderer: Proxmox.Utils.format_boolean,
		    dataIndex: 'enable',
		},
		{
		    header: gettext('Expire'),
		    width: 80,
		    sortable: true,
		    renderer: Proxmox.Utils.format_expire,
		    dataIndex: 'expire',
		},
		{
		    header: gettext('Name'),
		    width: 150,
		    sortable: true,
		    renderer: PVE.Utils.render_full_name,
		    dataIndex: 'firstname',
		},
		{
		    header: 'TFA',
		    width: 50,
		    sortable: true,
		    renderer: function(v) {
			let tfa_type = PVE.Parser.parseTfaType(v);
			if (tfa_type === undefined) {
			    return Proxmox.Utils.noText;
			} else if (tfa_type === 1) {
			    return Proxmox.Utils.yesText;
			} else {
			    return tfa_type;
			}
		    },
		    dataIndex: 'keys',
		},
		{
		    header: gettext('Comment'),
		    sortable: false,
		    renderer: Ext.String.htmlEncode,
		    dataIndex: 'comment',
		    flex: 1,
		},
	    ],
	    listeners: {
		activate: reload,
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
	Proxmox.Utils.monStoreErrors(me, store);
    },
});
Ext.define('PVE.dc.MetricServerView', {
    extend: 'Ext.grid.Panel',
    alias: ['widget.pveMetricServerView'],
    stateful: true,
    stateId: 'grid-metricserver',
    controller: {
	xclass: 'Ext.app.ViewController',
	render_type: function(value) {
	    switch (value) {
		case 'influxdb': return "InfluxDB";
		case 'graphite': return "Graphite";
		default: return Proxmox.Utils.unknownText;
	    }
	},
	editWindow: function(xtype, id) {
	    let me = this;
	    Ext.create(`PVE.dc.${xtype}Edit`, {
		serverid: id,
		autoShow: true,
		listeners: {
		    destroy: () => me.reload(),
		},
	    });
	},
	addServer: function(button) {
	    this.editWindow(button.text);
	},
	editServer: function() {
	    let me = this;
	    let view = me.getView();
	    let selection = view.getSelection();
	    if (!selection || selection.length < 1) {
		return;
	    }
	    let cfg = selection[0].data;
	    let xtype = me.render_type(cfg.type);
	    me.editWindow(xtype, cfg.id);
	},
	reload: function() {
	    this.getView().getStore().load();
	},
    },
    store: {
	autoLoad: true,
	id: 'metricservers',
	proxy: {
	    type: 'proxmox',
	    url: '/api2/json/cluster/metrics/server',
	},
    },
    columns: [
	{
	    text: gettext('Name'),
	    flex: 2,
	    dataIndex: 'id',
	},
	{
	    text: gettext('Type'),
	    flex: 1,
	    dataIndex: 'type',
	    renderer: 'render_type',
	},
	{
	    text: gettext('Enabled'),
	    dataIndex: 'disable',
	    width: 100,
	    renderer: Proxmox.Utils.format_neg_boolean,
	},
	{
	    text: gettext('Server'),
	    width: 200,
	    dataIndex: 'server',
	},
	{
	    text: gettext('Port'),
	    width: 100,
	    dataIndex: 'port',
	},
    ],
    tbar: [
	{
	    text: gettext('Add'),
	    menu: [
		{
		    text: 'Graphite',
		    iconCls: 'fa fa-fw fa-bar-chart',
		    handler: 'addServer',
		},
		{
		    text: 'InfluxDB',
		    iconCls: 'fa fa-fw fa-bar-chart',
		    handler: 'addServer',
		},
	    ],
	},
	{
	    text: gettext('Edit'),
	    xtype: 'proxmoxButton',
	    handler: 'editServer',
	    disabled: true,
	},
	{
	    xtype: 'proxmoxStdRemoveButton',
	    baseurl: `/api2/extjs/cluster/metrics/server`,
	    callback: 'reload',
	},
    ],
    listeners: {
	itemdblclick: 'editServer',
    },
    initComponent: function() {
	var me = this;
	me.callParent();
	Proxmox.Utils.monStoreErrors(me, me.getStore());
    },
});
Ext.define('PVE.dc.MetricServerBaseEdit', {
    extend: 'Proxmox.window.Edit',
    mixins: ['Proxmox.Mixin.CBind'],
    cbindData: function() {
	let me = this;
	me.isCreate = !me.serverid;
	me.serverid = me.serverid || "";
	me.url = `/api2/extjs/cluster/metrics/server/${me.serverid}`;
	me.method = me.isCreate ? 'POST' : 'PUT';
	if (!me.isCreate) {
	    me.subject = `${me.subject}: ${me.serverid}`;
	}
	return {};
    },
    submitUrl: function(url, values) {
	return this.isCreate ? `${url}/${values.id}` : url;
    },
    initComponent: function() {
	let me = this;
	me.callParent();
	if (me.serverid) {
	    me.load({
		success: function(response, options) {
		    let values = response.result.data;
		    values.enable = !values.disable;
		    me.down('inputpanel').setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.dc.InfluxDBEdit', {
    extend: 'PVE.dc.MetricServerBaseEdit',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'metric_server_influxdb',
    subject: 'InfluxDB',
    cbindData: function() {
	let me = this;
	me.callParent();
	me.tokenEmptyText = me.isCreate ? '' : gettext('unchanged');
	return {};
    },
    items: [
	{
	    xtype: 'inputpanel',
	    cbind: {
		isCreate: '{isCreate}',
	    },
	    onGetValues: function(values) {
		let me = this;
		values.disable = values.enable ? 0 : 1;
		delete values.enable;
		PVE.Utils.delete_if_default(values, 'verify-certificate', '1', me.isCreate);
		return values;
	    },
	    column1: [
		{
		    xtype: 'hidden',
		    name: 'type',
		    value: 'influxdb',
		    cbind: {
			submitValue: '{isCreate}',
		    },
		},
		{
		    xtype: 'pmxDisplayEditField',
		    name: 'id',
		    fieldLabel: gettext('Name'),
		    allowBlank: false,
		    cbind: {
			editable: '{isCreate}',
			value: '{serverid}',
		    },
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'server',
		    fieldLabel: gettext('Server'),
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'port',
		    fieldLabel: gettext('Port'),
		    value: 8089,
		    minValue: 1,
		    maximum: 65536,
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxKVComboBox',
		    name: 'influxdbproto',
		    fieldLabel: gettext('Protocol'),
		    value: '__default__',
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		    comboItems: [
			['__default__', 'UDP'],
			['http', 'HTTP'],
			['https', 'HTTPS'],
		    ],
		    listeners: {
			change: function(field, value) {
			    let me = this;
			    let view = me.up('inputpanel');
			    let isUdp = value !== 'http' && value !== 'https';
			    view.down('field[name=organization]').setDisabled(isUdp);
			    view.down('field[name=bucket]').setDisabled(isUdp);
			    view.down('field[name=token]').setDisabled(isUdp);
			    view.down('field[name=api-path-prefix]').setDisabled(isUdp);
			    view.down('field[name=mtu]').setDisabled(!isUdp);
			    view.down('field[name=timeout]').setDisabled(isUdp);
			    view.down('field[name=max-body-size]').setDisabled(isUdp);
			    view.down('field[name=verify-certificate]').setDisabled(value !== 'https');
			},
		    },
		},
	    ],
	    column2: [
		{
		    xtype: 'checkbox',
		    name: 'enable',
		    fieldLabel: gettext('Enabled'),
		    inputValue: 1,
		    uncheckedValue: 0,
		    checked: true,
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'organization',
		    fieldLabel: gettext('Organization'),
		    emptyText: 'proxmox',
		    disabled: true,
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'bucket',
		    fieldLabel: gettext('Bucket'),
		    emptyText: 'proxmox',
		    disabled: true,
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'token',
		    fieldLabel: gettext('Token'),
		    disabled: true,
		    allowBlank: true,
		    deleteEmpty: false,
		    submitEmpty: false,
		    cbind: {
			disabled: '{!isCreate}',
			emptyText: '{tokenEmptyText}',
		    },
		},
	    ],
	    advancedColumn1: [
		{
		    xtype: 'proxmoxtextfield',
		    name: 'api-path-prefix',
		    fieldLabel: gettext('API Path Prefix'),
		    allowBlank: true,
		    disabled: true,
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'timeout',
		    fieldLabel: gettext('Timeout (s)'),
		    disabled: true,
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		    minValue: 1,
		    emptyText: 1,
		},
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'verify-certificate',
		    fieldLabel: gettext('Verify Certificate'),
		    value: 1,
		    uncheckedValue: 0,
		    disabled: true,
		},
	    ],
	    advancedColumn2: [
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'max-body-size',
		    fieldLabel: gettext('Batch Size (b)'),
		    minValue: 1,
		    emptyText: '25000000',
		    submitEmpty: false,
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'mtu',
		    fieldLabel: 'MTU',
		    minValue: 1,
		    emptyText: '1500',
		    submitEmpty: false,
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		},
	    ],
	},
    ],
});
Ext.define('PVE.dc.GraphiteEdit', {
    extend: 'PVE.dc.MetricServerBaseEdit',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'metric_server_graphite',
    subject: 'Graphite',
    items: [
	{
	    xtype: 'inputpanel',
	    onGetValues: function(values) {
		values.disable = values.enable ? 0 : 1;
		delete values.enable;
		return values;
	    },
	    column1: [
		{
		    xtype: 'hidden',
		    name: 'type',
		    value: 'graphite',
		    cbind: {
			submitValue: '{isCreate}',
		    },
		},
		{
		    xtype: 'pmxDisplayEditField',
		    name: 'id',
		    fieldLabel: gettext('Name'),
		    allowBlank: false,
		    cbind: {
			editable: '{isCreate}',
			value: '{serverid}',
		    },
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'server',
		    fieldLabel: gettext('Server'),
		    allowBlank: false,
		},
	    ],
	    column2: [
		{
		    xtype: 'checkbox',
		    name: 'enable',
		    fieldLabel: gettext('Enabled'),
		    inputValue: 1,
		    uncheckedValue: 0,
		    checked: true,
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'port',
		    fieldLabel: gettext('Port'),
		    value: 2003,
		    minimum: 1,
		    maximum: 65536,
		    allowBlank: false,
		},
		{
		    fieldLabel: gettext('Path'),
		    xtype: 'proxmoxtextfield',
		    emptyText: 'proxmox',
		    name: 'path',
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		},
	    ],
	    advancedColumn1: [
		{
		    xtype: 'proxmoxKVComboBox',
		    name: 'proto',
		    fieldLabel: gettext('Protocol'),
		    value: '__default__',
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		    comboItems: [
			['__default__', 'UDP'],
			['tcp', 'TCP'],
		    ],
		    listeners: {
			change: function(field, value) {
			    let me = this;
			    me.up('inputpanel').down('field[name=timeout]').setDisabled(value !== 'tcp');
			    me.up('inputpanel').down('field[name=mtu]').setDisabled(value === 'tcp');
			},
		    },
		},
	    ],
	    advancedColumn2: [
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'mtu',
		    fieldLabel: 'MTU',
		    minimum: 1,
		    emptyText: '1500',
		    submitEmpty: false,
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		},
		{
		    xtype: 'proxmoxintegerfield',
		    name: 'timeout',
		    fieldLabel: gettext('TCP Timeout'),
		    disabled: true,
		    cbind: {
			deleteEmpty: '{!isCreate}',
		    },
		    minValue: 1,
		    emptyText: 1,
		},
	    ],
	},
    ],
});
Ext.define('PVE.dc.UserTagAccessEdit', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveUserTagAccessEdit',
    subject: gettext('User Tag Access'),
    onlineHelp: 'datacenter_configuration_file',
    url: '/api2/extjs/cluster/options',
    hintText: gettext('NOTE: The following tags are also defined as registered tags.'),
    controller: {
	xclass: 'Ext.app.ViewController',
	tagChange: function(field, value) {
	    let me = this;
	    let view = me.getView();
	    let also_registered = [];
	    value = Ext.isArray(value) ? value : value.split(';');
	    value.forEach(tag => {
		if (view.registered_tags.indexOf(tag) !== -1) {
		    also_registered.push(tag);
		}
	    });
	    let hint_field = me.lookup('hintField');
	    hint_field.setVisible(also_registered.length > 0);
	    if (also_registered.length > 0) {
		hint_field.setValue(`${view.hintText} ${also_registered.join(', ')}`);
	    }
	},
    },
    items: [
	{
	    xtype: 'inputpanel',
	    setValues: function(values) {
		this.up('pveUserTagAccessEdit').registered_tags = values?.['registered-tags'] ?? [];
		let data = values?.['user-tag-access'] ?? {};
		return Proxmox.panel.InputPanel.prototype.setValues.call(this, data);
	    },
	    onGetValues: function(values) {
		if (values === undefined || Object.keys(values).length === 0) {
		    return { 'delete': 'user-tag-access' };
		}
		return {
		    'user-tag-access': PVE.Parser.printPropertyString(values),
		};
	    },
	    items: [
		{
		    name: 'user-allow',
		    fieldLabel: gettext('Mode'),
		    xtype: 'proxmoxKVComboBox',
		    deleteEmpty: false,
		    value: '__default__',
		    comboItems: [
			['__default__', Proxmox.Utils.defaultText + ' (free)'],
			['free', 'free'],
			['existing', 'existing'],
			['list', 'list'],
			['none', 'none'],
		    ],
		    defaultValue: '__default__',
		},
		{
		    xtype: 'displayfield',
		    fieldLabel: gettext('Predefined Tags'),
		},
		{
		    name: 'user-allow-list',
		    xtype: 'pveListField',
		    emptyText: gettext('No Tags defined'),
		    fieldTitle: gettext('Tag'),
		    maskRe: PVE.Utils.tagCharRegex,
		    gridConfig: {
			height: 200,
			scrollable: true,
		    },
		    listeners: {
			change: 'tagChange',
		    },
		},
		{
		    hidden: true,
		    xtype: 'displayfield',
		    reference: 'hintField',
		    userCls: 'pmx-hint',
		},
	    ],
	},
    ],
});
Ext.define('PVE.dc.RegisteredTagsEdit', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveRegisteredTagEdit',
    subject: gettext('Registered Tags'),
    onlineHelp: 'datacenter_configuration_file',
    url: '/api2/extjs/cluster/options',
    hintText: gettext('NOTE: The following tags are also defined in the user allow list.'),
    controller: {
	xclass: 'Ext.app.ViewController',
	tagChange: function(field, value) {
	    let me = this;
	    let view = me.getView();
	    let also_allowed = [];
	    value = Ext.isArray(value) ? value : value.split(';');
	    value.forEach(tag => {
		if (view.allowed_tags.indexOf(tag) !== -1) {
		    also_allowed.push(tag);
		}
	    });
	    let hint_field = me.lookup('hintField');
	    hint_field.setVisible(also_allowed.length > 0);
	    if (also_allowed.length > 0) {
		hint_field.setValue(`${view.hintText} ${also_allowed.join(', ')}`);
	    }
	},
    },
    items: [
	{
	    xtype: 'inputpanel',
	    setValues: function(values) {
		let allowed_tags = values?.['user-tag-access']?.['user-allow-list'] ?? [];
		this.up('pveRegisteredTagEdit').allowed_tags = allowed_tags;
		let tags = values?.['registered-tags'];
		return Proxmox.panel.InputPanel.prototype.setValues.call(this, { tags });
	    },
	    onGetValues: function(values) {
		if (!values.tags) {
		    return {
			'delete': 'registered-tags',
		    };
		} else {
		    return {
			'registered-tags': values.tags,
		    };
		}
	    },
	    items: [
		{
		    name: 'tags',
		    xtype: 'pveListField',
		    maskRe: PVE.Utils.tagCharRegex,
		    gridConfig: {
			height: 200,
			scrollable: true,
			emptyText: gettext('No Tags defined'),
		    },
		    listeners: {
			change: 'tagChange',
		    },
		},
		{
		    hidden: true,
		    xtype: 'displayfield',
		    reference: 'hintField',
		    userCls: 'pmx-hint',
		},
	    ],
	},
    ],
});
Ext.define('PVE.lxc.CmdMenu', {
    extend: 'Ext.menu.Menu',
    showSeparator: false,
    initComponent: function() {
	let me = this;
	let info = me.pveSelNode.data;
	if (!info.node) {
	    throw "no node name specified";
	}
	if (!info.vmid) {
	    throw "no CT ID specified";
	}
	let vm_command = function(cmd, params) {
	    Proxmox.Utils.API2Request({
		params: params,
		url: `/nodes/${info.node}/${info.type}/${info.vmid}/status/${cmd}`,
		method: 'POST',
		failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
	    });
	};
	let confirmedVMCommand = (cmd, params) => {
	    let msg = Proxmox.Utils.format_task_description(`vz${cmd}`, info.vmid);
	    Ext.Msg.confirm(gettext('Confirm'), msg, btn => {
		if (btn === 'yes') {
		    vm_command(cmd, params);
		}
	    });
	};
	let caps = Ext.state.Manager.get('GuiCap');
	let standalone = PVE.data.ResourceStore.getNodes().length < 2;
	let running = false, stopped = true, suspended = false;
	switch (info.status) {
	    case 'running':
		running = true;
		stopped = false;
		break;
	    case 'paused':
		stopped = false;
		suspended = true;
		break;
	    default: break;
	}
	me.title = 'CT ' + info.vmid;
	me.items = [
	    {
		text: gettext('Start'),
		iconCls: 'fa fa-fw fa-play',
		disabled: running,
		handler: () => vm_command('start'),
	    },
	    {
		text: gettext('Shutdown'),
		iconCls: 'fa fa-fw fa-power-off',
		disabled: stopped || suspended,
		handler: () => confirmedVMCommand('shutdown'),
	    },
	    {
		text: gettext('Stop'),
		iconCls: 'fa fa-fw fa-stop',
		disabled: stopped,
		tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
		handler: () => confirmedVMCommand('stop'),
	    },
	    {
		text: gettext('Reboot'),
		iconCls: 'fa fa-fw fa-refresh',
		disabled: stopped,
		tooltip: Ext.String.format(gettext('Reboot {0}'), 'CT'),
		handler: () => confirmedVMCommand('reboot'),
	    },
	    {
		xtype: 'menuseparator',
		hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone'],
	    },
	    {
		text: gettext('Clone'),
		iconCls: 'fa fa-fw fa-clone',
		hidden: !caps.vms['VM.Clone'],
		handler: () => PVE.window.Clone.wrap(info.node, info.vmid, me.isTemplate, 'lxc'),
	    },
	    {
		text: gettext('Migrate'),
		iconCls: 'fa fa-fw fa-send-o',
		hidden: standalone || !caps.vms['VM.Migrate'],
		handler: function() {
		    Ext.create('PVE.window.Migrate', {
			vmtype: 'lxc',
			nodename: info.node,
			vmid: info.vmid,
			autoShow: true,
		    });
		},
	    },
	    {
		text: gettext('Convert to template'),
		iconCls: 'fa fa-fw fa-file-o',
		handler: function() {
		    let msg = Proxmox.Utils.format_task_description('vztemplate', info.vmid);
		    Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
			if (btn === 'yes') {
			    Proxmox.Utils.API2Request({
				url: `/nodes/${info.node}/lxc/${info.vmid}/template`,
				method: 'POST',
				failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus),
			    });
			}
		    });
		},
	    },
	    { xtype: 'menuseparator' },
	    {
		text: gettext('Console'),
		iconCls: 'fa fa-fw fa-terminal',
		handler: () =>
		    PVE.Utils.openDefaultConsoleWindow(true, 'lxc', info.vmid, info.node, info.vmname),
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.lxc.Config', {
    extend: 'PVE.panel.Config',
    alias: 'widget.pveLXCConfig',
    onlineHelp: 'chapter_pct',
    userCls: 'proxmox-tags-full',
    initComponent: function() {
        var me = this;
	var vm = me.pveSelNode.data;
	var nodename = vm.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = vm.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	var template = !!vm.template;
	var running = !!vm.uptime;
	var caps = Ext.state.Manager.get('GuiCap');
	var base_url = '/nodes/' + nodename + '/lxc/' + vmid;
	me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
	    url: '/api2/json' + base_url + '/status/current',
	    interval: 1000,
	});
	var vm_command = function(cmd, params) {
	    Proxmox.Utils.API2Request({
		params: params,
		url: base_url + "/status/" + cmd,
		waitMsgTarget: me,
		method: 'POST',
		failure: function(response, opts) {
		    Ext.Msg.alert('Error', response.htmlStatus);
		},
	    });
	};
	var startBtn = Ext.create('Ext.Button', {
	    text: gettext('Start'),
	    disabled: !caps.vms['VM.PowerMgmt'] || running,
	    hidden: template,
	    handler: function() {
		vm_command('start');
	    },
	    iconCls: 'fa fa-play',
	});
	var shutdownBtn = Ext.create('PVE.button.Split', {
	    text: gettext('Shutdown'),
	    disabled: !caps.vms['VM.PowerMgmt'] || !running,
	    hidden: template,
	    confirmMsg: Proxmox.Utils.format_task_description('vzshutdown', vmid),
	    handler: function() {
		vm_command('shutdown');
	    },
	    menu: {
		items: [{
		    text: gettext('Reboot'),
		    disabled: !caps.vms['VM.PowerMgmt'],
		    confirmMsg: Proxmox.Utils.format_task_description('vzreboot', vmid),
		    tooltip: Ext.String.format(gettext('Reboot {0}'), 'CT'),
		    handler: function() {
			vm_command("reboot");
		    },
		    iconCls: 'fa fa-refresh',
		},
		{
		    text: gettext('Stop'),
		    disabled: !caps.vms['VM.PowerMgmt'],
		    confirmMsg: Proxmox.Utils.format_task_description('vzstop', vmid),
		    tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
		    dangerous: true,
		    handler: function() {
			vm_command("stop");
		    },
		    iconCls: 'fa fa-stop',
		}],
	    },
	    iconCls: 'fa fa-power-off',
	});
	var migrateBtn = Ext.create('Ext.Button', {
	    text: gettext('Migrate'),
	    disabled: !caps.vms['VM.Migrate'],
	    hidden: PVE.data.ResourceStore.getNodes().length < 2,
	    handler: function() {
		var win = Ext.create('PVE.window.Migrate', {
		    vmtype: 'lxc',
		    nodename: nodename,
		    vmid: vmid,
		});
		win.show();
	    },
	    iconCls: 'fa fa-send-o',
	});
	var moreBtn = Ext.create('Proxmox.button.Button', {
	    text: gettext('More'),
	    menu: {
 items: [
		{
		    text: gettext('Clone'),
		    iconCls: 'fa fa-fw fa-clone',
		    hidden: !caps.vms['VM.Clone'],
		    handler: function() {
			PVE.window.Clone.wrap(nodename, vmid, template, 'lxc');
		    },
		},
		{
		    text: gettext('Convert to template'),
		    disabled: template,
		    xtype: 'pveMenuItem',
		    iconCls: 'fa fa-fw fa-file-o',
		    hidden: !caps.vms['VM.Allocate'],
		    confirmMsg: Proxmox.Utils.format_task_description('vztemplate', vmid),
		    handler: function() {
			Proxmox.Utils.API2Request({
			    url: base_url + '/template',
			    waitMsgTarget: me,
			    method: 'POST',
			    failure: function(response, opts) {
				Ext.Msg.alert('Error', response.htmlStatus);
			    },
			});
		    },
		},
		{
		    iconCls: 'fa fa-heartbeat ',
		    hidden: !caps.nodes['Sys.Console'],
		    text: gettext('Manage HA'),
		    handler: function() {
			var ha = vm.hastate;
			Ext.create('PVE.ha.VMResourceEdit', {
			    vmid: vmid,
			    guestType: 'ct',
			    isCreate: !ha || ha === 'unmanaged',
			}).show();
		    },
		},
		{
		    text: gettext('Remove'),
		    disabled: !caps.vms['VM.Allocate'],
		    itemId: 'removeBtn',
		    handler: function() {
			Ext.create('PVE.window.SafeDestroyGuest', {
			    url: base_url,
			    item: { type: 'CT', id: vmid },
			    taskName: 'vzdestroy',
			}).show();
		    },
		    iconCls: 'fa fa-trash-o',
		},
	    ],
},
	});
	var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
	    disabled: !caps.vms['VM.Console'],
	    consoleType: 'lxc',
	    consoleName: vm.name,
	    hidden: template,
	    nodename: nodename,
	    vmid: vmid,
	});
	var statusTxt = Ext.create('Ext.toolbar.TextItem', {
	    data: {
		lock: undefined,
	    },
	    tpl: [
		'',
		' ({lock})',
		'',
	    ],
	});
	let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
	    tags: vm.tags,
	    canEdit: !!caps.vms['VM.Config.Options'],
	    listeners: {
		change: function(tags) {
		    Proxmox.Utils.API2Request({
			url: base_url + '/config',
			method: 'PUT',
			params: {
			    tags,
			},
			success: function() {
			    me.statusStore.load();
			},
			failure: function(response) {
			    Ext.Msg.alert('Error', response.htmlStatus);
			    me.statusStore.load();
			},
		    });
		},
	    },
	});
	let vm_text = `${vm.vmid} (${vm.name})`;
	Ext.apply(me, {
	    title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm_text, nodename),
	    hstateid: 'lxctab',
	    tbarSpacing: false,
	    tbar: [statusTxt, tagsContainer, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
	    defaults: { statusStore: me.statusStore },
	    items: [
		{
		    title: gettext('Summary'),
		    xtype: 'pveGuestSummary',
		    iconCls: 'fa fa-book',
		    itemId: 'summary',
		},
	    ],
	});
	if (caps.vms['VM.Console'] && !template) {
	    me.items.push(
		{
		    title: gettext('Console'),
		    itemId: 'consolejs',
		    iconCls: 'fa fa-terminal',
		    xtype: 'pveNoVncConsole',
		    vmid: vmid,
		    consoleType: 'lxc',
		    xtermjs: true,
		    nodename: nodename,
		},
	    );
	}
	me.items.push(
	    {
		title: gettext('Resources'),
		itemId: 'resources',
		expandedOnInit: true,
		iconCls: 'fa fa-cube',
		xtype: 'pveLxcRessourceView',
	    },
	    {
		title: gettext('Network'),
		iconCls: 'fa fa-exchange',
		itemId: 'network',
		xtype: 'pveLxcNetworkView',
	    },
	    {
		title: gettext('DNS'),
		iconCls: 'fa fa-globe',
		itemId: 'dns',
		xtype: 'pveLxcDNS',
	    },
	    {
		title: gettext('Options'),
		itemId: 'options',
		iconCls: 'fa fa-gear',
		xtype: 'pveLxcOptions',
	    },
	    {
		title: gettext('Task History'),
		itemId: 'tasks',
		iconCls: 'fa fa-list-alt',
		xtype: 'proxmoxNodeTasks',
		nodename: nodename,
		preFilter: {
		    vmid,
		},
	    },
	);
	if (caps.vms['VM.Backup']) {
	    me.items.push({
		title: gettext('Backup'),
		iconCls: 'fa fa-floppy-o',
		xtype: 'pveBackupView',
		itemId: 'backup',
	    },
	    {
		title: gettext('Replication'),
		iconCls: 'fa fa-retweet',
		xtype: 'pveReplicaView',
		itemId: 'replication',
	    });
	}
	if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback'] ||
	    caps.vms['VM.Audit']) && !template) {
	    me.items.push({
		title: gettext('Snapshots'),
		iconCls: 'fa fa-history',
		xtype: 'pveGuestSnapshotTree',
		type: 'lxc',
		itemId: 'snapshot',
	    });
	}
	if (caps.vms['VM.Console']) {
	    me.items.push(
		{
		    xtype: 'pveFirewallRules',
		    title: gettext('Firewall'),
		    iconCls: 'fa fa-shield',
		    allow_iface: true,
		    base_url: base_url + '/firewall/rules',
		    list_refs_url: base_url + '/firewall/refs',
		    itemId: 'firewall',
		},
		{
		    xtype: 'pveFirewallOptions',
		    groups: ['firewall'],
		    iconCls: 'fa fa-gear',
		    onlineHelp: 'pve_firewall_vm_container_configuration',
		    title: gettext('Options'),
		    base_url: base_url + '/firewall/options',
		    fwtype: 'vm',
		    itemId: 'firewall-options',
		},
		{
		    xtype: 'pveFirewallAliases',
		    title: gettext('Alias'),
		    groups: ['firewall'],
		    iconCls: 'fa fa-external-link',
		    base_url: base_url + '/firewall/aliases',
		    itemId: 'firewall-aliases',
		},
		{
		    xtype: 'pveIPSet',
		    title: gettext('IPSet'),
		    groups: ['firewall'],
		    iconCls: 'fa fa-list-ol',
		    base_url: base_url + '/firewall/ipset',
		    list_refs_url: base_url + '/firewall/refs',
		    itemId: 'firewall-ipset',
		},
		{
		    title: gettext('Log'),
		    groups: ['firewall'],
		    iconCls: 'fa fa-list',
		    onlineHelp: 'chapter_pve_firewall',
		    itemId: 'firewall-fwlog',
		    xtype: 'proxmoxLogView',
		    url: '/api2/extjs' + base_url + '/firewall/log',
		},
	    );
	}
	if (caps.vms['Permissions.Modify']) {
	    me.items.push({
		xtype: 'pveACLView',
		title: gettext('Permissions'),
		itemId: 'permissions',
		iconCls: 'fa fa-unlock',
		path: '/vms/' + vmid,
	    });
	}
	me.callParent();
	var prevStatus = 'unknown';
	me.mon(me.statusStore, 'load', function(s, records, success) {
	    var status;
	    var lock;
	    var rec;
	    if (!success) {
		status = 'unknown';
	    } else {
		rec = s.data.get('status');
		status = rec ? rec.data.value : 'unknown';
		rec = s.data.get('template');
		template = rec ? rec.data.value : false;
		rec = s.data.get('lock');
		lock = rec ? rec.data.value : undefined;
	    }
	    statusTxt.update({ lock: lock });
	    rec = s.data.get('tags');
	    tagsContainer.loadTags(rec?.data?.value);
	    startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template);
	    shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
	    me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
	    consoleBtn.setDisabled(template);
	    if (prevStatus === 'stopped' && status === 'running') {
		let con = me.down('#consolejs');
		if (con) {
		    con.reload();
		}
	    }
	    prevStatus = status;
	});
	me.on('afterrender', function() {
	    me.statusStore.startUpdate();
	});
	me.on('destroy', function() {
	    me.statusStore.stopUpdate();
	});
    },
});
Ext.define('PVE.lxc.CreateWizard', {
    extend: 'PVE.window.Wizard',
    mixins: ['Proxmox.Mixin.CBind'],
    viewModel: {
	data: {
	    nodename: '',
	    storage: '',
	    unprivileged: true,
	},
	formulas: {
	    cgroupMode: function(get) {
		const nodeInfo = PVE.data.ResourceStore.getNodes().find(
		    node => node.node === get('nodename'),
		);
		return nodeInfo ? nodeInfo['cgroup-mode'] : 2;
	    },
	},
    },
    cbindData: {
	nodename: undefined,
    },
    subject: gettext('LXC Container'),
    items: [
	{
	    xtype: 'inputpanel',
	    title: gettext('General'),
	    onlineHelp: 'pct_general',
	    column1: [
		{
		    xtype: 'pveNodeSelector',
		    name: 'nodename',
		    cbind: {
			selectCurNode: '{!nodename}',
			preferredValue: '{nodename}',
		    },
		    bind: {
			value: '{nodename}',
		    },
		    fieldLabel: gettext('Node'),
		    allowBlank: false,
		    onlineValidator: true,
		},
		{
		    xtype: 'pveGuestIDSelector',
		    name: 'vmid', // backend only knows vmid
		    guestType: 'lxc',
		    value: '',
		    loadNextFreeID: true,
		    validateExists: false,
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'hostname',
		    vtype: 'DnsName',
		    value: '',
		    fieldLabel: gettext('Hostname'),
		    skipEmptyText: true,
		    allowBlank: true,
		},
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'unprivileged',
		    value: true,
		    bind: {
			value: '{unprivileged}',
		    },
		    fieldLabel: gettext('Unprivileged container'),
		},
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'features',
		    inputValue: 'nesting=1',
		    value: true,
		    bind: {
			disabled: '{!unprivileged}',
		    },
		    fieldLabel: gettext('Nesting'),
		},
	    ],
	    column2: [
		{
		    xtype: 'pvePoolSelector',
		    fieldLabel: gettext('Resource Pool'),
		    name: 'pool',
		    value: '',
		    allowBlank: true,
		},
		{
		    xtype: 'textfield',
		    inputType: 'password',
		    name: 'password',
		    value: '',
		    fieldLabel: gettext('Password'),
		    allowBlank: false,
		    minLength: 5,
		    change: function(f, value) {
			if (f.rendered) {
			    f.up().down('field[name=confirmpw]').validate();
			}
		    },
		},
		{
		    xtype: 'textfield',
		    inputType: 'password',
		    name: 'confirmpw',
		    value: '',
		    fieldLabel: gettext('Confirm password'),
		    allowBlank: true,
		    submitValue: false,
		    validator: function(value) {
			var pw = this.up().down('field[name=password]').getValue();
			if (pw !== value) {
			    return "Passwords do not match!";
			}
			return true;
		    },
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'ssh-public-keys',
		    value: '',
		    fieldLabel: gettext('SSH public key'),
		    allowBlank: true,
		    validator: function(value) {
			let pwfield = this.up().down('field[name=password]');
			if (value.length) {
			    let key = PVE.Parser.parseSSHKey(value);
			    if (!key) {
				return "Failed to recognize ssh key";
			    }
			    pwfield.allowBlank = true;
			} else {
			    pwfield.allowBlank = false;
			}
			pwfield.validate();
			return true;
		    },
		    afterRender: function() {
			if (!window.FileReader) {
			    return; // No FileReader support in this browser
			}
			let cancelEvent = ev => {
			    ev = ev.event;
			    if (ev.preventDefault) {
				ev.preventDefault();
			    }
			};
			this.inputEl.on('dragover', cancelEvent);
			this.inputEl.on('dragenter', cancelEvent);
			this.inputEl.on('drop', ev => {
			    cancelEvent(ev);
			    let files = ev.event.dataTransfer.files;
			    PVE.Utils.loadSSHKeyFromFile(files[0], v => this.setValue(v));
			});
		    },
		},
		{
		    xtype: 'filebutton',
		    name: 'file',
		    hidden: !window.FileReader,
		    text: gettext('Load SSH Key File'),
		    listeners: {
			change: function(btn, e, value) {
			    e = e.event;
			    let field = this.up().down('proxmoxtextfield[name=ssh-public-keys]');
			    PVE.Utils.loadSSHKeyFromFile(e.target.files[0], v => field.setValue(v));
			    btn.reset();
			},
		    },
		},
	    ],
	},
	{
	    xtype: 'inputpanel',
	    title: gettext('Template'),
	    onlineHelp: 'pct_container_images',
	    column1: [
		{
		    xtype: 'pveStorageSelector',
		    name: 'tmplstorage',
		    fieldLabel: gettext('Storage'),
		    storageContent: 'vztmpl',
		    autoSelect: true,
		    allowBlank: false,
		    bind: {
			value: '{storage}',
			nodename: '{nodename}',
		    },
		},
		{
		    xtype: 'pveFileSelector',
		    name: 'ostemplate',
		    storageContent: 'vztmpl',
		    fieldLabel: gettext('Template'),
		    bind: {
			storage: '{storage}',
			nodename: '{nodename}',
		    },
		    allowBlank: false,
		},
	    ],
	},
	{
	    xtype: 'pveMultiMPPanel',
	    title: gettext('Disks'),
	    insideWizard: true,
	    isCreate: true,
	    unused: false,
	    confid: 'rootfs',
	},
	{
	    xtype: 'pveLxcCPUInputPanel',
	    title: gettext('CPU'),
	    insideWizard: true,
	},
	{
	    xtype: 'pveLxcMemoryInputPanel',
	    title: gettext('Memory'),
	    insideWizard: true,
	},
	{
	    xtype: 'pveLxcNetworkInputPanel',
	    title: gettext('Network'),
	    insideWizard: true,
	    bind: {
		nodename: '{nodename}',
	    },
	    isCreate: true,
	},
	{
	    xtype: 'pveLxcDNSInputPanel',
	    title: gettext('DNS'),
	    insideWizard: true,
	},
	{
	    title: gettext('Confirm'),
	    layout: 'fit',
	    items: [
		{
		    xtype: 'grid',
		    store: {
			model: 'KeyValue',
			sorters: [{
				property: 'key',
				direction: 'ASC',
			}],
		    },
		    columns: [
			{ header: 'Key', width: 150, dataIndex: 'key' },
			{ header: 'Value', flex: 1, dataIndex: 'value' },
		    ],
		},
	    ],
	    dockedItems: [
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'start',
		    dock: 'bottom',
		    margin: '5 0 0 0',
		    boxLabel: gettext('Start after created'),
		},
	    ],
	    listeners: {
		show: function(panel) {
		    let wizard = this.up('window');
		    let kv = wizard.getValues();
		    let data = [];
		    Ext.Object.each(kv, function(key, value) {
			if (key === 'delete' || key === 'tmplstorage') { // ignore
			    return;
			}
			if (key === 'password') { // don't show pw
			    return;
			}
			data.push({ key: key, value: value });
		    });
		    let summaryStore = panel.down('grid').getStore();
		    summaryStore.suspendEvents();
		    summaryStore.removeAll();
		    summaryStore.add(data);
		    summaryStore.sort();
		    summaryStore.resumeEvents();
		    summaryStore.fireEvent('refresh');
		},
	    },
	    onSubmit: function() {
		let wizard = this.up('window');
		let kv = wizard.getValues();
		delete kv.delete;
		let nodename = kv.nodename;
		delete kv.nodename;
		delete kv.tmplstorage;
		if (!kv.pool.length) {
		    delete kv.pool;
		}
		if (!kv.password.length && kv['ssh-public-keys']) {
		    delete kv.password;
		}
		Proxmox.Utils.API2Request({
		    url: `/nodes/${nodename}/lxc`,
		    waitMsgTarget: wizard,
		    method: 'POST',
		    params: kv,
		    success: function(response, opts) {
			Ext.create('Proxmox.window.TaskViewer', {
			    autoShow: true,
			    upid: response.result.data,
			});
			wizard.close();
		    },
		    failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
		});
	    },
	},
    ],
});
Ext.define('PVE.lxc.DNSInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveLxcDNSInputPanel',
    insideWizard: false,
    onGetValues: function(values) {
	var me = this;
	var deletes = [];
	if (!values.searchdomain && !me.insideWizard) {
	    deletes.push('searchdomain');
	}
	if (values.nameserver) {
	    let list = values.nameserver.split(/[ ,;]+/);
	    values.nameserver = list.join(' ');
	} else if (!me.insideWizard) {
	    deletes.push('nameserver');
	}
	if (deletes.length) {
	    values.delete = deletes.join(',');
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	var items = [
	    {
		xtype: 'proxmoxtextfield',
		name: 'searchdomain',
		skipEmptyText: true,
		fieldLabel: gettext('DNS domain'),
		emptyText: gettext('use host settings'),
		allowBlank: true,
	    },
	    {
		xtype: 'proxmoxtextfield',
		fieldLabel: gettext('DNS servers'),
		vtype: 'IP64AddressWithSuffixList',
		allowBlank: true,
		emptyText: gettext('use host settings'),
		name: 'nameserver',
		itemId: 'nameserver',
	    },
	];
	if (me.insideWizard) {
	    me.column1 = items;
	} else {
	    me.items = items;
	}
	me.callParent();
    },
});
Ext.define('PVE.lxc.DNSEdit', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	var ipanel = Ext.create('PVE.lxc.DNSInputPanel');
	Ext.apply(me, {
	    subject: gettext('Resources'),
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    if (values.nameserver) {
			values.nameserver.replace(/[,;]/, ' ');
			values.nameserver.replace(/^\s+/, '');
		    }
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.lxc.DNS', {
    extend: 'Proxmox.grid.PendingObjectGrid',
    alias: ['widget.pveLxcDNS'],
    onlineHelp: 'pct_container_network',
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = me.pveSelNode.data.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	var caps = Ext.state.Manager.get('GuiCap');
	var rows = {
	    hostname: {
		required: true,
		defaultValue: me.pveSelNode.data.name,
		header: gettext('Hostname'),
		editor: caps.vms['VM.Config.Network'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Hostname'),
		    items: {
			xtype: 'inputpanel',
			items: {
			    fieldLabel: gettext('Hostname'),
			    xtype: 'textfield',
			    name: 'hostname',
			    vtype: 'DnsName',
			    allowBlank: true,
			    emptyText: 'CT' + vmid.toString(),
			},
			onGetValues: function(values) {
			    var params = values;
			    if (values.hostname === undefined ||
				values.hostname === null ||
				values.hostname === '') {
				params = { hostname: 'CT'+vmid.toString() };
			    }
			    return params;
			},
		    },
		} : undefined,
	    },
	    searchdomain: {
		header: gettext('DNS domain'),
		defaultValue: '',
		editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
		renderer: function(value) {
		    return value || gettext('use host settings');
		},
	    },
	    nameserver: {
		header: gettext('DNS server'),
		defaultValue: '',
		editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
		renderer: function(value) {
		    return value || gettext('use host settings');
		},
	    },
	};
	var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
	var reload = function() {
	    me.rstore.load();
	};
	var sm = Ext.create('Ext.selection.RowModel', {});
	var run_editor = function() {
	    var rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    var rowdef = rows[rec.data.key];
	    if (!rowdef.editor) {
		return;
	    }
	    var win;
	    if (Ext.isString(rowdef.editor)) {
		win = Ext.create(rowdef.editor, {
		    pveSelNode: me.pveSelNode,
		    confid: rec.data.key,
		    url: '/api2/extjs/nodes/' + nodename + '/lxc/' + vmid + '/config',
		});
	    } else {
		var config = Ext.apply({
		    pveSelNode: me.pveSelNode,
		    confid: rec.data.key,
		    url: '/api2/extjs/nodes/' + nodename + '/lxc/' + vmid + '/config',
		}, rowdef.editor);
		win = Ext.createWidget(rowdef.editor.xtype, config);
		win.load();
	    }
	    //win.load();
	    win.show();
	    win.on('destroy', reload);
	};
	var edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    enableFn: function(rec) {
		var rowdef = rows[rec.data.key];
		return !!rowdef.editor;
	    },
	    handler: run_editor,
	});
	var revert_btn = new PVE.button.PendingRevert();
	var set_button_status = function() {
	    let button_sm = me.getSelectionModel();
	    let rec = button_sm.getSelection()[0];
	    if (!rec) {
		edit_btn.disable();
		return;
	    }
	    let key = rec.data.key;
	    let rowdef = rows[key];
	    edit_btn.setDisabled(!rowdef.editor);
	    let pending = rec.data.delete || me.hasPendingChanges(key);
	    revert_btn.setDisabled(!pending);
	};
	Ext.apply(me, {
	    url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/pending",
	    selModel: sm,
	    cwidth1: 150,
	    interval: 5000,
	    run_editor: run_editor,
	    tbar: [edit_btn, revert_btn],
	    rows: rows,
	    editorConfig: {
		url: "/api2/extjs/" + baseurl,
	    },
	    listeners: {
		itemdblclick: run_editor,
		selectionchange: set_button_status,
		activate: reload,
	    },
	});
	me.callParent();
	me.on('activate', me.rstore.startUpdate);
	me.on('destroy', me.rstore.stopUpdate);
	me.on('deactivate', me.rstore.stopUpdate);
	me.mon(me.getStore(), 'datachanged', function() {
	    set_button_status();
	});
    },
});
Ext.define('PVE.lxc.FeaturesInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveLxcFeaturesInputPanel',
    // used to save the mounts fstypes until sending
    mounts: [],
    fstypes: ['nfs', 'cifs'],
    viewModel: {
	parent: null,
	data: {
	    unprivileged: false,
	},
	formulas: {
	    privilegedOnly: function(get) {
		return get('unprivileged') ? gettext('privileged only') : '';
	    },
	    unprivilegedOnly: function(get) {
		return !get('unprivileged') ? gettext('unprivileged only') : '';
	    },
	},
    },
    items: [
	{
	    xtype: 'proxmoxcheckbox',
	    fieldLabel: gettext('keyctl'),
	    name: 'keyctl',
	    bind: {
		disabled: '{!unprivileged}',
		boxLabel: '{unprivilegedOnly}',
	    },
	},
	{
	    xtype: 'proxmoxcheckbox',
	    fieldLabel: gettext('Nesting'),
	    name: 'nesting',
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'nfs',
	    fieldLabel: 'NFS',
	    bind: {
		disabled: '{unprivileged}',
		boxLabel: '{privilegedOnly}',
	    },
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'cifs',
	    fieldLabel: 'SMB/CIFS',
	    bind: {
		disabled: '{unprivileged}',
		boxLabel: '{privilegedOnly}',
	    },
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'fuse',
	    fieldLabel: 'FUSE',
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'mknod',
	    fieldLabel: gettext('Create Device Nodes'),
	    boxLabel: gettext('Experimental'),
	},
    ],
    onGetValues: function(values) {
	var me = this;
	var mounts = me.mounts;
	me.fstypes.forEach(function(fs) {
	    if (values[fs]) {
		mounts.push(fs);
	    }
	    delete values[fs];
	});
	if (mounts.length) {
	    values.mount = mounts.join(';');
	}
	var featuresstring = PVE.Parser.printPropertyString(values, undefined);
	if (featuresstring === '') {
	    return { 'delete': 'features' };
	}
	return { features: featuresstring };
    },
    setValues: function(values) {
	var me = this;
	me.viewModel.set('unprivileged', values.unprivileged);
	if (values.features) {
	    var res = PVE.Parser.parsePropertyString(values.features);
	    me.mounts = [];
	    if (res.mount) {
		res.mount.split(/[; ]/).forEach(function(item) {
		    if (me.fstypes.indexOf(item) === -1) {
			me.mounts.push(item);
		    } else {
			res[item] = 1;
		    }
		});
	    }
	    this.callParent([res]);
	}
    },
    initComponent: function() {
	let me = this;
	me.mounts = []; // reset state
	me.callParent();
    },
});
Ext.define('PVE.lxc.FeaturesEdit', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveLxcFeaturesEdit',
    subject: gettext('Features'),
    autoLoad: true,
    width: 350,
    items: [{
	xtype: 'pveLxcFeaturesInputPanel',
    }],
});
Ext.define('PVE.lxc.MountPointInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveLxcMountPointInputPanel',
    onlineHelp: 'pct_container_storage',
    insideWizard: false,
    unused: false, // add unused disk imaged
    unprivileged: false,
    vmconfig: {}, // used to select unused disks
    setUnprivileged: function(unprivileged) {
	var me = this;
	var vm = me.getViewModel();
	me.unprivileged = unprivileged;
	vm.set('unpriv', unprivileged);
    },
    onGetValues: function(values) {
	var me = this;
	var confid = me.confid || "mp"+values.mpid;
	me.mp.file = me.down('field[name=file]').getValue();
	if (me.unused) {
	    confid = "mp"+values.mpid;
	} else if (me.isCreate) {
	    me.mp.file = values.hdstorage + ':' + values.disksize;
	}
	// delete unnecessary fields
	delete values.mpid;
	delete values.hdstorage;
	delete values.disksize;
	delete values.diskformat;
	let setMPOpt = (k, src, v) => PVE.Utils.propertyStringSet(me.mp, src, k, v);
	setMPOpt('mp', values.mp);
	let mountOpts = (values.mountoptions || []).join(';');
	setMPOpt('mountoptions', values.mountoptions, mountOpts);
	setMPOpt('mp', values.mp);
	setMPOpt('backup', values.backup);
	setMPOpt('quota', values.quota);
	setMPOpt('ro', values.ro);
	setMPOpt('acl', values.acl);
	setMPOpt('replicate', values.replicate);
	let res = {};
	res[confid] = PVE.Parser.printLxcMountPoint(me.mp);
	return res;
    },
    setMountPoint: function(mp) {
	let me = this;
	let vm = me.getViewModel();
	vm.set('mptype', mp.type);
	if (mp.mountoptions) {
	    mp.mountoptions = mp.mountoptions.split(';');
	}
	me.mp = mp;
	me.filterMountOptions();
	me.setValues(mp);
    },
    filterMountOptions: function() {
	let me = this;
	if (me.confid === 'rootfs') {
	    let field = me.down('field[name=mountoptions]');
	    let exclude = ['nodev', 'noexec'];
	    let filtered = field.comboItems.filter(v => !exclude.includes(v[0]));
	    field.setComboItems(filtered);
	}
    },
    updateVMConfig: function(vmconfig) {
	let me = this;
	let vm = me.getViewModel();
	me.vmconfig = vmconfig;
	vm.set('unpriv', vmconfig.unprivileged);
	me.down('field[name=mpid]').validate();
    },
    setVMConfig: function(vmconfig) {
	let me = this;
	me.updateVMConfig(vmconfig);
	PVE.Utils.forEachMP((bus, i) => {
	    let name = "mp" + i.toString();
	    if (!Ext.isDefined(vmconfig[name])) {
		me.down('field[name=mpid]').setValue(i);
		return false;
	    }
	    return undefined;
	});
    },
    setNodename: function(nodename) {
	let me = this;
	let vm = me.getViewModel();
	vm.set('node', nodename);
	me.down('#diskstorage').setNodename(nodename);
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    'field[name=mpid]': {
		change: function(field, value) {
		    let me = this;
		    let view = this.getView();
		    if (view.confid !== 'rootfs') {
			view.fireEvent('diskidchange', view, `mp${value}`);
		    }
		    field.validate();
		},
	    },
	    '#hdstorage': {
		change: function(field, newValue) {
		    let me = this;
		    if (!newValue) {
			return;
		    }
		    let rec = field.store.getById(newValue);
		    if (!rec) {
			return;
		    }
		    me.getViewModel().set('type', rec.data.type);
		},
	    },
	},
	init: function(view) {
	    let me = this;
	    let vm = this.getViewModel();
	    view.mp = {};
	    vm.set('confid', view.confid);
	    vm.set('unused', view.unused);
	    vm.set('node', view.nodename);
	    vm.set('unpriv', view.unprivileged);
	    vm.set('hideStorSelector', view.unused || !view.isCreate);
	    if (view.isCreate) { // can be array if created from unused disk
		vm.set('isIncludedInBackup', true);
		if (view.insideWizard) {
		    view.filterMountOptions();
		}
	    }
	    if (view.selectFree) {
		view.setVMConfig(view.vmconfig);
	    }
	},
    },
    viewModel: {
	data: {
	    unpriv: false,
	    unused: false,
	    showStorageSelector: false,
	    mptype: '',
	    type: '',
	    confid: '',
	    node: '',
	},
	formulas: {
	    quota: function(get) {
		return !(get('type') === 'zfs' ||
			 get('type') === 'zfspool' ||
			 get('unpriv') ||
			 get('isBind'));
	    },
	    hasMP: function(get) {
		return !!get('confid') && !get('unused');
	    },
	    isRoot: function(get) {
		return get('confid') === 'rootfs';
	    },
	    isBind: function(get) {
		return get('mptype') === 'bind';
	    },
	    isBindOrRoot: function(get) {
		return get('isBind') || get('isRoot');
	    },
	},
    },
    column1: [
	{
	    xtype: 'proxmoxintegerfield',
	    name: 'mpid',
	    fieldLabel: gettext('Mount Point ID'),
	    minValue: 0,
	    maxValue: PVE.Utils.mp_counts.mp - 1,
	    hidden: true,
	    allowBlank: false,
	    disabled: true,
	    bind: {
		hidden: '{hasMP}',
		disabled: '{hasMP}',
	    },
	    validator: function(value) {
		let view = this.up('inputpanel');
		if (!view.rendered) {
		    return undefined;
		}
		if (Ext.isDefined(view.vmconfig["mp"+value])) {
		    return "Mount point is already in use.";
		}
		return true;
	    },
	},
	{
	    xtype: 'pveDiskStorageSelector',
	    itemId: 'diskstorage',
	    storageContent: 'rootdir',
	    hidden: true,
	    autoSelect: true,
	    selectformat: false,
	    defaultSize: 8,
	    bind: {
		hidden: '{hideStorSelector}',
		disabled: '{hideStorSelector}',
		nodename: '{node}',
	    },
	},
	{
	    xtype: 'textfield',
	    disabled: true,
	    submitValue: false,
	    fieldLabel: gettext('Disk image'),
	    name: 'file',
	    bind: {
		hidden: '{!hideStorSelector}',
	    },
	},
    ],
    column2: [
	{
	    xtype: 'textfield',
	    name: 'mp',
	    value: '',
	    emptyText: gettext('/some/path'),
	    allowBlank: false,
	    disabled: true,
	    fieldLabel: gettext('Path'),
	    bind: {
		hidden: '{isRoot}',
		disabled: '{isRoot}',
	    },
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'backup',
	    fieldLabel: gettext('Backup'),
	    autoEl: {
		tag: 'div',
		'data-qtip': gettext('Include volume in backup job'),
	    },
	    bind: {
		hidden: '{isRoot}',
		disabled: '{isBindOrRoot}',
		value: '{isIncludedInBackup}',
	    },
	},
    ],
    advancedColumn1: [
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'quota',
	    defaultValue: 0,
	    bind: {
		disabled: '{!quota}',
	    },
	    fieldLabel: gettext('Enable quota'),
	    listeners: {
		disable: function() {
		    this.reset();
		},
	    },
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'ro',
	    defaultValue: 0,
	    bind: {
		hidden: '{isRoot}',
		disabled: '{isRoot}',
	    },
	    fieldLabel: gettext('Read-only'),
	},
	{
	    xtype: 'proxmoxKVComboBox',
	    name: 'mountoptions',
	    fieldLabel: gettext('Mount options'),
	    deleteEmpty: false,
	    comboItems: [
		['lazytime', 'lazytime'],
		['noatime', 'noatime'],
		['nodev', 'nodev'],
		['noexec', 'noexec'],
		['nosuid', 'nosuid'],
	    ],
	    multiSelect: true,
	    value: [],
	    allowBlank: true,
	},
    ],
    advancedColumn2: [
	{
	    xtype: 'proxmoxKVComboBox',
	    name: 'acl',
	    fieldLabel: 'ACLs',
	    deleteEmpty: false,
	    comboItems: [
		['__default__', Proxmox.Utils.defaultText],
		['1', Proxmox.Utils.enabledText],
		['0', Proxmox.Utils.disabledText],
	    ],
	    value: '__default__',
	    bind: {
		disabled: '{isBind}',
	    },
	    allowBlank: true,
	},
	{
	    xtype: 'proxmoxcheckbox',
	    inputValue: '0', // reverses the logic
	    name: 'replicate',
	    fieldLabel: gettext('Skip replication'),
	},
    ],
});
Ext.define('PVE.lxc.MountPointEdit', {
    extend: 'Proxmox.window.Edit',
    unprivileged: false,
    initComponent: function() {
	let me = this;
	let nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	let unused = me.confid && me.confid.match(/^unused\d+$/);
	me.isCreate = me.confid ? unused : true;
	let ipanel = Ext.create('PVE.lxc.MountPointInputPanel', {
	    confid: me.confid,
	    nodename: nodename,
	    unused: unused,
	    unprivileged: me.unprivileged,
	    isCreate: me.isCreate,
	});
	let subject;
	if (unused) {
	    subject = gettext('Unused Disk');
	} else if (me.isCreate) {
	    subject = gettext('Mount Point');
	} else {
	    subject = gettext('Mount Point') + ' (' + me.confid + ')';
	}
	Ext.apply(me, {
	    subject: subject,
	    defaultFocus: me.confid !== 'rootfs' ? 'textfield[name=mp]' : 'tool',
	    items: ipanel,
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		ipanel.setVMConfig(response.result.data);
		if (me.confid) {
		    let value = response.result.data[me.confid];
		    let mp = PVE.Parser.parseLxcMountPoint(value);
		    if (!mp) {
			Ext.Msg.alert(gettext('Error'), 'Unable to parse mount point options');
			me.close();
			return;
		    }
		    ipanel.setMountPoint(mp);
		    me.isValid(); // trigger validation
		}
	    },
	});
    },
});
Ext.define('PVE.window.MPResize', {
    extend: 'Ext.window.Window',
    resizable: false,
    resize_disk: function(disk, size) {
	var me = this;
        var params = { disk: disk, size: '+' + size + 'G' };
	Proxmox.Utils.API2Request({
	    params: params,
	    url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/resize',
	    waitMsgTarget: me,
	    method: 'PUT',
	    failure: function(response, opts) {
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
	    },
	    success: function(response, opts) {
		var upid = response.result.data;
		var win = Ext.create('Proxmox.window.TaskViewer', { upid: upid });
		win.show();
		me.close();
	    },
	});
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.vmid) {
	    throw "no VM ID specified";
	}
	var items = [
	    {
		xtype: 'displayfield',
		name: 'disk',
		value: me.disk,
		fieldLabel: gettext('Disk'),
		vtype: 'StorageId',
		allowBlank: false,
	    },
	];
	me.hdsizesel = Ext.createWidget('numberfield', {
	    name: 'size',
	    minValue: 0,
	    maxValue: 128*1024,
	    decimalPrecision: 3,
	    value: '0',
	    fieldLabel: gettext('Size Increment') + ' (GiB)',
	    allowBlank: false,
	});
	items.push(me.hdsizesel);
	me.formPanel = Ext.create('Ext.form.Panel', {
	    bodyPadding: 10,
	    border: false,
	    fieldDefaults: {
		labelWidth: 120,
		anchor: '100%',
	    },
	    items: items,
	});
	var form = me.formPanel.getForm();
	var submitBtn;
	me.title = gettext('Resize disk');
	submitBtn = Ext.create('Ext.Button', {
	    text: gettext('Resize disk'),
	    handler: function() {
		if (form.isValid()) {
		    var values = form.getValues();
		    me.resize_disk(me.disk, values.size);
		}
	    },
	});
	Ext.apply(me, {
	    modal: true,
	    border: false,
	    layout: 'fit',
	    buttons: [submitBtn],
	    items: [me.formPanel],
	});
	me.callParent();
    },
});
Ext.define('PVE.lxc.NetworkInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveLxcNetworkInputPanel',
    insideWizard: false,
    onlineHelp: 'pct_container_network',
    setNodename: function(nodename) {
	let me = this;
	if (!nodename || me.nodename === nodename) {
	    return;
	}
	me.nodename = nodename;
	let bridgeSelector = me.query("[isFormField][name=bridge]")[0];
	bridgeSelector.setNodename(nodename);
    },
    onGetValues: function(values) {
	let me = this;
	let id;
	if (me.isCreate) {
	    id = values.id;
	    delete values.id;
	} else {
	    id = me.ifname;
	}
	let newdata = {};
	if (id) {
	    if (values.ipv6mode !== 'static') {
		values.ip6 = values.ipv6mode;
	    }
	    if (values.ipv4mode !== 'static') {
		values.ip = values.ipv4mode;
	    }
	    newdata[id] = PVE.Parser.printLxcNetwork(values);
	}
	return newdata;
    },
    initComponent: function() {
	let me = this;
	let cdata = {};
	if (me.insideWizard) {
	    me.ifname = 'net0';
	    cdata.name = 'eth0';
	    me.dataCache = {};
	}
	cdata.firewall = me.insideWizard || me.isCreate;
	if (!me.dataCache) {
	    throw "no dataCache specified";
	}
	if (!me.isCreate) {
	    if (!me.ifname) {
		throw "no interface name specified";
	    }
	    if (!me.dataCache[me.ifname]) {
		throw "no such interface '" + me.ifname + "'";
	    }
	    cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]);
	}
	for (let i = 0; i < 32; i++) {
	    let ifname = 'net' + i.toString();
	    if (me.isCreate && !me.dataCache[ifname]) {
		me.ifname = ifname;
		break;
	    }
	}
	me.column1 = [
	    {
		xtype: 'hidden',
		name: 'id',
		value: me.ifname,
	    },
	    {
		xtype: 'textfield',
		name: 'name',
		fieldLabel: gettext('Name'),
		emptyText: '(e.g., eth0)',
		allowBlank: false,
		value: cdata.name,
		validator: function(value) {
		    for (const [key, netRaw] of Object.entries(me.dataCache)) {
			if (!key.match(/^net\d+/) || key === me.ifname) {
			    continue;
			}
			let net = PVE.Parser.parseLxcNetwork(netRaw);
			if (net.name === value) {
			    return "interface name already in use";
			}
		    }
		    return true;
		},
	    },
	    {
		xtype: 'textfield',
		name: 'hwaddr',
		fieldLabel: gettext('MAC address'),
		vtype: 'MacAddress',
		value: cdata.hwaddr,
		allowBlank: true,
		emptyText: 'auto',
	    },
	    {
		xtype: 'PVE.form.BridgeSelector',
		name: 'bridge',
		nodename: me.nodename,
		fieldLabel: gettext('Bridge'),
		value: cdata.bridge,
		allowBlank: false,
	    },
	    {
		xtype: 'pveVlanField',
		name: 'tag',
		value: cdata.tag,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Firewall'),
		name: 'firewall',
		value: cdata.firewall,
	    },
	];
	let dhcp4 = cdata.ip === 'dhcp';
	if (dhcp4) {
	    cdata.ip = '';
	    cdata.gw = '';
	}
	let auto6 = cdata.ip6 === 'auto';
	let dhcp6 = cdata.ip6 === 'dhcp';
	if (auto6 || dhcp6) {
	    cdata.ip6 = '';
	    cdata.gw6 = '';
	}
	me.column2 = [
	    {
		layout: {
		    type: 'hbox',
		    align: 'middle',
		},
		border: false,
		margin: '0 0 5 0',
		items: [
		    {
			xtype: 'label',
			text: 'IPv4:', // do not localize
		    },
		    {
			xtype: 'radiofield',
			boxLabel: gettext('Static'),
			name: 'ipv4mode',
			inputValue: 'static',
			checked: !dhcp4,
			margin: '0 0 0 10',
			listeners: {
			    change: function(cb, value) {
				me.down('field[name=ip]').setEmptyText(
				    value ? Proxmox.Utils.NoneText : "",
				);
				me.down('field[name=ip]').setDisabled(!value);
				me.down('field[name=gw]').setDisabled(!value);
			    },
			},
		    },
		    {
			xtype: 'radiofield',
			boxLabel: 'DHCP', // do not localize
			name: 'ipv4mode',
			inputValue: 'dhcp',
			checked: dhcp4,
			margin: '0 0 0 10',
		    },
		],
	    },
	    {
		xtype: 'textfield',
		name: 'ip',
		vtype: 'IPCIDRAddress',
		value: cdata.ip,
		emptyText: dhcp4 ? '' : Proxmox.Utils.NoneText,
		disabled: dhcp4,
		fieldLabel: 'IPv4/CIDR', // do not localize
	    },
	    {
		xtype: 'textfield',
		name: 'gw',
		value: cdata.gw,
		vtype: 'IPAddress',
		disabled: dhcp4,
		fieldLabel: gettext('Gateway') + ' (IPv4)',
		margin: '0 0 3 0', // override bottom margin to account for the menuseparator
	    },
	    {
		xtype: 'menuseparator',
		height: '3',
		margin: '0',
	    },
	    {
		layout: {
		    type: 'hbox',
		    align: 'middle',
		},
		border: false,
		margin: '0 0 5 0',
		items: [
		    {
			xtype: 'label',
			text: 'IPv6:', // do not localize
		    },
		    {
			xtype: 'radiofield',
			boxLabel: gettext('Static'),
			name: 'ipv6mode',
			inputValue: 'static',
			checked: !(auto6 || dhcp6),
			margin: '0 0 0 10',
			listeners: {
			    change: function(cb, value) {
				me.down('field[name=ip6]').setEmptyText(
				    value ? Proxmox.Utils.NoneText : "",
				);
				me.down('field[name=ip6]').setDisabled(!value);
				me.down('field[name=gw6]').setDisabled(!value);
			    },
			},
		    },
		    {
			xtype: 'radiofield',
			boxLabel: 'DHCP', // do not localize
			name: 'ipv6mode',
			inputValue: 'dhcp',
			checked: dhcp6,
			margin: '0 0 0 10',
		    },
		    {
			xtype: 'radiofield',
			boxLabel: 'SLAAC', // do not localize
			name: 'ipv6mode',
			inputValue: 'auto',
			checked: auto6,
			margin: '0 0 0 10',
		    },
		],
	    },
	    {
		xtype: 'textfield',
		name: 'ip6',
		value: cdata.ip6,
		emptyText: dhcp6 || auto6 ? '' : Proxmox.Utils.NoneText,
		vtype: 'IP6CIDRAddress',
		disabled: dhcp6 || auto6,
		fieldLabel: 'IPv6/CIDR', // do not localize
	    },
	    {
		xtype: 'textfield',
		name: 'gw6',
		vtype: 'IP6Address',
		value: cdata.gw6,
		disabled: dhcp6 || auto6,
		fieldLabel: gettext('Gateway') + ' (IPv6)',
	    },
	];
	me.advancedColumn1 = [
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Disconnect'),
		name: 'link_down',
		value: cdata.link_down,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		fieldLabel: 'MTU',
		emptyText: gettext('Same as bridge'),
		name: 'mtu',
		value: cdata.mtu,
		minValue: 576,
		maxValue: 65535,
	    },
	];
	me.advancedColumn2 = [
	    {
		xtype: 'numberfield',
		name: 'rate',
		fieldLabel: gettext('Rate limit') + ' (MB/s)',
		minValue: 0,
		maxValue: 10*1024,
		value: cdata.rate,
		emptyText: 'unlimited',
		allowBlank: true,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.lxc.NetworkEdit', {
    extend: 'Proxmox.window.Edit',
    isAdd: true,
    initComponent: function() {
	let me = this;
	if (!me.dataCache) {
	    throw "no dataCache specified";
	}
	if (!me.nodename) {
	    throw "no node name specified";
	}
	Ext.apply(me, {
	    subject: gettext('Network Device') + ' (veth)',
	    digest: me.dataCache.digest,
	    items: [
		{
		    xtype: 'pveLxcNetworkInputPanel',
		    ifname: me.ifname,
		    nodename: me.nodename,
		    dataCache: me.dataCache,
		    isCreate: me.isCreate,
		},
	    ],
	});
	me.callParent();
    },
});
Ext.define('PVE.lxc.NetworkView', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveLxcNetworkView',
    onlineHelp: 'pct_container_network',
    dataCache: {}, // used to store result of last load
    stateful: true,
    stateId: 'grid-lxc-network',
    load: function() {
	let me = this;
	Proxmox.Utils.setErrorMask(me, true);
	Proxmox.Utils.API2Request({
	    url: me.url,
	    failure: function(response, opts) {
		Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus);
	    },
	    success: function(response, opts) {
		Proxmox.Utils.setErrorMask(me, false);
		let result = Ext.decode(response.responseText);
		me.dataCache = result.data || {};
		let records = [];
		for (const [key, value] of Object.entries(me.dataCache)) {
		    if (key.match(/^net\d+/)) {
			let net = PVE.Parser.parseLxcNetwork(value);
			net.id = key;
			records.push(net);
		    }
		}
		me.store.loadData(records);
		me.down('button[name=addButton]').setDisabled(records.length >= 32);
	    },
	});
    },
    initComponent: function() {
	let me = this;
	let nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	let vmid = me.pveSelNode.data.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	let caps = Ext.state.Manager.get('GuiCap');
	me.url = `/nodes/${nodename}/lxc/${vmid}/config`;
	let store = new Ext.data.Store({
	    model: 'pve-lxc-network',
	    sorters: [
		{
		    property: 'id',
		    direction: 'ASC',
		},
	    ],
	});
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec || !caps.vms['VM.Config.Network']) {
		return false; // disable default-propagation when triggered by grid dblclick
	    }
	    Ext.create('PVE.lxc.NetworkEdit', {
		url: me.url,
		nodename: nodename,
		dataCache: me.dataCache,
		ifname: rec.data.id,
		listeners: {
		    destroy: () => me.load(),
		},
		autoShow: true,
	    });
	    return undefined; // make eslint happier
	};
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: [
		{
		    text: gettext('Add'),
		    name: 'addButton',
		    disabled: !caps.vms['VM.Config.Network'],
		    handler: function() {
			Ext.create('PVE.lxc.NetworkEdit', {
			    url: me.url,
			    nodename: nodename,
			    isCreate: true,
			    dataCache: me.dataCache,
			    listeners: {
				destroy: () => me.load(),
			    },
			    autoShow: true,
			});
		    },
		},
		{
		    xtype: 'proxmoxButton',
		    text: gettext('Remove'),
		    disabled: true,
		    selModel: sm,
		    enableFn: function(rec) {
			return !!caps.vms['VM.Config.Network'];
		    },
		    confirmMsg: ({ data }) =>
			Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${data.id}'`),
		    handler: function(btn, e, rec) {
			Proxmox.Utils.API2Request({
			    url: me.url,
			    waitMsgTarget: me,
			    method: 'PUT',
			    params: {
				'delete': rec.data.id,
				digest: me.dataCache.digest,
			    },
			    callback: () => me.load(),
			    failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
			});
		    },
		},
		{
		    xtype: 'proxmoxButton',
		    text: gettext('Edit'),
		    selModel: sm,
		    disabled: true,
		    enableFn: rec => !!caps.vms['VM.Config.Network'],
		    handler: run_editor,
		},
	    ],
	    columns: [
		{
		    header: 'ID',
		    width: 50,
		    dataIndex: 'id',
		},
		{
		    header: gettext('Name'),
		    width: 80,
		    dataIndex: 'name',
		},
		{
		    header: gettext('Bridge'),
		    width: 80,
		    dataIndex: 'bridge',
		},
		{
		    header: gettext('Firewall'),
		    width: 80,
		    dataIndex: 'firewall',
		    renderer: Proxmox.Utils.format_boolean,
		},
		{
		    header: gettext('VLAN Tag'),
		    width: 80,
		    dataIndex: 'tag',
		},
		{
		    header: gettext('MAC address'),
		    width: 110,
		    dataIndex: 'hwaddr',
		},
		{
		    header: gettext('IP address'),
		    width: 150,
		    dataIndex: 'ip',
		    renderer: function(value, metaData, rec) {
			if (rec.data.ip && rec.data.ip6) {
			    return rec.data.ip + "
" + rec.data.ip6;
			} else if (rec.data.ip6) {
			    return rec.data.ip6;
			} else {
			    return rec.data.ip;
			}
		    },
		},
		{
		    header: gettext('Gateway'),
		    width: 150,
		    dataIndex: 'gw',
		    renderer: function(value, metaData, rec) {
			if (rec.data.gw && rec.data.gw6) {
			    return rec.data.gw + "
" + rec.data.gw6;
			} else if (rec.data.gw6) {
			    return rec.data.gw6;
			} else {
			    return rec.data.gw;
			}
		    },
		},
		{
		    header: gettext('MTU'),
		    width: 80,
		    dataIndex: 'mtu',
		},
		{
		    header: gettext('Disconnected'),
		    width: 100,
		    dataIndex: 'link_down',
		    renderer: Proxmox.Utils.format_boolean,
		},
	    ],
	    listeners: {
		activate: me.load,
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
   },
}, function() {
    Ext.define('pve-lxc-network', {
	extend: "Ext.data.Model",
	proxy: { type: 'memory' },
	fields: [
	    'id',
	    'name',
	    'hwaddr',
	    'bridge',
	    'ip',
	    'gw',
	    'ip6',
	    'gw6',
	    'tag',
	    'firewall',
	    'mtu',
	    'link_down',
	],
    });
});
Ext.define('PVE.lxc.Options', {
    extend: 'Proxmox.grid.PendingObjectGrid',
    alias: ['widget.pveLxcOptions'],
    onlineHelp: 'pct_options',
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = me.pveSelNode.data.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	var caps = Ext.state.Manager.get('GuiCap');
	var rows = {
	    onboot: {
		header: gettext('Start at boot'),
		defaultValue: '',
		renderer: Proxmox.Utils.format_boolean,
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Start at boot'),
		    items: {
			xtype: 'proxmoxcheckbox',
			name: 'onboot',
			uncheckedValue: 0,
			defaultValue: 0,
			fieldLabel: gettext('Start at boot'),
		    },
		} : undefined,
	    },
	    startup: {
		header: gettext('Start/Shutdown order'),
		defaultValue: '',
		renderer: PVE.Utils.render_kvm_startup,
		editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify']
		    ? {
			xtype: 'pveWindowStartupEdit',
			onlineHelp: 'pct_startup_and_shutdown',
		    } : undefined,
	    },
	    ostype: {
		header: gettext('OS Type'),
		defaultValue: Proxmox.Utils.unknownText,
	    },
	    arch: {
		header: gettext('Architecture'),
		defaultValue: Proxmox.Utils.unknownText,
	    },
	    console: {
		header: '/dev/console',
		defaultValue: 1,
		renderer: Proxmox.Utils.format_enabled_toggle,
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: '/dev/console',
		    items: {
			xtype: 'proxmoxcheckbox',
			name: 'console',
			uncheckedValue: 0,
			defaultValue: 1,
			deleteDefaultValue: true,
			checked: true,
			fieldLabel: '/dev/console',
		    },
		} : undefined,
	    },
	    tty: {
		header: gettext('TTY count'),
		defaultValue: 2,
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('TTY count'),
		    items: {
			xtype: 'proxmoxintegerfield',
			name: 'tty',
			minValue: 0,
			maxValue: 6,
			value: 2,
			fieldLabel: gettext('TTY count'),
			emptyText: gettext('Default'),
			deleteEmpty: true,
		    },
		} : undefined,
	    },
	    cmode: {
		header: gettext('Console mode'),
		defaultValue: 'tty',
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Console mode'),
		    items: {
			xtype: 'proxmoxKVComboBox',
			name: 'cmode',
			deleteEmpty: true,
			value: '__default__',
			comboItems: [
			    ['__default__', Proxmox.Utils.defaultText + " (tty)"],
			    ['tty', "/dev/tty[X]"],
			    ['console', "/dev/console"],
			    ['shell', "shell"],
			],
			fieldLabel: gettext('Console mode'),
		    },
		} : undefined,
	    },
	    protection: {
		header: gettext('Protection'),
		defaultValue: false,
		renderer: Proxmox.Utils.format_boolean,
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Protection'),
		    items: {
			xtype: 'proxmoxcheckbox',
			name: 'protection',
			uncheckedValue: 0,
			defaultValue: 0,
			deleteDefaultValue: true,
			fieldLabel: gettext('Enabled'),
		    },
		} : undefined,
	    },
	    unprivileged: {
		header: gettext('Unprivileged container'),
		renderer: Proxmox.Utils.format_boolean,
		defaultValue: 0,
	    },
	    features: {
		header: gettext('Features'),
		defaultValue: Proxmox.Utils.noneText,
		editor: 'PVE.lxc.FeaturesEdit',
	    },
	    hookscript: {
		header: gettext('Hookscript'),
	    },
	};
	var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
	var sm = Ext.create('Ext.selection.RowModel', {});
	var edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    enableFn: function(rec) {
		var rowdef = rows[rec.data.key];
		return !!rowdef.editor;
	    },
	    handler: function() { me.run_editor(); },
	});
	var revert_btn = new PVE.button.PendingRevert();
	var set_button_status = function() {
	    let button_sm = me.getSelectionModel();
	    let rec = button_sm.getSelection()[0];
	    if (!rec) {
		edit_btn.disable();
		return;
	    }
	    var key = rec.data.key;
	    var pending = rec.data.delete || me.hasPendingChanges(key);
	    var rowdef = rows[key];
	    if (key === 'features') {
		let unprivileged = me.getStore().getById('unprivileged').data.value;
		let root = Proxmox.UserName === 'root@pam';
		let vmalloc = caps.vms['VM.Allocate'];
		edit_btn.setDisabled(!(root || (vmalloc && unprivileged)));
	    } else {
		edit_btn.setDisabled(!rowdef.editor);
	    }
	    revert_btn.setDisabled(!pending);
	};
	Ext.apply(me, {
	    url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/pending",
	    selModel: sm,
	    interval: 5000,
	    tbar: [edit_btn, revert_btn],
	    rows: rows,
	    editorConfig: {
		url: '/api2/extjs/' + baseurl,
	    },
	    listeners: {
		itemdblclick: me.run_editor,
		selectionchange: set_button_status,
	    },
	});
	me.callParent();
	me.on('activate', me.rstore.startUpdate);
	me.on('destroy', me.rstore.stopUpdate);
	me.on('deactivate', me.rstore.stopUpdate);
	me.mon(me.getStore(), 'datachanged', function() {
	    set_button_status();
	});
    },
});
var labelWidth = 120;
Ext.define('PVE.lxc.MemoryEdit', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	Ext.apply(me, {
	    subject: gettext('Memory'),
	    items: Ext.create('PVE.lxc.MemoryInputPanel'),
	});
	me.callParent();
	me.load();
    },
});
Ext.define('PVE.lxc.CPUEdit', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveLxcCPUEdit',
    viewModel: {
	data: {
	    cgroupMode: 2,
	},
    },
    initComponent: function() {
	let me = this;
	me.getViewModel().set('cgroupMode', me.cgroupMode);
	Ext.apply(me, {
	    subject: gettext('CPU'),
	    items: Ext.create('PVE.lxc.CPUInputPanel'),
	});
	me.callParent();
	me.load();
    },
});
// The view model of the parent shoul contain a 'cgroupMode' variable (or params for v2 are used).
Ext.define('PVE.lxc.CPUInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveLxcCPUInputPanel',
    onlineHelp: 'pct_cpu',
    insideWizard: false,
    viewModel: {
	formulas: {
	    cpuunitsDefault: (get) => get('cgroupMode') === 1 ? 1024 : 100,
	    cpuunitsMax: (get) => get('cgroupMode') === 1 ? 500000 : 10000,
	},
    },
    onGetValues: function(values) {
	let me = this;
	let cpuunitsDefault = me.getViewModel().get('cpuunitsDefault');
	PVE.Utils.delete_if_default(values, 'cpulimit', '0', me.insideWizard);
	PVE.Utils.delete_if_default(values, 'cpuunits', `${cpuunitsDefault}`, me.insideWizard);
	return values;
    },
    advancedColumn1: [
	{
	    xtype: 'numberfield',
	    name: 'cpulimit',
	    minValue: 0,
	    value: '',
	    step: 1,
	    fieldLabel: gettext('CPU limit'),
	    allowBlank: true,
	    emptyText: gettext('unlimited'),
	},
    ],
    advancedColumn2: [
	{
	    xtype: 'proxmoxintegerfield',
	    name: 'cpuunits',
	    fieldLabel: gettext('CPU units'),
	    value: '',
	    minValue: 8,
	    maxValue: '10000',
	    emptyText: '100',
	    bind: {
		emptyText: '{cpuunitsDefault}',
		maxValue: '{cpuunitsMax}',
	    },
	    labelWidth: labelWidth,
	    deleteEmpty: true,
	    allowBlank: true,
	},
    ],
    initComponent: function() {
	var me = this;
	me.column1 = [
	    {
		xtype: 'proxmoxintegerfield',
		name: 'cores',
		minValue: 1,
		maxValue: 8192,
		value: me.insideWizard ? 1 : '',
		fieldLabel: gettext('Cores'),
		allowBlank: true,
		deleteEmpty: true,
		emptyText: gettext('unlimited'),
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.lxc.MemoryInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveLxcMemoryInputPanel',
    onlineHelp: 'pct_memory',
    insideWizard: false,
    initComponent: function() {
	var me = this;
	var items = [
	    {
		xtype: 'proxmoxintegerfield',
		name: 'memory',
		minValue: 16,
		value: '512',
		step: 32,
		fieldLabel: gettext('Memory') + ' (MiB)',
		labelWidth: labelWidth,
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'swap',
		minValue: 0,
		value: '512',
		step: 32,
		fieldLabel: gettext('Swap') + ' (MiB)',
		labelWidth: labelWidth,
		allowBlank: false,
	    },
	];
	if (me.insideWizard) {
	    me.column1 = items;
	} else {
	    me.items = items;
	}
	me.callParent();
    },
});
Ext.define('PVE.lxc.RessourceView', {
    extend: 'Proxmox.grid.PendingObjectGrid',
    alias: ['widget.pveLxcRessourceView'],
    onlineHelp: 'pct_configuration',
    renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
	let me = this;
	let rowdef = me.rows[key] || {};
	let txt = rowdef.header || key;
	let icon = '';
	metaData.tdAttr = "valign=middle";
	if (rowdef.tdCls) {
	    metaData.tdCls = rowdef.tdCls;
	} else if (rowdef.iconCls) {
	    icon = ``;
	    metaData.tdCls += " pve-itype-fa";
	}
	// only return icons in grid but not remove dialog
	if (rowIndex !== undefined) {
	    return icon + txt;
	} else {
	    return txt;
	}
    },
    initComponent: function() {
	var me = this;
	let confid;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = me.pveSelNode.data.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	var caps = Ext.state.Manager.get('GuiCap');
	var diskCap = caps.vms['VM.Config.Disk'];
	var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined;
	const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename);
	let cpuEditor = {
	    xtype: 'pveLxcCPUEdit',
	    cgroupMode: nodeInfo['cgroup-mode'],
	};
	var rows = {
	    memory: {
		header: gettext('Memory'),
		editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
		defaultValue: 512,
		tdCls: 'pmx-itype-icon-memory',
		group: 1,
		renderer: function(value) {
		    return Proxmox.Utils.format_size(value*1024*1024);
		},
	    },
	    swap: {
		header: gettext('Swap'),
		editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
		defaultValue: 512,
		iconCls: 'refresh',
		group: 2,
		renderer: function(value) {
		    return Proxmox.Utils.format_size(value*1024*1024);
		},
	    },
	    cores: {
		header: gettext('Cores'),
		editor: caps.vms['VM.Config.CPU'] ? cpuEditor : undefined,
		defaultValue: '',
		tdCls: 'pmx-itype-icon-processor',
		group: 3,
		renderer: function(value) {
		    var cpulimit = me.getObjectValue('cpulimit');
		    var cpuunits = me.getObjectValue('cpuunits');
		    var res;
		    if (value) {
			res = value;
		    } else {
			res = gettext('unlimited');
		    }
		    if (cpulimit) {
			res += ' [cpulimit=' + cpulimit + ']';
		    }
		    if (cpuunits) {
			res += ' [cpuunits=' + cpuunits + ']';
		    }
		    return res;
		},
	    },
	    rootfs: {
		header: gettext('Root Disk'),
		defaultValue: Proxmox.Utils.noneText,
		editor: mpeditor,
		iconCls: 'hdd-o',
		group: 4,
	    },
	    cpulimit: {
		visible: false,
	    },
	    cpuunits: {
		visible: false,
	    },
	    unprivileged: {
		visible: false,
	    },
	};
	PVE.Utils.forEachMP(function(bus, i) {
	    confid = bus + i;
	    var group = 5;
	    var header;
	    if (bus === 'mp') {
		header = gettext('Mount Point') + ' (' + confid + ')';
	    } else {
		header = gettext('Unused Disk') + ' ' + i;
		group += 1;
	    }
	    rows[confid] = {
		group: group,
		order: i,
		tdCls: 'pve-itype-icon-storage',
		editor: mpeditor,
		header: header,
	    };
	}, true);
	var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
	me.selModel = Ext.create('Ext.selection.RowModel', {});
	var run_resize = function() {
	    var rec = me.selModel.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    var win = Ext.create('PVE.window.MPResize', {
		disk: rec.data.key,
		nodename: nodename,
		vmid: vmid,
	    });
	    win.show();
	};
	var run_remove = function(b, e, rec) {
	    Proxmox.Utils.API2Request({
		url: '/api2/extjs/' + baseurl,
		waitMsgTarget: me,
		method: 'PUT',
		params: {
		    'delete': rec.data.key,
		},
		failure: function(response, opts) {
		    Ext.Msg.alert('Error', response.htmlStatus);
		},
	    });
	};
	let run_move = function() {
	    let rec = me.selModel.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    var win = Ext.create('PVE.window.HDMove', {
		disk: rec.data.key,
		nodename: nodename,
		vmid: vmid,
		type: 'lxc',
	    });
	    win.show();
	    win.on('destroy', me.reload, me);
	};
	let run_reassign = function() {
	    let rec = me.selModel.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    Ext.create('PVE.window.GuestDiskReassign', {
		disk: rec.data.key,
		nodename: nodename,
		autoShow: true,
		vmid: vmid,
		type: 'lxc',
		listeners: {
		    destroy: () => me.reload(),
		},
	    });
	};
	var edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    selModel: me.selModel,
	    disabled: true,
	    enableFn: function(rec) {
		if (!rec) {
		    return false;
		}
		var rowdef = rows[rec.data.key];
		return !!rowdef.editor;
	    },
	    handler: function() { me.run_editor(); },
	});
	var remove_btn = new Proxmox.button.Button({
	    text: gettext('Remove'),
	    defaultText: gettext('Remove'),
	    altText: gettext('Detach'),
	    selModel: me.selModel,
	    disabled: true,
	    dangerous: true,
	    confirmMsg: function(rec) {
		let warn = Ext.String.format(gettext('Are you sure you want to remove entry {0}'));
		if (this.text === this.altText) {
		    warn = gettext('Are you sure you want to detach entry {0}');
		}
		let rendered = me.renderKey(rec.data.key, {}, rec);
		let msg = Ext.String.format(warn, `'${rendered}'`);
		if (rec.data.key.match(/^unused\d+$/)) {
		    msg += " " + gettext('This will permanently erase all data.');
		}
		return msg;
	    },
	    handler: run_remove,
	    listeners: {
		render: function(btn) {
		    // hack: calculate the max button width on first display to prevent the whole
		    // toolbar to move when we switch between the "Remove" and "Detach" labels
		    let def = btn.getSize().width;
		    btn.setText(btn.altText);
		    let alt = btn.getSize().width;
		    btn.setText(btn.defaultText);
		    let optimal = alt > def ? alt : def;
		    btn.setSize({ width: optimal });
		},
	    },
	});
	let move_menuitem = new Ext.menu.Item({
	    text: gettext('Move Storage'),
	    tooltip: gettext('Move volume to another storage'),
	    iconCls: 'fa fa-database',
	    selModel: me.selModel,
	    handler: run_move,
	});
	let reassign_menuitem = new Ext.menu.Item({
	    text: gettext('Reassign Owner'),
	    tooltip: gettext('Reassign volume to another CT'),
	    iconCls: 'fa fa-cube',
	    handler: run_reassign,
	    reference: 'reassing_item',
	});
	let resize_menuitem = new Ext.menu.Item({
	    text: gettext('Resize'),
	    iconCls: 'fa fa-plus',
	    selModel: me.selModel,
	    handler: run_resize,
	});
	let volumeaction_btn = new Proxmox.button.Button({
	    text: gettext('Volume Action'),
	    disabled: true,
	    menu: {
		items: [
		    move_menuitem,
		    reassign_menuitem,
		    resize_menuitem,
		],
	    },
	});
	let revert_btn = new PVE.button.PendingRevert();
	let set_button_status = function() {
	    let rec = me.selModel.getSelection()[0];
	    if (!rec) {
		edit_btn.disable();
		remove_btn.disable();
		volumeaction_btn.disable();
		revert_btn.disable();
		return;
	    }
	    let { key, value, 'delete': isDelete } = rec.data;
	    let rowdef = rows[key];
	    let pending = isDelete || me.hasPendingChanges(key);
	    let isRootFS = key === 'rootfs';
	    let isDisk = isRootFS || key.match(/^(mp|unused)\d+/);
	    let isUnusedDisk = key.match(/^unused\d+/);
	    let isUsedDisk = isDisk && !isUnusedDisk;
	    let noedit = isDelete || !rowdef.editor;
	    if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) {
		let mp = PVE.Parser.parseLxcMountPoint(value);
		if (mp.type !== 'volume') {
		    noedit = true;
		}
	    }
	    edit_btn.setDisabled(noedit);
	    volumeaction_btn.setDisabled(!isDisk || !diskCap);
	    move_menuitem.setDisabled(isUnusedDisk);
	    reassign_menuitem.setDisabled(isRootFS);
	    resize_menuitem.setDisabled(isUnusedDisk);
	    remove_btn.setDisabled(!isDisk || isRootFS || !diskCap || pending);
	    revert_btn.setDisabled(!pending);
	    remove_btn.setText(isUsedDisk ? remove_btn.altText : remove_btn.defaultText);
	};
	let sorterFn = function(rec1, rec2) {
	    let v1 = rec1.data.key, v2 = rec2.data.key;
	    let g1 = rows[v1].group || 0, g2 = rows[v2].group || 0;
	    if (g1 - g2 !== 0) {
		return g1 - g2;
	    }
	    let order1 = rows[v1].order || 0, order2 = rows[v2].order || 0;
	    if (order1 - order2 !== 0) {
		return order1 - order2;
	    }
	    if (v1 > v2) {
		return 1;
	    } else if (v1 < v2) {
	        return -1;
	    } else {
		return 0;
	    }
	};
	Ext.apply(me, {
	    url: `/api2/json/nodes/${nodename}/lxc/${vmid}/pending`,
	    selModel: me.selModel,
	    interval: 2000,
	    cwidth1: 170,
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: new Ext.menu.Menu({
			items: [
			    {
				text: gettext('Mount Point'),
				iconCls: 'fa fa-fw fa-hdd-o black',
				disabled: !caps.vms['VM.Config.Disk'],
				handler: function() {
				    Ext.create('PVE.lxc.MountPointEdit', {
					autoShow: true,
					url: `/api2/extjs/${baseurl}`,
					unprivileged: me.getObjectValue('unprivileged'),
					pveSelNode: me.pveSelNode,
					listeners: {
					    destroy: () => me.reload(),
					},
				    });
				},
			    },
			],
		    }),
		},
		edit_btn,
		remove_btn,
		volumeaction_btn,
		revert_btn,
	    ],
	    rows: rows,
	    sorterFn: sorterFn,
	    editorConfig: {
		pveSelNode: me.pveSelNode,
		url: '/api2/extjs/' + baseurl,
	    },
	    listeners: {
		itemdblclick: me.run_editor,
		selectionchange: set_button_status,
	    },
	});
	me.callParent();
	me.on('activate', me.rstore.startUpdate);
	me.on('destroy', me.rstore.stopUpdate);
	me.on('deactivate', me.rstore.stopUpdate);
	me.mon(me.getStore(), 'datachanged', function() {
	    set_button_status();
	});
	Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') });
    },
});
Ext.define('PVE.lxc.MultiMPPanel', {
    extend: 'PVE.panel.MultiDiskPanel',
    alias: 'widget.pveMultiMPPanel',
    onlineHelp: 'pct_container_storage',
    controller: {
	xclass: 'Ext.app.ViewController',
	// count of mps + rootfs
	maxCount: PVE.Utils.mp_counts.mp + 1,
	getNextFreeDisk: function(vmconfig) {
	    let nextFreeDisk;
	    if (!vmconfig.rootfs) {
		return {
		    confid: 'rootfs',
		};
	    } else {
		for (let i = 0; i < PVE.Utils.mp_counts.mp; i++) {
		    let confid = `mp${i}`;
		    if (!vmconfig[confid]) {
			nextFreeDisk = {
			    confid,
			};
			break;
		    }
		}
	    }
	    return nextFreeDisk;
	},
	addPanel: function(itemId, vmconfig, nextFreeDisk) {
	    let me = this;
	    return me.getView().add({
		vmconfig,
		border: false,
		showAdvanced: Ext.state.Manager.getProvider().get('proxmox-advanced-cb'),
		xtype: 'pveLxcMountPointInputPanel',
		confid: nextFreeDisk.confid === 'rootfs' ? 'rootfs' : null,
		bind: {
		    nodename: '{nodename}',
		    unprivileged: '{unprivileged}',
		},
		padding: '0 5 0 10',
		itemId,
		selectFree: true,
		isCreate: true,
		insideWizard: true,
	    });
	},
	getBaseVMConfig: function() {
	    let me = this;
	    return {
		unprivileged: me.getViewModel().get('unprivileged'),
	    };
	},
	diskSorter: {
	    sorterFn: function(rec1, rec2) {
		if (rec1.data.name === 'rootfs') {
		    return -1;
		} else if (rec2.data.name === 'rootfs') {
		    return 1;
		}
		let mp_match = /^mp(\d+)$/;
		let [, id1] = mp_match.exec(rec1.data.name);
		let [, id2] = mp_match.exec(rec2.data.name);
		return parseInt(id1, 10) - parseInt(id2, 10);
	    },
	},
	deleteDisabled: (view, rI, cI, item, rec) => rec.data.name === 'rootfs',
    },
});
Ext.define('PVE.menu.Item', {
    extend: 'Ext.menu.Item',
    alias: 'widget.pveMenuItem',
    // set to wrap the handler callback in a confirm dialog  showing this text
    confirmMsg: false,
    // set to focus 'No' instead of 'Yes' button and show a warning symbol
    dangerous: false,
    initComponent: function() {
	let me = this;
	if (me.handler) {
	    me.setHandler(me.handler, me.scope);
	}
	me.callParent();
    },
    setHandler: function(fn, scope) {
	let me = this;
	me.scope = scope;
	me.handler = function(button, e) {
	    if (me.confirmMsg) {
		Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
		Ext.Msg.show({
		    title: gettext('Confirm'),
		    icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
		    msg: me.confirmMsg,
		    buttons: Ext.Msg.YESNO,
		    defaultFocus: me.dangerous ? 'no' : 'yes',
		    callback: function(btn) {
			if (btn === 'yes') {
			    Ext.callback(fn, me.scope, [me, e], 0, me);
			}
		    },
		});
	    } else {
		Ext.callback(fn, me.scope, [me, e], 0, me);
	    }
	};
    },
});
Ext.define('PVE.menu.TemplateMenu', {
    extend: 'Ext.menu.Menu',
    initComponent: function() {
	let me = this;
	let info = me.pveSelNode.data;
	if (!info.node) {
	    throw "no node name specified";
	}
	if (!info.vmid) {
	    throw "no VM ID specified";
	}
	let guestType = me.pveSelNode.data.type;
	if (guestType !== 'qemu' && guestType !== 'lxc') {
	    throw `invalid guest type ${guestType}`;
	}
	let template = me.pveSelNode.data.template;
	me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + info.vmid;
	let caps = Ext.state.Manager.get('GuiCap');
	let standaloneNode = PVE.data.ResourceStore.getNodes().length < 2;
	me.items = [
	    {
		text: gettext('Migrate'),
		iconCls: 'fa fa-fw fa-send-o',
		hidden: standaloneNode || !caps.vms['VM.Migrate'],
		handler: function() {
		    Ext.create('PVE.window.Migrate', {
			vmtype: guestType,
			nodename: info.node,
			vmid: info.vmid,
			autoShow: true,
		    });
		},
	    },
	    {
		text: gettext('Clone'),
		iconCls: 'fa fa-fw fa-clone',
		hidden: !caps.vms['VM.Clone'],
		handler: function() {
		    Ext.create('PVE.window.Clone', {
			nodename: info.node,
			guestType: guestType,
			vmid: info.vmid,
			isTemplate: template,
			autoShow: true,
		    });
		},
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.ceph.CephInstallWizardInfo', {
    extend: 'Ext.panel.Panel',
    xtype: 'pveCephInstallWizardInfo',
    html: `Ceph?
    "Ceph is a unified,
    distributed storage system, designed for excellent performance, reliability,
    and scalability."
    
    Ceph is currently not installed on this node. This wizard
    will guide you through the installation. Click on the next button below
    to begin. After the initial installation, the wizard will offer to create
    an initial configuration. This configuration step is only
    needed once per cluster and will be skipped if a config is already present.
    
    
    Before starting the installation, please take a look at our documentation,
    by clicking the help button below. If you want to gain deeper knowledge about
    Ceph, visit ceph.com.
    
`,
});
Ext.define('PVE.ceph.CephVersionSelector', {
    extend: 'Ext.form.field.ComboBox',
    xtype: 'pveCephVersionSelector',
    fieldLabel: gettext('Ceph version to install'),
    displayField: 'display',
    valueField: 'release',
    queryMode: 'local',
    editable: false,
    forceSelection: true,
    store: {
	fields: [
	    'release',
	    'version',
	    {
		name: 'display',
		calculate: d => `${d.release} (${d.version})`,
	    },
	],
	proxy: {
	    type: 'memory',
	    reader: {
		type: 'json',
	    },
	},
	data: [
	    { release: "octopus", version: "15.2" },
	    { release: "pacific", version: "16.2" },
	    { release: "quincy", version: "17.2" },
	],
    },
});
Ext.define('PVE.ceph.CephHighestVersionDisplay', {
    extend: 'Ext.form.field.Display',
    xtype: 'pveCephHighestVersionDisplay',
    fieldLabel: gettext('Ceph in the cluster'),
    value: 'unknown',
    // called on success with (release, versionTxt, versionParts)
    gotNewestVersion: Ext.emptyFn,
    initComponent: function() {
	let me = this;
	me.callParent(arguments);
	Proxmox.Utils.API2Request({
	    method: 'GET',
	    url: '/cluster/ceph/metadata',
	    params: {
		scope: 'versions',
	    },
	    waitMsgTarget: me,
	    success: (response) => {
		let res = response.result;
		if (!res || !res.data || !res.data.node) {
		    me.setValue(
			gettext('Could not detect a ceph installation in the cluster'),
		    );
		    return;
		}
		let nodes = res.data.node;
		if (me.nodename) {
		    // can happen on ceph purge, we do not yet cleanup old version data
		    delete nodes[me.nodename];
		}
		let maxversion = [];
		let maxversiontext = "";
		for (const [_nodename, data] of Object.entries(nodes)) {
		    let version = data.version.parts;
		    if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
			maxversion = version;
			maxversiontext = data.version.str;
		    }
		}
		// FIXME: get from version selector store
		const major2release = {
		    13: 'luminous',
		    14: 'nautilus',
		    15: 'octopus',
		    16: 'pacific',
		    17: 'quincy',
		};
		let release = major2release[maxversion[0]] || 'unknown';
		let newestVersionTxt = `${Ext.String.capitalize(release)} (${maxversiontext})`;
		if (release === 'unknown') {
		    me.setValue(
			gettext('Could not detect a ceph installation in the cluster'),
		    );
		} else {
		    me.setValue(Ext.String.format(
			gettext('Newest ceph version in cluster is {0}'),
			newestVersionTxt,
		    ));
		}
		me.gotNewestVersion(release, maxversiontext, maxversion);
	    },
	    failure: function(response, opts) {
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
	    },
	});
    },
});
Ext.define('PVE.ceph.CephInstallWizard', {
    extend: 'PVE.window.Wizard',
    alias: 'widget.pveCephInstallWizard',
    mixins: ['Proxmox.Mixin.CBind'],
    resizable: false,
    nodename: undefined,
    viewModel: {
	data: {
	    nodename: '',
	    cephRelease: 'quincy',
	    configuration: true,
	    isInstalled: false,
	},
    },
    cbindData: {
	nodename: undefined,
    },
    title: gettext('Setup'),
    navigateNext: function() {
	var tp = this.down('#wizcontent');
	var atab = tp.getActiveTab();
	var next = tp.items.indexOf(atab) + 1;
	var ntab = tp.items.getAt(next);
	if (ntab) {
	    ntab.enable();
	    tp.setActiveTab(ntab);
	}
    },
    setInitialTab: function(index) {
	var tp = this.down('#wizcontent');
	var initialTab = tp.items.getAt(index);
	initialTab.enable();
	tp.setActiveTab(initialTab);
    },
    onShow: function() {
	    this.callParent(arguments);
	    var isInstalled = this.getViewModel().get('isInstalled');
	    if (isInstalled) {
		this.getViewModel().set('configuration', false);
		this.setInitialTab(2);
	    }
    },
    items: [
	{
	    xtype: 'panel',
	    title: gettext('Info'),
	    viewModel: {}, // needed to inherit parent viewModel data
	    border: false,
	    bodyBorder: false,
	    onlineHelp: 'chapter_pveceph',
	    layout: {
		type: 'vbox',
		align: 'stretch',
	    },
	    defaults: {
		border: false,
		bodyBorder: false,
	    },
	    items: [
		{
		    xtype: 'pveCephInstallWizardInfo',
		},
		{
		    flex: 1,
		},
		{
		    xtype: 'pveCephHighestVersionDisplay',
		    labelWidth: 180,
		    cbind: {
			nodename: '{nodename}',
		    },
		    gotNewestVersion: function(release, maxversiontext, maxversion) {
			if (release === 'unknown') {
			    return;
			}
			let wizard = this.up('pveCephInstallWizard');
			wizard.getViewModel().set('cephRelease', release);
		    },
		},
		{
		    xtype: 'pveCephVersionSelector',
		    labelWidth: 180,
		    submitValue: false,
		    bind: {
			value: '{cephRelease}',
		    },
		    listeners: {
			change: function(field, release) {
			    let wizard = this.up('pveCephInstallWizard');
			    wizard.down('#next').setText(
				Ext.String.format(gettext('Start {0} installation'), release),
			    );
			},
		    },
		},
	    ],
	    listeners: {
		activate: function() {
		    // notify owning container that it should display a help button
		    if (this.onlineHelp) {
			Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
		    }
		    let wizard = this.up('pveCephInstallWizard');
		    let release = wizard.getViewModel().get('cephRelease');
		    wizard.down('#back').hide(true);
		    wizard.down('#next').setText(
			Ext.String.format(gettext('Start {0} installation'), release),
		    );
		},
		deactivate: function() {
		    if (this.onlineHelp) {
			Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
		    }
		    this.up('pveCephInstallWizard').down('#next').setText(gettext('Next'));
		},
	    },
	},
	{
	    title: gettext('Installation'),
	    xtype: 'panel',
	    layout: 'fit',
	    cbind: {
		nodename: '{nodename}',
	    },
	    viewModel: {}, // needed to inherit parent viewModel data
	    listeners: {
		afterrender: function() {
		    var me = this;
		    if (this.getViewModel().get('isInstalled')) {
			this.mask("Ceph is already installed, click next to create your configuration.", ['pve-static-mask']);
		    } else {
			me.down('pveNoVncConsole').fireEvent('activate');
		    }
		},
		activate: function() {
		    let me = this;
		    const nodename = me.nodename;
		    me.updateStore = Ext.create('Proxmox.data.UpdateStore', {
			storeid: 'ceph-status-' + nodename,
			interval: 1000,
			proxy: {
			    type: 'proxmox',
			    url: '/api2/json/nodes/' + nodename + '/ceph/status',
			},
			listeners: {
			    load: function(rec, response, success, operation) {
				if (success) {
				    me.updateStore.stopUpdate();
				    me.down('textfield').setValue('success');
				} else if (operation.error.statusText.match("not initialized", "i")) {
				    me.updateStore.stopUpdate();
				    me.up('pveCephInstallWizard').getViewModel().set('configuration', false);
				    me.down('textfield').setValue('success');
				} else if (operation.error.statusText.match("rados_connect failed", "i")) {
				    me.updateStore.stopUpdate();
				    me.up('pveCephInstallWizard').getViewModel().set('configuration', true);
				    me.down('textfield').setValue('success');
				} else if (!operation.error.statusText.match("not installed", "i")) {
				    Proxmox.Utils.setErrorMask(me, operation.error.statusText);
				}
			    },
			},
		    });
		    me.updateStore.startUpdate();
		},
		destroy: function() {
		    var me = this;
		    if (me.updateStore) {
			me.updateStore.stopUpdate();
		    }
		},
	    },
	    items: [
		{
		    xtype: 'pveNoVncConsole',
		    itemId: 'jsconsole',
		    consoleType: 'cmd',
		    xtermjs: true,
		    cbind: {
			nodename: '{nodename}',
		    },
		    beforeLoad: function() {
			let me = this;
			let wizard = me.up('pveCephInstallWizard');
			let release = wizard.getViewModel().get('cephRelease');
			me.cmdOpts = `--version\0${release}`;
		    },
		    cmd: 'ceph_install',
		},
		{
		    xtype: 'textfield',
		    name: 'installSuccess',
		    value: '',
		    allowBlank: false,
		    submitValue: false,
		    hidden: true,
		},
	    ],
	},
	{
	    xtype: 'inputpanel',
	    title: gettext('Configuration'),
	    onlineHelp: 'chapter_pveceph',
	    height: 300,
	    cbind: {
		nodename: '{nodename}',
	    },
	    viewModel: {
		data: {
		    replicas: undefined,
		    minreplicas: undefined,
		},
	    },
	    listeners: {
		activate: function() {
		    this.up('pveCephInstallWizard').down('#submit').setText(gettext('Next'));
		},
		afterrender: function() {
		    if (this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
			this.mask("Configuration already initialized", ['pve-static-mask']);
		    } else {
			this.unmask();
		    }
		},
		deactivate: function() {
		    this.up('pveCephInstallWizard').down('#submit').setText(gettext('Finish'));
		},
	    },
	    column1: [
		{
		    xtype: 'displayfield',
		    value: gettext('Ceph cluster configuration') + ':',
		},
		{
		    xtype: 'proxmoxNetworkSelector',
		    name: 'network',
		    value: '',
		    fieldLabel: 'Public Network IP/CIDR',
		    autoSelect: false,
		    bind: {
			allowBlank: '{configuration}',
		    },
		    cbind: {
			nodename: '{nodename}',
		    },
		},
		{
		    xtype: 'proxmoxNetworkSelector',
		    name: 'cluster-network',
		    fieldLabel: 'Cluster Network IP/CIDR',
		    allowBlank: true,
		    autoSelect: false,
		    emptyText: gettext('Same as Public Network'),
		    cbind: {
			nodename: '{nodename}',
		    },
		},
		// FIXME: add hint about cluster network and/or reference user to docs??
	    ],
	    column2: [
		{
		    xtype: 'displayfield',
		    value: gettext('First Ceph monitor') + ':',
		},
		{
		    xtype: 'displayfield',
		    fieldLabel: gettext('Monitor node'),
		    cbind: {
			value: '{nodename}',
		    },
		},
		{
		    xtype: 'displayfield',
		    value: gettext('Additional monitors are recommended. They can be created at any time in the Monitor tab.'),
		    userCls: 'pmx-hint',
		},
	    ],
	    advancedColumn1: [
		{
		    xtype: 'numberfield',
		    name: 'size',
		    fieldLabel: 'Number of replicas',
		    bind: {
			value: '{replicas}',
		    },
		    maxValue: 7,
		    minValue: 2,
		    emptyText: '3',
		},
		{
		    xtype: 'numberfield',
		    name: 'min_size',
		    fieldLabel: 'Minimum replicas',
		    bind: {
			maxValue: '{replicas}',
			value: '{minreplicas}',
		    },
		    minValue: 2,
		    maxValue: 3,
		    setMaxValue: function(value) {
			this.maxValue = Ext.Number.from(value, 2);
			// allow enough to avoid split brains with max 'size', but more makes simply no sense
			if (this.maxValue > 4) {
			    this.maxValue = 4;
			}
			this.toggleSpinners();
			this.validate();
		    },
		    emptyText: '2',
		},
	    ],
	    onGetValues: function(values) {
		['cluster-network', 'size', 'min_size'].forEach(function(field) {
		    if (!values[field]) {
			delete values[field];
		    }
		});
		return values;
	    },
	    onSubmit: function() {
		var me = this;
		if (!this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
		    var wizard = me.up('window');
		    var kv = wizard.getValues();
		    delete kv.delete;
		    var nodename = me.nodename;
		    delete kv.nodename;
		    Proxmox.Utils.API2Request({
			url: `/nodes/${nodename}/ceph/init`,
			waitMsgTarget: wizard,
			method: 'POST',
			params: kv,
			success: function() {
			    Proxmox.Utils.API2Request({
				url: `/nodes/${nodename}/ceph/mon/${nodename}`,
				waitMsgTarget: wizard,
				method: 'POST',
				success: function() {
				    me.up('pveCephInstallWizard').navigateNext();
				},
				failure: function(response, opts) {
				    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
				},
			    });
			},
			failure: function(response, opts) {
			    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
			},
		    });
		} else {
		    me.up('pveCephInstallWizard').navigateNext();
		}
	    },
	},
	{
	    title: gettext('Success'),
	    xtype: 'panel',
	    border: false,
	    bodyBorder: false,
	    onlineHelp: 'pve_ceph_install',
	    html: 'Installation successful!
'+
	    'The basic installation and configuration is complete. Depending on your setup, some of the following steps are required to start using Ceph:
'+
		'- Install Ceph on other nodes'+
		'
- Create additional Ceph Monitors'+
		'
- Create Ceph OSDs'+
		'
- Create Ceph Pools
'+
	    'To learn more, click on the help button below.
',
	    listeners: {
		activate: function() {
		    // notify owning container that it should display a help button
		    if (this.onlineHelp) {
			Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
		    }
		    var tp = this.up('#wizcontent');
		    var idx = tp.items.indexOf(this)-1;
		    for (;idx >= 0; idx--) {
			var nc = tp.items.getAt(idx);
			if (nc) {
			    nc.disable();
			}
		    }
		},
		deactivate: function() {
		    if (this.onlineHelp) {
			Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
		    }
		},
	    },
	    onSubmit: function() {
		var wizard = this.up('pveCephInstallWizard');
		wizard.close();
	    },
	},
    ],
});
Ext.define('PVE.node.CephConfigDb', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.pveNodeCephConfigDb',
    border: false,
    store: {
	proxy: {
	    type: 'proxmox',
	},
    },
    columns: [
	{
	    dataIndex: 'section',
	    text: 'WHO',
	    width: 100,
	},
	{
	    dataIndex: 'mask',
	    text: 'MASK',
	    hidden: true,
	    width: 80,
	},
	{
	    dataIndex: 'level',
	    hidden: true,
	    text: 'LEVEL',
	},
	{
	    dataIndex: 'name',
	    flex: 1,
	    text: 'OPTION',
	},
	{
	    dataIndex: 'value',
	    flex: 1,
	    text: 'VALUE',
	},
	{
	    dataIndex: 'can_update_at_runtime',
	    text: 'Runtime Updatable',
	    hidden: true,
	    width: 80,
	    renderer: Proxmox.Utils.format_boolean,
	},
    ],
    initComponent: function() {
        var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	me.store.proxy.url = '/api2/json/nodes/' + nodename + '/ceph/cfg/db';
	me.callParent();
	Proxmox.Utils.monStoreErrors(me, me.getStore());
	me.getStore().load();
    },
});
Ext.define('PVE.node.CephConfig', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveNodeCephConfig',
    bodyStyle: 'white-space:pre',
    bodyPadding: 5,
    border: false,
    scrollable: true,
    load: function() {
	var me = this;
	Proxmox.Utils.API2Request({
	    url: me.url,
	    waitMsgTarget: me,
	    failure: function(response, opts) {
		me.update(gettext('Error') + " " + response.htmlStatus);
		var msg = response.htmlStatus;
		PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node,
		    function(win) {
			me.mon(win, 'cephInstallWindowClosed', function() {
			    me.load();
			});
		    },
		);
	    },
	    success: function(response, opts) {
		var data = response.result.data;
		me.update(Ext.htmlEncode(data));
	    },
	});
    },
    initComponent: function() {
        var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	Ext.apply(me, {
	    url: '/nodes/' + nodename + '/ceph/cfg/raw',
	    listeners: {
		activate: function() {
		    me.load();
		},
	    },
	});
	me.callParent();
	me.load();
    },
});
Ext.define('PVE.node.CephConfigCrush', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveNodeCephConfigCrush',
    onlineHelp: 'chapter_pveceph',
    layout: 'border',
    items: [{
	    title: gettext('Configuration'),
	    xtype: 'pveNodeCephConfig',
	    region: 'center',
	},
	{
	    title: 'Crush Map', // do not localize
	    xtype: 'pveNodeCephCrushMap',
	    region: 'east',
	    split: true,
	    width: '50%',
	},
	{
	    title: gettext('Configuration Database'),
	    xtype: 'pveNodeCephConfigDb',
	    region: 'south',
	    split: true,
	    weight: -30,
	    height: '50%',
    }],
    initComponent: function() {
	var me = this;
	me.defaults = {
	    pveSelNode: me.pveSelNode,
	};
	me.callParent();
    },
});
Ext.define('PVE.node.CephCrushMap', {
    extend: 'Ext.panel.Panel',
    alias: ['widget.pveNodeCephCrushMap'],
    bodyStyle: 'white-space:pre',
    bodyPadding: 5,
    border: false,
    stateful: true,
    stateId: 'layout-ceph-crush',
    scrollable: true,
    load: function() {
	var me = this;
	Proxmox.Utils.API2Request({
	    url: me.url,
	    waitMsgTarget: me,
	    failure: function(response, opts) {
		me.update(gettext('Error') + " " + response.htmlStatus);
		var msg = response.htmlStatus;
		PVE.Utils.showCephInstallOrMask(
		    me.ownerCt,
		    msg,
		    me.pveSelNode.data.node,
		    win => me.mon(win, 'cephInstallWindowClosed', () => me.load()),
		);
	    },
	    success: function(response, opts) {
		var data = response.result.data;
		me.update(Ext.htmlEncode(data));
	    },
	});
    },
    initComponent: function() {
	let me = this;
	let nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	Ext.apply(me, {
	    url: `/nodes/${nodename}/ceph/crush`,
	    listeners: {
		activate: () => me.load(),
	    },
	});
	me.callParent();
	me.load();
    },
});
Ext.define('PVE.CephCreateFS', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveCephCreateFS',
    showTaskViewer: true,
    onlineHelp: 'pveceph_fs_create',
    subject: 'Ceph FS',
    isCreate: true,
    method: 'POST',
    setFSName: function(fsName) {
	var me = this;
	if (fsName === '' || fsName === undefined) {
	    fsName = 'cephfs';
	}
	me.url = "/nodes/" + me.nodename + "/ceph/fs/" + fsName;
    },
    items: [
	{
	    xtype: 'textfield',
	    fieldLabel: gettext('Name'),
	    name: 'name',
	    value: 'cephfs',
	    listeners: {
		change: function(f, value) {
		    this.up('pveCephCreateFS').setFSName(value);
		},
	    },
	    submitValue: false, // already encoded in apicall URL
	    emptyText: 'cephfs',
	},
	{
	    xtype: 'proxmoxintegerfield',
	    fieldLabel: 'Placement Groups',
	    name: 'pg_num',
	    value: 128,
	    emptyText: 128,
	    minValue: 8,
	    maxValue: 32768,
	    allowBlank: false,
	},
	{
	    xtype: 'proxmoxcheckbox',
	    fieldLabel: gettext('Add as Storage'),
	    value: true,
	    name: 'add-storage',
	    autoEl: {
		tag: 'div',
		 'data-qtip': gettext('Add the new CephFS to the cluster storage configuration.'),
	    },
	},
    ],
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	me.setFSName();
	me.callParent();
    },
});
Ext.define('PVE.NodeCephFSPanel', {
    extend: 'Ext.panel.Panel',
    xtype: 'pveNodeCephFSPanel',
    mixins: ['Proxmox.Mixin.CBind'],
    title: gettext('CephFS'),
    onlineHelp: 'pveceph_fs',
    border: false,
    defaults: {
	border: false,
	cbind: {
	    nodename: '{nodename}',
	},
    },
    viewModel: {
	parent: null,
	data: {
	    mdsCount: 0,
	},
	formulas: {
	    canCreateFS: function(get) {
		return get('mdsCount') > 0;
	    },
	},
    },
    items: [
	{
	    xtype: 'grid',
	    emptyText: Ext.String.format(gettext('No {0} configured.'), 'CephFS'),
	    controller: {
		xclass: 'Ext.app.ViewController',
		init: function(view) {
		    view.rstore = Ext.create('Proxmox.data.UpdateStore', {
			autoLoad: true,
			xtype: 'update',
			interval: 5 * 1000,
			autoStart: true,
			storeid: 'pve-ceph-fs',
			proxy: {
			    type: 'proxmox',
			    url: `/api2/json/nodes/${view.nodename}/ceph/fs`,
			},
			model: 'pve-ceph-fs',
		    });
		    view.setStore(Ext.create('Proxmox.data.DiffStore', {
			rstore: view.rstore,
			sorters: {
			    property: 'name',
			    direction: 'ASC',
			},
		    }));
		    // manages the "install ceph?" overlay
		    PVE.Utils.monitor_ceph_installed(view, view.rstore, view.nodename, true);
		    view.on('destroy', () => view.rstore.stopUpdate());
		},
		onCreate: function() {
		    let view = this.getView();
		    view.rstore.stopUpdate();
		    Ext.create('PVE.CephCreateFS', {
			autoShow: true,
			nodename: view.nodename,
			listeners: {
			    destroy: () => view.rstore.startUpdate(),
			},
		    });
		},
	    },
	    tbar: [
		{
		    text: gettext('Create CephFS'),
		    reference: 'createButton',
		    handler: 'onCreate',
		    bind: {
			disabled: '{!canCreateFS}',
		    },
		},
	    ],
	    columns: [
		{
		    header: gettext('Name'),
		    flex: 1,
		    dataIndex: 'name',
		},
		{
		    header: 'Data Pool',
		    flex: 1,
		    dataIndex: 'data_pool',
		},
		{
		    header: 'Metadata Pool',
		    flex: 1,
		    dataIndex: 'metadata_pool',
		},
	    ],
	    cbind: {
		nodename: '{nodename}',
	    },
	},
	{
	    xtype: 'pveNodeCephMDSList',
	    title: gettext('Metadata Servers'),
	    stateId: 'grid-ceph-mds',
	    type: 'mds',
	    storeLoadCallback: function(store, records, success) {
		var vm = this.getViewModel();
		if (!success || !records) {
		    vm.set('mdsCount', 0);
		    return;
		}
		let count = 0;
		for (const mds of records) {
		    if (mds.data.state === 'up:standby') {
			count++;
		    }
		}
		vm.set('mdsCount', count);
	    },
	    cbind: {
		nodename: '{nodename}',
	    },
	},
    ],
}, function() {
    Ext.define('pve-ceph-fs', {
	extend: 'Ext.data.Model',
	fields: ['name', 'data_pool', 'metadata_pool'],
	proxy: {
	    type: 'proxmox',
	    url: "/api2/json/nodes/localhost/ceph/fs",
	},
	idProperty: 'name',
    });
});
Ext.define('PVE.ceph.Log', {
    extend: 'Proxmox.panel.LogView',
    xtype: 'cephLogView',
    nodename: undefined,
    failCallback: function(response) {
	var me = this;
	var msg = response.htmlStatus;
	var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename,
	    function(win) {
		me.mon(win, 'cephInstallWindowClosed', function() {
		    me.loadTask.delay(200);
		});
	    },
	);
	if (!windowShow) {
	    Proxmox.Utils.setErrorMask(me, msg);
	}
    },
});
Ext.define('PVE.node.CephMonMgrList', {
    extend: 'Ext.container.Container',
    xtype: 'pveNodeCephMonMgr',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'chapter_pveceph',
    defaults: {
	border: false,
	onlineHelp: 'chapter_pveceph',
	flex: 1,
    },
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    items: [
	{
	    xtype: 'pveNodeCephServiceList',
	    cbind: { pveSelNode: '{pveSelNode}' },
	    type: 'mon',
	    additionalColumns: [
		{
		    header: gettext('Quorum'),
		    width: 70,
		    sortable: true,
		    renderer: Proxmox.Utils.format_boolean,
		    dataIndex: 'quorum',
		},
	    ],
	    stateId: 'grid-ceph-monitor',
	    showCephInstallMask: true,
	    title: gettext('Monitor'),
	},
	{
	    xtype: 'pveNodeCephServiceList',
	    type: 'mgr',
	    stateId: 'grid-ceph-manager',
	    cbind: { pveSelNode: '{pveSelNode}' },
	    title: gettext('Manager'),
	},
    ],
});
Ext.define('PVE.CephCreateOsd', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveCephCreateOsd',
    subject: 'Ceph OSD',
    showProgress: true,
    onlineHelp: 'pve_ceph_osds',
    initComponent: function() {
        let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	me.isCreate = true;
	Proxmox.Utils.API2Request({
	    url: `/nodes/${me.nodename}/ceph/crush`,
	    method: 'GET',
	    failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
	    success: function({ result: { data } }) {
		let classes = [...new Set(
		    Array.from(
			data.matchAll(/^device\s[0-9]*\sosd\.[0-9]*\sclass\s(.*)$/gim),
			m => m[1],
		    ).filter(v => !['hdd', 'ssd', 'nvme'].includes(v)),
		)].map(v => [v, v]);
		if (classes.length) {
		    let kvField = me.down('field[name=crush-device-class]');
		    kvField.setComboItems([...kvField.comboItems, ...classes]);
		}
	    },
	});
        Ext.applyIf(me, {
	    url: "/nodes/" + me.nodename + "/ceph/osd",
	    method: 'POST',
	    items: [
		{
		    xtype: 'inputpanel',
		    onGetValues: function(values) {
			Object.keys(values || {}).forEach(function(name) {
			    if (values[name] === '') {
				delete values[name];
			    }
			});
			return values;
		    },
		    column1: [
			{
			    xtype: 'pmxDiskSelector',
			    name: 'dev',
			    nodename: me.nodename,
			    diskType: 'unused',
			    includePartitions: true,
			    fieldLabel: gettext('Disk'),
			    allowBlank: false,
			},
		    ],
		    column2: [
			{
			    xtype: 'pmxDiskSelector',
			    name: 'db_dev',
			    nodename: me.nodename,
			    diskType: 'journal_disks',
			    includePartitions: true,
			    fieldLabel: gettext('DB Disk'),
			    value: '',
			    autoSelect: false,
			    allowBlank: true,
			    emptyText: 'use OSD disk',
			    listeners: {
				change: function(field, val) {
				    me.down('field[name=db_dev_size]').setDisabled(!val);
				},
			    },
			},
			{
			    xtype: 'numberfield',
			    name: 'db_dev_size',
			    fieldLabel: gettext('DB size') + ' (GiB)',
			    minValue: 1,
			    maxValue: 128*1024,
			    decimalPrecision: 2,
			    allowBlank: true,
			    disabled: true,
			    emptyText: gettext('Automatic'),
			},
		    ],
		    advancedColumn1: [
			{
			    xtype: 'proxmoxcheckbox',
			    name: 'encrypted',
			    fieldLabel: gettext('Encrypt OSD'),
			},
			{
			    xtype: 'proxmoxKVComboBox',
			    comboItems: [
				['hdd', 'HDD'],
				['ssd', 'SSD'],
				['nvme', 'NVMe'],
			    ],
			    name: 'crush-device-class',
			    nodename: me.nodename,
			    fieldLabel: gettext('Device Class'),
			    value: '',
			    autoSelect: false,
			    allowBlank: true,
			    editable: true,
			    emptyText: 'auto detect',
			    deleteEmpty: !me.isCreate,
			},
		    ],
		    advancedColumn2: [
			{
			    xtype: 'pmxDiskSelector',
			    name: 'wal_dev',
			    nodename: me.nodename,
			    diskType: 'journal_disks',
			    includePartitions: true,
			    fieldLabel: gettext('WAL Disk'),
			    value: '',
			    autoSelect: false,
			    allowBlank: true,
			    emptyText: 'use OSD/DB disk',
			    listeners: {
				change: function(field, val) {
				    me.down('field[name=wal_dev_size]').setDisabled(!val);
				},
			    },
			},
			{
			    xtype: 'numberfield',
			    name: 'wal_dev_size',
			    fieldLabel: gettext('WAL size') + ' (GiB)',
			    minValue: 0.5,
			    maxValue: 128*1024,
			    decimalPrecision: 2,
			    allowBlank: true,
			    disabled: true,
			    emptyText: gettext('Automatic'),
			},
		    ],
		},
		{
		    xtype: 'displayfield',
		    padding: '5 0 0 0',
		    userCls: 'pmx-hint',
		    value: 'Note: Ceph is not compatible with disks backed by a hardware ' +
			   'RAID controller. For details see ' +
			   'the reference documentation.',
		},
	    ],
	});
	me.callParent();
    },
});
Ext.define('PVE.CephRemoveOsd', {
    extend: 'Proxmox.window.Edit',
    alias: ['widget.pveCephRemoveOsd'],
    isRemove: true,
    showProgress: true,
    method: 'DELETE',
    items: [
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'cleanup',
	    checked: true,
	    labelWidth: 130,
	    fieldLabel: gettext('Cleanup Disks'),
	},
	{
	    xtype: 'displayfield',
	    name: 'osd-flag-hint',
	    userCls: 'pmx-hint',
	    value: gettext('Global flags limiting the self healing of Ceph are enabled.'),
	    hidden: true,
	},
	{
	    xtype: 'displayfield',
	    name: 'degraded-objects-hint',
	    userCls: 'pmx-hint',
	    value: gettext('Objects are degraded. Consider waiting until the cluster is healthy.'),
	    hidden: true,
	},
    ],
    initComponent: function() {
        let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (me.osdid === undefined || me.osdid < 0) {
	    throw "no osdid specified";
	}
	me.isCreate = true;
	me.title = gettext('Destroy') + ': Ceph OSD osd.' + me.osdid.toString();
        Ext.applyIf(me, {
	    url: "/nodes/" + me.nodename + "/ceph/osd/" + me.osdid.toString(),
        });
        me.callParent();
	if (me.warnings.flags) {
	    me.down('field[name=osd-flag-hint]').setHidden(false);
	}
	if (me.warnings.degraded) {
	    me.down('field[name=degraded-objects-hint]').setHidden(false);
	}
    },
});
Ext.define('PVE.CephSetFlags', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveCephSetFlags',
    showProgress: true,
    width: 720,
    layout: 'fit',
    onlineHelp: 'pve_ceph_osds',
    isCreate: true,
    title: Ext.String.format(gettext('Manage {0}'), 'Global OSD Flags'),
    submitText: gettext('Apply'),
    items: [
	{
	    xtype: 'inputpanel',
	    onGetValues: function(values) {
		let me = this;
		let val = {};
		me.down('#flaggrid').getStore().each((rec) => {
		    val[rec.data.name] = rec.data.value ? 1 : 0;
		});
		return val;
	    },
	    items: [
		{
		    xtype: 'grid',
		    itemId: 'flaggrid',
		    store: {
			listeners: {
			    update: function() {
				this.commitChanges();
			    },
			},
		    },
		    columns: [
			{
			    text: gettext('Enable'),
			    xtype: 'checkcolumn',
			    width: 75,
			    dataIndex: 'value',
			},
			{
			    text: 'Name',
			    dataIndex: 'name',
			},
			{
			    text: 'Description',
			    flex: 1,
			    dataIndex: 'description',
			},
		    ],
		},
	    ],
	},
    ],
    initComponent: function() {
        let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
        Ext.applyIf(me, {
	    url: "/cluster/ceph/flags",
	    method: 'PUT',
	});
	me.callParent();
	let grid = me.down('#flaggrid');
	me.load({
	    success: function(response, options) {
		let data = response.result.data;
		grid.getStore().setData(data);
		// re-align after store load, else the window is not centered
		me.alignTo(Ext.getBody(), 'c-c');
	    },
	});
    },
});
Ext.define('PVE.node.CephOsdTree', {
    extend: 'Ext.tree.Panel',
    alias: ['widget.pveNodeCephOsdTree'],
    onlineHelp: 'chapter_pveceph',
    viewModel: {
	data: {
	    nodename: '',
	    flags: [],
	    maxversion: '0',
	    mixedversions: false,
	    versions: {},
	    isOsd: false,
	    downOsd: false,
	    upOsd: false,
	    inOsd: false,
	    outOsd: false,
	    osdid: '',
	    osdhost: '',
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	reload: function() {
	    let me = this;
	    let view = me.getView();
	    let vm = me.getViewModel();
	    let nodename = vm.get('nodename');
	    let sm = view.getSelectionModel();
	    Proxmox.Utils.API2Request({
                url: "/nodes/" + nodename + "/ceph/osd",
		waitMsgTarget: view,
		method: 'GET',
		failure: function(response, opts) {
		    let msg = response.htmlStatus;
		    PVE.Utils.showCephInstallOrMask(view, msg, nodename, win =>
			view.mon(win, 'cephInstallWindowClosed', () => { me.reload(); }),
		    );
		},
		success: function(response, opts) {
		    let data = response.result.data;
		    let selected = view.getSelection();
		    let name;
		    if (selected.length) {
			name = selected[0].data.name;
		    }
		    data.versions = data.versions || {};
		    vm.set('versions', data.versions);
		    // extract max version
		    let maxversion = "0";
		    let mixedversions = false;
		    let traverse;
		    traverse = function(node, fn) {
			fn(node);
			if (Array.isArray(node.children)) {
			    node.children.forEach(c => { traverse(c, fn); });
			}
		    };
		    traverse(data.root, node => {
			// compatibility for old api call
			if (node.type === 'host' && !node.version) {
			    node.version = data.versions[node.name];
			}
			if (node.version === undefined) {
			    return;
			}
			if (PVE.Utils.compare_ceph_versions(node.version, maxversion) !== 0 && maxversion !== "0") {
			    mixedversions = true;
			}
			if (PVE.Utils.compare_ceph_versions(node.version, maxversion) > 0) {
			    maxversion = node.version;
			}
		    });
		    vm.set('maxversion', maxversion);
		    vm.set('mixedversions', mixedversions);
		    sm.deselectAll();
		    view.setRootNode(data.root);
		    view.expandAll();
		    if (name) {
			let node = view.getRootNode().findChild('name', name, true);
			if (node) {
			    view.setSelection([node]);
			}
		    }
		    let flags = data.flags.split(',');
		    vm.set('flags', flags);
		},
	    });
	},
	osd_cmd: function(comp) {
	    let me = this;
	    let vm = this.getViewModel();
	    let cmd = comp.cmd;
	    let params = comp.params || {};
	    let osdid = vm.get('osdid');
	    let doRequest = function() {
		let targetnode = vm.get('osdhost');
		// cmds not node specific and need to work if the OSD node is down
		if (['in', 'out'].includes(cmd)) {
		    targetnode = vm.get('nodename');
		}
		Proxmox.Utils.API2Request({
		    url: `/nodes/${targetnode}/ceph/osd/${osdid}/${cmd}`,
		    waitMsgTarget: me.getView(),
		    method: 'POST',
		    params: params,
		    success: () => { me.reload(); },
		    failure: function(response, opts) {
			Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		    },
		});
	    };
	    if (cmd === 'scrub') {
		Ext.MessageBox.defaultButton = params.deep === 1 ? 2 : 1;
		Ext.Msg.show({
		    title: gettext('Confirm'),
		    icon: params.deep === 1 ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
		    msg: params.deep !== 1
		       ? Ext.String.format(gettext("Scrub OSD.{0}"), osdid)
		       : Ext.String.format(gettext("Deep Scrub OSD.{0}"), osdid) +
			   "
Caution: This can reduce performance while it is running.",
		    buttons: Ext.Msg.YESNO,
		    callback: function(btn) {
			if (btn !== 'yes') {
			    return;
			}
			doRequest();
		    },
		});
	    } else {
		doRequest();
	    }
	},
	create_osd: function() {
	    let me = this;
	    let vm = this.getViewModel();
	    Ext.create('PVE.CephCreateOsd', {
		nodename: vm.get('nodename'),
		taskDone: () => { me.reload(); },
	    }).show();
	},
	destroy_osd: async function() {
	    let me = this;
	    let vm = this.getViewModel();
	    let warnings = {
		flags: false,
		degraded: false,
	    };
	    let flagsPromise = Proxmox.Async.api2({
		url: `/cluster/ceph/flags`,
		method: 'GET',
	    });
	    let statusPromise = Proxmox.Async.api2({
		url: `/cluster/ceph/status`,
		method: 'GET',
	    });
	    me.getView().mask(gettext('Loading...'));
	    try {
		let result = await Promise.all([flagsPromise, statusPromise]);
		let flagsData = result[0].result.data;
		let statusData = result[1].result.data;
		let flags = Array.from(
		    flagsData.filter(v => v.value),
		    v => v.name,
		).filter(v => ['norebalance', 'norecover', 'noout'].includes(v));
		if (flags.length) {
		    warnings.flags = true;
		}
		if (Object.keys(statusData.pgmap).includes('degraded_objects')) {
		    warnings.degraded = true;
		}
	    } catch (error) {
		Ext.Msg.alert(gettext('Error'), error.htmlStatus);
		me.getView().unmask();
		return;
	    }
	    me.getView().unmask();
	    Ext.create('PVE.CephRemoveOsd', {
		nodename: vm.get('osdhost'),
		osdid: vm.get('osdid'),
		warnings: warnings,
		taskDone: () => { me.reload(); },
		autoShow: true,
	    });
	},
	set_flags: function() {
	    let me = this;
	    let vm = this.getViewModel();
	    Ext.create('PVE.CephSetFlags', {
		nodename: vm.get('nodename'),
		taskDone: () => { me.reload(); },
	    }).show();
	},
	service_cmd: function(comp) {
	    let me = this;
	    let vm = this.getViewModel();
	    let cmd = comp.cmd || comp;
	    let doRequest = function() {
		Proxmox.Utils.API2Request({
		    url: `/nodes/${vm.get('osdhost')}/ceph/${cmd}`,
		    params: { service: "osd." + vm.get('osdid') },
		    waitMsgTarget: me.getView(),
		    method: 'POST',
		    success: function(response, options) {
			let upid = response.result.data;
			let win = Ext.create('Proxmox.window.TaskProgress', {
			    upid: upid,
			    taskDone: () => { me.reload(); },
			});
			win.show();
		    },
		    failure: function(response, opts) {
			Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		    },
		});
	    };
	    if (cmd === "stop") {
		Proxmox.Utils.API2Request({
		    url: `/nodes/${vm.get('osdhost')}/ceph/cmd-safety`,
		    params: {
			service: 'osd',
			id: vm.get('osdid'),
			action: 'stop',
		    },
		    waitMsgTarget: me.getView(),
		    method: 'GET',
		    success: function({ result: { data } }) {
			if (!data.safe) {
			    Ext.Msg.show({
				title: gettext('Warning'),
				message: data.status,
				icon: Ext.Msg.WARNING,
				buttons: Ext.Msg.OKCANCEL,
				buttonText: { ok: gettext('Stop OSD') },
				fn: function(selection) {
				    if (selection === 'ok') {
					doRequest();
				    }
				},
			    });
			} else {
			    doRequest();
			}
		    },
		    failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
		});
	    } else {
		doRequest();
	    }
	},
	run_details: function(view, rec) {
	    if (rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0) {
		this.details();
	    }
	},
	details: function() {
	    let vm = this.getViewModel();
	    Ext.create('PVE.CephOsdDetails', {
		nodename: vm.get('osdhost'),
		osdid: vm.get('osdid'),
	    }).show();
	},
	set_selection_status: function(tp, selection) {
	    if (selection.length < 1) {
		return;
	    }
	    let rec = selection[0];
	    let vm = this.getViewModel();
	    let isOsd = rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0;
	    vm.set('isOsd', isOsd);
	    vm.set('downOsd', isOsd && rec.data.status === 'down');
	    vm.set('upOsd', isOsd && rec.data.status !== 'down');
	    vm.set('inOsd', isOsd && rec.data.in);
	    vm.set('outOsd', isOsd && !rec.data.in);
	    vm.set('osdid', isOsd ? rec.data.id : undefined);
	    vm.set('osdhost', isOsd ? rec.data.host : undefined);
	},
	render_status: function(value, metaData, rec) {
	    if (!value) {
		return value;
	    }
	    let inout = rec.data.in ? 'in' : 'out';
	    let updownicon = value === 'up' ? 'good fa-arrow-circle-up'
		: 'critical fa-arrow-circle-down';
	    let inouticon = rec.data.in ? 'good fa-circle'
		: 'warning fa-circle-o';
	    let text = value + '  / ' +
		inout + ' ';
	    return text;
	},
	render_wal: function(value, metaData, rec) {
	    if (!value &&
		rec.data.osdtype === 'bluestore' &&
		rec.data.type === 'osd') {
		return 'N/A';
	    }
	    return value;
	},
	render_version: function(value, metadata, rec) {
	    let vm = this.getViewModel();
	    let versions = vm.get('versions');
	    let icon = "";
	    let version = value || "";
	    let maxversion = vm.get('maxversion');
	    if (value && PVE.Utils.compare_ceph_versions(value, maxversion) !== 0) {
		let host_version = rec.parentNode?.data?.version || versions[rec.data.host] || "";
		if (rec.data.type === 'host' || PVE.Utils.compare_ceph_versions(host_version, maxversion) !== 0) {
		    icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE');
		} else {
		    icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
		}
	    } else if (value && vm.get('mixedversions')) {
		icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK');
	    }
	    return icon + version;
	},
	render_osd_val: function(value, metaData, rec) {
	    return rec.data.type === 'osd' ? value : '';
	},
	render_osd_weight: function(value, metaData, rec) {
	    if (rec.data.type !== 'osd') {
		return '';
	    }
	    return Ext.util.Format.number(value, '0.00###');
	},
	render_osd_latency: function(value, metaData, rec) {
	    if (rec.data.type !== 'osd') {
		return '';
	    }
	    let commit_ms = rec.data.commit_latency_ms,
	        apply_ms = rec.data.apply_latency_ms;
	    return apply_ms + ' / ' + commit_ms;
	},
	render_osd_size: function(value, metaData, rec) {
	    return this.render_osd_val(Proxmox.Utils.render_size(value), metaData, rec);
	},
	control: {
	    '#': {
		selectionchange: 'set_selection_status',
	    },
	},
	init: function(view) {
	    let me = this;
	    let vm = this.getViewModel();
	    if (!view.pveSelNode.data.node) {
		throw "no node name specified";
	    }
	    vm.set('nodename', view.pveSelNode.data.node);
	    me.callParent();
	    me.reload();
	},
    },
    stateful: true,
    stateId: 'grid-ceph-osd',
    rootVisible: false,
    useArrows: true,
    listeners: {
	itemdblclick: 'run_details',
    },
    columns: [
	{
	    xtype: 'treecolumn',
	    text: 'Name',
	    dataIndex: 'name',
	    width: 150,
	},
	{
	    text: 'Type',
	    dataIndex: 'type',
	    hidden: true,
	    align: 'right',
	    width: 75,
	},
	{
	    text: gettext("Class"),
	    dataIndex: 'device_class',
	    align: 'right',
	    width: 75,
	},
	{
	    text: "OSD Type",
	    dataIndex: 'osdtype',
	    align: 'right',
	    width: 100,
	},
	{
	    text: "Bluestore Device",
	    dataIndex: 'blfsdev',
	    align: 'right',
	    width: 75,
	    hidden: true,
	},
	{
	    text: "DB Device",
	    dataIndex: 'dbdev',
	    align: 'right',
	    width: 75,
	    hidden: true,
	},
	{
	    text: "WAL Device",
	    dataIndex: 'waldev',
	    align: 'right',
	    renderer: 'render_wal',
	    width: 75,
	    hidden: true,
	},
	{
	    text: 'Status',
	    dataIndex: 'status',
	    align: 'right',
	    renderer: 'render_status',
	    width: 120,
	},
	{
	    text: gettext('Version'),
	    dataIndex: 'version',
	    align: 'right',
	    renderer: 'render_version',
	},
	{
	    text: 'weight',
	    dataIndex: 'crush_weight',
	    align: 'right',
	    renderer: 'render_osd_weight',
	    width: 90,
	},
	{
	    text: 'reweight',
	    dataIndex: 'reweight',
	    align: 'right',
	    renderer: 'render_osd_weight',
	    width: 90,
	},
	{
	    text: gettext('Used') + ' (%)',
	    dataIndex: 'percent_used',
	    align: 'right',
	    renderer: function(value, metaData, rec) {
		if (rec.data.type !== 'osd') {
		    return '';
		}
		return Ext.util.Format.number(value, '0.00');
	    },
	    width: 100,
	},
	{
	    text: gettext('Total'),
	    dataIndex: 'total_space',
	    align: 'right',
	    renderer: 'render_osd_size',
	    width: 100,
	},
	{
	    text: 'Apply/Commit
Latency (ms)',
	    dataIndex: 'apply_latency_ms',
	    align: 'right',
	    renderer: 'render_osd_latency',
	    width: 120,
	},
	{
	    text: 'PGs',
	    dataIndex: 'pgs',
	    align: 'right',
	    renderer: 'render_osd_val',
	    width: 90,
	},
    ],
    tbar: {
	items: [
	    {
		text: gettext('Reload'),
		iconCls: 'fa fa-refresh',
		handler: 'reload',
	    },
	    '-',
	    {
		text: gettext('Create') + ': OSD',
		handler: 'create_osd',
	    },
	    {
		text: Ext.String.format(gettext('Manage {0}'), 'Global Flags'),
		handler: 'set_flags',
	    },
	    '->',
	    {
		xtype: 'tbtext',
		data: {
		    osd: undefined,
		},
		bind: {
		    data: {
			osd: "{osdid}",
		    },
		},
		tpl: [
		    '',
		    'osd.{osd}:',
		    '',
		    gettext('No OSD selected'),
		    '',
		],
	    },
	    {
		text: gettext('Details'),
		iconCls: 'fa fa-info-circle',
		disabled: true,
		bind: {
		    disabled: '{!isOsd}',
		},
		handler: 'details',
	    },
	    {
		text: gettext('Start'),
		iconCls: 'fa fa-play',
		disabled: true,
		bind: {
		    disabled: '{!downOsd}',
		},
		cmd: 'start',
		handler: 'service_cmd',
	    },
	    {
		text: gettext('Stop'),
		iconCls: 'fa fa-stop',
		disabled: true,
		bind: {
		    disabled: '{!upOsd}',
		},
		cmd: 'stop',
		handler: 'service_cmd',
	    },
	    {
		text: gettext('Restart'),
		iconCls: 'fa fa-refresh',
		disabled: true,
		bind: {
		    disabled: '{!upOsd}',
		},
		cmd: 'restart',
		handler: 'service_cmd',
	    },
	    '-',
	    {
		text: 'Out',
		iconCls: 'fa fa-circle-o',
		disabled: true,
		bind: {
		    disabled: '{!inOsd}',
		},
		cmd: 'out',
		handler: 'osd_cmd',
	    },
	    {
		text: 'In',
		iconCls: 'fa fa-circle',
		disabled: true,
		bind: {
		    disabled: '{!outOsd}',
		},
		cmd: 'in',
		handler: 'osd_cmd',
	    },
	    '-',
	    {
		text: gettext('More'),
		iconCls: 'fa fa-bars',
		disabled: true,
		bind: {
		    disabled: '{!isOsd}',
		},
		menu: [
		    {
			text: gettext('Scrub'),
			iconCls: 'fa fa-shower',
			cmd: 'scrub',
			handler: 'osd_cmd',
		    },
		    {
			text: gettext('Deep Scrub'),
			iconCls: 'fa fa-bath',
			cmd: 'scrub',
			params: {
			    deep: 1,
			},
			handler: 'osd_cmd',
		    },
		    {
			text: gettext('Destroy'),
			itemId: 'remove',
			iconCls: 'fa fa-fw fa-trash-o',
			bind: {
			    disabled: '{!downOsd}',
			},
			handler: 'destroy_osd',
		    },
		],
	    },
	],
    },
    fields: [
	'name', 'type', 'status', 'host', 'in', 'id',
	{ type: 'number', name: 'reweight' },
	{ type: 'number', name: 'percent_used' },
	{ type: 'integer', name: 'bytes_used' },
	{ type: 'integer', name: 'total_space' },
	{ type: 'integer', name: 'apply_latency_ms' },
	{ type: 'integer', name: 'commit_latency_ms' },
	{ type: 'string', name: 'device_class' },
	{ type: 'string', name: 'osdtype' },
	{ type: 'string', name: 'blfsdev' },
	{ type: 'string', name: 'dbdev' },
	{ type: 'string', name: 'waldev' },
	{
 type: 'string', name: 'version', calculate: function(data) {
	    return PVE.Utils.parse_ceph_version(data);
	},
},
	{
 type: 'string', name: 'iconCls', calculate: function(data) {
	    let iconMap = {
		host: 'fa-building',
		osd: 'fa-hdd-o',
		root: 'fa-server',
	    };
	    return `fa x-fa-tree ${iconMap[data.type] ?? 'fa-folder-o'}`;
	},
},
	{ type: 'number', name: 'crush_weight' },
    ],
});
Ext.define('pve-osd-details-devices', {
    extend: 'Ext.data.Model',
    fields: ['device', 'type', 'physical_device', 'size', 'support_discard', 'dev_node'],
    idProperty: 'device',
});
Ext.define('PVE.CephOsdDetails', {
    extend: 'Ext.window.Window',
    alias: ['widget.pveCephOsdDetails'],
    mixins: ['Proxmox.Mixin.CBind'],
    cbindData: function() {
	let me = this;
	me.baseUrl = `/nodes/${me.nodename}/ceph/osd/${me.osdid}`;
	return {
	    title: `${gettext('Details')}: OSD ${me.osdid}`,
	};
    },
    viewModel: {
	data: {
	    device: '',
	},
    },
    modal: true,
    width: 650,
    minHeight: 250,
    resizable: true,
    cbind: {
	title: '{title}',
    },
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    defaults: {
	layout: 'fit',
	border: false,
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	reload: function() {
	    let view = this.getView();
	    Proxmox.Utils.API2Request({
		url: `${view.baseUrl}/metadata`,
		waitMsgTarget: view.lookup('detailsTabs'),
		method: 'GET',
		failure: function(response, opts) {
		    Proxmox.Utils.setErrorMask(view.lookup('detailsTabs'), response.htmlStatus);
		},
		success: function(response, opts) {
		    let d = response.result.data;
		    let osdData = Object.keys(d.osd).sort().map(x => ({ key: x, value: d.osd[x] }));
		    view.osdStore.loadData(osdData);
		    let devices = view.lookup('devices');
		    let deviceStore = devices.getStore();
		    deviceStore.loadData(d.devices);
		    view.lookup('osdGeneral').rstore.fireEvent('load', view.osdStore, osdData, true);
		    view.lookup('osdNetwork').rstore.fireEvent('load', view.osdStore, osdData, true);
		    // select 'block' device automatically on first load
		    if (devices.getSelection().length === 0) {
			devices.setSelection(deviceStore.findRecord('device', 'block'));
		    }
		},
	    });
	},
	showDevInfo: function(grid, selected) {
	    let view = this.getView();
	    if (selected[0]) {
		let device = selected[0].data.device;
		this.getViewModel().set('device', device);
		let detailStore = view.lookup('volumeDetails');
		detailStore.rstore.getProxy().setUrl(`api2/json${view.baseUrl}/lv-info`);
		detailStore.rstore.getProxy().setExtraParams({ 'type': device });
		detailStore.setLoading();
		detailStore.rstore.load({ callback: () => detailStore.setLoading(false) });
	    }
	},
	init: function() {
	    this.reload();
	},
	control: {
	    'grid[reference=devices]': {
		selectionchange: 'showDevInfo',
	    },
	},
    },
    tbar: [
	{
	    text: gettext('Reload'),
	    iconCls: 'fa fa-refresh',
	    handler: 'reload',
	},
    ],
    initComponent: function() {
        let me = this;
	me.osdStore = Ext.create('Proxmox.data.ObjectStore');
	Ext.applyIf(me, {
	    items: [
		{
		    xtype: 'tabpanel',
		    reference: 'detailsTabs',
		    items: [
			{
			    xtype: 'proxmoxObjectGrid',
			    reference: 'osdGeneral',
			    tooltip: gettext('Various information about the OSD'),
			    rstore: me.osdStore,
			    title: gettext('General'),
			    viewConfig: {
				enableTextSelection: true,
			    },
			    gridRows: [
				{
				    xtype: 'text',
				    name: 'version',
				    text: gettext('Version'),
				},
				{
				    xtype: 'text',
				    name: 'hostname',
				    text: gettext('Hostname'),
				},
				{
				    xtype: 'text',
				    name: 'osd_data',
				    text: gettext('OSD data path'),
				},
				{
				    xtype: 'text',
				    name: 'osd_objectstore',
				    text: gettext('OSD object store'),
				},
				{
				    xtype: 'text',
				    name: 'mem_usage',
				    text: gettext('Memory usage'),
				    renderer: Proxmox.Utils.render_size,
				},
				{
				    xtype: 'text',
				    name: 'pid',
				    text: `${gettext('Process ID')} (PID)`,
				},
			    ],
			},
			{
			    xtype: 'proxmoxObjectGrid',
			    reference: 'osdNetwork',
			    tooltip: gettext('Addresses and ports used by the OSD service'),
			    rstore: me.osdStore,
			    title: gettext('Network'),
			    viewConfig: {
				enableTextSelection: true,
			    },
			    gridRows: [
				{
				    xtype: 'text',
				    name: 'front_addr',
				    text: `${gettext('Front Address')}
(Client & Monitor)`,
				    renderer: PVE.Utils.render_ceph_osd_addr,
				},
				{
				    xtype: 'text',
				    name: 'hb_front_addr',
				    text: gettext('Heartbeat Front Address'),
				    renderer: PVE.Utils.render_ceph_osd_addr,
				},
				{
				    xtype: 'text',
				    name: 'back_addr',
				    text: `${gettext('Back Address')}
(OSD)`,
				    renderer: PVE.Utils.render_ceph_osd_addr,
				},
				{
				    xtype: 'text',
				    name: 'hb_back_addr',
				    text: gettext('Heartbeat Back Address'),
				    renderer: PVE.Utils.render_ceph_osd_addr,
				},
			    ],
			},
			{
			    xtype: 'panel',
			    title: 'Devices',
			    tooltip: gettext('Physical devices used by the OSD'),
			    items: [
				{
				    xtype: 'grid',
				    border: false,
				    reference: 'devices',
				    store: {
					model: 'pve-osd-details-devices',
				    },
				    columns: {
					items: [
					    { text: gettext('Device'), dataIndex: 'device' },
					    { text: gettext('Type'), dataIndex: 'type' },
					    {
						text: gettext('Physical Device'),
						dataIndex: 'physical_device',
					    },
					    {
						text: gettext('Size'),
						dataIndex: 'size',
						renderer: Proxmox.Utils.render_size,
					    },
					    {
						text: 'Discard',
						dataIndex: 'support_discard',
						hidden: true,
					    },
					    {
						text: gettext('Device node'),
						dataIndex: 'dev_node',
						hidden: true,
					    },
					],
					defaults: {
					    tdCls: 'pointer',
					    flex: 1,
					},
				    },
				},
				{
				    xtype: 'proxmoxObjectGrid',
				    reference: 'volumeDetails',
				    maskOnLoad: true,
				    viewConfig: {
					enableTextSelection: true,
				    },
				    bind: {
					title: Ext.String.format(
						gettext('Volume Details for {0}'),
						'{device}',
					),
				    },
				    rows: {
					creation_time: {
					    header: gettext('Creation time'),
					},
					lv_name: {
					    header: gettext('LV Name'),
					},
					lv_path: {
					    header: gettext('LV Path'),
					},
					lv_uuid: {
					    header: gettext('LV UUID'),
					},
					vg_name: {
					    header: gettext('VG Name'),
					},
				    },
				    url: 'nodes/', //placeholder will be set when device is selected
				},
			    ],
			},
		    ],
		},
	    ],
	});
	me.callParent();
    },
});
Ext.define('PVE.CephPoolInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveCephPoolInputPanel',
    mixins: ['Proxmox.Mixin.CBind'],
    showProgress: true,
    onlineHelp: 'pve_ceph_pools',
    subject: 'Ceph Pool',
    column1: [
	{
	    xtype: 'pmxDisplayEditField',
	    fieldLabel: gettext('Name'),
	    cbind: {
		editable: '{isCreate}',
		value: '{pool_name}',
	    },
	    name: 'name',
	    allowBlank: false,
	},
	{
	    xtype: 'pmxDisplayEditField',
	    cbind: {
		editable: '{!isErasure}',
	    },
	    fieldLabel: gettext('Size'),
	    name: 'size',
	    editConfig: {
		xtype: 'proxmoxintegerfield',
		value: 3,
		minValue: 2,
		maxValue: 7,
		allowBlank: false,
		listeners: {
		    change: function(field, val) {
			let size = Math.round(val / 2);
			if (size > 1) {
			    field.up('inputpanel').down('field[name=min_size]').setValue(size);
			}
		    },
		},
	    },
	},
    ],
    column2: [
	{
	    xtype: 'proxmoxKVComboBox',
	    fieldLabel: 'PG Autoscale Mode',
	    name: 'pg_autoscale_mode',
	    comboItems: [
		['warn', 'warn'],
		['on', 'on'],
		['off', 'off'],
	    ],
	    value: 'on', // FIXME: check ceph version and only default to on on octopus and newer
	    allowBlank: false,
	    autoSelect: false,
	    labelWidth: 140,
	},
	{
	    xtype: 'proxmoxcheckbox',
	    fieldLabel: gettext('Add as Storage'),
	    cbind: {
		value: '{isCreate}',
		hidden: '{!isCreate}',
	    },
	    name: 'add_storages',
	    labelWidth: 140,
	    autoEl: {
		tag: 'div',
		'data-qtip': gettext('Add the new pool to the cluster storage configuration.'),
	    },
	},
    ],
    advancedColumn1: [
	{
	    xtype: 'proxmoxintegerfield',
	    fieldLabel: gettext('Min. Size'),
	    name: 'min_size',
	    value: 2,
	    cbind: {
		minValue: (get) => get('isCreate') ? 2 : 1,
	    },
	    maxValue: 7,
	    allowBlank: false,
	    listeners: {
		change: function(field, minSize) {
		    let panel = field.up('inputpanel');
		    let size = panel.down('field[name=size]').getValue();
		    let showWarning = minSize < (size / 2) && minSize !== size;
		    let fieldLabel = gettext('Min. Size');
		    if (showWarning) {
			fieldLabel = gettext('Min. Size') + ' ';
		    }
		    panel.down('field[name=min_size-warning]').setHidden(!showWarning);
		    field.setFieldLabel(fieldLabel);
		},
	    },
	},
	{
	    xtype: 'displayfield',
	    name: 'min_size-warning',
	    userCls: 'pmx-hint',
	    value: gettext('min_size < size/2 can lead to data loss, incomplete PGs or unfound objects.'),
	    hidden: true,
	},
	{
	    xtype: 'pmxDisplayEditField',
	    cbind: {
		editable: '{!isErasure}',
		nodename: '{nodename}',
		isCreate: '{isCreate}',
	    },
	    fieldLabel: 'Crush Rule', // do not localize
	    name: 'crush_rule',
	    editConfig: {
		xtype: 'pveCephRuleSelector',
		allowBlank: false,
	    },
	},
	{
	    xtype: 'proxmoxintegerfield',
	    fieldLabel: '# of PGs',
	    name: 'pg_num',
	    value: 128,
	    minValue: 1,
	    maxValue: 32768,
	    allowBlank: false,
	    emptyText: 128,
	},
    ],
    advancedColumn2: [
	{
	    xtype: 'numberfield',
	    fieldLabel: gettext('Target Ratio'),
	    name: 'target_size_ratio',
	    minValue: 0,
	    decimalPrecision: 3,
	    allowBlank: true,
	    emptyText: '0.0',
	    autoEl: {
		tag: 'div',
		'data-qtip': gettext('The ratio of storage amount this pool will consume compared to other pools with ratios. Used for auto-scaling.'),
	    },
	},
	{
	    xtype: 'pveSizeField',
	    name: 'target_size',
	    fieldLabel: gettext('Target Size'),
	    unit: 'GiB',
	    minValue: 0,
	    allowBlank: true,
	    allowZero: true,
	    emptyText: '0',
	    emptyValue: 0,
	    autoEl: {
		tag: 'div',
		'data-qtip': gettext('The amount of data eventually stored in this pool. Used for auto-scaling.'),
	    },
	},
	{
	    xtype: 'displayfield',
	    userCls: 'pmx-hint',
	    value: Ext.String.format(gettext('{0} takes precedence.'), gettext('Target Ratio')), // FIXME: tooltip?
	},
	{
	    xtype: 'proxmoxintegerfield',
	    fieldLabel: 'Min. # of PGs',
	    name: 'pg_num_min',
	    minValue: 0,
	    allowBlank: true,
	    emptyText: '0',
	},
    ],
    onGetValues: function(values) {
	Object.keys(values || {}).forEach(function(name) {
	    if (values[name] === '') {
		delete values[name];
	    }
	});
	return values;
    },
});
Ext.define('PVE.Ceph.PoolEdit', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveCephPoolEdit',
    mixins: ['Proxmox.Mixin.CBind'],
    cbindData: {
	pool_name: '',
	isCreate: (cfg) => !cfg.pool_name,
    },
    cbind: {
	autoLoad: get => !get('isCreate'),
	url: get => get('isCreate')
	    ? `/nodes/${get('nodename')}/ceph/pool`
	    : `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}`,
	loadUrl: get => `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}/status`,
	method: get => get('isCreate') ? 'POST' : 'PUT',
    },
    showProgress: true,
    subject: gettext('Ceph Pool'),
    items: [{
	xtype: 'pveCephPoolInputPanel',
	cbind: {
	    nodename: '{nodename}',
	    pool_name: '{pool_name}',
	    isErasure: '{isErasure}',
	    isCreate: '{isCreate}',
	},
    }],
});
Ext.define('PVE.node.Ceph.PoolList', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveNodeCephPoolList',
    onlineHelp: 'chapter_pveceph',
    stateful: true,
    stateId: 'grid-ceph-pools',
    bufferedRenderer: false,
    features: [{ ftype: 'summary' }],
    columns: [
	{
	    text: gettext('Name'),
	    minWidth: 120,
	    flex: 2,
	    sortable: true,
	    dataIndex: 'pool_name',
	},
	{
	    text: gettext('Type'),
	    minWidth: 100,
	    flex: 1,
	    dataIndex: 'type',
	    hidden: true,
	},
	{
	    text: gettext('Size') + '/min',
	    minWidth: 100,
	    flex: 1,
	    align: 'right',
	    renderer: (v, meta, rec) => `${v}/${rec.data.min_size}`,
	    dataIndex: 'size',
	},
	{
	    text: '# of Placement Groups',
	    flex: 1,
	    minWidth: 100,
	    align: 'right',
	    dataIndex: 'pg_num',
	},
	{
	    text: gettext('Optimal # of PGs'),
	    flex: 1,
	    minWidth: 100,
	    align: 'right',
	    dataIndex: 'pg_num_final',
	    renderer: function(value, metaData) {
		if (!value) {
		    value = ' n/a';
		    metaData.tdAttr = 'data-qtip="Needs pg_autoscaler module enabled."';
		}
		return value;
	    },
	},
	{
	    text: gettext('Min. # of PGs'),
	    flex: 1,
	    minWidth: 100,
	    align: 'right',
	    dataIndex: 'pg_num_min',
	    hidden: true,
	},
	{
	    text: gettext('Target Ratio'),
	    flex: 1,
	    minWidth: 100,
	    align: 'right',
	    dataIndex: 'target_size_ratio',
	    renderer: Ext.util.Format.numberRenderer('0.0000'),
	    hidden: true,
	},
	{
	    text: gettext('Target Size'),
	    flex: 1,
	    minWidth: 100,
	    align: 'right',
	    dataIndex: 'target_size',
	    hidden: true,
	    renderer: function(v, metaData, rec) {
		let value = Proxmox.Utils.render_size(v);
		if (rec.data.target_size_ratio > 0) {
		    value = ' ' + value;
		    metaData.tdAttr = 'data-qtip="Target Size Ratio takes precedence over Target Size."';
		}
		return value;
	    },
	},
	{
	    text: gettext('Autoscale Mode'),
	    flex: 1,
	    minWidth: 100,
	    align: 'right',
	    dataIndex: 'pg_autoscale_mode',
	},
	{
	    text: 'CRUSH Rule (ID)',
	    flex: 1,
	    align: 'right',
	    minWidth: 150,
	    renderer: (v, meta, rec) => `${v} (${rec.data.crush_rule})`,
	    dataIndex: 'crush_rule_name',
	},
	{
	    text: gettext('Used') + ' (%)',
	    flex: 1,
	    minWidth: 150,
	    sortable: true,
	    align: 'right',
	    dataIndex: 'bytes_used',
	    summaryType: 'sum',
	    summaryRenderer: Proxmox.Utils.render_size,
	    renderer: function(v, meta, rec) {
		let percentage = Ext.util.Format.percent(rec.data.percent_used, '0.00');
		let used = Proxmox.Utils.render_size(v);
		return `${used} (${percentage})`;
	    },
	},
    ],
    initComponent: function() {
        var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var sm = Ext.create('Ext.selection.RowModel', {});
	var rstore = Ext.create('Proxmox.data.UpdateStore', {
	    interval: 3000,
	    storeid: 'ceph-pool-list' + nodename,
	    model: 'ceph-pool-list',
	    proxy: {
		type: 'proxmox',
		url: `/api2/json/nodes/${nodename}/ceph/pool`,
	    },
	});
	let store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore });
	// manages the "install ceph?" overlay
	PVE.Utils.monitor_ceph_installed(me, rstore, nodename);
	var run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec || !rec.data.pool_name) {
		return;
	    }
	    Ext.create('PVE.Ceph.PoolEdit', {
		title: gettext('Edit') + ': Ceph Pool',
		nodename: nodename,
		pool_name: rec.data.pool_name,
		isErasure: rec.data.type === 'erasure',
		autoShow: true,
		listeners: {
		    destroy: () => rstore.load(),
		},
	    });
	};
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: [
		{
		    text: gettext('Create'),
		    handler: function() {
			Ext.create('PVE.Ceph.PoolEdit', {
			    title: gettext('Create') + ': Ceph Pool',
			    isCreate: true,
			    isErasure: false,
			    nodename: nodename,
			    autoShow: true,
			    listeners: {
				destroy: () => rstore.load(),
			    },
			});
		    },
		},
		{
		    xtype: 'proxmoxButton',
		    text: gettext('Edit'),
		    selModel: sm,
		    disabled: true,
		    handler: run_editor,
		},
		{
		    xtype: 'proxmoxButton',
		    text: gettext('Destroy'),
		    selModel: sm,
		    disabled: true,
		    handler: function() {
			let rec = sm.getSelection()[0];
			if (!rec || !rec.data.pool_name) {
			    return;
			}
			let poolName = rec.data.pool_name;
			Ext.create('Proxmox.window.SafeDestroy', {
			    showProgress: true,
			    url: `/nodes/${nodename}/ceph/pool/${poolName}`,
			    params: {
				remove_storages: 1,
			    },
			    item: {
				type: 'CephPool',
				id: poolName,
			    },
			    taskName: 'cephdestroypool',
			    autoShow: true,
			    listeners: {
				destroy: () => rstore.load(),
			    },
			});
		    },
		},
	    ],
	    listeners: {
		activate: () => rstore.startUpdate(),
		destroy: () => rstore.stopUpdate(),
		itemdblclick: run_editor,
	    },
	});
	me.callParent();
    },
}, function() {
    Ext.define('ceph-pool-list', {
	extend: 'Ext.data.Model',
	fields: ['pool_name',
		  { name: 'pool', type: 'integer' },
		  { name: 'size', type: 'integer' },
		  { name: 'min_size', type: 'integer' },
		  { name: 'pg_num', type: 'integer' },
		  { name: 'pg_num_min', type: 'integer' },
		  { name: 'bytes_used', type: 'integer' },
		  { name: 'percent_used', type: 'number' },
		  { name: 'crush_rule', type: 'integer' },
		  { name: 'crush_rule_name', type: 'string' },
		  { name: 'pg_autoscale_mode', type: 'string' },
		  { name: 'pg_num_final', type: 'integer' },
		  { name: 'target_size_ratio', type: 'number' },
		  { name: 'target_size', type: 'integer' },
		],
	idProperty: 'pool_name',
    });
});
Ext.define('PVE.form.CephRuleSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveCephRuleSelector',
    allowBlank: false,
    valueField: 'name',
    displayField: 'name',
    editable: false,
    queryMode: 'local',
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    throw "no nodename given";
	}
	me.originalAllowBlank = me.allowBlank;
	me.allowBlank = true;
	Ext.apply(me, {
	    store: {
		fields: ['name'],
		sorters: 'name',
		proxy: {
		    type: 'proxmox',
		    url: `/api2/json/nodes/${me.nodename}/ceph/rules`,
		},
		autoLoad: {
		    callback: (records, op, success) => {
			if (me.isCreate && success && records.length > 0) {
			    me.select(records[0]);
			}
			me.allowBlank = me.originalAllowBlank;
			delete me.originalAllowBlank;
			me.validate();
		    },
		},
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.CephCreateService', {
    extend: 'Proxmox.window.Edit',
    mixins: ['Proxmox.Mixin.CBind'],
    xtype: 'pveCephCreateService',
    method: 'POST',
    isCreate: true,
    showProgress: true,
    width: 450,
    setNode: function(node) {
	let me = this;
	me.nodename = node;
	me.updateUrl();
    },
    setExtraID: function(extraID) {
	let me = this;
	me.extraID = me.type === 'mds' ? `-${extraID}` : '';
	me.updateUrl();
    },
    updateUrl: function() {
	let me = this;
	let extraID = me.extraID ?? '';
	let node = me.nodename;
	me.url = `/nodes/${node}/ceph/${me.type}/${node}${extraID}`;
    },
    defaults: {
	labelWidth: 75,
    },
    items: [
	{
	    xtype: 'pveNodeSelector',
	    fieldLabel: gettext('Host'),
	    selectCurNode: true,
	    allowBlank: false,
	    submitValue: false,
	    listeners: {
		change: function(f, value) {
		    let view = this.up('pveCephCreateService');
		    view.setNode(value);
		},
	    },
	},
	{
	    xtype: 'textfield',
	    fieldLabel: gettext('Extra ID'),
	    regex: /[a-zA-Z0-9]+/,
	    regexText: gettext('ID may only consist of alphanumeric characters'),
	    submitValue: false,
	    emptyText: Proxmox.Utils.NoneText,
	    cbind: {
		disabled: get => get('type') !== 'mds',
		hidden: get => get('type') !== 'mds',
	    },
	    listeners: {
		change: function(f, value) {
		    let view = this.up('pveCephCreateService');
		    view.setExtraID(value);
		},
	    },
	},
	{
	    xtype: 'component',
	    border: false,
	    padding: '5 2',
	    style: {
		fontSize: '12px',
	    },
	    userCls: 'pmx-hint',
	    cbind: {
		hidden: get => get('type') !== 'mds',
	    },
	    html: gettext('The Extra ID allows creating multiple MDS per node, which increases redundancy with more than one CephFS.'),
	},
    ],
    initComponent: function() {
        let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.type) {
	    throw "no type specified";
	}
	me.setNode(me.nodename);
        me.callParent();
    },
});
Ext.define('PVE.node.CephServiceController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.CephServiceList',
    render_status: (value, metadata, rec) => value,
    render_version: function(value, metadata, rec) {
	if (value === undefined) {
	    return '';
	}
	let view = this.getView();
	let host = rec.data.host, nodev = [0];
	if (view.nodeversions[host] !== undefined) {
	    nodev = view.nodeversions[host].version.parts;
	}
	let icon = '';
	if (PVE.Utils.compare_ceph_versions(view.maxversion, nodev) > 0) {
	    icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE');
	} else if (PVE.Utils.compare_ceph_versions(nodev, value) > 0) {
	    icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
	} else if (view.mixedversions) {
	    icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK');
	}
	return icon + value;
    },
    getMaxVersions: function(store, records, success) {
	if (!success || records.length < 1) {
	    return;
	}
	let me = this;
	let view = me.getView();
	view.nodeversions = records[0].data.node;
	view.maxversion = [];
	view.mixedversions = false;
	for (const [_nodename, data] of Object.entries(view.nodeversions)) {
	    let res = PVE.Utils.compare_ceph_versions(data.version.parts, view.maxversion);
	    if (res !== 0 && view.maxversion.length > 0) {
		view.mixedversions = true;
	    }
	    if (res > 0) {
		view.maxversion = data.version.parts;
	    }
	}
    },
    init: function(view) {
	if (view.pveSelNode) {
	    view.nodename = view.pveSelNode.data.node;
	}
	if (!view.nodename) {
	    throw "no node name specified";
	}
	if (!view.type) {
	    throw "no type specified";
	}
	view.versionsstore = Ext.create('Proxmox.data.UpdateStore', {
	    autoStart: true,
	    interval: 10000,
	    storeid: `ceph-versions-${view.type}-list${view.nodename}`,
	    proxy: {
		type: 'proxmox',
		url: "/api2/json/cluster/ceph/metadata?scope=versions",
	    },
	});
	view.versionsstore.on('load', this.getMaxVersions, this);
	view.on('destroy', view.versionsstore.stopUpdate);
	view.rstore = Ext.create('Proxmox.data.UpdateStore', {
	    autoStart: true,
	    interval: 3000,
	    storeid: `ceph-${view.type}-list${view.nodename}`,
	    model: 'ceph-service-list',
	    proxy: {
		type: 'proxmox',
		url: `/api2/json/nodes/${view.nodename}/ceph/${view.type}`,
	    },
	});
	view.setStore(Ext.create('Proxmox.data.DiffStore', {
	    rstore: view.rstore,
	    sorters: [{ property: 'name' }],
	}));
	if (view.storeLoadCallback) {
	    view.rstore.on('load', view.storeLoadCallback, this);
	}
	view.on('destroy', view.rstore.stopUpdate);
	if (view.showCephInstallMask) {
	    PVE.Utils.monitor_ceph_installed(view, view.rstore, view.nodename, true);
	}
    },
    service_cmd: function(rec, cmd) {
	let view = this.getView();
	if (!rec.data.host) {
	    Ext.Msg.alert(gettext('Error'), "entry has no host");
	    return;
	}
	let doRequest = function() {
	    Proxmox.Utils.API2Request({
		url: `/nodes/${rec.data.host}/ceph/${cmd}`,
		method: 'POST',
		params: { service: view.type + '.' + rec.data.name },
		success: function(response, options) {
		    Ext.create('Proxmox.window.TaskProgress', {
			autoShow: true,
			upid: response.result.data,
			taskDone: () => view.rstore.load(),
		    });
		},
		failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
	    });
	};
	if (cmd === "stop" && ['mon', 'mds'].includes(view.type)) {
	    Proxmox.Utils.API2Request({
		url: `/nodes/${rec.data.host}/ceph/cmd-safety`,
		params: {
		    service: view.type,
		    id: rec.data.name,
		    action: 'stop',
		},
		method: 'GET',
		success: function({ result: { data } }) {
		    let stopText = {
			mon: gettext('Stop MON'),
			mds: gettext('Stop MDS'),
		    };
		    if (!data.safe) {
			Ext.Msg.show({
			    title: gettext('Warning'),
			    message: data.status,
			    icon: Ext.Msg.WARNING,
			    buttons: Ext.Msg.OKCANCEL,
			    buttonText: { ok: stopText[view.type] },
			    fn: function(selection) {
				if (selection === 'ok') {
				    doRequest();
				}
			    },
			});
		    } else {
			doRequest();
		    }
		},
		failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
	    });
	} else {
	    doRequest();
	}
    },
    onChangeService: function(button) {
	let me = this;
	let record = me.getView().getSelection()[0];
	me.service_cmd(record, button.action);
    },
    showSyslog: function() {
	let view = this.getView();
	let rec = view.getSelection()[0];
	let service = `ceph-${view.type}@${rec.data.name}`;
	Ext.create('Ext.window.Window', {
	    title: `${gettext('Syslog')}: ${service}`,
	    autoShow: true,
	    modal: true,
	    width: 800,
	    height: 400,
	    layout: 'fit',
	    items: [{
		xtype: 'proxmoxLogView',
		url: `/api2/extjs/nodes/${rec.data.host}/syslog?service=${encodeURIComponent(service)}`,
		log_select_timespan: 1,
	    }],
	});
    },
    onCreate: function() {
	let view = this.getView();
	Ext.create('PVE.CephCreateService', {
	    autoShow: true,
	    nodename: view.nodename,
	    subject: view.getTitle(),
	    type: view.type,
	    taskDone: () => view.rstore.load(),
	});
    },
});
Ext.define('PVE.node.CephServiceList', {
    extend: 'Ext.grid.GridPanel',
    xtype: 'pveNodeCephServiceList',
    onlineHelp: 'chapter_pveceph',
    emptyText: gettext('No such service configured.'),
    stateful: true,
    // will be called when the store loads
    storeLoadCallback: Ext.emptyFn,
    // if set to true, does shows the ceph install mask if needed
    showCephInstallMask: false,
    controller: 'CephServiceList',
    tbar: [
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Start'),
	    iconCls: 'fa fa-play',
	    action: 'start',
	    disabled: true,
	    enableFn: rec => rec.data.state === 'stopped' || rec.data.state === 'unknown',
	    handler: 'onChangeService',
	},
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Stop'),
	    iconCls: 'fa fa-stop',
	    action: 'stop',
	    enableFn: rec => rec.data.state !== 'stopped',
	    disabled: true,
	    handler: 'onChangeService',
	},
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Restart'),
	    iconCls: 'fa fa-refresh',
	    action: 'restart',
	    disabled: true,
	    enableFn: rec => rec.data.state !== 'stopped',
	    handler: 'onChangeService',
	},
	'-',
	{
	    text: gettext('Create'),
	    reference: 'createButton',
	    handler: 'onCreate',
	},
	{
	    text: gettext('Destroy'),
	    xtype: 'proxmoxStdRemoveButton',
	    getUrl: function(rec) {
		let view = this.up('grid');
		if (!rec.data.host) {
		    Ext.Msg.alert(gettext('Error'), "entry has no host, cannot build API url");
		    return '';
		}
		return `/nodes/${rec.data.host}/ceph/${view.type}/${rec.data.name}`;
	    },
	    callback: function(options, success, response) {
		let view = this.up('grid');
		if (!success) {
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		    return;
		}
		Ext.create('Proxmox.window.TaskProgress', {
		    autoShow: true,
		    upid: response.result.data,
		    taskDone: () => view.rstore.load(),
		});
	    },
	    handler: function(btn, event, rec) {
		let me = this;
		let view = me.up('grid');
		let doRequest = function() {
		    Proxmox.button.StdRemoveButton.prototype.handler.call(me, btn, event, rec);
		};
		if (view.type === 'mon') {
		    Proxmox.Utils.API2Request({
			url: `/nodes/${rec.data.host}/ceph/cmd-safety`,
			params: {
			    service: view.type,
			    id: rec.data.name,
			    action: 'destroy',
			},
			method: 'GET',
			success: function({ result: { data } }) {
			    if (!data.safe) {
				Ext.Msg.show({
				    title: gettext('Warning'),
				    message: data.status,
				    icon: Ext.Msg.WARNING,
				    buttons: Ext.Msg.OKCANCEL,
				    buttonText: { ok: gettext('Destroy MON') },
				    fn: function(selection) {
					if (selection === 'ok') {
					    doRequest();
					}
				    },
				});
			    } else {
				doRequest();
			    }
			},
			failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
		    });
		} else {
		    doRequest();
		}
	    },
	},
	'-',
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Syslog'),
	    disabled: true,
	    handler: 'showSyslog',
	},
    ],
    columns: [
	{
	    header: gettext('Name'),
	    flex: 1,
	    sortable: true,
	    renderer: function(v) {
		return this.type + '.' + v;
	    },
	    dataIndex: 'name',
	},
	{
	    header: gettext('Host'),
	    flex: 1,
	    sortable: true,
	    renderer: function(v) {
		return v || Proxmox.Utils.unknownText;
	    },
	    dataIndex: 'host',
	},
	{
	    header: gettext('Status'),
	    flex: 1,
	    sortable: false,
	    renderer: 'render_status',
	    dataIndex: 'state',
	},
	{
	    header: gettext('Address'),
	    flex: 3,
	    sortable: true,
	    renderer: function(v) {
		return v || Proxmox.Utils.unknownText;
	    },
	    dataIndex: 'addr',
	},
	{
	    header: gettext('Version'),
	    flex: 3,
	    sortable: true,
	    dataIndex: 'version',
	    renderer: 'render_version',
	},
    ],
    initComponent: function() {
	let me = this;
	if (me.additionalColumns) {
	    me.columns = me.columns.concat(me.additionalColumns);
	}
	me.callParent();
    },
}, function() {
    Ext.define('ceph-service-list', {
	extend: 'Ext.data.Model',
	fields: [
	    'addr',
	    'name',
	    'fs_name',
	    'rank',
	    'host',
	    'quorum',
	    'state',
	    'ceph_version',
	    'ceph_version_short',
	    {
		type: 'string',
		name: 'version',
		calculate: data => PVE.Utils.parse_ceph_version(data),
	    },
	],
	idProperty: 'name',
    });
});
Ext.define('PVE.node.CephMDSServiceController', {
    extend: 'PVE.node.CephServiceController',
    alias: 'controller.CephServiceMDSList',
    render_status: (value, mD, rec) => rec.data.fs_name ? `${value} (${rec.data.fs_name})` : value,
});
Ext.define('PVE.node.CephMDSList', {
    extend: 'PVE.node.CephServiceList',
    xtype: 'pveNodeCephMDSList',
    controller: {
	type: 'CephServiceMDSList',
    },
});
Ext.define('PVE.ceph.Services', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveCephServices',
    layout: {
	type: 'hbox',
	align: 'stretch',
    },
    bodyPadding: '0 5 20',
    defaults: {
	xtype: 'box',
	style: {
	    'text-align': 'center',
	},
    },
    items: [
	{
	    flex: 1,
	    xtype: 'pveCephServiceList',
	    itemId: 'mons',
	    title: gettext('Monitors'),
	},
	{
	    flex: 1,
	    xtype: 'pveCephServiceList',
	    itemId: 'mgrs',
	    title: gettext('Managers'),
	},
	{
	    flex: 1,
	    xtype: 'pveCephServiceList',
	    itemId: 'mdss',
	    title: gettext('Meta Data Servers'),
	},
    ],
    updateAll: function(metadata, status) {
	var me = this;
	const healthstates = {
	    'HEALTH_UNKNOWN': 0,
	    'HEALTH_ERR': 1,
	    'HEALTH_WARN': 2,
	    'HEALTH_UPGRADE': 3,
	    'HEALTH_OLD': 4,
	    'HEALTH_OK': 5,
	};
	// order guarantee since es2020, but browsers did so before. Note, integers would break it.
	const healthmap = Object.keys(healthstates);
	let maxversion = "00.0.00";
	Object.values(metadata.node || {}).forEach(function(node) {
	    if (PVE.Utils.compare_ceph_versions(node?.version?.parts, maxversion) > 0) {
		maxversion = node?.version?.parts;
	    }
	});
	var quorummap = status && status.quorum_names ? status.quorum_names : [];
	let monmessages = {}, mgrmessages = {}, mdsmessages = {};
	if (status) {
	    if (status.health) {
		Ext.Object.each(status.health.checks, function(key, value, _obj) {
		    if (!Ext.String.startsWith(key, "MON_")) {
			return;
		    }
		    for (let i = 0; i < value.detail.length; i++) {
			let match = value.detail[i].message.match(/mon.([a-zA-Z0-9\-.]+)/);
			if (!match) {
			    continue;
			}
			let monid = match[1];
			if (!monmessages[monid]) {
			    monmessages[monid] = {
				worstSeverity: healthstates.HEALTH_OK,
				messages: [],
			    };
			}
			let severityIcon = PVE.Utils.get_ceph_icon_html(value.severity, true);
			let details = value.detail.reduce((acc, v) => `${acc}\n${v.message}`, '');
			monmessages[monid].messages.push(severityIcon + details);
			if (healthstates[value.severity] < monmessages[monid].worstSeverity) {
			    monmessages[monid].worstSeverity = healthstates[value.severity];
			}
		    }
		});
	    }
	    if (status.mgrmap) {
		mgrmessages[status.mgrmap.active_name] = "active";
		status.mgrmap.standbys.forEach(function(mgr) {
		    mgrmessages[mgr.name] = "standby";
		});
	    }
	    if (status.fsmap) {
		status.fsmap.by_rank.forEach(function(mds) {
		    mdsmessages[mds.name] = 'rank: ' + mds.rank + "; " + mds.status;
		});
	    }
	}
	let checks = {
	    mon: function(mon) {
		if (quorummap.indexOf(mon.name) !== -1) {
		    mon.health = healthstates.HEALTH_OK;
		} else {
		    mon.health = healthstates.HEALTH_ERR;
		}
		if (monmessages[mon.name]) {
		    if (monmessages[mon.name].worstSeverity < mon.health) {
			mon.health = monmessages[mon.name].worstSeverity;
		    }
		    Array.prototype.push.apply(mon.messages, monmessages[mon.name].messages);
		}
		return mon;
	    },
	    mgr: function(mgr) {
		if (mgrmessages[mgr.name] === 'active') {
		    mgr.title = '' + mgr.title + '';
		    mgr.statuses.push(gettext('Status') + ': active');
		} else if (mgrmessages[mgr.name] === 'standby') {
		    mgr.statuses.push(gettext('Status') + ': standby');
		} else if (mgr.health > healthstates.HEALTH_WARN) {
		    mgr.health = healthstates.HEALTH_WARN;
		}
		return mgr;
	    },
	    mds: function(mds) {
		if (mdsmessages[mds.name]) {
		    mds.title = '' + mds.title + '';
		    mds.statuses.push(gettext('Status') + ': ' + mdsmessages[mds.name]+"");
		} else if (mds.addr !== Proxmox.Utils.unknownText) {
		    mds.statuses.push(gettext('Status') + ': standby');
		}
		return mds;
	    },
	};
	for (let type of ['mon', 'mgr', 'mds']) {
	    var ids = Object.keys(metadata[type] || {});
	    me[type] = {};
	    for (let id of ids) {
		const [name, host] = id.split('@');
		let result = {
		    id: id,
		    health: healthstates.HEALTH_OK,
		    statuses: [],
		    messages: [],
		    name: name,
		    title: metadata[type][id].name || name,
		    host: host,
		    version: PVE.Utils.parse_ceph_version(metadata[type][id]),
		    service: metadata[type][id].service,
		    addr: metadata[type][id].addr || metadata[type][id].addrs || Proxmox.Utils.unknownText,
		};
		result.statuses = [
		    gettext('Host') + ": " + host,
		    gettext('Address') + ": " + result.addr,
		];
		if (checks[type]) {
		    result = checks[type](result);
		}
		if (result.service && !result.version) {
		    result.messages.push(
			PVE.Utils.get_ceph_icon_html('HEALTH_UNKNOWN', true) +
			gettext('Stopped'),
		    );
		    result.health = healthstates.HEALTH_UNKNOWN;
		}
		if (!result.version && result.addr === Proxmox.Utils.unknownText) {
		    result.health = healthstates.HEALTH_UNKNOWN;
		}
		if (result.version) {
		    result.statuses.push(gettext('Version') + ": " + result.version);
		    if (PVE.Utils.compare_ceph_versions(result.version, maxversion) !== 0) {
			let host_version = metadata.node[host]?.version?.parts || metadata.version?.[host] || "";
			if (PVE.Utils.compare_ceph_versions(host_version, maxversion) === 0) {
			    if (result.health > healthstates.HEALTH_OLD) {
				result.health = healthstates.HEALTH_OLD;
			    }
			    result.messages.push(
				PVE.Utils.get_ceph_icon_html('HEALTH_OLD', true) +
				gettext('A newer version was installed but old version still running, please restart'),
			    );
			} else {
			    if (result.health > healthstates.HEALTH_UPGRADE) {
				result.health = healthstates.HEALTH_UPGRADE;
			    }
			    result.messages.push(
				PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE', true) +
				gettext('Other cluster members use a newer version of this service, please upgrade and restart'),
			    );
			}
		    }
		}
		result.statuses.push(''); // empty line
		result.text = result.statuses.concat(result.messages).join('
');
		result.health = healthmap[result.health];
		me[type][id] = result;
	    }
	}
	me.getComponent('mons').updateAll(Object.values(me.mon));
	me.getComponent('mgrs').updateAll(Object.values(me.mgr));
	me.getComponent('mdss').updateAll(Object.values(me.mds));
    },
});
Ext.define('PVE.ceph.ServiceList', {
    extend: 'Ext.container.Container',
    xtype: 'pveCephServiceList',
    style: {
	'text-align': 'center',
    },
    defaults: {
	xtype: 'box',
	style: {
	    'text-align': 'center',
	},
    },
    items: [
	{
	    itemId: 'title',
	    data: {
		title: '',
	    },
	    tpl: '{title}
',
	},
    ],
    updateAll: function(list) {
	var me = this;
	me.suspendLayout = true;
	list.sort((a, b) => a.id > b.id ? 1 : a.id < b.id ? -1 : 0);
	if (!me.ids) {
	    me.ids = [];
	}
	let pendingRemoval = {};
	me.ids.forEach(id => { pendingRemoval[id] = true; }); // mark all as to-remove first here
	for (let i = 0; i < list.length; i++) {
	    let service = me.getComponent(list[i].id);
	    if (!service) {
		// services and list are sorted, so just insert at i + 1 (first el. is the title)
		service = me.insert(i + 1, {
		    xtype: 'pveCephServiceWidget',
		    itemId: list[i].id,
		});
		me.ids.push(list[i].id);
	    } else {
		delete pendingRemoval[list[i].id]; // drop exisiting from for-removal
	    }
	    service.updateService(list[i].title, list[i].text, list[i].health);
	}
	Object.keys(pendingRemoval).forEach(id => me.remove(id)); // GC
	me.suspendLayout = false;
	me.updateLayout();
    },
    initComponent: function() {
	var me = this;
	me.callParent();
	me.getComponent('title').update({
	    title: me.title,
	});
    },
});
Ext.define('PVE.ceph.ServiceWidget', {
    extend: 'Ext.Component',
    alias: 'widget.pveCephServiceWidget',
    userCls: 'monitor inline-block',
    data: {
	title: '0',
	health: 'HEALTH_ERR',
	text: '',
	iconCls: PVE.Utils.get_health_icon(),
    },
    tpl: [
	'{title}: ',
	'',
    ],
    updateService: function(title, text, health) {
	var me = this;
	me.update(Ext.apply(me.data, {
	    health: health,
	    text: text,
	    title: title,
	    iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[health]),
	}));
	if (me.tooltip) {
	    me.tooltip.setHtml(text);
	}
    },
    listeners: {
	destroy: function() {
	    let me = this;
	    if (me.tooltip) {
		me.tooltip.destroy();
		delete me.tooltip;
	    }
	},
	mouseenter: {
	    element: 'el',
	    fn: function(events, element) {
		let view = this.component;
		if (!view) {
		    return;
		}
		if (!view.tooltip || view.data.text !== view.tooltip.html) {
		    view.tooltip = Ext.create('Ext.tip.ToolTip', {
			target: view.el,
			trackMouse: true,
			dismissDelay: 0,
			renderTo: Ext.getBody(),
			html: view.data.text,
		    });
		}
		view.tooltip.show();
	    },
	},
	mouseleave: {
	    element: 'el',
	    fn: function(events, element) {
		let view = this.component;
		if (view.tooltip) {
		    view.tooltip.destroy();
		    delete view.tooltip;
		}
	    },
	},
    },
});
Ext.define('PVE.node.CephStatus', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveNodeCephStatus',
    onlineHelp: 'chapter_pveceph',
    scrollable: true,
    bodyPadding: 5,
    layout: {
	type: 'column',
    },
    defaults: {
	padding: 5,
    },
    items: [
	{
	    xtype: 'panel',
	    title: gettext('Health'),
	    bodyPadding: 10,
	    plugins: 'responsive',
	    responsiveConfig: {
		'width < 1600': {
		    minHeight: 230,
		    columnWidth: 1,
		},
		'width >= 1600': {
		    minHeight: 500,
		    columnWidth: 0.5,
		},
	    },
	    layout: {
		type: 'hbox',
		align: 'stretch',
	    },
	    items: [
		{
		    xtype: 'container',
		    layout: {
			type: 'vbox',
			align: 'stretch',
		    },
		    flex: 1,
		    items: [
			{
			    xtype: 'pveHealthWidget',
			    itemId: 'overallhealth',
			    flex: 1,
			    title: gettext('Status'),
			},
			{
			    xtype: 'displayfield',
			    itemId: 'versioninfo',
			    fieldLabel: gettext('Ceph Version'),
			    value: "",
			    autoEl: {
				tag: 'div',
				'data-qtip': gettext('The newest version installed in the Cluster.'),
			    },
			    padding: '10 0 0 0',
			    style: {
				'text-align': 'center',
			    },
			},
		    ],
		},
		{
		    xtype: 'grid',
		    itemId: 'warnings',
		    flex: 2,
		    stateful: true,
		    stateId: 'ceph-status-warnings',
		    // we load the store manually, to show an emptyText specify an empty intermediate store
		    store: {
			trackRemoved: false,
			data: [],
		    },
		    updateHealth: function(health) {
			let checks = health.checks || {};
			let checkRecords = Object.keys(checks).sort().map(key => {
			    let check = checks[key];
			    return {
				id: key,
				summary: check.summary.message,
				detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, ''),
				severity: check.severity,
			    };
			});
			this.getStore().loadRawData(checkRecords, false);
		    },
		    emptyText: gettext('No Warnings/Errors'),
		    columns: [
			{
			    dataIndex: 'severity',
			    header: gettext('Severity'),
			    align: 'center',
			    width: 70,
			    renderer: function(value) {
				let health = PVE.Utils.map_ceph_health[value];
				let icon = PVE.Utils.get_health_icon(health);
				return ``;
			    },
			    sorter: {
				sorterFn: function(a, b) {
				    let health = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK'];
				    return health.indexOf(b.data.severity) - health.indexOf(a.data.severity);
				},
			    },
			},
			{
			    dataIndex: 'summary',
			    header: gettext('Summary'),
			    flex: 1,
			},
			{
			    xtype: 'actioncolumn',
			    width: 40,
			    align: 'center',
			    tooltip: gettext('Detail'),
			    items: [
				{
				    iconCls: 'x-fa fa-info-circle',
				    handler: function(grid, rowindex, colindex, item, e, record) {
					var win = Ext.create('Ext.window.Window', {
					    title: gettext('Detail'),
					    resizable: true,
					    modal: true,
					    width: 650,
					    height: 400,
					    layout: {
						type: 'fit',
					    },
					    items: [{
						scrollable: true,
						padding: 10,
						xtype: 'box',
						html: [
						    '' + Ext.htmlEncode(record.data.summary) + '',
						    '' + Ext.htmlEncode(record.data.detail) + '
',
						],
					    }],
					});
					win.show();
				    },
				},
			    ],
			},
		    ],
		},
	    ],
	},
	{
	    xtype: 'pveCephStatusDetail',
	    itemId: 'statusdetail',
	    plugins: 'responsive',
	    responsiveConfig: {
		'width < 1600': {
		    columnWidth: 1,
		    minHeight: 250,
		},
		'width >= 1600': {
		    columnWidth: 0.5,
		    minHeight: 300,
		},
	    },
	    title: gettext('Status'),
	},
	{
	    xtype: 'pveCephServices',
	    title: gettext('Services'),
	    itemId: 'services',
	    plugins: 'responsive',
	    layout: {
		type: 'hbox',
		align: 'stretch',
	    },
	    responsiveConfig: {
		'width < 1600': {
		    columnWidth: 1,
		    minHeight: 200,
		},
		'width >= 1600': {
		    columnWidth: 0.5,
		    minHeight: 200,
		},
	    },
	},
	{
	    xtype: 'panel',
	    title: gettext('Performance'),
	    columnWidth: 1,
	    bodyPadding: 5,
	    layout: {
		type: 'hbox',
		align: 'center',
	    },
	    items: [
		{
		    xtype: 'container',
		    flex: 1,
		    items: [
			{
			    xtype: 'proxmoxGauge',
			    itemId: 'space',
			    title: gettext('Usage'),
			},
			{
			    flex: 1,
			    border: false,
			},
			{
			    xtype: 'container',
			    itemId: 'recovery',
			    hidden: true,
			    padding: 25,
			    items: [
				{
				    xtype: 'pveRunningChart',
				    itemId: 'recoverychart',
				    title: gettext('Recovery') +'/ '+ gettext('Rebalance'),
				    renderer: PVE.Utils.render_bandwidth,
				    height: 100,
				},
				{
				    xtype: 'progressbar',
				    itemId: 'recoveryprogress',
				},
			    ],
			},
		    ],
		},
		{
		    xtype: 'container',
		    flex: 2,
		    defaults: {
			padding: 0,
			height: 100,
		    },
		    items: [
			{
			    xtype: 'pveRunningChart',
			    itemId: 'reads',
			    title: gettext('Reads'),
			    renderer: PVE.Utils.render_bandwidth,
			},
			{
			    xtype: 'pveRunningChart',
			    itemId: 'writes',
			    title: gettext('Writes'),
			    renderer: PVE.Utils.render_bandwidth,
			},
			{
			    xtype: 'pveRunningChart',
			    itemId: 'readiops',
			    title: 'IOPS: ' + gettext('Reads'),
			    renderer: Ext.util.Format.numberRenderer('0,000'),
			},
			{
			    xtype: 'pveRunningChart',
			    itemId: 'writeiops',
			    title: 'IOPS: ' + gettext('Writes'),
			    renderer: Ext.util.Format.numberRenderer('0,000'),
			},
		    ],
		},
	    ],
	},
    ],
    updateAll: function(store, records, success) {
	if (!success || records.length === 0) {
	    return;
	}
	var me = this;
	var rec = records[0];
	me.status = rec.data;
	// add health panel
	me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
	me.down('#warnings').updateHealth(rec.data.health || {}); // add errors to gridstore
	me.getComponent('services').updateAll(me.metadata || {}, rec.data);
	me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
	// add performance data
	let pgmap = rec.data.pgmap;
	let used = pgmap.bytes_used;
	let total = pgmap.bytes_total;
	var text = Ext.String.format(gettext('{0} of {1}'),
	    Proxmox.Utils.render_size(used),
	    Proxmox.Utils.render_size(total),
	);
	// update the usage widget
	me.down('#space').updateValue(used/total, text);
	let readiops = pgmap.read_op_per_sec;
	let writeiops = pgmap.write_op_per_sec;
	let reads = pgmap.read_bytes_sec || 0;
	let writes = pgmap.write_bytes_sec || 0;
	// update the graphs
	me.reads.addDataPoint(reads);
	me.writes.addDataPoint(writes);
	me.readiops.addDataPoint(readiops);
	me.writeiops.addDataPoint(writeiops);
	let degraded = pgmap.degraded_objects || 0;
	let misplaced = pgmap.misplaced_objects || 0;
	let unfound = pgmap.unfound_objects || 0;
	let unhealthy = degraded + unfound + misplaced;
	// update recovery
	if (pgmap.recovering_objects_per_sec !== undefined || unhealthy > 0) {
	    let toRecoverObjects = pgmap.misplaced_total || pgmap.unfound_total || pgmap.degraded_total || 0;
	    if (toRecoverObjects === 0) {
		return; // FIXME: unexpected return and leaves things possible visible when it shouldn't?
	    }
	    let recovered = toRecoverObjects - unhealthy || 0;
	    let speed = pgmap.recovering_bytes_per_sec || 0;
	    let recoveryRatio = recovered / toRecoverObjects;
	    let txt = `${(recoveryRatio * 100).toFixed(2)}%`;
	    if (speed > 0) {
		let obj_per_sec = speed / (4 * 1024 * 1024); // 4 MiB per Object
		let duration = Proxmox.Utils.format_duration_human(unhealthy/obj_per_sec);
		let speedTxt = PVE.Utils.render_bandwidth(speed);
		txt += ` (${speedTxt} - ${duration} left)`;
	    }
	    me.down('#recovery').setVisible(true);
	    me.down('#recoveryprogress').updateValue(recoveryRatio);
	    me.down('#recoveryprogress').updateText(txt);
	    me.down('#recoverychart').addDataPoint(speed);
	} else {
	    me.down('#recovery').setVisible(false);
	    me.down('#recoverychart').addDataPoint(0);
	}
    },
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	me.callParent();
	var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph';
	me.store = Ext.create('Proxmox.data.UpdateStore', {
	    storeid: 'ceph-status-' + (nodename || 'cluster'),
	    interval: 5000,
	    proxy: {
		type: 'proxmox',
		url: baseurl + '/status',
	    },
	});
	me.metadatastore = Ext.create('Proxmox.data.UpdateStore', {
	    storeid: 'ceph-metadata-' + (nodename || 'cluster'),
	    interval: 15*1000,
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/cluster/ceph/metadata',
	    },
	});
	// save references for the updatefunction
	me.iops = me.down('#iops');
	me.readiops = me.down('#readiops');
	me.writeiops = me.down('#writeiops');
	me.reads = me.down('#reads');
	me.writes = me.down('#writes');
	// manages the "install ceph?" overlay
	PVE.Utils.monitor_ceph_installed(me, me.store, nodename);
	me.mon(me.store, 'load', me.updateAll, me);
	me.mon(me.metadatastore, 'load', function(store, records, success) {
	    if (!success || records.length < 1) {
		return;
	    }
	    me.metadata = records[0].data;
	    // update services
	    me.getComponent('services').updateAll(me.metadata, me.status || {});
	    // update detailstatus panel
	    me.getComponent('statusdetail').updateAll(me.metadata, me.status || {});
	    let maxversion = [];
	    let maxversiontext = "";
	    for (const [_nodename, data] of Object.entries(me.metadata.node)) {
		let version = data.version.parts;
		if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
		    maxversion = version;
		    maxversiontext = data.version.str;
		}
	    }
	    me.down('#versioninfo').setValue(maxversiontext);
	}, me);
	me.on('destroy', me.store.stopUpdate);
	me.on('destroy', me.metadatastore.stopUpdate);
	me.store.startUpdate();
	me.metadatastore.startUpdate();
    },
});
Ext.define('PVE.ceph.StatusDetail', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveCephStatusDetail',
    layout: {
	type: 'hbox',
	align: 'stretch',
    },
    bodyPadding: '0 5',
    defaults: {
	xtype: 'box',
	style: {
	    'text-align': 'center',
	},
    },
    items: [{
	flex: 1,
	itemId: 'osds',
	maxHeight: 250,
	scrollable: true,
	padding: '0 10 5 10',
	data: {
	    total: 0,
	    upin: 0,
	    upout: 0,
	    downin: 0,
	    downout: 0,
	    oldOSD: [],
	    ghostOSD: [],
	},
	tpl: [
	    'OSDs
',
	    '',
	    '| ',
	    ' | ',
	    gettext('In'),
	    '',
	    ' | ',
	    gettext('Out'),
	    '',
	    ' | 
',
	    '',
	    '| ',
	    gettext('Up'),
	    '',
	    ' | {upin}',
	    ' | {upout}',
	    ' | 
',
	    '',
	    '| ',
	    gettext('Down'),
	    '',
	    ' | {downin}',
	    ' | {downout}',
	    ' | 
',
	    '
',
	    '
',
	    gettext('Total'),
	    ': {total}',
	    '
',
	    '',
	    ' ' + gettext('Outdated OSDs') + "
",
	    '',
	    '
',
	    'osd.{id}:
',
	    '{version}
',
	    '',
	    '',
	    '
',
	    ` ${gettext('Ghost OSDs')}
`,
	    ``,
	    '
',
	    'osd.{id}
',
	    '',
	    '',
	    '
';
			record.get('states').forEach(function(state) {
			    html += '
' +
				state.state_name + ': ' + state.count.toString();
			});
			tooltip.setHtml(html);
		    },
		},
		subStyle: {
		    strokeStyle: false,
		},
	    },
	],
    },
    {
	flex: 1.6,
	itemId: 'pgs',
	padding: '0 10',
	maxHeight: 250,
	scrollable: true,
	data: {
	    states: [],
	},
	tpl: [
	    'PGs
',
	    '',
	    ' {state_name}:
',
	    '{count}
',
	    '',
	    '',
	],
    }],
    // similar to mgr dashboard
    pgstates: {
	// clean
	clean: 1,
	active: 1,
	// working
	activating: 2,
	backfill_wait: 2,
	backfilling: 2,
	creating: 2,
	deep: 2,
	degraded: 2,
	forced_backfill: 2,
	forced_recovery: 2,
	peered: 2,
	peering: 2,
	recovering: 2,
	recovery_wait: 2,
	remapped: 2,
	repair: 2,
	scrubbing: 2,
	snaptrim: 2,
	snaptrim_wait: 2,
	// error
	backfill_toofull: 3,
	backfill_unfound: 3,
	down: 3,
	incomplete: 3,
	inconsistent: 3,
	recovery_toofull: 3,
	recovery_unfound: 3,
	snaptrim_error: 3,
	stale: 3,
	undersized: 3,
    },
    statecategories: [
	{
	    text: gettext('Unknown'),
	    count: 0,
	    states: [],
	    cls: 'faded',
	},
	{
	    text: gettext('Clean'),
	    cls: 'good',
	},
	{
	    text: gettext('Working'),
	    cls: 'warning',
	},
	{
	    text: gettext('Error'),
	    cls: 'critical',
	},
    ],
    checkThemeColors: function() {
	let me = this;
	let rootStyle = getComputedStyle(document.documentElement);
	// get color
	let background = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff";
	// set the colors
	me.chart.setBackground(background);
	me.chart.redraw();
    },
    updateAll: function(metadata, status) {
	let me = this;
	me.suspendLayout = true;
	let maxversion = "0";
	Object.values(metadata.node || {}).forEach(function(node) {
	    if (PVE.Utils.compare_ceph_versions(node?.version?.parts, maxversion) > 0) {
		maxversion = node.version.parts;
	    }
	});
	let oldOSD = [], ghostOSD = [];
	metadata.osd?.forEach(osd => {
	    let version = PVE.Utils.parse_ceph_version(osd);
	    if (version !== undefined) {
		if (PVE.Utils.compare_ceph_versions(version, maxversion) !== 0) {
		    oldOSD.push({
			id: osd.id,
			version: version,
		    });
		}
	    } else {
		if (Object.keys(osd).length > 1) {
		    console.warn('got OSD entry with no valid version but other keys', osd);
		}
		ghostOSD.push({
		    id: osd.id,
		});
	    }
	});
	// update PGs sorted
	let pgmap = status.pgmap || {};
	let pgs_by_state = pgmap.pgs_by_state || [];
	pgs_by_state.sort(function(a, b) {
	    return a.state_name < b.state_name?-1:a.state_name === b.state_name?0:1;
	});
	me.statecategories.forEach(function(cat) {
	    cat.count = 0;
	    cat.states = [];
	});
	pgs_by_state.forEach(function(state) {
	    let states = state.state_name.split(/[^a-z]+/);
	    let result = 0;
	    for (let i = 0; i < states.length; i++) {
		if (me.pgstates[states[i]] > result) {
		    result = me.pgstates[states[i]];
		}
	    }
	    // for the list
	    state.cls = me.statecategories[result].cls;
	    me.statecategories[result].count += state.count;
	    me.statecategories[result].states.push(state);
	});
	me.chart.getStore().setData(me.statecategories);
	me.getComponent('pgs').update({ states: pgs_by_state });
	let health = status.health || {};
	// we collect monitor/osd information from the checks
	const downinregex = /(\d+) osds down/;
	let downin_osds = 0;
	Ext.Object.each(health.checks, function(key, value, obj) {
	    var found = null;
	    if (key === 'OSD_DOWN') {
		found = value.summary.message.match(downinregex);
		if (found !== null) {
		    downin_osds = parseInt(found[1], 10);
		}
	    }
	});
	let osdmap = status.osdmap || {};
	if (typeof osdmap.osdmap !== "undefined") {
	    osdmap = osdmap.osdmap;
	}
	// update OSDs counts
	let total_osds = osdmap.num_osds || 0;
	let in_osds = osdmap.num_in_osds || 0;
	let up_osds = osdmap.num_up_osds || 0;
	let down_osds = total_osds - up_osds;
	let downout_osds = down_osds - downin_osds;
	let upin_osds = in_osds - downin_osds;
	let upout_osds = up_osds - upin_osds;
	let osds = {
	    total: total_osds,
	    upin: upin_osds,
	    upout: upout_osds,
	    downin: downin_osds,
	    downout: downout_osds,
	    oldOSD: oldOSD,
	    ghostOSD,
	};
	let osdcomponent = me.getComponent('osds');
	osdcomponent.update(Ext.apply(osdcomponent.data, osds));
	me.suspendLayout = false;
	me.updateLayout();
    },
     initComponent: function() {
	var me = this;
	me.callParent();
	me.chart = me.getComponent('pgchart');
	me.checkThemeColors();
	// switch colors on media query changes
	me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
	me.themeListener = (e) => { me.checkThemeColors(); };
	me.mediaQueryList.addEventListener("change", me.themeListener);
    },
    doDestroy: function() {
	let me = this;
	me.mediaQueryList.removeEventListener("change", me.themeListener);
	me.callParent();
    },
});
Ext.define('PVE.node.ACMEAccountCreate', {
    extend: 'Proxmox.window.Edit',
    mixins: ['Proxmox.Mixin.CBind'],
    width: 450,
    title: gettext('Register Account'),
    isCreate: true,
    method: 'POST',
    submitText: gettext('Register'),
    url: '/cluster/acme/account',
    showTaskViewer: true,
    defaultExists: false,
    items: [
	{
	    xtype: 'proxmoxtextfield',
	    fieldLabel: gettext('Account Name'),
	    name: 'name',
	    cbind: {
		emptyText: (get) => get('defaultExists') ? '' : 'default',
		allowBlank: (get) => !get('defaultExists'),
	    },
	},
	{
	    xtype: 'textfield',
	    name: 'contact',
	    vtype: 'email',
	    allowBlank: false,
	    fieldLabel: gettext('E-Mail'),
	},
	{
	    xtype: 'proxmoxComboGrid',
	    name: 'directory',
	    allowBlank: false,
	    valueField: 'url',
	    displayField: 'name',
	    fieldLabel: gettext('ACME Directory'),
	    store: {
		autoLoad: true,
		fields: ['name', 'url'],
		idProperty: ['name'],
		proxy: {
		    type: 'proxmox',
		    url: '/api2/json/cluster/acme/directories',
		},
		sorters: {
		    property: 'name',
		    direction: 'ASC',
		},
	    },
	    listConfig: {
		columns: [
		    {
			header: gettext('Name'),
			dataIndex: 'name',
			flex: 1,
		    },
		    {
			header: gettext('URL'),
			dataIndex: 'url',
			flex: 1,
		    },
		],
	    },
	    listeners: {
		change: function(combogrid, value) {
		    var me = this;
		    if (!value) {
			return;
		    }
		    var disp = me.up('window').down('#tos_url_display');
		    var field = me.up('window').down('#tos_url');
		    var checkbox = me.up('window').down('#tos_checkbox');
		    disp.setValue(gettext('Loading'));
		    field.setValue(undefined);
		    checkbox.setValue(undefined);
		    checkbox.setHidden(true);
		    Proxmox.Utils.API2Request({
			url: '/cluster/acme/tos',
			method: 'GET',
			params: {
			    directory: value,
			},
			success: function(response, opt) {
			    field.setValue(response.result.data);
			    disp.setValue(response.result.data);
			    checkbox.setHidden(false);
			},
			failure: function(response, opt) {
			    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
			},
		    });
		},
	    },
	},
	{
	    xtype: 'displayfield',
	    itemId: 'tos_url_display',
	    renderer: PVE.Utils.render_optional_url,
	    name: 'tos_url_display',
	},
	{
	    xtype: 'hidden',
	    itemId: 'tos_url',
	    name: 'tos_url',
	},
	{
	    xtype: 'proxmoxcheckbox',
	    itemId: 'tos_checkbox',
	    boxLabel: gettext('Accept TOS'),
	    submitValue: false,
	    validateValue: function(value) {
		if (value && this.checked) {
		    return true;
		}
		return false;
	    },
	},
    ],
});
Ext.define('PVE.node.ACMEAccountView', {
    extend: 'Proxmox.window.Edit',
    width: 600,
    fieldDefaults: {
	labelWidth: 140,
    },
    title: gettext('Account'),
    items: [
	{
	    xtype: 'displayfield',
	    fieldLabel: gettext('E-Mail'),
	    name: 'email',
	},
	{
	    xtype: 'displayfield',
	    fieldLabel: gettext('Created'),
	    name: 'createdAt',
	},
	{
	    xtype: 'displayfield',
	    fieldLabel: gettext('Status'),
	    name: 'status',
	},
	{
	    xtype: 'displayfield',
	    fieldLabel: gettext('Directory'),
	    renderer: PVE.Utils.render_optional_url,
	    name: 'directory',
	},
	{
	    xtype: 'displayfield',
	    fieldLabel: gettext('Terms of Services'),
	    renderer: PVE.Utils.render_optional_url,
	    name: 'tos',
	},
    ],
    initComponent: function() {
	var me = this;
	if (!me.accountname) {
	    throw "no account name defined";
	}
	me.url = '/cluster/acme/account/' + me.accountname;
	me.callParent();
	// hide OK/Reset button, because we just want to show data
	me.down('toolbar[dock=bottom]').setVisible(false);
	me.load({
	    success: function(response) {
		var data = response.result.data;
		data.email = data.account.contact[0];
		data.createdAt = data.account.createdAt;
		data.status = data.account.status;
		me.setValues(data);
	    },
	});
    },
});
Ext.define('PVE.node.ACMEDomainEdit', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveACMEDomainEdit',
    subject: gettext('Domain'),
    isCreate: false,
    width: 450,
    onlineHelp: 'sysadmin_certificate_management',
    items: [
	{
	    xtype: 'inputpanel',
	    onGetValues: function(values) {
		let me = this;
		let win = me.up('pveACMEDomainEdit');
		let nodeconfig = win.nodeconfig;
		let olddomain = win.domain || {};
		let params = {
		    digest: nodeconfig.digest,
		};
		let configkey = olddomain.configkey;
		let acmeObj = PVE.Parser.parseACME(nodeconfig.acme);
		if (values.type === 'dns') {
		    if (!olddomain.configkey || olddomain.configkey === 'acme') {
			// look for first free slot
			for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
			    if (nodeconfig[`acmedomain${i}`] === undefined) {
				configkey = `acmedomain${i}`;
				break;
			    }
			}
			if (olddomain.domain) {
			    // we have to remove the domain from the acme domainlist
			    PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain);
			    params.acme = PVE.Parser.printACME(acmeObj);
			}
		    }
		    delete values.type;
		    params[configkey] = PVE.Parser.printPropertyString(values, 'domain');
		} else {
		    if (olddomain.configkey && olddomain.configkey !== 'acme') {
			// delete the old dns entry
			params.delete = [olddomain.configkey];
		    }
		    // add new, remove old and make entries unique
		    PVE.Utils.add_domain_to_acme(acmeObj, values.domain);
		    PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain);
		    params.acme = PVE.Parser.printACME(acmeObj);
		}
		return params;
	    },
	    items: [
		{
		    xtype: 'proxmoxKVComboBox',
		    name: 'type',
		    fieldLabel: gettext('Challenge Type'),
		    allowBlank: false,
		    value: 'standalone',
		    comboItems: [
			['standalone', 'HTTP'],
			['dns', 'DNS'],
		    ],
		    validator: function(value) {
			let me = this;
			let win = me.up('pveACMEDomainEdit');
			let oldconfigkey = win.domain ? win.domain.configkey : undefined;
			let val = me.getValue();
			if (val === 'dns' && (!oldconfigkey || oldconfigkey === 'acme')) {
			    // we have to check if there is a 'acmedomain' slot left
			    let found = false;
			    for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
				if (!win.nodeconfig[`acmedomain${i}`]) {
				    found = true;
				}
			    }
			    if (!found) {
				return gettext('Only 5 Domains with type DNS can be configured');
			    }
			}
			return true;
		    },
		    listeners: {
			change: function(cb, value) {
			    let me = this;
			    let view = me.up('pveACMEDomainEdit');
			    let pluginField = view.down('field[name=plugin]');
			    pluginField.setDisabled(value !== 'dns');
			    pluginField.setHidden(value !== 'dns');
			},
		    },
		},
		{
		    xtype: 'hidden',
		    name: 'alias',
		},
		{
		    xtype: 'pveACMEPluginSelector',
		    name: 'plugin',
		    disabled: true,
		    hidden: true,
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'domain',
		    allowBlank: false,
		    vtype: 'DnsName',
		    value: '',
		    fieldLabel: gettext('Domain'),
		},
	    ],
	},
    ],
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    throw 'no nodename given';
	}
	if (!me.nodeconfig) {
	    throw 'no nodeconfig given';
	}
	me.isCreate = !me.domain;
	if (me.isCreate) {
	    me.domain = `${me.nodename}.`; // TODO: FQDN of node
	}
	me.url = `/api2/extjs/nodes/${me.nodename}/config`;
	me.callParent();
	if (!me.isCreate) {
	    me.setValues(me.domain);
	} else {
	    me.setValues({ domain: me.domain });
	}
    },
});
Ext.define('pve-acme-domains', {
    extend: 'Ext.data.Model',
    fields: ['domain', 'type', 'alias', 'plugin', 'configkey'],
    idProperty: 'domain',
});
Ext.define('PVE.node.ACME', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.pveACMEView',
    margin: '10 0 0 0',
    title: 'ACME',
    emptyText: gettext('No Domains configured'),
    viewModel: {
	data: {
	    domaincount: 0,
	    account: undefined, // the account we display
	    configaccount: undefined, // the account set in the config
	    accountEditable: false,
	    accountsAvailable: false,
	},
	formulas: {
	    canOrder: (get) => !!get('account') && get('domaincount') > 0,
	    editBtnIcon: (get) => 'fa black fa-' + (get('accountEditable') ? 'check' : 'pencil'),
	    editBtnText: (get) => get('accountEditable') ? gettext('Apply') : gettext('Edit'),
	    accountTextHidden: (get) => get('accountEditable') || !get('accountsAvailable'),
	    accountValueHidden: (get) => !get('accountEditable') || !get('accountsAvailable'),
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	init: function(view) {
	    let accountSelector = this.lookup('accountselector');
	    accountSelector.store.on('load', this.onAccountsLoad, this);
	},
	onAccountsLoad: function(store, records, success) {
	    let me = this;
	    let vm = me.getViewModel();
	    let configaccount = vm.get('configaccount');
	    vm.set('accountsAvailable', records.length > 0);
	    if (me.autoChangeAccount && records.length > 0) {
		me.changeAccount(records[0].data.name, () => {
		    vm.set('accountEditable', false);
		    me.reload();
		});
		me.autoChangeAccount = false;
	    } else if (configaccount) {
		if (store.findExact('name', configaccount) !== -1) {
		    vm.set('account', configaccount);
		} else {
		    vm.set('account', null);
		}
	    }
	},
	addDomain: function() {
	    let me = this;
	    let view = me.getView();
	    Ext.create('PVE.node.ACMEDomainEdit', {
		nodename: view.nodename,
		nodeconfig: view.nodeconfig,
		apiCallDone: function() {
		    me.reload();
		},
	    }).show();
	},
	editDomain: function() {
	    let me = this;
	    let view = me.getView();
	    let selection = view.getSelection();
	    if (selection.length < 1) return;
	    Ext.create('PVE.node.ACMEDomainEdit', {
		nodename: view.nodename,
		nodeconfig: view.nodeconfig,
		domain: selection[0].data,
		apiCallDone: function() {
		    me.reload();
		},
	    }).show();
	},
	removeDomain: function() {
	    let me = this;
	    let view = me.getView();
	    let selection = view.getSelection();
	    if (selection.length < 1) return;
	    let rec = selection[0].data;
	    let params = {};
	    if (rec.configkey !== 'acme') {
		params.delete = rec.configkey;
	    } else {
		let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
		PVE.Utils.remove_domain_from_acme(acme, rec.domain);
		params.acme = PVE.Parser.printACME(acme);
	    }
	    Proxmox.Utils.API2Request({
		method: 'PUT',
		url: `/nodes/${view.nodename}/config`,
		params,
		success: function(response, opt) {
		    me.reload();
		},
		failure: function(response, opt) {
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		},
	    });
	},
	toggleEditAccount: function() {
	    let me = this;
	    let vm = me.getViewModel();
	    let editable = vm.get('accountEditable');
	    if (editable) {
		me.changeAccount(vm.get('account'), function() {
		    vm.set('accountEditable', false);
		    me.reload();
		});
	    } else {
		vm.set('accountEditable', true);
	    }
	},
	changeAccount: function(account, callback) {
	    let me = this;
	    let view = me.getView();
	    let params = {};
	    let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
	    acme.account = account;
	    params.acme = PVE.Parser.printACME(acme);
	    Proxmox.Utils.API2Request({
		method: 'PUT',
		waitMsgTarget: view,
		url: `/nodes/${view.nodename}/config`,
		params,
		success: function(response, opt) {
		    if (Ext.isFunction(callback)) {
			callback();
		    }
		},
		failure: function(response, opt) {
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		},
	    });
	},
	order: function() {
	    let me = this;
	    let view = me.getView();
	    Proxmox.Utils.API2Request({
		method: 'POST',
		params: {
		    force: 1,
		},
		url: `/nodes/${view.nodename}/certificates/acme/certificate`,
		success: function(response, opt) {
		    Ext.create('Proxmox.window.TaskViewer', {
		        upid: response.result.data,
		        taskDone: function(success) {
			    me.orderFinished(success);
		        },
		    }).show();
		},
		failure: function(response, opt) {
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		},
	    });
	},
	orderFinished: function(success) {
	    if (!success) return;
	    var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!');
	    Ext.getBody().mask(txt, ['pve-static-mask']);
	    // reload after 10 seconds automatically
	    Ext.defer(function() {
		window.location.reload(true);
	    }, 10000);
	},
	reload: function() {
	    let me = this;
	    let view = me.getView();
	    view.rstore.load();
	},
	addAccount: function() {
	    let me = this;
	    Ext.create('PVE.node.ACMEAccountCreate', {
		autoShow: true,
		taskDone: function() {
		    me.reload();
		    let accountSelector = me.lookup('accountselector');
		    me.autoChangeAccount = true;
		    accountSelector.store.load();
		},
	    });
	},
    },
    tbar: [
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Add'),
	    handler: 'addDomain',
	    selModel: false,
	},
	{
	    xtype: 'proxmoxButton',
	    text: gettext('Edit'),
	    disabled: true,
	    handler: 'editDomain',
	},
	{
	    xtype: 'proxmoxStdRemoveButton',
	    handler: 'removeDomain',
	},
	'-',
	{
	    xtype: 'button',
	    reference: 'order',
	    text: gettext('Order Certificates Now'),
	    bind: {
		disabled: '{!canOrder}',
	    },
	    handler: 'order',
	},
	'-',
	{
	    xtype: 'displayfield',
	    value: gettext('Using Account') + ':',
	    bind: {
		hidden: '{!accountsAvailable}',
	    },
	},
	{
	    xtype: 'displayfield',
	    reference: 'accounttext',
	    renderer: (val) => val || Proxmox.Utils.NoneText,
	    bind: {
		value: '{account}',
		hidden: '{accountTextHidden}',
	    },
	},
	{
	    xtype: 'pveACMEAccountSelector',
	    hidden: true,
	    reference: 'accountselector',
	    bind: {
		value: '{account}',
		hidden: '{accountValueHidden}',
	    },
	},
	{
	    xtype: 'button',
	    iconCls: 'fa black fa-pencil',
	    bind: {
		iconCls: '{editBtnIcon}',
		text: '{editBtnText}',
		hidden: '{!accountsAvailable}',
	    },
	    handler: 'toggleEditAccount',
	},
	{
	    xtype: 'displayfield',
	    value: gettext('No Account available.'),
	    bind: {
		hidden: '{accountsAvailable}',
	    },
	},
	{
	    xtype: 'button',
	    hidden: true,
	    reference: 'accountlink',
	    text: gettext('Add ACME Account'),
	    bind: {
		hidden: '{accountsAvailable}',
	    },
	    handler: 'addAccount',
	},
    ],
    updateStore: function(store, records, success) {
	let me = this;
	let data = [];
	let rec;
	if (success && records.length > 0) {
	    rec = records[0];
	} else {
	    rec = {
		data: {},
	    };
	}
	me.nodeconfig = rec.data; // save nodeconfig for updates
	let account = 'default';
	if (rec.data.acme) {
	    let obj = PVE.Parser.parseACME(rec.data.acme);
	    (obj.domains || []).forEach(domain => {
		if (domain === '') return;
		let record = {
		    domain,
		    type: 'standalone',
		    configkey: 'acme',
		};
		data.push(record);
	    });
	    if (obj.account) {
		account = obj.account;
	    }
	}
	let vm = me.getViewModel();
	let oldaccount = vm.get('account');
	// account changed, and we do not edit currently, load again to verify
	if (oldaccount !== account && !vm.get('accountEditable')) {
	    vm.set('configaccount', account);
	    me.lookup('accountselector').store.load();
	}
	for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
	    let acmedomain = rec.data[`acmedomain${i}`];
	    if (!acmedomain) continue;
	    let record = PVE.Parser.parsePropertyString(acmedomain, 'domain');
	    record.type = 'dns';
	    record.configkey = `acmedomain${i}`;
	    data.push(record);
	}
	vm.set('domaincount', data.length);
	me.store.loadData(data, false);
    },
    listeners: {
	itemdblclick: 'editDomain',
    },
    columns: [
	{
	    dataIndex: 'domain',
	    flex: 5,
	    text: gettext('Domain'),
	},
	{
	    dataIndex: 'type',
	    flex: 1,
	    text: gettext('Type'),
	},
	{
	    dataIndex: 'plugin',
	    flex: 1,
	    text: gettext('Plugin'),
	},
    ],
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no nodename given";
	}
	me.rstore = Ext.create('Proxmox.data.UpdateStore', {
	    interval: 10 * 1000,
	    autoStart: true,
	    storeid: `pve-node-domains-${me.nodename}`,
	    proxy: {
		type: 'proxmox',
		url: `/api2/json/nodes/${me.nodename}/config`,
	    },
	});
	me.store = Ext.create('Ext.data.Store', {
	    model: 'pve-acme-domains',
	    sorters: 'domain',
	});
	me.callParent();
	me.mon(me.rstore, 'load', 'updateStore', me);
	Proxmox.Utils.monStoreErrors(me, me.rstore);
	me.on('destroy', me.rstore.stopUpdate, me.rstore);
    },
});
Ext.define('PVE.node.CertificateView', {
    extend: 'Ext.container.Container',
    xtype: 'pveCertificatesView',
    onlineHelp: 'sysadmin_certificate_management',
    mixins: ['Proxmox.Mixin.CBind'],
    scrollable: 'y',
    items: [
	{
	    xtype: 'pveCertView',
	    border: 0,
	    cbind: {
		nodename: '{nodename}',
	    },
	},
	{
	    xtype: 'pveACMEView',
	    border: 0,
	    cbind: {
		nodename: '{nodename}',
	    },
	},
    ],
});
Ext.define('PVE.node.CertificateViewer', {
    extend: 'Proxmox.window.Edit',
    title: gettext('Certificate'),
    fieldDefaults: {
	labelWidth: 120,
    },
    width: 800,
    items: {
	xtype: 'inputpanel',
	maxHeight: 900,
	scrollable: 'y',
	columnT: [
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Name'),
		name: 'filename',
	    },
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Fingerprint'),
		name: 'fingerprint',
	    },
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Issuer'),
		name: 'issuer',
	    },
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Subject'),
		name: 'subject',
	    },
	],
	column1: [
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Public Key Type'),
		name: 'public-key-type',
	    },
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Public Key Size'),
		name: 'public-key-bits',
	    },
	],
	column2: [
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Valid Since'),
		renderer: Proxmox.Utils.render_timestamp,
		name: 'notbefore',
	    },
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Expires'),
		renderer: Proxmox.Utils.render_timestamp,
		name: 'notafter',
	    },
	],
	columnB: [
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Subject Alternative Names'),
		name: 'san',
		renderer: PVE.Utils.render_san,
	    },
	    {
		xtype: 'fieldset',
		title: gettext('Raw Certificate'),
		collapsible: true,
		collapsed: true,
		items: [{
		    xtype: 'textarea',
		    name: 'pem',
		    editable: false,
		    grow: true,
		    growMax: 350,
		    fieldStyle: {
			'white-space': 'pre-wrap',
			'font-family': 'monospace',
		    },
		}],
	    },
	],
    },
    initComponent: function() {
	let me = this;
	if (!me.cert) {
	    throw "no cert given";
	}
	if (!me.nodename) {
	    throw "no nodename given";
	}
	me.url = `/nodes/${me.nodename}/certificates/info`;
	me.callParent();
	// hide OK/Reset button, because we just want to show data
	me.down('toolbar[dock=bottom]').setVisible(false);
	me.load({
	    success: function(response) {
		if (Ext.isArray(response.result.data)) {
		    for (const item of response.result.data) {
			if (item.filename === me.cert) {
			    me.setValues(item);
			    return;
			}
		    }
		}
	    },
	});
    },
});
Ext.define('PVE.node.CertUpload', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveCertUpload',
    title: gettext('Upload Custom Certificate'),
    resizable: false,
    isCreate: true,
    submitText: gettext('Upload'),
    method: 'POST',
    width: 600,
    apiCallDone: function(success, response, options) {
	if (!success) {
	    return;
	}
	let txt = gettext('API server will be restarted to use new certificates, please reload web-interface!');
	Ext.getBody().mask(txt, ['pve-static-mask']);
	Ext.defer(() => window.location.reload(true), 10000); // reload after 10 seconds automatically
    },
    items: {
	xtype: 'inputpanel',
	onGetValues: function(values) {
	    values.restart = 1;
	    values.force = 1;
	    if (!values.key) {
		delete values.key;
	    }
	    return values;
	},
	items: [
	    {
		fieldLabel: gettext('Private Key (Optional)'),
		labelAlign: 'top',
		emptyText: gettext('No change'),
		name: 'key',
		xtype: 'textarea',
	    },
	    {
		xtype: 'filebutton',
		text: gettext('From File'),
		listeners: {
		    change: function(btn, e, value) {
			let form = this.up('form');
			for (const file of e.event.target.files) {
			    PVE.Utils.loadFile(file, res => form.down('field[name=key]').setValue(res));
			}
			btn.reset();
		    },
		},
	    },
	    {
		fieldLabel: gettext('Certificate Chain'),
		labelAlign: 'top',
		allowBlank: false,
		name: 'certificates',
		xtype: 'textarea',
	    },
	    {
		xtype: 'filebutton',
		text: gettext('From File'),
		listeners: {
		    change: function(btn, e, value) {
			let form = this.up('form');
			for (const file of e.event.target.files) {
			    PVE.Utils.loadFile(file, res => form.down('field[name=certificates]').setValue(res));
			}
			btn.reset();
		    },
		},
	    },
	],
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    throw "no nodename given";
	}
	me.url = `/nodes/${me.nodename}/certificates/custom`;
	me.callParent();
    },
});
Ext.define('pve-certificate', {
    extend: 'Ext.data.Model',
    fields: ['filename', 'fingerprint', 'issuer', 'notafter', 'notbefore', 'subject', 'san', 'public-key-bits', 'public-key-type'],
    idProperty: 'filename',
});
Ext.define('PVE.node.Certificates', {
    extend: 'Ext.grid.Panel',
    xtype: 'pveCertView',
    tbar: [
	{
	    xtype: 'button',
	    text: gettext('Upload Custom Certificate'),
	    handler: function() {
		let view = this.up('grid');
		Ext.create('PVE.node.CertUpload', {
		    nodename: view.nodename,
		    listeners: {
			destroy: () => view.reload(),
		    },
		    autoShow: true,
		});
	    },
	},
	{
	    xtype: 'proxmoxStdRemoveButton',
	    itemId: 'deletebtn',
	    text: gettext('Delete Custom Certificate'),
	    dangerous: true,
	    selModel: false,
	    getUrl: function(rec) {
		let view = this.up('grid');
		return `/nodes/${view.nodename}/certificates/custom?restart=1`;
	    },
	    confirmMsg: gettext('Delete custom certificate and switch to generated one?'),
	    callback: function(options, success, response) {
		if (success) {
		    let txt = gettext('API server will be restarted to use new certificates, please reload web-interface!');
		    Ext.getBody().mask(txt, ['pve-static-mask']);
		    // reload after 10 seconds automatically
		    Ext.defer(() => window.location.reload(true), 10000);
		}
	    },
	},
	'-',
	{
	    xtype: 'proxmoxButton',
	    itemId: 'viewbtn',
	    disabled: true,
	    text: gettext('View Certificate'),
	    handler: function() {
		this.up('grid').viewCertificate();
	    },
	},
    ],
    columns: [
	{
	    header: gettext('File'),
	    width: 150,
	    dataIndex: 'filename',
	},
	{
	    header: gettext('Issuer'),
	    flex: 1,
	    dataIndex: 'issuer',
	},
	{
	    header: gettext('Subject'),
	    flex: 1,
	    dataIndex: 'subject',
	},
	{
	    header: gettext('Public Key Alogrithm'),
	    flex: 1,
	    dataIndex: 'public-key-type',
	    hidden: true,
	},
	{
	    header: gettext('Public Key Size'),
	    flex: 1,
	    dataIndex: 'public-key-bits',
	    hidden: true,
	},
	{
	    header: gettext('Valid Since'),
	    width: 150,
	    dataIndex: 'notbefore',
	    renderer: Proxmox.Utils.render_timestamp,
	},
	{
	    header: gettext('Expires'),
	    width: 150,
	    dataIndex: 'notafter',
	    renderer: Proxmox.Utils.render_timestamp,
	},
	{
	    header: gettext('Subject Alternative Names'),
	    flex: 1,
	    dataIndex: 'san',
	    renderer: PVE.Utils.render_san,
	},
	{
	    header: gettext('Fingerprint'),
	    dataIndex: 'fingerprint',
	    hidden: true,
	},
	{
	    header: gettext('PEM'),
	    dataIndex: 'pem',
	    hidden: true,
	},
    ],
    reload: function() {
	this.rstore.load();
    },
    viewCertificate: function() {
	let me = this;
	let selection = me.getSelection();
	if (!selection || selection.length < 1) {
	    return;
	}
	var win = Ext.create('PVE.node.CertificateViewer', {
	    cert: selection[0].data.filename,
	    nodename: me.nodename,
	});
	win.show();
    },
    listeners: {
	itemdblclick: 'viewCertificate',
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no nodename given";
	}
	me.rstore = Ext.create('Proxmox.data.UpdateStore', {
	    storeid: 'certs-' + me.nodename,
	    model: 'pve-certificate',
	    proxy: {
		type: 'proxmox',
		    url: '/api2/json/nodes/' + me.nodename + '/certificates/info',
	    },
	});
	me.store = {
	    type: 'diff',
	    rstore: me.rstore,
	};
	me.callParent();
	me.mon(me.rstore, 'load', store => me.down('#deletebtn').setDisabled(!store.getById('pveproxy-ssl.pem')));
	me.rstore.startUpdate();
	me.on('destroy', me.rstore.stopUpdate, me.rstore);
    },
});
Ext.define('PVE.node.CmdMenu', {
    extend: 'Ext.menu.Menu',
    xtype: 'nodeCmdMenu',
    showSeparator: false,
    items: [
	{
	    text: gettext('Create VM'),
	    itemId: 'createvm',
	    iconCls: 'fa fa-desktop',
	    handler: function() {
		Ext.create('PVE.qemu.CreateWizard', {
		    nodename: this.up('menu').nodename,
		    autoShow: true,
		});
	    },
	},
	{
	    text: gettext('Create CT'),
	    itemId: 'createct',
	    iconCls: 'fa fa-cube',
	    handler: function() {
		Ext.create('PVE.lxc.CreateWizard', {
		    nodename: this.up('menu').nodename,
		    autoShow: true,
		});
	    },
	},
	{ xtype: 'menuseparator' },
	{
	    text: gettext('Bulk Start'),
	    itemId: 'bulkstart',
	    iconCls: 'fa fa-fw fa-play',
	    handler: function() {
		Ext.create('PVE.window.BulkAction', {
		    nodename: this.up('menu').nodename,
		    title: gettext('Bulk Start'),
		    btnText: gettext('Start'),
		    action: 'startall',
		    autoShow: true,
		});
	    },
	},
	{
	    text: gettext('Bulk Shutdown'),
	    itemId: 'bulkstop',
	    iconCls: 'fa fa-fw fa-stop',
	    handler: function() {
		Ext.create('PVE.window.BulkAction', {
		    nodename: this.up('menu').nodename,
		    title: gettext('Bulk Shutdown'),
		    btnText: gettext('Shutdown'),
		    action: 'stopall',
		    autoShow: true,
		});
	    },
	},
	{
	    text: gettext('Bulk Migrate'),
	    itemId: 'bulkmigrate',
	    iconCls: 'fa fa-fw fa-send-o',
	    handler: function() {
		Ext.create('PVE.window.BulkAction', {
		    nodename: this.up('menu').nodename,
		    title: gettext('Bulk Migrate'),
		    btnText: gettext('Migrate'),
		    action: 'migrateall',
		    autoShow: true,
		});
	    },
	},
	{ xtype: 'menuseparator' },
	{
	    text: gettext('Shell'),
	    itemId: 'shell',
	    iconCls: 'fa fa-fw fa-terminal',
	    handler: function() {
		let nodename = this.up('menu').nodename;
		PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, nodename, undefined);
	    },
	},
	{ xtype: 'menuseparator' },
	{
	    text: gettext('Wake-on-LAN'),
	    itemId: 'wakeonlan',
	    iconCls: 'fa fa-fw fa-power-off',
	    handler: function() {
		let nodename = this.up('menu').nodename;
		Proxmox.Utils.API2Request({
		    url: `/nodes/${nodename}/wakeonlan`,
		    method: 'POST',
		    failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
		    success: function(response, opts) {
			Ext.Msg.show({
			    title: 'Success',
			    icon: Ext.Msg.INFO,
			    msg: Ext.String.format(
				gettext("Wake on LAN packet send for '{0}': '{1}'"),
				nodename,
				response.result.data,
			    ),
			});
		    },
		});
	    },
	},
    ],
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    throw 'no nodename specified';
	}
	me.title = gettext('Node') + " '" + me.nodename + "'";
	me.callParent();
	let caps = Ext.state.Manager.get('GuiCap');
	if (!caps.vms['VM.Allocate']) {
	    me.getComponent('createct').setDisabled(true);
	    me.getComponent('createvm').setDisabled(true);
	}
	if (!caps.vms['VM.Migrate']) {
	    me.getComponent('bulkmigrate').setDisabled(true);
	}
	if (!caps.vms['VM.PowerMgmt']) {
	    me.getComponent('bulkstart').setDisabled(true);
	    me.getComponent('bulkstop').setDisabled(true);
	}
	if (!caps.nodes['Sys.PowerMgmt']) {
	    me.getComponent('wakeonlan').setDisabled(true);
	}
	if (!caps.nodes['Sys.Console']) {
	    me.getComponent('shell').setDisabled(true);
	}
	if (me.pveSelNode.data.running) {
	    me.getComponent('wakeonlan').setDisabled(true);
	}
    },
});
Ext.define('PVE.node.Config', {
    extend: 'PVE.panel.Config',
    alias: 'widget.PVE.node.Config',
    onlineHelp: 'chapter_system_administration',
    initComponent: function() {
        var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var caps = Ext.state.Manager.get('GuiCap');
	me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
	    url: "/api2/json/nodes/" + nodename + "/status",
	    interval: 5000,
	});
	var node_command = function(cmd) {
	    Proxmox.Utils.API2Request({
		params: { command: cmd },
		url: '/nodes/' + nodename + '/status',
		method: 'POST',
		waitMsgTarget: me,
		failure: function(response, opts) {
		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		},
	    });
	};
	var actionBtn = Ext.create('Ext.Button', {
	    text: gettext('Bulk Actions'),
	    iconCls: 'fa fa-fw fa-ellipsis-v',
	    disabled: !caps.vms['VM.PowerMgmt'] && !caps.vms['VM.Migrate'],
	    menu: new Ext.menu.Menu({
		items: [
		    {
			text: gettext('Bulk Start'),
			iconCls: 'fa fa-fw fa-play',
			disabled: !caps.vms['VM.PowerMgmt'],
			handler: function() {
			    Ext.create('PVE.window.BulkAction', {
				autoShow: true,
				nodename: nodename,
				title: gettext('Bulk Start'),
				btnText: gettext('Start'),
				action: 'startall',
			    });
			},
		    },
		    {
			text: gettext('Bulk Shutdown'),
			iconCls: 'fa fa-fw fa-stop',
			disabled: !caps.vms['VM.PowerMgmt'],
			handler: function() {
			    Ext.create('PVE.window.BulkAction', {
				autoShow: true,
				nodename: nodename,
				title: gettext('Bulk Shutdown'),
				btnText: gettext('Shutdown'),
				action: 'stopall',
			    });
			},
		    },
		    {
			text: gettext('Bulk Migrate'),
			iconCls: 'fa fa-fw fa-send-o',
			disabled: !caps.vms['VM.Migrate'],
			handler: function() {
			    Ext.create('PVE.window.BulkAction', {
				autoShow: true,
				nodename: nodename,
				title: gettext('Bulk Migrate'),
				btnText: gettext('Migrate'),
				action: 'migrateall',
			    });
			},
		    },
		],
	    }),
	});
	let restartBtn = Ext.create('Proxmox.button.Button', {
	    text: gettext('Reboot'),
	    disabled: !caps.nodes['Sys.PowerMgmt'],
	    dangerous: true,
	    confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename),
	    handler: function() {
		node_command('reboot');
	    },
	    iconCls: 'fa fa-undo',
	});
	var shutdownBtn = Ext.create('Proxmox.button.Button', {
	    text: gettext('Shutdown'),
	    disabled: !caps.nodes['Sys.PowerMgmt'],
	    dangerous: true,
	    confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename),
	    handler: function() {
		node_command('shutdown');
	    },
	    iconCls: 'fa fa-power-off',
	});
	var shellBtn = Ext.create('PVE.button.ConsoleButton', {
	    disabled: !caps.nodes['Sys.Console'],
	    text: gettext('Shell'),
	    consoleType: 'shell',
	    nodename: nodename,
	});
	me.items = [];
	Ext.apply(me, {
	    title: gettext('Node') + " '" + nodename + "'",
	    hstateid: 'nodetab',
	    defaults: {
		statusStore: me.statusStore,
	    },
	    tbar: [restartBtn, shutdownBtn, shellBtn, actionBtn],
	});
	if (caps.nodes['Sys.Audit']) {
	    me.items.push(
		{
		    xtype: 'pveNodeSummary',
		    title: gettext('Summary'),
		    iconCls: 'fa fa-book',
		    itemId: 'summary',
		},
		{
		    xtype: 'pmxNotesView',
		    title: gettext('Notes'),
		    iconCls: 'fa fa-sticky-note-o',
		    itemId: 'notes',
		},
	    );
	}
	if (caps.nodes['Sys.Console']) {
	    me.items.push(
		{
		    xtype: 'pveNoVncConsole',
		    title: gettext('Shell'),
		    iconCls: 'fa fa-terminal',
		    itemId: 'jsconsole',
		    consoleType: 'shell',
		    xtermjs: true,
		    nodename: nodename,
		},
	    );
	}
	if (caps.nodes['Sys.Audit']) {
	    me.items.push(
		{
		    xtype: 'proxmoxNodeServiceView',
		    title: gettext('System'),
		    iconCls: 'fa fa-cogs',
		    itemId: 'services',
		    expandedOnInit: true,
		    restartCommand: 'reload', // avoid disruptions
		    startOnlyServices: {
			'pveproxy': true,
			'pvedaemon': true,
			'pve-cluster': true,
		    },
		    nodename: nodename,
		    onlineHelp: 'pve_service_daemons',
		},
		{
		    xtype: 'proxmoxNodeNetworkView',
		    title: gettext('Network'),
		    iconCls: 'fa fa-exchange',
		    itemId: 'network',
		    showApplyBtn: true,
		    groups: ['services'],
		    nodename: nodename,
		    onlineHelp: 'sysadmin_network_configuration',
		},
		{
		    xtype: 'pveCertificatesView',
		    title: gettext('Certificates'),
		    iconCls: 'fa fa-certificate',
		    itemId: 'certificates',
		    groups: ['services'],
		    nodename: nodename,
		},
		{
		    xtype: 'proxmoxNodeDNSView',
		    title: gettext('DNS'),
		    iconCls: 'fa fa-globe',
		    groups: ['services'],
		    itemId: 'dns',
		    nodename: nodename,
		    onlineHelp: 'sysadmin_network_configuration',
		},
		{
		    xtype: 'proxmoxNodeHostsView',
		    title: gettext('Hosts'),
		    iconCls: 'fa fa-globe',
		    groups: ['services'],
		    itemId: 'hosts',
		    nodename: nodename,
		    onlineHelp: 'sysadmin_network_configuration',
		},
		{
		    xtype: 'proxmoxNodeOptionsView',
		    title: gettext('Options'),
		    iconCls: 'fa fa-gear',
		    groups: ['services'],
		    itemId: 'options',
		    nodename: nodename,
		    onlineHelp: 'proxmox_node_management',
		},
		{
		    xtype: 'proxmoxNodeTimeView',
		    title: gettext('Time'),
		    itemId: 'time',
		    groups: ['services'],
		    nodename: nodename,
		    iconCls: 'fa fa-clock-o',
		});
	}
	if (caps.nodes['Sys.Syslog']) {
	    me.items.push({
		xtype: 'proxmoxJournalView',
		title: 'Syslog',
		iconCls: 'fa fa-list',
		groups: ['services'],
		disabled: !caps.nodes['Sys.Syslog'],
		itemId: 'syslog',
		url: "/api2/extjs/nodes/" + nodename + "/journal",
	    });
	    if (caps.nodes['Sys.Modify']) {
		me.items.push({
		    xtype: 'proxmoxNodeAPT',
		    title: gettext('Updates'),
		    iconCls: 'fa fa-refresh',
		    expandedOnInit: true,
		    disabled: !caps.nodes['Sys.Console'],
		    // do we want to link to system updates instead?
		    itemId: 'apt',
		    upgradeBtn: {
			xtype: 'pveConsoleButton',
			disabled: Proxmox.UserName !== 'root@pam',
			text: gettext('Upgrade'),
			consoleType: 'upgrade',
			nodename: nodename,
		    },
		    nodename: nodename,
		});
		me.items.push({
		    xtype: 'proxmoxNodeAPTRepositories',
		    title: gettext('Repositories'),
		    iconCls: 'fa fa-files-o',
		    itemId: 'aptrepositories',
		    nodename: nodename,
		    onlineHelp: 'sysadmin_package_repositories',
		    groups: ['apt'],
		});
	    }
	}
	if (caps.nodes['Sys.Audit']) {
	    me.items.push(
		{
		    xtype: 'pveFirewallRules',
		    iconCls: 'fa fa-shield',
		    title: gettext('Firewall'),
		    allow_iface: true,
		    base_url: '/nodes/' + nodename + '/firewall/rules',
		    list_refs_url: '/cluster/firewall/refs',
		    itemId: 'firewall',
		},
		{
		    xtype: 'pveFirewallOptions',
		    title: gettext('Options'),
		    iconCls: 'fa fa-gear',
		    onlineHelp: 'pve_firewall_host_specific_configuration',
		    groups: ['firewall'],
		    base_url: '/nodes/' + nodename + '/firewall/options',
		    fwtype: 'node',
		    itemId: 'firewall-options',
		});
	}
	if (caps.nodes['Sys.Audit']) {
	    me.items.push(
		{
		    xtype: 'pmxDiskList',
		    title: gettext('Disks'),
		    itemId: 'storage',
		    expandedOnInit: true,
		    iconCls: 'fa fa-hdd-o',
		    nodename: nodename,
		    includePartitions: true,
		    supportsWipeDisk: true,
		},
		{
		    xtype: 'pveLVMList',
		    title: 'LVM',
		    itemId: 'lvm',
		    onlineHelp: 'chapter_lvm',
		    iconCls: 'fa fa-square',
		    groups: ['storage'],
		},
		{
		    xtype: 'pveLVMThinList',
		    title: 'LVM-Thin',
		    itemId: 'lvmthin',
		    onlineHelp: 'chapter_lvm',
		    iconCls: 'fa fa-square-o',
		    groups: ['storage'],
		},
		{
		    xtype: 'pveDirectoryList',
		    title: Proxmox.Utils.directoryText,
		    itemId: 'directory',
		    onlineHelp: 'chapter_storage',
		    iconCls: 'fa fa-folder',
		    groups: ['storage'],
		},
		{
		    title: 'ZFS',
		    itemId: 'zfs',
		    onlineHelp: 'chapter_zfs',
		    iconCls: 'fa fa-th-large',
		    groups: ['storage'],
		    xtype: 'pveZFSList',
		},
		{
		    xtype: 'pveNodeCephStatus',
		    title: 'Ceph',
		    itemId: 'ceph',
		    iconCls: 'fa fa-ceph',
		},
		{
		    xtype: 'pveNodeCephConfigCrush',
		    title: gettext('Configuration'),
		    iconCls: 'fa fa-gear',
		    groups: ['ceph'],
		    itemId: 'ceph-config',
		},
		{
		    xtype: 'pveNodeCephMonMgr',
		    title: gettext('Monitor'),
		    iconCls: 'fa fa-tv',
		    groups: ['ceph'],
		    itemId: 'ceph-monlist',
		},
		{
		    xtype: 'pveNodeCephOsdTree',
		    title: 'OSD',
		    iconCls: 'fa fa-hdd-o',
		    groups: ['ceph'],
		    itemId: 'ceph-osdtree',
		},
		{
		    xtype: 'pveNodeCephFSPanel',
		    title: 'CephFS',
		    iconCls: 'fa fa-folder',
		    groups: ['ceph'],
		    nodename: nodename,
		    itemId: 'ceph-cephfspanel',
		},
		{
		    xtype: 'pveNodeCephPoolList',
		    title: 'Pools',
		    iconCls: 'fa fa-sitemap',
		    groups: ['ceph'],
		    itemId: 'ceph-pools',
		},
		{
		    xtype: 'pveReplicaView',
		    iconCls: 'fa fa-retweet',
		    title: gettext('Replication'),
		    itemId: 'replication',
		},
	    );
	}
	if (caps.nodes['Sys.Syslog']) {
	    me.items.push(
		{
		    xtype: 'proxmoxLogView',
		    title: gettext('Log'),
		    iconCls: 'fa fa-list',
		    groups: ['firewall'],
		    onlineHelp: 'chapter_pve_firewall',
		    url: '/api2/extjs/nodes/' + nodename + '/firewall/log',
		    itemId: 'firewall-fwlog',
		},
		{
		    xtype: 'cephLogView',
		    title: gettext('Log'),
		    itemId: 'ceph-log',
		    iconCls: 'fa fa-list',
		    groups: ['ceph'],
		    onlineHelp: 'chapter_pveceph',
		    url: "/api2/extjs/nodes/" + nodename + "/ceph/log",
		    nodename: nodename,
		});
	}
	me.items.push(
	    {
		title: gettext('Task History'),
		iconCls: 'fa fa-list-alt',
		itemId: 'tasks',
		nodename: nodename,
		xtype: 'proxmoxNodeTasks',
		extraFilter: [
		    {
			xtype: 'pveGuestIDSelector',
			fieldLabel: gettext('VMID'),
			allowBlank: true,
			name: 'vmid',
		    },
		],
	    },
	    {
		title: gettext('Subscription'),
		iconCls: 'fa fa-support',
		itemId: 'support',
		xtype: 'pveNodeSubscription',
		nodename: nodename,
	    },
	);
	me.callParent();
	me.mon(me.statusStore, 'load', function(store, records, success) {
	    let uptimerec = store.data.get('uptime');
	    let powermgmt = caps.nodes['Sys.PowerMgmt'] && uptimerec && uptimerec.data.value;
	    restartBtn.setDisabled(!powermgmt);
	    shutdownBtn.setDisabled(!powermgmt);
	    shellBtn.setDisabled(!powermgmt);
	});
	me.on('afterrender', function() {
	    me.statusStore.startUpdate();
	});
	me.on('destroy', function() {
	    me.statusStore.stopUpdate();
	});
    },
});
Ext.define('PVE.node.CreateDirectory', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveCreateDirectory',
    subject: Proxmox.Utils.directoryText,
    showProgress: true,
    onlineHelp: 'chapter_storage',
    initComponent: function() {
        var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	me.isCreate = true;
        Ext.applyIf(me, {
	    url: "/nodes/" + me.nodename + "/disks/directory",
	    method: 'POST',
	    items: [
		{
		    xtype: 'pmxDiskSelector',
		    name: 'device',
		    nodename: me.nodename,
		    diskType: 'unused',
		    includePartitions: true,
		    fieldLabel: gettext('Disk'),
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxKVComboBox',
		    comboItems: [
			['ext4', 'ext4'],
			['xfs', 'xfs'],
		    ],
		    fieldLabel: gettext('Filesystem'),
		    name: 'filesystem',
		    value: '',
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'name',
		    fieldLabel: gettext('Name'),
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'add_storage',
		    fieldLabel: gettext('Add Storage'),
		    value: '1',
		},
            ],
        });
        me.callParent();
    },
});
Ext.define('PVE.node.Directorylist', {
    extend: 'Ext.grid.Panel',
    xtype: 'pveDirectoryList',
    viewModel: {
	data: {
	    path: '',
	},
	formulas: {
	    dirName: (get) => get('path')?.replace('/mnt/pve/', '') || undefined,
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	destroyDirectory: function() {
	    let me = this;
	    let vm = me.getViewModel();
	    let view = me.getView();
	    const dirName = vm.get('dirName');
	    if (!view.nodename) {
		throw "no node name specified";
	    }
	    if (!dirName) {
		throw "no directory name specified";
	    }
	    Ext.create('PVE.window.SafeDestroyStorage', {
		url: `/nodes/${view.nodename}/disks/directory/${dirName}`,
		item: { id: dirName },
		taskName: 'dirremove',
		taskDone: () => { view.reload(); },
	    }).show();
	},
    },
    stateful: true,
    stateId: 'grid-node-directory',
    columns: [
	{
	    text: gettext('Path'),
	    dataIndex: 'path',
	    flex: 1,
	},
	{
	    header: gettext('Device'),
	    flex: 1,
	    dataIndex: 'device',
	},
	{
	    header: gettext('Type'),
	    width: 100,
	    dataIndex: 'type',
	},
	{
	    header: gettext('Options'),
	    width: 100,
	    dataIndex: 'options',
	},
	{
	    header: gettext('Unit File'),
	    hidden: true,
	    dataIndex: 'unitfile',
	},
    ],
    rootVisible: false,
    useArrows: true,
    tbar: [
	{
	    text: gettext('Reload'),
	    iconCls: 'fa fa-refresh',
	    handler: function() {
		this.up('panel').reload();
	    },
	},
	{
	    text: `${gettext('Create')}: ${gettext('Directory')}`,
	    handler: function() {
		let view = this.up('panel');
		Ext.create('PVE.node.CreateDirectory', {
		    nodename: view.nodename,
		    listeners: {
			destroy: () => view.reload(),
		    },
		    autoShow: true,
		});
	    },
	},
	'->',
	{
	    xtype: 'tbtext',
	    data: {
		dirName: undefined,
	    },
	    bind: {
		data: {
		    dirName: "{dirName}",
		},
	    },
	    tpl: [
		'',
		gettext('Directory') + ' {dirName}:',
		'',
		Ext.String.format(gettext('No {0} selected'), gettext('directory')),
		'',
	    ],
	},
	{
	    text: gettext('More'),
	    iconCls: 'fa fa-bars',
	    disabled: true,
	    bind: {
		disabled: '{!dirName}',
	    },
	    menu: [
		{
		    text: gettext('Destroy'),
		    itemId: 'remove',
		    iconCls: 'fa fa-fw fa-trash-o',
		    handler: 'destroyDirectory',
		    disabled: true,
		    bind: {
			disabled: '{!dirName}',
		    },
		},
	    ],
	},
    ],
    reload: function() {
	let me = this;
	me.store.load();
	me.store.sort();
    },
    listeners: {
	activate: function() {
	    this.reload();
	},
	selectionchange: function(model, selected) {
	    let me = this;
	    let vm = me.getViewModel();
	    vm.set('path', selected[0]?.data.path || '');
	},
    },
    initComponent: function() {
        let me = this;
	me.nodename = me.pveSelNode.data.node;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	Ext.apply(me, {
	    store: {
		fields: ['path', 'device', 'type', 'options', 'unitfile'],
		proxy: {
		    type: 'proxmox',
		    url: `/api2/json/nodes/${me.nodename}/disks/directory`,
		},
		sorters: 'path',
	    },
	});
	me.callParent();
	Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
	me.reload();
    },
});
Ext.define('PVE.node.CreateLVM', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveCreateLVM',
    onlineHelp: 'chapter_lvm',
    subject: 'LVM Volume Group',
    showProgress: true,
    isCreate: true,
    initComponent: function() {
        let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	me.isCreate = true;
        Ext.applyIf(me, {
	    url: `/nodes/${me.nodename}/disks/lvm`,
	    method: 'POST',
	    items: [
		{
		    xtype: 'pmxDiskSelector',
		    name: 'device',
		    nodename: me.nodename,
		    diskType: 'unused',
		    includePartitions: true,
		    fieldLabel: gettext('Disk'),
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'name',
		    fieldLabel: gettext('Name'),
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'add_storage',
		    fieldLabel: gettext('Add Storage'),
		    value: '1',
		},
	    ],
	});
	me.callParent();
    },
});
Ext.define('PVE.node.LVMList', {
    extend: 'Ext.tree.Panel',
    xtype: 'pveLVMList',
    viewModel: {
	data: {
	    volumeGroup: '',
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	destroyVolumeGroup: function() {
	    let me = this;
	    let vm = me.getViewModel();
	    let view = me.getView();
	    const volumeGroup = vm.get('volumeGroup');
	    if (!view.nodename) {
		throw "no node name specified";
	    }
	    if (!volumeGroup) {
		throw "no volume group specified";
	    }
	    Ext.create('PVE.window.SafeDestroyStorage', {
		url: `/nodes/${view.nodename}/disks/lvm/${volumeGroup}`,
		item: { id: volumeGroup },
		taskName: 'lvmremove',
		taskDone: () => { view.reload(); },
	    }).show();
	},
    },
    emptyText: PVE.Utils.renderNotFound('VGs'),
    stateful: true,
    stateId: 'grid-node-lvm',
    rootVisible: false,
    useArrows: true,
    columns: [
	{
	    xtype: 'treecolumn',
	    text: gettext('Name'),
	    dataIndex: 'name',
	    flex: 1,
	},
	{
	    text: gettext('Number of LVs'),
	    dataIndex: 'lvcount',
	    width: 150,
	    align: 'right',
	},
	{
	    header: gettext('Assigned to LVs'),
	    width: 130,
	    dataIndex: 'usage',
	    tdCls: 'x-progressbar-default-cell',
	    xtype: 'widgetcolumn',
	    widget: {
		xtype: 'pveProgressBar',
	    },
	},
	{
	    header: gettext('Size'),
	    width: 100,
	    align: 'right',
	    sortable: true,
	    renderer: Proxmox.Utils.format_size,
	    dataIndex: 'size',
	},
	{
	    header: gettext('Free'),
	    width: 100,
	    align: 'right',
	    sortable: true,
	    renderer: Proxmox.Utils.format_size,
	    dataIndex: 'free',
	},
    ],
    tbar: [
	{
	    text: gettext('Reload'),
	    iconCls: 'fa fa-refresh',
	    handler: function() {
		this.up('panel').reload();
	    },
	},
	{
	    text: gettext('Create') + ': Volume Group',
	    handler: function() {
		let view = this.up('panel');
		Ext.create('PVE.node.CreateLVM', {
		    nodename: view.nodename,
		    taskDone: () => view.reload(),
		    autoShow: true,
		});
	    },
	},
	'->',
	{
	    xtype: 'tbtext',
	    data: {
		volumeGroup: undefined,
	    },
	    bind: {
		data: {
		    volumeGroup: "{volumeGroup}",
		},
	    },
	    tpl: [
		'',
		'Volume group {volumeGroup}:',
		'',
		Ext.String.format(gettext('No {0} selected'), 'volume group'),
		'',
	    ],
	},
	{
	    text: gettext('More'),
	    iconCls: 'fa fa-bars',
	    disabled: true,
	    bind: {
		disabled: '{!volumeGroup}',
	    },
	    menu: [
		{
		    text: gettext('Destroy'),
		    itemId: 'remove',
		    iconCls: 'fa fa-fw fa-trash-o',
		    handler: 'destroyVolumeGroup',
		    disabled: true,
		    bind: {
			disabled: '{!volumeGroup}',
		    },
		},
	    ],
	},
    ],
    reload: function() {
	let me = this;
	let sm = me.getSelectionModel();
	Proxmox.Utils.API2Request({
	    url: `/nodes/${me.nodename}/disks/lvm`,
	    waitMsgTarget: me,
	    method: 'GET',
	    failure: (response, opts) => Proxmox.Utils.setErrorMask(me, response.htmlStatus),
	    success: function(response, opts) {
		sm.deselectAll();
		me.setRootNode(response.result.data);
		me.expandAll();
	    },
	});
    },
    listeners: {
	activate: function() {
	    this.reload();
	},
	selectionchange: function(model, selected) {
	    let me = this;
	    let vm = me.getViewModel();
	    if (selected.length < 1 || selected[0].data.parentId !== 'root') {
		vm.set('volumeGroup', '');
	    } else {
		vm.set('volumeGroup', selected[0].data.name);
	    }
	},
    },
    selModel: 'treemodel',
    fields: [
	'name',
	'size',
	'free',
	{
	    type: 'string',
	    name: 'iconCls',
	    calculate: data => `fa x-fa-tree fa-${data.leaf ? 'hdd-o' : 'object-group'}`,
	},
	{
	    type: 'number',
	    name: 'usage',
	    calculate: data => (data.size - data.free) / data.size,
	},
    ],
    sorters: 'name',
    initComponent: function() {
	let me = this;
	me.nodename = me.pveSelNode.data.node;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	me.callParent();
	me.reload();
    },
});
Ext.define('PVE.node.CreateLVMThin', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveCreateLVMThin',
    onlineHelp: 'chapter_lvm',
    subject: 'LVM Thinpool',
    showProgress: true,
    isCreate: true,
    initComponent: function() {
        let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
        Ext.applyIf(me, {
	    url: `/nodes/${me.nodename}/disks/lvmthin`,
	    method: 'POST',
	    items: [
		{
		    xtype: 'pmxDiskSelector',
		    name: 'device',
		    nodename: me.nodename,
		    diskType: 'unused',
		    includePartitions: true,
		    fieldLabel: gettext('Disk'),
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxtextfield',
		    name: 'name',
		    fieldLabel: gettext('Name'),
		    allowBlank: false,
		},
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'add_storage',
		    fieldLabel: gettext('Add Storage'),
		    value: '1',
		},
            ],
        });
        me.callParent();
    },
});
Ext.define('PVE.node.LVMThinList', {
    extend: 'Ext.grid.Panel',
    xtype: 'pveLVMThinList',
    viewModel: {
	data: {
	    thinPool: '',
	    volumeGroup: '',
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	destroyThinPool: function() {
	    let me = this;
	    let vm = me.getViewModel();
	    let view = me.getView();
	    const thinPool = vm.get('thinPool');
	    const volumeGroup = vm.get('volumeGroup');
	    if (!view.nodename) {
		throw "no node name specified";
	    }
	    if (!thinPool) {
		throw "no thin pool specified";
	    }
	    if (!volumeGroup) {
		throw "no volume group specified";
	    }
	    Ext.create('PVE.window.SafeDestroyStorage', {
		url: `/nodes/${view.nodename}/disks/lvmthin/${thinPool}`,
		params: { 'volume-group': volumeGroup },
		item: { id: `${volumeGroup}/${thinPool}` },
		taskName: 'lvmthinremove',
		taskDone: () => { view.reload(); },
	    }).show();
	},
    },
    emptyText: PVE.Utils.renderNotFound('Thin-Pool'),
    stateful: true,
    stateId: 'grid-node-lvmthin',
    rootVisible: false,
    useArrows: true,
    columns: [
	{
	    text: gettext('Name'),
	    dataIndex: 'lv',
	    flex: 1,
	},
	{
	    header: 'Volume Group',
	    width: 110,
	    dataIndex: 'vg',
	},
	{
	    header: gettext('Usage'),
	    width: 110,
	    dataIndex: 'usage',
	    tdCls: 'x-progressbar-default-cell',
	    xtype: 'widgetcolumn',
	    widget: {
		xtype: 'pveProgressBar',
	    },
	},
	{
	    header: gettext('Size'),
	    width: 100,
	    align: 'right',
	    sortable: true,
	    renderer: Proxmox.Utils.format_size,
	    dataIndex: 'lv_size',
	},
	{
	    header: gettext('Used'),
	    width: 100,
	    align: 'right',
	    sortable: true,
	    renderer: Proxmox.Utils.format_size,
	    dataIndex: 'used',
	},
	{
	    header: gettext('Metadata Usage'),
	    width: 120,
	    dataIndex: 'metadata_usage',
	    tdCls: 'x-progressbar-default-cell',
	    xtype: 'widgetcolumn',
	    widget: {
		xtype: 'pveProgressBar',
	    },
	},
	{
	    header: gettext('Metadata Size'),
	    width: 120,
	    align: 'right',
	    sortable: true,
	    renderer: Proxmox.Utils.format_size,
	    dataIndex: 'metadata_size',
	},
	{
	    header: gettext('Metadata Used'),
	    width: 125,
	    align: 'right',
	    sortable: true,
	    renderer: Proxmox.Utils.format_size,
	    dataIndex: 'metadata_used',
	},
    ],
    tbar: [
	{
	    text: gettext('Reload'),
	    iconCls: 'fa fa-refresh',
	    handler: function() {
		this.up('panel').reload();
	    },
	},
	{
	    text: gettext('Create') + ': Thinpool',
	    handler: function() {
		var view = this.up('panel');
		Ext.create('PVE.node.CreateLVMThin', {
		    nodename: view.nodename,
		    taskDone: () => view.reload(),
		    autoShow: true,
		});
	    },
	},
	'->',
	{
	    xtype: 'tbtext',
	    data: {
		thinPool: undefined,
		volumeGroup: undefined,
	    },
	    bind: {
		data: {
		    thinPool: "{thinPool}",
		    volumeGroup: "{volumeGroup}",
		},
	    },
	    tpl: [
		'',
		'',
		'Thinpool {volumeGroup}/{thinPool}:',
		'', // volumeGroup
		'Missing volume group (node running old version?)',
		'',
		'', // thinPool
		Ext.String.format(gettext('No {0} selected'), 'thinpool'),
		'',
	    ],
	},
	{
	    text: gettext('More'),
	    iconCls: 'fa fa-bars',
	    disabled: true,
	    bind: {
		disabled: '{!volumeGroup || !thinPool}',
	    },
	    menu: [
		{
		    text: gettext('Destroy'),
		    itemId: 'remove',
		    iconCls: 'fa fa-fw fa-trash-o',
		    handler: 'destroyThinPool',
		    disabled: true,
		    bind: {
			disabled: '{!volumeGroup || !thinPool}',
		    },
		},
	    ],
	},
    ],
    reload: function() {
	let me = this;
	me.store.load();
	me.store.sort();
    },
    listeners: {
	activate: function() {
	    this.reload();
	},
	selectionchange: function(model, selected) {
	    let me = this;
	    let vm = me.getViewModel();
	    vm.set('volumeGroup', selected[0]?.data.vg || '');
	    vm.set('thinPool', selected[0]?.data.lv || '');
	},
    },
    initComponent: function() {
	let me = this;
	me.nodename = me.pveSelNode.data.node;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	Ext.apply(me, {
	    store: {
		fields: [
		    'lv',
		    'lv_size',
		    'used',
		    'metadata_size',
		    'metadata_used',
		    {
			type: 'number',
			name: 'usage',
			calculate: data => data.used / data.lv_size,
		    },
		    {
			type: 'number',
			name: 'metadata_usage',
			calculate: data => data.metadata_used / data.metadata_size,
		    },
		],
		proxy: {
		    type: 'proxmox',
		    url: `/api2/json/nodes/${me.nodename}/disks/lvmthin`,
		},
		sorters: 'lv',
	    },
	});
	me.callParent();
	Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
	me.reload();
    },
});
Ext.define('PVE.node.StatusView', {
    extend: 'Proxmox.panel.StatusView',
    alias: 'widget.pveNodeStatus',
    height: 300,
    bodyPadding: '15 5 15 5',
    layout: {
	type: 'table',
	columns: 2,
	tableAttrs: {
	    style: {
		width: '100%',
	    },
	},
    },
    defaults: {
	xtype: 'pmxInfoWidget',
	padding: '0 10 5 10',
    },
    items: [
	{
	    itemId: 'cpu',
	    iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
	    title: gettext('CPU usage'),
	    valueField: 'cpu',
	    maxField: 'cpuinfo',
	    renderer: Proxmox.Utils.render_node_cpu_usage,
	},
	{
	    itemId: 'wait',
	    iconCls: 'fa fa-fw fa-clock-o',
	    title: gettext('IO delay'),
	    valueField: 'wait',
	    rowspan: 2,
	},
	{
	    itemId: 'load',
	    iconCls: 'fa fa-fw fa-tasks',
	    title: gettext('Load average'),
	    printBar: false,
	    textField: 'loadavg',
	},
	{
	    xtype: 'box',
	    colspan: 2,
	    padding: '0 0 20 0',
	},
	{
	    iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
	    itemId: 'memory',
	    title: gettext('RAM usage'),
	    valueField: 'memory',
	    maxField: 'memory',
	    renderer: Proxmox.Utils.render_node_size_usage,
	},
	{
	    itemId: 'ksm',
	    printBar: false,
	    title: gettext('KSM sharing'),
	    textField: 'ksm',
	    renderer: function(record) {
		return Proxmox.Utils.render_size(record.shared);
	    },
	    padding: '0 10 10 10',
	},
	{
	    iconCls: 'fa fa-fw fa-hdd-o',
	    itemId: 'rootfs',
	    title: '/ ' + gettext('HD space'),
	    valueField: 'rootfs',
	    maxField: 'rootfs',
	    renderer: Proxmox.Utils.render_node_size_usage,
	},
	{
	    iconCls: 'fa fa-fw fa-refresh',
	    itemId: 'swap',
	    printSize: true,
	    title: gettext('SWAP usage'),
	    valueField: 'swap',
	    maxField: 'swap',
	    renderer: Proxmox.Utils.render_node_size_usage,
	},
	{
	    xtype: 'box',
	    colspan: 2,
	    padding: '0 0 20 0',
	},
	{
	    itemId: 'cpus',
	    colspan: 2,
	    printBar: false,
	    title: gettext('CPU(s)'),
	    textField: 'cpuinfo',
	    renderer: Proxmox.Utils.render_cpu_model,
	    value: '',
	},
	{
	    itemId: 'kversion',
	    colspan: 2,
	    title: gettext('Kernel Version'),
	    printBar: false,
	    textField: 'kversion',
	    value: '',
	},
	{
	    itemId: 'version',
	    colspan: 2,
	    printBar: false,
	    title: gettext('PVE Manager Version'),
	    textField: 'pveversion',
	    value: '',
	},
    ],
    updateTitle: function() {
	var me = this;
	var uptime = Proxmox.Utils.render_uptime(me.getRecordValue('uptime'));
	me.setTitle(me.pveSelNode.data.node + ' (' + gettext('Uptime') + ': ' + uptime + ')');
    },
    initComponent: function() {
	let me = this;
	let stateProvider = Ext.state.Manager.getProvider();
	let repoLink = stateProvider.encodeHToken({
	    view: "server",
	    rid: `node/${me.pveSelNode.data.node}`,
	    ltab: "tasks",
	    nodetab: "aptrepositories",
	});
	me.items.push({
	    xtype: 'pmxNodeInfoRepoStatus',
	    itemId: 'repositoryStatus',
	    product: 'Proxmox VE',
	    repoLink: `#${repoLink}`,
	});
	me.callParent();
    },
});
Ext.define('PVE.node.SubscriptionKeyEdit', {
    extend: 'Proxmox.window.Edit',
    title: gettext('Upload Subscription Key'),
    width: 300,
    items: {
	xtype: 'textfield',
	name: 'key',
	value: '',
	fieldLabel: gettext('Subscription Key'),
    },
    initComponent: function() {
	var me = this;
	me.callParent();
	me.load();
    },
});
Ext.define('PVE.node.Subscription', {
    extend: 'Proxmox.grid.ObjectGrid',
    alias: ['widget.pveNodeSubscription'],
    onlineHelp: 'getting_help',
    viewConfig: {
	enableTextSelection: true,
    },
    showReport: function() {
	var me = this;
	var getReportFileName = function() {
	    var now = Ext.Date.format(new Date(), 'D-d-F-Y-G-i');
	    return `${me.nodename}-pve-report-${now}.txt`;
	};
	var view = Ext.createWidget('component', {
	    itemId: 'system-report-view',
	    scrollable: true,
	    style: {
		'white-space': 'pre',
		'font-family': 'monospace',
		padding: '5px',
	    },
	});
	var reportWindow = Ext.create('Ext.window.Window', {
	    title: gettext('System Report'),
	    width: 1024,
	    height: 600,
	    layout: 'fit',
	    modal: true,
	    buttons: [
		'->',
		{
		    text: gettext('Download'),
		    handler: function() {
			var fileContent = Ext.String.htmlDecode(reportWindow.getComponent('system-report-view').html);
			var fileName = getReportFileName();
			// Internet Explorer
			if (window.navigator.msSaveOrOpenBlob) {
			    navigator.msSaveOrOpenBlob(new Blob([fileContent]), fileName);
			} else {
			    var element = document.createElement('a');
			    element.setAttribute('href', 'data:text/plain;charset=utf-8,' +
			      encodeURIComponent(fileContent));
			    element.setAttribute('download', fileName);
			    element.style.display = 'none';
			    document.body.appendChild(element);
			    element.click();
			    document.body.removeChild(element);
			}
		    },
		},
	    ],
	    items: view,
	});
	Proxmox.Utils.API2Request({
	    url: '/api2/extjs/nodes/' + me.nodename + '/report',
	    method: 'GET',
	    waitMsgTarget: me,
	    failure: function(response) {
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
	    },
	    success: function(response) {
		var report = Ext.htmlEncode(response.result.data);
		reportWindow.show();
		view.update(report);
	    },
	});
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	var reload = function() {
	    me.rstore.load();
	};
	var baseurl = '/nodes/' + me.nodename + '/subscription';
	var render_status = function(value) {
	    var message = me.getObjectValue('message');
	    if (message) {
		return value + ": " + message;
	    }
	    return value;
	};
	var rows = {
	    productname: {
		header: gettext('Type'),
	    },
	    key: {
		header: gettext('Subscription Key'),
	    },
	    status: {
		header: gettext('Status'),
		renderer: render_status,
	    },
	    message: {
		visible: false,
	    },
	    serverid: {
		header: gettext('Server ID'),
	    },
	    sockets: {
		header: gettext('Sockets'),
	    },
	    checktime: {
		header: gettext('Last checked'),
		renderer: Proxmox.Utils.render_timestamp,
	    },
	    nextduedate: {
		header: gettext('Next due date'),
	    },
	    signature: {
		header: gettext('Signed/Offline'),
		renderer: (value) => {
		    if (value) {
			return gettext('Yes');
		    } else {
			return gettext('No');
		    }
		},
	    },
	};
	Ext.apply(me, {
	    url: '/api2/json' + baseurl,
	    cwidth1: 170,
	    tbar: [
		{
		    text: gettext('Upload Subscription Key'),
		    handler: function() {
			var win = Ext.create('PVE.node.SubscriptionKeyEdit', {
			    url: '/api2/extjs/' + baseurl,
			});
			win.show();
			win.on('destroy', reload);
		    },
		},
		{
		    text: gettext('Check'),
		    handler: function() {
			Proxmox.Utils.API2Request({
			    params: { force: 1 },
			    url: baseurl,
			    method: 'POST',
			    waitMsgTarget: me,
			    failure: function(response, opts) {
				Ext.Msg.alert(gettext('Error'), response.htmlStatus);
			    },
			    callback: reload,
			});
		    },
		},
		{
		    text: gettext('Remove Subscription'),
		    xtype: 'proxmoxStdRemoveButton',
		    confirmMsg: gettext('Are you sure you want to remove the subscription key?'),
		    baseurl: baseurl,
		    dangerous: true,
		    selModel: false,
		    callback: reload,
		},
		'-',
		{
		    text: gettext('System Report'),
		    handler: function() {
			Proxmox.Utils.checked_command(function() { me.showReport(); });
		    },
		},
	    ],
	    rows: rows,
	    listeners: {
		activate: reload,
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.node.Summary', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveNodeSummary',
    scrollable: true,
    bodyPadding: 5,
    showVersions: function() {
	var me = this;
	// Note: we use simply text/html here, because ExtJS grid has problems
	// with cut&paste
	var nodename = me.pveSelNode.data.node;
	var view = Ext.createWidget('component', {
	    autoScroll: true,
	    id: 'pkgversions',
	    padding: 5,
	    style: {
		'white-space': 'pre',
		'font-family': 'monospace',
	    },
	});
	var win = Ext.create('Ext.window.Window', {
	    title: gettext('Package versions'),
	    width: 600,
	    height: 600,
	    layout: 'fit',
	    modal: true,
	    items: [view],
	    buttons: [
		{
		    xtype: 'button',
		    iconCls: 'fa fa-clipboard',
		    handler: function(button) {
			window.getSelection().selectAllChildren(
			    document.getElementById('pkgversions'),
			);
			document.execCommand("copy");
		    },
		    text: gettext('Copy'),
		},
		{
		    text: gettext('Ok'),
		    handler: function() {
			this.up('window').close();
		    },
		},
	    ],
	});
	Proxmox.Utils.API2Request({
	    waitMsgTarget: me,
	    url: `/nodes/${nodename}/apt/versions`,
	    method: 'GET',
	    failure: function(response, opts) {
		win.close();
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
	    },
	    success: function(response, opts) {
		win.show();
		let text = '';
		Ext.Array.each(response.result.data, function(rec) {
		    let version = "not correctly installed";
		    let pkg = rec.Package;
		    if (rec.OldVersion && rec.CurrentState === 'Installed') {
			version = rec.OldVersion;
		    }
		    if (rec.RunningKernel) {
			text += `${pkg}: ${version} (running kernel: ${rec.RunningKernel})\n`;
		    } else if (rec.ManagerVersion) {
			text += `${pkg}: ${version} (running version: ${rec.ManagerVersion})\n`;
		    } else {
			text += `${pkg}: ${version}\n`;
		    }
		});
		view.update(Ext.htmlEncode(text));
	    },
	});
    },
    updateRepositoryStatus: function() {
	let me = this;
	let repoStatus = me.nodeStatus.down('#repositoryStatus');
	let nodename = me.pveSelNode.data.node;
	Proxmox.Utils.API2Request({
	    url: `/nodes/${nodename}/apt/repositories`,
	    method: 'GET',
	    failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
	    success: response => repoStatus.setRepositoryInfo(response.result.data['standard-repos']),
	});
	Proxmox.Utils.API2Request({
	    url: `/nodes/${nodename}/subscription`,
	    method: 'GET',
	    failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
	    success: function(response, opts) {
		const res = response.result;
		const subscription = res?.data?.status.toLowerCase() === 'active';
		repoStatus.setSubscriptionStatus(subscription);
	    },
	});
    },
    initComponent: function() {
        var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	if (!me.statusStore) {
	    throw "no status storage specified";
	}
	var rstore = me.statusStore;
	var version_btn = new Ext.Button({
	    text: gettext('Package versions'),
	    handler: function() {
		Proxmox.Utils.checked_command(function() { me.showVersions(); });
	    },
	});
	var rrdstore = Ext.create('Proxmox.data.RRDStore', {
	    rrdurl: "/api2/json/nodes/" + nodename + "/rrddata",
	    model: 'pve-rrd-node',
	});
	let nodeStatus = Ext.create('PVE.node.StatusView', {
	    xtype: 'pveNodeStatus',
	    rstore: rstore,
	    width: 770,
	    pveSelNode: me.pveSelNode,
	});
	Ext.apply(me, {
	    tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' }],
	    nodeStatus: nodeStatus,
	    items: [
		{
		    xtype: 'container',
		    itemId: 'itemcontainer',
		    layout: 'column',
		    minWidth: 700,
		    defaults: {
			minHeight: 325,
			padding: 5,
			columnWidth: 1,
		    },
		    items: [
			nodeStatus,
			{
			    xtype: 'proxmoxRRDChart',
			    title: gettext('CPU usage'),
			    fields: ['cpu', 'iowait'],
			    fieldTitles: [gettext('CPU usage'), gettext('IO delay')],
			    unit: 'percent',
			    store: rrdstore,
			},
			{
			    xtype: 'proxmoxRRDChart',
			    title: gettext('Server load'),
			    fields: ['loadavg'],
			    fieldTitles: [gettext('Load average')],
			    store: rrdstore,
			},
			{
			    xtype: 'proxmoxRRDChart',
			    title: gettext('Memory usage'),
			    fields: ['memtotal', 'memused'],
			    fieldTitles: [gettext('Total'), gettext('RAM usage')],
			    unit: 'bytes',
			    powerOfTwo: true,
			    store: rrdstore,
			},
			{
			    xtype: 'proxmoxRRDChart',
			    title: gettext('Network traffic'),
			    fields: ['netin', 'netout'],
			    store: rrdstore,
			},
		    ],
		    listeners: {
			resize: function(panel) {
			    Proxmox.Utils.updateColumns(panel);
			},
		    },
		},
	    ],
	    listeners: {
		activate: function() {
		    rstore.setInterval(1000);
		    rstore.startUpdate(); // just to be sure
		    rrdstore.startUpdate();
		},
		destroy: function() {
		    rstore.setInterval(5000); // don't stop it, it's not ours!
		    rrdstore.stopUpdate();
		},
	    },
	});
	me.updateRepositoryStatus();
	me.callParent();
	let sp = Ext.state.Manager.getProvider();
	me.mon(sp, 'statechange', function(provider, key, value) {
	    if (key !== 'summarycolumns') {
		return;
	    }
	    Proxmox.Utils.updateColumns(me.getComponent('itemcontainer'));
	});
    },
});
Ext.define('PVE.node.CreateZFS', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveCreateZFS',
    onlineHelp: 'chapter_zfs',
    subject: 'ZFS',
    showProgress: true,
    isCreate: true,
    width: 800,
    viewModel: {
	data: {
	    raidLevel: 'single',
	},
	formulas: {
	    isDraid: get => get('raidLevel')?.startsWith("draid"),
	},
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	Ext.apply(me, {
	    url: `/nodes/${me.nodename}/disks/zfs`,
	    method: 'POST',
	    items: [
		{
		    xtype: 'inputpanel',
		    onGetValues: function(values) {
			if (values.draidData || values.draidSpares) {
			    let opt = { data: values.draidData, spares: values.draidSpares };
			    values['draid-config'] = PVE.Parser.printPropertyString(opt);
			}
			delete values.draidData;
			delete values.draidSpares;
			return values;
		    },
		    column1: [
			{
			    xtype: 'proxmoxtextfield',
			    name: 'name',
			    fieldLabel: gettext('Name'),
			    allowBlank: false,
			    maxLength: 128, // ZFS_MAX_DATASET_NAME_LEN is (256 - some edge case)
			    validator: v => {
				// see zpool_name_valid function in libzfs_zpool.c
				if (v.match(/^(mirror|raidz|draid|spare)/) || v === 'log') {
				    return gettext('Cannot use reserved pool name');
				} else if (!v.match(/^[a-zA-Z][a-zA-Z0-9\-_.]*$/)) {
				    // note: zfs would support also : and whitespace, but we don't
				    return gettext("Invalid characters in pool name");
				}
				return true;
			    },
			},
			{
			    xtype: 'proxmoxcheckbox',
			    name: 'add_storage',
			    fieldLabel: gettext('Add Storage'),
			    value: '1',
			},
		    ],
		    column2: [
			{
			    xtype: 'proxmoxKVComboBox',
			    fieldLabel: gettext('RAID Level'),
			    name: 'raidlevel',
			    value: 'single',
			    comboItems: [
				['single', gettext('Single Disk')],
				['mirror', 'Mirror'],
				['raid10', 'RAID10'],
				['raidz', 'RAIDZ'],
				['raidz2', 'RAIDZ2'],
				['raidz3', 'RAIDZ3'],
				['draid', 'dRAID'],
				['draid2', 'dRAID2'],
				['draid3', 'dRAID3'],
			    ],
			    bind: {
				value: '{raidLevel}',
			    },
			},
			{
			    xtype: 'proxmoxKVComboBox',
			    fieldLabel: gettext('Compression'),
			    name: 'compression',
			    value: 'on',
			    comboItems: [
				['on', 'on'],
				['off', 'off'],
				['gzip', 'gzip'],
				['lz4', 'lz4'],
				['lzjb', 'lzjb'],
				['zle', 'zle'],
				['zstd', 'zstd'],
			    ],
			},
			{
			    xtype: 'proxmoxintegerfield',
			    fieldLabel: gettext('ashift'),
			    minValue: 9,
			    maxValue: 16,
			    value: '12',
			    name: 'ashift',
			},
		    ],
		    columnB: [
			{
			    xtype: 'fieldset',
			    title: gettext('dRAID Config'),
			    collapsible: false,
			    bind: {
				hidden: '{!isDraid}',
			    },
			    layout: 'hbox',
			    padding: '5px 10px',
			    defaults: {
				flex: 1,
				layout: 'anchor',
			    },
			    items: [{
				xtype: 'proxmoxintegerfield',
				name: 'draidData',
				fieldLabel: gettext('Data Devs'),
				minValue: 1,
				allowBlank: false,
				disabled: true,
				hidden: true,
				bind: {
				    disabled: '{!isDraid}',
				    hidden: '{!isDraid}',
				},
				padding: '0 10 0 0',
			    },
			    {
				xtype: 'proxmoxintegerfield',
				name: 'draidSpares',
				fieldLabel: gettext('Spares'),
				minValue: 0,
				allowBlank: false,
				disabled: true,
				hidden: true,
				bind: {
				    disabled: '{!isDraid}',
				    hidden: '{!isDraid}',
				},
				padding: '0 0 0 10',
			    }],
			},
			{
			    xtype: 'pmxMultiDiskSelector',
			    name: 'devices',
			    nodename: me.nodename,
			    diskType: 'unused',
			    includePartitions: true,
			    height: 200,
			    emptyText: gettext('No Disks unused'),
			    itemId: 'disklist',
			},
		    ],
		},
		{
		    xtype: 'displayfield',
		    padding: '5 0 0 0',
		    userCls: 'pmx-hint',
		    value: 'Note: ZFS is not compatible with disks backed by a hardware ' +
		       'RAID controller. For details see the reference documentation.',
		},
	    ],
	});
        me.callParent();
    },
});
Ext.define('PVE.node.ZFSList', {
    extend: 'Ext.grid.Panel',
    xtype: 'pveZFSList',
    viewModel: {
	data: {
	    pool: '',
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	destroyPool: function() {
	    let me = this;
	    let vm = me.getViewModel();
	    let view = me.getView();
	    const pool = vm.get('pool');
	    if (!view.nodename) {
		throw "no node name specified";
	    }
	    if (!pool) {
		throw "no pool specified";
	    }
	    Ext.create('PVE.window.SafeDestroyStorage', {
		url: `/nodes/${view.nodename}/disks/zfs/${pool}`,
		item: { id: pool },
		taskName: 'zfsremove',
		taskDone: () => { view.reload(); },
	    }).show();
	},
    },
    stateful: true,
    stateId: 'grid-node-zfs',
    columns: [
	{
	    text: gettext('Name'),
	    dataIndex: 'name',
	    flex: 1,
	},
	{
	    header: gettext('Size'),
	    renderer: Proxmox.Utils.format_size,
	    dataIndex: 'size',
	},
	{
	    header: gettext('Free'),
	    renderer: Proxmox.Utils.format_size,
	    dataIndex: 'free',
	},
	{
	    header: gettext('Allocated'),
	    renderer: Proxmox.Utils.format_size,
	    dataIndex: 'alloc',
	},
	{
	    header: gettext('Fragmentation'),
	    renderer: function(value) {
		return value.toString() + '%';
	    },
	    dataIndex: 'frag',
	},
	{
	    header: gettext('Health'),
	    renderer: PVE.Utils.render_zfs_health,
	    dataIndex: 'health',
	},
	{
	    header: gettext('Deduplication'),
	    hidden: true,
	    renderer: function(value) {
		return value.toFixed(2).toString() + 'x';
	    },
	    dataIndex: 'dedup',
	},
    ],
    rootVisible: false,
    useArrows: true,
    tbar: [
	{
	    text: gettext('Reload'),
	    iconCls: 'fa fa-refresh',
	    handler: function() {
		this.up('panel').reload();
	    },
	},
	{
	    text: gettext('Create') + ': ZFS',
	    handler: function() {
		let view = this.up('panel');
		Ext.create('PVE.node.CreateZFS', {
		    nodename: view.nodename,
		    listeners: {
			destroy: () => view.reload(),
		    },
		    autoShow: true,
		});
	    },
	},
	{
	    text: gettext('Detail'),
	    itemId: 'detailbtn',
	    disabled: true,
	    handler: function() {
		let view = this.up('panel');
		let selection = view.getSelection();
		if (selection.length) {
		    view.show_detail(selection[0].get('name'));
		}
	    },
	},
	'->',
	{
	    xtype: 'tbtext',
	    data: {
		pool: undefined,
	    },
	    bind: {
		data: {
		    pool: "{pool}",
		},
	    },
	    tpl: [
		'',
		'Pool {pool}:',
		'',
		Ext.String.format(gettext('No {0} selected'), 'pool'),
		'',
	    ],
	},
	{
	    text: gettext('More'),
	    iconCls: 'fa fa-bars',
	    disabled: true,
	    bind: {
		disabled: '{!pool}',
	    },
	    menu: [
		{
		    text: gettext('Destroy'),
		    itemId: 'remove',
		    iconCls: 'fa fa-fw fa-trash-o',
		    handler: 'destroyPool',
		    disabled: true,
		    bind: {
			disabled: '{!pool}',
		    },
		},
	    ],
	},
    ],
    show_detail: function(zpool) {
	let me = this;
	Ext.create('Proxmox.window.ZFSDetail', {
	    zpool,
	    nodename: me.nodename,
	}).show();
    },
    set_button_status: function() {
	var me = this;
    },
    reload: function() {
	var me = this;
	me.store.load();
	me.store.sort();
    },
    listeners: {
	activate: function() {
	    this.reload();
	},
	selectionchange: function(model, selected) {
	    let me = this;
	    let vm = me.getViewModel();
	    me.down('#detailbtn').setDisabled(selected.length === 0);
	    vm.set('pool', selected[0]?.data.name || '');
	},
	itemdblclick: function(grid, record) {
	    this.show_detail(record.get('name'));
	},
    },
    initComponent: function() {
	let me = this;
	me.nodename = me.pveSelNode.data.node;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	Ext.apply(me, {
	    store: {
		fields: ['name', 'size', 'free', 'alloc', 'dedup', 'frag', 'health'],
		proxy: {
		    type: 'proxmox',
		    url: `/api2/json/nodes/${me.nodename}/disks/zfs`,
		},
		sorters: 'name',
	    },
	});
	me.callParent();
	Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
	me.reload();
    },
});
Ext.define('Proxmox.node.NodeOptionsView', {
    extend: 'Proxmox.grid.ObjectGrid',
    alias: ['widget.proxmoxNodeOptionsView'],
    mixins: ['Proxmox.Mixin.CBind'],
    cbindData: function(_initialconfig) {
	let me = this;
	let baseUrl = `/nodes/${me.nodename}/config`;
	me.url = `/api2/json${baseUrl}`;
	me.editorConfig = {
	    url: `/api2/extjs/${baseUrl}`,
	};
	return {};
    },
    listeners: {
	itemdblclick: function() { this.run_editor(); },
	activate: function() { this.rstore.startUpdate(); },
	destroy: function() { this.rstore.stopUpdate(); },
	deactivate: function() { this.rstore.stopUpdate(); },
    },
    tbar: [
	{
	    text: gettext('Edit'),
	    xtype: 'proxmoxButton',
	    disabled: true,
	    handler: btn => btn.up('grid').run_editor(),
	},
    ],
    gridRows: [
	{
	    xtype: 'integer',
	    name: 'startall-onboot-delay',
	    text: gettext('Start on boot delay'),
	    minValue: 0,
	    maxValue: 300,
	    labelWidth: 130,
	    deleteEmpty: true,
	    renderer: function(value) {
		if (value === undefined) {
		    return Proxmox.Utils.defaultText;
		}
		let secString = value === '1' ? gettext('Second') : gettext('Seconds');
		return `${value} ${secString}`;
	    },
	},
	{
	    xtype: 'text',
	    name: 'wakeonlan',
	    text: gettext('MAC address for Wake on LAN'),
	    vtype: 'MacAddress',
	    labelWidth: 150,
	    deleteEmpty: true,
	    renderer: function(value) {
		if (value === undefined) {
		    return Proxmox.Utils.NoneText;
		}
		return value;
	    },
	},
    ],
});
Ext.define('PVE.pool.Config', {
    extend: 'PVE.panel.Config',
    alias: 'widget.pvePoolConfig',
    onlineHelp: 'pveum_pools',
    initComponent: function() {
        var me = this;
	var pool = me.pveSelNode.data.pool;
	if (!pool) {
	    throw "no pool specified";
	}
	Ext.apply(me, {
	    title: Ext.String.format(gettext("Resource Pool") + ': ' + pool),
	    hstateid: 'pooltab',
	    items: [
		{
		    title: gettext('Summary'),
		    iconCls: 'fa fa-book',
		    xtype: 'pvePoolSummary',
		    itemId: 'summary',
		},
		{
		    title: gettext('Members'),
		    xtype: 'pvePoolMembers',
		    iconCls: 'fa fa-th',
		    pool: pool,
		    itemId: 'members',
		},
		{
		    xtype: 'pveACLView',
		    title: gettext('Permissions'),
		    iconCls: 'fa fa-unlock',
		    itemId: 'permissions',
		    path: '/pool/' + pool,
		},
	    ],
	});
	me.callParent();
   },
});
Ext.define('PVE.pool.StatusView', {
    extend: 'Proxmox.grid.ObjectGrid',
    alias: ['widget.pvePoolStatusView'],
    disabled: true,
    title: gettext('Status'),
    cwidth1: 150,
    interval: 30000,
    //height: 195,
    initComponent: function() {
	var me = this;
	var pool = me.pveSelNode.data.pool;
	if (!pool) {
	    throw "no pool specified";
	}
	var rows = {
	    comment: {
		header: gettext('Comment'),
		renderer: Ext.String.htmlEncode,
		required: true,
	    },
	};
	Ext.apply(me, {
	    url: "/api2/json/pools/" + pool,
	    rows: rows,
	});
	me.callParent();
    },
});
Ext.define('PVE.pool.Summary', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pvePoolSummary',
    initComponent: function() {
        var me = this;
	var pool = me.pveSelNode.data.pool;
	if (!pool) {
	    throw "no pool specified";
	}
	var statusview = Ext.create('PVE.pool.StatusView', {
	    pveSelNode: me.pveSelNode,
	    style: 'padding-top:0px',
	});
	var rstore = statusview.rstore;
	Ext.apply(me, {
	    autoScroll: true,
	    bodyStyle: 'padding:10px',
	    defaults: {
		style: 'padding-top:10px',
		width: 800,
	    },
	    items: [statusview],
	});
	me.on('activate', rstore.startUpdate);
	me.on('destroy', rstore.stopUpdate);
	me.callParent();
    },
});
Ext.define('PVE.window.IPInfo', {
    extend: 'Ext.window.Window',
    width: 600,
    title: gettext('Guest Agent Network Information'),
    height: 300,
    layout: {
	type: 'fit',
    },
    modal: true,
    items: [
	{
	    xtype: 'grid',
	    store: {},
	    emptyText: gettext('No network information'),
	    columns: [
		{
		    dataIndex: 'name',
		    text: gettext('Name'),
		    flex: 3,
		},
		{
		    dataIndex: 'hardware-address',
		    text: gettext('MAC address'),
		    width: 140,
		},
		{
		    dataIndex: 'ip-addresses',
		    text: gettext('IP address'),
		    align: 'right',
		    flex: 4,
		    renderer: function(val) {
			if (!Ext.isArray(val)) {
			    return '';
			}
			var ips = [];
			val.forEach(function(ip) {
			    var addr = ip['ip-address'];
			    var pref = ip.prefix;
			    if (addr && pref) {
				ips.push(addr + '/' + pref);
			    }
			});
			return ips.join('
');
		    },
		},
	    ],
	},
    ],
});
Ext.define('PVE.qemu.AgentIPView', {
    extend: 'Ext.container.Container',
    xtype: 'pveAgentIPView',
    layout: {
	type: 'hbox',
	align: 'top',
    },
    nics: [],
    items: [
	{
	    xtype: 'box',
	    html: ' IPs',
	},
	{
	    xtype: 'container',
	    flex: 1,
	    layout: {
		type: 'vbox',
		align: 'right',
		pack: 'end',
	    },
	    items: [
		{
		    xtype: 'label',
		    flex: 1,
		    itemId: 'ipBox',
		    style: {
			'text-align': 'right',
		    },
		},
		{
		    xtype: 'button',
		    itemId: 'moreBtn',
		    hidden: true,
		    ui: 'default-toolbar',
		    handler: function(btn) {
			let view = this.up('pveAgentIPView');
			var win = Ext.create('PVE.window.IPInfo');
			win.down('grid').getStore().setData(view.nics);
			win.show();
		    },
		    text: gettext('More'),
		},
	    ],
	},
    ],
    getDefaultIps: function(nics) {
	var me = this;
	var ips = [];
	nics.forEach(function(nic) {
	    if (nic['hardware-address'] &&
		nic['hardware-address'] !== '00:00:00:00:00:00' &&
		nic['hardware-address'] !== '0:0:0:0:0:0') {
		var nic_ips = nic['ip-addresses'] || [];
		nic_ips.forEach(function(ip) {
		    var p = ip['ip-address'];
		    // show 2 ips at maximum
		    if (ips.length < 2) {
			ips.push(p);
		    }
		});
	    }
	});
	return ips;
    },
    startIPStore: function(store, records, success) {
	var me = this;
	let agentRec = store.getById('agent');
	let state = store.getById('status');
	me.agent = agentRec && agentRec.data.value === 1;
	me.running = state && state.data.value === 'running';
	var caps = Ext.state.Manager.get('GuiCap');
	if (!caps.vms['VM.Monitor']) {
	    var errorText = gettext("Requires '{0}' Privileges");
	    me.updateStatus(false, Ext.String.format(errorText, 'VM.Monitor'));
	    return;
	}
	if (me.agent && me.running && me.ipStore.isStopped) {
	    me.ipStore.startUpdate();
	} else if (me.ipStore.isStopped) {
	    me.updateStatus();
	}
    },
    updateStatus: function(unsuccessful, defaulttext) {
	var me = this;
	var text = defaulttext || gettext('No network information');
	var more = false;
	if (unsuccessful) {
	    text = gettext('Guest Agent not running');
	} else if (me.agent && me.running) {
	    if (Ext.isArray(me.nics) && me.nics.length) {
		more = true;
		var ips = me.getDefaultIps(me.nics);
		if (ips.length !== 0) {
		    text = ips.join('
');
		}
	    } else if (me.nics && me.nics.error) {
		text = Ext.String.format(text, me.nics.error.desc);
	    }
	} else if (me.agent) {
	    text = gettext('Guest Agent not running');
	} else {
	    text = gettext('No Guest Agent configured');
	}
	var ipBox = me.down('#ipBox');
	ipBox.update(text);
	var moreBtn = me.down('#moreBtn');
	moreBtn.setVisible(more);
    },
    initComponent: function() {
	var me = this;
	if (!me.rstore) {
	    throw 'rstore not given';
	}
	if (!me.pveSelNode) {
	    throw 'pveSelNode not given';
	}
	var nodename = me.pveSelNode.data.node;
	var vmid = me.pveSelNode.data.vmid;
	me.ipStore = Ext.create('Proxmox.data.UpdateStore', {
	    interval: 10000,
	    storeid: 'pve-qemu-agent-' + vmid,
	    method: 'POST',
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/nodes/' + nodename + '/qemu/' + vmid + '/agent/network-get-interfaces',
	    },
	});
	me.callParent();
	me.mon(me.ipStore, 'load', function(store, records, success) {
	    if (records && records.length) {
		me.nics = records[0].data.result;
	    } else {
		me.nics = undefined;
	    }
	    me.updateStatus(!success);
	});
	me.on('destroy', me.ipStore.stopUpdate, me.ipStore);
	// if we already have info about the vm, use it immediately
	if (me.rstore.getCount()) {
	    me.startIPStore(me.rstore, me.rstore.getData(), false);
	}
	// check if the guest agent is there on every statusstore load
	me.mon(me.rstore, 'load', me.startIPStore, me);
    },
});
Ext.define('PVE.qemu.AudioInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveAudioInputPanel',
    // FIXME: enable once we bumped doc-gen so this ref is included
    //onlineHelp: 'qm_audio_device',
    onGetValues: function(values) {
	var ret = PVE.Parser.printPropertyString(values);
	if (ret === '') {
	    return {
		'delete': 'audio0',
	    };
	}
	return {
	    audio0: ret,
	};
    },
    items: [{
	name: 'device',
	xtype: 'proxmoxKVComboBox',
	value: 'ich9-intel-hda',
	fieldLabel: gettext('Audio Device'),
	comboItems: [
	    ['ich9-intel-hda', 'ich9-intel-hda'],
	    ['intel-hda', 'intel-hda'],
	    ['AC97', 'AC97'],
	],
    }, {
	name: 'driver',
	xtype: 'proxmoxKVComboBox',
	value: 'spice',
	fieldLabel: gettext('Backend Driver'),
	comboItems: [
	    ['spice', 'SPICE'],
	    ['none', `${Proxmox.Utils.NoneText} (${gettext('Dummy Device')})`],
	],
    }],
});
Ext.define('PVE.qemu.AudioEdit', {
    extend: 'Proxmox.window.Edit',
    vmconfig: undefined,
    subject: gettext('Audio Device'),
    items: [{
	xtype: 'pveAudioInputPanel',
    }],
    initComponent: function() {
	var me = this;
	me.callParent();
	me.load({
	    success: function(response) {
		me.vmconfig = response.result.data;
		var audio0 = me.vmconfig.audio0;
		if (audio0) {
		    me.setValues(PVE.Parser.parsePropertyString(audio0));
		}
	    },
	});
    },
});
Ext.define('pve-boot-order-entry', {
    extend: 'Ext.data.Model',
    fields: [
	{ name: 'name', type: 'string' },
	{ name: 'enabled', type: 'bool' },
	{ name: 'desc', type: 'string' },
    ],
});
Ext.define('PVE.qemu.BootOrderPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveQemuBootOrderPanel',
    onlineHelp: 'qm_bootorder',
    vmconfig: {}, // store loaded vm config
    store: undefined,
    inUpdate: false,
    controller: {
	xclass: 'Ext.app.ViewController',
	init: function(view) {
	    let me = this;
	    let grid = me.lookup('grid');
	    let marker = me.lookup('marker');
	    let emptyWarning = me.lookup('emptyWarning');
	    marker.originalValue = undefined;
	    view.store = Ext.create('Ext.data.Store', {
		model: 'pve-boot-order-entry',
		listeners: {
		    update: function() {
			this.commitChanges();
			let val = view.calculateValue();
			if (marker.originalValue === undefined) {
			    marker.originalValue = val;
			}
			view.inUpdate = true;
			marker.setValue(val);
			view.inUpdate = false;
			marker.checkDirty();
			emptyWarning.setHidden(val !== '');
			grid.getView().refresh();
		    },
		},
	    });
	    grid.setStore(view.store);
	},
    },
    isCloudinit: (v) => v.match(/media=cdrom/) && v.match(/[:/]vm-\d+-cloudinit/),
    isDisk: function(value) {
	return PVE.Utils.bus_match.test(value);
    },
    isBootdev: function(dev, value) {
	return (this.isDisk(dev) && !this.isCloudinit(value)) ||
	    (/^net\d+/).test(dev) ||
	    (/^hostpci\d+/).test(dev) ||
	    ((/^usb\d+/).test(dev) && !(/spice/).test(value));
    },
    setVMConfig: function(vmconfig) {
	let me = this;
	me.vmconfig = vmconfig;
	me.store.removeAll();
	let boot = PVE.Parser.parsePropertyString(me.vmconfig.boot, "legacy");
	let bootorder = [];
	if (boot.order) {
	    bootorder = boot.order.split(';').map(dev => ({ name: dev, enabled: true }));
	} else if (!(/^\s*$/).test(me.vmconfig.boot)) {
	    // legacy style, transform to new bootorder
	    let order = boot.legacy || 'cdn';
	    let bootdisk = me.vmconfig.bootdisk || undefined;
	    // get the first 4 characters (acdn)
	    // ignore the rest (there should never be more than 4)
	    let orderList = order.split('').slice(0, 4);
	    // build bootdev list
	    for (let i = 0; i < orderList.length; i++) {
		let list = [];
		if (orderList[i] === 'c') {
		    if (bootdisk !== undefined && me.vmconfig[bootdisk]) {
			list.push(bootdisk);
		    }
		} else if (orderList[i] === 'd') {
		    Ext.Object.each(me.vmconfig, function(key, value) {
			if (me.isDisk(key) && value.match(/media=cdrom/) && !me.isCloudinit(value)) {
			    list.push(key);
			}
		    });
		} else if (orderList[i] === 'n') {
		    Ext.Object.each(me.vmconfig, function(key, value) {
			if ((/^net\d+/).test(key)) {
			    list.push(key);
			}
		    });
		}
		// Object.each iterates in random order, sort alphabetically
		list.sort();
		list.forEach(dev => bootorder.push({ name: dev, enabled: true }));
	    }
	}
	// add disabled devices as well
	let disabled = [];
	Ext.Object.each(me.vmconfig, function(key, value) {
	    if (me.isBootdev(key, value) &&
		!Ext.Array.some(bootorder, x => x.name === key)) {
		disabled.push(key);
	    }
	});
	disabled.sort();
	disabled.forEach(dev => bootorder.push({ name: dev, enabled: false }));
	// add descriptions
	bootorder.forEach(entry => {
	    entry.desc = me.vmconfig[entry.name];
	});
	me.store.insert(0, bootorder);
	me.store.fireEvent("update");
    },
    calculateValue: function() {
	let me = this;
	return me.store.getData().items
	    .filter(x => x.data.enabled)
	    .map(x => x.data.name)
	    .join(';');
    },
    onGetValues: function() {
	let me = this;
	// Note: we allow an empty value, so no 'delete' option
	let val = { order: me.calculateValue() };
	let res = { boot: PVE.Parser.printPropertyString(val) };
	return res;
    },
    items: [
	{
	    xtype: 'grid',
	    reference: 'grid',
	    margin: '0 0 5 0',
	    minHeight: 150,
	    defaults: {
		sortable: false,
		hideable: false,
		draggable: false,
	    },
	    columns: [
		{
		    header: '#',
		    flex: 4,
		    renderer: (value, metaData, record, rowIndex) => {
			let dragHandle = "";
			let idx = (rowIndex + 1).toString();
			if (record.get('enabled')) {
			    return dragHandle + idx;
			} else {
			    return dragHandle + "" + idx + "";
			}
		    },
		},
		{
		    xtype: 'checkcolumn',
		    header: gettext('Enabled'),
		    dataIndex: 'enabled',
		    flex: 4,
		},
		{
		    header: gettext('Device'),
		    dataIndex: 'name',
		    flex: 6,
		    renderer: (value, metaData, record, rowIndex) => {
			let desc = record.get('desc');
			let icon = '', iconCls;
			if (value.match(/^net\d+$/)) {
			    iconCls = 'exchange';
			} else if (desc.match(/media=cdrom/)) {
			    metaData.tdCls = 'pve-itype-icon-cdrom';
			} else {
			    iconCls = 'hdd-o';
			}
			if (iconCls !== undefined) {
			    metaData.tdCls += 'pve-itype-fa';
			    icon = ``;
			}
			return icon + value;
		    },
		},
		{
		    header: gettext('Description'),
		    dataIndex: 'desc',
		    flex: 20,
		},
	    ],
	    viewConfig: {
		plugins: {
		    ptype: 'gridviewdragdrop',
		    dragText: gettext('Drag and drop to reorder'),
		},
	    },
	    listeners: {
		drop: function() {
		    // doesn't fire automatically on reorder
		    this.getStore().fireEvent("update");
		},
	    },
	},
	{
	    xtype: 'component',
	    html: gettext('Drag and drop to reorder'),
	},
	{
	    xtype: 'displayfield',
	    reference: 'emptyWarning',
	    userCls: 'pmx-hint',
	    value: gettext('Warning: No devices selected, the VM will probably not boot!'),
	},
	{
	    // for dirty marking and 'reset' function
	    xtype: 'field',
	    reference: 'marker',
	    hidden: true,
	    setValue: function(val) {
		let me = this;
		let panel = me.up('pveQemuBootOrderPanel');
		// on form reset, go back to original state
		if (!panel.inUpdate) {
		    panel.setVMConfig(panel.vmconfig);
		}
		// not a subclass, so no callParent; just do it manually
		me.setRawValue(me.valueToRaw(val));
		return me.mixins.field.setValue.call(me, val);
	    },
	},
    ],
});
Ext.define('PVE.qemu.BootOrderEdit', {
    extend: 'Proxmox.window.Edit',
    items: [{
	xtype: 'pveQemuBootOrderPanel',
	itemId: 'inputpanel',
    }],
    subject: gettext('Boot Order'),
    width: 640,
    initComponent: function() {
	let me = this;
	me.callParent();
	me.load({
	    success: ({ result }) => me.down('#inputpanel').setVMConfig(result.data),
	});
    },
});
Ext.define('PVE.qemu.CDInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveQemuCDInputPanel',
    insideWizard: false,
    onGetValues: function(values) {
	var me = this;
	var confid = me.confid || values.controller + values.deviceid;
	me.drive.media = 'cdrom';
	if (values.mediaType === 'iso') {
	    me.drive.file = values.cdimage;
	} else if (values.mediaType === 'cdrom') {
	    me.drive.file = 'cdrom';
	} else {
	    me.drive.file = 'none';
	}
	var params = {};
	params[confid] = PVE.Parser.printQemuDrive(me.drive);
	return params;
    },
    setVMConfig: function(vmconfig) {
	var me = this;
	if (me.bussel) {
	    me.bussel.setVMConfig(vmconfig, 'cdrom');
	}
    },
    setDrive: function(drive) {
	var me = this;
	var values = {};
	if (drive.file === 'cdrom') {
	    values.mediaType = 'cdrom';
	} else if (drive.file === 'none') {
	    values.mediaType = 'none';
	} else {
	    values.mediaType = 'iso';
	    var match = drive.file.match(/^([^:]+):/);
	    if (match) {
		values.cdstorage = match[1];
		values.cdimage = drive.file;
	    }
	}
	me.drive = drive;
	me.setValues(values);
    },
    setNodename: function(nodename) {
	var me = this;
	me.cdstoragesel.setNodename(nodename);
	me.cdfilesel.setStorage(undefined, nodename);
    },
    initComponent: function() {
	var me = this;
	me.drive = {};
	var items = [];
	if (!me.confid) {
	    me.bussel = Ext.create('PVE.form.ControllerSelector', {
		withVirtIO: false,
	    });
	    items.push(me.bussel);
	}
	items.push({
	    xtype: 'radiofield',
	    name: 'mediaType',
	    inputValue: 'iso',
	    boxLabel: gettext('Use CD/DVD disc image file (iso)'),
	    checked: true,
	    listeners: {
		change: function(f, value) {
		    if (!me.rendered) {
			return;
		    }
		    me.down('field[name=cdstorage]').setDisabled(!value);
		    var cdImageField = me.down('field[name=cdimage]');
		    cdImageField.setDisabled(!value);
		    if (value) {
			cdImageField.validate();
		    } else {
			cdImageField.reset();
		    }
		},
	    },
	});
	me.cdfilesel = Ext.create('PVE.form.FileSelector', {
	    name: 'cdimage',
	    nodename: me.nodename,
	    storageContent: 'iso',
	    fieldLabel: gettext('ISO image'),
	    labelAlign: 'right',
	    allowBlank: false,
	});
	me.cdstoragesel = Ext.create('PVE.form.StorageSelector', {
	    name: 'cdstorage',
	    nodename: me.nodename,
	    fieldLabel: gettext('Storage'),
	    labelAlign: 'right',
	    storageContent: 'iso',
	    allowBlank: false,
	    autoSelect: me.insideWizard,
	    listeners: {
		change: function(f, value) {
		    me.cdfilesel.setStorage(value);
		},
	    },
	});
	items.push(me.cdstoragesel);
	items.push(me.cdfilesel);
	items.push({
	    xtype: 'radiofield',
	    name: 'mediaType',
	    inputValue: 'cdrom',
	    boxLabel: gettext('Use physical CD/DVD Drive'),
	});
	items.push({
	    xtype: 'radiofield',
	    name: 'mediaType',
	    inputValue: 'none',
	    boxLabel: gettext('Do not use any media'),
	});
	me.items = items;
	me.callParent();
    },
});
Ext.define('PVE.qemu.CDEdit', {
    extend: 'Proxmox.window.Edit',
    width: 400,
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	me.isCreate = !me.confid;
	var ipanel = Ext.create('PVE.qemu.CDInputPanel', {
	    confid: me.confid,
	    nodename: nodename,
	});
	Ext.applyIf(me, {
	    subject: 'CD/DVD Drive',
	    items: [ipanel],
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		ipanel.setVMConfig(response.result.data);
		if (me.confid) {
		    var value = response.result.data[me.confid];
		    var drive = PVE.Parser.parseQemuDrive(me.confid, value);
		    if (!drive) {
			Ext.Msg.alert('Error', 'Unable to parse drive options');
			me.close();
			return;
		    }
		    ipanel.setDrive(drive);
		}
	    },
	});
    },
});
Ext.define('PVE.qemu.CIDriveInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveCIDriveInputPanel',
    insideWizard: false,
    vmconfig: {}, // used to select usused disks
    onGetValues: function(values) {
	var me = this;
	var drive = {};
	var params = {};
	drive.file = values.hdstorage + ":cloudinit";
	drive.format = values.diskformat;
	params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive);
	return params;
    },
    setNodename: function(nodename) {
	var me = this;
	me.down('#hdstorage').setNodename(nodename);
	me.down('#hdimage').setStorage(undefined, nodename);
    },
    setVMConfig: function(config) {
	var me = this;
	me.down('#drive').setVMConfig(config, 'cdrom');
    },
    initComponent: function() {
	var me = this;
	me.drive = {};
	me.items = [
	    {
		xtype: 'pveControllerSelector',
		withVirtIO: false,
		itemId: 'drive',
		fieldLabel: gettext('CloudInit Drive'),
		name: 'drive',
	    },
	    {
		xtype: 'pveDiskStorageSelector',
		itemId: 'storselector',
		storageContent: 'images',
		nodename: me.nodename,
		hideSize: true,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.qemu.CIDriveEdit', {
    extend: 'Proxmox.window.Edit',
    xtype: 'pveCIDriveEdit',
    isCreate: true,
    subject: gettext('CloudInit Drive'),
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	me.items = [{
	    xtype: 'pveCIDriveInputPanel',
	    itemId: 'cipanel',
	    nodename: nodename,
	}];
	me.callParent();
	me.load({
	    success: function(response, opts) {
		me.down('#cipanel').setVMConfig(response.result.data);
	    },
	});
    },
});
Ext.define('PVE.qemu.CloudInit', {
    extend: 'Proxmox.grid.PendingObjectGrid',
    xtype: 'pveCiPanel',
    onlineHelp: 'qm_cloud_init',
    tbar: [
	{
	    xtype: 'proxmoxButton',
	    disabled: true,
	    dangerous: true,
	    confirmMsg: function(rec) {
		let view = this.up('grid');
		var warn = gettext('Are you sure you want to remove entry {0}');
		var entry = rec.data.key;
		var msg = Ext.String.format(warn, "'"
		    + view.renderKey(entry, {}, rec) + "'");
		return msg;
	    },
	    enableFn: function(record) {
		let view = this.up('grid');
		var caps = Ext.state.Manager.get('GuiCap');
		if (view.rows[record.data.key].never_delete ||
		    !caps.vms['VM.Config.Network']) {
		    return false;
		}
		if (record.data.key === 'cipassword' && !record.data.value) {
		    return false;
		}
		return true;
	    },
	    handler: function() {
		let view = this.up('grid');
		let records = view.getSelection();
		if (!records || !records.length) {
		    return;
		}
		var id = records[0].data.key;
		var match = id.match(/^net(\d+)$/);
		if (match) {
		    id = 'ipconfig' + match[1];
		}
		var params = {};
		params.delete = id;
		Proxmox.Utils.API2Request({
		    url: view.baseurl + '/config',
		    waitMsgTarget: view,
		    method: 'PUT',
		    params: params,
		    failure: function(response, opts) {
			Ext.Msg.alert('Error', response.htmlStatus);
		    },
		    callback: function() {
			view.reload();
		    },
		});
	    },
	    text: gettext('Remove'),
	},
	{
	    xtype: 'proxmoxButton',
	    disabled: true,
	    enableFn: function(rec) {
		let view = this.up('pveCiPanel');
		return !!view.rows[rec.data.key].editor;
	    },
	    handler: function() {
		let view = this.up('grid');
		view.run_editor();
	    },
	    text: gettext('Edit'),
	},
	'-',
	{
	    xtype: 'button',
	    itemId: 'savebtn',
	    text: gettext('Regenerate Image'),
	    handler: function() {
		let view = this.up('grid');
		var eject_params = {};
		var insert_params = {};
		let disk = PVE.Parser.parseQemuDrive(view.ciDriveId, view.ciDrive);
		var storage = '';
		var stormatch = disk.file.match(/^([^:]+):/);
		if (stormatch) {
		    storage = stormatch[1];
		}
		eject_params[view.ciDriveId] = 'none,media=cdrom';
		insert_params[view.ciDriveId] = storage + ':cloudinit';
		var failure = function(response, opts) {
		    Ext.Msg.alert('Error', response.htmlStatus);
		};
		Proxmox.Utils.API2Request({
		    url: view.baseurl + '/config',
		    waitMsgTarget: view,
		    method: 'PUT',
		    params: eject_params,
		    failure: failure,
		    callback: function() {
			Proxmox.Utils.API2Request({
			    url: view.baseurl + '/config',
			    waitMsgTarget: view,
			    method: 'PUT',
			    params: insert_params,
			    failure: failure,
			    callback: function() {
				view.reload();
			    },
			});
		    },
		});
	    },
	},
    ],
    border: false,
    set_button_status: function(rstore, records, success) {
	if (!success || records.length < 1) {
	    return;
	}
	var me = this;
	var found;
	records.forEach(function(record) {
	    if (found) {
		return;
	    }
	    var id = record.data.key;
	    var value = record.data.value;
	    var ciregex = new RegExp("vm-" + me.pveSelNode.data.vmid + "-cloudinit");
		if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) {
		    found = id;
		    me.ciDriveId = found;
		    me.ciDrive = value;
		}
	});
	me.down('#savebtn').setDisabled(!found);
	me.setDisabled(!found);
	if (!found) {
	    me.getView().mask(gettext('No CloudInit Drive found'), ['pve-static-mask']);
	} else {
	    me.getView().unmask();
	}
    },
    renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
	var me = this;
	var rows = me.rows;
	var rowdef = rows[key] || {};
	var icon = "";
	if (rowdef.iconCls) {
	    icon = ' ';
	}
	return icon + (rowdef.header || key);
    },
    listeners: {
	activate: function() {
	    var me = this;
	    me.rstore.startUpdate();
	},
	itemdblclick: function() {
	    var me = this;
	    me.run_editor();
	},
    },
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = me.pveSelNode.data.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	var caps = Ext.state.Manager.get('GuiCap');
	me.baseurl = '/api2/extjs/nodes/' + nodename + '/qemu/' + vmid;
	me.url = me.baseurl + '/pending';
	me.editorConfig.url = me.baseurl + '/config';
	me.editorConfig.pveSelNode = me.pveSelNode;
	let caps_ci = caps.vms['VM.Config.Cloudinit'] || caps.vms['VM.Config.Network'];
	/* editor is string and object */
	me.rows = {
	    ciuser: {
		header: gettext('User'),
		iconCls: 'fa fa-user',
		never_delete: true,
		defaultValue: '',
		editor: caps_ci ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('User'),
		    items: [
			{
			    xtype: 'proxmoxtextfield',
			    deleteEmpty: true,
			    emptyText: Proxmox.Utils.defaultText,
			    fieldLabel: gettext('User'),
			    name: 'ciuser',
			},
		    ],
		} : undefined,
		renderer: function(value) {
		    return value || Proxmox.Utils.defaultText;
		},
	    },
	    cipassword: {
		header: gettext('Password'),
		iconCls: 'fa fa-unlock',
		defaultValue: '',
		editor: caps_ci ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Password'),
		    items: [
			{
			    xtype: 'proxmoxtextfield',
			    inputType: 'password',
			    deleteEmpty: true,
			    emptyText: Proxmox.Utils.noneText,
			    fieldLabel: gettext('Password'),
			    name: 'cipassword',
			},
		    ],
		} : undefined,
		renderer: function(value) {
		    return value || Proxmox.Utils.noneText;
		},
	    },
	    searchdomain: {
		header: gettext('DNS domain'),
		iconCls: 'fa fa-globe',
		editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
		never_delete: true,
		defaultValue: gettext('use host settings'),
	    },
	    nameserver: {
		header: gettext('DNS servers'),
		iconCls: 'fa fa-globe',
		editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
		never_delete: true,
		defaultValue: gettext('use host settings'),
	    },
	    sshkeys: {
		header: gettext('SSH public key'),
		iconCls: 'fa fa-key',
		editor: caps_ci ? 'PVE.qemu.SSHKeyEdit' : undefined,
		never_delete: true,
		renderer: function(value) {
		    value = decodeURIComponent(value);
		    var keys = value.split('\n');
		    var text = [];
		    keys.forEach(function(key) {
			if (key.length) {
			    let res = PVE.Parser.parseSSHKey(key);
			    if (res) {
				key = Ext.String.htmlEncode(res.comment);
				if (res.options) {
				    key += ' (' + gettext('with options') + ')';
				}
				text.push(key);
				return;
			    }
			    // Most likely invalid at this point, so just stick to
			    // the old value.
			    text.push(Ext.String.htmlEncode(key));
			}
		    });
		    if (text.length) {
			return text.join('
');
		    } else {
			return Proxmox.Utils.noneText;
		    }
		},
		defaultValue: '',
	    },
	};
	var i;
	var ipconfig_renderer = function(value, md, record, ri, ci, store, pending) {
	    var id = record.data.key;
	    var match = id.match(/^net(\d+)$/);
	    var val = '';
	    if (match) {
		val = me.getObjectValue('ipconfig'+match[1], '', pending);
	    }
	    return val;
	};
	for (i = 0; i < 32; i++) {
	    // we want to show an entry for every network device
	    // even if it is empty
	    me.rows['net' + i.toString()] = {
		multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()],
		header: gettext('IP Config') + ' (net' + i.toString() +')',
		editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined,
		iconCls: 'fa fa-exchange',
		renderer: ipconfig_renderer,
	    };
	    me.rows['ipconfig' + i.toString()] = {
		visible: false,
	    };
	}
	PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function(type, id) {
	    me.rows[type+id] = {
		visible: false,
	    };
	});
	me.callParent();
	me.mon(me.rstore, 'load', me.set_button_status, me);
    },
});
Ext.define('PVE.qemu.CmdMenu', {
    extend: 'Ext.menu.Menu',
    showSeparator: false,
    initComponent: function() {
	let me = this;
	let info = me.pveSelNode.data;
	if (!info.node) {
	    throw "no node name specified";
	}
	if (!info.vmid) {
	    throw "no VM ID specified";
	}
	let vm_command = function(cmd, params) {
	    Proxmox.Utils.API2Request({
		params: params,
		url: `/nodes/${info.node}/${info.type}/${info.vmid}/status/${cmd}`,
		method: 'POST',
		failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
	    });
	};
	let confirmedVMCommand = (cmd, params, confirmTask) => {
	    let task = confirmTask || `qm${cmd}`;
	    let msg = Proxmox.Utils.format_task_description(task, info.vmid);
	    Ext.Msg.confirm(gettext('Confirm'), msg, btn => {
		if (btn === 'yes') {
		    vm_command(cmd, params);
		}
	    });
	};
	let caps = Ext.state.Manager.get('GuiCap');
	let standalone = PVE.data.ResourceStore.getNodes().length < 2;
	let running = false, stopped = true, suspended = false;
	switch (info.status) {
	    case 'running':
		running = true;
		stopped = false;
		break;
	    case 'suspended':
		stopped = false;
		suspended = true;
		break;
	    case 'paused':
		stopped = false;
		suspended = true;
		break;
	    default: break;
	}
	me.title = "VM " + info.vmid;
	me.items = [
	    {
		text: gettext('Start'),
		iconCls: 'fa fa-fw fa-play',
		hidden: running || suspended,
		disabled: running || suspended,
		handler: () => vm_command('start'),
	    },
	    {
		text: gettext('Pause'),
		iconCls: 'fa fa-fw fa-pause',
		hidden: stopped || suspended,
		disabled: stopped || suspended,
		handler: () => confirmedVMCommand('suspend', undefined, 'qmpause'),
	    },
	    {
		text: gettext('Hibernate'),
		iconCls: 'fa fa-fw fa-download',
		hidden: stopped || suspended,
		disabled: stopped || suspended,
		tooltip: gettext('Suspend to disk'),
		handler: () => confirmedVMCommand('suspend', { todisk: 1 }),
	    },
	    {
		text: gettext('Resume'),
		iconCls: 'fa fa-fw fa-play',
		hidden: !suspended,
		handler: () => vm_command('resume'),
	    },
	    {
		text: gettext('Shutdown'),
		iconCls: 'fa fa-fw fa-power-off',
		disabled: stopped || suspended,
		handler: () => confirmedVMCommand('shutdown'),
	    },
	    {
		text: gettext('Stop'),
		iconCls: 'fa fa-fw fa-stop',
		disabled: stopped,
		tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
		handler: () => confirmedVMCommand('stop'),
	    },
	    {
		text: gettext('Reboot'),
		iconCls: 'fa fa-fw fa-refresh',
		disabled: stopped,
		tooltip: Ext.String.format(gettext('Reboot {0}'), 'VM'),
		handler: () => confirmedVMCommand('reboot'),
	    },
	    {
		xtype: 'menuseparator',
		hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone'],
	    },
	    {
		text: gettext('Migrate'),
		iconCls: 'fa fa-fw fa-send-o',
		hidden: standalone || !caps.vms['VM.Migrate'],
		handler: function() {
		    Ext.create('PVE.window.Migrate', {
			vmtype: 'qemu',
			nodename: info.node,
			vmid: info.vmid,
			autoShow: true,
		    });
		},
	    },
	    {
		text: gettext('Clone'),
		iconCls: 'fa fa-fw fa-clone',
		hidden: !caps.vms['VM.Clone'],
		handler: () => PVE.window.Clone.wrap(info.node, info.vmid, me.isTemplate, 'qemu'),
	    },
	    {
		text: gettext('Convert to template'),
		iconCls: 'fa fa-fw fa-file-o',
		hidden: !caps.vms['VM.Allocate'],
		handler: function() {
		    let msg = Proxmox.Utils.format_task_description('qmtemplate', info.vmid);
		    Ext.Msg.confirm(gettext('Confirm'), msg, btn => {
			if (btn === 'yes') {
			    Proxmox.Utils.API2Request({
				url: `/nodes/${info.node}/qemu/${info.vmid}/template`,
				method: 'POST',
				failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus),
			    });
			}
		    });
		},
	    },
	    { xtype: 'menuseparator' },
	    {
		text: gettext('Console'),
		iconCls: 'fa fa-fw fa-terminal',
		handler: function() {
		    Proxmox.Utils.API2Request({
			url: `/nodes/${info.node}/qemu/${info.vmid}/status/current`,
			failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus),
			success: function({ result: { data } }, opts) {
			    PVE.Utils.openDefaultConsoleWindow(
				{
				    spice: data.spice,
				    xtermjs: data.serial,
				},
				'kvm',
				info.vmid,
				info.node,
				info.name,
			    );
			},
		    });
		},
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.qemu.Config', {
    extend: 'PVE.panel.Config',
    alias: 'widget.PVE.qemu.Config',
    onlineHelp: 'chapter_virtual_machines',
    userCls: 'proxmox-tags-full',
    initComponent: function() {
        var me = this;
	var vm = me.pveSelNode.data;
	var nodename = vm.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = vm.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	var template = !!vm.template;
	var running = !!vm.uptime;
	var caps = Ext.state.Manager.get('GuiCap');
	var base_url = '/nodes/' + nodename + "/qemu/" + vmid;
	me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
	    url: '/api2/json' + base_url + '/status/current',
	    interval: 1000,
	});
	var vm_command = function(cmd, params) {
	    Proxmox.Utils.API2Request({
		params: params,
		url: base_url + '/status/' + cmd,
		waitMsgTarget: me,
		method: 'POST',
		failure: function(response, opts) {
		    Ext.Msg.alert('Error', response.htmlStatus);
		},
	    });
	};
	var resumeBtn = Ext.create('Ext.Button', {
	    text: gettext('Resume'),
	    disabled: !caps.vms['VM.PowerMgmt'],
	    hidden: true,
	    handler: function() {
		vm_command('resume');
	    },
	    iconCls: 'fa fa-play',
	});
	var startBtn = Ext.create('Ext.Button', {
	    text: gettext('Start'),
	    disabled: !caps.vms['VM.PowerMgmt'] || running,
	    hidden: template,
	    handler: function() {
		vm_command('start');
	    },
	    iconCls: 'fa fa-play',
	});
	var migrateBtn = Ext.create('Ext.Button', {
	    text: gettext('Migrate'),
	    disabled: !caps.vms['VM.Migrate'],
	    hidden: PVE.data.ResourceStore.getNodes().length < 2,
	    handler: function() {
		var win = Ext.create('PVE.window.Migrate', {
		    vmtype: 'qemu',
		    nodename: nodename,
		    vmid: vmid,
		});
		win.show();
	    },
	    iconCls: 'fa fa-send-o',
	});
	var moreBtn = Ext.create('Proxmox.button.Button', {
	    text: gettext('More'),
	    menu: {
 items: [
		{
		    text: gettext('Clone'),
		    iconCls: 'fa fa-fw fa-clone',
		    hidden: !caps.vms['VM.Clone'],
		    handler: function() {
			PVE.window.Clone.wrap(nodename, vmid, template, 'qemu');
		    },
		},
		{
		    text: gettext('Convert to template'),
		    disabled: template,
		    xtype: 'pveMenuItem',
		    iconCls: 'fa fa-fw fa-file-o',
		    hidden: !caps.vms['VM.Allocate'],
		    confirmMsg: Proxmox.Utils.format_task_description('qmtemplate', vmid),
		    handler: function() {
			Proxmox.Utils.API2Request({
			    url: base_url + '/template',
			    waitMsgTarget: me,
			    method: 'POST',
			    failure: function(response, opts) {
				Ext.Msg.alert('Error', response.htmlStatus);
			    },
			});
		    },
		},
		{
		    iconCls: 'fa fa-heartbeat ',
		    hidden: !caps.nodes['Sys.Console'],
		    text: gettext('Manage HA'),
		    handler: function() {
			var ha = vm.hastate;
			Ext.create('PVE.ha.VMResourceEdit', {
			    vmid: vmid,
			    isCreate: !ha || ha === 'unmanaged',
			}).show();
		    },
		},
		{
		    text: gettext('Remove'),
		    itemId: 'removeBtn',
		    disabled: !caps.vms['VM.Allocate'],
		    handler: function() {
			Ext.create('PVE.window.SafeDestroyGuest', {
			    url: base_url,
			    item: { type: 'VM', id: vmid },
			    taskName: 'qmdestroy',
			}).show();
		    },
		    iconCls: 'fa fa-trash-o',
		},
	    ],
},
	});
	var shutdownBtn = Ext.create('PVE.button.Split', {
	    text: gettext('Shutdown'),
	    disabled: !caps.vms['VM.PowerMgmt'] || !running,
	    hidden: template,
	    confirmMsg: Proxmox.Utils.format_task_description('qmshutdown', vmid),
	    handler: function() {
		vm_command('shutdown');
	    },
	    menu: {
		items: [{
		    text: gettext('Reboot'),
		    disabled: !caps.vms['VM.PowerMgmt'],
		    tooltip: Ext.String.format(gettext('Shutdown, apply pending changes and reboot {0}'), 'VM'),
		    confirmMsg: Proxmox.Utils.format_task_description('qmreboot', vmid),
		    handler: function() {
			vm_command("reboot");
		    },
		    iconCls: 'fa fa-refresh',
		}, {
		    text: gettext('Pause'),
		    disabled: !caps.vms['VM.PowerMgmt'],
		    confirmMsg: Proxmox.Utils.format_task_description('qmpause', vmid),
		    handler: function() {
			vm_command("suspend");
		    },
		    iconCls: 'fa fa-pause',
		}, {
		    text: gettext('Hibernate'),
		    disabled: !caps.vms['VM.PowerMgmt'],
		    confirmMsg: Proxmox.Utils.format_task_description('qmsuspend', vmid),
		    tooltip: gettext('Suspend to disk'),
		    handler: function() {
			vm_command("suspend", { todisk: 1 });
		    },
		    iconCls: 'fa fa-download',
		}, {
		    text: gettext('Stop'),
		    disabled: !caps.vms['VM.PowerMgmt'],
		    dangerous: true,
		    tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
		    confirmMsg: Proxmox.Utils.format_task_description('qmstop', vmid),
		    handler: function() {
			vm_command("stop", { timeout: 30 });
		    },
		    iconCls: 'fa fa-stop',
		}, {
		    text: gettext('Reset'),
		    disabled: !caps.vms['VM.PowerMgmt'],
		    tooltip: Ext.String.format(gettext('Reset {0} immediately'), 'VM'),
		    confirmMsg: Proxmox.Utils.format_task_description('qmreset', vmid),
		    handler: function() {
			vm_command("reset");
		    },
		    iconCls: 'fa fa-bolt',
		}],
	    },
	    iconCls: 'fa fa-power-off',
	});
	var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
	    disabled: !caps.vms['VM.Console'],
	    hidden: template,
	    consoleType: 'kvm',
	    // disable spice/xterm for default action until status api call succeeded
	    enableSpice: false,
	    enableXtermjs: false,
	    consoleName: vm.name,
	    nodename: nodename,
	    vmid: vmid,
	});
	var statusTxt = Ext.create('Ext.toolbar.TextItem', {
	    data: {
		lock: undefined,
	    },
	    tpl: [
		'',
		' ({lock})',
		'',
	    ],
	});
	let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
	    tags: vm.tags,
	    canEdit: !!caps.vms['VM.Config.Options'],
	    listeners: {
		change: function(tags) {
		    Proxmox.Utils.API2Request({
			url: base_url + '/config',
			method: 'PUT',
			params: {
			    tags,
			},
			success: function() {
			    me.statusStore.load();
			},
			failure: function(response) {
			    Ext.Msg.alert('Error', response.htmlStatus);
			    me.statusStore.load();
			},
		    });
		},
	    },
	});
	let vm_text = `${vm.vmid} (${vm.name})`;
	Ext.apply(me, {
	    title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm_text, nodename),
	    hstateid: 'kvmtab',
	    tbarSpacing: false,
	    tbar: [statusTxt, tagsContainer, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
	    defaults: { statusStore: me.statusStore },
	    items: [
		{
		    title: gettext('Summary'),
		    xtype: 'pveGuestSummary',
		    iconCls: 'fa fa-book',
		    itemId: 'summary',
		},
	    ],
	});
	if (caps.vms['VM.Console'] && !template) {
	    me.items.push({
		title: gettext('Console'),
		itemId: 'console',
		iconCls: 'fa fa-terminal',
		xtype: 'pveNoVncConsole',
		vmid: vmid,
		consoleType: 'kvm',
		nodename: nodename,
	    });
	}
	me.items.push(
	    {
		title: gettext('Hardware'),
		itemId: 'hardware',
		iconCls: 'fa fa-desktop',
		xtype: 'PVE.qemu.HardwareView',
	    },
	    {
		title: 'Cloud-Init',
		itemId: 'cloudinit',
		iconCls: 'fa fa-cloud',
		xtype: 'pveCiPanel',
	    },
	    {
		title: gettext('Options'),
		iconCls: 'fa fa-gear',
		itemId: 'options',
		xtype: 'PVE.qemu.Options',
	    },
	    {
		title: gettext('Task History'),
		itemId: 'tasks',
		xtype: 'proxmoxNodeTasks',
		iconCls: 'fa fa-list-alt',
		nodename: nodename,
		preFilter: {
		    vmid,
		},
	    },
	);
	if (caps.vms['VM.Monitor'] && !template) {
	    me.items.push({
		title: gettext('Monitor'),
		iconCls: 'fa fa-eye',
		itemId: 'monitor',
		xtype: 'pveQemuMonitor',
	    });
	}
	if (caps.vms['VM.Backup']) {
	    me.items.push({
		title: gettext('Backup'),
		iconCls: 'fa fa-floppy-o',
		xtype: 'pveBackupView',
		itemId: 'backup',
	    },
	    {
		title: gettext('Replication'),
		iconCls: 'fa fa-retweet',
		xtype: 'pveReplicaView',
		itemId: 'replication',
	    });
	}
	if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback'] ||
	    caps.vms['VM.Audit']) && !template) {
	    me.items.push({
		title: gettext('Snapshots'),
		iconCls: 'fa fa-history',
		type: 'qemu',
		xtype: 'pveGuestSnapshotTree',
		itemId: 'snapshot',
	    });
	}
	if (caps.vms['VM.Console']) {
	    me.items.push(
		{
		    xtype: 'pveFirewallRules',
		    title: gettext('Firewall'),
		    iconCls: 'fa fa-shield',
		    allow_iface: true,
		    base_url: base_url + '/firewall/rules',
		    list_refs_url: base_url + '/firewall/refs',
		    itemId: 'firewall',
		},
		{
		    xtype: 'pveFirewallOptions',
		    groups: ['firewall'],
		    iconCls: 'fa fa-gear',
		    onlineHelp: 'pve_firewall_vm_container_configuration',
		    title: gettext('Options'),
		    base_url: base_url + '/firewall/options',
		    fwtype: 'vm',
		    itemId: 'firewall-options',
		},
		{
		    xtype: 'pveFirewallAliases',
		    title: gettext('Alias'),
		    groups: ['firewall'],
		    iconCls: 'fa fa-external-link',
		    base_url: base_url + '/firewall/aliases',
		    itemId: 'firewall-aliases',
		},
		{
		    xtype: 'pveIPSet',
		    title: gettext('IPSet'),
		    groups: ['firewall'],
		    iconCls: 'fa fa-list-ol',
		    base_url: base_url + '/firewall/ipset',
		    list_refs_url: base_url + '/firewall/refs',
		    itemId: 'firewall-ipset',
		},
		{
		    title: gettext('Log'),
		    groups: ['firewall'],
		    iconCls: 'fa fa-list',
		    onlineHelp: 'chapter_pve_firewall',
		    itemId: 'firewall-fwlog',
		    xtype: 'proxmoxLogView',
		    url: '/api2/extjs' + base_url + '/firewall/log',
		},
	    );
	}
	if (caps.vms['Permissions.Modify']) {
	    me.items.push({
		xtype: 'pveACLView',
		title: gettext('Permissions'),
		iconCls: 'fa fa-unlock',
		itemId: 'permissions',
		path: '/vms/' + vmid,
	    });
	}
	me.callParent();
	var prevQMPStatus = 'unknown';
        me.mon(me.statusStore, 'load', function(s, records, success) {
	    var status;
	    var qmpstatus;
	    var spice = false;
	    var xtermjs = false;
	    var lock;
	    var rec;
	    if (!success) {
		status = qmpstatus = 'unknown';
	    } else {
		rec = s.data.get('status');
		status = rec ? rec.data.value : 'unknown';
		rec = s.data.get('qmpstatus');
		qmpstatus = rec ? rec.data.value : 'unknown';
		rec = s.data.get('template');
		template = rec ? rec.data.value : false;
		rec = s.data.get('lock');
		lock = rec ? rec.data.value : undefined;
		spice = !!s.data.get('spice');
		xtermjs = !!s.data.get('serial');
	    }
	    rec = s.data.get('tags');
	    tagsContainer.loadTags(rec?.data?.value);
	    if (template) {
		return;
	    }
	    var resume = ['prelaunch', 'paused', 'suspended'].indexOf(qmpstatus) !== -1;
	    if (resume || lock === 'suspended') {
		startBtn.setVisible(false);
		resumeBtn.setVisible(true);
	    } else {
		startBtn.setVisible(true);
		resumeBtn.setVisible(false);
	    }
	    consoleBtn.setEnableSpice(spice);
	    consoleBtn.setEnableXtermJS(xtermjs);
	    statusTxt.update({ lock: lock });
	    let guest_running = status === 'running' &&
		!(qmpstatus === "shutdown" || qmpstatus === "prelaunch");
	    startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || template || guest_running);
	    shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
	    me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
	    consoleBtn.setDisabled(template);
	    let wasStopped = ['prelaunch', 'stopped', 'suspended'].indexOf(prevQMPStatus) !== -1;
	    if (wasStopped && qmpstatus === 'running') {
		let con = me.down('#console');
		if (con) {
		    con.reload();
		}
	    }
	    prevQMPStatus = qmpstatus;
	});
	me.on('afterrender', function() {
	    me.statusStore.startUpdate();
	});
	me.on('destroy', function() {
	    me.statusStore.stopUpdate();
	});
   },
});
Ext.define('PVE.qemu.CreateWizard', {
    extend: 'PVE.window.Wizard',
    alias: 'widget.pveQemuCreateWizard',
    mixins: ['Proxmox.Mixin.CBind'],
    viewModel: {
	data: {
	    nodename: '',
	    current: {
		scsihw: '',
	    },
	},
	formulas: {
	    cgroupMode: function(get) {
		const nodeInfo = PVE.data.ResourceStore.getNodes().find(
		    node => node.node === get('nodename'),
		);
		return nodeInfo ? nodeInfo['cgroup-mode'] : 2;
	    },
	},
    },
    cbindData: {
	nodename: undefined,
    },
    subject: gettext('Virtual Machine'),
    items: [
	{
	    xtype: 'inputpanel',
	    title: gettext('General'),
	    onlineHelp: 'qm_general_settings',
	    column1: [
		{
		    xtype: 'pveNodeSelector',
		    name: 'nodename',
		    cbind: {
			selectCurNode: '{!nodename}',
			preferredValue: '{nodename}',
		    },
		    bind: {
			value: '{nodename}',
		    },
		    fieldLabel: gettext('Node'),
		    allowBlank: false,
		    onlineValidator: true,
		},
		{
		    xtype: 'pveGuestIDSelector',
		    name: 'vmid',
		    guestType: 'qemu',
		    value: '',
		    loadNextFreeID: true,
		    validateExists: false,
		},
		{
		    xtype: 'textfield',
		    name: 'name',
		    vtype: 'DnsName',
		    value: '',
		    fieldLabel: gettext('Name'),
		    allowBlank: true,
		},
	    ],
	    column2: [
		{
		    xtype: 'pvePoolSelector',
		    fieldLabel: gettext('Resource Pool'),
		    name: 'pool',
		    value: '',
		    allowBlank: true,
		},
	    ],
	    advancedColumn1: [
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'onboot',
		    uncheckedValue: 0,
		    defaultValue: 0,
		    deleteDefaultValue: true,
		    fieldLabel: gettext('Start at boot'),
		},
	    ],
	    advancedColumn2: [
		{
		    xtype: 'textfield',
		    name: 'order',
		    defaultValue: '',
		    emptyText: 'any',
		    labelWidth: 120,
		    fieldLabel: gettext('Start/Shutdown order'),
		},
		{
		    xtype: 'textfield',
		    name: 'up',
		    defaultValue: '',
		    emptyText: 'default',
		    labelWidth: 120,
		    fieldLabel: gettext('Startup delay'),
		},
		{
		    xtype: 'textfield',
		    name: 'down',
		    defaultValue: '',
		    emptyText: 'default',
		    labelWidth: 120,
		    fieldLabel: gettext('Shutdown timeout'),
		},
	    ],
	    onGetValues: function(values) {
		['name', 'pool', 'onboot', 'agent'].forEach(function(field) {
		    if (!values[field]) {
			delete values[field];
		    }
		});
		var res = PVE.Parser.printStartup({
		    order: values.order,
		    up: values.up,
		    down: values.down,
		});
		if (res) {
		    values.startup = res;
		}
		delete values.order;
		delete values.up;
		delete values.down;
		return values;
	    },
	},
	{
	    xtype: 'container',
	    layout: 'hbox',
	    defaults: {
		flex: 1,
		padding: '0 10',
	    },
	    title: gettext('OS'),
	    items: [
		{
		    xtype: 'pveQemuCDInputPanel',
		    bind: {
			nodename: '{nodename}',
		    },
		    confid: 'ide2',
		    insideWizard: true,
		},
		{
		    xtype: 'pveQemuOSTypePanel',
		    insideWizard: true,
		},
	    ],
	},
	{
	    xtype: 'pveQemuSystemPanel',
	    title: gettext('System'),
	    isCreate: true,
	    insideWizard: true,
	},
	{
	    xtype: 'pveMultiHDPanel',
	    bind: {
		nodename: '{nodename}',
	    },
	    title: gettext('Disks'),
	},
	{
	    xtype: 'pveQemuProcessorPanel',
	    insideWizard: true,
	    title: gettext('CPU'),
	},
	{
	    xtype: 'pveQemuMemoryPanel',
	    insideWizard: true,
	    title: gettext('Memory'),
	},
	{
	    xtype: 'pveQemuNetworkInputPanel',
	    bind: {
		nodename: '{nodename}',
	    },
	    title: gettext('Network'),
	    insideWizard: true,
	},
	{
	    title: gettext('Confirm'),
	    layout: 'fit',
	    items: [
		{
		    xtype: 'grid',
		    store: {
			model: 'KeyValue',
			sorters: [{
			    property: 'key',
			    direction: 'ASC',
			}],
		    },
		    columns: [
			{ header: 'Key', width: 150, dataIndex: 'key' },
			{ header: 'Value', flex: 1, dataIndex: 'value' },
		    ],
		},
	    ],
	    dockedItems: [
		{
		    xtype: 'proxmoxcheckbox',
		    name: 'start',
		    dock: 'bottom',
		    margin: '5 0 0 0',
		    boxLabel: gettext('Start after created'),
		},
	    ],
	    listeners: {
		show: function(panel) {
		    var kv = this.up('window').getValues();
		    var data = [];
		    Ext.Object.each(kv, function(key, value) {
			if (key === 'delete') { // ignore
			    return;
			}
			data.push({ key: key, value: value });
		    });
		    var summarystore = panel.down('grid').getStore();
		    summarystore.suspendEvents();
		    summarystore.removeAll();
		    summarystore.add(data);
		    summarystore.sort();
		    summarystore.resumeEvents();
		    summarystore.fireEvent('refresh');
		},
	    },
	    onSubmit: function() {
		var wizard = this.up('window');
		var kv = wizard.getValues();
		delete kv.delete;
		var nodename = kv.nodename;
		delete kv.nodename;
		Proxmox.Utils.API2Request({
		    url: '/nodes/' + nodename + '/qemu',
		    waitMsgTarget: wizard,
		    method: 'POST',
		    params: kv,
		    success: function(response) {
			wizard.close();
		    },
		    failure: function(response, opts) {
			Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		    },
		});
	    },
	},
    ],
});
Ext.define('PVE.qemu.DisplayInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveDisplayInputPanel',
    onlineHelp: 'qm_display',
    onGetValues: function(values) {
	let ret = PVE.Parser.printPropertyString(values, 'type');
	if (ret === '') {
	    return { 'delete': 'vga' };
	}
	return { vga: ret };
    },
    items: [{
	name: 'type',
	xtype: 'proxmoxKVComboBox',
	value: '__default__',
	deleteEmpty: false,
	fieldLabel: gettext('Graphic card'),
	comboItems: Object.entries(PVE.Utils.kvm_vga_drivers),
	validator: function(v) {
	    let cfg = this.up('proxmoxWindowEdit').vmconfig || {};
	    if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) {
		let fmt = gettext("Serial interface '{0}' is not correctly configured.");
		return Ext.String.format(fmt, v);
	    }
	    return true;
	},
	listeners: {
	    change: function(cb, val) {
		if (!val) {
		    return;
		}
		let memoryfield = this.up('panel').down('field[name=memory]');
		let disableMemoryField = false;
		if (val === "cirrus") {
		    memoryfield.setEmptyText("4");
		} else if (val === "std" || val.match(/^qxl\d?$/) || val === "vmware") {
		    memoryfield.setEmptyText("16");
		} else if (val.match(/^virtio/)) {
		    memoryfield.setEmptyText("256");
		} else if (val.match(/^(serial\d|none)$/)) {
		    memoryfield.setEmptyText("N/A");
		    disableMemoryField = true;
		} else {
		    console.debug("unexpected display type", val);
		    memoryfield.setEmptyText(Proxmox.Utils.defaultText);
		}
		memoryfield.setDisabled(disableMemoryField);
	    },
	},
    },
    {
	xtype: 'proxmoxintegerfield',
	emptyText: Proxmox.Utils.defaultText,
	fieldLabel: gettext('Memory') + ' (MiB)',
	minValue: 4,
	maxValue: 512,
	step: 4,
	name: 'memory',
    }],
});
Ext.define('PVE.qemu.DisplayEdit', {
    extend: 'Proxmox.window.Edit',
    vmconfig: undefined,
    subject: gettext('Display'),
    width: 350,
    items: [{
	xtype: 'pveDisplayInputPanel',
    }],
    initComponent: function() {
	let me = this;
	me.callParent();
	me.load({
	    success: function(response) {
		me.vmconfig = response.result.data;
		let vga = me.vmconfig.vga || '__default__';
		me.setValues(PVE.Parser.parsePropertyString(vga, 'type'));
	    },
	});
    },
});
/* 'change' property is assigned a string and then a function */
Ext.define('PVE.qemu.HDInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveQemuHDInputPanel',
    onlineHelp: 'qm_hard_disk',
    insideWizard: false,
    unused: false, // ADD usused disk imaged
    vmconfig: {}, // used to select usused disks
    viewModel: {
	data: {
	    isSCSI: false,
	    isVirtIO: false,
	    isSCSISingle: false,
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	onControllerChange: function(field) {
	    let me = this;
	    let vm = this.getViewModel();
	    let value = field.getValue();
	    vm.set('isSCSI', value.match(/^scsi/));
	    vm.set('isVirtIO', value.match(/^virtio/));
	    me.fireIdChange();
	},
	fireIdChange: function() {
	    let view = this.getView();
	    view.fireEvent('diskidchange', view, view.bussel.getConfId());
	},
	control: {
	    'field[name=controller]': {
		change: 'onControllerChange',
		afterrender: 'onControllerChange',
	    },
	    'field[name=deviceid]': {
		change: 'fireIdChange',
	    },
	    'field[name=scsiController]': {
		change: function(f, value) {
		    let vm = this.getViewModel();
		    vm.set('isSCSISingle', value === 'virtio-scsi-single');
		},
	    },
	},
	init: function(view) {
	    var vm = this.getViewModel();
	    if (view.isCreate) {
		vm.set('isIncludedInBackup', true);
	    }
	    if (view.confid) {
		vm.set('isSCSI', view.confid.match(/^scsi/));
		vm.set('isVirtIO', view.confid.match(/^virtio/));
	    }
	},
    },
    onGetValues: function(values) {
	var me = this;
	var params = {};
	var confid = me.confid || values.controller + values.deviceid;
	if (me.unused) {
	    me.drive.file = me.vmconfig[values.unusedId];
	    confid = values.controller + values.deviceid;
	} else if (me.isCreate) {
	    if (values.hdimage) {
		me.drive.file = values.hdimage;
	    } else {
		me.drive.file = values.hdstorage + ":" + values.disksize;
	    }
	    me.drive.format = values.diskformat;
	}
	PVE.Utils.propertyStringSet(me.drive, !values.backup, 'backup', '0');
	PVE.Utils.propertyStringSet(me.drive, values.noreplicate, 'replicate', 'no');
	PVE.Utils.propertyStringSet(me.drive, values.discard, 'discard', 'on');
	PVE.Utils.propertyStringSet(me.drive, values.ssd, 'ssd', 'on');
	PVE.Utils.propertyStringSet(me.drive, values.iothread, 'iothread', 'on');
	PVE.Utils.propertyStringSet(me.drive, values.readOnly, 'ro', 'on');
	PVE.Utils.propertyStringSet(me.drive, values.cache, 'cache');
	PVE.Utils.propertyStringSet(me.drive, values.aio, 'aio');
	['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'].forEach(name => {
	    let burst_name = `${name}_max`;
	    PVE.Utils.propertyStringSet(me.drive, values[name], name);
	    PVE.Utils.propertyStringSet(me.drive, values[burst_name], burst_name);
	});
	params[confid] = PVE.Parser.printQemuDrive(me.drive);
	return params;
    },
    updateVMConfig: function(vmconfig) {
	var me = this;
	me.vmconfig = vmconfig;
	me.bussel?.updateVMConfig(vmconfig);
    },
    setVMConfig: function(vmconfig) {
	var me = this;
	me.vmconfig = vmconfig;
	if (me.bussel) {
	    me.bussel.setVMConfig(vmconfig);
	    me.scsiController.setValue(vmconfig.scsihw);
	}
	if (me.unusedDisks) {
	    var disklist = [];
	    Ext.Object.each(vmconfig, function(key, value) {
		if (key.match(/^unused\d+$/)) {
		    disklist.push([key, value]);
		}
	    });
	    me.unusedDisks.store.loadData(disklist);
	    me.unusedDisks.setValue(me.confid);
	}
    },
    setDrive: function(drive) {
	var me = this;
	me.drive = drive;
	var values = {};
	var match = drive.file.match(/^([^:]+):/);
	if (match) {
	    values.hdstorage = match[1];
	}
	values.hdimage = drive.file;
	values.backup = PVE.Parser.parseBoolean(drive.backup, 1);
	values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1);
	values.diskformat = drive.format || 'raw';
	values.cache = drive.cache || '__default__';
	values.discard = drive.discard === 'on';
	values.ssd = PVE.Parser.parseBoolean(drive.ssd);
	values.iothread = PVE.Parser.parseBoolean(drive.iothread);
	values.readOnly = PVE.Parser.parseBoolean(drive.ro);
	values.aio = drive.aio || '__default__';
	values.mbps_rd = drive.mbps_rd;
	values.mbps_wr = drive.mbps_wr;
	values.iops_rd = drive.iops_rd;
	values.iops_wr = drive.iops_wr;
	values.mbps_rd_max = drive.mbps_rd_max;
	values.mbps_wr_max = drive.mbps_wr_max;
	values.iops_rd_max = drive.iops_rd_max;
	values.iops_wr_max = drive.iops_wr_max;
	me.setValues(values);
    },
    setNodename: function(nodename) {
	var me = this;
	me.down('#hdstorage').setNodename(nodename);
	me.down('#hdimage').setStorage(undefined, nodename);
    },
    hasAdvanced: true,
    initComponent: function() {
	var me = this;
	me.drive = {};
	let column1 = [];
	let column2 = [];
	let advancedColumn1 = [];
	let advancedColumn2 = [];
	if (!me.confid || me.unused) {
	    me.bussel = Ext.create('PVE.form.ControllerSelector', {
		vmconfig: me.vmconfig,
		selectFree: true,
	    });
	    column1.push(me.bussel);
	    me.scsiController = Ext.create('Ext.form.field.Display', {
		fieldLabel: gettext('SCSI Controller'),
		reference: 'scsiController',
		name: 'scsiController',
		bind: me.insideWizard ? {
		    value: '{current.scsihw}',
		    visible: '{isSCSI}',
		} : {
		    visible: '{isSCSI}',
		},
		renderer: PVE.Utils.render_scsihw,
		submitValue: false,
		hidden: true,
	    });
	    column1.push(me.scsiController);
	}
	if (me.unused) {
	    me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', {
		name: 'unusedId',
		fieldLabel: gettext('Disk image'),
		matchFieldWidth: false,
		listConfig: {
		    width: 350,
		},
		data: [],
		allowBlank: false,
	    });
	    column1.push(me.unusedDisks);
	} else if (me.isCreate) {
	    column1.push({
		xtype: 'pveDiskStorageSelector',
		storageContent: 'images',
		name: 'disk',
		nodename: me.nodename,
		autoSelect: me.insideWizard,
	    });
	} else {
	    column1.push({
		xtype: 'textfield',
		disabled: true,
		submitValue: false,
		fieldLabel: gettext('Disk image'),
                name: 'hdimage',
	    });
	}
	column2.push(
	    {
		xtype: 'CacheTypeSelector',
		name: 'cache',
		value: '__default__',
		fieldLabel: gettext('Cache'),
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Discard'),
		reference: 'discard',
		name: 'discard',
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'iothread',
		fieldLabel: 'IO thread',
		clearOnDisable: true,
		bind: me.insideWizard || me.isCreate ? {
		    disabled: '{!isVirtIO && !isSCSI}',
		    // Checkbox.setValue handles Arrays in a different way, therefore cast to bool
		    value: '{!!isVirtIO || (isSCSI && isSCSISingle)}',
		} : {
		    disabled: '{!isVirtIO && !isSCSI}',
		},
	    },
	);
	advancedColumn1.push(
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('SSD emulation'),
		name: 'ssd',
		clearOnDisable: true,
		bind: {
		    disabled: '{isVirtIO}',
		},
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'readOnly', // `ro` in the config, we map in get/set values
		defaultValue: 0,
		fieldLabel: gettext('Read-only'),
		clearOnDisable: true,
		bind: {
		    disabled: '{!isVirtIO && !isSCSI}',
		},
	    },
	);
	advancedColumn2.push(
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Backup'),
		autoEl: {
		    tag: 'div',
		    'data-qtip': gettext('Include volume in backup job'),
		},
		name: 'backup',
		bind: {
		    value: '{isIncludedInBackup}',
		},
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Skip replication'),
		name: 'noreplicate',
	    },
	    {
		xtype: 'proxmoxKVComboBox',
		name: 'aio',
		fieldLabel: gettext('Async IO'),
		allowBlank: false,
		value: '__default__',
		comboItems: [
		    ['__default__', Proxmox.Utils.defaultText + ' (io_uring)'],
		    ['io_uring', 'io_uring'],
		    ['native', 'native'],
		    ['threads', 'threads'],
		],
	    },
	);
	let labelWidth = 140;
	let bwColumn1 = [
	    {
		xtype: 'numberfield',
		name: 'mbps_rd',
		minValue: 1,
		step: 1,
		fieldLabel: gettext('Read limit') + ' (MB/s)',
		labelWidth: labelWidth,
		emptyText: gettext('unlimited'),
	    },
	    {
		xtype: 'numberfield',
		name: 'mbps_wr',
		minValue: 1,
		step: 1,
		fieldLabel: gettext('Write limit') + ' (MB/s)',
		labelWidth: labelWidth,
		emptyText: gettext('unlimited'),
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'iops_rd',
		minValue: 10,
		step: 10,
		fieldLabel: gettext('Read limit') + ' (ops/s)',
		labelWidth: labelWidth,
		emptyText: gettext('unlimited'),
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'iops_wr',
		minValue: 10,
		step: 10,
		fieldLabel: gettext('Write limit') + ' (ops/s)',
		labelWidth: labelWidth,
		emptyText: gettext('unlimited'),
	    },
	];
	let bwColumn2 = [
	    {
		xtype: 'numberfield',
		name: 'mbps_rd_max',
		minValue: 1,
		step: 1,
		fieldLabel: gettext('Read max burst') + ' (MB)',
		labelWidth: labelWidth,
		emptyText: gettext('default'),
	    },
	    {
		xtype: 'numberfield',
		name: 'mbps_wr_max',
		minValue: 1,
		step: 1,
		fieldLabel: gettext('Write max burst') + ' (MB)',
		labelWidth: labelWidth,
		emptyText: gettext('default'),
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'iops_rd_max',
		minValue: 10,
		step: 10,
		fieldLabel: gettext('Read max burst') + ' (ops)',
		labelWidth: labelWidth,
		emptyText: gettext('default'),
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'iops_wr_max',
		minValue: 10,
		step: 10,
		fieldLabel: gettext('Write max burst') + ' (ops)',
		labelWidth: labelWidth,
		emptyText: gettext('default'),
	    },
	];
	me.items = [
	    {
		xtype: 'tabpanel',
		plain: true,
		bodyPadding: 10,
		border: 0,
		items: [
		    {
			title: gettext('Disk'),
			xtype: 'inputpanel',
			reference: 'diskpanel',
			column1,
			column2,
			advancedColumn1,
			advancedColumn2,
			showAdvanced: me.showAdvanced,
			getValues: () => ({}),
		    },
		    {
			title: gettext('Bandwidth'),
			xtype: 'inputpanel',
			reference: 'bwpanel',
			column1: bwColumn1,
			column2: bwColumn2,
			showAdvanced: me.showAdvanced,
			getValues: () => ({}),
		    },
		],
	    },
	];
	me.callParent();
    },
    setAdvancedVisible: function(visible) {
	this.lookup('diskpanel').setAdvancedVisible(visible);
	this.lookup('bwpanel').setAdvancedVisible(visible);
    },
});
Ext.define('PVE.qemu.HDEdit', {
    extend: 'Proxmox.window.Edit',
    isAdd: true,
    backgroundDelay: 5,
    width: 600,
    bodyPadding: 0,
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var unused = me.confid && me.confid.match(/^unused\d+$/);
	me.isCreate = me.confid ? unused : true;
	var ipanel = Ext.create('PVE.qemu.HDInputPanel', {
	    confid: me.confid,
	    nodename: nodename,
	    unused: unused,
	    isCreate: me.isCreate,
	});
	if (unused) {
	    me.subject = gettext('Unused Disk');
	} else if (me.isCreate) {
            me.subject = gettext('Hard Disk');
	} else {
           me.subject = gettext('Hard Disk') + ' (' + me.confid + ')';
	}
	me.items = [ipanel];
	me.callParent();
	/* 'data' is assigned an empty array in same file, and here we
	 * use it like an object
	 */
	me.load({
	    success: function(response, options) {
		ipanel.setVMConfig(response.result.data);
		if (me.confid) {
		    var value = response.result.data[me.confid];
		    var drive = PVE.Parser.parseQemuDrive(me.confid, value);
		    if (!drive) {
			Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options');
			me.close();
			return;
		    }
		    ipanel.setDrive(drive);
		    me.isValid(); // trigger validation
		}
	    },
	});
    },
});
Ext.define('PVE.qemu.EFIDiskInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveEFIDiskInputPanel',
    insideWizard: false,
    unused: false, // ADD usused disk imaged
    vmconfig: {}, // used to select usused disks
    onGetValues: function(values) {
	var me = this;
	if (me.disabled) {
	    return {};
	}
	var confid = 'efidisk0';
	if (values.hdimage) {
	    me.drive.file = values.hdimage;
	} else {
	    // we use 1 here, because for efi the size gets overridden from the backend
	    me.drive.file = values.hdstorage + ":1";
	}
	// always default to newer 4m type with secure boot support, if we're
	// adding a new EFI disk there can't be any old state anyway
	me.drive.efitype = '4m';
	me.drive['pre-enrolled-keys'] = values.preEnrolledKeys;
	delete values.preEnrolledKeys;
	me.drive.format = values.diskformat;
	let params = {};
	params[confid] = PVE.Parser.printQemuDrive(me.drive);
	return params;
    },
    setNodename: function(nodename) {
	var me = this;
	me.down('#hdstorage').setNodename(nodename);
	me.down('#hdimage').setStorage(undefined, nodename);
    },
    setDisabled: function(disabled) {
	let me = this;
	me.down('pveDiskStorageSelector').setDisabled(disabled);
	me.down('proxmoxcheckbox[name=preEnrolledKeys]').setDisabled(disabled);
	me.callParent(arguments);
    },
    initComponent: function() {
	var me = this;
	me.drive = {};
	me.items = [
	    {
		xtype: 'pveDiskStorageSelector',
		name: 'efidisk0',
		storageLabel: gettext('EFI Storage'),
		storageContent: 'images',
		nodename: me.nodename,
		disabled: me.disabled,
		hideSize: true,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'preEnrolledKeys',
		checked: true,
		fieldLabel: gettext("Pre-Enroll keys"),
		disabled: me.disabled,
		//boxLabel: '(e.g., Microsoft secure-boot keys')',
		autoEl: {
		    tag: 'div',
		    'data-qtip': gettext('Use EFIvars image with standard distribution and Microsoft secure boot keys enrolled.'),
		},
	    },
	    {
		xtype: 'label',
		text: gettext("Warning: The VM currently does not uses 'OVMF (UEFI)' as BIOS."),
		userCls: 'pmx-hint',
		hidden: me.usesEFI,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.qemu.EFIDiskEdit', {
    extend: 'Proxmox.window.Edit',
    isAdd: true,
    subject: gettext('EFI Disk'),
    width: 450,
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	me.items = [{
	    xtype: 'pveEFIDiskInputPanel',
	    onlineHelp: 'qm_bios_and_uefi',
	    confid: me.confid,
	    nodename: nodename,
	    usesEFI: me.usesEFI,
	    isCreate: true,
	}];
	me.callParent();
    },
});
Ext.define('PVE.qemu.TPMDiskInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveTPMDiskInputPanel',
    unused: false,
    vmconfig: {},
    onGetValues: function(values) {
	var me = this;
	if (me.disabled) {
	    return {};
	}
	var confid = 'tpmstate0';
	if (values.hdimage) {
	    me.drive.file = values.hdimage;
	} else {
	    // size is constant, so just use 1
	    me.drive.file = values.hdstorage + ":1";
	}
	me.drive.version = values.version;
	var params = {};
	params[confid] = PVE.Parser.printQemuDrive(me.drive);
	return params;
    },
    setNodename: function(nodename) {
	var me = this;
	me.down('#hdstorage').setNodename(nodename);
	me.down('#hdimage').setStorage(undefined, nodename);
    },
    setDisabled: function(disabled) {
	let me = this;
	me.down('pveDiskStorageSelector').setDisabled(disabled);
	me.down('proxmoxKVComboBox[name=version]').setDisabled(disabled);
	me.callParent(arguments);
    },
    initComponent: function() {
	var me = this;
	me.drive = {};
	me.items = [
	    {
		xtype: 'pveDiskStorageSelector',
		name: me.disktype + '0',
		storageLabel: gettext('TPM Storage'),
		storageContent: 'images',
		nodename: me.nodename,
		disabled: me.disabled,
		hideSize: true,
		hideFormat: true,
	    },
	    {
		xtype: 'proxmoxKVComboBox',
		name: 'version',
		value: 'v2.0',
		fieldLabel: gettext('Version'),
		deleteEmpty: false,
		disabled: me.disabled,
		comboItems: [
		    ['v1.2', 'v1.2'],
		    ['v2.0', 'v2.0'],
		],
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.qemu.TPMDiskEdit', {
    extend: 'Proxmox.window.Edit',
    isAdd: true,
    subject: gettext('TPM State'),
    width: 450,
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	me.items = [{
	    xtype: 'pveTPMDiskInputPanel',
	    //onlineHelp: 'qm_tpm', FIXME: add once available
	    confid: me.confid,
	    nodename: nodename,
	    isCreate: true,
	}];
	me.callParent();
    },
});
Ext.define('PVE.window.HDMove', {
    extend: 'Proxmox.window.Edit',
    mixins: ['Proxmox.Mixin.CBind'],
    resizable: false,
    modal: true,
    width: 350,
    border: false,
    layout: 'fit',
    showReset: false,
    showTaskViewer: true,
    method: 'POST',
    cbindData: function() {
	let me = this;
	return {
	    disk: me.disk,
	    isQemu: me.type === 'qemu',
	    nodename: me.nodename,
	    url: () => {
		let endpoint = me.type === 'qemu' ? 'move_disk' : 'move_volume';
		return `/nodes/${me.nodename}/${me.type}/${me.vmid}/${endpoint}`;
	    },
	};
    },
    cbind: {
	title: get => get('isQemu') ? gettext("Move disk") : gettext('Move Volume'),
	submitText: get => get('title'),
	qemu: '{isQemu}',
	url: '{url}',
    },
    getValues: function() {
	let me = this;
	let values = me.formPanel.getForm().getValues();
	let params = {
	    storage: values.hdstorage,
	};
	params[me.qemu ? 'disk' : 'volume'] = me.disk;
	if (values.diskformat && me.qemu) {
	    params.format = values.diskformat;
	}
	if (values.deleteDisk) {
	    params.delete = 1;
	}
	return params;
    },
    items: [
	{
	    xtype: 'form',
	    reference: 'moveFormPanel',
	    border: false,
	    fieldDefaults: {
		labelWidth: 100,
		anchor: '100%',
	    },
	    items: [
		{
		    xtype: 'displayfield',
		    cbind: {
			name: get => get('isQemu') ? 'disk' : 'volume',
			fieldLabel: get => get('isQemu') ? gettext('Disk') : gettext('Mount Point'),
			value: '{disk}',
		    },
		    allowBlank: false,
		},
		{
		    xtype: 'pveDiskStorageSelector',
		    storageLabel: gettext('Target Storage'),
		    cbind: {
			nodename: '{nodename}',
			storageContent: get => get('isQemu') ? 'images' : 'rootdir',
			hideFormat: get => get('disk') === 'tpmstate0',
		    },
		    hideSize: true,
		},
		{
		    xtype: 'proxmoxcheckbox',
		    fieldLabel: gettext('Delete source'),
		    name: 'deleteDisk',
		    uncheckedValue: 0,
		    checked: false,
		},
	    ],
	},
    ],
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.vmid) {
	    throw "no VM ID specified";
	}
	if (!me.type) {
	    throw "no type specified";
	}
	me.callParent();
    },
});
Ext.define('PVE.window.HDResize', {
    extend: 'Ext.window.Window',
    resizable: false,
    resize_disk: function(disk, size) {
	var me = this;
        var params = { disk: disk, size: '+' + size + 'G' };
	Proxmox.Utils.API2Request({
	    params: params,
	    url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/resize',
	    waitMsgTarget: me,
	    method: 'PUT',
	    failure: function(response, opts) {
		Ext.Msg.alert(gettext('Error'), response.htmlStatus);
	    },
	    success: function(response, options) {
		me.close();
	    },
	});
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	if (!me.vmid) {
	    throw "no VM ID specified";
	}
	var items = [
	    {
		xtype: 'displayfield',
		name: 'disk',
		value: me.disk,
		fieldLabel: gettext('Disk'),
		vtype: 'StorageId',
		allowBlank: false,
	    },
	];
	me.hdsizesel = Ext.createWidget('numberfield', {
	    name: 'size',
	    minValue: 0,
	    maxValue: 128*1024,
	    decimalPrecision: 3,
	    value: '0',
	    fieldLabel: gettext('Size Increment') + ' (GiB)',
	    allowBlank: false,
	});
	items.push(me.hdsizesel);
	me.formPanel = Ext.create('Ext.form.Panel', {
	    bodyPadding: 10,
	    border: false,
	    fieldDefaults: {
		labelWidth: 140,
		anchor: '100%',
	    },
	    items: items,
	});
	var form = me.formPanel.getForm();
	var submitBtn;
	me.title = gettext('Resize disk');
	submitBtn = Ext.create('Ext.Button', {
	    text: gettext('Resize disk'),
	    handler: function() {
		if (form.isValid()) {
		    var values = form.getValues();
		    me.resize_disk(me.disk, values.size);
		}
	    },
	});
	Ext.apply(me, {
	    modal: true,
	    width: 250,
	    height: 150,
	    border: false,
	    layout: 'fit',
	    buttons: [submitBtn],
	    items: [me.formPanel],
	});
	me.callParent();
    },
});
Ext.define('PVE.qemu.HardwareView', {
    extend: 'Proxmox.grid.PendingObjectGrid',
    alias: ['widget.PVE.qemu.HardwareView'],
    onlineHelp: 'qm_virtual_machines_settings',
    renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
	var me = this;
	var rows = me.rows;
	var rowdef = rows[key] || {};
	var iconCls = rowdef.iconCls;
	var icon = '';
	var txt = rowdef.header || key;
	metaData.tdAttr = "valign=middle";
	if (rowdef.isOnStorageBus) {
	    var value = me.getObjectValue(key, '', false);
	    if (value === '') {
		value = me.getObjectValue(key, '', true);
	    }
	    if (value.match(/vm-.*-cloudinit/)) {
		iconCls = 'cloud';
		txt = rowdef.cloudheader;
	    } else if (value.match(/media=cdrom/)) {
		metaData.tdCls = 'pve-itype-icon-cdrom';
		return rowdef.cdheader;
	    }
	}
	if (rowdef.tdCls) {
	    metaData.tdCls = rowdef.tdCls;
	} else if (iconCls) {
	    icon = "";
	    metaData.tdCls += " pve-itype-fa";
	}
	// only return icons in grid but not remove dialog
	if (rowIndex !== undefined) {
	    return icon + txt;
	} else {
	    return txt;
	}
    },
    initComponent: function() {
	var me = this;
	const { node: nodename, vmid } = me.pveSelNode.data;
	if (!nodename) {
	    throw "no node name specified";
	} else if (!vmid) {
	    throw "no VM ID specified";
	}
	const caps = Ext.state.Manager.get('GuiCap');
	const diskCap = caps.vms['VM.Config.Disk'];
	const cdromCap = caps.vms['VM.Config.CDROM'];
	let isCloudInitKey = v => v && v.toString().match(/vm-.*-cloudinit/);
	const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename);
	let processorEditor = {
	    xtype: 'pveQemuProcessorEdit',
	    cgroupMode: nodeInfo['cgroup-mode'],
	};
	let rows = {
	    memory: {
		header: gettext('Memory'),
		editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
		never_delete: true,
		defaultValue: '512',
		tdCls: 'pve-itype-icon-memory',
		group: 2,
		multiKey: ['memory', 'balloon', 'shares'],
		renderer: function(value, metaData, record, ri, ci, store, pending) {
		    var res = '';
		    var max = me.getObjectValue('memory', 512, pending);
		    var balloon = me.getObjectValue('balloon', undefined, pending);
		    var shares = me.getObjectValue('shares', undefined, pending);
		    res = Proxmox.Utils.format_size(max*1024*1024);
		    if (balloon !== undefined && balloon > 0) {
			res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res;
			if (shares) {
			    res += ' [shares=' + shares +']';
			}
		    } else if (balloon === 0) {
			res += ' [balloon=0]';
		    }
		    return res;
		},
	    },
	    sockets: {
		header: gettext('Processors'),
		never_delete: true,
		editor: caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']
		    ? processorEditor : undefined,
		tdCls: 'pve-itype-icon-cpu',
		group: 3,
		defaultValue: '1',
		multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
		renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
		    var sockets = me.getObjectValue('sockets', 1, pending);
		    var model = me.getObjectValue('cpu', undefined, pending);
		    var cores = me.getObjectValue('cores', 1, pending);
		    var numa = me.getObjectValue('numa', undefined, pending);
		    var vcpus = me.getObjectValue('vcpus', undefined, pending);
		    var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
		    var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
		    let res = Ext.String.format(
		        '{0} ({1} sockets, {2} cores)', sockets * cores, sockets, cores);
		    if (model) {
			res += ' [' + model + ']';
		    }
		    if (numa) {
			res += ' [numa=' + numa +']';
		    }
		    if (vcpus) {
			res += ' [vcpus=' + vcpus +']';
		    }
		    if (cpulimit) {
			res += ' [cpulimit=' + cpulimit +']';
		    }
		    if (cpuunits) {
			res += ' [cpuunits=' + cpuunits +']';
		    }
		    return res;
		},
	    },
	    bios: {
		header: 'BIOS',
		group: 4,
		never_delete: true,
		editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
		defaultValue: '',
		iconCls: 'microchip',
		renderer: PVE.Utils.render_qemu_bios,
	    },
	    vga: {
		header: gettext('Display'),
		editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
		never_delete: true,
		iconCls: 'desktop',
		group: 5,
		defaultValue: '',
		renderer: PVE.Utils.render_kvm_vga_driver,
	    },
	    machine: {
		header: gettext('Machine'),
		editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined,
		iconCls: 'cogs',
		never_delete: true,
		group: 6,
		defaultValue: '',
		renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
		    let ostype = me.getObjectValue('ostype', undefined, pending);
		    if (PVE.Utils.is_windows(ostype) &&
			(!value || value === 'pc' || value === 'q35')) {
			return value === 'q35' ? 'pc-q35-5.1' : 'pc-i440fx-5.1';
		    }
		    return PVE.Utils.render_qemu_machine(value);
		},
	    },
	    scsihw: {
		header: gettext('SCSI Controller'),
		iconCls: 'database',
		editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
		renderer: PVE.Utils.render_scsihw,
		group: 7,
		never_delete: true,
		defaultValue: '',
	    },
	    vmstate: {
		header: gettext('Hibernation VM State'),
		iconCls: 'download',
		del_extra_msg: gettext('The saved VM state will be permanently lost.'),
		group: 100,
	    },
	    cores: {
		visible: false,
	    },
	    cpu: {
		visible: false,
	    },
	    numa: {
		visible: false,
	    },
	    balloon: {
		visible: false,
	    },
	    hotplug: {
		visible: false,
	    },
	    vcpus: {
		visible: false,
	    },
	    cpuunits: {
		visible: false,
	    },
	    cpulimit: {
		visible: false,
	    },
	    shares: {
		visible: false,
	    },
	    ostype: {
		visible: false,
	    },
	};
	PVE.Utils.forEachBus(undefined, function(type, id) {
	    let confid = type + id;
	    rows[confid] = {
		group: 10,
		iconCls: 'hdd-o',
		editor: 'PVE.qemu.HDEdit',
		isOnStorageBus: true,
		header: gettext('Hard Disk') + ' (' + confid +')',
		cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
		cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')',
	    };
	});
	for (let i = 0; i < PVE.Utils.hardware_counts.net; i++) {
	    let confid = "net" + i.toString();
	    rows[confid] = {
		group: 15,
		order: i,
		iconCls: 'exchange',
		editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
		never_delete: !caps.vms['VM.Config.Network'],
		header: gettext('Network Device') + ' (' + confid +')',
	    };
	}
	rows.efidisk0 = {
	    group: 20,
	    iconCls: 'hdd-o',
	    editor: null,
	    never_delete: !caps.vms['VM.Config.Disk'],
	    header: gettext('EFI Disk'),
	};
	rows.tpmstate0 = {
	    group: 22,
	    iconCls: 'hdd-o',
	    editor: null,
	    never_delete: !caps.vms['VM.Config.Disk'],
	    header: gettext('TPM State'),
	};
	for (let i = 0; i < PVE.Utils.hardware_counts.usb; i++) {
	    let confid = "usb" + i.toString();
	    rows[confid] = {
		group: 25,
		order: i,
		iconCls: 'usb',
		editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
		never_delete: !caps.nodes['Sys.Console'],
		header: gettext('USB Device') + ' (' + confid + ')',
	    };
	}
	for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
	    let confid = "hostpci" + i.toString();
	    rows[confid] = {
		group: 30,
		order: i,
		tdCls: 'pve-itype-icon-pci',
		never_delete: !caps.nodes['Sys.Console'],
		editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
		header: gettext('PCI Device') + ' (' + confid + ')',
	    };
	}
	for (let i = 0; i < PVE.Utils.hardware_counts.serial; i++) {
	    let confid = "serial" + i.toString();
	    rows[confid] = {
		group: 35,
		order: i,
		tdCls: 'pve-itype-icon-serial',
		never_delete: !caps.nodes['Sys.Console'],
		header: gettext('Serial Port') + ' (' + confid + ')',
	    };
	}
	rows.audio0 = {
	    group: 40,
	    iconCls: 'volume-up',
	    editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined,
	    never_delete: !caps.vms['VM.Config.HWType'],
	    header: gettext('Audio Device'),
	};
	for (let i = 0; i < 256; i++) {
	    rows["unused" + i.toString()] = {
		group: 99,
		order: i,
		iconCls: 'hdd-o',
		del_extra_msg: gettext('This will permanently erase all data.'),
		editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
		header: gettext('Unused Disk') + ' ' + i.toString(),
	    };
	}
	rows.rng0 = {
	    group: 45,
	    tdCls: 'pve-itype-icon-die',
	    editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.RNGEdit' : undefined,
	    never_delete: !caps.nodes['Sys.Console'],
	    header: gettext("VirtIO RNG"),
	};
	var sorterFn = function(rec1, rec2) {
	    var v1 = rec1.data.key;
	    var v2 = rec2.data.key;
	    var g1 = rows[v1].group || 0;
	    var g2 = rows[v2].group || 0;
	    var order1 = rows[v1].order || 0;
	    var order2 = rows[v2].order || 0;
	    if (g1 - g2 !== 0) {
		return g1 - g2;
	    }
	    if (order1 - order2 !== 0) {
		return order1 - order2;
	    }
	    if (v1 > v2) {
		return 1;
	    } else if (v1 < v2) {
	        return -1;
	    } else {
		return 0;
	    }
	};
	let baseurl = `nodes/${nodename}/qemu/${vmid}/config`;
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec || !rows[rec.data.key]?.editor) {
		return;
	    }
	    let rowdef = rows[rec.data.key];
	    let editor = rowdef.editor;
	    if (rowdef.isOnStorageBus) {
		let value = me.getObjectValue(rec.data.key, '', true);
		if (isCloudInitKey(value)) {
		    return;
		} else if (value.match(/media=cdrom/)) {
		    editor = 'PVE.qemu.CDEdit';
		} else if (!diskCap) {
		    return;
		}
	    }
	    let commonOpts = {
		autoShow: true,
		pveSelNode: me.pveSelNode,
		confid: rec.data.key,
		url: `/api2/extjs/${baseurl}`,
		listeners: {
		    destroy: () => me.reload(),
		},
	    };
	    if (Ext.isString(editor)) {
		Ext.create(editor, commonOpts);
	    } else {
		let win = Ext.createWidget(rowdef.editor.xtype, Ext.apply(commonOpts, rowdef.editor));
		win.load();
	    }
	};
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    selModel: sm,
	    disabled: true,
	    handler: run_editor,
	});
	let move_menuitem = new Ext.menu.Item({
	    text: gettext('Move Storage'),
	    tooltip: gettext('Move disk to another storage'),
	    iconCls: 'fa fa-database',
	    selModel: sm,
	    handler: () => {
		let rec = sm.getSelection()[0];
		if (!rec) {
		    return;
		}
		Ext.create('PVE.window.HDMove', {
		    autoShow: true,
		    disk: rec.data.key,
		    nodename: nodename,
		    vmid: vmid,
		    type: 'qemu',
		    listeners: {
			destroy: () => me.reload(),
		    },
		});
	    },
	});
	let reassign_menuitem = new Ext.menu.Item({
	    text: gettext('Reassign Owner'),
	    tooltip: gettext('Reassign disk to another VM'),
	    iconCls: 'fa fa-desktop',
	    selModel: sm,
	    handler: () => {
		let rec = sm.getSelection()[0];
		if (!rec) {
		    return;
		}
		Ext.create('PVE.window.GuestDiskReassign', {
		    autoShow: true,
		    disk: rec.data.key,
		    nodename: nodename,
		    vmid: vmid,
		    type: 'qemu',
		    listeners: {
			destroy: () => me.reload(),
		    },
		});
	    },
	});
	let resize_menuitem = new Ext.menu.Item({
	    text: gettext('Resize'),
	    iconCls: 'fa fa-plus',
	    selModel: sm,
	    handler: () => {
		let rec = sm.getSelection()[0];
		if (!rec) {
		    return;
		}
		Ext.create('PVE.window.HDResize', {
		    autoShow: true,
		    disk: rec.data.key,
		    nodename: nodename,
		    vmid: vmid,
		    listeners: {
			destroy: () => me.reload(),
		    },
		});
	    },
	});
	let diskaction_btn = new Proxmox.button.Button({
	    text: gettext('Disk Action'),
	    disabled: true,
	    menu: {
		items: [
		    move_menuitem,
		    reassign_menuitem,
		    resize_menuitem,
		],
	    },
	});
	let remove_btn = new Proxmox.button.Button({
	    text: gettext('Remove'),
	    defaultText: gettext('Remove'),
	    altText: gettext('Detach'),
	    selModel: sm,
	    disabled: true,
	    dangerous: true,
	    RESTMethod: 'PUT',
	    confirmMsg: function(rec) {
		let warn = gettext('Are you sure you want to remove entry {0}');
		if (this.text === this.altText) {
		    warn = gettext('Are you sure you want to detach entry {0}');
		}
		let rendered = me.renderKey(rec.data.key, {}, rec);
		let msg = Ext.String.format(warn, `'${rendered}'`);
		if (rows[rec.data.key].del_extra_msg) {
		    msg += '
' + rows[rec.data.key].del_extra_msg;
		}
		return msg;
	    },
	    handler: function(btn, e, rec) {
		Proxmox.Utils.API2Request({
		    url: '/api2/extjs/' + baseurl,
		    waitMsgTarget: me,
		    method: btn.RESTMethod,
		    params: {
			'delete': rec.data.key,
		    },
		    callback: () => me.reload(),
		    failure: response => Ext.Msg.alert('Error', response.htmlStatus),
		    success: function(response, options) {
			if (btn.RESTMethod === 'POST') {
			    Ext.create('Proxmox.window.TaskProgress', {
				autoShow: true,
				upid: response.result.data,
				listeners: {
				    destroy: () => me.reload(),
				},
			    });
			}
		    },
		});
	    },
	    listeners: {
		render: function(btn) {
		    // hack: calculate the max button width on first display to prevent the whole
		    // toolbar to move when we switch between the "Remove" and "Detach" labels
		    var def = btn.getSize().width;
		    btn.setText(btn.altText);
		    var alt = btn.getSize().width;
		    btn.setText(btn.defaultText);
		    var optimal = alt > def ? alt : def;
		    btn.setSize({ width: optimal });
		},
	    },
	});
	let revert_btn = new PVE.button.PendingRevert({
	    apiurl: '/api2/extjs/' + baseurl,
	});
	let efidisk_menuitem = Ext.create('Ext.menu.Item', {
	    text: gettext('EFI Disk'),
	    iconCls: 'fa fa-fw fa-hdd-o black',
	    disabled: !caps.vms['VM.Config.Disk'],
	    handler: function() {
		let { data: bios } = me.rstore.getData().map.bios || {};
		Ext.create('PVE.qemu.EFIDiskEdit', {
		    autoShow: true,
		    url: '/api2/extjs/' + baseurl,
		    pveSelNode: me.pveSelNode,
		    usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf',
		    listeners: {
			destroy: () => me.reload(),
		    },
		});
	    },
	});
	let counts = {};
	let isAtLimit = (type) => counts[type] >= PVE.Utils.hardware_counts[type];
	let isAtUsbLimit = () => {
	    let ostype = me.getObjectValue('ostype');
	    let machine = me.getObjectValue('machine');
	    return counts.usb >= PVE.Utils.get_max_usb_count(ostype, machine);
	};
	let set_button_status = function() {
	    let selection_model = me.getSelectionModel();
	    let rec = selection_model.getSelection()[0];
	    counts = {}; // en/disable hardwarebuttons
	    let hasCloudInit = false;
	    me.rstore.getData().items.forEach(function({ id, data }) {
		if (!hasCloudInit && (isCloudInitKey(data.value) || isCloudInitKey(data.pending))) {
		    hasCloudInit = true;
		    return;
		}
		let match = id.match(/^([^\d]+)\d+$/);
		if (match && PVE.Utils.hardware_counts[match[1]] !== undefined) {
		    let type = match[1];
		    counts[type] = (counts[type] || 0) + 1;
		}
	    });
	    // heuristic only for disabling some stuff, the backend has the final word.
	    const noSysConsolePerm = !caps.nodes['Sys.Console'];
	    const noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType'];
	    const noVMConfigNetPerm = !caps.vms['VM.Config.Network'];
	    const noVMConfigDiskPerm = !caps.vms['VM.Config.Disk'];
	    const noVMConfigCDROMPerm = !caps.vms['VM.Config.CDROM'];
	    const noVMConfigCloudinitPerm = !caps.vms['VM.Config.Cloudinit'];
	    me.down('#addUsb').setDisabled(noSysConsolePerm || isAtUsbLimit());
	    me.down('#addPci').setDisabled(noSysConsolePerm || isAtLimit('hostpci'));
	    me.down('#addAudio').setDisabled(noVMConfigHWTypePerm || isAtLimit('audio'));
	    me.down('#addSerial').setDisabled(noVMConfigHWTypePerm || isAtLimit('serial'));
	    me.down('#addNet').setDisabled(noVMConfigNetPerm || isAtLimit('net'));
	    me.down('#addRng').setDisabled(noSysConsolePerm || isAtLimit('rng'));
	    efidisk_menuitem.setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk'));
	    me.down('#addTpmState').setDisabled(noSysConsolePerm || isAtLimit('tpmstate'));
	    me.down('#addCloudinitDrive').setDisabled(noVMConfigCDROMPerm || noVMConfigCloudinitPerm || hasCloudInit);
	    if (!rec) {
		remove_btn.disable();
		edit_btn.disable();
		diskaction_btn.disable();
		revert_btn.disable();
		return;
	    }
	    const { key, value } = rec.data;
	    const row = rows[key];
	    const deleted = !!rec.data.delete;
	    const pending = deleted || me.hasPendingChanges(key);
	    const isCloudInit = isCloudInitKey(value);
	    const isCDRom = value && !!value.toString().match(/media=cdrom/);
	    const isUnusedDisk = key.match(/^unused\d+/);
	    const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom;
	    const isDisk = isUnusedDisk || isUsedDisk;
	    const isEfi = key === 'efidisk0';
	    const tpmMoveable = key === 'tpmstate0' && !me.pveSelNode.data.running;
	    let cannotDelete = deleted || row.never_delete;
	    cannotDelete ||= isCDRom && !cdromCap;
	    cannotDelete ||= isDisk && !diskCap;
	    cannotDelete ||= isCloudInit && noVMConfigCloudinitPerm;
	    remove_btn.setDisabled(cannotDelete);
	    remove_btn.setText(isUsedDisk && !isCloudInit ? remove_btn.altText : remove_btn.defaultText);
	    remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
	    edit_btn.setDisabled(
	        deleted || !row.editor || isCloudInit || (isCDRom && !cdromCap) || (isDisk && !diskCap));
	    diskaction_btn.setDisabled(
		pending ||
		!diskCap ||
		isCloudInit ||
		!(isDisk || isEfi || tpmMoveable),
	    );
	    move_menuitem.setDisabled(isUnusedDisk);
	    reassign_menuitem.setDisabled(pending || (isEfi || tpmMoveable));
	    resize_menuitem.setDisabled(pending || !isUsedDisk);
	    revert_btn.setDisabled(!pending);
	};
	let editorFactory = (classPath, extraOptions) => {
	    extraOptions = extraOptions || {};
	    return () => Ext.create(`PVE.qemu.${classPath}`, {
		autoShow: true,
		url: `/api2/extjs/${baseurl}`,
		pveSelNode: me.pveSelNode,
		listeners: {
		    destroy: () => me.reload(),
		},
		isAdd: true,
		isCreate: true,
		...extraOptions,
	    });
	};
	Ext.apply(me, {
	    url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
	    interval: 5000,
	    selModel: sm,
	    run_editor: run_editor,
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: new Ext.menu.Menu({
			cls: 'pve-add-hw-menu',
			items: [
			    {
				text: gettext('Hard Disk'),
				iconCls: 'fa fa-fw fa-hdd-o black',
				disabled: !caps.vms['VM.Config.Disk'],
				handler: editorFactory('HDEdit'),
			    },
			    {
				text: gettext('CD/DVD Drive'),
				iconCls: 'pve-itype-icon-cdrom',
				disabled: !caps.vms['VM.Config.CDROM'],
				handler: editorFactory('CDEdit'),
			    },
			    {
				text: gettext('Network Device'),
				itemId: 'addNet',
				iconCls: 'fa fa-fw fa-exchange black',
				disabled: !caps.vms['VM.Config.Network'],
				handler: editorFactory('NetworkEdit'),
			    },
			    efidisk_menuitem,
			    {
				text: gettext('TPM State'),
				itemId: 'addTpmState',
				iconCls: 'fa fa-fw fa-hdd-o black',
				disabled: !caps.vms['VM.Config.Disk'],
				handler: editorFactory('TPMDiskEdit'),
			    },
			    {
				text: gettext('USB Device'),
				itemId: 'addUsb',
				iconCls: 'fa fa-fw fa-usb black',
				disabled: !caps.nodes['Sys.Console'],
				handler: editorFactory('USBEdit'),
			    },
			    {
				text: gettext('PCI Device'),
				itemId: 'addPci',
				iconCls: 'pve-itype-icon-pci',
				disabled: !caps.nodes['Sys.Console'],
				handler: editorFactory('PCIEdit'),
			    },
			    {
				text: gettext('Serial Port'),
				itemId: 'addSerial',
				iconCls: 'pve-itype-icon-serial',
				disabled: !caps.vms['VM.Config.Options'],
				handler: editorFactory('SerialEdit'),
			    },
			    {
				text: gettext('CloudInit Drive'),
				itemId: 'addCloudinitDrive',
				iconCls: 'fa fa-fw fa-cloud black',
				disabled: !caps.vms['VM.Config.CDROM'] || !caps.vms['VM.Config.Cloudinit'],
				handler: editorFactory('CIDriveEdit'),
			    },
			    {
				text: gettext('Audio Device'),
				itemId: 'addAudio',
				iconCls: 'fa fa-fw fa-volume-up black',
				disabled: !caps.vms['VM.Config.HWType'],
				handler: editorFactory('AudioEdit'),
			    },
			    {
				text: gettext("VirtIO RNG"),
				itemId: 'addRng',
				iconCls: 'pve-itype-icon-die',
				disabled: !caps.nodes['Sys.Console'],
				handler: editorFactory('RNGEdit'),
			    },
			],
		    }),
		},
		remove_btn,
		edit_btn,
		diskaction_btn,
		revert_btn,
	    ],
	    rows: rows,
	    sorterFn: sorterFn,
	    listeners: {
		itemdblclick: run_editor,
		selectionchange: set_button_status,
	    },
	});
	me.callParent();
	me.on('activate', me.rstore.startUpdate, me.rstore);
	me.on('destroy', me.rstore.stopUpdate, me.rstore);
	me.mon(me.getStore(), 'datachanged', set_button_status, me);
    },
});
Ext.define('PVE.qemu.IPConfigPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveIPConfigPanel',
    insideWizard: false,
    vmconfig: {},
    onGetValues: function(values) {
	var me = this;
	if (values.ipv4mode !== 'static') {
	    values.ip = values.ipv4mode;
	}
	if (values.ipv6mode !== 'static') {
	    values.ip6 = values.ipv6mode;
	}
	var params = {};
	var cfg = PVE.Parser.printIPConfig(values);
	if (cfg === '') {
	    params.delete = [me.confid];
	} else {
	    params[me.confid] = cfg;
	}
	return params;
    },
    setVMConfig: function(config) {
	var me = this;
	me.vmconfig = config;
    },
    setIPConfig: function(confid, data) {
	var me = this;
	me.confid = confid;
	if (data.ip === 'dhcp') {
	    data.ipv4mode = data.ip;
	    data.ip = '';
	} else {
	    data.ipv4mode = 'static';
	}
	if (data.ip6 === 'dhcp' || data.ip6 === 'auto') {
	    data.ipv6mode = data.ip6;
	    data.ip6 = '';
	} else {
	    data.ipv6mode = 'static';
	}
	me.ipconfig = data;
	me.setValues(me.ipconfig);
    },
    initComponent: function() {
	var me = this;
	me.ipconfig = {};
	me.column1 = [
	    {
		xtype: 'displayfield',
		fieldLabel: gettext('Network Device'),
		value: me.netid,
	    },
	    {
		layout: {
		    type: 'hbox',
		    align: 'middle',
		},
		border: false,
		margin: '0 0 5 0',
		items: [
		    {
			xtype: 'label',
			text: gettext('IPv4') + ':',
		    },
		    {
			xtype: 'radiofield',
			boxLabel: gettext('Static'),
			name: 'ipv4mode',
			inputValue: 'static',
			checked: false,
			margin: '0 0 0 10',
			listeners: {
			    change: function(cb, value) {
				me.down('field[name=ip]').setDisabled(!value);
				me.down('field[name=gw]').setDisabled(!value);
			    },
			},
		    },
		    {
			xtype: 'radiofield',
			boxLabel: gettext('DHCP'),
			name: 'ipv4mode',
			inputValue: 'dhcp',
			checked: false,
			margin: '0 0 0 10',
		    },
		],
	    },
	    {
		xtype: 'textfield',
		name: 'ip',
		vtype: 'IPCIDRAddress',
		value: '',
		disabled: true,
		fieldLabel: gettext('IPv4/CIDR'),
	    },
	    {
		xtype: 'textfield',
		name: 'gw',
		value: '',
		vtype: 'IPAddress',
		disabled: true,
		fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')',
	    },
	];
	me.column2 = [
	    {
		xtype: 'displayfield',
	    },
	    {
		layout: {
		    type: 'hbox',
		    align: 'middle',
		},
		border: false,
		margin: '0 0 5 0',
		items: [
		    {
			xtype: 'label',
			text: gettext('IPv6') + ':',
		    },
		    {
			xtype: 'radiofield',
			boxLabel: gettext('Static'),
			name: 'ipv6mode',
			inputValue: 'static',
			checked: false,
			margin: '0 0 0 10',
			listeners: {
			    change: function(cb, value) {
				me.down('field[name=ip6]').setDisabled(!value);
				me.down('field[name=gw6]').setDisabled(!value);
			    },
			},
		    },
		    {
			xtype: 'radiofield',
			boxLabel: gettext('DHCP'),
			name: 'ipv6mode',
			inputValue: 'dhcp',
			checked: false,
			margin: '0 0 0 10',
		    },
		    {
			xtype: 'radiofield',
			boxLabel: gettext('SLAAC'),
			name: 'ipv6mode',
			inputValue: 'auto',
			checked: false,
			margin: '0 0 0 10',
		    },
		],
	    },
	    {
		xtype: 'textfield',
		name: 'ip6',
		value: '',
		vtype: 'IP6CIDRAddress',
		disabled: true,
		fieldLabel: gettext('IPv6/CIDR'),
	    },
	    {
		xtype: 'textfield',
		name: 'gw6',
		vtype: 'IP6Address',
		value: '',
		disabled: true,
		fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')',
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.qemu.IPConfigEdit', {
    extend: 'Proxmox.window.Edit',
    isAdd: true,
    initComponent: function() {
	var me = this;
	// convert confid from netX to ipconfigX
	var match = me.confid.match(/^net(\d+)$/);
	if (match) {
	    me.netid = me.confid;
	    me.confid = 'ipconfig' + match[1];
	}
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	me.isCreate = !me.confid;
	var ipanel = Ext.create('PVE.qemu.IPConfigPanel', {
	    confid: me.confid,
	    netid: me.netid,
	    nodename: nodename,
	});
	Ext.applyIf(me, {
	    subject: gettext('Network Config'),
	    items: ipanel,
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		me.vmconfig = response.result.data;
		var ipconfig = {};
		var value = me.vmconfig[me.confid];
		if (value) {
		    ipconfig = PVE.Parser.parseIPConfig(me.confid, value);
		    if (!ipconfig) {
			Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration'));
			me.close();
			return;
		    }
		}
		ipanel.setIPConfig(me.confid, ipconfig);
		ipanel.setVMConfig(me.vmconfig);
	    },
	});
    },
});
Ext.define('PVE.qemu.KeyboardEdit', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	Ext.applyIf(me, {
	    subject: gettext('Keyboard Layout'),
	    items: {
		xtype: 'VNCKeyboardSelector',
		name: 'keyboard',
		value: '__default__',
		fieldLabel: gettext('Keyboard Layout'),
	    },
	});
	me.callParent();
	me.load();
    },
});
Ext.define('PVE.qemu.MachineInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveMachineInputPanel',
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    'combobox[name=machine]': {
		change: 'onMachineChange',
	    },
	},
	onMachineChange: function(field, value) {
	    let me = this;
	    let version = me.lookup('version');
	    let store = version.getStore();
	    let oldRec = store.findRecord('id', version.getValue(), 0, false, false, true);
	    let type = value === 'q35' ? 'q35' : 'i440fx';
	    store.clearFilter();
	    store.addFilter(val => val.data.id === 'latest' || val.data.type === type);
	    if (!me.getView().isWindows) {
		version.setValue('latest');
	    } else {
		store.isWindows = true;
		if (!oldRec) {
		    return;
		}
		let oldVers = oldRec.data.version;
		// we already filtered by correct type, so just check version property
		let rec = store.findRecord('version', oldVers, 0, false, false, true);
		if (rec) {
		    version.select(rec);
		}
	    }
	},
    },
    onGetValues: function(values) {
	if (values.version && values.version !== 'latest') {
	    values.machine = values.version;
	    delete values.delete;
	}
	delete values.version;
	return values;
    },
    setValues: function(values) {
	let me = this;
	me.isWindows = values.isWindows;
	if (values.machine === 'pc') {
	    values.machine = '__default__';
	}
	if (me.isWindows) {
	    if (values.machine === '__default__') {
		values.version = 'pc-i440fx-5.1';
	    } else if (values.machine === 'q35') {
		values.version = 'pc-q35-5.1';
	    }
	}
	if (values.machine !== '__default__' && values.machine !== 'q35') {
	    values.version = values.machine;
	    values.machine = values.version.match(/q35/) ? 'q35' : '__default__';
	    // avoid hiding a pinned version
	    me.setAdvancedVisible(true);
	}
	this.callParent(arguments);
    },
    items: {
	xtype: 'proxmoxKVComboBox',
	name: 'machine',
	reference: 'machine',
	fieldLabel: gettext('Machine'),
	comboItems: [
	    ['__default__', PVE.Utils.render_qemu_machine('')],
	    ['q35', 'q35'],
	],
    },
    advancedItems: [
	{
	    xtype: 'combobox',
	    name: 'version',
	    reference: 'version',
	    fieldLabel: gettext('Version'),
	    emptyText: gettext('Latest'),
	    value: 'latest',
	    editable: false,
	    valueField: 'id',
	    displayField: 'version',
	    queryParam: false,
	    store: {
		autoLoad: true,
		fields: ['id', 'type', 'version'],
		proxy: {
		    type: 'proxmox',
		    url: "/api2/json/nodes/localhost/capabilities/qemu/machines",
		},
		listeners: {
		    load: function(records) {
			if (!this.isWindows) {
			    this.insert(0, { id: 'latest', type: 'any', version: gettext('Latest') });
			}
		    },
		},
	    },
	},
	{
	    xtype: 'displayfield',
	    fieldLabel: gettext('Note'),
	    value: gettext('Machine version change may affect hardware layout and settings in the guest OS.'),
	},
    ],
});
Ext.define('PVE.qemu.MachineEdit', {
    extend: 'Proxmox.window.Edit',
    subject: gettext('Machine'),
    items: {
	xtype: 'pveMachineInputPanel',
    },
    width: 400,
    initComponent: function() {
	let me = this;
	me.callParent();
	me.load({
	    success: function(response) {
		let conf = response.result.data;
		let values = {
		    machine: conf.machine || '__default__',
		};
		values.isWindows = PVE.Utils.is_windows(conf.ostype);
		me.setValues(values);
	    },
	});
    },
});
Ext.define('PVE.qemu.MemoryInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveQemuMemoryPanel',
    onlineHelp: 'qm_memory',
    insideWizard: false,
    viewModel: {}, // inherit data from createWizard if insideWizard
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    '#': {
		afterrender: 'setMemory',
	    },
	},
	setMemory: function() {
	    let me = this;
	    let view = me.getView(), viewModel = me.getViewModel();
	    if (view.insideWizard) {
		let memory = view.down('pveMemoryField[name=memory]');
		// NOTE: we only set memory but that then sets balloon in its change handler
		if (viewModel.get('current.ostype') === 'win11') {
		    memory.setValue('4096');
		} else {
		    memory.setValue('2048');
		}
	    }
	},
    },
    onGetValues: function(values) {
	var me = this;
	var res = {};
	res.memory = values.memory;
	res.balloon = values.balloon;
	if (!values.ballooning) {
	    res.balloon = 0;
	    res.delete = 'shares';
	} else if (values.memory === values.balloon) {
	    delete res.balloon;
	    res.delete = 'balloon,shares';
	} else if (Ext.isDefined(values.shares) && values.shares !== "") {
	    res.shares = values.shares;
	} else {
	    res.delete = "shares";
	}
	return res;
    },
    initComponent: function() {
	var me = this;
	var labelWidth = 160;
	me.items= [
	    {
		xtype: 'pveMemoryField',
		labelWidth: labelWidth,
		fieldLabel: gettext('Memory') + ' (MiB)',
		name: 'memory',
		value: '512', // better defaults get set via the view controllers afterrender
		minValue: 1,
		step: 32,
		hotplug: me.hotplug,
		listeners: {
		    change: function(f, value, old) {
			var bf = me.down('field[name=balloon]');
			var balloon = bf.getValue();
			bf.setMaxValue(value);
			if (balloon === old) {
			    bf.setValue(value);
			}
			bf.validate();
		    },
		},
	    },
	];
	me.advancedItems= [
	    {
		xtype: 'pveMemoryField',
		name: 'balloon',
		minValue: 1,
		maxValue: me.insideWizard ? 2048 : 512,
		value: '512', // better defaults get set (indirectly) via the view controllers afterrender
		step: 32,
		fieldLabel: gettext('Minimum memory') + ' (MiB)',
		hotplug: me.hotplug,
		labelWidth: labelWidth,
		allowBlank: false,
		listeners: {
		    change: function(f, value) {
			var memory = me.down('field[name=memory]').getValue();
			var shares = me.down('field[name=shares]');
			shares.setDisabled(value === memory);
		    },
		},
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'shares',
		disabled: true,
		minValue: 0,
		maxValue: 50000,
		value: '',
		step: 10,
		fieldLabel: gettext('Shares'),
		labelWidth: labelWidth,
		allowBlank: true,
		emptyText: Proxmox.Utils.defaultText + ' (1000)',
		submitEmptyText: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		labelWidth: labelWidth,
		value: '1',
		name: 'ballooning',
		fieldLabel: gettext('Ballooning Device'),
		listeners: {
		    change: function(f, value) {
			var bf = me.down('field[name=balloon]');
			var shares = me.down('field[name=shares]');
			var memory = me.down('field[name=memory]');
			bf.setDisabled(!value);
			shares.setDisabled(!value || bf.getValue() === memory.getValue());
		    },
		},
	    },
	];
	if (me.insideWizard) {
	    me.column1 = me.items;
	    me.items = undefined;
	    me.advancedColumn1 = me.advancedItems;
	    me.advancedItems = undefined;
	}
	me.callParent();
    },
});
Ext.define('PVE.qemu.MemoryEdit', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	var memoryhotplug;
	if (me.hotplug) {
	    Ext.each(me.hotplug.split(','), function(el) {
		if (el === 'memory') {
		    memoryhotplug = 1;
	        }
	    });
	}
	var ipanel = Ext.create('PVE.qemu.MemoryInputPanel', {
	    hotplug: memoryhotplug,
	});
	Ext.apply(me, {
	    subject: gettext('Memory'),
	    items: [ipanel],
	    // uncomment the following to use the async configiguration API
	    // backgroundDelay: 5,
	    width: 400,
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		var data = response.result.data;
		var values = {
		    ballooning: data.balloon === 0 ? '0' : '1',
		    shares: data.shares,
		    memory: data.memory || '512',
		    balloon: data.balloon > 0 ? data.balloon : data.memory || '512',
		};
		ipanel.setValues(values);
	    },
	});
    },
});
Ext.define('PVE.qemu.Monitor', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveQemuMonitor',
    // start to trim saved command output once there are *both*, more than `commandLimit` commands
    // executed and the total of saved in+output is over `lineLimit` lines; repeat by dropping one
    // full command output until either condition is false again
    commandLimit: 10,
    lineLimit: 5000,
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = me.pveSelNode.data.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	var history = [];
	var histNum = -1;
	let commands = [];
	var textbox = Ext.createWidget('panel', {
	    region: 'center',
	    xtype: 'panel',
	    autoScroll: true,
	    border: true,
	    margins: '5 5 5 5',
	    bodyStyle: 'font-family: monospace;',
	});
	var scrollToEnd = function() {
	    var el = textbox.getTargetEl();
	    var dom = Ext.getDom(el);
	    var clientHeight = dom.clientHeight;
	    // BrowserBug: clientHeight reports 0 in IE9 StrictMode
            // Instead we are using offsetHeight and hardcoding borders
            if (Ext.isIE9 && Ext.isStrict) {
		clientHeight = dom.offsetHeight + 2;
            }
	    dom.scrollTop = dom.scrollHeight - clientHeight;
	};
	var refresh = function() {
	    textbox.update(`${commands.flat(2).join('\n')}`);
	    scrollToEnd();
	};
	let recordInput = line => {
	    commands.push([line]);
	    // drop oldest commands and their output until we're not over both limits anymore
	    while (commands.length > me.commandLimit && commands.flat(2).length > me.lineLimit) {
		commands.shift();
	    }
	};
	let addResponse = lines => commands[commands.length - 1].push(lines);
	var executeCmd = function(cmd) {
	    recordInput("# " + Ext.htmlEncode(cmd), true);
	    if (cmd) {
		history.unshift(cmd);
		if (history.length > 20) {
		    history.splice(20);
		}
	    }
	    histNum = -1;
	    refresh();
	    Proxmox.Utils.API2Request({
		params: { command: cmd },
		url: '/nodes/' + nodename + '/qemu/' + vmid + "/monitor",
		method: 'POST',
		waitMsgTarget: me,
		success: function(response, opts) {
		    var res = response.result.data;
		    addResponse(res.split('\n').map(line => Ext.htmlEncode(line)));
		    refresh();
		},
		failure: function(response, opts) {
		    Ext.Msg.alert('Error', response.htmlStatus);
		},
	    });
	};
	Ext.apply(me, {
	    layout: { type: 'border' },
	    border: false,
	    items: [
		textbox,
		{
		    region: 'south',
		    margins: '0 5 5 5',
		    border: false,
		    xtype: 'textfield',
		    name: 'cmd',
		    value: '',
		    fieldStyle: 'font-family: monospace;',
		    allowBlank: true,
		    listeners: {
			afterrender: function(f) {
			    f.focus(false);
			    recordInput("Type 'help' for help.");
			    refresh();
			},
			specialkey: function(f, e) {
			    var key = e.getKey();
			    switch (key) {
				case e.ENTER:
				    var cmd = f.getValue();
				    f.setValue('');
				    executeCmd(cmd);
				    break;
				case e.PAGE_UP:
				    textbox.scrollBy(0, -0.9*textbox.getHeight(), false);
				    break;
				case e.PAGE_DOWN:
				    textbox.scrollBy(0, 0.9*textbox.getHeight(), false);
				    break;
				case e.UP:
				    if (histNum + 1 < history.length) {
					f.setValue(history[++histNum]);
				    }
				    e.preventDefault();
				    break;
				case e.DOWN:
				    if (histNum > 0) {
					f.setValue(history[--histNum]);
				    }
				    e.preventDefault();
				    break;
				default:
				    break;
			    }
			},
		    },
		},
	    ],
	    listeners: {
		show: function() {
		    var field = me.query('textfield[name="cmd"]')[0];
		    field.focus(false, true);
		},
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.qemu.MultiHDPanel', {
    extend: 'PVE.panel.MultiDiskPanel',
    alias: 'widget.pveMultiHDPanel',
    onlineHelp: 'qm_hard_disk',
    controller: {
	xclass: 'Ext.app.ViewController',
	// maxCount is the sum of all controller ids - 1 (ide2 is fixed in the wizard)
	maxCount: Object.values(PVE.Utils.diskControllerMaxIDs)
		.reduce((previous, current) => previous+current, 0) - 1,
	getNextFreeDisk: function(vmconfig) {
	    let clist = PVE.Utils.sortByPreviousUsage(vmconfig);
	    return PVE.Utils.nextFreeDisk(clist, vmconfig);
	},
	addPanel: function(itemId, vmconfig, nextFreeDisk) {
	    let me = this;
	    return me.getView().add({
		vmconfig,
		border: false,
		showAdvanced: Ext.state.Manager.getProvider().get('proxmox-advanced-cb'),
		xtype: 'pveQemuHDInputPanel',
		bind: {
		    nodename: '{nodename}',
		},
		padding: '0 0 0 5',
		itemId,
		isCreate: true,
		insideWizard: true,
	    });
	},
	getBaseVMConfig: function() {
	    let me = this;
	    let vm = me.getViewModel();
	    return {
		ide2: 'media=cdrom',
		scsihw: vm.get('current.scsihw'),
		ostype: vm.get('current.ostype'),
	    };
	},
	diskSorter: {
	    sorterFn: function(rec1, rec2) {
		let [, name1, id1] = PVE.Utils.bus_match.exec(rec1.data.name);
		let [, name2, id2] = PVE.Utils.bus_match.exec(rec2.data.name);
		if (name1 === name2) {
		    return parseInt(id1, 10) - parseInt(id2, 10);
		}
		return name1 < name2 ? -1 : 1;
	    },
	},
	deleteDisabled: () => false,
    },
});
Ext.define('PVE.qemu.NetworkInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveQemuNetworkInputPanel',
    onlineHelp: 'qm_network_device',
    insideWizard: false,
    onGetValues: function(values) {
	var me = this;
	me.network.model = values.model;
	if (values.nonetwork) {
	    return {};
	} else {
	    me.network.bridge = values.bridge;
	    me.network.tag = values.tag;
	    me.network.firewall = values.firewall;
	}
	me.network.macaddr = values.macaddr;
	me.network.disconnect = values.disconnect;
	me.network.queues = values.queues;
	me.network.mtu = values.mtu;
	if (values.rate) {
	    me.network.rate = values.rate;
	} else {
	    delete me.network.rate;
	}
	var params = {};
	params[me.confid] = PVE.Parser.printQemuNetwork(me.network);
	return params;
    },
    viewModel: {
	data: {
	    networkModel: undefined,
	    mtu: '',
	},
	formulas: {
	    isVirtio: get => get('networkModel') === 'virtio',
	    showMtuHint: get => get('mtu') === 1,
	},
    },
    setNetwork: function(confid, data) {
	var me = this;
	me.confid = confid;
	if (data) {
	    data.networkmode = data.bridge ? 'bridge' : 'nat';
	} else {
	    data = {};
	    data.networkmode = 'bridge';
	}
	me.network = data;
	me.setValues(me.network);
    },
    setNodename: function(nodename) {
	var me = this;
	me.bridgesel.setNodename(nodename);
    },
    initComponent: function() {
	var me = this;
	me.network = {};
	me.confid = 'net0';
	me.column1 = [];
	me.column2 = [];
	me.bridgesel = Ext.create('PVE.form.BridgeSelector', {
	    name: 'bridge',
	    fieldLabel: gettext('Bridge'),
	    nodename: me.nodename,
	    autoSelect: true,
	    allowBlank: false,
	});
	me.column1 = [
	    me.bridgesel,
	    {
		xtype: 'pveVlanField',
		name: 'tag',
		value: '',
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Firewall'),
		name: 'firewall',
		checked: me.insideWizard || me.isCreate,
	    },
	];
	me.advancedColumn1 = [
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Disconnect'),
		name: 'disconnect',
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'mtu',
		fieldLabel: 'MTU',
		bind: {
		    disabled: '{!isVirtio}',
		    value: '{mtu}',
		},
		emptyText: '1500 (1 = bridge MTU)',
		minValue: 1,
		maxValue: 65520,
		allowBlank: true,
		validator: val => val === '' || val >= 576 || val === '1'
		    ? true
		    : gettext('MTU needs to be >= 576 or 1 to inherit the MTU from the underlying bridge.'),
	    },
	];
	if (me.insideWizard) {
	    me.column1.unshift({
		xtype: 'checkbox',
		name: 'nonetwork',
		inputValue: 'none',
		boxLabel: gettext('No network device'),
		listeners: {
		    change: function(cb, value) {
			var fields = [
			    'disconnect',
			    'bridge',
			    'tag',
			    'firewall',
			    'model',
			    'macaddr',
			    'rate',
			    'queues',
			    'mtu',
			];
			fields.forEach(function(fieldname) {
			    me.down('field[name='+fieldname+']').setDisabled(value);
			});
			me.down('field[name=bridge]').validate();
		    },
		},
	    });
	    me.column2.unshift({
		xtype: 'displayfield',
	    });
	}
	me.column2.push(
	    {
		xtype: 'pveNetworkCardSelector',
		name: 'model',
		fieldLabel: gettext('Model'),
		bind: '{networkModel}',
		value: PVE.qemu.OSDefaults.generic.networkCard,
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'macaddr',
		fieldLabel: gettext('MAC address'),
		vtype: 'MacAddress',
		allowBlank: true,
		emptyText: 'auto',
	    });
	me.advancedColumn2 = [
	    {
		xtype: 'numberfield',
		name: 'rate',
		fieldLabel: gettext('Rate limit') + ' (MB/s)',
		minValue: 0,
		maxValue: 10*1024,
		value: '',
		emptyText: 'unlimited',
		allowBlank: true,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'queues',
		fieldLabel: 'Multiqueue',
		minValue: 1,
		maxValue: 64,
		value: '',
		allowBlank: true,
	    },
	];
	me.advancedColumnB = [
	    {
		xtype: 'displayfield',
		userCls: 'pmx-hint',
		value: gettext("Use the special value '1' to inherit the MTU value from the underlying bridge"),
		bind: {
		    hidden: '{!showMtuHint}',
		},
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.qemu.NetworkEdit', {
    extend: 'Proxmox.window.Edit',
    isAdd: true,
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	me.isCreate = !me.confid;
	var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', {
	    confid: me.confid,
	    nodename: nodename,
	    isCreate: me.isCreate,
	});
	Ext.applyIf(me, {
	    subject: gettext('Network Device'),
	    items: ipanel,
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		var i, confid;
		me.vmconfig = response.result.data;
		if (!me.isCreate) {
		    var value = me.vmconfig[me.confid];
		    var network = PVE.Parser.parseQemuNetwork(me.confid, value);
		    if (!network) {
			Ext.Msg.alert(gettext('Error'), 'Unable to parse network options');
			me.close();
			return;
		    }
		    ipanel.setNetwork(me.confid, network);
		} else {
		    for (i = 0; i < 100; i++) {
			confid = 'net' + i.toString();
			if (!Ext.isDefined(me.vmconfig[confid])) {
			    me.confid = confid;
			    break;
			}
		    }
		    let ostype = me.vmconfig.ostype;
		    let defaults = PVE.qemu.OSDefaults.getDefaults(ostype);
		    let data = {
			model: defaults.networkCard,
		    };
		    ipanel.setNetwork(me.confid, data);
		}
	    },
	});
    },
});
/*
 * This class holds performance *recommended* settings for the PVE Qemu wizards
 * the *mandatory* settings are set in the PVE::QemuServer
 * config_to_command sub
 * We store this here until we get the data from the API server
*/
// this is how you would add an hypothetic FreeBSD > 10 entry
//
//virtio-blk is stable but virtIO net still
//   problematic as of 10.3
// see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=165059
//	addOS({
//	    parent: 'generic', // inherits defaults
//	    pveOS: 'freebsd10', // must match a radiofield in OSTypeEdit.js
//	    busType: 'virtio' // must match a pveBusController value
//			    // networkCard muss match a pveNetworkCardSelector
Ext.define('PVE.qemu.OSDefaults', {
    singleton: true, // will also force creation when loaded
    constructor: function() {
	let me = this;
	let addOS = function(settings) {
		if (Object.prototype.hasOwnProperty.call(settings, 'parent')) {
		    var child = Ext.clone(me[settings.parent]);
		    me[settings.pveOS] = Ext.apply(child, settings);
		} else {
		    throw "Could not find your genitor";
		}
	    };
	// default values
	me.generic = {
	    busType: 'ide',
	    networkCard: 'e1000',
	    busPriority: {
		    ide: 4,
		    sata: 3,
		    scsi: 2,
		    virtio: 1,
	    },
	    scsihw: 'virtio-scsi-single',
	};
       // virtio-net is in kernel since 2.6.25
       // virtio-scsi since 3.2 but backported in RHEL with 2.6 kernel
	addOS({
	    pveOS: 'l26',
	    parent: 'generic',
	    busType: 'scsi',
	    busPriority: {
		    scsi: 4,
		    virtio: 3,
		    sata: 2,
		    ide: 1,
	    },
	    networkCard: 'virtio',
	});
	// recommandation from http://wiki.qemu.org/Windows2000
	addOS({
	    pveOS: 'w2k',
	    parent: 'generic',
	    networkCard: 'rtl8139',
	    scsihw: '',
	});
	// https://pve.proxmox.com/wiki/Windows_XP_Guest_Notes
	addOS({
	    pveOS: 'wxp',
	    parent: 'w2k',
	});
	me.getDefaults = function(ostype) {
	    if (PVE.qemu.OSDefaults[ostype]) {
		return PVE.qemu.OSDefaults[ostype];
	    } else {
		return PVE.qemu.OSDefaults.generic;
	    }
	};
    },
});
Ext.define('PVE.qemu.OSTypeInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveQemuOSTypePanel',
    onlineHelp: 'qm_os_settings',
    insideWizard: false,
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    'combobox[name=osbase]': {
		change: 'onOSBaseChange',
	    },
	    'combobox[name=ostype]': {
		afterrender: 'onOSTypeChange',
		change: 'onOSTypeChange',
	    },
	},
	onOSBaseChange: function(field, value) {
	    this.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]);
	},
	onOSTypeChange: function(field) {
	    var me = this, ostype = field.getValue();
	    if (!me.getView().insideWizard) {
		return;
	    }
	    var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype);
	    me.setWidget('pveBusSelector', targetValues.busType);
	    me.setWidget('pveNetworkCardSelector', targetValues.networkCard);
	    var scsihw = targetValues.scsihw || '__default__';
	    this.getViewModel().set('current.scsihw', scsihw);
	    this.getViewModel().set('current.ostype', ostype);
	},
	setWidget: function(widget, newValue) {
	    // changing a widget is safe only if ComponentQuery.query returns us
	    // a single value array
	    var widgets = Ext.ComponentQuery.query('pveQemuCreateWizard ' + widget);
	    if (widgets.length === 1) {
		widgets[0].setValue(newValue);
	    } else {
		// ignore multiple disks, we only want to set the type if there is a single disk
	    }
	},
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: 'displayfield',
		value: gettext('Guest OS') + ':',
		hidden: !me.insideWizard,
	    },
	    {
		xtype: 'combobox',
		submitValue: false,
		name: 'osbase',
		fieldLabel: gettext('Type'),
		editable: false,
		queryMode: 'local',
		value: 'Linux',
		store: Object.keys(PVE.Utils.kvm_ostypes),
	    },
	    {
		xtype: 'combobox',
		name: 'ostype',
		reference: 'ostype',
		fieldLabel: gettext('Version'),
		value: 'l26',
		allowBlank: false,
		editable: false,
		queryMode: 'local',
		valueField: 'val',
		displayField: 'desc',
		store: {
		    fields: ['desc', 'val'],
		    data: PVE.Utils.kvm_ostypes.Linux,
		    listeners: {
			datachanged: function(store) {
			    var ostype = me.lookup('ostype');
			    var old_val = ostype.getValue();
			    if (!me.insideWizard && old_val && store.find('val', old_val) !== -1) {
				ostype.setValue(old_val);
			    } else {
				ostype.setValue(store.getAt(0));
			    }
			},
		    },
		},
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.qemu.OSTypeEdit', {
    extend: 'Proxmox.window.Edit',
    subject: 'OS Type',
    items: [{ xtype: 'pveQemuOSTypePanel' }],
    initComponent: function() {
	var me = this;
	me.callParent();
	me.load({
	    success: function(response, options) {
		var value = response.result.data.ostype || 'other';
		var osinfo = PVE.Utils.get_kvm_osinfo(value);
		me.setValues({ ostype: value, osbase: osinfo.base });
	    },
	});
    },
});
Ext.define('PVE.qemu.Options', {
    extend: 'Proxmox.grid.PendingObjectGrid',
    alias: ['widget.PVE.qemu.Options'],
    onlineHelp: 'qm_options',
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var vmid = me.pveSelNode.data.vmid;
	if (!vmid) {
	    throw "no VM ID specified";
	}
	var caps = Ext.state.Manager.get('GuiCap');
	var rows = {
	    name: {
		required: true,
		defaultValue: me.pveSelNode.data.name,
		header: gettext('Name'),
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Name'),
		    items: {
			xtype: 'inputpanel',
			items: {
			    xtype: 'textfield',
			    name: 'name',
			    vtype: 'DnsName',
			    value: '',
			    fieldLabel: gettext('Name'),
			    allowBlank: true,
			},
			onGetValues: function(values) {
			    var params = values;
			    if (values.name === undefined ||
				values.name === null ||
				values.name === '') {
				params = { 'delete': 'name' };
			    }
			    return params;
			},
		    },
		} : undefined,
	    },
	    onboot: {
		header: gettext('Start at boot'),
		defaultValue: '',
		renderer: Proxmox.Utils.format_boolean,
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Start at boot'),
		    items: {
			xtype: 'proxmoxcheckbox',
			name: 'onboot',
			uncheckedValue: 0,
			defaultValue: 0,
			deleteDefaultValue: true,
			fieldLabel: gettext('Start at boot'),
		    },
		} : undefined,
	    },
	    startup: {
		header: gettext('Start/Shutdown order'),
		defaultValue: '',
		renderer: PVE.Utils.render_kvm_startup,
		editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify']
		    ? {
			xtype: 'pveWindowStartupEdit',
			onlineHelp: 'qm_startup_and_shutdown',
		    } : undefined,
	    },
	    ostype: {
		header: gettext('OS Type'),
		editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.OSTypeEdit' : undefined,
		renderer: PVE.Utils.render_kvm_ostype,
		defaultValue: 'other',
	    },
	    bootdisk: {
		visible: false,
	    },
	    boot: {
		header: gettext('Boot Order'),
		defaultValue: 'cdn',
		editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined,
		multiKey: ['boot', 'bootdisk'],
		renderer: function(order, metaData, record, rowIndex, colIndex, store, pending) {
		    if (/^\s*$/.test(order)) {
			return gettext('(No boot device selected)');
		    }
		    let boot = PVE.Parser.parsePropertyString(order, "legacy");
		    if (boot.order) {
			let list = boot.order.split(';');
			let ret = '';
			list.forEach(dev => {
			    if (ret) {
				ret += ', ';
			    }
			    ret += dev;
			});
			return ret;
		    }
		    // legacy style and fallback
		    let i;
		    var text = '';
		    var bootdisk = me.getObjectValue('bootdisk', undefined, pending);
		    order = boot.legacy || 'cdn';
		    for (i = 0; i < order.length; i++) {
			if (text) {
			    text += ', ';
			}
			var sel = order.substring(i, i + 1);
			if (sel === 'c') {
			    if (bootdisk) {
				text += bootdisk;
			    } else {
				text += gettext('first disk');
			    }
			} else if (sel === 'n') {
			    text += gettext('any net');
			} else if (sel === 'a') {
			    text += gettext('Floppy');
			} else if (sel === 'd') {
			    text += gettext('any CD-ROM');
			} else {
			    text += sel;
			}
		    }
		    return text;
		},
	    },
	    tablet: {
		header: gettext('Use tablet for pointer'),
		defaultValue: true,
		renderer: Proxmox.Utils.format_boolean,
		editor: caps.vms['VM.Config.HWType'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Use tablet for pointer'),
		    items: {
			xtype: 'proxmoxcheckbox',
			name: 'tablet',
			checked: true,
			uncheckedValue: 0,
			defaultValue: 1,
			deleteDefaultValue: true,
			fieldLabel: gettext('Enabled'),
		    },
		} : undefined,
	    },
	    hotplug: {
		header: gettext('Hotplug'),
		defaultValue: 'disk,network,usb',
		renderer: PVE.Utils.render_hotplug_features,
		editor: caps.vms['VM.Config.HWType'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Hotplug'),
		    items: {
			xtype: 'pveHotplugFeatureSelector',
			name: 'hotplug',
			value: '',
			multiSelect: true,
			fieldLabel: gettext('Hotplug'),
			allowBlank: true,
		    },
		} : undefined,
	    },
	    acpi: {
		header: gettext('ACPI support'),
		defaultValue: true,
		renderer: Proxmox.Utils.format_boolean,
		editor: caps.vms['VM.Config.HWType'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('ACPI support'),
		    items: {
			xtype: 'proxmoxcheckbox',
			name: 'acpi',
			checked: true,
			uncheckedValue: 0,
			defaultValue: 1,
			deleteDefaultValue: true,
			fieldLabel: gettext('Enabled'),
		    },
		} : undefined,
	    },
	    kvm: {
		header: gettext('KVM hardware virtualization'),
		defaultValue: true,
		renderer: Proxmox.Utils.format_boolean,
		editor: caps.vms['VM.Config.HWType'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('KVM hardware virtualization'),
		    items: {
			xtype: 'proxmoxcheckbox',
			name: 'kvm',
			checked: true,
			uncheckedValue: 0,
			defaultValue: 1,
			deleteDefaultValue: true,
			fieldLabel: gettext('Enabled'),
		    },
		} : undefined,
	    },
	    freeze: {
		header: gettext('Freeze CPU at startup'),
		defaultValue: false,
		renderer: Proxmox.Utils.format_boolean,
		editor: caps.vms['VM.PowerMgmt'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Freeze CPU at startup'),
		    items: {
			xtype: 'proxmoxcheckbox',
			name: 'freeze',
			uncheckedValue: 0,
			defaultValue: 0,
			deleteDefaultValue: true,
			labelWidth: 140,
			fieldLabel: gettext('Freeze CPU at startup'),
		    },
		} : undefined,
	    },
	    localtime: {
		header: gettext('Use local time for RTC'),
		defaultValue: '__default__',
		renderer: PVE.Utils.render_localtime,
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Use local time for RTC'),
		    width: 400,
		    items: {
			xtype: 'proxmoxKVComboBox',
			name: 'localtime',
			value: '__default__',
			comboItems: [
			    ['__default__', PVE.Utils.render_localtime('__default__')],
			    [1, PVE.Utils.render_localtime(1)],
			    [0, PVE.Utils.render_localtime(0)],
			],
			labelWidth: 140,
			fieldLabel: gettext('Use local time for RTC'),
		    },
		} : undefined,
	    },
	    startdate: {
		header: gettext('RTC start date'),
		defaultValue: 'now',
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('RTC start date'),
		    items: {
			xtype: 'proxmoxtextfield',
			name: 'startdate',
			deleteEmpty: true,
			value: 'now',
			fieldLabel: gettext('RTC start date'),
			vtype: 'QemuStartDate',
			allowBlank: true,
		    },
		} : undefined,
	    },
	    smbios1: {
		header: gettext('SMBIOS settings (type1)'),
		defaultValue: '',
		renderer: Ext.String.htmlEncode,
		editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.Smbios1Edit' : undefined,
	    },
	    agent: {
		header: 'QEMU Guest Agent',
		defaultValue: false,
		renderer: PVE.Utils.render_qga_features,
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Qemu Agent'),
		    width: 350,
		    onlineHelp: 'qm_qemu_agent',
		    items: {
			xtype: 'pveAgentFeatureSelector',
			name: 'agent',
		    },
		} : undefined,
	    },
	    protection: {
		header: gettext('Protection'),
		defaultValue: false,
		renderer: Proxmox.Utils.format_boolean,
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Protection'),
		    items: {
			xtype: 'proxmoxcheckbox',
			name: 'protection',
			uncheckedValue: 0,
			defaultValue: 0,
			deleteDefaultValue: true,
			fieldLabel: gettext('Enabled'),
		    },
		} : undefined,
	    },
	    spice_enhancements: {
		header: gettext('Spice Enhancements'),
		defaultValue: false,
		renderer: PVE.Utils.render_spice_enhancements,
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('Spice Enhancements'),
		    onlineHelp: 'qm_spice_enhancements',
		    items: {
			xtype: 'pveSpiceEnhancementSelector',
			name: 'spice_enhancements',
		    },
		} : undefined,
	    },
	    vmstatestorage: {
		header: gettext('VM State storage'),
		defaultValue: '',
		renderer: val => val || gettext('Automatic'),
		editor: caps.vms['VM.Config.Options'] ? {
		    xtype: 'proxmoxWindowEdit',
		    subject: gettext('VM State storage'),
		    onlineHelp: 'chapter_virtual_machines', // FIXME: use 'qm_vmstatestorage' once available
		    width: 350,
		    items: {
			xtype: 'pveStorageSelector',
			storageContent: 'images',
			allowBlank: true,
			emptyText: gettext("Automatic (Storage used by the VM, or 'local')"),
			autoSelect: false,
			deleteEmpty: true,
			skipEmptyText: true,
			nodename: nodename,
			name: 'vmstatestorage',
		    },
		} : undefined,
	    },
	    hookscript: {
		header: gettext('Hookscript'),
	    },
	};
	var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
	var edit_btn = new Ext.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    handler: function() { me.run_editor(); },
	});
	var revert_btn = new PVE.button.PendingRevert();
	var set_button_status = function() {
	    var sm = me.getSelectionModel();
	    var rec = sm.getSelection()[0];
	    if (!rec) {
		edit_btn.disable();
		return;
	    }
	    var key = rec.data.key;
	    var pending = rec.data.delete || me.hasPendingChanges(key);
	    var rowdef = rows[key];
	    edit_btn.setDisabled(!rowdef.editor);
	    revert_btn.setDisabled(!pending);
	};
	Ext.apply(me, {
	    url: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/pending",
	    interval: 5000,
	    cwidth1: 250,
	    tbar: [edit_btn, revert_btn],
	    rows: rows,
	    editorConfig: {
		url: "/api2/extjs/" + baseurl,
	    },
	    listeners: {
		itemdblclick: me.run_editor,
		selectionchange: set_button_status,
	    },
	});
	me.callParent();
	me.on('activate', () => me.rstore.startUpdate());
	me.on('destroy', () => me.rstore.stopUpdate());
	me.on('deactivate', () => me.rstore.stopUpdate());
	me.mon(me.getStore(), 'datachanged', function() {
	    set_button_status();
	});
    },
});
Ext.define('PVE.qemu.PCIInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    onlineHelp: 'qm_pci_passthrough_vm_config',
    setVMConfig: function(vmconfig) {
	var me = this;
	me.vmconfig = vmconfig;
	var hostpci = me.vmconfig[me.confid] || '';
	var values = PVE.Parser.parsePropertyString(hostpci, 'host');
	if (values.host) {
	    if (!values.host.match(/^[0-9a-f]{4}:/i)) { // add optional domain
		values.host = "0000:" + values.host;
	    }
	    if (values.host.length < 11) { // 0000:00:00 format not 0000:00:00.0
		values.host += ".0";
		values.multifunction = true;
	    }
	}
	values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0);
	values.pcie = PVE.Parser.parseBoolean(values.pcie, 0);
	values.rombar = PVE.Parser.parseBoolean(values.rombar, 1);
	me.setValues(values);
	if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) {
	    // machine is not set to some variant of q35, so we disable pcie
	    var pcie = me.down('field[name=pcie]');
	    pcie.setDisabled(true);
	    pcie.setBoxLabel(gettext('Q35 only'));
	}
	if (values.romfile) {
	    me.down('field[name=romfile]').setVisible(true);
	}
    },
    onGetValues: function(values) {
	let me = this;
	if (!me.confid) {
	    for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
		if (!me.vmconfig['hostpci' + i.toString()]) {
		    me.confid = 'hostpci' + i.toString();
		    break;
		}
	    }
	    // FIXME: what if no confid was found??
	}
	values.host.replace(/^0000:/, ''); // remove optional '0000' domain
	if (values.multifunction) {
	    values.host = values.host.substring(0, values.host.indexOf('.')); // skip the '.X'
	    delete values.multifunction;
	}
	if (values.rombar) {
	    delete values.rombar;
	} else {
	    values.rombar = 0;
	}
	if (!values.romfile) {
	    delete values.romfile;
	}
	let ret = {};
	ret[me.confid] = PVE.Parser.printPropertyString(values, 'host');
	return ret;
    },
    initComponent: function() {
	let me = this;
	me.nodename = me.pveSelNode.data.node;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	me.column1 = [
	    {
		xtype: 'pvePCISelector',
		fieldLabel: gettext('Device'),
		name: 'host',
		nodename: me.nodename,
		allowBlank: false,
		onLoadCallBack: function(store, records, success) {
		    if (!success || !records.length) {
			return;
		    }
		    if (records.every((val) => val.data.iommugroup === -1)) { // no IOMMU groups
			let warning = Ext.create('Ext.form.field.Display', {
			    columnWidth: 1,
			    padding: '0 0 10 0',
			    value: 'No IOMMU detected, please activate it.' +
				   'See Documentation for further information.',
			    userCls: 'pmx-hint',
			});
			me.items.insert(0, warning);
			me.updateLayout(); // insert does not trigger that
		    }
		},
		listeners: {
		    change: function(pcisel, value) {
			if (!value) {
			    return;
			}
			let pciDev = pcisel.getStore().getById(value);
			let mdevfield = me.down('field[name=mdev]');
			mdevfield.setDisabled(!pciDev || !pciDev.data.mdev);
			if (!pciDev) {
			    return;
			}
			if (pciDev.data.mdev) {
			    mdevfield.setPciID(value);
			}
			let iommu = pciDev.data.iommugroup;
			if (iommu === -1) {
			    return;
			}
			// try to find out if there are more devices in that iommu group
			let id = pciDev.data.id.substring(0, 5); // 00:00
			let count = 0;
			pcisel.getStore().each(({ data }) => {
			    if (data.iommugroup === iommu && data.id.substring(0, 5) !== id) {
				count++;
				return false;
			    }
			    return true;
			});
			let warning = me.down('#iommuwarning');
			if (count && !warning) {
			    warning = Ext.create('Ext.form.field.Display', {
				columnWidth: 1,
				padding: '0 0 10 0',
				itemId: 'iommuwarning',
				value: 'The selected Device is not in a seperate IOMMU group, make sure this is intended.',
				userCls: 'pmx-hint',
			    });
			    me.items.insert(0, warning);
			    me.updateLayout(); // insert does not trigger that
			} else if (!count && warning) {
			    me.remove(warning);
			}
		    },
		},
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('All Functions'),
		name: 'multifunction',
	    },
	];
	me.column2 = [
	    {
		xtype: 'pveMDevSelector',
		name: 'mdev',
		disabled: true,
		fieldLabel: gettext('MDev Type'),
		nodename: me.nodename,
		listeners: {
		    change: function(field, value) {
			let multiFunction = me.down('field[name=multifunction]');
			if (value) {
			    multiFunction.setValue(false);
			}
			multiFunction.setDisabled(!!value);
		    },
		},
	    },
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: gettext('Primary GPU'),
		name: 'x-vga',
	    },
	];
	me.advancedColumn1 = [
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: 'ROM-Bar',
		name: 'rombar',
	    },
	    {
		xtype: 'displayfield',
		submitValue: true,
		hidden: true,
		fieldLabel: 'ROM-File',
		name: 'romfile',
	    },
	    {
		xtype: 'textfield',
		name: 'vendor-id',
		fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Vendor')),
		emptyText: gettext('From Device'),
		vtype: 'PciId',
		allowBlank: true,
		submitEmpty: false,
	    },
	    {
		xtype: 'textfield',
		name: 'device-id',
		fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Device')),
		emptyText: gettext('From Device'),
		vtype: 'PciId',
		allowBlank: true,
		submitEmpty: false,
	    },
	];
	me.advancedColumn2 = [
	    {
		xtype: 'proxmoxcheckbox',
		fieldLabel: 'PCI-Express',
		name: 'pcie',
	    },
	    {
		xtype: 'textfield',
		name: 'sub-vendor-id',
		fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Sub-Vendor')),
		emptyText: gettext('From Device'),
		vtype: 'PciId',
		allowBlank: true,
		submitEmpty: false,
	    },
	    {
		xtype: 'textfield',
		name: 'sub-device-id',
		fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Sub-Device')),
		emptyText: gettext('From Device'),
		vtype: 'PciId',
		allowBlank: true,
		submitEmpty: false,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.qemu.PCIEdit', {
    extend: 'Proxmox.window.Edit',
    subject: gettext('PCI Device'),
    vmconfig: undefined,
    isAdd: true,
    initComponent: function() {
	let me = this;
	me.isCreate = !me.confid;
	let ipanel = Ext.create('PVE.qemu.PCIInputPanel', {
	    confid: me.confid,
	    pveSelNode: me.pveSelNode,
	});
	Ext.apply(me, {
	    items: [ipanel],
	});
	me.callParent();
	me.load({
	    success: ({ result }) => ipanel.setVMConfig(result.data),
	});
    },
});
// The view model of the parent shoul contain a 'cgroupMode' variable (or params for v2 are used).
Ext.define('PVE.qemu.ProcessorInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.pveQemuProcessorPanel',
    onlineHelp: 'qm_cpu',
    insideWizard: false,
    viewModel: {
	data: {
	    socketCount: 1,
	    coreCount: 1,
	    showCustomModelPermWarning: false,
	    userIsRoot: false,
	},
	formulas: {
	    totalCoreCount: get => get('socketCount') * get('coreCount'),
	    cpuunitsDefault: (get) => get('cgroupMode') === 1 ? 1024 : 100,
	    cpuunitsMin: (get) => get('cgroupMode') === 1 ? 2 : 1,
	    cpuunitsMax: (get) => get('cgroupMode') === 1 ? 262144 : 10000,
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	init: function() {
	    let me = this;
	    let viewModel = me.getViewModel();
	    viewModel.set('userIsRoot', Proxmox.UserName === 'root@pam');
	},
    },
    onGetValues: function(values) {
	let me = this;
	let cpuunitsDefault = me.getViewModel().get('cpuunitsDefault');
	if (Array.isArray(values.delete)) {
	    values.delete = values.delete.join(',');
	}
	PVE.Utils.delete_if_default(values, 'cpulimit', '0', me.insideWizard);
	PVE.Utils.delete_if_default(values, 'cpuunits', `${cpuunitsDefault}`, me.insideWizard);
	// build the cpu options:
	me.cpu.cputype = values.cputype;
	if (values.flags) {
	    me.cpu.flags = values.flags;
	} else {
	    delete me.cpu.flags;
	}
	delete values.cputype;
	delete values.flags;
	var cpustring = PVE.Parser.printQemuCpu(me.cpu);
	// remove cputype delete request:
	var del = values.delete;
	delete values.delete;
	if (del) {
	    del = del.split(',');
	    Ext.Array.remove(del, 'cputype');
	} else {
	    del = [];
	}
	if (cpustring) {
	    values.cpu = cpustring;
	} else {
	    del.push('cpu');
	}
	var delarr = del.join(',');
	if (delarr) {
	    values.delete = delarr;
	}
	return values;
    },
    setValues: function(values) {
	let me = this;
	let type = values.cputype;
	let typeSelector = me.lookupReference('cputype');
	let typeStore = typeSelector.getStore();
	typeStore.on('load', (store, records, success) => {
	    if (!success || !type || records.some(x => x.data.name === type)) {
		return;
	    }
	    // if we get here, a custom CPU model is selected for the VM but we
	    // don't have permission to configure it - it will not be in the
	    // list retrieved from the API, so add it manually to allow changing
	    // other processor options
	    typeStore.add({
		name: type,
		displayname: type.replace(/^custom-/, ''),
		custom: 1,
		vendor: gettext("Unknown"),
	    });
	    typeSelector.select(type);
	});
	me.callParent([values]);
    },
    cpu: {},
    column1: [
	{
	    xtype: 'proxmoxintegerfield',
	    name: 'sockets',
	    minValue: 1,
	    maxValue: 4,
	    value: '1',
	    fieldLabel: gettext('Sockets'),
	    allowBlank: false,
	    bind: {
		value: '{socketCount}',
	    },
	},
	{
	    xtype: 'proxmoxintegerfield',
	    name: 'cores',
	    minValue: 1,
	    maxValue: 128,
	    value: '1',
	    fieldLabel: gettext('Cores'),
	    allowBlank: false,
	    bind: {
		value: '{coreCount}',
	    },
	},
    ],
    column2: [
	{
	    xtype: 'CPUModelSelector',
	    name: 'cputype',
	    reference: 'cputype',
	    fieldLabel: gettext('Type'),
	},
	{
	    xtype: 'displayfield',
	    fieldLabel: gettext('Total cores'),
	    name: 'totalcores',
	    isFormField: false,
	    bind: {
		value: '{totalCoreCount}',
	    },
	},
    ],
    columnB: [
	{
	    xtype: 'displayfield',
	    userCls: 'pmx-hint',
	    value: gettext('WARNING: You do not have permission to configure custom CPU types, if you change the type you will not be able to go back!'),
	    hidden: true,
	    bind: {
		hidden: '{!showCustomModelPermWarning}',
	    },
	},
    ],
    advancedColumn1: [
	{
	    xtype: 'proxmoxintegerfield',
	    name: 'vcpus',
	    minValue: 1,
	    maxValue: 1,
	    value: '',
	    fieldLabel: gettext('VCPUs'),
	    deleteEmpty: true,
	    allowBlank: true,
	    emptyText: '1',
	    bind: {
		emptyText: '{totalCoreCount}',
		maxValue: '{totalCoreCount}',
	    },
	},
	{
	    xtype: 'numberfield',
	    name: 'cpulimit',
	    minValue: 0,
	    maxValue: 128, // api maximum
	    value: '',
	    step: 1,
	    fieldLabel: gettext('CPU limit'),
	    allowBlank: true,
	    emptyText: gettext('unlimited'),
	},
	{
	    xtype: 'proxmoxtextfield',
	    name: 'affinity',
	    vtype: 'CpuSet',
	    value: '',
	    fieldLabel: gettext('CPU Affinity'),
	    allowBlank: true,
	    emptyText: gettext("All Cores"),
	    deleteEmpty: true,
	    bind: {
		disabled: '{!userIsRoot}',
	    },
	},
    ],
    advancedColumn2: [
	{
	    xtype: 'proxmoxintegerfield',
	    name: 'cpuunits',
	    fieldLabel: gettext('CPU units'),
	    minValue: '1',
	    maxValue: '10000',
	    value: '',
	    emptyText: '100',
	    bind: {
		minValue: '{cpuunitsMin}',
		maxValue: '{cpuunitsMax}',
		emptyText: '{cpuunitsDefault}',
	    },
	    deleteEmpty: true,
	    allowBlank: true,
	},
	{
	    xtype: 'proxmoxcheckbox',
	    fieldLabel: gettext('Enable NUMA'),
	    name: 'numa',
	    uncheckedValue: 0,
	},
    ],
    advancedColumnB: [
	{
	    xtype: 'label',
	    text: 'Extra CPU Flags:',
	},
	{
	    xtype: 'vmcpuflagselector',
	    name: 'flags',
	},
    ],
});
Ext.define('PVE.qemu.ProcessorEdit', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveQemuProcessorEdit',
    width: 700,
    viewModel: {
	data: {
	    cgroupMode: 2,
	},
    },
    initComponent: function() {
	let me = this;
	me.getViewModel().set('cgroupMode', me.cgroupMode);
	var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel');
	Ext.apply(me, {
	    subject: gettext('Processors'),
	    items: ipanel,
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		var data = response.result.data;
		var value = data.cpu;
		if (value) {
		    var cpu = PVE.Parser.parseQemuCpu(value);
		    ipanel.cpu = cpu;
		    data.cputype = cpu.cputype;
		    if (cpu.flags) {
			data.flags = cpu.flags;
		    }
		    let caps = Ext.state.Manager.get('GuiCap');
		    if (data.cputype.indexOf('custom-') === 0 &&
			!caps.nodes['Sys.Audit']) {
			let vm = ipanel.getViewModel();
			vm.set("showCustomModelPermWarning", true);
		    }
		}
		me.setValues(data);
	    },
	});
    },
});
Ext.define('PVE.qemu.BiosEdit', {
    extend: 'Proxmox.window.Edit',
    alias: 'widget.pveQemuBiosEdit',
    onlineHelp: 'qm_bios_and_uefi',
    subject: 'BIOS',
    autoLoad: true,
    viewModel: {
	data: {
	    bios: '__default__',
	    efidisk0: false,
	},
	formulas: {
	    showEFIDiskHint: (get) => get('bios') === 'ovmf' && !get('efidisk0'),
	},
    },
    items: [
	{
	    xtype: 'pveQemuBiosSelector',
	    onlineHelp: 'qm_bios_and_uefi',
	    name: 'bios',
	    value: '__default__',
	    bind: '{bios}',
	    fieldLabel: 'BIOS',
	},
	{
	    xtype: 'displayfield',
	    name: 'efidisk0',
	    bind: '{efidisk0}',
	    hidden: true,
	},
	{
	    xtype: 'displayfield',
	    userCls: 'pmx-hint',
	    value: gettext('You need to add an EFI disk for storing the EFI settings. See the online help for details.'),
	    bind: {
		hidden: '{!showEFIDiskHint}',
	    },
	},
    ],
});
Ext.define('PVE.qemu.RNGInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveRNGInputPanel',
    onlineHelp: 'qm_virtio_rng',
    onGetValues: function(values) {
	if (values.max_bytes === "") {
	    values.max_bytes = "0";
	} else if (values.max_bytes === "1024" && values.period === "") {
	    delete values.max_bytes;
	}
	var ret = PVE.Parser.printPropertyString(values);
	return {
	    rng0: ret,
	};
    },
    setValues: function(values) {
	if (values.max_bytes === 0) {
	    values.max_bytes = null;
	}
	this.callParent(arguments);
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    '#max_bytes': {
		change: function(el, newVal) {
		    let limitWarning = this.lookupReference('limitWarning');
		    limitWarning.setHidden(!!newVal);
		},
	    },
	    '#source': {
		change: function(el, newVal) {
		    let limitWarning = this.lookupReference('sourceWarning');
		    limitWarning.setHidden(newVal !== '/dev/random');
		},
	    },
	},
    },
    items: [{
	itemId: 'source',
	name: 'source',
	xtype: 'proxmoxKVComboBox',
	value: '/dev/urandom',
	fieldLabel: gettext('Entropy source'),
	labelWidth: 130,
	comboItems: [
	    ['/dev/urandom', '/dev/urandom'],
	    ['/dev/random', '/dev/random'],
	    ['/dev/hwrng', '/dev/hwrng'],
	],
    },
    {
	xtype: 'numberfield',
	itemId: 'max_bytes',
	name: 'max_bytes',
	minValue: 0,
	step: 1,
	value: 1024,
	fieldLabel: gettext('Limit (Bytes/Period)'),
	labelWidth: 130,
	emptyText: gettext('unlimited'),
    },
    {
	xtype: 'numberfield',
	name: 'period',
	minValue: 1,
	step: 1,
	fieldLabel: gettext('Period') + ' (ms)',
	labelWidth: 130,
	emptyText: '1000',
    },
    {
	xtype: 'displayfield',
	reference: 'sourceWarning',
	value: gettext('Using /dev/random as entropy source is discouraged, as it can lead to host entropy starvation. /dev/urandom is preferred, and does not lead to a decrease in security in practice.'),
	userCls: 'pmx-hint',
	hidden: true,
    },
    {
	xtype: 'displayfield',
	reference: 'limitWarning',
	value: gettext('Disabling the limiter can potentially allow a guest to overload the host. Proceed with caution.'),
	userCls: 'pmx-hint',
	hidden: true,
    }],
});
Ext.define('PVE.qemu.RNGEdit', {
    extend: 'Proxmox.window.Edit',
    subject: gettext('VirtIO RNG'),
    items: [{
	xtype: 'pveRNGInputPanel',
    }],
    initComponent: function() {
	var me = this;
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response) {
		    me.vmconfig = response.result.data;
		    var rng0 = me.vmconfig.rng0;
		    if (rng0) {
			me.setValues(PVE.Parser.parsePropertyString(rng0));
		    }
		},
	    });
	}
    },
});
Ext.define('PVE.qemu.SSHKeyInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveQemuSSHKeyInputPanel',
    insideWizard: false,
    onGetValues: function(values) {
	var me = this;
	if (values.sshkeys) {
	    values.sshkeys.trim();
	}
	if (!values.sshkeys.length) {
	    values = {};
	    values.delete = 'sshkeys';
	    return values;
	} else {
	    values.sshkeys = encodeURIComponent(values.sshkeys);
	}
	return values;
    },
    items: [
	{
	    xtype: 'textarea',
	    itemId: 'sshkeys',
	    name: 'sshkeys',
	    height: 250,
	},
	{
	    xtype: 'filebutton',
	    itemId: 'filebutton',
	    name: 'file',
	    text: gettext('Load SSH Key File'),
	    fieldLabel: 'test',
	    listeners: {
		change: function(btn, e, value) {
		    let view = this.up('inputpanel');
		    e = e.event;
		    Ext.Array.each(e.target.files, function(file) {
			PVE.Utils.loadSSHKeyFromFile(file, function(res) {
			    let keysField = view.down('#sshkeys');
			    var old = keysField.getValue();
			    keysField.setValue(old + res);
			});
		    });
		    btn.reset();
		},
	    },
	},
    ],
    initComponent: function() {
	var me = this;
	me.callParent();
	if (!window.FileReader) {
	    me.down('#filebutton').setVisible(false);
	}
    },
});
Ext.define('PVE.qemu.SSHKeyEdit', {
    extend: 'Proxmox.window.Edit',
    width: 800,
    initComponent: function() {
	var me = this;
	var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel');
	Ext.apply(me, {
	    subject: gettext('SSH Keys'),
	    items: [ipanel],
	});
	me.callParent();
	if (!me.create) {
	    me.load({
		success: function(response, options) {
		    var data = response.result.data;
		    if (data.sshkeys) {
			data.sshkeys = decodeURIComponent(data.sshkeys);
			ipanel.setValues(data);
		    }
		},
	    });
	}
    },
});
Ext.define('PVE.qemu.ScsiHwEdit', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	Ext.applyIf(me, {
	    subject: gettext('SCSI Controller Type'),
	    items: {
		xtype: 'pveScsiHwSelector',
		name: 'scsihw',
		value: '__default__',
		fieldLabel: gettext('Type'),
	    },
	});
	me.callParent();
	me.load();
    },
});
Ext.define('PVE.qemu.SerialnputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    autoComplete: false,
    setVMConfig: function(vmconfig) {
	var me = this, i;
	me.vmconfig = vmconfig;
	for (i = 0; i < 4; i++) {
	    var port = 'serial' + i.toString();
	    if (!me.vmconfig[port]) {
		me.down('field[name=serialid]').setValue(i);
		break;
	    }
	}
    },
    onGetValues: function(values) {
	var me = this;
	var id = 'serial' + values.serialid;
	delete values.serialid;
	values[id] = 'socket';
	return values;
    },
    items: [
	{
	    xtype: 'proxmoxintegerfield',
	    name: 'serialid',
	    fieldLabel: gettext('Serial Port'),
	    minValue: 0,
	    maxValue: 3,
	    allowBlank: false,
	    validator: function(id) {
		if (!this.rendered) {
		    return true;
		}
		let view = this.up('panel');
		if (view.vmconfig !== undefined && Ext.isDefined(view.vmconfig['serial' + id])) {
			return "This device is already in use.";
		}
		return true;
	    },
	},
    ],
});
Ext.define('PVE.qemu.SerialEdit', {
    extend: 'Proxmox.window.Edit',
    vmconfig: undefined,
    isAdd: true,
    subject: gettext('Serial Port'),
    initComponent: function() {
	var me = this;
	// for now create of (socket) serial port only
	me.isCreate = true;
	var ipanel = Ext.create('PVE.qemu.SerialnputPanel', {});
	Ext.apply(me, {
	    items: [ipanel],
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		ipanel.setVMConfig(response.result.data);
	    },
	});
    },
});
Ext.define('PVE.qemu.Smbios1InputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    alias: 'widget.PVE.qemu.Smbios1InputPanel',
    insideWizard: false,
    smbios1: {},
    onGetValues: function(values) {
	var me = this;
	var params = {
	    smbios1: PVE.Parser.printQemuSmbios1(values),
	};
	return params;
    },
    setSmbios1: function(data) {
	var me = this;
	me.smbios1 = data;
	me.setValues(me.smbios1);
    },
    items: [
	{
	    xtype: 'textfield',
	    fieldLabel: 'UUID',
	    regex: /^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/,
	    name: 'uuid',
	},
	{
	    xtype: 'textareafield',
	    fieldLabel: gettext('Manufacturer'),
	    fieldStyle: {
		height: '2em',
		minHeight: '2em',
	    },
	    name: 'manufacturer',
	},
	{
	    xtype: 'textareafield',
	    fieldLabel: gettext('Product'),
	    fieldStyle: {
		height: '2em',
		minHeight: '2em',
	    },
	    name: 'product',
	},
	{
	    xtype: 'textareafield',
	    fieldLabel: gettext('Version'),
	    fieldStyle: {
		height: '2em',
		minHeight: '2em',
	    },
	    name: 'version',
	},
	{
	    xtype: 'textareafield',
	    fieldLabel: gettext('Serial'),
	    fieldStyle: {
		height: '2em',
		minHeight: '2em',
	    },
	    name: 'serial',
	},
	{
	    xtype: 'textareafield',
	    fieldLabel: 'SKU',
	    fieldStyle: {
		height: '2em',
		minHeight: '2em',
	    },
	    name: 'sku',
	},
	{
	    xtype: 'textareafield',
	    fieldLabel: gettext('Family'),
	    fieldStyle: {
		height: '2em',
		minHeight: '2em',
	    },
	    name: 'family',
	},
    ],
});
Ext.define('PVE.qemu.Smbios1Edit', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	var ipanel = Ext.create('PVE.qemu.Smbios1InputPanel', {});
	Ext.applyIf(me, {
	    subject: gettext('SMBIOS settings (type1)'),
	    width: 450,
	    items: ipanel,
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		me.vmconfig = response.result.data;
		var value = me.vmconfig.smbios1;
		if (value) {
		    var data = PVE.Parser.parseQemuSmbios1(value);
		    if (!data) {
			Ext.Msg.alert(gettext('Error'), 'Unable to parse smbios options');
			me.close();
			return;
		    }
		    ipanel.setSmbios1(data);
		}
	    },
	});
    },
});
Ext.define('PVE.qemu.SystemInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pveQemuSystemPanel',
    onlineHelp: 'qm_system_settings',
    viewModel: {
	data: {
	    efi: false,
	    addefi: true,
	},
	formulas: {
	    efidisk: function(get) {
		return get('efi') && get('addefi');
	    },
	},
    },
    onGetValues: function(values) {
	if (values.vga && values.vga.substr(0, 6) === 'serial') {
	    values['serial' + values.vga.substr(6, 1)] = 'socket';
	}
	delete values.hdimage;
	delete values.hdstorage;
	delete values.diskformat;
	delete values.preEnrolledKeys; // efidisk
	delete values.version; // tpmstate
	return values;
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	scsihwChange: function(field, value) {
	    var me = this;
	    if (me.getView().insideWizard) {
		me.getViewModel().set('current.scsihw', value);
	    }
	},
	biosChange: function(field, value) {
	    var me = this;
	    if (me.getView().insideWizard) {
		me.getViewModel().set('efi', value === 'ovmf');
	    }
	},
	control: {
	    'pveScsiHwSelector': {
		change: 'scsihwChange',
	    },
	    'pveQemuBiosSelector': {
		change: 'biosChange',
	    },
	    '#': {
		afterrender: 'setMachine',
	    },
	},
	setMachine: function() {
	    let me = this;
	    let vm = this.getViewModel();
	    let ostype = vm.get('current.ostype');
	    if (ostype === 'win11') {
		me.lookup('machine').setValue('q35');
		me.lookup('bios').setValue('ovmf');
		me.lookup('addtpmbox').setValue(true);
	    }
	},
    },
    column1: [
	{
	    xtype: 'proxmoxKVComboBox',
	    value: '__default__',
	    deleteEmpty: false,
	    fieldLabel: gettext('Graphic card'),
	    name: 'vga',
	    comboItems: Object.entries(PVE.Utils.kvm_vga_drivers),
	},
	{
	    xtype: 'proxmoxKVComboBox',
	    name: 'machine',
	    reference: 'machine',
	    value: '__default__',
	    fieldLabel: gettext('Machine'),
	    comboItems: [
		['__default__', PVE.Utils.render_qemu_machine('')],
		['q35', 'q35'],
	    ],
	},
	{
	    xtype: 'displayfield',
	    value: gettext('Firmware'),
	},
	{
	    xtype: 'pveQemuBiosSelector',
	    name: 'bios',
	    reference: 'bios',
	    value: '__default__',
	    fieldLabel: 'BIOS',
	},
	{
	    xtype: 'proxmoxcheckbox',
	    bind: {
		value: '{addefi}',
		hidden: '{!efi}',
		disabled: '{!efi}',
	    },
	    hidden: true,
	    submitValue: false,
	    disabled: true,
	    fieldLabel: gettext('Add EFI Disk'),
	},
	{
	    xtype: 'pveEFIDiskInputPanel',
	    name: 'efidisk0',
	    storageContent: 'images',
	    bind: {
		nodename: '{nodename}',
		hidden: '{!efi}',
		disabled: '{!efidisk}',
	    },
	    autoSelect: false,
	    disabled: true,
	    hidden: true,
	    hideSize: true,
	    usesEFI: true,
	},
    ],
    column2: [
	{
	    xtype: 'pveScsiHwSelector',
	    name: 'scsihw',
	    value: '__default__',
	    bind: {
		value: '{current.scsihw}',
	    },
	    fieldLabel: gettext('SCSI Controller'),
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'agent',
	    uncheckedValue: 0,
	    defaultValue: 0,
	    deleteDefaultValue: true,
	    fieldLabel: gettext('Qemu Agent'),
	},
	{
	    // fake for spacing
	    xtype: 'displayfield',
	    value: ' ',
	},
	{
	    xtype: 'proxmoxcheckbox',
	    reference: 'addtpmbox',
	    bind: {
		value: '{addtpm}',
	    },
	    submitValue: false,
	    fieldLabel: gettext('Add TPM'),
	},
	{
	    xtype: 'pveTPMDiskInputPanel',
	    name: 'tpmstate0',
	    storageContent: 'images',
	    bind: {
		nodename: '{nodename}',
		hidden: '{!addtpm}',
		disabled: '{!addtpm}',
	    },
	    disabled: true,
	    hidden: true,
	},
    ],
});
Ext.define('PVE.qemu.USBInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    mixins: ['Proxmox.Mixin.CBind'],
    autoComplete: false,
    onlineHelp: 'qm_usb_passthrough',
    viewModel: {
	data: {},
    },
    setVMConfig: function(vmconfig) {
	var me = this;
	me.vmconfig = vmconfig;
	let max_usb = PVE.Utils.get_max_usb_count(me.vmconfig.ostype, me.vmconfig.machine);
	if (max_usb > PVE.Utils.hardware_counts.usb_old) {
	    me.down('field[name=usb3]').setDisabled(true);
	}
    },
    onGetValues: function(values) {
	var me = this;
	if (!me.confid) {
	    let max_usb = PVE.Utils.get_max_usb_count(me.vmconfig.ostype, me.vmconfig.machine);
	    for (let i = 0; i < max_usb; i++) {
		let id = 'usb' + i.toString();
		if (!me.vmconfig[id]) {
		    me.confid = id;
		    break;
		}
	    }
	}
	var val = "";
	var type = me.down('radiofield').getGroupValue();
	switch (type) {
	    case 'spice':
		val = 'spice';
		break;
	    case 'hostdevice':
	    case 'port':
		val = 'host=' + values[type];
		delete values[type];
		break;
	    default:
		throw "invalid type selected";
	}
	if (values.usb3) {
	    delete values.usb3;
	    val += ',usb3=1';
	}
	values[me.confid] = val;
	return values;
    },
    items: [
	{
	    xtype: 'fieldcontainer',
	    defaultType: 'radiofield',
	    layout: 'fit',
	    items: [
		{
		    name: 'usb',
		    inputValue: 'spice',
		    boxLabel: gettext('Spice Port'),
		    submitValue: false,
		    checked: true,
		},
		{
		    name: 'usb',
		    inputValue: 'hostdevice',
		    boxLabel: gettext('Use USB Vendor/Device ID'),
		    reference: 'hostdevice',
		    submitValue: false,
		},
		{
		    xtype: 'pveUSBSelector',
		    disabled: true,
		    type: 'device',
		    name: 'hostdevice',
		    cbind: { pveSelNode: '{pveSelNode}' },
		    bind: { disabled: '{!hostdevice.checked}' },
		    editable: true,
		    allowBlank: false,
		    fieldLabel: gettext('Choose Device'),
		    labelAlign: 'right',
		},
		{
		    name: 'usb',
		    inputValue: 'port',
		    boxLabel: gettext('Use USB Port'),
		    reference: 'port',
		    submitValue: false,
		},
		{
		    xtype: 'pveUSBSelector',
		    disabled: true,
		    name: 'port',
		    cbind: { pveSelNode: '{pveSelNode}' },
		    bind: { disabled: '{!port.checked}' },
		    editable: true,
		    type: 'port',
		    allowBlank: false,
		    fieldLabel: gettext('Choose Port'),
		    labelAlign: 'right',
		},
		{
		    xtype: 'checkbox',
		    name: 'usb3',
		    inputValue: true,
		    checked: true,
		    reference: 'usb3',
		    fieldLabel: gettext('Use USB3'),
		},
	    ],
	},
    ],
});
Ext.define('PVE.qemu.USBEdit', {
    extend: 'Proxmox.window.Edit',
    vmconfig: undefined,
    isAdd: true,
    width: 400,
    subject: gettext('USB Device'),
    initComponent: function() {
	var me = this;
	me.isCreate = !me.confid;
	var ipanel = Ext.create('PVE.qemu.USBInputPanel', {
	    confid: me.confid,
	    pveSelNode: me.pveSelNode,
	});
	Ext.apply(me, {
	    items: [ipanel],
	});
	me.callParent();
	me.load({
	    success: function(response, options) {
		ipanel.setVMConfig(response.result.data);
		if (me.isCreate) {
		    return;
		}
		var data = response.result.data[me.confid].split(',');
		var port, hostdevice, usb3 = false;
		var type = 'spice';
		for (let i = 0; i < data.length; i++) {
		    if (/^(host=)?(0x)?[a-zA-Z0-9]{4}:(0x)?[a-zA-Z0-9]{4}$/.test(data[i])) {
			hostdevice = data[i];
			hostdevice = hostdevice.replace('host=', '').replace('0x', '');
			type = 'hostdevice';
		    } else if (/^(host=)?(\d+)-(\d+(\.\d+)*)$/.test(data[i])) {
			port = data[i];
			port = port.replace('host=', '');
			type = 'port';
		    }
		    if (/^usb3=(1|on|true)$/.test(data[i])) {
			usb3 = true;
		    }
		}
		var values = {
		    usb: type,
		    hostdevice: hostdevice,
		    port: port,
		    usb3: usb3,
		};
		ipanel.setValues(values);
	    },
	});
    },
});
Ext.define('PVE.sdn.Browser', {
    extend: 'PVE.panel.Config',
    alias: 'widget.PVE.sdn.Browser',
    onlineHelp: 'chapter_pvesdn',
    initComponent: function() {
	let me = this;
	let nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	let sdnId = me.pveSelNode.data.sdn;
	if (!sdnId) {
	    throw "no sdn ID specified";
	}
	me.items = [];
	Ext.apply(me, {
	    title: Ext.String.format(gettext("Zone {0} on node {1}"), `'${sdnId}'`, `'${nodename}'`),
	    hstateid: 'sdntab',
	});
	const caps = Ext.state.Manager.get('GuiCap');
	if (caps.sdn['SDN.Audit']) {
	    me.items.push({
		xtype: 'pveSDNZoneContentView',
		title: gettext('Content'),
		iconCls: 'fa fa-th',
		itemId: 'content',
	    });
	}
	if (caps.sdn['Permissions.Modify']) {
	    me.items.push({
		xtype: 'pveACLView',
		title: gettext('Permissions'),
		iconCls: 'fa fa-unlock',
		itemId: 'permissions',
		path: `/sdn/zones/${sdnId}`,
	    });
	}
	me.callParent();
   },
});
Ext.define('PVE.sdn.ControllerView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveSDNControllerView'],
    onlineHelp: 'pvesdn_config_controllers',
    stateful: true,
    stateId: 'grid-sdn-controller',
    createSDNControllerEditWindow: function(type, sid) {
	var schema = PVE.Utils.sdncontrollerSchema[type];
	if (!schema || !schema.ipanel) {
	    throw "no editor registered for controller type: " + type;
	}
	Ext.create('PVE.sdn.controllers.BaseEdit', {
	    paneltype: 'PVE.sdn.controllers.' + schema.ipanel,
	    type: type,
	    controllerid: sid,
	    autoShow: true,
	    listeners: {
		destroy: this.reloadStore,
	    },
	});
    },
    initComponent: function() {
	var me = this;
	var store = new Ext.data.Store({
	    model: 'pve-sdn-controller',
	    proxy: {
		type: 'proxmox',
		url: "/api2/json/cluster/sdn/controllers?pending=1",
	    },
	    sorters: {
		property: 'controller',
		direction: 'ASC',
	    },
	});
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    let type = rec.data.type, controller = rec.data.controller;
	    me.createSDNControllerEditWindow(type, controller);
	};
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/cluster/sdn/controllers/',
	    callback: () => store.load(),
	});
	// else we cannot dynamically generate the add menu handlers
	let addHandleGenerator = function(type) {
	    return function() { me.createSDNControllerEditWindow(type); };
	};
	let addMenuItems = [];
	for (const [type, controller] of Object.entries(PVE.Utils.sdncontrollerSchema)) {
	    if (controller.hideAdd) {
		continue;
	    }
	    addMenuItems.push({
		text: PVE.Utils.format_sdncontroller_type(type),
		iconCls: 'fa fa-fw fa-' + controller.faIcon,
		handler: addHandleGenerator(type),
	    });
	}
	Ext.apply(me, {
	    store: store,
	    reloadStore: () => store.load(),
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: new Ext.menu.Menu({
			items: addMenuItems,
		    }),
		},
		remove_btn,
		edit_btn,
	    ],
	    columns: [
		{
		    header: 'ID',
		    flex: 2,
		    sortable: true,
		    dataIndex: 'controller',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'controller', 1);
		    },
		},
		{
		    header: gettext('Type'),
		    flex: 1,
		    sortable: true,
		    dataIndex: 'type',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'type', 1);
		    },
		},
		{
		    header: gettext('Node'),
		    flex: 1,
		    sortable: true,
		    dataIndex: 'node',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'node', 1);
		    },
		},
		{
		    header: gettext('State'),
		    width: 100,
		    dataIndex: 'state',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending_state(rec, value);
		    },
		},
	    ],
	    listeners: {
		activate: () => store.load(),
		itemdblclick: run_editor,
	    },
	});
	store.load();
	me.callParent();
    },
});
Ext.define('PVE.sdn.Status', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveSDNStatus',
    onlineHelp: 'chapter_pvesdn',
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    initComponent: function() {
	var me = this;
	me.rstore = Ext.create('Proxmox.data.ObjectStore', {
	    interval: me.interval,
	    model: 'pve-sdn-status',
	    storeid: 'pve-store-' + ++Ext.idSeed,
	    groupField: 'type',
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/cluster/resources',
	    },
	});
	me.items = [{
	    xtype: 'pveSDNStatusView',
	    title: gettext('Status'),
	    rstore: me.rstore,
	    border: 0,
	    collapsible: true,
	    padding: '0 0 20 0',
	}];
	me.callParent();
	me.on('activate', me.rstore.startUpdate);
    },
});
Ext.define('PVE.sdn.StatusView', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveSDNStatusView',
    sortPriority: {
	sdn: 1,
	node: 2,
	status: 3,
    },
    initComponent: function() {
	var me = this;
	if (!me.rstore) {
	    throw "no rstore given";
	}
	Proxmox.Utils.monStoreErrors(me, me.rstore);
	var store = Ext.create('Proxmox.data.DiffStore', {
	    rstore: me.rstore,
	    sortAfterUpdate: true,
	    sorters: [{
		sorterFn: function(rec1, rec2) {
		    var p1 = me.sortPriority[rec1.data.type];
		    var p2 = me.sortPriority[rec2.data.type];
		    return p1 !== p2 ? p1 > p2 ? 1 : -1 : 0;
		},
	    }],
	    filters: {
		property: 'type',
		value: 'sdn',
		operator: '==',
	    },
	});
	Ext.apply(me, {
	    store: store,
	    stateful: false,
	    tbar: [
		{
		    text: gettext('Apply'),
		    handler: function() {
			Proxmox.Utils.API2Request({
			    url: '/cluster/sdn/',
			    method: 'PUT',
			    waitMsgTarget: me,
			    failure: function(response, opts) {
				Ext.Msg.alert(gettext('Error'), response.htmlStatus);
			    },
			});
		    },
		},
	    ],
	    viewConfig: {
		trackOver: false,
	    },
	    columns: [
		{
		    header: 'SDN',
		    width: 80,
		    dataIndex: 'sdn',
		},
		{
		    header: gettext('Node'),
		    width: 80,
		    dataIndex: 'node',
		},
		{
		    header: gettext('Status'),
		    width: 80,
		    flex: 1,
		    dataIndex: 'status',
		},
	    ],
	});
	me.callParent();
	me.on('activate', me.rstore.startUpdate);
	me.on('destroy', me.rstore.stopUpdate);
    },
}, function() {
    Ext.define('pve-sdn-status', {
	extend: 'Ext.data.Model',
	fields: [
	    'id', 'type', 'node', 'status', 'sdn',
	],
	idProperty: 'id',
    });
});
Ext.define('PVE.sdn.VnetInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    mixins: ['Proxmox.Mixin.CBind'],
    onGetValues: function(values) {
	let me = this;
	if (me.isCreate) {
	    values.type = 'vnet';
	}
	if (!values.vlanaware) {
	    delete values.vlanaware;
	}
	return values;
    },
    items: [
	{
	    xtype: 'pmxDisplayEditField',
	    name: 'vnet',
	    cbind: {
		editable: '{isCreate}',
	    },
	    maxLength: 8,
	    flex: 1,
	    allowBlank: false,
	    fieldLabel: gettext('Name'),
	},
	{
	    xtype: 'textfield',
	    name: 'alias',
	    fieldLabel: gettext('Alias'),
	    allowBlank: true,
	},
	{
	    xtype: 'pveSDNZoneSelector',
	    fieldLabel: gettext('Zone'),
	    name: 'zone',
	    value: '',
	    allowBlank: false,
	},
	{
	    xtype: 'proxmoxintegerfield',
	    name: 'tag',
	    minValue: 1,
	    maxValue: 16777216,
	    fieldLabel: gettext('Tag'),
	    allowBlank: true,
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'vlanaware',
	    uncheckedValue: 0,
	    checked: false,
	    fieldLabel: gettext('VLAN Aware'),
	},
    ],
});
Ext.define('PVE.sdn.VnetEdit', {
    extend: 'Proxmox.window.Edit',
    subject: gettext('VNet'),
    vnet: undefined,
    width: 350,
    initComponent: function() {
	var me = this;
	me.isCreate = me.vnet === undefined;
	if (me.isCreate) {
	    me.url = '/api2/extjs/cluster/sdn/vnets';
	    me.method = 'POST';
	} else {
	    me.url = '/api2/extjs/cluster/sdn/vnets/' + me.vnet;
	    me.method = 'PUT';
	}
	let ipanel = Ext.create('PVE.sdn.VnetInputPanel', {
	    isCreate: me.isCreate,
	});
	Ext.apply(me, {
	    items: [
		ipanel,
	    ],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    let values = response.result.data;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.sdn.VnetView', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveSDNVnetView',
    onlineHelp: 'pvesdn_config_vnet',
    stateful: true,
    stateId: 'grid-sdn-vnet',
    subnetview_panel: undefined,
    initComponent: function() {
	let me = this;
	let store = new Ext.data.Store({
	    model: 'pve-sdn-vnet',
	    proxy: {
                type: 'proxmox',
		url: "/api2/json/cluster/sdn/vnets?pending=1",
	    },
	    sorters: {
		property: 'vnet',
		direction: 'ASC',
	    },
	});
	let reload = () => store.load();
	let sm = Ext.create('Ext.selection.RowModel', {});
        let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    let win = Ext.create('PVE.sdn.VnetEdit', {
		autoShow: true,
		onlineHelp: 'pvesdn_config_vnet',
		vnet: rec.data.vnet,
	    });
	    win.on('destroy', reload);
        };
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/cluster/sdn/vnets/',
	    callback: reload,
	});
	let set_button_status = function() {
	    var rec = me.selModel.getSelection()[0];
	    if (!rec || rec.data.state === 'deleted') {
		edit_btn.disable();
		remove_btn.disable();
	    }
	};
	Ext.apply(me, {
	    store: store,
	    reloadStore: reload,
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    tbar: [
		{
		    text: gettext('Create'),
		    handler: function() {
			let win = Ext.create('PVE.sdn.VnetEdit', {
			    autoShow: true,
			    onlineHelp: 'pvesdn_config_vnet',
			    type: 'vnet',
			});
			win.on('destroy', reload);
		    },
		},
		remove_btn,
		edit_btn,
	    ],
	    columns: [
		{
		    header: 'ID',
		    flex: 2,
		    dataIndex: 'vnet',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'vnet', 1);
		    },
		},
		{
		    header: gettext('Alias'),
		    flex: 1,
		    dataIndex: 'alias',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'alias');
		    },
		},
		{
		    header: gettext('Zone'),
		    flex: 1,
		    dataIndex: 'zone',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'zone');
		    },
		},
		{
		    header: gettext('Tag'),
		    flex: 1,
		    dataIndex: 'tag',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'tag');
		    },
		},
		{
		    header: gettext('VLAN Aware'),
		    flex: 1,
		    dataIndex: 'vlanaware',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'vlanaware');
		    },
		},
		{
		    header: gettext('State'),
		    width: 100,
		    dataIndex: 'state',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending_state(rec, value);
		    },
		},
	    ],
	    listeners: {
		activate: reload,
		itemdblclick: run_editor,
		selectionchange: set_button_status,
		show: reload,
		select: function(_sm, rec) {
		    let url = `/cluster/sdn/vnets/${rec.data.vnet}/subnets`;
		    me.subnetview_panel.setBaseUrl(url);
		},
		deselect: function() {
		    me.subnetview_panel.setBaseUrl(undefined);
		},
	    },
	});
	store.load();
	me.callParent();
    },
});
Ext.define('PVE.sdn.Vnet', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveSDNVnet',
    title: 'Vnet',
    onlineHelp: 'pvesdn_config_vnet',
    initComponent: function() {
	var me = this;
	var subnetview_panel = Ext.createWidget('pveSDNSubnetView', {
	    title: gettext('Subnets'),
	    region: 'center',
	    border: false,
	});
	var vnetview_panel = Ext.createWidget('pveSDNVnetView', {
	    title: 'Vnets',
	    region: 'west',
	    subnetview_panel: subnetview_panel,
	    width: '50%',
	    border: false,
	    split: true,
	});
	Ext.apply(me, {
	    layout: 'border',
	    items: [vnetview_panel, subnetview_panel],
	    listeners: {
		show: function() {
		    subnetview_panel.fireEvent('show', subnetview_panel);
		},
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.sdn.SubnetInputPanel', {
    extend: 'Proxmox.panel.InputPanel',
    mixins: ['Proxmox.Mixin.CBind'],
    onGetValues: function(values) {
	let me = this;
	if (me.isCreate) {
	    values.type = 'subnet';
	    values.subnet = values.cidr;
	    delete values.cidr;
	}
	if (!values.gateway) {
	    delete values.gateway;
	}
	if (!values.snat) {
	    delete values.snat;
	}
	return values;
    },
    items: [
	{
	    xtype: 'pmxDisplayEditField',
	    name: 'cidr',
	    cbind: {
		editable: '{isCreate}',
	    },
	    flex: 1,
	    allowBlank: false,
	    fieldLabel: gettext('Subnet'),
	},
	{
	    xtype: 'textfield',
	    name: 'gateway',
	    vtype: 'IP64Address',
	    fieldLabel: gettext('Gateway'),
	    allowBlank: true,
	},
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'snat',
	    uncheckedValue: 0,
	    checked: false,
	    fieldLabel: 'SNAT',
	},
	{
	    xtype: 'proxmoxtextfield',
	    name: 'dnszoneprefix',
	    skipEmptyText: true,
	    fieldLabel: gettext('DNS zone prefix'),
	    allowBlank: true,
	},
    ],
});
Ext.define('PVE.sdn.SubnetEdit', {
    extend: 'Proxmox.window.Edit',
    subject: gettext('Subnet'),
    subnet: undefined,
    width: 350,
    base_url: undefined,
    initComponent: function() {
	var me = this;
	me.isCreate = me.subnet === undefined;
	if (me.isCreate) {
	    me.url = me.base_url;
	    me.method = 'POST';
	} else {
	    me.url = me.base_url + '/' + me.subnet;
	    me.method = 'PUT';
	}
	let ipanel = Ext.create('PVE.sdn.SubnetInputPanel', {
	    isCreate: me.isCreate,
	});
	Ext.apply(me, {
	    items: [
		ipanel,
	    ],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    let values = response.result.data;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.sdn.SubnetView', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveSDNSubnetView',
    stateful: true,
    stateId: 'grid-sdn-subnet',
    base_url: undefined,
    remove_btn: undefined,
    setBaseUrl: function(url) {
	let me = this;
	me.base_url = url;
	if (url === undefined) {
	    me.store.removeAll();
	    me.create_btn.disable();
	} else {
	    me.remove_btn.baseurl = url + '/';
	    me.store.setProxy({
		type: 'proxmox',
		url: '/api2/json/' + url + '?pending=1',
	    });
	    me.create_btn.enable();
	    me.store.load();
	}
    },
    initComponent: function() {
	let me = this;
	let store = new Ext.data.Store({
	    model: 'pve-sdn-subnet',
	});
	let reload = function() {
	    store.load();
	};
	let sm = Ext.create('Ext.selection.RowModel', {});
        let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    let win = Ext.create('PVE.sdn.SubnetEdit', {
		autoShow: true,
		subnet: rec.data.subnet,
		base_url: me.base_url,
	    });
	    win.on('destroy', reload);
        };
	me.create_btn = new Proxmox.button.Button({
	    text: gettext('Create'),
	    disabled: true,
	    handler: function() {
		let win = Ext.create('PVE.sdn.SubnetEdit', {
		    autoShow: true,
		    base_url: me.base_url,
		    type: 'subnet',
		});
		win.on('destroy', reload);
	    },
	});
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	me.remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: me.base_url + '/',
	    callback: () => store.load(),
	});
	let set_button_status = function() {
	    var rec = me.selModel.getSelection()[0];
	    if (!rec || rec.data.state === 'deleted') {
		edit_btn.disable();
		me.remove_btn.disable();
	    }
	};
	Ext.apply(me, {
	    store: store,
	    reloadStore: reload,
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    tbar: [
		me.create_btn,
		me.remove_btn,
		edit_btn,
	    ],
	    columns: [
		{
		    header: 'ID',
		    flex: 2,
		    dataIndex: 'cidr',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'cidr', 1);
		    },
		},
		{
		    header: gettext('Gateway'),
		    flex: 1,
		    dataIndex: 'gateway',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'gateway');
		    },
		},
		{
		    header: 'SNAT',
		    flex: 1,
		    dataIndex: 'snat',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'snat');
		    },
		},
		{
		    header: gettext('Dns prefix'),
		    flex: 1,
		    dataIndex: 'dnszoneprefix',
                    renderer: function(value, metaData, rec) {
                        return PVE.Utils.render_sdn_pending(rec, value, 'dnszoneprefix');
		    },
                },
		{
		    header: gettext('State'),
		    width: 100,
		    dataIndex: 'state',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending_state(rec, value);
		    },
		},
	    ],
	    listeners: {
		activate: reload,
		itemdblclick: run_editor,
		selectionchange: set_button_status,
	    },
	});
	me.callParent();
        if (me.base_url) {
            me.setBaseUrl(me.base_url); // load
        }
    },
}, function() {
    Ext.define('pve-sdn-subnet', {
	extend: 'Ext.data.Model',
	fields: [
	    'cidr',
	    'gateway',
	    'snat',
	],
	idProperty: 'subnet',
    });
});
Ext.define('PVE.sdn.ZoneContentView', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveSDNZoneContentView',
    stateful: true,
    stateId: 'grid-sdnzone-content',
    viewConfig: {
	trackOver: false,
	loadMask: false,
    },
    features: [
	{
	    ftype: 'grouping',
	    groupHeaderTpl: '{name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
	},
    ],
    initComponent: function() {
	var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var zone = me.pveSelNode.data.sdn;
	if (!zone) {
	    throw "no zone ID specified";
	}
	var baseurl = "/nodes/" + nodename + "/sdn/zones/" + zone + "/content";
	var store = Ext.create('Ext.data.Store', {
	    model: 'pve-sdnzone-content',
	    groupField: 'content',
	    proxy: {
		type: 'proxmox',
		url: '/api2/json' + baseurl,
	    },
	    sorters: {
		property: 'vnet',
		direction: 'ASC',
	    },
	});
	var sm = Ext.create('Ext.selection.RowModel', {});
	var reload = function() {
	    store.load();
	};
	Proxmox.Utils.monStoreErrors(me, store);
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: [
	    ],
	    columns: [
		{
		    header: 'VNet',
		    width: 100,
		    sortable: true,
		    dataIndex: 'vnet',
		},
		{
		    header: 'Alias',
		    width: 300,
		    sortable: true,
		    dataIndex: 'alias',
		},
		{
		    header: gettext('Status'),
		    width: 100,
		    sortable: true,
		    dataIndex: 'status',
		},
		{
		    header: gettext('Details'),
		    flex: 1,
		    dataIndex: 'statusmsg',
		},
	    ],
	    listeners: {
		activate: reload,
	    },
	});
	me.callParent();
    },
}, function() {
    Ext.define('pve-sdnzone-content', {
	extend: 'Ext.data.Model',
	fields: [
	    'vnet', 'status', 'statusmsg',
	    {
		name: 'text',
		convert: function(value, record) {
		    // check for volid, because if you click on a grouping header,
		    // it calls convert (but with an empty volid)
		    if (value || record.data.vnet === null) {
			return value;
		    }
		    return PVE.Utils.format_sdnvnet_type(value, {}, record);
		},
	    },
	],
	idProperty: 'vnet',
    });
});
Ext.define('PVE.sdn.ZoneView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveSDNZoneView'],
    onlineHelp: 'pvesdn_config_zone',
    stateful: true,
    stateId: 'grid-sdn-zone',
    createSDNEditWindow: function(type, sid) {
	let schema = PVE.Utils.sdnzoneSchema[type];
	if (!schema || !schema.ipanel) {
	    throw "no editor registered for zone type: " + type;
	}
	Ext.create('PVE.sdn.zones.BaseEdit', {
	    paneltype: 'PVE.sdn.zones.' + schema.ipanel,
	    type: type,
	    zone: sid,
	    autoShow: true,
	    listeners: {
		destroy: this.reloadStore,
	    },
	});
    },
    initComponent: function() {
	let me = this;
	let store = new Ext.data.Store({
	    model: 'pve-sdn-zone',
	    proxy: {
                type: 'proxmox',
		url: "/api2/json/cluster/sdn/zones?pending=1",
	    },
	    sorters: {
		property: 'zone',
		direction: 'ASC',
	    },
	});
	let reload = function() {
	    store.load();
	};
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    let type = rec.data.type,
		zone = rec.data.zone;
	    me.createSDNEditWindow(type, zone);
	};
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/cluster/sdn/zones/',
	    callback: reload,
	});
	let set_button_status = function() {
	    var rec = me.selModel.getSelection()[0];
	    if (!rec || rec.data.state === 'deleted') {
		edit_btn.disable();
		remove_btn.disable();
	    }
	};
	// else we cannot dynamically generate the add menu handlers
	let addHandleGenerator = function(type) {
	    return function() { me.createSDNEditWindow(type); };
	};
	let addMenuItems = [];
	for (const [type, zone] of Object.entries(PVE.Utils.sdnzoneSchema)) {
	    if (zone.hideAdd) {
		continue;
	    }
	    addMenuItems.push({
		text: PVE.Utils.format_sdnzone_type(type),
		iconCls: 'fa fa-fw fa-' + zone.faIcon,
		handler: addHandleGenerator(type),
	    });
	}
	Ext.apply(me, {
	    store: store,
	    reloadStore: reload,
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: new Ext.menu.Menu({
			items: addMenuItems,
		    }),
		},
		remove_btn,
		edit_btn,
	    ],
	    columns: [
		{
		    header: 'ID',
		    width: 100,
		    dataIndex: 'zone',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'zone', 1);
		    },
		},
		{
		    header: gettext('Type'),
		    width: 100,
		    dataIndex: 'type',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'type', 1);
		    },
		},
		{
		    header: 'MTU',
		    width: 50,
		    dataIndex: 'mtu',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'mtu');
		    },
		},
		{
		    header: 'Ipam',
		    flex: 3,
		    dataIndex: 'ipam',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'ipam');
		    },
		},
		{
		    header: gettext('Domain'),
		    flex: 3,
		    dataIndex: 'dnszone',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'dnszone');
		    },
		},
		{
		    header: gettext('Dns'),
		    flex: 3,
		    dataIndex: 'dns',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'dns');
		    },
		},
		{
		    header: gettext('Reverse dns'),
		    flex: 3,
		    dataIndex: 'reversedns',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'reversedns');
		    },
		},
		{
		    header: gettext('Nodes'),
		    flex: 3,
		    dataIndex: 'nodes',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending(rec, value, 'nodes');
		    },
		},
		{
		    header: gettext('State'),
		    width: 100,
		    dataIndex: 'state',
		    renderer: function(value, metaData, rec) {
			return PVE.Utils.render_sdn_pending_state(rec, value);
		    },
		},
	    ],
	    listeners: {
		activate: reload,
		itemdblclick: run_editor,
		selectionchange: set_button_status,
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.sdn.Options', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveSDNOptions',
    title: 'Options',
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    onlineHelp: 'pvesdn_config_controllers',
    items: [
	{
	    xtype: 'pveSDNControllerView',
	    title: gettext('Controllers'),
	    flex: 1,
	    padding: '0 0 20 0',
	    border: 0,
	},
	{
	    xtype: 'pveSDNIpamView',
	    title: 'IPAMs',
	    flex: 1,
	    padding: '0 0 20 0',
	    border: 0,
	}, {
	    xtype: 'pveSDNDnsView',
	    title: 'DNS',
	    flex: 1,
	    border: 0,
	},
    ],
});
Ext.define('PVE.panel.SDNControllerBase', {
    extend: 'Proxmox.panel.InputPanel',
    type: '',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.controller;
	}
	return values;
    },
});
Ext.define('PVE.sdn.controllers.BaseEdit', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	me.isCreate = !me.controllerid;
	if (me.isCreate) {
	    me.url = '/api2/extjs/cluster/sdn/controllers';
	    me.method = 'POST';
	} else {
	    me.url = '/api2/extjs/cluster/sdn/controllers/' + me.controllerid;
	    me.method = 'PUT';
	}
	var ipanel = Ext.create(me.paneltype, {
	    type: me.type,
	    isCreate: me.isCreate,
	    controllerid: me.controllerid,
	});
	Ext.apply(me, {
	    subject: PVE.Utils.format_sdncontroller_type(me.type),
	    isAdd: true,
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    var ctypes = values.content || '';
		    values.content = ctypes.split(',');
		    if (values.nodes) {
			values.nodes = values.nodes.split(',');
		    }
		    values.enable = values.disable ? 0 : 1;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.sdn.controllers.EvpnInputPanel', {
    extend: 'PVE.panel.SDNControllerBase',
    onlineHelp: 'pvesdn_controller_plugin_evpn',
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'controller',
		maxLength: 8,
		value: me.controllerid || '',
		fieldLabel: 'ID',
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'asn',
		minValue: 1,
		maxValue: 4294967295,
		value: 65000,
		fieldLabel: 'ASN #',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'peers',
		fieldLabel: gettext('Peers'),
		allowBlank: false,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.controllers.BgpInputPanel', {
    extend: 'PVE.panel.SDNControllerBase',
    onlineHelp: 'pvesdn_controller_plugin_evpn',
    onGetValues: function(values) {
        var me = this;
        if (me.isCreate) {
            values.type = me.type;
	    values.controller = 'bgp' + values.node;
        } else {
            delete values.controller;
        }
        return values;
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: 'pveNodeSelector',
		name: 'node',
		fieldLabel: gettext('Node'),
		multiSelect: false,
		autoSelect: false,
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'asn',
		minValue: 1,
		maxValue: 4294967295,
		value: 65000,
		fieldLabel: 'ASN #',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'peers',
		fieldLabel: gettext('Peers'),
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'ebgp',
		uncheckedValue: 0,
		checked: false,
		fieldLabel: 'EBGP',
	    },
	];
	me.advancedItems = [
	    {
		xtype: 'textfield',
		name: 'loopback',
		fieldLabel: gettext('Loopback Interface'),
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'ebgp-multihop',
		minValue: 1,
		maxValue: 100,
		fieldLabel: 'ebgp-multihop',
		allowBlank: true,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'bgp-multipath-as-path-relax',
		uncheckedValue: 0,
		checked: false,
		fieldLabel: 'bgp-multipath-as-path-relax',
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.IpamView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveSDNIpamView'],
    stateful: true,
    stateId: 'grid-sdn-ipam',
    createSDNEditWindow: function(type, sid) {
	let schema = PVE.Utils.sdnipamSchema[type];
	if (!schema || !schema.ipanel) {
	    throw "no editor registered for ipam type: " + type;
	}
	Ext.create('PVE.sdn.ipams.BaseEdit', {
	    paneltype: 'PVE.sdn.ipams.' + schema.ipanel,
	    type: type,
	    ipam: sid,
	    autoShow: true,
	    listeners: {
		destroy: this.reloadStore,
	    },
	});
    },
    initComponent: function() {
	let me = this;
	let store = new Ext.data.Store({
	    model: 'pve-sdn-ipam',
	    proxy: {
		type: 'proxmox',
		url: "/api2/json/cluster/sdn/ipams",
	    },
	    sorters: {
		property: 'ipam',
		direction: 'ASC',
	    },
	});
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    let type = rec.data.type, ipam = rec.data.ipam;
	    me.createSDNEditWindow(type, ipam);
	};
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/cluster/sdn/ipams/',
	    callback: () => store.load(),
	});
	// else we cannot dynamically generate the add menu handlers
	let addHandleGenerator = function(type) {
	    return function() { me.createSDNEditWindow(type); };
	};
	let addMenuItems = [];
	for (const [type, ipam] of Object.entries(PVE.Utils.sdnipamSchema)) {
	    if (ipam.hideAdd) {
		continue;
	    }
	    addMenuItems.push({
		text: PVE.Utils.format_sdnipam_type(type),
		iconCls: 'fa fa-fw fa-' + ipam.faIcon,
		handler: addHandleGenerator(type),
	    });
	}
	Ext.apply(me, {
	    store: store,
	    reloadStore: () => store.load(),
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: new Ext.menu.Menu({
			items: addMenuItems,
		    }),
		},
		remove_btn,
		edit_btn,
	    ],
	    columns: [
		{
		    header: 'ID',
		    flex: 2,
		    dataIndex: 'ipam',
		},
		{
		    header: gettext('Type'),
		    flex: 1,
		    dataIndex: 'type',
		    renderer: PVE.Utils.format_sdnipam_type,
		},
		{
		    header: 'url',
		    flex: 1,
		    dataIndex: 'url',
		},
	    ],
	    listeners: {
		activate: () => store.load(),
		itemdblclick: run_editor,
	    },
	});
	store.load();
	me.callParent();
    },
});
Ext.define('PVE.panel.SDNIpamBase', {
    extend: 'Proxmox.panel.InputPanel',
    type: '',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.ipam;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	me.callParent();
    },
});
Ext.define('PVE.sdn.ipams.BaseEdit', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	me.isCreate = !me.ipam;
	if (me.isCreate) {
	    me.url = '/api2/extjs/cluster/sdn/ipams';
	    me.method = 'POST';
	} else {
	    me.url = '/api2/extjs/cluster/sdn/ipams/' + me.ipam;
	    me.method = 'PUT';
	}
	var ipanel = Ext.create(me.paneltype, {
	    type: me.type,
	    isCreate: me.isCreate,
	    ipam: me.ipam,
	});
	Ext.apply(me, {
	    subject: PVE.Utils.format_sdnipam_type(me.type),
	    isAdd: true,
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    var ctypes = values.content || '';
		    values.content = ctypes.split(',');
		    if (values.nodes) {
			values.nodes = values.nodes.split(',');
		    }
		    values.enable = values.disable ? 0 : 1;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.sdn.ipams.NetboxInputPanel', {
    extend: 'PVE.panel.SDNIpamBase',
    onlineHelp: 'pvesdn_ipam_plugin_netbox',
    onGetValues: function(values) {
        var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.ipam;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'ipam',
		maxLength: 10,
		value: me.zone || '',
		fieldLabel: 'ID',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'url',
		fieldLabel: gettext('Url'),
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'token',
		fieldLabel: gettext('Token'),
		allowBlank: false,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.ipams.PVEIpamInputPanel', {
    extend: 'PVE.panel.SDNIpamBase',
    onlineHelp: 'pvesdn_ipam_plugin_pveipam',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.ipam;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'ipam',
		maxLength: 10,
		value: me.zone || '',
		fieldLabel: 'ID',
		allowBlank: false,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.ipams.PhpIpamInputPanel', {
    extend: 'PVE.panel.SDNIpamBase',
    onlineHelp: 'pvesdn_ipam_plugin_phpipam',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.ipam;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'ipam',
		maxLength: 10,
		value: me.zone || '',
		fieldLabel: 'ID',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'url',
		fieldLabel: gettext('Url'),
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'token',
		fieldLabel: gettext('Token'),
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'section',
		fieldLabel: gettext('Section'),
		allowBlank: false,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.DnsView', {
    extend: 'Ext.grid.GridPanel',
    alias: ['widget.pveSDNDnsView'],
    stateful: true,
    stateId: 'grid-sdn-dns',
    createSDNEditWindow: function(type, sid) {
	let schema = PVE.Utils.sdndnsSchema[type];
	if (!schema || !schema.ipanel) {
	    throw "no editor registered for dns type: " + type;
	}
	Ext.create('PVE.sdn.dns.BaseEdit', {
	    paneltype: 'PVE.sdn.dns.' + schema.ipanel,
	    type: type,
	    dns: sid,
	    autoShow: true,
	    listeners: {
		destroy: this.reloadStore,
	    },
	});
    },
    initComponent: function() {
	let me = this;
	let store = new Ext.data.Store({
	    model: 'pve-sdn-dns',
	    proxy: {
		type: 'proxmox',
		url: "/api2/json/cluster/sdn/dns",
	    },
		sorters: {
		property: 'dns',
		direction: 'ASC',
	    },
	});
	let sm = Ext.create('Ext.selection.RowModel', {});
	let run_editor = function() {
	    let rec = sm.getSelection()[0];
	    if (!rec) {
		return;
	    }
	    let type = rec.data.type,
		dns = rec.data.dns;
	    me.createSDNEditWindow(type, dns);
	};
	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    disabled: true,
	    selModel: sm,
	    handler: run_editor,
	});
	let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
	    selModel: sm,
	    baseurl: '/cluster/sdn/dns/',
	    callback: () => store.load(),
	});
	// else we cannot dynamically generate the add menu handlers
	let addHandleGenerator = function(type) {
	    return function() { me.createSDNEditWindow(type); };
	};
	let addMenuItems = [];
	for (const [type, dns] of Object.entries(PVE.Utils.sdndnsSchema)) {
	    if (dns.hideAdd) {
		continue;
	    }
	    addMenuItems.push({
		text: PVE.Utils.format_sdndns_type(type),
		iconCls: 'fa fa-fw fa-' + dns.faIcon,
		handler: addHandleGenerator(type),
	    });
	}
	Ext.apply(me, {
	    store: store,
	    reloadStore: () => store.load(),
	    selModel: sm,
	    viewConfig: {
		trackOver: false,
	    },
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: new Ext.menu.Menu({
			items: addMenuItems,
		    }),
		},
		remove_btn,
		edit_btn,
	    ],
	    columns: [
		{
		    header: 'ID',
		    flex: 2,
		    dataIndex: 'dns',
		},
		{
		    header: gettext('Type'),
		    flex: 1,
		    dataIndex: 'type',
		    renderer: PVE.Utils.format_sdndns_type,
		},
		{
		    header: 'url',
		    flex: 1,
		    dataIndex: 'url',
		},
	    ],
	    listeners: {
		activate: () => store.load(),
		itemdblclick: run_editor,
	    },
	});
	store.load();
	me.callParent();
    },
});
Ext.define('PVE.panel.SDNDnsBase', {
    extend: 'Proxmox.panel.InputPanel',
    type: '',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.dns;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	me.callParent();
    },
});
Ext.define('PVE.sdn.dns.BaseEdit', {
    extend: 'Proxmox.window.Edit',
    initComponent: function() {
	var me = this;
	me.isCreate = !me.dns;
	if (me.isCreate) {
	    me.url = '/api2/extjs/cluster/sdn/dns';
	    me.method = 'POST';
	} else {
	    me.url = '/api2/extjs/cluster/sdn/dns/' + me.dns;
	    me.method = 'PUT';
	}
	var ipanel = Ext.create(me.paneltype, {
	    type: me.type,
	    isCreate: me.isCreate,
	    dns: me.dns,
	});
	Ext.apply(me, {
            subject: PVE.Utils.format_sdndns_type(me.type),
	    isAdd: true,
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    var ctypes = values.content || '';
		    values.content = ctypes.split(',');
		    if (values.nodes) {
			values.nodes = values.nodes.split(',');
		    }
		    values.enable = values.disable ? 0 : 1;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.sdn.dns.PowerdnsInputPanel', {
    extend: 'PVE.panel.SDNDnsBase',
    onlineHelp: 'pvesdn_dns_plugin_powerdns',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.dns;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'dns',
		maxLength: 10,
		value: me.dns || '',
		fieldLabel: 'ID',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'url',
		fieldLabel: 'url',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'key',
		fieldLabel: gettext('api key'),
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'ttl',
		fieldLabel: 'ttl',
		allowBlank: true,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.panel.SDNZoneBase', {
    extend: 'Proxmox.panel.InputPanel',
    type: '',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.zone;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
	me.advancedItems = [
	    {
		xtype: 'pveSDNIpamSelector',
		fieldLabel: gettext('Ipam'),
		name: 'ipam',
		value: 'pve',
		allowBlank: false,
	    },
	    {
		xtype: 'pveSDNDnsSelector',
		fieldLabel: gettext('Dns server'),
		name: 'dns',
		value: '',
		allowBlank: true,
	    },
	    {
		xtype: 'pveSDNDnsSelector',
		fieldLabel: gettext('Reverse Dns server'),
		name: 'reversedns',
		value: '',
		allowBlank: true,
	    },
	    {
		xtype: 'proxmoxtextfield',
		name: 'dnszone',
		skipEmptyText: true,
		fieldLabel: gettext('DNS zone'),
		allowBlank: true,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.zones.BaseEdit', {
    extend: 'Proxmox.window.Edit',
    width: 400,
    initComponent: function() {
	var me = this;
	me.isCreate = !me.zone;
	if (me.isCreate) {
	    me.url = '/api2/extjs/cluster/sdn/zones';
	    me.method = 'POST';
	} else {
	    me.url = '/api2/extjs/cluster/sdn/zones/' + me.zone;
	    me.method = 'PUT';
	}
	var ipanel = Ext.create(me.paneltype, {
	    type: me.type,
	    isCreate: me.isCreate,
	    zone: me.zone,
	});
	Ext.apply(me, {
	    subject: PVE.Utils.format_sdnzone_type(me.type),
	    isAdd: true,
	    items: [ipanel],
	});
	me.callParent();
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    var values = response.result.data;
		    var ctypes = values.content || '';
		    values.content = ctypes.split(',');
		    if (values.nodes) {
			values.nodes = values.nodes.split(',');
		    }
		    if (values.exitnodes) {
			values.exitnodes = values.exitnodes.split(',');
		    }
		    values.enable = values.disable ? 0 : 1;
		    ipanel.setValues(values);
		},
	    });
	}
    },
});
Ext.define('PVE.sdn.zones.EvpnInputPanel', {
    extend: 'PVE.panel.SDNZoneBase',
    onlineHelp: 'pvesdn_zone_plugin_evpn',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.zone;
	}
        if (!values.mac) {
            delete values.mac;
        }
        if (values['advertise-subnets'] === 0) {
            delete values['advertise-subnets'];
        }
        if (values['exitnodes-local-routing'] === 0) {
            delete values['exitnodes-local-routing'];
        }
        if (values['disable-arp-nd-suppression'] === 0) {
            delete values['disable-arp-nd-suppression'];
        }
        if (values['exitnodes-primary'] === '') {
            delete values['exitnodes-primary'];
        }
	return values;
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'zone',
		maxLength: 8,
		value: me.zone || '',
		fieldLabel: 'ID',
		allowBlank: false,
	    },
	    {
		xtype: 'pveSDNControllerSelector',
		fieldLabel: gettext('Controller'),
		name: 'controller',
		value: '',
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'vrf-vxlan',
		minValue: 1,
		maxValue: 16000000,
		fieldLabel: 'VRF-VXLAN Tag',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'mac',
		fieldLabel: gettext('Vnet MAC address'),
		vtype: 'MacAddress',
		allowBlank: true,
		emptyText: 'auto',
	    },
	    {
		xtype: 'pveNodeSelector',
		name: 'exitnodes',
		fieldLabel: gettext('Exit Nodes'),
		multiSelect: true,
		autoSelect: false,
	    },
	    {
		xtype: 'pveNodeSelector',
		name: 'exitnodes-primary',
		fieldLabel: gettext('Primary Exit Node'),
		multiSelect: false,
		autoSelect: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'exitnodes-local-routing',
		uncheckedValue: 0,
		checked: false,
		fieldLabel: gettext('Exit Nodes local routing'),
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'advertise-subnets',
		uncheckedValue: 0,
		checked: false,
		fieldLabel: gettext('Advertise subnets'),
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'disable-arp-nd-suppression',
		uncheckedValue: 0,
		checked: false,
		fieldLabel: gettext('Disable arp-nd suppression'),
	    },
	    {
		xtype: 'textfield',
		name: 'rt-import',
		fieldLabel: gettext('Route-target import'),
		allowBlank: true,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'mtu',
		minValue: 100,
		maxValue: 65000,
		fieldLabel: 'MTU',
		skipEmptyText: true,
		allowBlank: true,
		emptyText: 'auto',
	    },
	    {
		xtype: 'pveNodeSelector',
		name: 'nodes',
		fieldLabel: gettext('Nodes'),
		emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
		multiSelect: true,
		autoSelect: false,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.zones.QinQInputPanel', {
    extend: 'PVE.panel.SDNZoneBase',
    onlineHelp: 'pvesdn_zone_plugin_qinq',
    onGetValues: function(values) {
	let me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.sdn;
	}
	return values;
    },
    initComponent: function() {
	let me = this;
	me.items = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'zone',
		maxLength: 8,
		value: me.zone || '',
		fieldLabel: 'ID',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'bridge',
		fieldLabel: 'Bridge',
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'tag',
		minValue: 0,
		maxValue: 4096,
		fieldLabel: gettext('Service VLAN'),
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxKVComboBox',
		name: 'vlan-protocol',
		fieldLabel: gettext('Service-VLAN Protocol'),
		allowBlank: true,
		value: '802.1q',
		comboItems: [
		    ['802.1q', '802.1q'],
		    ['802.1ad', '802.1ad'],
		],
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'mtu',
		minValue: 100,
		maxValue: 65000,
		fieldLabel: 'MTU',
		skipEmptyText: true,
		allowBlank: true,
		emptyText: 'auto',
	    },
	    {
		xtype: 'pveNodeSelector',
		name: 'nodes',
		fieldLabel: gettext('Nodes'),
		emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
		multiSelect: true,
		autoSelect: false,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.zones.SimpleInputPanel', {
    extend: 'PVE.panel.SDNZoneBase',
    onlineHelp: 'pvesdn_zone_plugin_simple',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.zone;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
        me.items = [
           {
            xtype: me.isCreate ? 'textfield' : 'displayfield',
            name: 'zone',
            maxLength: 10,
            value: me.zone || '',
            fieldLabel: 'ID',
            allowBlank: false,
          },
          {
            xtype: 'proxmoxintegerfield',
            name: 'mtu',
            minValue: 100,
            maxValue: 65000,
            fieldLabel: 'MTU',
            skipEmptyText: true,
            allowBlank: true,
            emptyText: 'auto',
          },
          {
            xtype: 'pveNodeSelector',
            name: 'nodes',
            fieldLabel: gettext('Nodes'),
            emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
            multiSelect: true,
            autoSelect: false,
          },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.zones.VlanInputPanel', {
    extend: 'PVE.panel.SDNZoneBase',
    onlineHelp: 'pvesdn_zone_plugin_vlan',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.zone;
	}
	return values;
    },
    initComponent: function() {
	var me = this;
        me.items = [
           {
            xtype: me.isCreate ? 'textfield' : 'displayfield',
            name: 'zone',
            maxLength: 10,
            value: me.zone || '',
            fieldLabel: 'ID',
            allowBlank: false,
          },
          {
            xtype: 'textfield',
            name: 'bridge',
            fieldLabel: 'Bridge',
            allowBlank: false,
          },
          {
            xtype: 'proxmoxintegerfield',
            name: 'mtu',
            minValue: 100,
            maxValue: 65000,
            fieldLabel: 'MTU',
            skipEmptyText: true,
            allowBlank: true,
            emptyText: 'auto',
          },
          {
            xtype: 'pveNodeSelector',
            name: 'nodes',
            fieldLabel: gettext('Nodes'),
            emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
            multiSelect: true,
            autoSelect: false,
          },
	];
	me.callParent();
    },
});
Ext.define('PVE.sdn.zones.VxlanInputPanel', {
    extend: 'PVE.panel.SDNZoneBase',
    onlineHelp: 'pvesdn_zone_plugin_vxlan',
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.zone;
	}
	delete values.mode;
	return values;
    },
    initComponent: function() {
	var me = this;
	me.items = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		maxLength: 8,
		name: 'zone',
		value: me.zone || '',
		fieldLabel: 'ID',
		allowBlank: false,
	    },
	    {
		xtype: 'textfield',
		name: 'peers',
		fieldLabel: gettext('Peer Address List'),
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxintegerfield',
		name: 'mtu',
		minValue: 100,
		maxValue: 65000,
		fieldLabel: 'MTU',
		skipEmptyText: true,
		allowBlank: true,
		emptyText: 'auto',
	    },
	    {
		xtype: 'pveNodeSelector',
		name: 'nodes',
		fieldLabel: gettext('Nodes'),
		emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
		multiSelect: true,
		autoSelect: false,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.ContentView', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveStorageContentView',
    viewConfig: {
	trackOver: false,
	loadMask: false,
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    me.nodename = me.pveSelNode.data.node;
	    if (!me.nodename) {
		throw "no node name specified";
	    }
	}
	const nodename = me.nodename;
	if (!me.storage) {
	    me.storage = me.pveSelNode.data.storage;
	    if (!me.storage) {
		throw "no storage ID specified";
	    }
	}
	const storage = me.storage;
	var content = me.content;
	if (!content) {
	    throw "no content type specified";
	}
	const baseurl = `/nodes/${nodename}/storage/${storage}/content`;
	let store = me.store = Ext.create('Ext.data.Store', {
	    model: 'pve-storage-content',
	    proxy: {
                type: 'proxmox',
		url: '/api2/json' + baseurl,
		extraParams: {
		    content: content,
		},
	    },
	    sorters: {
		property: 'volid',
		direction: 'ASC',
	    },
	});
	if (!me.sm) {
	    me.sm = Ext.create('Ext.selection.RowModel', {});
	}
	let sm = me.sm;
	let reload = () => store.load();
	Proxmox.Utils.monStoreErrors(me, store);
	if (!me.tbar) {
	    me.tbar = [];
	}
	if (me.useUploadButton) {
	    me.tbar.unshift(
		{
		    xtype: 'button',
		    text: gettext('Upload'),
		    disabled: !me.enableUploadButton,
		    handler: function() {
			Ext.create('PVE.window.UploadToStorage', {
			    nodename: nodename,
			    storage: storage,
			    content: content,
			    autoShow: true,
			    taskDone: () => reload(),
			});
		    },
		},
		{
		    xtype: 'button',
		    text: gettext('Download from URL'),
		    disabled: !me.enableDownloadUrlButton,
		    handler: function() {
			Ext.create('PVE.window.DownloadUrlToStorage', {
			    nodename: nodename,
			    storage: storage,
			    content: content,
			    autoShow: true,
			    taskDone: () => reload(),
			});
		    },
		},
		'-',
	    );
	}
	if (!me.useCustomRemoveButton) {
	    me.tbar.push({
		xtype: 'proxmoxStdRemoveButton',
		selModel: sm,
		enableFn: rec => !rec?.data?.protected,
		delay: 5,
		callback: () => reload(),
		baseurl: baseurl + '/',
	    });
	}
	me.tbar.push(
	    '->',
	    gettext('Search') + ':',
	    ' ',
	    {
		xtype: 'textfield',
		width: 200,
		enableKeyEvents: true,
		emptyText: content === 'backup' ? gettext('Name, Format, Notes') : gettext('Name, Format'),
		listeners: {
		    keyup: {
			buffer: 500,
			fn: function(field) {
			    let needle = field.getValue().toLocaleLowerCase();
			    store.clearFilter(true);
			    store.filter([
				{
				    filterFn: ({ data }) =>
				        data.text?.toLocaleLowerCase().includes(needle) ||
				        data.notes?.toLocaleLowerCase().includes(needle),
				},
			    ]);
			},
		    },
		    change: function(field, newValue, oldValue) {
			if (newValue !== this.originalValue) {
			    this.triggers.clear.setVisible(true);
			}
		    },
		},
		triggers: {
		    clear: {
			cls: 'pmx-clear-trigger',
			weight: -1,
			hidden: true,
			handler: function() {
			    this.triggers.clear.setVisible(false);
			    this.setValue(this.originalValue);
			    store.clearFilter();
			},
		    },
		},
	    },
	);
	let availableColumns = {
	    'name': {
		header: gettext('Name'),
		flex: 2,
		sortable: true,
		renderer: PVE.Utils.render_storage_content,
		dataIndex: 'text',
	    },
	    'notes': {
		header: gettext('Notes'),
		flex: 1,
		renderer: Ext.htmlEncode,
		dataIndex: 'notes',
	    },
	    'protected': {
		header: ``,
		tooltip: gettext('Protected'),
		width: 30,
		renderer: v => v ? `` : '',
		sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0),
		dataIndex: 'protected',
	    },
	    'date': {
		header: gettext('Date'),
		width: 150,
		dataIndex: 'vdate',
	    },
	    'format': {
		header: gettext('Format'),
		width: 100,
		dataIndex: 'format',
	    },
	    'size': {
		header: gettext('Size'),
		width: 100,
		renderer: Proxmox.Utils.format_size,
		dataIndex: 'size',
	    },
	};
	let showColumns = me.showColumns || ['name', 'date', 'format', 'size'];
	Object.keys(availableColumns).forEach(function(key) {
	    if (!showColumns.includes(key)) {
		delete availableColumns[key];
	    }
	});
	if (me.extraColumns && typeof me.extraColumns === 'object') {
	    Object.assign(availableColumns, me.extraColumns);
	}
	const columns = Object.values(availableColumns);
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: me.tbar,
	    columns: columns,
	    listeners: {
		activate: reload,
	    },
	});
	me.callParent();
    },
}, function() {
    Ext.define('pve-storage-content', {
	extend: 'Ext.data.Model',
	fields: [
	    'volid', 'content', 'format', 'size', 'used', 'vmid',
	    'channel', 'id', 'lun', 'notes', 'verification',
	    {
		name: 'text',
		convert: function(value, record) {
		    // check for volid, because if you click on a grouping header,
		    // it calls convert (but with an empty volid)
		    if (value || record.data.volid === null) {
			return value;
		    }
		    return PVE.Utils.render_storage_content(value, {}, record);
		},
	    },
	    {
		name: 'vdate',
		convert: function(value, record) {
		    // check for volid, because if you click on a grouping header,
		    // it calls convert (but with an empty volid)
		    if (value || record.data.volid === null) {
			return value;
		    }
		    let t = record.data.content;
		    if (t === "backup") {
			let v = record.data.volid;
			let match = v.match(/(\d{4}_\d{2}_\d{2})-(\d{2}_\d{2}_\d{2})/);
			if (match) {
			    let date = match[1].replace(/_/g, '-');
			    let time = match[2].replace(/_/g, ':');
			    return date + " " + time;
			}
		    }
		    if (record.data.ctime) {
			let ctime = new Date(record.data.ctime * 1000);
			return Ext.Date.format(ctime, 'Y-m-d H:i:s');
		    }
		    return '';
		},
	    },
	],
	idProperty: 'volid',
    });
});
Ext.define('PVE.storage.BackupView', {
    extend: 'PVE.storage.ContentView',
    alias: 'widget.pveStorageBackupView',
    showColumns: ['name', 'notes', 'protected', 'date', 'format', 'size'],
    initComponent: function() {
	let me = this;
	let nodename = me.nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	let storage = me.storage = me.pveSelNode.data.storage;
	if (!storage) {
	    throw "no storage ID specified";
	}
	me.content = 'backup';
	let sm = me.sm = Ext.create('Ext.selection.RowModel', {});
	let pruneButton = Ext.create('Proxmox.button.Button', {
	    text: gettext('Prune group'),
	    disabled: true,
	    selModel: sm,
	    setBackupGroup: function(backup) {
		if (backup) {
		    let name = backup.text;
		    let vmid = backup.vmid;
		    let format = backup.format;
		    let vmtype;
		    if (name.startsWith('vzdump-lxc-') || format === "pbs-ct") {
			vmtype = 'lxc';
		    } else if (name.startsWith('vzdump-qemu-') || format === "pbs-vm") {
			vmtype = 'qemu';
		    }
		    if (vmid && vmtype) {
			this.setText(gettext('Prune group') + ` ${vmtype}/${vmid}`);
			this.vmid = vmid;
			this.vmtype = vmtype;
			this.setDisabled(false);
			return;
		    }
		}
		this.setText(gettext('Prune group'));
		this.vmid = null;
		this.vmtype = null;
		this.setDisabled(true);
	    },
	    handler: function(b, e, rec) {
		Ext.create('PVE.window.Prune', {
		    autoShow: true,
		    nodename,
		    storage,
		    backup_id: this.vmid,
		    backup_type: this.vmtype,
		    listeners: {
			destroy: () => me.store.load(),
		    },
		});
	    },
	});
	me.on('selectionchange', function(model, srecords, eOpts) {
	    if (srecords.length === 1) {
		pruneButton.setBackupGroup(srecords[0].data);
	    } else {
		pruneButton.setBackupGroup(null);
	    }
	});
	let isPBS = me.pluginType === 'pbs';
	me.tbar = [
	    {
		xtype: 'proxmoxButton',
		text: gettext('Restore'),
		selModel: sm,
		disabled: true,
		handler: function(b, e, rec) {
		    let vmtype;
		    if (PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format)) {
			vmtype = 'qemu';
		    } else if (PVE.Utils.volume_is_lxc_backup(rec.data.volid, rec.data.format)) {
			vmtype = 'lxc';
		    } else {
			return;
		    }
		    Ext.create('PVE.window.Restore', {
			autoShow: true,
			nodename,
			volid: rec.data.volid,
			volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
			vmtype,
			isPBS,
			listeners: {
			    destroy: () => me.store.load(),
			},
		    });
		},
	    },
	];
	if (isPBS) {
	    me.tbar.push({
		xtype: 'proxmoxButton',
		text: gettext('File Restore'),
		disabled: true,
		selModel: sm,
		handler: function(b, e, rec) {
		    let isVMArchive = PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format);
		    Ext.create('Proxmox.window.FileBrowser', {
			title: gettext('File Restore') + " - " + rec.data.text,
			listURL: `/api2/json/nodes/localhost/storage/${me.storage}/file-restore/list`,
			downloadURL: `/api2/json/nodes/localhost/storage/${me.storage}/file-restore/download`,
			extraParams: {
			    volume: rec.data.volid,
			},
			archive: isVMArchive ? 'all' : undefined,
			autoShow: true,
		    });
		},
	    });
	}
	me.tbar.push(
	    {
		xtype: 'proxmoxButton',
		text: gettext('Show Configuration'),
		disabled: true,
		selModel: sm,
		handler: function(b, e, rec) {
		    Ext.create('PVE.window.BackupConfig', {
			autoShow: true,
			volume: rec.data.volid,
			pveSelNode: me.pveSelNode,
		    });
		},
	    },
	    {
		xtype: 'proxmoxButton',
		text: gettext('Edit Notes'),
		disabled: true,
		selModel: sm,
		handler: function(b, e, rec) {
		    let volid = rec.data.volid;
		    Ext.create('Proxmox.window.Edit', {
			autoShow: true,
			autoLoad: true,
			width: 600,
			height: 400,
			resizable: true,
			title: gettext('Notes'),
			url: `/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`,
			layout: 'fit',
			items: [
			    {
				xtype: 'textarea',
				layout: 'fit',
				name: 'notes',
				height: '100%',
			    },
			],
			listeners: {
			    destroy: () => me.store.load(),
			},
		    });
		},
	    },
	    {
		xtype: 'proxmoxButton',
		text: gettext('Change Protection'),
		disabled: true,
		handler: function(button, event, record) {
		    const volid = record.data.volid;
		    Proxmox.Utils.API2Request({
			url: `/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`,
			method: 'PUT',
			waitMsgTarget: me,
			params: { 'protected': record.data.protected ? 0 : 1 },
			failure: response => Ext.Msg.alert('Error', response.htmlStatus),
			success: () => {
			    me.store.load({
				callback: () => sm.fireEvent('selectionchange', sm, [record]),
			    });
			},
		    });
		},
	    },
	    '-',
	    pruneButton,
	);
	if (isPBS) {
	    me.extraColumns = {
		encrypted: {
		    header: gettext('Encrypted'),
		    dataIndex: 'encrypted',
		    renderer: PVE.Utils.render_backup_encryption,
		    sorter: {
			property: 'encrypted',
			transform: encrypted => encrypted ? 1 : 0,
		    },
		},
		verification: {
		    header: gettext('Verify State'),
		    dataIndex: 'verification',
		    renderer: PVE.Utils.render_backup_verification,
		    sorter: {
			property: 'verification',
			transform: value => {
			    let state = value?.state ?? 'none';
			    let order = PVE.Utils.verificationStateOrder;
			    return order[state] ?? order.__default__;
			},
		    },
		},
	    };
	}
	me.callParent();
	me.store.getSorters().clear();
	me.store.setSorters([
	    {
		property: 'vmid',
		direction: 'ASC',
	    },
	    {
		property: 'vdate',
		direction: 'DESC',
	    },
	]);
    },
});
Ext.define('PVE.panel.StorageBase', {
    extend: 'Proxmox.panel.InputPanel',
    controller: 'storageEdit',
    type: '',
    onGetValues: function(values) {
	let me = this;
	if (me.isCreate) {
	    values.type = me.type;
	} else {
	    delete values.storage;
	}
	values.disable = values.enable ? 0 : 1;
	delete values.enable;
	return values;
    },
    initComponent: function() {
	let me = this;
	me.column1.unshift({
	    xtype: me.isCreate ? 'textfield' : 'displayfield',
	    name: 'storage',
	    value: me.storageId || '',
	    fieldLabel: 'ID',
	    vtype: 'StorageId',
	    allowBlank: false,
	});
	me.column2 = me.column2 || [];
	me.column2.unshift(
	    {
		xtype: 'pveNodeSelector',
		name: 'nodes',
		reference: 'storageNodeRestriction',
		disabled: me.storageId === 'local',
		fieldLabel: gettext('Nodes'),
		emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
		multiSelect: true,
		autoSelect: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'enable',
		checked: true,
		uncheckedValue: 0,
		fieldLabel: gettext('Enable'),
	    },
	);
	const qemuImgStorageTypes = ['dir', 'btrfs', 'nfs', 'cifs', 'glusterfs'];
	if (qemuImgStorageTypes.includes(me.type)) {
	    const preallocSelector = {
		xtype: 'pvePreallocationSelector',
		name: 'preallocation',
		fieldLabel: gettext('Preallocation'),
		allowBlank: false,
		deleteEmpty: !me.isCreate,
		value: '__default__',
	    };
	    me.advancedColumn1 = me.advancedColumn1 || [];
	    me.advancedColumn2 = me.advancedColumn2 || [];
	    if (me.advancedColumn2.length < me.advancedColumn1.length) {
		me.advancedColumn2.unshift(preallocSelector);
	    } else {
		me.advancedColumn1.unshift(preallocSelector);
	    }
	}
	me.callParent();
    },
});
Ext.define('PVE.storage.BaseEdit', {
    extend: 'Proxmox.window.Edit',
    apiCallDone: function(success, response, options) {
	let me = this;
	if (typeof me.ipanel.apiCallDone === "function") {
	    me.ipanel.apiCallDone(success, response, options);
	}
    },
    initComponent: function() {
	let me = this;
	me.isCreate = !me.storageId;
	if (me.isCreate) {
	    me.url = '/api2/extjs/storage';
	    me.method = 'POST';
	} else {
	    me.url = '/api2/extjs/storage/' + me.storageId;
	    me.method = 'PUT';
	}
	me.ipanel = Ext.create(me.paneltype, {
	    title: gettext('General'),
	    type: me.type,
	    isCreate: me.isCreate,
	    storageId: me.storageId,
	});
	Ext.apply(me, {
            subject: PVE.Utils.format_storage_type(me.type),
	    isAdd: true,
	    bodyPadding: 0,
	    items: {
		xtype: 'tabpanel',
		region: 'center',
		layout: 'fit',
		bodyPadding: 10,
		items: [
		    me.ipanel,
		    {
			xtype: 'pveBackupJobPrunePanel',
			title: gettext('Backup Retention'),
			hasMaxProtected: true,
			isCreate: me.isCreate,
			keepAllDefaultForCreate: true,
			showPBSHint: me.ipanel.isPBS,
			fallbackHintHtml: gettext('Without any keep option, the node\'s vzdump.conf or `keep-all` is used as fallback for backup jobs'),
		    },
		],
	    },
	});
	if (me.ipanel.extraTabs) {
	    me.ipanel.extraTabs.forEach(panel => {
		panel.isCreate = me.isCreate;
		me.items.items.push(panel);
	    });
	}
	me.callParent();
	if (!me.canDoBackups) {
	    // cannot mask now, not fully rendered until activated
	    me.down('pmxPruneInputPanel').needMask = true;
	}
	if (!me.isCreate) {
	    me.load({
		success: function(response, options) {
		    let values = response.result.data;
		    let ctypes = values.content || '';
		    values.content = ctypes.split(',');
		    if (values.nodes) {
			values.nodes = values.nodes.split(',');
		    }
		    values.enable = values.disable ? 0 : 1;
		    if (values['prune-backups']) {
			let retention = PVE.Parser.parsePropertyString(values['prune-backups']);
			delete values['prune-backups'];
			Object.assign(values, retention);
		    } else if (values.maxfiles !== undefined) {
			if (values.maxfiles > 0) {
			    values['keep-last'] = values.maxfiles;
			}
			delete values.maxfiles;
		    }
		    me.query('inputpanel').forEach(panel => {
			panel.setValues(values);
		    });
		},
	    });
	}
    },
});
Ext.define('PVE.storage.Browser', {
    extend: 'PVE.panel.Config',
    alias: 'widget.PVE.storage.Browser',
    onlineHelp: 'chapter_storage',
    initComponent: function() {
        let me = this;
	let nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	let storeid = me.pveSelNode.data.storage;
	if (!storeid) {
	    throw "no storage ID specified";
	}
	me.items = [
	    {
		title: gettext('Summary'),
		xtype: 'pveStorageSummary',
		iconCls: 'fa fa-book',
		itemId: 'summary',
	    },
	];
	let caps = Ext.state.Manager.get('GuiCap');
	Ext.apply(me, {
	    title: Ext.String.format(gettext("Storage {0} on node {1}"), `'${storeid}'`, `'${nodename}'`),
	    hstateid: 'storagetab',
	});
	if (
	    caps.storage['Datastore.Allocate'] ||
	    caps.storage['Datastore.AllocateSpace'] ||
	    caps.storage['Datastore.Audit']
	) {
	    let storageInfo = PVE.data.ResourceStore.findRecord(
		'id',
		`storage/${nodename}/${storeid}`,
		0, // startIndex
		false, // anyMatch
		true, // caseSensitive
		true, // exactMatch
	    );
	    let res = storageInfo.data;
	    let plugin = res.plugintype;
	    let contents = res.content.split(',');
	    let enableUpload = !!caps.storage['Datastore.AllocateTemplate'];
	    let enableDownloadUrl = enableUpload && !!(caps.nodes['Sys.Audit'] && caps.nodes['Sys.Modify']);
	    if (contents.includes('backup')) {
		me.items.push({
		    xtype: 'pveStorageBackupView',
		    title: gettext('Backups'),
		    iconCls: 'fa fa-floppy-o',
		    itemId: 'contentBackup',
		    pluginType: plugin,
		});
	    }
	    if (contents.includes('images')) {
		me.items.push({
		    xtype: 'pveStorageImageView',
		    title: gettext('VM Disks'),
		    iconCls: 'fa fa-hdd-o',
		    itemId: 'contentImages',
		    content: 'images',
		    pluginType: plugin,
		});
	    }
	    if (contents.includes('rootdir')) {
		me.items.push({
		    xtype: 'pveStorageImageView',
		    title: gettext('CT Volumes'),
		    iconCls: 'fa fa-hdd-o lxc',
		    itemId: 'contentRootdir',
		    content: 'rootdir',
		    pluginType: plugin,
		});
	    }
	    if (contents.includes('iso')) {
		me.items.push({
		    xtype: 'pveStorageContentView',
		    title: gettext('ISO Images'),
		    iconCls: 'pve-itype-treelist-item-icon-cdrom',
		    itemId: 'contentIso',
		    content: 'iso',
		    pluginType: plugin,
		    enableUploadButton: enableUpload,
		    enableDownloadUrlButton: enableDownloadUrl,
		    useUploadButton: true,
		});
	    }
	    if (contents.includes('vztmpl')) {
		me.items.push({
		    xtype: 'pveStorageTemplateView',
		    title: gettext('CT Templates'),
		    iconCls: 'fa fa-file-o lxc',
		    itemId: 'contentVztmpl',
		    pluginType: plugin,
		    enableUploadButton: enableUpload,
		    enableDownloadUrlButton: enableDownloadUrl,
		    useUploadButton: true,
		});
	    }
	    if (contents.includes('snippets')) {
		me.items.push({
		    xtype: 'pveStorageContentView',
		    title: gettext('Snippets'),
		    iconCls: 'fa fa-file-code-o',
		    itemId: 'contentSnippets',
		    content: 'snippets',
		    pluginType: plugin,
		});
	    }
	}
	if (caps.storage['Permissions.Modify']) {
	    me.items.push({
		xtype: 'pveACLView',
		title: gettext('Permissions'),
		iconCls: 'fa fa-unlock',
		itemId: 'permissions',
		path: `/storage/${storeid}`,
	    });
	}
	me.callParent();
   },
});
Ext.define('PVE.storage.CIFSScan', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveCIFSScan',
    queryParam: 'server',
    valueField: 'share',
    displayField: 'share',
    matchFieldWidth: false,
    listConfig: {
	loadingText: gettext('Scanning...'),
	width: 350,
    },
    doRawQuery: Ext.emptyFn,
    onTriggerClick: function() {
	var me = this;
	if (!me.queryCaching || me.lastQuery !== me.cifsServer) {
	    me.store.removeAll();
	}
	var params = {};
	if (me.cifsUsername) {
	    params.username = me.cifsUsername;
	}
	if (me.cifsPassword) {
	    params.password = me.cifsPassword;
	}
	if (me.cifsDomain) {
	    params.domain = me.cifsDomain;
	}
	me.store.getProxy().setExtraParams(params);
	me.allQuery = me.cifsServer;
	me.callParent();
    },
    resetProxy: function() {
	let me = this;
	me.lastQuery = null;
	if (!me.readOnly && !me.disabled) {
	    if (me.isExpanded) {
		me.collapse();
	    }
	}
    },
    setServer: function(server) {
	if (this.cifsServer !== server) {
	    this.cifsServer = server;
	    this.resetProxy();
	}
    },
    setUsername: function(username) {
	if (this.cifsUsername !== username) {
	    this.cifsUsername = username;
	    this.resetProxy();
	}
    },
    setPassword: function(password) {
	if (this.cifsPassword !== password) {
	    this.cifsPassword = password;
	    this.resetProxy();
	}
    },
    setDomain: function(domain) {
	if (this.cifsDomain !== domain) {
	    this.cifsDomain = domain;
	    this.resetProxy();
	}
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	let store = Ext.create('Ext.data.Store', {
	    fields: ['description', 'share'],
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/nodes/' + me.nodename + '/scan/cifs',
	    },
	});
	store.sort('share', 'ASC');
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
	let picker = me.getPicker();
	// don't use monStoreErrors directly, it doesn't copes well with comboboxes
	picker.mon(store, 'beforeload', function(s, operation, eOpts) {
	    picker.unmask();
	    delete picker.minHeight;
	});
	picker.mon(store.proxy, 'afterload', function(proxy, request, success) {
	    if (success) {
		Proxmox.Utils.setErrorMask(picker, false);
		return;
	    }
	    let error = request._operation.getError();
	    let msg = Proxmox.Utils.getResponseErrorMessage(error);
	    if (msg) {
		picker.minHeight = 100;
	    }
	    Proxmox.Utils.setErrorMask(picker, msg);
	});
    },
});
Ext.define('PVE.storage.CIFSInputPanel', {
    extend: 'PVE.panel.StorageBase',
    onlineHelp: 'storage_cifs',
    onGetValues: function(values) {
	let me = this;
	if (values.password?.length === 0) {
	    delete values.password;
	}
	if (values.username?.length === 0) {
	    delete values.username;
	}
	return me.callParent([values]);
    },
    initComponent: function() {
	var me = this;
	me.column1 = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'server',
		value: '',
		fieldLabel: gettext('Server'),
		allowBlank: false,
		listeners: {
		    change: function(f, value) {
			if (me.isCreate) {
			    var exportField = me.down('field[name=share]');
			    exportField.setServer(value);
			}
		    },
		},
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'username',
		value: '',
		fieldLabel: gettext('Username'),
		emptyText: gettext('Guest user'),
		listeners: {
		    change: function(f, value) {
			if (!me.isCreate) {
			    return;
			}
			var exportField = me.down('field[name=share]');
			exportField.setUsername(value);
		    },
		},
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		inputType: 'password',
		name: 'password',
		value: me.isCreate ? '' : '********',
		emptyText: me.isCreate ? gettext('None') : '',
		fieldLabel: gettext('Password'),
		minLength: 1,
		listeners: {
		    change: function(f, value) {
			let exportField = me.down('field[name=share]');
			exportField.setPassword(value);
		    },
		},
	    },
	    {
		xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield',
		name: 'share',
		value: '',
		fieldLabel: 'Share',
		allowBlank: false,
	    },
	];
	me.column2 = [
	    {
		xtype: 'pveContentTypeSelector',
		name: 'content',
		value: 'images',
		multiSelect: true,
		fieldLabel: gettext('Content'),
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'domain',
		value: me.isCreate ? '' : undefined,
		fieldLabel: gettext('Domain'),
		allowBlank: true,
		listeners: {
		    change: function(f, value) {
			if (me.isCreate) {
			    let exportField = me.down('field[name=share]');
			    exportField.setDomain(value);
			}
		    },
		},
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.CephFSInputPanel', {
    extend: 'PVE.panel.StorageBase',
    controller: 'cephstorage',
    onlineHelp: 'storage_cephfs',
    viewModel: {
	type: 'cephstorage',
    },
    setValues: function(values) {
	if (values.monhost) {
	    this.viewModel.set('pveceph', false);
	    this.lookupReference('pvecephRef').setValue(false);
	    this.lookupReference('pvecephRef').resetOriginalValue();
	}
	this.callParent([values]);
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	me.type = 'cephfs';
	me.column1 = [];
	me.column1.push(
	    {
		xtype: 'textfield',
		name: 'monhost',
		vtype: 'HostList',
		value: '',
		bind: {
		    disabled: '{pveceph}',
		    submitValue: '{!pveceph}',
		    hidden: '{pveceph}',
		},
		fieldLabel: 'Monitor(s)',
		allowBlank: false,
	    },
	    {
		xtype: 'displayfield',
		reference: 'monhost',
		bind: {
		    disabled: '{!pveceph}',
		    hidden: '{!pveceph}',
		},
		value: '',
		fieldLabel: 'Monitor(s)',
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'username',
		value: 'admin',
		bind: {
		    disabled: '{pveceph}',
		    submitValue: '{!pveceph}',
		},
		fieldLabel: gettext('User name'),
		allowBlank: true,
	    },
	);
	if (me.isCreate) {
	    me.column1.push({
		xtype: 'pveCephFSSelector',
		nodename: me.nodename,
		name: 'fs-name',
		bind: {
		    disabled: '{!pveceph}',
		    submitValue: '{pveceph}',
		    hidden: '{!pveceph}',
		},
		fieldLabel: gettext('FS Name'),
		allowBlank: false,
	    }, {
		xtype: 'textfield',
		nodename: me.nodename,
		name: 'fs-name',
		bind: {
		    disabled: '{pveceph}',
		    submitValue: '{!pveceph}',
		    hidden: '{pveceph}',
		},
		fieldLabel: gettext('FS Name'),
	    });
	}
	me.column2 = [
	    {
		xtype: 'pveContentTypeSelector',
		cts: ['backup', 'iso', 'vztmpl', 'snippets'],
		fieldLabel: gettext('Content'),
		name: 'content',
		value: 'backup',
		multiSelect: true,
		allowBlank: false,
	    },
	];
	me.columnB = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'keyring',
		fieldLabel: gettext('Secret Key'),
		value: me.isCreate ? '' : '***********',
		allowBlank: false,
		bind: {
		    hidden: '{pveceph}',
		    disabled: '{pveceph}',
		},
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'pveceph',
		reference: 'pvecephRef',
		bind: {
		    disabled: '{!pvecephPossible}',
		    value: '{pveceph}',
		},
		checked: true,
		uncheckedValue: 0,
		submitValue: false,
		hidden: !me.isCreate,
		boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS'),
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.DirInputPanel', {
    extend: 'PVE.panel.StorageBase',
    onlineHelp: 'storage_directory',
    initComponent: function() {
	var me = this;
	me.column1 = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'path',
		value: '',
		fieldLabel: gettext('Directory'),
		allowBlank: false,
	    },
	    {
		xtype: 'pveContentTypeSelector',
		name: 'content',
		value: 'images',
		multiSelect: true,
		fieldLabel: gettext('Content'),
		allowBlank: false,
	    },
	];
	me.column2 = [
	    {
		xtype: 'proxmoxcheckbox',
		name: 'shared',
		uncheckedValue: 0,
		fieldLabel: gettext('Shared'),
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.GlusterFsScan', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveGlusterFsScan',
    queryParam: 'server',
    valueField: 'volname',
    displayField: 'volname',
    matchFieldWidth: false,
    listConfig: {
	loadingText: 'Scanning...',
	width: 350,
    },
    doRawQuery: function() {
	// nothing
    },
    onTriggerClick: function() {
	var me = this;
	if (!me.queryCaching || me.lastQuery !== me.glusterServer) {
	    me.store.removeAll();
	}
	me.allQuery = me.glusterServer;
	me.callParent();
    },
    setServer: function(server) {
	var me = this;
	me.glusterServer = server;
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	var store = Ext.create('Ext.data.Store', {
	    fields: ['volname'],
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/nodes/' + me.nodename + '/scan/glusterfs',
	    },
	});
	store.sort('volname', 'ASC');
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.GlusterFsInputPanel', {
    extend: 'PVE.panel.StorageBase',
    onlineHelp: 'storage_glusterfs',
    initComponent: function() {
	var me = this;
	me.column1 = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'server',
		value: '',
		fieldLabel: gettext('Server'),
		allowBlank: false,
		listeners: {
		    change: function(f, value) {
			if (me.isCreate) {
			    var volumeField = me.down('field[name=volume]');
			    volumeField.setServer(value);
			    volumeField.setValue('');
			}
		    },
		},
	    },
	    {
		xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
		name: 'server2',
		value: '',
		fieldLabel: gettext('Second Server'),
		allowBlank: true,
	    },
	    {
		xtype: me.isCreate ? 'pveGlusterFsScan' : 'displayfield',
		name: 'volume',
		value: '',
		fieldLabel: 'Volume name',
		allowBlank: false,
	    },
	    {
		xtype: 'pveContentTypeSelector',
		cts: ['images', 'iso', 'backup', 'vztmpl', 'snippets'],
		name: 'content',
		value: 'images',
		multiSelect: true,
		fieldLabel: gettext('Content'),
		allowBlank: false,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.ImageView', {
    extend: 'PVE.storage.ContentView',
    alias: 'widget.pveStorageImageView',
    initComponent: function() {
	var me = this;
	var nodename = me.nodename = me.pveSelNode.data.node;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	var storage = me.storage = me.pveSelNode.data.storage;
	if (!me.storage) {
	    throw "no storage ID specified";
	}
	if (!me.content || (me.content !== 'images' && me.content !== 'rootdir')) {
	    throw "content needs to be either 'images' or 'rootdir'";
	}
	var sm = me.sm = Ext.create('Ext.selection.RowModel', {});
	var reload = function() {
	    me.store.load();
	};
	me.tbar = [
	    {
		xtype: 'proxmoxButton',
		selModel: sm,
		text: gettext('Remove'),
		disabled: true,
		handler: function(btn, event, rec) {
		    let url = `/nodes/${nodename}/storage/${storage}/content/${rec.data.volid}`;
		    var vmid = rec.data.vmid;
		    var store = PVE.data.ResourceStore;
		    if (vmid && store.findVMID(vmid)) {
			var guest_node = store.guestNode(vmid);
			var storage_path = 'storage/' + nodename + '/' + storage;
			// allow to delete local backed images if a VMID exists on another node.
			if (store.storageIsShared(storage_path) || guest_node === nodename) {
			    var msg = Ext.String.format(
				gettext("Cannot remove image, a guest with VMID '{0}' exists!"), vmid);
			    msg += '
' + gettext("You can delete the image from the guest's hardware pane");
			    Ext.Msg.show({
				title: gettext('Cannot remove disk image.'),
				icon: Ext.Msg.ERROR,
				msg: msg,
			    });
			    return;
			}
		    }
		    var win = Ext.create('Proxmox.window.SafeDestroy', {
			title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid),
			showProgress: true,
			url: url,
			item: { type: 'Image', id: vmid },
			taskName: 'unknownimgdel',
		    }).show();
		    win.on('destroy', reload);
		},
	    },
	];
	me.useCustomRemoveButton = true;
	me.callParent();
    },
});
Ext.define('PVE.storage.IScsiScan', {
    extend: 'PVE.form.ComboBoxSetStoreNode',
    alias: 'widget.pveIScsiScan',
    queryParam: 'portal',
    valueField: 'target',
    displayField: 'target',
    matchFieldWidth: false,
    allowBlank: false,
    listConfig: {
	width: 350,
	columns: [
	    {
		dataIndex: 'target',
		flex: 1,
	    },
	],
	emptyText: PVE.Utils.renderNotFound(gettext('iSCSI Target')),
    },
    config: {
	apiSuffix: '/scan/iscsi',
    },
    showNodeSelector: true,
    reload: function() {
	let me = this;
	if (!me.isDisabled()) {
	    me.getStore().load();
	}
    },
    setPortal: function(portal) {
	let me = this;
	me.portal = portal;
	me.getStore().getProxy().setExtraParams({ portal });
	me.reload();
    },
    setNodeName: function(value) {
	let me = this;
	me.callParent([value]);
	me.reload();
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	let store = Ext.create('Ext.data.Store', {
	    fields: ['target', 'portal'],
	    proxy: {
		type: 'proxmox',
		url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
	    },
	});
	store.sort('target', 'ASC');
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.IScsiInputPanel', {
    extend: 'PVE.panel.StorageBase',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'storage_open_iscsi',
    onGetValues: function(values) {
	let me = this;
	values.content = values.luns ? 'images' : 'none';
	delete values.luns;
	return me.callParent([values]);
    },
    setValues: function(values) {
	values.luns = values.content.indexOf('images') !== -1;
	this.callParent([values]);
    },
    column1: [
	{
	    xtype: 'pmxDisplayEditField',
	    cbind: {
		editable: '{isCreate}',
	    },
	    name: 'portal',
	    value: '',
	    fieldLabel: 'Portal',
	    allowBlank: false,
	    editConfig: {
		listeners: {
		    change: {
			fn: function(f, value) {
			    let panel = this.up('inputpanel');
			    let exportField = panel.lookup('iScsiTargetScan');
			    if (exportField) {
				exportField.setDisabled(!value);
				exportField.setPortal(value);
				exportField.setValue('');
			    }
			},
			buffer: 500,
		    },
		},
	    },
	},
	{
	    cbind: {
		xtype: (get) => get('isCreate') ? 'pveIScsiScan' : 'displayfield',
		readOnly: '{!isCreate}',
		disabled: '{isCreate}',
	    },
	    name: 'target',
	    value: '',
	    fieldLabel: gettext('Target'),
	    allowBlank: false,
	    reference: 'iScsiTargetScan',
	    listeners: {
		nodechanged: function(value) {
		    this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
		},
	    },
	},
    ],
    column2: [
	{
	    xtype: 'checkbox',
	    name: 'luns',
	    checked: true,
	    fieldLabel: gettext('Use LUNs directly'),
	},
    ],
});
Ext.define('PVE.storage.VgSelector', {
    extend: 'PVE.form.ComboBoxSetStoreNode',
    alias: 'widget.pveVgSelector',
    valueField: 'vg',
    displayField: 'vg',
    queryMode: 'local',
    editable: false,
    listConfig: {
	columns: [
	    {
		dataIndex: 'vg',
		flex: 1,
	    },
	],
	emptyText: PVE.Utils.renderNotFound('VGs'),
    },
    config: {
	apiSuffix: '/scan/lvm',
    },
    showNodeSelector: true,
    setNodeName: function(value) {
	let me = this;
	me.callParent([value]);
	me.getStore().load();
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	let store = Ext.create('Ext.data.Store', {
	    autoLoad: {}, // true,
	    fields: ['vg', 'size', 'free'],
	    proxy: {
		type: 'proxmox',
		url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
	    },
	});
	store.sort('vg', 'ASC');
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.BaseStorageSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveBaseStorageSelector',
    existingGroupsText: gettext("Existing volume groups"),
    queryMode: 'local',
    editable: false,
    value: '',
    valueField: 'storage',
    displayField: 'text',
    initComponent: function() {
	let me = this;
	let store = Ext.create('Ext.data.Store', {
	    autoLoad: {
		addRecords: true,
		params: {
		    type: 'iscsi',
		},
	    },
	    fields: ['storage', 'type', 'content',
		      {
			  name: 'text',
			  convert: function(value, record) {
			      if (record.data.storage) {
				  return record.data.storage + " (iSCSI)";
			      } else {
				  return me.existingGroupsText;
			      }
			  },
		      }],
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/storage/',
	    },
	});
	store.loadData([{ storage: '' }], true);
	store.sort('storage', 'ASC');
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.LunSelector', {
    extend: 'PVE.form.FileSelector',
    alias: 'widget.pveStorageLunSelector',
    nodename: 'localhost',
    storageContent: 'images',
    allowBlank: false,
    initComponent: function() {
	let me = this;
	if (PVE.data.ResourceStore.getNodes().length > 1) {
	    me.errorHeight = 140;
	    Ext.apply(me.listConfig ?? {}, {
		tbar: {
		    xtype: 'toolbar',
		    items: [
			{
			    xtype: "pveStorageScanNodeSelector",
			    autoSelect: false,
			    fieldLabel: gettext('Node to scan'),
			    listeners: {
				change: (_field, value) => me.setNodename(value),
			    },
			},
		    ],
		},
		emptyText: me.listConfig?.emptyText ?? PVE.Utils.renderNotFound(gettext('Volume')),
	    });
	}
	me.callParent();
    },
});
Ext.define('PVE.storage.LVMInputPanel', {
    extend: 'PVE.panel.StorageBase',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'storage_lvm',
    column1: [
	{
	    xtype: 'pveBaseStorageSelector',
	    name: 'basesel',
	    fieldLabel: gettext('Base storage'),
	    cbind: {
		disabled: '{!isCreate}',
		hidden: '{!isCreate}',
	    },
	    submitValue: false,
	    listeners: {
		change: function(f, value) {
		    let me = this;
		    let vgField = me.up('inputpanel').lookup('volumeGroupSelector');
		    let vgNameField = me.up('inputpanel').lookup('vgName');
		    let baseField = me.up('inputpanel').lookup('lunSelector');
		    vgField.setVisible(!value);
		    vgField.setDisabled(!!value);
		    baseField.setVisible(!!value);
		    baseField.setDisabled(!value);
		    baseField.setStorage(value);
		    vgNameField.setVisible(!!value);
		    vgNameField.setDisabled(!value);
		},
	    },
	},
	{
	    xtype: 'pveStorageLunSelector',
	    name: 'base',
	    fieldLabel: gettext('Base volume'),
	    reference: 'lunSelector',
	    hidden: true,
	    disabled: true,
	},
	{
	    xtype: 'pveVgSelector',
	    name: 'vgname',
	    fieldLabel: gettext('Volume group'),
	    reference: 'volumeGroupSelector',
	    cbind: {
		disabled: '{!isCreate}',
		hidden: '{!isCreate}',
	    },
	    allowBlank: false,
	    listeners: {
		nodechanged: function(value) {
		    this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
		},
	    },
	},
	{
	    name: 'vgname',
	    fieldLabel: gettext('Volume group'),
	    reference: 'vgName',
	    cbind: {
		xtype: (get) => get('isCreate') ? 'textfield' : 'displayfield',
		hidden: '{isCreate}',
		disabled: '{isCreate}',
	    },
	    value: '',
	    allowBlank: false,
	},
	{
	    xtype: 'pveContentTypeSelector',
	    cts: ['images', 'rootdir'],
	    fieldLabel: gettext('Content'),
	    name: 'content',
	    value: ['images', 'rootdir'],
	    multiSelect: true,
	    allowBlank: false,
	},
    ],
    column2: [
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'shared',
	    uncheckedValue: 0,
	    fieldLabel: gettext('Shared'),
	},
    ],
});
Ext.define('PVE.storage.TPoolSelector', {
    extend: 'PVE.form.ComboBoxSetStoreNode',
    alias: 'widget.pveTPSelector',
    queryParam: 'vg',
    valueField: 'lv',
    displayField: 'lv',
    editable: false,
    allowBlank: false,
    listConfig: {
	emptyText: PVE.Utils.renderNotFound('Thin-Pool'),
	columns: [
	    {
		dataIndex: 'lv',
		flex: 1,
	    },
	],
    },
    config: {
	apiSuffix: '/scan/lvmthin',
    },
    reload: function() {
	let me = this;
	if (!me.isDisabled()) {
	    me.getStore().load();
	}
    },
    setVG: function(myvg) {
	let me = this;
	me.vg = myvg;
	me.getStore().getProxy().setExtraParams({ vg: myvg });
	me.reload();
    },
    setNodeName: function(value) {
	let me = this;
	me.callParent([value]);
	me.reload();
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	let store = Ext.create('Ext.data.Store', {
	    fields: ['lv'],
	    proxy: {
		type: 'proxmox',
		url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
	    },
	});
	store.sort('lv', 'ASC');
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.BaseVGSelector', {
    extend: 'PVE.form.ComboBoxSetStoreNode',
    alias: 'widget.pveBaseVGSelector',
    valueField: 'vg',
    displayField: 'vg',
    queryMode: 'local',
    editable: false,
    allowBlank: false,
    listConfig: {
	columns: [
	    {
		dataIndex: 'vg',
		flex: 1,
	    },
	],
    },
    showNodeSelector: true,
    config: {
	apiSuffix: '/scan/lvm',
    },
    setNodeName: function(value) {
	let me = this;
	me.callParent([value]);
	me.getStore().load();
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	let store = Ext.create('Ext.data.Store', {
	    autoLoad: {},
	    fields: ['vg', 'size', 'free'],
	    proxy: {
		type: 'proxmox',
		url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
	    },
	});
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.LvmThinInputPanel', {
    extend: 'PVE.panel.StorageBase',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'storage_lvmthin',
    column1: [
	{
	    xtype: 'pmxDisplayEditField',
	    cbind: {
		editable: '{isCreate}',
	    },
	    name: 'vgname',
	    fieldLabel: gettext('Volume group'),
	    editConfig: {
		xtype: 'pveBaseVGSelector',
		listeners: {
		    nodechanged: function(value) {
			let panel = this.up('inputpanel');
			panel.lookup('thinPoolSelector').setNodeName(value);
			panel.lookup('storageNodeRestriction').setValue(value);
		    },
		    change: function(f, value) {
			let vgField = this.up('inputpanel').lookup('thinPoolSelector');
			if (vgField && !f.isDisabled()) {
			    vgField.setDisabled(!value);
			    vgField.setVG(value);
			    vgField.setValue('');
			}
		    },
		},
	    },
	},
	{
	    xtype: 'pmxDisplayEditField',
	    cbind: {
		editable: '{isCreate}',
	    },
	    name: 'thinpool',
	    fieldLabel: gettext('Thin Pool'),
	    allowBlank: false,
	    editConfig: {
		xtype: 'pveTPSelector',
		reference: 'thinPoolSelector',
		disabled: true,
	    },
	},
	{
	    xtype: 'pveContentTypeSelector',
	    cts: ['images', 'rootdir'],
	    fieldLabel: gettext('Content'),
	    name: 'content',
	    value: ['images', 'rootdir'],
	    multiSelect: true,
	    allowBlank: false,
	},
    ],
});
Ext.define('PVE.storage.BTRFSInputPanel', {
    extend: 'PVE.panel.StorageBase',
    onlineHelp: 'storage_btrfs',
    initComponent: function() {
	let me = this;
	me.column1 = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'path',
		value: '',
		fieldLabel: gettext('Path'),
		allowBlank: false,
	    },
	    {
		xtype: 'pveContentTypeSelector',
		name: 'content',
		value: ['images', 'rootdir'],
		multiSelect: true,
		fieldLabel: gettext('Content'),
		allowBlank: false,
	    },
	];
	me.columnB = [
	    {
		xtype: 'displayfield',
		userCls: 'pmx-hint',
		value: `BTRFS integration is currently a technology preview.`,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.NFSScan', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.pveNFSScan',
    queryParam: 'server',
    valueField: 'path',
    displayField: 'path',
    matchFieldWidth: false,
    listConfig: {
	loadingText: gettext('Scanning...'),
	width: 350,
    },
    doRawQuery: function() {
	// do nothing
    },
    onTriggerClick: function() {
	var me = this;
	if (!me.queryCaching || me.lastQuery !== me.nfsServer) {
	    me.store.removeAll();
	}
	me.allQuery = me.nfsServer;
	me.callParent();
    },
    setServer: function(server) {
	var me = this;
	me.nfsServer = server;
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	var store = Ext.create('Ext.data.Store', {
	    fields: ['path', 'options'],
	    proxy: {
		type: 'proxmox',
		url: '/api2/json/nodes/' + me.nodename + '/scan/nfs',
	    },
	});
	store.sort('path', 'ASC');
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.NFSInputPanel', {
    extend: 'PVE.panel.StorageBase',
    onlineHelp: 'storage_nfs',
    options: [],
    onGetValues: function(values) {
	var me = this;
	var i;
	var res = [];
	for (i = 0; i < me.options.length; i++) {
	    var item = me.options[i];
	    if (!item.match(/^vers=(.*)$/)) {
		res.push(item);
	    }
	}
	if (values.nfsversion && values.nfsversion !== '__default__') {
	    res.push('vers=' + values.nfsversion);
	}
	delete values.nfsversion;
	values.options = res.join(',');
	if (values.options === '') {
	    delete values.options;
	    if (!me.isCreate) {
		values.delete = "options";
	    }
	}
	return me.callParent([values]);
    },
    setValues: function(values) {
	var me = this;
	if (values.options) {
	    me.options = values.options.split(',');
	    me.options.forEach(function(item) {
		var match = item.match(/^vers=(.*)$/);
		if (match) {
		    values.nfsversion = match[1];
		}
	    });
	}
	return me.callParent([values]);
    },
    initComponent: function() {
	var me = this;
	me.column1 = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'server',
		value: '',
		fieldLabel: gettext('Server'),
		allowBlank: false,
		listeners: {
		    change: function(f, value) {
			if (me.isCreate) {
			    var exportField = me.down('field[name=export]');
			    exportField.setServer(value);
			    exportField.setValue('');
			}
		    },
		},
	    },
	    {
		xtype: me.isCreate ? 'pveNFSScan' : 'displayfield',
		name: 'export',
		value: '',
		fieldLabel: 'Export',
		allowBlank: false,
	    },
	    {
		xtype: 'pveContentTypeSelector',
		name: 'content',
		value: 'images',
		multiSelect: true,
		fieldLabel: gettext('Content'),
		allowBlank: false,
	    },
	];
	me.advancedColumn2 = [
	    {
		xtype: 'proxmoxKVComboBox',
		fieldLabel: gettext('NFS Version'),
		name: 'nfsversion',
		value: '__default__',
		deleteEmpty: false,
		comboItems: [
			['__default__', Proxmox.Utils.defaultText],
			['3', '3'],
			['4', '4'],
			['4.1', '4.1'],
			['4.2', '4.2'],
		],
	    },
	];
	me.callParent();
    },
});
/*global QRCode*/
Ext.define('PVE.Storage.PBSKeyShow', {
    extend: 'Ext.window.Window',
    xtype: 'pvePBSKeyShow',
    mixins: ['Proxmox.Mixin.CBind'],
    width: 600,
    modal: true,
    resizable: false,
    title: gettext('Important: Save your Encryption Key'),
    // avoid that esc closes this by mistake, force user to more manual action
    onEsc: Ext.emptyFn,
    closable: false,
    items: [
	{
	    xtype: 'form',
	    layout: {
		type: 'vbox',
		align: 'stretch',
	    },
	    bodyPadding: 10,
	    border: false,
	    defaults: {
		anchor: '100%',
		border: false,
		padding: '10 0 0 0',
            },
	    items: [
		{
		    xtype: 'textfield',
		    fieldLabel: gettext('Key'),
		    labelWidth: 80,
		    inputId: 'encryption-key-value',
		    cbind: {
			value: '{key}',
		    },
		    editable: false,
		},
		{
		    xtype: 'component',
		    html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.')
		        + '
' + gettext('We recommend the following safe-keeping strategy:'),
		},
		{
		    xtyp: 'container',
		    layout: 'hbox',
		    items: [
			{
			    xtype: 'component',
			    html: '1. ' + gettext('Save the key in your password manager.'),
			    flex: 1,
			},
			{
			    xtype: 'button',
			    text: gettext('Copy Key'),
			    iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small',
			    cls: 'x-btn-default-toolbar-small proxmox-inline-button',
			    width: 110,
			    handler: function(b) {
				document.getElementById('encryption-key-value').select();
				document.execCommand("copy");
			    },
			},
		    ],
		},
		{
		    xtype: 'container',
		    layout: 'hbox',
		    items: [
			{
			    xtype: 'component',
			    html: '2. ' + gettext('Download the key to a USB (pen) drive, placed in secure vault.'),
			    flex: 1,
			},
			{
			    xtype: 'button',
			    text: gettext('Download'),
			    iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small',
			    cls: 'x-btn-default-toolbar-small proxmox-inline-button',
			    width: 110,
			    handler: function(b) {
				let win = this.up('window');
				let pveID = PVE.ClusterName || window.location.hostname;
				let name = `pve-${pveID}-storage-${win.sid}.enc`;
				let hiddenElement = document.createElement('a');
				hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key);
				hiddenElement.target = '_blank';
				hiddenElement.download = name;
				hiddenElement.click();
			    },
			},
		    ],
		},
		{
		    xtype: 'container',
		    layout: 'hbox',
		    items: [
			{
			    xtype: 'component',
			    html: '3. ' + gettext('Print as paperkey, laminated and placed in secure vault.'),
			    flex: 1,
			},
			{
			    xtype: 'button',
			    text: gettext('Print Key'),
			    iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small',
			    cls: 'x-btn-default-toolbar-small proxmox-inline-button',
			    width: 110,
			    handler: function(b) {
				let win = this.up('window');
				win.paperkey(win.key);
			    },
			},
		    ],
		},
	    ],
	},
	{
	    xtype: 'component',
	    border: false,
	    padding: '10 10 10 10',
	    userCls: 'pmx-hint',
	    html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'),
	},
    ],
    buttons: [
	{
	    text: gettext('Close'),
	    handler: function(b) {
		let win = this.up('window');
		win.close();
	    },
	},
    ],
    paperkey: function(keyString) {
	let me = this;
	const key = JSON.parse(keyString);
	const qrwidth = 500;
	let qrdiv = document.createElement('div');
	let qrcode = new QRCode(qrdiv, {
	    width: qrwidth,
	    height: qrwidth,
	    correctLevel: QRCode.CorrectLevel.H,
	});
	qrcode.makeCode(keyString);
	let shortKeyFP = '';
	if (key.fingerprint) {
	    shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint);
	}
	let printFrame = document.createElement("iframe");
	Object.assign(printFrame.style, {
	    position: "fixed",
	    right: "0",
	    bottom: "0",
	    width: "0",
	    height: "0",
	    border: "0",
	});
	const prettifiedKey = JSON.stringify(key, null, 2);
	const keyQrBase64 = qrdiv.children[0].toDataURL("image/png");
	const html = `
	Encryption Key - Storage '${me.sid}' (${shortKeyFP})
-----BEGIN PROXMOX BACKUP KEY-----
${prettifiedKey}
-----END PROXMOX BACKUP KEY-----
	 `;
	printFrame.src = "data:text/html;base64," + btoa(html);
	document.body.appendChild(printFrame);
	me.on('destroy', () => document.body.removeChild(printFrame));
    },
});
Ext.define('PVE.panel.PBSEncryptionKeyTab', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pvePBSEncryptionKeyTab',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'storage_pbs_encryption',
    onGetValues: function(form) {
	let values = {};
	if (form.cryptMode === 'upload') {
	    values['encryption-key'] = form['crypt-key-upload'];
	} else if (form.cryptMode === 'autogenerate') {
	    values['encryption-key'] = 'autogen';
	} else if (form.cryptMode === 'none') {
	    if (!this.isCreate) {
		values.delete = ['encryption-key'];
	    }
	}
	return values;
    },
    setValues: function(values) {
	let me = this;
	let vm = me.getViewModel();
	let cryptKeyInfo = values['encryption-key'];
	if (cryptKeyInfo) {
	    let icon = ' ';
	    if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint
		let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo);
		values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`;
	    } else {
		// old key without FP
		values['crypt-key-fp'] = icon + gettext('Active');
	    }
	} else {
	    values['crypt-key-fp'] = gettext('None');
	    let cryptModeNone = me.down('radiofield[inputValue=none]');
	    cryptModeNone.setBoxLabel(gettext('Do not encrypt backups'));
	    cryptModeNone.setValue(true);
	}
	vm.set('keepCryptVisible', !!cryptKeyInfo);
	vm.set('allowEdit', !cryptKeyInfo);
	me.callParent([values]);
    },
    viewModel: {
	data: {
	    allowEdit: true,
	    keepCryptVisible: false,
	},
	formulas: {
	    showDangerousHint: get => {
		let allowEdit = get('allowEdit');
		return get('keepCryptVisible') && allowEdit;
	    },
	},
    },
    items: [
	{
	    xtype: 'displayfield',
	    name: 'crypt-key-fp',
	    fieldLabel: gettext('Encryption Key'),
	    padding: '2 0',
	},
	{
	    xtype: 'checkbox',
	    name: 'crypt-allow-edit',
	    boxLabel: gettext('Edit existing encryption key (dangerous!)'),
	    hidden: true,
	    submitValue: false,
	    isDirty: () => false,
	    bind: {
		hidden: '{!keepCryptVisible}',
		value: '{allowEdit}',
	    },
	},
	{
	    xtype: 'radiofield',
	    name: 'cryptMode',
	    inputValue: 'keep',
	    boxLabel: gettext('Keep encryption key'),
	    padding: '0 0 0 25',
	    cbind: {
		hidden: '{isCreate}',
		checked: '{!isCreate}',
	    },
	    bind: {
		hidden: '{!keepCryptVisible}',
		disabled: '{!allowEdit}',
	    },
	},
	{
	    xtype: 'radiofield',
	    name: 'cryptMode',
	    inputValue: 'none',
	    checked: true,
	    padding: '0 0 0 25',
	    cbind: {
		disabled: '{!isCreate}',
		checked: '{isCreate}',
		boxLabel: get => get('isCreate')
		    ? gettext('Do not encrypt backups')
		    : gettext('Delete existing encryption key'),
	    },
	    bind: {
		disabled: '{!allowEdit}',
	    },
	},
	{
	    xtype: 'radiofield',
	    name: 'cryptMode',
	    inputValue: 'autogenerate',
	    boxLabel: gettext('Auto-generate a client encryption key'),
	    padding: '0 0 0 25',
	    cbind: {
		disabled: '{!isCreate}',
	    },
	    bind: {
		disabled: '{!allowEdit}',
	    },
	},
	{
	    xtype: 'radiofield',
	    name: 'cryptMode',
	    inputValue: 'upload',
	    boxLabel: gettext('Upload an existing client encryption key'),
	    padding: '0 0 0 25',
	    cbind: {
		disabled: '{!isCreate}',
	    },
	    bind: {
		disabled: '{!allowEdit}',
	    },
	    listeners: {
		change: function(f, value) {
		    let panel = this.up('inputpanel');
		    if (!panel.rendered) {
			return;
		    }
		    let uploadKeyField = panel.down('field[name=crypt-key-upload]');
		    uploadKeyField.setDisabled(!value);
		    uploadKeyField.setHidden(!value);
		    let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]');
		    uploadKeyButton.setDisabled(!value);
		    uploadKeyButton.setHidden(!value);
		    if (value) {
			uploadKeyField.validate();
		    } else {
			uploadKeyField.reset();
		    }
		},
	    },
	},
	{
	    xtype: 'fieldcontainer',
	    layout: 'hbox',
	    items: [
		{
		    xtype: 'proxmoxtextfield',
		    name: 'crypt-key-upload',
		    fieldLabel: gettext('Key'),
		    value: '',
		    disabled: true,
		    hidden: true,
		    allowBlank: false,
		    labelAlign: 'right',
		    flex: 1,
		    emptyText: gettext('You can drag-and-drop a key file here.'),
		    validator: function(value) {
			if (value.length) {
			    let key;
			    try {
				key = JSON.parse(value);
			    } catch (e) {
				return "Failed to parse key - " + e;
			    }
			    if (key.data === undefined) {
				return "Does not seems like a valid Proxmox Backup key!";
			    }
			}
			return true;
		    },
		    afterRender: function() {
			if (!window.FileReader) {
			    // No FileReader support in this browser
			    return;
			}
			let cancel = function(ev) {
			    ev = ev.event;
			    if (ev.preventDefault) {
				ev.preventDefault();
			    }
			};
			this.inputEl.on('dragover', cancel);
			this.inputEl.on('dragenter', cancel);
			this.inputEl.on('drop', ev => {
			    cancel(ev);
			    let files = ev.event.dataTransfer.files;
			    PVE.Utils.loadTextFromFile(files[0], v => this.setValue(v));
			});
		    },
		},
		{
		    xtype: 'filebutton',
		    name: 'crypt-upload-button',
		    iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small',
		    cls: 'x-btn-default-toolbar-small proxmox-inline-button',
		    margin: '0 0 0 4',
		    disabled: true,
		    hidden: true,
		    listeners: {
			change: function(btn, e, value) {
			    let ev = e.event;
			    let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]');
			    PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v));
			    btn.reset();
			},
		    },
		},
	    ],
	},
	{
	    xtype: 'component',
	    border: false,
	    padding: '5 2',
	    userCls: 'pmx-hint',
	    html: // `${gettext('Warning')}: ` +
	      ` ` +
	      gettext('Deleting or replacing the encryption key will break restoring backups created with it!'),
	    hidden: true,
	    bind: {
		hidden: '{!showDangerousHint}',
	    },
	},
    ],
});
Ext.define('PVE.storage.PBSInputPanel', {
    extend: 'PVE.panel.StorageBase',
    onlineHelp: 'storage_pbs',
    apiCallDone: function(success, response, options) {
	let res = response.result.data;
	if (!(res && res.config && res.config['encryption-key'])) {
	    return;
	}
	let key = res.config['encryption-key'];
	Ext.create('PVE.Storage.PBSKeyShow', {
	    autoShow: true,
	    sid: res.storage,
	    key: key,
	});
    },
    isPBS: true, // HACK
    extraTabs: [
	{
	    xtype: 'pvePBSEncryptionKeyTab',
	    title: gettext('Encryption'),
	},
    ],
    setValues: function(values) {
	let me = this;
	let server = values.server;
	if (values.port !== undefined) {
	    if (Proxmox.Utils.IP6_match.test(server)) {
		server = `[${server}]`;
	    }
	    server += `:${values.port}`;
	}
	values.hostport = server;
	return me.callParent([values]);
    },
    initComponent: function() {
	var me = this;
	me.column1 = [
	    {
		xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
		fieldLabel: gettext('Server'),
		allowBlank: false,
		name: 'hostport',
		submitValue: false,
		vtype: 'HostPort',
		listeners: {
		    change: function(field, newvalue) {
			let server = newvalue;
			let port;
			let match = Proxmox.Utils.HostPort_match.exec(newvalue);
			if (match === null) {
			    match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue);
			    if (match === null) {
				match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue);
			    }
			}
			if (match !== null) {
			    server = match[1];
			    if (match[2] !== undefined) {
				port = match[2];
			    }
			}
			field.up('inputpanel').down('field[name=server]').setValue(server);
			field.up('inputpanel').down('field[name=port]').setValue(port);
		    },
		},
	    },
	    {
		xtype: 'proxmoxtextfield',
		hidden: true,
		name: 'server',
		submitValue: me.isCreate, // it is fixed
	    },
	    {
		xtype: 'proxmoxtextfield',
		hidden: true,
		deleteEmpty: !me.isCreate,
		name: 'port',
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'username',
		value: '',
		emptyText: gettext('Example') + ': admin@pbs',
		fieldLabel: gettext('Username'),
		regex: /\S+@\w+/,
		regexText: gettext('Example') + ': admin@pbs',
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		inputType: 'password',
		name: 'password',
		value: me.isCreate ? '' : '********',
		emptyText: me.isCreate ? gettext('None') : '',
		fieldLabel: gettext('Password'),
		allowBlank: false,
	    },
	];
	me.column2 = [
	    {
		xtype: 'displayfield',
		name: 'content',
		value: 'backup',
		submitValue: true,
		fieldLabel: gettext('Content'),
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'datastore',
		value: '',
		fieldLabel: 'Datastore',
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'namespace',
		value: '',
		emptyText: gettext('Root'),
		fieldLabel: gettext('Namespace'),
		allowBlank: true,
	    },
	];
	me.columnB = [
	    {
		xtype: 'proxmoxtextfield',
		name: 'fingerprint',
		value: me.isCreate ? null : undefined,
		fieldLabel: gettext('Fingerprint'),
		emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
		regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
		regexText: gettext('Example') + ': AB:CD:EF:...',
		deleteEmpty: !me.isCreate,
		allowBlank: true,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.Ceph.Model', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.cephstorage',
    data: {
	pveceph: true,
	pvecephPossible: true,
	namespacePresent: false,
    },
});
Ext.define('PVE.storage.Ceph.Controller', {
    extend: 'PVE.controller.StorageEdit',
    alias: 'controller.cephstorage',
    control: {
	'#': {
	    afterrender: 'queryMonitors',
	},
	'textfield[name=username]': {
	    disable: 'resetField',
	},
	'displayfield[name=monhost]': {
	    enable: 'queryMonitors',
	},
	'textfield[name=monhost]': {
	    disable: 'resetField',
	    enable: 'resetField',
	},
	'textfield[name=namespace]': {
	    change: 'updateNamespaceHint',
	},
    },
    resetField: function(field) {
	field.reset();
    },
    updateNamespaceHint: function(field, newVal, oldVal) {
	this.getViewModel().set('namespacePresent', newVal);
    },
    queryMonitors: function(field, newVal, oldVal) {
	// we get called with two signatures, the above one for a field
	// change event and the afterrender from the view, this check only
	// can be true for the field change one and omit the API request if
	// pveceph got unchecked - as it's not needed there.
	if (field && !newVal && oldVal) {
	    return;
	}
	var view = this.getView();
	var vm = this.getViewModel();
	if (!(view.isCreate || vm.get('pveceph'))) {
	    return; // only query on create or if editing a pveceph store
	}
	var monhostField = this.lookupReference('monhost');
	Proxmox.Utils.API2Request({
	    url: '/api2/json/nodes/localhost/ceph/mon',
	    method: 'GET',
	    scope: this,
	    callback: function(options, success, response) {
		var data = response.result.data;
		if (response.status === 200) {
		    if (data.length > 0) {
			var monhost = Ext.Array.pluck(data, 'name').sort().join(',');
			monhostField.setValue(monhost);
			monhostField.resetOriginalValue();
			if (view.isCreate) {
			    vm.set('pvecephPossible', true);
			}
		    } else {
			vm.set('pveceph', false);
		    }
		} else {
		    vm.set('pveceph', false);
		    vm.set('pvecephPossible', false);
		}
	    },
	});
    },
});
Ext.define('PVE.storage.RBDInputPanel', {
    extend: 'PVE.panel.StorageBase',
    controller: 'cephstorage',
    onlineHelp: 'ceph_rados_block_devices',
    viewModel: {
	type: 'cephstorage',
    },
    setValues: function(values) {
	if (values.monhost) {
	    this.viewModel.set('pveceph', false);
	    this.lookupReference('pvecephRef').setValue(false);
	    this.lookupReference('pvecephRef').resetOriginalValue();
	}
	if (values.namespace) {
	    this.getViewModel().set('namespacePresent', true);
	}
	this.callParent([values]);
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	me.type = 'rbd';
	me.column1 = [];
	if (me.isCreate) {
	    me.column1.push({
		xtype: 'pveCephPoolSelector',
		nodename: me.nodename,
		name: 'pool',
		bind: {
		    disabled: '{!pveceph}',
		    submitValue: '{pveceph}',
		    hidden: '{!pveceph}',
		},
		fieldLabel: gettext('Pool'),
		allowBlank: false,
	    }, {
		xtype: 'textfield',
		name: 'pool',
		value: 'rbd',
		bind: {
		    disabled: '{pveceph}',
		    submitValue: '{!pveceph}',
		    hidden: '{pveceph}',
		},
		fieldLabel: gettext('Pool'),
		allowBlank: false,
	    });
	} else {
	    me.column1.push({
		xtype: 'displayfield',
		nodename: me.nodename,
		name: 'pool',
		fieldLabel: gettext('Pool'),
		allowBlank: false,
	    });
	}
	me.column1.push(
	    {
		xtype: 'textfield',
		name: 'monhost',
		vtype: 'HostList',
		bind: {
		    disabled: '{pveceph}',
		    submitValue: '{!pveceph}',
		    hidden: '{pveceph}',
		},
		value: '',
		fieldLabel: 'Monitor(s)',
		allowBlank: false,
	    },
	    {
		xtype: 'displayfield',
		reference: 'monhost',
		bind: {
		    disabled: '{!pveceph}',
		    hidden: '{!pveceph}',
		},
		value: '',
		fieldLabel: 'Monitor(s)',
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'username',
		bind: {
		    disabled: '{pveceph}',
		    submitValue: '{!pveceph}',
		},
		value: 'admin',
		fieldLabel: gettext('User name'),
		allowBlank: true,
	    },
	);
	me.column2 = [
	    {
		xtype: 'pveContentTypeSelector',
		cts: ['images', 'rootdir'],
		fieldLabel: gettext('Content'),
		name: 'content',
		value: ['images'],
		multiSelect: true,
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'krbd',
		uncheckedValue: 0,
		fieldLabel: 'KRBD',
	    },
	];
	me.columnB = [
	    {
		xtype: me.isCreate ? 'textarea' : 'displayfield',
		name: 'keyring',
		fieldLabel: 'Keyring',
		value: me.isCreate ? '' : '***********',
		allowBlank: false,
		bind: {
		    hidden: '{pveceph}',
		    disabled: '{pveceph}',
		},
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'pveceph',
		reference: 'pvecephRef',
		bind: {
		    disabled: '{!pvecephPossible}',
		    value: '{pveceph}',
		},
		checked: true,
		uncheckedValue: 0,
		submitValue: false,
		hidden: !me.isCreate,
		boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool'),
	    },
	];
	me.advancedColumn1 = [
	    {
		xtype: 'pmxDisplayEditField',
		editable: me.isCreate,
		name: 'namespace',
		value: '',
		fieldLabel: gettext('Namespace'),
		allowBlank: true,
	    },
	];
	me.advancedColumn2 = [
	    {
		xtype: 'displayfield',
		name: 'namespace-hint',
		userCls: 'pmx-hint',
		value: gettext('RBD namespaces must be created manually!'),
		bind: {
		    hidden: '{!namespacePresent}',
		},
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.StatusView', {
    extend: 'Proxmox.panel.StatusView',
    alias: 'widget.pveStorageStatusView',
    height: 230,
    title: gettext('Status'),
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    defaults: {
	xtype: 'pmxInfoWidget',
	padding: '0 30 5 30',
    },
    items: [
	{
	    xtype: 'box',
	    height: 30,
	},
	{
	    itemId: 'enabled',
	    title: gettext('Enabled'),
	    printBar: false,
	    textField: 'disabled',
	    renderer: Proxmox.Utils.format_neg_boolean,
	},
	{
	    itemId: 'active',
	    title: gettext('Active'),
	    printBar: false,
	    textField: 'active',
	    renderer: Proxmox.Utils.format_boolean,
	},
	{
	    itemId: 'content',
	    title: gettext('Content'),
	    printBar: false,
	    textField: 'content',
	    renderer: PVE.Utils.format_content_types,
	},
	{
	    itemId: 'type',
	    title: gettext('Type'),
	    printBar: false,
	    textField: 'type',
	    renderer: PVE.Utils.format_storage_type,
	},
	{
	    xtype: 'box',
	    height: 10,
	},
	{
	    itemId: 'usage',
	    title: gettext('Usage'),
	    valueField: 'used',
	    maxField: 'total',
	    renderer: (val, max) => {
		if (max === undefined) {
		    return val;
		}
		return Proxmox.Utils.render_size_usage(val, max, true);
	    },
	},
    ],
    updateTitle: function() {
	// nothing
    },
});
Ext.define('PVE.storage.Summary', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveStorageSummary',
    scrollable: true,
    bodyPadding: 5,
    tbar: [
	'->',
	{
	    xtype: 'proxmoxRRDTypeSelector',
	},
    ],
    layout: {
	type: 'column',
    },
    defaults: {
	padding: 5,
	columnWidth: 1,
    },
    initComponent: function() {
        var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var storage = me.pveSelNode.data.storage;
	if (!storage) {
	    throw "no storage ID specified";
	}
	var rstore = Ext.create('Proxmox.data.ObjectStore', {
	    url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status",
	    interval: 1000,
	});
	var rrdstore = Ext.create('Proxmox.data.RRDStore', {
	    rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata",
	    model: 'pve-rrd-storage',
	});
	Ext.apply(me, {
	    items: [
		{
		    xtype: 'pveStorageStatusView',
		    pveSelNode: me.pveSelNode,
		    rstore: rstore,
		},
		{
		    xtype: 'proxmoxRRDChart',
		    title: gettext('Usage'),
		    fields: ['total', 'used'],
		    fieldTitles: ['Total Size', 'Used Size'],
		    store: rrdstore,
		},
	    ],
	    listeners: {
		activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); },
		destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); },
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.grid.TemplateSelector', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveTemplateSelector',
    stateful: true,
    stateId: 'grid-template-selector',
    viewConfig: {
	trackOver: false,
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	var baseurl = "/nodes/" + me.nodename + "/aplinfo";
	var store = new Ext.data.Store({
	    model: 'pve-aplinfo',
	    groupField: 'section',
	    proxy: {
                type: 'proxmox',
		url: '/api2/json' + baseurl,
	    },
	});
	var sm = Ext.create('Ext.selection.RowModel', {});
	var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
            groupHeaderTpl: '{[ "Section: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
	});
	var reload = function() {
	    store.load();
	};
	Proxmox.Utils.monStoreErrors(me, store);
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: [
		'->',
		gettext('Search'),
		{
		    xtype: 'textfield',
		    width: 200,
		    enableKeyEvents: true,
		    listeners: {
			buffer: 500,
			keyup: function(field) {
			    var value = field.getValue().toLowerCase();
			    store.clearFilter(true);
			    store.filterBy(function(rec) {
				return rec.data.package.toLowerCase().indexOf(value) !== -1 ||
				rec.data.headline.toLowerCase().indexOf(value) !== -1;
			    });
			},
		    },
		},
	    ],
	    features: [groupingFeature],
	    columns: [
		{
		    header: gettext('Type'),
		    width: 80,
		    dataIndex: 'type',
		},
		{
		    header: gettext('Package'),
		    flex: 1,
		    dataIndex: 'package',
		},
		{
		    header: gettext('Version'),
		    width: 80,
		    dataIndex: 'version',
		},
		{
		    header: gettext('Description'),
		    flex: 1.5,
		    renderer: Ext.String.htmlEncode,
		    dataIndex: 'headline',
		},
	    ],
	    listeners: {
		afterRender: reload,
	    },
	});
	me.callParent();
    },
}, function() {
    Ext.define('pve-aplinfo', {
	extend: 'Ext.data.Model',
	fields: [
	    'template', 'type', 'package', 'version', 'headline', 'infopage',
	    'description', 'os', 'section',
	],
	idProperty: 'template',
    });
});
Ext.define('PVE.storage.TemplateDownload', {
    extend: 'Ext.window.Window',
    alias: 'widget.pveTemplateDownload',
    modal: true,
    title: gettext('Templates'),
    layout: 'fit',
    width: 900,
    height: 600,
    initComponent: function() {
        var me = this;
	var grid = Ext.create('PVE.grid.TemplateSelector', {
	    border: false,
	    scrollable: true,
	    nodename: me.nodename,
	});
	var sm = grid.getSelectionModel();
	var submitBtn = Ext.create('Proxmox.button.Button', {
	    text: gettext('Download'),
	    disabled: true,
	    selModel: sm,
	    handler: function(button, event, rec) {
		Proxmox.Utils.API2Request({
		    url: '/nodes/' + me.nodename + '/aplinfo',
		    params: {
			storage: me.storage,
			template: rec.data.template,
		    },
		    method: 'POST',
		    failure: function(response, opts) {
			Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		    },
		    success: function(response, options) {
			var upid = response.result.data;
			Ext.create('Proxmox.window.TaskViewer', {
			    upid: upid,
			    listeners: {
				destroy: me.reloadGrid,
			    },
			}).show();
			me.close();
		    },
		});
	    },
	});
        Ext.apply(me, {
	    items: grid,
	    buttons: [submitBtn],
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.TemplateView', {
    extend: 'PVE.storage.ContentView',
    alias: 'widget.pveStorageTemplateView',
    initComponent: function() {
	var me = this;
	var nodename = me.nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var storage = me.storage = me.pveSelNode.data.storage;
	if (!storage) {
	    throw "no storage ID specified";
	}
	me.content = 'vztmpl';
	var reload = function() {
	    me.store.load();
	};
	var templateButton = Ext.create('Proxmox.button.Button', {
	    itemId: 'tmpl-btn',
	    text: gettext('Templates'),
	    handler: function() {
		var win = Ext.create('PVE.storage.TemplateDownload', {
		    nodename: nodename,
		    storage: storage,
		    reloadGrid: reload,
		});
		win.show();
	    },
	});
	me.tbar = [templateButton];
	me.useUploadButton = true;
	me.callParent();
    },
});
Ext.define('PVE.storage.ZFSInputPanel', {
    extend: 'PVE.panel.StorageBase',
    viewModel: {
	parent: null,
	data: {
	    isLIO: false,
	    isComstar: true,
	    hasWriteCacheOption: true,
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    'field[name=iscsiprovider]': {
		change: 'changeISCSIProvider',
	    },
	},
	changeISCSIProvider: function(f, newVal, oldVal) {
	    var vm = this.getViewModel();
	    vm.set('isLIO', newVal === 'LIO');
	    vm.set('isComstar', newVal === 'comstar');
	    vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
	},
    },
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.content = 'images';
	}
	values.nowritecache = values.writecache ? 0 : 1;
	delete values.writecache;
	return me.callParent([values]);
    },
    setValues: function(values) {
	values.writecache = values.nowritecache ? 0 : 1;
	this.callParent([values]);
    },
    initComponent: function() {
	var me = this;
	me.column1 = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'portal',
		value: '',
		fieldLabel: gettext('Portal'),
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'pool',
		value: '',
		fieldLabel: gettext('Pool'),
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'blocksize',
		value: '4k',
		fieldLabel: gettext('Block Size'),
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'target',
		value: '',
		fieldLabel: gettext('Target'),
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'comstar_tg',
		value: '',
		fieldLabel: gettext('Target group'),
		bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
		allowBlank: true,
	    },
	];
	me.column2 = [
	    {
		xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield',
		name: 'iscsiprovider',
		value: 'comstar',
		fieldLabel: gettext('iSCSI Provider'),
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'sparse',
		checked: false,
		uncheckedValue: 0,
		fieldLabel: gettext('Thin provision'),
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'writecache',
		checked: true,
		bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' },
		uncheckedValue: 0,
		fieldLabel: gettext('Write cache'),
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'comstar_hg',
		value: '',
		bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
		fieldLabel: gettext('Host group'),
		allowBlank: true,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'lio_tpg',
		value: '',
		bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
		allowBlank: false,
		fieldLabel: gettext('Target portal group'),
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.ZFSPoolSelector', {
    extend: 'PVE.form.ComboBoxSetStoreNode',
    alias: 'widget.pveZFSPoolSelector',
    valueField: 'pool',
    displayField: 'pool',
    queryMode: 'local',
    editable: false,
    allowBlank: false,
    listConfig: {
	columns: [
	    {
		dataIndex: 'pool',
		flex: 1,
	    },
	],
	emptyText: PVE.Utils.renderNotFound(gettext('ZFS Pool')),
    },
    config: {
	apiSuffix: '/scan/zfs',
    },
    showNodeSelector: true,
    setNodeName: function(value) {
	let me = this;
	me.callParent([value]);
	me.getStore().load();
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	let store = Ext.create('Ext.data.Store', {
	    autoLoad: {}, // true,
	    fields: ['pool', 'size', 'free'],
	    proxy: {
		type: 'proxmox',
		url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
	    },
	});
	store.sort('pool', 'ASC');
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.ZFSPoolInputPanel', {
    extend: 'PVE.panel.StorageBase',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'storage_zfspool',
    column1: [
	{
	    xtype: 'pmxDisplayEditField',
	    cbind: {
		editable: '{isCreate}',
	    },
	    name: 'pool',
	    fieldLabel: gettext('ZFS Pool'),
	    allowBlank: false,
	    editConfig: {
		xtype: 'pveZFSPoolSelector',
		reference: 'zfsPoolSelector',
		listeners: {
		    nodechanged: function(value) {
			this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
		    },
		},
	    },
	},
	{
	    xtype: 'pveContentTypeSelector',
	    cts: ['images', 'rootdir'],
	    fieldLabel: gettext('Content'),
	    name: 'content',
	    value: ['images', 'rootdir'],
	    multiSelect: true,
	    allowBlank: false,
	},
    ],
    column2: [
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'sparse',
	    checked: false,
	    uncheckedValue: 0,
	    fieldLabel: gettext('Thin provision'),
	},
	{
	    xtype: 'textfield',
	    name: 'blocksize',
	    emptyText: '8k',
	    fieldLabel: gettext('Block Size'),
	    allowBlank: true,
	},
    ],
});
/*
 * Workspace base class
 *
 * popup login window when auth fails (call onLogin handler)
 * update (re-login) ticket every 15 minutes
 *
 */
Ext.define('PVE.Workspace', {
    extend: 'Ext.container.Viewport',
    title: 'Proxmox Virtual Environment',
    loginData: null, // Data from last login call
    onLogin: function(loginData) {
	// override me
    },
    // private
    updateLoginData: function(loginData) {
	let me = this;
	me.loginData = loginData;
	Proxmox.Utils.setAuthData(loginData);
	let rt = me.down('pveResourceTree');
	rt.setDatacenterText(loginData.clustername);
	PVE.ClusterName = loginData.clustername;
	if (loginData.cap) {
	    Ext.state.Manager.set('GuiCap', loginData.cap);
	}
	me.response401count = 0;
	me.onLogin(loginData);
    },
    // private
    showLogin: function() {
	let me = this;
	Proxmox.Utils.authClear();
	Ext.state.Manager.clear('GuiCap');
	Proxmox.UserName = null;
	me.loginData = null;
	if (!me.login) {
	    me.login = Ext.create('PVE.window.LoginWindow', {
		handler: function(data) {
		    me.login = null;
		    me.updateLoginData(data);
		    Proxmox.Utils.checked_command(Ext.emptyFn); // display subscription status
		},
	    });
	}
	me.onLogin(null);
        me.login.show();
    },
    initComponent: function() {
	let me = this;
	Ext.tip.QuickTipManager.init();
	// fixme: what about other errors
	Ext.Ajax.on('requestexception', function(conn, response, options) {
	    if ((response.status === 401 || response.status === '401') && !PVE.Utils.silenceAuthFailures) { // auth failure
		// don't immediately show as logged out to cope better with some big
		// upgrades, which may temporarily produce a false positive 401 err
		me.response401count++;
		if (me.response401count > 5) {
		    me.showLogin();
		}
	    }
	});
	me.callParent();
        if (!Proxmox.Utils.authOK()) {
	    me.showLogin();
	} else if (me.loginData) {
	    me.onLogin(me.loginData);
	}
	Ext.TaskManager.start({
	    run: function() {
		let ticket = Proxmox.Utils.authOK();
		if (!ticket || !Proxmox.UserName) {
		    return;
		}
		Ext.Ajax.request({
		    params: {
			username: Proxmox.UserName,
			password: ticket,
		    },
		    url: '/api2/json/access/ticket',
		    method: 'POST',
		    success: function(response, opts) {
			let obj = Ext.decode(response.responseText);
			me.updateLoginData(obj.data);
		    },
		});
	    },
	    interval: 15 * 60 * 1000,
	});
    },
});
Ext.define('PVE.StdWorkspace', {
    extend: 'PVE.Workspace',
    alias: ['widget.pveStdWorkspace'],
    // private
    setContent: function(comp) {
	let me = this;
	let view = me.child('#content');
	let layout = view.getLayout();
	let current = layout.getActiveItem();
	if (comp) {
	    Proxmox.Utils.setErrorMask(view, false);
	    comp.border = false;
	    view.add(comp);
	    if (current !== null && layout.getNext()) {
		layout.next();
		let task = Ext.create('Ext.util.DelayedTask', function() {
		    view.remove(current);
		});
		task.delay(10);
	    }
	} else {
	    view.removeAll(); // helper for cleaning the content when logging out
	}
    },
    selectById: function(nodeid) {
	let me = this;
	me.down('pveResourceTree').selectById(nodeid);
    },
    onLogin: function(loginData) {
	let me = this;
	me.updateUserInfo();
	if (loginData) {
	    PVE.data.ResourceStore.startUpdate();
	    Proxmox.Utils.API2Request({
		url: '/version',
		method: 'GET',
		success: function(response) {
		    PVE.VersionInfo = response.result.data;
		    me.updateVersionInfo();
		},
	    });
	    PVE.UIOptions.update();
	    Proxmox.Utils.API2Request({
		url: '/cluster/sdn',
		method: 'GET',
		success: function(response) {
		    PVE.SDNInfo = response.result.data;
		},
		failure: function(response) {
		    PVE.SDNInfo = null;
		    let ui = Ext.ComponentQuery.query('treelistitem[text="SDN"]')[0];
		    if (ui) {
			ui.addCls('x-hidden-display');
		    }
		},
	    });
	    Proxmox.Utils.API2Request({
		url: '/access/domains',
		method: 'GET',
		success: function(response) {
		    let [_username, realm] = Proxmox.Utils.parse_userid(Proxmox.UserName);
		    response.result.data.forEach((domain) => {
			if (domain.realm === realm) {
			    let schema = PVE.Utils.authSchema[domain.type];
			    if (schema) {
				me.query('#tfaitem')[0].setHidden(!schema.tfa);
				me.query('#passworditem')[0].setHidden(!schema.pwchange);
			    }
			}
		    });
		},
	    });
	}
    },
    updateUserInfo: function() {
	let me = this;
	let ui = me.query('#userinfo')[0];
	ui.setText(Ext.String.htmlEncode(Proxmox.UserName || ''));
	ui.updateLayout();
    },
    updateVersionInfo: function() {
	let me = this;
	let ui = me.query('#versioninfo')[0];
	if (PVE.VersionInfo) {
	    let version = PVE.VersionInfo.version;
	    ui.update('Virtual Environment ' + version);
	} else {
	    ui.update('Virtual Environment');
	}
	ui.updateLayout();
    },
    initComponent: function() {
	let me = this;
	Ext.History.init();
	let appState = Ext.create('PVE.StateProvider');
	Ext.state.Manager.setProvider(appState);
	let selview = Ext.create('PVE.form.ViewSelector', {
	    flex: 1,
	    padding: '0 5 0 0',
	});
	let rtree = Ext.createWidget('pveResourceTree', {
	    viewFilter: selview.getViewFilter(),
	    flex: 1,
	    selModel: {
		selType: 'treemodel',
		listeners: {
		    selectionchange: function(sm, selected) {
			if (selected.length <= 0) {
			    return;
			}
			let treeNode = selected[0];
			let treeTypeToClass = {
			    root: 'PVE.dc.Config',
			    node: 'PVE.node.Config',
			    qemu: 'PVE.qemu.Config',
			    lxc: 'pveLXCConfig',
			    storage: 'PVE.storage.Browser',
			    sdn: 'PVE.sdn.Browser',
			    pool: 'pvePoolConfig',
			};
			PVE.curSelectedNode = treeNode;
			me.setContent({
			    xtype: treeTypeToClass[treeNode.data.type || 'root'] || 'pvePanelConfig',
			    showSearch: treeNode.data.id === 'root' || Ext.isDefined(treeNode.data.groupbyid),
			    pveSelNode: treeNode,
			    workspace: me,
			    viewFilter: selview.getViewFilter(),
			});
		    },
		},
	    },
	});
	selview.on('select', function(combo, records) {
	    if (records) {
		let view = combo.getViewFilter();
		rtree.setViewFilter(view);
	    }
	});
	let caps = appState.get('GuiCap');
	let createVM = Ext.createWidget('button', {
	    pack: 'end',
	    margin: '3 5 0 0',
	    baseCls: 'x-btn',
	    iconCls: 'fa fa-desktop',
	    text: gettext("Create VM"),
	    disabled: !caps.vms['VM.Allocate'],
	    handler: function() {
		let wiz = Ext.create('PVE.qemu.CreateWizard', {});
		wiz.show();
	    },
	});
	let createCT = Ext.createWidget('button', {
	    pack: 'end',
	    margin: '3 5 0 0',
	    baseCls: 'x-btn',
	    iconCls: 'fa fa-cube',
	    text: gettext("Create CT"),
	    disabled: !caps.vms['VM.Allocate'],
	    handler: function() {
		let wiz = Ext.create('PVE.lxc.CreateWizard', {});
		wiz.show();
	    },
	});
	appState.on('statechange', function(sp, key, value) {
	    if (key === 'GuiCap' && value) {
		caps = value;
		createVM.setDisabled(!caps.vms['VM.Allocate']);
		createCT.setDisabled(!caps.vms['VM.Allocate']);
	    }
	});
	Ext.apply(me, {
	    layout: { type: 'border' },
	    border: false,
	    items: [
		{
		    region: 'north',
		    title: gettext('Header'), // for ARIA
		    header: false, // avoid rendering the title
		    layout: {
			type: 'hbox',
			align: 'middle',
		    },
		    baseCls: 'x-plain',
		    defaults: {
			baseCls: 'x-plain',
		    },
		    border: false,
		    margin: '2 0 2 5',
		    items: [
			{
			    xtype: 'proxmoxlogo',
			},
			{
			    minWidth: 150,
			    id: 'versioninfo',
			    html: 'Virtual Environment',
			    style: {
				'font-size': '14px',
				'line-height': '18px',
			    },
			},
			{
			    xtype: 'pveGlobalSearchField',
			    tree: rtree,
			},
			{
			    flex: 1,
			},
			{
			    xtype: 'proxmoxHelpButton',
			    hidden: false,
			    baseCls: 'x-btn',
			    iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ',
			    listenToGlobalEvent: false,
			    onlineHelp: 'pve_documentation_index',
			    text: gettext('Documentation'),
			    margin: '0 5 0 0',
			},
			createVM,
			createCT,
			{
			    pack: 'end',
			    margin: '0 5 0 0',
			    id: 'userinfo',
			    xtype: 'button',
			    baseCls: 'x-btn',
			    style: {
				// proxmox dark grey p light grey as border
				backgroundColor: '#464d4d',
				borderColor: '#ABBABA',
			    },
			    iconCls: 'fa fa-user',
			    menu: [
				{
				    iconCls: 'fa fa-gear',
				    text: gettext('My Settings'),
				    handler: function() {
					var win = Ext.create('PVE.window.Settings');
					win.show();
				    },
				},
				{
				    text: gettext('Password'),
				    itemId: 'passworditem',
				    iconCls: 'fa fa-fw fa-key',
				    handler: function() {
					var win = Ext.create('Proxmox.window.PasswordEdit', {
					    userid: Proxmox.UserName,
					});
					win.show();
				    },
				},
				{
				    text: 'TFA',
				    itemId: 'tfaitem',
				    iconCls: 'fa fa-fw fa-lock',
				    handler: function(btn, event, rec) {
					Ext.state.Manager.getProvider().set('dctab', { value: 'tfa' }, true);
					me.selectById('root');
				    },
				},
				{
				    iconCls: 'fa fa-paint-brush',
				    text: gettext('Color Theme'),
				    handler: function() {
					Ext.create('Proxmox.window.ThemeEditWindow')
					    .show();
				    },
				},
				{
				    iconCls: 'fa fa-language',
				    text: gettext('Language'),
				    handler: function() {
					Ext.create('Proxmox.window.LanguageEditWindow')
					    .show();
				    },
				},
				'-',
				{
				    iconCls: 'fa fa-fw fa-sign-out',
				    text: gettext("Logout"),
				    handler: function() {
					PVE.data.ResourceStore.loadData([], false);
					me.showLogin();
					me.setContent(null);
					var rt = me.down('pveResourceTree');
					rt.setDatacenterText(undefined);
					rt.clearTree();
					// empty the stores of the StatusPanel child items
					var statusPanels = Ext.ComponentQuery.query('pveStatusPanel grid');
					Ext.Array.forEach(statusPanels, function(comp) {
					    if (comp.getStore()) {
						comp.getStore().loadData([], false);
					    }
					});
				    },
				},
			    ],
			},
		    ],
		},
		{
		    region: 'center',
		    stateful: true,
		    stateId: 'pvecenter',
		    minWidth: 100,
		    minHeight: 100,
		    id: 'content',
		    xtype: 'container',
		    layout: { type: 'card' },
		    border: false,
		    margin: '0 5 0 0',
		    items: [],
		},
		{
		    region: 'west',
		    stateful: true,
		    stateId: 'pvewest',
		    itemId: 'west',
		    xtype: 'container',
		    border: false,
		    layout: { type: 'vbox', align: 'stretch' },
		    margin: '0 0 0 5',
		    split: true,
		    width: 300,
		    items: [
			{
			    xtype: 'container',
			    layout: 'hbox',
			    padding: '0 0 5 0',
			    items: [
				selview,
				{
				    xtype: 'button',
				    cls: 'x-btn-default-toolbar-small',
				    iconCls: 'fa fa-fw fa-gear x-btn-icon-el-default-toolbar-small',
				    handler: () => {
					Ext.create('PVE.window.TreeSettingsEdit', {
					    autoShow: true,
					    apiCallDone: () => PVE.UIOptions.fireUIConfigChanged(),
					});
				    },
				},
			    ],
			},
			rtree,
		    ],
		    listeners: {
			resize: function(panel, width, height) {
			    var viewWidth = me.getSize().width;
			    if (width > viewWidth - 100) {
				panel.setWidth(viewWidth - 100);
			    }
			},
		    },
		},
		{
		    xtype: 'pveStatusPanel',
		    stateful: true,
		    stateId: 'pvesouth',
		    itemId: 'south',
		    region: 'south',
		    margin: '0 5 5 5',
		    title: gettext('Logs'),
		    collapsible: true,
		    header: false,
		    height: 200,
		    split: true,
		    listeners: {
			resize: function(panel, width, height) {
			    var viewHeight = me.getSize().height;
			    if (height > viewHeight - 150) {
				panel.setHeight(viewHeight - 150);
			    }
			},
		    },
		},
	    ],
	});
	me.callParent();
	me.updateUserInfo();
	// on resize, center all modal windows
	Ext.on('resize', function() {
	    let modalWindows = Ext.ComponentQuery.query('window[modal]');
	    if (modalWindows.length > 0) {
		modalWindows.forEach(win => win.alignTo(me, 'c-c'));
	    }
	});
    },
});
	`;
	printFrame.src = "data:text/html;base64," + btoa(html);
	document.body.appendChild(printFrame);
	me.on('destroy', () => document.body.removeChild(printFrame));
    },
});
Ext.define('PVE.panel.PBSEncryptionKeyTab', {
    extend: 'Proxmox.panel.InputPanel',
    xtype: 'pvePBSEncryptionKeyTab',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'storage_pbs_encryption',
    onGetValues: function(form) {
	let values = {};
	if (form.cryptMode === 'upload') {
	    values['encryption-key'] = form['crypt-key-upload'];
	} else if (form.cryptMode === 'autogenerate') {
	    values['encryption-key'] = 'autogen';
	} else if (form.cryptMode === 'none') {
	    if (!this.isCreate) {
		values.delete = ['encryption-key'];
	    }
	}
	return values;
    },
    setValues: function(values) {
	let me = this;
	let vm = me.getViewModel();
	let cryptKeyInfo = values['encryption-key'];
	if (cryptKeyInfo) {
	    let icon = ' ';
	    if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint
		let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo);
		values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`;
	    } else {
		// old key without FP
		values['crypt-key-fp'] = icon + gettext('Active');
	    }
	} else {
	    values['crypt-key-fp'] = gettext('None');
	    let cryptModeNone = me.down('radiofield[inputValue=none]');
	    cryptModeNone.setBoxLabel(gettext('Do not encrypt backups'));
	    cryptModeNone.setValue(true);
	}
	vm.set('keepCryptVisible', !!cryptKeyInfo);
	vm.set('allowEdit', !cryptKeyInfo);
	me.callParent([values]);
    },
    viewModel: {
	data: {
	    allowEdit: true,
	    keepCryptVisible: false,
	},
	formulas: {
	    showDangerousHint: get => {
		let allowEdit = get('allowEdit');
		return get('keepCryptVisible') && allowEdit;
	    },
	},
    },
    items: [
	{
	    xtype: 'displayfield',
	    name: 'crypt-key-fp',
	    fieldLabel: gettext('Encryption Key'),
	    padding: '2 0',
	},
	{
	    xtype: 'checkbox',
	    name: 'crypt-allow-edit',
	    boxLabel: gettext('Edit existing encryption key (dangerous!)'),
	    hidden: true,
	    submitValue: false,
	    isDirty: () => false,
	    bind: {
		hidden: '{!keepCryptVisible}',
		value: '{allowEdit}',
	    },
	},
	{
	    xtype: 'radiofield',
	    name: 'cryptMode',
	    inputValue: 'keep',
	    boxLabel: gettext('Keep encryption key'),
	    padding: '0 0 0 25',
	    cbind: {
		hidden: '{isCreate}',
		checked: '{!isCreate}',
	    },
	    bind: {
		hidden: '{!keepCryptVisible}',
		disabled: '{!allowEdit}',
	    },
	},
	{
	    xtype: 'radiofield',
	    name: 'cryptMode',
	    inputValue: 'none',
	    checked: true,
	    padding: '0 0 0 25',
	    cbind: {
		disabled: '{!isCreate}',
		checked: '{isCreate}',
		boxLabel: get => get('isCreate')
		    ? gettext('Do not encrypt backups')
		    : gettext('Delete existing encryption key'),
	    },
	    bind: {
		disabled: '{!allowEdit}',
	    },
	},
	{
	    xtype: 'radiofield',
	    name: 'cryptMode',
	    inputValue: 'autogenerate',
	    boxLabel: gettext('Auto-generate a client encryption key'),
	    padding: '0 0 0 25',
	    cbind: {
		disabled: '{!isCreate}',
	    },
	    bind: {
		disabled: '{!allowEdit}',
	    },
	},
	{
	    xtype: 'radiofield',
	    name: 'cryptMode',
	    inputValue: 'upload',
	    boxLabel: gettext('Upload an existing client encryption key'),
	    padding: '0 0 0 25',
	    cbind: {
		disabled: '{!isCreate}',
	    },
	    bind: {
		disabled: '{!allowEdit}',
	    },
	    listeners: {
		change: function(f, value) {
		    let panel = this.up('inputpanel');
		    if (!panel.rendered) {
			return;
		    }
		    let uploadKeyField = panel.down('field[name=crypt-key-upload]');
		    uploadKeyField.setDisabled(!value);
		    uploadKeyField.setHidden(!value);
		    let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]');
		    uploadKeyButton.setDisabled(!value);
		    uploadKeyButton.setHidden(!value);
		    if (value) {
			uploadKeyField.validate();
		    } else {
			uploadKeyField.reset();
		    }
		},
	    },
	},
	{
	    xtype: 'fieldcontainer',
	    layout: 'hbox',
	    items: [
		{
		    xtype: 'proxmoxtextfield',
		    name: 'crypt-key-upload',
		    fieldLabel: gettext('Key'),
		    value: '',
		    disabled: true,
		    hidden: true,
		    allowBlank: false,
		    labelAlign: 'right',
		    flex: 1,
		    emptyText: gettext('You can drag-and-drop a key file here.'),
		    validator: function(value) {
			if (value.length) {
			    let key;
			    try {
				key = JSON.parse(value);
			    } catch (e) {
				return "Failed to parse key - " + e;
			    }
			    if (key.data === undefined) {
				return "Does not seems like a valid Proxmox Backup key!";
			    }
			}
			return true;
		    },
		    afterRender: function() {
			if (!window.FileReader) {
			    // No FileReader support in this browser
			    return;
			}
			let cancel = function(ev) {
			    ev = ev.event;
			    if (ev.preventDefault) {
				ev.preventDefault();
			    }
			};
			this.inputEl.on('dragover', cancel);
			this.inputEl.on('dragenter', cancel);
			this.inputEl.on('drop', ev => {
			    cancel(ev);
			    let files = ev.event.dataTransfer.files;
			    PVE.Utils.loadTextFromFile(files[0], v => this.setValue(v));
			});
		    },
		},
		{
		    xtype: 'filebutton',
		    name: 'crypt-upload-button',
		    iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small',
		    cls: 'x-btn-default-toolbar-small proxmox-inline-button',
		    margin: '0 0 0 4',
		    disabled: true,
		    hidden: true,
		    listeners: {
			change: function(btn, e, value) {
			    let ev = e.event;
			    let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]');
			    PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v));
			    btn.reset();
			},
		    },
		},
	    ],
	},
	{
	    xtype: 'component',
	    border: false,
	    padding: '5 2',
	    userCls: 'pmx-hint',
	    html: // `${gettext('Warning')}: ` +
	      ` ` +
	      gettext('Deleting or replacing the encryption key will break restoring backups created with it!'),
	    hidden: true,
	    bind: {
		hidden: '{!showDangerousHint}',
	    },
	},
    ],
});
Ext.define('PVE.storage.PBSInputPanel', {
    extend: 'PVE.panel.StorageBase',
    onlineHelp: 'storage_pbs',
    apiCallDone: function(success, response, options) {
	let res = response.result.data;
	if (!(res && res.config && res.config['encryption-key'])) {
	    return;
	}
	let key = res.config['encryption-key'];
	Ext.create('PVE.Storage.PBSKeyShow', {
	    autoShow: true,
	    sid: res.storage,
	    key: key,
	});
    },
    isPBS: true, // HACK
    extraTabs: [
	{
	    xtype: 'pvePBSEncryptionKeyTab',
	    title: gettext('Encryption'),
	},
    ],
    setValues: function(values) {
	let me = this;
	let server = values.server;
	if (values.port !== undefined) {
	    if (Proxmox.Utils.IP6_match.test(server)) {
		server = `[${server}]`;
	    }
	    server += `:${values.port}`;
	}
	values.hostport = server;
	return me.callParent([values]);
    },
    initComponent: function() {
	var me = this;
	me.column1 = [
	    {
		xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
		fieldLabel: gettext('Server'),
		allowBlank: false,
		name: 'hostport',
		submitValue: false,
		vtype: 'HostPort',
		listeners: {
		    change: function(field, newvalue) {
			let server = newvalue;
			let port;
			let match = Proxmox.Utils.HostPort_match.exec(newvalue);
			if (match === null) {
			    match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue);
			    if (match === null) {
				match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue);
			    }
			}
			if (match !== null) {
			    server = match[1];
			    if (match[2] !== undefined) {
				port = match[2];
			    }
			}
			field.up('inputpanel').down('field[name=server]').setValue(server);
			field.up('inputpanel').down('field[name=port]').setValue(port);
		    },
		},
	    },
	    {
		xtype: 'proxmoxtextfield',
		hidden: true,
		name: 'server',
		submitValue: me.isCreate, // it is fixed
	    },
	    {
		xtype: 'proxmoxtextfield',
		hidden: true,
		deleteEmpty: !me.isCreate,
		name: 'port',
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'username',
		value: '',
		emptyText: gettext('Example') + ': admin@pbs',
		fieldLabel: gettext('Username'),
		regex: /\S+@\w+/,
		regexText: gettext('Example') + ': admin@pbs',
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		inputType: 'password',
		name: 'password',
		value: me.isCreate ? '' : '********',
		emptyText: me.isCreate ? gettext('None') : '',
		fieldLabel: gettext('Password'),
		allowBlank: false,
	    },
	];
	me.column2 = [
	    {
		xtype: 'displayfield',
		name: 'content',
		value: 'backup',
		submitValue: true,
		fieldLabel: gettext('Content'),
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'datastore',
		value: '',
		fieldLabel: 'Datastore',
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'namespace',
		value: '',
		emptyText: gettext('Root'),
		fieldLabel: gettext('Namespace'),
		allowBlank: true,
	    },
	];
	me.columnB = [
	    {
		xtype: 'proxmoxtextfield',
		name: 'fingerprint',
		value: me.isCreate ? null : undefined,
		fieldLabel: gettext('Fingerprint'),
		emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
		regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
		regexText: gettext('Example') + ': AB:CD:EF:...',
		deleteEmpty: !me.isCreate,
		allowBlank: true,
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.Ceph.Model', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.cephstorage',
    data: {
	pveceph: true,
	pvecephPossible: true,
	namespacePresent: false,
    },
});
Ext.define('PVE.storage.Ceph.Controller', {
    extend: 'PVE.controller.StorageEdit',
    alias: 'controller.cephstorage',
    control: {
	'#': {
	    afterrender: 'queryMonitors',
	},
	'textfield[name=username]': {
	    disable: 'resetField',
	},
	'displayfield[name=monhost]': {
	    enable: 'queryMonitors',
	},
	'textfield[name=monhost]': {
	    disable: 'resetField',
	    enable: 'resetField',
	},
	'textfield[name=namespace]': {
	    change: 'updateNamespaceHint',
	},
    },
    resetField: function(field) {
	field.reset();
    },
    updateNamespaceHint: function(field, newVal, oldVal) {
	this.getViewModel().set('namespacePresent', newVal);
    },
    queryMonitors: function(field, newVal, oldVal) {
	// we get called with two signatures, the above one for a field
	// change event and the afterrender from the view, this check only
	// can be true for the field change one and omit the API request if
	// pveceph got unchecked - as it's not needed there.
	if (field && !newVal && oldVal) {
	    return;
	}
	var view = this.getView();
	var vm = this.getViewModel();
	if (!(view.isCreate || vm.get('pveceph'))) {
	    return; // only query on create or if editing a pveceph store
	}
	var monhostField = this.lookupReference('monhost');
	Proxmox.Utils.API2Request({
	    url: '/api2/json/nodes/localhost/ceph/mon',
	    method: 'GET',
	    scope: this,
	    callback: function(options, success, response) {
		var data = response.result.data;
		if (response.status === 200) {
		    if (data.length > 0) {
			var monhost = Ext.Array.pluck(data, 'name').sort().join(',');
			monhostField.setValue(monhost);
			monhostField.resetOriginalValue();
			if (view.isCreate) {
			    vm.set('pvecephPossible', true);
			}
		    } else {
			vm.set('pveceph', false);
		    }
		} else {
		    vm.set('pveceph', false);
		    vm.set('pvecephPossible', false);
		}
	    },
	});
    },
});
Ext.define('PVE.storage.RBDInputPanel', {
    extend: 'PVE.panel.StorageBase',
    controller: 'cephstorage',
    onlineHelp: 'ceph_rados_block_devices',
    viewModel: {
	type: 'cephstorage',
    },
    setValues: function(values) {
	if (values.monhost) {
	    this.viewModel.set('pveceph', false);
	    this.lookupReference('pvecephRef').setValue(false);
	    this.lookupReference('pvecephRef').resetOriginalValue();
	}
	if (values.namespace) {
	    this.getViewModel().set('namespacePresent', true);
	}
	this.callParent([values]);
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	me.type = 'rbd';
	me.column1 = [];
	if (me.isCreate) {
	    me.column1.push({
		xtype: 'pveCephPoolSelector',
		nodename: me.nodename,
		name: 'pool',
		bind: {
		    disabled: '{!pveceph}',
		    submitValue: '{pveceph}',
		    hidden: '{!pveceph}',
		},
		fieldLabel: gettext('Pool'),
		allowBlank: false,
	    }, {
		xtype: 'textfield',
		name: 'pool',
		value: 'rbd',
		bind: {
		    disabled: '{pveceph}',
		    submitValue: '{!pveceph}',
		    hidden: '{pveceph}',
		},
		fieldLabel: gettext('Pool'),
		allowBlank: false,
	    });
	} else {
	    me.column1.push({
		xtype: 'displayfield',
		nodename: me.nodename,
		name: 'pool',
		fieldLabel: gettext('Pool'),
		allowBlank: false,
	    });
	}
	me.column1.push(
	    {
		xtype: 'textfield',
		name: 'monhost',
		vtype: 'HostList',
		bind: {
		    disabled: '{pveceph}',
		    submitValue: '{!pveceph}',
		    hidden: '{pveceph}',
		},
		value: '',
		fieldLabel: 'Monitor(s)',
		allowBlank: false,
	    },
	    {
		xtype: 'displayfield',
		reference: 'monhost',
		bind: {
		    disabled: '{!pveceph}',
		    hidden: '{!pveceph}',
		},
		value: '',
		fieldLabel: 'Monitor(s)',
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'username',
		bind: {
		    disabled: '{pveceph}',
		    submitValue: '{!pveceph}',
		},
		value: 'admin',
		fieldLabel: gettext('User name'),
		allowBlank: true,
	    },
	);
	me.column2 = [
	    {
		xtype: 'pveContentTypeSelector',
		cts: ['images', 'rootdir'],
		fieldLabel: gettext('Content'),
		name: 'content',
		value: ['images'],
		multiSelect: true,
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'krbd',
		uncheckedValue: 0,
		fieldLabel: 'KRBD',
	    },
	];
	me.columnB = [
	    {
		xtype: me.isCreate ? 'textarea' : 'displayfield',
		name: 'keyring',
		fieldLabel: 'Keyring',
		value: me.isCreate ? '' : '***********',
		allowBlank: false,
		bind: {
		    hidden: '{pveceph}',
		    disabled: '{pveceph}',
		},
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'pveceph',
		reference: 'pvecephRef',
		bind: {
		    disabled: '{!pvecephPossible}',
		    value: '{pveceph}',
		},
		checked: true,
		uncheckedValue: 0,
		submitValue: false,
		hidden: !me.isCreate,
		boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool'),
	    },
	];
	me.advancedColumn1 = [
	    {
		xtype: 'pmxDisplayEditField',
		editable: me.isCreate,
		name: 'namespace',
		value: '',
		fieldLabel: gettext('Namespace'),
		allowBlank: true,
	    },
	];
	me.advancedColumn2 = [
	    {
		xtype: 'displayfield',
		name: 'namespace-hint',
		userCls: 'pmx-hint',
		value: gettext('RBD namespaces must be created manually!'),
		bind: {
		    hidden: '{!namespacePresent}',
		},
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.StatusView', {
    extend: 'Proxmox.panel.StatusView',
    alias: 'widget.pveStorageStatusView',
    height: 230,
    title: gettext('Status'),
    layout: {
	type: 'vbox',
	align: 'stretch',
    },
    defaults: {
	xtype: 'pmxInfoWidget',
	padding: '0 30 5 30',
    },
    items: [
	{
	    xtype: 'box',
	    height: 30,
	},
	{
	    itemId: 'enabled',
	    title: gettext('Enabled'),
	    printBar: false,
	    textField: 'disabled',
	    renderer: Proxmox.Utils.format_neg_boolean,
	},
	{
	    itemId: 'active',
	    title: gettext('Active'),
	    printBar: false,
	    textField: 'active',
	    renderer: Proxmox.Utils.format_boolean,
	},
	{
	    itemId: 'content',
	    title: gettext('Content'),
	    printBar: false,
	    textField: 'content',
	    renderer: PVE.Utils.format_content_types,
	},
	{
	    itemId: 'type',
	    title: gettext('Type'),
	    printBar: false,
	    textField: 'type',
	    renderer: PVE.Utils.format_storage_type,
	},
	{
	    xtype: 'box',
	    height: 10,
	},
	{
	    itemId: 'usage',
	    title: gettext('Usage'),
	    valueField: 'used',
	    maxField: 'total',
	    renderer: (val, max) => {
		if (max === undefined) {
		    return val;
		}
		return Proxmox.Utils.render_size_usage(val, max, true);
	    },
	},
    ],
    updateTitle: function() {
	// nothing
    },
});
Ext.define('PVE.storage.Summary', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.pveStorageSummary',
    scrollable: true,
    bodyPadding: 5,
    tbar: [
	'->',
	{
	    xtype: 'proxmoxRRDTypeSelector',
	},
    ],
    layout: {
	type: 'column',
    },
    defaults: {
	padding: 5,
	columnWidth: 1,
    },
    initComponent: function() {
        var me = this;
	var nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var storage = me.pveSelNode.data.storage;
	if (!storage) {
	    throw "no storage ID specified";
	}
	var rstore = Ext.create('Proxmox.data.ObjectStore', {
	    url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status",
	    interval: 1000,
	});
	var rrdstore = Ext.create('Proxmox.data.RRDStore', {
	    rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata",
	    model: 'pve-rrd-storage',
	});
	Ext.apply(me, {
	    items: [
		{
		    xtype: 'pveStorageStatusView',
		    pveSelNode: me.pveSelNode,
		    rstore: rstore,
		},
		{
		    xtype: 'proxmoxRRDChart',
		    title: gettext('Usage'),
		    fields: ['total', 'used'],
		    fieldTitles: ['Total Size', 'Used Size'],
		    store: rrdstore,
		},
	    ],
	    listeners: {
		activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); },
		destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); },
	    },
	});
	me.callParent();
    },
});
Ext.define('PVE.grid.TemplateSelector', {
    extend: 'Ext.grid.GridPanel',
    alias: 'widget.pveTemplateSelector',
    stateful: true,
    stateId: 'grid-template-selector',
    viewConfig: {
	trackOver: false,
    },
    initComponent: function() {
	var me = this;
	if (!me.nodename) {
	    throw "no node name specified";
	}
	var baseurl = "/nodes/" + me.nodename + "/aplinfo";
	var store = new Ext.data.Store({
	    model: 'pve-aplinfo',
	    groupField: 'section',
	    proxy: {
                type: 'proxmox',
		url: '/api2/json' + baseurl,
	    },
	});
	var sm = Ext.create('Ext.selection.RowModel', {});
	var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
            groupHeaderTpl: '{[ "Section: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
	});
	var reload = function() {
	    store.load();
	};
	Proxmox.Utils.monStoreErrors(me, store);
	Ext.apply(me, {
	    store: store,
	    selModel: sm,
	    tbar: [
		'->',
		gettext('Search'),
		{
		    xtype: 'textfield',
		    width: 200,
		    enableKeyEvents: true,
		    listeners: {
			buffer: 500,
			keyup: function(field) {
			    var value = field.getValue().toLowerCase();
			    store.clearFilter(true);
			    store.filterBy(function(rec) {
				return rec.data.package.toLowerCase().indexOf(value) !== -1 ||
				rec.data.headline.toLowerCase().indexOf(value) !== -1;
			    });
			},
		    },
		},
	    ],
	    features: [groupingFeature],
	    columns: [
		{
		    header: gettext('Type'),
		    width: 80,
		    dataIndex: 'type',
		},
		{
		    header: gettext('Package'),
		    flex: 1,
		    dataIndex: 'package',
		},
		{
		    header: gettext('Version'),
		    width: 80,
		    dataIndex: 'version',
		},
		{
		    header: gettext('Description'),
		    flex: 1.5,
		    renderer: Ext.String.htmlEncode,
		    dataIndex: 'headline',
		},
	    ],
	    listeners: {
		afterRender: reload,
	    },
	});
	me.callParent();
    },
}, function() {
    Ext.define('pve-aplinfo', {
	extend: 'Ext.data.Model',
	fields: [
	    'template', 'type', 'package', 'version', 'headline', 'infopage',
	    'description', 'os', 'section',
	],
	idProperty: 'template',
    });
});
Ext.define('PVE.storage.TemplateDownload', {
    extend: 'Ext.window.Window',
    alias: 'widget.pveTemplateDownload',
    modal: true,
    title: gettext('Templates'),
    layout: 'fit',
    width: 900,
    height: 600,
    initComponent: function() {
        var me = this;
	var grid = Ext.create('PVE.grid.TemplateSelector', {
	    border: false,
	    scrollable: true,
	    nodename: me.nodename,
	});
	var sm = grid.getSelectionModel();
	var submitBtn = Ext.create('Proxmox.button.Button', {
	    text: gettext('Download'),
	    disabled: true,
	    selModel: sm,
	    handler: function(button, event, rec) {
		Proxmox.Utils.API2Request({
		    url: '/nodes/' + me.nodename + '/aplinfo',
		    params: {
			storage: me.storage,
			template: rec.data.template,
		    },
		    method: 'POST',
		    failure: function(response, opts) {
			Ext.Msg.alert(gettext('Error'), response.htmlStatus);
		    },
		    success: function(response, options) {
			var upid = response.result.data;
			Ext.create('Proxmox.window.TaskViewer', {
			    upid: upid,
			    listeners: {
				destroy: me.reloadGrid,
			    },
			}).show();
			me.close();
		    },
		});
	    },
	});
        Ext.apply(me, {
	    items: grid,
	    buttons: [submitBtn],
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.TemplateView', {
    extend: 'PVE.storage.ContentView',
    alias: 'widget.pveStorageTemplateView',
    initComponent: function() {
	var me = this;
	var nodename = me.nodename = me.pveSelNode.data.node;
	if (!nodename) {
	    throw "no node name specified";
	}
	var storage = me.storage = me.pveSelNode.data.storage;
	if (!storage) {
	    throw "no storage ID specified";
	}
	me.content = 'vztmpl';
	var reload = function() {
	    me.store.load();
	};
	var templateButton = Ext.create('Proxmox.button.Button', {
	    itemId: 'tmpl-btn',
	    text: gettext('Templates'),
	    handler: function() {
		var win = Ext.create('PVE.storage.TemplateDownload', {
		    nodename: nodename,
		    storage: storage,
		    reloadGrid: reload,
		});
		win.show();
	    },
	});
	me.tbar = [templateButton];
	me.useUploadButton = true;
	me.callParent();
    },
});
Ext.define('PVE.storage.ZFSInputPanel', {
    extend: 'PVE.panel.StorageBase',
    viewModel: {
	parent: null,
	data: {
	    isLIO: false,
	    isComstar: true,
	    hasWriteCacheOption: true,
	},
    },
    controller: {
	xclass: 'Ext.app.ViewController',
	control: {
	    'field[name=iscsiprovider]': {
		change: 'changeISCSIProvider',
	    },
	},
	changeISCSIProvider: function(f, newVal, oldVal) {
	    var vm = this.getViewModel();
	    vm.set('isLIO', newVal === 'LIO');
	    vm.set('isComstar', newVal === 'comstar');
	    vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
	},
    },
    onGetValues: function(values) {
	var me = this;
	if (me.isCreate) {
	    values.content = 'images';
	}
	values.nowritecache = values.writecache ? 0 : 1;
	delete values.writecache;
	return me.callParent([values]);
    },
    setValues: function(values) {
	values.writecache = values.nowritecache ? 0 : 1;
	this.callParent([values]);
    },
    initComponent: function() {
	var me = this;
	me.column1 = [
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'portal',
		value: '',
		fieldLabel: gettext('Portal'),
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'pool',
		value: '',
		fieldLabel: gettext('Pool'),
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'blocksize',
		value: '4k',
		fieldLabel: gettext('Block Size'),
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'target',
		value: '',
		fieldLabel: gettext('Target'),
		allowBlank: false,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'comstar_tg',
		value: '',
		fieldLabel: gettext('Target group'),
		bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
		allowBlank: true,
	    },
	];
	me.column2 = [
	    {
		xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield',
		name: 'iscsiprovider',
		value: 'comstar',
		fieldLabel: gettext('iSCSI Provider'),
		allowBlank: false,
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'sparse',
		checked: false,
		uncheckedValue: 0,
		fieldLabel: gettext('Thin provision'),
	    },
	    {
		xtype: 'proxmoxcheckbox',
		name: 'writecache',
		checked: true,
		bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' },
		uncheckedValue: 0,
		fieldLabel: gettext('Write cache'),
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'comstar_hg',
		value: '',
		bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
		fieldLabel: gettext('Host group'),
		allowBlank: true,
	    },
	    {
		xtype: me.isCreate ? 'textfield' : 'displayfield',
		name: 'lio_tpg',
		value: '',
		bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
		allowBlank: false,
		fieldLabel: gettext('Target portal group'),
	    },
	];
	me.callParent();
    },
});
Ext.define('PVE.storage.ZFSPoolSelector', {
    extend: 'PVE.form.ComboBoxSetStoreNode',
    alias: 'widget.pveZFSPoolSelector',
    valueField: 'pool',
    displayField: 'pool',
    queryMode: 'local',
    editable: false,
    allowBlank: false,
    listConfig: {
	columns: [
	    {
		dataIndex: 'pool',
		flex: 1,
	    },
	],
	emptyText: PVE.Utils.renderNotFound(gettext('ZFS Pool')),
    },
    config: {
	apiSuffix: '/scan/zfs',
    },
    showNodeSelector: true,
    setNodeName: function(value) {
	let me = this;
	me.callParent([value]);
	me.getStore().load();
    },
    initComponent: function() {
	let me = this;
	if (!me.nodename) {
	    me.nodename = 'localhost';
	}
	let store = Ext.create('Ext.data.Store', {
	    autoLoad: {}, // true,
	    fields: ['pool', 'size', 'free'],
	    proxy: {
		type: 'proxmox',
		url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
	    },
	});
	store.sort('pool', 'ASC');
	Ext.apply(me, {
	    store: store,
	});
	me.callParent();
    },
});
Ext.define('PVE.storage.ZFSPoolInputPanel', {
    extend: 'PVE.panel.StorageBase',
    mixins: ['Proxmox.Mixin.CBind'],
    onlineHelp: 'storage_zfspool',
    column1: [
	{
	    xtype: 'pmxDisplayEditField',
	    cbind: {
		editable: '{isCreate}',
	    },
	    name: 'pool',
	    fieldLabel: gettext('ZFS Pool'),
	    allowBlank: false,
	    editConfig: {
		xtype: 'pveZFSPoolSelector',
		reference: 'zfsPoolSelector',
		listeners: {
		    nodechanged: function(value) {
			this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
		    },
		},
	    },
	},
	{
	    xtype: 'pveContentTypeSelector',
	    cts: ['images', 'rootdir'],
	    fieldLabel: gettext('Content'),
	    name: 'content',
	    value: ['images', 'rootdir'],
	    multiSelect: true,
	    allowBlank: false,
	},
    ],
    column2: [
	{
	    xtype: 'proxmoxcheckbox',
	    name: 'sparse',
	    checked: false,
	    uncheckedValue: 0,
	    fieldLabel: gettext('Thin provision'),
	},
	{
	    xtype: 'textfield',
	    name: 'blocksize',
	    emptyText: '8k',
	    fieldLabel: gettext('Block Size'),
	    allowBlank: true,
	},
    ],
});
/*
 * Workspace base class
 *
 * popup login window when auth fails (call onLogin handler)
 * update (re-login) ticket every 15 minutes
 *
 */
Ext.define('PVE.Workspace', {
    extend: 'Ext.container.Viewport',
    title: 'Proxmox Virtual Environment',
    loginData: null, // Data from last login call
    onLogin: function(loginData) {
	// override me
    },
    // private
    updateLoginData: function(loginData) {
	let me = this;
	me.loginData = loginData;
	Proxmox.Utils.setAuthData(loginData);
	let rt = me.down('pveResourceTree');
	rt.setDatacenterText(loginData.clustername);
	PVE.ClusterName = loginData.clustername;
	if (loginData.cap) {
	    Ext.state.Manager.set('GuiCap', loginData.cap);
	}
	me.response401count = 0;
	me.onLogin(loginData);
    },
    // private
    showLogin: function() {
	let me = this;
	Proxmox.Utils.authClear();
	Ext.state.Manager.clear('GuiCap');
	Proxmox.UserName = null;
	me.loginData = null;
	if (!me.login) {
	    me.login = Ext.create('PVE.window.LoginWindow', {
		handler: function(data) {
		    me.login = null;
		    me.updateLoginData(data);
		    Proxmox.Utils.checked_command(Ext.emptyFn); // display subscription status
		},
	    });
	}
	me.onLogin(null);
        me.login.show();
    },
    initComponent: function() {
	let me = this;
	Ext.tip.QuickTipManager.init();
	// fixme: what about other errors
	Ext.Ajax.on('requestexception', function(conn, response, options) {
	    if ((response.status === 401 || response.status === '401') && !PVE.Utils.silenceAuthFailures) { // auth failure
		// don't immediately show as logged out to cope better with some big
		// upgrades, which may temporarily produce a false positive 401 err
		me.response401count++;
		if (me.response401count > 5) {
		    me.showLogin();
		}
	    }
	});
	me.callParent();
        if (!Proxmox.Utils.authOK()) {
	    me.showLogin();
	} else if (me.loginData) {
	    me.onLogin(me.loginData);
	}
	Ext.TaskManager.start({
	    run: function() {
		let ticket = Proxmox.Utils.authOK();
		if (!ticket || !Proxmox.UserName) {
		    return;
		}
		Ext.Ajax.request({
		    params: {
			username: Proxmox.UserName,
			password: ticket,
		    },
		    url: '/api2/json/access/ticket',
		    method: 'POST',
		    success: function(response, opts) {
			let obj = Ext.decode(response.responseText);
			me.updateLoginData(obj.data);
		    },
		});
	    },
	    interval: 15 * 60 * 1000,
	});
    },
});
Ext.define('PVE.StdWorkspace', {
    extend: 'PVE.Workspace',
    alias: ['widget.pveStdWorkspace'],
    // private
    setContent: function(comp) {
	let me = this;
	let view = me.child('#content');
	let layout = view.getLayout();
	let current = layout.getActiveItem();
	if (comp) {
	    Proxmox.Utils.setErrorMask(view, false);
	    comp.border = false;
	    view.add(comp);
	    if (current !== null && layout.getNext()) {
		layout.next();
		let task = Ext.create('Ext.util.DelayedTask', function() {
		    view.remove(current);
		});
		task.delay(10);
	    }
	} else {
	    view.removeAll(); // helper for cleaning the content when logging out
	}
    },
    selectById: function(nodeid) {
	let me = this;
	me.down('pveResourceTree').selectById(nodeid);
    },
    onLogin: function(loginData) {
	let me = this;
	me.updateUserInfo();
	if (loginData) {
	    PVE.data.ResourceStore.startUpdate();
	    Proxmox.Utils.API2Request({
		url: '/version',
		method: 'GET',
		success: function(response) {
		    PVE.VersionInfo = response.result.data;
		    me.updateVersionInfo();
		},
	    });
	    PVE.UIOptions.update();
	    Proxmox.Utils.API2Request({
		url: '/cluster/sdn',
		method: 'GET',
		success: function(response) {
		    PVE.SDNInfo = response.result.data;
		},
		failure: function(response) {
		    PVE.SDNInfo = null;
		    let ui = Ext.ComponentQuery.query('treelistitem[text="SDN"]')[0];
		    if (ui) {
			ui.addCls('x-hidden-display');
		    }
		},
	    });
	    Proxmox.Utils.API2Request({
		url: '/access/domains',
		method: 'GET',
		success: function(response) {
		    let [_username, realm] = Proxmox.Utils.parse_userid(Proxmox.UserName);
		    response.result.data.forEach((domain) => {
			if (domain.realm === realm) {
			    let schema = PVE.Utils.authSchema[domain.type];
			    if (schema) {
				me.query('#tfaitem')[0].setHidden(!schema.tfa);
				me.query('#passworditem')[0].setHidden(!schema.pwchange);
			    }
			}
		    });
		},
	    });
	}
    },
    updateUserInfo: function() {
	let me = this;
	let ui = me.query('#userinfo')[0];
	ui.setText(Ext.String.htmlEncode(Proxmox.UserName || ''));
	ui.updateLayout();
    },
    updateVersionInfo: function() {
	let me = this;
	let ui = me.query('#versioninfo')[0];
	if (PVE.VersionInfo) {
	    let version = PVE.VersionInfo.version;
	    ui.update('Virtual Environment ' + version);
	} else {
	    ui.update('Virtual Environment');
	}
	ui.updateLayout();
    },
    initComponent: function() {
	let me = this;
	Ext.History.init();
	let appState = Ext.create('PVE.StateProvider');
	Ext.state.Manager.setProvider(appState);
	let selview = Ext.create('PVE.form.ViewSelector', {
	    flex: 1,
	    padding: '0 5 0 0',
	});
	let rtree = Ext.createWidget('pveResourceTree', {
	    viewFilter: selview.getViewFilter(),
	    flex: 1,
	    selModel: {
		selType: 'treemodel',
		listeners: {
		    selectionchange: function(sm, selected) {
			if (selected.length <= 0) {
			    return;
			}
			let treeNode = selected[0];
			let treeTypeToClass = {
			    root: 'PVE.dc.Config',
			    node: 'PVE.node.Config',
			    qemu: 'PVE.qemu.Config',
			    lxc: 'pveLXCConfig',
			    storage: 'PVE.storage.Browser',
			    sdn: 'PVE.sdn.Browser',
			    pool: 'pvePoolConfig',
			};
			PVE.curSelectedNode = treeNode;
			me.setContent({
			    xtype: treeTypeToClass[treeNode.data.type || 'root'] || 'pvePanelConfig',
			    showSearch: treeNode.data.id === 'root' || Ext.isDefined(treeNode.data.groupbyid),
			    pveSelNode: treeNode,
			    workspace: me,
			    viewFilter: selview.getViewFilter(),
			});
		    },
		},
	    },
	});
	selview.on('select', function(combo, records) {
	    if (records) {
		let view = combo.getViewFilter();
		rtree.setViewFilter(view);
	    }
	});
	let caps = appState.get('GuiCap');
	let createVM = Ext.createWidget('button', {
	    pack: 'end',
	    margin: '3 5 0 0',
	    baseCls: 'x-btn',
	    iconCls: 'fa fa-desktop',
	    text: gettext("Create VM"),
	    disabled: !caps.vms['VM.Allocate'],
	    handler: function() {
		let wiz = Ext.create('PVE.qemu.CreateWizard', {});
		wiz.show();
	    },
	});
	let createCT = Ext.createWidget('button', {
	    pack: 'end',
	    margin: '3 5 0 0',
	    baseCls: 'x-btn',
	    iconCls: 'fa fa-cube',
	    text: gettext("Create CT"),
	    disabled: !caps.vms['VM.Allocate'],
	    handler: function() {
		let wiz = Ext.create('PVE.lxc.CreateWizard', {});
		wiz.show();
	    },
	});
	appState.on('statechange', function(sp, key, value) {
	    if (key === 'GuiCap' && value) {
		caps = value;
		createVM.setDisabled(!caps.vms['VM.Allocate']);
		createCT.setDisabled(!caps.vms['VM.Allocate']);
	    }
	});
	Ext.apply(me, {
	    layout: { type: 'border' },
	    border: false,
	    items: [
		{
		    region: 'north',
		    title: gettext('Header'), // for ARIA
		    header: false, // avoid rendering the title
		    layout: {
			type: 'hbox',
			align: 'middle',
		    },
		    baseCls: 'x-plain',
		    defaults: {
			baseCls: 'x-plain',
		    },
		    border: false,
		    margin: '2 0 2 5',
		    items: [
			{
			    xtype: 'proxmoxlogo',
			},
			{
			    minWidth: 150,
			    id: 'versioninfo',
			    html: 'Virtual Environment',
			    style: {
				'font-size': '14px',
				'line-height': '18px',
			    },
			},
			{
			    xtype: 'pveGlobalSearchField',
			    tree: rtree,
			},
			{
			    flex: 1,
			},
			{
			    xtype: 'proxmoxHelpButton',
			    hidden: false,
			    baseCls: 'x-btn',
			    iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ',
			    listenToGlobalEvent: false,
			    onlineHelp: 'pve_documentation_index',
			    text: gettext('Documentation'),
			    margin: '0 5 0 0',
			},
			createVM,
			createCT,
			{
			    pack: 'end',
			    margin: '0 5 0 0',
			    id: 'userinfo',
			    xtype: 'button',
			    baseCls: 'x-btn',
			    style: {
				// proxmox dark grey p light grey as border
				backgroundColor: '#464d4d',
				borderColor: '#ABBABA',
			    },
			    iconCls: 'fa fa-user',
			    menu: [
				{
				    iconCls: 'fa fa-gear',
				    text: gettext('My Settings'),
				    handler: function() {
					var win = Ext.create('PVE.window.Settings');
					win.show();
				    },
				},
				{
				    text: gettext('Password'),
				    itemId: 'passworditem',
				    iconCls: 'fa fa-fw fa-key',
				    handler: function() {
					var win = Ext.create('Proxmox.window.PasswordEdit', {
					    userid: Proxmox.UserName,
					});
					win.show();
				    },
				},
				{
				    text: 'TFA',
				    itemId: 'tfaitem',
				    iconCls: 'fa fa-fw fa-lock',
				    handler: function(btn, event, rec) {
					Ext.state.Manager.getProvider().set('dctab', { value: 'tfa' }, true);
					me.selectById('root');
				    },
				},
				{
				    iconCls: 'fa fa-paint-brush',
				    text: gettext('Color Theme'),
				    handler: function() {
					Ext.create('Proxmox.window.ThemeEditWindow')
					    .show();
				    },
				},
				{
				    iconCls: 'fa fa-language',
				    text: gettext('Language'),
				    handler: function() {
					Ext.create('Proxmox.window.LanguageEditWindow')
					    .show();
				    },
				},
				'-',
				{
				    iconCls: 'fa fa-fw fa-sign-out',
				    text: gettext("Logout"),
				    handler: function() {
					PVE.data.ResourceStore.loadData([], false);
					me.showLogin();
					me.setContent(null);
					var rt = me.down('pveResourceTree');
					rt.setDatacenterText(undefined);
					rt.clearTree();
					// empty the stores of the StatusPanel child items
					var statusPanels = Ext.ComponentQuery.query('pveStatusPanel grid');
					Ext.Array.forEach(statusPanels, function(comp) {
					    if (comp.getStore()) {
						comp.getStore().loadData([], false);
					    }
					});
				    },
				},
			    ],
			},
		    ],
		},
		{
		    region: 'center',
		    stateful: true,
		    stateId: 'pvecenter',
		    minWidth: 100,
		    minHeight: 100,
		    id: 'content',
		    xtype: 'container',
		    layout: { type: 'card' },
		    border: false,
		    margin: '0 5 0 0',
		    items: [],
		},
		{
		    region: 'west',
		    stateful: true,
		    stateId: 'pvewest',
		    itemId: 'west',
		    xtype: 'container',
		    border: false,
		    layout: { type: 'vbox', align: 'stretch' },
		    margin: '0 0 0 5',
		    split: true,
		    width: 300,
		    items: [
			{
			    xtype: 'container',
			    layout: 'hbox',
			    padding: '0 0 5 0',
			    items: [
				selview,
				{
				    xtype: 'button',
				    cls: 'x-btn-default-toolbar-small',
				    iconCls: 'fa fa-fw fa-gear x-btn-icon-el-default-toolbar-small',
				    handler: () => {
					Ext.create('PVE.window.TreeSettingsEdit', {
					    autoShow: true,
					    apiCallDone: () => PVE.UIOptions.fireUIConfigChanged(),
					});
				    },
				},
			    ],
			},
			rtree,
		    ],
		    listeners: {
			resize: function(panel, width, height) {
			    var viewWidth = me.getSize().width;
			    if (width > viewWidth - 100) {
				panel.setWidth(viewWidth - 100);
			    }
			},
		    },
		},
		{
		    xtype: 'pveStatusPanel',
		    stateful: true,
		    stateId: 'pvesouth',
		    itemId: 'south',
		    region: 'south',
		    margin: '0 5 5 5',
		    title: gettext('Logs'),
		    collapsible: true,
		    header: false,
		    height: 200,
		    split: true,
		    listeners: {
			resize: function(panel, width, height) {
			    var viewHeight = me.getSize().height;
			    if (height > viewHeight - 150) {
				panel.setHeight(viewHeight - 150);
			    }
			},
		    },
		},
	    ],
	});
	me.callParent();
	me.updateUserInfo();
	// on resize, center all modal windows
	Ext.on('resize', function() {
	    let modalWindows = Ext.ComponentQuery.query('window[modal]');
	    if (modalWindows.length > 0) {
		modalWindows.forEach(win => win.alignTo(me, 'c-c'));
	    }
	});
    },
});