This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git
The following commit(s) were added to refs/heads/master by this push:
new f748697 projects: Enabling Role based Users in Projects (#382)
f748697 is described below
commit f748697b6c76427aedfbb55104f2c32e72738fba
Author: Pearl Dsilva <[email protected]>
AuthorDate: Tue Aug 4 14:24:51 2020 +0530
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 <[email protected]>
---
src/components/header/ProjectMenu.vue | 1 +
src/components/view/DetailsTab.vue | 14 +
src/components/view/InfoCard.vue | 15 +
src/components/view/ListView.vue | 11 +
src/components/view/ResourceView.vue | 3 +-
src/config/section/project.js | 44 +-
src/locales/en.json | 22 +
src/store/modules/user.js | 27 +-
src/views/AutogenView.vue | 17 +-
src/views/project/AccountsTab.vue | 216 +++++++---
src/views/project/AddAccountOrUserToProject.vue | 332 ++++++++++++++++
src/views/project/InvitationsTemplate.vue | 23 +-
src/views/project/ProjectDetailsTab.vue | 57 +++
src/views/project/iam/ProjectRolePermissionTab.vue | 442 +++++++++++++++++++++
src/views/project/iam/ProjectRoleTab.vue | 308 ++++++++++++++
15 files changed, 1459 insertions(+), 73 deletions(-)
diff --git a/src/components/header/ProjectMenu.vue
b/src/components/header/ProjectMenu.vue
index f55f963..11d72dd 100644
--- a/src/components/header/ProjectMenu.vue
+++ b/src/components/header/ProjectMenu.vue
@@ -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}"`)
diff --git a/src/components/view/DetailsTab.vue
b/src/components/view/DetailsTab.vue
index 12f623d..bdb63d9 100644
--- a/src/components/view/DetailsTab.vue
+++ b/src/components/view/DetailsTab.vue
@@ -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()
}
}
}
diff --git a/src/components/view/InfoCard.vue b/src/components/view/InfoCard.vue
index 8e9a020..175189e 100644
--- a/src/components/view/InfoCard.vue
+++ b/src/components/view/InfoCard.vue
@@ -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">
diff --git a/src/components/view/ListView.vue b/src/components/view/ListView.vue
index 87aae72..5ab0cf6 100644
--- a/src/components/view/ListView.vue
+++ b/src/components/view/ListView.vue
@@ -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'"
diff --git a/src/components/view/ResourceView.vue
b/src/components/view/ResourceView.vue
index c7035a9..582dfc4 100644
--- a/src/components/view/ResourceView.vue
+++ b/src/components/view/ResourceView.vue
@@ -88,7 +88,8 @@ export default {
data () {
return {
activeTab: '',
- networkService: null
+ networkService: null,
+ projectAccount: null
}
},
watch: {
diff --git a/src/config/section/project.js b/src/config/section/project.js
index 5769923..d3ad858 100644
--- a/src/config/section/project.js
+++ b/src/config/section/project.js
@@ -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
+ }
}
]
}
diff --git a/src/locales/en.json b/src/locales/en.json
index 4d92e47..d3078e5 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -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 [...]
"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",
diff --git a/src/store/modules/user.js b/src/store/modules/user.js
index c39e2f4..76271ff 100644
--- a/src/store/modules/user.js
+++ b/src/store/modules/user.js
@@ -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)
+ })
+ })
}
}
}
diff --git a/src/views/AutogenView.vue b/src/views/AutogenView.vue
index c60706c..9277f54 100644
--- a/src/views/AutogenView.vue
+++ b/src/views/AutogenView.vue
@@ -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
diff --git a/src/views/project/AccountsTab.vue
b/src/views/project/AccountsTab.vue
index 53b28d9..8c986c5 100644
--- a/src/views/project/AccountsTab.vue
+++ b/src/views/project/AccountsTab.vue
@@ -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,55 +176,139 @@ export default {
params.projectId = this.resource.id
params.page = this.page
params.pageSize = this.pageSize
-
- this.loading = true
-
+ this.updateProjectApi = this.$store.getters.apis.updateProject
+ this.fetchUsers()
+ this.fetchProjectAccounts(params)
+ if (this.isProjectRolesSupported()) {
+ this.fetchProjectRoles()
+ }
+ },
+ changePage (page, pageSize) {
+ this.page = page
+ this.pageSize = pageSize
+ this.fetchData()
+ },
+ changePageSize (currentPage, pageSize) {
+ 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 = false
+ this.loading.projectAccount = false
})
},
- changePage (page, pageSize) {
- this.page = page
- this.pageSize = pageSize
- this.fetchData()
- },
- changePageSize (currentPage, pageSize) {
- this.page = currentPage
- this.pageSize = pageSize
- this.fetchData()
- },
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)
diff --git a/src/views/project/AddAccountOrUserToProject.vue
b/src/views/project/AddAccountOrUserToProject.vue
new file mode 100644
index 0000000..0275ed7
--- /dev/null
+++ b/src/views/project/AddAccountOrUserToProject.vue
@@ -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>
diff --git a/src/views/project/InvitationsTemplate.vue
b/src/views/project/InvitationsTemplate.vue
index 3b95a84..719f4a0 100644
--- a/src/views/project/InvitationsTemplate.vue
+++ b/src/views/project/InvitationsTemplate.vue
@@ -116,6 +116,11 @@ export default {
scopedSlots: { customRender: 'project' }
},
{
+ title: this.$t('label.account'),
+ dataIndex: 'account',
+ scopedSlots: { customRender: 'account' }
+ },
+ {
title: this.$t('label.domain'),
dataIndex: 'domain',
scopedSlots: { customRender: '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
diff --git a/src/views/project/ProjectDetailsTab.vue
b/src/views/project/ProjectDetailsTab.vue
new file mode 100644
index 0000000..ebffb59
--- /dev/null
+++ b/src/views/project/ProjectDetailsTab.vue
@@ -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>
diff --git a/src/views/project/iam/ProjectRolePermissionTab.vue
b/src/views/project/iam/ProjectRolePermissionTab.vue
new file mode 100644
index 0000000..b986108
--- /dev/null
+++ b/src/views/project/iam/ProjectRolePermissionTab.vue
@@ -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>
diff --git a/src/views/project/iam/ProjectRoleTab.vue
b/src/views/project/iam/ProjectRoleTab.vue
new file mode 100644
index 0000000..75a3124
--- /dev/null
+++ b/src/views/project/iam/ProjectRoleTab.vue
@@ -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>