Commit 2072fc23 authored by Steven Zou's avatar Steven Zou
Browse files

Implement tag detail component & refactor vul summary bar chart

parent 4d2a2363
......@@ -45,6 +45,7 @@ export interface Tag extends Base {
author: string;
created: Date;
signature?: string;
vulnerability?: VulnerabilitySummary;
}
/**
......@@ -157,28 +158,28 @@ export interface SystemInfo {
//Not finalized yet
export enum VulnerabilitySeverity {
LOW, MEDIUM, HIGH, UNKNOWN, NONE
NONE, UNKNOWN, LOW, MEDIUM, HIGH
}
export interface ScanningBaseResult {
export interface VulnerabilityBase {
id: string;
severity: VulnerabilitySeverity;
package: string;
version: string;
}
export interface ScanningDetailResult extends ScanningBaseResult {
export interface VulnerabilityItem extends VulnerabilityBase {
fixedVersion: string;
layer: string;
description: string;
}
export interface ScanningResultSummary {
totalComponents: number;
noneComponents: number;
completeTimestamp: Date;
high: ScanningBaseResult[];
medium: ScanningBaseResult[];
low: ScanningBaseResult[];
unknown: ScanningBaseResult[];
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;
}
\ No newline at end of file
......@@ -5,8 +5,10 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { Http, URLSearchParams } from '@angular/http';
import { HTTP_JSON_OPTIONS } from '../utils';
import { ScanningDetailResult } from './interface';
import { VulnerabilitySeverity, ScanningBaseResult, ScanningResultSummary } from './interface';
import {
VulnerabilityItem,
VulnerabilitySummary
} from './interface';
/**
* Get the vulnerabilities scanning results for the specified tag.
......@@ -21,22 +23,22 @@ export abstract class ScanningResultService {
*
* @abstract
* @param {string} tagId
* @returns {(Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary)}
* @returns {(Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary)}
*
* @memberOf ScanningResultService
*/
abstract getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary;
abstract getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
/**
* Get the detailed vulnerabilities scanning results.
*
* @abstract
* @param {string} tagId
* @returns {(Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[])}
* @returns {(Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[])}
*
* @memberOf ScanningResultService
*/
abstract getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[];
abstract getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
}
@Injectable()
......@@ -47,7 +49,7 @@ export class ScanningResultDefaultService extends ScanningResultService {
super();
}
getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary {
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
if (!tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}
......@@ -55,7 +57,7 @@ export class ScanningResultDefaultService extends ScanningResultService {
return Observable.of({});
}
getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[] {
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
if (!tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}
......
......@@ -52,7 +52,19 @@ export abstract class TagService {
*
* @memberOf TagService
*/
abstract deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<Tag> | any;
abstract deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<any> | any;
/**
* Get the specified tag.
*
* @abstract
* @param {string} repositoryName
* @param {string} tag
* @returns {(Observable<Tag> | Promise<Tag> | Tag)}
*
* @memberOf TagService
*/
abstract getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable<Tag> | Promise<Tag> | Tag;
}
/**
......@@ -113,4 +125,15 @@ export class TagDefaultService extends TagService {
.then(response => response)
.catch(error => Promise.reject(error));
}
public getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable<Tag> | Promise<Tag> | Tag {
if (!repositoryName || !tag) {
return Promise.reject("Bad argument");
}
let url: string = `${this._baseUrl}/${repositoryName}/tags/${tag}`;
return this.http.get(url, HTTP_JSON_OPTIONS).toPromise()
.then(response => response.json() as Tag)
.catch(error => Promise.reject(error));
}
}
\ No newline at end of file
import { Type } from '@angular/core';
import { TagComponent } from './tag.component';
import { TagDetailComponent } from './tag-detail.component';
export * from './tag.component';
export * from './tag-detail.component';
export const TAG_DIRECTIVES: Type<any>[] = [
TagComponent
TagComponent,
TagDetailComponent
];
\ No newline at end of file
export const TAG_DETAIL_STYLES: string = `
.overview-section {
background-color: white;
padding-bottom: 36px;
border-bottom: 1px solid #cccccc;
}
.detail-section {
background-color: #fafafa;
padding-left: 12px;
padding-right: 24px;
}
.title-block {
display: inline-block;
}
.title-wrapper {
padding-top: 12px;
}
.tag-name {
font-weight: 300;
font-size: 32px;
}
.tag-timestamp {
font-weight: 400;
font-size: 12px;
margin-top: 6px;
}
.rotate-90 {
-webkit-transform: rotate(-90deg);
/*Firefox*/
-moz-transform: rotate(-90deg);
/*Chrome*/
-ms-transform: rotate(-90deg);
/*IE9 、IE10*/
-o-transform: rotate(-90deg);
/*Opera*/
transform: rotate(-90deg);
}
.arrow-back {
cursor: pointer;
}
.arrow-block {
border-right: 2px solid #cccccc;
margin-right: 6px;
display: inline-flex;
padding: 6px 6px 6px 12px;
}
.vulnerability-block {
margin-bottom: 12px;
}
.summary-block {
margin-top: 24px;
display: inline-flex;
flex-wrap: row wrap;
}
.image-summary {
margin-right: 36px;
margin-left: 18px;
}
.flex-block {
display: inline-flex;
flex-wrap: row wrap;
justify-content: space-around;
}
.vulnerabilities-info {
padding-left: 24px;
}
.vulnerabilities-info .third-column {
margin-left: 36px;
}
.vulnerabilities-info .second-column,
.vulnerabilities-info .fourth-column {
text-align: left;
margin-left: 6px;
}
.vulnerabilities-info .second-row {
margin-top: 6px;
}
.detail-title {
font-weight: 500;
font-size: 14px;
}
.image-detail-label {
text-align: right;
}
.image-detail-value {
text-align: left;
margin-left: 6px;
font-weight: 500;
}
`;
\ No newline at end of file
export const TAG_DETAIL_HTML: string = `
<div>
<section class="overview-section">
<div class="title-wrapper">
<div class="title-block arrow-block">
<clr-icon class="rotate-90 arrow-back" shape="arrow" size="36" (click)="onBack()"></clr-icon>
</div>
<div class="title-block">
<div class="tag-name">
{{tagDetails.name}}:v{{tagDetails.docker_version}}
</div>
<div class="tag-timestamp">
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{tagDetails.author}}
</div>
</div>
</div>
<div class="summary-block">
<div class="image-summary">
<div class="detail-title">
{{'TAG.IMAGE_DETAILS' | translate }}
</div>
<div class="flex-block">
<div class="image-detail-label">
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
<div>{{'TAG.OS' | translate }}</div>
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
</div>
<div class="image-detail-value">
<div>{{tagDetails.architecture}}</div>
<div>{{tagDetails.os}}</div>
<div>{{scanCompletedDatetime | date}}</div>
</div>
</div>
</div>
<div>
<div class="detail-title">
{{'TAG.IMAGE_VULNERABILITIES' | translate }}
</div>
<div class="flex-block vulnerabilities-info">
<div>
<div>
<clr-icon shape="error" size="24" class="is-error"></clr-icon>
</div>
<div class="second-row">
<clr-icon shape="exclamation-triangle" size="24" class="is-warning"></clr-icon>
</div>
</div>
<div class="second-column">
<div>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}</div>
<div class="second-row">{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}</div>
</div>
<div class="third-column">
<div>
<clr-icon shape="play" size="20" class="is-warning rotate-90"></clr-icon>
</div>
<div class="second-row">
<clr-icon shape="help" size="20"></clr-icon>
</div>
</div>
<div class="fourth-column">
<div>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}</div>
<div class="second-row">{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}</div>
</div>
</div>
</div>
</div>
</section>
<section class="detail-section">
<div class="vulnerability-block">
<hbr-vulnerabilities-grid tagId="tagId"></hbr-vulnerabilities-grid>
</div>
<div>
<ng-content></ng-content>
</div>
</section>
</div>
`;
\ No newline at end of file
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { SharedModule } from '../shared/shared.module';
import { ResultGridComponent } from '../vulnerability-scanning/result-grid.component';
import { TagDetailComponent } from './tag-detail.component';
import { ErrorHandler } from '../error-handler/error-handler';
import { Tag, VulnerabilitySummary } from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
describe('TagDetailComponent (inline template)', () => {
let comp: TagDetailComponent;
let fixture: ComponentFixture<TagDetailComponent>;
let tagService: TagService;
let spy: 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()
};
let mockTag: Tag = {
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "nginx",
"architecture": "amd64",
"os": "linux",
"docker_version": "1.12.3",
"author": "steven",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null,
vulnerability: mockVulnerability
};
let config: IServiceConfig = {
repositoryBaseEndpoint: '/api/repositories/testing'
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
declarations: [
TagDetailComponent,
ResultGridComponent
],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(TagDetailComponent);
comp = fixture.componentInstance;
comp.tagId = "mock_tag";
comp.repositoryId = "mock_repo";
tagService = fixture.debugElement.injector.get(TagService);
spy = spyOn(tagService, 'getTag').and.returnValues(Promise.resolve(mockTag));
fixture.detectChanges();
});
it('should load data', async(() => {
expect(spy.calls.any).toBeTruthy();
}));
it('should rightly display tag name and version', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('nginx:v1.12.3');
});
}));
it('should display tag details', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.image-detail-value');
expect(el).toBeTruthy();
let el2: HTMLElement = el.querySelector('div');
expect(el2).toBeTruthy();
expect(el2.textContent).toEqual("amd64");
});
}));
it('should display vulnerability details', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.second-column');
expect(el).toBeTruthy();
let el2: HTMLElement = el.querySelector('div');
expect(el2).toBeTruthy();
expect(el2.textContent.trim()).toEqual("10 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
});
}));
});
\ No newline at end of file
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 { toPromise } from '../utils';
import { ErrorHandler } from '../error-handler/index';
@Component({
selector: 'hbr-tag-detail',
styles: [TAG_DETAIL_STYLES],
template: TAG_DETAIL_HTML,
providers: []
})
export class TagDetailComponent implements OnInit {
@Input() tagId: string;
@Input() repositoryId: string;
tagDetails: Tag = {
name: "--",
author: "--",
created: new Date(),
architecture: "--",
os: "--",
docker_version: "--",
digest: "--"
};
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
constructor(
private tagService: TagService,
private errorHandler: ErrorHandler) { }
ngOnInit(): void {
if (this.repositoryId && this.tagId) {
toPromise<Tag>(this.tagService.getTag(this.repositoryId, this.tagId))
.then(response => this.tagDetails = response)
.catch(error => this.errorHandler.error(error))
}
}
onBack(): void {
this.backEvt.emit(this.tagId);
}
public get highCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.package_with_high : 0;
}
public get mediumCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.package_with_medium : 0;
}
public get lowCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.package_With_low : 0;
}
public get unknownCount(): number {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.package_with_unknown : 0;
}
public get scanCompletedDatetime(): Date {
return this.tagDetails && this.tagDetails.vulnerability ?
this.tagDetails.vulnerability.complete_timestamp : new Date();
}
public get suffixForHigh(): string {
return this.highCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
}
public get suffixForMedium(): string {
return this.mediumCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
}
public get suffixForLow(): string {
return this.lowCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
}
public get suffixForUnknown(): string {
return this.unknownCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
}
}
......@@ -86,7 +86,7 @@ describe('TagComponent (inline template)', ()=> {
fixture.detectChanges();
});
it('Should load data', async(()=>{
it('should load data', async(()=>{
expect(spy.calls.any).toBeTruthy();
}));
......
......@@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { DebugElement } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ScanningResultSummary, VulnerabilitySeverity, ScanningBaseResult } from '../service/index';
import { VulnerabilitySummary } from '../service/index';
import { ResultBarChartComponent, ScanState } from './result-bar-chart.component';
import { ResultTipComponent } from './result-tip.component';
......@@ -16,11 +16,18 @@ describe('ResultBarChartComponent (inline template)', () => {
let component: ResultBarChartComponent;
let fixture: ComponentFixture<ResultBarChartComponent>;
let serviceConfig: IServiceConfig;
let scanningService: ScanningResultService;
let spy: jasmine.Spy;
let testConfig: IServiceConfig = {
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
};
let mockData: 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()
};
beforeEach(async(() => {
TestBed.configureTestingModule({
......@@ -32,8 +39,7 @@ describe('ResultBarChartComponent (inline template)', () => {