Merge pull request #3859 from pengpengshui/batch-delection

Add batch-delete operation in project module, replication module and user module
This commit is contained in:
pengpengshui 2018-01-03 10:27:55 +08:00 committed by GitHub
commit c5e434bd14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 977 additions and 431 deletions

View File

@ -0,0 +1,27 @@
/**
* Created by pengf on 11/22/2017.
*/
export class BatchInfo {
name: string;
status: string;
loading: boolean;
errorState: boolean;
errorInfo: string;
constructor() {
this.status = 'pending';
this.loading = false;
this.errorState = false;
this.errorInfo = '';
}
}
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;
}

View File

@ -18,4 +18,12 @@ export const CONFIRMATION_DIALOG_STYLE: string = `
width: 80%;
white-space: pre-wrap;
}
.batchInfoUl{
padding: 20px; list-style-type: none;
}
.batchInfoUl li {line-height: 24px;border-bottom: 1px solid #e8e8e8;}
.batchInfoUl li span:first-child {padding-right: 20px; width: 210px; display: inline-block; color:#666;}
.batchInfoUl li span:last-child {width: 260px; display: inline-block; color:#666;}
.batchInfoUl li span i {display: inline-block; line-height: 1.2em; font-size: 0.8em; color: #999;}
.batchInfoUl li span a{cursor: pointer; text-decoration: underline;}
`;

View File

@ -6,19 +6,33 @@ export const CONFIRMATION_DIALOG_TEMPLATE: string = `
<clr-icon shape="warning" class="is-warning" size="64"></clr-icon>
</div>
<div class="confirmation-content">{{dialogContent}}</div>
<div>
<ul class="batchInfoUl">
<li *ngFor="let info of batchInfors">
<span> <i class="spinner spinner-inline spinner-pos" [hidden]='!info.loading'></i>&nbsp;&nbsp;{{info.name}}</span>
<span *ngIf="!info.errorInfo.length" [style.color]="colorChange(info)">{{info.status}}</span>
<span *ngIf="info.errorInfo.length" [style.color]="colorChange(info)">
<a (click)="toggleErrorTitle(errorInfo)" >{{info.status}}</a><br>
<i #errorInfo style="display: none;">{{info.errorInfo}}</i>
</span>
</li>
</ul>
</div>
</div>
<div class="modal-footer" [ngSwitch]="buttons">
<ng-template [ngSwitchCase]="0">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{ 'BUTTON.CONFIRM' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{'BUTTON.CONFIRM' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="cancel()">{{'BUTTON.CLOSE' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="1">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.NO' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{ 'BUTTON.YES' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="2">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-danger" (click)="confirm()">{{ 'BUTTON.DELETE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="isDelete">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-danger" (click)="confirm()" [hidden]="isDelete">{{'BUTTON.DELETE' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="cancel()" [disabled]="!batchOverStatus" [hidden]="!isDelete">{{'BUTTON.CLOSE' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="3">
<button type="button" class="btn btn-primary" (click)="cancel()">{{'BUTTON.CLOSE' | translate}}</button>

View File

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, EventEmitter, Output } from '@angular/core';
import {Component, EventEmitter, Input, Output} from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ConfirmationMessage } from './confirmation-message';
@ -20,6 +20,7 @@ import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../
import { CONFIRMATION_DIALOG_TEMPLATE } from './confirmation-dialog.component.html';
import { CONFIRMATION_DIALOG_STYLE } from './confirmation-dialog.component.css';
import {BatchInfo} from "./confirmation-batch-message";
@Component({
selector: 'confirmation-dialog',
@ -36,6 +37,9 @@ export class ConfirmationDialogComponent {
@Output() confirmAction = new EventEmitter<ConfirmationAcknowledgement>();
@Output() cancelAction = new EventEmitter<ConfirmationAcknowledgement>();
@Input() batchInfors: BatchInfo[] = [];
isDelete: boolean = false;
constructor(
private translate: TranslateService) {}
@ -51,7 +55,29 @@ export class ConfirmationDialogComponent {
this.opened = true;
}
get batchOverStatus(): boolean {
if (this.batchInfors.length) {
return this.batchInfors.every(item => item.loading === false);
}
return false;
}
colorChange(list: BatchInfo) {
if (!list.loading && !list.errorState) {
return 'green';
}else if (!list.loading && list.errorState) {
return 'red';
}else {
return '#666';
}
}
toggleErrorTitle(errorSpan: any) {
errorSpan.style.display = (errorSpan.style.display === 'none') ? 'block' : 'none';
}
close(): void {
this.batchInfors = [];
this.opened = false;
}
@ -68,6 +94,7 @@ export class ConfirmationDialogComponent {
data,
target
));
this.isDelete = false;
this.close();
}
@ -77,6 +104,11 @@ export class ConfirmationDialogComponent {
return;
}
if (this.batchInfors.length) {
this.batchInfors.every(item => item.loading = true);
this.isDelete = true;
}
let data: any = this.message.data ? this.message.data : {};
let target = this.message.targetId ? this.message.targetId : ConfirmationTargets.EMPTY;
let message = new ConfirmationAcknowledgement(
@ -85,6 +117,5 @@ export class ConfirmationDialogComponent {
target
);
this.confirmAction.emit(message);
this.close();
}
}
}

View File

@ -1,12 +1,8 @@
export const ENDPOINT_TEMPLATE: string = `
<div>
<div style="margin-top: -24px;">
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between" style="height: 24px;">
<div class="flex-items-xs-middle option-left">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'DESTINATION.ENDPOINT' | translate}}</button>
<create-edit-endpoint (reload)="reload($event)"></create-edit-endpoint>
</div>
<div class="row flex-items-xs-between" style="height: 24px; float:right;">
<div class="flex-items-xs-middle option-right">
<hbr-filter [withDivider]="true" filterPlaceholder='{{"REPLICATION.FILTER_TARGETS_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)" [currentValue]="targetName"></hbr-filter>
<span class="refresh-btn" (click)="refreshTargets()">
@ -16,17 +12,20 @@ export const ENDPOINT_TEMPLATE: string = `
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading">
<clr-datagrid [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-secondary" (click)="openModal()">{{'DESTINATION.NEW_ENDPOINT' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length ===1)" (click)="editTargets(selectedRow)" >{{'DESTINATION.TITLE_EDIT' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow.length" (click)="deleteTargets(selectedRow)">{{'DESTINATION.DELETE' | translate}}</button>
</div>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'DESTINATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'endpoint'">{{'DESTINATION.URL' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'insecure'">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'DESTINATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of targets" [clrDgItem]='t'>
<clr-dg-action-overflow>
<button class="action-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</button>
<button class="action-item" (click)="deleteTarget(t)">{{'DESTINATION.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
<clr-dg-cell>
@ -42,6 +41,7 @@ export const ENDPOINT_TEMPLATE: string = `
</clr-datagrid>
</div>
</div>
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
<confirmation-dialog #confirmationDialog [batchInfors]="batchDelectionInfos" (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
<create-edit-endpoint (reload)="reload($event)"></create-edit-endpoint>
</div>
`;

View File

@ -57,7 +57,7 @@ describe('EndpointComponent (inline template)', () => {
}
];
let mockOne: Endpoint = {
let mockOne: Endpoint[] = [{
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
@ -65,7 +65,7 @@ describe('EndpointComponent (inline template)', () => {
"password": "",
"insecure": false,
"type": 0
};
}];
let comp: EndpointComponent;
let fixture: ComponentFixture<EndpointComponent>;
@ -105,7 +105,7 @@ describe('EndpointComponent (inline template)', () => {
spy = spyOn(endpointService, 'getEndpoints').and.returnValues(Promise.resolve(mockData));
spyOnRules = spyOn(endpointService, 'getEndpointWithReplicationRules').and.returnValue([]);
spyOne = spyOn(endpointService, 'getEndpoint').and.returnValue(Promise.resolve(mockOne));
spyOne = spyOn(endpointService, 'getEndpoint').and.returnValue(Promise.resolve(mockOne[0]));
fixture.detectChanges();
});
@ -123,7 +123,7 @@ describe('EndpointComponent (inline template)', () => {
fixture.detectChanges();
fixture.whenStable().then(()=>{
fixture.detectChanges();
comp.editTarget(mockOne);
comp.editTargets(mockOne);
fixture.detectChanges();
expect(comp.target.name).toEqual('target_01');
});

View File

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Endpoint, ReplicationRule } from '../service/interface';
import { EndpointService } from '../service/endpoint.service';
@ -35,6 +35,8 @@ import { ENDPOINT_TEMPLATE } from './endpoint.component.html';
import { toPromise, CustomComparator } from '../utils';
import { State, Comparator } from 'clarity-angular';
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
import {Observable} from "rxjs/Observable";
@Component({
selector: 'hbr-endpoint',
@ -42,7 +44,7 @@ import { State, Comparator } from 'clarity-angular';
styles: [ENDPOINT_STYLE],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EndpointComponent implements OnInit {
export class EndpointComponent implements OnInit, OnDestroy {
@ViewChild(CreateEditEndpointComponent)
createEditEndpointComponent: CreateEditEndpointComponent;
@ -62,6 +64,8 @@ export class EndpointComponent implements OnInit {
creationTimeComparator: Comparator<Endpoint> = new CustomComparator<Endpoint>('creation_time', 'date');
timerHandler: any;
selectedRow: Endpoint[] = [];
batchDelectionInfos: BatchInfo[] = [];
get initEndpoint(): Endpoint {
return {
@ -82,31 +86,6 @@ export class EndpointComponent implements OnInit {
this.forceRefreshView(1000);
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TARGET &&
message.state === ConfirmationState.CONFIRMED) {
let targetId = message.data;
toPromise<number>(this.endpointService
.deleteEndpoint(targetId))
.then(
response => {
this.translateService.get('DESTINATION.DELETED_SUCCESS')
.subscribe(res => this.errorHandler.info(res));
this.reload(true);
}).catch(
error => {
if (error && error.status === 412) {
this.translateService.get('DESTINATION.FAILED_TO_DELETE_TARGET_IN_USED')
.subscribe(res => this.errorHandler.error(res));
} else {
this.errorHandler.error(error);
}
});
}
}
ngOnInit(): void {
this.targetName = '';
this.retrieve();
@ -117,6 +96,9 @@ export class EndpointComponent implements OnInit {
this.subscription.unsubscribe();
}
}
selectedChange(): void {
this.forceRefreshView(5000);
}
retrieve(): void {
this.loading = true;
@ -152,8 +134,9 @@ export class EndpointComponent implements OnInit {
this.target = this.initEndpoint;
}
editTarget(target: Endpoint) {
if (target) {
editTargets(targets: Endpoint[]) {
if (targets && targets.length === 1) {
let target= targets[0];
let editable = true;
if (!target.id) {
return;
@ -173,20 +156,69 @@ export class EndpointComponent implements OnInit {
}
}
deleteTarget(target: Endpoint) {
if (target) {
let targetId = target.id;
deleteTargets(targets: Endpoint[]) {
if (targets && targets.length) {
let targetNames: string[] = [];
this.batchDelectionInfos = [];
targets.forEach(target => {
targetNames.push(target.name);
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = target.name;
this.batchDelectionInfos.push(initBatchMessage);
});
let deletionMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE_TARGET',
'REPLICATION.DELETION_SUMMARY_TARGET',
target.name || '',
target.id,
targetNames.join(', ') || '',
targets,
ConfirmationTargets.TARGET,
ConfirmationButtons.DELETE_CANCEL);
this.confirmationDialogComponent.open(deletionMessage);
}
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TARGET &&
message.state === ConfirmationState.CONFIRMED) {
let targetLists: Endpoint[] = message.data;
if (targetLists && targetLists.length) {
let promiseLists: any[] = [];
targetLists.forEach(target => {
this.delOperate(target.id, target.name);
})
Promise.all(promiseLists).then((item) => {
this.selectedRow = [];
this.reload(true);
this.forceRefreshView(2000);
});
}
}
}
delOperate(id: number | string, name: string) {
let findedList = this.batchDelectionInfos.find(data => data.name === name);
return toPromise<number>(this.endpointService
.deleteEndpoint(id))
.then(
response => {
this.translateService.get('BATCH.DELETED_SUCCESS')
.subscribe(res => {
findedList = BathInfoChanges(findedList, res);
});
}).catch(
error => {
if (error && error.status === 412) {
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
this.translateService.get('DESTINATION.FAILED_TO_DELETE_TARGET_IN_USED')).subscribe(res => {
findedList = BathInfoChanges(findedList, res[0], false, true, res[1]);
});
} else {
this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
}
});
}
//Forcely refresh the view
forceRefreshView(duration: number): void {
//Reset timer

View File

@ -1,6 +1,13 @@
export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<div>
<clr-datagrid [clrDgLoading]="loading">
<div style="margin-top: -24px;">
<clr-datagrid [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedRow" (clrDgSingleSelectedChange)="selectedChange()">
<clr-dg-action-bar>
<div class="btn-group">
<button type="button" *ngIf="creationAvailable" class="btn btn-sm btn-secondary" (click)="openModal()">{{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button>
<button type="button" *ngIf="!creationAvailable" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="editRule(selectedRow)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button type="button" *ngIf="!creationAvailable" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
</div>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'project_name'" *ngIf="!projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'description'">{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
@ -9,11 +16,7 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<clr-dg-column [clrDgSortBy]="enabledComparator">{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'REPLICATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let p of changedRules" [clrDgItem]="p" (click)="selectRule(p)" [style.backgroundColor]="(projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''">
<clr-dg-action-overflow *ngIf="!readonly">
<button class="action-item" (click)="editRule(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button class="action-item" (click)="toggleRule(p)">{{ (p.enabled === 0 ? 'REPLICATION.ENABLE' : 'REPLICATION.DISABLE') | translate}}</button>
<button class="action-item" (click)="deleteRule(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>
<ng-template [ngIf]="!projectScope">
<a href="javascript:void(0)" (click)="redirectTo(p)">{{p.name}}</a>
@ -38,7 +41,7 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<clr-dg-pagination #pagination [clrDgPageSize]="5"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
<confirmation-dialog #toggleConfirmDialog (confirmAction)="toggleConfirm($event)"></confirmation-dialog>
<confirmation-dialog #deletionConfirmDialog (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
<confirmation-dialog #toggleConfirmDialog [batchInfors]="batchDelectionInfos" (confirmAction)="toggleConfirm($event)"></confirmation-dialog>
<confirmation-dialog #deletionConfirmDialog [batchInfors]="batchDelectionInfos" (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
</div>
`;

