This is an automated email from the ASF dual-hosted git repository.

sureshanaparti pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.20 by this push:
     new 243872a7710 Use infinite scroll select (#11991)
243872a7710 is described below

commit 243872a77103494b3c030439e4ee24071a03ef2a
Author: Vishesh <[email protected]>
AuthorDate: Thu Jan 29 11:40:23 2026 +0530

    Use infinite scroll select (#11991)
    
    * addresses the domain selection (listed after the page size) with keyword 
search
---
 ui/src/components/view/DedicateDomain.vue          | 129 +++++-------
 ui/src/components/widgets/InfiniteScrollSelect.vue |  91 ++++++++-
 ui/src/views/iam/AddUser.vue                       | 121 ++++-------
 ui/src/views/infra/UsageRecords.vue                | 112 +++++-----
 ui/src/views/storage/CreateTemplate.vue            | 111 ++++------
 ui/src/views/storage/UploadLocalVolume.vue         | 225 ++++++++-------------
 ui/src/views/storage/UploadVolume.vue              | 216 ++++++++------------
 ui/src/views/tools/CreateWebhook.vue               | 124 ++++--------
 ui/src/views/tools/ManageVolumes.vue               | 157 ++++++--------
 9 files changed, 558 insertions(+), 728 deletions(-)

diff --git a/ui/src/components/view/DedicateDomain.vue 
b/ui/src/components/view/DedicateDomain.vue
index 0b3645ce418..4b8cc31ae46 100644
--- a/ui/src/components/view/DedicateDomain.vue
+++ b/ui/src/components/view/DedicateDomain.vue
@@ -18,52 +18,44 @@
 <template>
   <div class="form">
     <div class="form__item" :class="{'error': domainError}">
-      <a-spin :spinning="domainsLoading">
-        <p class="form__label">{{ $t('label.domain') }}<span 
class="required">*</span></p>
-        <p class="required required-label">{{ $t('label.required') }}</p>
-        <a-select
-          style="width: 100%"
-          showSearch
-          optionFilterProp="label"
-          :filterOption="(input, option) => {
-            return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
-          }"
-          @change="handleChangeDomain"
-          v-focus="true"
-          v-model:value="domainId">
-          <a-select-option
-            v-for="(domain, index) in domainsList"
-            :value="domain.id"
-            :key="index"
-            :label="domain.path || domain.name || domain.description">
-            {{ domain.path || domain.name || domain.description }}
-          </a-select-option>
-        </a-select>
-      </a-spin>
+      <p class="form__label">{{ $t('label.domain') }}<span 
class="required">*</span></p>
+      <p class="required required-label">{{ $t('label.required') }}</p>
+      <infinite-scroll-select
+        style="width: 100%"
+        v-model:value="domainId"
+        api="listDomains"
+        :apiParams="domainsApiParams"
+        resourceType="domain"
+        optionValueKey="id"
+        optionLabelKey="path"
+        defaultIcon="block-outlined"
+        v-focus="true"
+        @change-option-value="handleChangeDomain" />
     </div>
-    <div class="form__item" v-if="accountsList">
+    <div class="form__item">
       <p class="form__label">{{ $t('label.account') }}</p>
-      <a-select
+      <infinite-scroll-select
         style="width: 100%"
-        @change="handleChangeAccount"
-        showSearch
-        optionFilterProp="value"
-        :filterOption="(input, option) => {
-          return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
-        }" >
-        <a-select-option v-for="(account, index) in accountsList" 
:value="account.name" :key="index">
-          {{ account.name }}
-        </a-select-option>
-      </a-select>
+        v-model:value="selectedAccount"
+        api="listAccounts"
+        :apiParams="accountsApiParams"
+        resourceType="account"
+        optionValueKey="name"
+        optionLabelKey="name"
+        defaultIcon="team-outlined"
+        @change-option-value="handleChangeAccount" />
     </div>
   </div>
 </template>
 
 <script>
