Merge pull request #5074 from ninjadq/ldap_search_ui

Add LDAP search UI
This commit is contained in:
Steven Zou 2018-07-03 15:30:18 +08:00 committed by GitHub
commit 6dfccc7dea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1648 additions and 365 deletions

View File

@ -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

View File

@ -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

View File

@ -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/

View File

@ -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",

View File

@ -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);

View File

@ -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
}

View File

@ -48,7 +48,9 @@ export function getCurrentLanguage(translateService: TranslateService) {
BaseModule,
AccountModule,
HarborRoutingModule,
ConfigurationModule,
ConfigurationModule
],
exports: [
],
providers: [
AppConfigService,

View File

@ -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,

View File

@ -28,6 +28,13 @@
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
</a>
<a *ngIf='isLdapMode'
clrVerticalNavLink
routerLink="/harbor/groups"
routerLinkActive="active">
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}}
</a>
<a clrVerticalNavLink
routerLink="/harbor/registries"
routerLinkActive="active">

View File

@ -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;

View File

@ -19,50 +19,44 @@
<section class="form-block" *ngIf="showUAA">
<div class="form-group">
<label for="uaa-endpoint" class="required">{{'CONFIG.UAA.ENDPOINT' | translate}}</label>
<input type="text" id="uaa-endpoint" name="uaa-endpoint" size="35"
[(ngModel)]="currentConfig.uaa_endpoint.value" [disabled]="!currentConfig.uaa_verify_cert.editable">
<input type="text" id="uaa-endpoint" name="uaa-endpoint" size="35" [(ngModel)]="currentConfig.uaa_endpoint.value" [disabled]="!currentConfig.uaa_verify_cert.editable">
</div>
<div class="form-group">
<label for="uaa-id" class="required">{{'CONFIG.UAA.CLIENT_ID' | translate}}</label>
<input type="text" id="uaa-cid" name="uaa-client-id" size="35"
[(ngModel)]="currentConfig.uaa_client_id.value" [disabled]="!currentConfig.uaa_verify_cert.editable">
<input type="text" id="uaa-cid" name="uaa-client-id" size="35" [(ngModel)]="currentConfig.uaa_client_id.value" [disabled]="!currentConfig.uaa_verify_cert.editable">
</div>
<div class="form-group">
<label for="uaa-id" class="required">{{'CONFIG.UAA.CLIENT_SECRET' | translate}}</label>
<input type="text" id="uaa-secret" name="uaa-client-secret" type="password" size="35"
[(ngModel)]="currentConfig.uaa_client_secret.value" [disabled]="!currentConfig.uaa_verify_cert.editable">
<input type="text" id="uaa-secret" name="uaa-client-secret" type="password" size="35" [(ngModel)]="currentConfig.uaa_client_secret.value"
[disabled]="!currentConfig.uaa_verify_cert.editable">
</div>
<div class="form-group">
<label for="uaa-cert" class="required">{{'CONFIG.UAA.VERIFY_CERT' | translate}}</label>
<clr-checkbox id="uaa-cert" name="uaa-cert" [(clrChecked)]="currentConfig.uaa_verify_cert.value"
[clrDisabled]="!currentConfig.uaa_verify_cert.editable"></clr-checkbox>
<clr-checkbox id="uaa-cert" name="uaa-cert" [(clrChecked)]="currentConfig.uaa_verify_cert.value" [clrDisabled]="!currentConfig.uaa_verify_cert.editable"></clr-checkbox>
</div>
</section>
<section class="form-block" *ngIf="showLdap">
<div class="form-group">
<label for="ldapUrl" class="required">{{'CONFIG.LDAP.URL' | translate}}</label>
<label for="ldapUrl" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right" [class.invalid]="ldapUrlInput.invalid && (ldapUrlInput.dirty || ldapUrlInput.touched)">
<input name="ldapUrl" type="text" #ldapUrlInput="ngModel" [(ngModel)]="currentConfig.ldap_url.value"
required
id="ldapUrl"
size="40"
[disabled]="disabled(currentConfig.ldap_url)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<label for="ldapUrl" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right"
[class.invalid]="ldapUrlInput.invalid && (ldapUrlInput.dirty || ldapUrlInput.touched)">
<input name="ldapUrl" type="text" #ldapUrlInput="ngModel" [(ngModel)]="currentConfig.ldap_url.value" required id="ldapUrl"
size="40" [disabled]="disabled(currentConfig.ldap_url)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="ldapSearchDN">{{'CONFIG.LDAP.SEARCH_DN' | translate}}</label>
<label for="ldapSearchDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="ldapSearchDN" type="text" #ldapSearchDNInput="ngModel" [(ngModel)]="currentConfig.ldap_search_dn.value"
id="ldapSearchDN"
size="40" [disabled]="disabled(currentConfig.ldap_search_dn)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<input name="ldapSearchDN" type="text" #ldapSearchDNInput="ngModel" [(ngModel)]="currentConfig.ldap_search_dn.value" id="ldapSearchDN"
size="40" [disabled]="disabled(currentConfig.ldap_search_dn)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-1px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.LDAP_SEARCH_DN' | translate}}</span>
@ -71,25 +65,23 @@
<div class="form-group">
<label for="ldapSearchPwd">{{'CONFIG.LDAP.SEARCH_PWD' | translate}}</label>
<label for="ldapSearchPwd" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="ldapSearchPwd" type="password" #ldapSearchPwdInput="ngModel" [(ngModel)]="currentConfig.ldap_search_password.value"
id="ldapSearchPwd"
size="40" [disabled]="disabled(currentConfig.ldap_search_password)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<input name="ldapSearchPwd" type="password" #ldapSearchPwdInput="ngModel" [(ngModel)]="currentConfig.ldap_search_password.value"
id="ldapSearchPwd" size="40" [disabled]="disabled(currentConfig.ldap_search_password)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="ldapBaseDN" class="required">{{'CONFIG.LDAP.BASE_DN' | translate}}</label>
<label for="ldapBaseDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right" [class.invalid]="ldapBaseDNInput.invalid && (ldapBaseDNInput.dirty || ldapBaseDNInput.touched)">
<input name="ldapBaseDN" type="text" #ldapBaseDNInput="ngModel" [(ngModel)]="currentConfig.ldap_base_dn.value"
required
id="ldapBaseDN"
size="40" [disabled]="disabled(currentConfig.ldap_base_dn)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<label for="ldapBaseDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right"
[class.invalid]="ldapBaseDNInput.invalid && (ldapBaseDNInput.dirty || ldapBaseDNInput.touched)">
<input name="ldapBaseDN" type="text" #ldapBaseDNInput="ngModel" [(ngModel)]="currentConfig.ldap_base_dn.value" required id="ldapBaseDN"
size="40" [disabled]="disabled(currentConfig.ldap_base_dn)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top: -1px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.LDAP_BASE_DN' | translate}}</span>
@ -98,25 +90,23 @@
<div class="form-group">
<label for="ldapFilter">{{'CONFIG.LDAP.FILTER' | translate}}</label>
<label for="ldapFilter" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="ldapFilter" type="text" #ldapFilterInput="ngModel" [(ngModel)]="currentConfig.ldap_filter.value"
id="ldapFilter"
size="40" [disabled]="disabled(currentConfig.ldap_filter)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<input name="ldapFilter" type="text" #ldapFilterInput="ngModel" [(ngModel)]="currentConfig.ldap_filter.value" id="ldapFilter"
size="40" [disabled]="disabled(currentConfig.ldap_filter)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="ldapUid" class="required">{{'CONFIG.LDAP.UID' | translate}}</label>
<label for="ldapUid" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right" [class.invalid]="ldapUidInput.invalid && (ldapUidInput.dirty || ldapUidInput.touched)">
<input name="ldapUid" type="text" #ldapUidInput="ngModel" [(ngModel)]="currentConfig.ldap_uid.value"
required
id="ldapUid"
size="40" [disabled]="disabled(currentConfig.ldap_uid)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<label for="ldapUid" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right"
[class.invalid]="ldapUidInput.invalid && (ldapUidInput.dirty || ldapUidInput.touched)">
<input name="ldapUid" type="text" #ldapUidInput="ngModel" [(ngModel)]="currentConfig.ldap_uid.value" required id="ldapUid"
size="40" [disabled]="disabled(currentConfig.ldap_uid)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top: -1px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.LDAP_UID' | translate}}</span>
@ -138,13 +128,72 @@
</div>
<div class="form-group">
<label for="ldapVerifyCert">{{'CONFIG.LDAP.VERIFY_CERT' | translate}}</label>
<clr-checkbox name="ldapVerifyCert" id="ldapVerifyCert" [clrChecked]="currentConfig.ldap_verify_cert.value" [clrDisabled]="disabled(currentConfig.ldap_scope)" (clrCheckedChange)="setVerifyCertValue($event)">
<clr-checkbox name="ldapVerifyCert" id="ldapVerifyCert" [clrChecked]="currentConfig.ldap_verify_cert.value" [clrDisabled]="disabled(currentConfig.ldap_scope)"
(clrCheckedChange)="setVerifyCertValue($event)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-7px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_CERT' | translate}}</span>
</a>
</clr-checkbox>
</div>
<div class="form-group">
<label for="ldapGroupBaseDN" class="required">{{'CONFIG.LDAP.LDAP_GROUP_BASE_DN' | translate}}</label>
<label for="ldapGroupBaseDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="ldapGroupBaseDN" type="text" #ldapGroupDNInput="ngModel" [(ngModel)]="currentConfig.ldap_group_base_dn.value"
id="ldapGroupBaseDN" size="40" [disabled]="disabled(currentConfig.ldap_group_base_dn)">
<span class="tooltip-content"> {{'TOOLTIP.ITEM_REQUIRED' | translate}} </span>
</label>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.LDAP.LDAP_GROUP_BASE_DN_INFO' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
<div class="form-group">
<label for="ldapGroupFilter" class="required">{{'CONFIG.LDAP.LDAP_GROUP_FILTER' | translate}}</label>
<label for="ldapGroupFilter" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="ldapGroupFilter" type="text" #ldapGroupFilterInput="ngModel" [(ngModel)]="currentConfig.ldap_group_search_filter.value"
id="ldapGroupFilter" size="40" [disabled]="disabled(currentConfig.ldap_group_search_filter)">
<span class="tooltip-content"> {{'TOOLTIP.ITEM_REQUIRED' | translate}} </span>
</label>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.LDAP.LDAP_GROUP_FILTER_INFO' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
<div class="form-group">
<label for="ldapGroupGID" class="required">{{'CONFIG.LDAP.LDAP_GROUP_GID' | translate}}</label>
<label for="ldapGroupGID" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-lg tooltip-top-right">
<input name="ldapGroupGID" type="text" #ldapGroupDNInput="ngModel" [(ngModel)]="currentConfig.ldap_group_attribute_name.value"
id="ldapGroupGID" size="40" [disabled]="disabled(currentConfig.ldap_group_attribute_name)">
<span class="tooltip-content"> {{'TOOLTIP.ITEM_REQUIRED' | translate}} </span>
</label>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.LDAP.LDAP_GROUP_GID_INFO' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
<div class="form-group">
<label for="ldapGroupScope">{{'CONFIG.LDAP.GROUP_SCOPE' | translate}}</label>
<div class="select">
<select id="ldapGroupScope" name="ldapGroupScope" [(ngModel)]="currentConfig.ldap_group_search_scope.value" [disabled]="disabled(currentConfig.ldap_group_search_scope)">
<option value="0">{{'CONFIG.SCOPE_BASE' | translate }}</option>
<option value="1">{{'CONFIG.SCOPE_ONE_LEVEL' | translate }}</option>
<option value="2">{{'CONFIG.SCOPE_SUBTREE' | translate }}</option>
</select>
</div>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.LDAP.GROUP_SCOPE_INFO' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
</section>
<section class="form-block">
<div class="form-group">
@ -165,8 +214,10 @@
<clr-checkbox name="selfReg" id="selfReg" [(ngModel)]="currentConfig.self_registration.value" [disabled]="disabled(currentConfig.self_registration)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-7px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span *ngIf="checkable; else elseBlock" class="tooltip-content">{{'CONFIG.TOOLTIP.SELF_REGISTRATION_ENABLE' | translate}}</span>
<ng-template #elseBlock><span class="tooltip-content">{{'CONFIG.TOOLTIP.SELF_REGISTRATION_DISABLE' | translate}}</span></ng-template>
<span *ngIf="checkable; else elseBlock" class="tooltip-content">{{'CONFIG.TOOLTIP.SELF_REGISTRATION_ENABLE' | translate}}</span>
<ng-template #elseBlock>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.SELF_REGISTRATION_DISABLE' | translate}}</span>
</ng-template>
</a>
</clr-checkbox>
</div>