View File

@ -42,6 +42,8 @@ import { toPromise, CustomComparator } from '../utils';
import { State, Comparator } from 'clarity-angular';
import { LIST_REPLICATION_RULE_TEMPLATE } from './list-replication-rule.component.html';
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
import {Observable} from "rxjs/Observable";
@Component({
selector: 'hbr-list-replication-rule',
@ -64,6 +66,7 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
@Output() editOne = new EventEmitter<ReplicationRule>();
@Output() toggleOne = new EventEmitter<ReplicationRule>();
@Output() redirect = new EventEmitter<ReplicationRule>();
@Output() openNewRule = new EventEmitter<any>();
projectScope: boolean = false;
@ -72,6 +75,9 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
ruleName: string;
canDeleteRule: boolean;
selectedRow: ReplicationRule;
batchDelectionInfos: BatchInfo[] = [];
@ViewChild('toggleConfirmDialog')
toggleConfirmDialog: ConfirmationDialogComponent;
@ -89,6 +95,11 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
setInterval(() => ref.markForCheck(), 500);
}
public get creationAvailable(): boolean {
return !this.readonly && this.projectId ? true : false;
}
ngOnInit(): void {
//Global scope
if (!this.projectScope) {
@ -110,6 +121,11 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
}
}
selectedChange(): void {
let hnd = setInterval(() => this.ref.markForCheck(), 200);
setTimeout(() => clearInterval(hnd), 2000);
}
retrieveRules(ruleName: string = ''): void {
this.loading = true;
toPromise<ReplicationRule[]>(this.replicationService
@ -139,17 +155,22 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
toggleConfirm(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TOGGLE_CONFIRM &&
message.state === ConfirmationState.CONFIRMED) {
message.source === ConfirmationTargets.TOGGLE_CONFIRM &&
message.state === ConfirmationState.CONFIRMED) {
this.batchDelectionInfos = [];
let rule: ReplicationRule = message.data;
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = rule.name;
this.batchDelectionInfos.push(initBatchMessage);
if (rule) {
rule.enabled = rule.enabled === 0 ? 1 : 0;
toPromise<any>(this.replicationService
.enableReplicationRule(rule.id || '', rule.enabled))
.then(() =>
this.translateService.get('REPLICATION.TOGGLED_SUCCESS')
.subscribe(res => this.errorHandler.info(res)))
.catch(error => this.errorHandler.error(error));
.enableReplicationRule(rule.id || '', rule.enabled))
.then(() =>
this.translateService.get('REPLICATION.TOGGLED_SUCCESS')
.subscribe(res => this.batchDelectionInfos[0].status = res))
.catch(error => this.batchDelectionInfos[0].status = error);
}
}
}
@ -158,19 +179,26 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
if (message &&
message.source === ConfirmationTargets.POLICY &&
message.state === ConfirmationState.CONFIRMED) {
let rule: ReplicationRule = message.data;
toPromise<any>(this.replicationService
.deleteReplicationRule(message.data))
.deleteReplicationRule(rule.id))
.then(() => {
this.translateService.get('REPLICATION.DELETED_SUCCESS')
.subscribe(res => this.errorHandler.info(res));
this.translateService.get('BATCH.DELETED_SUCCESS')
.subscribe(res => {
this.batchDelectionInfos[0] = BathInfoChanges(this.batchDelectionInfos[0], res);
});
this.reload.emit(true);
})
.catch(error => {
if (error && error.status === 412) {
this.translateService.get('REPLICATION.FAILED_TO_DELETE_POLICY_ENABLED')
.subscribe(res => this.errorHandler.error(res));
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
this.translateService.get('REPLICATION.FAILED_TO_DELETE_POLICY_ENABLED')).subscribe(res => {
this.batchDelectionInfos[0] = BathInfoChanges(this.batchDelectionInfos[0], res[0], false, true, res[1]);
});
} else {
this.errorHandler.error(error);
this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => {
this.batchDelectionInfos[0] = BathInfoChanges(this.batchDelectionInfos[0], res, false, true);
});
}
});
}
@ -185,8 +213,12 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
this.redirect.emit(rule);
}
editRule(rule: ReplicationRule) {
this.editOne.emit(rule);
openModal(): void {
this.openNewRule.emit();
}
editRule(rules: ReplicationRule) {
this.editOne.emit(rules);
}
toggleRule(rule: ReplicationRule) {
@ -228,15 +260,19 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
'REPLICATION.DELETION_TITLE_FAILURE',
'REPLICATION.DELETION_SUMMARY_FAILURE',
rule.name || '',
rule.id,
rule,
ConfirmationTargets.POLICY,
ConfirmationButtons.CLOSE);
} else {
this.batchDelectionInfos = [];
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = rule.name;
this.batchDelectionInfos.push(initBatchMessage);
deletionMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE',
'REPLICATION.DELETION_SUMMARY',
rule.name || '',
rule.id,
rule,
ConfirmationTargets.POLICY,
ConfirmationButtons.DELETE_CANCEL);
}

View File

@ -1,11 +1,7 @@
export const REPLICATION_TEMPLATE: string = `
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between" style="height:32px;">
<div class="flex-xs-middle option-left">
<button *ngIf="creationAvailable" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
</div>
<div class="row flex-items-xs-between" style="height:32px; float:right">
<div class="flex-xs-middle option-right">
<div class="select" style="float: left; top: 8px;">
<select (change)="doFilterRuleStatus($event)">
@ -20,7 +16,7 @@ export const REPLICATION_TEMPLATE: string = `
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<hbr-list-replication-rule #listReplicationRule [readonly]="readonly" [projectId]="projectId" (selectOne)="selectOneRule($event)" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)"></hbr-list-replication-rule>
<hbr-list-replication-rule #listReplicationRule [readonly]="readonly" [projectId]="projectId" (selectOne)="selectOneRule($event)" (openNewRule)="openModal()" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)"></hbr-list-replication-rule>
</div>
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between" style="height:60px;">
@ -76,4 +72,5 @@ export const REPLICATION_TEMPLATE: string = `
</clr-datagrid>
</div>
<job-log-viewer #replicationLogViewer></job-log-viewer>
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
</div>`;

View File

