Commit e60e4c12 authored by Steven Zou's avatar Steven Zou
Browse files

integrate with vulnerability API

parent 4d9eeac4
......@@ -76,7 +76,7 @@ If **projectId** is set to the id of specified project, then only show the repli
<hbr-endpoint></hbr-endpoint>
```
* **Repository and Tag Management View[updating]**
* **Repository and Tag Management View**
**projectId** is used to specify which projects the repositories are from.
......@@ -98,6 +98,19 @@ watchTagClickEvent(tag: Tag): void {
```
* **Tag detail view**
This view is linked by the repository stack view only when the Clair is enabled in Harbor.
**tagId** is an @Input property and used to specify the tag of which details are displayed.
**repositoryId** is an @Input property and used to specified the repository to which the tag is belonged.
**backEvt** is an @Output event emitter and used to distribute the click event of the back arrow in the detail page.
```
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="..." [repositoryId]="..."></hbr-tag-detail>
```
## Configurations
All the related configurations are defined in the **HarborModuleConfig** interface.
......@@ -111,6 +124,7 @@ export const DefaultServiceConfig: IServiceConfig = {
targetBaseEndpoint: "/api/targets",
replicationRuleEndpoint: "/api/policies/replication",
replicationJobEndpoint: "/api/jobs/replication",
vulnerabilityScanningBaseEndpoint: "/api/repositories",
enablei18Support: false,
defaultLang: DEFAULT_LANG, //'en-us'
langCookieKey: DEFAULT_LANG_COOKIE_KEY, //'harbor-lang'
......@@ -147,6 +161,8 @@ It supports partially overriding. For the items not overridden, default values w
* **replicationJobEndpoint:** The base endpoint of the service used to handle the replication jobs. Default is "/api/jobs/replication".
* **vulnerabilityScanningBaseEndpoint:** The base endpoint of the service used to handle the vulnerability scanning results.Default value is "/api/repositories".
* **langCookieKey:** The cookie key used to store the current used language preference. Default is "harbor-lang".
* **supportedLangs:** Declare what languages are supported. Default is ['en-us', 'zh-cn', 'es-es'].
......@@ -215,11 +231,14 @@ HarborLibraryModule.forRoot({
...
```
**3. user session(Ongoing/Discussing)**
Some components may need the user authorization and authentication information to display different views. There might be two alternatives to select:
**3. user session**
Some components may need the user authorization and authentication information to display different views. The following way of handing user session is supported by the library.
* Use @Input properties or interface to let top component or page to pass the required user session information in.
* Component retrieves the required information from some API provided by top component or page when necessary.
```
//In the above repository stack view, the user session informations are passed via @input properties.
[hasSignedIn]="..." [hasProjectAdminRole]="..."
```
**4. services**
The library has its own service implementations to communicate with backend APIs and transfer data. If you want to use your own data handling logic, you can implement your own services based on the defined interfaces.
......@@ -606,9 +625,9 @@ export class MyScanningResultService extends ScanningResultService {
*
* @memberOf ScanningResultService
*/
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary{
...
}
getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary{
...
}
/**
* Get the detailed vulnerabilities scanning results.
......@@ -619,9 +638,24 @@ export class MyScanningResultService extends ScanningResultService {
*
* @memberOf ScanningResultService
*/
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]{
...
}
getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]{
...
}
/**
* Start a new vulnerability scanning
*
* @abstract
* @param {string} repoName
* @param {string} tagId
* @returns {(Observable<any> | Promise<any> | any)}
*
* @memberOf ScanningResultService
*/
startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any {
...
}
}
...
......@@ -656,4 +690,4 @@ HarborLibraryModule.forRoot({
})
...
```
\ No newline at end of file
```
{
"name": "harbor-ui",
"version": "0.1.0",
"version": "0.2.0",
"description": "Harbor shared UI components based on Clarity and Angular4",
"scripts": {
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
......
{
"name": "harbor-ui",
"version": "0.1.42",
"version": "0.2.0",
"description": "Harbor shared UI components based on Clarity and Angular4",
"author": "VMware",
"module": "index.js",
......
......@@ -60,6 +60,7 @@ export const DefaultServiceConfig: IServiceConfig = {
targetBaseEndpoint: "/api/targets",
replicationRuleEndpoint: "/api/policies/replication",
replicationJobEndpoint: "/api/jobs/replication",
vulnerabilityScanningBaseEndpoint: "/api/repositories",
enablei18Support: false,
defaultLang: DEFAULT_LANG,
langCookieKey: DEFAULT_LANG_COOKIE_KEY,
......
......@@ -21,7 +21,7 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
<clr-dg-cell>{{r.name}}</clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
</clr-dg-row>
<clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
......
......@@ -14,6 +14,7 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
import { TagService, TagDefaultService } from '../service/tag.service';
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
import { click } from '../utils';
......@@ -90,7 +91,8 @@ describe('RepositoryComponentStackview (inline template)', () => {
RepositoryStackviewComponent,
TagComponent,
ConfirmationDialogComponent,
FilterComponent
FilterComponent,
VULNERABILITY_DIRECTIVES
],
providers: [
ErrorHandler,
......
......@@ -72,6 +72,10 @@ export class RepositoryStackviewComponent implements OnInit {
return this.systemInfo ? this.systemInfo.with_notary : false;
}
public get withClair(): boolean {
return this.systemInfo ? this.systemInfo.with_clair : false;
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.REPOSITORY &&
......
......@@ -45,7 +45,7 @@ export interface Tag extends Base {
author: string;
created: Date;
signature?: string;
vulnerability?: VulnerabilitySummary;
scan_overview?: VulnerabilitySummary;
}
/**
......@@ -145,6 +145,7 @@ export interface AccessLogItem {
*
*/
export interface SystemInfo {
with_clair?: boolean;
with_notary?: boolean;
with_admiral?: boolean;
admiral_endpoint?: string;
......@@ -156,9 +157,8 @@ export interface SystemInfo {
harbor_version?: string;
}
//Not finalized yet
export enum VulnerabilitySeverity {
NONE, UNKNOWN, LOW, MEDIUM, HIGH
_SEVERITY, NONE, UNKNOWN, LOW, MEDIUM, HIGH
}
export interface VulnerabilityBase {
......@@ -170,18 +170,27 @@ export interface VulnerabilityBase {
export interface VulnerabilityItem extends VulnerabilityBase {
fixedVersion: string;
layer: string;
layer?: string;
description: string;
}
export interface VulnerabilitySummary {
total_package: number;
package_with_none: number;
package_with_high?: number;
package_with_medium?: number;
package_With_low?: number;
package_with_unknown?: number;
complete_timestamp: Date;
image_digest?: string;
scan_status: string;
job_id?: number;
severity: VulnerabilitySeverity;
components: VulnerabilityComponents;
update_time: Date; //Use as complete timestamp
}
export interface VulnerabilityComponents {
total: number;
summary: VulnerabilitySeverityMetrics[];
}
export interface VulnerabilitySeverityMetrics {
severity: VulnerabilitySeverity;
count: number;
}
export interface TagClickEvent {
......
......@@ -3,7 +3,8 @@ import 'rxjs/add/observable/of';
import { Injectable, Inject } from "@angular/core";
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { Http, URLSearchParams } from '@angular/http';
import { HTTP_JSON_OPTIONS } from '../utils';
import { buildHttpRequestOptions, HTTP_JSON_OPTIONS } from '../utils';
import { RequestQueryParams } from './RequestQueryParams';
import {
VulnerabilityItem,
......@@ -27,7 +28,7 @@ export abstract class ScanningResultService {
*
* @memberOf ScanningResultService
*/
abstract getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
abstract getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
/**
* Get the detailed vulnerabilities scanning results.
......@@ -38,30 +39,60 @@ export abstract class ScanningResultService {
*
* @memberOf ScanningResultService
*/
abstract getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
abstract getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
/**
* Start a new vulnerability scanning
*
* @abstract
* @param {string} repoName
* @param {string} tagId
* @returns {(Observable<any> | Promise<any> | any)}
*
* @memberOf ScanningResultService
*/
abstract startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any;
}
@Injectable()
export class ScanningResultDefaultService extends ScanningResultService {
_baseUrl: string = '/api/repositories';
constructor(
private http: Http,
@Inject(SERVICE_CONFIG) private config: IServiceConfig) {
super();
if (this.config && this.config.vulnerabilityScanningBaseEndpoint) {
this._baseUrl = this.config.vulnerabilityScanningBaseEndpoint;
}
}
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
if (!tagId || tagId.trim() === '') {
getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}
return Observable.of({});
}
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
if (!tagId || tagId.trim() === '') {
getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}
return this.http.get(`${this._baseUrl}/${repoName}/tags/${tagId}/vulnerability/details`, buildHttpRequestOptions(queryParams)).toPromise()
.then(response => response.json() as VulnerabilityItem[])
.catch(error => Promise.reject(error));
}
startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any {
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}
return Observable.of([]);
return this.http.post(`${this._baseUrl}/${repoName}/tags/${tagId}/scan`, null).toPromise()
.then(() => { return true })
.catch(error => Promise.reject(error));
}
}
\ No newline at end of file
......@@ -7,10 +7,10 @@ export const TAG_DETAIL_HTML: string = `
</div>
<div class="title-block">
<div class="tag-name">
{{tagDetails.name}}:v{{tagDetails.docker_version}}
{{tagDetails.name}}
</div>
<div class="tag-timestamp">
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{tagDetails.author}}
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{author | translate}}
</div>
</div>
</div>
......@@ -23,11 +23,13 @@ export const TAG_DETAIL_HTML: string = `
<div class="image-detail-label">
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
<div>{{'TAG.OS' | translate }}</div>
<div>{{'TAG.DOCKER_VERSION' | translate }}</div>
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
</div>
<div class="image-detail-value">
<div>{{tagDetails.architecture}}</div>
<div>{{tagDetails.os}}</div>
<div>{{tagDetails.docker_version}}</div>
<div>{{scanCompletedDatetime | date}}</div>
</div>
</div>
......@@ -67,7 +69,7 @@ export const TAG_DETAIL_HTML: string = `
</section>
<section class="detail-section">
<div class="vulnerability-block">
<hbr-vulnerabilities-grid tagId="tagId"></hbr-vulnerabilities-grid>
<hbr-vulnerabilities-grid [repositoryId]="repositoryId" [tagId]="tagId"></hbr-vulnerabilities-grid>
</div>
<div>
<ng-content></ng-content>
......
......@@ -5,25 +5,40 @@ import { ResultGridComponent } from '../vulnerability-scanning/result-grid.compo
import { TagDetailComponent } from './tag-detail.component';
import { ErrorHandler } from '../error-handler/error-handler';
import { Tag, VulnerabilitySummary } from '../service/interface';
import { Tag, VulnerabilitySummary, VulnerabilityItem, VulnerabilitySeverity } from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
import { FilterComponent } from '../filter/index';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
describe('TagDetailComponent (inline template)', () => {
let comp: TagDetailComponent;
let fixture: ComponentFixture<TagDetailComponent>;
let tagService: TagService;
let scanningService: ScanningResultService;
let spy: jasmine.Spy;
let vulSpy: jasmine.Spy;
let mockVulnerability: VulnerabilitySummary = {
total_package: 124,
package_with_none: 92,
package_with_high: 10,
package_with_medium: 6,
package_With_low: 13,
package_with_unknown: 3,
complete_timestamp: new Date()
scan_status: VULNERABILITY_SCAN_STATUS.finished,
severity: 5,
update_time: new Date(),
components: {
total: 124,
summary: [{
severity: 1,
count: 90
}, {
severity: 3,
count: 10
}, {
severity: 4,
count: 10
}, {
severity: 5,
count: 13
}]
}
};
let mockTag: Tag = {
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
......@@ -34,7 +49,7 @@ describe('TagDetailComponent (inline template)', () => {
"author": "steven",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null,
vulnerability: mockVulnerability
scan_overview: mockVulnerability
};
let config: IServiceConfig = {
......@@ -70,6 +85,22 @@ describe('TagDetailComponent (inline template)', () => {
tagService = fixture.debugElement.injector.get(TagService);
spy = spyOn(tagService, 'getTag').and.returnValues(Promise.resolve(mockTag));
let mockData: VulnerabilityItem[] = [];
for (let i = 0; i < 30; i++) {
let res: VulnerabilityItem = {
id: "CVE-2016-" + (8859 + i),
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
package: "package_" + i,
layer: "layer_" + i,
version: '4.' + i + ".0",
fixedVersion: '4.' + i + '.11',
description: "Mock data"
};
mockData.push(res);
}
scanningService = fixture.debugElement.injector.get(ScanningResultService);
vulSpy = spyOn(scanningService, 'getVulnerabilityScanningResults').and.returnValue(Promise.resolve(mockData));
fixture.detectChanges();
});
......@@ -85,7 +116,7 @@ describe('TagDetailComponent (inline template)', () => {
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('nginx:v1.12.3');
expect(el.textContent.trim()).toEqual('nginx');
});
}));
......@@ -113,7 +144,7 @@ describe('TagDetailComponent (inline template)', () => {
expect(el).toBeTruthy();
let el2: HTMLElement = el.querySelector('div');
expect(el2).toBeTruthy();
expect(el2.textContent.trim()).toEqual("10 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
expect(el2.textContent.trim()).toEqual("13 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
});
}));
......
......@@ -3,7 +3,7 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { TAG_DETAIL_STYLES } from './tag-detail.component.css';
import { TAG_DETAIL_HTML } from './tag-detail.component.html';
import { TagService, Tag } from '../service/index';
import { TagService, Tag, VulnerabilitySeverity } from '../service/index';
import { toPromise } from '../utils';
import { ErrorHandler } from '../error-handler/index';
......@@ -15,6 +15,11 @@ import { ErrorHandler } from '../error-handler/index';
providers: []
})
export class TagDetailComponent implements OnInit {
_highCount: number = 0;
_mediumCount: number = 0;
_lowCount: number = 0;
_unknownCount: number = 0;
@Input() tagId: string;
@Input() repositoryId: string;
tagDetails: Tag = {
......@@ -36,7 +41,32 @@ export class TagDetailComponent implements OnInit {
ngOnInit(): void {
if (this.repositoryId && this.tagId) {
toPromise<Tag>(this.tagService.getTag(this.repositoryId, this.tagId))
.then(response => this.tagDetails = response)
.then(response => {
this.tagDetails = response;
if (this.tagDetails &&
this.tagDetails.scan_overview &&
this.tagDetails.scan_overview.components &&
this.tagDetails.scan_overview.components.summary) {
this.tagDetails.scan_overview.components.summary.forEach(item => {
switch (item.severity) {
case VulnerabilitySeverity.UNKNOWN:
this._unknownCount += item.count;
break;
case VulnerabilitySeverity.LOW:
this._lowCount += item.count;
break;
case VulnerabilitySeverity.MEDIUM:
this._mediumCount += item.count;
break;
case VulnerabilitySeverity.HIGH:
this._highCount += item.count;
break;
default:
break;
}
});
}
})
.catch(error => this.errorHandler.error(error))
}
}
......@@ -45,29 +75,29 @@ export class TagDetailComponent implements OnInit {
this.backEvt.emit(this.tagId);
}
public get author(): string {
return this.tagDetails && this.tagDetails.author? this.tagDetails.author: 'TAG.ANONYMITY';
}
public get highCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.package_with_high : 0;
return this._highCount;
}
public get mediumCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.package_with_medium : 0;
return this._mediumCount;
}
public get lowCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.package_With_low : 0;
return this._lowCount;
}
public get unknownCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.package_with_unknown : 0;
return this._unknownCount;
}
public get scanCompletedDatetime(): Date {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.complete_timestamp : new Date();
return this.tagDetails && this.tagDetails.scan_overview ?
this.tagDetails.scan_overview.update_time : new Date();
}
public get suffixForHigh(): string {
......
......@@ -30,4 +30,11 @@ export const TAG_STYLE = `
:host >>> .datagrid .datagrid-body .datagrid-row-master {
background-color: #eee;
}
.truncated {
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow:ellipsis;
}
`;
\ No newline at end of file
......@@ -14,23 +14,27 @@ export const TAG_TEMPLATE = `
<h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2>
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
<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 [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-column style="width: 80px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 180px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 150px;" *ngIf="withClair">{{'VULNERABILITY.SINGULAR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 100px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'architecture'" *ngIf="!withClair">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'os'" *ngIf="!withClair">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow>
<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><a href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a></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-dg-cell style="width: 80px;" [ngSwitch]="withClair">
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a>
<span *ngSwitchDefault>{{t.name}}</span>
</clr-dg-cell>