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

[cloudstack] branch 4.18 updated: ui: assorted improvements (#7833)

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

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


The following commit(s) were added to refs/heads/4.18 by this push:
     new feb9509547a ui: assorted improvements (#7833)
feb9509547a is described below

commit feb9509547aa27f00b8be40da0857197bf798857
Author: Rohit Yadav <ro...@shapeblue.com>
AuthorDate: Fri Aug 11 14:00:04 2023 +0530

    ui: assorted improvements (#7833)
    
    This PR aims to polish the UI with following tweaks and changes:
    
    - Increase resource and os-logo icons both in list view, user-menu bar and VM deployment form
    - Fix css issues in VM deployment form when resource icons are on some of the templates/isos but not all
    - Replace edit icon in the resource icon editting button on the infocard, in resource view
    - Fix css marging/padding issue for nav bar and left-branding/logo
    - Introduce a new Limits option in the user menu, to allow users to see their own limits when they log in
    - Rename resource tab to limits tab for accounts, project and domains
    - Introduce a new copy-label component, that can be clicked to copy strings; use in info-card and list view for entites such as IP addresses and UUIDs
    - Add router-link to /zones/ in case of user-accounts (when /zone isn't routable in the UI)
    - Show better list of nics and ssh keys pairs in infocard for VM resource view
    - Standardise most resources to show state/status columns right after resource name (wherever applicable)
    - Remove displayname column in VM list view, add cpu number and memory by default
    - Add k8s version column in k8s list view
    - Add size and phy size columns in case of template and ISOs list view, only for root/domain admins
    - Add phy network router-link in case of guest VLAN list view; rearrange columns list for consistency
    - Add snapshot phy size column in the snapshot list view; and router-link for volume in the snapshot list view; and missing/useful details in the volume snapshot details view
    - Add a create and add data disk feature in Instances tab, just like we've add nic feature in the same
    
    Signed-off-by: Rohit Yadav <ro...@shapeblue.com>
---
 ui/public/locales/en.json                          |  7 ++-
 ui/src/components/header/UserMenu.vue              |  8 ++-
 ui/src/components/menu/SideMenu.vue                |  2 -
 ui/src/components/view/DetailsTab.vue              | 15 +++++
 ui/src/components/view/InfoCard.vue                | 72 +++++++++++++++-------
 ui/src/components/view/ListView.vue                | 39 ++++++++++--
 ui/src/components/widgets/CopyLabel.vue            | 51 +++++++++++++++
 ui/src/config/section/account.js                   |  4 +-
 ui/src/config/section/compute.js                   | 22 ++++---
 ui/src/config/section/domain.js                    |  4 +-
 ui/src/config/section/image.js                     | 26 ++++++--
 ui/src/config/section/network.js                   | 16 ++---
 ui/src/config/section/project.js                   |  4 +-
 ui/src/config/section/storage.js                   | 13 ++--
 ui/src/utils/request.js                            |  2 +
 ui/src/views/compute/CreateAutoScaleVmGroup.vue    | 42 ++++++++-----
 ui/src/views/compute/DeployVM.vue                  | 44 ++++++++-----
 ui/src/views/compute/InstanceTab.vue               | 43 +++++++++++--
 .../views/compute/wizard/TemplateIsoRadioGroup.vue | 14 ++---
 ui/src/views/dashboard/CapacityDashboard.vue       |  2 +-
 ui/src/views/image/TemplateZones.vue               |  2 +-
 ui/src/views/storage/CreateVolume.vue              | 39 +++++++++++-
 22 files changed, 356 insertions(+), 115 deletions(-)

diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 0691c2da4ad..f93cbd233f7 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -61,6 +61,7 @@
 "label.action.create.snapshot.from.vmsnapshot": "Create snapshot from VM snapshot",
 "label.action.create.template.from.volume": "Create template from volume",
 "label.action.create.volume": "Create volume",
+"label.action.create.volume.add": "Create and Add Volume",
 "label.action.delete.account": "Delete account",
 "label.action.delete.backup.offering": "Delete backup offering",
 "label.action.delete.cluster": "Delete cluster",
@@ -1135,7 +1136,8 @@
 "label.license.agreements": "License agreements",
 "label.limit": "Limit",
 "label.limitcpuuse": "CPU cap",
-"label.limits": "Configure limits",
+"label.limits": "Limits",
+"label.limits.configure": "Configure limits",
 "label.link.domain.to.ldap": "Link domain to LDAP",
 "label.linklocalip": "Link-local/Control IP address",
 "label.linux": "Linux",
@@ -1785,6 +1787,7 @@
 "label.snapshotlimit": "Snapshot limits",
 "label.snapshotmemory": "Snapshot memory",
 "label.snapshots": "Snapshots",
+"label.snapshottype": "Snapshot Type",
 "label.sockettimeout": "Socket timeout",
 "label.softwareversion": "Software version",
 "label.source.based": "SourceBased",
@@ -2140,6 +2143,7 @@
 "label.volumename": "Volume name",
 "label.volumes": "Volumes",
 "label.volumetotal": "Volume",
+"label.volumetype": "Volume Type",
 "label.vpc": "VPC",
 "label.vpc.id": "VPC ID",
 "label.vpc.offerings": "VPC offerings",
@@ -2347,6 +2351,7 @@
 "message.attach.volume": "Please fill in the following data to attach a new volume. If you are attaching a disk volume to a Windows based virtual machine, you will need to reboot the instance to see the attached disk.",
 "message.attach.volume.failed": "Failed to attach volume.",
 "message.attach.volume.progress": "Attaching volume",
+"message.attach.volume.success": "Successfully attached the volume to the instance",
 "message.authorization.failed": "Session expired, authorization verification failed.",
 "message.autoscale.loadbalancer.update": "The load balancer rule can be updated only when autoscale VM group is DISABLED.",
 "message.autoscale.policies.update": "The scale up/down policies can be updated only when autoscale VM group is DISABLED.",
diff --git a/ui/src/components/header/UserMenu.vue b/ui/src/components/header/UserMenu.vue
index d2b7787f39e..0c7434594be 100644
--- a/ui/src/components/header/UserMenu.vue
+++ b/ui/src/components/header/UserMenu.vue
@@ -27,7 +27,7 @@
     <a-dropdown>
       <span class="user-menu-dropdown action">
         <span v-if="image">
-          <resource-icon :image="image" size="2x" style="margin-right: 5px"/>
+          <resource-icon :image="image" size="4x" style="margin-right: 5px; margin-top: -3px"/>
         </span>
         <a-avatar v-else-if="userInitials" class="user-menu-avatar avatar" size="small" :style="{ backgroundColor: '#1890ff', color: 'white' }">
           {{ userInitials }}
@@ -45,6 +45,12 @@
                 <span class="user-menu-item-name">{{ $t('label.profilename') }}</span>
             </a-menu-item>
           </router-link>
+          <router-link :to="{ path: '/account/' + $store.getters.userInfo.accountid, query: { tab: 'limits' } }">
+            <a-menu-item class="user-menu-item" key="0">
+                <ControlOutlined class="user-menu-item-icon" />
+                <span class="user-menu-item-name">{{ $t('label.limits') }}</span>
+            </a-menu-item>
+          </router-link>
           <a @click="toggleUseBrowserTimezone">
             <a-menu-item class="user-menu-item" key="1">
                 <ClockCircleOutlined class="user-menu-item-icon" />
diff --git a/ui/src/components/menu/SideMenu.vue b/ui/src/components/menu/SideMenu.vue
index d370f6e1c89..c26755021d2 100644
--- a/ui/src/components/menu/SideMenu.vue
+++ b/ui/src/components/menu/SideMenu.vue
@@ -119,14 +119,12 @@ export default {
 
     .ant-menu-light {
       border-right-color: transparent;
-      padding: 14px 0;
     }
   }
 
   &.dark {
     .ant-menu-dark {
       border-right-color: transparent;
-      padding: 14px 0;
     }
   }
 }
diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue
index 17a4e958dd0..e359f14d86b 100644
--- a/ui/src/components/view/DetailsTab.vue
+++ b/ui/src/components/view/DetailsTab.vue
@@ -51,6 +51,21 @@
               {{ dataResource.rootdisksize }} GB
             </div>
           </div>
+          <div v-else-if="['template', 'iso'].includes($route.meta.name) && item === 'size'">
+            <div>
+              {{ parseFloat(dataResource.size / (1024.0 * 1024.0 * 1024.0)).toFixed(2) }} GB
+            </div>
+          </div>
+          <div v-else-if="['volume', 'snapshot', 'template', 'iso'].includes($route.meta.name) && item === 'physicalsize'">
+            <div>
+              {{ parseFloat(dataResource.physicalsize / (1024.0 * 1024.0 * 1024.0)).toFixed(2) }} GB
+            </div>
+          </div>
+          <div v-else-if="['volume', 'snapshot', 'template', 'iso'].includes($route.meta.name) && item === 'virtualsize'">
+            <div>
+              {{ parseFloat(dataResource.virtualsize / (1024.0 * 1024.0 * 1024.0)).toFixed(2) }} GB
+            </div>
+          </div>
           <div v-else-if="['name', 'type'].includes(item)">
             <span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(dataResource[item])">{{ $t(dataResource[item].toLowerCase()) }}</span>
             <span v-else>{{ dataResource[item] }}</span>
diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue
index 0b9ebf42657..72df182d80e 100644
--- a/ui/src/components/view/InfoCard.vue
+++ b/ui/src/components/view/InfoCard.vue
@@ -27,7 +27,7 @@
               v-clipboard:copy="name" >
               <upload-resource-icon v-if="'uploadResourceIcon' in $store.getters.apis" :visible="showUpload" :resource="resource" @handle-close="showUpload(false)"/>
               <div class="ant-upload-preview" v-if="$showIcon() && !$route.path.includes('zones')">
-                <camera-outlined class="upload-icon"/>
+                <edit-outlined class="upload-icon"/>
               </div>
               <slot name="avatar">
                 <span v-if="(resource.icon && resource.icon.base64image || images.template || images.iso || resourceIcon) && !['router', 'systemvm', 'volume'].includes($route.path.split('/')[1])">
@@ -119,14 +119,14 @@
           <div class="resource-detail-item__label">{{ $t('label.id') }}</div>
           <div class="resource-detail-item__details">
             <tooltip-button
-              tooltipPlacement="right"
+              tooltipPlacement="top"
               :tooltip="$t('label.copyid')"
               icon="barcode-outlined"
               type="dashed"
               size="small"
               :copyResource="resource.id"
               @onClick="$message.success($t('label.copied.clipboard'))" />
-            <span style="margin-left: 10px;">{{ resource.id }}</span>
+            <span style="margin-left: 10px;"><copy-label :label="resource.id" /></span>
           </div>
         </div>
         <div class="resource-detail-item" v-if="resource.ostypename && resource.ostypeid">
@@ -139,6 +139,29 @@
             <span style="margin-left: 8px">{{ resource.ostypename }}</span>
           </div>
         </div>
+        <div class="resource-detail-item" v-if="resource.ipaddress">
+          <div class="resource-detail-item__label">{{ $t('label.ip') }}</div>
+          <div class="resource-detail-item__details">
+            <environment-outlined
+              @click="$message.success(`${$t('label.copied.clipboard')} : ${ ipaddress }`)"
+              v-clipboard:copy="ipaddress" />
+            <router-link v-if="!isStatic && resource.ipaddressid" :to="{ path: '/publicip/' + resource.ipaddressid }">
+              <copy-label :label="ipaddress" />
+            </router-link>
+            <span v-else>
+              <span v-if="ipaddress.includes(',')">
+                <span
+                v-for="(value, index) in ipaddress.split(',')"
+                :key="index">
+                  <copy-label :label="value" /><br/>
+                </span>
+              </span>
+              <span v-else>
+                <copy-label :label="ipaddress" />
+              </span>
+            </span>
+          </div>
+        </div>
         <div class="resource-detail-item" v-if="('cpunumber' in resource && 'cpuspeed' in resource) || resource.cputotal">
           <div class="resource-detail-item__label">{{ $t('label.cpu') }}</div>
           <div class="resource-detail-item__details">
@@ -292,11 +315,19 @@
                 v-for="(eth, index) in resource.nic"
                 :key="eth.id"
                 style="margin-left: -24px; margin-top: 5px;">
-                <api-outlined /><strong>eth{{ index }}</strong> {{ eth.ip6address ? eth.ipaddress + ', ' + eth.ip6address : eth.ipaddress }}
-                <router-link v-if="!isStatic && eth.networkname && eth.networkid" :to="{ path: '/guestnetwork/' + eth.networkid }">({{ eth.networkname }})</router-link>
+                <api-outlined />
+                <strong>eth{{ index }}</strong>&nbsp;
+                <copy-label :label="eth.ip6address ? eth.ipaddress + ', ' + eth.ip6address : eth.ipaddress" />&nbsp;
                 <a-tag v-if="eth.isdefault">
                   {{ $t('label.default') }}
-                </a-tag >
+                </a-tag ><br/>
+                <span v-if="!isStatic && eth.networkname && eth.networkid">
+                  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+                  <apartment-outlined/>
+                  <router-link :to="{ path: '/guestnetwork/' + eth.networkid      }">
+                    {{ eth.networkname }}
+                  </router-link>
+                </span>
               </div>
             </div>
           </div>
@@ -324,16 +355,6 @@
             <span>{{ resource.loadbalancer.name }} ( {{ resource.loadbalancer.publicip }}:{{ resource.loadbalancer.publicport }})</span>
           </div>
         </div>
-        <div class="resource-detail-item" v-if="resource.ipaddress">
-          <div class="resource-detail-item__label">{{ $t('label.ip') }}</div>
-          <div class="resource-detail-item__details">
-            <environment-outlined
-              @click="$message.success(`${$t('label.copied.clipboard')} : ${ ipaddress }`)"
-              v-clipboard:copy="ipaddress" />
-            <router-link v-if="!isStatic && resource.ipaddressid" :to="{ path: '/publicip/' + resource.ipaddressid }">{{ ipaddress }}</router-link>
-            <span v-else>{{ ipaddress }}</span>
-          </div>
-        </div>
         <div class="resource-detail-item" v-if="resource.projectid || resource.projectname">
           <div class="resource-detail-item__label">{{ $t('label.project') }}</div>
           <div class="resource-detail-item__details">
@@ -368,10 +389,15 @@
         <div class="resource-detail-item" v-if="resource.keypairs && resource.keypairs.length > 0">
           <div class="resource-detail-item__label">{{ $t('label.keypairs') }}</div>
           <div class="resource-detail-item__details">
-            <key-outlined />
-            <li v-for="keypair in keypairs" :key="keypair">
-              <router-link :to="{ path: '/ssh/' + keypair }" style="margin-right: 5px">{{ keypair }}</router-link>
-            </li>
+            <div>
+              <div
+                v-for="keypair in keypairs"
+                :key="keypair"
+                style="margin-top: 5px;">
+                <key-outlined />
+                <router-link :to="{ path: '/ssh/' + keypair }" style="margin-right: 5px">{{ keypair }}</router-link>
+              </div>
+            </div>
           </div>
         </div>
         <div class="resource-detail-item" v-if="resource.resourcetype && resource.resourceid && routeFromResourceType">
@@ -420,7 +446,8 @@
           <div class="resource-detail-item__label">{{ $t('label.publicip') }}</div>
           <div class="resource-detail-item__details">
             <gateway-outlined />
-            <router-link :to="{ path: '/publicip/' + resource.publicipid }">{{ resource.publicip }} </router-link>
+            <router-link v-if="resource.publicipid" :to="{ path: '/publicip/' + resource.publicipid }">{{ resource.publicip }} </router-link>
+            <copy-label :label="resource.publicip"/>
           </div>
         </div>
         <div class="resource-detail-item" v-if="resource.vpcid">
@@ -554,6 +581,7 @@
             </span>
             <global-outlined v-else />
             <router-link v-if="!isStatic && $router.resolve('/zone/' + resource.zoneid).matched[0].redirect !== '/exception/404'" :to="{ path: '/zone/' + resource.zoneid }">{{ resource.zone || resource.zonename || resource.zoneid }}</router-link>
+            <router-link v-else-if="$router.resolve('/zones/' + resource.zoneid).matched[0].redirect !== '/exception/404'" :to="{ path: '/zones/' + resource.zoneid }">{{ resource.zone || resource.zonename || resource.zoneid }}</router-link>
             <span v-else>{{ resource.zone || resource.zonename || resource.zoneid }}</span>
           </div>
         </div>
@@ -733,6 +761,7 @@ import { createPathBasedOnVmType } from '@/utils/plugins'
 import Console from '@/components/widgets/Console'
 import OsLogo from '@/components/widgets/OsLogo'
 import Status from '@/components/widgets/Status'
+import CopyLabel from '@/components/widgets/CopyLabel'
 import TooltipButton from '@/components/widgets/TooltipButton'
 import UploadResourceIcon from '@/components/view/UploadResourceIcon'
 import eventBus from '@/config/eventBus'
@@ -745,6 +774,7 @@ export default {
     Console,
     OsLogo,
     Status,
+    CopyLabel,
     TooltipButton,
     UploadResourceIcon,
     ResourceIcon,
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue
index 7ccb028f986..38b8b519138 100644
--- a/ui/src/components/view/ListView.vue
+++ b/ui/src/components/view/ListView.vue
@@ -73,9 +73,9 @@
     <template #name="{text, record}">
       <span v-if="['vm'].includes($route.path.split('/')[1])" style="margin-right: 5px">
         <span v-if="record.icon && record.icon.base64image">
-          <resource-icon :image="record.icon.base64image" size="1x"/>
+          <resource-icon :image="record.icon.base64image" size="2x"/>
         </span>
-        <os-logo v-else :osId="record.ostypeid" :osName="record.osdisplayname" size="lg" />
+        <os-logo v-else :osId="record.ostypeid" :osName="record.osdisplayname" size="2x" />
       </span>
       <span style="min-width: 120px" >
         <QuickView
@@ -88,8 +88,8 @@
           <tooltip-button type="dashed" size="small" icon="LoginOutlined" @onClick="changeProject(record)" />
         </span>
         <span v-if="$showIcon() && !['vm'].includes($route.path.split('/')[1])" style="margin-right: 5px">
-          <resource-icon v-if="$showIcon() && record.icon && record.icon.base64image" :image="record.icon.base64image" size="1x"/>
-          <os-logo v-else-if="record.ostypename" :osName="record.ostypename" size="1x" />
+          <resource-icon v-if="$showIcon() && record.icon && record.icon.base64image" :image="record.icon.base64image" size="2x"/>
+          <os-logo v-else-if="record.ostypename" :osName="record.ostypename" size="2x" />
           <render-icon v-else-if="typeof $route.meta.icon ==='string'" style="font-size: 16px;" :icon="$route.meta.icon"/>
           <render-icon v-else style="font-size: 16px;" :svgIcon="$route.meta.icon" />
         </span>
@@ -143,7 +143,7 @@
     </template>
     <template #username="{text, record}">
       <span v-if="$showIcon() && !['vm'].includes($route.path.split('/')[1])" style="margin-right: 5px">
-        <resource-icon v-if="$showIcon() && record.icon && record.icon.base64image" :image="record.icon.base64image" size="1x"/>
+        <resource-icon v-if="$showIcon() && record.icon && record.icon.base64image" :image="record.icon.base64image" size="2x"/>
         <user-outlined v-else style="font-size: 16px;" />
       </span>
       <router-link :to="{ path: $route.path + '/' + record.id }" v-if="['/accountuser', '/vpnuser'].includes($route.path)">{{ text }}</router-link>
@@ -162,7 +162,9 @@
     </template>
     <template #ipaddress="{ text, record }" href="javascript:;">
       <router-link v-if="['/publicip', '/privategw'].includes($route.path)" :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
-      <span v-else>{{ text }}</span>
+      <span v-else>
+        <copy-label :label="text" />
+      </span>
       <span v-if="record.issourcenat">
         &nbsp;
         <a-tag>source-nat</a-tag>
@@ -188,6 +190,25 @@
     <template #virtualmachinename="{ text, record }">
       <router-link :to="{ path: '/vm/' + record.virtualmachineid }">{{ text }}</router-link>
     </template>
+    <template #volumename="{ text, record }">
+      <router-link :to="{ path: '/volume/' + record.volumeid }">{{ text }}</router-link>
+    </template>
+    <template #size="{ text }">
+      <span v-if="text">
+        {{ parseFloat(parseFloat(text) / 1024.0 / 1024.0 / 1024.0).toFixed(2) }} GiB
+      </span>
+    </template>
+    <template #physicalsize="{ text }">
+      <span v-if="text">
+        {{ parseFloat(parseFloat(text) / 1024.0 / 1024.0 / 1024.0).toFixed(2) }} GiB
+      </span>
+    </template>
+    <template #physicalnetworkname="{ text, record }">
+      <router-link :to="{ path: '/physicalnetwork/' + record.physicalnetworkid }">{{ text }}</router-link>
+    </template>
+    <template #serviceofferingname="{ text, record }">
+      <router-link :to="{ path: '/computeoffering/' + record.serviceofferingid }">{{ text }}</router-link>
+    </template>
     <template #hypervisor="{ text, record }">
       <span v-if="$route.name === 'hypervisorcapability'">
         <router-link :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
@@ -198,6 +219,9 @@
       <status v-if="$route.path.startsWith('/host')" :text="getHostState(record)" displayText />
       <status v-else :text="text ? text : ''" displayText :styles="{ 'min-width': '80px' }" />
     </template>
+    <template #status="{ text }">
+      <status :text="text ? text : ''" displayText />
+    </template>
     <template #allocationstate="{ text }">
       <status :text="text ? text : ''" displayText />
     </template>
@@ -303,6 +327,7 @@
     </template>
     <template #zonename="{ text, record }">
       <router-link v-if="$router.resolve('/zone/' + record.zoneid).matched[0].redirect !== '/exception/404'" :to="{ path: '/zone/' + record.zoneid }">{{ text }}</router-link>
+      <router-link v-else-if="$router.resolve('/zones/' + record.zoneid).matched[0].redirect !== '/exception/404'" :to="{ path: '/zones/' + record.zoneid }">{{ text }}</router-link>
       <span v-else>{{ text }}</span>
     </template>
     <template #rolename="{ text, record }">
@@ -431,6 +456,7 @@ import { api } from '@/api'
 import OsLogo from '@/components/widgets/OsLogo'
 import Status from '@/components/widgets/Status'
 import QuickView from '@/components/view/QuickView'
+import CopyLabel from '@/components/widgets/CopyLabel'
 import TooltipButton from '@/components/widgets/TooltipButton'
 import ResourceIcon from '@/components/view/ResourceIcon'
 import ResourceLabel from '@/components/widgets/ResourceLabel'
@@ -442,6 +468,7 @@ export default {
     OsLogo,
     Status,
     QuickView,
+    CopyLabel,
     TooltipButton,
     ResourceIcon,
     ResourceLabel
diff --git a/ui/src/components/widgets/CopyLabel.vue b/ui/src/components/widgets/CopyLabel.vue
new file mode 100644
index 00000000000..650f678206e
--- /dev/null
+++ b/ui/src/components/widgets/CopyLabel.vue
@@ -0,0 +1,51 @@
+// 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>
+  <span @click="$message.success($t('label.copied.clipboard') + ': ' + label)">
+    <a-tooltip :title="tooltip ? tooltip : $t('label.copy')" :placement="tooltipPlacement">
+      <a href="javascript:;" v-clipboard:copy="label">{{ label }}</a>
+    </a-tooltip>
+  </span>
+</template>
+
+<script>
+
+export default {
+  name: 'CopyLabel',
+  props: {
+    label: {
+      type: String,
+      default: ''
+    },
+    tooltip: {
+      type: String,
+      default: ''
+    },
+    tooltipPlacement: {
+      type: String,
+      default: 'top'
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  .tooltip-icon {
+    color: rgba(0,0,0,.45);
+  }
+</style>
diff --git a/ui/src/config/section/account.js b/ui/src/config/section/account.js
index 21878c1fe36..f7af6f06cf5 100644
--- a/ui/src/config/section/account.js
+++ b/ui/src/config/section/account.js
@@ -41,11 +41,11 @@ export default {
       component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
     },
     {
-      name: 'resources',
+      name: 'limits',
       component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceCountUsage.vue')))
     },
     {
-      name: 'limits',
+      name: 'limits.configure',
       show: (record, route, user) => { return ['Admin', 'DomainAdmin'].includes(user.roletype) },
       component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceLimitTab.vue')))
     },
diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js
index 6aa2beff643..e9a16ff6322 100644
--- a/ui/src/config/section/compute.js
+++ b/ui/src/config/section/compute.js
@@ -46,8 +46,8 @@ export default {
         return filters
       },
       columns: () => {
-        const fields = ['name', 'displayname', 'state', 'ipaddress']
-        const metricsFields = ['cpunumber', 'cpuused', 'cputotal',
+        const fields = ['name', 'state', 'ipaddress']
+        const metricsFields = ['cpunumber', 'cputotal', 'cpuused', 'memorytotal',
           {
             memoryused: (record) => {
               if (record.memoryintfreekbs <= 0 || record.memorykbs <= 0) {
@@ -56,7 +56,7 @@ export default {
               return parseFloat(100.0 * (record.memorykbs - record.memoryintfreekbs) / record.memorykbs).toFixed(2) + '%'
             }
           },
-          'memorytotal', 'networkread', 'networkwrite', 'diskread', 'diskwrite', 'diskiopstotal']
+          'networkread', 'networkwrite', 'diskread', 'diskwrite', 'diskiopstotal']
 
         if (store.getters.metrics) {
           fields.push(...metricsFields)
@@ -66,18 +66,17 @@ export default {
           fields.splice(2, 0, 'instancename')
           fields.push('account')
           fields.push('hostname')
-          fields.push('zonename')
         } else if (store.getters.userInfo.roletype === 'DomainAdmin') {
           fields.push('account')
-          fields.push('zonename')
         } else {
-          fields.push('zonename')
+          fields.push('serviceofferingname')
         }
+        fields.push('zonename')
         return fields
       },
       searchFilters: ['name', 'zoneid', 'domainid', 'account', 'groupid', 'tags'],
       details: () => {
-        var fields = ['displayname', 'name', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename',
+        var fields = ['name', 'displayname', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename',
           'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account',
           'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy', 'hostcontrolstate']
         const listZoneHaveSGEnabled = store.getters.zones.filter(zone => zone.securitygroupsenabled === true)
@@ -455,7 +454,7 @@ export default {
       docHelp: 'plugins/cloudstack-kubernetes-service.html',
       permission: ['listKubernetesClusters'],
       columns: (store) => {
-        var fields = ['name', 'state', 'size', 'cpunumber', 'memory']
+        var fields = ['name', 'state', 'size', 'cpunumber', 'memory', 'kubernetesversionname']
         if (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) {
           fields.push('account')
         }
@@ -548,7 +547,7 @@ export default {
       docHelp: 'adminguide/autoscale_without_netscaler.html',
       resourceType: 'AutoScaleVmGroup',
       permission: ['listAutoScaleVmGroups'],
-      columns: ['name', 'account', 'associatednetworkname', 'publicip', 'publicport', 'privateport', 'minmembers', 'maxmembers', 'availablevirtualmachinecount', 'state'],
+      columns: ['name', 'state', 'associatednetworkname', 'publicip', 'publicport', 'privateport', 'minmembers', 'maxmembers', 'availablevirtualmachinecount', 'account'],
       details: ['name', 'id', 'account', 'domain', 'associatednetworkname', 'associatednetworkid', 'lbruleid', 'lbprovider', 'publicip', 'publicipid', 'publicport', 'privateport', 'minmembers', 'maxmembers', 'availablevirtualmachinecount', 'interval', 'state', 'created'],
       related: [{
         name: 'vm',
@@ -652,7 +651,7 @@ export default {
       docHelp: 'adminguide/virtual_machines.html#changing-the-vm-name-os-or-group',
       resourceType: 'VMInstanceGroup',
       permission: ['listInstanceGroups'],
-      columns: ['name', 'account'],
+      columns: ['name', 'account', 'domain'],
       details: ['name', 'id', 'account', 'domain', 'created'],
       related: [{
         name: 'vm',
@@ -706,6 +705,7 @@ export default {
         var fields = ['name', 'fingerprint']
         if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
           fields.push('account')
+          fields.push('domain')
         }
         return fields
       },
@@ -783,6 +783,7 @@ export default {
         var fields = ['name', 'id']
         if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
           fields.push('account')
+          fields.push('domain')
         }
         return fields
       },
@@ -854,6 +855,7 @@ export default {
         var fields = ['name', 'type', 'description']
         if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
           fields.push('account')
+          fields.push('domain')
         }
         return fields
       },
diff --git a/ui/src/config/section/domain.js b/ui/src/config/section/domain.js
index b11a2b3818c..a8648b10f76 100644
--- a/ui/src/config/section/domain.js
+++ b/ui/src/config/section/domain.js
@@ -44,12 +44,12 @@ export default {
       component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
     },
     {
-      name: 'resources',
+      name: 'limits',
       show: (record, route, user) => { return ['Admin', 'DomainAdmin'].includes(user.roletype) },
       component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceCountUsage.vue')))
     },
     {
-      name: 'limits',
+      name: 'limits.configure',
       show: (record, route, user) => { return ['Admin'].includes(user.roletype) || (['DomainAdmin'].includes(user.roletype) && record.id !== user.domainid) },
       component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceLimitTab.vue')))
     },
diff --git a/ui/src/config/section/image.js b/ui/src/config/section/image.js
index c95e0b96d3c..bf1083575dd 100644
--- a/ui/src/config/section/image.js
+++ b/ui/src/config/section/image.js
@@ -35,8 +35,17 @@ export default {
       resourceType: 'Template',
       filters: ['self', 'shared', 'featured', 'community'],
       columns: () => {
-        var fields = ['name', 'hypervisor', 'ostypename']
+        var fields = ['name',
+          {
+            state: (record) => {
+              if (record.isready) {
+                return 'Ready'
+              }
+              return 'Not Ready'
+            }
+          }, 'ostypename', 'hypervisor']
         if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
+          fields.push('size')
           fields.push('account')
         }
         if (['Admin'].includes(store.getters.userInfo.roletype)) {
@@ -45,8 +54,8 @@ export default {
         return fields
       },
       details: () => {
-        var fields = ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'format', 'ostypename', 'size', 'isready', 'passwordenabled',
-          'directdownload', 'deployasis', 'ispublic', 'isfeatured', 'isextractable', 'isdynamicallyscalable', 'crosszones', 'type',
+        var fields = ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'format', 'ostypename', 'size', 'physicalsize', 'isready', 'passwordenabled',
+          'crossZones', 'directdownload', 'deployasis', 'ispublic', 'isfeatured', 'isextractable', 'isdynamicallyscalable', 'crosszones', 'type',
           'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy']
         if (['Admin'].includes(store.getters.userInfo.roletype)) {
           fields.push('templatetype', 'url')
@@ -186,8 +195,17 @@ export default {
       resourceType: 'ISO',
       filters: ['self', 'shared', 'featured', 'community'],
       columns: () => {
-        var fields = ['name', 'ostypename']
+        var fields = ['name',
+          {
+            state: (record) => {
+              if (record.isready) {
+                return 'Ready'
+              }
+              return 'Not Ready'
+            }
+          }, 'ostypename']
         if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
+          fields.push('size')
           fields.push('account')
         }
         if (['Admin'].includes(store.getters.userInfo.roletype)) {
diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js
index 2f4b5b0f000..3105798b0a4 100644
--- a/ui/src/config/section/network.js
+++ b/ui/src/config/section/network.js
@@ -33,7 +33,7 @@ export default {
       permission: ['listNetworks'],
       resourceType: 'Network',
       columns: () => {
-        var fields = ['name', 'state', 'type', 'vpcname', 'cidr', 'ip6cidr', 'broadcasturi', 'domain', 'account', 'zonename']
+        var fields = ['name', 'state', 'type', 'vpcname', 'cidr', 'ip6cidr', 'broadcasturi', 'account', 'domain', 'zonename']
         if (!isAdmin()) {
           fields = fields.filter(function (e) { return e !== 'broadcasturi' })
         }
@@ -189,7 +189,7 @@ export default {
       docHelp: 'adminguide/networking_and_traffic.html#configuring-a-virtual-private-cloud',
       permission: ['listVPCs'],
       resourceType: 'Vpc',
-      columns: ['name', 'state', 'displaytext', 'cidr', 'account', 'zonename'],
+      columns: ['name', 'state', 'displaytext', 'cidr', 'account', 'domain', 'zonename'],
       details: ['name', 'id', 'displaytext', 'cidr', 'networkdomain', 'ip6routes', 'ispersistent', 'redundantvpcrouter', 'restartrequired', 'zonename', 'account', 'domain', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu'],
       searchFilters: ['name', 'zoneid', 'domainid', 'account', 'tags'],
       related: [{
@@ -319,8 +319,8 @@ export default {
       docHelp: 'adminguide/networking_and_traffic.html#reserving-public-ip-addresses-and-vlans-for-accounts',
       permission: ['listPublicIpAddresses'],
       resourceType: 'PublicIpAddress',
-      columns: ['ipaddress', 'state', 'associatednetworkname', 'virtualmachinename', 'allocated', 'account', 'zonename'],
-      details: ['ipaddress', 'id', 'associatednetworkname', 'virtualmachinename', 'networkid', 'issourcenat', 'isstaticnat', 'virtualmachinename', 'vmipaddress', 'vlan', 'allocated', 'account', 'zonename'],
+      columns: ['ipaddress', 'state', 'associatednetworkname', 'virtualmachinename', 'allocated', 'account', 'domain', 'zonename'],
+      details: ['ipaddress', 'id', 'associatednetworkname', 'virtualmachinename', 'networkid', 'issourcenat', 'isstaticnat', 'virtualmachinename', 'vmipaddress', 'vlan', 'allocated', 'account', 'domain', 'zonename'],
       filters: ['allocated', 'reserved', 'free'],
       component: shallowRef(() => import('@/views/network/PublicIpResource.vue')),
       tabs: [{
@@ -423,7 +423,7 @@ export default {
       icon: 'gateway-outlined',
       hidden: true,
       permission: ['listPrivateGateways'],
-      columns: ['ipaddress', 'state', 'gateway', 'netmask', 'account'],
+      columns: ['ipaddress', 'state', 'gateway', 'netmask', 'account', 'domain'],
       details: ['ipaddress', 'gateway', 'netmask', 'vlan', 'sourcenatsupported', 'aclname', 'account', 'domain', 'zone', 'associatednetwork', 'associatednetworkid'],
       tabs: [{
         name: 'details',
@@ -709,7 +709,7 @@ export default {
       title: 'label.vpncustomergatewayid',
       icon: 'lock-outlined',
       permission: ['listVpnCustomerGateways'],
-      columns: ['name', 'gateway', 'cidrlist', 'ipsecpsk', 'account'],
+      columns: ['name', 'gateway', 'cidrlist', 'ipsecpsk', 'account', 'domain'],
       details: ['name', 'id', 'gateway', 'cidrlist', 'ipsecpsk', 'ikepolicy', 'ikelifetime', 'ikeversion', 'esppolicy', 'esplifetime', 'dpd', 'splitconnections', 'forceencap', 'account', 'domain'],
       searchFilters: ['keyword', 'domainid', 'account'],
       resourceType: 'VPNCustomerGateway',
@@ -908,8 +908,8 @@ export default {
       permission: ['listGuestVlans'],
       resourceType: 'GuestVlan',
       filters: ['allocatedonly', 'all'],
-      columns: ['vlan', 'zonename', 'physicalnetworkname', 'allocationstate', 'taken', 'domain', 'account', 'project'],
-      details: ['vlan', 'zonename', 'physicalnetworkname', 'allocationstate', 'taken', 'domain', 'account', 'project', 'isdedicated'],
+      columns: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'zonename'],
+      details: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'isdedicated', 'zonename'],
       searchFilters: ['zoneid'],
       tabs: [{
         name: 'details',
diff --git a/ui/src/config/section/project.js b/ui/src/config/section/project.js
index a9bfb954647..a5370e28ef9 100644
--- a/ui/src/config/section/project.js
+++ b/ui/src/config/section/project.js
@@ -47,11 +47,11 @@ export default {
       }
     },
     {
-      name: 'resources',
+      name: 'limits',
       component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceCountUsage.vue')))
     },
     {
-      name: 'limits',
+      name: 'limits.configure',
       show: (record, route, user) => { return ['Admin', 'DomainAdmin'].includes(user.roletype) },
       component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceLimitTab.vue')))
     },
diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js
index e8a5ecd8128..920cdb5bbb6 100644
--- a/ui/src/config/section/storage.js
+++ b/ui/src/config/section/storage.js
@@ -38,7 +38,7 @@ export default {
         }
       },
       columns: () => {
-        const fields = ['name', 'state', 'type', 'vmname', 'sizegb']
+        const fields = ['name', 'state', 'sizegb', 'type', 'vmname']
         const metricsFields = ['diskkbsread', 'diskkbswrite', 'diskiopstotal']
 
         if (store.getters.userInfo.roletype === 'Admin') {
@@ -307,14 +307,15 @@ export default {
       permission: ['listSnapshots'],
       resourceType: 'Snapshot',
       columns: () => {
-        var fields = ['name', 'state', 'volumename', 'intervaltype', 'created']
+        var fields = ['name', 'state', 'volumename', 'intervaltype', 'physicalsize', 'created']
         if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
-          fields.push('domain')
           fields.push('account')
+          fields.push('domain')
         }
+        fields.push('zonename')
         return fields
       },
-      details: ['name', 'id', 'volumename', 'intervaltype', 'account', 'domain', 'created'],
+      details: ['name', 'id', 'volumename', 'volumetype', 'snapshottype', 'intervaltype', 'physicalsize', 'virtualsize', 'account', 'domain', 'created'],
       tabs: [
         {
           name: 'details',
@@ -380,8 +381,8 @@ export default {
       columns: () => {
         const fields = ['displayname', 'state', 'name', 'type', 'current', 'parentName', 'created']
         if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
-          fields.push('domain')
           fields.push('account')
+          fields.push('domain')
         }
         return fields
       },
@@ -446,7 +447,7 @@ export default {
       title: 'label.backup',
       icon: 'cloud-upload-outlined',
       permission: ['listBackups'],
-      columns: [{ name: (record) => { return record.virtualmachinename } }, 'virtualmachinename', 'status', 'type', 'created', 'account', 'zone'],
+      columns: [{ name: (record) => { return record.virtualmachinename } }, 'status', 'virtualmachinename', 'type', 'created', 'account', 'domain', 'zone'],
       details: ['virtualmachinename', 'id', 'type', 'externalid', 'size', 'virtualsize', 'volumes', 'backupofferingname', 'zone', 'account', 'domain', 'created'],
       actions: [
         {
diff --git a/ui/src/utils/request.js b/ui/src/utils/request.js
index 9b9ce273b13..02402398ff0 100644
--- a/ui/src/utils/request.js
+++ b/ui/src/utils/request.js
@@ -109,6 +109,8 @@ const err = (error) => {
         if (originalPath !== '/user/login') {
           router.push({ path: '/user/login', query: { redirect: originalPath } })
         }
+        store.commit('SET_COUNT_NOTIFY', 0)
+        notification.destroy()
       })
     }
     if (response.status === 404) {
diff --git a/ui/src/views/compute/CreateAutoScaleVmGroup.vue b/ui/src/views/compute/CreateAutoScaleVmGroup.vue
index 4dcb2955274..b764f8185e2 100644
--- a/ui/src/views/compute/CreateAutoScaleVmGroup.vue
+++ b/ui/src/views/compute/CreateAutoScaleVmGroup.vue
@@ -34,27 +34,28 @@
                   <div style="margin-top: 15px">
                     <span>{{ $t('message.select.a.zone') }}</span><br/>
                     <a-form-item :label="$t('label.zoneid')" name="zoneid" ref="zoneid">
-                      <div v-if="zones.length <= 8">
-                        <a-row type="flex" :gutter="5" justify="start">
+                      <div v-if="zones.length <= 9">
+                        <a-row type="flex" :gutter="[16,18]" justify="start">
                           <div v-for="(zoneItem, idx) in zones" :key="idx">
                             <a-radio-group
                               :key="idx"
+                              :size="large"
                               v-model:value="form.zoneid"
                               @change="onSelectZoneId(zoneItem.id)">
-                              <a-col :span="8">
-                                <a-card-grid style="width:200px;" :title="zoneItem.name" :hoverable="false">
-                                  <a-radio :value="zoneItem.id">
-                                    <div>
-                                      <resource-icon
-                                        v-if="zoneItem && zoneItem.icon && zoneItem.icon.base64image"
-                                        :image="zoneItem.icon.base64image"
-                                        size="36"
-                                        style="marginTop: -30px; marginLeft: 60px" />
-                                      <global-outlined v-else :style="{fontSize: '36px', marginLeft: '60px', marginTop: '-40px'}"/>
-                                    </div>
-                                  </a-radio>
-                                  <a-card-meta title="" :description="zoneItem.name" style="text-align:center; paddingTop: 10px;" />
-                                </a-card-grid>
+                              <a-col :span="6">
+                                <a-radio-button
+                                  :value="zoneItem.id"
+                                  style="border-width: 2px"
+                                  class="zone-radio-button">
+                                  <span>
+                                    <resource-icon
+                                      v-if="zoneItem && zoneItem.icon && zoneItem.icon.base64image"
+                                      :image="zoneItem.icon.base64image"
+                                      size="2x" />
+                                    <global-outlined size="2x" v-else />
+                                    {{ zoneItem.name }}
+                                    </span>
+                                </a-radio-button>
                               </a-col>
                             </a-radio-group>
                           </div>
@@ -2938,6 +2939,15 @@ export default {
     margin: 0 0 1.2rem;
   }
 
+  .zone-radio-button {
+    width:100%;
+    min-width: 225px;
+    height: 60px;
+    display: flex;
+    padding-left: 20px;
+    align-items: center;
+  }
+
   .vm-info-card {
     .ant-card-body {
       min-height: 250px;
diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue
index c6376969646..bb94c87db74 100644
--- a/ui/src/views/compute/DeployVM.vue
+++ b/ui/src/views/compute/DeployVM.vue
@@ -34,27 +34,28 @@
                   <div style="margin-top: 15px">
                     <span>{{ $t('message.select.a.zone') }}</span><br/>
                     <a-form-item :label="$t('label.zoneid')" name="zoneid" ref="zoneid">
-                      <div v-if="zones.length <= 8">
-                        <a-row type="flex" :gutter="5" justify="start">
+                      <div v-if="zones.length <= 9">
+                        <a-row type="flex" :gutter="[16, 18]" justify="start">
                           <div v-for="(zoneItem, idx) in zones" :key="idx">
                             <a-radio-group
                               :key="idx"
+                              :size="large"
                               v-model:value="form.zoneid"
                               @change="onSelectZoneId(zoneItem.id)">
-                              <a-col :span="8">
-                                <a-card-grid style="width:200px;" :title="zoneItem.name" :hoverable="false">
-                                  <a-radio :value="zoneItem.id">
-                                    <div>
-                                      <resource-icon
-                                        v-if="zoneItem && zoneItem.icon && zoneItem.icon.base64image"
-                                        :image="zoneItem.icon.base64image"
-                                        size="36"
-                                        style="marginTop: -30px; marginLeft: 60px" />
-                                      <global-outlined v-else :style="{fontSize: '36px', marginLeft: '60px', marginTop: '-40px'}"/>
-                                    </div>
-                                  </a-radio>
-                                  <a-card-meta title="" :description="zoneItem.name" style="text-align:center; paddingTop: 10px;" />
-                                </a-card-grid>
+                              <a-col :span="6">
+                                <a-radio-button
+                                  :value="zoneItem.id"
+                                  style="border-width: 2px"
+                                  class="zone-radio-button">
+                                  <span>
+                                    <resource-icon
+                                      v-if="zoneItem && zoneItem.icon && zoneItem.icon.base64image"
+                                      :image="zoneItem.icon.base64image"
+                                      size="2x" />
+                                    <global-outlined size="2x" v-else />
+                                    {{ zoneItem.name }}
+                                    </span>
+                                </a-radio-button>
                               </a-col>
                             </a-radio-group>
                           </div>
@@ -74,7 +75,7 @@
                       >
                         <a-select-option v-for="zone1 in zones" :key="zone1.id" :label="zone1.name">
                           <span>
-                            <resource-icon v-if="zone1.icon && zone1.icon.base64image" :image="zone1.icon.base64image" size="1x" style="margin-right: 5px"/>
+                            <resource-icon v-if="zone1.icon && zone1.icon.base64image" :image="zone1.icon.base64image" size="2x" style="margin-right: 5px"/>
                             <global-outlined v-else style="margin-right: 5px" />
                             {{ zone1.name }}
                           </span>
@@ -2695,6 +2696,15 @@ export default {
     margin: 0 0 1.2rem;
   }
 
+  .zone-radio-button {
+    width:100%;
+    min-width: 225px;
+    height: 60px;
+    display: flex;
+    padding-left: 20px;
+    align-items: center;
+  }
+
   .vm-info-card {
     .ant-card-body {
       min-height: 250px;
diff --git a/ui/src/views/compute/InstanceTab.vue b/ui/src/views/compute/InstanceTab.vue
index 2b1572b1c16..4ab7fb6f2d5 100644
--- a/ui/src/views/compute/InstanceTab.vue
+++ b/ui/src/views/compute/InstanceTab.vue
@@ -34,13 +34,21 @@
         <barcode-outlined /> {{ vm.isoid }}
       </a-tab-pane>
       <a-tab-pane :tab="$t('label.volumes')" key="volumes">
-        <volumes-tab :resource="vm" :items="volumes" :loading="loading" />
+        <a-button
+          type="primary"
+          style="width: 100%; margin-bottom: 10px"
+          @click="showAddVolModal"
+          :loading="loading"
+          :disabled="!('createVolume' in $store.getters.apis)">
+          <template #icon><plus-outlined /></template> {{ $t('label.action.create.volume.add') }}
+        </a-button>
+        <volumes-tab :resource="vm" :loading="loading" />
       </a-tab-pane>
       <a-tab-pane :tab="$t('label.nics')" key="nics" v-if="'listNics' in $store.getters.apis">
         <a-button
-          type="dashed"
+          type="primary"
           style="width: 100%; margin-bottom: 10px"
-          @click="showAddModal"
+          @click="showAddNicModal"
           :loading="loadingNic"
           :disabled="!('addNicToVirtualMachine' in $store.getters.apis)">
           <template #icon><plus-outlined /></template> {{ $t('label.network.addvm') }}
@@ -130,6 +138,16 @@
       </a-tab-pane>
     </a-tabs>
 
+    <a-modal
+      :visible="showAddVolumeModal"
+      :title="$t('label.action.create.volume.add')"
+      :maskClosable="false"
+      :closable="true"
+      :footer="null"
+      @cancel="closeModals">
+      <CreateVolume :resource="resource" @close-action="closeModals" />
+    </a-modal>
+
     <a-modal
       :visible="showAddNetworkModal"
       :title="$t('label.network.addvm')"
@@ -289,6 +307,7 @@ 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 CreateVolume from '@/views/storage/CreateVolume'
 import NicsTable from '@/views/network/NicsTable'
 import ListResourceTable from '@/components/view/ListResourceTable'
 import TooltipButton from '@/components/widgets/TooltipButton'
@@ -304,6 +323,7 @@ export default {
     StatsTab,
     EventsTab,
     DetailSettings,
+    CreateVolume,
     NicsTable,
     ListResourceTable,
     TooltipButton,
@@ -328,9 +348,11 @@ export default {
       vm: {},
       totalStorage: 0,
       currentTab: 'details',
+      showAddVolumeModal: false,
       showAddNetworkModal: false,
       showUpdateIpModal: false,
       showSecondaryIpModal: false,
+      diskOfferings: [],
       addNetworkData: {
         allNetworks: [],
         network: '',
@@ -408,6 +430,14 @@ export default {
         }
       })
     },
+    listDiskOfferings () {
+      api('listDiskOfferings', {
+        listAll: 'true',
+        zoneid: this.vm.zoneid
+      }).then(response => {
+        this.diskOfferings = response.listdiskofferingsresponse.diskoffering
+      })
+    },
     listNetworks () {
       api('listNetworks', {
         listAll: 'true',
@@ -456,11 +486,16 @@ export default {
         this.listIps.loading = false
       })
     },
-    showAddModal () {
+    showAddVolModal () {
+      this.showAddVolumeModal = true
+      this.listDiskOfferings()
+    },
+    showAddNicModal () {
       this.showAddNetworkModal = true
       this.listNetworks()
     },
     closeModals () {
+      this.showAddVolumeModal = false
       this.showAddNetworkModal = false
       this.showUpdateIpModal = false
       this.showSecondaryIpModal = false
diff --git a/ui/src/views/compute/wizard/TemplateIsoRadioGroup.vue b/ui/src/views/compute/wizard/TemplateIsoRadioGroup.vue
index 83c247f90bb..00fa2ef9a25 100644
--- a/ui/src/views/compute/wizard/TemplateIsoRadioGroup.vue
+++ b/ui/src/views/compute/wizard/TemplateIsoRadioGroup.vue
@@ -37,13 +37,15 @@
                 v-if="item.icon && item.icon.base64image"
                 class="radio-group__os-logo"
                 :image="item.icon.base64image"
-                size="1x" />
+                size="2x" />
               <os-logo
                 v-else
                 class="radio-group__os-logo"
+                size="2x"
                 :osId="item.ostypeid"
                 :os-name="item.osName" />
-              {{ item.displaytext }}&nbsp;
+              &nbsp;
+              {{ item.displaytext }}
             </a-radio>
           </a-radio-group>
         </a-list-item>
@@ -156,15 +158,11 @@ export default {
     margin: 0.5rem 0;
 
     :deep(.ant-radio) {
-      margin-right: 20px;
+      margin-right: 0px;
     }
 
     &__os-logo {
-      position: absolute;
-      top: 0;
-      left: 0;
-      margin-top: 2px;
-      margin-left: 23px;
+      margin-top: -4px;
     }
   }
 
diff --git a/ui/src/views/dashboard/CapacityDashboard.vue b/ui/src/views/dashboard/CapacityDashboard.vue
index 1dc67a70c89..2d7b31ed467 100644
--- a/ui/src/views/dashboard/CapacityDashboard.vue
+++ b/ui/src/views/dashboard/CapacityDashboard.vue
@@ -31,7 +31,7 @@
             }" >
             <a-select-option v-for="(zone, index) in zones" :key="index" :label="zone.name">
               <span>
-                <resource-icon v-if="zone.icon && zone.icon.base64image" :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/>
+                <resource-icon v-if="zone.icon && zone.icon.base64image" :image="zone.icon.base64image" size="2x" style="margin-right: 5px"/>
                 <global-outlined v-else style="margin-right: 5px" />
                 {{ zone.name }}
               </span>
diff --git a/ui/src/views/image/TemplateZones.vue b/ui/src/views/image/TemplateZones.vue
index d4811c0c944..a2f8834be6e 100644
--- a/ui/src/views/image/TemplateZones.vue
+++ b/ui/src/views/image/TemplateZones.vue
@@ -37,7 +37,7 @@
       :rowKey="record => record.zoneid">
       <template #zonename="{record}">
         <span v-if="fetchZoneIcon(record.zoneid)">
-          <resource-icon :image="zoneIcon" size="1x" style="margin-right: 5px"/>
+          <resource-icon :image="zoneIcon" size="2x" style="margin-right: 5px"/>
         </span>
         <global-outlined v-else style="margin-right: 5px" />
         <span> {{ record.zonename }} </span>
diff --git a/ui/src/views/storage/CreateVolume.vue b/ui/src/views/storage/CreateVolume.vue
index 7f11033c64a..e068e4a8072 100644
--- a/ui/src/views/storage/CreateVolume.vue
+++ b/ui/src/views/storage/CreateVolume.vue
@@ -35,7 +35,7 @@
           v-model:value="form.name"
           :placeholder="apiParams.name.description" />
       </a-form-item>
-      <a-form-item ref="zoneid" name="zoneid" v-if="!createVolumeFromSnapshot">
+      <a-form-item ref="zoneid" name="zoneid" v-if="!createVolumeFromVM && !createVolumeFromSnapshot">
         <template #label>
           <tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/>
         </template>
@@ -150,6 +150,9 @@ export default {
     }
   },
   computed: {
+    createVolumeFromVM () {
+      return this.$route.path.startsWith('/vm/')
+    },
     createVolumeFromSnapshot () {
       return this.$route.path.startsWith('/snapshot')
     }
@@ -192,7 +195,11 @@ export default {
     },
     fetchData () {
       this.loading = true
-      api('listZones', { showicon: true }).then(json => {
+      const params = { showicon: true }
+      if (this.createVolumeFromVM) {
+        params.id = this.resource.zoneid
+      }
+      api('listZones', params).then(json => {
         this.zones = json.listzonesresponse.zone || []
         this.form.zoneid = this.zones[0].id || ''
         this.fetchDiskOfferings(this.form.zoneid)
@@ -221,6 +228,12 @@ export default {
       this.formRef.value.validate().then(() => {
         const formRaw = toRaw(this.form)
         const values = this.handleRemoveFields(formRaw)
+        if (this.createVolumeFromVM) {
+          values.account = this.resource.account
+          values.domainid = this.resource.domainid
+          values.virtualmachineid = this.resource.id
+          values.zoneid = this.resource.zoneid
+        }
         if (this.createVolumeFromSnapshot) {
           values.snapshotid = this.resource.id
         }
@@ -231,6 +244,25 @@ export default {
             title: this.$t('message.success.create.volume'),
             description: values.name,
             successMessage: this.$t('message.success.create.volume'),
+            successMethod: (result) => {
+              this.closeModal()
+              if (this.createVolumeFromVM) {
+                const params = {}
+                params.id = result.jobresult.volume.id
+                params.virtualmachineid = this.resource.id
+                api('attachVolume', params).then(response => {
+                  this.$pollJob({
+                    jobId: response.attachvolumeresponse.jobid,
+                    title: this.$t('message.success.attach.volume'),
+                    description: values.name,
+                    successMessage: this.$t('message.attach.volume.success'),
+                    errorMessage: this.$t('message.attach.volume.failed'),
+                    loadingMessage: this.$t('message.attach.volume.progress'),
+                    catchMessage: this.$t('error.fetching.async.job.result')
+                  })
+                })
+              }
+            },
             errorMessage: this.$t('message.create.volume.failed'),
             loadingMessage: this.$t('message.create.volume.processing'),
             catchMessage: this.$t('error.fetching.async.job.result')
@@ -262,7 +294,8 @@ export default {
   width: 80vw;
 
   @media (min-width: 500px) {
-    width: 400px;
+    min-width: 400px;
+    width: 100%;
   }
 }
 </style>