| // 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> | |
| <a-button | |
| type="primary" | |
| @click="createVgpuProfile" | |
| > | |
| {{ $t('label.add.vgpu.profile') }} | |
| </a-button> | |
| <list-view | |
| :tabLoading="tabLoading" | |
| :columns="columns" | |
| :items="items" | |
| :columnKeys="columnKeys" | |
| :selectedColumns="selectedColumnKeys" | |
| ref="listview" | |
| @update-selected-columns="updateSelectedColumns" | |
| @refresh="this.fetchData" | |
| > | |
| <template #actionButtons="{ record }"> | |
| <a-space> | |
| <!-- Edit Action --> | |
| <a-tooltip :title="$t('label.edit')"> | |
| <a-button | |
| type="primary" | |
| size="small" | |
| shape="circle" | |
| @click="editVgpuProfile(record)" | |
| > | |
| <template #icon><edit-outlined /></template> | |
| </a-button> | |
| </a-tooltip> | |
| <!-- Delete Action --> | |
| <a-popconfirm | |
| :title="$t('message.confirm.delete.vgpu.profile')" | |
| @confirm="deleteVgpuProfile(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> | |
| </list-view> | |
| <!-- Create/Update VGPU Profile Modal --> | |
| <a-modal | |
| :visible="showModal" | |
| :title="isEditing ? $t('label.update.vgpu.profile') : $t('label.add.vgpu.profile')" | |
| :closable="true" | |
| :maskClosable="false" | |
| :footer="null" | |
| @cancel="closeModal" | |
| centered | |
| width="450px" | |
| > | |
| <a-spin | |
| :spinning="actionLoading" | |
| v-ctrl-enter="handleSubmit" | |
| > | |
| <a-form | |
| ref="vgpuForm" | |
| :model="form" | |
| :rules="rules" | |
| layout="vertical" | |
| @finish="handleSubmit" | |
| > | |
| <a-form-item | |
| v-for="field in formFields" | |
| :key="field.key" | |
| :name="field.key" | |
| :ref="field.key" | |
| :label="field.label" | |
| > | |
| <!-- Input field --> | |
| <a-input | |
| v-if="field.type === 'input'" | |
| v-model:value="form[field.key]" | |
| :placeholder="field.placeholder" | |
| :v-focus="field.key === 'name'" | |
| /> | |
| <!-- Number input field --> | |
| <a-input-number | |
| v-else-if="field.type === 'number'" | |
| style="width: 100%" | |
| v-model:value="form[field.key]" | |
| :min="field.min" | |
| :placeholder="field.placeholder" | |
| /> | |
| </a-form-item> | |
| <div | |
| :span="24" | |
| class="action-button" | |
| > | |
| <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button> | |
| <a-button | |
| type="primary" | |
| ref="submit" | |
| @click="handleSubmit" | |
| >{{ $t('label.ok') }}</a-button> | |
| </div> | |
| </a-form> | |
| </a-spin> | |
| </a-modal> | |
| </div> | |
| </template> | |
| <script> | |
| import { reactive, toRaw } from 'vue' | |
| import { getAPI, postAPI } from '@/api' | |
| import { genericCompare } from '@/utils/sort.js' | |
| import ListView from '@/components/view/ListView' | |
| import { EditOutlined, DeleteOutlined } from '@ant-design/icons-vue' | |
| export default { | |
| name: 'VgpuProfilesTab', | |
| components: { | |
| ListView, | |
| EditOutlined, | |
| DeleteOutlined | |
| }, | |
| props: { | |
| resource: { | |
| type: Object, | |
| required: true | |
| }, | |
| loading: { | |
| type: Boolean, | |
| required: true | |
| } | |
| }, | |
| data () { | |
| return { | |
| tabLoading: false, | |
| columnKeys: ['name', 'videoram', 'maxheads', 'resolution', 'maxvgpuperphysicalgpu'], | |
| selectedColumnKeys: [], | |
| columns: [], | |
| cols: [], | |
| items: [], | |
| showModal: false, | |
| isEditing: false, | |
| selectedVgpuProfile: null, | |
| actionLoading: false, | |
| form: {}, | |
| rules: {}, | |
| formFields: [], | |
| createApiParams: {}, | |
| updateApiParams: {} | |
| } | |
| }, | |
| computed: { | |
| isAdmin () { | |
| return this.$store.getters.userInfo.roletype === 'Admin' | |
| } | |
| }, | |
| created () { | |
| this.fetchApiParams() | |
| this.selectedColumnKeys = this.columnKeys | |
| this.updateColumns() | |
| this.fetchData() | |
| }, | |
| methods: { | |
| fetchApiParams () { | |
| this.createApiParams = this.$getApiParams('createVgpuProfile') || {} | |
| this.updateApiParams = this.$getApiParams('updateVgpuProfile') || {} | |
| }, | |
| generateFormFields (isEdit = false) { | |
| const apiParams = isEdit ? this.updateApiParams : this.createApiParams | |
| const fields = [] | |
| const fieldOrder = ['name', 'description', 'videoram', 'maxheads', 'maxresolutionx', 'maxresolutiony', 'maxvgpuperphysicalgpu'] | |
| fieldOrder.forEach(paramName => { | |
| if (paramName === 'gpucardid' && !isEdit) return // Skip gpucardid for create as it's auto-populated | |
| const param = apiParams[paramName] | |
| if (!param && !isEdit) return // For create, only include params that exist in API | |
| const field = { | |
| key: paramName, | |
| label: this.$t(`label.${paramName}`), | |
| required: param?.required || (paramName === 'name' || paramName === 'maxvgpuperphysicalgpu'), | |
| placeholder: param?.description || this.$t(`label.${paramName}`), | |
| type: this.getFieldType(paramName, param) | |
| } | |
| fields.push(field) | |
| }) | |
| this.formFields = fields | |
| }, | |
| getFieldType (paramName, param) { | |
| const numberFields = ['maxvgpuperphysicalgpu'] | |
| const selectFields = [] | |
| if (numberFields.includes(paramName)) { | |
| return 'number' | |
| } else if (selectFields.includes(paramName)) { | |
| return 'select' | |
| } else { | |
| return 'input' | |
| } | |
| }, | |
| initForm (isEdit = false, record = null) { | |
| const formData = { | |
| name: '', | |
| description: '', | |
| maxvgpuperphysicalgpu: 1 | |
| } | |
| if (isEdit && record) { | |
| formData.name = record.name || '' | |
| formData.description = record.description || '' | |
| formData.maxvgpuperphysicalgpu = record.maxvgpuperphysicalgpu || 1 | |
| formData.videoram = record.videoram || 0 | |
| formData.maxheads = record.maxheads || 0 | |
| formData.maxresolutionx = record.maxresolutionx || 0 | |
| formData.maxresolutiony = record.maxresolutiony || 0 | |
| } | |
| this.form = reactive(formData) | |
| const validationRules = { | |
| name: [{ required: true, message: this.$t('message.error.required.input') }], | |
| maxvgpuperphysicalgpu: [{ required: true, message: this.$t('message.error.required.input') }] | |
| } | |
| this.rules = reactive(validationRules) | |
| }, | |
| createVgpuProfile () { | |
| this.isEditing = false | |
| this.selectedVgpuProfile = null | |
| this.generateFormFields(false) | |
| this.initForm(false) | |
| this.showModal = true | |
| }, | |
| editVgpuProfile (record) { | |
| this.isEditing = true | |
| this.selectedVgpuProfile = record | |
| this.generateFormFields(true) | |
| this.initForm(true, record) | |
| this.showModal = true | |
| }, | |
| closeModal () { | |
| this.showModal = false | |
| this.isEditing = false | |
| this.selectedVgpuProfile = null | |
| this.actionLoading = false | |
| }, | |
| filterOption (input, option) { | |
| return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 | |
| }, | |
| handleSubmit (e) { | |
| if (e) { | |
| e.preventDefault() | |
| } | |
| if (this.actionLoading) return | |
| this.$refs.vgpuForm.validate().then(() => { | |
| this.actionLoading = true | |
| const formRaw = toRaw(this.form) | |
| if (this.isEditing) { | |
| this.updateVgpuProfile(formRaw) | |
| } else { | |
| this.createVgpuProfileSubmit(formRaw) | |
| } | |
| }).catch((error) => { | |
| this.$message.error(error) | |
| }) | |
| }, | |
| createVgpuProfileSubmit (formRaw) { | |
| const params = { | |
| name: formRaw.name, | |
| gpucardid: this.resource.id, | |
| maxvgpuperphysicalgpu: formRaw.maxvgpuperphysicalgpu | |
| } | |
| if (formRaw.description) { | |
| params.description = formRaw.description | |
| } | |
| postAPI('createVgpuProfile', params).then(response => { | |
| this.$message.success(this.$t('message.success.create.vgpu.profile')) | |
| this.closeModal() | |
| this.fetchData() | |
| }).catch(error => { | |
| console.error('Error creating vGPU profile:', error) | |
| this.$notifyError(error) | |
| }).finally(() => { | |
| this.actionLoading = false | |
| }) | |
| }, | |
| updateVgpuProfile (formRaw) { | |
| const params = { | |
| id: this.selectedVgpuProfile.id | |
| } | |
| // Add only fields that have values | |
| if (formRaw.name) { | |
| params.name = formRaw.name | |
| } | |
| if (formRaw.description) { | |
| params.description = formRaw.description | |
| } | |
| if (formRaw.maxvgpuperphysicalgpu) { | |
| params.maxvgpuperphysicalgpu = formRaw.maxvgpuperphysicalgpu | |
| } | |
| postAPI('updateVgpuProfile', params).then(() => { | |
| this.$message.success(this.$t('message.success.update.vgpu.profile')) | |
| this.closeModal() | |
| this.fetchData() | |
| }).catch(error => { | |
| console.error('Error updating vGPU profile:', error) | |
| this.$notifyError(error) | |
| }).finally(() => { | |
| this.actionLoading = false | |
| }) | |
| }, | |
| deleteVgpuProfile (record) { | |
| postAPI('deleteVgpuProfile', { | |
| id: record.id | |
| }).then(() => { | |
| this.$message.success(this.$t('message.success.delete.vgpu.profile')) | |
| this.fetchData() | |
| }).catch(error => { | |
| console.error('Error deleting vGPU profile:', error) | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| fetchData () { | |
| this.fetchVgpuProfiles() | |
| }, | |
| 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: columnKey === 'name' ? this.$t('label.vgpu.profile') : this.$t('label.' + String(columnKey).toLowerCase()), | |
| dataIndex: columnKey, | |
| sorter: (a, b) => { return genericCompare(a[columnKey] || '', b[columnKey] || '') } | |
| }) | |
| } | |
| // Add actions column for admin users | |
| if (this.isAdmin) { | |
| this.columns.push({ | |
| key: 'vgpuActions', | |
| title: this.$t('label.actions'), | |
| width: 120, | |
| fixed: 'right' | |
| }) | |
| } | |
| if (this.columns.length > 0) { | |
| this.columns[this.columns.length - 1].customFilterDropdown = true | |
| } | |
| }, | |
| fetchVgpuProfiles () { | |
| getAPI('listVgpuProfiles', { | |
| gpucardid: this.resource.id | |
| }).then(res => { | |
| this.items = res?.listvgpuprofilesresponse?.vgpuprofile || [] | |
| }) | |
| } | |
| } | |
| } | |
| </script> |