diff --git a/.travis.yml b/.travis.yml index 9d0b8d016..55a4c8659 100644 --- a/.travis.yml +++ b/.travis.yml @@ -79,7 +79,7 @@ script: - sudo mkdir -p /harbor - sudo mv ./VERSION /harbor/UIVERSION - sudo service postgresql stop - - sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 + - sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 - cat ./src/ui_ng/npm-ut-test-results - sudo ./tests/testprepare.sh - sudo make -f make/photon/Makefile _build_db _build_registry -e VERSIONTAG=dev -e CLAIRDBVERSION=dev -e REGISTRYVERSION=v2.6.2 @@ -105,7 +105,7 @@ script: - sudo rm -rf /data/config/* - sudo rm -rf /data/database/* - ls /data/cert - - sudo make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 NOTARYFLAG=true CLAIRFLAG=true + - sudo make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 NOTARYFLAG=true CLAIRFLAG=true - sleep 10 - docker ps - ./tests/validatecontainers.sh diff --git a/docs/compile_guide.md b/docs/compile_guide.md index 475b0cdd3..a2d0560c4 100644 --- a/docs/compile_guide.md +++ b/docs/compile_guide.md @@ -50,19 +50,19 @@ You can compile the code by one of the three approaches: * Build, install and bring up Harbor without Notary: ```sh - $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 + $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 ``` * Build, install and bring up Harbor with Notary: ```sh - $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 NOTARYFLAG=true + $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 NOTARYFLAG=true ``` * Build, install and bring up Harbor with Clair: ```sh - $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 CLAIRFLAG=true + $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 CLAIRFLAG=true ``` #### II. Compile code with your own Golang environment, then build Harbor diff --git a/make/dev/nodeclarity/entrypoint.sh b/make/dev/nodeclarity/entrypoint.sh index b4899bb6d..bec32011d 100644 --- a/make/dev/nodeclarity/entrypoint.sh +++ b/make/dev/nodeclarity/entrypoint.sh @@ -34,7 +34,7 @@ sed -i 's/* as//g' src/app/shared/gauge/gauge.component.js cp ./dist/build.min.js ../ui/static/ cp -r ./src/i18n/ ../ui/static/ -cp ./src/styles.scss ../ui/static/ +cp ./src/styles.css ../ui/static/ cp -r ./src/images/ ../ui/static/ cp ./src/setting.json ../ui/static/ diff --git a/src/ui_ng/angular-cli.json b/src/ui_ng/angular-cli.json index 93021a4b1..87732b1de 100644 --- a/src/ui_ng/angular-cli.json +++ b/src/ui_ng/angular-cli.json @@ -20,7 +20,7 @@ "styles": [ "../node_modules/clarity-icons/clarity-icons.min.css", "../node_modules/clarity-ui/clarity-ui.min.css", - "styles.scss" + "styles.css" ], "scripts": [ "../node_modules/core-js/client/shim.min.js", diff --git a/src/ui_ng/lib/src/config/config.ts b/src/ui_ng/lib/src/config/config.ts index 07631722a..29e4f0feb 100644 --- a/src/ui_ng/lib/src/config/config.ts +++ b/src/ui_ng/lib/src/config/config.ts @@ -65,6 +65,10 @@ export class Configuration { ldap_uid: StringValueItem; ldap_url: StringValueItem; ldap_verify_cert: BoolValueItem; + ldap_group_base_dn: StringValueItem; + ldap_group_search_filter: StringValueItem; + ldap_group_attribute_name: StringValueItem; + ldap_group_search_scope: NumberValueItem; uaa_client_id: StringValueItem; uaa_client_secret?: StringValueItem; uaa_endpoint: StringValueItem; @@ -96,6 +100,10 @@ export class Configuration { this.ldap_uid = new StringValueItem("", true); this.ldap_url = new StringValueItem("", true); this.ldap_verify_cert = new BoolValueItem(true, true); + this.ldap_group_base_dn = new StringValueItem("", true); + this.ldap_group_search_filter = new StringValueItem("", true); + this.ldap_group_attribute_name = new StringValueItem("", true); + this.ldap_group_search_scope = new NumberValueItem(0, true); this.uaa_client_id = new StringValueItem("", true); this.uaa_client_secret = new StringValueItem("", true); this.uaa_endpoint = new StringValueItem("", true); diff --git a/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts b/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts index b4b160ee1..70934be98 100644 --- a/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts +++ b/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts @@ -4,6 +4,7 @@ */ export class BatchInfo { + id?: number; name: string; status: string; loading: boolean; @@ -17,11 +18,17 @@ export class BatchInfo { } } -export function BathInfoChanges(list: BatchInfo, status: string, loading = false, errStatus = false, errorInfo = '') { - list.status = status; - list.loading = loading; - list.errorState = errStatus; - list.errorInfo = errorInfo; - return list; +export function BathInfoChanges(batchInfo: BatchInfo, status: string, loading = false, errStatus = false, errorInfo = '') { + batchInfo.status = status; + batchInfo.loading = loading; + batchInfo.errorState = errStatus; + batchInfo.errorInfo = errorInfo; + return batchInfo; +} + +export enum BatchOperations { + Idle, + Delete, + ChangeRole } diff --git a/src/ui_ng/src/app/app.module.ts b/src/ui_ng/src/app/app.module.ts index 31da2cc0b..7d7b0023b 100644 --- a/src/ui_ng/src/app/app.module.ts +++ b/src/ui_ng/src/app/app.module.ts @@ -48,7 +48,9 @@ export function getCurrentLanguage(translateService: TranslateService) { BaseModule, AccountModule, HarborRoutingModule, - ConfigurationModule, + ConfigurationModule + ], + exports: [ ], providers: [ AppConfigService, diff --git a/src/ui_ng/src/app/base/base.module.ts b/src/ui_ng/src/app/base/base.module.ts index 48b894c69..dffa8751c 100644 --- a/src/ui_ng/src/app/base/base.module.ts +++ b/src/ui_ng/src/app/base/base.module.ts @@ -19,6 +19,7 @@ import { ProjectModule } from '../project/project.module'; import { UserModule } from '../user/user.module'; import { AccountModule } from '../account/account.module'; import { RepositoryModule } from '../repository/repository.module'; +import { GroupModule } from '../group/group.module'; import { NavigatorComponent } from './navigator/navigator.component'; import { GlobalSearchComponent } from './global-search/global-search.component'; @@ -36,7 +37,8 @@ import { SearchTriggerService } from './global-search/search-trigger.service'; UserModule, AccountModule, RouterModule, - RepositoryModule + RepositoryModule, + GroupModule ], declarations: [ NavigatorComponent, diff --git a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html index a47bf1940..125bbcc9b 100644 --- a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html +++ b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html @@ -28,6 +28,13 @@ {{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}} + + + {{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}} + diff --git a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.ts b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.ts index 3473602f3..3ae2fe5e6 100644 --- a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.ts +++ b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.ts @@ -13,6 +13,8 @@ // limitations under the License. import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs/Subscription'; +import { AppConfigService } from '../..//app-config.service'; import { ModalEvent } from '../modal-event'; import { modalEvents } from '../modal-events.const'; @@ -23,11 +25,7 @@ import { NavigatorComponent } from '../navigator/navigator.component'; import { SessionService } from '../../shared/session.service'; import { AboutDialogComponent } from '../../shared/about-dialog/about-dialog.component'; - import { SearchTriggerService } from '../global-search/search-trigger.service'; - -import { Subscription } from 'rxjs/Subscription'; - import { CommonRoutes } from '../../shared/shared.const'; @Component({ @@ -61,7 +59,8 @@ export class HarborShellComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private router: Router, private session: SessionService, - private searchTrigger: SearchTriggerService) { } + private searchTrigger: SearchTriggerService, + private appConfigService: AppConfigService) { } ngOnInit() { this.searchSub = this.searchTrigger.searchTriggerChan$.subscribe(searchEvt => { @@ -98,6 +97,11 @@ export class HarborShellComponent implements OnInit, OnDestroy { return account != null && account.has_admin_role; } + public get isLdapMode(): boolean { + let appConfig = this.appConfigService.getConfig(); + return appConfig.auth_mode === 'ldap_auth'; + } + public get isUserExisting(): boolean { let account = this.session.getCurrentUser(); return account != null; diff --git a/src/ui_ng/src/app/config/auth/config-auth.component.html b/src/ui_ng/src/app/config/auth/config-auth.component.html index 2b0464815..47cd4ffe4 100644 --- a/src/ui_ng/src/app/config/auth/config-auth.component.html +++ b/src/ui_ng/src/app/config/auth/config-auth.component.html @@ -19,50 +19,44 @@
- +
- +
- +
- +
- +
+ + + {{'TOOLTIP.ITEM_REQUIRED' | translate}} + + {{'CONFIG.TOOLTIP.LDAP_SEARCH_DN' | translate}} @@ -71,25 +65,23 @@
+ + + {{'TOOLTIP.ITEM_REQUIRED' | translate}} + +
- + {{'CONFIG.TOOLTIP.LDAP_BASE_DN' | translate}} @@ -98,25 +90,23 @@
+ + + {{'TOOLTIP.ITEM_REQUIRED' | translate}} + +
+
+ + + + + + {{'CONFIG.LDAP.LDAP_GROUP_BASE_DN_INFO' | translate}} + + +
+
+ + + + + + {{'CONFIG.LDAP.LDAP_GROUP_FILTER_INFO' | translate}} + + +
+
+ + + + + + {{'CONFIG.LDAP.LDAP_GROUP_GID_INFO' | translate}} + + +
+
+ +
+ +
+ + + + {{'CONFIG.LDAP.GROUP_SCOPE_INFO' | translate}} + + +
@@ -165,8 +214,10 @@ - {{'CONFIG.TOOLTIP.SELF_REGISTRATION_ENABLE' | translate}} - {{'CONFIG.TOOLTIP.SELF_REGISTRATION_DISABLE' | translate}} + {{'CONFIG.TOOLTIP.SELF_REGISTRATION_ENABLE' | translate}} + + {{'CONFIG.TOOLTIP.SELF_REGISTRATION_DISABLE' | translate}} +
diff --git a/src/ui_ng/src/app/config/auth/config-auth.component.scss b/src/ui_ng/src/app/config/auth/config-auth.component.scss index e69de29bb..9634e8199 100644 --- a/src/ui_ng/src/app/config/auth/config-auth.component.scss +++ b/src/ui_ng/src/app/config/auth/config-auth.component.scss @@ -0,0 +1,3 @@ +clr-tooltip { + top: -1px; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/config/config.component.ts b/src/ui_ng/src/app/config/config.component.ts index 226795dc7..675c4c524 100644 --- a/src/ui_ng/src/app/config/config.component.ts +++ b/src/ui_ng/src/app/config/config.component.ts @@ -371,7 +371,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy { .catch(error => { this.testingLDAPOnGoing = false; let err = error._body; - if (!err) { + if (!err || !err.trim()) { err = 'UNKNOWN'; } this.msgHandler.showError('CONFIG.TEST_LDAP_FAILED', { 'param': err }); diff --git a/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.html b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.html new file mode 100644 index 000000000..d883b74bb --- /dev/null +++ b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.html @@ -0,0 +1,43 @@ + + + + + + + \ No newline at end of file diff --git a/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.scss b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.spec.ts b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.spec.ts new file mode 100644 index 000000000..a26876f21 --- /dev/null +++ b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddGroupModalComponent } from './add-group-modal.component'; + +describe('AddGroupModalComponent', () => { + let component: AddGroupModalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AddGroupModalComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddGroupModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.ts b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.ts new file mode 100644 index 000000000..9a6fa9eae --- /dev/null +++ b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.ts @@ -0,0 +1,109 @@ +import { Subscription } from 'rxjs/Subscription'; +import { Component, OnInit, EventEmitter, Output, ChangeDetectorRef, OnDestroy, ViewChild } from "@angular/core"; +import { NgForm } from "@angular/forms"; +import "rxjs/add/operator/finally"; + +import { GroupService } from "../group.service"; +import { MessageHandlerService } from "./../../shared/message-handler/message-handler.service"; +import { SessionService } from "./../../shared/session.service"; +import { UserGroup } from "./../group"; + +@Component({ + selector: "hbr-add-group-modal", + templateUrl: "./add-group-modal.component.html", + styleUrls: ["./add-group-modal.component.scss"] +}) +export class AddGroupModalComponent implements OnInit, OnDestroy { + opened = false; + mode = "create"; + dnTooltip = 'TOOLTIP.ITEM_REQUIRED'; + + group: UserGroup = new UserGroup(); + + formChangeSubscription: Subscription; + + @ViewChild('groupForm') + groupForm: NgForm; + + submitted = false; + + @Output() dataChange = new EventEmitter(); + + constructor( + private session: SessionService, + private msgHandler: MessageHandlerService, + private groupService: GroupService, + private cdr: ChangeDetectorRef + ) {} + + ngOnInit() { } + + + ngOnDestroy() { } + + public get isDNInvalid(): boolean { + let dnControl = this.groupForm.controls['ldap_group_dn']; + return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched); + } + + public get isFormValid(): boolean { + return this.groupForm.valid; + } + + public open(group?: UserGroup, editMode: boolean = false): void { + this.resetGroup(); + if (editMode) { + this.mode = "edit"; + Object.assign(this.group, group); + } else { + this.mode = "create"; + } + this.opened = true; + } + + public close(): void { + this.opened = false; + this.resetGroup(); + } + + save(): void { + if (this.mode === "create") { + this.createGroup(); + } else { + this.editGroup(); + } + } + + createGroup() { + let groupCopy = Object.assign({}, this.group); + this.groupService + .createGroup(groupCopy) + .finally(() => this.close()) + .subscribe( + res => { + this.msgHandler.showSuccess("GROUP.ADD_GROUP_SUCCESS"); + this.dataChange.emit(); + }, + error => this.msgHandler.handleError(error) + ); + } + + editGroup() { + let groupCopy = Object.assign({}, this.group); + this.groupService + .editGroup(groupCopy) + .finally(() => this.close()) + .subscribe( + res => { + this.msgHandler.showSuccess("ADD_GROUP_FAILURE"); + this.dataChange.emit(); + }, + error => this.msgHandler.handleError(error) + ); + } + + resetGroup() { + this.group = new UserGroup(); + this.groupForm.reset(); + } +} diff --git a/src/ui_ng/src/app/group/group.component.html b/src/ui_ng/src/app/group/group.component.html new file mode 100644 index 000000000..a2d3b313c --- /dev/null +++ b/src/ui_ng/src/app/group/group.component.html @@ -0,0 +1,40 @@ +
+
+

{{'GROUP.GROUP' | translate}}

+
+ + + + + +
+
+ + + + + + + + {{'GROUP.NAME' | translate}} + {{'GROUP.TYPE' | translate}} + {{'GROUP.DN' | translate}} + + + {{group.group_name}} + {{groupToSring(group.group_type) | translate}} + {{group.ldap_group_dn}} + + + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'USER.OF' | translate }} {{pagination.totalItems}} {{'GROUP.GROUPS' | translate}} + + + +
+ +
+
\ No newline at end of file diff --git a/src/ui_ng/src/app/group/group.component.scss b/src/ui_ng/src/app/group/group.component.scss new file mode 100644 index 000000000..0d3b56629 --- /dev/null +++ b/src/ui_ng/src/app/group/group.component.scss @@ -0,0 +1,45 @@ +.custom-add-button { + font-size: 12px; + margin-left: -12px; +} + +.filter-icon { + position: relative; + right: -12px; +} + +.filter-pos { + float: right; + margin-right: 24px; + position: relative; + top: 10px; +} + +.action-panel-pos { + position: relative; + padding-left: 12px; + margin-top: 12px; +} + +.refresh-btn { + position: absolute; + right: 6px; + top: 17px; + cursor: pointer; +} + +.refresh-btn:hover { + color: #007CBB; +} + +.hide-create { + visibility: hidden !important; +} + +.rightPos { + position: absolute; + right: 20px; + margin-top: -7px; + height: 32px; + z-index: 100; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/group/group.component.spec.ts b/src/ui_ng/src/app/group/group.component.spec.ts new file mode 100644 index 000000000..50601d077 --- /dev/null +++ b/src/ui_ng/src/app/group/group.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GroupComponent } from './group.component'; + +describe('GroupComponent', () => { + let component: GroupComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GroupComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ui_ng/src/app/group/group.component.ts b/src/ui_ng/src/app/group/group.component.ts new file mode 100644 index 000000000..6dbd6949d --- /dev/null +++ b/src/ui_ng/src/app/group/group.component.ts @@ -0,0 +1,166 @@ +import { SessionService } from "./../shared/session.service"; +import { TranslateService } from "@ngx-translate/core"; +import { Observable } from "rxjs/Observable"; +import { Subscription } from "rxjs/Subscription"; +import { Component, OnInit, ViewChild, OnDestroy } from "@angular/core"; +import {operateChanges, OperateInfo, OperationService, OperationState} from "harbor-ui"; + +import { + ConfirmationTargets, + ConfirmationState, + ConfirmationButtons +} from "../shared/shared.const"; +import { ConfirmationMessage } from "../shared/confirmation-dialog/confirmation-message"; +import { ConfirmationDialogService } from "./../shared/confirmation-dialog/confirmation-dialog.service"; +import { AddGroupModalComponent } from "./add-group-modal/add-group-modal.component"; +import { UserGroup } from "./group"; +import { GroupService } from "./group.service"; +import { MessageHandlerService } from "../shared/message-handler/message-handler.service"; + +@Component({ + selector: "app-group", + templateUrl: "./group.component.html", + styleUrls: ["./group.component.scss"] +}) +export class GroupComponent implements OnInit, OnDestroy { + searchTerm = ""; + loading = true; + groups: UserGroup[] = []; + currentPage = 1; + totalCount = 0; + selectedGroups: UserGroup[] = []; + currentTerm = ""; + delSub: Subscription; + batchOps = 'idle'; + batchInfos = new Map(); + + @ViewChild(AddGroupModalComponent) newGroupModal: AddGroupModalComponent; + + constructor( + private operationService: OperationService, + private translate: TranslateService, + private operateDialogService: ConfirmationDialogService, + private groupService: GroupService, + private msgHandler: MessageHandlerService, + private session: SessionService + ) {} + + ngOnInit() { + this.loadData(); + this.delSub = this.operateDialogService.confirmationConfirm$.subscribe( + message => { + if ( + message && + message.state === ConfirmationState.CONFIRMED && + message.source === ConfirmationTargets.PROJECT_MEMBER + ) { + if (this.batchOps === 'delete') { + this.deleteGroups(); + } + } + } + ); + } + ngOnDestroy(): void { + this.delSub.unsubscribe(); + } + + refresh(): void { + this.loadData(); + } + + loadData(): void { + this.loading = true; + this.groupService.getUserGroups().subscribe(groups => { + this.groups = groups.filter(group => { + if (!group.group_name) {group.group_name = ''; } + return group.group_name.includes(this.searchTerm); + } + ); + this.loading = false; + }); + } + + addGroup(): void { + this.newGroupModal.open(); + } + + editGroup(): void { + this.newGroupModal.open(this.selectedGroups[0], true); + } + + openDeleteConfirmationDialog(): void { + // open delete modal + this.batchOps = 'delete'; + let nameArr: string[] = []; + if (this.selectedGroups.length > 0) { + this.selectedGroups.forEach(group => { + nameArr.push(group.group_name); + }); + // batchInfo.id = group.id; + let deletionMessage = new ConfirmationMessage( + "MEMBER.DELETION_TITLE", + "MEMBER.DELETION_SUMMARY", + nameArr.join(","), + this.selectedGroups, + ConfirmationTargets.PROJECT_MEMBER, + ConfirmationButtons.DELETE_CANCEL + ); + this.operateDialogService.openComfirmDialog(deletionMessage); + } + } + + deleteGroups() { + let obs = this.selectedGroups.map(group => { + let operMessage = new OperateInfo(); + operMessage.name = 'OPERATION.DELETE_GROUP'; + operMessage.data.id = group.id; + operMessage.state = OperationState.progressing; + operMessage.data.name = group.group_name; + + this.operationService.publishInfo(operMessage); + return this.groupService + .deleteGroup(group.id) + .flatMap(response => { + return this.translate.get("BATCH.DELETED_SUCCESS").flatMap(res => { + operateChanges(operMessage, OperationState.success); + return Observable.of(res); + }); + }) + .catch(err => { + return this.translate.get("BATCH.DELETED_FAILURE").flatMap(res => { + operateChanges(operMessage, OperationState.failure, res); + return Observable.of(res); + }); + }); + }); + + Observable.forkJoin(obs).subscribe( + res => { + this.selectedGroups = []; + this.batchOps = 'idle'; + this.loadData(); + }, + err => this.msgHandler.handleError(err) + ); + } + + groupToSring(type: number) { + if (type === 1) {return 'GROUP.LDAP_TYPE'; } else {return 'UNKNOWN'; } + } + + doFilter(groupName: string): void { + this.searchTerm = groupName; + this.loadData(); + } + get canAddGroup(): boolean { + return this.session.currentUser.has_admin_role; + } + + get canEditGroup(): boolean { + return ( + this.selectedGroups.length === 1 && + this.session.currentUser.has_admin_role + ); + } +} diff --git a/src/ui_ng/src/app/group/group.module.ts b/src/ui_ng/src/app/group/group.module.ts new file mode 100644 index 000000000..45464c1e7 --- /dev/null +++ b/src/ui_ng/src/app/group/group.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { SharedModule } from '../shared/shared.module'; +import { GroupComponent } from './group.component'; +import { AddGroupModalComponent } from './add-group-modal/add-group-modal.component'; +import { GroupService } from './group.service'; + + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + FormsModule, + ReactiveFormsModule + ], + exports: [ + GroupComponent, + AddGroupModalComponent, + FormsModule, + ReactiveFormsModule + ], + providers: [ GroupService ], + declarations: [GroupComponent, AddGroupModalComponent] +}) +export class GroupModule { } diff --git a/src/ui_ng/src/app/group/group.service.spec.ts b/src/ui_ng/src/app/group/group.service.spec.ts new file mode 100644 index 000000000..21236835b --- /dev/null +++ b/src/ui_ng/src/app/group/group.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { GroupService } from './group.service'; + +describe('GroupService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [GroupService] + }); + }); + + it('should be created', inject([GroupService], (service: GroupService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/ui_ng/src/app/group/group.service.ts b/src/ui_ng/src/app/group/group.service.ts new file mode 100644 index 000000000..290f03bc4 --- /dev/null +++ b/src/ui_ng/src/app/group/group.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from "@angular/core"; +import { Http, Response } from "@angular/http"; +import { Observable } from "rxjs/Observable"; +import "rxjs/add/observable/of"; +import "rxjs/add/operator/delay"; +import "rxjs/add/operator/toPromise"; + +import { UserGroup } from "./group"; +import { HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS } from "../shared/shared.utils"; + +const userGroupEndpoint = "/api/usergroups"; +const ldapGroupSearchEndpoint = "/api/ldap/groups/search?groupname="; + +@Injectable() +export class GroupService { + constructor(private http: Http) {} + + private extractData(res: Response) { + if (res.text() === '') {return []; }; + return res.json() || []; + } + private handleErrorObservable(error: Response | any) { + console.error(error.message || error); + return Observable.throw(error.message || error); + } + + getUserGroups(): Observable { + return this.http.get(userGroupEndpoint, HTTP_GET_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(error => { + return this.handleErrorObservable(error); + }); + } + + createGroup(group: UserGroup): Observable { + return this.http + .post(userGroupEndpoint, group, HTTP_JSON_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } + + getGroup(group_id: number): Observable { + return this.http + .get(`${userGroupEndpoint}/${group_id}`, HTTP_JSON_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } + + editGroup(group: UserGroup): Observable { + return this.http + .put(`${userGroupEndpoint}/${group.id}`, group, HTTP_JSON_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } + + deleteGroup(group_id: number): Observable { + return this.http + .delete(`${userGroupEndpoint}/${group_id}`) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } + + searchGroup(group_name: string): Observable { + return this.http + .get(`${ldapGroupSearchEndpoint}${group_name}`, HTTP_GET_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } +} diff --git a/src/ui_ng/src/app/group/group.ts b/src/ui_ng/src/app/group/group.ts new file mode 100644 index 000000000..855681162 --- /dev/null +++ b/src/ui_ng/src/app/group/group.ts @@ -0,0 +1,12 @@ +export class UserGroup { + id?: number; + group_name?: string; + group_type: number; + ldap_group_dn?: string; + + constructor() { + { + this.group_type = 1; + } + } +} diff --git a/src/ui_ng/src/app/harbor-routing.module.ts b/src/ui_ng/src/app/harbor-routing.module.ts index 0de5f5d00..3e52fa267 100644 --- a/src/ui_ng/src/app/harbor-routing.module.ts +++ b/src/ui_ng/src/app/harbor-routing.module.ts @@ -27,6 +27,7 @@ import { ConfigurationComponent } from './config/config.component'; import { UserComponent } from './user/user.component'; import { SignInComponent } from './account/sign-in/sign-in.component'; import { ResetPasswordComponent } from './account/password-setting/reset-password/reset-password.component'; +import { GroupComponent } from './group/group.component'; import { TotalReplicationPageComponent } from './replication/total-replication/total-replication-page.component'; import { DestinationPageComponent } from './replication/destination/destination-page.component'; @@ -74,6 +75,11 @@ const harborRoutes: Routes = [ component: UserComponent, canActivate: [SystemAdminGuard] }, + { + path: 'groups', + component: GroupComponent, + canActivate: [SystemAdminGuard] + }, { path: 'registries', component: DestinationPageComponent, diff --git a/src/ui_ng/src/app/project/member/add-group/add-group.component.html b/src/ui_ng/src/app/project/member/add-group/add-group.component.html new file mode 100644 index 000000000..3c1c0cfaf --- /dev/null +++ b/src/ui_ng/src/app/project/member/add-group/add-group.component.html @@ -0,0 +1,104 @@ + + + + + + \ No newline at end of file diff --git a/src/ui_ng/src/app/project/member/add-group/add-group.component.scss b/src/ui_ng/src/app/project/member/add-group/add-group.component.scss new file mode 100644 index 000000000..1d9d5d98b --- /dev/null +++ b/src/ui_ng/src/app/project/member/add-group/add-group.component.scss @@ -0,0 +1,18 @@ +clr-datagrid { + ::ng-deep .datagrid { + margin-top: 0; + } +} + +.row { + margin-top: 12px; +} + +.modeSelectradios { + margin-top: 21px; +} +.filterTool { + position: relative; + z-index: 100; + right: 15px; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/project/member/add-group/add-group.component.spec.ts b/src/ui_ng/src/app/project/member/add-group/add-group.component.spec.ts new file mode 100644 index 000000000..e78989208 --- /dev/null +++ b/src/ui_ng/src/app/project/member/add-group/add-group.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddGroupComponent } from './add-group.component'; + +describe('AddGroupComponent', () => { + let component: AddGroupComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AddGroupComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddGroupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ui_ng/src/app/project/member/add-group/add-group.component.ts b/src/ui_ng/src/app/project/member/add-group/add-group.component.ts new file mode 100644 index 000000000..b5ea17a16 --- /dev/null +++ b/src/ui_ng/src/app/project/member/add-group/add-group.component.ts @@ -0,0 +1,167 @@ +import { ChangeDetectorRef, ChangeDetectionStrategy, ViewChild } from "@angular/core"; +import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; +import { NgForm } from '@angular/forms'; + +import { forkJoin } from "rxjs/observable/forkJoin"; +import { Observable } from "rxjs/Observable"; +import "rxjs/observable/of"; +import { TranslateService } from '@ngx-translate/core'; + +import "rxjs/observable/timer"; +import {operateChanges, OperateInfo, OperationService, OperationState} from "harbor-ui"; + +import { UserGroup } from "./../../../group/group"; +import { MemberService } from "./../member.service"; +import { GroupService } from "../../../group/group.service"; +import { ProjectRoles } from "../../../shared/shared.const"; +import { MessageHandlerService } from '../../../shared/message-handler/message-handler.service'; +import { Member } from "../member"; + +@Component({ + selector: "add-group", + templateUrl: "./add-group.component.html", + styleUrls: ["./add-group.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AddGroupComponent implements OnInit { + opened = false; + createGroupMode = false; + onLoading = false; + roles = ProjectRoles; + currentTerm = ''; + + selectedRole = 1; + group = new UserGroup(); + selectedGroups: UserGroup[] = []; + groups: UserGroup[] = []; + + dnTooltip = 'TOOLTIP.ITEM_REQUIRED'; + + @Input() projectId: number; + @Input() memberList: Member[] = []; + @Output() added = new EventEmitter(); + + @ViewChild('groupForm') + groupForm: NgForm; + + 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.onLoading = false; + this.ref.detectChanges(); + }); + } + + doFilter(name: string) { + this.currentTerm = name; + this.loadGroups(); + } + + resetModaldata() { + this.group = new UserGroup(); + 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) + .flatMap(response => { + return this.translateService.get("BATCH.DELETED_SUCCESS") + .flatMap(res => { + operateChanges(operMessage, OperationState.success); + return Observable.of(res); + }); }) + .catch(error => { + return this.translateService.get("BATCH.DELETED_FAILURE") + .flatMap(res => { + operateChanges(operMessage, OperationState.failure, res); + return Observable.of(res); + }); }) + .catch(error => Observable.of(error.status)); + }); + forkJoin(GroupAdders$) + .subscribe(results => { + if (results.some(code => code < 200 || code > 299)) { + this.added.emit(false); + } else { + this.added.emit(true); + } + }); + this.opened = false; + } + + 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.opened = false; + } +} diff --git a/src/ui_ng/src/app/project/member/add-member/add-member.component.html b/src/ui_ng/src/app/project/member/add-member/add-member.component.html index 6cb1cdac9..309d289f3 100644 --- a/src/ui_ng/src/app/project/member/add-member/add-member.component.html +++ b/src/ui_ng/src/app/project/member/add-member/add-member.component.html @@ -1,18 +1,19 @@ - - +