From 2072fc237e1fa8722afe29570130e6499d6746a4 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Mon, 12 Jun 2017 19:40:51 +0800 Subject: [PATCH] Implement tag detail component & refactor vul summary bar chart --- src/ui_ng/lib/src/service/interface.ts | 23 +- src/ui_ng/lib/src/service/scanning.service.ts | 18 +- src/ui_ng/lib/src/service/tag.service.ts | 25 ++- src/ui_ng/lib/src/tag/index.ts | 6 +- .../lib/src/tag/tag-detail.component.css.ts | 109 ++++++++++ .../lib/src/tag/tag-detail.component.html.ts | 77 +++++++ .../lib/src/tag/tag-detail.component.spec.ts | 118 ++++++++++ src/ui_ng/lib/src/tag/tag-detail.component.ts | 88 ++++++++ src/ui_ng/lib/src/tag/tag.component.spec.ts | 2 +- .../result-bar-chart.component.spec.ts | 120 +++++------ .../result-bar-chart.component.ts | 103 +-------- .../result-grid.component.spec.ts | 8 +- .../result-grid.component.ts | 12 +- .../result-tip.component.spec.ts | 27 ++- .../result-tip.component.ts | 204 +++++++++--------- .../vulnerability-scanning/scanning.css.ts | 17 +- .../vulnerability-scanning/scanning.html.ts | 50 +++-- 17 files changed, 692 insertions(+), 315 deletions(-) create mode 100644 src/ui_ng/lib/src/tag/tag-detail.component.css.ts create mode 100644 src/ui_ng/lib/src/tag/tag-detail.component.html.ts create mode 100644 src/ui_ng/lib/src/tag/tag-detail.component.spec.ts create mode 100644 src/ui_ng/lib/src/tag/tag-detail.component.ts diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index 9e1ac030e9..fe8c1e5f45 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -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 diff --git a/src/ui_ng/lib/src/service/scanning.service.ts b/src/ui_ng/lib/src/service/scanning.service.ts index 38c053dd01..12ae0687c4 100644 --- a/src/ui_ng/lib/src/service/scanning.service.ts +++ b/src/ui_ng/lib/src/service/scanning.service.ts @@ -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 | Promise | ScanningResultSummary)} + * @returns {(Observable | Promise | VulnerabilitySummary)} * * @memberOf ScanningResultService */ - abstract getScanningResultSummary(tagId: string): Observable | Promise | ScanningResultSummary; + abstract getVulnerabilityScanningSummary(tagId: string): Observable | Promise | VulnerabilitySummary; /** * Get the detailed vulnerabilities scanning results. * * @abstract * @param {string} tagId - * @returns {(Observable | Promise | ScanningDetailResult[])} + * @returns {(Observable | Promise | VulnerabilityItem[])} * * @memberOf ScanningResultService */ - abstract getScanningResults(tagId: string): Observable | Promise | ScanningDetailResult[]; + abstract getVulnerabilityScanningResults(tagId: string): Observable | Promise | VulnerabilityItem[]; } @Injectable() @@ -47,7 +49,7 @@ export class ScanningResultDefaultService extends ScanningResultService { super(); } - getScanningResultSummary(tagId: string): Observable | Promise | ScanningResultSummary { + getVulnerabilityScanningSummary(tagId: string): Observable | Promise | 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 | Promise | ScanningDetailResult[] { + getVulnerabilityScanningResults(tagId: string): Observable | Promise | VulnerabilityItem[] { if (!tagId || tagId.trim() === '') { return Promise.reject('Bad argument'); } diff --git a/src/ui_ng/lib/src/service/tag.service.ts b/src/ui_ng/lib/src/service/tag.service.ts index dbce14f864..6242bcfc79 100644 --- a/src/ui_ng/lib/src/service/tag.service.ts +++ b/src/ui_ng/lib/src/service/tag.service.ts @@ -52,7 +52,19 @@ export abstract class TagService { * * @memberOf TagService */ - abstract deleteTag(repositoryName: string, tag: string): Observable | Promise | any; + abstract deleteTag(repositoryName: string, tag: string): Observable | Promise | any; + + /** + * Get the specified tag. + * + * @abstract + * @param {string} repositoryName + * @param {string} tag + * @returns {(Observable | Promise | Tag)} + * + * @memberOf TagService + */ + abstract getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable | Promise | 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 | Promise | 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 diff --git a/src/ui_ng/lib/src/tag/index.ts b/src/ui_ng/lib/src/tag/index.ts index 8faf3020b9..0154044270 100644 --- a/src/ui_ng/lib/src/tag/index.ts +++ b/src/ui_ng/lib/src/tag/index.ts @@ -1,7 +1,11 @@ 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[] = [ - TagComponent + TagComponent, + TagDetailComponent ]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.css.ts b/src/ui_ng/lib/src/tag/tag-detail.component.css.ts new file mode 100644 index 0000000000..32bf839ba5 --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag-detail.component.css.ts @@ -0,0 +1,109 @@ +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 diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.html.ts b/src/ui_ng/lib/src/tag/tag-detail.component.html.ts new file mode 100644 index 0000000000..21df9f81a8 --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag-detail.component.html.ts @@ -0,0 +1,77 @@ +export const TAG_DETAIL_HTML: string = ` +
+
+
+
+ +
+
+
+ {{tagDetails.name}}:v{{tagDetails.docker_version}} +
+
+ {{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{tagDetails.author}} +
+
+
+
+
+
+ {{'TAG.IMAGE_DETAILS' | translate }} +
+
+
+
{{'TAG.ARCHITECTURE' | translate }}
+
{{'TAG.OS' | translate }}
+
{{'TAG.SCAN_COMPLETION_TIME' | translate }}
+
+
+
{{tagDetails.architecture}}
+
{{tagDetails.os}}
+
{{scanCompletedDatetime | date}}
+
+
+
+
+
+ {{'TAG.IMAGE_VULNERABILITIES' | translate }} +
+
+
+
+ +
+
+ +
+
+
+
{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}
+
{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}
+
+
+
+ +
+
+ +
+
+
+
{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}
+
{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts new file mode 100644 index 0000000000..039415b550 --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts @@ -0,0 +1,118 @@ +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; + 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 diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.ts b/src/ui_ng/lib/src/tag/tag-detail.component.ts new file mode 100644 index 0000000000..b579d8eb9d --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag-detail.component.ts @@ -0,0 +1,88 @@ +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 = new EventEmitter(); + + constructor( + private tagService: TagService, + private errorHandler: ErrorHandler) { } + + ngOnInit(): void { + if (this.repositoryId && this.tagId) { + toPromise(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"; + } +} diff --git a/src/ui_ng/lib/src/tag/tag.component.spec.ts b/src/ui_ng/lib/src/tag/tag.component.spec.ts index 6685d1a2db..8f7f7a0520 100644 --- a/src/ui_ng/lib/src/tag/tag.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag.component.spec.ts @@ -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(); })); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts index 37d9476230..e3fbb19ba0 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts @@ -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; 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)', () => { ResultTipComponent], providers: [ ErrorHandler, - { provide: SERVICE_CONFIG, useValue: testConfig }, - { provide: ScanningResultService, useClass: ScanningResultDefaultService } + { provide: SERVICE_CONFIG, useValue: testConfig } ] }); @@ -43,52 +49,9 @@ describe('ResultBarChartComponent (inline template)', () => { fixture = TestBed.createComponent(ResultBarChartComponent); component = fixture.componentInstance; component.tagId = "mockTag"; - component.state = ScanState.COMPLETED; + component.state = ScanState.UNKNOWN; serviceConfig = TestBed.get(SERVICE_CONFIG); - scanningService = fixture.debugElement.injector.get(ScanningResultService); - let mockData: ScanningResultSummary = { - totalComponents: 21, - noneComponents: 7, - completeTimestamp: new Date(), - high: [], - medium: [], - low: [], - unknown: [] - }; - - for (let i = 0; i < 14; i++) { - let res: ScanningBaseResult = { - id: "CVE-2016-" + (8859 + i), - package: "package_" + i, - version: '4.' + i + ".0", - severity: VulnerabilitySeverity.UNKNOWN - }; - - switch (i % 4) { - case 0: - res.severity = VulnerabilitySeverity.HIGH; - mockData.high.push(res); - break; - case 1: - res.severity = VulnerabilitySeverity.MEDIUM; - mockData.medium.push(res); - break; - case 2: - res.severity = VulnerabilitySeverity.LOW; - mockData.low.push(res); - break; - case 3: - res.severity = VulnerabilitySeverity.UNKNOWN; - mockData.unknown.push(res); - break; - default: - break; - } - } - - spy = spyOn(scanningService, 'getScanningResultSummary') - .and.returnValue(Promise.resolve(mockData)); fixture.detectChanges(); }); @@ -102,22 +65,57 @@ describe('ResultBarChartComponent (inline template)', () => { expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing"); }); - it('should inject and call the ScanningResultService', () => { - expect(scanningService).toBeTruthy(); - expect(spy.calls.any()).toBe(true, 'getScanningResultSummary called'); - }); - - it('should get data from ScanningResultService', async(() => { + it('should show a button if status is PENDING', async(() => { + component.state = ScanState.PENDING; fixture.detectChanges(); fixture.whenStable().then(() => { // wait for async getRecentLogs fixture.detectChanges(); - expect(component.summary).toBeTruthy(); - expect(component.summary.totalComponents).toEqual(21); - expect(component.summary.high.length).toEqual(4); - expect(component.summary.medium.length).toEqual(4); - expect(component.summary.low.length).toEqual(3); - expect(component.summary.noneComponents).toEqual(7); + + let el: HTMLElement = fixture.nativeElement.querySelector('.scanning-button'); + expect(el).toBeTruthy(); + }); + })); + + it('should show progress if status is SCANNING', async(() => { + component.state = ScanState.SCANNING; + fixture.detectChanges(); + + fixture.whenStable().then(() => { // wait for async getRecentLogs + fixture.detectChanges(); + + let el: HTMLElement = fixture.nativeElement.querySelector('.progress'); + expect(el).toBeTruthy(); + }); + })); + + it('should show QUEUED if status is QUEUED', async(() => { + component.state = ScanState.QUEUED; + fixture.detectChanges(); + + fixture.whenStable().then(() => { // wait for async getRecentLogs + fixture.detectChanges(); + + let el: HTMLElement = fixture.nativeElement.querySelector('.bar-state'); + expect(el).toBeTruthy(); + let el2: HTMLElement = el.querySelector('span'); + expect(el2).toBeTruthy(); + expect(el2.textContent).toEqual('VULNERABILITY.STATE.QUEUED'); + + }); + })); + + it('should show summary bar chart if status is COMPLETED', async(() => { + component.state = ScanState.COMPLETED; + component.summary = mockData; + fixture.detectChanges(); + + fixture.whenStable().then(() => { // wait for async getRecentLogs + fixture.detectChanges(); + + let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none'); + expect(el).not.toBeNull(); + expect(el.style.width).toEqual("74px"); }); })); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts index 34b67c3bcd..43bd4fec80 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts @@ -2,16 +2,9 @@ import { Component, Input, Output, - EventEmitter, - OnInit + EventEmitter } from '@angular/core'; -import { - ScanningResultService, - ScanningResultSummary -} from '../service/index'; -import { ErrorHandler } from '../error-handler/index'; -import { toPromise } from '../utils'; -import { MAX_TIP_WIDTH } from './result-tip.component'; +import { VulnerabilitySummary } from '../service/index'; import { SCANNING_STYLES } from './scanning.css'; import { BAR_CHART_COMPONENT_HTML } from './scanning.html'; @@ -25,37 +18,21 @@ export enum ScanState { } @Component({ - selector: 'hbr-scan-result-bar', + selector: 'hbr-vulnerability-bar', styles: [SCANNING_STYLES], template: BAR_CHART_COMPONENT_HTML }) -export class ResultBarChartComponent implements OnInit { +export class ResultBarChartComponent { @Input() tagId: string = ""; @Input() state: ScanState = ScanState.UNKNOWN; - @Input() summary: ScanningResultSummary = { - totalComponents: 0, - noneComponents: 0, - completeTimestamp: new Date(), - high: [], - medium: [], - low: [], - unknown: [] + @Input() summary: VulnerabilitySummary = { + total_package: 0, + package_with_none: 0, + complete_timestamp: new Date() }; @Output() startScanning: EventEmitter = new EventEmitter(); - constructor( - private scanningService: ScanningResultService, - private errorHandler: ErrorHandler) { } - - ngOnInit(): void { - toPromise(this.scanningService.getScanningResultSummary(this.tagId)) - .then((summary: ScanningResultSummary) => { - this.summary = summary; - }) - .catch(error => { - this.errorHandler.error(error); - }) - } + constructor() { } public get completed(): boolean { return this.state === ScanState.COMPLETED; @@ -86,66 +63,4 @@ export class ResultBarChartComponent implements OnInit { this.startScanning.emit(this.tagId); } } - - public get hasHigh(): boolean { - return this.summary && this.summary.high && this.summary.high.length > 0; - } - - public get hasMedium(): boolean { - return this.summary && this.summary.medium && this.summary.medium.length > 0; - } - - public get hasLow(): boolean { - return this.summary && this.summary.low && this.summary.low.length > 0; - } - - public get hasUnknown(): boolean { - return this.summary && this.summary.unknown && this.summary.unknown.length > 0; - } - - public get hasNone(): boolean { - return this.summary && this.summary.noneComponents > 0; - } - - /** - * Calculate the percent width of each severity. - * - * @param {string} flag - * 'h': high - * 'm': medium - * 'l': low - * 'u': unknown - * 'n': none - * @returns {number} - * - * @memberOf ResultBarChartComponent - */ - percent(flag: string): number { - if (!this.summary || this.summary.totalComponents === 0) { - return 0; - } - - let numerator: number = 0; - switch (flag) { - case 'h': - numerator = this.summary.high.length; - break; - case 'm': - numerator = this.summary.medium.length; - break; - case 'l': - numerator = this.summary.low.length; - break; - case 'u': - numerator = this.summary.unknown.length; - break; - default: - numerator = this.summary.noneComponents; - break; - } - - return Math.round((numerator / this.summary.totalComponents) * MAX_TIP_WIDTH); - } - - } diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts index d0578f70d3..1213c62adc 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts @@ -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 { ScanningDetailResult, VulnerabilitySeverity, RequestQueryParams } from '../service/index'; +import { VulnerabilityItem, VulnerabilitySeverity, RequestQueryParams } from '../service/index'; import { ResultGridComponent } from './result-grid.component'; import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service'; @@ -43,9 +43,9 @@ describe('ResultGridComponent (inline template)', () => { serviceConfig = TestBed.get(SERVICE_CONFIG); scanningService = fixture.debugElement.injector.get(ScanningResultService); - let mockData: ScanningDetailResult[] = []; + let mockData: VulnerabilityItem[] = []; for (let i = 0; i < 30; i++) { - let res: ScanningDetailResult = { + let res: VulnerabilityItem = { id: "CVE-2016-" + (8859 + i), severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM, package: "package_" + i, @@ -57,7 +57,7 @@ describe('ResultGridComponent (inline template)', () => { mockData.push(res); } - spy = spyOn(scanningService, 'getScanningResults') + spy = spyOn(scanningService, 'getVulnerabilityScanningResults') .and.returnValue(Promise.resolve(mockData)); fixture.detectChanges(); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts index 8356d7eb00..a3d340b628 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, Input } from '@angular/core'; import { ScanningResultService, - ScanningDetailResult + VulnerabilityItem } from '../service/index'; import { ErrorHandler } from '../error-handler/index'; @@ -10,12 +10,12 @@ import { GRID_COMPONENT_HTML } from './scanning.html'; import { SCANNING_STYLES } from './scanning.css'; @Component({ - selector: 'hbr-scan-result-grid', + selector: 'hbr-vulnerabilities-grid', styles: [SCANNING_STYLES], template: GRID_COMPONENT_HTML }) export class ResultGridComponent implements OnInit { - scanningResults: ScanningDetailResult[] = []; + scanningResults: VulnerabilityItem[] = []; @Input() tagId: string; constructor( @@ -27,13 +27,13 @@ export class ResultGridComponent implements OnInit { this.loadResults(this.tagId); } - showDetail(result: ScanningDetailResult): void { + showDetail(result: VulnerabilityItem): void { console.log(result.id); } loadResults(tagId: string): void { - toPromise(this.scanningService.getScanningResults(tagId)) - .then((results: ScanningDetailResult[]) => { + toPromise(this.scanningService.getVulnerabilityScanningResults(tagId)) + .then((results: VulnerabilityItem[]) => { this.scanningResults = results; }) .catch(error => { this.errorHandler.error(error) }) diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts index 824037ff6d..48114db9ae 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts @@ -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 { ScanningDetailResult, VulnerabilitySeverity } from '../service/index'; +import { VulnerabilitySummary } from '../service/index'; import { ResultTipComponent } from './result-tip.component'; import { SharedModule } from '../shared/shared.module'; @@ -16,6 +16,15 @@ describe('ResultTipComponent (inline template)', () => { let testConfig: IServiceConfig = { vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing" }; + let mockData:VulnerabilitySummary = { + total_package: 124, + package_with_none: 90, + package_with_high: 13, + package_with_medium: 10, + package_With_low: 10, + package_with_unknown: 1, + complete_timestamp: new Date() + }; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -31,14 +40,26 @@ describe('ResultTipComponent (inline template)', () => { beforeEach(() => { fixture = TestBed.createComponent(ResultTipComponent); component = fixture.componentInstance; - component.percent = 50; + component.summary = mockData; fixture.detectChanges(); }); it('should be created', () => { expect(component).toBeTruthy(); - expect(component.severity).toEqual(VulnerabilitySeverity.UNKNOWN); }); + it('should reader the bar with different width', async(() => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none'); + expect(el).not.toBeNull(); + expect(el.style.width).toEqual("73px"); + let el2: HTMLElement = fixture.nativeElement.querySelector('.bar-block-high'); + expect(el2).not.toBeNull(); + expect(el2.style.width).toEqual("10px"); + }); + })); + }); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts index cf35ae8365..f97078ecc8 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts @@ -1,9 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; +import { VulnerabilitySummary, VulnerabilitySeverity } from '../service/index'; import { TranslateService } from '@ngx-translate/core'; -import { - ScanningBaseResult, - VulnerabilitySeverity -} from '../service/index'; + import { SCANNING_STYLES } from './scanning.css'; import { TIP_COMPONENT_HTML } from './scanning.html'; @@ -11,127 +9,141 @@ export const MIN_TIP_WIDTH = 5; export const MAX_TIP_WIDTH = 100; @Component({ - selector: 'hbr-scan-result-tip', + selector: 'hbr-vulnerability-summary-chart', template: TIP_COMPONENT_HTML, styles: [SCANNING_STYLES] }) export class ResultTipComponent implements OnInit { - _percent: number = 5; - _tipTitle: string = ''; + _tipTitle: string = ""; - @Input() severity: VulnerabilitySeverity = VulnerabilitySeverity.UNKNOWN; - @Input() completeDateTime: Date = new Date(); //Temp - @Input() data: ScanningBaseResult[] = []; - @Input() noneNumber: number = 0; - @Input() - public get percent(): number { - return this._percent; - } + @Input() summary: VulnerabilitySummary = { + total_package: 0, + package_with_none: 0, + complete_timestamp: new Date() + }; - public set percent(percent: number) { - this._percent = percent; - if (this._percent < MIN_TIP_WIDTH) { - this._percent = MIN_TIP_WIDTH; - } - - if (this._percent > MAX_TIP_WIDTH) { - this._percent = MAX_TIP_WIDTH; - } - } - - _getSeverityKey(): string { - switch (this.severity) { - case VulnerabilitySeverity.HIGH: - return 'VULNERABILITY.CHART.SEVERITY_HIGH'; - case VulnerabilitySeverity.MEDIUM: - return 'VULNERABILITY.CHART.SEVERITY_MEDIUM'; - case VulnerabilitySeverity.LOW: - return 'VULNERABILITY.CHART.SEVERITY_LOW'; - case VulnerabilitySeverity.NONE: - return 'VULNERABILITY.CHART.SEVERITY_NONE'; - default: - return 'VULNERABILITY.CHART.SEVERITY_UNKNOWN'; - } - } - - constructor(private translateService: TranslateService) { } + constructor(private translate: TranslateService) { } ngOnInit(): void { - this.translateService.get(this._getSeverityKey()) + this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE', + { totalVulnerability: this.totalVulnerabilities, totalPackages: this.summary.total_package }) .subscribe((res: string) => this._tipTitle = res); } + tipWidth(severity: VulnerabilitySeverity): string { + let n: number = 0; + let m: number = this.summary ? this.summary.total_package : 0; + + if (m === 0) { + return 0 + 'px'; + } + + switch (severity) { + case VulnerabilitySeverity.HIGH: + n = this.highCount; + break; + case VulnerabilitySeverity.MEDIUM: + n = this.mediumCount; + break; + case VulnerabilitySeverity.LOW: + n = this.lowCount; + break; + case VulnerabilitySeverity.UNKNOWN: + n = this.unknownCount; + break; + case VulnerabilitySeverity.NONE: + n = this.noneCount; + break; + default: + n = 0; + break; + } + + let width: number = Math.round((n/m)*MAX_TIP_WIDTH); + if(width < MIN_TIP_WIDTH){ + width = MIN_TIP_WIDTH; + } + + return width + 'px'; + } + + + unitText(count: number): string { + if (count > 1) { + return "VULNERABILITY.PLURAL"; + } + + return "VULNERABILITY.SINGULAR"; + } + + public get totalVulnerabilities(): number { + return this.summary.total_package - this.summary.package_with_none; + } + + public get hasHigh(): boolean { + return this.highCount > 0; + } + + public get hasMedium(): boolean { + return this.mediumCount > 0; + } + + public get hasLow(): boolean { + return this.lowCount > 0; + } + + public get hasUnknown(): boolean { + return this.unknownCount > 0; + } + + public get hasNone(): boolean { + return this.noneCount > 0; + } + public get tipTitle(): string { - if (!this.data) { - return ''; - } - - let dataSize: number = this.data.length; - return this._tipTitle + ' (' + dataSize + ')'; + return this._tipTitle; } - public get hasResultsToList(): boolean { - return this.data && - this.data.length > 0 && ( - this.severity !== VulnerabilitySeverity.NONE && - this.severity !== VulnerabilitySeverity.UNKNOWN - ); + public get highCount(): number { + return this.summary && this.summary.package_with_high ? this.summary.package_with_high : 0; } - public get tipWidth(): string { - return this.percent + 'px'; + public get mediumCount(): number { + return this.summary && this.summary.package_with_medium ? this.summary.package_with_medium : 0; } - public get tipClass(): string { - let baseClass: string = "tip-wrapper tip-block"; - - switch (this.severity) { - case VulnerabilitySeverity.HIGH: - return baseClass + " bar-block-high"; - case VulnerabilitySeverity.MEDIUM: - return baseClass + " bar-block-medium"; - case VulnerabilitySeverity.LOW: - return baseClass + " bar-block-low"; - case VulnerabilitySeverity.NONE: - return baseClass + " bar-block-none"; - default: - return baseClass + " bar-block-unknown" - } - + public get lowCount(): number { + return this.summary && this.summary.package_With_low ? this.summary.package_With_low : 0; } - public get isHigh(): boolean { - return this.severity === VulnerabilitySeverity.HIGH; + public get unknownCount(): number { + return this.summary && this.summary.package_with_unknown ? this.summary.package_with_unknown : 0; + } + public get noneCount(): number { + return this.summary && this.summary.package_with_none ? this.summary.package_with_none : 0; } - public get isMedium(): boolean { - return this.severity === VulnerabilitySeverity.MEDIUM; + public get highSuffix(): string { + return this.unitText(this.highCount); } - public get isLow(): boolean { - return this.severity === VulnerabilitySeverity.LOW; + public get mediumSuffix(): string { + return this.unitText(this.mediumCount); } - public get isNone(): boolean { - return this.severity === VulnerabilitySeverity.NONE; + public get lowSuffix(): string { + return this.unitText(this.lowCount); } - public get isUnknown(): boolean { - return this.severity === VulnerabilitySeverity.UNKNOWN; + public get unknownSuffix(): string { + return this.unitText(this.unknownCount); } - public get tipIconClass(): string { - switch (this.severity) { - case VulnerabilitySeverity.HIGH: - return "is-error"; - case VulnerabilitySeverity.MEDIUM: - return "is-warning"; - case VulnerabilitySeverity.LOW: - return "is-info"; - case VulnerabilitySeverity.NONE: - return "is-success"; - default: - return "is-highlight" - } + public get noneSuffix(): string { + return this.unitText(this.noneCount); + } + + public get maxWidth(): string { + return MAX_TIP_WIDTH+"px"; } } diff --git a/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts b/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts index a16c9ba939..c10a11f7a3 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts @@ -55,7 +55,7 @@ export const SCANNING_STYLES: string = ` .bar-tooltip-font { font-size: 13px; - color: #565656; + color: #ffffff; } .bar-tooltip-font-title { @@ -63,19 +63,16 @@ export const SCANNING_STYLES: string = ` } .bar-summary { - margin-top: 5px; + margin-top: 12px; + text-align: left; } .bar-scanning-time { - margin-left: 26px; + margin-top: 12px; } -.bar-summary ul { - margin-left: 24px; -} - -.bar-summary ul li { - list-style-type: none; - margin: 2px; +.bar-summary-item { + margin-top: 3px; + margin-bottom: 3px; } `; \ No newline at end of file diff --git a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts index 95fdbfc244..a7ee640d6b 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts @@ -1,24 +1,40 @@ export const TIP_COMPONENT_HTML: string = ` -
+
-
+
+
+
+
+
- - - - - {{tipTitle}}
-
- {{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} - {{completeDateTime | date}} -
-
    -
  • {{item.id}} {{item.version}} {{item.package}}
  • -
+
+
+ + {{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{ highSuffix | translate }}
+
+ + {{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{ mediumSuffix | translate }} +
+
+ + {{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{ lowSuffix | translate }} +
+
+ + {{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{ unknownSuffix | translate }} +
+
+ + {{noneCount}} {{'VULNERABILITY.SEVERITY.NONE' | translate }} {{ noneSuffix | translate }} +
+
+
+ {{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} + {{summary.complete_timestamp | date}}
@@ -75,11 +91,7 @@ export const BAR_CHART_COMPONENT_HTML: string = `
- - - - - +