fix issue about replication rule delete and username autocomplete and… …

This commit is contained in:
Fuhui Peng (c) 2017-09-21 14:50:15 +08:00
parent be0b31e5ba
commit 148c6ecc53
18 changed files with 176 additions and 20 deletions

View File

@ -26,7 +26,7 @@ import {
} from '@angular/core';
import { ReplicationService } from '../service/replication.service';
import { ReplicationRule } from '../service/interface';
import {ReplicationJob, ReplicationJobItem, ReplicationRule} from '../service/interface';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
@ -70,6 +70,7 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
rules: ReplicationRule[];
changedRules: ReplicationRule[];
ruleName: string;
canDeleteRule: boolean;
@ViewChild('toggleConfirmDialog')
toggleConfirmDialog: ConfirmationDialogComponent;
@ -199,15 +200,48 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
this.toggleConfirmDialog.open(toggleConfirmMessage);
}
jobList(): Promise<void> {
let ruleData: ReplicationJobItem[];
this.canDeleteRule = true;
let count: number = 0;
return toPromise<ReplicationJob>(this.replicationService
.getJobs(this.selectedId))
.then(response => {
ruleData = response.data;
if (ruleData.length) {
ruleData.forEach(job => {
if ((job.status === 'pending') || (job.status === 'running') || (job.status === 'retrying')) {
count ++;
}
});
}
this.canDeleteRule = count > 0 ? false : true;
})
.catch(error => this.errorHandler.error(error));
}
deleteRule(rule: ReplicationRule) {
let deletionMessage: ConfirmationMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE',
'REPLICATION.DELETION_SUMMARY',
rule.name || '',
rule.id,
ConfirmationTargets.POLICY,
ConfirmationButtons.DELETE_CANCEL);
this.deletionConfirmDialog.open(deletionMessage);
this.jobList().then(() => {
let deletionMessage: ConfirmationMessage;
if (!this.canDeleteRule) {
deletionMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE_FAILURE',
'REPLICATION.DELETION_SUMMARY_FAILURE',
rule.name || '',
rule.id,
ConfirmationTargets.POLICY,
ConfirmationButtons.CLOSE);
} else {
deletionMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE',
'REPLICATION.DELETION_SUMMARY',
rule.name || '',
rule.id,
ConfirmationTargets.POLICY,
ConfirmationButtons.DELETE_CANCEL);
}
this.deletionConfirmDialog.open(deletionMessage);
});
}
}

