Refactor add group component (#15518)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
孙世军 2021-09-02 15:30:43 +08:00 committed by GitHub
parent 6b8c5c9edd
commit 383635e970
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 461 additions and 871 deletions

View File

@ -1,108 +1,43 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="'true'" [clrModalClosable]="false">
<h3 class="modal-title">{{'MEMBER.IMPORT_GROUP' | translate}}</h3>
<clr-modal [(clrModalOpen)]="addGroupOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'GROUP.NEW_MEMBER' | translate}}</h3>
<div class="modal-body">
<label>{{ 'MEMBER.NEW_GROUP_INFO' | translate}}</label>
<div class="modeSelectradios">
<clr-radio-wrapper>
<input clrRadio type="radio" name="modeRadios" [value]="false" id="select_group" [(ngModel)]="createGroupMode">
<label for="select_group">{{'MEMBER.ADD_GROUP_SELECT' | translate}}</label>
</clr-radio-wrapper>
<clr-radio-wrapper>
<input clrRadio type="radio" name="modeRadios" [value]="true" id="create_group" [(ngModel)]="createGroupMode">
<label for="create_group">{{'MEMBER.CREATE_GROUP_SELECT' | translate}}</label>
</clr-radio-wrapper>
</div>
<div *ngIf="createGroupMode">
<form #groupForm="ngForm" class="clr-form clr-form-horizontal">
<div class="clr-form-control">
<label class="required clr-control-label">{{ 'MEMBER.LDAP_SEARCH_DN' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="isDNInvalid">
<div class="clr-input-wrapper">
<input class="clr-input" type="text" name="ldap_group_dn" size="45"
required
[(ngModel)]="group.ldap_group_dn"
#groupDN="ngModel">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<inline-alert class="modal-title"></inline-alert>
<label>{{ 'GROUP.NEW_USER_INFO' | translate}}</label>
<form #groupForm="ngForm" class="clr-form clr-form-horizontal">
<div class="clr-form-control">
<label class="required clr-control-label">{{'GROUP.GROUP' | translate}} {{'GROUP.NAME' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="(groupName.invalid && (groupName.touched||groupName.dirty)) || !isGroupNameValid">
<div class="clr-input-wrapper" (mouseleave)="leaveInput()">
<input class="clr-input" type="text" id="group-name" [(ngModel)]="memberGroup.group_name"
name="groupName"
#groupName="ngModel"
required autocomplete="off" (keyup)="input()">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
<div class="selectBox" [style.display]="searchedGroups.length ? 'block' : 'none'" >
<ul>
<li *ngFor="let group of searchedGroups" (click)="selectGroup(group.group_name)">{{group.group_name}}</li>
</ul>
</div>
<clr-control-error *ngIf="isDNInvalid" class="tooltip-content">
{{dnTooltip | translate}}
</clr-control-error>
</div>
<clr-control-error *ngIf="(groupName.invalid && (groupName.touched||groupName.dirty)) || !isGroupNameValid" class="tooltip-content">
{{ groupTooltip | translate }}
</clr-control-error>
</div>
</div>
<div class="clr-form-control">
<label class="clr-control-label">{{'MEMBER.LDAP_SEARCH_NAME' | translate}}</label>
<label class="clr-control-label">{{'GROUP.ROLE' | translate}}</label>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input class="clr-input" type="text" name="ldap_group_name" size="35" [(ngModel)]="group.group_name">
</div>
<clr-radio-wrapper *ngFor="let projectRoot of projectRoots">
<input clrRadio type="radio" name="member_role" id="{{'check_root_project_' + projectRoot.NAME}}" [value]="projectRoot.VALUE" [(ngModel)]="roleId">
<label for="{{'check_root_project_' + projectRoot.NAME}}">{{ projectRoot.LABEL | translate}}</label>
</clr-radio-wrapper>
</div>
</div>
<div class="clr-form-control">
<label class="clr-control-label" for="member_role1">{{ 'MEMBER.ROLE' | translate}}</label>
<div class="clr-select-wrapper">
<select class="clr-select" id="member_role1" name="member_role" [(ngModel)]="selectedRole">
<option *ngFor="let role of roles" [ngValue]="role.id"> {{role.value | translate}}</option>
</select>
</div>
</div>
</form>
</div>
<div *ngIf="!createGroupMode">
<div>
<div class="row flex-items-xs-between">
<div class="left">
</div>
<div class="right">
<hbr-filter [withDivider]="true" filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filterEvt)="doFilter($event)"
[currentValue]="currentTerm"></hbr-filter>
<span class="refresh-btn" (click)="loadGroups()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-1 col-md-1 col-sm-1 col-xs-1">
<label>{{'MEMBER.LDAP_GROUP' | translate}}</label>
</div>
<div class="class=col-lg-11 col-md-11 col-sm-11 col-xs-11">
<clr-datagrid class="datagrid-compact" [(clrDgSelected)]="selectedGroups" [clrDgLoading]="onLoading">
<clr-dg-column [clrDgField]="'group_name'">{{'MEMBER.LDAP_SEARCH_NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'ldap_group_dn'">{{'MEMBER.LDAP_SEARCH_DN' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'property'">{{'MEMBER.LDAP_PROPERTY' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let group of groups" [clrDgItem]="group">
<clr-dg-cell>{{group.group_name}}</clr-dg-cell>
<clr-dg-cell>{{group.ldap_group_dn}}</clr-dg-cell>
<clr-dg-cell>{{group.property}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="5" [clrDgTotalItems]="totalCount">
<span *ngIf="groups?.length">
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'MEMBER.OF' | translate}}
</span>
{{groups?.length}} {{'MEMBER.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<div class="clr-form clr-form-horizontal">
<div class="clr-form-control">
<label class="clr-control-label" for="member_role1">{{ 'MEMBER.ROLE' | translate}}</label>
<div class="clr-select-wrapper">
<select class="clr-select" id="member_role2" [(ngModel)]="selectedRole">
<option *ngFor="let role of roles" [ngValue]="role.id"> {{role.value | translate}}</option>
</select>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid" (click)="onSave()">{{'BUTTON.SAVE' | translate}}</button>
<button [clrLoading]="btnStatus" type="button" class="btn btn-primary" [disabled]="!isValid()" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -1,23 +1,27 @@
clr-datagrid {
::ng-deep .datagrid {
margin-top: 0;
}
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}
.row {
margin-top: 12px;
.selectBox{
position: absolute;
width: 173px;
height: auto;
background-color: white;
border: 1px solid rgba(0,0,0,.15);
border-right-width: 2px;
border-bottom-width: 2px;
border-radius: 6px;
box-shadow: 0 5px 10px rgba(0,0,0,.2);
z-index: 100;
}
.flex-items-xs-between{
display: flex;
.selectBox ul li{
list-style: none;
padding: 3px 20px;
cursor: pointer;
}
.modeSelectradios {
margin-top: 21px;
.selectBox ul li:hover{
color: #262626;
background-image: linear-gradient(180deg,#f5f5f5 0,#e8e8e8);
background-repeat: repeat-x;
}
.left {
flex: 1;
}
.right {
position: relative;
z-index: 100;
right: 15px;
}

View File

@ -1,41 +1,41 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ClarityModule } from '@clr/angular';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { FormsModule } from '@angular/forms';
import { of } from "rxjs";
import { AppConfigService } from "../../../../services/app-config.service";
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { SharedTestingModule } from "../../../../shared/shared.module";
import { AddGroupComponent } from './add-group.component';
import { GroupService } from "../../../left-side-nav/group/group.service";
import { MemberService } from "../member.service";
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { OperationService } from "../../../../shared/components/operation/operation.service";
import { MemberService } from 'ng-swagger-gen/services/member.service';
describe('AddGroupComponent', () => {
describe('AddHttpAuthGroupComponent', () => {
let component: AddGroupComponent;
let fixture: ComponentFixture<AddGroupComponent>;
let fakeMessageHandlerService = null;
let fakeOperationService = null;
let fakeGroupService = null;
let fakeMemberService = null;
let fakeAppConfigService = {
isLdapMode: function () {
return true;
}
};
let fakeMemberService = {listProjectMembers: function() {
return of(null);
}};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AddGroupComponent],
imports: [
ClarityModule,
FormsModule,
TranslateModule.forRoot()
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
imports: [
SharedTestingModule
],
providers: [
TranslateService,
{ provide: MessageHandlerService, useValue: fakeMessageHandlerService },
{ provide: OperationService, useValue: fakeOperationService },
{ provide: GroupService, useValue: fakeGroupService },
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: MemberService, useValue: fakeMemberService }
]
}).compileComponents();
],
})
.compileComponents();
}));
beforeEach(() => {

View File

@ -1,170 +1,233 @@
import { forkJoin, of as observableOf, throwError as observableThrowError } from "rxjs";
import { catchError, mergeMap } from 'rxjs/operators';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 { debounceTime, finalize, switchMap } from 'rxjs/operators';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { UserGroup } from "../../../left-side-nav/group/group";
import { MemberService } from "../member.service";
import { GroupService } from "../../../left-side-nav/group/group.service";
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { OperationService } from "../../../../shared/components/operation/operation.service";
import { operateChanges, OperateInfo, OperationState } from "../../../../shared/components/operation/operate";
import { ProjectRoles } from "../../../../shared/entities/shared.const";
import { errorHandler } from "../../../../shared/units/shared.utils";
import { ProjectMemberEntity } from "../../../../../../ng-swagger-gen/models/project-member-entity";
import { AppConfigService } from "../../../../services/app-config.service";
import { ProjectRootInterface } from "../../../../shared/services";
import { GroupType, PROJECT_ROOTS } from "../../../../shared/entities/shared.const";
import { InlineAlertComponent } from "../../../../shared/components/inline-alert/inline-alert.component";
import { UsergroupService } from "../../../../../../ng-swagger-gen/services/usergroup.service";
import { of, Subject, Subscription } from "rxjs";
import { UserGroup } from 'ng-swagger-gen/models/user-group';
import { ClrLoadingState } from "@clr/angular";
import { MemberService } from 'ng-swagger-gen/services/member.service';
import { MessageHandlerService } from "../../../../shared/services/message-handler.service";
@Component({
selector: "add-group",
templateUrl: "./add-group.component.html",
styleUrls: ["./add-group.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
selector: 'add-group',
templateUrl: './add-group.component.html',
styleUrls: ['./add-group.component.scss'],
})
export class AddGroupComponent implements OnInit {
opened = false;
createGroupMode = false;
onLoading = false;
roles = ProjectRoles;
currentTerm = '';
selectedRole = 1;
group = new UserGroup(1);
selectedGroups: UserGroup[] = [];
groups: UserGroup[] = [];
totalCount = 0;
export class AddGroupComponent implements OnInit, OnDestroy {
projectRoots: ProjectRootInterface[] = PROJECT_ROOTS;
memberGroup: UserGroup = {
group_name: ''
};
roleId: number = 1; // default value is 1(project admin);
addGroupOpened: boolean = false;
staticBackdrop: boolean = true;
closable: boolean = false;
dnTooltip = 'TOOLTIP.ITEM_REQUIRED';
@ViewChild('groupForm', {static: true})
currentForm: NgForm;
@ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent;
@Input() projectId: number;
@Input() memberList: ProjectMemberEntity[] = [];
@Output() added = new EventEmitter<boolean>();
@ViewChild('groupForm')
groupForm: NgForm;
checkOnGoing: boolean = false;
searchedGroups: UserGroup[] = [];
groupChecker: Subject<string> = new Subject<string>();
groupSearcher: Subject<string> = new Subject<string>();
groupCheckerSub: Subscription;
groupSearcherSub: Subscription;
btnStatus: ClrLoadingState = ClrLoadingState.DEFAULT;
isGroupNameValid: boolean = true;
groupTooltip: string = 'MEMBER.GROUP_NAME_REQUIRED';
isNameChecked: boolean = false; // this is only for LDAP mode
constructor(private memberService: MemberService,
private appConfigService: AppConfigService, private messageHandlerService: MessageHandlerService,
private userGroupService: UsergroupService) { }
constructor(
private translateService: TranslateService,
private msgHandler: MessageHandlerService,
private operationService: OperationService,
private ref: ChangeDetectorRef,
private groupService: GroupService,
private memberService: MemberService
) {}
ngOnInit() { }
public get isValid(): boolean {
if (this.createGroupMode) {
return this.groupForm && this.groupForm.valid;
} else {
return this.selectedGroups.length > 0;
}
}
public get isDNInvalid(): boolean {
if (!this.groupForm) {return false; }
let dnControl = this.groupForm.controls['ldap_group_dn'];
return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched);
}
loadGroups() {
this.onLoading = true;
this.groupService.getUserGroups().subscribe(groups => {
this.groups = groups.filter(group => {
if (!group.group_name) {group.group_name = ''; }
return group.group_name.includes(this.currentTerm)
&& !this.memberList.some(member => member.entity_type === 'g' && member.entity_id === group.id);
});
this.totalCount = groups.length;
this.onLoading = false;
this.ref.detectChanges();
});
}
doFilter(name: string) {
this.currentTerm = name;
this.loadGroups();
}
resetModaldata() {
this.createGroupMode = false;
this.group = new UserGroup(1);
this.selectedRole = 1;
this.selectedGroups = [];
this.groups = [];
}
public open() {
this.resetModaldata();
this.loadGroups();
this.opened = true;
this.ref.detectChanges();
}
public close() {
this.resetModaldata();
this.opened = false;
}
onSave() {
if (!this.createGroupMode) {
this.addGroups();
} else {
this.createGroupAsMember();
}
}
onCancel() {
this.opened = false;
}
addGroups() {
let GroupAdders$ = this.selectedGroups.map(group => {
let operMessage = new OperateInfo();
operMessage.name = 'OPERATION.ADD_GROUP';
operMessage.data.id = group.id;
operMessage.state = OperationState.progressing;
operMessage.data.name = group.group_name;
this.operationService.publishInfo(operMessage);
return this.memberService
.addGroupMember(this.projectId, group, this.selectedRole).pipe(
mergeMap(response => {
return this.translateService.get("BATCH.DELETED_SUCCESS").pipe(
mergeMap(res => {
operateChanges(operMessage, OperationState.success);
return observableOf(res);
})); }),
catchError(
error => {
const message = errorHandler(error);
this.translateService.get(message).subscribe(res =>
operateChanges(operMessage, OperationState.failure, res)
);
return observableThrowError(error);
}),
catchError(error => observableThrowError(error)), );
});
forkJoin(GroupAdders$)
.subscribe(results => {
if (results.some(code => code < 200 || code > 299)) {
this.added.emit(false);
} else {
this.added.emit(true);
ngOnInit(): void {
if (!this.groupCheckerSub) {
this.groupCheckerSub = this.groupChecker.pipe(
debounceTime(500),
switchMap((name) => {
if (name) {
this.checkOnGoing = true;
const params: MemberService.ListProjectMembersParams = {
projectNameOrId: this.projectId.toString(),
page: 1,
pageSize: 10,
entityname: name
};
return this.memberService.listProjectMembers(params).pipe(finalize(() => this.checkOnGoing = false));
} else {
return of([]);
}
})).subscribe(res => {
if (res && res.length) {
if (res.filter(g => g.entity_name === this.memberGroup.group_name).length > 0) {
this.isGroupNameValid = false;
this.groupTooltip = 'MEMBER.GROUP_ALREADY_ADDED';
}
}
}, error => {
this.msgHandler.handleError(error);
});
this.opened = false;
}
if (!this.groupSearcherSub) {
this.groupSearcherSub = this.groupSearcher.pipe(
debounceTime(500),
switchMap(name => {
if (name) {
return this.userGroupService.searchUserGroups({
page: 1,
pageSize: 10,
groupname: name
});
} else {
return of([]);
}
})
).subscribe(res => {
if (res) {
this.searchedGroups = res;
}
// for LDAP mode, if input group name is not found from search result, then show "Group name does not exists" error
if (this.appConfigService.isLdapMode() && this.memberGroup.group_name) {
let flag = false;
this.searchedGroups.forEach(item => {
if (item.group_name === this.memberGroup.group_name) {
flag = true;
}
});
if (!flag) {
this.isGroupNameValid = false;
this.groupTooltip = 'MEMBER.NON_EXISTENT_GROUP';
} else { // it means input group name is valid
this.isNameChecked = true;
}
}
});
}
}
ngOnDestroy() {
if (this.groupCheckerSub) {
this.groupCheckerSub.unsubscribe();
this.groupCheckerSub = null;
}
if (this.groupSearcherSub) {
this.groupSearcherSub.unsubscribe();
this.groupSearcherSub = null;
}
}
createGroupAsMember() {
let groupCopy = Object.assign({}, this.group);
this.memberService.addGroupMember(this.projectId, groupCopy, this.selectedRole)
.subscribe(
res => this.added.emit(true),
err => {
this.msgHandler.handleError(err);
this.added.emit(false);
this.btnStatus = ClrLoadingState.LOADING;
if (this.appConfigService.isHttpAuthMode()) {
this.memberGroup.group_type = GroupType.HTTP_TYPE;
}
if (this.appConfigService.isLdapMode()) {
this.memberGroup.group_type = GroupType.LDAP_TYPE;
}
if (this.appConfigService.isOidcMode()) {
this.memberGroup.group_type = GroupType.OIDC_TYPE;
}
this.memberService.createProjectMember({
projectNameOrId: this.projectId.toString(),
projectMember: {
role_id: this.roleId,
member_group: this.memberGroup
}
);
this.opened = false;
}).subscribe(
res => {
this.messageHandlerService.showSuccess('MEMBER.ADDED_SUCCESS');
this.btnStatus = ClrLoadingState.SUCCESS;
this.addGroupOpened = false;
this.added.emit(true);
},
err => {
this.btnStatus = ClrLoadingState.ERROR;
this.inlineAlert.showInlineError(err);
this.added.emit(false);
}
);
}
onSubmit(): void {
this.createGroupAsMember();
}
onCancel() {
this.addGroupOpened = false;
}
openAddGroupModal(): void {
this.currentForm.reset({
member_role: 1
});
this.addGroupOpened = true;
this.inlineAlert.close();
this.memberGroup = {
group_name: ''
};
this.isGroupNameValid = true;
this.groupTooltip = "MEMBER.USERNAME_IS_REQUIRED";
this.searchedGroups = [];
}
isValid(): boolean {
if (this.appConfigService.isLdapMode()) {
if (!this.isNameChecked) {
return false;
}
}
return this.isGroupNameValid && this.currentForm &&
this.currentForm.valid &&
!this.checkOnGoing;
}
selectGroup(groupName) {
if (this.appConfigService.isLdapMode()) {
this.isNameChecked = true;
this.isGroupNameValid = true;
}
this.memberGroup.group_name = groupName;
this.groupChecker.next(groupName);
this.searchedGroups = [];
}
leaveInput() {
this.searchedGroups = [];
}
input() {
if (this.appConfigService.isLdapMode()) {
this.isNameChecked = false;
}
this.groupChecker.next(this.memberGroup.group_name);
this.groupSearcher.next(this.memberGroup.group_name);
if (!this.memberGroup.group_name) {
this.searchedGroups = [];
this.isGroupNameValid = false;
this.groupTooltip = 'MEMBER.GROUP_NAME_REQUIRED';
} else {
this.isGroupNameValid = true;
}
}
}

View File

@ -1,37 +0,0 @@
<clr-modal [(clrModalOpen)]="addHttpAuthOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'GROUP.NEW_MEMBER' | translate}}</h3>
<inline-alert class="modal-title padding-0"></inline-alert>
<div class="modal-body">
<label>{{ 'GROUP.NEW_USER_INFO' | translate}}</label>
<form #memberForm="ngForm" class="clr-form clr-form-horizontal">
<div class="clr-form-control">
<label class="required clr-control-label">{{'GROUP.GROUP' | translate}} {{'GROUP.NAME' | translate}}</label>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input class="clr-input" type="text" id="member_name" [(ngModel)]="member_group.group_name"
name="member_name"
size="20"
minlength="3"
#memberName="ngModel"
required autocomplete="off">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
</div>
</div>
</div>
<div class="clr-form-control">
<label class="clr-control-label">{{'GROUP.ROLE' | translate}}</label>
<div class="clr-control-container">
<clr-radio-wrapper *ngFor="let projectRoot of projectRoots">
<input clrRadio type="radio" name="member_role" id="{{'check_root_project_' + projectRoot.NAME}}" [value]="projectRoot.VALUE" [(ngModel)]="role_id">
<label for="{{'check_root_project_' + projectRoot.NAME}}">{{ projectRoot.LABEL | translate}}</label>
</clr-radio-wrapper>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -1,8 +0,0 @@
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}
.padding-0 {
padding: 0;
}

View File

@ -1,50 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { of } from "rxjs";
import { MemberService } from '../member.service';
import { AppConfigService } from "../../../../services/app-config.service";
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { AddHttpAuthGroupComponent } from './add-http-auth-group.component';
import { SharedTestingModule } from "../../../../shared/shared.module";
describe('AddHttpAuthGroupComponent', () => {
let component: AddHttpAuthGroupComponent;
let fixture: ComponentFixture<AddHttpAuthGroupComponent>;
let fakeAppConfigService = {
isHttpAuthMode: function () {
return true;
}
};
let fakeMemberService = {addGroupMember: function() {
return of(null);
}};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AddHttpAuthGroupComponent],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
imports: [
SharedTestingModule
],
providers: [
TranslateService,
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: MemberService, useValue: fakeMemberService }
],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddHttpAuthGroupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,113 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 { finalize } from 'rxjs/operators';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { UserService } from '../../../left-side-nav/user/user.service';
import { MemberService } from '../member.service';
import { UserGroup } from "../../../left-side-nav/group/group";
import { AppConfigService } from "../../../../services/app-config.service";
import { ProjectRootInterface } from "../../../../shared/services";
import { GroupType, PROJECT_ROOTS } from "../../../../shared/entities/shared.const";
import { InlineAlertComponent } from "../../../../shared/components/inline-alert/inline-alert.component";
import { errorHandler } from "../../../../shared/units/shared.utils";
@Component({
selector: 'add-http-auth-group',
templateUrl: './add-http-auth-group.component.html',
styleUrls: ['./add-http-auth-group.component.scss'],
providers: [UserService]
})
export class AddHttpAuthGroupComponent implements OnInit {
projectRoots: ProjectRootInterface[];
member_group: UserGroup = { group_name: '', group_type: 2 };
role_id: number;
addHttpAuthOpened: boolean;
memberForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('memberForm', {static: true})
currentForm: NgForm;
@ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent;
@Input() projectId: number;
@Output() added = new EventEmitter<boolean>();
checkOnGoing: boolean = false;
constructor(private memberService: MemberService,
private appConfigService: AppConfigService,
private translateService: TranslateService) { }
ngOnInit(): void {
this.projectRoots = PROJECT_ROOTS;
this.member_group = new UserGroup(this.appConfigService.isHttpAuthMode() ? GroupType.HTTP_TYPE : GroupType.OIDC_TYPE);
}
createGroupAsMember() {
this.checkOnGoing = true;
this.memberService.addGroupMember(this.projectId, this.member_group, this.role_id)
.pipe(
finalize(() => {
this.checkOnGoing = false;
}
))
.subscribe(
res => {
this.role_id = null;
this.addHttpAuthOpened = false;
this.added.emit(true);
},
err => {
let errorMessageKey: string = errorHandler(err);
this.translateService
.get(errorMessageKey)
.subscribe(errorMessage => this.inlineAlert.showInlineError(errorMessage));
this.added.emit(false);
}
);
}
onSubmit(): void {
this.createGroupAsMember();
}
onCancel() {
this.role_id = null;
this.addHttpAuthOpened = false;
}
openAddMemberModal(): void {
this.currentForm.reset();
this.addHttpAuthOpened = true;
this.role_id = 1;
this.inlineAlert.close();
}
public get isValid(): boolean {
return this.currentForm &&
this.currentForm.valid &&
!this.checkOnGoing;
}
}

View File

@ -1,27 +1,27 @@
<clr-modal [(clrModalOpen)]="addMemberOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3>
<div class="modal-body">
<inline-alert class="modal-title"></inline-alert>
<label>{{ 'MEMBER.NEW_USER_INFO' | translate}}</label>
<form #memberForm="ngForm" class="clr-form clr-form-horizontal">
<div class="clr-form-control">
<label class="required clr-control-label">{{'MEMBER.NAME' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="!isMemberNameValid">
<div class="clr-control-container" [class.clr-error]="(memberName.invalid && (memberName.touched||memberName.dirty)) || !isMemberNameValid">
<div class="clr-input-wrapper" (mouseleave)="leaveInput()">
<input class="clr-input" type="text" id="member_name" [(ngModel)]="member.entity_name"
<input class="clr-input" type="text" id="member_name" [(ngModel)]="member.username"
name="member_name"
size="20"
#memberName="ngModel"
required
(keyup)="handleValidation()" autocomplete="off">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
<div class="selectBox" [style.display]="selectUserName.length ? 'block' : 'none'" >
<div class="selectBox" [style.display]="searchedUserLists.length ? 'block' : 'none'" >
<ul>
<li *ngFor="let name of selectUserName" (click)="selectedName(name)">{{name}}</li>
<li *ngFor="let user of searchedUserLists" (click)="selectedName(user.username)">{{user.username}}</li>
</ul>
</div>
</div>
<clr-control-error *ngIf="!isMemberNameValid" class="tooltip-content">
<clr-control-error *ngIf="(memberName.invalid && (memberName.touched||memberName.dirty)) || !isMemberNameValid" class="tooltip-content">
{{ memberTooltip | translate }}
</clr-control-error>
</div>
@ -30,23 +30,23 @@
<label class="clr-control-label">{{'MEMBER.ROLE' | translate}}</label>
<div class="clr-control-container">
<clr-radio-wrapper>
<input clrRadio type="radio" name="member_role" id="checkrads_project_admin" [value]=1 [(ngModel)]="member.role_id">
<input clrRadio type="radio" name="member_role" id="checkrads_project_admin" [value]=1 [(ngModel)]="roleId">
<label for="checkrads_project_admin">{{'MEMBER.PROJECT_ADMIN' | translate}}</label>
</clr-radio-wrapper>
<clr-radio-wrapper>
<input clrRadio type="radio" name="member_role" id="checkrads_project_maintainer" [value]=4 [(ngModel)]="member.role_id">
<input clrRadio type="radio" name="member_role" id="checkrads_project_maintainer" [value]=4 [(ngModel)]="roleId">
<label for="checkrads_project_maintainer">{{'MEMBER.PROJECT_MAINTAINER' | translate}}</label>
</clr-radio-wrapper>
<clr-radio-wrapper>
<input clrRadio type="radio" name="member_role" id="checkrads_developer" [value]=2 [(ngModel)]="member.role_id">
<input clrRadio type="radio" name="member_role" id="checkrads_developer" [value]=2 [(ngModel)]="roleId">
<label for="checkrads_developer">{{'MEMBER.DEVELOPER' | translate}}</label>
</clr-radio-wrapper>
<clr-radio-wrapper>
<input clrRadio type="radio" name="member_role" id="checkrads_guest" [value]=3 [(ngModel)]="member.role_id">
<input clrRadio type="radio" name="member_role" id="checkrads_guest" [value]=3 [(ngModel)]="roleId">
<label for="checkrads_guest">{{'MEMBER.GUEST' | translate}}</label>
</clr-radio-wrapper>
<clr-radio-wrapper>
<input clrRadio type="radio" name="member_role" id="checkrads_limited_guest" [value]=5 [(ngModel)]="member.role_id">
<input clrRadio type="radio" name="member_role" id="checkrads_limited_guest" [value]=5 [(ngModel)]="roleId">
<label for="checkrads_limited_guest">{{'MEMBER.LIMITED_GUEST' | translate}}</label>
</clr-radio-wrapper>
</div>
@ -55,6 +55,6 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
<button [clrLoading]="btnStatus" type="button" class="btn btn-primary" [disabled]="!isValid()" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -6,7 +6,6 @@
position: absolute;
width: 173px;
height: auto;
border: 1px solid #ccc;
background-color: white;
border: 1px solid rgba(0,0,0,.15);
border-right-width: 2px;
@ -17,7 +16,8 @@
}
.selectBox ul li{
list-style: none;
padding: 3px 20px
padding: 3px 20px;
cursor: pointer;
}
.selectBox ul li:hover{
color: #262626;

View File

@ -1,23 +1,23 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AddMemberComponent } from './add-member.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { MemberService } from '../member.service';
import { UserService } from '../../../left-side-nav/user/user.service';
import { of } from 'rxjs';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { ActivatedRoute } from '@angular/router';
import { SharedTestingModule } from "../../../../shared/shared.module";
import { MemberService } from 'ng-swagger-gen/services/member.service';
describe('AddMemberComponent', () => {
let component: AddMemberComponent;
let fixture: ComponentFixture<AddMemberComponent>;
const mockMemberService = {
getUsersNameList: () => {
listProjectMembers: () => {
return of([]);
}
};
const mockUserService = {
getUsersNameList: () => {
listUsers: () => {
return of([
[], []
]);

View File

@ -11,64 +11,51 @@
// 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 { debounceTime, finalize } from 'rxjs/operators';
import { AfterViewChecked, Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild, } from '@angular/core';
import { debounceTime, finalize, switchMap } from 'rxjs/operators';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild, } from '@angular/core';
import { NgForm } from '@angular/forms';
import { ActivatedRoute } from "@angular/router";
import { forkJoin, Subject } from "rxjs";
import { TranslateService } from '@ngx-translate/core';
import { of, Subject, Subscription } from "rxjs";
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { UserService } from '../../../left-side-nav/user/user.service';
import { User } from "../../../left-side-nav/user/user";
import { Project } from "../../project";
import { Member } from '../member';
import { MemberService } from '../member.service';
import { ErrorHandler } from '../../../../shared/units/error-handler';
import { InlineAlertComponent } from "../../../../shared/components/inline-alert/inline-alert.component";
import { ProjectMemberEntity } from "../../../../../../ng-swagger-gen/models/project-member-entity";
import { ClrLoadingState } from "@clr/angular";
import { MemberService } from 'ng-swagger-gen/services/member.service';
import { UserService } from 'ng-swagger-gen/services/user.service';
import { UserResp } from "../../../../../../ng-swagger-gen/models/user-resp";
import { UserEntity } from "../../../../../../ng-swagger-gen/models/user-entity";
@Component({
selector: 'add-member',
templateUrl: 'add-member.component.html',
styleUrls: ['add-member.component.scss'],
providers: [UserService],
})
export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
@Input() memberList: ProjectMemberEntity[] = [];
member: ProjectMemberEntity = new Member();
addMemberOpened: boolean;
memberForm: NgForm;
export class AddMemberComponent implements OnInit, OnDestroy {
member: UserEntity = {};
addMemberOpened: boolean = false;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('memberForm', {static: true})
currentForm: NgForm;
hasChanged: boolean;
@ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent;
@Input() projectId: number;
@Output() added = new EventEmitter<boolean>();
isMemberNameValid: boolean = true;
memberTooltip: string = 'MEMBER.USERNAME_IS_REQUIRED';
nameChecker: Subject<string> = new Subject<string>();
searcher: Subject<string> = new Subject<string>();
nameCheckerSub: Subscription;
searcherSub: Subscription;
checkOnGoing: boolean = false;
selectUserName: string[] = [];
userLists: User[];
searchedUserLists: UserResp[] = [];
btnStatus: ClrLoadingState = ClrLoadingState.DEFAULT;
roleId: number = 1; // default value is 1(project admin)
constructor(private memberService: MemberService,
private userService: UserService,
private errorHandle: ErrorHandler,
private messageHandlerService: MessageHandlerService,
private translateService: TranslateService,
private route: ActivatedRoute) { }
ngOnInit(): void {
@ -78,115 +65,130 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
if (hasProjectAdminRole) {
this.nameChecker.pipe(
debounceTime(500))
.subscribe((name: string) => {
let cont = this.currentForm.controls['member_name'];
if (cont) {
this.isMemberNameValid = cont.valid;
if (cont.valid) {
this.checkOnGoing = true;
forkJoin([this.userService.getUsersNameList(cont.value, 20), this.memberService
.listMembers(this.projectId, cont.value)]).subscribe((res: Array<any>) => {
this.userLists = res[0];
if (res[1].filter(m => { return m.entity_name === cont.value; }).length > 0) {
this.isMemberNameValid = false;
this.memberTooltip = 'MEMBER.USERNAME_ALREADY_EXISTS';
}
this.checkOnGoing = false;
if (this.userLists && this.userLists.length) {
this.selectUserName = [];
this.userLists.forEach(data => {
if (data.username.startsWith(cont.value) && !this.memberList.find(mem => mem.entity_name === data.username)) {
if (this.selectUserName.length < 10) {
this.selectUserName.push(data.username);
}
}
});
}
}, error => {
this.checkOnGoing = false;
});
} else {
this.memberTooltip = 'MEMBER.USERNAME_IS_REQUIRED';
if (!this.searcherSub) {
this.searcherSub = this.searcher.pipe(
debounceTime(500),
switchMap(name => {
if (name) {
return this.userService.listUsers({
page: 1,
pageSize: 10,
q: encodeURIComponent(`username=~${name}`)
});
} else {
return of([]);
}
}),
).subscribe(res => {
if (res) {
this.searchedUserLists = res;
}
});
}
if (!this.nameCheckerSub) {
this.nameCheckerSub = this.nameChecker.pipe(
debounceTime(500),
switchMap(name => {
if (name) {
this.checkOnGoing = true;
return this.memberService.listProjectMembers({
page: 1,
pageSize: 10,
projectNameOrId: this.projectId.toString(),
entityname: name
}).pipe(
finalize(() => this.checkOnGoing = false)
);
} else {
return of([]);
}
}),
).subscribe(res => {
if (res && res.length) {
if (res.filter(m => m.entity_name === this.member.username).length > 0) {
this.isMemberNameValid = false;
this.memberTooltip = 'MEMBER.USERNAME_ALREADY_EXISTS';
}
}
});
}
}
}
ngOnDestroy(): void {
this.nameChecker.unsubscribe();
if (this.nameCheckerSub) {
this.nameCheckerSub.unsubscribe();
this.nameCheckerSub = null;
}
if (this.searcherSub) {
this.searcherSub.unsubscribe();
this.searcherSub = null;
}
}
onSubmit(): void {
if (!this.member.entity_name || this.member.entity_name.length === 0) { return; }
if (!this.member.username || this.member.username.length === 0) { return; }
this.btnStatus = ClrLoadingState.LOADING;
this.memberService
.addUserMember(this.projectId, {username: this.member.entity_name}, +this.member.role_id).pipe(
finalize(() => {
this.addMemberOpened = false;
this.member.role_id = null;
}
))
.createProjectMember({
projectNameOrId: this.projectId.toString(),
projectMember: {
role_id: this.roleId,
member_user: this.member
}
})
.subscribe(
() => {
this.addMemberOpened = false;
this.btnStatus = ClrLoadingState.SUCCESS;
this.messageHandlerService.showSuccess('MEMBER.ADDED_SUCCESS');
this.added.emit(true);
},
error => {
this.errorHandle.error(error);
this.btnStatus = ClrLoadingState.ERROR;
this.inlineAlert.showInlineError(error);
});
}
selectedName(username: string) {
this.member.entity_name = username;
this.selectUserName = [];
this.member.username = username;
this.nameChecker.next(username);
this.searchedUserLists = [];
}
onCancel() {
this.addMemberOpened = false;
this.memberForm.reset();
}
leaveInput() {
this.selectUserName = [];
this.searchedUserLists = [];
}
ngAfterViewChecked(): void {
if (this.memberForm !== this.currentForm) {
this.memberForm = this.currentForm;
}
if (this.memberForm) {
this.memberForm.valueChanges.subscribe(data => {
let memberName = data['member_name'];
if (memberName && memberName !== '') {
this.hasChanged = true;
} else {
this.hasChanged = false;
}
});
}
}
openAddMemberModal(): void {
this.currentForm.reset();
this.member = new Member();
this.currentForm.reset({
member_role: 1
});
this.inlineAlert.close();
this.member = {};
this.addMemberOpened = true;
this.hasChanged = false;
this.member.role_id = 1;
this.member.entity_name = '';
this.member.username = '';
this.isMemberNameValid = true;
this.memberTooltip = 'MEMBER.USERNAME_IS_REQUIRED';
this.selectUserName = [];
this.searchedUserLists = [];
}
handleValidation(): void {
let cont = this.currentForm.controls['member_name'];
if (cont) {
this.nameChecker.next(cont.value);
this.nameChecker.next(this.member.username);
this.searcher.next(this.member.username);
if (!this.member.username) {
this.searchedUserLists = [];
this.isMemberNameValid = false;
this.memberTooltip = 'MEMBER.USERNAME_IS_REQUIRED';
} else {
this.isMemberNameValid = true;
}
}
public get isValid(): boolean {
isValid(): boolean {
return this.currentForm &&
this.currentForm.valid &&
this.isMemberNameValid &&

View File

@ -16,7 +16,7 @@
<button class="btn btn-secondary" (click)="openAddMemberModal()" [disabled]="!hasCreateMemberPermission">
<span><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'MEMBER.USER' | translate }}</span>
</button>
<button class="btn btn-secondary" (click)="openAddGroupModal()" [disabled]="!hasCreateMemberPermission || !((isLdapMode && currentUser?.has_admin_role) || isHttpAuthMode || isOidcMode)">
<button class="btn btn-secondary" (click)="openAddGroupModal()" [disabled]="!hasCreateMemberPermission || !(isLdapMode || isHttpAuthMode || isOidcMode)">
<span><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'MEMBER.LDAP_GROUP' | translate}}</span>
</button>
<clr-dropdown [clrCloseMenuOnItemClick]="false" class="btn btn-link" clrDropdownTrigger>
@ -54,7 +54,6 @@
</clr-dg-footer>
</clr-datagrid>
</div>
<add-member [projectId]="projectId" [memberList]="members" (added)="addedMember($event)"></add-member>
<add-group [projectId]="projectId" [memberList]="members" (added)="addedGroup($event)"></add-group>
<add-http-auth-group [projectId]="projectId" (added)="addedGroup($event)"></add-http-auth-group>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
<add-group [projectId]="projectId" (added)="addedGroup($event)"></add-group>
</div>

View File

@ -19,8 +19,6 @@ import { TranslateService } from "@ngx-translate/core";
import { MessageHandlerService } from "../../../shared/services/message-handler.service";
import { SessionService } from "../../../shared/services/session.service";
import { SessionUser } from "../../../shared/entities/session-user";
import { AddGroupComponent } from './add-group/add-group.component';
import { AddHttpAuthGroupComponent } from './add-http-auth-group/add-http-auth-group.component';
import { AddMemberComponent } from "./add-member/add-member.component";
import { AppConfigService } from "../../../services/app-config.service";
import { OperationService } from "../../../shared/components/operation/operation.service";
@ -35,6 +33,7 @@ import { DEFAULT_PAGE_SIZE } from "../../../shared/units/utils";
import { MemberService } from "../../../../../ng-swagger-gen/services/member.service";
import { ClrDatagridStateInterface } from "@clr/angular";
import { ProjectMemberEntity } from "../../../../../ng-swagger-gen/models/project-member-entity";
import { AddGroupComponent } from './add-group/add-group.component';
@Component({
templateUrl: "member.component.html",
@ -64,11 +63,8 @@ export class MemberComponent implements OnInit, OnDestroy {
isOidcMode: boolean;
@ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent;
@ViewChild(AddGroupComponent)
addGroupComponent: AddGroupComponent;
@ViewChild(AddHttpAuthGroupComponent)
addHttpAuthGroupComponent: AddHttpAuthGroupComponent;
hasCreateMemberPermission: boolean;
hasUpdateMemberPermission: boolean;
hasDeleteMemberPermission: boolean;
@ -193,11 +189,7 @@ export class MemberComponent implements OnInit, OnDestroy {
// Add group
openAddGroupModal() {
if (this.isLdapMode) {
this.addGroupComponent.open();
} else {
this.addHttpAuthGroupComponent.openAddMemberModal();
}
this.addGroupComponent.openAddGroupModal();
}
addedGroup(result: boolean) {
this.searchMember = "";

View File

@ -3,9 +3,8 @@ import { RouterModule, Routes } from "@angular/router";
import { SharedModule } from "../../../shared/shared.module";
import { MemberComponent } from "./member.component";
import { AddMemberComponent } from "./add-member/add-member.component";
import { AddHttpAuthGroupComponent } from "./add-http-auth-group/add-http-auth-group.component";
import { AddGroupComponent } from "./add-group/add-group.component";
import { MemberService } from "./member.service";
import { AddGroupComponent } from './add-group/add-group.component';
const routes: Routes = [
{
@ -17,15 +16,11 @@ const routes: Routes = [
declarations: [
MemberComponent,
AddMemberComponent,
AddHttpAuthGroupComponent,
AddGroupComponent
AddGroupComponent,
],
imports: [
RouterModule.forChild(routes),
SharedModule
],
providers: [
MemberService
]
})
export class MemberModule { }

View File

@ -1,18 +0,0 @@
import { inject, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MemberService } from './member.service';
describe('MemberService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [MemberService]
});
});
it('should be created', inject([MemberService], (service: MemberService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -1,72 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { User } from '../../left-side-nav/user/user';
import { Member } from './member';
import { Observable, throwError as observableThrowError } from "rxjs";
import { catchError, map } from 'rxjs/operators';
import { CURRENT_BASE_HREF, HTTP_GET_OPTIONS, HTTP_JSON_OPTIONS } from "../../../shared/units/utils";
@Injectable()
export class MemberService {
constructor(private http: HttpClient) {}
listMembers(projectId: number, entity_name: string): Observable<Member[]> {
return this.http
.get(`${ CURRENT_BASE_HREF }/projects/${projectId}/members?entityname=${entity_name}`, HTTP_GET_OPTIONS).pipe(
map(response => response as Member[]),
catchError(error => observableThrowError(error)), );
}
addUserMember(projectId: number, user: User, roleId: number): Observable<any> {
let member_user = {};
if (user.user_id) {
member_user = {user_id: user.user_id};
} else if (user.username) {
member_user = {username: user.username};
} else {
return;
}
return this.http.post(
`${ CURRENT_BASE_HREF }/projects/${projectId}/members`,
{
role_id: roleId,
member_user: member_user
},
HTTP_JSON_OPTIONS).pipe(
catchError(error => observableThrowError(error)), );
}
addGroupMember(projectId: number, group: any, roleId: number): Observable<any> {
return this.http
.post(`${ CURRENT_BASE_HREF }/projects/${projectId}/members`,
{ role_id: roleId, member_group: group},
HTTP_JSON_OPTIONS).pipe(
catchError(error => observableThrowError(error)), );
}
changeMemberRole(projectId: number, userId: number, roleId: number): Observable<any> {
return this.http
.put(`${ CURRENT_BASE_HREF }/projects/${projectId}/members/${userId}`, { role_id: roleId }, HTTP_JSON_OPTIONS)
.pipe(catchError(error => observableThrowError(error)));
}
deleteMember(projectId: number, memberId: number): Observable<any> {
return this.http
.delete(`${ CURRENT_BASE_HREF }/projects/${projectId}/members/${memberId}`)
.pipe(catchError(error => observableThrowError(error)));
}
}

View File

@ -1,40 +0,0 @@
// Copyright Project Harbor Authors
//
// 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.
/*
{
"user_id": 1,
"username": "admin",
"email": "",
"password": "",
"realname": "",
"comment": "",
"deleted": false,
"role_name": "projectAdmin",
"role_id": 1,
"has_admin_role": false,
"reset_uuid": "",
"creation_time": "0001-01-01T00:00:00Z",
"update_time": "0001-01-01T00:00:00Z"
}
*/
export class Member {
id: number;
project_id: number;
entity_name: string;
role_name: string;
role_id: number;
entity_id: number;
entity_type: string;
}

View File

@ -1,8 +0,0 @@
import { TargetExistsValidatorDirective } from './target-exists-directive';
describe('TargetExistsValidatorDirective', () => {
it('should create an instance', () => {
const directive = new TargetExistsValidatorDirective(null, null);
expect(directive).toBeTruthy();
});
});

View File

@ -1,78 +0,0 @@
// Copyright Project Harbor Authors
//
// 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 { Directive, OnChanges, Input, SimpleChanges } from '@angular/core';
import { NG_ASYNC_VALIDATORS, Validator, Validators, ValidatorFn, AbstractControl } from '@angular/forms';
import { MemberService } from '../../base/project/member/member.service';
import { Member } from '../../base/project/member/member';
import { Observable } from 'rxjs';
import { ProjectDefaultService, ProjectService } from "../services";
@Directive({
selector: '[targetExists]',
providers: [
MemberService,
{ provide: ProjectService, useClass: ProjectDefaultService },
{ provide: NG_ASYNC_VALIDATORS, useExisting: TargetExistsValidatorDirective, multi: true },
]
})
export class TargetExistsValidatorDirective implements Validator, OnChanges {
@Input() targetExists: string;
@Input() projectId: number;
valFn = Validators.nullValidator;
constructor(
private projectService: ProjectService,
private memberService: MemberService) { }
ngOnChanges(changes: SimpleChanges): void {
const change = changes['targetExists'];
if (change) {
const target: string = change.currentValue;
this.valFn = this.targetExistsValidator(target);
} else {
this.valFn = Validators.nullValidator;
}
}
validate(control: AbstractControl): { [key: string]: any } {
return this.valFn(control);
}
targetExistsValidator(target: string): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
switch (target) {
case 'PROJECT_NAME':
return new Observable(observer => {
this.projectService
.checkProjectExists(control.value)
.subscribe(res => observer.next({ 'targetExists': true }), error => observer.next(null));
});
case 'MEMBER_NAME':
return new Observable(observer => {
this.memberService
.listMembers(this.projectId, control.value)
.subscribe((members: Member[]) => {
return members.filter(m => {
if (m.entity_name === control.value) {
return true;
}
return null;
}).length > 0 ?
observer.next({ 'targetExists': true }) : observer.next(null);
}, error => observer.next(null));
});
}
};
}
}

View File

@ -16,7 +16,6 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { map, catchError } from "rxjs/operators";
import { Observable, throwError as observableThrowError } from "rxjs";
import { SessionUser, SessionUserBackend } from '../entities/session-user';
import { Member } from '../../base/project/member/member';
import { SessionViewmodelFactory } from './session.viewmodel.factory';
import {
HTTP_FORM_OPTIONS,
@ -27,6 +26,7 @@ import {
import { FlushAll } from "../units/cache-util";
import { SignInCredential } from "../../account/sign-in/sign-in-credential";
import { DeFaultLang } from "../entities/shared.const";
import { ProjectMemberEntity } from "../../../../ng-swagger-gen/models/project-member-entity";
const signInUrl = '/c/login';
@ -52,7 +52,7 @@ const langMap = {
})
export class SessionService {
currentUser: SessionUser = null;
projectMembers: Member[];
projectMembers: ProjectMemberEntity[];
constructor(private http: HttpClient, public sessionViewmodel: SessionViewmodelFactory) { }
// Handle the related exceptions
@ -180,11 +180,11 @@ export class SessionService {
.pipe(catchError(error => this.handleError(error)));
}
setProjectMembers(projectMembers: Member[]): void {
setProjectMembers(projectMembers: ProjectMemberEntity[]): void {
this.projectMembers = projectMembers;
}
getProjectMembers(): Member[] {
getProjectMembers(): ProjectMemberEntity[] {
return this.projectMembers;
}

View File

@ -334,7 +334,10 @@
"SWITCH_TITLE": "Bestätige Rollenwechsel des Projekt-Mitglieds",
"SWITCH_SUMMARY": "Soll die Rolle des Mitglieds {{param}} geändert werden?",
"SET_ROLE": "Rolle festlegen",
"REMOVE": "Entfernen"
"REMOVE": "Entfernen",
"GROUP_NAME_REQUIRED": "Group name is required",
"NON_EXISTENT_GROUP": "Group name does not exists",
"GROUP_ALREADY_ADDED": "Group name has been already added to this project"
},
"ROBOT_ACCOUNT": {
"NAME": "Name",

View File

@ -321,9 +321,9 @@
"GROUP_TYPE": "Group",
"USER_TYPE": "User",
"USERNAME_IS_REQUIRED": "Username is required",
"USERNAME_DOES_NOT_EXISTS": "Username does not exist.",
"USERNAME_ALREADY_EXISTS": "Username already exists.",
"UNKNOWN_ERROR": "Unknown error occurred while adding member.",
"USERNAME_DOES_NOT_EXISTS": "Username does not exist",
"USERNAME_ALREADY_EXISTS": "Username has been already added to this project",
"UNKNOWN_ERROR": "Unknown error occurred while adding member",
"FILTER_PLACEHOLDER": "Filter Members",
"DELETION_TITLE": "Confirm project members deletion",
"DELETION_SUMMARY": "Do you want to delete project members {{param}}?",
@ -334,7 +334,10 @@
"SWITCH_TITLE": "Confirm project members switch",
"SWITCH_SUMMARY": "Do you want to switch project members {{param}}?",
"SET_ROLE": "Set Role",
"REMOVE": "Remove"
"REMOVE": "Remove",
"GROUP_NAME_REQUIRED": "Group name is required",
"NON_EXISTENT_GROUP": "Group name does not exists",
"GROUP_ALREADY_ADDED": "Group name has been already added to this project"
},
"ROBOT_ACCOUNT": {
"NAME": "Name",

View File

@ -335,7 +335,10 @@
"SWITCH_TITLE": "Confirm project members switch",
"SWITCH_SUMMARY": "Do you want to switch project members {{param}}?",
"SET_ROLE": "Set Role",
"REMOVE": "Remove"
"REMOVE": "Remove",
"GROUP_NAME_REQUIRED": "Group name is required",
"NON_EXISTENT_GROUP": "Group name does not exists",
"GROUP_ALREADY_ADDED": "Group name has been already added to this project"
},
"ROBOT_ACCOUNT": {
"NAME": "Name",

View File

@ -326,7 +326,10 @@
"SWITCHED_SUCCESS": "Rôle du membre changé avec succés.",
"OF": "de",
"SET_ROLE": "Set Role",
"REMOVE": "Remove"
"REMOVE": "Remove",
"GROUP_NAME_REQUIRED": "Group name is required",
"NON_EXISTENT_GROUP": "Group name does not exists",
"GROUP_ALREADY_ADDED": "Group name has been already added to this project"
},
"ROBOT_ACCOUNT": {
"NAME": "Nom",

View File

@ -332,7 +332,10 @@
"SWITCH_TITLE": "Confirmar alteração dos membros do projeto",
"SWITCH_SUMMARY": "Você quer alterar os membros do projeto {{param}}?",
"SET_ROLE": "DEFINIR FUNÇÃO",
"REMOVE": "Remover"
"REMOVE": "Remover",
"GROUP_NAME_REQUIRED": "Group name is required",
"NON_EXISTENT_GROUP": "Group name does not exists",
"GROUP_ALREADY_ADDED": "Group name has been already added to this project"
},
"ROBOT_ACCOUNT": {
"NAME": "Nome",

View File

@ -334,7 +334,10 @@
"SWITCH_TITLE": "Proje üyelerinin geçişini onayla",
"SWITCH_SUMMARY": "Proje üyelerini değiştirmek ister misiniz? {{param}}?",
"SET_ROLE": "ROL ATA",
"REMOVE": "Sil"
"REMOVE": "Sil",
"GROUP_NAME_REQUIRED": "Group name is required",
"NON_EXISTENT_GROUP": "Group name does not exists",
"GROUP_ALREADY_ADDED": "Group name has been already added to this project"
},
"ROBOT_ACCOUNT": {
"NAME": "İsim",

View File

@ -319,21 +319,24 @@
"MEMBER_TYPE": "成员类型",
"GROUP_TYPE": "组",
"USER_TYPE": "用户",
"USERNAME_IS_REQUIRED": "用户名为必填项",
"USERNAME_DOES_NOT_EXISTS": "用户名不存在",
"USERNAME_ALREADY_EXISTS": "用户名已存在。",
"UNKNOWN_ERROR": "添加成员时发生未知错误",
"USERNAME_IS_REQUIRED": "用户名为必填项",
"USERNAME_DOES_NOT_EXISTS": "用户名不存在",
"USERNAME_ALREADY_EXISTS": "该用户已存在于当前项目",
"UNKNOWN_ERROR": "添加成员时发生未知错误",
"FILTER_PLACEHOLDER": "过滤成员",
"DELETION_TITLE": "删除项目成员确认",
"DELETION_SUMMARY": "你确认删除项目成员 {{param}}?",
"ADDED_SUCCESS": "成功新增成员",
"DELETED_SUCCESS": "成功删除成员",
"SWITCHED_SUCCESS": "切换角色成功",
"ADDED_SUCCESS": "成功新增成员",
"DELETED_SUCCESS": "成功删除成员",
"SWITCHED_SUCCESS": "切换角色成功",
"OF": "共计",
"SWITCH_TITLE": "切换项目成员确认",
"SWITCH_SUMMARY": "你确认切换项目成员 {{param}}??",
"SET_ROLE": "设置角色",
"REMOVE": "移除成员"
"REMOVE": "移除成员",
"GROUP_NAME_REQUIRED": "组名称为必填项",
"NON_EXISTENT_GROUP": "组名称不存在",
"GROUP_ALREADY_ADDED": "该组已存在于当前项目"
},
"ROBOT_ACCOUNT": {
"NAME": "名称",

View File

@ -331,7 +331,10 @@
"SWITCH_TITLE": "切換項目成員確認",
"SWITCH_SUMMARY": "你確認切換項目成員{{param}}??",
"SET_ROLE": "設置角色",
"REMOVE": "移除成員"
"REMOVE": "移除成員",
"GROUP_NAME_REQUIRED": "Group name is required",
"NON_EXISTENT_GROUP": "Group name does not exists",
"GROUP_ALREADY_ADDED": "Group name has been already added to this project"
},
"ROBOT_ACCOUNT":{
"NAME": "名稱",