Merge pull request #4172 from pengpengshui/batchDelection
Modify replication list unsave when modify username or possword
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 31 KiB |
@ -102,12 +102,12 @@ You can add members with different roles to an existing project. You can add a L
|
||||
![browse project](img/new_add_member.png)
|
||||
|
||||
### Updating and removing members
|
||||
You can update or remove a member by clicking the icon on the left.
|
||||
You can check one or more members, then click `MEMBER ACTION`, choose one role to batch switch checked members's roles. You can also click `MEMBER.REMOVE` to batch remove checked members.
|
||||
|
||||
![browse project](img/new_remove_update_member.png)
|
||||
|
||||
## Replicating images
|
||||
Images replication is used to replicate repositories from one Harbor instance to another.
|
||||
Images replication is used to replicate repositories from one Harbor instance to another.
|
||||
|
||||
The function is project-oriented, and once the system administrator set a rule to one project, all repositories under the project that match the defined [filter](#replication-filter) patterns will be replicated to the remote registry when the [triggering condition](#replication-triggering-condition) is triggered. Each repository will start a job to run. If the project does not exist on the remote registry, a new project will be created automatically, but if it already exists and the user configured in policy has no write privilege to it, the process will fail. The member information will not be replicated.
|
||||
|
||||
@ -162,7 +162,7 @@ Entering a keyword in the search field at the top lists all matching projects an
|
||||
|
||||
## Administrator options
|
||||
### Managing user
|
||||
Administrator can add "Administrator" role to an ordinary user by click button on the left and select "Set as Administrator". To delete a user, select "Delete". Deleting user is only supported under database authentication mode.
|
||||
Administrator can add "Administrator" role to one or more ordinary users by checking checkboxes and clicking `SET AS ADMINISTRATOR`. To delete users, checked checkboxes and select `DELETE`. Deleting user is only supported under database authentication mode.
|
||||
|
||||
![browse project](img/new_set_admin_remove_user.png)
|
||||
|
||||
|
@ -170,7 +170,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
|
||||
|
||||
//Open the modal now
|
||||
this.open();
|
||||
this.forceRefreshView(1000);
|
||||
this.forceRefreshView(2000);
|
||||
})
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
} else {
|
||||
@ -208,12 +208,12 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
|
||||
.then(
|
||||
response => {
|
||||
this.inlineAlert.showInlineSuccess({ message: "DESTINATION.TEST_CONNECTION_SUCCESS" });
|
||||
this.forceRefreshView(1000);
|
||||
this.forceRefreshView(2000);
|
||||
this.testOngoing = false;
|
||||
}).catch(
|
||||
error => {
|
||||
this.inlineAlert.showInlineError('DESTINATION.TEST_CONNECTION_FAILURE');
|
||||
this.forceRefreshView(1000);
|
||||
this.forceRefreshView(2000);
|
||||
this.testOngoing = false;
|
||||
});
|
||||
}
|
||||
@ -240,6 +240,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
|
||||
this.reload.emit(true);
|
||||
this.onGoing = false;
|
||||
this.close();
|
||||
this.forceRefreshView(2000);
|
||||
}).catch(error => {
|
||||
this.onGoing = false;
|
||||
let errorMessageKey = this.handleErrorMessageKey(error.status);
|
||||
@ -248,10 +249,10 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
|
||||
.subscribe(res => {
|
||||
this.inlineAlert.showInlineError(res);
|
||||
});
|
||||
this.forceRefreshView(1000);
|
||||
this.forceRefreshView(2000);
|
||||
}
|
||||
);
|
||||
this.forceRefreshView(1000);
|
||||
|
||||
}
|
||||
|
||||
updateEndpoint() {
|
||||
@ -285,6 +286,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
|
||||
this.reload.emit(true);
|
||||
this.close();
|
||||
this.onGoing = false;
|
||||
this.forceRefreshView(2000);
|
||||
})
|
||||
.catch(
|
||||
error => {
|
||||
@ -295,10 +297,10 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
|
||||
this.inlineAlert.showInlineError(res);
|
||||
});
|
||||
this.onGoing = false;
|
||||
this.forceRefreshView(1000);
|
||||
this.forceRefreshView(2000);
|
||||
}
|
||||
);
|
||||
this.forceRefreshView(1000);
|
||||
|
||||
}
|
||||
|
||||
handleErrorMessageKey(status: number): string {
|
||||
|
@ -16,7 +16,7 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
|
||||
<clr-dg-row *clrDgItems="let p of changedRules" [clrDgItem]="p" (click)="selectRule(p)" [style.backgroundColor]="(projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''">
|
||||
<clr-dg-cell>{{p.name}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="!projectScope">
|
||||
<a href="javascript:void(0)">{{p.projects?.length>0 ? p.projects[0].name : ''}}</a>
|
||||
<a href="javascript:void(0)" (click)="redirectTo(p)">{{p.projects?.length>0 ? p.projects[0].name : ''}}</a>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{p.description ? p.description : '-'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{p.targets?.length>0 ? p.targets[0].name : ''}}</clr-dg-cell>
|
||||
|
@ -66,6 +66,7 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
||||
@Output() selectOne = new EventEmitter<ReplicationRule>();
|
||||
@Output() editOne = new EventEmitter<ReplicationRule>();
|
||||
@Output() toggleOne = new EventEmitter<ReplicationRule>();
|
||||
@Output() hideJobs = new EventEmitter<any>();
|
||||
@Output() redirect = new EventEmitter<ReplicationRule>();
|
||||
@Output() openNewRule = new EventEmitter<any>();
|
||||
@Output() replicateManual = new EventEmitter<ReplicationRule[]>();
|
||||
@ -132,6 +133,8 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
||||
if (this.rules && this.rules.length > 0) {
|
||||
this.selectedId = this.rules[0].id || '';
|
||||
this.selectOne.emit(this.rules[0]);
|
||||
} else {
|
||||
this.hideJobs.emit();
|
||||
}
|
||||
this.changedRules = this.rules;
|
||||
this.selectedRow = this.changedRules[0];
|
||||
|
@ -11,7 +11,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" [isSystemAdmin]="isSystemAdmin" (replicateManual)=replicateManualRule($event) (selectOne)="selectOneRule($event)" (openNewRule)="openModal()" (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" [isSystemAdmin]="isSystemAdmin" (replicateManual)=replicateManualRule($event) (selectOne)="selectOneRule($event)" (hideJobs)="hideJobs()" (openNewRule)="openModal()" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)"></hbr-list-replication-rule>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="padding-left:0px;">
|
||||
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
|
@ -352,6 +352,10 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
this.loadFirstPage();
|
||||
}
|
||||
|
||||
hideJobs() {
|
||||
this.search.ruleId = 0;
|
||||
}
|
||||
|
||||
stopJobs() {
|
||||
if (this.jobs && this.jobs.length) {
|
||||
this.isStopOnGoing = true;
|
||||
|
@ -39,7 +39,7 @@ export const TAG_TEMPLATE = `
|
||||
<clr-dg-column style="width: 150px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="width: 140px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *ngFor="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-cell class="truncated" style="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>
|
||||
|
@ -31,7 +31,7 @@
|
||||
"clarity-icons": "^0.10.17",
|
||||
"clarity-ui": "^0.10.17",
|
||||
"core-js": "^2.4.1",
|
||||
"harbor-ui": "0.6.34",
|
||||
"harbor-ui": "0.6.37",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
|
@ -31,7 +31,7 @@
|
||||
<clr-datagrid [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="SelectedChange()">
|
||||
<clr-dg-column>{{'MEMBER.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'MEMBER.ROLE' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *ngFor="let m of members" [clrDgItem]="m">
|
||||
<clr-dg-row *clrDgItems="let m of members" [clrDgItem]="m">
|
||||
<clr-dg-cell>{{m.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="ChangeRoleOngoing(m.username)" class="spinner spinner-inline"> Loading... </span>
|
||||
|
@ -25,6 +25,7 @@ const FAKE_PASSWORD = 'rjGcfuRu';
|
||||
styleUrls: ['replication-rule.css']
|
||||
|
||||
})
|
||||
|
||||
export class ReplicationRuleComponent implements OnInit, OnDestroy {
|
||||
_localTime: Date = new Date();
|
||||
policyId: number;
|
||||
@ -33,7 +34,7 @@ export class ReplicationRuleComponent implements OnInit, OnDestroy {
|
||||
isFilterHide: boolean = false;
|
||||
weeklySchedule: boolean;
|
||||
isScheduleOpt: boolean;
|
||||
isImmediate: boolean = true;
|
||||
isImmediate: boolean = false;
|
||||
noProjectInfo: string = "";
|
||||
noSelectedProject: boolean = true;
|
||||
noSelectedEndpoint: boolean = true;
|
||||
@ -59,6 +60,8 @@ export class ReplicationRuleComponent implements OnInit, OnDestroy {
|
||||
ruleForm: FormGroup;
|
||||
copyUpdateForm: ReplicationRule;
|
||||
|
||||
emptyEndpoint = new Target();
|
||||
|
||||
@ViewChild(ListProjectModelComponent)
|
||||
projectListModel: ListProjectModelComponent;
|
||||
|
||||
@ -91,7 +94,6 @@ export class ReplicationRuleComponent implements OnInit, OnDestroy {
|
||||
private confirmService: ConfirmationDialogService,
|
||||
public ref: ChangeDetectorRef) {
|
||||
this.createForm();
|
||||
|
||||
Promise.all([this.repService.getEndpoints(), this.repService.listProjects()])
|
||||
.then(res => {
|
||||
if (!res[0]) {
|
||||
@ -99,6 +101,7 @@ export class ReplicationRuleComponent implements OnInit, OnDestroy {
|
||||
}else {
|
||||
this.targetList = res[0];
|
||||
if (!this.policyId) {
|
||||
res[0].unshift(this.emptyEndpoint);
|
||||
this.setTarget([res[0][0]]);
|
||||
this.realEndpointData.userName = res[0][0].username;
|
||||
this.realEndpointData.password = FAKE_PASSWORD;
|
||||
@ -270,6 +273,10 @@ export class ReplicationRuleComponent implements OnInit, OnDestroy {
|
||||
|
||||
targetChange($event: any) {
|
||||
if ($event && $event.target && event.target['value']) {
|
||||
if ($event.target['value'] === '-1') {
|
||||
this.noSelectedEndpoint = true;
|
||||
return;
|
||||
}
|
||||
let selecedTarget: Target = this.targetList.find(target => target.id === +$event.target['value']);
|
||||
this.setTarget([selecedTarget]);
|
||||
this.noSelectedEndpoint = false;
|
||||
@ -437,10 +444,9 @@ export class ReplicationRuleComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public hasFormChange(): boolean {
|
||||
return !isEmptyObject(this.getChanges());
|
||||
return !isEmptyObject(this.getChanges()) || !compareValue(this.firstEndpointData, this.realEndpointData);
|
||||
}
|
||||
|
||||
|
||||
onSubmit() {
|
||||
this.inProgress = true;
|
||||
let endpointId: string | number = this.ruleForm.value.targets[0].id;
|
||||
|
@ -37,8 +37,7 @@ label:first-child {
|
||||
|
||||
.projectInput{float: left;}
|
||||
.projectInput input{background-color: white;}
|
||||
.switchIcon{width:20px;height:20px; margin-top: 16px;margin-left: 10px;}
|
||||
.switchIcon{width:20px;height:20px; margin-top: 10px;margin-left: 10px; cursor: pointer;}
|
||||
.addEndpoint{ margin-top: .25em !important;}
|
||||
.shadow{position: absolute;top: 8px;}
|
||||
.shadow1{width:270px; height: 24px;background-color: #fafafa; z-index: 10; top:5px;}
|
||||
.hoverBg:hover{display: none;}
|
||||
.is-solid{cursor: pointer;}
|
@ -57,15 +57,14 @@
|
||||
<label class="col-md-4 form-group-label-override">{{'DESTINATION.ENDPOINT' | translate}} <span class="colorRed">*</span></label>
|
||||
<div formArrayName="targets">
|
||||
<div class="select endpointSelect pull-left" *ngFor="let target of targets.controls; let i= index" [formGroupName]="i">
|
||||
<select id="ruleTarget " class="inputWidth" (mouseenter)="noSelectedEndpoint= false" (change)="targetChange($event)" formControlName="id">
|
||||
<option *ngFor="let target of targetList" value="{{target.id}}">{{target.name}}: {{target.endpoint}}</option>
|
||||
<select id="ruleTarget " class="inputWidth" (change)="targetChange($event)" formControlName="id">
|
||||
<option *ngFor="let target of targetList" value="{{target.id}}">{{target.name}}<span [hidden]="!target.name">:</span> {{target.endpoint}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-info btn-sm addEndpoint" (click)="openModal()"><clr-icon shape="plus"></clr-icon> {{'REPLICATION.NEW' | translate}}</button>
|
||||
<div [hidden]="noSelectedEndpoint">userName: <input type="text" [(ngModel)]="realEndpointData.userName" [ngModelOptions]="{standalone:true}"></div>
|
||||
<div [hidden]="noSelectedEndpoint">password: <input type="password" [(ngModel)]="realEndpointData.password" [ngModelOptions]="{standalone:true}"></div>
|
||||
</div>
|
||||
<div class="shadow shadow1 hoverBg" #shadowDiv [hidden]="!noSelectedEndpoint || !targetList.length"></div>
|
||||
</div>
|
||||
|
||||
<!--Trigger-->
|
||||
|