| // 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> |