projects: Enabling Role based Users in Projects (#382)

Enables creating role based users in projects
UI for feature: apache/cloudstack#4128

Also addresses issue: #485

Co-authored-by: Pearl Dsilva <pearl.dsilva@shapeblue.com>
Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Pearl Dsilva 2020-08-04 14:24:51 +05:30 committed by Rohit Yadav
parent 2b7624e45c
commit fa934769d6
15 changed files with 1464 additions and 78 deletions

View File

@ -91,6 +91,7 @@ export default {
},
changeProject (index) {
const project = this.projects[index]
this.$store.dispatch('ProjectView', project.id)
this.$store.dispatch('SetProject', project)
this.$store.dispatch('ToggleTheme', project.id === undefined ? 'light' : 'dark')
this.$message.success(`Switched to "${project.displaytext}"`)

View File

@ -93,6 +93,20 @@ export default {
},
$route () {
this.dedicatedSectionActive = this.dedicatedRoutes.includes(this.$route.meta.name)
this.fetchProjectAdmins()
}
},
methods: {
fetchProjectAdmins () {
if (!this.resource.owner) {
return false
}
var owners = this.resource.owner
var projectAdmins = []
for (var owner of owners) {
projectAdmins.push(Object.keys(owner).includes('user') ? owner.account + '(' + owner.user + ')' : owner.account)
}
this.resource.account = projectAdmins.join()
}
}
}

View File

@ -459,6 +459,21 @@
<span v-else>{{ resource.zone || resource.zonename || resource.zoneid }}</span>
</div>
</div>
<div class="resource-detail-item" v-if="resource.owner">
<div class="resource-detail-item__label">{{ $t('label.owners') }}</div>
<div class="resource-detail-item__details">
<a-icon type="user" />
<template v-for="(item,idx) in resource.owner">
<span style="margin-right:5px" :key="idx">
<span v-if="$store.getters.userInfo.roletype !== 'User'">
<router-link v-if="'user' in item" :to="{ path: '/accountuser', query: { username: item.user, domainid: resource.domainid }}">{{ item.account + '(' + item.user + ')' }}</router-link>
<router-link v-else :to="{ path: '/account', query: { name: item.account, domainid: resource.domainid } }">{{ item.account }}</router-link>
</span>
<span v-else>{{ item.user ? item.account + '(' + item.user + ')' : item.account }}</span>
</span>
</template>
</div>
</div>
<div class="resource-detail-item" v-if="resource.account && !resource.account.startsWith('PrjAcct-')">
<div class="resource-detail-item__label">{{ $t('label.account') }}</div>
<div class="resource-detail-item__details">

View File

@ -169,6 +169,17 @@
<router-link :to="{ path: '/pod/' + record.podid }">{{ text }}</router-link>
</a>
<span slot="account" slot-scope="text, record">
<template v-if="record.owner">
<template v-for="(item,idx) in record.owner">
<span style="margin-right:5px" :key="idx">
<span v-if="$store.getters.userInfo.roletype !== 'User'">
<router-link v-if="'user' in item" :to="{ path: '/accountuser', query: { username: item.user, domainid: record.domainid }}">{{ item.account + '(' + item.user + ')' }}</router-link>
<router-link v-else :to="{ path: '/account', query: { name: item.account, domainid: record.domainid } }">{{ item.account }}</router-link>
</span>
<span v-else>{{ item.user ? item.account + '(' + item.user + ')' : item.account }}</span>
</span>
</template>
</template>
<template v-if="text && !text.startsWith('PrjAcct-')">
<router-link
v-if="'quota' in record && $router.resolve(`${$route.path}/${record.account}`) !== '404'"

View File