View File

@ -0,0 +1,3 @@
clr-tooltip {
top: -1px;
}

View File

@ -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 });

View File

@ -0,0 +1,43 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true" [clrModalClosable]="false">
<h3 class="modal-title" *ngIf="mode === 'create'">{{'GROUP.IMPORT_LDAP_GROUP' | translate}}</h3>
<h3 class="modal-title" *ngIf="mode !== 'create'">{{'GROUP.EDIT' | translate}}</h3>
<div class="modal-body">
<form class="form" #groupForm="ngForm">
<section class="form-block">
<div class="form-group">
<label for="ldap_group_dn" class="required">{{ 'GROUP.GROUP_DN' | translate}}</label>
<label for="ldap_group_dn"
aria-haspopup="true"
role="tooltip"
class="tooltip tooltip-validation tooltip-sm tooltip-right"
[class.invalid]="isDNInvalid">
<input type="text" id="ldap_group_dn" name="ldap_group_dn"
required
[(ngModel)]="group.ldap_group_dn"
#groupDN="ngModel">
<span class="tooltip-content">
{{dnTooltip | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="type">{{'GROUP.TYPE' | translate}}</label>
<label id="type">LDAP</label>
</div>
<div class="form-group">
<label for="group_name">{{'GROUP.NAME' | translate}}</label>
<label for="group_name">
<input type="text" id="group_name" name="group_name"
[(ngModel)]="group.group_name"
#groupDN="ngModel">
</label>
</div>
</section>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isFormValid" (click)="save()">{{'BUTTON.SAVE' | translate | translate}}</button>
</div>
</clr-modal>

View File

@ -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<AddGroupModalComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddGroupModalComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddGroupModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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();
}
}

View File

