| // 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> | |
| <div> | |
| <!-- GPU Device Management Buttons (before tabs, for Host resource type only) --> | |
| <div | |
| v-if="resourceType === 'Host'" | |
| style="margin-bottom: 16px;" | |
| > | |
| <a-space wrap> | |
| <!-- GPU Device Discovery --> | |
| <a-popconfirm | |
| :title="$t('message.confirm.discover.gpu.devices')" | |
| @confirm="discoverGpuDevices" | |
| okText="Yes" | |
| cancelText="No" | |
| > | |
| <a-button type="default"> | |
| {{ $t('label.discover.gpu.devices') }} | |
| </a-button> | |
| </a-popconfirm> | |
| <!-- Add GPU Device (Admin Only) --> | |
| <a-button | |
| v-if="isAdmin" | |
| type="primary" | |
| @click="showAddGpuDeviceModal" | |
| > | |
| {{ $t('label.gpu.devices.add') }} | |
| </a-button> | |
| </a-space> | |
| </div> | |
| <!-- For VMs: Show tabs only for admin, summary only for regular users --> | |
| <div v-if="resourceType === 'VirtualMachine' && !isAdmin"> | |
| <!-- Summary only for non-admin users viewing VMs --> | |
| <GPUSummaryTab | |
| ref="summaryTabSimple" | |
| :resource="resource" | |
| :resourceType="resourceType" | |
| :loading="loading" | |
| @refresh="$emit('refresh')" | |
| /> | |
| </div> | |
| <!-- For admins on VMs or all users on other resource types: Show tabs --> | |
| <a-tabs | |
| v-else | |
| defaultActiveKey="summary" | |
| :tabBarStyle="{ marginBottom: '16px' }" | |
| > | |
| <!-- Summary Tab --> | |
| <a-tab-pane | |
| key="summary" | |
| :tab="$t('label.gpu.summary')" | |
| > | |
| <GPUSummaryTab | |
| ref="summaryTab" | |
| :resource="resource" | |
| :resourceType="resourceType" | |
| :loading="loading" | |
| @refresh="$emit('refresh')" | |
| /> | |
| </a-tab-pane> | |
| <!-- Devices Tab --> | |
| <a-tab-pane | |
| key="devices" | |
| :tab="$t('label.gpu.devices')" | |
| > | |
| <GPUDevicesTab | |
| ref="devicesTab" | |
| :resource="resource" | |
| :resourceType="resourceType" | |
| :loading="loading" | |
| @refresh="refresh(true)" | |
| /> | |
| </a-tab-pane> | |
| </a-tabs> | |
| <!-- Add GPU Device Modal --> | |
| <a-modal | |
| :visible="addGpuDeviceModalVisible" | |
| :title="$t('label.gpu.devices.add')" | |
| @ok="addGpuDevice" | |
| @cancel="addGpuDeviceModalVisible = false" | |
| > | |
| <a-form layout="vertical"> | |
| <a-form-item | |
| v-for="field in createFormFields" | |
| :key="field.key" | |
| :label="field.label" | |
| :required="field.required" | |
| > | |
| <!-- Input field --> | |
| <a-input | |
| v-if="field.type === 'input'" | |
| v-model:value="gpuDeviceForm[field.key]" | |
| :placeholder="field.placeholder" | |
| /> | |
| <!-- Select field --> | |
| <a-select | |
| v-else-if="field.type === 'select'" | |
| v-model:value="gpuDeviceForm[field.key]" | |
| :placeholder="field.placeholder" | |
| :loading="field.loading" | |
| :show-search="field.showSearch" | |
| :filter-option="filterOption" | |
| :allow-clear="field.allowClear" | |
| @change="field.onChange" | |
| > | |
| <a-select-option | |
| v-for="option in field.options" | |
| :key="option.value" | |
| :value="option.value" | |
| > | |
| {{ option.label }} | |
| </a-select-option> | |
| </a-select> | |
| </a-form-item> | |
| </a-form> | |
| </a-modal> | |
| </div> | |
| </template> | |
| <script> | |
| import { getAPI, postAPI } from '@/api' | |
| import GPUSummaryTab from '@/components/view/GPUSummaryTab' | |
| import GPUDevicesTab from '@/components/view/GPUDevicesTab' | |
| export default { | |
| name: 'GPUTab', | |
| components: { | |
| GPUSummaryTab, | |
| GPUDevicesTab | |
| }, | |
| props: { | |
| resource: { | |
| type: Object, | |
| required: true | |
| }, | |
| loading: { | |
| type: Boolean, | |
| required: true | |
| }, | |
| resourceType: { | |
| type: String, | |
| required: true, | |
| default: 'Host' | |
| } | |
| }, | |
| data () { | |
| return { | |
| addGpuDeviceModalVisible: false, | |
| gpuDeviceForm: {}, | |
| gpuCards: [], | |
| vgpuProfiles: [], | |
| parentGpuDevices: [], | |
| loadingGpuCards: false, | |
| loadingVgpuProfiles: false, | |
| loadingParentDevices: false, | |
| createApiParams: {}, | |
| createFormFields: [] | |
| } | |
| }, | |
| computed: { | |
| isAdmin () { | |
| return this.$store.getters.userInfo.roletype === 'Admin' | |
| } | |
| }, | |
| created () { | |
| this.fetchApiParams() | |
| }, | |
| methods: { | |
| discoverGpuDevices () { | |
| if (!this.resource.id) { | |
| return | |
| } | |
| getAPI('discoverGpuDevices', { | |
| id: this.resource.id | |
| }).then(() => { | |
| this.$notification.success({ | |
| message: this.$t('label.success'), | |
| description: this.$t('message.success.discover.gpu.devices') | |
| }) | |
| this.refresh() | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| fetchApiParams () { | |
| this.createApiParams = this.$getApiParams('createGpuDevice') || {} | |
| this.generateFormFields() | |
| }, | |
| generateFormFields () { | |
| const fields = [] | |
| const fieldOrder = ['busaddress', 'gpucardid', 'vgpuprofileid', 'type', 'numanode', 'parentgpudeviceid'] | |
| fieldOrder.forEach(paramName => { | |
| if (paramName === 'hostid') return // Skip hostid as it's auto-populated | |
| const param = this.createApiParams[paramName] | |
| if (!param) return | |
| const field = { | |
| key: paramName, | |
| label: this.$t(`label.${paramName}`), | |
| required: param.required || ['busaddress', 'gpucardid', 'vgpuprofileid'].includes(paramName), | |
| placeholder: param.description, | |
| type: this.getFieldType(paramName, param) | |
| } | |
| // Add special configurations for dropdown fields | |
| if (field.type === 'select') { | |
| field.options = this.getFieldOptions(paramName) | |
| field.loading = this.getFieldLoading(paramName) | |
| field.showSearch = true | |
| field.allowClear = false | |
| if (paramName === 'gpucardid') { | |
| field.onChange = this.onGpuCardChange | |
| } | |
| } | |
| fields.push(field) | |
| }) | |
| this.createFormFields = fields | |
| }, | |
| getFieldType (paramName, param) { | |
| const selectFields = ['gpucardid', 'vgpuprofileid', 'parentgpudeviceid', 'type'] | |
| return selectFields.includes(paramName) ? 'select' : 'input' | |
| }, | |
| getFieldOptions (paramName) { | |
| switch (paramName) { | |
| case 'gpucardid': | |
| return this.gpuCards.map(card => ({ value: card.id, label: card.name })) | |
| case 'vgpuprofileid': | |
| return this.vgpuProfiles.map(profile => ({ value: profile.id, label: profile.name })) | |
| case 'parentgpudeviceid': | |
| return this.parentGpuDevices.map(device => ({ | |
| value: device.id, | |
| label: `${device.gpucardname} - ${device.busaddress}` | |
| })) | |
| case 'type': | |
| return [ | |
| { value: 'PCI', label: 'PCI' }, | |
| { value: 'MDEV', label: 'MDEV' }, | |
| { value: 'VGPUOnly', label: 'VGPUOnly' } | |
| ] | |
| default: | |
| return [] | |
| } | |
| }, | |
| getFieldLoading (paramName) { | |
| switch (paramName) { | |
| case 'gpucardid': | |
| return this.loadingGpuCards | |
| case 'vgpuprofileid': | |
| return this.loadingVgpuProfiles | |
| case 'parentgpudeviceid': | |
| return this.loadingParentDevices | |
| default: | |
| return false | |
| } | |
| }, | |
| onGpuCardChange (gpucardid) { | |
| // Clear the selected vGPU profile when GPU card changes | |
| this.gpuDeviceForm.vgpuprofileid = null | |
| // Fetch vGPU profiles for the selected GPU card | |
| if (gpucardid) { | |
| this.fetchVgpuProfiles(gpucardid) | |
| } else { | |
| this.vgpuProfiles = [] | |
| } | |
| }, | |
| fetchGpuCards () { | |
| this.loadingGpuCards = true | |
| getAPI('listGpuCards').then(json => { | |
| this.gpuCards = json?.listgpucardsresponse?.gpucard || [] | |
| this.generateFormFields() // Refresh form fields with new data | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }).finally(() => { | |
| this.loadingGpuCards = false | |
| }) | |
| }, | |
| fetchVgpuProfiles (gpucardid = null) { | |
| this.loadingVgpuProfiles = true | |
| const params = {} | |
| if (gpucardid) { | |
| params.gpucardid = gpucardid | |
| } | |
| getAPI('listVgpuProfiles', params).then(json => { | |
| this.vgpuProfiles = json?.listvgpuprofilesresponse?.vgpuprofile || [] | |
| this.generateFormFields() // Refresh form fields with new data | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }).finally(() => { | |
| this.loadingVgpuProfiles = false | |
| }) | |
| }, | |
| fetchParentGpuDevices () { | |
| if (!this.resource.id || this.resourceType !== 'Host') { | |
| return | |
| } | |
| this.loadingParentDevices = true | |
| getAPI('listGpuDevices', { hostid: this.resource.id }).then(json => { | |
| const devices = json?.listgpudevicesresponse?.gpudevice || [] | |
| // Only include devices that can be parent devices (not virtual GPU devices) | |
| this.parentGpuDevices = devices.filter(device => !device.parentgpudeviceid) | |
| this.generateFormFields() // Refresh form fields with new data | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }).finally(() => { | |
| this.loadingParentDevices = false | |
| }) | |
| }, | |
| filterOption (input, option) { | |
| return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 | |
| }, | |
| showAddGpuDeviceModal () { | |
| this.gpuDeviceForm = { | |
| type: 'PCI' // Set default type | |
| } | |
| this.vgpuProfiles = [] // Clear profiles initially | |
| this.fetchGpuCards() | |
| this.fetchParentGpuDevices() | |
| this.addGpuDeviceModalVisible = true | |
| }, | |
| addGpuDevice () { | |
| // Validate required fields | |
| if (!this.gpuDeviceForm.busaddress || !this.gpuDeviceForm.gpucardid || !this.gpuDeviceForm.vgpuprofileid) { | |
| this.$notification.warning({ | |
| message: this.$t('label.warning'), | |
| description: this.$t('message.please.fill.required.fields') | |
| }) | |
| return | |
| } | |
| const params = { | |
| hostid: this.resource.id, | |
| busaddress: this.gpuDeviceForm.busaddress, | |
| gpucardid: this.gpuDeviceForm.gpucardid, | |
| vgpuprofileid: this.gpuDeviceForm.vgpuprofileid | |
| } | |
| // Add optional parameters only if they have values | |
| if (this.gpuDeviceForm.type) { | |
| params.type = this.gpuDeviceForm.type | |
| } | |
| if (this.gpuDeviceForm.numanode) { | |
| params.numanode = this.gpuDeviceForm.numanode | |
| } | |
| if (this.gpuDeviceForm.parentgpudeviceid) { | |
| params.parentgpudeviceid = this.gpuDeviceForm.parentgpudeviceid | |
| } | |
| postAPI('createGpuDevice', params).then(() => { | |
| this.$notification.success({ | |
| message: this.$t('label.success'), | |
| description: this.$t('message.success.add.gpu.device') | |
| }) | |
| this.addGpuDeviceModalVisible = false | |
| this.refresh() | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| refresh (skipDevicesTab = false) { | |
| // Refresh summary tabs | |
| const summaryTab = this.$refs.summaryTab || this.$refs.summaryTabSimple | |
| if (summaryTab?.refresh) { | |
| summaryTab.refresh() | |
| } | |
| // Refresh devices tab only if not skipped | |
| if (!skipDevicesTab && this.$refs.devicesTab?.refresh) { | |
| this.$refs.devicesTab.refresh() | |
| } | |
| // Emit refresh to parent | |
| this.$emit('refresh') | |
| } | |
| } | |
| } | |
| </script> | |
| <style scoped> | |
| /* Background colors for GPU device types */ | |
| :deep(.parent-gpu-row) { | |
| background-color: #fafafa; | |
| } | |
| :deep(.vgpu-row) { | |
| background-color: #f0f8ff; | |
| } | |
| :deep(.parent-gpu-row:hover) { | |
| background-color: #f5f5f5 !important; | |
| } | |
| :deep(.vgpu-row:hover) { | |
| background-color: #e6f4ff !important; | |
| } | |
| /* Text truncation for long names */ | |
| :deep(.ant-table-tbody .ant-table-cell) { | |
| max-width: 200px; | |
| } | |
| :deep(.ant-table-tbody .ant-table-cell:has([data-column="gpucardname"]), | |
| .ant-table-tbody .ant-table-cell:has([data-column="vgpuprofilename"]), | |
| .ant-table-tbody .ant-table-cell:has([data-column="hostname"])) { | |
| max-width: 150px; | |
| } | |
| .text-ellipsis { | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| max-width: 100%; | |
| display: inline-block; | |
| } | |
| </style> |