Copilot commented on code in PR #9520:
URL: https://github.com/apache/cloudstack/pull/9520#discussion_r2736751645


##########
ui/src/components/view/SearchFilter.vue:
##########
@@ -0,0 +1,555 @@
+// 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>
+  <a-row
+    style="overflow: auto"
+    :wrap="false"
+  >
+    <template
+      v-for="(filter) in this.searchFilters"
+      :key="filter"

Review Comment:
   `:key="filter"` uses the entire filter object as the Vue key, which can lead 
to duplicate/unstable keys (e.g., all become "[object Object]") and incorrect 
tag reuse when the list changes. Use a stable unique key derived from filter 
identity (e.g., filter.key + tagIdx, or the v-for index).
   ```suggestion
         v-for="(filter, index) in this.searchFilters"
         :key="index"
   ```



##########
ui/src/components/view/SearchFilter.vue:
##########
@@ -0,0 +1,555 @@
+// 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>
+  <a-row
+    style="overflow: auto"
+    :wrap="false"
+  >
+    <template
+      v-for="(filter) in this.searchFilters"
+      :key="filter"
+    >
+      <a-col v-if="!['page', 'pagesize', 'q', 'keyword', 
'tags'].includes(filter.key)">
+        <a-tag
+          v-if="!filter.isTag"
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="retrieveFieldLabel(filter.key) + ': ' + filter.value"
+            :placement="tooltipPlacement"
+          >

Review Comment:
   `tooltipPlacement` is referenced in the template but is not defined as a 
prop/data/computed value in this component, so it will always be `undefined` 
and may produce runtime warnings. Define it (as other tooltip components do) or 
remove the binding and rely on the tooltip’s default placement.