@ -11,15 +11,17 @@ export const REPOSITORY_LISTVIEW_TEMPLATE = `
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading">
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-secondary" (click)="deleteRepos(selectedRow)" [disabled]="!(selectedRow.length && hasProjectAdminRole)">{{'REPOSITORY.DELETE' | translate}}</button>
</div>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="tagsCountComparator">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let r of repositories">
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-row *clrDgItems="let r of repositories" [clrDgItem]="r">
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name}}</a></clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
@ -36,6 +38,6 @@ export const REPOSITORY_LISTVIEW_TEMPLATE = `
</clr-datagrid>
</div>
</div>
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
<confirmation-dialog #confirmationDialog [batchInfors]="batchDelectionInfos" (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
</div>
`;

View File

@ -43,6 +43,8 @@ import {
doFiltering,
doSorting
} from '../utils';
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
import {Observable} from "rxjs/Observable";
@Component({
selector: 'hbr-repository-listview',
@ -63,12 +65,14 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
lastFilteredRepoName: string;
repositories: RepositoryItem[];
systemInfo: SystemInfo;
selectedRow: RepositoryItem[] = [];
loading = true;
loading: boolean = true;
@ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent;
batchDelectionInfos: BatchInfo[] = [];
pullCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('pull_count', 'number');
tagsCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('tags_count', 'number');
@ -83,7 +87,6 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
private translateService: TranslateService,
private repositoryService: RepositoryService,
private systemInfoService: SystemInfoService,
private translate: TranslateService,
private tagService: TagService,
private ref: ChangeDetectorRef,
private router: Router) { }
@ -110,35 +113,6 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
return this.withClair && !this.isClairDBReady;
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.REPOSITORY &&
message.state === ConfirmationState.CONFIRMED) {
let repoName = message.data;
toPromise<number>(this.repositoryService
.deleteRepository(repoName))
.then(
response => {
this.refresh();
let st: State = this.getStateAfterDeletion();
if (!st) {
this.refresh();
} else {
this.clrLoad(st);
}
this.translateService.get('REPOSITORY.DELETED_REPO_SUCCESS')
.subscribe(res => this.errorHandler.info(res));
}).catch(error => {
if (error.status === '412') {
this.translateService.get('REPOSITORY.TAGS_SIGNED')
.subscribe(res => this.errorHandler.info(res));
return;
}
this.errorHandler.error(error);
});
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['projectId'] && changes['projectId'].currentValue) {
this.refresh();
@ -154,6 +128,60 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
this.lastFilteredRepoName = '';
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.REPOSITORY &&
message.state === ConfirmationState.CONFIRMED) {
let promiseLists: any[] = [];
let repoNames: string[] = message.data.split(',');
repoNames.forEach(repoName => {
promiseLists.push(this.delOperate(repoName));
});
Promise.all(promiseLists).then((item) => {
this.selectedRow = [];
this.refresh();
let st: State = this.getStateAfterDeletion();
if (!st) {
this.refresh();
} else {
this.clrLoad(st);
}
});
}
}
delOperate(repoName: string) {
let findedList = this.batchDelectionInfos.find(data => data.name === repoName);
if (this.signedCon[repoName].length !== 0) {
this.translateService.get('REPOSITORY.DELETION_TITLE_REPO_SIGNED').subscribe(res => {
findedList.status = res;
});
} else {
return toPromise<number>(this.repositoryService
.deleteRepository(repoName))
.then(
response => {
this.translateService.get('BATCH.DELETED_SUCCESS').subscribe(res => {
findedList = BathInfoChanges(findedList, res);
});
}).catch(error => {
if (error.status === "412") {
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
this.translateService.get('REPOSITORY.TAGS_SIGNED')).subscribe(res => {
findedList = BathInfoChanges(findedList, res[0], false, true, res[1]);
});
return;
}
this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
});
}
}
doSearchRepoNames(repoName: string) {
this.lastFilteredRepoName = repoName;
this.currentPage = 1;
@ -172,15 +200,29 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
Object.assign(this.signedCon, event);
}
deleteRepo(repoName: string) {
if (this.signedCon[repoName]) {
this.signedDataSet(repoName);
} else {
this.getTagInfo(repoName).then(() => {
this.signedDataSet(repoName);
deleteRepos(repoLists: RepositoryItem[]) {
if (repoLists && repoLists.length) {
let repoNames: string[] = [];
this.batchDelectionInfos = [];
let repArr: any[] = [];
repoLists.forEach(repo => {
repoNames.push(repo.name);
let initBatchMessage = new BatchInfo();
initBatchMessage.name = repo.name;
this.batchDelectionInfos.push(initBatchMessage);
if (!this.signedCon[repo.name]) {
repArr.push(this.getTagInfo(repo.name));
}
});
Promise.all(repArr).then(() => {
this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', '', repoNames.join(','), 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL);
});
}
}
getTagInfo(repoName: string): Promise<void> {
// this.signedNameArr = [];
this.signedCon[repoName] = [];
@ -207,7 +249,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
}
confirmationDialogSet(summaryTitle: string, signature: string, repoName: string, summaryKey: string, button: ConfirmationButtons): void {
this.translate.get(summaryKey,
this.translateService.get(summaryKey,
{
repoName: repoName,
signedImages: signature,
@ -228,6 +270,10 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
});
}
selectedChange(): void {
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
}
refresh() {
this.doSearchRepoNames('');
}

View File

@ -1,5 +1,5 @@
export const TAG_TEMPLATE = `
<confirmation-dialog class="hidden-tag" #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
<confirmation-dialog class="hidden-tag" #confirmationDialog [batchInfors]="batchDelectionInfos" (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
<clr-modal class="hidden-tag" [(clrModalOpen)]="showTagManifestOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body">
@ -22,7 +22,14 @@ export const TAG_TEMPLATE = `
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar style="margin-bottom: 0;">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-secondary" *ngIf="canScanNow(selectedRow)" [disabled]="!(selectedRow.length==1)" (click)="scanNow(selectedRow)">{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)" >{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasProjectAdminRole" (click)="deleteTags(selectedRow)" [disabled]="!selectedRow.length">{{'REPOSITORY.DELETE' | translate}}</button>
</div>
</clr-dg-action-bar>
<clr-dg-column style="min-width: 160px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column style="width: 90px;" [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 120px; max-width:220px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
@ -33,11 +40,6 @@ export const TAG_TEMPLATE = `
<clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow>
<button class="action-item" *ngIf="canScanNow(t)" (click)="scanNow(t.name)">{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button class="action-item" *ngIf="hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell class="truncated" style="min-width: 160px;" [ngSwitch]="withClair">
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
<span *ngSwitchDefault>{{t.name}}</span>

View File

@ -55,6 +55,8 @@ import { TranslateService } from '@ngx-translate/core';
import { State, Comparator } from 'clarity-angular';
import {CopyInputComponent} from '../push-image/copy-input.component';
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
import {Observable} from "rxjs/Observable";
@Component({
selector: 'hbr-tag',
@ -88,11 +90,13 @@ export class TagComponent implements OnInit {
staticBackdrop: boolean = true;
closable: boolean = false;
lastFilteredTagName: string;
batchDelectionInfos: BatchInfo[] = [];
createdComparator: Comparator<Tag> = new CustomComparator<Tag>('created', 'date');
loading: boolean = false;
copyFailed: boolean = false;
selectedRow: Tag[] = [];
@ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent;
@ -113,28 +117,6 @@ export class TagComponent implements OnInit {
private channel: ChannelService
) { }
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tag: Tag = message.data;
if (tag) {
if (tag.signature) {
return;
} else {
toPromise<number>(this.tagService
.deleteTag(this.repoName, tag.name))
.then(
response => {
this.retrieve();
this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS')
.subscribe(res => this.errorHandler.info(res));
}).catch(error => this.errorHandler.error(error));
}
}
}
}
ngOnInit() {
if (!this.projectId) {
this.errorHandler.error('Project ID cannot be unset.');
@ -149,6 +131,11 @@ export class TagComponent implements OnInit {
this.lastFilteredTagName = '';
}
selectedChange(): void {
let hnd = setInterval(() => this.ref.markForCheck(), 200);
setTimeout(() => clearInterval(hnd), 2000);
}
doSearchTagNames(tagName: string) {
this.lastFilteredTagName = tagName;
this.currentPage = 1;
@ -203,6 +190,8 @@ export class TagComponent implements OnInit {
this.doSearchTagNames('');
}
retrieve() {
this.tags = [];
let signatures: string[] = [] ;
@ -261,35 +250,82 @@ export class TagComponent implements OnInit {
}
}
deleteTag(tag: Tag) {
if (tag) {
deleteTags(tags: Tag[]) {
if (tags && tags.length) {
let tagNames: string[] = [];
this.batchDelectionInfos = [];
tags.forEach(tag => {
tagNames.push(tag.name);
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = tag.name;
this.batchDelectionInfos.push(initBatchMessage);
});
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
if (tag.signature) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
buttons = ConfirmationButtons.CLOSE;
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.name;
} else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
buttons = ConfirmationButtons.DELETE_CANCEL;
content = tag.name;
}
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
buttons = ConfirmationButtons.DELETE_CANCEL;
content = tagNames.join(' , ');
let message = new ConfirmationMessage(
titleKey,
summaryKey,
content,
tag,
tags,
ConfirmationTargets.TAG,
buttons);
this.confirmationDialog.open(message);
}
}
showDigestId(tag: Tag) {
if (tag) {
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tags: Tag[] = message.data;
if (tags && tags.length) {
let promiseLists: any[] = [];
tags.forEach(tag => {
this.delOperate(tag.signature, tag.name);
});
Promise.all(promiseLists).then((item) => {
this.selectedRow = [];
this.retrieve();
});
}
}
}
delOperate(signature: any, name: string) {
let findedList = this.batchDelectionInfos.find(data => data.name === name);
if (signature) {
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
this.translateService.get('REPOSITORY.DELETION_SUMMARY_TAG_DENIED')).subscribe(res => {
let wrongInfo: string = res[1] + 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + name;
findedList = BathInfoChanges(findedList, res[0], false, true, wrongInfo);
});
} else {
return toPromise<number>(this.tagService
.deleteTag(this.repoName, name))
.then(
response => {
this.translateService.get('BATCH.DELETED_SUCCESS')
.subscribe(res => {
findedList = BathInfoChanges(findedList, res);
});
}).catch(error => {
this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
});
}
}
showDigestId(tag: Tag[]) {
if (tag && (tag.length === 1)) {
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
this.digestId = tag.digest;
this.digestId = tag[0].digest;
this.showTagManifestOpened = true;
this.copyFailed = false;
}
@ -338,19 +374,22 @@ export class TagComponent implements OnInit {
}
// Whether show the 'scan now' menu
canScanNow(t: Tag): boolean {
canScanNow(t: Tag[]): boolean {
if (!this.withClair) { return false; }
if (!this.hasProjectAdminRole) { return false; }
let st: string = this.scanStatus(t);
let st: string = this.scanStatus(t[0]);
return st !== VULNERABILITY_SCAN_STATUS.pending &&
st !== VULNERABILITY_SCAN_STATUS.running;
}
// Trigger scan
scanNow(tagId: string): void {
if (tagId) {
this.channel.publishScanEvent(this.repoName + '/' + tagId);
scanNow(t: Tag[]): void {
if (t && t.length) {
t.forEach((data: any) => {
let tagId = data.name;
this.channel.publishScanEvent(this.repoName + '/' + tagId);
});
}
}

View File

@ -31,7 +31,7 @@
"clarity-icons": "^0.10.17",
"clarity-ui": "^0.10.17",
"core-js": "^2.4.1",
"harbor-ui": "0.6.2",
"harbor-ui": "0.6.5",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",

View File

@ -1,15 +1,16 @@
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading">
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-secondary" (click)="addNewProject()" *ngIf="projectCreationRestriction">{{'PROJECT.NEW_PROJECT' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length && (isSystemAdmin || canDelete))" (click)="deleteProjects(selectedRow)" >{{'PROJECT.DELETE' | translate}}</button>
</div>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="accessLevelComparator">{{'PROJECT.ACCESS_LEVEL' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="showRoleInfo" [clrDgSortBy]="roleComparator">{{'PROJECT.ROLE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="repoCountComparator">{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="timeComparator">{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of projects">
<clr-dg-action-overflow [hidden]="!(p.current_user_role_id === 1 || isSystemAdmin)">
<button class="action-item" (click)="newReplicationRule(p)" [hidden]="!isSystemAdmin">{{'PROJECT.REPLICATION_RULE' | translate}}</button>
<button class="action-item" (click)="toggleProject(p)">{{'PROJECT.MAKE' | translate}} {{(p.metadata.public === 'false' ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </button>
<button class="action-item" (click)="deleteProject(p)">{{'PROJECT.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-row *clrDgItems="let p of projects" [clrDgItem]="p">
<clr-dg-cell><a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a></clr-dg-cell>
<clr-dg-cell>{{ (p.metadata.public === 'true' ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell>
<clr-dg-cell *ngIf="showRoleInfo">{{roleInfo[p.current_user_role_id] | translate}}</clr-dg-cell>

View File

@ -17,7 +17,7 @@ import {
Input,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnDestroy
OnDestroy, EventEmitter
} from '@angular/core';
import { Router, NavigationExtras } from '@angular/router';
import { Project } from '../project';
@ -35,6 +35,10 @@ import { Subscription } from 'rxjs/Subscription';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../shared/shared.const';
import {TranslateService} from "@ngx-translate/core";
import {BatchInfo, BathInfoChanges} from "../../shared/confirmation-dialog/confirmation-batch-message";
import {Observable} from "rxjs/Observable";
import {AppConfigService} from "../../app-config.service";
@Component({
selector: 'list-project',
@ -46,6 +50,10 @@ export class ListProjectComponent implements OnDestroy {
projects: Project[] = [];
filteredType: number = 0;//All projects
searchKeyword: string = "";
selectedRow: Project[] = [];
batchDelectionInfos: BatchInfo[] = [];
@Output() addProject = new EventEmitter<void>();
roleInfo = RoleInfo;
repoCountComparator: Comparator<Project> = new CustomComparator<Project>("repo_count", "number");
@ -60,42 +68,20 @@ export class ListProjectComponent implements OnDestroy {
constructor(
private session: SessionService,
private appConfigService: AppConfigService,
private router: Router,
private searchTrigger: SearchTriggerService,
private proService: ProjectService,
private msgHandler: MessageHandlerService,
private statisticHandler: StatisticHandler,
private translate: TranslateService,
private deletionDialogService: ConfirmationDialogService,
private ref: ChangeDetectorRef) {
this.subscription = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.PROJECT) {
let projectId = message.data;
this.proService
.deleteProject(projectId)
.subscribe(
response => {
this.msgHandler.showSuccess('PROJECT.DELETED_SUCCESS');
let st: State = this.getStateAfterDeletion();
if (!st) {
this.refresh();
} else {
this.clrLoad(st);
this.statisticHandler.refresh();
}
},
error => {
if (error && error.status === 412) {
this.msgHandler.showError('PROJECT.FAILED_TO_DELETE_PROJECT', '');
} else {
this.msgHandler.handleError(error);
}
}
);
let hnd = setInterval(() => ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
this.delProjects(message.data);
}
});
@ -107,17 +93,41 @@ export class ListProjectComponent implements OnDestroy {
return this.filteredType !== 2;
}
get projectCreationRestriction(): boolean {
let account = this.session.getCurrentUser();
if (account) {
switch (this.appConfigService.getConfig().project_creation_restriction) {
case 'adminonly':
return (account.has_admin_role === 1);
case 'everyone':
return true;
}
}
return false;
}
public get isSystemAdmin(): boolean {
let account = this.session.getCurrentUser();
return account != null && account.has_admin_role > 0;
}
public get canDelete(): boolean {
if (this.projects.length) {
return this.projects.some((pro: Project) => pro.current_user_role_id === 1);
}
return false;
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
addNewProject(): void {
this.addProject.emit();
}
goToLink(proId: number): void {
this.searchTrigger.closeSearch(true);
@ -125,6 +135,11 @@ export class ListProjectComponent implements OnDestroy {
this.router.navigate(linkUrl);
}
selectedChange(): void {
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
}
clrLoad(state: State) {
//Keep state for future filtering and sorting
this.currentState = state;
@ -194,16 +209,68 @@ export class ListProjectComponent implements OnDestroy {
}
}
deleteProject(p: Project) {
let deletionMessage = new ConfirmationMessage(
'PROJECT.DELETION_TITLE',
'PROJECT.DELETION_SUMMARY',
p.name,
p.project_id,
ConfirmationTargets.PROJECT,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
deleteProjects(p: Project[]) {
let nameArr: string[] = [];
this.batchDelectionInfos = [];
if (p && p.length) {
p.forEach(data => {
nameArr.push(data.name);
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = data.name;
this.batchDelectionInfos.push(initBatchMessage);
});
this.deletionDialogService.addBatchInfoList(this.batchDelectionInfos);
let deletionMessage = new ConfirmationMessage(
'PROJECT.DELETION_TITLE',
'PROJECT.DELETION_SUMMARY',
nameArr.join(','),
p,
ConfirmationTargets.PROJECT,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
}
delProjects(datas: Project[]) {
let observableLists: any[] = [];
if (datas && datas.length) {
datas.forEach(data => {
observableLists.push(this.delOperate(data.project_id, data.name));
});
Promise.all(observableLists).then(item => {
let st: State = this.getStateAfterDeletion();
this.selectedRow = [];
if (!st) {
this.refresh();
} else {
this.clrLoad(st);
this.statisticHandler.refresh();
}
});
}
}
delOperate(id: number, name: string) {
let findedList = this.batchDelectionInfos.find(list => list.name === name);
return this.proService.deleteProject(id)
.then(
() => {
this.translate.get('BATCH.DELETED_SUCCESS').subscribe(res => {
findedList = BathInfoChanges(findedList, res);
});
},
error => {
if (error && error.status === 412) {
Observable.forkJoin(this.translate.get('BATCH.DELETED_FAILURE'),
this.translate.get('PROJECT.FAILED_TO_DELETE_PROJECT')).subscribe(res => {
findedList = BathInfoChanges(findedList, res[0], false, true, res[1]);
});
} else {
this.translate.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
}
});
}
refresh(): void {
@ -242,7 +309,7 @@ export class ListProjectComponent implements OnDestroy {
}
getStateAfterDeletion(): State {
let total: number = this.totalCount - 1;
let total: number = this.totalCount - this.selectedRow.length;
if (total <= 0) { return null; }
let totalPages: number = Math.ceil(total / this.pageSize);

View File

@ -12,4 +12,8 @@
.refresh-btn:hover {
color: #007CBB;
}
:host >>> .btn-group-overflow .dropdown-toggle {
line-height: 24px;
height: 24px;
}

View File

@ -2,8 +2,6 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="top: 8px;">
<div class="row flex-items-xs-between">
<div class="flex-xs-middle option-left" style="position: relative; top: 10px;">
<button *ngIf="hasProjectAdminRole" class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.MEMBER' | translate }}</button>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div>
<div class="flex-xs-middle option-right">
<hbr-filter [withDivider]="true" filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearch($event)" [currentValue]="searchMember"></hbr-filter>
@ -14,16 +12,21 @@
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid>
<clr-datagrid [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="SelectedChange()">
<clr-dg-action-bar>
<div class="btn-group">
<clr-button-group [clrMenuPosition]="'bottom-right'">
<clr-button class="btn btn-sm btn-secondary" (click)="openAddMemberModal()" [disabled]="!hasProjectAdminRole">{{'MEMBER.NEW_MEMBER' | translate }}</clr-button>
<clr-button class="btn btn-sm btn-secondary" (click)="deleteMembers(selectedRow)" [disabled]="!(selectedRow.length && hasProjectAdminRole)">{{'MEMBER.DELETE' | translate}}</clr-button>
<clr-button class="btn btn-sm btn-secondary" [clrInMenu]="true" (click)="changeRole(selectedRow, 1)" [disabled]="!(selectedRow.length && hasProjectAdminRole)">{{'MEMBER.PROJECT_ADMIN' | translate}}</clr-button>
<clr-button class="btn btn-sm btn-secondary" [clrInMenu]="true" (click)="changeRole(selectedRow, 2)" [disabled]="!(selectedRow.length && hasProjectAdminRole)">{{'MEMBER.DEVELOPER' | translate}}</clr-button>
<clr-button class="btn btn-sm btn-secondary" [clrInMenu]="true" (click)="changeRole(selectedRow, 3)" [disabled]="!(selectedRow.length && hasProjectAdminRole)">{{'MEMBER.GUEST' | translate}}</clr-button>
</clr-button-group>
</div>
</clr-dg-action-bar>
<clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let m of members">
<clr-dg-action-overflow [hidden]="m.user_id === currentUser.user_id || !hasProjectAdminRole">
<button class="action-item" [hidden]="m.role_id === 1" (click)="changeRole(m, 1)">{{'MEMBER.PROJECT_ADMIN' | translate}}</button>
<button class="action-item" [hidden]="m.role_id === 2" (click)="changeRole(m, 2)">{{'MEMBER.DEVELOPER' | translate}}</button>
<button class="action-item" [hidden]="m.role_id === 3" (click)="changeRole(m, 3)">{{'MEMBER.GUEST' | translate}}</button>
<button class="action-item" (click)="deleteMember(m)">{{'MEMBER.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-row *clrDgItems="let m of members" [clrDgItem]="m">
<clr-dg-cell>{{m.username}}</clr-dg-cell>
<clr-dg-cell>{{roleInfo[m.role_id] | translate}}</clr-dg-cell>
</clr-dg-row>
@ -34,4 +37,5 @@
</clr-dg-footer>
</clr-datagrid>
</div>
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
</div>

View File

@ -38,6 +38,8 @@ import 'rxjs/add/observable/throw';
import { Subscription } from 'rxjs/Subscription';
import { Project } from '../../project/project';
import {TranslateService} from "@ngx-translate/core";
import {BatchInfo, BathInfoChanges} from "../../shared/confirmation-dialog/confirmation-batch-message";
@Component({
templateUrl: 'member.component.html',
@ -58,11 +60,14 @@ export class MemberComponent implements OnInit, OnDestroy {
hasProjectAdminRole: boolean;
searchMember: string;
selectedRow: Member[] = [];
batchDelectionInfos: BatchInfo[] = [];
constructor(
private route: ActivatedRoute,
private router: Router,
private memberService: MemberService,
private translate: TranslateService,
private messageHandlerService: MessageHandlerService,
private deletionDialogService: ConfirmationDialogService,
private session: SessionService,
@ -72,15 +77,7 @@ export class MemberComponent implements OnInit, OnDestroy {
if (message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.PROJECT_MEMBER) {
this.memberService
.deleteMember(this.projectId, message.data)
.subscribe(
response => {
this.messageHandlerService.showSuccess('MEMBER.DELETED_SUCCESS');
this.retrieve(this.projectId, '');
},
error => this.messageHandlerService.handleError(error)
);
this.deleteMem(message.data);
}
});
let hnd = setInterval(()=>ref.markForCheck(), 100);
@ -110,7 +107,7 @@ export class MemberComponent implements OnInit, OnDestroy {
ngOnInit() {
//Get projectId from route params snapshot.
this.projectId = +this.route.snapshot.parent.params['id'];
this.projectId = +this.route.snapshot.parent.params['id'];
//Get current user from registered resolver.
this.currentUser = this.session.getCurrentUser();
let resolverData = this.route.snapshot.parent.data;
@ -129,30 +126,93 @@ export class MemberComponent implements OnInit, OnDestroy {
this.retrieve(this.projectId, '');
}
changeRole(m: Member, roleId: number) {
if(m) {
this.memberService
.changeMemberRole(this.projectId, m.user_id, roleId)
.subscribe(
response => {
this.messageHandlerService.showSuccess('MEMBER.SWITCHED_SUCCESS');
this.retrieve(this.projectId, '');
},
error => this.messageHandlerService.handleError(error)
);
changeRole(m: Member[], roleId: number) {
if (m) {
let promiseList: any[] = [];
m.forEach(data => {
if (!(data.user_id === this.currentUser.user_id || !this.hasProjectAdminRole)) {
promiseList.push(this.memberService.changeMemberRole(this.projectId, data.user_id, roleId));
}
})
Promise.all(promiseList).then(num => {
if (num.length === promiseList.length) {
this.messageHandlerService.showSuccess('MEMBER.SWITCHED_SUCCESS');
this.retrieve(this.projectId, '');
}
},
error => {
this.messageHandlerService.handleError(error);
}
);
}
}
deleteMember(m: Member) {
let deletionMessage: ConfirmationMessage = new ConfirmationMessage(
'MEMBER.DELETION_TITLE',
'MEMBER.DELETION_SUMMARY',
m.username,
m.user_id,
ConfirmationTargets.PROJECT_MEMBER,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
deleteMembers(m: Member[]) {
let nameArr: string[] = [];
this.batchDelectionInfos = [];
if (m && m.length) {
m.forEach(data => {
nameArr.push(data.username);
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = data.username;
this.batchDelectionInfos.push(initBatchMessage);
});
this.deletionDialogService.addBatchInfoList(this.batchDelectionInfos);
let deletionMessage = new ConfirmationMessage(
'PROJECT.DELETION_TITLE',
'PROJECT.DELETION_SUMMARY',
nameArr.join(','),
m,
ConfirmationTargets.PROJECT_MEMBER,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
}
deleteMem(members: Member[]) {
if (members && members.length) {
let promiseLists: any[] = [];
members.forEach(member => {
if (member.user_id === this.currentUser.user_id) {
let findedList = this.batchDelectionInfos.find(data => data.name === member.username);
this.translate.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
}else {
promiseLists.push(this.delOperate(this.projectId, member.user_id, member.username));
}
});
Promise.all(promiseLists).then(item => {
this.selectedRow = [];
this.retrieve(this.projectId, '');
});
}
}
delOperate(projectId: number, memberId: number, username: string) {
let findedList = this.batchDelectionInfos.find(data => data.name === username);
return this.memberService
.deleteMember(projectId, memberId)
.then(
response => {
this.translate.get('BATCH.DELETED_SUCCESS').subscribe(res => {
findedList = BathInfoChanges(findedList, res);
});
},
error => {
this.translate.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
}
);
}
SelectedChange(): void {
//this.forceRefreshView(5000);
}
doSearch(searchMember: string) {

View File

@ -41,17 +41,17 @@ export class MemberService {
.catch(error=>Observable.throw(error));
}
changeMemberRole(projectId: number, userId: number, roleId: number): Observable<any> {
changeMemberRole(projectId: number, userId: number, roleId: number): Promise<any> {
return this.http
.put(`/api/projects/${projectId}/members/${userId}`, { roles: [ roleId ]}, HTTP_JSON_OPTIONS)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
.put(`/api/projects/${projectId}/members/${userId}`, { roles: [ roleId ]}, HTTP_JSON_OPTIONS).toPromise()
.then(response=>response.status)
.catch(error=>Promise.reject(error));
}
deleteMember(projectId: number, userId: number): Observable<any> {
deleteMember(projectId: number, userId: number): Promise<any> {
return this.http
.delete(`/api/projects/${projectId}/members/${userId}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
.delete(`/api/projects/${projectId}/members/${userId}`).toPromise()
.then(response=>response.status)
.catch(error=>Promise.reject(error));
}
}

View File

@ -16,4 +16,7 @@
.refresh-btn:hover {
color: #007CBB;
}
.rightPos {
position: absolute; right: 20px; margin-top: 16px; height:32px;
}

View File

@ -6,11 +6,7 @@
<statistics-panel></statistics-panel>
</div>
</div>
<div class="row flex-items-xs-between" style="height:32px;">
<div class="option-left">
<button *ngIf="projectCreationRestriction" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button>
<create-project (create)="createProject($event)"></create-project>
</div>
<div class="row flex-items-xs-between rightPos">
<div class="option-right">
<div class="select" style="float: left; left:-6px; top:8px;">
<select (change)="doFilterProjects()" [(ngModel)]="selecteType">
@ -25,6 +21,7 @@
</span>
</div>
</div>
<list-project></list-project>
<create-project (create)="createProject($event)"></create-project>
<list-project (addProject)="openModal()"></list-project>
</div>
</div>

View File

@ -16,8 +16,6 @@ import { Router } from '@angular/router';
import { Project } from './project';
import { CreateProjectComponent } from './create-project/create-project.component';
import { ListProjectComponent } from './list-project/list-project.component';
import { AppConfigService } from '../app-config.service';
import { SessionService } from '../shared/session.service';
import { ProjectTypes } from '../shared/shared.const';
@Component({
@ -49,9 +47,7 @@ export class ProjectComponent implements OnInit {
}
}
constructor(
private appConfigService: AppConfigService,
private sessionService: SessionService) {
constructor() {
}
ngOnInit(): void {
@ -61,19 +57,6 @@ export class ProjectComponent implements OnInit {
}
}
get projectCreationRestriction(): boolean {
let account = this.sessionService.getCurrentUser();
if (account) {
switch (this.appConfigService.getConfig().project_creation_restriction) {
case 'adminonly':
return (account.has_admin_role === 1);
case 'everyone':
return true;
}
}
return false;
}
openModal(): void {
this.creationProject.newProject();
}

View File

@ -74,11 +74,11 @@ export class ProjectService {
.catch(error => Observable.throw(error));
}
deleteProject(projectId: number): Observable<any> {
deleteProject(projectId: number): Promise<any> {
return this.http
.delete(`/api/projects/${projectId}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
.delete(`/api/projects/${projectId}`).toPromise()
.then(response=>response.status)
.catch(error=>Promise.reject(error));
}
checkProjectExists(projectName: string): Observable<any> {

View File

@ -0,0 +1,27 @@
/**
* Created by pengf on 11/22/2017.
*/
export class BatchInfo {
name: string;
status: string;
loading: boolean;
errorState: boolean;
errorInfo: string;
constructor() {
this.status = 'pending';
this.loading = false;
this.errorState = false;
this.errorInfo = '';
}
}
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;
}

View File

@ -16,4 +16,12 @@
vertical-align: middle;
width: 80%;
white-space: pre-wrap;
}
}
.batchInfoUl{
padding: 20px; list-style-type: none;
}
.batchInfoUl li {line-height: 24px;border-bottom: 1px solid #e8e8e8;}
.batchInfoUl li span:first-child {padding-right: 20px; width: 210px; display: inline-block; color:#666;}
.batchInfoUl li span:last-child {width: 260px; display: inline-block; color:#666;}
.batchInfoUl li span i {display: inline-block; line-height: 1.2em; font-size: 0.8em; color: #999;}
.batchInfoUl li span a{cursor: pointer; text-decoration: underline;}

View File

@ -5,6 +5,18 @@
<clr-icon shape="warning" class="is-warning" size="64"></clr-icon>
</div>
<div class="confirmation-content">{{dialogContent}}</div>
<div>
<ul class="batchInfoUl">
<li *ngFor="let info of resultLists">
<span> <i class="spinner spinner-inline spinner-pos" [hidden]='!info.loading'></i>&nbsp;&nbsp;{{info.name}}</span>
<span *ngIf="!info.errorInfo.length" [style.color]="colorChange(info)">{{info.status}}</span>
<span *ngIf="info.errorInfo.length" [style.color]="colorChange(info)">
<a (click)="toggleErrorTitle(errorInfo)" >{{info.status}}</a><br>
<i #errorInfo style="display: none;">{{info.errorInfo}}</i>
</span>
</li>
</ul>
</div>
</div>
<div class="modal-footer" [ngSwitch]="buttons">
<ng-template [ngSwitchCase]="0">
@ -16,8 +28,9 @@
<button type="button" class="btn btn-primary" (click)="confirm()">{{ 'BUTTON.YES' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="2">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-danger" (click)="confirm()">{{ 'BUTTON.DELETE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="isDelete">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-danger" (click)="confirm()" [hidden]="isDelete">{{'BUTTON.DELETE' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="cancel()" [disabled]="!batchOverStatus" [hidden]="!isDelete">{{'BUTTON.CLOSE' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="3">
<button type="button" class="btn btn-primary" (click)="cancel()">{{'BUTTON.CLOSE' | translate}}</button>

View File

@ -19,6 +19,7 @@ import { ConfirmationDialogService } from './confirmation-dialog.service';
import { ConfirmationMessage } from './confirmation-message';
import { ConfirmationAcknowledgement } from './confirmation-state-message';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared.const';
import {BatchInfo} from "./confirmation-batch-message";
@Component({
selector: 'confiramtion-dialog',
@ -31,8 +32,31 @@ export class ConfirmationDialogComponent implements OnDestroy {
dialogTitle: string = "";
dialogContent: string = "";
message: ConfirmationMessage;
resultLists: BatchInfo[] = [];
annouceSubscription: Subscription;
batchInfoSubscription: Subscription;
buttons: ConfirmationButtons;
isDelete: boolean = false;
get batchOverStatus(): boolean {
if (this.resultLists.length) {
return this.resultLists.every(item => item.loading === false);
}
return false;
}
colorChange(list: BatchInfo) {
if (!list.loading && !list.errorState) {
return 'green';
}else if (!list.loading && list.errorState) {
return 'red';
}else {
return '#666';
}
}
toggleErrorTitle(errorSpan: any) {
errorSpan.style.display = (errorSpan.style.display === 'none') ? 'block' : 'none';
}
constructor(
private confirmationService: ConfirmationDialogService,
@ -47,12 +71,19 @@ export class ConfirmationDialogComponent implements OnDestroy {
this.buttons = msg.buttons;
this.open();
});
this.batchInfoSubscription = confirmationService.confirmationBatch$.subscribe(data => {
this.resultLists = data;
});
}
ngOnDestroy(): void {
if (this.annouceSubscription) {
this.annouceSubscription.unsubscribe();
}
if (this.batchInfoSubscription) {
this.resultLists = [];
this.batchInfoSubscription.unsubscribe();
}
}
open(): void {
@ -60,6 +91,7 @@ export class ConfirmationDialogComponent implements OnDestroy {
}
close(): void {
this.resultLists = [];
this.opened = false;
}
@ -76,6 +108,7 @@ export class ConfirmationDialogComponent implements OnDestroy {
data,
target
));
this.isDelete = false;
this.close();
}
@ -85,6 +118,11 @@ export class ConfirmationDialogComponent implements OnDestroy {
return;
}
if (this.resultLists.length) {
this.resultLists.every(item => item.loading = true);
this.isDelete = true;
}
let data: any = this.message.data ? this.message.data : {};
let target = this.message.targetId ? this.message.targetId : ConfirmationTargets.EMPTY;
this.confirmationService.confirm(new ConfirmationAcknowledgement(
@ -92,6 +130,6 @@ export class ConfirmationDialogComponent implements OnDestroy {
data,
target
));
this.close();
}
}

View File

@ -17,14 +17,17 @@ import { Subject } from 'rxjs/Subject';
import { ConfirmationMessage } from './confirmation-message';
import { ConfirmationState } from '../shared.const';
import { ConfirmationAcknowledgement } from './confirmation-state-message';
import {BatchInfo} from "./confirmation-batch-message";
@Injectable()
export class ConfirmationDialogService {
confirmationAnnoucedSource = new Subject<ConfirmationMessage>();
confirmationConfirmSource = new Subject<ConfirmationAcknowledgement>();
confirmationBatchSource = new Subject<BatchInfo[]>();
confirmationAnnouced$ = this.confirmationAnnoucedSource.asObservable();
confirmationConfirm$ = this.confirmationConfirmSource.asObservable();
confirmationBatch$ = this.confirmationBatchSource.asObservable();
//User confirm the action
public confirm(ack: ConfirmationAcknowledgement): void {
@ -40,4 +43,7 @@ export class ConfirmationDialogService {
public openComfirmDialog(message: ConfirmationMessage): void {
this.confirmationAnnoucedSource.next(message);
}
public addBatchInfoList(data: BatchInfo[]): void {
this.confirmationBatchSource.next(data);
}
}

View File

@ -38,4 +38,7 @@
.hide-create {
visibility: hidden !important;
}
.rightPos {
position: absolute; right: 20px; margin-top: -7px; height:32px; z-index: 100;
}

View File

@ -1,10 +1,7 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}</h2>
<div class="action-panel-pos">
<span>
<button [class.hide-create]="!canCreateUser" type="submit" class="btn btn-link custom-add-button" (click)="addNewUser()"><clr-icon shape="add"></clr-icon> {{'USER.ADD_ACTION' | translate}}</button>
</span>
<div class="action-panel-pos rightPos">
<hbr-filter [withDivider]="true" class="filter-pos" filterPlaceholder='{{"USER.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)" [currentValue]="currentTerm"></hbr-filter>
<span class="refresh-btn">
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress" (click)="refresh()"></clr-icon>
@ -12,16 +9,19 @@
</span>
</div>
<div>
<clr-datagrid (clrDgRefresh)="load($event)" [clrDgLoading]="inProgress">
<clr-datagrid (clrDgRefresh)="load($event)" [clrDgLoading]="inProgress" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="SelectedChange()">
<clr-dg-action-bar>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-secondary" (click)="addNewUser()">{{'USER.ADD_ACTION' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!ifSameRole" (click)="changeAdminRole()" >{{ISADMNISTRATOR | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" (click)="deleteUsers(selectedRow)" [disabled]="!selectedRow.length">{{'USER.DEL_ACTION' | translate}}</button>
</div>
</clr-dg-action-bar>
<clr-dg-column>{{'USER.COLUMN_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_ADMIN' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_EMAIL' | translate}}</clr-dg-column>
<clr-dg-column>{{'USER.COLUMN_REG_NAME' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let user of users" [clrDgItem]="user">
<clr-dg-action-overflow [hidden]="isMySelf(user.user_id)">
<button class="action-item" (click)="changeAdminRole(user)">{{adminActions(user)}}</button>
<button class="action-item" (click)="deleteUser(user)">{{'USER.DEL_ACTION' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-row *clrDgItems="let user of users" [clrDgItem]="user">
<clr-dg-cell>{{user.username}}</clr-dg-cell>
<clr-dg-cell>{{isSystemAdmin(user)}}</clr-dg-cell>
<clr-dg-cell>{{user.email}}</clr-dg-cell>

View File

@ -21,16 +21,17 @@ import { NewUserModalComponent } from './new-user-modal.component';
import { TranslateService } from '@ngx-translate/core';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const'
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import { SessionService } from '../shared/session.service';
import { AppConfigService } from '../app-config.service';
import {BatchInfo, BathInfoChanges} from '../shared/confirmation-dialog/confirmation-batch-message';
/**
* NOTES:
* Pagination for this component is a temporary workaround solution. It will be replaced in future release.
*
*
* @export
* @class UserComponent
* @implements {OnInit}
@ -52,6 +53,9 @@ export class UserComponent implements OnInit, OnDestroy {
private adminMenuText: string = "";
private adminColumn: string = "";
private deletionSubscription: Subscription;
selectedRow: User[] = [];
ISADMNISTRATOR: string = "USER.ENABLE_ADMIN_ACTION";
batchDelectionInfos: BatchInfo[] = [];
currentTerm: string;
totalCount: number = 0;
@ -91,7 +95,7 @@ export class UserComponent implements OnInit, OnDestroy {
}
private isMatchFilterTerm(terms: string, testedItem: string): boolean {
return testedItem.toLowerCase().indexOf(terms.toLowerCase()) != -1;
return testedItem.toLowerCase().indexOf(terms.toLowerCase()) !== -1;
}
public get canCreateUser(): boolean {
@ -103,6 +107,25 @@ export class UserComponent implements OnInit, OnDestroy {
}
}
public get ifSameRole(): boolean {
let usersRole: number[] = [];
this.selectedRow.forEach(user => {
if (user.user_id === 0 || this.isMySelf(user.user_id)) {
return false;
}
usersRole.push(user.has_admin_role);
})
if (usersRole.length && usersRole.every(num => num === 0)) {
this.ISADMNISTRATOR = 'USER.ENABLE_ADMIN_ACTION';
return true;
}
if (usersRole.length && usersRole.every(num => num === 1)) {
this.ISADMNISTRATOR = 'USER.DISABLE_ADMIN_ACTION';
return true;
}
return false;
}
isSystemAdmin(u: User): string {
if (!u) {
return "{{MISS}}";
@ -125,9 +148,7 @@ export class UserComponent implements OnInit, OnDestroy {
return this.onGoing;
}
ngOnInit(): void {
this.forceRefreshView(5000);
}
ngOnInit(): void {}
ngOnDestroy(): void {
if (this.deletionSubscription) {
@ -155,73 +176,105 @@ export class UserComponent implements OnInit, OnDestroy {
});
}
//Disable the admin role for the specified user
changeAdminRole(user: User): void {
//Double confirm user is existing
if (!user || user.user_id === 0) {
return;
}
// Disable the admin role for the specified user
changeAdminRole(): void {
let promiseLists: any[] = [];
if (this.selectedRow.length) {
if (this.ISADMNISTRATOR === 'USER.ENABLE_ADMIN_ACTION') {
for (let i = 0; i < this.selectedRow.length; i++) {
// Double confirm user is existing
if (this.selectedRow[i].user_id === 0 || this.isMySelf(this.selectedRow[i].user_id)) {
continue;
}
let updatedUser: User = new User();
updatedUser.user_id = this.selectedRow[i].user_id;
if (this.isMySelf(user.user_id)) {
return;
}
updatedUser.has_admin_role = 1; //Set as admin
promiseLists.push(this.userService.updateUserRole(updatedUser));
}
}
if (this.ISADMNISTRATOR === 'USER.DISABLE_ADMIN_ACTION') {
for (let i = 0; i < this.selectedRow.length; i++) {
// Double confirm user is existing
if (this.selectedRow[i].user_id === 0 || this.isMySelf(this.selectedRow[i].user_id)) {
continue;
}
let updatedUser: User = new User();
updatedUser.user_id = this.selectedRow[i].user_id;
//Value copy
let updatedUser: User = new User();
updatedUser.user_id = user.user_id;
updatedUser.has_admin_role = 0; //Set as none admin
promiseLists.push(this.userService.updateUserRole(updatedUser));
}
}
if (user.has_admin_role === 0) {
updatedUser.has_admin_role = 1;//Set as admin
} else {
updatedUser.has_admin_role = 0;//Set as none admin
}
this.userService.updateUserRole(updatedUser)
.then(() => {
//Change view now
user.has_admin_role = updatedUser.has_admin_role;
this.forceRefreshView(5000);
})
.catch(error => {
this.msgHandler.handleError(error);
})
Promise.all(promiseLists).then(() => {
this.selectedRow = [];
this.refresh()
})
.catch(error => {
this.selectedRow = [];
this.msgHandler.handleError(error);
});
}
}
//Delete the specified user
deleteUser(user: User): void {
if (!user) {
return;
}
if (this.isMySelf(user.user_id)) {
return; //Double confirm
}
deleteUsers(users: User[]): void {
let userArr: string[] = [];
this.batchDelectionInfos = [];
if (users && users.length) {
for (let i = 0; i < users.length; i++) {
if (this.isMySelf(users[i].user_id)) {
continue; //Double confirm
}
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = users[i].username;
this.batchDelectionInfos.push(initBatchMessage);
userArr.push(users[i].username);
}
this.deletionDialogService.addBatchInfoList(this.batchDelectionInfos);
//Confirm deletion
let msg: ConfirmationMessage = new ConfirmationMessage(
"USER.DELETION_TITLE",
"USER.DELETION_SUMMARY",
user.username,
user,
userArr.join(','),
users,
ConfirmationTargets.USER,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(msg);
}
}
delUser(user: User): void {
this.userService.deleteUser(user.user_id)
.then(() => {
//Remove it from current user list
//and then view refreshed
delUser(users: User[]): void {
//this.batchInfoDialog.open();
let promiseLists: any[] = [];
if (users && users.length) {
users.forEach(user => {
promiseLists.push(this.delOperate(user.user_id, user.username));
});
Promise.all(promiseLists).then((item) => {
this.selectedRow = [];
this.currentTerm = '';
this.msgHandler.showSuccess("USER.DELETE_SUCCESS");
this.msgHandler.showSuccess('USER.DELETE_SUCCESS');
this.refresh();
})
.catch(error => {
this.msgHandler.handleError(error);
});
}
}
delOperate(id: number, name: string) {
let findedList = this.batchDelectionInfos.find(data => data.name === name);
return this.userService.deleteUser(id).then(() => {
this.translate.get('BATCH.DELETE_SUCCESS').subscribe(res => {
findedList = BathInfoChanges(findedList, res);
});
}).catch(error => {
this.translate.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
});
}
//Refresh the user list
@ -284,6 +337,10 @@ export class UserComponent implements OnInit, OnDestroy {
this.refreshUser(0, 15);
}
SelectedChange(): void {
this.forceRefreshView(5000);
}
forceRefreshView(duration: number): void {
//Reset timer
if (this.timerHandler) {

View File

@ -35,6 +35,10 @@
"COPY": "COPY",
"EDIT": "EDIT"
},
"BATCH": {
"DELETED_SUCCESS": "Deleted successfully",
"DELETED_FAILURE": "Deleted failed"
},
"TOOLTIP": {
"EMAIL": "Email should be a valid email address like name@example.com.",
"USER_NAME": "Cannot contain special characters and maximum length should be 20 characters.",
@ -102,7 +106,7 @@
"LOGS": "Logs"
},
"USER": {
"ADD_ACTION": "USER",
"ADD_ACTION": "New User",
"ENABLE_ADMIN_ACTION": "Set as Administrator",
"DISABLE_ADMIN_ACTION": "Revoke Administrator",
"DEL_ACTION": "Delete",
@ -117,7 +121,7 @@
"SAVE_SUCCESS": "New user created successfully.",
"DELETION_TITLE": "Confirm user deletion",
"DELETION_SUMMARY": "Do you want to delete user {{param}}?",
"DELETE_SUCCESS": "User deleted successfully.",
"DELETE_SUCCESS": "Users deleted successfully.",
"ITEMS": "items",
"OF": "of"
},
@ -151,7 +155,7 @@
"FILTER_PLACEHOLDER": "Filter Projects",
"REPLICATION_RULE": "Replication Rule",
"CREATED_SUCCESS": "Created project successfully.",
"DELETED_SUCCESS": "Deleted project successfully.",
"DELETED_SUCCESS": "Deleted projects successfully.",
"TOGGLED_SUCCESS": "Toggled project successfully.",
"FAILED_TO_DELETE_PROJECT": "Project contains repositories or replication rules cannot be deleted.",
"INLINE_HELP_PUBLIC": "When a project is set to public, anyone has read permission to the repositories under this project, and the user does not need to run \"docker login\" before pulling images under this project.",
@ -199,8 +203,8 @@
"DELETION_TITLE": "Confirm project member deletion",
"DELETION_SUMMARY": "Do you want to delete project member {{param}}?",
"ADDED_SUCCESS": "Added member successfully.",
"DELETED_SUCCESS": "Deleted member successfully.",
"SWITCHED_SUCCESS": "Switched member role successfully.",
"DELETED_SUCCESS": "Deleted members successfully.",
"SWITCHED_SUCCESS": "Switched members role successfully.",
"OF": "of"
},
"AUDIT_LOG": {
@ -287,8 +291,8 @@
"CONFIRM_TOGGLE_DISABLE_POLICY": "After disabling the rule, all unfinished replication jobs of this rule will be stopped and canceled. \nPlease confirm to continue.",
"CREATED_SUCCESS": "Created replication rule successfully.",
"UPDATED_SUCCESS": "Updated replication rule successfully.",
"DELETED_SUCCESS": "Deleted replication rule successfully.",
"DELETED_FAILED": "Deleted replication rule failed.",
"DELETED_SUCCESS": "Deleted replications rule successfully.",
"DELETED_FAILED": "Deleted replications rule failed.",
"TOGGLED_SUCCESS": "Toggled replication rule status successfully.",
"CANNOT_EDIT": "Replication rule cannot be changed while it is enabled.",
"POLICY_ALREADY_EXISTS": "Replication rule already exists.",
@ -311,7 +315,7 @@
"TEST_CONNECTION": "Test Connection",
"TITLE_EDIT": "Edit Endpoint",
"TITLE_ADD": "Create Endpoint",
"DELETE": "Delete Endpoint",
"DELETE": "Delete",
"TESTING_CONNECTION": "Testing Connection...",
"TEST_CONNECTION_SUCCESS": "Connection tested successfully.",
"TEST_CONNECTION_FAILURE": "Failed to ping endpoint.",
@ -323,8 +327,8 @@
"ITEMS": "items",
"CREATED_SUCCESS": "Created endpoint successfully.",
"UPDATED_SUCCESS": "Updated endpoint successfully.",
"DELETED_SUCCESS": "Deleted endpoint successfully.",
"DELETED_FAILED": "Deleted endpoint failed.",
"DELETED_SUCCESS": "Deleted endpoints successfully.",
"DELETED_FAILED": "Deleted endpoints failed.",
"CANNOT_EDIT": "Endpoint cannot be changed while the replication rule is enabled.",
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use.",
"PLACEHOLDER": "We couldn't find any endpoints!"
@ -361,8 +365,8 @@
"OF": "of",
"ITEMS": "items",
"POP_REPOS": "Popular Repositories",
"DELETED_REPO_SUCCESS": "Deleted repository successfully.",
"DELETED_TAG_SUCCESS": "Deleted tag successfully.",
"DELETED_REPO_SUCCESS": "Deleted repositories successfully.",
"DELETED_TAG_SUCCESS": "Deleted tags successfully.",
"COPY": "Copy",
"NOTARY_IS_UNDETERMINED": "Cannot determine the signature of this tag.",
"PLACEHOLDER": "We couldn't find any repositories!",

View File

@ -102,7 +102,7 @@
"LOGS": "Logs"
},
"USER": {
"ADD_ACTION": "USUARIO",
"ADD_ACTION": "New User",
"ENABLE_ADMIN_ACTION": "Marcar como Administrador",
"DISABLE_ADMIN_ACTION": "Desmarcar como Administrador",
"DEL_ACTION": "Eliminar",
@ -311,7 +311,7 @@
"TEST_CONNECTION": "Comprobar conexión",
"TITLE_EDIT": "Editar Endpoint",
"TITLE_ADD": "Crear Endpoint",
"DELETE": "Eliminar Endpoint",
"DELETE": "Eliminar",
"TESTING_CONNECTION": "Comprobar conexión...",
"TEST_CONNECTION_SUCCESS": "Conexión comprobada satisfactoriamente.",
"TEST_CONNECTION_FAILURE": "Fallo al comprobar el endpoint.",

View File

@ -102,7 +102,7 @@
"LOGS": "日志"
},
"USER": {
"ADD_ACTION": "用户",
"ADD_ACTION": "创建用户",
"ENABLE_ADMIN_ACTION": "设置为管理员",
"DISABLE_ADMIN_ACTION": "取消管理员",
"DEL_ACTION": "删除",
@ -311,7 +311,7 @@
"TEST_CONNECTION": "测试连接",
"TITLE_EDIT": "编辑目标",
"TITLE_ADD": "新建目标",
"DELETE": "删除目标",
"DELETE": "删除",
"TESTING_CONNECTION": "正在测试连接...",
"TEST_CONNECTION_SUCCESS": "测试连接成功。",
"TEST_CONNECTION_FAILURE": "测试连接失败。",

View File

@ -47,3 +47,4 @@
-o-transform: rotate(-90deg);
transform: rotate(-90deg);
}
.datagrid-spinner{margin-top: 24px;}

View File

@ -25,9 +25,10 @@ Assign User Admin
Click Element xpath=//harbor-user//hbr-filter//clr-icon
Input Text xpath=//harbor-user//hbr-filter//input ${user}
Sleep 2
Click Element xpath=//harbor-user/div/div/h2
Click Element xpath=//harbor-user//clr-datagrid//clr-dg-action-overflow
Click Element xpath=//harbor-user//clr-dg-action-overflow//button[contains(.,'Admin')]
#select checkbox
Click Element //clr-dg-row[contains(.,"${user}")]//label
#click assign admin
Click Element //button[contains(.,'Set as')]
Sleep 1
Switch to User Tag

View File

@ -16,6 +16,6 @@
Documentation This resource provides any keywords related to the Harbor private registry appliance
*** Variables ***
${project_create_xpath} //project//div[@class="option-left"]/button
${project_create_xpath} //clr-dg-action-bar//button[contains(.,'New')]
${self_reg_xpath} //input[@id="clr-checkbox-selfReg"]
${test_ldap_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[3]
${test_ldap_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[3]

View File

@ -29,7 +29,6 @@ Go Into Project
Sleep 8
Wait Until Page Contains ${project}
Click Element xpath=//*[@id="results"]/list-project-ro//clr-dg-cell[contains(.,"${project}")]/a
Sleep 2
Capture Page Screenshot gointo_${project}.png
@ -65,26 +64,34 @@ Change Project Member Role
Sleep 2
Click Element xpath=${project_member_tag_xpath}
Sleep 1
Click Element xpath=//project-detail//clr-dg-row[contains(.,'${user}')]//clr-dg-action-overflow
Sleep 1
Click Element xpath=//project-detail//clr-dg-action-overflow//button[contains(.,"${role}")]
Click Element xpath=//project-detail//clr-dg-row[contains(.,'${user}')]//label
Sleep 1
#change role
Click Element //button[@class='btn dropdown-toggle']
Click Element //button[contains(.,'${role}')]
#Click Element xpath=//project-detail//clr-dg-action-overflow//button[contains(.,"${role}")]
Sleep 2
Wait Until Page Contains ${role}
User Can Change Role
[arguments] ${username}
Page Should Contain Element xpath=//project-detail//clr-dg-row[contains(.,'${username}')]//clr-dg-action-overflow
Click Element xpath=//clr-dg-row[contains(.,'${username}')]//input/../label
Click Element xpath=//button[@class='btn dropdown-toggle']
Page Should Not Contain Element xpath=//button[@disabled='' and contains(.,'Admin')]
User Can Not Change Role
[arguments] ${username}
Page Should Contain Element xpath=//project-detail//clr-dg-row[contains(.,'${username}')]//clr-dg-action-overflow[@hidden=""]
Click Element xpath=//clr-dg-row[contains(.,'${username}')]//input/../label
Click Element xpath=//button[@class='btn dropdown-toggle']
Page Should Contain Element xpath=//button[@disabled='' and contains(.,'Admin')]
#this keyworkd seems will not use any more, will delete in the future
Non-admin View Member Account
[arguments] ${times}
Xpath Should Match X Times //project-detail//clr-dg-action-overflow[@hidden=""] ${times}
Xpath Should Match X Times //clr-dg-row-master ${times}
User Can Not Add Member
Page Should Not Contain Element xpath=${project_member_add_button_xpath}
Page Should Contain Element xpath=//button[@disabled='' and contains(.,'New')]
Add Guest Member To Project
[arguments] ${member}
@ -99,11 +106,12 @@ Add Guest Member To Project
Delete Project Member
[arguments] ${member}
Click Element xpath=//project-detail//clr-dg-row[contains(.,'${member}')]//clr-dg-action-overflow
Click Element xpath=//clr-dg-row[contains(.,'${member}')]//input/../label
Click Element xpath=${project_member_delete_button_xpath}
Sleep 1
Click Element xpath=${project_member_delete_confirmation_xpath}
Sleep 1
Click Element xpath=//button[contains(.,'CLOSE')]
User Should Be Owner Of Project
[Arguments] ${user} ${pwd} ${project}
@ -144,8 +152,8 @@ User Should Be Guest
Project Should Display ${project}
Go Into Project ${project}
Switch To Member
Non-admin View Member Account 2
User Can Not Add Member
Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Guest')]
Logout Harbor
Pull image ${ip} ${user} ${pwd} ${project} hello-world
Cannot Push image ${ip} ${user} ${pwd} ${project} hello-world
@ -156,8 +164,8 @@ User Should Be Developer
Project Should Display ${project}
Go Into Project ${project}
Switch To Member
Non-admin View Member Account 2
User Can Not Add Member
Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Developer')]
Logout Harbor
Push Image With Tag ${ip} ${user} ${pwd} ${project} hello-world ${ip}/${project}/hello-world:v1
@ -169,5 +177,6 @@ User Should Be Admin
Switch To Member
Add Guest Member To Project ${guest}
User Can Change Role ${guest}
Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Admin')]
Logout Harbor
Push Image With Tag ${ip} ${user} ${pwd} ${project} hello-world ${ip}/${project}/hello-world:v2
Push Image With Tag ${ip} ${user} ${pwd} ${project} hello-world ${ip}/${project}/hello-world:v2

View File

@ -24,6 +24,8 @@ ${project_member_add_save_button_xpath} /html/body/harbor-app/harbor-shell/clr-
${project_member_search_button_xpath} //project-detail//hbr-filter/span/clr-icon
${project_member_search_text_xpath} //project-detail//hbr-filter/span/input
${project_member_add_confirmation_ok_xpath} //project-detail//add-member//button[2]
${project_member_search_button_xpath2} //button[contains(.,'New')]
${project_member_add_button_xpath2} //project-detail//add-member//button[2]
${project_member_guest_radio_checkbox} //project-detail//form//input[@id='checkrads_guest']
${project_member_delete_button_xpath} //project-detail//clr-dg-cell//clr-dg-action-overflow//button[contains(.,"Delete")]
${project_member_delete_confirmation_xpath} //confiramtion-dialog//button[2]
${project_member_delete_button_xpath} //button[contains(.,"Delete")]
${project_member_delete_confirmation_xpath} //confiramtion-dialog//button[2]

View File

@ -29,7 +29,7 @@ Create An New Project
Input Text xpath=${project_name_xpath} ${projectname}
Sleep 3
Run Keyword If '${public}' == 'true' Click Element xpath=${project_public_xpath}
Click Element css=${project_save_css}
Click Element xpath=//button[contains(.,'OK')]
Sleep 4
Wait Until Page Contains ${projectname}
Wait Until Page Contains Project Admin
@ -79,33 +79,50 @@ Search Private Projects
Make Project Private
[Arguments] ${projectname}
Go Into Project ${project name}
Sleep 1
Click Element xpath=//project//list-project//clr-dg-row[contains(.,'${projectname}')]//clr-dg-action-overflow/button
Click element xpath=//project//list-project//clr-dg-action-overflow//button[contains(.,"Make Private")]
Click Element xpath=//project-detail//a[contains(.,'Configuration')]
#Click element xpath=//project//list-project//clr-dg-row-master[contains(.,'${projectname}')]//clr-dg-action-overflow
#Click element xpath=//project//list-project//clr-dg-action-overflow//button[contains(.,"Make Private")]
Checkbox Should Be Selected xpath=//input[@name='public']
Click Element //clr-checkbox[@name='public']//label
Click Element //button[contains(.,'SAVE')]
Make Project Public
[Arguments] ${projectname}
Go Into Project ${project name}
Sleep 1
Click element xpath=//project//list-project//clr-dg-row[contains(.,'${projectname}')]//clr-dg-action-overflow/button
Click element xpath=//project//list-project//clr-dg-action-overflow//button[contains(.,"Make Public")]
Click Element xpath=//project-detail//a[contains(.,'Configuration')]
#Click element xpath=//project//list-project//clr-dg-row-master[contains(.,'${projectname}')]//clr-dg-action-overflow
#Click element xpath=//project//list-project//clr-dg-action-overflow//button[contains(.,"Make Public")]
Checkbox Should Not Be Selected xpath=//input[@name='public']
Click Element //clr-checkbox[@name='public']//label
Click Element //button[contains(.,'SAVE')]
Delete Repo
[Arguments] ${projectname}
Click Element xpath=//project-detail//clr-dg-row[contains(.,"${projectname}")]//clr-dg-action-overflow
#Click Element xpath=//project-detail//clr-dg-row-master[contains(.,"${projectname}")]//clr-dg-action-overflow
Click Element xpath=//clr-dg-row[contains(.,"${projectname}")]//clr-checkbox//label
Sleep 1
Click Element xpath=//clr-dg-action-overflow//button[contains(.,"Delete")]
Click Element xpath=//button[contains(.,"Delete")]
Sleep 1
Click Element xpath=//clr-modal//button[contains(.,"DELETE")]
Sleep 2
Click Element xpath=//clr-modal//button[2]
Sleep 1
Click Element xpath=//button[contains(.,"CLOSE")]
Delete Project
[Arguments] ${projectname}
Sleep 1
Click Element //list-project//clr-dg-row[contains(.,'${projectname}')]//clr-dg-action-overflow/button
Click Element //list-project//clr-dg-action-overflow//button[contains(.,'Delete')]
#Click Element //list-project//clr-dg-row-master[contains(.,'${projname}')]//clr-dg-action-overflow
#Click Element //list-project//clr-dg-row-master[contains(.,'${projname}')]//clr-dg-action-overflow//button[contains(.,'Delete')]
#click delete button to confirm
Click Element xpath=//clr-dg-row[contains(.,"${projectname}")]//clr-checkbox//label
Sleep 1
Click Element //confiramtion-dialog//button[contains(.,'DELETE')]
Click Element xpath=//button[contains(.,"Delete")]
Sleep 1
Click Element xpath=//clr-modal//button[2]
Sleep 1
Click Element xpath=//button[contains(.,"CLOSE")]
Project Should Not Be Deleted
[Arguments] ${projname}
@ -184,12 +201,15 @@ Go Into Repo
Expand Repo
[Arguments] ${projectname}
Click Element //repository//clr-dg-row[contains(.,'${projectname}')]//button/clr-icon
sleep 1
Sleep 1
Scan Repo
[Arguments] ${tagname}
Click Element //hbr-tag//clr-dg-row[contains(.,'${tagname}')]//clr-dg-action-overflow
Click Element //hbr-tag//clr-dg-action-overflow//button[contains(.,'Scan')]
#Click Element //hbr-tag//clr-dg-row-master[contains(.,'${tagname}')]//clr-dg-action-overflow
#Click Element //hbr-tag//clr-dg-row-master[contains(.,'${tagname}')]//clr-dg-action-overflow//button[contains(.,'Scan')]
#select one tag
Click Element //clr-dg-row[contains(.,"${tagname}")]//label
Click Element //button[contains(.,'Scan')]
Sleep 15
Edit Repo Info

View File

@ -18,7 +18,7 @@ Documentation This resource provides any keywords related to the Harbor private
*** Variables ***
${create_project_button_css} .btn
${project_name_xpath} //*[@id="create_project_name"]
${project_public_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project/div/div/div[2]/div[1]/create-project/clr-modal/div/div[1]/div/div[1]/div/div[2]/form/section/div[2]/div/label
${project_public_xpath} //input[@name='public']/..//label
${project_save_css} html body.no-scrolling harbor-app harbor-shell clr-main-container.main-container div.content-container div.content-area.content-area-override project div.row div.col-lg-12.col-md-12.col-sm-12.col-xs-12 div.row.flex-items-xs-between div.option-left create-project clr-modal div.modal div.modal-dialog div.modal-content div.modal-footer button.btn.btn-primary
${log_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/a[2]
${projects_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/a[1]

View File

@ -22,13 +22,14 @@ ${HARBOR_VERSION} v1.1.1
*** Keywords ***
Create An New Rule With New Endpoint
[Arguments] ${policy_name} ${policy_description} ${destination_name} ${destination_url} ${destination_username} ${destination_password}
Click element xpath=${new_name_xpath}
Click element ${new_name_xpath}
Sleep 2
Input Text xpath=${policy_name_xpath} ${policy_name}
Input Text xpath=${policy_description_xpath} ${policy_description}
Click element xpath=${policy_enable_checkbox}
#Click element xpath=${policy_enable_checkbox}
#enable attribute is droped in new ui
Click element xpath=${policy_endpoint_checkbox}
Input text xpath=${destination_name_xpath} ${destination_name}
@ -40,4 +41,4 @@ Create An New Rule With New Endpoint
Capture Page Screenshot rule_${policy_name}.png
Wait Until Page Contains ${policy_name}
Wait Until Page Contains ${policy_description}
Wait Until Page Contains ${destination_name}
Wait Until Page Contains ${destination_name}

View File

@ -16,13 +16,13 @@
Documentation This resource provides any keywords related to the Harbor private registry appliance
*** Variables ***
${new_name_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/replicaton/div/hbr-replication/div/div[1]/div/div[1]/button/clr-icon
${new_name_xpath} //hbr-list-replication-rule//button[contains(.,'New')]
${policy_name_xpath} //*[@id="policy_name"]
${policy_description_xpath} //*[@id="policy_description"]
${policy_enable_checkbox} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/replicaton/div/hbr-replication/div/div[1]/div/div[1]/create-edit-rule/clr-modal/div/div[1]/div/div[1]/div/div[2]/form/section/div[3]/div/label
${policy_endpoint_checkbox} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/replicaton/div/hbr-replication/div/div[1]/div/div[1]/create-edit-rule/clr-modal/div/div[1]/div/div[1]/div/div[2]/form/section/div[4]/div[2]/label
${policy_enable_checkbox} //input[@id='policy_enable']/../label
${policy_endpoint_checkbox} //input[@id='check_new']/../label
${destination_name_xpath} //*[@id='destination_name']
${destination_url_xpath} //*[@id='destination_url']
${destination_username_xpath} //*[@id='destination_username']
${destination_password_xpath} //*[@id='destination_password']
${replicaton_save_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/replicaton/div/hbr-replication/div/div[1]/div/div[1]/create-edit-rule/clr-modal/div/div[1]/div/div[1]/div/div[3]/button[3]
${replicaton_save_xpath} //button[contains(.,'OK')]

View File

@ -252,10 +252,11 @@ Test Case - Scan A Tag In The Repo
${d}= get current date result_format=%m%s
Create An New Project With New User url=${HARBOR_URL} username=tester${d} email=tester${d}@vmware.com realname=tester${d} newPassword=Test1@34 comment=harbor projectname=project${d} public=false
Push Image ${ip} tester${d} Test1@34 project${d} hello-world
Go Into Project project${d}
Go Into Repo project${d}/hello-world
Scan Repo latest
Summary Chart Should Display latest
Edit Repo Info
#Edit Repo Info
Close Browser
Test Case - Manage Project Member
@ -270,7 +271,6 @@ Test Case - Manage Project Member
Create An New User url=${HARBOR_URL} username=carol${d} email=carol${d}@vmware.com realname=carol${d} newPassword=Test1@34 comment=harbor
Logout Harbor
User Should Be Owner Of Project alice${d} Test1@34 project${d}
User Should Not Be A Member Of Project bob${d} Test1@34 project${d}
Manage Project Member alice${d} Test1@34 project${d} bob${d} Add
User Should Be Guest bob${d} Test1@34 project${d}