Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ui/public/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,6 @@
"message": "🤔 <strong>Sample Announcement</strong>: New Feature Available: Check out our latest dashboard improvements! <a href='/features'>Learn more</a>",
"startDate": "2025-06-01T00:00:00Z",
"endDate": "2025-07-16T00:00:00Z"
}
},
"advisoriesDisabled": false
}
10 changes: 10 additions & 0 deletions ui/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,12 @@
"label.go.back": "Go back",
"label.go.to.compute.offerings": "Go to Compute Offerings",
"label.go.to.global.settings": "Go to Global Settings",
"label.go.to.networks": "Go to Networks",
"label.go.to.templates": "Go to Templates",
"label.go.to.isos": "Go to ISOs",
"label.go.to.volumes": "Go to Volumes",
"label.go.to.kubernetes.isos": "Go to Kubernetes ISOs",
"label.go.to.snapshots": "Go to Volume Snapshots",
"label.gpu": "GPU",
"label.gpucardid": "GPU Card",
"label.gpucardname": "GPU Card",
Expand Down Expand Up @@ -3091,9 +3096,14 @@
"message.add.ip.v6.firewall.rule.failed": "Failed to add IPv6 firewall rule",
"message.add.ip.v6.firewall.rule.processing": "Adding IPv6 firewall rule...",
"message.add.ip.v6.firewall.rule.success": "Added IPv6 firewall rule",
"message.advisory.instance.compute.offering.missing": "No compute offering found for deploying an Instance.",
"message.advisory.instance.image.missing": "No suitable Template/ISO/Volume/Volume Snapshot found for deploying an Instance. Please make sure you have a Template/ISO/Volume/Snapshot ready for Instance deployment.",
"message.advisory.instance.network.missing": "No suitable Network found for deploying an Instance. Please create a Network to be used by the Instance.",
"message.advisory.cks.endpoint.url.not.configured": "Endpoint URL which will be used by Kubernetes clusters is not configured correctly",
"message.advisory.cks.min.offering": "No suitable Compute Offering found for Kubernetes cluster nodes with minimum required resources (2 vCPU, 2 GB RAM)",
"message.advisory.cks.version.check": "No Kubernetes version found that can be used to deploy a Kubernetes cluster",
"message.advisory.vnf.appliance.compute.offering.missing": "No compute offering found for deploying a VNF appliance.",
"message.advisory.vnf.appliance.template.missing": "No VNF Template found for deploying a VNF appliance. Please make sure you have a VNF Template for appliance deployment.",
"message.redeliver.webhook.delivery": "Redeliver this Webhook delivery",
"message.remove.ip.v6.firewall.rule.failed": "Failed to remove IPv6 firewall rule",
"message.remove.ip.v6.firewall.rule.processing": "Removing IPv6 firewall rule...",
Expand Down
6 changes: 4 additions & 2 deletions ui/src/config/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import tools from '@/config/section/tools'
import quota from '@/config/section/plugin/quota'
import cloudian from '@/config/section/plugin/cloudian'

const isAdvisoriesDisabled = () => vueProps.$config.advisoriesDisabled ?? false

