| // 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="attachedIsos.length > 0"> | |
| <div v-for="iso in attachedIsos" :key="iso.id" style="margin-bottom: 12px;"> | |
| <usb-outlined /> | |
| <router-link :to="{ path: '/iso/' + iso.id }">{{ iso.displaytext || iso.name }}</router-link> | |
| <a-tag style="margin-left: 8px;">{{ slotLabel(iso.deviceseq) }}</a-tag> | |
| <a-tag v-if="iso.bootable" color="blue" style="margin-left: 4px;">{{ $t('label.bootable') }}</a-tag> | |
| <br/> | |
| <barcode-outlined /> {{ iso.id }} | |
| </div> | |
| </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="dataResource.backupprovider === 'kboss' | |
| ? ['name', 'status', 'compressionstatus', 'validationstatus', 'size', 'virtualsize', 'type', 'intervaltype', 'created'] | |
| : ['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="'listResourceSchedule' in $store.getters.apis && !dataResource.autoscalevmgroupid" | |
| > | |
| <ResourceSchedules | |
| :resource="vm" | |
| resourceType="VirtualMachine" | |
| :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 ResourceSchedules from '@/views/compute/ResourceSchedules.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, | |
| ResourceSchedules, | |
| 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() | |
| }, | |
| computed: { | |
| attachedIsos () { | |
| if (this.vm.isos && this.vm.isos.length > 0) { | |
| return [...this.vm.isos].sort((a, b) => (a.deviceseq || 0) - (b.deviceseq || 0)) | |
| } | |
| if (this.vm.isoid) { | |
| return [{ | |
| id: this.vm.isoid, | |
| name: this.vm.isoname, | |
| displaytext: this.vm.isodisplaytext, | |
| deviceseq: 3 | |
| }] | |
| } | |
| return [] | |
| } | |
| }, | |
| methods: { | |
| slotLabel (deviceseq) { | |
| // 3 -> hdc, 4 -> hdd, ... matches LibvirtVMDef.getDevLabel for the IDE bus on KVM. | |
| if (typeof deviceseq !== 'number') return '' | |
| return 'hd' + String.fromCharCode('a'.charCodeAt(0) + deviceseq - 1) | |
| }, | |
| 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> |