You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by da...@apache.org on 2022/08/29 11:42:11 UTC

[cloudstack] branch main updated: VM stats history visualization (#6401)

This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 9f7e0cc2d5 VM stats history visualization (#6401)
9f7e0cc2d5 is described below

commit 9f7e0cc2d57585168e9571393f9fa4113344b89f
Author: José Flauzino <jo...@gmail.com>
AuthorDate: Mon Aug 29 08:42:03 2022 -0300

    VM stats history visualization (#6401)
    
    * Add VM stats view feature
    
    * Add dynamic label to the submit button of the VM stats filtering modal
    
    * Improve CPU info message
    
    * Reduce identation
    
    * Remove missed line
    
    * Fix timestamp when 'Use local timezone' is selected
    
    * Fix bug when filtering for all data
    
    * Support for toggle 'Use local timezone' button
    
    Co-authored-by: joseflauzino <jo...@scclouds.com.br>
    Co-authored-by: Daniel Augusto Veronezi Salvador <38...@users.noreply.github.com>
---
 ui/package.json                                    |   4 +
 ui/public/locales/en.json                          |  38 ++
 ui/public/locales/pt_BR.json                       |  38 ++
 ui/src/components/view/StatsTab.vue                | 714 +++++++++++++++++++++
 .../view/chart/LineChart.vue}                      |  59 +-
 ui/src/components/view/stats/FilterStats.vue       | 165 +++++
 ui/src/components/view/stats/ResourceStatsInfo.vue |  90 +++
 .../style/{component => components}/dashboard.less |   0
 .../view/StatsTab.scss}                            |  46 +-
 ui/src/utils/plugins.js                            |  12 +
 ui/src/views/compute/InstanceTab.vue               |   5 +
 11 files changed, 1112 insertions(+), 59 deletions(-)

diff --git a/ui/package.json b/ui/package.json
index 42d33a402f..bc35e50b12 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -42,6 +42,8 @@
     "antd-theme-webpack-plugin": "^1.3.9",
     "axios": "^0.21.1",
     "babel-plugin-require-context-hook": "^1.0.0",
+    "chart.js": "^3.7.1",
+    "chartjs-adapter-moment": "^1.0.0",
     "core-js": "^3.21.1",
     "enquire.js": "^2.1.6",
     "js-cookie": "^2.2.1",
@@ -52,6 +54,8 @@
     "npm-check-updates": "^6.0.1",
     "nprogress": "^0.2.0",
     "vue": "^3.2.31",
+    "vue-chartjs": "^4.0.7",
+    "vue-clipboard2": "^0.3.1",
     "vue-cropper": "^1.0.2",
     "vue-i18n": "^9.1.6",
     "vue-loader": "^16.2.0",
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index cf7bbe1cbb..14b6580592 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -263,6 +263,7 @@
 "label.alerts": "Alerts",
 "label.algorithm": "Algorithm",
 "label.all": "All",
+"label.all.available.data": "All available data",
 "label.all.ipv6": "All IPv6",
 "label.all.zone": "All zones",
 "label.allocated": "Allocated",
@@ -437,6 +438,7 @@
 "label.copyid": "Copy ID",
 "label.cpu": "CPU",
 "label.cpu.sockets": "CPU sockets",
+"label.cpu.usage.info": "CPU usage information",
 "label.cpuallocated": "CPU allocated for VMs",
 "label.cpuallocatedghz": "CPU allocated",
 "label.cpulimit": "CPU limits",
@@ -579,6 +581,7 @@
 "label.disk.offerings": "Disk offerings",
 "label.disk.selection": "Disk selection",
 "label.disk.size": "Disk size",
+"label.disk.usage.info": "Disk usage information",
 "label.diskbytesreadrate": "Disk read rate (BPS)",
 "label.diskbyteswriterate": "Disk write rate (BPS)",
 "label.diskiopsmax": "Max IOPS",
@@ -662,6 +665,7 @@
 "label.enable.vpc.offering": "Enable VPC offering",
 "label.enable.vpn": "Enable remote access VPN",
 "label.end": "End",
+"label.end.date.and.time": "End date and time",
 "label.end.ip": "End IP",
 "label.end.reserved.system.ip": "End reserved system IP",
 "label.end.vlan": "End VLAN",
@@ -1059,7 +1063,12 @@
 "label.may.continue": "You may now continue.",
 "label.mb.memory": "MB memory",
 "label.memory": "Memory",
+"label.memory.free": "Memory free",
+"label.memory.maximum.mb": "Max memory (in MB)",
 "label.memory.mb": "Memory (in MB)",
+"label.memory.usage.info": "Memory usage information",
+"label.memory.total": "Memory total",
+"label.memory.used": "Memory used",
 "label.memoryallocated": "Memory allocated",
 "label.memoryallocatedgb": "Memory allocated",
 "label.memorylimit": "Memory limits (MiB)",
@@ -1081,6 +1090,8 @@
 "label.migrate.instance.specific.storages": "Migrate volume(s) of the instance to specific primary storages",
 "label.migrate.systemvm.to": "Migrate system VM to",
 "label.migrate.volume": "Migrate volume",
+"message.memory.usage.info.hypervisor.additionals": "The data shown may not reflect the actual memory usage if the VM does not have the additional hypervisor tools installed",
+"message.memory.usage.info.negative.value": "If the VM's memory usage cannot be obtained from the hypervisor, the lines for free memory in the raw data graph and memory usage in the percentage graph will be disabled",
 "message.migrate.volume.tooltip": "Volume can be migrated to any suitable storage pool. Admin has to choose the appropriate disk offering to replace, that supports the new storage pool",
 "label.migrate.with.storage": "Migrate with storage",
 "label.migrating": "Migrating",
@@ -1127,6 +1138,7 @@
 "label.network.permissions": "Network permissions",
 "label.network.selection": "Network selection",
 "label.network.service.providers": "Network service providers",
+"label.network.usage.info": "Network usage information",
 "label.networkdevicetype": "Type",
 "label.networkdomain": "Network domain",
 "label.networkid": "Network",
@@ -1182,6 +1194,8 @@
 "label.offerha": "Offer HA",
 "label.offeringtype": "Compute offering type",
 "label.ok": "OK",
+"label.only.end.date.and.time": "Only end date and time",
+"label.only.start.date.and.time": "Only start date and time",
 "label.open.documentation": "Open documentation",
 "label.open.url": "Open URL in browser",
 "label.opendaylight": "OpenDaylight",
@@ -1236,6 +1250,7 @@
 "label.pcidevice": "GPU",
 "label.per.account": "Per account",
 "label.per.zone": "Per zone",
+"label.percentage": "Percentage",
 "label.perfectforwardsecrecy": "Perfect forward secrecy",
 "label.perform.fresh.checks": "Perform fresh checks",
 "label.performfreshchecks": "Perform fresh checks",
@@ -1349,12 +1364,14 @@
 "label.rados.secret": "RADOS secret",
 "label.rados.user": "RADOS user",
 "label.ram": "RAM",
+"label.raw.data": "Raw data",
 "label.rbd": "RBD",
 "label.rbdid": "Cephx user",
 "label.rbdmonitor": "Ceph monitor",
 "label.rbdpool": "Ceph pool",
 "label.rbdsecret": "Cephx secret",
 "label.read": "Read",
+"label.read.and.write": "Read \u0026 Write",
 "label.read.io": "Read (IO)",
 "label.readonly": "Read-Only",
 "label.reason": "Reason",
@@ -1491,10 +1508,15 @@
 "label.securitygroups": "Security groups",
 "label.securitygroupsenabled": "Security groups enabled",
 "label.select": "Select",
+"label.see.more.info.cpu.usage": "See more info about CPU usage",
+"label.see.more.info.memory.usage": "See more info about memory usage",
+"label.see.more.info.network.usage": "See more info about network usage",
+"label.see.more.info.disk.usage": "See more info about disk usage",
 "label.select-view": "Select view",
 "label.select.a.zone": "Select a zone",
 "label.select.deployment.infrastructure": "Select deployment infrastructure",
 "label.select.network": "Select Network",
+"label.select.period": "Select period",
 "label.select.project": "Select project",
 "label.select.projects": "Select projects",
 "label.select.ps": "Select primary storage",
@@ -1571,6 +1593,7 @@
 "label.sshkeypairs": "SSH key pairs",
 "label.sslcertificates": "SSL certificates",
 "label.start": "Start",
+"label.start.date.and.time": "Start date and time",
 "label.start.ip": "Start IP",
 "label.start.lb.vm": "Start LB VM",
 "label.start.reserved.system.ip": "Start reserved system IP",
@@ -1803,6 +1826,9 @@
 "label.vm.password": "Password of the VM is",
 "label.vm.snapshots": "VM snapshots",
 "label.vm.start": "Start",
+"label.vm.stats.filter.period": "From <b>{startDate}</b> to <b>{endDate}</b>",
+"label.vm.stats.filter.starting": "Starting <b>{startDate}</b>.",
+"label.vm.stats.filter.up.to": "Up to <b>{endDate}</b>.",
 "label.vmfs": "VMFS",
 "label.vmipaddress": "VM IP address",
 "label.vmlimit": "Instance limits",
@@ -2008,6 +2034,7 @@
 "message.adding.netscaler.device": "Adding Netscaler device",
 "message.adding.netscaler.provider": "Adding Netscaler provider",
 "message.advanced.security.group": "Choose this if you wish to use security groups to provide guest VM isolation.",
+"message.alert.show.all.stats.data": "It can return a lot of data depending on the settings for collecting and retaining stats.",
 "message.allowed": "Allowed",
 "message.apply.success": "Applied successfully.",
 "message.assign.instance.another": "Please specify the account type, domain, account name and network (optional) of the new account. <br> If the default NIC of the VM is on a shared network, CloudStack will check if the network can be used by the new account if you do not specify one network. <br> If the default NIC of the VM is on a isolated network, and the new account has more one isolated networks, you should specify one.",
@@ -2077,6 +2104,8 @@
 "message.confirm.start.lb.vm": "Please confirm you want to start LB VM.",
 "message.confirm.sync.storage": "Please confirm you want to sync the storage pool",
 "message.confirm.upgrade.router.newer.template": "Please confirm that you want to upgrade router to use newer template.",
+"message.cpu.usage.info.multi.cpu": "The CPU usage percentage can exceed 100% if the VM has more than 1 vCPU or when CPU Cap is not enabled. This behavior happens according to how the hypervisor being used accounts the stats.",
+"message.cpu.usage.info.cpu.cap": "If a VM has only 1 vCPU and the CPU Cap on its compute offering is disabled, then the percentage of CPU usage can also go over 100%. This happens because the VM can use more CPUs than it has been allocated.",
 "message.create.compute.offering": "Compute offering created",
 "message.create.internallb": "Creating internal LB",
 "message.create.internallb.failed": "Failed to create internal LB.",
@@ -2150,6 +2179,8 @@
 "message.disable.vpn.processing": "Disabling VPN...",
 "message.discovering.feature": "Discovering features, please wait...",
 "message.disk.offering.created": "Disk offering created:",
+"message.disk.usage.info.data.points": "Each data point represents the difference in read/write data since the last data point.",
+"message.disk.usage.info.sum.of.disks": "The disk usage shown is made up of the sum of read/write data from all the disks in the VM.",
 "message.download.volume": "Please click the link to download the volume:<p><a href=\"#\">00000</a>",
 "message.download.volume.confirm": "Please confirm that you want to download this volume.",
 "message.edit.acl.failed": "Failed to edit ACL rule",
@@ -2189,6 +2220,7 @@
 "message.error.discovering.feature": "Exception caught while discovering features.",
 "message.error.display.text": "Please enter display text.",
 "message.error.enable.saml": "Unable to find users IDs to enable SAML single sign on, kindly enable it manually.",
+"message.error.end.date.and.time": "Please select the end date and time!",
 "message.error.endip": "Please enter end IP.",
 "message.error.gateway": "Please enter gateway.",
 "message.error.host.name": "Please enter host name.",
@@ -2254,6 +2286,7 @@
 "message.error.smb.username": "Please enter SMB username.",
 "message.error.specify.sticky.name": "Please specify a sticky name.",
 "message.error.sr.namelabel": "Please enter SR Name-Label.",
+"message.error.start.date.and.time": "Please select the start date and time!",
 "message.error.startip": "Please enter start IP.",
 "message.error.storage.tags": "Please enter storage tags.",
 "message.error.target.iqn": "Please enter target IQN.",
@@ -2362,6 +2395,9 @@
 "message.network.removenic": "Please confirm that want to remove this NIC, which will also remove the associated network from the VM.",
 "message.network.secondaryip": "Please confirm that you would like to acquire a new secondary IP for this NIC. \n NOTE: You need to manually configure the newly-acquired secondary IP inside the virtual machine.",
 "message.network.updateip": "Please confirm that you would like to change the IP address for this NIC on VM.",
+"message.network.usage.info.data.points": "Each data point represents the difference in data traffic since the last data point.",
+"message.network.usage.info.sum.of.vnics": "The network usage shown is made up of the sum of data traffic from all the vNICs in the VM.",
+"message.no.data.to.show.for.period": "No data to show for the selected period.",
 "message.no.description": "No description entered.",
 "message.offering.internet.protocol.warning": "WARNING: IPv6 supported networks use static routing and will require upstream routes to be configured manually.",
 "message.offering.ipv6.warning": "Please refer documentation for creating IPv6 enabled network/VPC offering <a href='http://docs.cloudstack.apache.org/en/latest/plugins/ipv6.html#isolated-network-and-vpc-tier'>IPv6 support in CloudStack - Isolated networks and VPC tiers</a>",
@@ -2423,9 +2459,11 @@
 "message.select.affinity.groups": "Please select any affinity groups you want this VM to belong to:",
 "message.select.destination.image.stores": "Please select Image Store(s) to which data is to be migrated to",
 "message.select.disk.offering": "Please select a disk offering for disk",
+"message.select.end.date.and.time": "Select an end date & time.",
 "message.select.migration.policy": "Please select a migration policy.",
 "message.select.nic.network": "Please select a network for NIC",
 "message.select.security.groups": "Please select security group(s) for your new VM.",
+"message.select.start.date.and.time": "Select a start date & time.",
 "message.select.zone.description": "Select type of zone basic/advanced.",
 "message.select.zone.hint": "This is the type of zone deployment that you want to use. Basic zone: provides a single network where each VM instance is assigned an IP directly from the network. Guest isolation can be provided through layer-3 means such as security groups (IP address source filtering). Advanced zone: For more sophisticated network topologies. This network model provides the most flexibility in defining guest networks and providing custom network offerings such as firewall, [...]
 "message.server.description": "NFS, iSCSI, or PreSetup: IP address or DNS name of the storage device. VMWare PreSetup: IP address or DNS name of the vCenter server. Linstor: http(s) url of the linstor-controller.",
diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json
index 1e861397b1..88972c5da1 100644
--- a/ui/public/locales/pt_BR.json
+++ b/ui/public/locales/pt_BR.json
@@ -249,6 +249,7 @@
 "label.alerts": "Alertas",
 "label.algorithm": "Algoritmo",
 "label.all": "Todos",
+"label.all.available.data": "Todos os dados dispon\u00edveis",
 "label.all.zone": "Todas as zonas",
 "label.allocated": "Alocado",
 "label.allocationstate": "Estado da aloca\u00e7\u00e3o",
@@ -405,6 +406,7 @@
 "label.copyid": "Copiar ID",
 "label.cpu": "CPU",
 "label.cpu.sockets": "Sockets da CPU",
+"label.cpu.usage.info": "Informa\u00e7\u00f5es sobre o uso de CPU",
 "label.cpuallocated": "CPU alocada por VMs",
 "label.cpuallocatedghz": "Alocado",
 "label.cpulimit": "Limite de CPU",
@@ -541,6 +543,7 @@
 "label.disk.offerings": "Ofertas de disco",
 "label.disk.selection": "Sele\u00e7\u00e3o de disco",
 "label.disk.size": "Tamanho do disco",
+"label.disk.usage.info": "Informa\u00e7\u00f5es sobre o uso de disco",
 "label.diskbytesreadrate": "Taxa de leitura de bytes do disco (BPS)",
 "label.diskbyteswriterate": "Taxa de escrita de bytes no disco (BPS)",
 "label.diskiopsmax": "M\u00e1x IOPS",
@@ -617,6 +620,7 @@
 "label.enable.vpc.offering": "Habilitar oferta VPC",
 "label.enable.vpn": "Habilitar VPN",
 "label.end": "Fim",
+"label.end.date.and.time": "Data e hor\u00e1rio final",
 "label.end.ip": "IP final",
 "label.end.reserved.system.ip": "Fim dos IPs reservados para o sistema",
 "label.end.vlan": "VLAN final",
@@ -678,6 +682,7 @@
 "label.forceencap": "For\u00e7ar encapsulamento UDP de pacotes ESP",
 "label.forgedtransmits": "Transmiss\u00f5es forjadas",
 "label.format": "Formato",
+"label.free": "Livre",
 "label.friday": "Sexta-feira",
 "label.from": "de",
 "label.from.lb": "de LB",
@@ -989,6 +994,10 @@
 "label.may.continue": "Voc\u00ea pode continuar agora",
 "label.mb.memory": "MB de mem\u00f3ria",
 "label.memory": "Mem\u00f3ria (em MB)",
+"label.memory.free": "Mem\u00f3ria livre",
+"label.memory.total": "Mem\u00f3ria total",
+"label.memory.usage.info": "Informa\u00e7\u00f5es sobre o uso de mem\u00f3ria",
+"label.memory.used": "Mem\u00f3ria usada",
 "label.memory.mb": "Mem\u00f3ria (em MB)",
 "label.memoryallocated": "Mem\u00f3ria alocada",
 "label.memoryallocatedgb": "Alocado",
@@ -1051,6 +1060,7 @@
 "label.network.offerings": "Oferta de rede",
 "label.network.selection": "Sele\u00e7\u00e3o da rede",
 "label.network.service.providers": "Provedores de servi\u00e7os de rede",
+"label.network.usage.info": "Informa\u00e7\u00f5es sobre o uso de rede",
 "label.networkdevicetype": "Tipo",
 "label.networkdomain": "Dom\u00ednio de rede",
 "label.networkid": "Rede",
@@ -1105,6 +1115,8 @@
 "label.offerha": "Oferta HA",
 "label.offeringtype": "Tipo da oferta de computa\u00e7\u00e3o",
 "label.ok": "OK",
+"label.only.end.date.and.time": "Apenas data e hor\u00e1rio final",
+"label.only.start.date.and.time": "Apenas data e hor\u00e1rio inicial",
 "label.open.documentation": "Abrir documenta\u00e7\u00e3o",
 "label.open.url": "Abrir URL no navegador",
 "label.opendaylight": "OpenDaylight",
@@ -1157,6 +1169,7 @@
 "label.pcidevice": "GPU",
 "label.per.account": "Por conta",
 "label.per.zone": "Por zona",
+"label.percentage": "Porcentagem",
 "label.perfectforwardsecrecy": "Perfect Forward secrecy",
 "label.perform.fresh.checks": "Realizar novas verifica\u00e7\u00f5es",
 "label.permission": "Permiss\u00e3o",
@@ -1267,12 +1280,14 @@
 "label.rados.secret": "Segredo RADOS",
 "label.rados.user": "Usu\u00e1rio RADOS",
 "label.ram": "RAM",
+"label.raw.data": "Dados Brutos",
 "label.rbd": "RDB",
 "label.rbdid": "Usu\u00e1rio Ceph",
 "label.rbdmonitor": "Monitor Ceph",
 "label.rbdpool": "Pool Ceph",
 "label.rbdsecret": "Segredo Cephx",
 "label.read": "Leitura",
+"label.read.and.write": "Leitura e escrita",
 "label.read.io": "Leitura (IO)",
 "label.readonly": "Apenas leitura",
 "label.reason": "Motivo",
@@ -1404,10 +1419,15 @@
 "label.securitygroup": "Grupo de seguran\u00e7a",
 "label.securitygroupenabled": "Grupo de seguran\u00e7a ativado",
 "label.securitygroups": "Grupos de seguran\u00e7a",
+"label.see.more.info.cpu.usage": "Ver mais informa\u00e7\u00f5es sobre o uso de CPU",
+"label.see.more.info.memory.usage": "Ver mais informa\u00e7\u00f5es sobre o uso de mem\u00f3ria",
+"label.see.more.info.network.usage": "Ver mais informa\u00e7\u00f5es sobre o uso de rede",
+"label.see.more.info.disk.usage": "Ver mais informa\u00e7\u00f5es sobre o uso de disco",
 "label.select": "Selecionar",
 "label.select-view": "Selecionar visualiza\u00e7\u00e3o",
 "label.select.a.zone": "Selecione uma zona",
 "label.select.deployment.infrastructure": "Selecione uma infraestrutura de implanta\u00e7\u00e3o",
+"label.select.period": "Selecionar per\u00edodo",
 "label.select.project": "Selecionar projeto",
 "label.select.projects": "Selecionar projetos",
 "label.select.tier": "Selecionar camada",
@@ -1480,6 +1500,7 @@
 "label.sshkeypairs": "Par de chaves SSH",
 "label.sslcertificates": "Certificados SSL",
 "label.start": "Iniciar",
+"label.start.date.and.time": "Data e hor\u00e1rio inicial",
 "label.start.ip": "IP do in\u00edcio",
 "label.start.lb.vm": "Iniciar VM LB",
 "label.start.reserved.system.ip": "In\u00edcio dos IPs reservados para o sistema",
@@ -1689,6 +1710,9 @@
 "label.vm.password": "Senha para a VM \u00e9",
 "label.vm.snapshots": "Snapshot de VM",
 "label.vm.start": "In\u00edcio",
+"label.vm.stats.filter.period": "De <b>{startDate}</b> at\u00e9 <b>{endDate}</b>.",
+"label.vm.stats.filter.starting": "A partir de <b>{startDate}</b>.",
+"label.vm.stats.filter.up.to": "At\u00e9 <b>{endDate}</b>.",
 "label.vmfs": "VMFS",
 "label.vmipaddress": "Endere\u00e7o IP da VM",
 "label.vmlimit": "Limites de inst\u00e2ncias",
@@ -1882,6 +1906,7 @@
 "message.adding.netscaler.device": "Adicionando dispositivo Nescaler",
 "message.adding.netscaler.provider": "Adicionando Netscaler provider",
 "message.advanced.security.group": "Escolha esta op\u00e7\u00e3o se desejar utilizar grupos de seguran\u00e7a para isolamento das VMs guest.",
+"message.alert.show.all.stats.data": "Isso pode retornar muitos dados dependendo das configura\u00e7\u00f5es de coleta e reten\u00e7\u00e3o de estat\u00edsticas.",
 "message.apply.success": "Aplicado com sucesso",
 "message.assign.instance.another": "Favor especificar o tipo de conta, dom\u00ednio, nome da conta e rede (opcional) da nova conta. <br> Se o NIC padr\u00e3o da VM estiver em uma rede compartilhada, o CloudStack verificar\u00e1 se a rede pode ser usada pela nova conta se voc\u00ea n\u00e3o especificar uma rede. <br> Se o NIC padr\u00e3o da VM estiver em uma rede isolada, e a nova conta tiver mais uma rede isolada, voc\u00ea deve especificar uma.",
 "message.assign.vm.failed": "Falha na designa\u00e7\u00e3o de VM",
@@ -1945,6 +1970,8 @@
 "message.confirm.start.lb.vm": "Confirme que voc\u00ea deseja iniciar esta LB VM",
 "message.confirm.sync.storage": "Por favor, confirme que voc\u00ea gostaria de sincronizar o pool de armazenamento",
 "message.confirm.upgrade.router.newer.template": "Por favor confirme que voc\u00ea deseja atualizar o roteador para usar o template mais recente.",
+"message.cpu.usage.info.multi.cpu": "O percentual de uso de CPU pode passar de 100% quando uma VM possui mais de 1 vCPU. Isso pode acontecer dependendo da forma que o virtualizador contabiliza as estat\u00edsticas.",
+"message.cpu.usage.info.cpu.cap": "Se uma VM possui apenas 1 vCPU e o CPU Cap da oferta de computa\u00e7\u00e3o estiver desabilitado, o percentual de uso de CPU tamb\u00e9m pode passar de 100%. Isso porque a VM pode utilizar mais CPUs do que lhe foi atribu\u00eddo",
 "message.create.compute.offering": "Oferta de computa\u00e7\u00e3o criada",
 "message.create.internallb": "Criando LB interno",
 "message.create.internallb.failed": "Falha ao criar LB interno",
@@ -2016,6 +2043,8 @@
 "message.disable.vpn.processing": "Desabilitando VPN...",
 "message.discovering.feature": "Descobrindo funcionalidades, por favor aguarde...",
 "message.disk.offering.created": "Oferta de disco criada:",
+"message.disk.usage.info.data.points": "Cada ponto no gr\u00e1fico representa a diferen\u00e7a de dados lidos/escritos desde a \u00faltima coleta de estat\u00edstica realizada (o ponto anterior)",
+"message.disk.usage.info.sum.of.disks": "O uso de disco apresentado \u00e9 composto pela soma de dados lidos/escritos por todos os discos da VM",
 "message.download.volume": "Clique <a href=\"#\">00000</a> para baixar o disco",
 "message.download.volume.confirm": "Por favor confirme que voc\u00ea quer baixar este volume",
 "message.edit.acl.failed": "Falha ao editar regra ACL",
@@ -2052,6 +2081,7 @@
 "message.error.discovering.feature": "Exce\u00e7\u00e3o lan\u00e7ada durante a descoberta de funcionalidades",
 "message.error.display.text": "Por favor, insira o texto de exibi\u00e7\u00e3o",
 "message.error.enable.saml": "Incapaz de encontrar IDs de usu\u00e1rios para ativar o SAML Single Sign On, por favor, ative-o manualmente.",
+"message.error.end.date.and.time": "Por favor, selecione a data e hor\u00e1rio final.",
 "message.error.endip": "Por favor, insira o IP final",
 "message.error.gateway": "Por favor, insira o gateway",
 "message.error.host.name": "Por favor, insira o nome do host",
@@ -2113,6 +2143,7 @@
 "message.error.smb.username": "Por favor, insira o usu\u00e1rio do SMB",
 "message.error.specify.sticky.name": "Por favor, especifique o nome sticky",
 "message.error.sr.namelabel": "Por favor, insira o nome-R\u00f3tulo SR",
+"message.error.start.date.and.time": "Por favor, selecione a data e hor\u00e1rio inicial.",
 "message.error.startip": "Por favor, insira o IP de \u00cdnicio",
 "message.error.storage.tags": "Por favor, insira as tags de armazenamento",
 "message.error.target.iqn": "Por favor, insira o Target IQN",
@@ -2181,6 +2212,8 @@
 "message.load.host.failed": "Falha ao carregar os hosts",
 "message.lock.account": "Confirme se voc\u00ea deseja bloquear esta conta. Bloqueando a conta, todos os usu\u00e1rios desta conta n\u00e3o estar\u00e3o mais habilitados a gerenciar os recursos na nuvem. Os recursos existentes (cloud server) ainda poder\u00e3o ser acessados.",
 "message.login.failed": "Falha no login",
+"message.memory.usage.info.hypervisor.additionals": "Os dados apresentados podem n\u00e3o refletir o real uso de mem\u00f3ria se a VM n\u00e3o possuir as ferramentas adicionais do virtualizador instaladas",
+"message.memory.usage.info.negative.value": "Se n\u00e3o for poss\u00edvel obter do hypervisor o uso de mem\u00f3ria da VM, ser\u00e3o desabilitadas as linhas de mem\u00f3ria livre do gr\u00e1fico de dados brutos e de uso de mem\u00f3ria no gr\u00e1fico de percentual",
 "message.migrate.instance.to.host": "Por favor confirme que voc\u00ea deseja migrar a inst\u00e2ncia para outro host.",
 "message.migrate.instance.to.ps": "Por favor confirme que voc\u00ea deseja migrar a inst\u00e2ncia para outro armazenamento prim\u00e1rio.",
 "message.migrate.router.confirm": "Por favor confirme o host que voc\u00ea deseja migrar o roteador para:",
@@ -2210,6 +2243,9 @@
 "message.network.removenic": "Por favor, confirme que deseja remover esta interface de rede, esta a\u00e7\u00e3o tamb\u00e9m ir\u00e1 remover a rede associada \u00e0 VM.",
 "message.network.secondaryip": "Por favor, confirme que voc\u00ea gostaria de adquirir um novo IP secund\u00e1rio para este NIC. \n NOTA: Voc\u00ea precisa configurar manualmente o IP secund\u00e1rio rec\u00e9m-adquirido dentro da m\u00e1quina virtual.",
 "message.network.updateip": "Por favor, confirme que voc\u00ea gostaria de mudar o endere\u00e7o IP da NIC em VM.",
+"message.network.usage.info.data.points": "Cada ponto no gr\u00e1fico representa a diferen\u00e7a de dados trafegados desde a \u00faltima coleta de estat\u00edstica realizada (o ponto anterior)",
+"message.network.usage.info.sum.of.vnics": "O uso de rede apresentado \u00e9 composto pela soma de dados trafegados por todas as vNICs da VM",
+"message.no.data.to.show.for.period": "Nenhum dado para mostrar no per\u00edodo selecionado.",
 "message.no.description": "Nenhuma descri\u00e7\u00e3o inserida.",
 "message.ovf.configurations": "H\u00e1 propriedades OVF dispon\u00edveis para a personaliza\u00e7\u00e3o do aparelho selecionado. Por favor, edite os valores de forma apropriada.As ofertas incompat\u00edveis de computa\u00e7\u00e3o ser\u00e3o desativadas.",
 "message.path.description": "NFS: caminho exportado do servidor. VMFS: /datacenter name/datastore name. SharedMountPoint: caminho onde o armazenamento prim\u00e1rio \u00e9 montado, tal como /mnt/primary",
@@ -2267,9 +2303,11 @@
 "message.select.affinity.groups": "Por favor, selecione quaisquer grupos de afinidade que voc\u00ea deseja que esta VM perten\u00e7a:",
 "message.select.destination.image.stores": "Por favor, selecione o(s) armazenamento(s) de imagem(ns) para os quais os dados devem ser migrados",
 "message.select.disk.offering": "Por favor, selecione uma oferta de disco para o disco",
+"message.select.end.date.and.time": "Selecione uma data e hor\u00e1rio final.",
 "message.select.migration.policy": "Por favor, selecione uma pol\u00edtica de migra\u00e7\u00e3o",
 "message.select.nic.network": "Por favor, selecione uma rede para o NIC",
 "message.select.security.groups": "Por favor selecione o(s) grupo(s) de seguran\u00e7a para sua nova VM",
+"message.select.start.date.and.time": "Selecione uma data e hor\u00e1rio inicial.",
 "message.select.zone.description": "Selecione o tipo de zona b\u00e1sica/avan\u00e7ada",
 "message.select.zone.hint": "Este \u00e9 o tipo de implanta\u00e7\u00e3o de zona que voc\u00ea deseja utilizar. Zona b\u00e1sica: fornece uma \u00fanica rede na qual para cada inst\u00e2ncia de VM \u00e9 atribu\u00eddo um IP diretamente da rede. O isolamento do guest pode ser fornecido atrav\u00e9s de meios da camada 3, tais como grupos de seguran\u00e7a (filtragem de source IP). Zona avan\u00e7ada: para topologias de rede mais sofisticadas. este modelo de rede oferece  maior flexibilida [...]
 "message.server.description": "NFS, iSCSI, ou PreSetup: endere\u00e7o IP ou nome DNS do dispositivo de armazenamento. VMWare PreSetup: endere\u00e7o IP ou nome DNS do servidor vCenter. Linstor: URL HTTP(S) do linstor-controller.",
diff --git a/ui/src/components/view/StatsTab.vue b/ui/src/components/view/StatsTab.vue
new file mode 100644
index 0000000000..60d5793f8b
--- /dev/null
+++ b/ui/src/components/view/StatsTab.vue
@@ -0,0 +1,714 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div class="container">
+    <a-modal
+      v-model:visible="showFilterStatsModal"
+      :title="$t('label.select.period')"
+      :maskClosable="false"
+      :footer="null">
+      <filter-stats
+        :startDateProp="startDate"
+        :endDateProp="endDate"
+        @closeAction="closeAction"
+        @onSubmit="handleSubmit"/>
+    </a-modal>
+    <a-modal
+      v-model:visible="showResourceInfoModal"
+      :title="resourceInfoModalTitle"
+      :footer="null">
+      <resource-stats-info :resourceType="resourceTypeToShowInfo" :key="resourceTypeToShowInfo"/>
+    </a-modal>
+    <a-row class="chart-row">
+      <a-col>
+        <span class="ant-tag">
+          <a-button @click="openFilter()">
+            <FilterOutlined/>
+          </a-button>
+          <span v-html="formatedPeriod"></span>
+        </span>
+      </a-col>
+    </a-row>
+    <div v-if="loaded">
+      <div v-if="chartLabels.length > 0">
+        <a-row class="chart-row">
+          <a-col>
+            <strong>CPU</strong>
+            <InfoCircleOutlined class="info-icon" :title="$t('label.see.more.info.cpu.usage')" @click="onClickShowResourceInfoModal('CPU')"/>
+            <line-chart
+              :chartData="prepareData(resourceUsageHistory.cpu)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.cpu, 100, 10), '%')"
+              :width="1024"
+              :height="250"
+            />
+          </a-col>
+        </a-row>
+        <a-row class="chart-row">
+          <a-col>
+            <strong>{{ $t('label.memory') }}</strong>
+            <InfoCircleOutlined class="info-icon" :title="$t('label.see.more.info.memory.usage')" @click="onClickShowResourceInfoModal('MEM')"/>
+            <a-select class="chart-type-select" v-model:value="selectedMemoryChartType">
+              <a-select-option v-for="(type, typeIndex) in memoryChartTypes" :key="typeIndex">
+                {{ type }}
+              </a-select-option>
+            </a-select>
+            <a-select v-model:value="selectedMemoryUsageType">
+              <a-select-option v-for="(type, typeIndex) in memoryUsageTypes" :key="typeIndex">
+                {{ type }}
+              </a-select-option>
+            </a-select>
+            <a-select v-model:value="selectedMemoryUnitOfMeasurement" v-if="selectedMemoryChartType === 0">
+              <a-select-option v-for="unit in memoryUnitsOfMeasurement" :key="unit">
+                {{ unit }}
+              </a-select-option>
+            </a-select>
+            <line-chart
+              v-if="selectedMemoryChartType === 0 && selectedMemoryUsageType === 0 && selectedMemoryUnitOfMeasurement === 'MB'"
+              :chartData="prepareData(resourceUsageHistory.memory.rawData.used.inMB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.memory.rawData.used.inMB, 10, 100), ' MB')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedMemoryChartType === 0 && selectedMemoryUsageType === 0 && selectedMemoryUnitOfMeasurement === 'GB'"
+              :chartData="prepareData(resourceUsageHistory.memory.rawData.used.inGB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.memory.rawData.used.inGB, 1, 1), ' GB')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedMemoryChartType === 0 && selectedMemoryUsageType === 1 && selectedMemoryUnitOfMeasurement === 'MB'"
+              :chartData="prepareData(resourceUsageHistory.memory.rawData.free.inMB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.memory.rawData.free.inMB, 10, 100), ' MB')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedMemoryChartType === 0 && selectedMemoryUsageType === 1 && selectedMemoryUnitOfMeasurement === 'GB'"
+              :chartData="prepareData(resourceUsageHistory.memory.rawData.free.inGB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.memory.rawData.free.inGB, 1, 1), ' GB')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedMemoryChartType === 1 && selectedMemoryUsageType === 0"
+              :chartData="prepareData(resourceUsageHistory.memory.percentage.used)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.memory.percentage.used, 100, 10), '%')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedMemoryChartType === 1 && selectedMemoryUsageType === 1"
+              :chartData="prepareData(resourceUsageHistory.memory.percentage.free)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.memory.percentage.free, 100, 10), '%')"
+              :width="1024"
+              :height="250"
+            />
+          </a-col>
+        </a-row>
+        <a-row class="chart-row">
+          <a-col>
+            <strong>{{ $t('label.network') }}</strong>
+            <InfoCircleOutlined class="info-icon" :title="$t('label.see.more.info.network.usage')" @click="onClickShowResourceInfoModal('NET')"/>
+            <a-select v-model:value="selectedNetworkUnitOfMeasurement">
+              <a-select-option v-for="unit in networkUnitsOfMeasurement" :key="unit">
+                {{ unit }}
+              </a-select-option>
+            </a-select>
+            <line-chart
+              v-if="selectedNetworkUnitOfMeasurement === 'KiB'"
+              :chartData="prepareData(resourceUsageHistory.network.inKiB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.network.inKiB, 100, 100), ' KiB')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedNetworkUnitOfMeasurement === 'MiB'"
+              :chartData="prepareData(resourceUsageHistory.network.inMiB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.network.inMiB, 100, 100), ' MiB')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedNetworkUnitOfMeasurement === 'GiB'"
+              :chartData="prepareData(resourceUsageHistory.network.inGiB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.network.inGiB, 1, 1), ' GiB')"
+              :width="1024"
+              :height="250"
+            />
+          </a-col>
+        </a-row>
+        <a-row class="chart-row">
+          <a-col>
+            <strong>{{ $t('label.disk') }}</strong>
+            <InfoCircleOutlined class="info-icon" :title="$t('label.see.more.info.disk.usage')" @click="onClickShowResourceInfoModal('DISK')"/>
+            <a-select class="chart-type-select" v-model:value="selectedDiskChartType">
+              <a-select-option v-for="(type, typeIndex) in diskChartTypes" :key="typeIndex">
+                {{ type }}
+              </a-select-option>
+            </a-select>
+            <a-select
+              v-if="selectedDiskChartType === 1"
+              v-model:value="selectedDiskUnitOfMeasurement">
+              <a-select-option v-for="unit in diskUnitsOfMeasurement" :key="unit">
+                {{ unit }}
+              </a-select-option>
+            </a-select>
+            <line-chart
+              v-if="selectedDiskChartType === 0"
+              :chartData="prepareData(resourceUsageHistory.disk.iops)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.disk.iops, 100, 100), ' IOPS')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedDiskChartType === 1 && selectedDiskUnitOfMeasurement === 'KiB'"
+              :chartData="prepareData(resourceUsageHistory.disk.readAndWrite.inKiB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.disk.readAndWrite.inKiB, 100, 100), ' KiB')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedDiskChartType === 1 && selectedDiskUnitOfMeasurement === 'MiB'"
+              :chartData="prepareData(resourceUsageHistory.disk.readAndWrite.inMiB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.disk.readAndWrite.inMiB, 10, 10), ' MiB')"
+              :width="1024"
+              :height="250"
+            />
+            <line-chart
+              v-if="selectedDiskChartType === 1 && selectedDiskUnitOfMeasurement === 'GiB'"
+              :chartData="prepareData(resourceUsageHistory.disk.readAndWrite.inGiB)"
+              :chartOptions="getChartOptions(calculateMaxYAxisAndStepSize(resourceUsageHistory.disk.readAndWrite.inGiB, 1, 1), ' GiB')"
+              :width="1024"
+              :height="250"
+            />
+          </a-col>
+        </a-row>
+      </div>
+      <div v-else>
+        <a-alert :message="$t('message.no.data.to.show.for.period')" banner />
+      </div>
+    </div>
+    <a-spin v-else></a-spin>
+  </div>
+</template>
+<script>
+import { api } from '@/api'
+import moment from 'moment'
+import 'chartjs-adapter-moment'
+import FilterStats from './stats/FilterStats'
+import ResourceStatsInfo from './stats/ResourceStatsInfo'
+import LineChart from './chart/LineChart'
+
+export default {
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  components: {
+    FilterStats,
+    ResourceStatsInfo,
+    LineChart
+  },
+  data () {
+    return {
+      resourceTypeToShowInfo: null,
+      showResourceInfoModal: false,
+      resourceInfoModalTitle: null,
+      loaded: false,
+      showCpuInfo: false,
+      showFilterStatsModal: false,
+      endDate: this.getEndDate(),
+      startDate: this.getStartDate(),
+      formatedPeriod: null,
+      selectedMemoryChartType: 0,
+      selectedMemoryUsageType: 0,
+      memoryChartTypes: [this.$t('label.raw.data'), this.$t('label.percentage')],
+      memoryUsageTypes: [this.$t('label.used'), this.$t('label.free')],
+      selectedMemoryUnitOfMeasurement: 'GB',
+      memoryUnitsOfMeasurement: ['MB', 'GB'],
+      selectedNetworkUnitOfMeasurement: 'MiB',
+      networkUnitsOfMeasurement: ['KiB', 'MiB', 'GiB'],
+      selectedDiskChartType: 0,
+      diskChartTypes: ['IOPS', this.$t('label.read.and.write')],
+      selectedDiskUnitOfMeasurement: 'KiB',
+      diskUnitsOfMeasurement: ['KiB', 'MiB', 'GiB'],
+      chartLabels: [],
+      resourceUsageHistory: {
+        cpu: [],
+        memory: {
+          percentage: {
+            free: [],
+            used: []
+          },
+          rawData: {
+            free: {
+              inMB: [],
+              inGB: []
+            },
+            used: {
+              inMB: [],
+              inGB: []
+            }
+          }
+        },
+        network: {
+          inKiB: [],
+          inMiB: [],
+          inGiB: []
+        },
+        disk: {
+          iops: [],
+          readAndWrite: {
+            inKiB: [],
+            inMiB: [],
+            inGiB: []
+          }
+        }
+      }
+    }
+  },
+  mounted () {
+    this.fetchData()
+  },
+  computed: {
+    usebrowsertimezone: function () {
+      return this.$store.getters.usebrowsertimezone
+    }
+  },
+  watch: {
+    resource: function (newItem) {
+      if (!newItem || !newItem.id) {
+        return
+      }
+      this.fetchData()
+    },
+    usebrowsertimezone: function () {
+      if (this.startDate) {
+        this.startDate = this.onToggleUseBrowserTimezone(new Date(this.startDate))
+      }
+      if (this.endDate) {
+        this.endDate = this.onToggleUseBrowserTimezone(new Date(this.endDate))
+      }
+      this.fetchData()
+    }
+  },
+  methods: {
+    openFilter () {
+      this.showFilterStatsModal = true
+    },
+    onClickShowResourceInfoModal (resource) {
+      switch (resource) {
+        case 'CPU':
+          this.resourceInfoModalTitle = this.$t('label.cpu.usage.info')
+          break
+        case 'MEM':
+          this.resourceInfoModalTitle = this.$t('label.memory.usage.info')
+          break
+        case 'NET':
+          this.resourceInfoModalTitle = this.$t('label.network.usage.info')
+          break
+        case 'DISK':
+          this.resourceInfoModalTitle = this.$t('label.disk.usage.info')
+          break
+      }
+      this.resourceTypeToShowInfo = resource
+      this.showResourceInfoModal = true
+    },
+    handleSubmit (values) {
+      if (values.startDate) {
+        this.startDate = new Date(values.startDate)
+      } else {
+        this.startDate = null
+      }
+      if (values.endDate) {
+        this.endDate = new Date(values.endDate)
+      } else {
+        this.endDate = null
+      }
+      this.showFilterStatsModal = false
+      this.fetchData()
+    },
+    closeAction () {
+      this.showFilterStatsModal = false
+    },
+    getStartDate () {
+      var now = new Date()
+      if (!this.$store.getters.usebrowsertimezone) {
+        var dateInUTC = new Date(now.getTime() + now.getTimezoneOffset() * 60000)
+        return dateInUTC.setHours(dateInUTC.getHours() - 1)
+      }
+      now.setHours(now.getHours() - 1)
+      return now
+    },
+    getEndDate () {
+      var now = new Date()
+      if (this.$store.getters.usebrowsertimezone) {
+        return now
+      }
+      return new Date(now.getTime() + now.getTimezoneOffset() * 60000)
+    },
+    onToggleUseBrowserTimezone (date) {
+      if (this.$store.getters.usebrowsertimezone) {
+        return this.$toLocalDate(date)
+      }
+      return new Date(date.getTime() + date.getTimezoneOffset() * 60000)
+    },
+    convertAndFormatDateAppropriately (date) {
+      if (this.$store.getters.usebrowsertimezone) {
+        var dateInUTC = new Date(date).toISOString().split('T')
+        return dateInUTC[0] + ' ' + dateInUTC[1].split('-')[0].split('.')[0]
+      }
+      return moment(date).format('YYYY-MM-DD HH:mm:ss')
+    },
+    fetchData () {
+      this.loaded = false
+      this.showResourceInfoModal = false
+      this.formatPeriod()
+      var params = { id: this.resource.id }
+      if (this.startDate) {
+        params.startDate = this.convertAndFormatDateAppropriately(this.startDate)
+      }
+      if (this.endDate) {
+        params.endDate = this.convertAndFormatDateAppropriately(this.endDate)
+      }
+      api('listVirtualMachinesUsageHistory', params).then(response => {
+        this.handleStatsResponse(response)
+      }).catch(error => {
+        this.$notifyError(error)
+      })
+    },
+    formatPeriod () {
+      var formatedStartDate = null
+      var formatedEndDate = null
+      if (this.startDate) {
+        formatedStartDate = moment(this.startDate).format('MMM DD, YYYY') + ' at ' + moment(this.startDate).format('HH:mm:ss')
+      }
+      if (this.endDate) {
+        formatedEndDate = moment(this.endDate).format('MMM DD, YYYY') + ' at ' + moment(this.endDate).format('HH:mm:ss')
+      }
+      if (formatedStartDate && formatedEndDate) {
+        this.formatedPeriod = ' ' + this.$t('label.vm.stats.filter.period', { startDate: formatedStartDate, endDate: formatedEndDate })
+      } else if (formatedStartDate && !formatedEndDate) {
+        this.formatedPeriod = ' ' + this.$t('label.vm.stats.filter.starting', { startDate: formatedStartDate })
+      } else if (!formatedStartDate && formatedEndDate) {
+        this.formatedPeriod = ' ' + this.$t('label.vm.stats.filter.up.to', { endDate: formatedEndDate })
+      } else {
+        this.formatedPeriod = ' <b>' + this.$t('label.all.available.data') + '</b>'
+      }
+    },
+    handleStatsResponse (responseData) {
+      this.resetData()
+      const vm = responseData.listvirtualmachinesusagehistoryresponse.virtualmachine
+
+      const chartPointRadius = this.getChartPointRadius(vm[0].stats.length)
+
+      const blue = '#166ab7'
+      const green = '#389357'
+      const blueInRgba = 'rgba(24, 144, 255, 0.5)'
+      const greenInRgba = 'rgba(59, 198, 133, 0.65)'
+      const red = '#ff4d4f'
+      const redInRgba = 'rgb(255, 77, 79, 0.65)'
+
+      const cpuLine = { label: 'CPU', backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+      const memFreeLinePercent = { label: this.$t('label.memory.free'), backgroundColor: greenInRgba, borderColor: green, data: [], pointRadius: chartPointRadius }
+      const memUsedLinePercent = { label: this.$t('label.memory.used'), backgroundColor: redInRgba, borderColor: red, data: [], pointRadius: chartPointRadius }
+      const memAllocatedLineInMB = { label: this.$t('label.memoryallocated'), backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+      const memFreeLineInMB = { label: this.$t('label.memory.free'), backgroundColor: greenInRgba, borderColor: green, data: [], pointRadius: chartPointRadius }
+      const memUsedLineInMB = { label: this.$t('label.memory.used'), backgroundColor: redInRgba, borderColor: red, data: [], pointRadius: chartPointRadius }
+      const memAllocatedLineInGB = { label: this.$t('label.memoryallocated'), backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+      const memFreeLineInGB = { label: this.$t('label.memory.free'), backgroundColor: greenInRgba, borderColor: green, data: [], pointRadius: chartPointRadius }
+      const memUsedLineInGB = { label: this.$t('label.memory.used'), backgroundColor: redInRgba, borderColor: red, data: [], pointRadius: chartPointRadius }
+      const netDownloadLineInKiB = { label: 'Download', backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+      const netUploadLineInKiB = { label: 'Upload', backgroundColor: greenInRgba, borderColor: green, data: [], pointRadius: chartPointRadius }
+      const netDownloadLineInMiB = { label: 'Download', backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+      const netUploadLineInMiB = { label: 'Upload', backgroundColor: greenInRgba, borderColor: green, data: [], pointRadius: chartPointRadius }
+      const netDownloadLineInGiB = { label: 'Download', backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+      const netUploadLineInGiB = { label: 'Upload', backgroundColor: greenInRgba, borderColor: green, data: [], pointRadius: chartPointRadius }
+      const diskReadLineInKiB = { label: 'Read', backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+      const diskWriteLineInKiB = { label: 'Write', backgroundColor: greenInRgba, borderColor: green, data: [], pointRadius: chartPointRadius }
+      const diskReadLineInMiB = { label: 'Read', backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+      const diskWriteLineInMiB = { label: 'Write', backgroundColor: greenInRgba, borderColor: green, data: [], pointRadius: chartPointRadius }
+      const diskReadLineInGiB = { label: 'Read', backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+      const diskWriteLineInGiB = { label: 'Write', backgroundColor: greenInRgba, borderColor: green, data: [], pointRadius: chartPointRadius }
+      const diskIopsLine = { label: 'IOPS', backgroundColor: blueInRgba, borderColor: blue, data: [], pointRadius: chartPointRadius }
+
+      for (const element of vm[0].stats) {
+        var ts = this.$toLocalDate(element.timestamp)
+        const currentLabel = ts.split('T')[0] + ' ' + ts.split('T')[1].split('-')[0]
+        this.chartLabels.push(currentLabel)
+
+        cpuLine.data.push({ timestamp: currentLabel, stat: element.cpuused.split('%')[0] })
+
+        element.memoryusedkbs = element.memorykbs - element.memoryintfreekbs
+        memFreeLinePercent.data.push({ timestamp: currentLabel, stat: this.calculateMemoryPercentage(false, element.memorykbs, element.memoryintfreekbs) })
+        memUsedLinePercent.data.push({ timestamp: currentLabel, stat: this.calculateMemoryPercentage(true, element.memorykbs, element.memoryintfreekbs) })
+        memAllocatedLineInMB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.memorykbs, 1) })
+        memFreeLineInMB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.memoryintfreekbs, 1) })
+        memUsedLineInMB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.memoryusedkbs, 1) })
+        memAllocatedLineInGB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.memorykbs, 2) })
+        memFreeLineInGB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.memoryintfreekbs, 2) })
+        memUsedLineInGB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.memoryusedkbs, 2) })
+
+        netDownloadLineInKiB.data.push({ timestamp: currentLabel, stat: element.networkkbsread })
+        netUploadLineInKiB.data.push({ timestamp: currentLabel, stat: element.networkkbswrite })
+        netDownloadLineInMiB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.networkkbsread, 1) })
+        netUploadLineInMiB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.networkkbswrite, 1) })
+        netDownloadLineInGiB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.networkkbsread, 2) })
+        netUploadLineInGiB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.networkkbswrite, 2) })
+
+        diskReadLineInKiB.data.push({ timestamp: currentLabel, stat: element.diskkbsread })
+        diskWriteLineInKiB.data.push({ timestamp: currentLabel, stat: element.diskkbswrite })
+        diskReadLineInMiB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.diskkbsread, 1) })
+        diskWriteLineInMiB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.diskkbswrite, 1) })
+        diskReadLineInGiB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.diskkbsread, 2) })
+        diskWriteLineInGiB.data.push({ timestamp: currentLabel, stat: this.convertByteBasedUnitOfMeasure(element.diskkbswrite, 2) })
+        diskIopsLine.data.push({ timestamp: currentLabel, stat: element.diskiopstotal })
+      }
+
+      this.resourceUsageHistory.cpu.push(cpuLine)
+
+      this.resourceUsageHistory.memory.percentage.free.push(memFreeLinePercent)
+      this.resourceUsageHistory.memory.percentage.used.push(memUsedLinePercent)
+      this.resourceUsageHistory.memory.rawData.free.inMB.push(memFreeLineInMB)
+      this.resourceUsageHistory.memory.rawData.free.inMB.push(memAllocatedLineInMB)
+      this.resourceUsageHistory.memory.rawData.used.inMB.push(memUsedLineInMB)
+      this.resourceUsageHistory.memory.rawData.used.inMB.push(memAllocatedLineInMB)
+      this.resourceUsageHistory.memory.rawData.free.inGB.push(memFreeLineInGB)
+      this.resourceUsageHistory.memory.rawData.free.inGB.push(memAllocatedLineInGB)
+      this.resourceUsageHistory.memory.rawData.used.inGB.push(memUsedLineInGB)
+      this.resourceUsageHistory.memory.rawData.used.inGB.push(memAllocatedLineInGB)
+
+      this.resourceUsageHistory.network.inKiB.push(netDownloadLineInKiB)
+      this.resourceUsageHistory.network.inKiB.push(netUploadLineInKiB)
+      this.resourceUsageHistory.network.inMiB.push(netDownloadLineInMiB)
+      this.resourceUsageHistory.network.inMiB.push(netUploadLineInMiB)
+      this.resourceUsageHistory.network.inGiB.push(netDownloadLineInGiB)
+      this.resourceUsageHistory.network.inGiB.push(netUploadLineInGiB)
+
+      this.resourceUsageHistory.disk.readAndWrite.inKiB.push(diskReadLineInKiB)
+      this.resourceUsageHistory.disk.readAndWrite.inKiB.push(diskWriteLineInKiB)
+      this.resourceUsageHistory.disk.readAndWrite.inMiB.push(diskReadLineInMiB)
+      this.resourceUsageHistory.disk.readAndWrite.inMiB.push(diskWriteLineInMiB)
+      this.resourceUsageHistory.disk.readAndWrite.inGiB.push(diskReadLineInGiB)
+      this.resourceUsageHistory.disk.readAndWrite.inGiB.push(diskWriteLineInGiB)
+      this.resourceUsageHistory.disk.iops.push(diskIopsLine)
+
+      this.loaded = true
+    },
+    /**
+     * Calculates the ideal chart points radius based on the number of data points and the screen width.
+     * @param numberOfDataPoints the number of data points.
+     * @returns the ideal chart points radius (which is the size of the points on the chart).
+     */
+    getChartPointRadius (numberOfDataPoints) {
+      const maxSizeLimit = 3
+      const minSizeLimit = 2
+      const minSize = 0.1 // the smallest value that allows to render the point in the chart
+      const result = (screen.width * 0.04) / numberOfDataPoints
+      if (result > maxSizeLimit) {
+        return maxSizeLimit
+      } else if (result < minSizeLimit) {
+        return minSize
+      }
+      return parseFloat(result).toFixed(2)
+    },
+    /**
+     * Converts a value (Byte-based) from an unit to other one. For example: from Byte to KiB; from GiB to MiB; etc.
+     * To use it consider the following sequence: Byte -> KiB -> MiB -> GiB ...
+     * So, from Byte to MiB there are 2 steps, while from MiB to Byte there are -2 steps.
+     * @param value the value to be converted.
+     * @param step the number of steps between Byte-based units of measure.
+     * @returns the converted value.
+     */
+    convertByteBasedUnitOfMeasure (value, step) {
+      if (value === 0) {
+        return 0.00
+      }
+      if (step === 0) {
+        return value
+      }
+      if (step > 0) {
+        return parseFloat(value / (Math.pow(1024, step))).toFixed(2)
+      }
+      return parseFloat(value * (Math.pow(1024, Math.abs(step)))).toFixed(2)
+    },
+    resetData () {
+      this.chartLabels = []
+      this.resourceUsageHistory.cpu = []
+      this.resourceUsageHistory.memory.percentage.free = []
+      this.resourceUsageHistory.memory.percentage.used = []
+      this.resourceUsageHistory.memory.rawData.free.inMB = []
+      this.resourceUsageHistory.memory.rawData.free.inGB = []
+      this.resourceUsageHistory.memory.rawData.used.inMB = []
+      this.resourceUsageHistory.memory.rawData.used.inGB = []
+      this.resourceUsageHistory.network.inKiB = []
+      this.resourceUsageHistory.network.inMiB = []
+      this.resourceUsageHistory.network.inGiB = []
+      this.resourceUsageHistory.disk.iops = []
+      this.resourceUsageHistory.disk.readAndWrite.inKiB = []
+      this.resourceUsageHistory.disk.readAndWrite.inMiB = []
+      this.resourceUsageHistory.disk.readAndWrite.inGiB = []
+    },
+    /**
+     * Calculates the memory percentage.
+     * @param isUsed "true" if the memory used percentage should be returned, "false" if the free memory percentage should be returned.
+     * @param memoryTotalInKB the memory total (in KB).
+     * @param memoryFreeInKB the memory free (in KB).
+     * @returns the percentage of used/free memory.
+     */
+    calculateMemoryPercentage (isUsed, memoryTotalInKB, memoryFreeInKB) {
+      if (memoryTotalInKB == null || memoryFreeInKB == null) {
+        return -1
+      }
+      if (isUsed) {
+        return parseFloat(100.0 * (memoryTotalInKB - memoryFreeInKB) / memoryTotalInKB).toFixed(2)
+      }
+      return parseFloat(100.0 * memoryFreeInKB / memoryTotalInKB).toFixed(2)
+    },
+    /**
+     * Calculates the maximum Y axis and the step size based on the chart data.
+     * @param chartLines the chart lines with their respective data.
+     * @param initialMaxValue the initial maximum value to the Y axis.
+     * @param incrementValue the increment value.
+     * @returns an object containing the maximum Y axis and the step size for the chart.
+     */
+    calculateMaxYAxisAndStepSize (chartLines, initialMaxYAxis, incrementValue) {
+      const numberOfLabelsOnYaxis = 4
+      var highestValue = 0
+      var maxYAxis = initialMaxYAxis
+      for (const line of chartLines) {
+        for (const d of line.data) {
+          const currentValue = parseFloat(d.stat)
+          if (currentValue > highestValue) {
+            highestValue = currentValue
+            while (highestValue > maxYAxis) {
+              maxYAxis += incrementValue
+            }
+          }
+        }
+      }
+      return { maxYAxes: maxYAxis, stepSize: maxYAxis / numberOfLabelsOnYaxis }
+    },
+    /**
+     * Returns the chart options.
+     * @param yAxesStepSize the step size for the Y axes.
+     * @param yAxesUnitOfMeasurement the unit of measurement label used on the Y axes.
+     * @returns the chart options.
+     */
+    getChartOptions (yAxesOptions, yAxesUnitOfMeasurement) {
+      var chartOptions = {
+        responsive: true,
+        maintainAspectRatio: false,
+        scales: {
+          yAxis: {
+            min: 0,
+            max: yAxesOptions.maxYAxes,
+            reverse: false,
+            ticks: {
+              stepSize: yAxesOptions.stepSize,
+              callback: function (label) {
+                return label + yAxesUnitOfMeasurement
+              }
+            }
+          },
+          xAxis: {
+            type: 'time',
+            autoSkip: false,
+            time: {
+              parser: 'YYYY-MM-DD HH:mm:ss',
+              unit: 'second',
+              displayFormats: {
+                second: 'HH:mm:ss'
+              }
+            }
+          }
+        }
+      }
+      const dateTimes = this.convertStringArrayToDateArray(JSON.parse(JSON.stringify(this.chartLabels)))
+      const averageDifference = this.averageDifferenceBetweenTimes(dateTimes)
+      chartOptions.scales.xAxis.time.stepSize = this.calculateStepSize(this.chartLabels.length, averageDifference)
+      return chartOptions
+    },
+    convertStringArrayToDateArray (stringArray) {
+      const dateArray = []
+      for (const element of stringArray) {
+        dateArray.push(new Date(element.replace(' ', 'T')))
+      }
+      return dateArray
+    },
+    averageDifferenceBetweenTimes (timeList) {
+      const oneSecond = 1000 // 1 second represented as milliseconds
+      const differences = []
+      var previus = timeList.splice(0, 1)[0]
+      for (const time of timeList) {
+        differences.push((time - previus) / oneSecond) // push the difference in seconds
+        previus = time
+      }
+      if (differences.length === 0) {
+        return 1
+      }
+      const averageDifference = Math.trunc(differences.reduce((a, b) => a + b, 0) / differences.length)
+      return averageDifference
+    },
+    calculateStepSize (numberOfDataPoints, differenceBetweenTimes) {
+      const idealNumberOfLabels = 8
+      const result = numberOfDataPoints / idealNumberOfLabels
+      if (result > 1) {
+        return result * differenceBetweenTimes
+      }
+      return differenceBetweenTimes
+    },
+    prepareData (chartData) {
+      const datasetList = []
+      for (const element of chartData) {
+        datasetList.push(
+          {
+            backgroundColor: element.backgroundColor,
+            borderColor: element.borderColor,
+            borderWidth: 3,
+            label: element.label,
+            data: element.data.map(d => d.stat),
+            hidden: this.hideLine(element.data.map(d => d.stat)),
+            pointRadius: element.pointRadius,
+            fill: 'origin'
+          }
+        )
+      }
+      return {
+        labels: this.chartLabels,
+        datasets: datasetList
+      }
+    },
+    hideLine (data) {
+      for (const d of data) {
+        if (d < 0) {
+          return true
+        }
+      }
+      return false
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/style/components/view/StatsTab.scss';
+</style>
diff --git a/ui/src/style/component/dashboard.less b/ui/src/components/view/chart/LineChart.vue
similarity index 51%
copy from ui/src/style/component/dashboard.less
copy to ui/src/components/view/chart/LineChart.vue
index 2acbe9df05..1b9206917d 100644
--- a/ui/src/style/component/dashboard.less
+++ b/ui/src/components/view/chart/LineChart.vue
@@ -15,36 +15,41 @@
 // specific language governing permissions and limitations
 // under the License.
 
-.ant-pro-capacity-dashboard {
-  &__wrapper {
-    display: flex;
-    margin-bottom: 24px;
-    flex-wrap: nowrap;
+<template>
+  <Line
+    :chart-options="chartOptions"
+    :chart-data="chartData"
+    :width="width"
+    :height="height"
+  />
+</template>
 
-    .mobile & {
-      flex-wrap: wrap;
-    }
-  }
+<script>
+import { Line } from 'vue-chartjs'
+import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, CategoryScale, TimeScale, LinearScale, PointElement, Filler } from 'chart.js'
 
-  &__select {
-    width: 100%;
+ChartJS.register(Title, Tooltip, Legend, LineElement, CategoryScale, TimeScale, LinearScale, PointElement, Filler)
 
-    .mobile & {
-      margin-bottom: 12px;
+export default {
+  name: 'LineChart',
+  components: { Line },
+  props: {
+    chartData: {
+      type: Object,
+      required: true
+    },
+    chartOptions: {
+      type: Object,
+      default: () => {}
+    },
+    width: {
+      type: Number,
+      default: 650
+    },
+    height: {
+      type: Number,
+      default: 250
     }
   }
-
-  &__button {
-    width: auto;
-  }
-
-  &__tile-wrapper:after {
-    content: "";
-    clear: both;
-    display: table;
-  }
-
-  &__alert-wrapper {
-    clear: both;
-  }
 }
+</script>
diff --git a/ui/src/components/view/stats/FilterStats.vue b/ui/src/components/view/stats/FilterStats.vue
new file mode 100644
index 0000000000..20cd63af87
--- /dev/null
+++ b/ui/src/components/view/stats/FilterStats.vue
@@ -0,0 +1,165 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <a-form
+    class="form-layout"
+    :ref="formRef"
+    :model="form"
+    @finish="handleSubmit"
+    layout="vertical">
+    <div v-show="!(!showStartDate || !showEndDate) || (!showStartDate && !showEndDate)">
+      <a-form-item :label="$t('label.all.available.data')" ref="allData" name="allData">
+        <a-switch v-model:checked="allDataIsChecked" @change="onToggleAllData"/>
+      </a-form-item>
+      <div v-show="showAllDataAlert">
+        <a-alert :message="$t('message.alert.show.all.stats.data')" banner />
+      </div>
+    </div>
+    <div v-show="showStartDate">
+      <a-form-item :label="$t('label.only.start.date.and.time')" ref="allData" name="allData">
+        <a-switch v-model:checked="onlyStartDateIsChecked" @change="onToggleStartDate"/>
+      </a-form-item>
+      <a-form-item :label="$t('label.start.date.and.time')" ref="startDate" name="startDate" :rules="[{ required: showStartDate, message: `${this.$t('message.error.start.date.and.time')}` }]">
+        <a-date-picker
+          v-model:value="form.startDate"
+          show-time
+          :placeholder="$t('message.select.start.date.and.time')"/>
+      </a-form-item>
+    </div>
+    <div v-show="showEndDate">
+      <a-form-item :label="$t('label.only.end.date.and.time')">
+        <a-switch v-model:checked="onlyEndDateIsChecked"  @change="onToggleEndDate"/>
+      </a-form-item>
+      <a-form-item :label="$t('label.end.date.and.time')" ref="endDate" name="endDate" :rules="[{ required: showEndDate, message: `${this.$t('message.error.end.date.and.time')}` }]">
+        <a-date-picker
+          v-model:value="form.endDate"
+          show-time
+          :placeholder="$t('message.select.end.date.and.time')"/>
+      </a-form-item>
+    </div>
+  </a-form>
+  <div :span="24" class="action-button">
+    <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
+    <a-button type="primary" @click="handleSubmit">{{ submitButtonLabel }}</a-button>
+  </div>
+</template>
+
+<script>
+import { ref, reactive, toRaw } from 'vue'
+import moment from 'moment'
+
+export default {
+  name: 'FilterStats',
+  emits: ['closeAction', 'onSubmit'],
+  props: {
+    startDateProp: {
+      type: [Date, String, Number],
+      required: false
+    },
+    endDateProp: {
+      type: [Date, String, Number],
+      required: false
+    }
+  },
+  computed: {
+    startDate () {
+      if (this.startDateProp) {
+        return moment(this.startDateProp)
+      }
+      return null
+    },
+    endDate () {
+      if (this.endDateProp) {
+        return moment(this.endDateProp)
+      }
+      return null
+    }
+  },
+  data () {
+    return {
+      allDataIsChecked: false,
+      onlyStartDateIsChecked: false,
+      onlyEndDateIsChecked: false,
+      showAllDataAlert: false,
+      showAllData: true,
+      showStartDate: true,
+      showEndDate: true,
+      submitButtonLabel: this.$t('label.ok')
+    }
+  },
+  updated () {
+    this.form.startDate = this.startDate
+    this.form.endDate = this.endDate
+  },
+  created () {
+    this.initForm()
+  },
+  methods: {
+    initForm () {
+      this.formRef = ref()
+      this.form = reactive({
+        startDate: this.startDate,
+        endDate: this.endDate
+      })
+    },
+    handleSubmit (e) {
+      e.preventDefault()
+      this.formRef.value.validate().then(() => {
+        this.submitButtonLabel = this.$t('label.refresh')
+        const values = toRaw(this.form)
+        this.$emit('onSubmit', values)
+      }).catch(error => {
+        this.formRef.value.scrollToField(error.errorFields[0].name)
+      })
+    },
+    closeAction () {
+      this.$emit('closeAction')
+    },
+    onToggleAllData () {
+      this.showAllDataAlert = !this.showAllDataAlert
+      if (this.showAllDataAlert) {
+        this.showStartDate = false
+        this.showEndDate = false
+        this.form.startDate = null
+        this.form.endDate = null
+      } else {
+        this.showStartDate = true
+        this.showEndDate = true
+      }
+      this.resetSubmitButton()
+    },
+    onToggleStartDate () {
+      this.showEndDate = !this.showEndDate
+      if (this.showEndDate === false) {
+        this.form.endDate = null
+      }
+      this.resetSubmitButton()
+    },
+    onToggleEndDate () {
+      this.showStartDate = !this.showStartDate
+      if (this.showStartDate === false) {
+        this.form.startDate = null
+      }
+      this.resetSubmitButton()
+    },
+    resetSubmitButton () {
+      this.submitButtonLabel = this.$t('label.ok')
+    }
+  }
+}
+</script>
diff --git a/ui/src/components/view/stats/ResourceStatsInfo.vue b/ui/src/components/view/stats/ResourceStatsInfo.vue
new file mode 100644
index 0000000000..e6f0d06800
--- /dev/null
+++ b/ui/src/components/view/stats/ResourceStatsInfo.vue
@@ -0,0 +1,90 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div>
+    <div v-if="messages.length > 1">
+      <ul>
+        <li v-for="(msg, index) in messages" :key="index">
+          <div v-if="index === messages.length - 1">
+            {{ msg }}.
+          </div>
+          <div v-else>
+            {{ msg }};
+          </div>
+        </li>
+      </ul>
+    </div>
+    <p v-else-if="messages.length === 1">
+      {{ messages[0] }}.
+    </p>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ResourceStatsInfo',
+  props: {
+    resourceType: {
+      type: String,
+      default: null
+    }
+  },
+  data () {
+    return {
+      info: [
+        {
+          resourceType: 'CPU',
+          messageList: [
+            this.$t('message.cpu.usage.info.multi.cpu'),
+            this.$t('message.cpu.usage.info.cpu.cap')
+          ]
+        },
+        {
+          resourceType: 'MEM',
+          messageList: [
+            this.$t('message.memory.usage.info.negative.value'),
+            this.$t('message.memory.usage.info.hypervisor.additionals')
+          ]
+        },
+        {
+          resourceType: 'NET',
+          messageList: [
+            this.$t('message.network.usage.info.sum.of.vnics'),
+            this.$t('message.network.usage.info.data.points')
+          ]
+        },
+        {
+          resourceType: 'DISK',
+          messageList: [
+            this.$t('message.disk.usage.info.sum.of.disks'),
+            this.$t('message.disk.usage.info.data.points')
+          ]
+        }
+      ],
+      messages: []
+    }
+  },
+  mounted () {
+    for (const element of this.info) {
+      if (element.resourceType === this.resourceType) {
+        this.messages = element.messageList
+      }
+    }
+  }
+}
+</script>
diff --git a/ui/src/style/component/dashboard.less b/ui/src/style/components/dashboard.less
similarity index 100%
copy from ui/src/style/component/dashboard.less
copy to ui/src/style/components/dashboard.less
diff --git a/ui/src/style/component/dashboard.less b/ui/src/style/components/view/StatsTab.scss
similarity index 66%
rename from ui/src/style/component/dashboard.less
rename to ui/src/style/components/view/StatsTab.scss
index 2acbe9df05..89e51ab0cb 100644
--- a/ui/src/style/component/dashboard.less
+++ b/ui/src/style/components/view/StatsTab.scss
@@ -15,36 +15,18 @@
 // specific language governing permissions and limitations
 // under the License.
 
-.ant-pro-capacity-dashboard {
-  &__wrapper {
-    display: flex;
-    margin-bottom: 24px;
-    flex-wrap: nowrap;
-
-    .mobile & {
-      flex-wrap: wrap;
-    }
-  }
-
-  &__select {
-    width: 100%;
-
-    .mobile & {
-      margin-bottom: 12px;
-    }
-  }
-
-  &__button {
-    width: auto;
-  }
-
-  &__tile-wrapper:after {
-    content: "";
-    clear: both;
-    display: table;
-  }
-
-  &__alert-wrapper {
-    clear: both;
-  }
+.ant-tag {
+  padding: 0 7px 0 0;
+}
+.ant-select {
+  margin-left: 10px;
+}
+.info-icon {
+  margin: 0 10px 0 5px;
+}
+.chart-row {
+  margin-bottom: 10%;
+}
+.chart-type-select {
+  min-width: 130px;
 }
diff --git a/ui/src/utils/plugins.js b/ui/src/utils/plugins.js
index 6f7954ef2d..0941c42674 100644
--- a/ui/src/utils/plugins.js
+++ b/ui/src/utils/plugins.js
@@ -305,6 +305,18 @@ export const toLocaleDatePlugin = {
       dateWithOffset = dateWithOffset.substring(0, dateWithOffset.length - 4)
       return dateWithOffset
     }
+
+    app.config.globalProperties.$toLocalDate = function (date) {
+      var timezoneOffset = this.$store.getters.timezoneoffset
+      if (this.$store.getters.usebrowsertimezone) {
+        // Since GMT+530 is returned as -330 (mins to GMT)
+        timezoneOffset = new Date().getTimezoneOffset() / -60
+      }
+      var milliseconds = Date.parse(date)
+      // e.g. "Tue, 08 Jun 2010 19:13:49 GMT", "Tue, 25 May 2010 12:07:01 UTC"
+      var dateWithOffset = new Date(milliseconds + (timezoneOffset * 60 * 60 * 1000))
+      return dateWithOffset.toISOString()
+    }
   }
 }
 
diff --git a/ui/src/views/compute/InstanceTab.vue b/ui/src/views/compute/InstanceTab.vue
index 9e4e84ff1f..ec6c7c3229 100644
--- a/ui/src/views/compute/InstanceTab.vue
+++ b/ui/src/views/compute/InstanceTab.vue
@@ -25,6 +25,9 @@
       <a-tab-pane :tab="$t('label.details')" key="details">
         <DetailsTab :resource="dataResource" :loading="loading" />
       </a-tab-pane>
+      <a-tab-pane :tab="$t('label.statistics')" key="stats">
+        <StatsTab :resource="resource"/>
+      </a-tab-pane>
       <a-tab-pane :tab="$t('label.iso')" key="cdrom" v-if="vm.isoid">
         <usb-outlined />
         <router-link :to="{ path: '/iso/' + vm.isoid }">{{ vm.isoname }}</router-link> <br/>
@@ -307,6 +310,7 @@ import { mixinDevice } from '@/utils/mixin.js'
 import ResourceLayout from '@/layouts/ResourceLayout'
 import Status from '@/components/widgets/Status'
 import DetailsTab from '@/components/view/DetailsTab'
+import StatsTab from '@/components/view/StatsTab'
 import EventsTab from '@/components/view/EventsTab'
 import DetailSettings from '@/components/view/DetailSettings'
 import NicsTable from '@/views/network/NicsTable'
@@ -320,6 +324,7 @@ export default {
   components: {
     ResourceLayout,
     DetailsTab,
+    StatsTab,
     EventsTab,
     DetailSettings,
     NicsTable,