Compare commits

...

11 Commits

Author SHA1 Message Date
Vishesh e544fcac8a
Merge 4676fb1090 into bce3e54a7e 2026-01-22 15:09:18 +01:00
Daman Arora bce3e54a7e
improve error handling for template upload notifications (#12412)
Co-authored-by: Daman Arora <daman.arora@shapeblue.com>
2026-01-22 15:02:46 +01:00
Nicolas Vazquez 6a9835904c
Fix for zoneids parameters length on updateAPIs (#12440) 2026-01-22 14:57:46 +01:00
Nicolas Vazquez 6846619a6f
Fix update network offering domainids size limitation (#12431) 2026-01-22 14:32:46 +01:00
Vishesh d1eb2822d9
Remove redundant Exceptions from logs for vm schedules (#12428) 2026-01-22 14:29:35 +01:00
Vishesh 4676fb1090
Merge branch '4.20' into use-infinite-scroll-select 2026-01-20 12:18:23 +05:30
vishesh92 4fc90c123d
Address comments 2026-01-12 14:42:41 +05:30
vishesh92 99dca0862a
fixup 2025-11-14 17:43:23 +05:30
vishesh92 f317e9cc16
fixup 2025-11-14 13:31:32 +05:30
vishesh92 c7d282cd2e
Address comments 2025-11-07 14:09:34 +05:30
vishesh92 10b60c27a0
Use infinite scroll select 2025-11-05 22:12:11 +05:30
18 changed files with 595 additions and 741 deletions

View File

@ -78,6 +78,7 @@ public class UpdateNetworkOfferingCmd extends BaseCmd {
@Parameter(name = ApiConstants.DOMAIN_ID, @Parameter(name = ApiConstants.DOMAIN_ID,
type = CommandType.STRING, type = CommandType.STRING,
length = 4096,
description = "The ID of the containing domain(s) as comma separated string, public for public offerings") description = "The ID of the containing domain(s) as comma separated string, public for public offerings")
private String domainIds; private String domainIds;

View File

@ -75,6 +75,7 @@ public class UpdateDiskOfferingCmd extends BaseCmd {
@Parameter(name = ApiConstants.ZONE_ID, @Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.STRING, type = CommandType.STRING,
description = "The ID of the containing zone(s) as comma separated string, all for all zones offerings", description = "The ID of the containing zone(s) as comma separated string, all for all zones offerings",
length = 4096,
since = "4.13") since = "4.13")
private String zoneIds; private String zoneIds;

View File

@ -69,6 +69,7 @@ public class UpdateServiceOfferingCmd extends BaseCmd {
@Parameter(name = ApiConstants.ZONE_ID, @Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.STRING, type = CommandType.STRING,
description = "The ID of the containing zone(s) as comma separated string, all for all zones offerings", description = "The ID of the containing zone(s) as comma separated string, all for all zones offerings",
length = 4096,
since = "4.13") since = "4.13")
private String zoneIds; private String zoneIds;

View File

@ -65,6 +65,7 @@ public class UpdateVPCOfferingCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.ZONE_ID, @Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.STRING, type = CommandType.STRING,
description = "The ID of the containing zone(s) as comma separated string, all for all zones offerings", description = "The ID of the containing zone(s) as comma separated string, all for all zones offerings",
length = 4096,
since = "4.13") since = "4.13")
private String zoneIds; private String zoneIds;

View File

@ -31,4 +31,6 @@ public interface VMScheduledJobDao extends GenericDao<VMScheduledJobVO, Long> {
int expungeJobsForSchedules(List<Long> scheduleId, Date dateAfter); int expungeJobsForSchedules(List<Long> scheduleId, Date dateAfter);
int expungeJobsBefore(Date currentTimestamp); int expungeJobsBefore(Date currentTimestamp);
VMScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp);
} }

View File

@ -39,6 +39,8 @@ public class VMScheduledJobDaoImpl extends GenericDaoBase<VMScheduledJobVO, Long
private final SearchBuilder<VMScheduledJobVO> expungeJobForScheduleSearch; private final SearchBuilder<VMScheduledJobVO> expungeJobForScheduleSearch;
private final SearchBuilder<VMScheduledJobVO> scheduleAndTimestampSearch;
static final String SCHEDULED_TIMESTAMP = "scheduled_timestamp"; static final String SCHEDULED_TIMESTAMP = "scheduled_timestamp";
static final String VM_SCHEDULE_ID = "vm_schedule_id"; static final String VM_SCHEDULE_ID = "vm_schedule_id";
@ -58,6 +60,11 @@ public class VMScheduledJobDaoImpl extends GenericDaoBase<VMScheduledJobVO, Long
expungeJobForScheduleSearch.and(VM_SCHEDULE_ID, expungeJobForScheduleSearch.entity().getVmScheduleId(), SearchCriteria.Op.IN); expungeJobForScheduleSearch.and(VM_SCHEDULE_ID, expungeJobForScheduleSearch.entity().getVmScheduleId(), SearchCriteria.Op.IN);
expungeJobForScheduleSearch.and(SCHEDULED_TIMESTAMP, expungeJobForScheduleSearch.entity().getScheduledTime(), SearchCriteria.Op.GTEQ); expungeJobForScheduleSearch.and(SCHEDULED_TIMESTAMP, expungeJobForScheduleSearch.entity().getScheduledTime(), SearchCriteria.Op.GTEQ);
expungeJobForScheduleSearch.done(); expungeJobForScheduleSearch.done();
scheduleAndTimestampSearch = createSearchBuilder();
scheduleAndTimestampSearch.and(VM_SCHEDULE_ID, scheduleAndTimestampSearch.entity().getVmScheduleId(), SearchCriteria.Op.EQ);
scheduleAndTimestampSearch.and(SCHEDULED_TIMESTAMP, scheduleAndTimestampSearch.entity().getScheduledTime(), SearchCriteria.Op.EQ);
scheduleAndTimestampSearch.done();
} }
/** /**
@ -92,4 +99,12 @@ public class VMScheduledJobDaoImpl extends GenericDaoBase<VMScheduledJobVO, Long
sc.setParameters(SCHEDULED_TIMESTAMP, date); sc.setParameters(SCHEDULED_TIMESTAMP, date);
return expunge(sc); return expunge(sc);
} }
@Override
public VMScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp) {
SearchCriteria<VMScheduledJobVO> sc = scheduleAndTimestampSearch.create();
sc.setParameters(VM_SCHEDULE_ID, scheduleId);
sc.setParameters(SCHEDULED_TIMESTAMP, scheduledTimestamp);
return findOneBy(sc);
}
} }

View File

@ -162,7 +162,13 @@ public class VMSchedulerImpl extends ManagerBase implements VMScheduler, Configu
} }
Date scheduledDateTime = Date.from(ts.toInstant()); Date scheduledDateTime = Date.from(ts.toInstant());
VMScheduledJobVO scheduledJob = new VMScheduledJobVO(vmSchedule.getVmId(), vmSchedule.getId(), vmSchedule.getAction(), scheduledDateTime); VMScheduledJobVO scheduledJob = vmScheduledJobDao.findByScheduleAndTimestamp(vmSchedule.getId(), scheduledDateTime);
if (scheduledJob != null) {
logger.trace("Job is already scheduled for schedule {} at {}", vmSchedule, scheduledDateTime);
return scheduledDateTime;
}
scheduledJob = new VMScheduledJobVO(vmSchedule.getVmId(), vmSchedule.getId(), vmSchedule.getAction(), scheduledDateTime);
try { try {
vmScheduledJobDao.persist(scheduledJob); vmScheduledJobDao.persist(scheduledJob);
ActionEventUtils.onScheduledActionEvent(User.UID_SYSTEM, vm.getAccountId(), actionEventMap.get(vmSchedule.getAction()), ActionEventUtils.onScheduledActionEvent(User.UID_SYSTEM, vm.getAccountId(), actionEventMap.get(vmSchedule.getAction()),

View File

@ -18,52 +18,44 @@
<template> <template>
<div class="form"> <div class="form">
<div class="form__item" :class="{'error': domainError}"> <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="form__label">{{ $t('label.domain') }}<span class="required">*</span></p> <p class="required required-label">{{ $t('label.required') }}</p>
<p class="required required-label">{{ $t('label.required') }}</p> <infinite-scroll-select
<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>
</div>
<div class="form__item" v-if="accountsList">
<p class="form__label">{{ $t('label.account') }}</p>
<a-select
style="width: 100%" style="width: 100%"
@change="handleChangeAccount" v-model:value="domainId"
showSearch api="listDomains"
optionFilterProp="value" :apiParams="domainsApiParams"
:filterOption="(input, option) => { resourceType="domain"
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="id"
}" > optionLabelKey="path"
<a-select-option v-for="(account, index) in accountsList" :value="account.name" :key="index"> defaultIcon="block-outlined"
{{ account.name }} v-focus="true"
</a-select-option> @change-option-value="handleChangeDomain" />
</a-select> </div>
<div class="form__item">
<p class="form__label">{{ $t('label.account') }}</p>
<infinite-scroll-select
style="width: 100%"
v-model:value="selectedAccount"
api="listAccounts"
:apiParams="accountsApiParams"
resourceType="account"
optionValueKey="name"
optionLabelKey="name"
defaultIcon="team-outlined"
@change-option-value="handleChangeAccount" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { api } from '@/api' import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'
export default { export default {
name: 'DedicateDomain', name: 'DedicateDomain',
components: {
InfiniteScrollSelect
},
props: { props: {
error: { error: {
type: Boolean, type: Boolean,
@ -72,59 +64,48 @@ export default {
}, },
data () { data () {
return { return {
domainsLoading: false,
domainId: null, domainId: null,
accountsList: null, selectedAccount: null,
domainsList: null,
domainError: false 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: { watch: {
error () { error () {
this.domainError = this.error this.domainError = this.error
} }
}, },
created () { created () {
this.fetchData()
}, },
methods: { methods: {
fetchData () { handleChangeDomain (domainId) {
this.domainsLoading = true this.domainId = domainId
api('listDomains', { this.selectedAccount = null
listAll: true, this.$emit('domainChange', domainId)
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)
this.domainError = false this.domainError = false
this.fetchAccounts()
}, },
handleChangeAccount (e) { handleChangeAccount (accountName) {
this.$emit('accountChange', e) this.selectedAccount = accountName
this.$emit('accountChange', accountName)
} }
} }
} }

View File

@ -41,8 +41,10 @@
- optionValueKey (String, optional): Property to use as the value for options (e.g., 'name'). Default is 'id' - 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' - 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 - 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 - 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' - 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: 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 - @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" :filter-option="false"
:loading="loading" :loading="loading"
show-search show-search
:allowClear="allowClear"
placeholder="Select" placeholder="Select"
@search="onSearchTimed" @search="onSearchTimed"
@popupScroll="onScroll" @popupScroll="onScroll"
@ -75,9 +78,9 @@
</div> </div>
</div> </div>
</template> </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>
<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"/> <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" /> <render-icon v-else :icon="defaultIcon" style="margin-right: 5px" />
</span> </span>
@ -124,6 +127,10 @@ export default {
type: Object, type: Object,
default: null default: null
}, },
allowClear: {
type: Boolean,
default: false
},
showIcon: { showIcon: {
type: Boolean, type: Boolean,
default: true default: true
@ -135,6 +142,10 @@ export default {
pageSize: { pageSize: {
type: Number, type: Number,
default: null default: null
},
selectFirstOption: {
type: Boolean,
default: false
} }
}, },
data () { data () {
@ -147,7 +158,8 @@ export default {
searchTimer: null, searchTimer: null,
scrollHandlerAttached: false, scrollHandlerAttached: false,
preselectedOptionValue: null, preselectedOptionValue: null,
successiveFetches: 0 successiveFetches: 0,
hasAutoSelectedFirst: false
} }
}, },
created () { created () {
@ -166,6 +178,36 @@ export default {
}, },
formattedSearchFooterMessage () { formattedSearchFooterMessage () {
return `${this.$t('label.showing.results.for').replace('%x', this.searchQuery)}` 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: { watch: {
@ -210,6 +252,7 @@ export default {
}).finally(() => { }).finally(() => {
if (this.successiveFetches === 0) { if (this.successiveFetches === 0) {
this.loading = false this.loading = false
this.autoSelectFirstOptionIfNeeded()
} }
}) })
}, },
@ -220,11 +263,10 @@ export default {
this.resetPreselectedOptionValue() this.resetPreselectedOptionValue()
return return
} }
const matchValue = Array.isArray(this.preselectedOptionValue) ? this.preselectedOptionValue[0] : this.preselectedOptionValue if (!this.preselectedMatch) {
const match = this.options.find(entry => entry[this.optionValueKey] === matchValue)
if (!match) {
this.successiveFetches++ this.successiveFetches++
if (this.options.length < this.totalCount) { // Exclude defaultOption from count when comparing with totalCount
if (this.apiOptionsCount < this.totalCount) {
this.fetchItems() this.fetchItems()
} else { } else {
this.resetPreselectedOptionValue() this.resetPreselectedOptionValue()
@ -246,6 +288,36 @@ export default {
this.preselectedOptionValue = null this.preselectedOptionValue = null
this.successiveFetches = 0 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) { onSearchTimed (value) {
clearTimeout(this.searchTimer) clearTimeout(this.searchTimer)
this.searchTimer = setTimeout(() => { this.searchTimer = setTimeout(() => {
@ -264,7 +336,8 @@ export default {
}, },
onScroll (e) { onScroll (e) {
const nearBottom = e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 10 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) { if (nearBottom && hasMore && !this.loading) {
this.fetchItems() this.fetchItems()
} }

View File

@ -218,18 +218,19 @@ export const notifierPlugin = {
if (error.response.status) { if (error.response.status) {
msg = `${i18n.global.t('message.request.failed')} (${error.response.status})` msg = `${i18n.global.t('message.request.failed')} (${error.response.status})`
} }
if (error.message) { if (error.response.headers?.['x-description']) {
desc = error.message
}
if (error.response.headers && 'x-description' in error.response.headers) {
desc = error.response.headers['x-description'] desc = error.response.headers['x-description']
} } else if (error.response.data) {
if (desc === '' && error.response.data) {
const responseKey = _.findKey(error.response.data, 'errortext') const responseKey = _.findKey(error.response.data, 'errortext')
if (responseKey) { if (responseKey) {
desc = error.response.data[responseKey].errortext desc = error.response.data[responseKey].errortext
} else if (typeof error.response.data === 'string') {
desc = error.response.data
} }
} }
if (!desc && error.message) {
desc = error.message
}
} }
let countNotify = store.getters.countNotify let countNotify = store.getters.countNotify
countNotify++ countNotify++

View File

@ -90,45 +90,31 @@
<template #label> <template #label>
<tooltip-label :title="$t('label.domainid')" :tooltip="apiParams.domainid.description"/> <tooltip-label :title="$t('label.domainid')" :tooltip="apiParams.domainid.description"/>
</template> </template>
<a-select <infinite-scroll-select
:loading="domainLoading"
v-model:value="form.domainid" v-model:value="form.domainid"
api="listDomains"
:apiParams="domainsApiParams"
resourceType="domain"
optionValueKey="id"
optionLabelKey="path"
defaultIcon="block-outlined"
:selectFirstOption="true"
:placeholder="apiParams.domainid.description" :placeholder="apiParams.domainid.description"
showSearch @change-option-value="handleDomainChange" />
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>
</a-form-item> </a-form-item>
<a-form-item name="account" ref="account" v-if="!account"> <a-form-item name="account" ref="account" v-if="!account">
<template #label> <template #label>
<tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/> <tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.account" v-model:value="form.account"
:loading="loadingAccount" api="listAccounts"
:placeholder="apiParams.account.description" :apiParams="accountsApiParams"
showSearch resourceType="account"
optionFilterProp="label" optionValueKey="name"
:filterOption="(input, option) => { optionLabelKey="name"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 defaultIcon="team-outlined"
}" > :placeholder="apiParams.account.description" />
<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>
</a-form-item> </a-form-item>
<a-form-item name="timezone" ref="timezone"> <a-form-item name="timezone" ref="timezone">
<template #label> <template #label>
@ -185,12 +171,14 @@ import { timeZone } from '@/utils/timezone'
import debounce from 'lodash/debounce' import debounce from 'lodash/debounce'
import ResourceIcon from '@/components/view/ResourceIcon' import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel' import TooltipLabel from '@/components/widgets/TooltipLabel'
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'
export default { export default {
name: 'AddUser', name: 'AddUser',
components: { components: {
TooltipLabel, TooltipLabel,
ResourceIcon ResourceIcon,
InfiniteScrollSelect
}, },
data () { data () {
this.fetchTimeZone = debounce(this.fetchTimeZone, 800) this.fetchTimeZone = debounce(this.fetchTimeZone, 800)
@ -198,14 +186,9 @@ export default {
loading: false, loading: false,
timeZoneLoading: false, timeZoneLoading: false,
timeZoneMap: [], timeZoneMap: [],
domainLoading: false,
domainsList: [],
selectedDomain: '',
samlEnable: false, samlEnable: false,
idpLoading: false, idpLoading: false,
idps: [], idps: [],
loadingAccount: false,
accountList: [],
account: null, account: null,
domainid: null domainid: null
} }
@ -218,6 +201,19 @@ export default {
computed: { computed: {
samlAllowed () { samlAllowed () {
return 'authorizeSamlSso' in this.$store.getters.apis 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: { methods: {
@ -241,53 +237,18 @@ export default {
fetchData () { fetchData () {
this.account = this.$route.query && this.$route.query.account ? this.$route.query.account : null 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 this.domainid = this.$route.query && this.$route.query.domainid ? this.$route.query.domainid : null
if (!this.domianid) { // Set initial domain if provided from route
this.fetchDomains() if (this.domainid) {
this.form.domainid = this.domainid
} }
this.fetchTimeZone() this.fetchTimeZone()
if (this.samlAllowed) { if (this.samlAllowed) {
this.fetchIdps() this.fetchIdps()
} }
}, },
fetchDomains () { handleDomainChange (domainId) {
this.domainLoading = true this.form.domainid = domainId
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 = []
this.form.account = null 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) { fetchTimeZone (value) {
this.timeZoneMap = [] this.timeZoneMap = []
@ -328,12 +289,14 @@ export default {
accounttype: 0 accounttype: 0
} }
// Account: use route query account if available, otherwise use form value (which is the account name)
if (this.account) { if (this.account) {
params.account = this.account params.account = this.account
} else if (this.accountList[values.account]) { } else if (values.account) {
params.account = this.accountList[values.account].name params.account = values.account
} }
// Domain: use route query domainid if available, otherwise use form value
if (this.domainid) { if (this.domainid) {
params.domainid = this.domainid params.domainid = this.domainid
} else if (values.domainid) { } else if (values.domainid) {

View File

@ -638,11 +638,7 @@ export default {
this.$emit('refresh-data') this.$emit('refresh-data')
this.closeAction() this.closeAction()
}).catch(e => { }).catch(e => {
this.$notification.error({ this.$notifyError(e)
message: this.$t('message.upload.failed'),
description: `${this.$t('message.upload.template.failed.description')} - ${e}`,
duration: 0
})
}) })
}, },
fetchCustomHypervisorName () { fetchCustomHypervisorName () {

View File

@ -121,15 +121,20 @@
ref="domain" ref="domain"
name="domain" name="domain"
> >
<a-auto-complete <infinite-scroll-select
v-model:value="form.domain" v-model:value="form.domain"
:options="domains" api="listDomains"
:apiParams="domainsApiParams"
resourceType="domain"
optionValueKey="id"
optionLabelKey="path"
defaultIcon="block-outlined"
:placeholder="$t('label.domain')" :placeholder="$t('label.domain')"
:filter-option="filterOption" :defaultOption="{ id: null, path: ''}"
:allowClear="true"
style="width: 100%;" style="width: 100%;"
@select="getAccounts" @change-option-value="handleDomainChange"
:dropdownMatchSelectWidth="false" @change-option="handleDomainOptionChange" />
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row>&nbsp; </a-row>&nbsp;
@ -150,15 +155,20 @@
ref="account" ref="account"
name="account" name="account"
> >
<a-auto-complete <infinite-scroll-select
v-model:value="form.account" v-model:value="form.account"
:options="accounts" api="listAccounts"
:apiParams="accountsApiParams"
resourceType="account"
optionValueKey="id"
optionLabelKey="name"
defaultIcon="team-outlined"
:placeholder="$t('label.account')" :placeholder="$t('label.account')"
:filter-option="filterOption"
:disabled="form.isRecursive" :disabled="form.isRecursive"
:dropdownMatchSelectWidth="false" :defaultOption="{ id: null, name: ''}"
@select="selectAccount" allowClear="true"
/> @change-option-value="selectAccount"
@change-option="selectAccountOption" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="3" v-if="'listUsageTypes' in $store.getters.apis"> <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 TooltipLabel from '@/components/widgets/TooltipLabel'
import TooltipButton from '@/components/widgets/TooltipButton' import TooltipButton from '@/components/widgets/TooltipButton'
import Status from '@/components/widgets/Status' import Status from '@/components/widgets/Status'
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
dayjs.extend(utc) dayjs.extend(utc)
@ -374,7 +385,8 @@ export default {
ListView, ListView,
Status, Status,
TooltipLabel, TooltipLabel,
TooltipButton TooltipButton,
InfiniteScrollSelect
}, },
props: { props: {
resource: { resource: {
@ -402,8 +414,6 @@ export default {
page: 1, page: 1,
pageSize: 20, pageSize: 20,
usageTypes: [], usageTypes: [],
domains: [],
accounts: [],
account: null, account: null,
domain: null, domain: null,
usageType: null, usageType: null,
@ -436,6 +446,23 @@ export default {
this.fetchData() this.fetchData()
this.updateColumns() this.updateColumns()
}, },
computed: {
domainsApiParams () {
return {
listall: true
}
},
accountsApiParams () {
if (!this.form.domain) {
return {
listall: true
}
}
return {
domainid: this.form.domain
}
}
},
methods: { methods: {
clearFilters () { clearFilters () {
this.formRef.value.resetFields() this.formRef.value.resetFields()
@ -445,8 +472,6 @@ export default {
this.usageType = null this.usageType = null
this.page = 1 this.page = 1
this.pageSize = 20 this.pageSize = 20
this.getAccounts()
}, },
disabledDate (current) { disabledDate (current) {
return current && current > dayjs().endOf('day') return current && current > dayjs().endOf('day')
@ -473,8 +498,6 @@ export default {
this.listUsageServerMetrics() this.listUsageServerMetrics()
this.getUsageTypes() this.getUsageTypes()
this.getAllUsageRecordColumns() this.getAllUsageRecordColumns()
this.getDomains()
this.getAccounts()
if (!this.$store.getters.customColumns[this.$store.getters.userInfo.id]) { 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.$store.getters.customColumns[this.$store.getters.userInfo.id][this.$route.path] = this.selectedColumnKeys 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) 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) { selectUsageType (value, option) {
if (option && option.id) { if (option && option.id) {
this.usageType = option this.usageType = option
@ -548,24 +561,12 @@ export default {
} }
} }
}, },
getDomains () { handleDomainChange (domainId) {
api('listDomains', { listAll: true }).then(json => { this.form.domain = domainId
if (json && json.listdomainsresponse && json.listdomainsresponse.domain) { this.form.account = null
this.domains = [{ id: null, value: '' }, ...json.listdomainsresponse.domain.map(x => {
return {
id: x.id,
value: x.path
}
})]
}
})
}, },
getAccounts (value, option) { handleDomainOptionChange (option) {
var params = {
listAll: true
}
if (option && option.id) { if (option && option.id) {
params.domainid = option.id
this.domain = option this.domain = option
} else { } else {
this.domain = null this.domain = null
@ -573,16 +574,19 @@ export default {
this.formRef.value.resetFields('domain') this.formRef.value.resetFields('domain')
} }
} }
api('listAccounts', params).then(json => { },
if (json && json.listaccountsresponse && json.listaccountsresponse.account) { selectAccount (accountId) {
this.accounts = [{ id: null, value: '' }, ...json.listaccountsresponse.account.map(x => { this.form.account = accountId
return { },
id: x.id, selectAccountOption (option) {
value: x.name if (option && option.id) {
} this.account = option
})] } else {
this.account = null
if (this.formRef?.value) {
this.formRef.value.resetFields('account')
} }
}) }
}, },
getParams (page, pageSize) { getParams (page, pageSize) {
const formRaw = toRaw(this.form) const formRaw = toRaw(this.form)

View File

@ -73,42 +73,32 @@
<template #label> <template #label>
<tooltip-label :title="$t('label.domainid')" :tooltip="apiParams.domainid.description"/> <tooltip-label :title="$t('label.domainid')" :tooltip="apiParams.domainid.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.domainid" v-model:value="form.domainid"
showSearch api="listDomains"
optionFilterProp="label" :apiParams="domainsApiParams"
:filterOption="(input, option) => { resourceType="domain"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="id"
}" optionLabelKey="path"
:loading="domainLoading" defaultIcon="block-outlined"
allowClear="true"
:placeholder="apiParams.domainid.description" :placeholder="apiParams.domainid.description"
@change="val => { handleDomainChange(val) }"> @change-option-value="handleDomainChange" />
<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>
</a-form-item> </a-form-item>
<a-form-item name="account" ref="account" v-if="domainid"> <a-form-item name="account" ref="account" v-if="domainid">
<template #label> <template #label>
<tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/> <tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.account" v-model:value="form.account"
showSearch api="listAccounts"
optionFilterProp="label" :apiParams="accountsApiParams"
:filterOption="(input, option) => { resourceType="account"
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="name"
}" optionLabelKey="name"
:placeholder="apiParams.account.description" defaultIcon="team-outlined"
@change="val => { handleAccountChange(val) }"> allowClear="true"
<a-select-option v-for="(acc, index) in accounts" :value="acc.name" :key="index"> :placeholder="apiParams.account.description" />
{{ acc.name }}
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
@ -199,13 +189,15 @@ import { api } from '@/api'
import { mixinForm } from '@/utils/mixin' import { mixinForm } from '@/utils/mixin'
import ResourceIcon from '@/components/view/ResourceIcon' import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel' import TooltipLabel from '@/components/widgets/TooltipLabel'
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'
export default { export default {
name: 'CreateTemplate', name: 'CreateTemplate',
mixins: [mixinForm], mixins: [mixinForm],
components: { components: {
ResourceIcon, ResourceIcon,
TooltipLabel TooltipLabel,
InfiniteScrollSelect
}, },
props: { props: {
resource: { resource: {
@ -219,9 +211,6 @@ export default {
zones: [], zones: [],
osTypes: {}, osTypes: {},
loading: false, loading: false,
domains: [],
accounts: [],
domainLoading: false,
domainid: null, domainid: null,
account: null, account: null,
architectureTypes: {} architectureTypes: {}
@ -230,6 +219,21 @@ export default {
computed: { computed: {
isAdminRole () { isAdminRole () {
return this.$store.getters.userInfo.roletype === 'Admin' 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 () { beforeCreate () {
@ -256,9 +260,6 @@ export default {
if (this.resource.intervaltype) { if (this.resource.intervaltype) {
this.fetchSnapshotZones() this.fetchSnapshotZones()
} }
if ('listDomains' in this.$store.getters.apis) {
this.fetchDomains()
}
this.architectureTypes.opts = this.$fetchCpuArchitectureTypes() this.architectureTypes.opts = this.$fetchCpuArchitectureTypes()
}, },
fetchOsTypes () { fetchOsTypes () {
@ -309,44 +310,16 @@ export default {
} }
}) })
}, },
fetchDomains () { handleDomainChange (domainId) {
const params = {} this.domainid = domainId
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
this.form.account = null this.form.account = null
this.account = null this.account = null
if ('listAccounts' in this.$store.getters.apis) {
await this.fetchAccounts()
}
}, },
fetchAccounts () { handleAccountChange (accountName) {
return new Promise((resolve, reject) => { if (accountName) {
api('listAccounts', { this.account = accountName
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
} else { } else {
this.account = acc this.account = null
} }
}, },
handleSubmit (e) { handleSubmit (e) {

View File

@ -57,43 +57,33 @@
<template #label> <template #label>
<tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/> <tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.zoneId" v-model:value="form.zoneId"
showSearch api="listZones"
optionFilterProp="label" :apiParams="zonesApiParams"
:filterOption="(input, option) => { resourceType="zone"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="id"
}" > optionLabelKey="name"
<a-select-option :value="zone.id" v-for="zone in zones" :key="zone.id" :label="zone.name || zone.description"> defaultIcon="global-outlined"
<span> selectFirstOption="true"
<resource-icon v-if="zone.icon" :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/> @change-option-value="handleZoneChange" />
<global-outlined v-else style="margin-right: 5px"/>
{{ zone.name || zone.description }}
</span>
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item name="diskofferingid" ref="diskofferingid"> <a-form-item name="diskofferingid" ref="diskofferingid">
<template #label> <template #label>
<tooltip-label :title="$t('label.diskofferingid')" :tooltip="apiParams.diskofferingid.description"/> <tooltip-label :title="$t('label.diskofferingid')" :tooltip="apiParams.diskofferingid.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.diskofferingid" 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" :placeholder="apiParams.diskofferingid.description"
showSearch @change-option="onChangeDiskOffering" />
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>
</a-form-item> </a-form-item>
<a-form-item ref="format" name="format"> <a-form-item ref="format" name="format">
<template #label> <template #label>
@ -124,38 +114,33 @@
<template #label> <template #label>
<tooltip-label :title="$t('label.domain')" :tooltip="apiParams.domainid.description"/> <tooltip-label :title="$t('label.domain')" :tooltip="apiParams.domainid.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.domainid" v-model:value="form.domainid"
showSearch api="listDomains"
optionFilterProp="label" :apiParams="domainsApiParams"
:filterOption="(input, option) => { resourceType="domain"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="id"
}" optionLabelKey="path"
:loading="domainLoading" defaultIcon="block-outlined"
:placeholder="$t('label.domainid')" :placeholder="$t('label.domainid')"
@change="val => { handleDomainChange(domainList[val].id) }"> allowClear="true"
<a-select-option v-for="(opt, optIndex) in domainList" :key="optIndex" :label="opt.path || opt.name || opt.description"> @change-option-value="handleDomainChange" />
{{ opt.path || opt.name || opt.description }}
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item name="account" ref="account" v-if="'listDomains' in $store.getters.apis"> <a-form-item name="account" ref="account" v-if="'listDomains' in $store.getters.apis">
<template #label> <template #label>
<tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/> <tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.account" v-model:value="form.account"
showSearch api="listAccounts"
optionFilterProp="value" :apiParams="accountsApiParams"
:filterOption="(input, option) => { resourceType="account"
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="name"
}" optionLabelKey="name"
defaultIcon="team-outlined"
allowClear="true"
:placeholder="$t('label.account')" :placeholder="$t('label.account')"
@change="val => { handleAccountChange(val) }"> @change-option-value="handleAccountChange" />
<a-select-option v-for="(acc, index) in accountList" :value="acc.name" :key="index">
{{ acc.name }}
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<div :span="24" class="action-button"> <div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-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 { mixinForm } from '@/utils/mixin'
import ResourceIcon from '@/components/view/ResourceIcon' import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel' import TooltipLabel from '@/components/widgets/TooltipLabel'
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'
export default { export default {
name: 'UploadLocalVolume', name: 'UploadLocalVolume',
mixins: [mixinForm], mixins: [mixinForm],
components: { components: {
ResourceIcon, ResourceIcon,
TooltipLabel TooltipLabel,
InfiniteScrollSelect
}, },
data () { data () {
return { return {
fileList: [], fileList: [],
zones: [],
domainList: [],
accountList: [],
offerings: [],
offeringLoading: false,
formats: ['RAW', 'VHD', 'VHDX', 'OVA', 'QCOW2'], formats: ['RAW', 'VHD', 'VHDX', 'OVA', 'QCOW2'],
domainId: null, domainId: null,
account: null, account: null,
uploadParams: null, uploadParams: null,
domainLoading: false, customDiskOffering: false,
isCustomizedDiskIOps: false,
loading: false, loading: false,
uploadPercentage: 0 uploadPercentage: 0
} }
@ -201,9 +184,38 @@ export default {
beforeCreate () { beforeCreate () {
this.apiParams = this.$getApiParams('getUploadParamsForVolume') 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 () { created () {
this.initForm() this.initForm()
this.fetchData()
}, },
methods: { methods: {
initForm () { initForm () {
@ -221,38 +233,18 @@ export default {
zoneId: [{ required: true, message: this.$t('message.error.select') }] zoneId: [{ required: true, message: this.$t('message.error.select') }]
}) })
}, },
listZones () { handleZoneChange (zoneId) {
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) {
this.form.zoneId = zoneId this.form.zoneId = zoneId
this.zoneId = zoneId // InfiniteScrollSelect will auto-reload disk offerings when apiParams changes
this.fetchDiskOfferings(zoneId)
}, },
fetchDiskOfferings (zoneId) { onChangeDiskOffering (offering) {
this.offeringLoading = true if (offering) {
this.offerings = [{ id: -1, name: '' }] this.customDiskOffering = offering.iscustomized || false
this.form.diskofferingid = undefined this.isCustomizedDiskIOps = offering.iscustomizediops || false
api('listDiskOfferings', { } else {
zoneid: zoneId, this.customDiskOffering = false
listall: true this.isCustomizedDiskIOps = false
}).then(json => { }
for (var offering of json.listdiskofferingsresponse.diskoffering) {
if (offering.iscustomized) {
this.offerings.push(offering)
}
}
}).finally(() => {
this.offeringLoading = false
})
}, },
handleRemove (file) { handleRemove (file) {
const index = this.fileList.indexOf(file) const index = this.fileList.indexOf(file)
@ -266,53 +258,14 @@ export default {
this.form.file = file this.form.file = file
return false return false
}, },
handleDomainChange (domain) { handleDomainChange (domainId) {
this.domainId = domain this.form.domainid = domainId
if ('listAccounts' in this.$store.getters.apis) { this.domainId = domainId
this.fetchAccounts() this.form.account = null
}
}, },
handleAccountChange (acc) { handleAccountChange (accountName) {
if (acc) { this.form.account = accountName
this.account = acc.name this.account = accountName
} else {
this.account = acc
}
},
fetchData () {
this.listZones()
if ('listDomains' in this.$store.getters.apis) {
this.fetchDomains()
}
},
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)
})
}, },
handleSubmit (e) { handleSubmit (e) {
e.preventDefault() e.preventDefault()

View File

@ -47,21 +47,16 @@
<template #label> <template #label>
<tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/> <tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.zoneId" v-model:value="form.zoneId"
showSearch api="listZones"
optionFilterProp="label" :apiParams="zonesApiParams"
:filterOption="(input, option) => { resourceType="zone"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="id"
}" > optionLabelKey="name"
<a-select-option :value="zone.id" v-for="zone in zones" :key="zone.id" :label="zone.name || zone.description"> defaultIcon="global-outlined"
<span> selectFirstOption="true"
<resource-icon v-if="zone.icon" :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/> @change-option-value="handleZoneChange" />
<global-outlined v-else style="margin-right: 5px"/>
{{ zone.name || zone.description }}
</span>
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item name="format" ref="format"> <a-form-item name="format" ref="format">
<template #label> <template #label>
@ -83,23 +78,17 @@
<template #label> <template #label>
<tooltip-label :title="$t('label.diskofferingid')" :tooltip="apiParams.diskofferingid.description || $t('label.diskoffering')"/> <tooltip-label :title="$t('label.diskofferingid')" :tooltip="apiParams.diskofferingid.description || $t('label.diskoffering')"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.diskofferingid" v-model:value="form.diskofferingid"
:loading="loading" api="listDiskOfferings"
@change="id => onChangeDiskOffering(id)" :apiParams="diskOfferingsApiParams"
showSearch resourceType="diskoffering"
optionFilterProp="label" optionValueKey="id"
:filterOption="(input, option) => { optionLabelKey="displaytext"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 defaultIcon="hdd-outlined"
}" > :defaultOption="{ id: null, displaytext: ''}"
<a-select-option allowClear="true"
v-for="(offering, index) in offerings" @change-option="onChangeDiskOffering" />
:value="offering.id"
:key="index"
:label="offering.displaytext || offering.name">
{{ offering.displaytext || offering.name }}
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item name="checksum" ref="checksum"> <a-form-item name="checksum" ref="checksum">
<template #label> <template #label>
@ -114,38 +103,33 @@
<template #label> <template #label>
<tooltip-label :title="$t('label.domain')" :tooltip="apiParams.domainid.description"/> <tooltip-label :title="$t('label.domain')" :tooltip="apiParams.domainid.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.domainid" v-model:value="form.domainid"
showSearch api="listDomains"
optionFilterProp="label" :apiParams="domainsApiParams"
:filterOption="(input, option) => { resourceType="domain"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="id"
}" optionLabelKey="path"
:loading="domainLoading" defaultIcon="block-outlined"
allowClear="true"
:placeholder="$t('label.domainid')" :placeholder="$t('label.domainid')"
@change="val => { handleDomainChange(domainList[val].id) }"> @change-option-value="handleDomainChange" />
<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>
</a-form-item> </a-form-item>
<a-form-item name="account" ref="account" v-if="'listDomains' in $store.getters.apis"> <a-form-item name="account" ref="account" v-if="'listDomains' in $store.getters.apis">
<template #label> <template #label>
<tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/> <tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.account" v-model:value="form.account"
showSearch api="listAccounts"
optionFilterProp="value" :apiParams="accountsApiParams"
:filterOption="(input, option) => { resourceType="account"
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="name"
}" optionLabelKey="name"
defaultIcon="team-outlined"
:placeholder="$t('label.account')" :placeholder="$t('label.account')"
@change="val => { handleAccountChange(val) }"> allowClear="true"
<a-select-option v-for="(acc, index) in accountList" :value="acc.name" :key="index"> @change-option-value="handleAccountChange" />
{{ acc.name }}
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<div :span="24" class="action-button"> <div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button> <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
@ -162,27 +146,26 @@ import { api } from '@/api'
import { mixinForm } from '@/utils/mixin' import { mixinForm } from '@/utils/mixin'
import ResourceIcon from '@/components/view/ResourceIcon' import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel' import TooltipLabel from '@/components/widgets/TooltipLabel'
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'
export default { export default {
name: 'UploadVolume', name: 'UploadVolume',
mixins: [mixinForm], mixins: [mixinForm],
components: { components: {
ResourceIcon, ResourceIcon,
TooltipLabel TooltipLabel,
InfiniteScrollSelect
}, },
data () { data () {
return { return {
zones: [],
domainList: [],
accountList: [],
formats: ['RAW', 'VHD', 'VHDX', 'OVA', 'QCOW2'], formats: ['RAW', 'VHD', 'VHDX', 'OVA', 'QCOW2'],
offerings: [],
zoneSelected: '', zoneSelected: '',
selectedDiskOfferingId: null, selectedDiskOfferingId: null,
domainId: null, domainId: null,
account: null, account: null,
uploadParams: null, uploadParams: null,
domainLoading: false, customDiskOffering: false,
isCustomizedDiskIOps: false,
loading: false, loading: false,
uploadPercentage: 0 uploadPercentage: 0
} }
@ -190,9 +173,36 @@ export default {
beforeCreate () { beforeCreate () {
this.apiParams = this.$getApiParams('uploadVolume') 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 () { created () {
this.initForm() this.initForm()
this.fetchData()
}, },
methods: { methods: {
initForm () { initForm () {
@ -207,78 +217,28 @@ export default {
format: [{ required: true, message: this.$t('message.error.select') }] format: [{ required: true, message: this.$t('message.error.select') }]
}) })
}, },
fetchData () { handleZoneChange (zoneId) {
this.loading = true this.form.zoneId = zoneId
api('listZones', { showicon: true }).then(json => { // InfiniteScrollSelect will auto-reload disk offerings when apiParams changes
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) { onChangeDiskOffering (offering) {
this.loading = true if (offering) {
api('listDiskOfferings', { this.customDiskOffering = offering.iscustomized || false
zoneid: zoneId, this.isCustomizedDiskIOps = offering.iscustomizediops || false
listall: true
}).then(json => {
this.offerings = json.listdiskofferingsresponse.diskoffering || []
}).finally(() => {
this.loading = false
})
},
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
} else { } 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) { handleSubmit (e) {
e.preventDefault() e.preventDefault()
if (this.loading) return if (this.loading) return

View File

@ -67,39 +67,33 @@
<info-circle-outlined style="color: rgba(0,0,0,.45)" /> <info-circle-outlined style="color: rgba(0,0,0,.45)" />
</a-tooltip> </a-tooltip>
</template> </template>
<a-select <infinite-scroll-select
id="domain-selection" id="domain-selection"
v-model:value="form.domainid" v-model:value="form.domainid"
showSearch api="listDomains"
optionFilterProp="label" :apiParams="domainsApiParams"
:filterOption="(input, option) => { resourceType="domain"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="id"
}" optionLabelKey="path"
:loading="domainLoading" defaultIcon="block-outlined"
:defaultOption="{ id: null, path: ''}"
allowClear="true"
:placeholder="apiParams.domainid.description" :placeholder="apiParams.domainid.description"
@change="val => { handleDomainChanged(val) }"> @change-option-value="handleDomainChanged" />
<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>
</a-form-item> </a-form-item>
<a-form-item name="account" ref="account" v-if="isAdminOrDomainAdmin && ['Local'].includes(form.scope) && form.domainid"> <a-form-item name="account" ref="account" v-if="isAdminOrDomainAdmin && ['Local'].includes(form.scope) && form.domainid">
<template #label> <template #label>
<tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/> <tooltip-label :title="$t('label.account')" :tooltip="apiParams.account.description"/>
</template> </template>
<a-select <infinite-scroll-select
v-model:value="form.account" v-model:value="form.account"
showSearch api="listAccounts"
optionFilterProp="label" :apiParams="accountsApiParams"
:filterOption="(input, option) => { resourceType="account"
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="name"
}" optionLabelKey="name"
:loading="accountLoading" defaultIcon="team-outlined"
:placeholder="apiParams.account.description"> :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>
</a-form-item> </a-form-item>
<a-form-item name="payloadurl" ref="payloadurl"> <a-form-item name="payloadurl" ref="payloadurl">
<template #label> <template #label>
@ -156,25 +150,22 @@
<script> <script>
import { ref, reactive, toRaw } from 'vue' import { ref, reactive, toRaw } from 'vue'
import { api } from '@/api' import { api } from '@/api'
import _ from 'lodash'
import { mixinForm } from '@/utils/mixin' import { mixinForm } from '@/utils/mixin'
import TooltipLabel from '@/components/widgets/TooltipLabel' import TooltipLabel from '@/components/widgets/TooltipLabel'
import TestWebhookDeliveryView from '@/components/view/TestWebhookDeliveryView' import TestWebhookDeliveryView from '@/components/view/TestWebhookDeliveryView'
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'
export default { export default {
name: 'CreateWebhook', name: 'CreateWebhook',
mixins: [mixinForm], mixins: [mixinForm],
components: { components: {
TooltipLabel, TooltipLabel,
TestWebhookDeliveryView TestWebhookDeliveryView,
InfiniteScrollSelect
}, },
props: {}, props: {},
data () { data () {
return { return {
domains: [],
domainLoading: false,
accounts: [],
accountLoading: false,
loading: false, loading: false,
testDeliveryAllowed: false, testDeliveryAllowed: false,
testDeliveryLoading: false testDeliveryLoading: false
@ -185,9 +176,6 @@ export default {
}, },
created () { created () {
this.initForm() this.initForm()
if (['Domain', 'Local'].includes(this.form.scope)) {
this.fetchDomainData()
}
}, },
computed: { computed: {
isAdminOrDomainAdmin () { isAdminOrDomainAdmin () {
@ -201,6 +189,21 @@ export default {
return this.form.payloadurl.toLowerCase().startsWith('https://') return this.form.payloadurl.toLowerCase().startsWith('https://')
} }
return false return false
},
domainsApiParams () {
return {
listAll: true,
showicon: true,
details: 'min'
}
},
accountsApiParams () {
if (!this.form.domainid) {
return null
}
return {
domainid: this.form.domainid
}
} }
}, },
methods: { methods: {
@ -228,46 +231,6 @@ export default {
updateTestDeliveryLoading (value) { updateTestDeliveryLoading (value) {
this.testDeliveryLoading = 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) { handleSubmit (e) {
e.preventDefault() e.preventDefault()
if (this.loading) return if (this.loading) return
@ -300,10 +263,8 @@ export default {
return return
} }
if (values.account) { if (values.account) {
const accountItem = _.find(this.accounts, (option) => option.id === values.account) // values.account is the account name (optionValueKey="name")
if (accountItem) { params.account = values.account
params.account = accountItem.name
}
} }
this.loading = true this.loading = true
api('createWebhook', params).then(json => { api('createWebhook', params).then(json => {
@ -331,14 +292,11 @@ export default {
}, 1) }, 1)
}, },
handleScopeChange (e) { handleScopeChange (e) {
if (['Domain', 'Local'].includes(this.form.scope)) { this.form.domainid = null
this.fetchDomainData() this.form.account = null
}
}, },
handleDomainChanged (domainid) { handleDomainChanged (domainid) {
if (domainid) { this.form.account = null
this.fetchAccountData()
}
} }
} }
} }

View File

@ -372,22 +372,16 @@
name="domain" name="domain"
ref="domain" ref="domain"
:label="$t('label.domain')"> :label="$t('label.domain')">
<a-select <infinite-scroll-select
@change="changeDomain"
v-model:value="importForm.selectedDomain" v-model:value="importForm.selectedDomain"
showSearch api="listDomains"
optionFilterProp="label" :apiParams="domainsApiParams"
:filterOption="(input, option) => { resourceType="domain"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="id"
}" > optionLabelKey="path"
<a-select-option v-for="domain in domains" :key="domain.name" :value="domain.id" :label="domain.path || domain.name || domain.description"> defaultIcon="block-outlined"
<span> allowClear="true"
<resource-icon v-if="domain && domain.icon" :image="domain.icon.base64image" size="1x" style="margin-right: 5px"/> @change-option-value="changeDomain" />
<block-outlined v-else style="margin-right: 5px" />
{{ domain.path || domain.name || domain.description }}
</span>
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
@ -395,22 +389,16 @@
name="account" name="account"
ref="account" ref="account"
:label="$t('label.account')"> :label="$t('label.account')">
<a-select <infinite-scroll-select
@change="changeAccount"
v-model:value="importForm.selectedAccount" v-model:value="importForm.selectedAccount"
showSearch api="listAccounts"
optionFilterProp="value" :apiParams="accountsApiParams"
:filterOption="(input, option) => { resourceType="account"
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="name"
}" > optionLabelKey="name"
<a-select-option v-for="account in accounts" :key="account.name" :value="account.name"> defaultIcon="team-outlined"
<span> allowClear="true"
<resource-icon v-if="account && account.icon" :image="account.icon.base64image" size="1x" style="margin-right: 5px"/> @change-option-value="changeAccount" />
<team-outlined v-else style="margin-right: 5px" />
{{ account.name }}
</span>
</a-select-option>
</a-select>
<span v-if="importForm.accountError" class="required">{{ $t('label.required') }}</span> <span v-if="importForm.accountError" class="required">{{ $t('label.required') }}</span>
</a-form-item> </a-form-item>
@ -419,22 +407,16 @@
name="project" name="project"
ref="project" ref="project"
:label="$t('label.project')"> :label="$t('label.project')">
<a-select <infinite-scroll-select
@change="changeProject"
v-model:value="importForm.selectedProject" v-model:value="importForm.selectedProject"
showSearch api="listProjects"
optionFilterProp="label" :apiParams="projectsApiParams"
:filterOption="(input, option) => { resourceType="project"
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 optionValueKey="id"
}" > optionLabelKey="name"
<a-select-option v-for="project in projects" :key="project.id" :value="project.id" :label="project.name"> defaultIcon="project-outlined"
<span> allowClear="true"
<resource-icon v-if="project && project.icon" :image="project.icon.base64image" size="1x" style="margin-right: 5px"/> @change-option-value="changeProject" />
<project-outlined v-else style="margin-right: 5px" />
{{ project.name }}
</span>
</a-select-option>
</a-select>
<span v-if="importForm.projectError" class="required">{{ $t('label.required') }}</span> <span v-if="importForm.projectError" class="required">{{ $t('label.required') }}</span>
</a-form-item> </a-form-item>
@ -480,6 +462,7 @@ import Status from '@/components/widgets/Status'
import SearchView from '@/components/view/SearchView' import SearchView from '@/components/view/SearchView'
import ResourceIcon from '@/components/view/ResourceIcon' import ResourceIcon from '@/components/view/ResourceIcon'
import TooltipLabel from '@/components/widgets/TooltipLabel.vue' import TooltipLabel from '@/components/widgets/TooltipLabel.vue'
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'
export default { export default {
components: { components: {
@ -487,7 +470,8 @@ export default {
Breadcrumb, Breadcrumb,
Status, Status,
SearchView, SearchView,
ResourceIcon ResourceIcon,
InfiniteScrollSelect
}, },
name: 'ManageVolumes', name: 'ManageVolumes',
data () { data () {
@ -607,7 +591,6 @@ export default {
this.page.managed = parseInt(this.$route.query.managedpage || 1) this.page.managed = parseInt(this.$route.query.managedpage || 1)
this.initForm() this.initForm()
this.fetchData() this.fetchData()
this.fetchDomains()
}, },
computed: { computed: {
isPageAllowed () { isPageAllowed () {
@ -629,6 +612,36 @@ export default {
showCluster () { showCluster () {
return this.poolscope !== 'zone' 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 () { showHost () {
return this.poolscope === 'host' return this.poolscope === 'host'
}, },
@ -970,53 +983,6 @@ export default {
this.updateQuery('scope', value) this.updateQuery('scope', value)
this.fetchOptions(this.params.zones, 'zones', 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 () { changeAccountType () {
this.importForm.selectedDomain = null this.importForm.selectedDomain = null
this.importForm.selectedAccount = null this.importForm.selectedAccount = null
@ -1029,8 +995,7 @@ export default {
this.importForm.selectedProject = null this.importForm.selectedProject = null
this.importForm.selectedDiskoffering = null this.importForm.selectedDiskoffering = null
this.diskOfferings = {} this.diskOfferings = {}
this.fetchAccounts() // InfiniteScrollSelect will auto-reload when apiParams changes
this.fetchProjects()
}, },
changeAccount () { changeAccount () {
this.importForm.selectedProject = null this.importForm.selectedProject = null