View File

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, ViewChild, Input, Output, OnDestroy, EventEmitter } from '@angular/core';
import { ResponseOptions, RequestOptions } from '@angular/http';
import { NgModel } from '@angular/forms';
@ -41,6 +41,8 @@ import { REPLICATION_STYLE } from './replication.component.css';
import { JobLogViewerComponent } from '../job-log-viewer/index';
import { State } from "clarity-angular";
import {Observable} from "rxjs/Observable";
import {Subscription} from "rxjs/Subscription";
const ruleStatus: { [key: string]: any } = [
{ 'key': 'all', 'description': 'REPLICATION.ALL_STATUS' },
@ -79,7 +81,7 @@ export class SearchOption {
template: REPLICATION_TEMPLATE,
styles: [REPLICATION_STYLE]
})
export class ReplicationComponent implements OnInit {
export class ReplicationComponent implements OnInit, OnDestroy {
@Input() projectId: number | string;
@Input() withReplicationJob: boolean;
@ -124,6 +126,7 @@ export class ReplicationComponent implements OnInit {
pageSize: number = DEFAULT_PAGE_SIZE;
currentState: State;
jobsLoading: boolean = false;
timerDelay: Subscription;
constructor(
private errorHandler: ErrorHandler,
@ -145,6 +148,12 @@ export class ReplicationComponent implements OnInit {
this.currentJobSearchOption = 0;
}
ngOnDestroy() {
if (this.timerDelay) {
this.timerDelay.unsubscribe();
}
}
openModal(): void {
this.createEditPolicyComponent.openCreateEditRule(true);
}
@ -197,6 +206,23 @@ export class ReplicationComponent implements OnInit {
this.totalCount = response.metadata.xTotalCount;
this.jobs = response.data;
if (!this.timerDelay) {
this.timerDelay = Observable.timer(10000, 10000).subscribe(() => {
let count: number = 0;
this.jobs.forEach((job) => {
if ((job.status === 'pending') || (job.status === 'running') || (job.status === 'retrying')) {
count ++;
}
});
if (count > 0) {
this.clrLoadJobs(this.currentState);
}else {
this.timerDelay.unsubscribe();
this.timerDelay = null;
}
});
}
//Do filtering and sorting
this.jobs = doFiltering<ReplicationJobItem>(this.jobs, state);
this.jobs = doSorting<ReplicationJobItem>(this.jobs, state);

View File

@ -75,6 +75,7 @@ describe('RepositoryComponentStackview (inline template)', () => {
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "1.11.5",
"size": "2049",
"architecture": "amd64",
"os": "linux",
"docker_version": "1.12.3",

View File

@ -51,6 +51,7 @@ export interface Repository {
export interface Tag extends Base {
digest: string;
name: string;
size: string;
architecture: string;
os: string;
docker_version: string;

View File

@ -14,6 +14,7 @@ describe('TagService', () => {
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "1.11.5",
"size": "2049",
"architecture": "amd64",
"os": "linux",
"docker_version": "1.12.3",

View File

@ -43,6 +43,7 @@ describe('TagDetailComponent (inline template)', () => {
let mockTag: Tag = {
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "nginx",
"size": "2049",
"architecture": "amd64",
"os": "linux",
"docker_version": "1.12.3",

View File

@ -24,6 +24,7 @@ export class TagDetailComponent implements OnInit {
@Input() repositoryId: string;
tagDetails: Tag = {
name: "--",
size: "--",
author: "--",
created: new Date(),
architecture: "--",

View File

@ -43,4 +43,9 @@ export const TAG_STYLE = `
color: red;
margin-right: 6px;
}
:host >>> .datagrid clr-dg-column {
min-width: 80px;
}
`;

View File

@ -16,14 +16,13 @@ export const TAG_TEMPLATE = `
<h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2>
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
<clr-dg-column style="width: 80px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 180px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column style="width: 160px;" *ngIf="withClair">{{'VULNERABILITY.SINGULAR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 100px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'architecture'" *ngIf="!withClair">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'os'" *ngIf="!withClair">{{'REPOSITORY.OS' | 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>
@ -35,6 +34,7 @@ export const TAG_TEMPLATE = `
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a>
<span *ngSwitchDefault>{{t.name}}</span>
</clr-dg-cell>
<clr-dg-cell style="width: 80px;">{{t.size}}</clr-dg-cell>
<clr-dg-cell style="min-width: 180px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
<clr-dg-cell style="width: 160px;" *ngIf="withClair">
<hbr-vulnerability-bar [repoName]="repoName" [tagId]="t.name" [summary]="t.scan_overview"></hbr-vulnerability-bar>
@ -50,8 +50,6 @@ export const TAG_TEMPLATE = `
<clr-dg-cell style="width: 100px;">{{t.author}}</clr-dg-cell>
<clr-dg-cell style="width: 160px;">{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.architecture}}</clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.os}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>

View File

@ -29,6 +29,7 @@ describe('TagComponent (inline template)', () => {
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "1.11.5",
"size": "2049",
"architecture": "amd64",
"os": "linux",
"docker_version": "1.12.3",

View File

@ -159,6 +159,9 @@ export class TagComponent implements OnInit {
if (t.signature !== null) {
signatures.push(t.name);
}
//size
t.size = this.sizeTransform(t.size);
});
this.tags = items;
let signedName: {[key: string]: string[]} = {};
@ -177,6 +180,19 @@ export class TagComponent implements OnInit {
setTimeout(() => clearInterval(hnd), 5000);
}
sizeTransform(tagSize: string): string {
let size: number = Number.parseInt(tagSize);
if (Math.pow(1024, 1) <= size && size < Math.pow(1024, 2)) {
return (size / Math.pow(1024, 1)).toFixed(2) + 'KB';
} else if (Math.pow(1024, 2) <= size && size < Math.pow(1024, 3)) {
return (size / Math.pow(1024, 2)).toFixed(2) + 'MB';
} else if (Math.pow(1024, 3) <= size && size < Math.pow(1024, 4)) {
return (size / Math.pow(1024, 3)).toFixed(2) + 'MB';
} else {
return size + 'B';
}
}
deleteTag(tag: Tag) {
if (tag) {
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;

View File

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

View File

@ -1,4 +1,26 @@
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}
.selectBox{
position: absolute;
width: 100%;
height: auto;
border: 1px solid #ccc;
background-color: white;
border: 1px solid rgba(0,0,0,.15);
border-right-width: 2px;
border-bottom-width: 2px;
border-radius: 6px;
box-shadow: 0 5px 10px rgba(0,0,0,.2);
z-index: 100;
}
.selectBox ul li{
list-style: none;
padding: 3px 20px
}
.selectBox ul li:hover{
color: #262626;
background-image: linear-gradient(180deg,#f5f5f5 0,#e8e8e8);
background-repeat: repeat-x;
}

View File

@ -6,16 +6,22 @@
<section class="form-block">
<div class="form-group">
<label for="member_name" class="col-md-4 form-group-label-override required">{{'MEMBER.NAME' | translate}}</label>
<label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="!isMemberNameValid" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left">
<input type="text" id="member_name" [(ngModel)]="member.username"
name="member_name"
<label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="!isMemberNameValid"
class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" (mouseleave)="leaveInput()">
<input type="text" id="member_name" [(ngModel)]="member.username"
name="member_name"
size="20"
#memberName="ngModel"
required
(keyup)='handleValidation()'>
(keyup)='handleValidation()' autocomplete="off">
<span class="tooltip-content">
{{ memberTooltip | translate }}
</span>
<div class="selectBox" [style.display]="selectUserName.length ? 'block' : 'none'" >
<ul>
<li *ngFor="let name of selectUserName" (click)="selectedName(name)">{{name}}</li>
</ul>
</div>
</label>
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
</div>

View File

@ -25,6 +25,7 @@ import { Response } from '@angular/http';
import { NgForm } from '@angular/forms';
import { MemberService } from '../member.service';
import { UserService } from '../../../user/user.service';
import { MessageHandlerService } from '../../../shared/message-handler/message-handler.service';
import { InlineAlertComponent } from '../../../shared/inline-alert/inline-alert.component';
@ -36,11 +37,13 @@ import { Member } from '../member';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import {User} from "../../../user/user";
@Component({
selector: 'add-member',
templateUrl: 'add-member.component.html',
styleUrls: ['add-member.component.css'],
providers: [UserService],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
@ -69,13 +72,21 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
memberTooltip: string = 'MEMBER.USERNAME_IS_REQUIRED';
nameChecker: Subject<string> = new Subject<string>();
checkOnGoing: boolean = false;
selectUserName: string[] = [];
userLists: User[];
constructor(private memberService: MemberService,
private userService: UserService,
private messageHandlerService: MessageHandlerService,
private translateService: TranslateService,
private ref: ChangeDetectorRef) { }
ngOnInit(): void {
this.userService.getUsers()
.then(users => {
this.userLists = users;
});
this.nameChecker
.debounceTime(500)
.distinctUntilChanged()
@ -97,6 +108,20 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
.catch(error => {
this.checkOnGoing = false;
});
//username autocomplete
if (this.userLists.length) {
this.selectUserName = [];
this.userLists.filter(data => {
if (data.username.startsWith(cont.value)) {
if (this.selectUserName.length < 10) {
this.selectUserName.push(data.username);
}
}
});
setTimeout(() => {
setInterval(() => this.ref.markForCheck(), 100);
}, 1000);
}
} else {
this.memberTooltip = 'MEMBER.USERNAME_IS_REQUIRED';
}
@ -148,6 +173,11 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
}, 1000);
}
selectedName(username: string) {
this.member.username = username;
this.selectUserName = [];
}
onCancel() {
if (this.hasChanged) {
this.inlineAlert.showInlineConfirmation({ message: 'ALERT.FORM_CHANGE_CONFIRMATION' });
@ -157,6 +187,9 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
}
}
leaveInput() {
this.selectUserName = [];
}
ngAfterViewChecked(): void {
if (this.memberForm !== this.currentForm) {
this.memberForm = this.currentForm;
@ -189,6 +222,7 @@ export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy {
this.member.username = '';
this.isMemberNameValid = true;
this.memberTooltip = 'MEMBER.USERNAME_IS_REQUIRED';
this.selectUserName = [];
}
handleValidation(): void {

View File

@ -215,6 +215,8 @@
"FILTER_JOBS_PLACEHOLDER": "Filter Jobs",
"DELETION_TITLE": "Confirm Rule Deletion",
"DELETION_SUMMARY": "Do you want to delete rule {{param}}?",
"DELETION_TITLE_FAILURE": "failed to delete Rule Deletion",
"DELETION_SUMMARY_FAILURE": "{{param}} have pending/running/retrying status",
"FILTER_TARGETS_PLACEHOLDER": "Filter Endpoints",
"DELETION_TITLE_TARGET": "Confirm Endpoint Deletion",
"DELETION_SUMMARY_TARGET": "Do you want to delete the endpoint {{param}}?",
@ -330,6 +332,7 @@
"DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted.\nDelete from Notary via this command:\n{{param}}",
"FILTER_FOR_REPOSITORIES": "Filter Repositories",
"TAG": "Tag",
"SIZE": "Size",
"SIGNED": "Signed",
"AUTHOR": "Author",
"CREATED": "Creation Time",

View File

@ -215,6 +215,8 @@
"FILTER_JOBS_PLACEHOLDER": "Filtrar Trabajos",
"DELETION_TITLE": "Confirmar Eliminación de Regla",
"DELETION_SUMMARY": "¿Quiere eliminar la regla {{param}}?",
"DELETION_TITLE_FAILURE": "failed to delete Rule Deletion",
"DELETION_SUMMARY_FAILURE": "{{param}} have pending/running/retrying status",
"FILTER_TARGETS_PLACEHOLDER": "Filtrar Endpoints",
"DELETION_TITLE_TARGET": "Confirmar Eliminación de Endpoint",
"DELETION_SUMMARY_TARGET": "¿Quiere eliminar el endpoint {{param}}?",
@ -331,6 +333,7 @@
"DELETION_SUMMARY_TAG_DENIED": "La etiqueta debe ser eliminada de la Notaría antes de eliminarla.\nEliminarla de la Notaría con este comando:\n{{param}}",
"FILTER_FOR_REPOSITORIES": "Filtrar Repositorios",
"TAG": "Etiqueta",
"SIZE": "Size",
"SIGNED": "Firmada",
"AUTHOR": "Autor",
"CREATED": "Fecha de creación",

View File

@ -215,6 +215,8 @@
"FILTER_JOBS_PLACEHOLDER": "过滤任务",
"DELETION_TITLE": "删除规则确认",
"DELETION_SUMMARY": "确认删除规则 {{param}}?",
"DELETION_TITLE_FAILURE": "规则确认删除失败",
"DELETION_SUMMARY_FAILURE": "{{param}} 有 pending/running/retrying 状态,不能删除",
"FILTER_TARGETS_PLACEHOLDER": "过滤目标",
"DELETION_TITLE_TARGET": "删除目标确认",
"DELETION_SUMMARY_TARGET": "确认删除目标 {{param}}?",
@ -330,6 +332,7 @@
"DELETION_SUMMARY_TAG_DENIED": "要删除此镜像标签必须首先从Notary中删除。\n请执行如下Notary命令删除:\n{{param}}",
"FILTER_FOR_REPOSITORIES": "过滤镜像仓库",
"TAG": "标签",
"SIZE": "大小",
"SIGNED": "已签名",
"AUTHOR": "作者",
"CREATED": "创建时间",