blob: 9576e70c8d58d762f9de1296749f526c77e73fc7 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<a-spin :spinning="loading">
<a-tabs
:activeKey="currentTab"
:tabPosition="device === 'mobile' ? 'top' : 'left'"
:animated="false"
@change="handleChangeTab">
<a-tab-pane :tab="$t('label.details')" key="details">
<DetailsTab :resource="dataResource" :loading="loading" />
</a-tab-pane>
<a-tab-pane :tab="$t('label.metrics')" key="stats">
<StatsTab :resource="resource"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.iso')" key="cdrom" v-if="vm.isoid">
<usb-outlined />
<router-link :to="{ path: '/iso/' + vm.isoid }">{{ vm.isoname }}</router-link> <br/>
<barcode-outlined /> {{ vm.isoid }}
</a-tab-pane>
<a-tab-pane :tab="$t('label.volumes')" key="volumes" v-if="'listVolumes' in $store.getters.apis">
<a-button
type="primary"
style="width: 100%; margin-bottom: 10px"
@click="showAddVolModal"
:loading="loading"
:disabled="!('createVolume' in $store.getters.apis) || this.vm.state === 'Error' || resource.hypervisor === 'External'">
<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.gpu')" key="gpu" v-if="dataResource.gpucardname">
<GPUTab
apiName="listGpuDevices"
:resource="dataResource"
:params="{virtualmachineid: dataResource.id}"
resourceType="VirtualMachine"
:columns="['gpucardname', 'vgpuprofilename', 'state'].concat($store.getters.userInfo.roletype === 'Admin' ? ['id', 'hostname'] : [])"
:routerlinks="(record) => { return { displayname: '/gpudevice/' + record.id } }"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.nics')" key="nics" v-if="'listNics' in $store.getters.apis">
<NicsTab :resource="vm"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.vm.snapshots')" key="vmsnapshots" v-if="'listVMSnapshot' in $store.getters.apis">
<ListResourceTable
apiName="listVMSnapshot"
:resource="dataResource"
:params="{virtualmachineid: dataResource.id}"
:columns="['displayname', 'state', 'type', 'created']"
:routerlinks="(record) => { return { displayname: '/vmsnapshot/' + record.id } }"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.backup')" key="backups" v-if="'listBackups' in $store.getters.apis">
<ListResourceTable
apiName="listBackups"
:resource="resource"
:params="{virtualmachineid: dataResource.id}"
:columns="['name', 'status', 'size', 'virtualsize', 'type', 'intervaltype', 'created']"
:routerlinks="(record) => { return { name: '/backup/' + record.id } }"
:showSearch="false"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.securitygroups')" key="securitygroups" v-if="dataResource.securitygroup && dataResource.securitygroup.length > 0 || $store.getters.showSecurityGroups">
<a-button
type="primary"
style="width: 100%; margin-bottom: 10px"
@click="showUpdateSGModal"
:loading="loading">
<template #icon><edit-outlined /></template> {{ $t('label.action.update.security.groups') }}
</a-button>
<ListResourceTable
:items="dataResource.securitygroup"
:columns="['name', 'description']"
:routerlinks="(record) => { return { name: '/securitygroups/' + record.id } }"
:showSearch="false"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.schedules')" key="schedules" v-if="'listVMSchedule' in $store.getters.apis">
<InstanceSchedules
:virtualmachine="vm"
:loading="loading"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.settings')" key="settings">
<DetailSettings :resource="dataResource" :loading="loading" />
</a-tab-pane>
<a-tab-pane :tab="$t('label.events')" key="events" v-if="'listEvents' in $store.getters.apis">
<events-tab :resource="dataResource" resourceType="VirtualMachine" :loading="loading" />
</a-tab-pane>
<a-tab-pane :tab="$t('label.annotations')" key="comments" v-if="'listAnnotations' in $store.getters.apis">
<AnnotationsTab
:resource="vm"
:items="annotations">
</AnnotationsTab>
</a-tab-pane>
</a-tabs>
<a-modal
:visible="showUpdateSecurityGroupsModal"
:title="$t('label.action.update.security.groups')"
:maskClosable="false"
:closable="true"
@ok="updateSecurityGroups"
@cancel="closeModals">
<security-group-selection
:zoneId="this.vm.zoneid"
:value="securitygroupids"
:loading="false"
:preFillContent="dataPreFill"
@select-security-group-item="($event) => updateSecurityGroupsSelection($event)"></security-group-selection>
</a-modal>
<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-spin>
</template>
<script>
import { getAPI, postAPI } from '@/api'
import { mixinDevice } from '@/utils/mixin.js'
import ResourceLayout from '@/layouts/ResourceLayout'
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 NicsTab from '@/views/network/NicsTab'
import InstanceSchedules from '@/views/compute/InstanceSchedules.vue'
import ListResourceTable from '@/components/view/ListResourceTable'
import TooltipButton from '@/components/widgets/TooltipButton'
import ResourceIcon from '@/components/view/ResourceIcon'
import AnnotationsTab from '@/components/view/AnnotationsTab'
import VolumesTab from '@/components/view/VolumesTab.vue'
import SecurityGroupSelection from '@views/compute/wizard/SecurityGroupSelection'
import GPUTab from '@/components/view/GPUTab.vue'
export default {
name: 'InstanceTab',
components: {
ResourceLayout,
DetailsTab,
StatsTab,
EventsTab,
DetailSettings,
CreateVolume,
NicsTab,
GPUTab,
InstanceSchedules,
ListResourceTable,
SecurityGroupSelection,
TooltipButton,
ResourceIcon,
AnnotationsTab,
VolumesTab
},
mixins: [mixinDevice],
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
inject: ['parentFetchData'],
data () {
return {
vm: {},
totalStorage: 0,
currentTab: 'details',
showUpdateSecurityGroupsModal: false,
showAddVolumeModal: false,
diskOfferings: [],
annotations: [],
dataResource: {},
dataPreFill: {},
securitygroupids: []
}
},
created () {
const self = this
this.dataResource = this.resource
this.vm = this.dataResource
this.fetchData()
window.addEventListener('popstate', function () {
self.setCurrentTab()
})
},
watch: {
resource: {
deep: true,
handler (newData, oldData) {
if (newData !== oldData) {
this.dataResource = newData
this.vm = this.dataResource
this.fetchData()
}
}
},
'$route.fullPath': function () {
this.setCurrentTab()
}
},
mounted () {
this.setCurrentTab()
},
methods: {
setCurrentTab () {
this.currentTab = this.$route.query.tab ? this.$route.query.tab : 'details'
},
handleChangeTab (e) {
this.currentTab = e
const query = Object.assign({}, this.$route.query)
query.tab = e
history.pushState(
{},
null,
'#' + this.$route.path + '?' + Object.keys(query).map(key => {
return (
encodeURIComponent(key) + '=' + encodeURIComponent(query[key])
)
}).join('&')
)
},
fetchData () {
this.annotations = []
if (!this.vm || !this.vm.id) {
return
}
getAPI('listAnnotations', { entityid: this.dataResource.id, entitytype: 'VM', annotationfilter: 'all' }).then(json => {
if (json.listannotationsresponse && json.listannotationsresponse.annotation) {
this.annotations = json.listannotationsresponse.annotation
}
})
},
listDiskOfferings () {
getAPI('listDiskOfferings', {
listAll: 'true',
zoneid: this.vm.zoneid
}).then(response => {
this.diskOfferings = response.listdiskofferingsresponse.diskoffering
})
},
showAddVolModal () {
this.showAddVolumeModal = true
this.listDiskOfferings()
},
showUpdateSGModal () {
this.loadingSG = true
if (this.vm.securitygroup && this.vm.securitygroup?.length > 0) {
this.securitygroupids = []
for (const sg of this.vm.securitygroup) {
this.securitygroupids.push(sg.id)
}
this.dataPreFill = { securitygroupids: this.securitygroupids }
}
this.showUpdateSecurityGroupsModal = true
this.loadingSG = false
},
closeModals () {
this.showAddVolumeModal = false
this.showUpdateSecurityGroupsModal = false
},
updateSecurityGroupsSelection (securitygroupids) {
this.securitygroupids = securitygroupids || []
},
updateSecurityGroups () {
postAPI('updateVirtualMachine', { id: this.vm.id, securitygroupids: this.securitygroupids.join(',') }).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.closeModals()
this.parentFetchData()
})
}
}
}
</script>
<style lang="scss" scoped>
.page-header-wrapper-grid-content-main {
width: 100%;
height: 100%;
min-height: 100%;
transition: 0.3s;
.vm-detail {
.svg-inline--fa {
margin-left: -1px;
margin-right: 8px;
}
span {
margin-left: 10px;
}
margin-bottom: 8px;
}
}
.list {
margin-top: 20px;
&__item {
display: flex;
flex-direction: column;
align-items: flex-start;
@media (min-width: 760px) {
flex-direction: row;
align-items: center;
}
}
}
.modal-form {
display: flex;
flex-direction: column;
&__label {
margin-top: 20px;
margin-bottom: 5px;
font-weight: bold;
&--no-margin {
margin-top: 0;
}
}
}
.actions {
display: flex;
flex-wrap: wrap;
button {
padding: 5px;
height: auto;
margin-bottom: 10px;
align-self: flex-start;
&:not(:last-child) {
margin-right: 10px;
}
}
}
.label {
font-weight: bold;
}
.attribute {
margin-bottom: 10px;
}
.ant-tag {
padding: 4px 10px;
height: auto;
margin-left: 5px;
}
.title {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
a {
margin-right: 30px;
margin-bottom: 10px;
}
.ant-tag {
margin-bottom: 10px;
}
&__details {
display: flex;
}
.tags {
margin-left: 10px;
}
}
.ant-list-item-meta-title {
margin-bottom: -10px;
}
.divider-small {
margin-top: 20px;
margin-bottom: 20px;
}
.list-item {
&:not(:first-child) {
padding-top: 25px;
}
}
</style>
<style scoped>
.wide-modal {
min-width: 50vw;
}
:deep(.ant-list-item) {
padding-top: 12px;
padding-bottom: 12px;
}
</style>