diff --git a/ui/public/config.json b/ui/public/config.json
index a067803ba7a2..81f06c0dbb47 100644
--- a/ui/public/config.json
+++ b/ui/public/config.json
@@ -116,5 +116,6 @@
"message": "🤔 Sample Announcement: New Feature Available: Check out our latest dashboard improvements! Learn more",
"startDate": "2025-06-01T00:00:00Z",
"endDate": "2025-07-16T00:00:00Z"
- }
+ },
+ "advisoriesDisabled": false
}
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 8bcc5d0a94bf..1257c30a75fa 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -1149,7 +1149,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",
@@ -3075,9 +3080,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...",
diff --git a/ui/src/config/router.js b/ui/src/config/router.js
index 43e8efd7b5d3..1de08942422a 100644
--- a/ui/src/config/router.js
+++ b/ui/src/config/router.js
@@ -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,
@@ -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,
@@ -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
}
diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js
index 32e888bb53dc..65cab7686718 100644
--- a/ui/src/config/section/compute.js
+++ b/ui/src/config/section/compute.js
@@ -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',
@@ -100,6 +101,119 @@ 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 }) &&
+ 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',
@@ -589,23 +703,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: [
{
@@ -647,19 +750,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: [
{
@@ -702,7 +794,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)) {
diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js
index 33b39d271726..09bc36e77ff9 100644
--- a/ui/src/config/section/network.js
+++ b/ui/src/config/section/network.js
@@ -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'
export default {
name: 'network',
@@ -397,6 +398,66 @@ 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,
+ 'listVnfTemplates',
+ { isvnf: true, templatefilter: 'executable', isready: true })
+ },
+ 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',
diff --git a/ui/src/utils/advisory/index.js b/ui/src/utils/advisory/index.js
new file mode 100644
index 000000000000..6ce944da085a
--- /dev/null
+++ b/ui/src/utils/advisory/index.js
@@ -0,0 +1,76 @@
+// 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} - Returns true if no items exist (advisory should be shown), false otherwise
+ */
+export async function hasNoItems (store, apiName, params = {}, filterFunc = 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
+ }
+
+ console.debug(`Checking if API ${apiName} has no items with params`, allParams)
+
+ try {
+ const json = await getAPI(apiName, allParams)
+ // Auto-derive response key: listNetworks -> listnetworksresponse
+ const responseKey = `${apiName.toLowerCase()}response`
+ const items = json?.[responseKey]?.[itemsKey] || []
+ if (filterFunc) {
+ const a = !items.some(filterFunc)
+ console.debug(`API ${apiName} has ${items.length} items, after filter has items: ${items.filter(filterFunc)[0]}, returning ${a}`)
+ const it = items.filter(filterFunc)
+ console.debug(`Filtered items:`, it)
+ return a
+ }
+ return items.length === 0
+ } catch (error) {
+ return false
+ }
+}