Update components for pagination and sorting.

This commit is contained in:
kunw 2017-05-25 19:14:35 +08:00
parent 565110d9f1
commit e1df278ab5
22 changed files with 233 additions and 292 deletions

View File

@ -203,6 +203,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
let payload: Endpoint = this.initEndpoint; let payload: Endpoint = this.initEndpoint;
if(this.targetNameHasChanged) { if(this.targetNameHasChanged) {
payload.name = this.target.name; payload.name = this.target.name;
delete payload.endpoint;
} }
if (this.endpointHasChanged) { if (this.endpointHasChanged) {
payload.endpoint = this.target.endpoint; payload.endpoint = this.target.endpoint;

View File

@ -16,10 +16,10 @@ export const ENDPOINT_TEMPLATE: string = `
</div> </div>
</div> </div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-column>{{'DESTINATION.NAME' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'name'">{{'DESTINATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.URL' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'endpoint'">{{'DESTINATION.URL' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let t of targets" [clrDgItem]='t'> <clr-dg-row *clrDgItems="let t of targets" [clrDgItem]='t'>
<clr-dg-action-overflow> <clr-dg-action-overflow>
<button class="action-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</button> <button class="action-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</button>
@ -29,7 +29,11 @@ export const ENDPOINT_TEMPLATE: string = `
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell> <clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
<clr-dg-cell>{{t.creation_time | date: 'short'}}</clr-dg-cell> <clr-dg-cell>{{t.creation_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer>{{ (targets ? targets.length : 0) }} {{'DESTINATION.ITEMS' | translate}}</clr-dg-footer> <clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'DESTINATION.OF' | translate}}
{{pagination.totalItems}} {{'DESTINATION.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid> </clr-datagrid>
</div> </div>
</div> </div>

View File

@ -116,8 +116,9 @@ describe('EndpointComponent (inline template)', () => {
it('should open create endpoint modal', async(() => { it('should open create endpoint modal', async(() => {
fixture.detectChanges(); fixture.detectChanges();
comp.editTarget(mockOne); fixture.whenStable().then(()=>{
fixture.whenStable().then(()=>{ fixture.detectChanges();
comp.editTarget(mockOne);
fixture.detectChanges(); fixture.detectChanges();
expect(comp.target.name).toEqual('target_01'); expect(comp.target.name).toEqual('target_01');
}); });
@ -125,7 +126,6 @@ describe('EndpointComponent (inline template)', () => {
it('should filter endpoints by keyword', async(() => { it('should filter endpoints by keyword', async(() => {
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(()=>{ fixture.whenStable().then(()=>{
fixture.detectChanges(); fixture.detectChanges();
comp.doSearchTargets('target_02'); comp.doSearchTargets('target_02');

View File

@ -32,7 +32,9 @@ import { CreateEditEndpointComponent } from '../create-edit-endpoint/create-edit
import { ENDPOINT_STYLE } from './endpoint.component.css'; import { ENDPOINT_STYLE } from './endpoint.component.css';
import { ENDPOINT_TEMPLATE } from './endpoint.component.html'; import { ENDPOINT_TEMPLATE } from './endpoint.component.html';
import { toPromise } from '../utils'; import { toPromise, CustomComparator } from '../utils';
import { State, Comparator } from 'clarity-angular';
@Component({ @Component({
selector: 'hbr-endpoint', selector: 'hbr-endpoint',
@ -55,6 +57,10 @@ export class EndpointComponent implements OnInit {
targetName: string; targetName: string;
subscription: Subscription; subscription: Subscription;
loading: boolean = false;
creationTimeComparator: Comparator<Endpoint> = new CustomComparator<Endpoint>('creation_time', 'date');
get initEndpoint(): Endpoint { get initEndpoint(): Endpoint {
return { return {
endpoint: "", endpoint: "",
@ -101,7 +107,7 @@ export class EndpointComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.targetName = ''; this.targetName = '';
this.retrieve(''); this.retrieve();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -110,29 +116,34 @@ export class EndpointComponent implements OnInit {
} }
} }
retrieve(targetName: string): void { retrieve(): void {
this.loading = true;
toPromise<Endpoint[]>(this.endpointService toPromise<Endpoint[]>(this.endpointService
.getEndpoints(targetName)) .getEndpoints(this.targetName))
.then( .then(
targets => { targets => {
this.targets = targets || []; this.targets = targets || [];
let hnd = setInterval(()=>this.ref.markForCheck(), 100); let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000); setTimeout(()=>clearInterval(hnd), 1000);
}).catch(error => this.errorHandler.error(error)); this.loading = false;
}).catch(error => {
this.errorHandler.error(error);
this.loading = false;
});
} }
doSearchTargets(targetName: string) { doSearchTargets(targetName: string) {
this.targetName = targetName; this.targetName = targetName;
this.retrieve(targetName); this.retrieve();
} }
refreshTargets() { refreshTargets() {
this.retrieve(''); this.retrieve();
} }
reload($event: any) { reload($event: any) {
this.targetName = ''; this.targetName = '';
this.retrieve(''); this.retrieve();
} }
openModal() { openModal() {

View File

@ -290,6 +290,7 @@ export const EN_US_LANG: any = {
"INVALID_NAME": "Invalid endpoint name.", "INVALID_NAME": "Invalid endpoint name.",
"FAILED_TO_GET_TARGET": "Failed to get endpoint.", "FAILED_TO_GET_TARGET": "Failed to get endpoint.",
"CREATION_TIME": "Creation Time", "CREATION_TIME": "Creation Time",
"OF": "of",
"ITEMS": "item(s)", "ITEMS": "item(s)",
"CREATED_SUCCESS": "Created endpoint successfully.", "CREATED_SUCCESS": "Created endpoint successfully.",
"UPDATED_SUCCESS": "Updated endpoint successfully.", "UPDATED_SUCCESS": "Updated endpoint successfully.",
@ -299,8 +300,7 @@ export const EN_US_LANG: any = {
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use." "FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use."
}, },
"REPOSITORY": { "REPOSITORY": {
"COPY_ID": "Copy ID", "COPY_DIGEST_ID": "Copy Digest ID",
"COPY_PARENT_ID": "Copy Parent ID",
"DELETE": "Delete", "DELETE": "Delete",
"NAME": "Name", "NAME": "Name",
"TAGS_COUNT": "Tags", "TAGS_COUNT": "Tags",
@ -324,6 +324,7 @@ export const EN_US_LANG: any = {
"OS": "OS", "OS": "OS",
"SHOW_DETAILS": "Show Details", "SHOW_DETAILS": "Show Details",
"REPOSITORIES": "Repositories", "REPOSITORIES": "Repositories",
"OF": "of",
"ITEMS": "item(s)", "ITEMS": "item(s)",
"POP_REPOS": "Popular Repositories", "POP_REPOS": "Popular Repositories",
"DELETED_REPO_SUCCESS": "Deleted repository successfully.", "DELETED_REPO_SUCCESS": "Deleted repository successfully.",

View File

@ -290,6 +290,7 @@ export const ZH_CN_LANG: any = {
"INVALID_NAME": "无效的目标名称。", "INVALID_NAME": "无效的目标名称。",
"FAILED_TO_GET_TARGET": "获取目标失败。", "FAILED_TO_GET_TARGET": "获取目标失败。",
"CREATION_TIME": "创建时间", "CREATION_TIME": "创建时间",
"OF": "共计",
"ITEMS": "条记录", "ITEMS": "条记录",
"CREATED_SUCCESS": "成功创建目标。", "CREATED_SUCCESS": "成功创建目标。",
"UPDATED_SUCCESS": "成功更新目标。", "UPDATED_SUCCESS": "成功更新目标。",
@ -299,8 +300,7 @@ export const ZH_CN_LANG: any = {
"FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。" "FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。"
}, },
"REPOSITORY": { "REPOSITORY": {
"COPY_ID": "复制ID", "COPY_DIGEST_ID": "复制摘要ID",
"COPY_PARENT_ID": "复制父级ID",
"DELETE": "删除", "DELETE": "删除",
"NAME": "名称", "NAME": "名称",
"TAGS_COUNT": "标签数", "TAGS_COUNT": "标签数",
@ -324,6 +324,7 @@ export const ZH_CN_LANG: any = {
"OS": "操作系统", "OS": "操作系统",
"SHOW_DETAILS": "显示详细", "SHOW_DETAILS": "显示详细",
"REPOSITORIES": "镜像仓库", "REPOSITORIES": "镜像仓库",
"OF": "共计",
"ITEMS": "条记录", "ITEMS": "条记录",
"POP_REPOS": "受欢迎的镜像仓库", "POP_REPOS": "受欢迎的镜像仓库",
"DELETED_REPO_SUCCESS": "成功删除镜像仓库。", "DELETED_REPO_SUCCESS": "成功删除镜像仓库。",

View File

@ -1,13 +1,13 @@
export const LIST_REPLICATION_RULE_TEMPLATE: string = ` export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<confirmation-dialog #toggleConfirmDialog (confirmAction)="toggleConfirm($event)"></confirmation-dialog> <confirmation-dialog #toggleConfirmDialog (confirmAction)="toggleConfirm($event)"></confirmation-dialog>
<confirmation-dialog #deletionConfirmDialog (confirmAction)="deletionConfirm($event)"></confirmation-dialog> <confirmation-dialog #deletionConfirmDialog (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
<clr-datagrid> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="projectless">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'project_name'" *ngIf="projectless">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'description'">{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'target_name'">{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="enabledComparator">{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let p of rules" [clrDgItem]="p" (click)="selectRule(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''"> <clr-dg-row *clrDgItems="let p of rules" [clrDgItem]="p" (click)="selectRule(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''">
<clr-dg-action-overflow> <clr-dg-action-overflow>
<button class="action-item" (click)="editRule(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button> <button class="action-item" (click)="editRule(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
@ -27,7 +27,7 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<clr-dg-cell>{{p.target_name}}</clr-dg-cell> <clr-dg-cell>{{p.target_name}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<ng-template [ngIf]="p.start_time === nullTime">-</ng-template> <ng-template [ngIf]="p.start_time === nullTime">-</ng-template>
<ng-template [ngIf]="p.start_time !== nullTime">{{p.start_time}}</ng-template> <ng-template [ngIf]="p.start_time !== nullTime">{{p.start_time | date: 'short'}}</ng-template>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
{{ (p.enabled === 1 ? 'REPLICATION.ENABLED' : 'REPLICATION.DISABLED') | translate}} {{ (p.enabled === 1 ? 'REPLICATION.ENABLED' : 'REPLICATION.DISABLED') | translate}}

View File

@ -25,9 +25,9 @@ import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ErrorHandler } from '../error-handler/error-handler'; import { ErrorHandler } from '../error-handler/error-handler';
import { toPromise } from '../utils'; import { toPromise, CustomComparator } from '../utils';
import { State } from 'clarity-angular'; import { State, Comparator } from 'clarity-angular';
import { LIST_REPLICATION_RULE_TEMPLATE } from './list-replication-rule.component.html'; import { LIST_REPLICATION_RULE_TEMPLATE } from './list-replication-rule.component.html';
@ -44,6 +44,8 @@ export class ListReplicationRuleComponent {
@Input() projectless: boolean; @Input() projectless: boolean;
@Input() selectedId: number | string; @Input() selectedId: number | string;
@Input() loading: boolean = false;
@Output() reload = new EventEmitter<boolean>(); @Output() reload = new EventEmitter<boolean>();
@Output() selectOne = new EventEmitter<ReplicationRule>(); @Output() selectOne = new EventEmitter<ReplicationRule>();
@Output() editOne = new EventEmitter<ReplicationRule>(); @Output() editOne = new EventEmitter<ReplicationRule>();
@ -55,11 +57,14 @@ export class ListReplicationRuleComponent {
@ViewChild('deletionConfirmDialog') @ViewChild('deletionConfirmDialog')
deletionConfirmDialog: ConfirmationDialogComponent; deletionConfirmDialog: ConfirmationDialogComponent;
startTimeComparator: Comparator<ReplicationRule> = new CustomComparator<ReplicationRule>('start_time', 'date');
enabledComparator: Comparator<ReplicationRule> = new CustomComparator<ReplicationRule>('enabled', 'number');
constructor( constructor(
private replicationService: ReplicationService, private replicationService: ReplicationService,
private translateService: TranslateService, private translateService: TranslateService,
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private ref: ChangeDetectorRef) { private ref: ChangeDetectorRef) {
setInterval(()=>ref.markForCheck(), 500); setInterval(()=>ref.markForCheck(), 500);
} }

View File

@ -1,8 +1,8 @@
export const LIST_REPOSITORY_TEMPLATE = ` export const LIST_REPOSITORY_TEMPLATE = `
<clr-datagrid (clrDgRefresh)="refresh($event)"> <clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="tagsCountComparator">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let r of repositories" [clrDgItem]='r'> <clr-dg-row *clrDgItems="let r of repositories" [clrDgItem]='r'>
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole"> <clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button> <button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
@ -12,7 +12,8 @@ export const LIST_REPOSITORY_TEMPLATE = `
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell> <clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer> <clr-dg-footer>
{{(repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}} {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
<clr-dg-pagination [clrDgPageSize]="15"></clr-dg-pagination> {{pagination.totalItems}}{{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
</clr-dg-footer> </clr-dg-footer>
</clr-datagrid>`; </clr-datagrid>`;

View File

@ -1,10 +1,12 @@
import { Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; import { Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
import { State } from 'clarity-angular'; import { State, Comparator } from 'clarity-angular';
import { Repository } from '../service/interface'; import { Repository } from '../service/interface';
import { LIST_REPOSITORY_TEMPLATE } from './list-repository.component.html'; import { LIST_REPOSITORY_TEMPLATE } from './list-repository.component.html';
import { CustomComparator } from '../utils';
@Component({ @Component({
selector: 'hbr-list-repository', selector: 'hbr-list-repository',
template: LIST_REPOSITORY_TEMPLATE, template: LIST_REPOSITORY_TEMPLATE,
@ -21,6 +23,10 @@ export class ListRepositoryComponent {
pageOffset: number = 1; pageOffset: number = 1;
pullCountComparator: Comparator<Repository> = new CustomComparator<Repository>('pull_count', 'number');
tagsCountComparator: Comparator<Repository> = new CustomComparator<Repository>('tags_count', 'number');
constructor( constructor(
private ref: ChangeDetectorRef) { private ref: ChangeDetectorRef) {
let hnd = setInterval(()=>ref.markForCheck(), 100); let hnd = setInterval(()=>ref.markForCheck(), 100);

View File

@ -19,9 +19,11 @@ import {
} from '../service/index'; } from '../service/index';
import { ErrorHandler } from '../error-handler/index'; import { ErrorHandler } from '../error-handler/index';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { toPromise } from '../utils'; import { toPromise, CustomComparator } from '../utils';
import { LOG_TEMPLATE, LOG_STYLES } from './recent-log.template'; import { LOG_TEMPLATE, LOG_STYLES } from './recent-log.template';
import { Comparator } from 'clarity-angular';
@Component({ @Component({
selector: 'hbr-log', selector: 'hbr-log',
styles: [LOG_STYLES], styles: [LOG_STYLES],
@ -35,6 +37,10 @@ export class RecentLogComponent implements OnInit {
lines: number = 10; //Support 10, 25 and 50 lines: number = 10; //Support 10, 25 and 50
currentTerm: string; currentTerm: string;
loading: boolean;
opTimeComparator: Comparator<AccessLog> = new CustomComparator<AccessLog>('op_time', 'date');
constructor( constructor(
private logService: AccessLogService, private logService: AccessLogService,
private errorHandler: ErrorHandler) { } private errorHandler: ErrorHandler) { }
@ -81,14 +87,17 @@ export class RecentLogComponent implements OnInit {
} }
this.onGoing = true; this.onGoing = true;
this.loading = true;
toPromise<AccessLog[]>(this.logService.getRecentLogs(this.lines)) toPromise<AccessLog[]>(this.logService.getRecentLogs(this.lines))
.then(response => { .then(response => {
this.onGoing = false; this.onGoing = false;
this.loading = false;
this.logsCache = response; //Keep the data this.logsCache = response; //Keep the data
this.recentLogs = this.logsCache.filter(log => log.username != "");//To display this.recentLogs = this.logsCache.filter(log => log.username != "");//To display
}) })
.catch(error => { .catch(error => {
this.onGoing = false; this.onGoing = false;
this.loading = false;
this.errorHandler.error(error); this.errorHandler.error(error);
}); });
} }

View File

@ -24,18 +24,18 @@ export const LOG_TEMPLATE: string = `
</div> </div>
</div> </div>
<div> <div>
<clr-datagrid> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'username'">{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'repo_name'">{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'repo_tag'">{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'operation'">{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="opTimeComparator">{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let l of recentLogs"> <clr-dg-row *clrDgItems="let l of recentLogs">
<clr-dg-cell>{{l.username}}</clr-dg-cell> <clr-dg-cell>{{l.username}}</clr-dg-cell>
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell> <clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell> <clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
<clr-dg-cell>{{l.operation}}</clr-dg-cell> <clr-dg-cell>{{l.operation}}</clr-dg-cell>
<clr-dg-cell>{{l.op_time}}</clr-dg-cell> <clr-dg-cell>{{l.op_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer>{{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer> <clr-dg-footer>{{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid> </clr-datagrid>

View File

@ -20,7 +20,7 @@ export const REPLICATION_TEMPLATE: string = `
</div> </div>
</div> </div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-replication-rule [rules]="changedRules" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOneRule($event)" (editOne)="openEditRule($event)" (reload)="reloadRules($event)"></list-replication-rule> <list-replication-rule [rules]="changedRules" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOneRule($event)" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading"></list-replication-rule>
</div> </div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between"> <div class="row flex-items-xs-between">
@ -46,19 +46,19 @@ export const REPLICATION_TEMPLATE: string = `
</div> </div>
</div> </div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'repository'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.STATUS' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.OPERATION' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'operation'">{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.END_TIME' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="updateTimeComparator">{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column> <clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let j of jobs" [clrDgItem]='j'> <clr-dg-row *clrDgItems="let j of jobs" [clrDgItem]='j'>
<clr-dg-cell>{{j.repository}}</clr-dg-cell> <clr-dg-cell>{{j.repository}}</clr-dg-cell>
<clr-dg-cell>{{j.status}}</clr-dg-cell> <clr-dg-cell>{{j.status}}</clr-dg-cell>
<clr-dg-cell>{{j.operation}}</clr-dg-cell> <clr-dg-cell>{{j.operation}}</clr-dg-cell>
<clr-dg-cell>{{j.creation_time}}</clr-dg-cell> <clr-dg-cell>{{j.creation_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{j.update_time}}</clr-dg-cell> <clr-dg-cell>{{j.update_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<a href="/api/jobs/replication/{{j.id}}/log" target="_BLANK"> <a href="/api/jobs/replication/{{j.id}}/log" target="_BLANK">
<clr-icon shape="clipboard"></clr-icon> <clr-icon shape="clipboard"></clr-icon>

View File

@ -24,7 +24,9 @@ import { ReplicationService } from '../service/replication.service';
import { RequestQueryParams } from '../service/RequestQueryParams'; import { RequestQueryParams } from '../service/RequestQueryParams';
import { ReplicationRule, ReplicationJob, Endpoint } from '../service/interface'; import { ReplicationRule, ReplicationJob, Endpoint } from '../service/interface';
import { toPromise } from '../utils'; import { toPromise, CustomComparator } from '../utils';
import { Comparator } from 'clarity-angular';
import { REPLICATION_TEMPLATE } from './replication.component.html'; import { REPLICATION_TEMPLATE } from './replication.component.html';
import { REPLICATION_STYLE } from './replication.component.css'; import { REPLICATION_STYLE } from './replication.component.css';
@ -81,8 +83,10 @@ export class ReplicationComponent implements OnInit {
changedRules: ReplicationRule[]; changedRules: ReplicationRule[];
initSelectedId: number | string; initSelectedId: number | string;
rules: ReplicationRule[]; rules: ReplicationRule[];
jobs: ReplicationJob[]; loading: boolean;
jobs: ReplicationJob[];
jobsTotalRecordCount: number; jobsTotalRecordCount: number;
jobsTotalPage: number; jobsTotalPage: number;
@ -93,23 +97,28 @@ export class ReplicationComponent implements OnInit {
@ViewChild(CreateEditRuleComponent) @ViewChild(CreateEditRuleComponent)
createEditPolicyComponent: CreateEditRuleComponent; createEditPolicyComponent: CreateEditRuleComponent;
creationTimeComparator: Comparator<ReplicationJob> = new CustomComparator<ReplicationJob>('creation_time', 'date');
updateTimeComparator: Comparator<ReplicationJob> = new CustomComparator<ReplicationJob>('update_time', 'date');
constructor( constructor(
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private replicationService: ReplicationService, private replicationService: ReplicationService,
private translateService: TranslateService) { private translateService: TranslateService) {
} }
ngOnInit(): void { ngOnInit() {
if(!this.projectId) { if(!this.projectId) {
this.errorHandler.warning('Project ID is unset.'); this.errorHandler.warning('Project ID is unset.');
} }
this.currentRuleStatus = this.ruleStatus[0]; this.currentRuleStatus = this.ruleStatus[0];
this.currentJobStatus = this.jobStatus[0]; this.currentJobStatus = this.jobStatus[0];
this.currentJobSearchOption = 0; this.currentJobSearchOption = 0;
this.retrieveRules(); this.retrieveRules();
} }
retrieveRules(): void { retrieveRules(): void {
this.loading = true;
toPromise<ReplicationRule[]>(this.replicationService toPromise<ReplicationRule[]>(this.replicationService
.getReplicationRules(this.projectId, this.search.ruleName)) .getReplicationRules(this.projectId, this.search.ruleName))
.then(response=>{ .then(response=>{
@ -122,8 +131,12 @@ export class ReplicationComponent implements OnInit {
this.search.ruleId = this.changedRules[0].id || ''; this.search.ruleId = this.changedRules[0].id || '';
this.fetchReplicationJobs(); this.fetchReplicationJobs();
} }
this.loading = false;
} }
).catch(error=>this.errorHandler.error(error)); ).catch(error=>{
this.errorHandler.error(error);
this.loading = false;
});
} }
openModal(): void { openModal(): void {
@ -147,13 +160,15 @@ export class ReplicationComponent implements OnInit {
params.set('repository', this.search.repoName); params.set('repository', this.search.repoName);
params.set('start_time', this.search.startTimestamp); params.set('start_time', this.search.startTimestamp);
params.set('end_time', this.search.endTimestamp); params.set('end_time', this.search.endTimestamp);
toPromise<ReplicationJob[]>(this.replicationService toPromise<ReplicationJob[]>(this.replicationService
.getJobs(this.search.ruleId, params)) .getJobs(this.search.ruleId, params))
.then( .then(
response=>{ response=>{
this.jobs = response; this.jobs = response;
}).catch(error=>this.errorHandler.error(error)); }).catch(error=>{
this.errorHandler.error(error);
});
} }
selectOneRule(rule: ReplicationRule) { selectOneRule(rule: ReplicationRule) {

View File

@ -11,30 +11,6 @@ export interface Base {
update_time?: Date; update_time?: Date;
} }
/**
* Interface for tag history
*
* @export
* @interface TagCompatibility
*/
export interface TagCompatibility {
v1Compatibility: string;
}
/**
* Interface for tag manifest
*
* @export
* @interface TagManifest
*/
export interface TagManifest {
schemaVersion: number;
name: string;
tag: string;
architecture: string;
history: TagCompatibility[];
}
/** /**
* Interface for Repository * Interface for Repository
* *
@ -59,10 +35,16 @@ export interface Repository extends Base {
* @interface Tag * @interface Tag
* @extends {Base} * @extends {Base}
*/ */
export interface Tag extends Base { export interface Tag extends Base {
tag: string; digest: string;
manifest: TagManifest; name: string;
signed?: number; //May NOT exist architecture: string;
os: string;
docker_version: string;
author: string;
created: Date;
signature?: string;
} }
/** /**

View File

@ -87,7 +87,7 @@ export class RepositoryDefaultService extends RepositoryService {
return Promise.reject('Bad argument'); return Promise.reject('Bad argument');
} }
let url: string = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories'; let url: string = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories';
url = `${url}/${repositoryName}/tags`; url = `${url}/${repositoryName}`;
return this.http.delete(url, HTTP_JSON_OPTIONS).toPromise() return this.http.delete(url, HTTP_JSON_OPTIONS).toPromise()
.then(response => response) .then(response => response)

View File

@ -4,41 +4,24 @@ import { TagService, TagDefaultService } from './tag.service';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { Tag, TagCompatibility, TagManifest } from './interface'; import { Tag } from './interface';
import { VerifiedSignature } from './tag.service'; import { VerifiedSignature } from './tag.service';
import { toPromise } from '../utils'; import { toPromise } from '../utils';
describe('TagService', () => { describe('TagService', () => {
let mockComp: TagCompatibility[] = [{ let mockTags: Tag[] = [
v1Compatibility: '{"architecture":"amd64","author":"NGINX Docker Maintainers \\"docker-maint@nginx.com\\"","config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["nginx","-g","daemon off;"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"container":"f1883a3fb44b0756a2a3b1e990736a44b1387183125351370042ce7bd9ffc338","container_config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\\"nginx\\" \\"-g\\" \\"daemon off;\\"]"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"created":"2016-11-08T22:41:15.912313785Z","docker_version":"1.12.3","id":"db3700426e6d7c1402667f42917109b2467dd49daa85d38ac99854449edc20b3","os":"linux","parent":"f3ef5f96caf99a18c6821487102c136b00e0275b1da0c7558d7090351f9d447e","throwaway":true}' {
}]; "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
let mockManifest: TagManifest = { "name": "1.11.5",
schemaVersion: 1, "architecture": "amd64",
name: 'library/nginx', "os": "linux",
tag: '1.11.5', "docker_version": "1.12.3",
architecture: 'amd64', "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
history: mockComp "created": new Date("2016-11-08T22:41:15.912313785Z"),
}; "signature": null
let mockTags: Tag[] = [{
tag: '1.11.5',
manifest: mockManifest
}];
let mockSignatures: VerifiedSignature[] = [{
tag: '1.11.5',
hashes: {
sha256: 'fake'
} }
}]; ];
let mockSignatures2: VerifiedSignature[] = [{
tag: '1.11.15',
hashes: {
sha256: 'fake2'
}
}];
const mockConfig: IServiceConfig = { const mockConfig: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories/testing" repositoryBaseEndpoint: "/api/repositories/testing"
@ -65,50 +48,4 @@ describe('TagService', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
})); }));
it('should get tags with signed status[1] if signatures exists', async(inject([TagDefaultService], (service: TagService) => {
expect(service).toBeTruthy();
let spy1: jasmine.Spy = spyOn(service, '_getTags')
.and.returnValue(Promise.resolve(mockTags));
let spy2: jasmine.Spy = spyOn(service, '_getSignatures')
.and.returnValue(Promise.resolve(mockSignatures));
toPromise<Tag[]>(service.getTags('library/nginx'))
.then(tags => {
expect(tags).toBeTruthy();
expect(tags.length).toBe(1);
expect(tags[0].signed).toBe(1);
});
})));
it('should get tags with not-signed status[0] if signatures exists', async(inject([TagDefaultService], (service: TagService) => {
expect(service).toBeTruthy();
let spy1: jasmine.Spy = spyOn(service, '_getTags')
.and.returnValue(Promise.resolve(mockTags));
let spy2: jasmine.Spy = spyOn(service, '_getSignatures')
.and.returnValue(Promise.resolve(mockSignatures2));
toPromise<Tag[]>(service.getTags('library/nginx'))
.then(tags => {
expect(tags).toBeTruthy();
expect(tags.length).toBe(1);
expect(tags[0].signed).toBe(0);
});
})));
it('should get tags with default signed status[-1] if signatures not exist', async(inject([TagDefaultService], (service: TagService) => {
expect(service).toBeTruthy();
let spy1: jasmine.Spy = spyOn(service, '_getTags')
.and.returnValue(Promise.resolve(mockTags));
let spy2: jasmine.Spy = spyOn(service, '_getSignatures')
.and.returnValue(Promise.reject("Error"));
toPromise<Tag[]>(service.getTags('library/nginx'))
.then(tags => {
expect(tags).toBeTruthy();
expect(tags.length).toBe(1);
expect(tags[0].signed).toBe(-1);
});
})));
}); });

View File

@ -100,27 +100,7 @@ export class TagDefaultService extends TagService {
if (!repositoryName) { if (!repositoryName) {
return Promise.reject("Bad argument"); return Promise.reject("Bad argument");
} }
return this._getTags(repositoryName, queryParams);
return this._getTags(repositoryName, queryParams)
.then(tags => {
return this._getSignatures(repositoryName)
.then(signatures => {
tags.forEach(tag => {
let foundOne: VerifiedSignature | undefined = signatures.find(signature => signature.tag === tag.tag);
if (foundOne) {
tag.signed = 1;//Signed
} else {
tag.signed = 0;//Not signed
}
});
return tags;
})
.catch(error => {
tags.forEach(tag => tag.signed = -1);//No signature info
return tags;
})
})
.catch(error => Promise.reject(error))
} }
public deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<Tag> | any { public deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<Tag> | any {

View File

@ -4,7 +4,7 @@ export const TAG_TEMPLATE = `
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3> <h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body"> <div class="modal-body">
<div class="row col-md-12"> <div class="row col-md-12">
<textarea rows="3" (click)="selectAndCopy($event)">{{tagID}}</textarea> <textarea rows="3" (click)="selectAndCopy($event)">{{digestId}}</textarea>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -12,36 +12,39 @@ export const TAG_TEMPLATE = `
</div> </div>
</clr-modal> </clr-modal>
<h2 class="sub-header-title">{{repoName}}</h2> <h2 class="sub-header-title">{{repoName}}</h2>
<clr-datagrid> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column> <clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column> <clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column> <clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'docker_version'">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'architecture'">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'os'">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'> <clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow> <clr-dg-action-overflow>
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button> <button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button> <button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow> </clr-dg-action-overflow>
<clr-dg-cell>{{t.tag}}</clr-dg-cell> <clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell> <clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signed"> <clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<clr-icon shape="check" *ngSwitchCase="1" style="color: #1D5100;"></clr-icon> <clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngSwitchCase="0" style="color: #C92100;"></clr-icon> <clr-icon shape="close" *ngSwitchCase="false" style="color: #C92100;"></clr-icon>
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right"> <a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="help" style="color: #565656;" size="16"></clr-icon> <clr-icon shape="help" style="color: #565656;" size="16"></clr-icon>
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span> <span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a> </a>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell> <clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created}}</clr-dg-cell> <clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell> <clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell>{{t.architecture}}</clr-dg-cell> <clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}</clr-dg-cell> <clr-dg-cell>{{t.os}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer>{{tags ? tags.length : 0}} {{'REPOSITORY.ITEMS' | translate}}</clr-dg-footer> <clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>`; </clr-datagrid>`;

View File

@ -8,7 +8,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
import { TagComponent } from './tag.component'; import { TagComponent } from './tag.component';
import { ErrorHandler } from '../error-handler/error-handler'; import { ErrorHandler } from '../error-handler/error-handler';
import { Tag, TagCompatibility, TagManifest, TagView } from '../service/interface'; import { Tag } from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { TagService, TagDefaultService } from '../service/tag.service'; import { TagService, TagDefaultService } from '../service/tag.service';
@ -19,21 +19,18 @@ describe('TagComponent (inline template)', ()=> {
let tagService: TagService; let tagService: TagService;
let spy: jasmine.Spy; let spy: jasmine.Spy;
let mockComp: TagCompatibility[] = [{ let mockTags: Tag[] = [
v1Compatibility: '{"architecture":"amd64","author":"NGINX Docker Maintainers \\"docker-maint@nginx.com\\"","config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["nginx","-g","daemon off;"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"container":"f1883a3fb44b0756a2a3b1e990736a44b1387183125351370042ce7bd9ffc338","container_config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\\"nginx\\" \\"-g\\" \\"daemon off;\\"]"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"created":"2016-11-08T22:41:15.912313785Z","docker_version":"1.12.3","id":"db3700426e6d7c1402667f42917109b2467dd49daa85d38ac99854449edc20b3","os":"linux","parent":"f3ef5f96caf99a18c6821487102c136b00e0275b1da0c7558d7090351f9d447e","throwaway":true}' {
}]; "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
let mockManifest: TagManifest = { "name": "1.11.5",
schemaVersion: 1, "architecture": "amd64",
name: 'library/nginx', "os": "linux",
tag: '1.11.5', "docker_version": "1.12.3",
architecture: 'amd64', "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
history: mockComp "created": new Date("2016-11-08T22:41:15.912313785Z"),
}; "signature": null
}
let mockTags: Tag[] = [{ ];
tag: '1.11.5',
manifest: mockManifest
}];
let config: IServiceConfig = { let config: IServiceConfig = {
repositoryBaseEndpoint: '/api/repositories/testing' repositoryBaseEndpoint: '/api/repositories/testing'

View File

@ -26,30 +26,17 @@ import { Tag, SessionInfo } from '../service/interface';
import { TAG_TEMPLATE } from './tag.component.html'; import { TAG_TEMPLATE } from './tag.component.html';
import { TAG_STYLE } from './tag.component.css'; import { TAG_STYLE } from './tag.component.css';
import { toPromise } from '../utils'; import { toPromise, CustomComparator } from '../utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
/** import { State, Comparator } from 'clarity-angular';
* Inteface for the tag view
*/
export interface TagView {
tag: string;
pullCommand: string;
signed: number;
author: string;
created: Date;
dockerVersion: string;
architecture: string;
os: string;
id: string;
parent: string;
}
@Component({ @Component({
selector: 'hbr-tag', selector: 'hbr-tag',
template: TAG_TEMPLATE, template: TAG_TEMPLATE,
styles: [ TAG_STYLE ] styles: [ TAG_STYLE ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TagComponent implements OnInit { export class TagComponent implements OnInit {
@ -59,7 +46,7 @@ export class TagComponent implements OnInit {
hasProjectAdminRole: boolean; hasProjectAdminRole: boolean;
tags: TagView[]; tags: Tag[];
registryUrl: string; registryUrl: string;
withNotary: boolean; withNotary: boolean;
@ -67,28 +54,17 @@ export class TagComponent implements OnInit {
showTagManifestOpened: boolean; showTagManifestOpened: boolean;
manifestInfoTitle: string; manifestInfoTitle: string;
tagID: string; digestId: string;
staticBackdrop: boolean = true; staticBackdrop: boolean = true;
closable: boolean = false; closable: boolean = false;
createdComparator: Comparator<Tag> = new CustomComparator<Tag>('created', 'date');
loading: boolean = false;
@ViewChild('confirmationDialog') @ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent; confirmationDialog: ConfirmationDialogComponent;
get initTagView() {
return {
tag: '',
pullCommand: '',
signed: -1,
author: '',
created: new Date(),
dockerVersion: '',
architecture: '',
os: '',
id: '',
parent: ''
};
}
constructor( constructor(
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private tagService: TagService, private tagService: TagService,
@ -99,14 +75,13 @@ export class TagComponent implements OnInit {
if (message && if (message &&
message.source === ConfirmationTargets.TAG message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) { && message.state === ConfirmationState.CONFIRMED) {
let tag = message.data; let tag: Tag = message.data;
if (tag) { if (tag) {
if (tag.signed) { if (tag.signature) {
return; return;
} else { } else {
let tagName = tag.tag;
toPromise<number>(this.tagService toPromise<number>(this.tagService
.deleteTag(this.repoName, tagName)) .deleteTag(this.repoName, tag.name))
.then( .then(
response => { response => {
this.retrieve(); this.retrieve();
@ -141,49 +116,34 @@ export class TagComponent implements OnInit {
retrieve() { retrieve() {
this.tags = []; this.tags = [];
this.loading = true;
toPromise<Tag[]>(this.tagService toPromise<Tag[]>(this.tagService
.getTags(this.repoName)) .getTags(this.repoName))
.then(items => this.listTags(items)) .then(items => {
.catch(error => this.errorHandler.error(error)); this.tags = items;
} this.loading = false;
})
listTags(tags: Tag[]): void { .catch(error => {
tags.forEach(t => { this.errorHandler.error(error);
let tag = this.initTagView; this.loading = false;
tag.tag = t.tag; });
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture'];
tag.author = data['author'];
if(!t.signed && t.signed !== 0) {
tag.signed = -1;
} else {
tag.signed = t.signed;
}
tag.created = data['created'];
tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
tag.os = data['os'];
tag.id = data['id'];
tag.parent = data['parent'];
this.tags.push(tag);
});
let hnd = setInterval(()=>this.ref.markForCheck(), 100); let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000); setTimeout(()=>clearInterval(hnd), 1000);
} }
deleteTag(tag: TagView) { deleteTag(tag: Tag) {
if (tag) { if (tag) {
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons; let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
if (tag.signed) { if (tag.signature) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED'; summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
buttons = ConfirmationButtons.CLOSE; buttons = ConfirmationButtons.CLOSE;
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.tag; content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.name;
} else { } else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG'; summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
buttons = ConfirmationButtons.DELETE_CANCEL; buttons = ConfirmationButtons.DELETE_CANCEL;
content = tag.tag; content = tag.name;
} }
let message = new ConfirmationMessage( let message = new ConfirmationMessage(
titleKey, titleKey,
@ -196,15 +156,10 @@ export class TagComponent implements OnInit {
} }
} }
showTagID(type: string, tag: TagView) { showDigestId(tag: Tag) {
if(tag) { if(tag) {
if(type === 'tag') { this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
this.manifestInfoTitle = 'REPOSITORY.COPY_ID'; this.digestId = tag.digest;
this.tagID = tag.id;
} else if(type === 'parent') {
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
this.tagID = tag.parent;
}
this.showTagManifestOpened = true; this.showTagManifestOpened = true;
} }
} }

View File

@ -3,6 +3,7 @@ import 'rxjs/add/operator/toPromise';
import { RequestOptions, Headers } from '@angular/http'; import { RequestOptions, Headers } from '@angular/http';
import { RequestQueryParams } from './service/RequestQueryParams'; import { RequestQueryParams } from './service/RequestQueryParams';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
import { Comparator } from 'clarity-angular';
/** /**
* Convert the different async channels to the Promise<T> type. * Convert the different async channels to the Promise<T> type.
@ -85,4 +86,36 @@ export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClic
} else { } else {
el.triggerEventHandler('click', eventObj); el.triggerEventHandler('click', eventObj);
} }
}
/**
* Comparator for fields with specific type.
*
*/
export class CustomComparator<T> implements Comparator<T> {
fieldName: string;
type: string;
constructor(fieldName: string, type: string) {
this.fieldName = fieldName;
this.type = type;
}
compare(a: {[key: string]: any| any[]}, b: {[key: string]: any| any[]}) {
let comp = 0;
if(a && b && a[this.fieldName] && b[this.fieldName]) {
let fieldA = a[this.fieldName];
let fieldB = b[this.fieldName];
switch(this.type) {
case "number":
comp = fieldB - fieldA;
break;
case "date":
comp = new Date(fieldB).getTime() - new Date(fieldA).getTime();
break;
}
}
return comp;
}
} }