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>
@@ -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