blob: 4ab7fb6f2d5719ef15a7341db23f9e57edd2610d [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">
<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="primary"
style="width: 100%; margin-bottom: 10px"
@click="showAddNicModal"
:loading="loadingNic"
:disabled="!('addNicToVirtualMachine' in $store.getters.apis)">
<template #icon><plus-outlined /></template> {{ $t('label.network.addvm') }}
</a-button>
<NicsTable :resource="vm" :loading="loading">
<template #actions="record">
<a-popconfirm
:title="$t('label.set.default.nic')"
@confirm="setAsDefault(record.nic)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
v-if="!record.nic.isdefault"
>
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.set.default.nic')"
:disabled="!('updateDefaultNicForVirtualMachine' in $store.getters.apis)"
icon="check-square-outlined" />
</a-popconfirm>
<tooltip-button
v-if="record.nic.type !== 'L2'"
tooltipPlacement="bottom"
:tooltip="$t('label.change.ip.address')"
icon="swap-outlined"
:disabled="!('updateVmNicIp' in $store.getters.apis)"
@onClick="onChangeIPAddress(record)" />
<tooltip-button
v-if="record.nic.type !== 'L2'"
tooltipPlacement="bottom"
:tooltip="$t('label.edit.secondary.ips')"
icon="environment-outlined"
:disabled="(!('addIpToNic' in $store.getters.apis) && !('addIpToNic' in $store.getters.apis))"
@onClick="onAcquireSecondaryIPAddress(record)" />
<a-popconfirm
:title="$t('message.network.removenic')"
@confirm="removeNIC(record.nic)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
v-if="!record.nic.isdefault"
>
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.action.remove.nic')"
:disabled="!('removeNicFromVirtualMachine' in $store.getters.apis)"
type="primary"
:danger="true"
icon="delete-outlined" />
</a-popconfirm>
</template>
</NicsTable>
</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="['id', 'status', 'type', 'created']"
:routerlinks="(record) => { return { id: '/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">
<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.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="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')"
:maskClosable="false"
:closable="true"
:footer="null"
@cancel="closeModals">
{{ $t('message.network.addvm.desc') }}
<a-form @finish="submitAddNetwork" v-ctrl-enter="submitAddNetwork">
<div class="modal-form">
<p class="modal-form__label">{{ $t('label.network') }}:</p>
<a-select
:value="addNetworkData.network"
@change="e => addNetworkData.network = e"
v-focus="true"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option
v-for="network in addNetworkData.allNetworks"
:key="network.id"
:value="network.id"
:label="network.name">
<span>
<resource-icon v-if="network.icon" :image="network.icon.base64image" size="1x" style="margin-right: 5px"/>
<apartment-outlined v-else style="margin-right: 5px" />
{{ network.name }}
</span>
</a-select-option>
</a-select>
<p class="modal-form__label">{{ $t('label.publicip') }}:</p>
<a-input v-model:value="addNetworkData.ip"></a-input>
</div>
<div :span="24" class="action-button">
<a-button @click="closeModals">{{ $t('label.cancel') }}</a-button>
<a-button type="primary" ref="submit" @click="submitAddNetwork">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</a-modal>
<a-modal
:visible="showUpdateIpModal"
:title="$t('label.change.ipaddress')"
:maskClosable="false"
:closable="true"
:footer="null"
@cancel="closeModals"
>
{{ $t('message.network.updateip') }}
<a-form @finish="submitUpdateIP" v-ctrl-enter="submitUpdateIP">
<div class="modal-form">
<p class="modal-form__label">{{ $t('label.publicip') }}:</p>
<a-select
v-if="editNicResource.type==='Shared'"
v-model:value="editIpAddressValue"
:loading="listIps.loading"
v-focus="editNicResource.type==='Shared'"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}">
<a-select-option v-for="ip in listIps.opts" :key="ip.ipaddress">
{{ ip.ipaddress }}
</a-select-option>
</a-select>
<a-input
v-else
v-model:value="editIpAddressValue"
v-focus="editNicResource.type!=='Shared'"></a-input>
</div>
<div :span="24" class="action-button">
<a-button @click="closeModals">{{ $t('label.cancel') }}</a-button>
<a-button type="primary" ref="submit" @click="submitUpdateIP">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</a-modal>
<a-modal
:visible="showSecondaryIpModal"
:title="$t('label.acquire.new.secondary.ip')"
:maskClosable="false"
:footer="null"
:closable="false"
class="wide-modal"
@cancel="closeModals"
>
<p>
{{ $t('message.network.secondaryip') }}
</p>
<a-divider />
<div v-ctrl-enter="submitSecondaryIP">
<div class="modal-form">
<p class="modal-form__label">{{ $t('label.publicip') }}:</p>
<a-select
v-if="editNicResource.type==='Shared'"
v-model:value="newSecondaryIp"
:loading="listIps.loading"
v-focus="editNicResource.type==='Shared'"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}">
<a-select-option v-for="ip in listIps.opts" :key="ip.ipaddress">
{{ ip.ipaddress }}
</a-select-option>
</a-select>
<a-input
v-else
:placeholder="$t('label.new.secondaryip.description')"
v-model:value="newSecondaryIp"
v-focus="editNicResource.type!=='Shared'"></a-input>
</div>
<div style="margin-top: 10px; display: flex; justify-content:flex-end;">
<a-button @click="submitSecondaryIP" ref="submit" type="primary" style="margin-right: 10px;">{{ $t('label.add.secondary.ip') }}</a-button>
<a-button @click="closeModals">{{ $t('label.close') }}</a-button>
</div>
</div>
<a-divider />
<a-list itemLayout="vertical">
<a-list-item v-for="(ip, index) in secondaryIPs" :key="index">
<a-popconfirm
:title="`${$t('label.action.release.ip')}?`"
@confirm="removeSecondaryIP(ip.id)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')"
>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.action.release.ip')"
type="primary"
:danger="true"
icon="delete-outlined" />
{{ ip.ipaddress }}
</a-popconfirm>
</a-list-item>
</a-list>
</a-modal>
</a-spin>
</template>
<script>
import { api } 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 NicsTable from '@/views/network/NicsTable'
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'
export default {
name: 'InstanceTab',
components: {
ResourceLayout,
DetailsTab,
StatsTab,
EventsTab,
DetailSettings,
CreateVolume,
NicsTable,
ListResourceTable,
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',
showAddVolumeModal: false,
showAddNetworkModal: false,
showUpdateIpModal: false,
showSecondaryIpModal: false,
diskOfferings: [],
addNetworkData: {
allNetworks: [],
network: '',
ip: ''
},
loadingNic: false,
editIpAddressNic: '',
editIpAddressValue: '',
editNetworkId: '',
secondaryIPs: [],
selectedNicId: '',
newSecondaryIp: '',
editNicResource: {},
listIps: {
loading: false,
opts: []
},
annotations: [],
dataResource: {}
}
},
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
}
api('listAnnotations', { entityid: this.dataResource.id, entitytype: 'VM', annotationfilter: 'all' }).then(json => {
if (json.listannotationsresponse && json.listannotationsresponse.annotation) {
this.annotations = json.listannotationsresponse.annotation
}
})
},
listDiskOfferings () {
api('listDiskOfferings', {
listAll: 'true',
zoneid: this.vm.zoneid
}).then(response => {
this.diskOfferings = response.listdiskofferingsresponse.diskoffering
})
},
listNetworks () {
api('listNetworks', {
listAll: 'true',
showicon: true,
zoneid: this.vm.zoneid
}).then(response => {
this.addNetworkData.allNetworks = response.listnetworksresponse.network.filter(network => !this.vm.nic.map(nic => nic.networkid).includes(network.id))
this.addNetworkData.network = this.addNetworkData.allNetworks[0].id
})
},
fetchSecondaryIPs (nicId) {
this.showSecondaryIpModal = true
this.selectedNicId = nicId
api('listNics', {
nicId: nicId,
keyword: '',
virtualmachineid: this.vm.id
}).then(response => {
this.secondaryIPs = response.listnicsresponse.nic[0].secondaryip
})
},
fetchPublicIps (networkid) {
this.listIps.loading = true
this.listIps.opts = []
api('listPublicIpAddresses', {
networkid: networkid,
allocatedonly: false,
forvirtualnetwork: false
}).then(json => {
const listPublicIps = json.listpublicipaddressesresponse.publicipaddress || []
listPublicIps.forEach(item => {
if (item.state === 'Free') {
this.listIps.opts.push({
ipaddress: item.ipaddress
})
}
})
this.listIps.opts.sort(function (a, b) {
const currentIp = a.ipaddress.replaceAll('.', '')
const nextIp = b.ipaddress.replaceAll('.', '')
if (parseInt(currentIp) < parseInt(nextIp)) { return -1 }
if (parseInt(currentIp) > parseInt(nextIp)) { return 1 }
return 0
})
}).finally(() => {
this.listIps.loading = false
})
},
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
this.addNetworkData.network = ''
this.addNetworkData.ip = ''
this.editIpAddressValue = ''
this.newSecondaryIp = ''
},
onChangeIPAddress (record) {
this.editNicResource = record.nic
this.editIpAddressNic = record.nic.id
this.showUpdateIpModal = true
if (record.nic.type === 'Shared') {
this.fetchPublicIps(record.nic.networkid)
}
},
onAcquireSecondaryIPAddress (record) {
if (record.nic.type === 'Shared') {
this.fetchPublicIps(record.nic.networkid)
} else {
this.listIps.opts = []
}
this.editNicResource = record.nic
this.editNetworkId = record.nic.networkid
this.fetchSecondaryIPs(record.nic.id)
},
submitAddNetwork () {
if (this.loadingNic) return
const params = {}
params.virtualmachineid = this.vm.id
params.networkid = this.addNetworkData.network
if (this.addNetworkData.ip) {
params.ipaddress = this.addNetworkData.ip
}
this.showAddNetworkModal = false
this.loadingNic = true
api('addNicToVirtualMachine', params).then(response => {
this.$pollJob({
jobId: response.addnictovirtualmachineresponse.jobid,
successMessage: this.$t('message.success.add.network'),
successMethod: () => {
this.loadingNic = false
this.closeModals()
},
errorMessage: this.$t('message.add.network.failed'),
errorMethod: () => {
this.loadingNic = false
this.closeModals()
},
loadingMessage: this.$t('message.add.network.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.loadingNic = false
this.closeModals()
this.parentFetchData()
}
})
}).catch(error => {
this.$notifyError(error)
this.loadingNic = false
})
},
setAsDefault (item) {
this.loadingNic = true
api('updateDefaultNicForVirtualMachine', {
virtualmachineid: this.vm.id,
nicid: item.id
}).then(response => {
this.$pollJob({
jobId: response.updatedefaultnicforvirtualmachineresponse.jobid,
successMessage: `${this.$t('label.success.set')} ${item.networkname} ${this.$t('label.as.default')}. ${this.$t('message.set.default.nic.manual')}.`,
successMethod: () => {
this.loadingNic = false
},
errorMessage: `${this.$t('label.error.setting')} ${item.networkname} ${this.$t('label.as.default')}`,
errorMethod: () => {
this.loadingNic = false
},
loadingMessage: `${this.$t('label.setting')} ${item.networkname} ${this.$t('label.as.default')}...`,
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.loadingNic = false
this.parentFetchData()
}
})
}).catch(error => {
this.$notifyError(error)
this.loadingNic = false
})
},
submitUpdateIP () {
if (this.loadingNic) return
this.loadingNic = true
this.showUpdateIpModal = false
const params = {
nicId: this.editIpAddressNic
}
if (this.editIpAddressValue) {
params.ipaddress = this.editIpAddressValue
}
api('updateVmNicIp', params).then(response => {
this.$pollJob({
jobId: response.updatevmnicipresponse.jobid,
successMessage: this.$t('message.success.update.ipaddress'),
successMethod: () => {
this.loadingNic = false
this.closeModals()
},
errorMessage: this.$t('label.error'),
errorMethod: () => {
this.loadingNic = false
this.closeModals()
},
loadingMessage: this.$t('message.update.ipaddress.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.loadingNic = false
this.closeModals()
this.parentFetchData()
}
})
})
.catch(error => {
this.$notifyError(error)
this.loadingNic = false
})
},
removeNIC (item) {
this.loadingNic = true
api('removeNicFromVirtualMachine', {
nicid: item.id,
virtualmachineid: this.vm.id
}).then(response => {
this.$pollJob({
jobId: response.removenicfromvirtualmachineresponse.jobid,
successMessage: this.$t('message.success.remove.nic'),
successMethod: () => {
this.loadingNic = false
},
errorMessage: this.$t('message.error.remove.nic'),
errorMethod: () => {
this.loadingNic = false
},
loadingMessage: this.$t('message.remove.nic.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.loadingNic = false
this.parentFetchData()
}
})
})
.catch(error => {
this.$notifyError(error)
this.loadingNic = false
})
},
submitSecondaryIP () {
if (this.loadingNic) return
this.loadingNic = true
const params = {}
params.nicid = this.selectedNicId
if (this.newSecondaryIp) {
params.ipaddress = this.newSecondaryIp
}
api('addIpToNic', params).then(response => {
this.$pollJob({
jobId: response.addiptovmnicresponse.jobid,
successMessage: this.$t('message.success.add.secondary.ipaddress'),
successMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
},
errorMessage: this.$t('message.error.add.secondary.ipaddress'),
errorMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
},
loadingMessage: this.$t('message.add.secondary.ipaddress.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
this.parentFetchData()
}
})
}).catch(error => {
this.$notifyError(error)
this.loadingNic = false
}).finally(() => {
this.newSecondaryIp = null
this.fetchPublicIps(this.editNetworkId)
})
},
removeSecondaryIP (id) {
this.loadingNic = true
api('removeIpFromNic', { id }).then(response => {
this.$pollJob({
jobId: response.removeipfromnicresponse.jobid,
successMessage: this.$t('message.success.remove.secondary.ipaddress'),
successMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
this.fetchPublicIps(this.editNetworkId)
},
errorMessage: this.$t('message.error.remove.secondary.ipaddress'),
errorMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
},
loadingMessage: this.$t('message.remove.secondary.ipaddress.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
this.parentFetchData()
}
})
}).catch(error => {
this.$notifyError(error)
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
})
}
}
}
</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>