mirror of
				https://github.com/apache/cloudstack.git
				synced 2025-10-26 08:42:29 +01:00 
			
		
		
		
	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 <rohit.yadav@shapeblue.com>
This commit is contained in:
		
							parent
							
								
									29c7b3167e
								
							
						
					
					
						commit
						feb9509547
					
				| @ -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.", | ||||
|  | ||||
| @ -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" /> | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
							
								
								
									
										51
									
								
								ui/src/components/widgets/CopyLabel.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								ui/src/components/widgets/CopyLabel.vue
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||
| @ -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'))) | ||||
|     }, | ||||
|  | ||||
| @ -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 | ||||
|       }, | ||||
|  | ||||
| @ -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'))) | ||||
|     }, | ||||
|  | ||||
| @ -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)) { | ||||
|  | ||||
| @ -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', | ||||
|  | ||||
| @ -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'))) | ||||
|     }, | ||||
|  | ||||
| @ -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: [ | ||||
|         { | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user