Add service offering category selection and management functionality

This commit is contained in:
Landry JUGE 2025-11-26 19:23:53 +01:00
parent e1e902db45
commit 6519904c53
7 changed files with 191 additions and 6 deletions

View File

@ -511,6 +511,7 @@
"label.capacitybytes": "Capacity bytes",
"label.capacityiops": "IOPS total",
"label.category": "Category",
"label.categoryname": "Category name",
"label.certchain": "Chain",
"label.certificate": "Certificate",
"label.certificate.chain": "Certificate chain",
@ -1165,6 +1166,10 @@
"label.guest.os.category": "Guest OS Category",
"label.guest.os.categories": "Guest OS Categories",
"label.guest.os.hypervisor.mappings": "Guest OS mappings",
"label.service.offering.categories": "Service Offering Categories",
"label.add.service.offering.category": "Add Service Offering Category",
"label.action.delete.service.offering.category": "Delete Service Offering Category",
"message.action.delete.service.offering.category": "Please confirm that you want to delete this service offering category",
"label.guest.start.ip": "Guest start IP",
"label.guest.traffic": "Guest traffic",
"label.guestcidraddress": "Guest CIDR",

View File

@ -1027,6 +1027,9 @@
<template v-if="column.key === 'vgpuActions'">
<slot name="actionButtons" :record="record" :actions="actions"></slot>
</template>
<template v-if="column.key === 'category' && $route.path.split('/')[1] === 'computeoffering'">
<router-link :to="{ path: '/serviceofferingcategory/' + record.categoryid }">{{ text }}</router-link>
</template>
</template>
<template #footer>
<span v-if="hasSelected">
@ -1309,6 +1312,9 @@ export default {
case 'guestoscategory':
apiCommand = 'updateOsCategory'
break
case 'serviceofferingcategory':
apiCommand = 'updateServiceOfferingCategory'
break
}
return apiCommand
},
@ -1322,7 +1328,7 @@ export default {
return new Promise((resolve, reject) => {
postAPI(apiCommand, {
id,
sortKey: index
sortkey: index
}).then((response) => {
resolve(response)
}).catch((reason) => {

View File

@ -202,6 +202,40 @@ export default {
}
]
},
{
name: 'serviceofferingcategory',
title: 'label.service.offering.categories',
icon: 'shopping-outlined',
permission: ['listServiceOfferingCategories'],
columns: ['name', 'order'],
details: ['name', 'id'],
actions: [
{
api: 'createServiceOfferingCategory',
icon: 'plus-outlined',
label: 'label.add.service.offering.category',
listView: true,
dataView: false,
args: ['name']
},
{
api: 'updateServiceOfferingCategory',
icon: 'edit-outlined',
label: 'label.edit',
dataView: true,
popup: true,
args: ['name']
},
{
api: 'deleteServiceOfferingCategory',
icon: 'delete-outlined',
label: 'label.action.delete.service.offering.category',
message: 'message.action.delete.service.offering.category',
dataView: true,
popup: true
}
]
},
{
name: 'guestos',
title: 'label.guest.os',

View File

@ -30,7 +30,7 @@ export default {
docHelp: 'adminguide/service_offerings.html#compute-and-disk-service-offerings',
icon: 'cloud-outlined',
permission: ['listServiceOfferings'],
searchFilters: ['name', 'gpuenabled', 'zoneid', 'domainid', 'cpunumber', 'cpuspeed', 'memory'],
searchFilters: ['name', 'gpuenabled', 'zoneid', 'domainid', 'cpunumber', 'cpuspeed', 'memory', 'category'],
params: () => {
var params = {}
if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
@ -39,9 +39,9 @@ export default {
return params
},
filters: ['active', 'inactive'],
columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'gpu', 'domain', 'zone', 'order'],
columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'gpu', 'category', 'domain', 'zone', 'order'],
details: () => {
var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'gpucardid', 'gpucardname', 'vgpuprofileid', 'vgpuprofilename', 'gpucount', 'gpudisplay', 'leaseexpiryaction', 'externaldetails']
var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'gpucardid', 'gpucardname', 'vgpuprofileid', 'vgpuprofilename', 'gpucount', 'gpudisplay', 'leaseexpiryaction', 'externaldetails', 'categoryid', 'category']
if (store.getters.apis.createServiceOffering &&
store.getters.apis.createServiceOffering.params.filter(x => x.name === 'storagepolicy').length > 0) {
fields.splice(6, 0, 'vspherestoragepolicy')
@ -96,8 +96,11 @@ export default {
label: 'label.edit',
docHelp: 'adminguide/service_offerings.html#modifying-or-deleting-a-service-offering',
dataView: true,
args: ['name', 'displaytext', 'storagetags', 'hosttags', 'externaldetails'],
args: ['name', 'displaytext', 'categoryid', 'storagetags', 'hosttags', 'externaldetails'],
mapping: {
categoryid: {
api: 'listServiceOfferingCategories'
},
externaldetails: {
transformedvalue: (record) => { return getFilteredExternalDetails(record.serviceofferingdetails) }
}

View File

@ -261,8 +261,11 @@
:minimum-cpunumber="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpunumber ? selectedTemplateConfiguration.cpunumber : 0"
:minimum-cpuspeed="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpuspeed ? selectedTemplateConfiguration.cpuspeed : 0"
:minimum-memory="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.memory ? selectedTemplateConfiguration.memory : 0"
:service-offering-categories="options.serviceOfferingCategories"
:selected-service-offering-category-id="selectedServiceOfferingCategoryId"
@select-compute-item="($event) => updateComputeOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('serviceOfferings', $event)"
@service-offering-category-change="($event) => handleServiceOfferingCategoryChange($event)"
></compute-offering-selection>
<compute-selection
v-if="serviceOffering && (serviceOffering.iscustomized || serviceOffering.iscustomizediops)"
@ -2271,6 +2274,46 @@ export default {
console.error('Error fetching guestOsCategories:', e)
})
},
fetchServiceOfferingCategories () {
this.loading.serviceOfferingCategories = true
return new Promise((resolve, reject) => {
getAPI('listServiceOfferingCategories').then(json => {
const categories = json.listserviceofferingcategoriesresponse.serviceofferingcategory || []
this.options.serviceOfferingCategories = [
{
id: '-1',
name: this.$t('label.all')
},
...categories
]
resolve()
}).catch(error => {
console.error('Error fetching service offering categories:', error)
this.options.serviceOfferingCategories = [
{
id: '-1',
name: this.$t('label.all')
}
]
reject(error)
}).finally(() => {
this.loading.serviceOfferingCategories = false
})
})
},
handleServiceOfferingCategoryChange (categoryId) {
this.selectedServiceOfferingCategoryId = categoryId
const params = {
page: 1,
pageSize: 10
}
if (categoryId && categoryId !== '-1') {
params.serviceofferingcategoryid = categoryId
} else {
params.serviceofferingcategoryid = null
}
this.handleSearchFilter('serviceOfferings', params)
},
changeArchitecture (arch) {
this.selectedArchitecture = arch
this.updateImages()
@ -3027,6 +3070,8 @@ export default {
if (this.isModernImageSelection && guestOsFetch) {
await guestOsFetch
}
// Load service offering categories
this.fetchServiceOfferingCategories()
this.fetchImages()
this.updateTemplateKey()
this.formModel = toRaw(this.form)

View File

@ -17,6 +17,25 @@
<template>
<div>
<a-form-item
:label="$t('label.category')"
name="serviceofferingcategoryid"
ref="serviceofferingcategoryid"
v-if="serviceOfferingCategories && serviceOfferingCategories.length > 0"
style="margin-bottom: 16px;">
<block-radio-group-select
:maxBlocks="16"
:items="serviceOfferingCategories"
:selectedValue="selectedServiceOfferingCategoryId"
:horizontalGutter="6"
:verticalGutter="6"
blockSize="medium"
@change="handleServiceOfferingCategoryChange">
<template #radio-option="{ item }">
{{ item.name }}
</template>
</block-radio-group-select>
</a-form-item>
<div style="margin-bottom: 16px; display: flex; justify-content: right; align-items: center;">
<div v-if="showGpuFilter" style="display: flex; align-items: center; margin-right: 16px;">
<span>{{ $t('label.show.only.gpu.enabled.offerings') }}</span>
@ -94,8 +113,13 @@
</template>
<script>
import BlockRadioGroupSelect from '@/components/widgets/BlockRadioGroupSelect.vue'
export default {
name: 'ComputeOfferingSelection',
components: {
BlockRadioGroupSelect
},
props: {
computeItems: {
type: Array,
@ -150,6 +174,14 @@ export default {
type: Boolean,
required: false,
default: false
},
serviceOfferingCategories: {
type: Array,
default: () => []
},
selectedServiceOfferingCategoryId: {
type: String,
default: '-1'
}
},
data () {
@ -314,6 +346,9 @@ export default {
}
},
methods: {
handleServiceOfferingCategoryChange (categoryId) {
this.$emit('service-offering-category-change', categoryId)
},
onSelectRow (value) {
this.selectedRowKeys = value
this.$emit('select-compute-item', value[0])
@ -381,4 +416,26 @@ export default {
:deep(.ant-table-tbody) > tr > td {
cursor: pointer;
}
.radio-option {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
.radio-opion__icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.ellipsis {
width: 100%;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>

View File

@ -42,6 +42,25 @@
v-model:value="form.displaytext"
:placeholder="$t('label.displaytext')"/>
</a-form-item>
<a-form-item name="categoryid" ref="categoryid" v-if="!isSystem">
<template #label>
<tooltip-label :title="$t('label.category')" :tooltip="apiParams.categoryid ? apiParams.categoryid.description : $t('label.category')"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.categoryid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="categoryLoading"
:placeholder="$t('label.category')">
<a-select-option v-for="category in categories" :key="category.id" :label="category.name">
{{ category.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="systemvmtype" ref="systemvmtype" v-if="isSystem">
<template #label>
<tooltip-label :title="$t('label.systemvmtype')" :tooltip="apiParams.systemvmtype.description"/>
@ -745,7 +764,9 @@ export default {
leaseexpiryaction: undefined,
vgpuProfiles: [],
vgpuProfileLoading: false,
externalDetailsEnabled: false
externalDetailsEnabled: false,
categories: [],
categoryLoading: false
}
},
beforeCreate () {
@ -857,6 +878,7 @@ export default {
this.fetchDomainData()
this.fetchZoneData()
this.fetchGPUCards()
this.fetchCategories()
if (isAdmin()) {
this.fetchStorageTagData()
this.fetchDeploymentPlannerData()
@ -882,6 +904,15 @@ export default {
this.gpuCardLoading = false
})
},
fetchCategories () {
this.categoryLoading = true
getAPI('listServiceOfferingCategories', {
}).then(json => {
this.categories = json.listserviceofferingcategoriesresponse.serviceofferingcategory || []
}).finally(() => {
this.categoryLoading = false
})
},
addDiskOffering () {
this.showDiskOfferingModal = true
},
@ -1089,6 +1120,10 @@ export default {
params.diskofferingid = values.diskofferingid
}
if (values.categoryid) {
params.categoryid = values.categoryid
}
// Add GPU parameters
if (values.vgpuprofile) {
params.vgpuprofileid = values.vgpuprofile