@ -88,7 +88,8 @@ export default {
data () {
return {
activeTab: '',
networkService: null
networkService: null,
projectAccount: null
}
},
watch: {

View File

@ -14,6 +14,7 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import store from '@/store'
export default {
name: 'project',
@ -28,7 +29,20 @@ export default {
tabs: [
{
name: 'details',
component: () => import('@/components/view/DetailsTab.vue')
component: () => import('@/views/project/ProjectDetailsTab.vue')
},
{
name: 'accounts',
component: () => import('@/views/project/AccountsTab.vue'),
show: (record, route, user) => { return ['Admin', 'DomainAdmin'].includes(user.roletype) || record.isCurrentUserProjectAdmin }
},
{
name: 'project.roles',
component: () => import('@/views/project/iam/ProjectRoleTab.vue'),
show: (record, route, user) => {
return (['Admin', 'DomainAdmin'].includes(user.roletype) || record.isCurrentUserProjectAdmin) &&
'listProjectRoles' in store.getters.apis
}
},
{
name: 'resources',
@ -38,11 +52,6 @@ export default {
name: 'limits',
show: (record, route, user) => { return ['Admin'].includes(user.roletype) },
component: () => import('@/components/view/ResourceLimitTab.vue')
},
{
name: 'accounts',
show: (record, route, user) => { return record.account === user.account || ['Admin', 'DomainAdmin'].includes(user.roletype) },
component: () => import('@/views/project/AccountsTab.vue')
}
],
actions: [
@ -84,7 +93,7 @@ export default {
dataView: true,
args: ['displaytext'],
show: (record, store) => {
return record.account === store.userInfo.account || ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)
return (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) || record.isCurrentUserProjectAdmin
}
},
{
@ -94,7 +103,7 @@ export default {
message: 'message.activate.project',
dataView: true,
show: (record, store) => {
return (record.account === store.userInfo.account || ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) && record.state === 'Suspended'
return ((['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) || record.isCurrentUserProjectAdmin) && record.state === 'Suspended'
}
},
{
@ -105,7 +114,8 @@ export default {
docHelp: 'adminguide/projects.html#sending-project-membership-invitations',
dataView: true,
show: (record, store) => {
return (record.account === store.userInfo.account || ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) && record.state !== 'Suspended'
return ((['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) ||
record.isCurrentUserProjectAdmin) && record.state !== 'Suspended'
}
},
{
@ -114,13 +124,11 @@ export default {
label: 'label.action.project.add.account',
docHelp: 'adminguide/projects.html#adding-project-members-from-the-ui',
dataView: true,
args: ['projectid', 'account', 'email'],
show: (record, store) => { return record.account === store.userInfo.account || ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) },
mapping: {
projectid: {
value: (record) => { return record.id }
}
}
popup: true,
show: (record, store) => {
return (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) || record.isCurrentUserProjectAdmin
},
component: () => import('@/views/project/AddAccountOrUserToProject.vue')
},
{
api: 'deleteProject',
@ -129,7 +137,9 @@ export default {
message: 'message.delete.project',
docHelp: 'adminguide/projects.html#suspending-or-deleting-a-project',
dataView: true,
show: (record, store) => { return record.account === store.userInfo.account || ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) }
show: (record, store) => {
return (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) || record.isCurrentUserProjectAdmin
}
}
]
}

View File

@ -213,6 +213,7 @@
"label.action.migrate.systemvm": "Migrate System VM",
"label.action.migrate.systemvm.processing": "Migrating System VM....",
"label.action.project.add.account": "Add Account to Project",
"label.action.project.add.user": "Add User to Project",
"label.action.reboot.instance": "Reboot Instance",
"label.action.reboot.instance.processing": "Rebooting Instance....",
"label.action.reboot.router": "Reboot Router",
@ -339,6 +340,7 @@
"label.add.portable.ip.range": "Add Portable IP Range",
"label.add.primary.storage": "Add Primary Storage",
"label.add.private.gateway": "Add Private Gateway",
"label.add.project.role": "Add Project Role",
"label.add.region": "Add Region",
"label.add.resources": "Add Resources",
"label.add.role": "Add Role",
@ -608,6 +610,7 @@
"label.create.nfs.secondary.staging.storage": "Create NFS Secondary Staging Store",
"label.create.nfs.secondary.staging.store": "Create NFS secondary staging store",
"label.create.project": "Create project",
"label.create.project.role": "Create Project Role",
"label.create.user": "Create user",
"label.create.site.vpn.connection": "Create Site-to-Site VPN Connection",
"label.create.site.vpn.gateway": "Create Site-to-Site VPN Gateway",
@ -675,6 +678,7 @@
"label.delete.pa": "Delete Palo Alto",
"label.delete.portable.ip.range": "Delete Portable IP Range",
"label.delete.project": "Delete project",
"label.delete.project.role": "Delete Project Role",
"label.delete.role": "Delete Role",
"label.delete.rule": "Delete rule",
"label.delete.secondary.staging.store": "Delete Secondary Staging Store",
@ -694,6 +698,8 @@
"label.deleting.failed": "Deleting Failed",
"label.deleting.iso": "Deleting ISO",
"label.deleting.processing": "Deleting....",
"label.demote.project.owner": "Demote account to Regular role",
"label.demote.project.owner.user": "Demote user to Regular role",
"label.deleting.template": "Deleting template",
"label.deny": "Deny",
"label.deploymentplanner": "Deployment planner",
@ -793,6 +799,7 @@
"label.edit.lb.rule": "Edit LB rule",
"label.edit.network.details": "Edit network details",
"label.edit.project.details": "Edit project details",
"label.edit.project.role": "Edit Project Role",
"label.edit.region": "Edit Region",
"label.edit.role": "Edit Role",
"label.edit.rule": "Edit rule",
@ -1239,6 +1246,7 @@
"label.macaddresschanges": "MAC Address Changes",
"label.macos": "MacOS",
"label.make.project.owner": "Make account project owner",
"label.make.user.project.owner": "Make user project owner",
"label.makeredundant": "Make redundant",
"label.manage": "Manage",
"label.manage.resources": "Manage Resources",
@ -1325,6 +1333,7 @@
"label.metrics.network.usage": "Network Usage",
"label.metrics.network.write": "Write",
"label.metrics.num.cpu.cores": "Cores",
"label.migrate.instance.to": "Migrate instance to",
"label.migrate.instance.to.host": "Migrate instance to another host",
"label.migrate.instance.to.ps": "Migrate instance to another primary storage",
@ -1503,6 +1512,7 @@
"label.owned.public.ips": "Owned Public IP Addresses",
"label.owner.account": "Owner Account",
"label.owner.domain": "Owner Domain",
"label.owners": "Owners",
"label.pa": "Palo Alto",
"label.page": "page",
"label.palo.alto.details": "Palo Alto details",
@ -1593,6 +1603,10 @@
"label.project.invitation": "Project Invitations",
"label.project.invite": "Invite to project",
"label.project.name": "Project name",
"label.project.owner": "Project Owner(s)",
"label.project.role": "Project Role",
"label.project.roles": "Project Roles",
"label.project.role.permissions": "Project Role Permissions",
"label.project.view": "Project View",
"label.projectaccountname": "Project Account Name",
"label.projectid": "Project ID",
@ -1712,6 +1726,8 @@
"label.remove.network.offering": "Remove network offering",
"label.remove.pf": "Remove port forwarding rule",
"label.remove.project.account": "Remove account from project",
"label.remove.project.role": "Remove project role",
"label.remove.project.user": "Remove user from project",
"label.remove.region": "Remove Region",
"label.remove.rule": "Remove rule",
"label.remove.ssh.key.pair": "Remove SSH Key Pair",
@ -2094,6 +2110,7 @@
"label.untagged": "Untagged",
"label.update.instance.group": "Update Instance Group",
"label.update.project.resources": "Update project resources",
"label.update.project.role": "Update project role",
"label.update.ssl": " SSL Certificate",
"label.update.ssl.cert": " SSL Certificate",
"label.update.to": "updated to",
@ -2120,6 +2137,7 @@
"label.usehttps": "Use HTTPS",
"label.usenewdiskoffering": "Replace disk offering?",
"label.user": "User",
"label.user.as.admin": "Make User the Project Admin",
"label.user.conflict": "Conflict",
"label.user.details": "User details",
"label.user.source": "source",
@ -2440,6 +2458,7 @@
"message.add.tag.for.networkacl": "Add tag for NetworkACL",
"message.add.tag.processing": "Adding new tag...",
"message.add.template": "Please enter the following data to create your new template",
"message.add.user.to.project": "This form is to enable adding specific users of an account to a project.<br>Furthermore, a ProjectRole may be added to the added user/account to allow/disallow API access at project level.<br> We can also specify the role with which the user should be added to a project - Admin/Regular; if not specified, it defaults to 'Regular'",
"message.add.volume": "Please fill in the following data to add a new volume.",
"message.add.vpn.connection.failed": "Adding VPN Connection failed",
"message.add.vpn.connection.processing": "Adding VPN Connection...",
@ -3183,6 +3202,9 @@
"message.zone.step.3.desc": "Please enter the following info to add a new pod",
"message.zonewizard.enable.local.storage": "WARNING: If you enable local storage for this zone, you must do the following, depending on where you would like your system VMs to launch:<br/><br/>1. If system VMs need to be launched in shared primary storage, shared primary storage needs to be added to the zone after creation. You must also start the zone in a disabled state.<br/><br/>2. If system VMs need to be launched in local primary storage, system.vm.use.local.storage needs to be set to true before you enable the zone.<br/><br/><br/>Would you like to continue?",
"messgae.validate.min": "Please enter a value greater than or equal to {0}.",
"migrate.from": "Migrate From",
"migrate.to": "Migrate To",
"migrationPolicy": "Migration Policy",
"network.rate": "Network Rate",
"router.health.checks": "Health Check",
"side.by.side": "Side by Side",

View File

@ -91,7 +91,6 @@ const user = {
return new Promise((resolve, reject) => {
login(userInfo).then(response => {
const result = response.loginresponse || {}
Cookies.set('account', result.account, { expires: 1 })
Cookies.set('domainid', result.domainid, { expires: 1 })
Cookies.set('role', result.type, { expires: 1 })
@ -100,7 +99,6 @@ const user = {
Cookies.set('userfullname', result.firstname + ' ' + result.lastname, { expires: 1 })
Cookies.set('userid', result.userid, { expires: 1 })
Cookies.set('username', result.username, { expires: 1 })
Vue.ls.set(ACCESS_TOKEN, result.sessionkey, 24 * 60 * 60 * 1000)
commit('SET_TOKEN', result.sessionkey)
@ -174,7 +172,7 @@ const user = {
})
}
api('listUsers', { username: Cookies.get('username'), listall: true }).then(response => {
api('listUsers', { username: Cookies.get('username') }).then(response => {
const result = response.listusersresponse.user[0]
commit('SET_INFO', result)
commit('SET_NAME', result.firstname + ' ' + result.lastname)
@ -248,6 +246,29 @@ const user = {
var jobsArray = Vue.ls.get(ASYNC_JOB_IDS, [])
jobsArray.push(jobJson)
commit('SET_ASYNC_JOB_IDS', jobsArray)
},
ProjectView ({ commit }, projectid) {
return new Promise((resolve, reject) => {
api('listApis', { projectid: projectid }).then(response => {
const apis = {}
const apiList = response.listapisresponse.api
for (var idx = 0; idx < apiList.length; idx++) {
const api = apiList[idx]
const apiName = api.name
apis[apiName] = {
params: api.params,
response: api.response
}
}
commit('SET_APIS', apis)
resolve(apis)
store.dispatch('GenerateRoutes', { apis }).then(() => {
router.addRoutes(store.getters.addRouters)
})
}).catch(error => {
reject(error)
})
})
}
}
}

View File

@ -576,6 +576,18 @@ export default {
this.items = []
}
if (['listTemplates', 'listIsos'].includes(this.apiName) && this.items.length > 1) {
this.items = [...new Map(this.items.map(x => [x.id, x])).values()]
}
if (this.apiName === 'listProjects' && this.items.length > 0) {
this.columns.map(col => {
if (col.title === 'Account') {
col.title = this.$t('label.project.owner')
}
})
}
for (let idx = 0; idx < this.items.length; idx++) {
this.items[idx].key = idx
for (const key in customRender) {
@ -818,7 +830,6 @@ export default {
execSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
console.log(values)
if (!err) {
const params = {}
if ('id' in this.resource && this.currentAction.params.map(i => { return i.name }).includes('id')) {
@ -868,10 +879,6 @@ export default {
}
}
console.log(this.currentAction)
console.log(this.resource)
console.log(params)
const resourceName = params.displayname || params.displaytext || params.name || params.hostname || params.username || params.ipaddress || params.virtualmachinename || this.resource.name
var hasJobId = false

View File

@ -21,29 +21,47 @@
<a-col :md="24" :lg="24">
<a-table
size="small"
:loading="loading"
:loading="loading.table"
:columns="columns"
:dataSource="dataSource"
:pagination="false"
:rowKey="record => record.accountid || record.account"
>
<span slot="action" v-if="record.role!==owner" slot-scope="text, record" class="account-button-action">
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.make.project.owner') }}
</template>
:rowKey="record => record.userid ? record.userid : (record.accountid || record.account)">
<span slot="user" slot-scope="text, record" v-if="record.userid">
{{ record.username }}
</span>
<span slot="projectrole" slot-scope="text, record" v-if="record.projectroleid">
{{ getProjectRole(record) }}
</span>
<span v-if="imProjectAdmin && dataSource.length > 1" slot="action" slot-scope="text, record" class="account-button-action">
<a-tooltip
slot="title"
placement="top"
:title="record.userid ? $t('label.make.user.project.owner') : $t('label.make.project.owner')">
<a-button
v-if="record.role !== owner"
type="default"
shape="circle"
icon="user"
icon="arrow-up"
size="small"
:disabled="!('updateProject' in $store.getters.apis)"
@click="onMakeProjectOwner(record)" />
@click="promoteAccount(record)" />
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.remove.project.account') }}
</template>
<a-tooltip
slot="title"
placement="top"
:title="record.userid ? $t('label.demote.project.owner.user') : $t('label.demote.project.owner')"
v-if="updateProjectApi.params.filter(x => x.name === 'swapowner').length > 0">
<a-button
v-if="record.role === owner"
type="default"
shape="circle"
icon="arrow-down"
size="small"
@click="demoteAccount(record)" />
</a-tooltip>
<a-tooltip
slot="title"
placement="top"
:title="record.userid ? $t('label.remove.project.user') : $t('label.remove.project.account')">
<a-button
type="danger"
shape="circle"
@ -89,11 +107,20 @@ export default {
return {
columns: [],
dataSource: [],
loading: false,
imProjectAdmin: false,
loading: {
user: false,
projectAccount: false,
roles: false,
table: false
},
page: 1,
pageSize: 10,
itemCount: 0,
owner: 'Admin'
users: [],
projectRoles: [],
owner: 'Admin',
role: 'Regular'
}
},
created () {
@ -101,27 +128,36 @@ export default {
{
title: this.$t('label.account'),
dataIndex: 'account',
width: '35%',
scopedSlots: { customRender: 'account' }
},
{
title: this.$t('label.role'),
title: this.$t('label.roletype'),
dataIndex: 'role',
scopedSlots: { customRender: 'role' }
},
{
title: this.$t('label.action'),
dataIndex: 'action',
fixed: 'right',
width: 100,
scopedSlots: { customRender: 'action' }
}
]
if (this.isProjectRolesSupported()) {
this.columns.splice(1, 0, {
title: this.$t('label.user'),
dataIndex: 'userid',
scopedSlots: { customRender: 'user' }
})
this.columns.splice(this.columns.length - 1, 0, {
title: this.$t('label.project.role'),
dataIndex: 'projectroleid',
scopedSlots: { customRender: 'projectrole' }
})
}
this.page = 1
this.pageSize = 10
this.itemCount = 0
},
inject: ['parentFetchData'],
mounted () {
this.fetchData()
},
@ -140,25 +176,12 @@ export default {
params.projectId = this.resource.id
params.page = this.page
params.pageSize = this.pageSize
this.loading = true
api('listProjectAccounts', params).then(json => {
const listProjectAccount = json.listprojectaccountsresponse.projectaccount
const itemCount = json.listprojectaccountsresponse.count
if (!listProjectAccount || listProjectAccount.length === 0) {
this.dataSource = []
return
}
this.itemCount = itemCount
this.dataSource = listProjectAccount
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
this.updateProjectApi = this.$store.getters.apis.updateProject
this.fetchUsers()
this.fetchProjectAccounts(params)
if (this.isProjectRolesSupported()) {
this.fetchProjectRoles()
}
},
changePage (page, pageSize) {
this.page = page
@ -166,29 +189,126 @@ export default {
this.fetchData()
},
changePageSize (currentPage, pageSize) {
this.page = currentPage
this.page = 0
this.pageSize = pageSize
this.fetchData()
},
isLoggedInUserProjectAdmin (user) {
if (['Admin', 'DomainAdmin'].includes(this.$store.getters.userInfo.roletype)) {
return true
}
// If I'm the logged in user Or if I'm the logged in account And I'm the owner
if (((user.userid && user.userid === this.$store.getters.userInfo.id) ||
user.account === this.$store.getters.userInfo.account) &&
user.role === this.owner) {
return true
}
return false
},
isProjectRolesSupported () {
return ('listProjectRoles' in this.$store.getters.apis)
},
getProjectRole (record) {
const projectRole = this.projectRoles.filter(role => role.id === record.projectroleid)
return projectRole[0].name || projectRole[0].id || null
},
fetchUsers () {
this.loading.user = true
api('listUsers', { listall: true }).then(response => {
this.users = response.listusersresponse.user || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading.user = false
})
},
fetchProjectRoles () {
this.loading.roles = true
api('listProjectRoles', { projectId: this.resource.id }).then(response => {
this.projectRoles = response.listprojectrolesresponse.projectrole || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading.roles = false
})
},
fetchProjectAccounts (params) {
this.loading.projectAccount = true
api('listProjectAccounts', params).then(json => {
const listProjectAccount = json.listprojectaccountsresponse.projectaccount
const itemCount = json.listprojectaccountsresponse.count
if (!listProjectAccount || listProjectAccount.length === 0) {
this.dataSource = []
return
}
for (const projectAccount of listProjectAccount) {
this.imProjectAdmin = this.isLoggedInUserProjectAdmin(projectAccount)
if (this.imProjectAdmin) {
break
}
}
this.itemCount = itemCount
this.dataSource = listProjectAccount
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading.projectAccount = false
})
},
onMakeProjectOwner (record) {
const title = this.$t('label.make.project.owner')
const loading = this.$message.loading(title + `${this.$t('label.in.progress.for')} ` + record.account, 0)
const params = {}
params.id = this.resource.id
params.account = record.account
this.updateProject(record, params, title, loading)
},
promoteAccount (record) {
var title = this.$t('label.make.project.owner')
const loading = this.$message.loading(title + `${this.$t('label.in.progress.for')} ` + record.account, 0)
const params = {}
params.id = this.resource.id
if (record.userid) {
params.userid = record.userid
// params.accountid = (record.user && record.user[0].accountid) || record.accountid
title = this.$t('label.make.user.project.owner')
} else {
params.account = record.account
}
params.roletype = this.owner
params.swapowner = false
this.updateProject(record, params, title, loading)
},
demoteAccount (record) {
var title = this.$t('label.demote.project.owner')
const loading = this.$message.loading(title + `${this.$t('label.in.progress.for')} ` + record.account, 0)
const params = {}
if (record.userid) {
params.userid = record.userid
// params.accountid = (record.user && record.user[0].accountid) || record.accountid
title = this.$t('label.demote.project.owner.user')
} else {
params.account = record.account
}
params.id = this.resource.id
params.roletype = 'Regular'
params.swapowner = false
this.updateProject(record, params, title, loading)
},
updateProject (record, params, title, loading) {
api('updateProject', params).then(json => {
const hasJobId = this.checkForAddAsyncJob(json, title, record.account)
if (hasJobId) {
this.fetchData()
}
}).catch(error => {
// show error
this.$notifyError(error)
}).finally(() => {
setTimeout(loading, 1000)
this.parentFetchData()
})
},
onShowConfirmDelete (record) {
@ -209,18 +329,22 @@ export default {
const title = this.$t('label.remove.project.account')
const loading = this.$message.loading(title + `${this.$t('label.in.progress.for')} ` + record.account, 0)
const params = {}
params.account = record.account
params.projectid = this.resource.id
api('deleteAccountFromProject', params).then(json => {
if (record.userid) {
params.userid = record.userid
this.deleteOperation('deleteUserFromProject', params, record, title, loading)
} else {
params.account = record.account
this.deleteOperation('deleteAccountFromProject', params, record, title, loading)
}
},
deleteOperation (apiName, params, record, title, loading) {
api(apiName, params).then(json => {
const hasJobId = this.checkForAddAsyncJob(json, title, record.account)
if (hasJobId) {
this.fetchData()
}
}).catch(error => {
// show error
this.$notifyError(error)
}).finally(() => {
setTimeout(loading, 1000)

View File

@ -0,0 +1,332 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<a-tabs class="form-layout">
<a-tab-pane key="1" :tab="$t('label.action.project.add.account')">
<a-form
:form="form"
@submit="addAccountToProject"
layout="vertical">
<a-form-item>
<span slot="label">
{{ $t('label.account') }}
<a-tooltip :title="apiParams.addAccountToProject.account.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-input
v-decorator="['account']"
:placeholder="apiParams.addAccountToProject.account.description"/>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.email') }}
<a-tooltip :title="apiParams.addAccountToProject.email.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-input v-decorator="[ 'email']"></a-input>
</a-form-item>
<a-form-item v-if="apiParams.addAccountToProject.projectroleid">
<span slot="label">
{{ $t('label.project.role') }}
<a-tooltip :title="apiParams.addAccountToProject.projectroleid.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-select
showSearch
v-decorator="['projectroleid']"
:loading="loading"
:placeholder="$t('label.project.role')"
>
<a-select-option v-for="role in projectRoles" :key="role.id">
{{ role.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="apiParams.addAccountToProject.roletype">
<span slot="label">
{{ $t('label.roletype') }}
<a-tooltip :title="apiParams.addAccountToProject.roletype.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-select
showSearch
v-decorator="['roletype']"
:placeholder="$t('label.roletype')">
<a-select-option value="Admin">Admin</a-select-option>
<a-select-option value="Regular">Regular</a-select-option>
</a-select>
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
<a-button type="primary" @click="addAccountToProject" :loading="loading">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" :tab="$t('label.action.project.add.user')" v-if="apiParams.addUserToProject">
<a-form
:form="form"
@submit="addUserToProject"
layout="vertical">
<p v-html="$t('message.add.user.to.project')"></p>
<a-form-item>
<span slot="label">
{{ $t('label.user') }}
<a-tooltip :title="apiParams.addUserToProject.username.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-input
v-decorator="['username']"
:placeholder="apiParams.addUserToProject.username.description"/>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.email') }}
<a-tooltip :title="apiParams.addUserToProject.email.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-input v-decorator="[ 'email']"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.project.role') }}
<a-tooltip :title="apiParams.addUserToProject.roletype.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-select
showSearch
v-decorator="['projectroleid']"
:loading="loading"
:placeholder="$t('label.project.role')"
>
<a-select-option v-for="role in projectRoles" :key="role.id">
{{ role.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.roletype') }}
<a-tooltip :title="apiParams.addUserToProject.roletype.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-select
showSearch
v-decorator="['roletype']"
:placeholder="$t('label.roletype')">
<a-select-option value="Admin">Admin</a-select-option>
<a-select-option value="Regular">Regular</a-select-option>
</a-select>
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
<a-button type="primary" @click="addUserToProject" :loading="loading">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import { api } from '@/api'
export default {
name: 'AddAccountOrUserToProject',
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
users: [],
accounts: [],
projectRoles: [],
selectedUser: null,
selectedAccount: null,
loading: false,
load: {
users: false,
accounts: false,
projectRoles: false
}
}
},
mounted () {
this.fetchData()
},
beforeCreate () {
this.form = this.$form.createForm(this)
const apis = ['addAccountToProject']
if ('addUserToProject' in this.$store.getters.apis) {
apis.push('addUserToProject')
}
this.apiParams = {}
for (var api of apis) {
const details = {}
const apiConfig = this.$store.getters.apis[api]
apiConfig.params.forEach(param => {
details[param.name] = param
})
this.apiParams[api] = details
}
},
methods: {
fetchData () {
this.fetchUsers()
this.fetchAccounts()
if (this.isProjectRolesSupported()) {
this.fetchProjectRoles()
}
},
fetchUsers () {
this.load.users = true
api('listUsers', { listall: true }).then(response => {
this.users = response.listusersresponse.user ? response.listusersresponse.user : []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.load.users = false
})
},
fetchAccounts () {
this.load.accounts = true
api('listAccounts', {
domainid: this.resource.domainid
}).then(response => {
this.accounts = response.listaccountsresponse.account || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.load.accounts = false
})
},
fetchProjectRoles () {
this.load.projectRoles = true
api('listProjectRoles', {
projectid: this.resource.id
}).then(response => {
this.projectRoles = response.listprojectrolesresponse.projectrole || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.load.projectRoles = false
})
},
isProjectRolesSupported () {
return ('listProjectRoles' in this.$store.getters.apis)
},
addAccountToProject (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
this.loading = true
var params = {
projectid: this.resource.id
}
for (const key in values) {
const input = values[key]
if (input === undefined) {
continue
}
params[key] = input
}
api('addAccountToProject', params).then(response => {
this.$pollJob({
jobId: response.addaccounttoprojectresponse.jobid,
successMessage: `Successfully added account ${params.account} to project`,
errorMessage: `Failed to add account: ${params.account} to project`,
loadingMessage: `Adding Account: ${params.account} to project...`,
catchMessage: 'Error encountered while fetching async job result'
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.$emit('refresh-data')
this.loading = false
this.closeAction()
})
})
},
addUserToProject (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
this.loading = true
var params = {
projectid: this.resource.id
}
for (const key in values) {
const input = values[key]
if (input === undefined) {
continue
}
params[key] = input
}
api('addUserToProject', params).then(response => {
this.$pollJob({
jobId: response.addusertoprojectresponse.jobid,
successMessage: `Successfully added user ${params.username} to project`,
errorMessage: `Failed to add user: ${params.username} to project`,
loadingMessage: `Adding User ${params.username} to project...`,
catchMessage: 'Error encountered while fetching async job result'
})
}).catch(error => {
console.log('catch')
this.$notifyError(error)
}).finally(() => {
this.$emit('refresh-data')
this.loading = false
this.closeAction()
})
})
},
closeAction () {
this.$emit('close-action')
}
}
}
</script>
<style lang="scss" scoped>
.form-layout {
width: 80vw;
@media (min-width: 600px) {
width: 450px;
}
}
.action-button {
text-align: right;
button {
margin-right: 5px;
}
}
</style>

View File

@ -115,6 +115,11 @@ export default {
dataIndex: 'project',
scopedSlots: { customRender: 'project' }
},
{
title: this.$t('label.account'),
dataIndex: 'account',
scopedSlots: { customRender: 'account' }
},
{
title: this.$t('label.domain'),
dataIndex: 'domain',
@ -152,6 +157,18 @@ export default {
this.page = 1
this.pageSize = 10
this.itemCount = 0
this.apiConfig = this.$store.getters.apis.listProjectInvitations || {}
this.apiParams = {}
this.apiConfig.params.forEach(param => {
this.apiParams[param.name] = param
})
if (this.apiParams.userid) {
this.columns.splice(2, 0, {
title: this.$t('label.user'),
dataIndex: 'userid',
scopedSlots: { customRender: 'user' }
})
}
},
mounted () {
this.fetchData()
@ -225,7 +242,11 @@ export default {
const params = {}
params.projectid = record.projectid
params.account = record.account
if (record.userid && record.userid !== null) {
params.userid = record.userid
} else {
params.account = record.account
}
params.domainid = record.domainid
params.accept = state

View File

@ -0,0 +1,57 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<DetailsTab :resource="resource" />
</template>
<script>
import DetailsTab from '@/components/view/DetailsTab'
export default {
name: 'ProjectDetailsTab',
components: {
DetailsTab
},
props: {
resource: {
type: Object,
required: true
}
},
watch: {
resource (newItem, oldItem) {
if (!newItem || !newItem.id) {
return
}
this.resource = newItem
this.fetchProjectAccounts()
}
},
methods: {
fetchProjectAccounts () {
var owner = this.resource.owner
owner = owner.filter(projectaccount => {
return (projectaccount.userid && projectaccount.userid === this.$store.getters.userInfo.id) ||
projectaccount.account === this.$store.getters.userInfo.account
})
var isCurrentUserProjectAdmin = false
if (owner.length > 0) {
isCurrentUserProjectAdmin = true
}
this.$set(this.resource, 'isCurrentUserProjectAdmin', isCurrentUserProjectAdmin)
}
}
}
</script>

View File

@ -0,0 +1,442 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<a-icon v-if="loadingTable" type="loading" class="main-loading-spinner"></a-icon>
<div v-else>
<div v-if="updateTable" class="loading-overlay">
<a-icon type="loading" />
</div>
<div
class="rules-list ant-list ant-list-bordered"
:class="{'rules-list--overflow-hidden' : updateTable}" >
<div class="rules-table-item ant-list-item">
<div class="rules-table__col rules-table__col--grab"></div>
<div class="rules-table__col rules-table__col--rule rules-table__col--new">
<a-auto-complete
:autoFocus="true"
:filterOption="filterOption"
:dataSource="apis"
:value="newRule"
@change="val => newRule = val"
placeholder="Rule"
:class="{'rule-dropdown-error' : newRuleSelectError}" />
</div>
<div class="rules-table__col rules-table__col--permission">
<permission-editable
:defaultValue="newRulePermission"
@change="onPermissionChange(null, $event)" />
</div>
<div class="rules-table__col rules-table__col--description">
<a-input v-model="newRuleDescription" placeholder="Description"></a-input>
</div>
<div class="rules-table__col rules-table__col--actions">
<a-tooltip
placement="bottom">
<template slot="title">
Save new Rule
</template>
<a-button
icon="plus"
type="primary"
shape="circle"
@click="onRuleSave"
>
</a-button>
</a-tooltip>
</div>
</div>
<draggable
v-model="rules"
@change="changeOrder"
handle=".drag-handle"
animation="200"
ghostClass="drag-ghost">
<transition-group type="transition">
<div
v-for="(record, index) in rules"
:key="`item-${index}`"
class="rules-table-item ant-list-item">
<div class="rules-table__col rules-table__col--grab drag-handle">
<a-icon type="drag"></a-icon>
</div>
<div class="rules-table__col rules-table__col--rule">
{{ record.rule }}
</div>
<div class="rules-table__col rules-table__col--permission">
<permission-editable
:defaultValue="record.permission"
@change="onPermissionChange(record, $event)" />
</div>
<div class="rules-table__col rules-table__col--description">
<template v-if="record.description">
{{ record.description }}
</template>
<div v-else class="no-description">
No description entered.
</div>
</div>
<div class="rules-table__col rules-table__col--actions">
<rule-delete
:record="record"
@delete="onRuleDelete(record.id)" />
</div>
</div>
</transition-group>
</draggable>
</div>
</div>
</template>
<script>
import { api } from '@/api'
import draggable from 'vuedraggable'
import PermissionEditable from '@/views/iam/PermissionEditable'
import RuleDelete from '@/views/iam/RuleDelete'
export default {
name: 'ProjectRolePermissionTab',
components: {
RuleDelete,
PermissionEditable,
draggable
},
props: {
resource: {
type: Object,
required: true
},
role: {
type: Object,
required: true
}
},
data () {
return {
loadingTable: true,
updateTable: false,
rules: null,
newRule: '',
newRulePermission: 'allow',
newRuleDescription: '',
newRuleSelectError: false,
drag: false,
apis: []
}
},
mounted () {
this.apis = Object.keys(this.$store.getters.apis).sort((a, b) => a.localeCompare(b))
this.fetchData()
},
watch: {
resource: function () {
this.fetchData(() => {
this.resetNewFields()
})
}
},
methods: {
filterOption (input, option) {
return (
option.componentOptions.children[0].text.toUpperCase().indexOf(input.toUpperCase()) >= 0
)
},
resetNewFields () {
this.newRule = ''
this.newRulePermission = 'allow'
this.newRuleDescription = ''
this.newRuleSelectError = false
},
fetchData (callback = null) {
if (!this.resource.id) return
api('listProjectRolePermissions', {
projectid: this.resource.id,
projectroleid: this.role.id
}).then(response => {
this.rules = response.listprojectrolepermissionsresponse.projectrolepermission
}).catch(error => {
console.error(error)
}).finally(() => {
this.loadingTable = false
this.updateTable = false
if (callback) callback()
})
},
changeOrder () {
api('updateProjectRolePermission', {}, 'POST', {
projectid: this.resource.id,
projectroleid: this.role.id,
ruleorder: this.rules.map(rule => rule.id)
}).catch(error => {
console.error(error)
}).finally(() => {
this.fetchData()
})
},
onRuleDelete (key) {
this.updateTable = true
api('deleteProjectRolePermission', {
id: key,
projectid: this.resource.id
}).catch(error => {
console.error(error)
}).finally(() => {
this.fetchData()
})
},
onPermissionChange (record, value) {
this.newRulePermission = value
if (!record) return
this.updateTable = true
api('updateProjectRolePermission', {
projectid: this.resource.id,
projectroleid: this.role.id,
projectrolepermissionid: record.id,
permission: value
}).then(() => {
this.fetchData()
}).catch(error => {
this.$notifyError(error)
})
},
onRuleSelect (value) {
this.newRule = value
},
onRuleSave () {
if (!this.newRule) {
this.newRuleSelectError = true
return
}
this.updateTable = true
api('createProjectRolePermission', {
rule: this.newRule,
permission: this.newRulePermission,
description: this.newRuleDescription,
projectroleid: this.role.id,
projectid: this.resource.id
}).then(() => {
}).catch(error => {
console.error(error)
this.$notifyError(error)
}).finally(() => {
this.resetNewFields()
this.fetchData()
})
}
}
}
</script>
<style lang="scss" scoped>
.main-loading-spinner {
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
}
.role-add-btn {
margin-bottom: 15px;
}
.new-role-controls {
display: flex;
button {
&:not(:last-child) {
margin-right: 5px;
}
}
}
.rules-list {
max-height: 600px;
overflow: auto;
&--overflow-hidden {
overflow: hidden;
}
}
.rules-table {
&-item {
position: relative;
display: flex;
align-items: stretch;
padding: 0;
flex-wrap: wrap;
@media (min-width: 760px) {
flex-wrap: nowrap;
padding-right: 25px;
}
}
&__col {
display: flex;
align-items: center;
padding: 15px;
@media (min-width: 760px) {
padding: 15px 0;
&:not(:first-child) {
padding-left: 20px;
}
&:not(:last-child) {
border-right: 1px solid #e8e8e8;
padding-right: 20px;
}
}
&--grab {
position: absolute;
top: 4px;
left: 0;
width: 100%;
@media (min-width: 760px) {
position: relative;
top: auto;
width: 35px;
padding-left: 25px;
justify-content: center;
}
}
&--rule,
&--description {
word-break: break-all;
flex: 1;
width: 100%;
@media (min-width: 760px) {
width: auto;
}
}
&--rule {
padding-left: 60px;
background-color: rgba(#e6f7ff, 0.7);
@media (min-width: 760px) {
padding-left: 0;
background: none;
}
}
&--permission {
justify-content: center;
width: 100%;
.ant-select {
width: 100%;
}
@media (min-width: 760px) {
width: auto;
.ant-select {
width: auto;
}
}
}
&--actions {
max-width: 60px;
width: 100%;
padding-right: 0;
@media (min-width: 760px) {
width: auto;
max-width: 70px;
padding-right: 15px;
}
}
&--new {
padding-left: 15px;
background-color: transparent;
div {
width: 100%;
}
}
}
}
.no-description {
opacity: 0.4;
font-size: 0.7rem;
@media (min-width: 760px) {
display: none;
}
}
.drag-handle {
cursor: pointer;
}
.drag-ghost {
opacity: 0.5;
background: #f0f2f5;
}
.loading-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: #39A7DE;
background-color: rgba(#fff, 0.8);
}
</style>
<style lang="scss">
.rules-table__col--new {
.ant-select {
width: 100%;
}
}
.rule-dropdown-error {
.ant-input {
border-color: #ff0000
}
}
</style>

View File

@ -0,0 +1,308 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<a-button type="dashed" icon="plus" style="width: 100%; margin-bottom: 15px" @click="openCreateModal">
{{ $t('label.create.project.role') }}
</a-button>
<a-row :gutter="12">
<a-col :md="24" :lg="24">
<a-table
size="small"
:loading="loading"
:columns="columns"
:dataSource="dataSource"
:rowKey="(record,idx) => record.projectid + '-' + idx"
:pagination="false">
<template slot="expandedRowRender" slot-scope="record">
<ProjectRolePermissionTab class="table" :resource="resource" :role="record"/>
</template>
<template slot="name" slot-scope="record"> {{ record }} </template>
<template slot="description" slot-scope="record">
{{ record }}
</template>
<span slot="action" slot-scope="text, record">
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.update.project.role') }}
</template>
<a-button
type="default"
shape="circle"
icon="edit"
size="small"
style="margin:10px"
@click="openUpdateModal(record)" />
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.remove.project.role') }}
</template>
<a-button
type="danger"
shape="circle"
icon="delete"
size="small"
@click="deleteProjectRole(record)"/>
</a-tooltip>
</span>
</a-table>
<a-modal title="Edit Project Role" v-model="editModalVisible" :footer="null" :afterClose="closeAction">
<a-form
:form="form"
@submit="updateProjectRole"
layout="vertical">
<a-form-item :label="$t('label.name')">
<a-input v-decorator="[ 'name' ]"></a-input>
</a-form-item>
<a-form-item :label="$t('label.description')">
<a-input v-decorator="[ 'description' ]"></a-input>
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
<a-button type="primary" @click="updateProjectRole" :loading="loading">{{ $t('label.ok') }}</a-button>
</div>
<span slot="action" slot-scope="text, record">
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.update.project.role') }}
</template>
<a-button
type="default"
shape="circle"
icon="edit"
size="small"
style="margin:10px"
@click="openUpdateModal(record)" />
</a-tooltip>
<a-tooltip placement="top">
<template slot="title">
{{ $t('label.remove.project.role') }}
</template>
<a-button
type="danger"
shape="circle"
icon="delete"
size="small"
@click="deleteProjectRole(record)"/>
</a-tooltip>
</span>
</a-form>
</a-modal>
<a-modal title="Create Project Role" v-model="createModalVisible" :footer="null" :afterClose="closeAction">
<a-form
:form="form"
@submit="createProjectRole"
layout="vertical">
<a-form-item :label="$t('label.name')">
<a-input v-decorator="[ 'name', { rules: [{ required: true, message: 'Please provide input' }] }]"></a-input>
</a-form-item>
<a-form-item :label="$t('label.description')">
<a-input v-decorator="[ 'description' ]"></a-input>
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
<a-button type="primary" @click="createProjectRole" :loading="loading">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</a-modal>
</a-col>
</a-row>
</div>
</template>
<script>
import { api } from '@/api'
import ProjectRolePermissionTab from '@/views/project/iam/ProjectRolePermissionTab'
export default {
name: 'ProjectRoleTab',
props: {
resource: {
type: Object,
required: true
}
},
components: {
ProjectRolePermissionTab
},
data () {
return {
columns: [],
dataSource: [],
loading: false,
createModalVisible: false,
editModalVisible: false,
selectedRole: null,
projectPermisssions: [],
customStyle: 'margin-bottom: -10px; border-bottom-style: none'
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
created () {
this.columns = [
{
title: this.$t('label.name'),
dataIndex: 'name',
width: '35%',
scopedSlots: { customRender: 'name' }
},
{
title: this.$t('label.description'),
dataIndex: 'description'
},
{
title: this.$t('label.action'),
dataIndex: 'action',
width: 100,
scopedSlots: { customRender: 'action' }
}
]
},
mounted () {
this.fetchData()
},
watch: {
resource (newItem, oldItem) {
if (!newItem || !newItem.id) {
return
}
this.resource = newItem
this.fetchData()
}
},
methods: {
fetchData () {
this.loading = true
api('listProjectRoles', { projectid: this.resource.id }).then(json => {
const projectRoles = json.listprojectrolesresponse.projectrole
if (!projectRoles || projectRoles.length === 0) {
this.dataSource = []
return
}
this.dataSource = projectRoles
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
},
openUpdateModal (role) {
this.selectedRole = role
this.editModalVisible = true
},
openCreateModal () {
this.createModalVisible = true
},
updateProjectRole (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
var params = {}
this.loading = true
params.projectid = this.resource.id
params.id = this.selectedRole.id
for (const key in values) {
const input = values[key]
if (input === undefined) {
continue
}
params[key] = input
}
api('updateProjectRole', params).then(response => {
this.$notification.success({
message: this.$t('label.update.project.role'),
description: this.$t('label.update.project.role')
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
this.fetchData()
this.closeAction()
})
})
},
closeAction () {
if (this.editModalVisible) {
this.editModalVisible = false
}
if (this.createModalVisible) {
this.createModalVisible = false
}
},
createProjectRole (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
this.loading = true
var params = {}
params.projectid = this.resource.id
for (const key in values) {
const input = values[key]
if (input === undefined) {
continue
}
params[key] = input
}
api('createProjectRole', params).then(response => {
this.$notification.success({
message: this.$t('label.create.project.role'),
description: this.$t('label.create.project.role')
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
this.fetchData()
this.closeAction()
})
})
},
deleteProjectRole (role) {
this.loading = true
api('deleteProjectRole', {
projectid: this.resource.id,
id: role.id
}).then(response => {
this.$notification.success({
message: this.$t('label.delete.project.role'),
description: this.$t('label.delete.project.role')
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
this.fetchData()
this.closeAction()
})
}
}
}
</script>
<style lang="scss" scoped>
.action-button {
text-align: right;
button {
margin-right: 5px;
}
}
</style>