feat: support user's default project

1. support set user's default project
2. display user's default project in the user list page/user detail page
3. support remove user's default project
4. update user e2e to adapt default project column

Change-Id: I4664429c0c01d195bed3701087e92776b068c69d
This commit is contained in:
zhangjingwei 2023-11-16 14:39:29 +08:00
parent 892b511562
commit 968e2cf577
16 changed files with 294 additions and 11 deletions

View File

@ -0,0 +1,10 @@
---
features:
- |
Support user's default project
1. Support set user's default project
2. Display user's default project in the user list page/user detail page
3. Support remove user's default project

View File

@ -5,4 +5,4 @@ features:
1. Add upgrade cluster action
2. Show quota info when resize cluster
2. Show quota info when resize cluster

View File

@ -149,6 +149,7 @@
"Applying": "Applying",
"Arch": "Arch",
"Architecture": "Architecture",
"Are you sure set the project { project } as the default project? User login is automatically logged into the default project.": "Are you sure set the project { project } as the default project? User login is automatically logged into the default project.",
"Are you sure to cancel transfer volume { name }? ": "Are you sure to cancel transfer volume { name }? ",
"Are you sure to delete instance { name }? ": "Are you sure to delete instance { name }? ",
"Are you sure to delete volume { name }? ": "Are you sure to delete volume { name }? ",
@ -157,6 +158,7 @@
"Are you sure to forbidden project { name }? Forbidden the project will have negative effect, and users associated with the project will not be able to log in if they are only assigned to the project": "Are you sure to forbidden project { name }? Forbidden the project will have negative effect, and users associated with the project will not be able to log in if they are only assigned to the project",
"Are you sure to forbidden user { name }? Forbidden the user will not allow login in ": "Are you sure to forbidden user { name }? Forbidden the user will not allow login in ",
"Are you sure to jump directly to the console? The console will open in a new page later.": "Are you sure to jump directly to the console? The console will open in a new page later.",
"Are you sure to remove the default project?": "Are you sure to remove the default project?",
"Are you sure to shelve instance { name }? ": "Are you sure to shelve instance { name }? ",
"Are you sure to { action } {name}?": "Are you sure to { action } {name}?",
"Are you sure to {action} (Host: {name})?": "Are you sure to {action} (Host: {name})?",
@ -663,6 +665,8 @@
"Debian": "Debian",
"Dedicated": "Dedicated",
"Default Policy": "Default Policy",
"Default Project": "Default Project",
"Default Project ID/Name": "Default Project ID/Name",
"Default is slaac, for details, see https://docs.openstack.org/neutron/latest/admin/config-ipv6.html": "Default is slaac, for details, see https://docs.openstack.org/neutron/latest/admin/config-ipv6.html",
"Defaults": "Defaults",
"Defines the admin state of the health monitor.": "Defines the admin state of the health monitor.",
@ -2042,8 +2046,10 @@
"Remote Security Group": "Remote Security Group",
"Remote Type": "Remote Type",
"Remove": "Remove",
"Remove Default Project": "Remove Default Project",
"Remove Network": "Remove Network",
"Remove Router": "Remove Router",
"Remove default project for user": "Remove default project for user",
"Rename": "Rename",
"Rename is to copy the current file to the new file address and delete the current file, which will affect the creation time of the file.": "Rename is to copy the current file to the new file address and delete the current file, which will affect the creation time of the file.",
"Replication Change": "Replication Change",
@ -2203,8 +2209,10 @@
"Set": "Set",
"Set Admin Password": "Set Admin Password",
"Set Boot Device": "Set Boot Device",
"Set Default Project": "Set Default Project",
"Set Domain Name PTR": "Set Domain Name PTR",
"Set IP": "Set IP",
"Set default project for user": "Set default project for user",
"Seychelles": "Seychelles",
"Share": "Share",
"Share Capacity (GiB)": "Share Capacity (GiB)",

View File

