| // 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> | |
| <!-- Toolbar for bulk actions --> | |
| <div | |
| v-if="resourceType === 'Host'" | |
| style="margin-bottom: 16px;" | |
| > | |
| <a-space wrap> | |
| <!-- Bulk GPU Device Actions --> | |
| <a-popconfirm | |
| :title="$t('message.confirm.manage.gpu.devices', { count: selectedDeviceCount })" | |
| @confirm="bulkManageGpuDevices" | |
| :disabled="!hasSelectedDevices" | |
| okText="Yes" | |
| cancelText="No" | |
| > | |
| <a-button | |
| type="primary" | |
| :disabled="!hasSelectedDevices" | |
| > | |
| {{ $t('label.gpu.devices.manage') }} | |
| </a-button> | |
| </a-popconfirm> | |
| <a-popconfirm | |
| :title="$t('message.confirm.unmanage.gpu.devices', { count: selectedDeviceCount })" | |
| @confirm="bulkUnmanageGpuDevices" | |
| :disabled="!hasSelectedDevices" | |
| okText="Yes" | |
| cancelText="No" | |
| > | |
| <a-button | |
| type="primary" | |
| danger | |
| :disabled="!hasSelectedDevices" | |
| > | |
| {{ $t('label.gpu.devices.unmanage') }} | |
| </a-button> | |
| </a-popconfirm> | |
| <a-popconfirm | |
| :title="$t('message.confirm.delete.gpu.devices', { count: selectedDeviceCount })" | |
| @confirm="bulkDeleteGpuDevices" | |
| :disabled="!hasSelectedDevices" | |
| okText="Yes" | |
| cancelText="No" | |
| > | |
| <a-button | |
| v-if="isAdmin" | |
| type="primary" | |
| danger | |
| :disabled="!hasSelectedDevices" | |
| > | |
| {{ $t('label.gpu.devices.delete') }} | |
| </a-button> | |
| </a-popconfirm> | |
| </a-space> | |
| </div> | |
| <a-table | |
| :loading="loading" | |
| :columns="columns" | |
| :dataSource="items" | |
| :pagination="false" | |
| :rowKey="record => record.id" | |
| :childrenColumnName="'children'" | |
| :defaultExpandAllRows="true" | |
| :expandedRowKeys="expandedRowKeys" | |
| @expand="onExpand" | |
| :rowSelection="resourceType === 'Host' && isAdmin ? { | |
| selectedRowKeys: selectedGpuDeviceIds, | |
| onChange: onGpuDeviceSelectionChange | |
| } : null" | |
| :customRow="customRowProps" | |
| size="small" | |
| > | |
| <template #bodyCell="{ column, record }"> | |
| <template v-if="column.key === 'busaddress'"> | |
| <span :style="{ paddingLeft: record.parentgpudeviceid ? '20px' : '0px' }"> | |
| {{ record.busaddress }} | |
| </span> | |
| </template> | |
| <template v-else-if="column.key === 'virtualmachinename'"> | |
| <div style="display: flex; align-items: center; gap: 8px;"> | |
| <Status | |
| v-if="record.vmstate" | |
| :text="record.vmstate" | |
| :displayText="false" | |
| :vmState="true" | |
| /> | |
| <div v-if="record.virtualmachinename"> | |
| <router-link | |
| v-if="record.virtualmachineid" | |
| :to="{ path: '/vm/' + record.virtualmachineid }" | |
| > | |
| {{ record.virtualmachinename }} | |
| </router-link> | |
| <span v-else>{{ record.virtualmachinename }}</span> | |
| </div> | |
| <span v-else>{{ record.virtualmachinename }}</span> | |
| </div> | |
| </template> | |
| <template v-else-if="column.key === 'gpucardname'"> | |
| <router-link | |
| v-if="record.gpucardid" | |
| :to="{ path: '/gpucard/' + record.gpucardid }" | |
| :title="record.gpucardname" | |
| class="text-ellipsis" | |
| > | |
| {{ record.gpucardname }} | |
| </router-link> | |
| <span | |
| v-else | |
| :title="record.gpucardname" | |
| class="text-ellipsis" | |
| >{{ record.gpucardname }}</span> | |
| </template> | |
| <template v-else-if="column.key === 'vgpuprofilename'"> | |
| <span v-if="!(record.children && record.children.length > 0)"> | |
| <router-link | |
| v-if="record.vgpuprofileid" | |
| :to="{ path: '/vgpuprofile/' + record.vgpuprofileid }" | |
| :title="record.vgpuprofilename" | |
| class="text-ellipsis" | |
| > | |
| {{ record.vgpuprofilename }} | |
| </router-link> | |
| <span | |
| v-else | |
| :title="record.vgpuprofilename" | |
| class="text-ellipsis" | |
| >{{ record.vgpuprofilename }}</span> | |
| </span> | |
| <span v-else></span> | |
| </template> | |
| <template v-else-if="column.key === 'hostname'"> | |
| <router-link | |
| v-if="record.hostid" | |
| :to="{ path: '/host/' + record.hostid }" | |
| :title="record.hostname" | |
| class="text-ellipsis" | |
| > | |
| {{ record.hostname }} | |
| </router-link> | |
| <span | |
| v-else | |
| :title="record.hostname" | |
| class="text-ellipsis" | |
| >{{ record.hostname }}</span> | |
| </template> | |
| <template v-else-if="column.key === 'managedstate'"> | |
| <Status | |
| v-if="!record.children || record.children.length === 0" | |
| :text="record.managedstate" | |
| :displayText="true" | |
| /> | |
| <span v-else></span> | |
| </template> | |
| <template v-else-if="column.key === 'state'"> | |
| <Status | |
| v-if="!record.children || record.children.length === 0" | |
| :text="record.state" | |
| :displayText="true" | |
| /> | |
| <span v-else></span> | |
| </template> | |
| <template v-else-if="column.key === 'actions'"> | |
| <a-space v-if="!record.children || record.children.length === 0"> | |
| <!-- Manage/Unmanage Action --> | |
| <a-popconfirm | |
| v-if="record.managedstate && record.managedstate.toLowerCase() === 'unmanaged'" | |
| :title="$t('message.confirm.manage.gpu.devices')" | |
| @confirm="manageGpuDevice(record)" | |
| okText="Yes" | |
| cancelText="No" | |
| > | |
| <a-tooltip :title="$t('label.gpu.devices.manage')"> | |
| <a-button | |
| type="primary" | |
| size="small" | |
| shape="circle" | |
| > | |
| <template #icon><play-circle-outlined /></template> | |
| </a-button> | |
| </a-tooltip> | |
| </a-popconfirm> | |
| <a-popconfirm | |
| v-else-if="record.managedstate && record.managedstate.toLowerCase() === 'managed'" | |
| :title="$t('message.confirm.unmanage.gpu.devices')" | |
| @confirm="unmanageGpuDevice(record)" | |
| okText="Yes" | |
| cancelText="No" | |
| > | |
| <a-tooltip :title="$t('label.gpu.devices.unmanage')"> | |
| <a-button | |
| size="small" | |
| shape="circle" | |
| > | |
| <template #icon><pause-circle-outlined /></template> | |
| </a-button> | |
| </a-tooltip> | |
| </a-popconfirm> | |
| <!-- Edit Action --> | |
| <a-tooltip :title="$t('label.edit')"> | |
| <a-button | |
| type="primary" | |
| size="small" | |
| shape="circle" | |
| @click="showUpdateGpuDeviceModal(record)" | |
| > | |
| <template #icon><edit-outlined /></template> | |
| </a-button> | |
| </a-tooltip> | |
| <!-- Delete Action --> | |
| <a-popconfirm | |
| :title="$t('message.confirm.delete.gpu.devices')" | |
| @confirm="deleteGpuDevice(record)" | |
| okText="Yes" | |
| cancelText="No" | |
| > | |
| <a-tooltip :title="$t('label.delete')"> | |
| <a-button | |
| type="primary" | |
| danger | |
| size="small" | |
| shape="circle" | |
| > | |
| <template #icon><delete-outlined /></template> | |
| </a-button> | |
| </a-tooltip> | |
| </a-popconfirm> | |
| </a-space> | |
| </template> | |
| </template> | |
| <!-- Custom Filter Dropdown for Column Selection --> | |
| <template #customFilterDropdown="{ column }"> | |
| <div | |
| v-if="column.key === 'columnFilter'" | |
| style="padding: 8px; min-width: 200px;" | |
| > | |
| <div style="margin-bottom: 8px; font-weight: 500;">{{ $t('label.select.columns') }}</div> | |
| <div style="margin-bottom: 8px;"> | |
| <a-space> | |
| <a-button | |
| size="small" | |
| @click="selectAllColumns" | |
| > | |
| {{ $t('label.select.all') }} | |
| </a-button> | |
| <a-button | |
| size="small" | |
| @click="clearAllColumns" | |
| > | |
| {{ $t('label.clear.all') }} | |
| </a-button> | |
| </a-space> | |
| </div> | |
| <div style="max-height: 200px; overflow-y: auto;"> | |
| <div | |
| v-for="columnKey in columnKeys" | |
| :key="columnKey" | |
| style="margin-bottom: 4px;" | |
| > | |
| <a-checkbox | |
| :checked="selectedColumnKeys.includes(columnKey)" | |
| @change="updateSelectedColumns(columnKey)" | |
| > | |
| {{ $t('label.' + String(columnKey).toLowerCase()) }} | |
| </a-checkbox> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| </a-table> | |
| </div> | |
| <!-- Update GPU Device Modal --> | |
| <a-modal | |
| :visible="updateGpuDeviceModalVisible" | |
| :title="$t('label.update.gpu.device')" | |
| @ok="updateGpuDevice" | |
| @cancel="updateGpuDeviceModalVisible = false" | |
| > | |
| <a-form layout="vertical"> | |
| <a-form-item | |
| v-for="field in updateFormFields" | |
| :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> | |
| </template> | |
| <script> | |
| import { getAPI, postAPI } from '@/api' | |
| import { genericCompare } from '@/utils/sort.js' | |
| import Status from '@/components/widgets/Status' | |
| import { EditOutlined, DeleteOutlined, PlayCircleOutlined, PauseCircleOutlined } from '@ant-design/icons-vue' | |
| export default { | |
| name: 'GPUDevicesTab', | |
| components: { | |
| Status, | |
| EditOutlined, | |
| DeleteOutlined, | |
| PlayCircleOutlined, | |
| PauseCircleOutlined | |
| }, | |
| emits: ['refresh'], | |
| props: { | |
| resource: { | |
| type: Object, | |
| required: true | |
| }, | |
| loading: { | |
| type: Boolean, | |
| default: false | |
| }, | |
| resourceType: { | |
| type: String, | |
| required: true, | |
| default: 'Host' | |
| } | |
| }, | |
| data () { | |
| return { | |
| columnKeys: ['gpucardname', 'vgpuprofilename', 'managedstate', 'state'], | |
| selectedColumnKeys: [], | |
| columns: [], | |
| items: [], | |
| expandedRowKeys: [], | |
| selectedGpuDeviceIds: [], | |
| updateGpuDeviceModalVisible: false, | |
| selectedGpuDevice: null, | |
| gpuDeviceForm: {}, | |
| gpuCards: [], | |
| vgpuProfiles: [], | |
| parentGpuDevices: [], | |
| loadingGpuCards: false, | |
| loadingVgpuProfiles: false, | |
| loadingParentDevices: false, | |
| updateApiParams: {}, | |
| updateFormFields: [] | |
| } | |
| }, | |
| computed: { | |
| hasSelectedDevices () { | |
| return this.selectedGpuDeviceIds.length > 0 | |
| }, | |
| selectedDeviceCount () { | |
| return this.selectedGpuDeviceIds.length | |
| }, | |
| isAdmin () { | |
| return this.$store.getters.userInfo.roletype === 'Admin' | |
| } | |
| }, | |
| created () { | |
| this.fetchUpdateApiParams() | |
| this.selectedColumnKeys = this.columnKeys | |
| if (this.resourceType === 'VirtualMachine') { | |
| // For VMs: GPU card, GPU profile, BUS ID/uuid, VRAM/metadata, Host (admin only) | |
| this.columnKeys = ['gpucardname', 'vgpuprofilename'] | |
| if (this.$store.getters.userInfo.roletype === 'Admin') { | |
| this.columnKeys.push('hostname', 'busaddress', 'managedstate', 'state') | |
| } | |
| } else { | |
| // For other resource types (like Host) | |
| this.columnKeys = ['busaddress', 'gpucardname', 'vgpuprofilename', 'managedstate', 'state', 'virtualmachinename'] | |
| } | |
| this.selectedColumnKeys = this.columnKeys | |
| this.updateColumns() | |
| this.fetchDevicesData() | |
| }, | |
| watch: { | |
| resource: { | |
| handler () { | |
| this.fetchDevicesData() | |
| } | |
| } | |
| }, | |
| methods: { | |
| fetchDevicesData () { | |
| if (!this.resource.id) { | |
| return | |
| } | |
| // Reset expanded keys when fetching new data | |
| this.expandedRowKeys = [] | |
| const params = {} | |
| if (this.resourceType === 'Host') { | |
| params.hostid = this.resource.id | |
| } else if (this.resourceType === 'VirtualMachine') { | |
| params.virtualmachineid = this.resource.id | |
| } | |
| getAPI('listGpuDevices', params).then(json => { | |
| const devices = json?.listgpudevicesresponse?.gpudevice || [] | |
| this.items = this.buildGpuTree(devices) | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| buildGpuTree (devices) { | |
| // Separate parent devices and vGPUs | |
| const parentDevices = [] | |
| const vgpuDevices = [] | |
| for (const device of devices) { | |
| if (device.parentgpudeviceid) { | |
| vgpuDevices.push(device) | |
| } else { | |
| parentDevices.push(device) | |
| } | |
| } | |
| // Sort parent devices by busaddress | |
| parentDevices.sort((a, b) => { | |
| const busAddressA = a.busaddress || '' | |
| const busAddressB = b.busaddress || '' | |
| return busAddressA.localeCompare(busAddressB) | |
| }) | |
| // Group vGPUs by their parent ID | |
| const vgpusByParent = {} | |
| vgpuDevices.forEach(vgpu => { | |
| const parentId = vgpu.parentgpudeviceid | |
| if (!vgpusByParent[parentId]) { | |
| vgpusByParent[parentId] = [] | |
| } | |
| vgpusByParent[parentId].push(vgpu) | |
| }) | |
| // Sort vGPUs within each parent group by busaddress | |
| Object.keys(vgpusByParent).forEach(parentId => { | |
| vgpusByParent[parentId].sort((a, b) => { | |
| const busA = a.busaddress || '' | |
| const busB = b.busaddress || '' | |
| return busA.localeCompare(busB, undefined, { numeric: true }) | |
| }) | |
| }) | |
| // Build tree structure and collect parent IDs that have children | |
| const expandedKeys = [] | |
| const treeData = parentDevices.map(parent => { | |
| const children = vgpusByParent[parent.id] || [] | |
| if (children.length > 0) { | |
| expandedKeys.push(parent.id) | |
| return { | |
| ...parent, | |
| children: children | |
| } | |
| } | |
| return parent | |
| }) | |
| // Set expanded row keys for all parents with children | |
| this.expandedRowKeys = expandedKeys | |
| if (treeData.length === 0) { | |
| // Sort standalone vGPU devices by busaddress | |
| return vgpuDevices.sort((a, b) => { | |
| const busA = a.busaddress || '' | |
| const busB = b.busaddress || '' | |
| return busA.localeCompare(busB, undefined, { numeric: true }) | |
| }) | |
| } | |
| return treeData | |
| }, | |
| validateBulkOperation () { | |
| if (this.selectedGpuDeviceIds.length === 0) { | |
| this.$notification.warning({ | |
| message: this.$t('label.warning'), | |
| description: this.$t('message.please.select.gpu.devices') | |
| }) | |
| return false | |
| } | |
| return true | |
| }, | |
| handleBulkOperationSuccess (messageKey, count) { | |
| this.$notification.success({ | |
| message: this.$t('label.success'), | |
| description: this.$t(messageKey, { count }) | |
| }) | |
| this.selectedGpuDeviceIds = [] | |
| this.refresh() | |
| }, | |
| updateSelectedColumns (key) { | |
| if (this.selectedColumnKeys.includes(key)) { | |
| this.selectedColumnKeys = this.selectedColumnKeys.filter(x => x !== key) | |
| } else { | |
| this.selectedColumnKeys.push(key) | |
| } | |
| this.updateColumns() | |
| }, | |
| updateColumns () { | |
| this.columns = [] | |
| for (var columnKey of this.columnKeys) { | |
| if (!this.selectedColumnKeys.includes(columnKey)) continue | |
| this.columns.push({ | |
| key: columnKey, | |
| title: this.$t('label.' + String(columnKey).toLowerCase()), | |
| dataIndex: columnKey, | |
| sorter: (a, b) => { return genericCompare(a[columnKey] || '', b[columnKey] || '') } | |
| }) | |
| } | |
| // Add actions column for admin users (only for Host resource type) | |
| if (this.isAdmin && this.resourceType === 'Host') { | |
| this.columns.push({ | |
| key: 'actions', | |
| title: this.$t('label.actions'), | |
| width: 120, | |
| fixed: 'right' | |
| }) | |
| } | |
| // Add column filter as the last column | |
| this.columns.push({ | |
| key: 'columnFilter', | |
| title: null, | |
| dataIndex: 'columnFilter', | |
| customFilterDropdown: true, | |
| onFilter: () => true | |
| }) | |
| }, | |
| onGpuDeviceSelectionChange (keys) { | |
| this.selectedGpuDeviceIds = keys | |
| }, | |
| onExpand (expanded, record) { | |
| if (expanded) { | |
| if (!this.expandedRowKeys.includes(record.id)) { | |
| this.expandedRowKeys.push(record.id) | |
| } | |
| } else { | |
| this.expandedRowKeys = this.expandedRowKeys.filter(key => key !== record.id) | |
| } | |
| }, | |
| customRowProps (record) { | |
| return { | |
| class: record.parentgpudeviceid ? 'vgpu-row' : 'parent-gpu-row' | |
| } | |
| }, | |
| selectAllColumns () { | |
| this.selectedColumnKeys = this.columnKeys | |
| this.updateColumns() | |
| }, | |
| clearAllColumns () { | |
| this.selectedColumnKeys = [] | |
| this.updateColumns() | |
| }, | |
| bulkManageGpuDevices () { | |
| if (!this.validateBulkOperation()) return | |
| getAPI('manageGpuDevice', { | |
| ids: this.selectedGpuDeviceIds.join(',') | |
| }).then(() => { | |
| this.handleBulkOperationSuccess('message.success.manage.gpu.devices', this.selectedGpuDeviceIds.length) | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| bulkUnmanageGpuDevices () { | |
| if (!this.validateBulkOperation()) return | |
| getAPI('unmanageGpuDevice', { | |
| ids: this.selectedGpuDeviceIds.join(',') | |
| }).then(() => { | |
| this.handleBulkOperationSuccess('message.success.unmanage.gpu.devices', this.selectedGpuDeviceIds.length) | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| manageGpuDevice (record) { | |
| getAPI('manageGpuDevice', { | |
| ids: record.id | |
| }).then(() => { | |
| this.$notification.success({ | |
| message: this.$t('label.success'), | |
| description: this.$t('message.success.manage.gpu.devices') | |
| }) | |
| this.refresh() | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| unmanageGpuDevice (record) { | |
| getAPI('unmanageGpuDevice', { | |
| ids: record.id | |
| }).then(() => { | |
| this.$notification.success({ | |
| message: this.$t('label.success'), | |
| description: this.$t('message.success.unmanage.gpu.devices') | |
| }) | |
| this.refresh() | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| filterOption (input, option) { | |
| return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 | |
| }, | |
| fetchUpdateApiParams () { | |
| this.updateApiParams = this.$getApiParams('updateGpuDevice') || {} | |
| this.generateUpdateFormFields() | |
| }, | |
| generateUpdateFormFields () { | |
| // Generate update form fields | |
| this.updateFormFields = this.buildFormFieldsFromParams(this.updateApiParams, 'update') | |
| }, | |
| buildFormFieldsFromParams (apiParams, formType) { | |
| const fields = [] | |
| const fieldOrder = ['gpucardid', 'vgpuprofileid', 'type', 'parentgpudeviceid'] | |
| fieldOrder.forEach(paramName => { | |
| if (paramName === 'hostid') return // Skip hostid as it's auto-populated | |
| const param = apiParams[paramName] | |
| if (!param) return | |
| const field = { | |
| key: paramName, | |
| label: this.$t(`label.${paramName}`), | |
| required: param.required, | |
| 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 = true | |
| if (paramName === 'gpucardid') { | |
| field.onChange = this.onGpuCardChange | |
| } | |
| } | |
| fields.push(field) | |
| }) | |
| return 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.generateUpdateFormFields() // 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.generateUpdateFormFields() // 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.generateUpdateFormFields() // Refresh form fields with new data | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }).finally(() => { | |
| this.loadingParentDevices = false | |
| }) | |
| }, | |
| showUpdateGpuDeviceModal (record) { | |
| this.selectedGpuDevice = record | |
| this.gpuDeviceForm = { | |
| gpucardid: record.gpucardid, | |
| vgpuprofileid: record.vgpuprofileid, | |
| type: record.type, | |
| numanode: record.numanode, | |
| parentgpudeviceid: record.parentgpudeviceid | |
| } | |
| this.fetchGpuCards() | |
| // Fetch vGPU profiles for the selected card if available | |
| if (record.gpucardid) { | |
| this.fetchVgpuProfiles(record.gpucardid) | |
| } else { | |
| this.vgpuProfiles = [] | |
| } | |
| this.fetchParentGpuDevices() | |
| this.updateGpuDeviceModalVisible = true | |
| }, | |
| updateGpuDevice () { | |
| const params = { | |
| id: this.selectedGpuDevice.id | |
| } | |
| // Add only fields that have values | |
| if (this.gpuDeviceForm.gpucardid) { | |
| params.gpucardid = this.gpuDeviceForm.gpucardid | |
| } | |
| if (this.gpuDeviceForm.vgpuprofileid) { | |
| params.vgpuprofileid = this.gpuDeviceForm.vgpuprofileid | |
| } | |
| 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('updateGpuDevice', params).then(() => { | |
| this.$notification.success({ | |
| message: this.$t('label.success'), | |
| description: this.$t('message.success.update.gpu.device') | |
| }) | |
| this.updateGpuDeviceModalVisible = false | |
| this.refresh() | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| deleteGpuDevice (record) { | |
| this.deleteGpuDevices([record.id], 'message.success.delete.gpu.devices') | |
| }, | |
| bulkDeleteGpuDevices () { | |
| if (!this.validateBulkOperation()) return | |
| this.deleteGpuDevices(this.selectedGpuDeviceIds, 'message.success.delete.gpu.devices') | |
| }, | |
| deleteGpuDevices (deviceIds, successMessageKey) { | |
| postAPI('deleteGpuDevice', { | |
| ids: deviceIds.join(',') | |
| }).then(() => { | |
| this.$notification.success({ | |
| message: this.$t('label.success'), | |
| description: this.$t(successMessageKey, { count: deviceIds.length }) | |
| }) | |
| if (deviceIds.length > 1) { | |
| this.selectedGpuDeviceIds = [] | |
| } | |
| this.refresh() | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| refresh () { | |
| this.fetchDevicesData() | |
| 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> |