@ -0,0 +1,40 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="custom-h2">{{'GROUP.GROUP' | translate}}</h2>
<div class="action-panel-pos rightPos">
<hbr-filter [withDivider]="true" class="filter-pos" filterPlaceholder='group name' (filterEvt)="doFilter($event)" [currentValue]="currentTerm"></hbr-filter>
<span class="refresh-btn">
<clr-icon shape="refresh" [hidden]="loading" ng-disabled="loading" (click)="refresh()"></clr-icon>
<span class="spinner spinner-inline" [hidden]="loading === false"></span>
</span>
</div>
<div>
<clr-datagrid [(clrDgSelected)]="selectedGroups" [clrDgLoading]="loading">
<clr-dg-action-bar >
<button type="button" class="btn btn-sm btn-secondary" (click)="addGroup()" [disabled]="!canAddGroup">
<clr-icon shape="plus" size="15"></clr-icon>&nbsp;{{'GROUP.ADD' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" (click)="editGroup()" [disabled]="!canEditGroup">
<clr-icon shape="pencil" size="15"></clr-icon>&nbsp;{{'GROUP.EDIT' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" (click)="openDeleteConfirmationDialog()" [disabled]="!canAddGroup">
<clr-icon shape="times" size="15"></clr-icon>&nbsp;{{'GROUP.DELETE' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column>{{'GROUP.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'GROUP.TYPE' | translate}}</clr-dg-column>
<clr-dg-column>{{'GROUP.DN' | 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>{{groupToSring(group.group_type) | translate}}</clr-dg-cell>
<clr-dg-cell>{{group.ldap_group_dn}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="15">
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'USER.OF' | translate }} {{pagination.totalItems}} {{'GROUP.GROUPS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
<hbr-add-group-modal (dataChange)="loadData()"></hbr-add-group-modal>
</div>
</div>

View File

@ -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;
}

View File

@ -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<GroupComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ GroupComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GroupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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
);
}
}

View File

@ -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 { }

View File

@ -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();
}));
});

View File

@ -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<UserGroup[]> {
return this.http.get(userGroupEndpoint, HTTP_GET_OPTIONS)
.map(response => {
return this.extractData(response);
})
.catch(error => {
return this.handleErrorObservable(error);
});
}
createGroup(group: UserGroup): Observable<any> {
return this.http
.post(userGroupEndpoint, group, HTTP_JSON_OPTIONS)
.map(response => {
return this.extractData(response);
})
.catch(this.handleErrorObservable);
}
getGroup(group_id: number): Observable<UserGroup> {
return this.http
.get(`${userGroupEndpoint}/${group_id}`, HTTP_JSON_OPTIONS)
.map(response => {
return this.extractData(response);
})
.catch(this.handleErrorObservable);
}
editGroup(group: UserGroup): Observable<any> {
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<any> {
return this.http
.delete(`${userGroupEndpoint}/${group_id}`)
.map(response => {
return this.extractData(response);
})
.catch(this.handleErrorObservable);
}
searchGroup(group_name: string): Observable<UserGroup[]> {
return this.http
.get(`${ldapGroupSearchEndpoint}${group_name}`, HTTP_GET_OPTIONS)
.map(response => {
return this.extractData(response);
})
.catch(this.handleErrorObservable);
}
}

View File

@ -0,0 +1,12 @@
export class UserGroup {
id?: number;
group_name?: string;
group_type: number;
ldap_group_dn?: string;
constructor() {
{
this.group_type = 1;
}
}
}

View File

@ -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,

View File

@ -0,0 +1,104 @@
<clr-modal [(clrModalOpen)]="opened" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="'true'" [clrModalClosable]="false">
<h3 class="modal-title">{{'MEMBER.IMPORT_GROUP' | translate}}</h3>
<div class="modal-body">
<label>{{ 'MEMBER.NEW_GROUP_INFO' | translate}}</label>
<div class="form-group modeSelectradios">
<div class="radio">
<input type="radio" name="modeRadios" [value]="false" id="select_group" [(ngModel)]="createGroupMode">
<label for="select_group">{{'MEMBER.ADD_GROUP_SELECT' | translate}}</label>
</div>
<div class="radio">
<input type="radio" name="modeRadios" [value]="true" id="create_group" [(ngModel)]="createGroupMode">
<label for="create_group">{{'MEMBER.CREATE_GROUP_SELECT' | translate}}</label>
</div>
</div>
<div *ngIf="createGroupMode">
<form #groupForm="ngForm">
<section class="form-block">
<div class="form-group">
<label for="ldap_group_dn" class="required">{{ 'MEMBER.LDAP_SEARCH_DN' | translate}}</label>
<label for="ldap_group_dn"
aria-haspopup="true"
role="tooltip"
class="tooltip tooltip-validation tooltip-md tooltip-right"
[class.invalid]="isDNInvalid">
<input type="text" name="ldap_group_dn" size="45"
required
[(ngModel)]="group.ldap_group_dn"
#groupDN="ngModel">
<span class="tooltip-content">
{{dnTooltip | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="name">{{'MEMBER.LDAP_SEARCH_NAME' | translate}}</label>
<input type="text" name="ldap_group_name" size="35" [(ngModel)]="group.group_name">
</div>
<div class="form-group">
<label for="member_role1">{{ 'MEMBER.ROLE' | translate}}</label>
<div class="select">
<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>
</section>
</form>
</div>
<div *ngIf="!createGroupMode">
<div class='row flex-items-xs-between'>
<div></div>
<div class="filterTool">
<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 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">
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'USER.OF' | translate }} {{pagination.totalItems}} {{'MEMBER.GROUPS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<div class="row">
<div class="col-lg-1 col-md-1 col-sm-1 col-xs-1">
<label>{{ 'MEMBER.ROLE' | translate}}</label>
</div>
<div class="class=col-lg-4 col-md-4 col-sm-2 col-xs-1">
<div class="select">
<select id="member_role2" [(ngModel)]="selectedRole">
<option *ngFor="let role of roles" [ngValue]="role.id"> {{role.value | translate}}</option>
</select>
</div>
</div>
</div>
</div>
</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>
</div>
</clr-modal>

View File

@ -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;
}

View File

@ -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<AddGroupComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddGroupComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddGroupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<boolean>();
@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;
}
}

View File

@ -1,18 +1,19 @@
<clr-modal [(clrModalOpen)]="addMemberOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<h3 class="modal-title">{{'MEMBER.NEW_USER' | translate}}</h3>
<div class="modal-body">
<label>{{ 'MEMBER.NEW_USER_INFO' | translate}}</label>
<form #memberForm="ngForm">
<section class="form-block">
<div class="form-group">
<label for="member_name" class="col-md-4 form-group-label-override required">{{'MEMBER.NAME' | translate}}</label>
<label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="!isMemberNameValid"
class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" (mouseleave)="leaveInput()">
class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left" (mouseleave)="leaveInput()">
<input type="text" id="member_name" [(ngModel)]="member.entity_name"
name="member_name"
size="20"
#memberName="ngModel"
required
size="20"
#memberName="ngModel"
required
(keyup)='handleValidation()' autocomplete="off">
<span class="tooltip-content">
{{ memberTooltip | translate }}
@ -28,15 +29,15 @@
<div class="form-group">
<label class="col-md-4 form-group-label-override">{{'MEMBER.ROLE' | translate}}</label>
<div class="radio">
<input type="radio" name="member_role" id="checkrads_project_admin" [value]="1" [(ngModel)]="member.role_id">
<input type="radio" name="member_role" id="checkrads_project_admin" [value]=1 [(ngModel)]="member.role_id">
<label for="checkrads_project_admin">{{'MEMBER.PROJECT_ADMIN' | translate}}</label>
</div>
<div class="radio">
<input type="radio" name="member_role" id="checkrads_developer" [value]="2" [(ngModel)]="member.role_id">
<input type="radio" name="member_role" id="checkrads_developer" [value]=2 [(ngModel)]="member.role_id">
<label for="checkrads_developer">{{'MEMBER.DEVELOPER' | translate}}</label>
</div>
<div class="radio">
<input type="radio" name="member_role" id="checkrads_guest" [value]="3" [(ngModel)]="member.role_id">
<input type="radio" name="member_role" id="checkrads_guest" [value]=3 [(ngModel)]="member.role_id">
<label for="checkrads_guest">{{'MEMBER.GUEST' | translate}}</label>
</div>
</div>

View File

@ -23,4 +23,4 @@
color: #262626;
background-image: linear-gradient(180deg,#f5f5f5 0,#e8e8e8);
background-repeat: repeat-x;
}
}

View File