@ -149,6 +149,7 @@
"Applying": "적용중",
"Arch": "아키텍처",
"Architecture": "아키텍처",
"Are you sure set the project { project } as the default project? User login is automatically logged into the default project.": "{ project } 프로젝트를 기본 프로젝트로 설정하시겠습니까? 사용자 로그인은 기본 프로젝트에 자동으로 로그인됩니다.",
"Are you sure to cancel transfer volume { name }? ": "볼륨 { name } 전송을 취소합니까?",
"Are you sure to delete instance { name }? ": "인스턴스 { name }를 삭제 하시겠습니까?",
"Are you sure to delete volume { name }? ": "볼륨 { name }을 삭제 하시겠습니까?",
@ -157,6 +158,7 @@
"Are you sure to forbidden project { name }? Forbidden the project will have negative effect, and users associated with the project will not be able to log in if they are only assigned to the project": "{ name } 프로젝트를 차단하시겠습니까? 프로젝트를 차단하면 부정적인 영향을 줄 수 있으며 프로젝트에 할당된 사용자라면 로그인 할 수 없게 됩니다.",
"Are you sure to forbidden user { name }? Forbidden the user will not allow login in ": "{ name } 사용자를 차단하시겠습니까? 사용자를 차단하면 로그인이 허용되지 않습니다.",
"Are you sure to jump directly to the console? The console will open in a new page later.": "콘솔로 이동하시겠습니까? 콘솔은 새 창에서 열리게됩니다.",
"Are you sure to remove the default project?": "기본 프로젝트를 제거하시겠습니까?",
"Are you sure to shelve instance { name }? ": "{ name } 인스턴스를 보관하시겠습니까?",
"Are you sure to { action } {name}?": "{ action } { name}을(를) 진행하시겠습니까?",
"Are you sure to {action} (Host: {name})?": "",
@ -663,6 +665,8 @@
"Debian": "",
"Dedicated": "",
"Default Policy": "",
"Default Project": "기본 프로젝트",
"Default Project ID/Name": "기본 프로젝트 ID/이름",
"Default is slaac, for details, see https://docs.openstack.org/neutron/latest/admin/config-ipv6.html": "기본값은 slaac입니다. 자세한 내용은 다음을 참조하세요. https://docs.openstack.org/neutron/latest/admin/config-ipv6.html",
"Defaults": "",
"Defines the admin state of the health monitor.": "Health monitor의 관리 상태를 정의합니다.",
@ -2042,8 +2046,10 @@
"Remote Security Group": "원격 보안 그룹",
"Remote Type": "원격 유형",
"Remove": "제거",
"Remove Default Project": "기본 프로젝트 제거",
"Remove Network": "네트워크 제거",
"Remove Router": "라우터 제거",
"Remove default project for user": "사용자의 기본 프로젝트 제거",
"Rename": "이름 변경",
"Rename is to copy the current file to the new file address and delete the current file, which will affect the creation time of the file.": "이름 변경은 현재 파일을 새 파일 주소로 복사한 후 현재 파일을 삭제하는 것을 의미하며, 파일의 생성 시간에 영향을 줍니다.",
"Replication Change": "복제 변경",
@ -2203,8 +2209,10 @@
"Set": "",
"Set Admin Password": "",
"Set Boot Device": "",
"Set Default Project": "기본 프로젝트 설정",
"Set Domain Name PTR": "",
"Set IP": "",
"Set default project for user": "사용자의 기본 프로젝트 설정",
"Seychelles": "",
"Share": "",
"Share Capacity (GiB)": "",

View File

