mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
ui: add an infinite scroll select component (#10840)
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
parent
112dfddd40
commit
951863c3fe
@ -1925,6 +1925,7 @@
|
|||||||
"label.sharedrouteripv6": "IPv6 address for the VR in this shared Network.",
|
"label.sharedrouteripv6": "IPv6 address for the VR in this shared Network.",
|
||||||
"label.sharewith": "Share with",
|
"label.sharewith": "Share with",
|
||||||
"label.showing": "Showing",
|
"label.showing": "Showing",
|
||||||
|
"label.showing.results.for": "Showing results for \"%x\"",
|
||||||
"label.shrinkok": "Shrink OK",
|
"label.shrinkok": "Shrink OK",
|
||||||
"label.shutdown": "Shutdown",
|
"label.shutdown": "Shutdown",
|
||||||
"label.shutdown.provider": "Shutdown provider",
|
"label.shutdown.provider": "Shutdown provider",
|
||||||
|
|||||||
@ -17,112 +17,75 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="header-notice-opener">
|
<span class="header-notice-opener">
|
||||||
<a-select
|
<infinite-scroll-select
|
||||||
v-if="!isDisabled()"
|
v-if="!isDisabled"
|
||||||
|
v-model:value="selectedProjectId"
|
||||||
class="project-select"
|
class="project-select"
|
||||||
:loading="loading"
|
api="listProjects"
|
||||||
v-model:value="projectSelected"
|
:apiParams="projectsApiParams"
|
||||||
:filterOption="filterProject"
|
resourceType="project"
|
||||||
@change="changeProject"
|
:defaultOption="defaultOption"
|
||||||
@focus="fetchData"
|
defaultIcon="project-outlined"
|
||||||
showSearch>
|
:pageSize="100"
|
||||||
|
@change-option="changeProject" />
|
||||||
<a-select-option
|
|
||||||
v-for="(project, index) in projects"
|
|
||||||
:key="index"
|
|
||||||
:label="project.displaytext || project.name">
|
|
||||||
<span>
|
|
||||||
<resource-icon v-if="project.icon && project.icon.base64image" :image="project.icon.base64image" size="1x" style="margin-right: 5px"/>
|
|
||||||
<project-outlined v-else style="margin-right: 5px" />
|
|
||||||
{{ project.displaytext || project.name }}
|
|
||||||
</span>
|
|
||||||
</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import store from '@/store'
|
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect'
|
||||||
import { api } from '@/api'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import ResourceIcon from '@/components/view/ResourceIcon'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProjectMenu',
|
name: 'ProjectMenu',
|
||||||
components: {
|
components: {
|
||||||
ResourceIcon
|
InfiniteScrollSelect
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
projects: [],
|
selectedProjectId: null,
|
||||||
loading: false
|
loading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.fetchData()
|
this.selectedProjectId = this.$store.getters?.project?.id || this.defaultOption.id
|
||||||
|
this.$store.dispatch('ToggleTheme', this.selectedProjectId ? 'dark' : 'light')
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
projectSelected () {
|
isDisabled () {
|
||||||
let projectIndex = 0
|
return !('listProjects' in this.$store.getters.apis)
|
||||||
if (this.$store.getters?.project?.id) {
|
},
|
||||||
projectIndex = this.projects.findIndex(project => project.id === this.$store.getters.project.id)
|
defaultOption () {
|
||||||
this.$store.dispatch('ToggleTheme', projectIndex === undefined ? 'light' : 'dark')
|
return { id: 0, name: this.$t('label.default.view') }
|
||||||
|
},
|
||||||
|
projectsApiParams () {
|
||||||
|
return {
|
||||||
|
details: 'min',
|
||||||
|
listall: true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return projectIndex
|
},
|
||||||
|
mounted () {
|
||||||
|
this.unwatchProject = this.$store.watch(
|
||||||
|
(state, getters) => getters.project?.id,
|
||||||
|
(newId) => {
|
||||||
|
this.selectedProjectId = newId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
if (this.unwatchProject) {
|
||||||
|
this.unwatchProject()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData () {
|
changeProject (project) {
|
||||||
if (this.isDisabled()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var page = 1
|
|
||||||
const projects = []
|
|
||||||
const getNextPage = () => {
|
|
||||||
this.loading = true
|
|
||||||
api('listProjects', { listAll: true, page: page, pageSize: 500, details: 'min', showIcon: true }).then(json => {
|
|
||||||
if (json?.listprojectsresponse?.project) {
|
|
||||||
projects.push(...json.listprojectsresponse.project)
|
|
||||||
}
|
|
||||||
if (projects.length < json.listprojectsresponse.count) {
|
|
||||||
page++
|
|
||||||
getNextPage()
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
this.loading = false
|
|
||||||
this.$store.commit('RELOAD_ALL_PROJECTS', projects)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
getNextPage()
|
|
||||||
},
|
|
||||||
isDisabled () {
|
|
||||||
return !Object.prototype.hasOwnProperty.call(store.getters.apis, 'listProjects')
|
|
||||||
},
|
|
||||||
changeProject (index) {
|
|
||||||
const project = this.projects[index]
|
|
||||||
this.$store.dispatch('ProjectView', project.id)
|
this.$store.dispatch('ProjectView', project.id)
|
||||||
this.$store.dispatch('SetProject', project)
|
this.$store.dispatch('SetProject', project)
|
||||||
this.$store.dispatch('ToggleTheme', project.id === undefined ? 'light' : 'dark')
|
this.$store.dispatch('ToggleTheme', project.id ? 'dark' : 'light')
|
||||||
this.$message.success(`${this.$t('message.switch.to')} "${project.displaytext || project.name}"`)
|
this.$message.success(`${this.$t('message.switch.to')} "${project.displaytext || project.name}"`)
|
||||||
if (this.$route.name !== 'dashboard') {
|
if (this.$route.name !== 'dashboard') {
|
||||||
this.$router.push({ name: 'dashboard' })
|
this.$router.push({ name: 'dashboard' })
|
||||||
}
|
}
|
||||||
},
|
|
||||||
filterProject (input, option) {
|
|
||||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.$store.watch(
|
|
||||||
(state, getters) => getters.allProjects,
|
|
||||||
(newValue, oldValue) => {
|
|
||||||
if (oldValue !== newValue && newValue !== undefined) {
|
|
||||||
this.projects = _.orderBy(newValue, ['displaytext'], ['asc'])
|
|
||||||
this.projects.unshift({ name: this.$t('label.default.view') })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
298
ui/src/components/widgets/InfiniteScrollSelect.vue
Normal file
298
ui/src/components/widgets/InfiniteScrollSelect.vue
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
// Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
// or more contributor license agreements. See the NOTICE file
|
||||||
|
// distributed with this work for additional information
|
||||||
|
// regarding copyright ownership. The ASF licenses this file
|
||||||
|
// to you under the Apache License, Version 2.0 (the
|
||||||
|
// "License"); you may not use this file except in compliance
|
||||||
|
// with the License. You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing,
|
||||||
|
// software distributed under the License is distributed on an
|
||||||
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
// KIND, either express or implied. See the License for the
|
||||||
|
// specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
<!--
|
||||||
|
InfiniteScrollSelect.vue
|
||||||
|
|
||||||
|
A reusable select component that supports:
|
||||||
|
- Infinite scrolling with paginated API
|
||||||
|
- Dynamic search filtering. Needs minimum
|
||||||
|
- Deduplicated option loading
|
||||||
|
- Auto-fetching of preselected value if not present in the initial result
|
||||||
|
|
||||||
|
Usage Example:
|
||||||
|
|
||||||
|
<infinite-scroll-select
|
||||||
|
v-model:value="form.account"
|
||||||
|
api="listAccounts"
|
||||||
|
:apiParams="accountsApiParams"
|
||||||
|
resourceType="account"
|
||||||
|
optionValueKey="name"
|
||||||
|
optionLabelKey="name"
|
||||||
|
@change-option-value="handleAccountNameChange" />
|
||||||
|
|
||||||
|
Props:
|
||||||
|
- api (String, required): API command name (e.g., 'listAccounts')
|
||||||
|
- apiParams (Object, optional): Additional parameters passed to the API
|
||||||
|
- resourceType (String, required): The key in the API response containing the resource array (e.g., 'account')
|
||||||
|
- 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
|
||||||
|
- 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'
|
||||||
|
|
||||||
|
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 (Function): Emits the selected option object when value changes. Works only when mode is not multiple
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Debounced remote filtering
|
||||||
|
- Custom dropdown footer/header (e.g., clear search button)
|
||||||
|
- Handles preselection and fetches missing option automatically
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
:filter-option="false"
|
||||||
|
:loading="loading"
|
||||||
|
show-search
|
||||||
|
placeholder="Select"
|
||||||
|
@search="onSearchTimed"
|
||||||
|
@popupScroll="onScroll"
|
||||||
|
@change="onChange"
|
||||||
|
>
|
||||||
|
<template #dropdownRender="{ menuNode: menu }">
|
||||||
|
<v-nodes :vnodes="menu" />
|
||||||
|
<div v-if="!!searchQuery">
|
||||||
|
<a-divider style="margin: 4px 0" />
|
||||||
|
<div class="select-list-footer">
|
||||||
|
<span>{{ formattedSearchFooterMessage }}</span>
|
||||||
|
<close-outlined
|
||||||
|
@mousedown="e => e.preventDefault()"
|
||||||
|
@click="onSearch()" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-select-option v-for="option in options" :key="option.id" :value="option[optionValueKey]">
|
||||||
|
<span>
|
||||||
|
<span v-if="showIcon">
|
||||||
|
<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>
|
||||||
|
<span>{{ option[optionLabelKey] }}</span>
|
||||||
|
</span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from '@/api'
|
||||||
|
import ResourceIcon from '@/components/view/ResourceIcon'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'InfiniteScrollSelect',
|
||||||
|
components: {
|
||||||
|
ResourceIcon,
|
||||||
|
VNodes: (_, { attrs }) => {
|
||||||
|
return attrs.vnodes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
api: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
apiParams: {
|
||||||
|
type: Object,
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
resourceType: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
optionValueKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'id'
|
||||||
|
},
|
||||||
|
optionLabelKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'name'
|
||||||
|
},
|
||||||
|
defaultOption: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
showIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
defaultIcon: {
|
||||||
|
type: String,
|
||||||
|
default: 'cloud-outlined'
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
options: [],
|
||||||
|
page: 1,
|
||||||
|
totalCount: null,
|
||||||
|
loading: false,
|
||||||
|
searchQuery: '',
|
||||||
|
searchTimer: null,
|
||||||
|
scrollHandlerAttached: false,
|
||||||
|
preselectedOptionValue: null,
|
||||||
|
successiveFetches: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.addDefaultOptionIfNeeded(true)
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.preselectedOptionValue = this.$attrs.value
|
||||||
|
this.fetchItems()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
maxSuccessiveFetches () {
|
||||||
|
return 10
|
||||||
|
},
|
||||||
|
computedPageSize () {
|
||||||
|
return this.pageSize || this.$store.getters.defaultListViewPageSize
|
||||||
|
},
|
||||||
|
formattedSearchFooterMessage () {
|
||||||
|
return `${this.$t('label.showing.results.for').replace('%x', this.searchQuery)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
apiParams () {
|
||||||
|
this.onSearch()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['change-option-value', 'change-option'],
|
||||||
|
methods: {
|
||||||
|
async fetchItems () {
|
||||||
|
if (this.successiveFetches === 0 && this.loading) return
|
||||||
|
this.loading = true
|
||||||
|
const params = {
|
||||||
|
page: this.page,
|
||||||
|
pagesize: this.computedPageSize
|
||||||
|
}
|
||||||
|
if (this.searchQuery && this.searchQuery.length > 0) {
|
||||||
|
params.keyword = this.searchQuery
|
||||||
|
}
|
||||||
|
if (this.apiParams) {
|
||||||
|
Object.assign(params, this.apiParams)
|
||||||
|
}
|
||||||
|
if (this.showIcon) {
|
||||||
|
params.showicon = true
|
||||||
|
}
|
||||||
|
api(this.api, params).then(json => {
|
||||||
|
const response = json[this.api.toLowerCase() + 'response'] || {}
|
||||||
|
if (this.totalCount === null) {
|
||||||
|
this.totalCount = response.count || 0
|
||||||
|
}
|
||||||
|
const newOpts = response[this.resourceType] || []
|
||||||
|
const existingOptions = new Set(this.options.map(o => o[this.optionValueKey]))
|
||||||
|
newOpts.forEach(opt => {
|
||||||
|
if (!existingOptions.has(opt[this.optionValueKey])) {
|
||||||
|
this.options.push(opt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.page++
|
||||||
|
this.checkAndFetchPreselectedOption()
|
||||||
|
}).catch(error => {
|
||||||
|
this.$notifyError(error)
|
||||||
|
}).finally(() => {
|
||||||
|
if (this.successiveFetches === 0) {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
checkAndFetchPreselectedOption () {
|
||||||
|
if (!this.preselectedOptionValue ||
|
||||||
|
(Array.isArray(this.preselectedOptionValue) && this.preselectedOptionValue.length === 0) ||
|
||||||
|
this.successiveFetches >= this.maxSuccessiveFetches) {
|
||||||
|
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) {
|
||||||
|
this.successiveFetches++
|
||||||
|
if (this.options.length < this.totalCount) {
|
||||||
|
this.fetchItems()
|
||||||
|
} else {
|
||||||
|
this.resetPreselectedOptionValue()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Array.isArray(this.preselectedOptionValue) && this.preselectedOptionValue.length > 1) {
|
||||||
|
this.preselectedOptionValue = this.preselectedOptionValue.filter(o => o !== match)
|
||||||
|
} else {
|
||||||
|
this.resetPreselectedOptionValue()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addDefaultOptionIfNeeded () {
|
||||||
|
if (this.defaultOption) {
|
||||||
|
this.options.push(this.defaultOption)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetPreselectedOptionValue () {
|
||||||
|
this.preselectedOptionValue = null
|
||||||
|
this.successiveFetches = 0
|
||||||
|
},
|
||||||
|
onSearchTimed (value) {
|
||||||
|
clearTimeout(this.searchTimer)
|
||||||
|
this.searchTimer = setTimeout(() => {
|
||||||
|
this.onSearch(value)
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
onSearch (value) {
|
||||||
|
this.searchQuery = value
|
||||||
|
this.page = 1
|
||||||
|
this.totalCount = null
|
||||||
|
this.options = []
|
||||||
|
if (!this.searchQuery) {
|
||||||
|
this.addDefaultOptionIfNeeded()
|
||||||
|
}
|
||||||
|
this.fetchItems()
|
||||||
|
},
|
||||||
|
onScroll (e) {
|
||||||
|
const nearBottom = e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 10
|
||||||
|
const hasMore = this.options.length < this.totalCount
|
||||||
|
if (nearBottom && hasMore && !this.loading) {
|
||||||
|
this.fetchItems()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChange (value) {
|
||||||
|
this.resetPreselectedOptionValue()
|
||||||
|
this.$emit('change-option-value', value)
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value === undefined || value == null) {
|
||||||
|
this.$emit('change-option', undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const match = this.options.find(entry => entry[this.optionValueKey] === value)
|
||||||
|
if (match) {
|
||||||
|
this.$emit('change-option', match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.select-list-footer {
|
||||||
|
margin: 4px 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -29,47 +29,27 @@
|
|||||||
<template #label>
|
<template #label>
|
||||||
<tooltip-label :title="$t('label.account')" :tooltip="apiParams.accountids.description"/>
|
<tooltip-label :title="$t('label.account')" :tooltip="apiParams.accountids.description"/>
|
||||||
</template>
|
</template>
|
||||||
<a-select
|
<infinite-scroll-select
|
||||||
v-model:value="form.accountids"
|
v-model:value="form.accountids"
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
:loading="accountLoading"
|
|
||||||
:placeholder="apiParams.accountids.description"
|
:placeholder="apiParams.accountids.description"
|
||||||
showSearch
|
api="listAccounts"
|
||||||
optionFilterProp="label"
|
:apiParams="accountsApiParams"
|
||||||
:filterOption="(input, option) => {
|
resourceType="account"
|
||||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
defaultIcon="team-outlined" />
|
||||||
}" >
|
|
||||||
<a-select-option v-for="(opt, optIndex) in accounts" :key="optIndex" :label="opt.name || opt.description">
|
|
||||||
<span>
|
|
||||||
<resource-icon v-if="opt.icon" :image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
|
|
||||||
<global-outlined style="margin-right: 5px" />
|
|
||||||
{{ opt.name || opt.description }}
|
|
||||||
</span>
|
|
||||||
</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="isAdminOrDomainAdmin()" name="projectids" ref="projectids">
|
<a-form-item v-if="isAdminOrDomainAdmin()" name="projectids" ref="projectids">
|
||||||
<template #label>
|
<template #label>
|
||||||
<tooltip-label :title="$t('label.project')" :tooltip="apiParams.projectids.description"/>
|
<tooltip-label :title="$t('label.project')" :tooltip="apiParams.projectids.description"/>
|
||||||
</template>
|
</template>
|
||||||
<a-select
|
<infinite-scroll-select
|
||||||
v-model:value="form.projectids"
|
v-model:value="form.projectids"
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
:loading="projectLoading"
|
|
||||||
:placeholder="apiParams.projectids.description"
|
:placeholder="apiParams.projectids.description"
|
||||||
showSearch
|
api="listProjects"
|
||||||
optionFilterProp="label"
|
:apiParams="projectsApiParams"
|
||||||
:filterOption="(input, option) => {
|
resourceType="project"
|
||||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
defaultIcon="project-outlined" />
|
||||||
}" >
|
|
||||||
<a-select-option v-for="(opt, optIndex) in projects" :key="optIndex" :label="opt.name || opt.description">
|
|
||||||
<span>
|
|
||||||
<resource-icon v-if="opt.icon" :image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
|
|
||||||
<global-outlined style="margin-right: 5px" />
|
|
||||||
{{ opt.name || opt.description }}
|
|
||||||
</span>
|
|
||||||
</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="!isAdminOrDomainAdmin()">
|
<a-form-item v-if="!isAdminOrDomainAdmin()">
|
||||||
<template #label>
|
<template #label>
|
||||||
@ -106,12 +86,14 @@ import { isAdminOrDomainAdmin } from '@/role'
|
|||||||
import { ref, reactive, toRaw } from 'vue'
|
import { ref, reactive, toRaw } from 'vue'
|
||||||
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'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CreateNetworkPermissions',
|
name: 'CreateNetworkPermissions',
|
||||||
components: {
|
components: {
|
||||||
TooltipLabel,
|
TooltipLabel,
|
||||||
ResourceIcon
|
ResourceIcon,
|
||||||
|
InfiniteScrollSelect
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
resource: {
|
resource: {
|
||||||
@ -121,11 +103,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false
|
||||||
accountLoading: false,
|
|
||||||
projectLoading: false,
|
|
||||||
accounts: [],
|
|
||||||
projects: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
@ -133,45 +111,24 @@ export default {
|
|||||||
this.form = reactive({})
|
this.form = reactive({})
|
||||||
this.rules = reactive({})
|
this.rules = reactive({})
|
||||||
this.apiParams = this.$getApiParams('createNetworkPermissions')
|
this.apiParams = this.$getApiParams('createNetworkPermissions')
|
||||||
this.fetchData()
|
},
|
||||||
|
computed: {
|
||||||
|
accountsApiParams () {
|
||||||
|
return {
|
||||||
|
details: 'min',
|
||||||
|
domainid: this.resource.domainid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
projectsApiParams () {
|
||||||
|
return {
|
||||||
|
details: 'min'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isAdminOrDomainAdmin () {
|
isAdminOrDomainAdmin () {
|
||||||
return isAdminOrDomainAdmin()
|
return isAdminOrDomainAdmin()
|
||||||
},
|
},
|
||||||
async fetchData () {
|
|
||||||
this.fetchAccountData()
|
|
||||||
this.fetchProjectData()
|
|
||||||
},
|
|
||||||
fetchAccountData () {
|
|
||||||
this.accounts = []
|
|
||||||
const params = {}
|
|
||||||
params.showicon = true
|
|
||||||
params.details = 'min'
|
|
||||||
params.domainid = this.resource.domainid
|
|
||||||
this.accountLoading = true
|
|
||||||
api('listAccounts', params).then(json => {
|
|
||||||
const listaccounts = json.listaccountsresponse.account || []
|
|
||||||
this.accounts = listaccounts
|
|
||||||
}).finally(() => {
|
|
||||||
this.accountLoading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
fetchProjectData () {
|
|
||||||
this.projects = []
|
|
||||||
const params = {}
|
|
||||||
params.listall = true
|
|
||||||
params.showicon = true
|
|
||||||
params.details = 'min'
|
|
||||||
params.domainid = this.resource.domainid
|
|
||||||
this.projectLoading = true
|
|
||||||
api('listProjects', params).then(json => {
|
|
||||||
const listProjects = json.listprojectsresponse.project || []
|
|
||||||
this.projects = listProjects
|
|
||||||
}).finally(() => {
|
|
||||||
this.projectLoading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleSubmit (e) {
|
handleSubmit (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (this.loading) return
|
if (this.loading) return
|
||||||
@ -179,31 +136,12 @@ export default {
|
|||||||
const values = toRaw(this.form)
|
const values = toRaw(this.form)
|
||||||
const params = {}
|
const params = {}
|
||||||
params.networkid = this.resource.id
|
params.networkid = this.resource.id
|
||||||
var accountIndexes = values.accountids
|
if (values.accountids && values.accountids.length > 0) {
|
||||||
var accountId = null
|
params.accountids = values.accountids.join(',')
|
||||||
if (accountIndexes && accountIndexes.length > 0) {
|
|
||||||
var accountIds = []
|
|
||||||
for (var i = 0; i < accountIndexes.length; i++) {
|
|
||||||
accountIds = accountIds.concat(this.accounts[accountIndexes[i]].id)
|
|
||||||
}
|
|
||||||
accountId = accountIds.join(',')
|
|
||||||
}
|
}
|
||||||
if (accountId) {
|
if (values.projectids && values.projectids.length > 0) {
|
||||||
params.accountids = accountId
|
params.projectids = values.projectids.join(',')
|
||||||
}
|
}
|
||||||
var projectIndexes = values.projectids
|
|
||||||
var projectId = null
|
|
||||||
if (projectIndexes && projectIndexes.length > 0) {
|
|
||||||
var projectIds = []
|
|
||||||
for (var j = 0; j < projectIndexes.length; j++) {
|
|
||||||
projectIds = projectIds.concat(this.projects[projectIndexes[j]].id)
|
|
||||||
}
|
|
||||||
projectId = projectIds.join(',')
|
|
||||||
}
|
|
||||||
if (projectId) {
|
|
||||||
params.projectids = projectId
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.accounts && values.accounts.length > 0) {
|
if (values.accounts && values.accounts.length > 0) {
|
||||||
params.accounts = values.accounts
|
params.accounts = values.accounts
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user