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;
if(this.targetNameHasChanged) {
payload.name = this.target.name;
delete payload.endpoint;
}
if (this.endpointHasChanged) {
payload.endpoint = this.target.endpoint;

View File

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

View File

@ -116,8 +116,9 @@ describe('EndpointComponent (inline template)', () => {
it('should open create endpoint modal', async(() => {
fixture.detectChanges();
comp.editTarget(mockOne);
fixture.whenStable().then(()=>{
fixture.whenStable().then(()=>{
fixture.detectChanges();
comp.editTarget(mockOne);
fixture.detectChanges();
expect(comp.target.name).toEqual('target_01');
});
@ -125,7 +126,6 @@ describe('EndpointComponent (inline template)', () => {
it('should filter endpoints by keyword', async(() => {
fixture.detectChanges();
fixture.whenStable().then(()=>{
fixture.detectChanges();
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_TEMPLATE } from './endpoint.component.html';
import { toPromise } from '../utils';
import { toPromise, CustomComparator } from '../utils';
import { State, Comparator } from 'clarity-angular';
@Component({
selector: 'hbr-endpoint',
@ -55,6 +57,10 @@ export class EndpointComponent implements OnInit {
targetName: string;
subscription: Subscription;
loading: boolean = false;
creationTimeComparator: Comparator<Endpoint> = new CustomComparator<Endpoint>('creation_time', 'date');
get initEndpoint(): Endpoint {
return {
endpoint: "",
@ -101,7 +107,7 @@ export class EndpointComponent implements OnInit {
ngOnInit(): void {
this.targetName = '';
this.retrieve('');
this.retrieve();
}
ngOnDestroy(): void {
@ -110,29 +116,34 @@ export class EndpointComponent implements OnInit {
}
}
retrieve(targetName: string): void {
retrieve(): void {
this.loading = true;
toPromise<Endpoint[]>(this.endpointService
.getEndpoints(targetName))
.getEndpoints(this.targetName))
.then(
targets => {
this.targets = targets || [];
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
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) {
this.targetName = targetName;
this.retrieve(targetName);
this.retrieve();
}
refreshTargets() {
this.retrieve('');
this.retrieve();
}
reload($event: any) {
this.targetName = '';
this.retrieve('');
this.retrieve();
}
openModal() {

View File

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

View File

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

View File

@ -1,13 +1,13 @@
export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<confirmation-dialog #toggleConfirmDialog (confirmAction)="toggleConfirm($event)"></confirmation-dialog>
<confirmation-dialog #deletionConfirmDialog (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
<clr-datagrid>
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="projectless">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'project_name'" *ngIf="projectless">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'description'">{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'target_name'">{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.LAST_START_TIME' | 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-action-overflow>
<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>
<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>
{{ (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 { 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';
@ -44,6 +44,8 @@ export class ListReplicationRuleComponent {
@Input() projectless: boolean;
@Input() selectedId: number | string;
@Input() loading: boolean = false;
@Output() reload = new EventEmitter<boolean>();
@Output() selectOne = new EventEmitter<ReplicationRule>();
@Output() editOne = new EventEmitter<ReplicationRule>();
@ -55,11 +57,14 @@ export class ListReplicationRuleComponent {
@ViewChild('deletionConfirmDialog')
deletionConfirmDialog: ConfirmationDialogComponent;
startTimeComparator: Comparator<ReplicationRule> = new CustomComparator<ReplicationRule>('start_time', 'date');
enabledComparator: Comparator<ReplicationRule> = new CustomComparator<ReplicationRule>('enabled', 'number');
constructor(
private replicationService: ReplicationService,
private translateService: TranslateService,
private errorHandler: ErrorHandler,
private ref: ChangeDetectorRef) {
private ref: ChangeDetectorRef) {
setInterval(()=>ref.markForCheck(), 500);
}

View File

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

View File

@ -1,10 +1,12 @@
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 { LIST_REPOSITORY_TEMPLATE } from './list-repository.component.html';
import { CustomComparator } from '../utils';
@Component({
selector: 'hbr-list-repository',
template: LIST_REPOSITORY_TEMPLATE,
@ -21,6 +23,10 @@ export class ListRepositoryComponent {
pageOffset: number = 1;
pullCountComparator: Comparator<Repository> = new CustomComparator<Repository>('pull_count', 'number');
tagsCountComparator: Comparator<Repository> = new CustomComparator<Repository>('tags_count', 'number');
constructor(
private ref: ChangeDetectorRef) {
let hnd = setInterval(()=>ref.markForCheck(), 100);

View File

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

View File

@ -24,18 +24,18 @@ export const LOG_TEMPLATE: string = `
</div>
</div>
<div>
<clr-datagrid>
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-column [clrDgField]="'username'">{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'repo_name'">{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'repo_tag'">{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'operation'">{{'AUDIT_LOG.OPERATION' | 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-cell>{{l.username}}</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.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-footer>{{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>

View File

@ -20,7 +20,7 @@ export const REPLICATION_TEMPLATE: string = `
</div>
</div>
<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 class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
@ -46,19 +46,19 @@ export const REPLICATION_TEMPLATE: string = `
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid>
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-column [clrDgField]="'repository'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'operation'">{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'REPLICATION.CREATION_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-row *clrDgItems="let j of jobs" [clrDgItem]='j'>
<clr-dg-cell>{{j.repository}}</clr-dg-cell>
<clr-dg-cell>{{j.status}}</clr-dg-cell>
<clr-dg-cell>{{j.operation}}</clr-dg-cell>
<clr-dg-cell>{{j.creation_time}}</clr-dg-cell>
<clr-dg-cell>{{j.update_time}}</clr-dg-cell>
<clr-dg-cell>{{j.creation_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{j.update_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>
<a href="/api/jobs/replication/{{j.id}}/log" target="_BLANK">
<clr-icon shape="clipboard"></clr-icon>

View File

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

View File

@ -11,30 +11,6 @@ export interface Base {
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
*
@ -59,10 +35,16 @@ export interface Repository extends Base {
* @interface Tag
* @extends {Base}
*/
export interface Tag extends Base {
tag: string;
manifest: TagManifest;
signed?: number; //May NOT exist
digest: string;
name: string;
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');
}
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()
.then(response => response)

View File

@ -4,41 +4,24 @@ import { TagService, TagDefaultService } from './tag.service';
import { SharedModule } from '../shared/shared.module';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { Tag, TagCompatibility, TagManifest } from './interface';
import { Tag } from './interface';
import { VerifiedSignature } from './tag.service';
import { toPromise } from '../utils';
describe('TagService', () => {
let mockComp: TagCompatibility[] = [{
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}'
}];
let mockManifest: TagManifest = {
schemaVersion: 1,
name: 'library/nginx',
tag: '1.11.5',
architecture: 'amd64',
history: mockComp
};
let mockTags: Tag[] = [{
tag: '1.11.5',
manifest: mockManifest
}];
let mockSignatures: VerifiedSignature[] = [{
tag: '1.11.5',
hashes: {
sha256: 'fake'
let mockTags: Tag[] = [
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "1.11.5",
"architecture": "amd64",
"os": "linux",
"docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null
}
}];
let mockSignatures2: VerifiedSignature[] = [{
tag: '1.11.15',
hashes: {
sha256: 'fake2'
}
}];
];
const mockConfig: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories/testing"
@ -65,50 +48,4 @@ describe('TagService', () => {
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) {
return Promise.reject("Bad argument");
}
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))
return this._getTags(repositoryName, queryParams);
}
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>
<div class="modal-body">
<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 class="modal-footer">
@ -12,36 +12,39 @@ export const TAG_TEMPLATE = `
</div>
</clr-modal>
<h2 class="sub-header-title">{{repoName}}</h2>
<clr-datagrid>
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.TAG' | 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>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'docker_version'">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'architecture'">{{'REPOSITORY.ARCHITECTURE' | 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-action-overflow>
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button>
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signed">
<clr-icon shape="check" *ngSwitchCase="1" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngSwitchCase="0" style="color: #C92100;"></clr-icon>
<clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></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">
<clr-icon shape="help" style="color: #565656;" size="16"></clr-icon>
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a>
</clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created}}</clr-dg-cell>
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}</clr-dg-cell>
</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>`;

View File

@ -8,7 +8,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
import { TagComponent } from './tag.component';
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 { TagService, TagDefaultService } from '../service/tag.service';
@ -19,21 +19,18 @@ describe('TagComponent (inline template)', ()=> {
let tagService: TagService;
let spy: jasmine.Spy;
let mockComp: TagCompatibility[] = [{
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}'
}];
let mockManifest: TagManifest = {
schemaVersion: 1,
name: 'library/nginx',
tag: '1.11.5',
architecture: 'amd64',
history: mockComp
};
let mockTags: Tag[] = [{
tag: '1.11.5',
manifest: mockManifest
}];
let mockTags: Tag[] = [
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "1.11.5",
"architecture": "amd64",
"os": "linux",
"docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null
}
];
let config: IServiceConfig = {
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_STYLE } from './tag.component.css';
import { toPromise } from '../utils';
import { toPromise, CustomComparator } from '../utils';
import { TranslateService } from '@ngx-translate/core';
/**
* 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;
}
import { State, Comparator } from 'clarity-angular';
@Component({
selector: 'hbr-tag',
template: TAG_TEMPLATE,
styles: [ TAG_STYLE ]
styles: [ TAG_STYLE ],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagComponent implements OnInit {
@ -59,7 +46,7 @@ export class TagComponent implements OnInit {
hasProjectAdminRole: boolean;
tags: TagView[];
tags: Tag[];
registryUrl: string;
withNotary: boolean;
@ -67,28 +54,17 @@ export class TagComponent implements OnInit {
showTagManifestOpened: boolean;
manifestInfoTitle: string;
tagID: string;
digestId: string;
staticBackdrop: boolean = true;
closable: boolean = false;
createdComparator: Comparator<Tag> = new CustomComparator<Tag>('created', 'date');
loading: boolean = false;
@ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent;
get initTagView() {
return {
tag: '',
pullCommand: '',
signed: -1,
author: '',
created: new Date(),
dockerVersion: '',
architecture: '',
os: '',
id: '',
parent: ''
};
}
constructor(
private errorHandler: ErrorHandler,
private tagService: TagService,
@ -99,14 +75,13 @@ export class TagComponent implements OnInit {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tag = message.data;
let tag: Tag = message.data;
if (tag) {
if (tag.signed) {
if (tag.signature) {
return;
} else {
let tagName = tag.tag;
toPromise<number>(this.tagService
.deleteTag(this.repoName, tagName))
.deleteTag(this.repoName, tag.name))
.then(
response => {
this.retrieve();
@ -141,49 +116,34 @@ export class TagComponent implements OnInit {
retrieve() {
this.tags = [];
this.loading = true;
toPromise<Tag[]>(this.tagService
.getTags(this.repoName))
.then(items => this.listTags(items))
.catch(error => this.errorHandler.error(error));
}
listTags(tags: Tag[]): void {
tags.forEach(t => {
let tag = this.initTagView;
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);
});
.then(items => {
this.tags = items;
this.loading = false;
})
.catch(error => {
this.errorHandler.error(error);
this.loading = false;
});
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}
deleteTag(tag: TagView) {
deleteTag(tag: Tag) {
if (tag) {
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
if (tag.signed) {
if (tag.signature) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
buttons = ConfirmationButtons.CLOSE;
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.tag;
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.name;
} else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
buttons = ConfirmationButtons.DELETE_CANCEL;
content = tag.tag;
content = tag.name;
}
let message = new ConfirmationMessage(
titleKey,
@ -196,15 +156,10 @@ export class TagComponent implements OnInit {
}
}
showTagID(type: string, tag: TagView) {
showDigestId(tag: Tag) {
if(tag) {
if(type === 'tag') {
this.manifestInfoTitle = 'REPOSITORY.COPY_ID';
this.tagID = tag.id;
} else if(type === 'parent') {
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
this.tagID = tag.parent;
}
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
this.digestId = tag.digest;
this.showTagManifestOpened = true;
}
}

View File

@ -3,6 +3,7 @@ import 'rxjs/add/operator/toPromise';
import { RequestOptions, Headers } from '@angular/http';
import { RequestQueryParams } from './service/RequestQueryParams';
import { DebugElement } from '@angular/core';
import { Comparator } from 'clarity-angular';
/**
* Convert the different async channels to the Promise<T> type.
@ -85,4 +86,36 @@ export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClic
} else {
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;
}
}