function generateRouterMap (section) {
var map = {
name: section.name,
Expand Down Expand Up @@ -81,7 +83,7 @@ function generateRouterMap (section) {
filters: child.filters,
params: child.params ? child.params : {},
columns: child.columns,
advisories: !vueProps.$config.advisoriesDisabled ? child.advisories : undefined,
advisories: !isAdvisoriesDisabled() ? child.advisories : undefined,
details: child.details,
searchFilters: child.searchFilters,
related: child.related,
Expand Down Expand Up @@ -181,7 +183,7 @@ function generateRouterMap (section) {
map.meta.columns = section.columns
}

if (!vueProps.$config.advisoriesDisabled && section.advisories) {
if (!isAdvisoriesDisabled() && section.advisories) {
map.meta.advisories = section.advisories
}

Expand Down
147 changes: 118 additions & 29 deletions ui/src/config/section/compute.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { isZoneCreated } from '@/utils/zone'
import { getAPI, postAPI, getBaseUrl } from '@/api'
import { getLatestKubernetesIsoParams } from '@/utils/acsrepo'
import kubernetesIcon from '@/assets/icons/kubernetes.svg?inline'
import { hasNoItems } from '@/utils/advisory'

export default {
name: 'compute',
Expand Down Expand Up @@ -100,6 +101,117 @@ export default {
tabs: [{
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/InstanceTab.vue')))
}],
advisories: [
{
id: 'instance-image-check',
severity: 'warning',
message: 'message.advisory.instance.image.missing',
condition: async (store) => {
return await hasNoItems(store, 'listTemplates', { isvnf: false, templatefilter: 'executable', isready: true }, null, 'listtemplatesresponse', 'template') &&
await hasNoItems(store, 'listIsos', { isofilter: 'executable', bootable: true, isready: true }) &&
await hasNoItems(store, 'listVolumes', { state: 'Ready' }) &&
await hasNoItems(store, 'listSnapshots')
},
actions: [
{
label: 'label.register.template',
show: (store) => { return ('registerTemplate' in store.getters.apis) },
primary: true,
run: (store, router) => {
router.push({ name: 'template', query: { action: 'registerTemplate' } })
return false
}
},
{
label: 'label.go.to.templates',
show: (store) => { return ('listTemplates' in store.getters.apis) },
run: (store, router) => {
router.push({ name: 'template' })
return false
}
},
{
label: 'label.go.to.isos',
show: (store) => { return ('listIsos' in store.getters.apis) },
run: (store, router) => {
router.push({ name: 'iso' })
return false
}
},
{
label: 'label.go.to.volumes',
show: (store) => { return ('listVolumes' in store.getters.apis) },
run: (store, router) => {
router.push({ name: 'volume' })
return false
}
},
{
label: 'label.go.to.snapshots',
show: (store) => { return ('listSnapshots' in store.getters.apis) },
run: (store, router) => {
router.push({ name: 'snapshot' })
return false
}
}
]
},
{
id: 'instance-compute-offering-check',
severity: 'warning',
message: 'message.advisory.instance.compute.offering.missing',
condition: async (store) => {
return await hasNoItems(store, 'listServiceOfferings', { issystem: false })
},
actions: [
{
label: 'label.add.compute.offering',
show: (store) => { return ('createServiceOffering' in store.getters.apis) },
primary: true,
run: (store, router) => {
router.push({ name: 'computeoffering', query: { action: 'createServiceOffering' } })
return false
}
},
{
label: 'label.go.to.compute.offerings',
show: (store) => { return ('listServiceOfferings' in store.getters.apis) },
run: (store, router) => {
router.push({ name: 'computeoffering' })
return false
}
}
]
},
{
id: 'instance-network-check',
severity: 'warning',
message: 'message.advisory.instance.network.missing',
dismissOnConditionFail: true,
condition: async (store) => {
return await hasNoItems(store, 'listNetworks')
},
actions: [
{
label: 'label.add.network',
show: (store) => { return ('createNetwork' in store.getters.apis) },
primary: true,
run: (store, router) => {
router.push({ name: 'guestnetwork', query: { action: 'createNetwork' } })
return false
}
},
{
label: 'label.go.to.networks',
show: (store) => { return ('listNetworks' in store.getters.apis) },
run: (store, router) => {
router.push({ name: 'guestnetworks' })
return false
}
}
]
}
],
actions: [
{
api: 'deployVirtualMachine',
Expand Down Expand Up @@ -589,23 +701,12 @@ export default {
id: 'cks-min-offering',
severity: 'warning',
message: 'message.advisory.cks.min.offering',
docsHelp: 'plugins/cloudstack-kubernetes-service.html',
dismissOnConditionFail: true,
condition: async (store) => {
if (!('listServiceOfferings' in store.getters.apis)) {
return false
}
const params = {
cpunumber: 2,
memory: 2048,
issystem: false
}
try {
const json = await getAPI('listServiceOfferings', params)
const offerings = json?.listserviceofferingsresponse?.serviceoffering || []
return !offerings.some(o => !o.iscustomized)
} catch (error) {}
return false
return await hasNoItems(store,
'listServiceOfferings',
{ cpunumber: 2, memory: 2048, issystem: false },
o => !o.iscustomized
)
},
actions: [
{
Expand Down Expand Up @@ -647,19 +748,8 @@ export default {
id: 'cks-version-check',
severity: 'warning',
message: 'message.advisory.cks.version.check',
docsHelp: 'plugins/cloudstack-kubernetes-service.html',
dismissOnConditionFail: true,
condition: async (store) => {
const api = 'listKubernetesSupportedVersions'
if (!(api in store.getters.apis)) {
return false
}
try {
const json = await getAPI(api, {})
const versions = json?.listkubernetessupportedversionsresponse?.kubernetessupportedversion || []
return versions.length === 0
} catch (error) {}
return false
return await hasNoItems(store, 'listKubernetesSupportedVersions')
},
actions: [
{
Expand Down Expand Up @@ -702,7 +792,6 @@ export default {
id: 'cks-endpoint-url',
severity: 'warning',
message: 'message.advisory.cks.endpoint.url.not.configured',
docsHelp: 'plugins/cloudstack-kubernetes-service.html',
dismissOnConditionFail: true,
condition: async (store) => {
if (!['Admin'].includes(store.getters.userInfo.roletype)) {
Expand Down
66 changes: 66 additions & 0 deletions ui/src/config/section/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import store from '@/store'
import tungsten from '@/assets/icons/tungsten.svg?inline'
import { isAdmin } from '@/role'
import { isZoneCreated } from '@/utils/zone'
import { hasNoItems } from '@/utils/advisory'
import { vueProps } from '@/vue-app'

export default {
Expand Down Expand Up @@ -401,6 +402,71 @@ export default {
tabs: [{
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/InstanceTab.vue')))
}],
advisories: [
{
id: 'vnfapp-image-check',
severity: 'warning',
message: 'message.advisory.vnf.appliance.template.missing',
condition: async (store) => {
return await hasNoItems(
store,
'listTemplates',
{ isvnf: false, templatefilter: 'executable', isready: true },
null,
'listtemplatesresponse',
'template'
)
},
actions: [
{
label: 'label.register.template',
show: (store) => { return ('registerTemplate' in store.getters.apis) },
primary: true,
run: (store, router) => {
router.push({ name: 'template', query: { action: 'registerTemplate' } })
return false
}
},
{
label: 'label.go.to.templates',
show: (store) => { return ('listTemplates' in store.getters.apis) },
primary: false,
run: (store, router) => {
router.push({ name: 'template' })
return false
}
}
]
},
{
id: 'vnfapp-compute-offering-check',
severity: 'warning',
message: 'message.advisory.vnf.appliance.compute.offering.missing',
condition: async (store) => {
return await hasNoItems(store, 'listServiceOfferings', { issystem: false })
},
actions: [
{
label: 'label.add.compute.offering',
show: (store) => { return ('createServiceOffering' in store.getters.apis) },
primary: true,
run: (store, router) => {
router.push({ name: 'computeoffering', query: { action: 'createServiceOffering' } })
return false
}
},
{
label: 'label.go.to.compute.offerings',
show: (store) => { return ('listServiceOfferings' in store.getters.apis) },
primary: false,
run: (store, router) => {
router.push({ name: 'computeoffering' })
return false
}
}
]
}
],
actions: [
{
api: 'deployVnfAppliance',
Expand Down
71 changes: 71 additions & 0 deletions ui/src/utils/advisory/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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.
import { getAPI } from '@/api'

/**
* Generic helper to check if an API has no items (useful for advisory conditions)
* @param {Object} store - Vuex store instance
* @param {string} apiName - Name of the API to call (e.g., 'listNetworks')
* @param {Object} params - Optional parameters to merge with defaults
* @param {Function} filterFunc - Optional function to filter items. If provided, returns true if no items match the filter.
* @param {string} itemsKey - Optional key for items array in response. If not provided, will be deduced from apiName
* @returns {Promise<boolean>} - Returns true if no items exist (advisory should be shown), false otherwise
*/
export async function hasNoItems (store, apiName, params = {}, filterFunc = null, responseKey = null, itemsKey = null) {
if (!(apiName in store.getters.apis)) {
return false
}

// If itemsKey not provided, deduce it from apiName
if (!itemsKey) {
// Remove 'list' prefix: listNetworks -> Networks
let key = apiName.replace(/^list/i, '')
// Convert to lowercase
key = key.toLowerCase()
// Handle plural forms: remove trailing 's' or convert 'ies' to 'y'
if (key.endsWith('ies')) {
key = key.slice(0, -3) + 'y'
} else if (key.endsWith('s')) {
key = key.slice(0, -1)
}
itemsKey = key
}

const allParams = {
listall: true,
...params
}

if (filterFunc == null) {
allParams.page = 1
allParams.pageSize = 1
}

try {
const json = await getAPI(apiName, allParams)
// Auto-derive response key: listNetworks -> listnetworksresponse
const apiResponseKey = responseKey || `${apiName.toLowerCase()}response`
const items = json?.[apiResponseKey]?.[itemsKey] || []
if (filterFunc) {
return !items.some(filterFunc)
}
return items.length === 0
} catch (error) {
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling silently returns false when an API call fails, which could hide legitimate errors and cause advisories not to appear when they should. Consider logging the error or providing more specific error handling, especially since API failures could indicate permission issues or connectivity problems that users should be aware of.

Suggested change
} catch (error) {
} catch (error) {
console.error(`Failed to fetch items for advisory check via API ${apiName}`, error)

Copilot uses AI. Check for mistakes.
console.error(`Failed to fetch items for advisory check via API ${apiName}`, error)
return false
}
}
Loading