diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts index 96c525738..56d1e137b 100644 --- a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts @@ -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 { + let ruleData: ReplicationJobItem[]; + this.canDeleteRule = true; + let count: number = 0; + return toPromise(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); + }); } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/replication/replication.component.ts b/src/ui_ng/lib/src/replication/replication.component.ts index 168626672..9e6ce673f 100644 --- a/src/ui_ng/lib/src/replication/replication.component.ts +++ b/src/ui_ng/lib/src/replication/replication.component.ts @@ -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(this.jobs, state); this.jobs = doSorting(this.jobs, state); diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts index 8d16be171..c54e9ad0e 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts @@ -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", diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index efe104cc4..a1de71e8a 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -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; diff --git a/src/ui_ng/lib/src/service/tag.service.spec.ts b/src/ui_ng/lib/src/service/tag.service.spec.ts index 9ccd122e5..3cdbb4cd7 100644 --- a/src/ui_ng/lib/src/service/tag.service.spec.ts +++ b/src/ui_ng/lib/src/service/tag.service.spec.ts @@ -14,6 +14,7 @@ describe('TagService', () => { { "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55", "name": "1.11.5", + "size": "2049", "architecture": "amd64", "os": "linux", "docker_version": "1.12.3", diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts index 2073c2164..5c5328fdc 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts @@ -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", diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.ts b/src/ui_ng/lib/src/tag/tag-detail.component.ts index cfcf3281a..cce0e8c62 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.ts @@ -24,6 +24,7 @@ export class TagDetailComponent implements OnInit { @Input() repositoryId: string; tagDetails: Tag = { name: "--", + size: "--", author: "--", created: new Date(), architecture: "--", diff --git a/src/ui_ng/lib/src/tag/tag.component.css.ts b/src/ui_ng/lib/src/tag/tag.component.css.ts index 80335b50b..e969fbe86 100644 --- a/src/ui_ng/lib/src/tag/tag.component.css.ts +++ b/src/ui_ng/lib/src/tag/tag.component.css.ts @@ -43,4 +43,9 @@ export const TAG_STYLE = ` color: red; margin-right: 6px; } + +:host >>> .datagrid clr-dg-column { + min-width: 80px; +} + `; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag.component.html.ts b/src/ui_ng/lib/src/tag/tag.component.html.ts index c5481cbda..6b8e07eb6 100644 --- a/src/ui_ng/lib/src/tag/tag.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag.component.html.ts @@ -16,14 +16,13 @@ export const TAG_TEMPLATE = `

{{repoName}}

{{'REPOSITORY.TAG' | translate}} + {{'REPOSITORY.SIZE' | translate}} {{'REPOSITORY.PULL_COMMAND' | translate}} {{'VULNERABILITY.SINGULAR' | translate}} {{'REPOSITORY.SIGNED' | translate}} {{'REPOSITORY.AUTHOR' | translate}} {{'REPOSITORY.CREATED' | translate}} {{'REPOSITORY.DOCKER_VERSION' | translate}} - {{'REPOSITORY.ARCHITECTURE' | translate}} - {{'REPOSITORY.OS' | translate}} {{'TGA.PLACEHOLDER' | translate }} @@ -35,6 +34,7 @@ export const TAG_TEMPLATE = ` {{t.name}} {{t.name}} + {{t.size}} docker pull {{registryUrl}}/{{repoName}}:{{t.name}} @@ -50,8 +50,6 @@ export const TAG_TEMPLATE = ` {{t.author}} {{t.created | date: 'short'}} {{t.docker_version}} - {{t.architecture}} - {{t.os}} {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} diff --git a/src/ui_ng/lib/src/tag/tag.component.spec.ts b/src/ui_ng/lib/src/tag/tag.component.spec.ts index 2f14b4824..2dfb306c1 100644 --- a/src/ui_ng/lib/src/tag/tag.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag.component.spec.ts @@ -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", diff --git a/src/ui_ng/lib/src/tag/tag.component.ts b/src/ui_ng/lib/src/tag/tag.component.ts index a056e6759..663c31e2a 100644 --- a/src/ui_ng/lib/src/tag/tag.component.ts +++ b/src/ui_ng/lib/src/tag/tag.component.ts @@ -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; diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index b9b228e80..7a921074c 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -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", diff --git a/src/ui_ng/src/app/project/member/add-member/add-member.component.css b/src/ui_ng/src/app/project/member/add-member/add-member.component.css index 0bcac82e9..8bf7f2952 100644 --- a/src/ui_ng/src/app/project/member/add-member/add-member.component.css +++ b/src/ui_ng/src/app/project/member/add-member/add-member.component.css @@ -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; } \ No newline at end of file diff --git a/src/ui_ng/src/app/project/member/add-member/add-member.component.html b/src/ui_ng/src/app/project/member/add-member/add-member.component.html index 7bc567e28..c754a366b 100644 --- a/src/ui_ng/src/app/project/member/add-member/add-member.component.html +++ b/src/ui_ng/src/app/project/member/add-member/add-member.component.html @@ -6,16 +6,22 @@
-
diff --git a/src/ui_ng/src/app/project/member/add-member/add-member.component.ts b/src/ui_ng/src/app/project/member/add-member/add-member.component.ts index e62b6883f..25c007a8d 100644 --- a/src/ui_ng/src/app/project/member/add-member/add-member.component.ts +++ b/src/ui_ng/src/app/project/member/add-member/add-member.component.ts @@ -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 = new Subject(); 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 { diff --git a/src/ui_ng/src/i18n/lang/en-us-lang.json b/src/ui_ng/src/i18n/lang/en-us-lang.json index 0968db2bb..dfecf5a1a 100644 --- a/src/ui_ng/src/i18n/lang/en-us-lang.json +++ b/src/ui_ng/src/i18n/lang/en-us-lang.json @@ -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", diff --git a/src/ui_ng/src/i18n/lang/es-es-lang.json b/src/ui_ng/src/i18n/lang/es-es-lang.json index d78a2d73c..50e93e292 100644 --- a/src/ui_ng/src/i18n/lang/es-es-lang.json +++ b/src/ui_ng/src/i18n/lang/es-es-lang.json @@ -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", diff --git a/src/ui_ng/src/i18n/lang/zh-cn-lang.json b/src/ui_ng/src/i18n/lang/zh-cn-lang.json index 00bb9f2f6..c03a41050 100644 --- a/src/ui_ng/src/i18n/lang/zh-cn-lang.json +++ b/src/ui_ng/src/i18n/lang/zh-cn-lang.json @@ -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": "创建时间",