@ -86,19 +86,18 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
private ref: ChangeDetectorRef) { }
ngOnInit(): void {
let resolverData = this.route.snapshot.parent.data;
let hasProjectAdminRole: boolean;
if (resolverData) {
hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
if (hasProjectAdminRole) {
this.userService.getUsers()
let resolverData = this.route.snapshot.parent.data;
let hasProjectAdminRole: boolean;
if (resolverData) {
hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
if (hasProjectAdminRole) {
this.userService.getUsers()
.then(users => {
this.userLists = users;
});
this.nameChecker
this.nameChecker
.debounceTime(500)
.distinctUntilChanged()
.subscribe((name: string) => {
@ -108,17 +107,17 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
if (cont.valid) {
this.checkOnGoing = true;
this.memberService
.listMembers(this.projectId, cont.value).toPromise()
.then((members: Member[]) => {
if (members.filter(m => { return m.entity_name === cont.value; }).length > 0) {
this.isMemberNameValid = false;
this.memberTooltip = 'MEMBER.USERNAME_ALREADY_EXISTS';
}
this.checkOnGoing = false;
})
.catch(error => {
this.checkOnGoing = false;
});
.listMembers(this.projectId, cont.value).toPromise()
.then((members: Member[]) => {
if (members.filter(m => { return m.entity_name === cont.value; }).length > 0) {
this.isMemberNameValid = false;
this.memberTooltip = 'MEMBER.USERNAME_ALREADY_EXISTS';
}
this.checkOnGoing = false;
})
.catch(error => {
this.checkOnGoing = false;
});
// username autocomplete
if (this.userLists && this.userLists.length) {
this.selectUserName = [];
@ -129,17 +128,17 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
}
}
});
let changeTimer = setInterval(() => this.ref.detectChanges(), 200);
setTimeout(() => {
setInterval(() => this.ref.markForCheck(), 100);
}, 1000);
clearInterval(changeTimer);
}, 2000);
}
} else {
this.memberTooltip = 'MEMBER.USERNAME_IS_REQUIRED';
}
}
});
}
}
}
ngOnDestroy(): void {
@ -149,12 +148,20 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
onSubmit(): void {
if (!this.member.entity_name || this.member.entity_name.length === 0) { return; }
this.memberService
.addMember(this.projectId, this.member.entity_name, +this.member.role_id)
.addUserMember(this.projectId, {username: this.member.entity_name}, +this.member.role_id)
.finally(() => {
this.addMemberOpened = false;
let changeTimer = setInterval(() => this.ref.detectChanges(), 200);
setTimeout(() => {
clearInterval(changeTimer);
}, 2000);
}
)
.subscribe(
response => {
() => {
this.messageHandlerService.showSuccess('MEMBER.ADDED_SUCCESS');
this.added.emit(true);
this.addMemberOpened = false;
// this.addMemberOpened = false;
},
error => {
if (error instanceof Response) {
@ -171,19 +178,15 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
}
if (this.messageHandlerService.isAppLevel(error)) {
this.messageHandlerService.handleError(error);
this.addMemberOpened = false;
// this.addMemberOpened = false;
} else {
this.translateService
.get(errorMessageKey)
.subscribe(errorMessage => this.inlineAlert.showInlineError(errorMessage));
.subscribe(errorMessage => this.messageHandlerService.handleError(errorMessage));
}
}
}
);
setTimeout(() => {
setInterval(() => this.ref.markForCheck(), 100);
}, 1000);
});
// this.addMemberOpened = false;
}
selectedName(username: string) {
@ -192,12 +195,8 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
}
onCancel() {
if (this.hasChanged) {
this.inlineAlert.showInlineConfirmation({ message: 'ALERT.FORM_CHANGE_CONFIRMATION' });
} else {
this.addMemberOpened = false;
this.memberForm.reset();
}
}
leaveInput() {
@ -212,7 +211,6 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
let memberName = data['member_name'];
if (memberName && memberName !== '') {
this.hasChanged = true;
this.inlineAlert.close();
} else {
this.hasChanged = false;
}
@ -220,12 +218,6 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
}
}
confirmCancel(confirmed: boolean) {
this.addMemberOpened = false;
this.inlineAlert.close();
this.memberForm.reset();
}
openAddMemberModal(): void {
this.currentForm.reset();
this.member = new Member();

View File

@ -14,27 +14,33 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-dg-action-bar>
<button class="btn btn-sm btn-secondary" (click)="openAddMemberModal()" [disabled]="!hasProjectAdminRole">
<span><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'MEMBER.NEW_MEMBER' | translate }}</span>
<span><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'MEMBER.NEW_USER' | translate }}</span>
</button>
<clr-dropdown id='member-action' [clrCloseMenuOnItemClick]="false" class="btn btn-sm btn-secondary" clrDropdownTrigger>
<button class="btn btn-sm btn-secondary" (click)="openAddGroupModal()" [disabled]="!hasProjectAdminRole">
<span><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'MEMBER.NEW_GROUP' | translate}}</span>
</button>
<clr-dropdown id='member-action' [clrCloseMenuOnItemClick]="false" class="btn btn-sm btn-link" clrDropdownTrigger>
<span>{{'MEMBER.ACTION' | translate}}<clr-icon shape="caret down"></clr-icon></span>
<clr-dropdown-menu *clrIfOpen>
<button class="btn btn-sm btn-secondary" (click)="changeRole(selectedRow, 1)" [disabled]="!(selectedRow.length && hasProjectAdminRole) || onlySelf">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button class="btn btn-sm btn-secondary" (click)="changeRole(selectedRow, 2)" [disabled]="!(selectedRow.length && hasProjectAdminRole) || onlySelf">{{'MEMBER.DEVELOPER' | translate}}</button>
<button class="btn btn-sm btn-secondary" (click)="changeRole(selectedRow, 3)" [disabled]="!(selectedRow.length && hasProjectAdminRole) || onlySelf">{{'MEMBER.GUEST' | translate}}</button>
<label class="dropdown-header">{{'MEMBER.SET_ROLE' | translate}}</label>
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 1)" [disabled]="!(selectedRow.length && hasProjectAdminRole) || onlySelf">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 2)" [disabled]="!(selectedRow.length && hasProjectAdminRole) || onlySelf">{{'MEMBER.DEVELOPER' | translate}}</button>
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 3)" [disabled]="!(selectedRow.length && hasProjectAdminRole) || onlySelf">{{'MEMBER.GUEST' | translate}}</button>
<div class="dropdown-divider"></div>
<button clrDropdownItem (click)="openDeleteMembersDialog(selectedRow)" [disabled]="!(selectedRow.length && hasProjectAdminRole) || onlySelf">{{'MEMBER.REMOVE' | translate}}</button>
</clr-dropdown-menu>
</clr-dropdown>
<button class="btn btn-sm btn-secondary" (click)="deleteMembers(selectedRow)" [disabled]="!(selectedRow.length && hasProjectAdminRole) || onlySelf">
<span><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'MEMBER.REMOVE' | translate}}</span>
</button>
</clr-dg-action-bar>
<clr-datagrid [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="SelectedChange()">
<clr-datagrid [(clrDgSelected)]="selectedRow" [clrDgLoading]="loading">
<clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.MEMBER_TYPE'| translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let m of members" [clrDgItem]="m">
<clr-dg-cell>{{m.entity_name}}</clr-dg-cell>
<clr-dg-cell>{{member_type_toString( m.entity_type) | translate}}</clr-dg-cell>
<clr-dg-cell>
<span>{{roleInfo[m.role_id] | translate}}</span>
<span *ngIf="ChangeRoleOngoing(m.id)" class="spinner spinner-inline"></span>
<span *ngIf="!ChangeRoleOngoing(m.id)">{{roleInfo[m.role_id] | translate}}</span>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
@ -45,4 +51,5 @@
</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>
</div>

View File

@ -22,4 +22,10 @@
z-index: 100;
right: 35px;
margin-top: 4px;
}
clr-datagrid {
::ng-deep clr-checkbox {
position: inherit;
}
}

View File