@ -149,6 +149,7 @@
"Applying": "申请中",
"Arch": "",
"Architecture": "架构",
"Are you sure set the project { project } as the default project? User login is automatically logged into the default project.": "确认设置项目 { project } 为默认项目吗?用户登录会自动登录到默认项目。",
"Are you sure to cancel transfer volume { name }? ": "确认要取消{ name }云硬盘转让?",
"Are you sure to delete instance { name }? ": "确认要删除云主机{ name } ",
"Are you sure to delete volume { name }? ": "确认要删除云硬盘{ name } ",
@ -157,6 +158,7 @@
"Are you sure to forbidden project { name }? Forbidden the project will have negative effect, and users associated with the project will not be able to log in if they are only assigned to the project": "确认要禁用项目{name}?禁用项目后将会产生负面影响,项目关联的用户如果只分配给该项目,将无法登陆",
"Are you sure to forbidden user { name }? Forbidden the user will not allow login in ": "确定禁用用户{name}? 禁用后用户将无法登陆",
"Are you sure to jump directly to the console? The console will open in a new page later.": "您确定要直接跳转到控制台吗?控制台稍后会在新页面中打开。",
"Are you sure to remove the default project?": "确认移除默认项目吗?",
"Are you sure to shelve instance { name }? ": "确认要归档云主机{ name }",
"Are you sure to { action } {name}?": "确认{ action }{name}",
"Are you sure to {action} (Host: {name})?": "确认{action} (主机: {name})?",
@ -245,7 +247,7 @@
"Base Info": "基本信息",
"Basic Parameters": "基本参数",
"Batch Allocate": "批量申请",
"Before deleting the project, it is recommended to clean up the resources under the project.": "删除项目前,建议先清理项目下的资源!",
"Before deleting the project, it is recommended to clean up the resources under the project.": "删除项目前,建议先清理项目下的资源",
"Belarus": "白俄罗斯",
"Belgium": "比利时",
"Belize": "伯利兹城",
@ -663,6 +665,8 @@
"Debian": "",
"Dedicated": "专用",
"Default Policy": "默认策略",
"Default Project": "默认项目",
"Default Project ID/Name": "默认项目ID/名称",
"Default is slaac, for details, see https://docs.openstack.org/neutron/latest/admin/config-ipv6.html": "默认使用slaac模式详细配置方式请查看 https://docs.openstack.org/neutron/latest/admin/config-ipv6.html",
"Defaults": "默认",
"Defines the admin state of the health monitor.": "定义运行状况监视器的管理状态。",
@ -2042,8 +2046,10 @@
"Remote Security Group": "远端安全组",
"Remote Type": "远端方式",
"Remove": "移除",
"Remove Default Project": "移除默认项目",
"Remove Network": "移除网络",
"Remove Router": "移除路由器",
"Remove default project for user": "移除用户默认项目",
"Rename": "重命名",
"Rename is to copy the current file to the new file address and delete the current file, which will affect the creation time of the file.": "重命名是把当前文件复制到新文件地址,并删除当前文件,会影响文件的创建时间。",
"Replication Change": "复制更改中",
@ -2203,8 +2209,10 @@
"Set": "设置",
"Set Admin Password": "设置管理员密码",
"Set Boot Device": "设置引导设备",
"Set Default Project": "设置默认项目",
"Set Domain Name PTR": "设置域名 PTR",
"Set IP": "设置IP",
"Set default project for user": "设置用户默认项目",
"Seychelles": "塞舌尔",
"Share": "共享",
"Share Capacity (GiB)": "共享容量 (GiB)",

View File

@ -0,0 +1,56 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import globalUserStore from 'stores/keystone/user';
export class RemoveDefaultProject extends ModalAction {
static id = 'remove-default-project';
static title = t('Remove Default Project');
static policy = 'identity:update_user';
get name() {
return t('Remove default project for user');
}
static policy = 'identity:update_user';
static allowed = (item, containerProps) => {
const { detail } = containerProps || {};
const { default_project_id } = detail;
return Promise.resolve(!!default_project_id);
};
get formItems() {
return [
{
name: 'name',
type: 'label',
content: t('Are you sure to remove the default project?'),
},
];
}
onSubmit = (value, containerProps) => {
const {
detail: { id },
} = containerProps;
return globalUserStore.setDefaultProject(id, null);
};
}
export default inject('rootStore')(observer(RemoveDefaultProject));

View File

@ -0,0 +1,67 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import globalUserStore from 'stores/keystone/user';
export class SetDefaultProject extends ModalAction {
static id = 'set-default-project';
static title = t('Set Default Project');
static policy = 'identity:update_user';
get name() {
return t('Set default project for user');
}
static policy = 'identity:update_user';
static allowed = (item, containerProps) => {
const { detail } = containerProps || {};
const { default_project_id } = detail;
return Promise.resolve(default_project_id !== item.id);
};
get formItems() {
return [
{
name: 'name',
type: 'label',
content: t(
'Are you sure set the project { project } as the default project? User login is automatically logged into the default project.',
{ project: this.item.name }
),
wrapperCol: {
xs: {
span: 24,
},
sm: {
span: 24,
},
},
},
];
}
onSubmit = (value, containerProps) => {
const {
detail: { id },
} = containerProps;
return globalUserStore.setDefaultProject(id, this.item.id);
};
}
export default inject('rootStore')(observer(SetDefaultProject));

