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>
+ <copy-label :label="eth.ip6address ? eth.ipaddress + ', ' + eth.ip6address : eth.ipaddress" />
<a-tag v-if="eth.isdefault">
{{ $t('label.default') }}
- </a-tag >
+ </a-tag ><br/>
+ <span v-if="!isStatic && eth.networkname && eth.networkid">
+
+ <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">
<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 }}
+
+ {{ 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>