@ -14,31 +14,27 @@
import { Component, OnInit, ViewChild, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SessionUser } from "../../shared/session-user";
import { Member } from "./member";
import { MemberService } from "./member.service";
import { AddMemberComponent } from "./add-member/add-member.component";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from "../../shared/shared.const";
import { ConfirmationDialogService } from "../../shared/confirmation-dialog/confirmation-dialog.service";
import { ConfirmationMessage } from "../../shared/confirmation-dialog/confirmation-message";
import { SessionService } from "../../shared/session.service";
import { RoleInfo } from "../../shared/shared.const";
import "rxjs/add/operator/switchMap";
import "rxjs/add/operator/catch";
import "rxjs/add/operator/map";
import "rxjs/add/observable/throw";
import { Subscription } from "rxjs/Subscription";
import { Project } from "../../project/project";
import {TranslateService} from "@ngx-translate/core";
import {operateChanges, OperateInfo, OperationService, OperationState} from "harbor-ui";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from "../../shared/shared.const";
import { ConfirmationDialogService } from "../../shared/confirmation-dialog/confirmation-dialog.service";
import { ConfirmationMessage } from "../../shared/confirmation-dialog/confirmation-message";
import { SessionService } from "../../shared/session.service";
import { RoleInfo } from "../../shared/shared.const";
import { Project } from "../../project/project";
import { Member } from "./member";
import { SessionUser } from "../../shared/session-user";
import { AddGroupComponent } from './add-group/add-group.component';
import { MemberService } from "./member.service";
import { AddMemberComponent } from "./add-member/add-member.component";
@Component({
templateUrl: "member.component.html",
styleUrls: ["./member.component.scss"],
@ -51,17 +47,25 @@ export class MemberComponent implements OnInit, OnDestroy {
roleInfo = RoleInfo;
delSub: Subscription;
@ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent;
currentUser: SessionUser;
hasProjectAdminRole: boolean;
batchOps = 'delete';
searchMember: string;
selectedRow: Member[] = [];
roleNum: number;
isDelete = false;
isChangeRole = false;
loading = false;
isChangingRole = false;
batchChangeRoleInfos = {};
@ViewChild(AddMemberComponent)
addMemberComponent: AddMemberComponent;
@ViewChild(AddGroupComponent)
addGroupComponent: AddGroupComponent;
constructor(
private route: ActivatedRoute,
@ -78,11 +82,8 @@ export class MemberComponent implements OnInit, OnDestroy {
if (message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.PROJECT_MEMBER) {
if (this.isDelete) {
this.deleteMem(message.data);
}
if (this.isChangeRole) {
this.changeOpe(message.data);
if (this.batchOps === 'delete') {
this.deleteMembers(message.data);
}
}
});
@ -90,22 +91,6 @@ export class MemberComponent implements OnInit, OnDestroy {
setTimeout(() => clearInterval(hnd), 1000);
}
retrieve(projectId: number, username: string) {
this.selectedRow = [];
this.memberService
.listMembers(projectId, username)
.subscribe(
response => {
this.members = response;
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 1000);
},
error => {
this.router.navigate(["/harbor", "projects"]);
this.messageHandlerService.handleError(error);
});
}
ngOnDestroy() {
if (this.delSub) {
this.delSub.unsubscribe();
@ -124,147 +109,6 @@ export class MemberComponent implements OnInit, OnDestroy {
this.retrieve(this.projectId, "");
}
openAddMemberModal() {
this.addMemberComponent.openAddMemberModal();
}
addedMember($event: any) {
this.searchMember = "";
this.retrieve(this.projectId, "");
}
get onlySelf(): boolean {
if (this.selectedRow.length === 1 && this.selectedRow[0].entity_id === this.currentUser.user_id) {
return true;
}
return false;
}
changeRole(m: Member[], roleId: number) {
if (m && m.length) {
this.isDelete = false;
this.isChangeRole = true;
this.roleNum = roleId;
this.changeOpe(m);
}
}
changeOpe(members: Member[]) {
if (members && members.length) {
let promiseList: any[] = [];
members.forEach(member => {
promiseList.push(this.changeOperate(this.projectId, this.roleNum, member));
});
Promise.all(promiseList).then(num => {
this.retrieve(this.projectId, "");
},
);
}
}
changeOperate(projectId: number, roleId: number, member: Member) {
// init operation info
let operMessage = new OperateInfo();
operMessage.name = 'OPERATION.SWITCH_ROLE';
operMessage.data.id = member.id;
operMessage.state = OperationState.progressing;
operMessage.data.name = member.entity_name;
this.operationService.publishInfo(operMessage);
if (member.entity_id === this.currentUser.user_id) {
this.translate.get("BATCH.SWITCH_FAILURE").subscribe(res => {
operateChanges(operMessage, OperationState.failure, res);
});
return null;
}
return this.memberService
.changeMemberRole(projectId, member.id, roleId)
.then(
response => {
this.translate.get("BATCH.SWITCH_SUCCESS").subscribe(res => {
operateChanges(operMessage, OperationState.success);
});
},
error => {
this.translate.get("BATCH.SWITCH_FAILURE").subscribe(res => {
operateChanges(operMessage, OperationState.failure, res);
});
}
);
}
deleteMembers(m: Member[]) {
this.isDelete = true;
this.isChangeRole = false;
let nameArr: string[] = [];
if (m && m.length) {
m.forEach(data => {
nameArr.push(data.entity_name);
});
let deletionMessage = new ConfirmationMessage(
"MEMBER.DELETION_TITLE",
"MEMBER.DELETION_SUMMARY",
nameArr.join(","),
m,
ConfirmationTargets.PROJECT_MEMBER,
ConfirmationButtons.DELETE_CANCEL
);
this.OperateDialogService.openComfirmDialog(deletionMessage);
}
}
deleteMem(members: Member[]) {
if (members && members.length) {
let promiseLists: any[] = [];
members.forEach(member => {
promiseLists.push(this.delOperate(this.projectId, member));
});
Promise.all(promiseLists).then(item => {
this.selectedRow = [];
this.retrieve(this.projectId, "");
});
}
}
delOperate(projectId: number, member: Member) {
// init operation info
let operMessage = new OperateInfo();
operMessage.name = 'OPERATION.DELETE_MEMBER';
operMessage.data.id = member.id;
operMessage.state = OperationState.progressing;
operMessage.data.name = member.entity_name;
this.operationService.publishInfo(operMessage);
if (member.entity_id === this.currentUser.user_id) {
this.translate.get("BATCH.DELETED_FAILURE").subscribe(res => {
operateChanges(operMessage, OperationState.failure, res);
});
return null;
}
return this.memberService
.deleteMember(projectId, member.id)
.then(
response => {
this.translate.get("BATCH.DELETED_SUCCESS").subscribe(res => {
operateChanges(operMessage, OperationState.success);
});
},
error => {
this.translate.get("BATCH.DELETED_FAILURE").subscribe(res => {
operateChanges(operMessage, OperationState.failure, res);
});
}
);
}
SelectedChange(): void {
// this.forceRefreshView(5000);
}
doSearch(searchMember: string) {
this.searchMember = searchMember;
this.retrieve(this.projectId, this.searchMember);
@ -273,4 +117,154 @@ export class MemberComponent implements OnInit, OnDestroy {
refresh() {
this.retrieve(this.projectId, "");
}
retrieve(projectId: number, username: string) {
this.loading = true;
this.selectedRow = [];
this.memberService
.listMembers(projectId, username)
.finally(() => this.loading = false)
.subscribe(
response => {
this.members = response;
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 1000);
},
error => {
this.router.navigate(["/harbor", "projects"]);
this.messageHandlerService.handleError(error);
});
}
get onlySelf(): boolean {
if (this.selectedRow.length === 1 &&
this.selectedRow[0].entity_type === 'u' &&
this.selectedRow[0].entity_id === this.currentUser.user_id) {
return true;
}
return false;
}
member_type_toString(user_type: string) {
if (user_type === 'u') {
return 'MEMBER.USER_TYPE';
} else {
return 'MEMBER.GROUP_TYPE';
}
}
// Add member
openAddMemberModal() {
this.addMemberComponent.openAddMemberModal();
}
addedMember(result: boolean) {
this.searchMember = "";
this.retrieve(this.projectId, "");
}
// Add group
openAddGroupModal() {
this.addGroupComponent.open();
}
addedGroup(result: boolean) {
this.searchMember = "";
this.retrieve(this.projectId, "");
}
changeMembersRole(members: Member[], roleId: number) {
if (!members) {
return;
}
let changeOperate = (projectId: number, member: Member, ) => {
return this.memberService
.changeMemberRole(projectId, member.id, roleId)
.then( () => this.batchChangeRoleInfos[member.id] = 'done')
.catch(error => this.messageHandlerService.handleError(error + ": " + member.entity_name));
};
// Preparation for members role change
this.batchChangeRoleInfos = {};
let RoleChangePromises: Promise<any>[] = [];
members.forEach(member => {
if (member.entity_type === 'u' && member.entity_id === this.currentUser.user_id) {
return;
}
this.batchChangeRoleInfos[member.id] = 'pending';
RoleChangePromises.push(changeOperate(this.projectId, member));
});
Promise.all(RoleChangePromises).then(() => {
this.retrieve(this.projectId, "");
});
}
ChangeRoleOngoing(entity_id: number) {
return this.batchChangeRoleInfos[entity_id] === 'pending';
}
// Delete members
openDeleteMembersDialog(members: Member[]) {
this.batchOps = 'delete';
let nameArr: string[] = [];
if (members && members.length) {
members.forEach(data => {
nameArr.push(data.entity_name);
});
let deletionMessage = new ConfirmationMessage(
"MEMBER.DELETION_TITLE",
"MEMBER.DELETION_SUMMARY",
nameArr.join(","),
members,
ConfirmationTargets.PROJECT_MEMBER,
ConfirmationButtons.DELETE_CANCEL
);
this.OperateDialogService.openComfirmDialog(deletionMessage);
}
}
deleteMembers(members: Member[]) {
if (!members) { return; }
let memberDeletingPromises: Promise<any>[] = [];
// Function to delete specific member
let deleteMember = (projectId: number, member: Member) => {
let operMessage = new OperateInfo();
operMessage.name = 'OPERATION.DELETE_MEMBER';
operMessage.data.id = member.id;
operMessage.state = OperationState.progressing;
operMessage.data.name = member.entity_name;
this.operationService.publishInfo(operMessage);
if (member.entity_type === 'u' && member.entity_id === this.currentUser.user_id) {
this.translate.get("BATCH.DELETED_FAILURE").subscribe(res => {
operateChanges(operMessage, OperationState.failure, res);
});
return null;
}
return this.memberService
.deleteMember(projectId, member.id)
.then(response => {
this.translate.get("BATCH.DELETED_SUCCESS").subscribe(res => {
operateChanges(operMessage, OperationState.success);
});
})
.catch(error => {
this.translate.get("BATCH.DELETED_FAILURE").subscribe(res => {
operateChanges(operMessage, OperationState.failure, res);
});
});
};
// Deleting member then wating for results
members.forEach(member => memberDeletingPromises.push(deleteMember(this.projectId, member)));
Promise.all(memberDeletingPromises).then(() => {
this.selectedRow = [];
this.batchOps = 'idle';
this.retrieve(this.projectId, "");
});
}
}

View File

@ -19,24 +19,48 @@ import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
import { Member } from './member';
import {HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS} from "../../shared/shared.utils";
import { User } from '../../user/user';
import { Member } from './member';
@Injectable()
export class MemberService {
constructor(private http: Http) {}
listMembers(projectId: number, username: string): Observable<Member[]> {
listMembers(projectId: number, entity_name: string): Observable<Member[]> {
return this.http
.get(`/api/projects/${projectId}/members?entityname=${username}`, HTTP_GET_OPTIONS)
.get(`/api/projects/${projectId}/members?entityname=${entity_name}`, HTTP_GET_OPTIONS)
.map(response => response.json() as Member[])
.catch(error => Observable.throw(error));
}
addMember(projectId: number, username: string, roleId: number): Observable<any> {
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(
`/api/projects/${projectId}/members`,
{
role_id: roleId,
member_user: member_user
},
HTTP_JSON_OPTIONS)
.map(response => response.status)
.catch(error => Observable.throw(error));
}
addGroupMember(projectId: number, group: any, roleId: number): Observable<any> {
return this.http
.post(`/api/projects/${projectId}/members`, { role_id: roleId, member_user: {username: username} }, HTTP_JSON_OPTIONS)
.post(`/api/projects/${projectId}/members`,
{ role_id: roleId, member_group: group},
HTTP_JSON_OPTIONS)
.map(response => response.status)
.catch(error => Observable.throw(error));
}
@ -48,9 +72,9 @@ export class MemberService {
.catch(error => Promise.reject(error));
}
deleteMember(projectId: number, userId: number): Promise<any> {
deleteMember(projectId: number, memberId: number): Promise<any> {
return this.http
.delete(`/api/projects/${projectId}/members/${userId}`).toPromise()
.delete(`/api/projects/${projectId}/members/${memberId}`).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}

View File

@ -26,13 +26,14 @@ import { ListProjectComponent } from './list-project/list-project.component';
import { ProjectDetailComponent } from './project-detail/project-detail.component';
import { MemberComponent } from './member/member.component';
import { AddMemberComponent } from './member/add-member/add-member.component';
import { AddGroupComponent } from './member/add-group/add-group.component';
import { ProjectService } from './project.service';
import { MemberService } from './member/member.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service';
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
import {ProjectLabelComponent} from "../project/project-label/project-label.component";
import { ProjectLabelComponent } from "../project/project-label/project-label.component";
@NgModule({
imports: [
@ -50,7 +51,8 @@ import {ProjectLabelComponent} from "../project/project-label/project-label.comp
MemberComponent,
AddMemberComponent,
TargetExistsValidatorDirective,
ProjectLabelComponent
ProjectLabelComponent,
AddGroupComponent
],
exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, ProjectService, MemberService]

View File

@ -78,3 +78,8 @@ export const enum ConfirmationButtons {
export const ProjectTypes = { 0: 'PROJECT.ALL_PROJECTS', 1: 'PROJECT.PRIVATE_PROJECTS', 2: 'PROJECT.PUBLIC_PROJECTS' };
export const RoleInfo = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST' };
export const RoleMapping = { 'projectAdmin': 'MEMBER.PROJECT_ADMIN', 'developer': 'MEMBER.DEVELOPER', 'guest': 'MEMBER.GUEST' };
export const ProjectRoles = [
{ id: 1, value: "MEMBER.PROJECT_ADMIN" },
{ id: 2, value: "MEMBER.DEVELOPER" },
{ id: 3, value: "MEMBER.GUEST" }
];

View File

@ -14,6 +14,7 @@
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { TranslateModule } from "@ngx-translate/core";
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CookieService } from "ngx-cookie";
import {
IServiceConfig,
@ -65,6 +66,8 @@ const uiLibConfig: IServiceConfig = {
CoreModule,
TranslateModule,
RouterModule,
FormsModule,
ReactiveFormsModule,
HarborLibraryModule.forRoot({
config: { provide: SERVICE_CONFIG, useValue: uiLibConfig },
errorHandler: { provide: ErrorHandler, useClass: MessageHandlerService }
@ -103,7 +106,9 @@ const uiLibConfig: IServiceConfig = {
ListProjectROComponent,
ListRepositoryROComponent,
GaugeComponent,
DateValidatorDirective
DateValidatorDirective,
FormsModule,
ReactiveFormsModule
],
providers: [
SessionService,

View File

@ -13,8 +13,13 @@
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" (click)="addNewUser()" [disabled]="!canCreateUser"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'USER.ADD_ACTION' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" id="set-admin" [disabled]="!ifSameRole" (click)="changeAdminRole()" ><clr-icon shape="wrench" size="16"></clr-icon>&nbsp;{{ISADMNISTRATOR | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" id="changePwd" [hidden]="!canCreateUser" [disabled]="!(selectedRow.length==1)" (click)="openChangePwdModal()" ><clr-icon shape="edit" size="16"></clr-icon>&nbsp;{{'RESET_PWD.TITLE' | translate | uppercase}}</button>
<button type="button" class="btn btn-sm btn-secondary" (click)="deleteUsers(selectedRow)" [disabled]="!selectedRow.length || onlySelf || !canCreateUser"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'USER.DEL_ACTION' | translate}}</button>
<clr-dropdown id='member-action' [clrCloseMenuOnItemClick]="false" class="btn btn-sm btn-link" clrDropdownTrigger>
<span>{{'BUTTON.ACTIONS' | translate}}<clr-icon shape="caret down"></clr-icon></span>
<clr-dropdown-menu *clrIfOpen>
<button type="button" class="btn btn-sm btn-secondary" id="changePwd" [hidden]="!canCreateUser" [disabled]="!(selectedRow.length==1)" (click)="openChangePwdModal()" ><clr-icon shape="edit" size="16"></clr-icon>&nbsp;{{'RESET_PWD.TITLE' | translate | uppercase}}</button>
<button type="button" class="btn btn-sm btn-secondary" (click)="deleteUsers(selectedRow)" [disabled]="!selectedRow.length || onlySelf || !canCreateUser"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'USER.DEL_ACTION' | translate}}</button>
</clr-dropdown-menu>
</clr-dropdown>
</clr-dg-action-bar>
<clr-dg-column>{{'USER.COLUMN_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_ADMIN' | translate}}</clr-dg-column>

View File

@ -15,10 +15,12 @@ import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { User } from './user';
import {HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS} from "../shared/shared.utils";
import { User, LDAPUser } from './user';
import LDAPUsertoUser from './user';
const userMgmtEndpoint = '/api/users';
const ldapUserEndpoint = '/api/ldap/users';
/**
* Define related methods to handle account and session corresponding things
@ -81,11 +83,33 @@ export class UserService {
}
return this.http.put(userMgmtEndpoint + '/' + uid + '/password',
{"old_password": newPassword, 'new_password': confirmPwd}, HTTP_JSON_OPTIONS)
{
"old_password": newPassword,
'new_password': confirmPwd
},
HTTP_JSON_OPTIONS)
.toPromise()
.then(response => response)
.catch(error => {
return Promise.reject(error);
});
}
// Get User from LDAP
getLDAPUsers(username: string): Promise<User[]> {
return this.http.get(`${ldapUserEndpoint}/search?username=${username}`, HTTP_GET_OPTIONS)
.toPromise()
.then(response => {
let ldapUser = response.json() as LDAPUser[] || [];
return ldapUser.map(u => LDAPUsertoUser(u));
})
.catch( error => this.handleError(error));
}
importLDAPUsers(usernames: string[]): Promise<any> {
return this.http.post(`${ldapUserEndpoint}/import`, JSON.stringify({ldap_uid_list: usernames}), HTTP_JSON_OPTIONS)
.toPromise()
.then(() => null )
.catch(err => this.handleError(err));
}
}

View File

@ -18,12 +18,31 @@
* @class User
*/
export class User {
user_id: number;
user_id?: number;
username?: string;
realname?: string;
email?: string;
password?: string;
comment?: string;
deleted?: boolean;
role_name?: string;
role_id?: number;
has_admin_role?: boolean;
reset_uuid?: string;
creation_time?: string;
update_time?: string;
}
export interface LDAPUser {
ldap_username: string;
ldap_realname: string;
ldap_email: string;
}
function LDAPUsertoUser(ldapU: LDAPUser): User {
let user = new User();
user.user_id = 0;
user.username = ldapU.ldap_username;
user.realname = ldapU.ldap_realname;
user.email = ldapU.ldap_email;
return user;
}
export default LDAPUsertoUser;

View File

@ -35,7 +35,8 @@
"COPY": "COPY",
"EDIT": "EDIT",
"SWITCH": "SWITCH",
"REPLICATE": "REPLICATE"
"REPLICATE": "REPLICATE",
"ACTIONS": "Actions"
},
"BATCH": {
"DELETED_SUCCESS": "Deleted successfully",
@ -112,6 +113,7 @@
"SYSTEM_MGMT": {
"NAME": "Administration",
"USER": "Users",
"GROUP": "Groups",
"REGISTRY": "Registries",
"REPLICATION": "Replications",
"CONFIG": "Configuration"
@ -200,6 +202,7 @@
"AUTOSCAN_POLICY": "Automatically scan images when they are pushed to the project registry."
},
"MEMBER": {
"NEW_USER": "New User",
"NEW_MEMBER": "New Member",
"MEMBER": "Member",
"NAME": "Name",
@ -211,6 +214,25 @@
"DELETE": "Delete",
"ITEMS": "items",
"ACTIONS": "Actions",
"USER": " User",
"USERS": "Users",
"EMAIL": "Email",
"ADD_USER": "Add User",
"NEW_USER_INFO": "Add an user to be a member of this project with specified role",
"NEW_GROUP": "New Group",
"IMPORT_GROUP": "Import New LDAP Group",
"NEW_GROUP_INFO": "Import a new LDAP group or select one or more existing groups to add with the specific role.",
"ADD_GROUP_SELECT": "Add a group as a project member",
"CREATE_GROUP_SELECT": "Import a new group from LDAP",
"LDAP_SEARCH_DN": "LDAP Group DN",
"LDAP_SEARCH_NAME": "Name",
"LDAP_GROUP": "Group",
"LDAP_GROUPS": "Groups",
"LDAP_PROPERTY": "Property",
"ACTION": "ACTION",
"MEMBER_TYPE": "Member Type",
"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.",
@ -224,8 +246,25 @@
"OF": "of",
"SWITCH_TITLE": "Confirm project members switch",
"SWITCH_SUMMARY": "Do you want to switch project members {{param}}?",
"ACTION": "SET ROLE",
"REMOVE": "REMOVE"
"SET_ROLE": "SET ROLE",
"REMOVE": "Remove"
},
"GROUP": {
"GROUP": "Group",
"GROUPS": "Groups",
"IMPORT_LDAP_GROUP": "Import LDAP Group",
"ADD": "New Group",
"EDIT": "Edit",
"DELETE": "Delete",
"NAME": "Name",
"TYPE": "Type",
"DN": "DN",
"GROUP_DN": "Ldap Group DN",
"PROPERTY": "Property",
"REG_TIME": "Registration Time",
"ADD_GROUP_SUCCESS": "Add group success",
"ADD_GROUP_FAILURE": "Add group failure",
"LDAP_TYPE": "LDAP"
},
"AUDIT_LOG": {
"USERNAME": "Username",
@ -507,7 +546,16 @@
"FILTER": "LDAP Filter",
"UID": "LDAP UID",
"SCOPE": "LDAP Scope",
"VERIFY_CERT": "LDAP Verify Cert"
"VERIFY_CERT": "LDAP Verify Cert",
"LDAP_GROUP_BASE_DN": "LDAP Group Base DN",
"LDAP_GROUP_BASE_DN_INFO": "The base DN of your LDAP group.",
"LDAP_GROUP_FILTER": "LDAP Group Filter",
"LDAP_GROUP_FILTER_INFO": "The filter of your LDAP group",
"LDAP_GROUP_GID": "LDAP Group GID",
"LDAP_GROUP_GID_INFO": "The Group gid of your LDAP group",
"GROUP_SCOPE": "LDAP Group Scope",
"GROUP_SCOPE_INFO": "The scope of your LDAP Group"
},
"UAA": {
"ENDPOINT": "UAA Endpoint",
@ -673,8 +721,11 @@
"DELETE_USER": "Delete user",
"DELETE_REGISTRY": "Delete registry",
"DELETE_REPLICATION": "Delete replication",
"DELETE_MEMBER": "Delete member",
"DELETE_MEMBER": "Delete user member",
"DELETE_GROUP": "Delete group member",
"SWITCH_ROLE": "Switch role",
"ADD_GROUP": "Add group member",
"ADD_USER": "Add user member",
"DELETE_LABEL": "Delete label",
"REPLICATION": "Replication",
"DAY_AGO": " day(s) ago",

View File

@ -35,7 +35,9 @@
"COPY": "COPY",
"EDIT": "EDITAR",
"SWITCH": "SWITCH",
"REPLICATE": "REPLICATE"
"REPLICATE": "REPLICATE",
"ACTIONS": "Actions"
},
"BATCH": {
"DELETED_SUCCESS": "Deleted successfully",
@ -83,6 +85,7 @@
"COMMENT": "Comentarios",
"PASSWORD": "Contraseña",
"SAVE_SUCCESS": "Perfil de usuario guardado satisfactoriamente.",
"ADMIN_RENAME_BUTTON": "Change username",
"ADMIN_RENAME_TIP": "Select the button in order to change the username to \"admin@harbor.local\". This operation can not be undone.",
"RENAME_SUCCESS": "Rename success!",
"RENAME_CONFIRM_INFO": "Warning, changing the name to admin@harbor.local can not be undone."
@ -112,6 +115,7 @@
"NAME": "Administración",
"USER": "Usuarios",
"REGISTRY": "Registries",
"GROUP": "Groups",
"REPLICATION": "Replicacións",
"CONFIG": "Configuración"
},
@ -199,6 +203,7 @@
"AUTOSCAN_POLICY": "Escanee automáticamente las imágenes cuando son enviadas al registro del proyecto."
},
"MEMBER": {
"NEW_USER": "New User",
"NEW_MEMBER": "Nuevo miembro",
"MEMBER": "Miembro",
"NAME": "Nombre",
@ -210,6 +215,25 @@
"DELETE": "Eliminar",
"ITEMS": "elementos",
"ACTIONS": "Acciones",
"USER": " User",
"USERS": "Users",
"EMAIL": "Email",
"ADD_USER": "Add User",
"NEW_USER_INFO": "Add an user to be a member of this project with specified role",
"NEW_GROUP": "New Group",
"IMPORT_GROUP": "Import New LDAP Group",
"NEW_GROUP_INFO": "Import a new LDAP group or select one or more existing groups to add with the specific role.",
"ADD_GROUP_SELECT": "Add a group as a project member",
"CREATE_GROUP_SELECT": "Import a new group from LDAP",
"LDAP_SEARCH_DN": "LDAP Group DN",
"LDAP_SEARCH_NAME": "Name",
"LDAP_GROUP": "Group",
"LDAP_GROUPS": "Groups",
"LDAP_PROPERTY": "Property",
"ACTION": "ACTION",
"MEMBER_TYPE": "Member Type",
"GROUP_TYPE": "Group",
"USER_TYPE": "User",
"USERNAME_IS_REQUIRED": "El nombre de usuario es obligatorio",
"USERNAME_DOES_NOT_EXISTS": "Ese nombre de usuario no existe.",
"USERNAME_ALREADY_EXISTS": "Ese nombre de usuario ya existe.",
@ -223,8 +247,24 @@
"OF": "of",
"SWITCH_TITLE": "Confirm project members switch",
"SWITCH_SUMMARY": "Do you want to switch project members {{param}}?",
"ACTION": "SET ROLE",
"REMOVE": "REMOVE"
"SET_ROLE": "SET ROLE",
"REMOVE": "Remove"
},
"GROUP": {
"GROUP": "Group",
"GROUPS": "Groups",
"IMPORT_LDAP_GROUP": "Import LDAP Group",
"ADD": "Add",
"EDIT": "Edit",
"DELETE": "Delete",
"TYPE": "Type",
"DN": "DN",
"GROUP_DN": "Ldap Group DN",
"PROPERTY": "Property",
"REG_TIME": "Registration Time",
"ADD_GROUP_SUCCESS": "Add group success",
"ADD_GROUP_FAILURE": "Add group failure",
"LDAP_TYPE": "LDAP"
},
"AUDIT_LOG": {
"USERNAME": "Nombre de usuario",
@ -506,7 +546,15 @@
"FILTER": "LDAP Filtro",
"UID": "LDAP UID",
"SCOPE": "LDAP Ámbito",
"VERIFY_CERT": "LDAP Verify Cert"
"VERIFY_CERT": "LDAP Verify Cert",
"LDAP_GROUP_BASE_DN": "LDAP Group Base DN",
"LDAP_GROUP_BASE_DN_INFO": "The base DN of your LDAP group.",
"LDAP_GROUP_FILTER": "LDAP Group Filter",
"LDAP_GROUP_FILTER_INFO": "The filter of your LDAP group",
"LDAP_GROUP_GID": "LDAP Group GID",
"LDAP_GROUP_GID_INFO": "The Group gid of your LDAP group",
"GROUP_SCOPE": "LDAP Group Scope",
"GROUP_SCOPE_INFO": "The scope of your LDAP Group"
},
"UAA": {
"ENDPOINT": "UAA Endpoint",
@ -672,8 +720,11 @@
"DELETE_USER": "Delete user",
"DELETE_REGISTRY": "Delete registry",
"DELETE_REPLICATION": "Delete replication",
"DELETE_MEMBER": "Delete member",
"DELETE_MEMBER": "Delete user member",
"DELETE_GROUP": "Delete group member",
"SWITCH_ROLE": "Switch role",
"ADD_GROUP": "Add group member",
"ADD_USER": "Add user member",
"DELETE_LABEL": "Delete label",
"REPLICATION": "Replication",
"DAY_AGO": " day(s) ago",

View File

@ -32,7 +32,8 @@
"YES": "OUI",
"NO": "NON",
"NEGATIVE": "NEGATIF",
"COPY": "COPIER"
"COPY": "COPIER",
"ACTIONS": "Actions"
},
"TOOLTIP": {
"EMAIL": "L'email doit être une adresse email valide comme name@example.com.",
@ -70,6 +71,7 @@
"COMMENT": "Commentaires",
"PASSWORD": "Mot de passe",
"SAVE_SUCCESS": "Profil utilisateur sauvegardé avec succès.",
"ADMIN_RENAME_BUTTON": "Change username",
"ADMIN_RENAME_TIP": "Select the button in order to change the username to \"admin@harbor.local\". This operation can not be undone.",
"RENAME_SUCCESS": "Rename success!",
"RENAME_CONFIRM_INFO": "Warning, changing the name to admin@harbor.local can not be undone."
@ -98,6 +100,7 @@
"SYSTEM_MGMT": {
"NAME": "Administration",
"USER": "Utilisateurs",
"GROUP": "Groups",
"REPLICATION": "Réplication",
"CONFIG": "Configuration"
},
@ -185,10 +188,30 @@
"AUTOSCAN_POLICY": "Analyser automatiquement les images lorsqu'elles sont envoyées au registre du projet."
},
"MEMBER": {
"NEW_USER": "New User",
"NEW_MEMBER": "Nouveau Membre",
"MEMBER": "Membre",
"NAME": "Nom",
"EMAIL": "Email",
"ROLE": "Rôle",
"ADD_USER": "Add User",
"NEW_USER_INFO": "Add an user to be a member of this project with specified role",
"NEW_GROUP": "New Group",
"IMPORT_GROUP": "Import New LDAP Group",
"NEW_GROUP_INFO": "Import a new LDAP group or select one or more existing groups to add with the specific role.",
"ADD_GROUP_SELECT": "Add a group as a project member",
"CREATE_GROUP_SELECT": "Import a new group from LDAP",
"LDAP_SEARCH_DN": "LDAP Group DN",
"LDAP_SEARCH_NAME": "Name",
"LDAP_GROUP": "Group",
"LDAP_GROUPS": "Groups",
"LDAP_PROPERTY": "Property",
"ACTION": "ACTION",
"USER": " User",
"USERS": "Users",
"MEMBER_TYPE": "Member Type",
"GROUP_TYPE": "Group",
"USER_TYPE": "User",
"SYS_ADMIN": "System Admin",
"PROJECT_ADMIN": "Project Admin",
"DEVELOPER": "Développeur",
@ -206,7 +229,26 @@
"ADDED_SUCCESS": "Membre ajouté avec succès.",
"DELETED_SUCCESS": "Membre supprimé avec succès.",
"SWITCHED_SUCCESS": "Rôle du membre changé avec succés.",
"OF": "de"
"OF": "de",
"SET_ROLE": "SET ROLE",
"REMOVE": "Remove"
},
"GROUP": {
"Group": "Group",
"GROUPS": "Groups",
"IMPORT_LDAP_GROUP": "Import LDAP Group",
"ADD": "Add",
"EDIT": "Edit",
"DELETE": "Delete",
"NAME": "Name",
"TYPE": "Type",
"DN": "DN",
"GROUP_DN": "Ldap Group DN",
"PROPERTY": "Property",
"REG_TIME": "Registration Time",
"ADD_GROUP_SUCCESS": "Add group success",
"ADD_GROUP_FAILURE": "Add group failure",
"LDAP_TYPE": "LDAP"
},
"AUDIT_LOG": {
"USERNAME": "Nom d'utilisateur",
@ -474,7 +516,16 @@
"BASE_DN": "Base DN LDAP",
"FILTER": "Filtre LDAP",
"UID": "UID LDAP",
"SCOPE": "Scope LDAP"
"SCOPE": "Scope LDAP",
"VERIFY_CERT": "LDAP Verify Cert",
"LDAP_GROUP_BASE_DN": "LDAP Group Base DN",
"LDAP_GROUP_BASE_DN_INFO": "The base DN of your LDAP group.",
"LDAP_GROUP_FILTER": "LDAP Group Filter",
"LDAP_GROUP_FILTER_INFO": "The filter of your LDAP group",
"LDAP_GROUP_GID": "LDAP Group GID",
"LDAP_GROUP_GID_INFO": "The Group gid of your LDAP group",
"GROUP_SCOPE": "LDAP Group Scope",
"GROUP_SCOPE_INFO": "The scope of your LDAP Group"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "Déclenchement d'analyse globale avec succès !",
@ -634,7 +685,10 @@
"DELETE_REGISTRY": "Delete registry",
"DELETE_REPLICATION": "Delete replication",
"DELETE_MEMBER": "Delete member",
"DELETE_GROUP": "Delete member group",
"SWITCH_ROLE": "Switch role",
"ADD_GROUP": "Add group member",
"ADD_USER": "Add user member",
"DELETE_LABEL": "Delete label",
"REPLICATION": "Replication",
"DAY_AGO": " day(s) ago",

View File

@ -35,7 +35,8 @@
"COPY": "拷贝",
"EDIT": "编辑",
"SWITCH": "切换",
"REPLICATE": "复制"
"REPLICATE": "复制",
"ACTIONS": "操作"
},
"BATCH": {
"DELETED_SUCCESS": "删除成功",
@ -85,7 +86,8 @@
"SAVE_SUCCESS": "成功保存用户设置。",
"ADMIN_RENAME_TIP": "单击将用户名改为 \"admin@harbor.local\", 注意这个操作是无法撤销的",
"RENAME_SUCCESS": "用户名更改成功!",
"RENAME_CONFIRM_INFO": "更改用户名为admin@harbor.local是无法撤销的, 你确定更改吗·?"
"ADMIN_RENAME_BUTTON": "更改用户名",
"RENAME_CONFIRM_INFO": "更改用户名为admin@harbor.local是无法撤销的, 你确定更改吗?"
},
"CHANGE_PWD": {
"TITLE": "修改密码",
@ -111,6 +113,7 @@
"SYSTEM_MGMT": {
"NAME": "系统管理",
"USER": "用户管理",
"GROUP": "组管理",
"REGISTRY": "仓库管理",
"REPLICATION": "复制管理",
"CONFIG": "配置管理"
@ -199,9 +202,11 @@
"AUTOSCAN_POLICY": "当镜像上传后,自动进行扫描"
},
"MEMBER": {
"NEW_USER": "新用户",
"NEW_MEMBER": "新建成员",
"MEMBER": "成员",
"NAME": "姓名",
"EMAIL": "邮箱",
"ROLE": "角色",
"SYS_ADMIN": "系统管理员",
"PROJECT_ADMIN": "项目管理员",
@ -210,6 +215,24 @@
"DELETE": "删除",
"ITEMS": "条记录",
"ACTIONS": "操作",
"USER": "用户",
"USERS": "用户",
"ADD_USER": "添加用户",
"NEW_USER_INFO": "添加用户到此项目中并给予相对应的角色",
"NEW_GROUP": "新增组",
"IMPORT_GROUP": "导入LDAP组",
"NEW_GROUP_INFO": "导入LDAP组同时以选择的角色添加到项目成员列表",
"ADD_GROUP_SELECT": "添加组到项目成员",
"CREATE_GROUP_SELECT": "从LDAP导入新的组",
"LDAP_SEARCH_DN": "LDAP Group DN",
"LDAP_SEARCH_NAME": "名称",
"LDAP_GROUP": "组",
"LDAP_GROUPS": "组",
"LDAP_PROPERTY": "属性",
"ACTION": "其他操作",
"MEMBER_TYPE": "成员类型",
"GROUP_TYPE": "组",
"USER_TYPE": "用户",
"USERNAME_IS_REQUIRED": "用户名为必填项。",
"USERNAME_DOES_NOT_EXISTS": "用户名不存在。",
"USERNAME_ALREADY_EXISTS": "用户名已存在。",
@ -223,9 +246,26 @@
"OF": "共计",
"SWITCH_TITLE": "切换项目成员确认",
"SWITCH_SUMMARY": "你确认切换项目成员 {{param}}??",
"ACTION": "设置角色",
"SET_ROLE": "设置角色",
"REMOVE": "移除成员"
},
"GROUP": {
"GROUP": "组",
"GROUPS": "组",
"IMPORT_LDAP_GROUP": "导入LDAP组",
"ADD": "新增",
"EDIT": "编辑",
"DELETE": "删除",
"NAME": "名称",
"TYPE": "类型",
"DN": "DN",
"PROPERTY": "属性",
"GROUP_DN": "LDAP 组域",
"REG_TIME": "注册时间",
"ADD_GROUP_SUCCESS": "添加组成功",
"ADD_GROUP_FAILURE": "添加组失败",
"LDAP_TYPE": "LDAP"
},
"AUDIT_LOG": {
"USERNAME": "用户名",
"REPOSITORY_NAME": "镜像名称",
@ -504,9 +544,17 @@
"SEARCH_PWD": "LDAP搜索密码",
"BASE_DN": "LDAP基础DN",
"FILTER": "LDAP过滤器",
"UID": "LDAP用户UID的属性",
"UID": "LDAP用户UID",
"SCOPE": "LDAP搜索范围",
"VERIFY_CERT": "LDAP 检查证书"
"VERIFY_CERT": "LDAP 检查证书",
"LDAP_GROUP_BASE_DN": "LDAP组基础DN",
"LDAP_GROUP_BASE_DN_INFO": "LDAP组的基础DN",
"LDAP_GROUP_FILTER": "LDAP组过滤器",
"LDAP_GROUP_FILTER_INFO": "LDAP组的过滤器",
"LDAP_GROUP_GID": "LDAP组GID",
"LDAP_GROUP_GID_INFO": "LDAP组的GID",
"GROUP_SCOPE": "LDAP组搜索范围",
"GROUP_SCOPE_INFO": "搜索范围"
},
"UAA": {
"ENDPOINT": "UAA Endpoint",
@ -670,10 +718,13 @@
"DELETE_REPO": "删除仓库",
"DELETE_TAG": "删除镜像标签",
"DELETE_USER": "删除用户",
"DELETE_REGISTRY": "Delete registry",
"DELETE_REGISTRY": "删除Registry",
"DELETE_REPLICATION": "删除复制",
"DELETE_MEMBER": "删除成员",
"DELETE_MEMBER": "删除用户成员",
"DELETE_GROUP": "删除组成员",
"SWITCH_ROLE": "切换角色",
"ADD_GROUP": "添加组成员",
"ADD_USER": "添加用户成员",
"DELETE_LABEL": "删除标签",
"REPLICATION": "复制",
"DAY_AGO": "天前",

View File

@ -19,7 +19,7 @@ Library OperatingSystem
*** Variables ***
${HARBOR_VERSION} v1.1.1
${CLAIR_BUILDER} 1.4.1
${CLAIR_BUILDER} 1.4.0
${GOLANG_VERSION} 1.9.2
*** Keywords ***