View File

@ -21,6 +21,8 @@ import ManageUser from './ManageUser';
import ManageUserGroup from './ManageUserGroup';
import ManageQuota from './ManageQuota';
import ModifyTags from './ModifyTags';
import SetDefaultProject from './SetDefaultProject';
import RemoveDefaultProject from './RemoveDefaultProject';
const actionConfigs = {
rowActions: {
@ -53,4 +55,16 @@ const actionConfigs = {
primaryActions: [Create],
};
export const actionConfigsInUserDetail = {
rowActions: {
firstAction: null,
moreActions: [
{
action: SetDefaultProject,
},
],
},
primaryActions: [RemoveDefaultProject],
};
export default actionConfigs;

View File

@ -20,7 +20,7 @@ import globalProjectStore, { ProjectStore } from 'stores/keystone/project';
import { yesNoOptions, emptyActionConfig } from 'utils/constants';
import { SimpleTag } from 'resources/nova/instance';
import { enabledColumn } from 'resources/keystone/domain';
import actionConfigs from './actions';
import actionConfigs, { actionConfigsInUserDetail } from './actions';
import styles from './index.less';
export class Projects extends Base {
@ -56,6 +56,10 @@ export class Projects extends Base {
return this.inDetailPage && this.path.includes('domain-admin/detail');
}
get forceRefreshTopDetailWhenListRefresh() {
return this.inUserDetail;
}
getUserProjectRole = (record) => {
// return [{role, groups}]
const { users = {}, groups = {} } = record || {};
@ -265,6 +269,9 @@ export class Projects extends Base {
get actionConfigs() {
if (this.inDetailPage) {
if (this.inUserDetail) {
return actionConfigsInUserDetail;
}
return emptyActionConfig;
}
return actionConfigs;

View File

@ -73,6 +73,11 @@ export class UserDetail extends Base {
title: t('Affiliated Domain'),
dataIndex: 'domainName',
},
{
title: t('Default Project'),
dataIndex: 'default_project_id',
render: (value) => this.detailData.defaultProject || value || '-',
},
{
title: t('Email'),
dataIndex: 'email',
@ -93,7 +98,7 @@ export class UserDetail extends Base {
const tabs = [
{
title: t('Subordinate Projects'),
key: 'user',
key: 'project',
component: Project,
},
{

View File

@ -0,0 +1,36 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { inject, observer } from 'mobx-react';
import { FormAction } from 'containers/Action';
export class SetDefaultProject extends FormAction {
static id = 'set-default-project';
static title = t('Set Default Project');
static allowed() {
return Promise.resolve(true);
}
static path = (item) => {
return `/identity/user-admin/detail/${item.id}?tab=project`;
};
get name() {
return t('Set Default Project');
}
}
export default inject('rootStore')(observer(SetDefaultProject));

View File

@ -19,6 +19,7 @@ import Enable from './Enable';
import Forbidden from './Forbidden';
import SystemRole from './SystemRole';
import Password from './Password';
import SetDefaultProject from './SetDefaultProject';
const actionConfigs = {
rowActions: {
@ -33,6 +34,9 @@ const actionConfigs = {
// {
// action: projectActionConfig,
// },
{
action: SetDefaultProject,
},
{
action: Password,
},

View File

@ -61,6 +61,14 @@ export class User extends Base {
dataIndex: 'real_name',
isHideable: true,
},
{
title: t('Default Project ID/Name'),
dataIndex: 'defaultProject',
isHideable: true,
routeName: 'projectDetailAdmin',
isLink: true,
idKey: 'default_project_id',
},
{
title: t('Roles'),
dataIndex: 'projectRoles',
@ -111,9 +119,12 @@ export class User extends Base {
},
},
{
title: t('Affiliated Domain'),
title: t('Affiliated Domain ID/Name'),
dataIndex: 'domainName',
isHideable: true,
routeName: 'domainDetailAdmin',
isLink: true,
idKey: 'domain_id',
},
{
title: t('System Roles'),
@ -201,10 +212,12 @@ export class User extends Base {
newParams.groupId = id;
newParams.withProjectRole = false;
newParams.withSystemRole = true;
newParams.withDefaultProject = true;
} else if (this.inDomainDetail) {
newParams.domain_id = id;
newParams.withProjectRole = false;
newParams.withSystemRole = true;
newParams.withDefaultProject = true;
} else if (this.inProjectDetail) {
newParams.projectId = id;
newParams.withProjectRole = true;
@ -216,6 +229,7 @@ export class User extends Base {
} else if (!this.inDetailPage) {
newParams.withProjectRole = false;
newParams.withSystemRole = true;
newParams.withDefaultProject = true;
}
return newParams;
};

View File

@ -139,6 +139,15 @@ export class UserStore extends Base {
return projects;
}
getUserDefaultProject = (user, projects) => {
const { default_project_id } = user;
if (!default_project_id) {
return;
}
const project = projects.find((p) => p.id === default_project_id);
user.defaultProject = project?.name;
};
getProjectMapRoles = (user, projectRoleAssignments, roles, projects) => {
const projectMapRoles = {};
const { id } = user;
@ -190,6 +199,7 @@ export class UserStore extends Base {
projects,
domains
) => {
this.getUserDefaultProject(user, projects);
const projectMapRoles = this.getProjectMapRoles(
user,
projectRoleAssignments,
@ -219,6 +229,7 @@ export class UserStore extends Base {
}
const {
withProjectRole = true,
withDefaultProject = true,
withSystemRole = true,
projectId,
roleId,
@ -238,7 +249,7 @@ export class UserStore extends Base {
? this.roleAssignmentClient.list({ 'scope.system': 'all' })
: null,
withRole ? this.roleClient.list() : null,
withProjectRole ? this.projectClient.list() : null,
withProjectRole || withDefaultProject ? this.projectClient.list() : null,
domain_id ? null : this.domainClient.list(),
];
const [
@ -282,6 +293,19 @@ export class UserStore extends Base {
return newItems;
}
async fetchUserDefaultProject(user) {
const { default_project_id } = user;
if (!default_project_id) {
return null;
}
try {
const { project } = await this.projectClient.show(default_project_id);
return project;
} catch (e) {
return null;
}
}
async detailDidFetch(item) {
const { id } = item;
const params = { 'user.id': id, 'scope.system': 'all' };
@ -289,15 +313,27 @@ export class UserStore extends Base {
this.roleAssignmentClient.list(params),
this.roleClient.list(),
this.domainClient.list(),
this.fetchUserDefaultProject(item),
];
const [systemRoleAssignmentsResult, roleResult, domainResult] =
await Promise.all(reqs);
const [
systemRoleAssignmentsResult,
roleResult,
domainResult,
defaultProject,
] = await Promise.all(reqs);
const { roles = [] } = roleResult || {};
const { domains = [] } = domainResult;
const { role_assignments: systemAssigns = [] } =
systemRoleAssignmentsResult || {};
return this.updateUser(item, [], systemAssigns, roles, [], domains);
return this.updateUser(
item,
[],
systemAssigns,
roles,
defaultProject ? [defaultProject] : [],
domains
);
}
@action
@ -355,6 +391,16 @@ export class UserStore extends Base {
};
return this.submitting(this.client.patch(id, reqBody));
}
@action
async setDefaultProject(id, defaultProject) {
const reqBody = {
user: {
default_project_id: defaultProject,
},
};
return this.submitting(this.client.patch(id, reqBody));
}
}
const globalUserStore = new UserStore();

View File

@ -62,7 +62,7 @@ describe('The User Page', () => {
.url()
.should('not.include', creatUrl)
.tableSearchText(name)
.waitStatusGreen(7);
.waitStatusGreen(8);
});
it('successfully detail', () => {

View File

@ -222,7 +222,7 @@ Cypress.Commands.add('createUser', ({ name }) => {
.formInput('real_name', name)
.clickFormActionSubmitButton()
.tableSearchText(name)
.waitStatusGreen(7);
.waitStatusGreen(8);
});
Cypress.Commands.add('createProject', ({ name }) => {