##########
ui/src/components/view/SearchFilter.vue:
##########
@@ -0,0 +1,555 @@
+// 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>
+  <a-row
+    style="overflow: auto"
+    :wrap="false"
+  >
+    <template
+      v-for="(filter) in this.searchFilters"
+      :key="filter"
+    >
+      <a-col v-if="!['page', 'pagesize', 'q', 'keyword', 
'tags'].includes(filter.key)">
+        <a-tag
+          v-if="!filter.isTag"
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="retrieveFieldLabel(filter.key) + ': ' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ retrieveFieldLabel(filter.key) }} : {{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+        <a-tag
+          v-else
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="$t('label.tag') + ': ' + filter.key + '=' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ $t('label.tag') }}: {{ filter.key }}={{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+      </a-col>
+    </template>
+  </a-row>
+</template>
+
+<script>
+
+import { api } from '@/api/index'
+
+export default {
+  name: 'SearchFilter',
+  props: {
+    filters: {
+      type: Array,
+      default: () => []
+    },
+    apiName: {
+      type: String,
+      default: () => ''
+    },
+    filterKey: {
+      type: String,
+      default: () => ''
+    },
+    filterValue: {
+      type: String,
+      default: () => ''
+    },
+    isTag: {
+      type: Boolean,
+      default: () => false
+    }
+  },
+  emits: ['removeFilter'],
+  data () {
+    return {
+      searchFilters: [],
+      apiMap: {
+        type: this.getType,
+        hypervisor: this.getHypervisor,
+        zoneid: {
+          apiName: 'listZones',
+          responseKey1: 'listzonesresponse',
+          responseKey2: 'zone',
+          field: 'name'
+        },
+        domainid: {
+          apiName: 'listDomains',
+          responseKey1: 'listdomainsresponse',
+          responseKey2: 'domain',
+          field: 'name'
+        },
+        account: {
+          apiName: 'listAccounts',
+          responseKey1: 'listaccountsresponse',
+          responseKey2: 'account',
+          field: 'name'
+        },
+        imagestoreid: {
+          apiName: 'listImageStores',
+          responseKey1: 'listimagestoresresponse',
+          responseKey2: 'imagestore',
+          field: 'name'
+        },
+        storageid: {
+          apiName: 'listStoragePools',
+          responseKey1: 'liststoragepoolsresponse',
+          responseKey2: 'storagepool',
+          field: 'name'
+        },
+        podid: {
+          apiName: 'listPods',
+          responseKey1: 'listpodsresponse',
+          responseKey2: 'pod',
+          field: 'name'
+        },
+        clusterid: {
+          apiName: 'listClusters',
+          responseKey1: 'listclustersresponse',
+          responseKey2: 'cluster',
+          field: 'name'
+        },
+        hostid: {
+          apiName: 'listHosts',
+          responseKey1: 'listhostsresponse',
+          responseKey2: 'host',
+          field: 'name'
+        },
+        groupid: {
+          apiName: 'listInstanceGroups',
+          responseKey1: 'listinstancegroupsresponse',
+          responseKey2: 'instancegroup',
+          field: 'name'
+        },
+        managementserverid: {
+          apiName: 'listManagementServers',
+          responseKey1: 'listmanagementserversresponse',
+          responseKey2: 'managementserver',
+          field: 'name'
+        },
+        serviceofferingid: {
+          apiName: 'listServiceOfferings',
+          responseKey1: 'listserviceofferingsresponse',
+          responseKey2: 'serviceoffering',
+          field: 'name'
+        },
+        diskofferingid: {
+          apiName: 'listDiskOfferings',
+          responseKey1: 'listdiskofferingsresponse',
+          responseKey2: 'diskoffering',
+          field: 'name'
+        },
+        networkid: {
+          apiName: 'listNetworks',
+          responseKey1: 'listnetworksresponse',
+          responseKey2: 'network',
+          field: 'name'
+        }
+      }
+    }
+  },
+  updated () {
+    this.searchFilters = this.filters
+    const promises = []
+    for (const idx in this.filters) {
+      const filter = this.filters[idx]
+      if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+        continue
+      }
+      promises.push(new Promise((resolve) => {
+        if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+          resolve()
+        }
+        if (filter.key === 'tags') {
+          this.searchFilters[idx] = {
+            key: filter.key,
+            value: filter.value,
+            isTag: true
+          }
+        } else {
+          this.getSearchFilters(filter.key, filter.value).then((value) => {
+            this.searchFilters[idx] = {
+              key: filter.key,
+              value: value,
+              isTag: filter.isTag
+            }
+          })
+        }
+        resolve()
+      }))
+    }
+    Promise.all(promises)
+  },
+  methods: {
+    getTrimmedText (text, length) {
+      if (!text) {
+        return ''
+      }
+      return (text.length <= length) ? text : (text.substring(0, length - 3) + 
'...')
+    },
+    retrieveFieldLabel (fieldName) {
+      if (fieldName === 'groupid') {
+        fieldName = 'group'
+      }
+      if (fieldName === 'keyword') {
+        if ('listAnnotations' in this.$store.getters.apis) {
+          return this.$t('label.annotation')
+        } else {
+          return this.$t('label.name')
+        }
+      }
+      return this.$t('label.' + fieldName)
+    },
+    async getSearchFilters (key, value) {
+      const val = this.getStaticFieldValue(key, value)
+      if (val !== '') {
+        return val
+      } else {
+        return this.getDynamicFieldValue(key, value)
+      }
+    },
+    getStaticFieldValue (key, value) {
+      let formattedValue = ''
+      if (key.includes('type')) {
+        if (this.$route.path === '/guestnetwork' || 
this.$route.path.includes('/guestnetwork/')) {
+          formattedValue = this.getGuestNetworkType(value)
+        } else if (this.$route.path === '/role' || 
this.$route.path.includes('/role/')) {
+          formattedValue = this.getRoleType(value)
+        }
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getScope(value)
+      }
+
+      if (key.includes('state')) {
+        formattedValue = this.getState(value)
+      }
+
+      if (key.includes('level')) {
+        formattedValue = this.getLevel(value)
+      }
+
+      if (key.includes('entitytype')) {
+        formattedValue = this.getEntityType(value)
+      }
+
+      if (key.includes('accounttype')) {
+        formattedValue = this.getAccountType(value)
+      }
+
+      if (key.includes('systemvmtype')) {
+        formattedValue = this.getSystemVmType(value)
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getStoragePoolScope(value)
+      }
+
+      if (key.includes('provider')) {
+        formattedValue = this.getImageStoreProvider(value)
+      }
+
+      if (key.includes('resourcetype')) {
+        formattedValue = value
+      }
+      return formattedValue
+    },
+    async getDynamicFieldValue (key, value) {
+      let formattedValue = ''
+
+      if (typeof this.apiMap[key] === 'function') {
+        formattedValue = await this.apiMap[key](value)
+      } else if (this.apiMap[key]) {
+        const apiName = this.apiMap[key].apiName
+        const responseKey1 = this.apiMap[key].responseKey1
+        const responseKey2 = this.apiMap[key].responseKey2
+        const field = this.apiMap[key].field
+        formattedValue = await this.getResourceNameById(apiName, responseKey1, 
responseKey2, value, field)
+      }
+      if (formattedValue === '') {
+        formattedValue = value
+      }
+      return formattedValue
+    },
+    getHypervisor (value) {
+      return new Promise((resolve) => {
+        api('listHypervisors').then(json => {
+          if (json?.listhypervisorsresponse?.hypervisor) {
+            for (const key in json.listhypervisorsresponse.hypervisor) {
+              const hypervisor = json.listhypervisorsresponse.hypervisor[key]
+              if (hypervisor.name === value) {
+                resolve(hypervisor.name)
+              }
+            }
+          }
+        }).catch(() => {
+          resolve(null)
+        })
+      })
+    },
+    getResourceNameById (apiName, responseKey1, responseKey2, id, field) {
+      return new Promise((resolve) => {
+        if (!this.$isValidUuid(id)) {
+          return resolve('')
+        }
+        api(apiName, { listAll: true, id: id }).then(json => {
+          if (json[responseKey1] && json[responseKey1][responseKey2]) {
+            resolve(json[responseKey1][responseKey2][0][field])
+          }
+        }).catch(() => {
+          resolve('')
+        })
+      })
+    },
+    getType (type) {
+      if (this.$route.path === '/alert') {
+        return this.getAlertType(type)
+      } else if (this.$route.path === '/affinitygroup') {
+        return this.getAffinityGroupType(type)
+      }
+    },
+    getAlertType (type) {
+      return new Promise((resolve) => {
+        api('listAlertTypes').then(json => {
+          const alertTypes = {}
+          for (const key in json.listalerttypesresponse.alerttype) {
+            const alerttype = json.listalerttypesresponse.alerttype[key]
+            alertTypes[alerttype.id] = alerttype.name
+          }
+          resolve(alertTypes[type])
+        }).catch(() => {
+          resolve(null)
+        })
+      })
+    },
+    getAffinityGroupType (type) {
+      return new Promise((resolve) => {
+        api('listAffinityGroupTypes').then(json => {
+          const alertTypes = {}
+          for (const key in 
json.listaffinitygrouptypesresponse.affinityGroupType) {
+            const affinityGroupType = 
json.listaffinitygrouptypesresponse.affinityGroupType[key]
+            if (affinityGroupType.type === 'host anti-affinity') {
+              alertTypes[affinityGroupType.type] = 'host anti-affinity 
(Strict)'
+            } else if (affinityGroupType.type === 'host affinity') {
+              alertTypes[affinityGroupType.type] = 'host affinity (Strict)'
+            } else if (affinityGroupType.type === 'non-strict host 
anti-affinity') {
+              alertTypes[affinityGroupType.type] = 'host anti-affinity 
(Non-Strict)'
+            } else if (affinityGroupType.type === 'non-strict host affinity') {
+              alertTypes[affinityGroupType.type] = 'host affinity (Non-Strict)'
+            }
+          }
+          this.alertTypes = alertTypes
+          resolve(alertTypes[type])
+        }).catch(() => {
+          resolve(null)
+        })
+      })
+    },
+    getGuestNetworkType (value) {
+      switch (value.toLowerCase()) {
+        case 'isolated':
+          return this.$t('label.isolated')
+        case 'shared':
+          return this.$t('label.shared')
+        case 'l2':
+          return this.$t('label.l2')
+      }
+    },
+    getAccountType (type) {
+      const types = []
+      if (this.apiName.indexOf('listAccounts') > -1) {
+        switch (type) {
+          case '1':
+            return 'Admin'
+          case '2':
+            return 'DomainAdmin'
+          case '0':
+            return 'User'
+        }
+      }
+      return types

Review Comment:
   `getAccountType()` maps user to `'0'`, but the search UI options use `'3'` 
for user accounts (see `SearchView.vue`’s account type options). This mismatch 
will display the raw value instead of a label. Align the mapping with the 
values produced by the search form and return `''` when not applicable (not an 
array).
   ```suggestion
         if (this.apiName.indexOf('listAccounts') > -1) {
           switch (String(type)) {
             case '1':
               return 'Admin'
             case '2':
               return 'DomainAdmin'
             case '3':
             case '0':
               return 'User'
             default:
               return ''
           }
         }
         return ''
   ```



##########
ui/src/components/view/SearchFilter.vue:
##########
@@ -0,0 +1,555 @@
+// 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>
+  <a-row
+    style="overflow: auto"
+    :wrap="false"
+  >
+    <template
+      v-for="(filter) in this.searchFilters"
+      :key="filter"
+    >
+      <a-col v-if="!['page', 'pagesize', 'q', 'keyword', 
'tags'].includes(filter.key)">
+        <a-tag
+          v-if="!filter.isTag"
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="retrieveFieldLabel(filter.key) + ': ' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ retrieveFieldLabel(filter.key) }} : {{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+        <a-tag
+          v-else
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="$t('label.tag') + ': ' + filter.key + '=' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ $t('label.tag') }}: {{ filter.key }}={{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+      </a-col>
+    </template>
+  </a-row>
+</template>
+
+<script>
+
+import { api } from '@/api/index'
+
+export default {
+  name: 'SearchFilter',
+  props: {
+    filters: {
+      type: Array,
+      default: () => []
+    },
+    apiName: {
+      type: String,
+      default: () => ''
+    },
+    filterKey: {
+      type: String,
+      default: () => ''
+    },
+    filterValue: {
+      type: String,
+      default: () => ''
+    },
+    isTag: {
+      type: Boolean,
+      default: () => false
+    }
+  },
+  emits: ['removeFilter'],
+  data () {
+    return {
+      searchFilters: [],
+      apiMap: {
+        type: this.getType,
+        hypervisor: this.getHypervisor,
+        zoneid: {
+          apiName: 'listZones',
+          responseKey1: 'listzonesresponse',
+          responseKey2: 'zone',
+          field: 'name'
+        },
+        domainid: {
+          apiName: 'listDomains',
+          responseKey1: 'listdomainsresponse',
+          responseKey2: 'domain',
+          field: 'name'
+        },
+        account: {
+          apiName: 'listAccounts',
+          responseKey1: 'listaccountsresponse',
+          responseKey2: 'account',
+          field: 'name'
+        },
+        imagestoreid: {
+          apiName: 'listImageStores',
+          responseKey1: 'listimagestoresresponse',
+          responseKey2: 'imagestore',
+          field: 'name'
+        },
+        storageid: {
+          apiName: 'listStoragePools',
+          responseKey1: 'liststoragepoolsresponse',
+          responseKey2: 'storagepool',
+          field: 'name'
+        },
+        podid: {
+          apiName: 'listPods',
+          responseKey1: 'listpodsresponse',
+          responseKey2: 'pod',
+          field: 'name'
+        },
+        clusterid: {
+          apiName: 'listClusters',
+          responseKey1: 'listclustersresponse',
+          responseKey2: 'cluster',
+          field: 'name'
+        },
+        hostid: {
+          apiName: 'listHosts',
+          responseKey1: 'listhostsresponse',
+          responseKey2: 'host',
+          field: 'name'
+        },
+        groupid: {
+          apiName: 'listInstanceGroups',
+          responseKey1: 'listinstancegroupsresponse',
+          responseKey2: 'instancegroup',
+          field: 'name'
+        },
+        managementserverid: {
+          apiName: 'listManagementServers',
+          responseKey1: 'listmanagementserversresponse',
+          responseKey2: 'managementserver',
+          field: 'name'
+        },
+        serviceofferingid: {
+          apiName: 'listServiceOfferings',
+          responseKey1: 'listserviceofferingsresponse',
+          responseKey2: 'serviceoffering',
+          field: 'name'
+        },
+        diskofferingid: {
+          apiName: 'listDiskOfferings',
+          responseKey1: 'listdiskofferingsresponse',
+          responseKey2: 'diskoffering',
+          field: 'name'
+        },
+        networkid: {
+          apiName: 'listNetworks',
+          responseKey1: 'listnetworksresponse',
+          responseKey2: 'network',
+          field: 'name'
+        }
+      }
+    }
+  },
+  updated () {
+    this.searchFilters = this.filters
+    const promises = []
+    for (const idx in this.filters) {
+      const filter = this.filters[idx]
+      if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+        continue
+      }
+      promises.push(new Promise((resolve) => {
+        if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+          resolve()
+        }
+        if (filter.key === 'tags') {
+          this.searchFilters[idx] = {
+            key: filter.key,
+            value: filter.value,
+            isTag: true
+          }
+        } else {
+          this.getSearchFilters(filter.key, filter.value).then((value) => {
+            this.searchFilters[idx] = {
+              key: filter.key,
+              value: value,
+              isTag: filter.isTag
+            }
+          })
+        }
+        resolve()
+      }))
+    }
+    Promise.all(promises)
+  },
+  methods: {
+    getTrimmedText (text, length) {
+      if (!text) {
+        return ''
+      }
+      return (text.length <= length) ? text : (text.substring(0, length - 3) + 
'...')
+    },
+    retrieveFieldLabel (fieldName) {
+      if (fieldName === 'groupid') {
+        fieldName = 'group'
+      }
+      if (fieldName === 'keyword') {
+        if ('listAnnotations' in this.$store.getters.apis) {
+          return this.$t('label.annotation')
+        } else {
+          return this.$t('label.name')
+        }
+      }
+      return this.$t('label.' + fieldName)
+    },
+    async getSearchFilters (key, value) {
+      const val = this.getStaticFieldValue(key, value)
+      if (val !== '') {
+        return val
+      } else {
+        return this.getDynamicFieldValue(key, value)
+      }
+    },
+    getStaticFieldValue (key, value) {
+      let formattedValue = ''
+      if (key.includes('type')) {
+        if (this.$route.path === '/guestnetwork' || 
this.$route.path.includes('/guestnetwork/')) {
+          formattedValue = this.getGuestNetworkType(value)
+        } else if (this.$route.path === '/role' || 
this.$route.path.includes('/role/')) {
+          formattedValue = this.getRoleType(value)
+        }
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getScope(value)
+      }
+
+      if (key.includes('state')) {
+        formattedValue = this.getState(value)
+      }
+
+      if (key.includes('level')) {
+        formattedValue = this.getLevel(value)
+      }
+
+      if (key.includes('entitytype')) {
+        formattedValue = this.getEntityType(value)
+      }
+
+      if (key.includes('accounttype')) {
+        formattedValue = this.getAccountType(value)
+      }
+
+      if (key.includes('systemvmtype')) {
+        formattedValue = this.getSystemVmType(value)
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getStoragePoolScope(value)
+      }
+

Review Comment:
   `getStaticFieldValue()` sets `formattedValue` twice for `scope` (lines 
243–245 and 267–269). For non-storage-pool pages, `getStoragePoolScope()` 
returns `undefined`, overwriting the earlier translated scope value; since 
`getSearchFilters()` treats any non-empty-string as “handled”, this can surface 
`undefined`/blank values. Avoid the duplicate `scope` block or ensure helper 
functions return `''` when not applicable so `getSearchFilters()` falls back 
correctly.



##########
ui/src/components/view/SearchFilter.vue:
##########
@@ -0,0 +1,555 @@
+// 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>
+  <a-row
+    style="overflow: auto"
+    :wrap="false"
+  >
+    <template
+      v-for="(filter) in this.searchFilters"
+      :key="filter"
+    >
+      <a-col v-if="!['page', 'pagesize', 'q', 'keyword', 
'tags'].includes(filter.key)">
+        <a-tag
+          v-if="!filter.isTag"
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="retrieveFieldLabel(filter.key) + ': ' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ retrieveFieldLabel(filter.key) }} : {{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+        <a-tag
+          v-else
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="$t('label.tag') + ': ' + filter.key + '=' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ $t('label.tag') }}: {{ filter.key }}={{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+      </a-col>
+    </template>
+  </a-row>
+</template>
+
+<script>
+
+import { api } from '@/api/index'
+
+export default {
+  name: 'SearchFilter',
+  props: {
+    filters: {
+      type: Array,
+      default: () => []
+    },
+    apiName: {
+      type: String,
+      default: () => ''
+    },
+    filterKey: {
+      type: String,
+      default: () => ''
+    },
+    filterValue: {
+      type: String,
+      default: () => ''
+    },
+    isTag: {
+      type: Boolean,
+      default: () => false
+    }
+  },
+  emits: ['removeFilter'],
+  data () {
+    return {
+      searchFilters: [],
+      apiMap: {
+        type: this.getType,
+        hypervisor: this.getHypervisor,
+        zoneid: {
+          apiName: 'listZones',
+          responseKey1: 'listzonesresponse',
+          responseKey2: 'zone',
+          field: 'name'
+        },
+        domainid: {
+          apiName: 'listDomains',
+          responseKey1: 'listdomainsresponse',
+          responseKey2: 'domain',
+          field: 'name'
+        },
+        account: {
+          apiName: 'listAccounts',
+          responseKey1: 'listaccountsresponse',
+          responseKey2: 'account',
+          field: 'name'
+        },
+        imagestoreid: {
+          apiName: 'listImageStores',
+          responseKey1: 'listimagestoresresponse',
+          responseKey2: 'imagestore',
+          field: 'name'
+        },
+        storageid: {
+          apiName: 'listStoragePools',
+          responseKey1: 'liststoragepoolsresponse',
+          responseKey2: 'storagepool',
+          field: 'name'
+        },
+        podid: {
+          apiName: 'listPods',
+          responseKey1: 'listpodsresponse',
+          responseKey2: 'pod',
+          field: 'name'
+        },
+        clusterid: {
+          apiName: 'listClusters',
+          responseKey1: 'listclustersresponse',
+          responseKey2: 'cluster',
+          field: 'name'
+        },
+        hostid: {
+          apiName: 'listHosts',
+          responseKey1: 'listhostsresponse',
+          responseKey2: 'host',
+          field: 'name'
+        },
+        groupid: {
+          apiName: 'listInstanceGroups',
+          responseKey1: 'listinstancegroupsresponse',
+          responseKey2: 'instancegroup',
+          field: 'name'
+        },
+        managementserverid: {
+          apiName: 'listManagementServers',
+          responseKey1: 'listmanagementserversresponse',
+          responseKey2: 'managementserver',
+          field: 'name'
+        },
+        serviceofferingid: {
+          apiName: 'listServiceOfferings',
+          responseKey1: 'listserviceofferingsresponse',
+          responseKey2: 'serviceoffering',
+          field: 'name'
+        },
+        diskofferingid: {
+          apiName: 'listDiskOfferings',
+          responseKey1: 'listdiskofferingsresponse',
+          responseKey2: 'diskoffering',
+          field: 'name'
+        },
+        networkid: {
+          apiName: 'listNetworks',
+          responseKey1: 'listnetworksresponse',
+          responseKey2: 'network',
+          field: 'name'
+        }
+      }
+    }
+  },
+  updated () {
+    this.searchFilters = this.filters
+    const promises = []
+    for (const idx in this.filters) {
+      const filter = this.filters[idx]
+      if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+        continue
+      }
+      promises.push(new Promise((resolve) => {
+        if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+          resolve()
+        }
+        if (filter.key === 'tags') {
+          this.searchFilters[idx] = {
+            key: filter.key,
+            value: filter.value,
+            isTag: true
+          }
+        } else {
+          this.getSearchFilters(filter.key, filter.value).then((value) => {
+            this.searchFilters[idx] = {
+              key: filter.key,
+              value: value,
+              isTag: filter.isTag
+            }
+          })
+        }
+        resolve()
+      }))
+    }
+    Promise.all(promises)
+  },
+  methods: {
+    getTrimmedText (text, length) {
+      if (!text) {
+        return ''
+      }
+      return (text.length <= length) ? text : (text.substring(0, length - 3) + 
'...')
+    },
+    retrieveFieldLabel (fieldName) {
+      if (fieldName === 'groupid') {
+        fieldName = 'group'
+      }
+      if (fieldName === 'keyword') {
+        if ('listAnnotations' in this.$store.getters.apis) {
+          return this.$t('label.annotation')
+        } else {
+          return this.$t('label.name')
+        }
+      }
+      return this.$t('label.' + fieldName)
+    },
+    async getSearchFilters (key, value) {
+      const val = this.getStaticFieldValue(key, value)
+      if (val !== '') {
+        return val
+      } else {
+        return this.getDynamicFieldValue(key, value)
+      }
+    },
+    getStaticFieldValue (key, value) {
+      let formattedValue = ''
+      if (key.includes('type')) {
+        if (this.$route.path === '/guestnetwork' || 
this.$route.path.includes('/guestnetwork/')) {
+          formattedValue = this.getGuestNetworkType(value)
+        } else if (this.$route.path === '/role' || 
this.$route.path.includes('/role/')) {
+          formattedValue = this.getRoleType(value)
+        }
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getScope(value)
+      }
+
+      if (key.includes('state')) {
+        formattedValue = this.getState(value)
+      }
+
+      if (key.includes('level')) {
+        formattedValue = this.getLevel(value)
+      }
+
+      if (key.includes('entitytype')) {
+        formattedValue = this.getEntityType(value)
+      }
+
+      if (key.includes('accounttype')) {
+        formattedValue = this.getAccountType(value)
+      }
+
+      if (key.includes('systemvmtype')) {
+        formattedValue = this.getSystemVmType(value)
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getStoragePoolScope(value)
+      }
+
+      if (key.includes('provider')) {
+        formattedValue = this.getImageStoreProvider(value)
+      }
+
+      if (key.includes('resourcetype')) {
+        formattedValue = value
+      }
+      return formattedValue
+    },
+    async getDynamicFieldValue (key, value) {
+      let formattedValue = ''
+
+      if (typeof this.apiMap[key] === 'function') {
+        formattedValue = await this.apiMap[key](value)
+      } else if (this.apiMap[key]) {
+        const apiName = this.apiMap[key].apiName
+        const responseKey1 = this.apiMap[key].responseKey1
+        const responseKey2 = this.apiMap[key].responseKey2
+        const field = this.apiMap[key].field
+        formattedValue = await this.getResourceNameById(apiName, responseKey1, 
responseKey2, value, field)
+      }
+      if (formattedValue === '') {
+        formattedValue = value
+      }
+      return formattedValue
+    },
+    getHypervisor (value) {
+      return new Promise((resolve) => {
+        api('listHypervisors').then(json => {
+          if (json?.listhypervisorsresponse?.hypervisor) {
+            for (const key in json.listhypervisorsresponse.hypervisor) {
+              const hypervisor = json.listhypervisorsresponse.hypervisor[key]
+              if (hypervisor.name === value) {
+                resolve(hypervisor.name)
+              }
+            }
+          }
+        }).catch(() => {
+          resolve(null)
+        })
+      })

Review Comment:
   `getHypervisor()` does not resolve the Promise when the API call succeeds 
but no matching hypervisor is found (or the response is missing the expected 
list). This leaves the awaiting code hanging indefinitely. Ensure the Promise 
always resolves (e.g., resolve('') after the loop / when the response has no 
matches).



##########
ui/src/components/view/SearchFilter.vue:
##########
@@ -0,0 +1,555 @@
+// 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>
+  <a-row
+    style="overflow: auto"
+    :wrap="false"
+  >
+    <template
+      v-for="(filter) in this.searchFilters"
+      :key="filter"
+    >
+      <a-col v-if="!['page', 'pagesize', 'q', 'keyword', 
'tags'].includes(filter.key)">
+        <a-tag
+          v-if="!filter.isTag"
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="retrieveFieldLabel(filter.key) + ': ' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ retrieveFieldLabel(filter.key) }} : {{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+        <a-tag
+          v-else
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="$t('label.tag') + ': ' + filter.key + '=' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ $t('label.tag') }}: {{ filter.key }}={{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+      </a-col>
+    </template>
+  </a-row>
+</template>
+
+<script>
+
+import { api } from '@/api/index'
+
+export default {
+  name: 'SearchFilter',
+  props: {
+    filters: {
+      type: Array,
+      default: () => []
+    },
+    apiName: {
+      type: String,
+      default: () => ''
+    },
+    filterKey: {
+      type: String,
+      default: () => ''
+    },
+    filterValue: {
+      type: String,
+      default: () => ''
+    },
+    isTag: {
+      type: Boolean,
+      default: () => false
+    }
+  },
+  emits: ['removeFilter'],
+  data () {
+    return {
+      searchFilters: [],
+      apiMap: {
+        type: this.getType,
+        hypervisor: this.getHypervisor,
+        zoneid: {
+          apiName: 'listZones',
+          responseKey1: 'listzonesresponse',
+          responseKey2: 'zone',
+          field: 'name'
+        },
+        domainid: {
+          apiName: 'listDomains',
+          responseKey1: 'listdomainsresponse',
+          responseKey2: 'domain',
+          field: 'name'
+        },
+        account: {
+          apiName: 'listAccounts',
+          responseKey1: 'listaccountsresponse',
+          responseKey2: 'account',
+          field: 'name'
+        },
+        imagestoreid: {
+          apiName: 'listImageStores',
+          responseKey1: 'listimagestoresresponse',
+          responseKey2: 'imagestore',
+          field: 'name'
+        },
+        storageid: {
+          apiName: 'listStoragePools',
+          responseKey1: 'liststoragepoolsresponse',
+          responseKey2: 'storagepool',
+          field: 'name'
+        },
+        podid: {
+          apiName: 'listPods',
+          responseKey1: 'listpodsresponse',
+          responseKey2: 'pod',
+          field: 'name'
+        },
+        clusterid: {
+          apiName: 'listClusters',
+          responseKey1: 'listclustersresponse',
+          responseKey2: 'cluster',
+          field: 'name'
+        },
+        hostid: {
+          apiName: 'listHosts',
+          responseKey1: 'listhostsresponse',
+          responseKey2: 'host',
+          field: 'name'
+        },
+        groupid: {
+          apiName: 'listInstanceGroups',
+          responseKey1: 'listinstancegroupsresponse',
+          responseKey2: 'instancegroup',
+          field: 'name'
+        },
+        managementserverid: {
+          apiName: 'listManagementServers',
+          responseKey1: 'listmanagementserversresponse',
+          responseKey2: 'managementserver',
+          field: 'name'
+        },
+        serviceofferingid: {
+          apiName: 'listServiceOfferings',
+          responseKey1: 'listserviceofferingsresponse',
+          responseKey2: 'serviceoffering',
+          field: 'name'
+        },
+        diskofferingid: {
+          apiName: 'listDiskOfferings',
+          responseKey1: 'listdiskofferingsresponse',
+          responseKey2: 'diskoffering',
+          field: 'name'
+        },
+        networkid: {
+          apiName: 'listNetworks',
+          responseKey1: 'listnetworksresponse',
+          responseKey2: 'network',
+          field: 'name'
+        }
+      }
+    }
+  },
+  updated () {
+    this.searchFilters = this.filters
+    const promises = []
+    for (const idx in this.filters) {
+      const filter = this.filters[idx]
+      if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+        continue
+      }
+      promises.push(new Promise((resolve) => {
+        if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+          resolve()
+        }
+        if (filter.key === 'tags') {
+          this.searchFilters[idx] = {
+            key: filter.key,
+            value: filter.value,
+            isTag: true
+          }
+        } else {
+          this.getSearchFilters(filter.key, filter.value).then((value) => {
+            this.searchFilters[idx] = {
+              key: filter.key,
+              value: value,
+              isTag: filter.isTag
+            }
+          })
+        }
+        resolve()
+      }))
+    }
+    Promise.all(promises)
+  },
+  methods: {
+    getTrimmedText (text, length) {
+      if (!text) {
+        return ''
+      }
+      return (text.length <= length) ? text : (text.substring(0, length - 3) + 
'...')
+    },
+    retrieveFieldLabel (fieldName) {
+      if (fieldName === 'groupid') {
+        fieldName = 'group'
+      }
+      if (fieldName === 'keyword') {
+        if ('listAnnotations' in this.$store.getters.apis) {
+          return this.$t('label.annotation')
+        } else {
+          return this.$t('label.name')
+        }
+      }
+      return this.$t('label.' + fieldName)
+    },
+    async getSearchFilters (key, value) {
+      const val = this.getStaticFieldValue(key, value)
+      if (val !== '') {
+        return val
+      } else {
+        return this.getDynamicFieldValue(key, value)
+      }
+    },
+    getStaticFieldValue (key, value) {
+      let formattedValue = ''
+      if (key.includes('type')) {
+        if (this.$route.path === '/guestnetwork' || 
this.$route.path.includes('/guestnetwork/')) {
+          formattedValue = this.getGuestNetworkType(value)
+        } else if (this.$route.path === '/role' || 
this.$route.path.includes('/role/')) {
+          formattedValue = this.getRoleType(value)
+        }
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getScope(value)
+      }
+
+      if (key.includes('state')) {
+        formattedValue = this.getState(value)
+      }
+
+      if (key.includes('level')) {
+        formattedValue = this.getLevel(value)
+      }
+
+      if (key.includes('entitytype')) {
+        formattedValue = this.getEntityType(value)
+      }
+
+      if (key.includes('accounttype')) {
+        formattedValue = this.getAccountType(value)
+      }
+
+      if (key.includes('systemvmtype')) {
+        formattedValue = this.getSystemVmType(value)
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getStoragePoolScope(value)
+      }
+
+      if (key.includes('provider')) {
+        formattedValue = this.getImageStoreProvider(value)
+      }
+
+      if (key.includes('resourcetype')) {
+        formattedValue = value
+      }
+      return formattedValue
+    },
+    async getDynamicFieldValue (key, value) {
+      let formattedValue = ''
+
+      if (typeof this.apiMap[key] === 'function') {
+        formattedValue = await this.apiMap[key](value)
+      } else if (this.apiMap[key]) {
+        const apiName = this.apiMap[key].apiName
+        const responseKey1 = this.apiMap[key].responseKey1
+        const responseKey2 = this.apiMap[key].responseKey2
+        const field = this.apiMap[key].field
+        formattedValue = await this.getResourceNameById(apiName, responseKey1, 
responseKey2, value, field)
+      }
+      if (formattedValue === '') {
+        formattedValue = value
+      }
+      return formattedValue
+    },
+    getHypervisor (value) {
+      return new Promise((resolve) => {
+        api('listHypervisors').then(json => {
+          if (json?.listhypervisorsresponse?.hypervisor) {
+            for (const key in json.listhypervisorsresponse.hypervisor) {
+              const hypervisor = json.listhypervisorsresponse.hypervisor[key]
+              if (hypervisor.name === value) {
+                resolve(hypervisor.name)
+              }
+            }
+          }
+        }).catch(() => {
+          resolve(null)
+        })
+      })
+    },
+    getResourceNameById (apiName, responseKey1, responseKey2, id, field) {
+      return new Promise((resolve) => {
+        if (!this.$isValidUuid(id)) {
+          return resolve('')
+        }
+        api(apiName, { listAll: true, id: id }).then(json => {
+          if (json[responseKey1] && json[responseKey1][responseKey2]) {
+            resolve(json[responseKey1][responseKey2][0][field])

Review Comment:
   `getResourceNameById()` only resolves when 
`json[responseKey1][responseKey2]` exists; otherwise the Promise never 
resolves, causing the UI to hang waiting for a label. Ensure you resolve `''` 
when the expected keys/items are missing or the array is empty.
   ```suggestion
             const items = json && json[responseKey1] && 
json[responseKey1][responseKey2]
             if (Array.isArray(items) && items.length > 0 && items[0] && 
items[0][field] !== undefined) {
               resolve(items[0][field])
             } else {
               resolve('')
   ```



##########
ui/src/views/AutogenView.vue:
##########
@@ -1126,6 +1138,42 @@ export default {
       eventBus.emit('action-closing', { action: this.currentAction })
       this.closeAction()
     },
+    getActiveFilters () {
+      const queryParams = Object.assign({}, this.$route.query)
+      const activeFilters = []
+      for (const filter in queryParams) {
+        if (!filter.startsWith('tags[')) {
+          activeFilters.push({
+            key: filter,
+            value: queryParams[filter],
+            isTag: false
+          })

Review Comment:
   `getActiveFilters()` currently turns every query param into a displayed 
“search filter”, including internal routing params like `action` (used to 
auto-open list actions and then removed in `fetchData`). This can cause 
transient/incorrect filter chips and allow users to remove non-search params. 
Consider explicitly excluding non-search query keys (e.g., `action`, and any 
other internal params) when building `activeFilters`.



##########
ui/src/components/view/SearchFilter.vue:
##########
@@ -0,0 +1,555 @@
+// 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>
+  <a-row
+    style="overflow: auto"
+    :wrap="false"
+  >
+    <template
+      v-for="(filter) in this.searchFilters"
+      :key="filter"
+    >
+      <a-col v-if="!['page', 'pagesize', 'q', 'keyword', 
'tags'].includes(filter.key)">
+        <a-tag
+          v-if="!filter.isTag"
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="retrieveFieldLabel(filter.key) + ': ' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ retrieveFieldLabel(filter.key) }} : {{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+        <a-tag
+          v-else
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="$t('label.tag') + ': ' + filter.key + '=' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ $t('label.tag') }}: {{ filter.key }}={{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+      </a-col>
+    </template>
+  </a-row>
+</template>
+
+<script>
+
+import { api } from '@/api/index'
+
+export default {
+  name: 'SearchFilter',
+  props: {
+    filters: {
+      type: Array,
+      default: () => []
+    },
+    apiName: {
+      type: String,
+      default: () => ''
+    },
+    filterKey: {
+      type: String,
+      default: () => ''
+    },
+    filterValue: {
+      type: String,
+      default: () => ''
+    },
+    isTag: {
+      type: Boolean,
+      default: () => false
+    }
+  },
+  emits: ['removeFilter'],
+  data () {
+    return {
+      searchFilters: [],
+      apiMap: {
+        type: this.getType,
+        hypervisor: this.getHypervisor,

Review Comment:
   `apiMap.type`/`apiMap.hypervisor` store unbound method references. When 
invoked as `this.apiMap[key](value)`, `this` inside `getType`/`getHypervisor` 
will be `apiMap` (not the component), breaking accesses like `this.$route`. 
Bind/call these functions with the component context (e.g., store lambdas or 
call with `.call(this, value)`).
   ```suggestion
           type: (value) => this.getType(value),
           hypervisor: (value) => this.getHypervisor(value),
   ```



##########
ui/src/components/view/SearchFilter.vue:
##########
@@ -0,0 +1,555 @@
+// 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>
+  <a-row
+    style="overflow: auto"
+    :wrap="false"
+  >
+    <template
+      v-for="(filter) in this.searchFilters"
+      :key="filter"
+    >
+      <a-col v-if="!['page', 'pagesize', 'q', 'keyword', 
'tags'].includes(filter.key)">
+        <a-tag
+          v-if="!filter.isTag"
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="retrieveFieldLabel(filter.key) + ': ' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ retrieveFieldLabel(filter.key) }} : {{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+        <a-tag
+          v-else
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="$t('label.tag') + ': ' + filter.key + '=' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ $t('label.tag') }}: {{ filter.key }}={{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+      </a-col>
+    </template>
+  </a-row>
+</template>
+
+<script>
+
+import { api } from '@/api/index'
+
+export default {
+  name: 'SearchFilter',
+  props: {
+    filters: {
+      type: Array,
+      default: () => []
+    },
+    apiName: {
+      type: String,
+      default: () => ''
+    },
+    filterKey: {
+      type: String,
+      default: () => ''
+    },
+    filterValue: {
+      type: String,
+      default: () => ''
+    },
+    isTag: {
+      type: Boolean,
+      default: () => false
+    }
+  },
+  emits: ['removeFilter'],
+  data () {
+    return {
+      searchFilters: [],
+      apiMap: {
+        type: this.getType,
+        hypervisor: this.getHypervisor,
+        zoneid: {
+          apiName: 'listZones',
+          responseKey1: 'listzonesresponse',
+          responseKey2: 'zone',
+          field: 'name'
+        },
+        domainid: {
+          apiName: 'listDomains',
+          responseKey1: 'listdomainsresponse',
+          responseKey2: 'domain',
+          field: 'name'
+        },
+        account: {
+          apiName: 'listAccounts',
+          responseKey1: 'listaccountsresponse',
+          responseKey2: 'account',
+          field: 'name'
+        },
+        imagestoreid: {
+          apiName: 'listImageStores',
+          responseKey1: 'listimagestoresresponse',
+          responseKey2: 'imagestore',
+          field: 'name'
+        },
+        storageid: {
+          apiName: 'listStoragePools',
+          responseKey1: 'liststoragepoolsresponse',
+          responseKey2: 'storagepool',
+          field: 'name'
+        },
+        podid: {
+          apiName: 'listPods',
+          responseKey1: 'listpodsresponse',
+          responseKey2: 'pod',
+          field: 'name'
+        },
+        clusterid: {
+          apiName: 'listClusters',
+          responseKey1: 'listclustersresponse',
+          responseKey2: 'cluster',
+          field: 'name'
+        },
+        hostid: {
+          apiName: 'listHosts',
+          responseKey1: 'listhostsresponse',
+          responseKey2: 'host',
+          field: 'name'
+        },
+        groupid: {
+          apiName: 'listInstanceGroups',
+          responseKey1: 'listinstancegroupsresponse',
+          responseKey2: 'instancegroup',
+          field: 'name'
+        },
+        managementserverid: {
+          apiName: 'listManagementServers',
+          responseKey1: 'listmanagementserversresponse',
+          responseKey2: 'managementserver',
+          field: 'name'
+        },
+        serviceofferingid: {
+          apiName: 'listServiceOfferings',
+          responseKey1: 'listserviceofferingsresponse',
+          responseKey2: 'serviceoffering',
+          field: 'name'
+        },
+        diskofferingid: {
+          apiName: 'listDiskOfferings',
+          responseKey1: 'listdiskofferingsresponse',
+          responseKey2: 'diskoffering',
+          field: 'name'
+        },
+        networkid: {
+          apiName: 'listNetworks',
+          responseKey1: 'listnetworksresponse',
+          responseKey2: 'network',
+          field: 'name'
+        }
+      }
+    }
+  },
+  updated () {
+    this.searchFilters = this.filters
+    const promises = []
+    for (const idx in this.filters) {
+      const filter = this.filters[idx]
+      if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+        continue
+      }
+      promises.push(new Promise((resolve) => {
+        if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+          resolve()
+        }
+        if (filter.key === 'tags') {
+          this.searchFilters[idx] = {
+            key: filter.key,
+            value: filter.value,
+            isTag: true
+          }
+        } else {
+          this.getSearchFilters(filter.key, filter.value).then((value) => {
+            this.searchFilters[idx] = {
+              key: filter.key,
+              value: value,
+              isTag: filter.isTag
+            }
+          })
+        }
+        resolve()
+      }))
+    }
+    Promise.all(promises)
+  },
+  methods: {
+    getTrimmedText (text, length) {
+      if (!text) {
+        return ''
+      }
+      return (text.length <= length) ? text : (text.substring(0, length - 3) + 
'...')
+    },
+    retrieveFieldLabel (fieldName) {
+      if (fieldName === 'groupid') {
+        fieldName = 'group'
+      }
+      if (fieldName === 'keyword') {
+        if ('listAnnotations' in this.$store.getters.apis) {
+          return this.$t('label.annotation')
+        } else {
+          return this.$t('label.name')
+        }
+      }
+      return this.$t('label.' + fieldName)
+    },
+    async getSearchFilters (key, value) {
+      const val = this.getStaticFieldValue(key, value)
+      if (val !== '') {
+        return val
+      } else {
+        return this.getDynamicFieldValue(key, value)
+      }
+    },
+    getStaticFieldValue (key, value) {
+      let formattedValue = ''
+      if (key.includes('type')) {
+        if (this.$route.path === '/guestnetwork' || 
this.$route.path.includes('/guestnetwork/')) {
+          formattedValue = this.getGuestNetworkType(value)
+        } else if (this.$route.path === '/role' || 
this.$route.path.includes('/role/')) {
+          formattedValue = this.getRoleType(value)
+        }
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getScope(value)
+      }
+
+      if (key.includes('state')) {
+        formattedValue = this.getState(value)
+      }
+
+      if (key.includes('level')) {
+        formattedValue = this.getLevel(value)
+      }
+
+      if (key.includes('entitytype')) {
+        formattedValue = this.getEntityType(value)
+      }
+
+      if (key.includes('accounttype')) {
+        formattedValue = this.getAccountType(value)
+      }
+
+      if (key.includes('systemvmtype')) {
+        formattedValue = this.getSystemVmType(value)
+      }
+
+      if (key.includes('scope')) {
+        formattedValue = this.getStoragePoolScope(value)
+      }
+
+      if (key.includes('provider')) {
+        formattedValue = this.getImageStoreProvider(value)
+      }
+
+      if (key.includes('resourcetype')) {
+        formattedValue = value
+      }
+      return formattedValue
+    },
+    async getDynamicFieldValue (key, value) {
+      let formattedValue = ''
+
+      if (typeof this.apiMap[key] === 'function') {
+        formattedValue = await this.apiMap[key](value)
+      } else if (this.apiMap[key]) {
+        const apiName = this.apiMap[key].apiName
+        const responseKey1 = this.apiMap[key].responseKey1
+        const responseKey2 = this.apiMap[key].responseKey2
+        const field = this.apiMap[key].field
+        formattedValue = await this.getResourceNameById(apiName, responseKey1, 
responseKey2, value, field)
+      }
+      if (formattedValue === '') {
+        formattedValue = value
+      }
+      return formattedValue
+    },
+    getHypervisor (value) {
+      return new Promise((resolve) => {
+        api('listHypervisors').then(json => {
+          if (json?.listhypervisorsresponse?.hypervisor) {
+            for (const key in json.listhypervisorsresponse.hypervisor) {
+              const hypervisor = json.listhypervisorsresponse.hypervisor[key]
+              if (hypervisor.name === value) {
+                resolve(hypervisor.name)
+              }
+            }
+          }
+        }).catch(() => {
+          resolve(null)
+        })
+      })
+    },
+    getResourceNameById (apiName, responseKey1, responseKey2, id, field) {
+      return new Promise((resolve) => {
+        if (!this.$isValidUuid(id)) {
+          return resolve('')
+        }
+        api(apiName, { listAll: true, id: id }).then(json => {
+          if (json[responseKey1] && json[responseKey1][responseKey2]) {
+            resolve(json[responseKey1][responseKey2][0][field])
+          }
+        }).catch(() => {
+          resolve('')
+        })
+      })
+    },
+    getType (type) {
+      if (this.$route.path === '/alert') {
+        return this.getAlertType(type)
+      } else if (this.$route.path === '/affinitygroup') {
+        return this.getAffinityGroupType(type)
+      }
+    },
+    getAlertType (type) {
+      return new Promise((resolve) => {
+        api('listAlertTypes').then(json => {
+          const alertTypes = {}
+          for (const key in json.listalerttypesresponse.alerttype) {
+            const alerttype = json.listalerttypesresponse.alerttype[key]
+            alertTypes[alerttype.id] = alerttype.name
+          }
+          resolve(alertTypes[type])
+        }).catch(() => {
+          resolve(null)
+        })
+      })
+    },
+    getAffinityGroupType (type) {
+      return new Promise((resolve) => {
+        api('listAffinityGroupTypes').then(json => {
+          const alertTypes = {}
+          for (const key in 
json.listaffinitygrouptypesresponse.affinityGroupType) {
+            const affinityGroupType = 
json.listaffinitygrouptypesresponse.affinityGroupType[key]
+            if (affinityGroupType.type === 'host anti-affinity') {
+              alertTypes[affinityGroupType.type] = 'host anti-affinity 
(Strict)'
+            } else if (affinityGroupType.type === 'host affinity') {
+              alertTypes[affinityGroupType.type] = 'host affinity (Strict)'
+            } else if (affinityGroupType.type === 'non-strict host 
anti-affinity') {
+              alertTypes[affinityGroupType.type] = 'host anti-affinity 
(Non-Strict)'
+            } else if (affinityGroupType.type === 'non-strict host affinity') {
+              alertTypes[affinityGroupType.type] = 'host affinity (Non-Strict)'
+            }
+          }
+          this.alertTypes = alertTypes
+          resolve(alertTypes[type])
+        }).catch(() => {
+          resolve(null)
+        })
+      })
+    },
+    getGuestNetworkType (value) {
+      switch (value.toLowerCase()) {
+        case 'isolated':
+          return this.$t('label.isolated')
+        case 'shared':
+          return this.$t('label.shared')
+        case 'l2':
+          return this.$t('label.l2')
+      }
+    },
+    getAccountType (type) {
+      const types = []
+      if (this.apiName.indexOf('listAccounts') > -1) {
+        switch (type) {
+          case '1':
+            return 'Admin'
+          case '2':
+            return 'DomainAdmin'
+          case '0':
+            return 'User'
+        }
+      }
+      return types
+    },
+    getSystemVmType (type) {
+      if (this.apiName.indexOf('listSystemVms') > -1) {
+        switch (type.toLowerCase()) {
+          case 'consoleproxy':
+            return this.$t('label.console.proxy.vm')
+          case 'secondarystoragevm':
+            return this.$t('label.secondary.storage.vm')
+        }
+      }
+    },
+    getStoragePoolScope (scope) {
+      if (this.apiName.indexOf('listStoragePools') > -1) {
+        switch (scope.toUpperCase()) {
+          case 'CLUSTER':
+            return this.$t('label.cluster')
+          case 'ZONE':
+            return this.$t('label.zone')
+          case 'REGION':
+            return this.$t('label.region')
+          case 'GLOBAL':
+            return this.$t('label.global')
+        }
+      }
+    },
+    getImageStoreProvider (provider) {
+      if (this.apiName.indexOf('listImageStores') > -1) {
+        switch (provider.toUpperCase()) {
+          case 'NFS':
+            return 'NFS'
+          case 'SMB':
+            return 'SMB/CIFS'
+          case 'S3':
+            return 'S3'
+          case 'SWIFT':
+            return 'Swift'
+        }
+      }
+    },
+    getRoleType (role) {
+      switch (role.toLowerCase()) {
+        case 'Admin'.toLowerCase():
+          return 'Admin'
+        case 'ResourceAdmin'.toLowerCase():
+          return 'ResourceAdmin'
+        case 'DomainAdmin'.toLowerCase():
+          return 'DomainAdmin'
+        case 'User'.toLowerCase():
+          return 'User'
+      }
+    },
+    getScope (scope) {
+      switch (scope.toLowerCase()) {
+        case 'local':
+          return this.$t('label.local')
+        case 'domain':
+          return this.$t('label.domain')
+        case 'global':
+          return this.$t('label.global')
+      }
+    },
+    getState (state) {
+      if (this.apiName.includes('listVolumes')) {
+        switch (state.toLowerCase()) {
+          case 'allocated':
+            return this.$t('label.allocated')
+          case 'ready':
+            return this.$t('label.isready')
+          case 'destroy':
+            return this.$t('label.destroy')
+          case 'expunging':
+            return this.$t('label.expunging')
+          case 'expunged':
+            return this.$t('label.expunged')
+          case 'migrating':
+            return this.$t('label.migrating')
+        }
+      } else if (this.apiName.includes('listKubernetesClusters')) {
+        switch (state.toLowerCase()) {
+          case 'created':
+            return this.$t('label.created')
+          case 'starting':
+            return this.$t('label.starting')
+          case 'running':
+            return this.$t('label.running')
+          case 'stopping':
+            return this.$t('label.stopping')
+          case 'stopped':
+            return this.$t('label.stopped')
+          case 'scaling':
+            return this.$t('label.scaling')
+          case 'upgrading':
+            return this.$t('label.upgrading')
+          case 'alert':
+            return this.$t('label.alert')
+          case 'recovering':
+            return this.$t('label.recovering')
+          case 'destroyed':
+            return this.$t('label.destroyed')
+          case 'destroying':
+            return this.$t('label.destroying')
+          case 'error':
+            return this.$t('label.error')
+        }
+      } else if (this.apiName.indexOf('listWebhooks') > -1) {
+        switch (state.toLowerCase()) {
+          case 'enabled':
+            return this.$t('label.enabled')
+          case 'disabled':
+            return this.$t('label.disabled')
+        }
+      }
+    },
+    getEntityType (type) {
+      let entityType = ''
+      if (this.apiName.indexOf('listAnnotations') > -1) {
+        const allowedTypes = {
+          VM: 'Virtual Machine',
+          HOST: 'Host',
+          VOLUME: 'Volume',
+          SNAPSHOT: 'Snapshot',
+          VM_SNAPSHOT: 'VM Snapshot',
+          INSTANCE_GROUP: 'Instance Group',
+          NETWORK: 'Network',
+          VPC: 'VPC',
+          PUBLIC_IP_ADDRESS: 'Public IP Address',
+          VPN_CUSTOMER_GATEWAY: 'VPC Customer Gateway',
+          TEMPLATE: 'Template',
+          ISO: 'ISO',
+          SSH_KEYPAIR: 'SSH Key Pair',
+          DOMAIN: 'Domain',
+          SERVICE_OFFERING: 'Service Offfering',

Review Comment:
   Typo in display label: "Service Offfering" should be "Service Offering".
   ```suggestion
             SERVICE_OFFERING: 'Service Offering',
   ```



##########
ui/src/components/view/SearchFilter.vue:
##########
@@ -0,0 +1,555 @@
+// 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>
+  <a-row
+    style="overflow: auto"
+    :wrap="false"
+  >
+    <template
+      v-for="(filter) in this.searchFilters"
+      :key="filter"
+    >
+      <a-col v-if="!['page', 'pagesize', 'q', 'keyword', 
'tags'].includes(filter.key)">
+        <a-tag
+          v-if="!filter.isTag"
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="retrieveFieldLabel(filter.key) + ': ' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ retrieveFieldLabel(filter.key) }} : {{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+        <a-tag
+          v-else
+          closable
+          @close="() => $emit('removeFilter', filter)"
+        >
+          <a-tooltip
+            :title="$t('label.tag') + ': ' + filter.key + '=' + filter.value"
+            :placement="tooltipPlacement"
+          >
+            {{ $t('label.tag') }}: {{ filter.key }}={{ 
getTrimmedText(filter.value, 20) }}
+          </a-tooltip>
+        </a-tag>
+      </a-col>
+    </template>
+  </a-row>
+</template>
+
+<script>
+
+import { api } from '@/api/index'
+
+export default {
+  name: 'SearchFilter',
+  props: {
+    filters: {
+      type: Array,
+      default: () => []
+    },
+    apiName: {
+      type: String,
+      default: () => ''
+    },
+    filterKey: {
+      type: String,
+      default: () => ''
+    },
+    filterValue: {
+      type: String,
+      default: () => ''
+    },
+    isTag: {
+      type: Boolean,
+      default: () => false
+    }
+  },
+  emits: ['removeFilter'],
+  data () {
+    return {
+      searchFilters: [],
+      apiMap: {
+        type: this.getType,
+        hypervisor: this.getHypervisor,
+        zoneid: {
+          apiName: 'listZones',
+          responseKey1: 'listzonesresponse',
+          responseKey2: 'zone',
+          field: 'name'
+        },
+        domainid: {
+          apiName: 'listDomains',
+          responseKey1: 'listdomainsresponse',
+          responseKey2: 'domain',
+          field: 'name'
+        },
+        account: {
+          apiName: 'listAccounts',
+          responseKey1: 'listaccountsresponse',
+          responseKey2: 'account',
+          field: 'name'
+        },
+        imagestoreid: {
+          apiName: 'listImageStores',
+          responseKey1: 'listimagestoresresponse',
+          responseKey2: 'imagestore',
+          field: 'name'
+        },
+        storageid: {
+          apiName: 'listStoragePools',
+          responseKey1: 'liststoragepoolsresponse',
+          responseKey2: 'storagepool',
+          field: 'name'
+        },
+        podid: {
+          apiName: 'listPods',
+          responseKey1: 'listpodsresponse',
+          responseKey2: 'pod',
+          field: 'name'
+        },
+        clusterid: {
+          apiName: 'listClusters',
+          responseKey1: 'listclustersresponse',
+          responseKey2: 'cluster',
+          field: 'name'
+        },
+        hostid: {
+          apiName: 'listHosts',
+          responseKey1: 'listhostsresponse',
+          responseKey2: 'host',
+          field: 'name'
+        },
+        groupid: {
+          apiName: 'listInstanceGroups',
+          responseKey1: 'listinstancegroupsresponse',
+          responseKey2: 'instancegroup',
+          field: 'name'
+        },
+        managementserverid: {
+          apiName: 'listManagementServers',
+          responseKey1: 'listmanagementserversresponse',
+          responseKey2: 'managementserver',
+          field: 'name'
+        },
+        serviceofferingid: {
+          apiName: 'listServiceOfferings',
+          responseKey1: 'listserviceofferingsresponse',
+          responseKey2: 'serviceoffering',
+          field: 'name'
+        },
+        diskofferingid: {
+          apiName: 'listDiskOfferings',
+          responseKey1: 'listdiskofferingsresponse',
+          responseKey2: 'diskoffering',
+          field: 'name'
+        },
+        networkid: {
+          apiName: 'listNetworks',
+          responseKey1: 'listnetworksresponse',
+          responseKey2: 'network',
+          field: 'name'
+        }
+      }
+    }
+  },
+  updated () {
+    this.searchFilters = this.filters
+    const promises = []
+    for (const idx in this.filters) {
+      const filter = this.filters[idx]
+      if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+        continue
+      }
+      promises.push(new Promise((resolve) => {
+        if (this.searchFilters[idx] && this.searchFilters[idx].value !== 
filter.value) {
+          resolve()
+        }
+        if (filter.key === 'tags') {
+          this.searchFilters[idx] = {
+            key: filter.key,
+            value: filter.value,
+            isTag: true
+          }
+        } else {
+          this.getSearchFilters(filter.key, filter.value).then((value) => {
+            this.searchFilters[idx] = {
+              key: filter.key,
+              value: value,
+              isTag: filter.isTag
+            }
+          })
+        }
+        resolve()
+      }))
+    }
+    Promise.all(promises)

Review Comment:
   The `updated()` hook assigns `this.searchFilters = this.filters` and then 
writes to `this.searchFilters[idx]`, which mutates the `filters` prop array 
(same reference) and can also trigger repeated update cycles. Replace this with 
a `watch` on `filters` that clones before mutation, or render directly from 
`filters` and keep formatted values in separate state keyed by filter identity.
   ```suggestion
     watch: {
       filters: {
         immediate: true,
         handler (newFilters) {
           const clonedFilters = newFilters.map(filter => ({ ...filter }))
           const promises = []
           for (let idx = 0; idx < clonedFilters.length; idx++) {
             const filter = clonedFilters[idx]
             promises.push(new Promise((resolve) => {
               if (filter.key === 'tags') {
                 clonedFilters[idx] = {
                   key: filter.key,
                   value: filter.value,
                   isTag: true
                 }
                 resolve()
               } else {
                 this.getSearchFilters(filter.key, filter.value).then((value) 
=> {
                   clonedFilters[idx] = {
                     key: filter.key,
                     value: value,
                     isTag: filter.isTag
                   }
                   resolve()
                 })
               }
             }))
           }
           Promise.all(promises).then(() => {
             this.searchFilters = clonedFilters
           })
         }
       }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to