mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-23 00:57:44 +01:00
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:
commit
c5e434bd14
@ -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;
|
||||
}
|
||||
|
@ -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;}
|
||||
`;
|
@ -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> {{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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
`;
|
@ -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');
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
`;
|
@ -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);
|
||||
}
|
||||
|
@ -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>`;
|
@ -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>
|
||||
`;
|
@ -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('');
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -12,4 +12,8 @@
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
:host >>> .btn-group-overflow .dropdown-toggle {
|
||||
line-height: 24px;
|
||||
height: 24px;
|
||||
}
|
@ -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>
|
@ -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) {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -16,4 +16,7 @@
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
.rightPos {
|
||||
position: absolute; right: 20px; margin-top: 16px; height:32px;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;}
|
||||
|
@ -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> {{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>
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -38,4 +38,7 @@
|
||||
|
||||
.hide-create {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
.rightPos {
|
||||
position: absolute; right: 20px; margin-top: -7px; height:32px; z-index: 100;
|
||||
}
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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!",
|
||||
|
@ -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.",
|
||||
|
@ -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": "测试连接失败。",
|
||||
|
@ -47,3 +47,4 @@
|
||||
-o-transform: rotate(-90deg);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.datagrid-spinner{margin-top: 24px;}
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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}
|
||||
|
@ -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')]
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user