Add ldap serach

1. Add group management
2. Add rewrite import user to member ui
3. Add import group to member
4. Add new items in configuration page
This commit is contained in:
Deng, Qian 2018-05-17 16:50:49 +08:00
parent e705224b3f
commit 72dfdd552f
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 ***