-import { api } from '@/api'
+import InfiniteScrollSelect from 
'@/components/widgets/InfiniteScrollSelect.vue'
 
 export default {
   name: 'DedicateDomain',
+  components: {
+    InfiniteScrollSelect
+  },
   props: {
     error: {
       type: Boolean,
@@ -72,59 +64,48 @@ export default {
   },
   data () {
     return {
-      domainsLoading: false,
       domainId: null,
-      accountsList: null,
-      domainsList: null,
+      selectedAccount: null,
       domainError: false
     }
   },
+  computed: {
+    domainsApiParams () {
+      return {
+        listall: true,
+        details: 'min'
+      }
+    },
+    accountsApiParams () {
+      if (!this.domainId) {
+        return {
+          listall: true,
+          showicon: true
+        }
+      }
+      return {
+        showicon: true,
+        domainid: this.domainId
+      }
+    }
+  },
   watch: {
     error () {
       this.domainError = this.error
     }
   },
   created () {
-    this.fetchData()
   },
   methods: {
-    fetchData () {
-      this.domainsLoading = true
-      api('listDomains', {
-        listAll: true,
-        details: 'min'
-      }).then(response => {
-        this.domainsList = response.listdomainsresponse.domain
-
-        if (this.domainsList[0]) {
-          this.domainId = this.domainsList[0].id
-          this.handleChangeDomain(this.domainId)
-        }
-      }).catch(error => {
-        this.$notifyError(error)
-      }).finally(() => {
-        this.domainsLoading = false
-      })
-    },
-    fetchAccounts () {
-      api('listAccounts', {
-        domainid: this.domainId
-      }).then(response => {
-        this.accountsList = response.listaccountsresponse.account || []
-        if (this.accountsList && this.accountsList.length === 0) {
-          this.handleChangeAccount(null)
-        }
-      }).catch(error => {
-        this.$notifyError(error)
-      })
-    },
-    handleChangeDomain (e) {
-      this.$emit('domainChange', e)
+    handleChangeDomain (domainId) {
+      this.domainId = domainId
+      this.selectedAccount = null
+      this.$emit('domainChange', domainId)
       this.domainError = false
-      this.fetchAccounts()
     },
-    handleChangeAccount (e) {
-      this.$emit('accountChange', e)
+    handleChangeAccount (accountName) {
+      this.selectedAccount = accountName
+      this.$emit('accountChange', accountName)
     }
   }
 }
diff --git a/ui/src/components/widgets/InfiniteScrollSelect.vue 
b/ui/src/components/widgets/InfiniteScrollSelect.vue
index f97faf390f8..da780b66b80 100644
--- a/ui/src/components/widgets/InfiniteScrollSelect.vue
+++ b/ui/src/components/widgets/InfiniteScrollSelect.vue
@@ -41,8 +41,10 @@
   - optionValueKey (String, optional): Property to use as the value for 
options (e.g., 'name'). Default is 'id'
   - optionLabelKey (String, optional): Property to use as the label for 
options (e.g., 'name'). Default is 'name'
   - defaultOption (Object, optional): Preselected object to include initially
+  - allowClear (Boolean, optional): Whether to allow clearing the selection. 
Default is false
   - showIcon (Boolean, optional): Whether to show icon for the options. 
Default is true
   - defaultIcon (String, optional): Icon to be shown when there is no resource 
icon for the option. Default is 'cloud-outlined'
+  - selectFirstOption (Boolean, optional): Whether to automatically select the 
first option when options are loaded. Default is false
 
   Events:
   - @change-option-value (Function): Emits the selected option value(s) when 
value(s) changes. Do not use @change as it will give warnings and may not work
@@ -58,6 +60,7 @@
     :filter-option="false"
     :loading="loading"
     show-search
+    :allowClear="allowClear"
     placeholder="Select"
     @search="onSearchTimed"
     @popupScroll="onScroll"
@@ -75,9 +78,9 @@
         </div>
       </div>
     </template>
-    <a-select-option v-for="option in options" :key="option.id" 
:value="option[optionValueKey]">
+    <a-select-option v-for="option in selectableOptions" :key="option.id" 
:value="option[optionValueKey]">
       <span>
-        <span v-if="showIcon">
+        <span v-if="showIcon && option.id !== null && option.id !== undefined">
           <resource-icon v-if="option.icon && option.icon.base64image" 
:image="option.icon.base64image" size="1x" style="margin-right: 5px"/>
           <render-icon v-else :icon="defaultIcon" style="margin-right: 5px" />
         </span>
@@ -124,6 +127,10 @@ export default {
       type: Object,
       default: null
     },
+    allowClear: {
+      type: Boolean,
+      default: false
+    },
     showIcon: {
       type: Boolean,
       default: true
@@ -135,6 +142,10 @@ export default {
     pageSize: {
       type: Number,
       default: null
+    },
+    selectFirstOption: {
+      type: Boolean,
+      default: false
     }
   },
   data () {
@@ -147,7 +158,8 @@ export default {
       searchTimer: null,
       scrollHandlerAttached: false,
       preselectedOptionValue: null,
-      successiveFetches: 0
+      successiveFetches: 0,
+      hasAutoSelectedFirst: false
     }
   },
   created () {
@@ -166,6 +178,36 @@ export default {
     },
     formattedSearchFooterMessage () {
       return `${this.$t('label.showing.results.for').replace('%x', 
this.searchQuery)}`
+    },
+    selectableOptions () {
+      const currentValue = this.$attrs.value
+      // Only filter out null/empty options when the current value is also 
null/undefined/empty
+      // This prevents such options from being selected and allows the 
placeholder to show instead
+      if (currentValue === null || currentValue === undefined || currentValue 
=== '') {
+        return this.options.filter(option => {
+          const optionValue = option[this.optionValueKey]
+          return optionValue !== null && optionValue !== undefined && 
optionValue !== ''
+        })
+      }
+      // When a valid value is selected, show all options
+      return this.options
+    },
+    apiOptionsCount () {
+      if (this.defaultOption) {
+        const defaultOptionValue = this.defaultOption[this.optionValueKey]
+        return this.options.filter(option => option[this.optionValueKey] !== 
defaultOptionValue).length
+      }
+      return this.options.length
+    },
+    preselectedMatchValue () {
+      // Extract the first value from preselectedOptionValue if it's an array, 
otherwise return the value itself
+      if (!this.preselectedOptionValue) return null
+      return Array.isArray(this.preselectedOptionValue) ? 
this.preselectedOptionValue[0] : this.preselectedOptionValue
+    },
+    preselectedMatch () {
+      // Find the matching option for the preselected value
+      if (!this.preselectedMatchValue) return null
+      return this.options.find(entry => entry[this.optionValueKey] === 
this.preselectedMatchValue) || null
     }
   },
   watch: {
@@ -210,6 +252,7 @@ export default {
       }).finally(() => {
         if (this.successiveFetches === 0) {
           this.loading = false
+          this.autoSelectFirstOptionIfNeeded()
         }
       })
     },
@@ -220,11 +263,10 @@ export default {
         this.resetPreselectedOptionValue()
         return
       }
-      const matchValue = Array.isArray(this.preselectedOptionValue) ? 
this.preselectedOptionValue[0] : this.preselectedOptionValue
-      const match = this.options.find(entry => entry[this.optionValueKey] === 
matchValue)
-      if (!match) {
+      if (!this.preselectedMatch) {
         this.successiveFetches++
-        if (this.options.length < this.totalCount) {
+        // Exclude defaultOption from count when comparing with totalCount
+        if (this.apiOptionsCount < this.totalCount) {
           this.fetchItems()
         } else {
           this.resetPreselectedOptionValue()
@@ -232,7 +274,7 @@ export default {
         return
       }
       if (Array.isArray(this.preselectedOptionValue) && 
this.preselectedOptionValue.length > 1) {
-        this.preselectedOptionValue = this.preselectedOptionValue.filter(o => 
o !== match)
+        this.preselectedOptionValue = this.preselectedOptionValue.filter(o => 
o !== this.preselectedMatchValue)
       } else {
         this.resetPreselectedOptionValue()
       }
@@ -246,6 +288,36 @@ export default {
       this.preselectedOptionValue = null
       this.successiveFetches = 0
     },
+    autoSelectFirstOptionIfNeeded () {
+      if (!this.selectFirstOption || this.hasAutoSelectedFirst) {
+        return
+      }
+      // Don't auto-select if there's a preselected value being fetched
+      if (this.preselectedOptionValue) {
+        return
+      }
+      const currentValue = this.$attrs.value
+      if (currentValue !== undefined && currentValue !== null && currentValue 
!== '') {
+        return
+      }
+      if (this.options.length === 0) {
+        return
+      }
+      if (this.searchQuery && this.searchQuery.length > 0) {
+        return
+      }
+      // Only auto-select after initial load is complete (no more successive 
fetches)
+      if (this.successiveFetches > 0) {
+        return
+      }
+      const firstOption = this.options[0]
+      if (firstOption) {
+        const firstValue = firstOption[this.optionValueKey]
+        this.hasAutoSelectedFirst = true
+        this.$emit('change-option-value', firstValue)
+        this.$emit('change-option', firstOption)
+      }
+    },
     onSearchTimed (value) {
       clearTimeout(this.searchTimer)
       this.searchTimer = setTimeout(() => {
@@ -264,7 +336,8 @@ export default {
     },
     onScroll (e) {
       const nearBottom = e.target.scrollTop + e.target.clientHeight >= 
e.target.scrollHeight - 10
-      const hasMore = this.options.length < this.totalCount
+      // Exclude defaultOption from count when comparing with totalCount
+      const hasMore = this.apiOptionsCount < this.totalCount
       if (nearBottom && hasMore && !this.loading) {
         this.fetchItems()
       }
diff --git a/ui/src/views/iam/AddUser.vue b/ui/src/views/iam/AddUser.vue
index 49bca327896..3f0bd018050 100644
--- a/ui/src/views/iam/AddUser.vue
+++ b/ui/src/views/iam/AddUser.vue
@@ -90,45 +90,31 @@
           <template #label>
             <tooltip-label :title="$t('label.domainid')" 
:tooltip="apiParams.domainid.description"/>
           </template>
-          <a-select
-            :loading="domainLoading"
+          <infinite-scroll-select
             v-model:value="form.domainid"
+            api="listDomains"
+            :apiParams="domainsApiParams"
+            resourceType="domain"
+            optionValueKey="id"
+            optionLabelKey="path"
+            defaultIcon="block-outlined"
+            :selectFirstOption="true"
             :placeholder="apiParams.domainid.description"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return  option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }" >
-            <a-select-option v-for="domain in domainsList" :key="domain.id" 
:label="domain.path || domain.name || domain.description">
-              <span>
-                <resource-icon v-if="domain && domain.icon" 
:image="domain.icon.base64image" size="1x" style="margin-right: 5px"/>
-                <block-outlined v-else style="margin-right: 5px" />
-                {{ domain.path || domain.name || domain.description }}
-              </span>
-            </a-select-option>
-          </a-select>
+            @change-option-value="handleDomainChange" />
         </a-form-item>
         <a-form-item name="account" ref="account" v-if="!account">
           <template #label>
             <tooltip-label :title="$t('label.account')" 
:tooltip="apiParams.account.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.account"
-            :loading="loadingAccount"
-            :placeholder="apiParams.account.description"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return  option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }" >
-            <a-select-option v-for="(item, idx) in accountList" :key="idx" 
:label="item.name">
-              <span>
-                <resource-icon v-if="item && item.icon" 
:image="item.icon.base64image" size="1x" style="margin-right: 5px"/>
-                <team-outlined v-else style="margin-right: 5px" />
-                {{ item.name }}
-              </span>
-            </a-select-option>
-          </a-select>
+            api="listAccounts"
+            :apiParams="accountsApiParams"
+            resourceType="account"
+            optionValueKey="name"
+            optionLabelKey="name"
+            defaultIcon="team-outlined"
+            :placeholder="apiParams.account.description" />
         </a-form-item>
         <a-form-item name="timezone" ref="timezone">
           <template #label>
@@ -185,12 +171,14 @@ import { timeZone } from '@/utils/timezone'
 import debounce from 'lodash/debounce'
 import ResourceIcon from '@/components/view/ResourceIcon'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
+import InfiniteScrollSelect from 
'@/components/widgets/InfiniteScrollSelect.vue'
 
 export default {
   name: 'AddUser',
   components: {
     TooltipLabel,
-    ResourceIcon
+    ResourceIcon,
+    InfiniteScrollSelect
   },
   data () {
     this.fetchTimeZone = debounce(this.fetchTimeZone, 800)
@@ -198,14 +186,9 @@ export default {
       loading: false,
       timeZoneLoading: false,
       timeZoneMap: [],
-      domainLoading: false,
-      domainsList: [],
-      selectedDomain: '',
       samlEnable: false,
       idpLoading: false,
       idps: [],
-      loadingAccount: false,
-      accountList: [],
       account: null,
       domainid: null
     }
@@ -218,6 +201,19 @@ export default {
   computed: {
     samlAllowed () {
       return 'authorizeSamlSso' in this.$store.getters.apis
+    },
+    domainsApiParams () {
+      return {
+        listall: true,
+        showicon: true,
+        details: 'min'
+      }
+    },
+    accountsApiParams () {
+      return {
+        showicon: true,
+        domainid: this.form?.domainid || null
+      }
     }
   },
   methods: {
@@ -241,53 +237,18 @@ export default {
     fetchData () {
       this.account = this.$route.query && this.$route.query.account ? 
this.$route.query.account : null
       this.domainid = this.$route.query && this.$route.query.domainid ? 
this.$route.query.domainid : null
-      if (!this.domianid) {
-        this.fetchDomains()
+      // Set initial domain if provided from route
+      if (this.domainid) {
+        this.form.domainid = this.domainid
       }
       this.fetchTimeZone()
       if (this.samlAllowed) {
         this.fetchIdps()
       }
     },
-    fetchDomains () {
-      this.domainLoading = true
-      var params = {
-        listAll: true,
-        showicon: true,
-        details: 'min'
-      }
-      api('listDomains', params).then(response => {
-        this.domainsList = response.listdomainsresponse.domain || []
-      }).catch(error => {
-        this.$notification.error({
-          message: `${this.$t('label.error')} ${error.response.status}`,
-          description: error.response.data.errorresponse.errortext
-        })
-      }).finally(() => {
-        const domainid = this.domainsList[0]?.id || ''
-        this.form.domainid = domainid
-        this.fetchAccount(domainid)
-        this.domainLoading = false
-      })
-    },
-    fetchAccount (domainid) {
-      this.accountList = []
+    handleDomainChange (domainId) {
+      this.form.domainid = domainId
       this.form.account = null
-      this.loadingAccount = true
-      var params = { listAll: true, showicon: true }
-      if (domainid) {
-        params.domainid = domainid
-      }
-      api('listAccounts', params).then(response => {
-        this.accountList = response.listaccountsresponse.account || []
-      }).catch(error => {
-        this.$notification.error({
-          message: `${this.$t('label.error')} ${error.response.status}`,
-          description: error.response.data.errorresponse.errortext
-        })
-      }).finally(() => {
-        this.loadingAccount = false
-      })
     },
     fetchTimeZone (value) {
       this.timeZoneMap = []
@@ -328,12 +289,14 @@ export default {
           accounttype: 0
         }
 
+        // Account: use route query account if available, otherwise use form 
value (which is the account name)
         if (this.account) {
           params.account = this.account
-        } else if (this.accountList[values.account]) {
-          params.account = this.accountList[values.account].name
+        } else if (values.account) {
+          params.account = values.account
         }
 
+        // Domain: use route query domainid if available, otherwise use form 
value
         if (this.domainid) {
           params.domainid = this.domainid
         } else if (values.domainid) {
diff --git a/ui/src/views/infra/UsageRecords.vue 
b/ui/src/views/infra/UsageRecords.vue
index feb1d88bd9b..0a41aa4052c 100644
--- a/ui/src/views/infra/UsageRecords.vue
+++ b/ui/src/views/infra/UsageRecords.vue
@@ -121,15 +121,20 @@
                   ref="domain"
                   name="domain"
                 >
-                  <a-auto-complete
+                  <infinite-scroll-select
                     v-model:value="form.domain"
-                    :options="domains"
+                    api="listDomains"
+                    :apiParams="domainsApiParams"
+                    resourceType="domain"
+                    optionValueKey="id"
+                    optionLabelKey="path"
+                    defaultIcon="block-outlined"
                     :placeholder="$t('label.domain')"
-                    :filter-option="filterOption"
+                    :defaultOption="{ id: null, path: ''}"
+                    :allowClear="true"
                     style="width: 100%;"
-                    @select="getAccounts"
-                    :dropdownMatchSelectWidth="false"
-                  />
+                    @change-option-value="handleDomainChange"
+                    @change-option="handleDomainOptionChange" />
                 </a-form-item>
               </a-col>
             </a-row>&nbsp;
@@ -150,15 +155,20 @@
               ref="account"
               name="account"
             >
-            <a-auto-complete
+              <infinite-scroll-select
                 v-model:value="form.account"
-                :options="accounts"
+                api="listAccounts"
+                :apiParams="accountsApiParams"
+                resourceType="account"
+                optionValueKey="id"
+                optionLabelKey="name"
+                defaultIcon="team-outlined"
                 :placeholder="$t('label.account')"
-                :filter-option="filterOption"
                 :disabled="form.isRecursive"
-                :dropdownMatchSelectWidth="false"
-                @select="selectAccount"
-              />
+                :defaultOption="{ id: null, name: ''}"
+                allowClear="true"
+                @change-option-value="selectAccount"
+                @change-option="selectAccountOption" />
             </a-form-item>
           </a-col>
           <a-col :span="3" v-if="'listUsageTypes' in $store.getters.apis">
@@ -361,6 +371,7 @@ import ListView from '@/components/view/ListView'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
 import TooltipButton from '@/components/widgets/TooltipButton'
 import Status from '@/components/widgets/Status'
+import InfiniteScrollSelect from 
'@/components/widgets/InfiniteScrollSelect.vue'
 
 dayjs.extend(relativeTime)
 dayjs.extend(utc)
@@ -374,7 +385,8 @@ export default {
     ListView,
     Status,
     TooltipLabel,
-    TooltipButton
+    TooltipButton,
+    InfiniteScrollSelect
   },
   props: {
     resource: {
@@ -402,8 +414,6 @@ export default {
       page: 1,
       pageSize: 20,
       usageTypes: [],
-      domains: [],
-      accounts: [],
       account: null,
       domain: null,
       usageType: null,
@@ -436,6 +446,23 @@ export default {
     this.fetchData()
     this.updateColumns()
   },
+  computed: {
+    domainsApiParams () {
+      return {
+        listall: true
+      }
+    },
+    accountsApiParams () {
+      if (!this.form.domain) {
+        return {
+          listall: true
+        }
+      }
+      return {
+        domainid: this.form.domain
+      }
+    }
+  },
   methods: {
     clearFilters () {
       this.formRef.value.resetFields()
@@ -445,8 +472,6 @@ export default {
       this.usageType = null
       this.page = 1
       this.pageSize = 20
-
-      this.getAccounts()
     },
     disabledDate (current) {
       return current && current > dayjs().endOf('day')
@@ -473,8 +498,6 @@ export default {
       this.listUsageServerMetrics()
       this.getUsageTypes()
       this.getAllUsageRecordColumns()
-      this.getDomains()
-      this.getAccounts()
       if (!this.$store.getters.customColumns[this.$store.getters.userInfo.id]) 
{
         this.$store.getters.customColumns[this.$store.getters.userInfo.id] = {}
         
this.$store.getters.customColumns[this.$store.getters.userInfo.id][this.$route.path]
 = this.selectedColumnKeys
@@ -528,16 +551,6 @@ export default {
         this.formRef.value.scrollToField(error.errorFields[0].name)
       })
     },
-    selectAccount (value, option) {
-      if (option && option.id) {
-        this.account = option
-      } else {
-        this.account = null
-        if (this.formRef?.value) {
-          this.formRef.value.resetFields('account')
-        }
-      }
-    },
     selectUsageType (value, option) {
       if (option && option.id) {
         this.usageType = option
@@ -548,24 +561,12 @@ export default {
         }
       }
     },
-    getDomains () {
-      api('listDomains', { listAll: true }).then(json => {
-        if (json && json.listdomainsresponse && 
json.listdomainsresponse.domain) {
-          this.domains = [{ id: null, value: '' }, 
...json.listdomainsresponse.domain.map(x => {
-            return {
-              id: x.id,
-              value: x.path
-            }
-          })]
-        }
-      })
+    handleDomainChange (domainId) {
+      this.form.domain = domainId
+      this.form.account = null
     },
-    getAccounts (value, option) {
-      var params = {
-        listAll: true
-      }
+    handleDomainOptionChange (option) {
       if (option && option.id) {
-        params.domainid = option.id
         this.domain = option
       } else {
         this.domain = null
@@ -573,16 +574,19 @@ export default {
           this.formRef.value.resetFields('domain')
         }
       }
-      api('listAccounts', params).then(json => {
-        if (json && json.listaccountsresponse && 
json.listaccountsresponse.account) {
-          this.accounts = [{ id: null, value: '' }, 
...json.listaccountsresponse.account.map(x => {
-            return {
-              id: x.id,
-              value: x.name
-            }
-          })]
+    },
+    selectAccount (accountId) {
+      this.form.account = accountId
+    },
+    selectAccountOption (option) {
+      if (option && option.id) {
+        this.account = option
+      } else {
+        this.account = null
+        if (this.formRef?.value) {
+          this.formRef.value.resetFields('account')
         }
-      })
+      }
     },
     getParams (page, pageSize) {
       const formRaw = toRaw(this.form)
diff --git a/ui/src/views/storage/CreateTemplate.vue 
b/ui/src/views/storage/CreateTemplate.vue
index d974092a5f8..819d0cc0ac3 100644
--- a/ui/src/views/storage/CreateTemplate.vue
+++ b/ui/src/views/storage/CreateTemplate.vue
@@ -73,42 +73,32 @@
           <template #label>
             <tooltip-label :title="$t('label.domainid')" 
:tooltip="apiParams.domainid.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.domainid"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }"
-            :loading="domainLoading"
+            api="listDomains"
+            :apiParams="domainsApiParams"
+            resourceType="domain"
+            optionValueKey="id"
+            optionLabelKey="path"
+            defaultIcon="block-outlined"
+            allowClear="true"
             :placeholder="apiParams.domainid.description"
-            @change="val => { handleDomainChange(val) }">
-            <a-select-option v-for="(opt, optIndex) in this.domains" 
:key="optIndex" :label="opt.path || opt.name || opt.description" 
:value="opt.id">
-              <span>
-                <resource-icon v-if="opt && opt.icon" 
:image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
-                <block-outlined v-else style="margin-right: 5px" />
-                {{ opt.path || opt.name || opt.description }}
-              </span>
-            </a-select-option>
-          </a-select>
+            @change-option-value="handleDomainChange" />
         </a-form-item>
         <a-form-item name="account" ref="account" v-if="domainid">
           <template #label>
             <tooltip-label :title="$t('label.account')" 
:tooltip="apiParams.account.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.account"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.value.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }"
-            :placeholder="apiParams.account.description"
-            @change="val => { handleAccountChange(val) }">
-            <a-select-option v-for="(acc, index) in accounts" 
:value="acc.name" :key="index">
-              {{ acc.name }}
-            </a-select-option>
-          </a-select>
+            api="listAccounts"
+            :apiParams="accountsApiParams"
+            resourceType="account"
+            optionValueKey="name"
+            optionLabelKey="name"
+            defaultIcon="team-outlined"
+            allowClear="true"
+            :placeholder="apiParams.account.description" />
         </a-form-item>
 
       <a-form-item
@@ -199,13 +189,15 @@ import { api } from '@/api'
 import { mixinForm } from '@/utils/mixin'
 import ResourceIcon from '@/components/view/ResourceIcon'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
+import InfiniteScrollSelect from 
'@/components/widgets/InfiniteScrollSelect.vue'
 
 export default {
   name: 'CreateTemplate',
   mixins: [mixinForm],
   components: {
     ResourceIcon,
-    TooltipLabel
+    TooltipLabel,
+    InfiniteScrollSelect
   },
   props: {
     resource: {
@@ -219,9 +211,6 @@ export default {
       zones: [],
       osTypes: {},
       loading: false,
-      domains: [],
-      accounts: [],
-      domainLoading: false,
       domainid: null,
       account: null,
       architectureTypes: {}
@@ -230,6 +219,21 @@ export default {
   computed: {
     isAdminRole () {
       return this.$store.getters.userInfo.roletype === 'Admin'
+    },
+    domainsApiParams () {
+      return {
+        listall: true,
+        showicon: true,
+        details: 'min'
+      }
+    },
+    accountsApiParams () {
+      if (!this.domainid) {
+        return null
+      }
+      return {
+        domainid: this.domainid
+      }
     }
   },
   beforeCreate () {
@@ -256,9 +260,6 @@ export default {
       if (this.resource.intervaltype) {
         this.fetchSnapshotZones()
       }
-      if ('listDomains' in this.$store.getters.apis) {
-        this.fetchDomains()
-      }
       this.architectureTypes.opts = this.$fetchCpuArchitectureTypes()
     },
     fetchOsTypes () {
@@ -309,44 +310,16 @@ export default {
         }
       })
     },
-    fetchDomains () {
-      const params = {}
-      params.listAll = true
-      params.showicon = true
-      params.details = 'min'
-      this.domainLoading = true
-      api('listDomains', params).then(json => {
-        this.domains = json.listdomainsresponse.domain
-      }).finally(() => {
-        this.domainLoading = false
-        this.handleDomainChange(null)
-      })
-    },
-    async handleDomainChange (domain) {
-      this.domainid = domain
+    handleDomainChange (domainId) {
+      this.domainid = domainId
       this.form.account = null
       this.account = null
-      if ('listAccounts' in this.$store.getters.apis) {
-        await this.fetchAccounts()
-      }
-    },
-    fetchAccounts () {
-      return new Promise((resolve, reject) => {
-        api('listAccounts', {
-          domainid: this.domainid
-        }).then(response => {
-          this.accounts = response?.listaccountsresponse?.account || []
-          resolve(this.accounts)
-        }).catch(error => {
-          this.$notifyError(error)
-        })
-      })
     },
-    handleAccountChange (acc) {
-      if (acc) {
-        this.account = acc.name
+    handleAccountChange (accountName) {
+      if (accountName) {
+        this.account = accountName
       } else {
-        this.account = acc
+        this.account = null
       }
     },
     handleSubmit (e) {
diff --git a/ui/src/views/storage/UploadLocalVolume.vue 
b/ui/src/views/storage/UploadLocalVolume.vue
index 75775c9010a..3a0bf4e129f 100644
--- a/ui/src/views/storage/UploadLocalVolume.vue
+++ b/ui/src/views/storage/UploadLocalVolume.vue
@@ -57,43 +57,33 @@
           <template #label>
             <tooltip-label :title="$t('label.zoneid')" 
:tooltip="apiParams.zoneid.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.zoneId"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }" >
-            <a-select-option :value="zone.id" v-for="zone in zones" 
:key="zone.id" :label="zone.name || zone.description">
-              <span>
-                <resource-icon v-if="zone.icon" :image="zone.icon.base64image" 
size="1x" style="margin-right: 5px"/>
-                <global-outlined v-else style="margin-right: 5px"/>
-                {{ zone.name || zone.description }}
-              </span>
-            </a-select-option>
-          </a-select>
+            api="listZones"
+            :apiParams="zonesApiParams"
+            resourceType="zone"
+            optionValueKey="id"
+            optionLabelKey="name"
+            defaultIcon="global-outlined"
+            selectFirstOption="true"
+            @change-option-value="handleZoneChange" />
         </a-form-item>
         <a-form-item name="diskofferingid" ref="diskofferingid">
           <template #label>
             <tooltip-label :title="$t('label.diskofferingid')" 
:tooltip="apiParams.diskofferingid.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.diskofferingid"
-            :loading="offeringLoading"
+            api="listDiskOfferings"
+            :apiParams="diskOfferingsApiParams"
+            resourceType="diskoffering"
+            optionValueKey="id"
+            optionLabelKey="displaytext"
+            defaultIcon="hdd-outlined"
+            :defaultOption="{ id: null, displaytext: ''}"
+            allowClear="true"
             :placeholder="apiParams.diskofferingid.description"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }" >
-            <a-select-option
-              v-for="(offering, index) in offerings"
-              :value="offering.id"
-              :key="index"
-              :label="offering.displaytext || offering.name">
-              {{ offering.displaytext || offering.name }}
-            </a-select-option>
-          </a-select>
+            @change-option="onChangeDiskOffering" />
         </a-form-item>
         <a-form-item ref="format" name="format">
           <template #label>
@@ -124,38 +114,33 @@
           <template #label>
             <tooltip-label :title="$t('label.domain')" 
:tooltip="apiParams.domainid.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.domainid"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }"
-            :loading="domainLoading"
+            api="listDomains"
+            :apiParams="domainsApiParams"
+            resourceType="domain"
+            optionValueKey="id"
+            optionLabelKey="path"
+            defaultIcon="block-outlined"
             :placeholder="$t('label.domainid')"
-            @change="val => { handleDomainChange(domainList[val].id) }">
-            <a-select-option v-for="(opt, optIndex) in domainList" 
:key="optIndex" :label="opt.path || opt.name || opt.description">
-              {{ opt.path || opt.name || opt.description }}
-            </a-select-option>
-          </a-select>
+            allowClear="true"
+            @change-option-value="handleDomainChange" />
         </a-form-item>
         <a-form-item name="account" ref="account" v-if="'listDomains' in 
$store.getters.apis">
           <template #label>
             <tooltip-label :title="$t('label.account')" 
:tooltip="apiParams.account.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.account"
-            showSearch
-            optionFilterProp="value"
-            :filterOption="(input, option) => {
-              return option.value.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }"
+            api="listAccounts"
+            :apiParams="accountsApiParams"
+            resourceType="account"
+            optionValueKey="name"
+            optionLabelKey="name"
+            defaultIcon="team-outlined"
+            allowClear="true"
             :placeholder="$t('label.account')"
-            @change="val => { handleAccountChange(val) }">
-            <a-select-option v-for="(acc, index) in accountList" 
:value="acc.name" :key="index">
-              {{ acc.name }}
-            </a-select-option>
-          </a-select>
+            @change-option-value="handleAccountChange" />
         </a-form-item>
         <div :span="24" class="action-button">
           <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
@@ -173,27 +158,25 @@ import { axios } from '../../utils/request'
 import { mixinForm } from '@/utils/mixin'
 import ResourceIcon from '@/components/view/ResourceIcon'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
+import InfiniteScrollSelect from 
'@/components/widgets/InfiniteScrollSelect.vue'
 
 export default {
   name: 'UploadLocalVolume',
   mixins: [mixinForm],
   components: {
     ResourceIcon,
-    TooltipLabel
+    TooltipLabel,
+    InfiniteScrollSelect
   },
   data () {
     return {
       fileList: [],
-      zones: [],
-      domainList: [],
-      accountList: [],
-      offerings: [],
-      offeringLoading: false,
       formats: ['RAW', 'VHD', 'VHDX', 'OVA', 'QCOW2'],
       domainId: null,
       account: null,
       uploadParams: null,
-      domainLoading: false,
+      customDiskOffering: false,
+      isCustomizedDiskIOps: false,
       loading: false,
       uploadPercentage: 0
     }
@@ -201,9 +184,38 @@ export default {
   beforeCreate () {
     this.apiParams = this.$getApiParams('getUploadParamsForVolume')
   },
+  computed: {
+    zonesApiParams () {
+      return {
+        showicon: true
+      }
+    },
+    diskOfferingsApiParams () {
+      if (!this.form.zoneId) {
+        return null
+      }
+      return {
+        zoneid: this.form.zoneId,
+        listall: true
+      }
+    },
+    domainsApiParams () {
+      return {
+        listall: true,
+        details: 'min'
+      }
+    },
+    accountsApiParams () {
+      if (!this.form.domainid) {
+        return null
+      }
+      return {
+        domainid: this.form.domainid
+      }
+    }
+  },
   created () {
     this.initForm()
-    this.fetchData()
   },
   methods: {
     initForm () {
@@ -221,38 +233,18 @@ export default {
         zoneId: [{ required: true, message: this.$t('message.error.select') }]
       })
     },
-    listZones () {
-      api('listZones', { showicon: true }).then(json => {
-        if (json && json.listzonesresponse && json.listzonesresponse.zone) {
-          this.zones = json.listzonesresponse.zone
-          this.zones = this.zones.filter(zone => zone.type !== 'Edge')
-          if (this.zones.length > 0) {
-            this.onZoneChange(this.zones[0].id)
-          }
-        }
-      })
-    },
-    onZoneChange (zoneId) {
+    handleZoneChange (zoneId) {
       this.form.zoneId = zoneId
-      this.zoneId = zoneId
-      this.fetchDiskOfferings(zoneId)
+      // InfiniteScrollSelect will auto-reload disk offerings when apiParams 
changes
     },
-    fetchDiskOfferings (zoneId) {
-      this.offeringLoading = true
-      this.offerings = [{ id: -1, name: '' }]
-      this.form.diskofferingid = undefined
-      api('listDiskOfferings', {
-        zoneid: zoneId,
-        listall: true
-      }).then(json => {
-        for (var offering of json.listdiskofferingsresponse.diskoffering) {
-          if (offering.iscustomized) {
-            this.offerings.push(offering)
-          }
-        }
-      }).finally(() => {
-        this.offeringLoading = false
-      })
+    onChangeDiskOffering (offering) {
+      if (offering) {
+        this.customDiskOffering = offering.iscustomized || false
+        this.isCustomizedDiskIOps = offering.iscustomizediops || false
+      } else {
+        this.customDiskOffering = false
+        this.isCustomizedDiskIOps = false
+      }
     },
     handleRemove (file) {
       const index = this.fileList.indexOf(file)
@@ -266,53 +258,14 @@ export default {
       this.form.file = file
       return false
     },
-    handleDomainChange (domain) {
-      this.domainId = domain
-      if ('listAccounts' in this.$store.getters.apis) {
-        this.fetchAccounts()
-      }
-    },
-    handleAccountChange (acc) {
-      if (acc) {
-        this.account = acc.name
-      } else {
-        this.account = acc
-      }
-    },
-    fetchData () {
-      this.listZones()
-      if ('listDomains' in this.$store.getters.apis) {
-        this.fetchDomains()
-      }
+    handleDomainChange (domainId) {
+      this.form.domainid = domainId
+      this.domainId = domainId
+      this.form.account = null
     },
-    fetchDomains () {
-      this.domainLoading = true
-      api('listDomains', {
-        listAll: true,
-        details: 'min'
-      }).then(response => {
-        this.domainList = response.listdomainsresponse.domain
-
-        if (this.domainList[0]) {
-          this.handleDomainChange(null)
-        }
-      }).catch(error => {
-        this.$notifyError(error)
-      }).finally(() => {
-        this.domainLoading = false
-      })
-    },
-    fetchAccounts () {
-      api('listAccounts', {
-        domainid: this.domainId
-      }).then(response => {
-        this.accountList = response.listaccountsresponse.account || []
-        if (this.accountList && this.accountList.length === 0) {
-          this.handleAccountChange(null)
-        }
-      }).catch(error => {
-        this.$notifyError(error)
-      })
+    handleAccountChange (accountName) {
+      this.form.account = accountName
+      this.account = accountName
     },
     handleSubmit (e) {
       e.preventDefault()
diff --git a/ui/src/views/storage/UploadVolume.vue 
b/ui/src/views/storage/UploadVolume.vue
index 937c3ad76aa..c2cbaabc225 100644
--- a/ui/src/views/storage/UploadVolume.vue
+++ b/ui/src/views/storage/UploadVolume.vue
@@ -47,21 +47,16 @@
           <template #label>
             <tooltip-label :title="$t('label.zoneid')" 
:tooltip="apiParams.zoneid.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.zoneId"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }" >
-            <a-select-option :value="zone.id" v-for="zone in zones" 
:key="zone.id" :label="zone.name || zone.description">
-              <span>
-                <resource-icon v-if="zone.icon" :image="zone.icon.base64image" 
size="1x" style="margin-right: 5px"/>
-                <global-outlined v-else style="margin-right: 5px"/>
-                {{ zone.name || zone.description }}
-              </span>
-            </a-select-option>
-          </a-select>
+            api="listZones"
+            :apiParams="zonesApiParams"
+            resourceType="zone"
+            optionValueKey="id"
+            optionLabelKey="name"
+            defaultIcon="global-outlined"
+            selectFirstOption="true"
+            @change-option-value="handleZoneChange" />
         </a-form-item>
         <a-form-item name="format" ref="format">
           <template #label>
@@ -83,23 +78,17 @@
           <template #label>
             <tooltip-label :title="$t('label.diskofferingid')" 
:tooltip="apiParams.diskofferingid.description || $t('label.diskoffering')"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.diskofferingid"
-            :loading="loading"
-            @change="id => onChangeDiskOffering(id)"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }" >
-            <a-select-option
-              v-for="(offering, index) in offerings"
-              :value="offering.id"
-              :key="index"
-              :label="offering.displaytext || offering.name">
-              {{ offering.displaytext || offering.name }}
-            </a-select-option>
-          </a-select>
+            api="listDiskOfferings"
+            :apiParams="diskOfferingsApiParams"
+            resourceType="diskoffering"
+            optionValueKey="id"
+            optionLabelKey="displaytext"
+            defaultIcon="hdd-outlined"
+            :defaultOption="{ id: null, displaytext: ''}"
+            allowClear="true"
+            @change-option="onChangeDiskOffering" />
         </a-form-item>
         <a-form-item name="checksum" ref="checksum">
           <template #label>
@@ -114,38 +103,33 @@
           <template #label>
             <tooltip-label :title="$t('label.domain')" 
:tooltip="apiParams.domainid.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.domainid"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }"
-            :loading="domainLoading"
+            api="listDomains"
+            :apiParams="domainsApiParams"
+            resourceType="domain"
+            optionValueKey="id"
+            optionLabelKey="path"
+            defaultIcon="block-outlined"
+            allowClear="true"
             :placeholder="$t('label.domainid')"
-            @change="val => { handleDomainChange(domainList[val].id) }">
-            <a-select-option v-for="(opt, optIndex) in domainList" 
:key="optIndex" :label="opt.path || opt.name || opt.description">
-              {{ opt.path || opt.name || opt.description }}
-            </a-select-option>
-          </a-select>
+            @change-option-value="handleDomainChange" />
         </a-form-item>
         <a-form-item name="account" ref="account" v-if="'listDomains' in 
$store.getters.apis">
           <template #label>
             <tooltip-label :title="$t('label.account')" 
:tooltip="apiParams.account.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.account"
-            showSearch
-            optionFilterProp="value"
-            :filterOption="(input, option) => {
-              return option.value.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }"
+            api="listAccounts"
+            :apiParams="accountsApiParams"
+            resourceType="account"
+            optionValueKey="name"
+            optionLabelKey="name"
+            defaultIcon="team-outlined"
             :placeholder="$t('label.account')"
-            @change="val => { handleAccountChange(val) }">
-            <a-select-option v-for="(acc, index) in accountList" 
:value="acc.name" :key="index">
-              {{ acc.name }}
-            </a-select-option>
-          </a-select>
+            allowClear="true"
+            @change-option-value="handleAccountChange" />
         </a-form-item>
         <div :span="24" class="action-button">
           <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
@@ -162,27 +146,26 @@ import { api } from '@/api'
 import { mixinForm } from '@/utils/mixin'
 import ResourceIcon from '@/components/view/ResourceIcon'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
+import InfiniteScrollSelect from 
'@/components/widgets/InfiniteScrollSelect.vue'
 
 export default {
   name: 'UploadVolume',
   mixins: [mixinForm],
   components: {
     ResourceIcon,
-    TooltipLabel
+    TooltipLabel,
+    InfiniteScrollSelect
   },
   data () {
     return {
-      zones: [],
-      domainList: [],
-      accountList: [],
       formats: ['RAW', 'VHD', 'VHDX', 'OVA', 'QCOW2'],
-      offerings: [],
       zoneSelected: '',
       selectedDiskOfferingId: null,
       domainId: null,
       account: null,
       uploadParams: null,
-      domainLoading: false,
+      customDiskOffering: false,
+      isCustomizedDiskIOps: false,
       loading: false,
       uploadPercentage: 0
     }
@@ -190,9 +173,36 @@ export default {
   beforeCreate () {
     this.apiParams = this.$getApiParams('uploadVolume')
   },
+  computed: {
+    zonesApiParams () {
+      return {
+        showicon: true
+      }
+    },
+    diskOfferingsApiParams () {
+      if (!this.form.zoneId) {
+        return null
+      }
+      return {
+        zoneid: this.form.zoneId,
+        listall: true
+      }
+    },
+    domainsApiParams () {
+      return {
+        listall: true,
+        details: 'min'
+      }
+    },
+    accountsApiParams () {
+      return {
+        domainid: this.form?.domainid || null,
+        showicon: true
+      }
+    }
+  },
   created () {
     this.initForm()
-    this.fetchData()
   },
   methods: {
     initForm () {
@@ -207,78 +217,28 @@ export default {
         format: [{ required: true, message: this.$t('message.error.select') }]
       })
     },
-    fetchData () {
-      this.loading = true
-      api('listZones', { showicon: true }).then(json => {
-        this.zones = json.listzonesresponse.zone || []
-        this.zones = this.zones.filter(zone => zone.type !== 'Edge')
-        this.form.zoneId = this.zones[0].id || ''
-        this.fetchDiskOfferings(this.form.zoneId)
-      }).finally(() => {
-        this.loading = false
-      })
-      if ('listDomains' in this.$store.getters.apis) {
-        this.fetchDomains()
-      }
-    },
-    fetchDiskOfferings (zoneId) {
-      this.loading = true
-      api('listDiskOfferings', {
-        zoneid: zoneId,
-        listall: true
-      }).then(json => {
-        this.offerings = json.listdiskofferingsresponse.diskoffering || []
-      }).finally(() => {
-        this.loading = false
-      })
+    handleZoneChange (zoneId) {
+      this.form.zoneId = zoneId
+      // InfiniteScrollSelect will auto-reload disk offerings when apiParams 
changes
     },
-    fetchDomains () {
-      this.domainLoading = true
-      api('listDomains', {
-        listAll: true,
-        details: 'min'
-      }).then(response => {
-        this.domainList = response.listdomainsresponse.domain
-
-        if (this.domainList[0]) {
-          this.handleDomainChange(null)
-        }
-      }).catch(error => {
-        this.$notifyError(error)
-      }).finally(() => {
-        this.domainLoading = false
-      })
-    },
-    fetchAccounts () {
-      api('listAccounts', {
-        domainid: this.domainId
-      }).then(response => {
-        this.accountList = response.listaccountsresponse.account || []
-        if (this.accountList && this.accountList.length === 0) {
-          this.handleAccountChange(null)
-        }
-      }).catch(error => {
-        this.$notifyError(error)
-      })
-    },
-    onChangeDiskOffering (id) {
-      const offering = this.offerings.filter(x => x.id === id)
-      this.customDiskOffering = offering[0]?.iscustomized || false
-      this.isCustomizedDiskIOps = offering[0]?.iscustomizediops || false
-    },
-    handleDomainChange (domain) {
-      this.domainId = domain
-      if ('listAccounts' in this.$store.getters.apis) {
-        this.fetchAccounts()
-      }
-    },
-    handleAccountChange (acc) {
-      if (acc) {
-        this.account = acc.name
+    onChangeDiskOffering (offering) {
+      if (offering) {
+        this.customDiskOffering = offering.iscustomized || false
+        this.isCustomizedDiskIOps = offering.iscustomizediops || false
       } else {
-        this.account = acc
+        this.customDiskOffering = false
+        this.isCustomizedDiskIOps = false
       }
     },
+    handleDomainChange (domainId) {
+      this.form.domainid = domainId
+      this.domainId = domainId
+      this.form.account = null
+    },
+    handleAccountChange (accountName) {
+      this.form.account = accountName
+      this.account = accountName
+    },
     handleSubmit (e) {
       e.preventDefault()
       if (this.loading) return
diff --git a/ui/src/views/tools/CreateWebhook.vue 
b/ui/src/views/tools/CreateWebhook.vue
index 2b437471977..ef07cc39ed0 100644
--- a/ui/src/views/tools/CreateWebhook.vue
+++ b/ui/src/views/tools/CreateWebhook.vue
@@ -67,39 +67,33 @@
               <info-circle-outlined style="color: rgba(0,0,0,.45)" />
             </a-tooltip>
           </template>
-          <a-select
+          <infinite-scroll-select
             id="domain-selection"
             v-model:value="form.domainid"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }"
-            :loading="domainLoading"
+            api="listDomains"
+            :apiParams="domainsApiParams"
+            resourceType="domain"
+            optionValueKey="id"
+            optionLabelKey="path"
+            defaultIcon="block-outlined"
+            :defaultOption="{ id: null, path: ''}"
+            allowClear="true"
             :placeholder="apiParams.domainid.description"
-            @change="val => { handleDomainChanged(val) }">
-            <a-select-option v-for="opt in domains" :key="opt.id" 
:label="opt.path || opt.name || opt.description || ''">
-              {{ opt.path || opt.name || opt.description }}
-            </a-select-option>
-          </a-select>
+            @change-option-value="handleDomainChanged" />
         </a-form-item>
         <a-form-item name="account" ref="account" v-if="isAdminOrDomainAdmin 
&& ['Local'].includes(form.scope) && form.domainid">
           <template #label>
             <tooltip-label :title="$t('label.account')" 
:tooltip="apiParams.account.description"/>
           </template>
-          <a-select
+          <infinite-scroll-select
             v-model:value="form.account"
-            showSearch
-            optionFilterProp="label"
-            :filterOption="(input, option) => {
-              return option.value.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
-            }"
-            :loading="accountLoading"
-            :placeholder="apiParams.account.description">
-            <a-select-option v-for="opt in accounts" :key="opt.id" 
:label="opt.name">
-              {{ opt.name }}
-            </a-select-option>
-          </a-select>
+            api="listAccounts"
+            :apiParams="accountsApiParams"
+            resourceType="account"
+            optionValueKey="name"
+            optionLabelKey="name"
+            defaultIcon="team-outlined"
+            :placeholder="apiParams.account.description" />
         </a-form-item>
         <a-form-item name="payloadurl" ref="payloadurl">
           <template #label>
@@ -156,25 +150,22 @@
 <script>
 import { ref, reactive, toRaw } from 'vue'
 import { api } from '@/api'
-import _ from 'lodash'
 import { mixinForm } from '@/utils/mixin'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
 import TestWebhookDeliveryView from '@/components/view/TestWebhookDeliveryView'
+import InfiniteScrollSelect from 
'@/components/widgets/InfiniteScrollSelect.vue'
 
 export default {
   name: 'CreateWebhook',
   mixins: [mixinForm],
   components: {
     TooltipLabel,
-    TestWebhookDeliveryView
+    TestWebhookDeliveryView,
+    InfiniteScrollSelect
   },
   props: {},
   data () {
     return {
-      domains: [],
-      domainLoading: false,
-      accounts: [],
-      accountLoading: false,
       loading: false,
       testDeliveryAllowed: false,
       testDeliveryLoading: false
@@ -185,9 +176,6 @@ export default {
   },
   created () {
     this.initForm()
-    if (['Domain', 'Local'].includes(this.form.scope)) {
-      this.fetchDomainData()
-    }
   },
   computed: {
     isAdminOrDomainAdmin () {
@@ -201,6 +189,21 @@ export default {
         return this.form.payloadurl.toLowerCase().startsWith('https://')
       }
       return false
+    },
+    domainsApiParams () {
+      return {
+        listAll: true,
+        showicon: true,
+        details: 'min'
+      }
+    },
+    accountsApiParams () {
+      if (!this.form.domainid) {
+        return null
+      }
+      return {
+        domainid: this.form.domainid
+      }
     }
   },
   methods: {
@@ -228,46 +231,6 @@ export default {
     updateTestDeliveryLoading (value) {
       this.testDeliveryLoading = value
     },
-    fetchDomainData () {
-      this.domainLoading = true
-      this.domains = [
-        {
-          id: null,
-          name: ''
-        }
-      ]
-      this.form.domainid = null
-      this.form.account = null
-      api('listDomains', {}).then(json => {
-        const listdomains = json.listdomainsresponse.domain
-        this.domains = this.domains.concat(listdomains)
-      }).finally(() => {
-        this.domainLoading = false
-        if (this.arrayHasItems(this.domains)) {
-          this.form.domainid = null
-        }
-      })
-    },
-    fetchAccountData () {
-      this.accounts = []
-      this.form.account = null
-      if (!this.form.domainid) {
-        return
-      }
-      this.accountLoading = true
-      var params = {
-        domainid: this.form.domainid
-      }
-      api('listAccounts', params).then(json => {
-        const listAccounts = json.listaccountsresponse.account || []
-        this.accounts = listAccounts
-      }).finally(() => {
-        this.accountLoading = false
-        if (this.arrayHasItems(this.accounts)) {
-          this.form.account = this.accounts[0].id
-        }
-      })
-    },
     handleSubmit (e) {
       e.preventDefault()
       if (this.loading) return
@@ -300,10 +263,8 @@ export default {
           return
         }
         if (values.account) {
-          const accountItem = _.find(this.accounts, (option) => option.id === 
values.account)
-          if (accountItem) {
-            params.account = accountItem.name
-          }
+          // values.account is the account name (optionValueKey="name")
+          params.account = values.account
         }
         this.loading = true
         api('createWebhook', params).then(json => {
@@ -331,14 +292,11 @@ export default {
       }, 1)
     },
     handleScopeChange (e) {
-      if (['Domain', 'Local'].includes(this.form.scope)) {
-        this.fetchDomainData()
-      }
+      this.form.domainid = null
+      this.form.account = null
     },
     handleDomainChanged (domainid) {
-      if (domainid) {
-        this.fetchAccountData()
-      }
+      this.form.account = null
     }
   }
 }
diff --git a/ui/src/views/tools/ManageVolumes.vue 
b/ui/src/views/tools/ManageVolumes.vue
index 94c06b4ce9c..7fe4a56c1bc 100644
--- a/ui/src/views/tools/ManageVolumes.vue
+++ b/ui/src/views/tools/ManageVolumes.vue
@@ -372,22 +372,16 @@
               name="domain"
               ref="domain"
               :label="$t('label.domain')">
-              <a-select
-                @change="changeDomain"
+              <infinite-scroll-select
                 v-model:value="importForm.selectedDomain"
-                showSearch
-                optionFilterProp="label"
-                :filterOption="(input, option) => {
-                  return  
option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
-                }" >
-                <a-select-option v-for="domain in domains" :key="domain.name" 
:value="domain.id" :label="domain.path || domain.name || domain.description">
-                <span>
-                  <resource-icon v-if="domain && domain.icon" 
:image="domain.icon.base64image" size="1x" style="margin-right: 5px"/>
-                  <block-outlined v-else style="margin-right: 5px" />
-                  {{ domain.path || domain.name || domain.description }}
-                </span>
-                </a-select-option>
-              </a-select>
+                api="listDomains"
+                :apiParams="domainsApiParams"
+                resourceType="domain"
+                optionValueKey="id"
+                optionLabelKey="path"
+                defaultIcon="block-outlined"
+                allowClear="true"
+                @change-option-value="changeDomain" />
             </a-form-item>
 
             <a-form-item
@@ -395,22 +389,16 @@
               name="account"
               ref="account"
               :label="$t('label.account')">
-              <a-select
-                @change="changeAccount"
+              <infinite-scroll-select
                 v-model:value="importForm.selectedAccount"
-                showSearch
-                optionFilterProp="value"
-                :filterOption="(input, option) => {
-                    return 
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
-                  }" >
-                <a-select-option v-for="account in accounts" 
:key="account.name" :value="account.name">
-                  <span>
-                    <resource-icon v-if="account && account.icon" 
:image="account.icon.base64image" size="1x" style="margin-right: 5px"/>
-                    <team-outlined v-else style="margin-right: 5px" />
-                    {{ account.name }}
-                  </span>
-                </a-select-option>
-              </a-select>
+                api="listAccounts"
+                :apiParams="accountsApiParams"
+                resourceType="account"
+                optionValueKey="name"
+                optionLabelKey="name"
+                defaultIcon="team-outlined"
+                allowClear="true"
+                @change-option-value="changeAccount" />
               <span v-if="importForm.accountError" class="required">{{ 
$t('label.required') }}</span>
             </a-form-item>
 
@@ -419,22 +407,16 @@
               name="project"
               ref="project"
               :label="$t('label.project')">
-              <a-select
-                @change="changeProject"
+              <infinite-scroll-select
                 v-model:value="importForm.selectedProject"
-                showSearch
-                optionFilterProp="label"
-                :filterOption="(input, option) => {
-                  return  
option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
-                }" >
-                <a-select-option v-for="project in projects" :key="project.id" 
:value="project.id" :label="project.name">
-                <span>
-                  <resource-icon v-if="project && project.icon" 
:image="project.icon.base64image" size="1x" style="margin-right: 5px"/>
-                  <project-outlined v-else style="margin-right: 5px" />
-                  {{ project.name }}
-                </span>
-                </a-select-option>
-              </a-select>
+                api="listProjects"
+                :apiParams="projectsApiParams"
+                resourceType="project"
+                optionValueKey="id"
+                optionLabelKey="name"
+                defaultIcon="project-outlined"
+                allowClear="true"
+                @change-option-value="changeProject" />
               <span v-if="importForm.projectError" class="required">{{ 
$t('label.required') }}</span>
             </a-form-item>
 
@@ -480,6 +462,7 @@ import Status from '@/components/widgets/Status'
 import SearchView from '@/components/view/SearchView'
 import ResourceIcon from '@/components/view/ResourceIcon'
 import TooltipLabel from '@/components/widgets/TooltipLabel.vue'
+import InfiniteScrollSelect from 
'@/components/widgets/InfiniteScrollSelect.vue'
 
 export default {
   components: {
@@ -487,7 +470,8 @@ export default {
     Breadcrumb,
     Status,
     SearchView,
-    ResourceIcon
+    ResourceIcon,
+    InfiniteScrollSelect
   },
   name: 'ManageVolumes',
   data () {
@@ -607,7 +591,6 @@ export default {
     this.page.managed = parseInt(this.$route.query.managedpage || 1)
     this.initForm()
     this.fetchData()
-    this.fetchDomains()
   },
   computed: {
     isPageAllowed () {
@@ -629,6 +612,36 @@ export default {
     showCluster () {
       return this.poolscope !== 'zone'
     },
+    domainsApiParams () {
+      return {
+        listall: true,
+        details: 'min',
+        showicon: true
+      }
+    },
+    accountsApiParams () {
+      if (!this.importForm.selectedDomain) {
+        return null
+      }
+      return {
+        domainid: this.importForm.selectedDomain,
+        showicon: true,
+        state: 'Enabled',
+        isrecursive: false
+      }
+    },
+    projectsApiParams () {
+      if (!this.importForm.selectedDomain) {
+        return null
+      }
+      return {
+        domainid: this.importForm.selectedDomain,
+        state: 'Active',
+        showicon: true,
+        details: 'min',
+        isrecursive: false
+      }
+    },
     showHost () {
       return this.poolscope === 'host'
     },
@@ -970,53 +983,6 @@ export default {
       this.updateQuery('scope', value)
       this.fetchOptions(this.params.zones, 'zones', value)
     },
-    fetchDomains () {
-      api('listDomains', {
-        response: 'json',
-        listAll: true,
-        showicon: true,
-        details: 'min'
-      }).then(response => {
-        this.domains = response.listdomainsresponse.domain || []
-      }).catch(error => {
-        this.$notifyError(error)
-      }).finally(() => {
-        this.loading = false
-      })
-    },
-    fetchAccounts () {
-      this.loading = true
-      api('listAccounts', {
-        response: 'json',
-        domainId: this.importForm.selectedDomain,
-        showicon: true,
-        state: 'Enabled',
-        isrecursive: false
-      }).then(response => {
-        this.accounts = response.listaccountsresponse.account || []
-      }).catch(error => {
-        this.$notifyError(error)
-      }).finally(() => {
-        this.loading = false
-      })
-    },
-    fetchProjects () {
-      this.loading = true
-      api('listProjects', {
-        response: 'json',
-        domainId: this.importForm.selectedDomain,
-        state: 'Active',
-        showicon: true,
-        details: 'min',
-        isrecursive: false
-      }).then(response => {
-        this.projects = response.listprojectsresponse.project || []
-      }).catch(error => {
-        this.$notifyError(error)
-      }).finally(() => {
-        this.loading = false
-      })
-    },
     changeAccountType () {
       this.importForm.selectedDomain = null
       this.importForm.selectedAccount = null
@@ -1029,8 +995,7 @@ export default {
       this.importForm.selectedProject = null
       this.importForm.selectedDiskoffering = null
       this.diskOfferings = {}
-      this.fetchAccounts()
-      this.fetchProjects()
+      // InfiniteScrollSelect will auto-reload when apiParams changes
     },
     changeAccount () {
       this.importForm.selectedProject = null

Reply via email to