diff --git a/src/ui_ng/lib/README.md b/src/ui_ng/lib/README.md index b42a7e74c..10ae806ee 100644 --- a/src/ui_ng/lib/README.md +++ b/src/ui_ng/lib/README.md @@ -76,7 +76,7 @@ If **projectId** is set to the id of specified project, then only show the repli ``` -* **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. + +``` + +``` ## 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 | Promise | VulnerabilitySummary{ - ... - } + getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable | Promise | VulnerabilitySummary{ + ... + } /** * Get the detailed vulnerabilities scanning results. @@ -619,9 +638,24 @@ export class MyScanningResultService extends ScanningResultService { * * @memberOf ScanningResultService */ - getVulnerabilityScanningResults(tagId: string): Observable | Promise | VulnerabilityItem[]{ - ... - } + getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable | Promise | VulnerabilityItem[]{ + ... + } + + + /** + * Start a new vulnerability scanning + * + * @abstract + * @param {string} repoName + * @param {string} tagId + * @returns {(Observable | Promise | any)} + * + * @memberOf ScanningResultService + */ + startVulnerabilityScanning(repoName: string, tagId: string): Observable | Promise | any { + ... + } } ... @@ -656,4 +690,4 @@ HarborLibraryModule.forRoot({ }) ... -``` \ No newline at end of file +``` diff --git a/src/ui_ng/lib/package.json b/src/ui_ng/lib/package.json index 92fab7b99..7db338ddf 100644 --- a/src/ui_ng/lib/package.json +++ b/src/ui_ng/lib/package.json @@ -1,6 +1,6 @@ { "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", diff --git a/src/ui_ng/lib/pkg/package.json b/src/ui_ng/lib/pkg/package.json index b83ce244b..3cd233c18 100644 --- a/src/ui_ng/lib/pkg/package.json +++ b/src/ui_ng/lib/pkg/package.json @@ -1,6 +1,6 @@ { "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", diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index 27a645976..d1cdfded4 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -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, diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts index 8ab942517..3d7882e7c 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts @@ -21,7 +21,7 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = ` {{r.name}} {{r.tags_count}} {{r.pull_count}} - + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts index de680f10d..c04d7639b 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts @@ -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, diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts index 8eb626c2d..97f86af63 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts @@ -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 && diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index 82a1404b7..4df7078ae 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -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 { diff --git a/src/ui_ng/lib/src/service/scanning.service.ts b/src/ui_ng/lib/src/service/scanning.service.ts index 12ae0687c..4f073f9ff 100644 --- a/src/ui_ng/lib/src/service/scanning.service.ts +++ b/src/ui_ng/lib/src/service/scanning.service.ts @@ -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 | Promise | VulnerabilitySummary; + abstract getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable | Promise | VulnerabilitySummary; /** * Get the detailed vulnerabilities scanning results. @@ -38,30 +39,60 @@ export abstract class ScanningResultService { * * @memberOf ScanningResultService */ - abstract getVulnerabilityScanningResults(tagId: string): Observable | Promise | VulnerabilityItem[]; + abstract getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable | Promise | VulnerabilityItem[]; + + + /** + * Start a new vulnerability scanning + * + * @abstract + * @param {string} repoName + * @param {string} tagId + * @returns {(Observable | Promise | any)} + * + * @memberOf ScanningResultService + */ + abstract startVulnerabilityScanning(repoName: string, tagId: string): Observable | Promise | 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 | Promise | VulnerabilitySummary { - if (!tagId || tagId.trim() === '') { + getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable | Promise | VulnerabilitySummary { + if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') { return Promise.reject('Bad argument'); } return Observable.of({}); } - getVulnerabilityScanningResults(tagId: string): Observable | Promise | VulnerabilityItem[] { - if (!tagId || tagId.trim() === '') { + getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable | Promise | VulnerabilityItem[] { + if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') { return Promise.reject('Bad argument'); } - return Observable.of([]); + 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 | Promise | any { + if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') { + return Promise.reject('Bad argument'); + } + + 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 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 index 21df9f81a..5980ba87a 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.html.ts @@ -7,10 +7,10 @@ export const TAG_DETAIL_HTML: string = `
- {{tagDetails.name}}:v{{tagDetails.docker_version}} + {{tagDetails.name}}
- {{'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}}
@@ -23,11 +23,13 @@ export const TAG_DETAIL_HTML: string = `
{{'TAG.ARCHITECTURE' | translate }}
{{'TAG.OS' | translate }}
+
{{'TAG.DOCKER_VERSION' | translate }}
{{'TAG.SCAN_COMPLETION_TIME' | translate }}
{{tagDetails.architecture}}
{{tagDetails.os}}
+
{{tagDetails.docker_version}}
{{scanCompletedDatetime | date}}
@@ -67,7 +69,7 @@ export const TAG_DETAIL_HTML: string = `
- +
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 index ecdbab517..44bd18979 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts @@ -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; 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"); }); })); diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.ts b/src/ui_ng/lib/src/tag/tag-detail.component.ts index b579d8eb9..daaf2182f 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.ts @@ -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(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 { diff --git a/src/ui_ng/lib/src/tag/tag.component.css.ts b/src/ui_ng/lib/src/tag/tag.component.css.ts index 8f292e5c2..6157b6cc6 100644 --- a/src/ui_ng/lib/src/tag/tag.component.css.ts +++ b/src/ui_ng/lib/src/tag/tag.component.css.ts @@ -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 diff --git a/src/ui_ng/lib/src/tag/tag.component.html.ts b/src/ui_ng/lib/src/tag/tag.component.html.ts index 1fb6ba5f0..9a17036b5 100644 --- a/src/ui_ng/lib/src/tag/tag.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag.component.html.ts @@ -14,23 +14,27 @@ export const TAG_TEMPLATE = `

{{repoName}}

- {{'REPOSITORY.TAG' | translate}} - {{'REPOSITORY.PULL_COMMAND' | translate}} - {{'REPOSITORY.SIGNED' | translate}} - {{'REPOSITORY.AUTHOR' | translate}} - {{'REPOSITORY.CREATED' | translate}} - {{'REPOSITORY.DOCKER_VERSION' | translate}} - {{'REPOSITORY.ARCHITECTURE' | translate}} - {{'REPOSITORY.OS' | translate}} + {{'REPOSITORY.TAG' | translate}} + {{'REPOSITORY.PULL_COMMAND' | translate}} + {{'REPOSITORY.SIGNED' | translate}} + {{'VULNERABILITY.SINGULAR' | translate}} + {{'REPOSITORY.AUTHOR' | translate}} + {{'REPOSITORY.CREATED' | translate}} + {{'REPOSITORY.DOCKER_VERSION' | translate}} + {{'REPOSITORY.ARCHITECTURE' | translate}} + {{'REPOSITORY.OS' | translate}} {{'TGA.PLACEHOLDER' | translate }} - {{t.name}} - docker pull {{registryUrl}}/{{repoName}}:{{t.name}} - + + {{t.name}} + {{t.name}} + + docker pull {{registryUrl}}/{{repoName}}:{{t.name}} + @@ -38,11 +42,14 @@ export const TAG_TEMPLATE = ` {{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}} - {{t.author}} - {{t.created | date: 'short'}} - {{t.docker_version}} - {{t.architecture}} - {{t.os}} + + + + {{t.author}} + {{t.created | date: 'short'}} + {{t.docker_version}} + {{t.architecture}} + {{t.os}} {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} 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 ce5877a85..0f91f9952 100644 --- a/src/ui_ng/lib/src/tag/tag.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { Router } from '@angular/router'; @@ -10,10 +10,14 @@ import { TagComponent } from './tag.component'; import { ErrorHandler } from '../error-handler/error-handler'; import { Tag } from '../service/interface'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; -import { TagService, TagDefaultService } from '../service/tag.service'; +import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index'; +import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index'; +import { FILTER_DIRECTIVES } from '../filter/index' + +import { Observable, Subscription } from 'rxjs/Rx'; + +describe('TagComponent (inline template)', () => { -describe('TagComponent (inline template)', ()=> { - let comp: TagComponent; let fixture: ComponentFixture; let tagService: TagService; @@ -32,27 +36,30 @@ describe('TagComponent (inline template)', ()=> { ]; let config: IServiceConfig = { - repositoryBaseEndpoint: '/api/repositories/testing' + repositoryBaseEndpoint: '/api/repositories/testing' }; - beforeEach(async(()=>{ + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ SharedModule ], declarations: [ TagComponent, - ConfirmationDialogComponent + ConfirmationDialogComponent, + VULNERABILITY_DIRECTIVES, + FILTER_DIRECTIVES ], providers: [ ErrorHandler, { provide: SERVICE_CONFIG, useValue: config }, - { provide: TagService, useClass: TagDefaultService } + { provide: TagService, useClass: TagDefaultService }, + { provide: ScanningResultService, useClass: ScanningResultDefaultService } ] }); })); - beforeEach(()=>{ + beforeEach(() => { fixture = TestBed.createComponent(TagComponent); comp = fixture.componentInstance; @@ -68,15 +75,15 @@ describe('TagComponent (inline template)', ()=> { fixture.detectChanges(); }); - it('should load data', async(()=>{ + it('should load data', async(() => { expect(spy.calls.any).toBeTruthy(); })); - it('should load and render data', async(()=>{ + it('should load and render data', async(() => { fixture.detectChanges(); - fixture.whenStable().then(()=>{ + fixture.whenStable().then(() => { fixture.detectChanges(); - let de: DebugElement = fixture.debugElement.query(del=>del.classes['datagrid-cell']); + let de: DebugElement = fixture.debugElement.query(del => del.classes['datagrid-cell']); fixture.detectChanges(); expect(de).toBeTruthy(); let el: HTMLElement = de.nativeElement; diff --git a/src/ui_ng/lib/src/tag/tag.component.ts b/src/ui_ng/lib/src/tag/tag.component.ts index 422d0fb24..ca009c603 100644 --- a/src/ui_ng/lib/src/tag/tag.component.ts +++ b/src/ui_ng/lib/src/tag/tag.component.ts @@ -11,7 +11,17 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, ViewChild, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { + Component, + OnInit, + ViewChild, + Input, + Output, + EventEmitter, + ChangeDetectionStrategy, + ChangeDetectorRef, + OnDestroy +} from '@angular/core'; import { TagService } from '../service/tag.service'; @@ -27,19 +37,25 @@ import { Tag, TagClickEvent } from '../service/interface'; import { TAG_TEMPLATE } from './tag.component.html'; import { TAG_STYLE } from './tag.component.css'; -import { toPromise, CustomComparator } from '../utils'; +import { toPromise, CustomComparator, VULNERABILITY_SCAN_STATUS } from '../utils'; import { TranslateService } from '@ngx-translate/core'; import { State, Comparator } from 'clarity-angular'; +import { ScanningResultService } from '../service/index'; + +import { Observable, Subscription } from 'rxjs/Rx'; + +const STATE_CHECK_INTERVAL: number = 2000;//2s + @Component({ selector: 'hbr-tag', template: TAG_TEMPLATE, styles: [TAG_STYLE], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TagComponent implements OnInit { +export class TagComponent implements OnInit, OnDestroy { @Input() projectId: number; @Input() repoName: string; @@ -49,6 +65,7 @@ export class TagComponent implements OnInit { @Input() hasProjectAdminRole: boolean; @Input() registryUrl: string; @Input() withNotary: boolean; + @Input() withClair: boolean; @Output() refreshRepo = new EventEmitter(); @Output() tagClickEvent = new EventEmitter(); @@ -66,6 +83,10 @@ export class TagComponent implements OnInit { loading: boolean = false; + stateCheckTimer: Subscription; + tagsInScanning: { [key: string]: any } = {}; + scanningTagCount: number = 0; + @ViewChild('confirmationDialog') confirmationDialog: ConfirmationDialogComponent; @@ -73,6 +94,7 @@ export class TagComponent implements OnInit { private errorHandler: ErrorHandler, private tagService: TagService, private translateService: TranslateService, + private scanningService: ScanningResultService, private ref: ChangeDetectorRef) { } confirmDeletion(message: ConfirmationAcknowledgement) { @@ -108,11 +130,24 @@ export class TagComponent implements OnInit { } this.retrieve(); + + this.stateCheckTimer = Observable.timer(STATE_CHECK_INTERVAL, STATE_CHECK_INTERVAL).subscribe(() => { + if (this.scanningTagCount > 0) { + this.updateScanningStates(); + } + }); + } + + ngOnDestroy(): void { + if (this.stateCheckTimer) { + this.stateCheckTimer.unsubscribe(); + } } retrieve() { this.tags = []; this.loading = true; + toPromise(this.tagService .getTags(this.repoName)) .then(items => { @@ -177,4 +212,47 @@ export class TagComponent implements OnInit { this.tagClickEvent.emit(evt); } } + + scanTag(tagId: string): void { + //Double check + if (this.tagsInScanning[tagId]) { + return; + } + toPromise(this.scanningService.startVulnerabilityScanning(this.repoName, tagId)) + .then(() => { + //Add to scanning map + this.tagsInScanning[tagId] = tagId; + //Counting + this.scanningTagCount += 1; + }) + .catch(error => this.errorHandler.error(error)); + } + + updateScanningStates(): void { + toPromise(this.tagService + .getTags(this.repoName)) + .then(items => { + console.debug("updateScanningStates called!"); + //Reset the scanning states + this.tagsInScanning = {}; + this.scanningTagCount = 0; + + items.forEach(item => { + if (item.scan_overview) { + if (item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.pending || + item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.running) { + this.tagsInScanning[item.name] = item.name; + this.scanningTagCount += 1; + } + } + }); + + this.tags = items; + }) + .catch(error => { + this.errorHandler.error(error); + }); + let hnd = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 1000); + } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/utils.ts b/src/ui_ng/lib/src/utils.ts index cd7c8eb77..26dc00707 100644 --- a/src/ui_ng/lib/src/utils.ts +++ b/src/ui_ng/lib/src/utils.ts @@ -74,18 +74,18 @@ export function buildHttpRequestOptions(params: RequestQueryParams): RequestOpti /** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */ export const ButtonClickEvents = { - left: { button: 0 }, - right: { button: 2 } + left: { button: 0 }, + right: { button: 2 } }; /** Simulate element click. Defaults to mouse left-button click event. */ export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void { - if (el instanceof HTMLElement) { - el.click(); - } else { - el.triggerEventHandler('click', eventObj); - } + if (el instanceof HTMLElement) { + el.click(); + } else { + el.triggerEventHandler('click', eventObj); + } } /** @@ -94,33 +94,45 @@ export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClic */ export class CustomComparator implements Comparator { - fieldName: string; - type: string; + 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) { - 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; - } + 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) { + 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; } - return comp; - } } /** * The default page size */ -export const DEFAULT_PAGE_SIZE: number = 15; \ No newline at end of file +export const DEFAULT_PAGE_SIZE: number = 15; + +/** + * The state of vulnerability scanning + */ +export const VULNERABILITY_SCAN_STATUS = { + unknown: "n/a", + pending: "pending", + running: "running", + error: "error", + stopped: "stopped", + finished: "finished" +}; \ No newline at end of file diff --git a/src/ui_ng/lib/src/vulnerability-scanning/index.ts b/src/ui_ng/lib/src/vulnerability-scanning/index.ts index 4b500ad7d..7773965dc 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/index.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/index.ts @@ -3,6 +3,7 @@ import { ResultGridComponent } from './result-grid.component'; import { ResultBarChartComponent } from './result-bar-chart.component'; import { ResultTipComponent } from './result-tip.component'; +export * from './result-tip.component'; export * from "./result-grid.component"; export * from './result-bar-chart.component'; 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 e3fbb19ba..e14c859d3 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 @@ -11,6 +11,7 @@ import { ScanningResultService, ScanningResultDefaultService } from '../service/ import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { ErrorHandler } from '../error-handler/index'; import { SharedModule } from '../shared/shared.module'; +import { VULNERABILITY_SCAN_STATUS } from '../utils'; describe('ResultBarChartComponent (inline template)', () => { let component: ResultBarChartComponent; @@ -20,13 +21,25 @@ describe('ResultBarChartComponent (inline template)', () => { 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() + 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 + }] + } }; beforeEach(async(() => { @@ -115,7 +128,7 @@ describe('ResultBarChartComponent (inline template)', () => { let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none'); expect(el).not.toBeNull(); - expect(el.style.width).toEqual("74px"); + expect(el.style.width).toEqual("73px"); }); })); 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 43bd4fec8..52cb72d94 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,11 +2,14 @@ import { Component, Input, Output, - EventEmitter + EventEmitter, + OnInit } from '@angular/core'; import { VulnerabilitySummary } from '../service/index'; import { SCANNING_STYLES } from './scanning.css'; import { BAR_CHART_COMPONENT_HTML } from './scanning.html'; +import { VULNERABILITY_SCAN_STATUS } from '../utils'; +import { VulnerabilitySeverity } from '../service/index'; export enum ScanState { COMPLETED, //Scanning work successfully completed @@ -22,18 +25,47 @@ export enum ScanState { styles: [SCANNING_STYLES], template: BAR_CHART_COMPONENT_HTML }) -export class ResultBarChartComponent { +export class ResultBarChartComponent implements OnInit { @Input() tagId: string = ""; - @Input() state: ScanState = ScanState.UNKNOWN; + @Input() state: ScanState = ScanState.PENDING; @Input() summary: VulnerabilitySummary = { - total_package: 0, - package_with_none: 0, - complete_timestamp: new Date() + scan_status: VULNERABILITY_SCAN_STATUS.unknown, + severity: VulnerabilitySeverity.UNKNOWN, + update_time: new Date(), + components: { + total: 0, + summary: [] + } }; @Output() startScanning: EventEmitter = new EventEmitter(); + scanningInProgress: boolean = false; constructor() { } + ngOnInit(): void { + if (this.summary && this.summary.scan_status) { + switch (this.summary.scan_status) { + case VULNERABILITY_SCAN_STATUS.unknown: + this.state = ScanState.UNKNOWN; + break; + case VULNERABILITY_SCAN_STATUS.error: + this.state = ScanState.ERROR; + break; + case VULNERABILITY_SCAN_STATUS.pending: + this.state = ScanState.QUEUED; + break; + case VULNERABILITY_SCAN_STATUS.stopped: + this.state = ScanState.PENDING; + break; + case VULNERABILITY_SCAN_STATUS.finished: + this.state = ScanState.COMPLETED; + break; + default: + break; + } + } + } + public get completed(): boolean { return this.state === ScanState.COMPLETED; } @@ -60,6 +92,7 @@ export class ResultBarChartComponent { scanNow(): void { if (this.tagId && this.tagId !== '') { + this.scanningInProgress = true; this.startScanning.emit(this.tagId); } } 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 99e4750f5..12d4f24e2 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,8 @@ import { Component, OnInit, Input } from '@angular/core'; import { ScanningResultService, - VulnerabilityItem + VulnerabilityItem, + VulnerabilitySeverity } from '../service/index'; import { ErrorHandler } from '../error-handler/index'; @@ -16,7 +17,10 @@ import { SCANNING_STYLES } from './scanning.css'; }) export class ResultGridComponent implements OnInit { scanningResults: VulnerabilityItem[] = []; + dataCache: VulnerabilityItem[] = []; + @Input() tagId: string; + @Input() repositoryId: string; constructor( private scanningService: ScanningResultService, @@ -24,26 +28,48 @@ export class ResultGridComponent implements OnInit { ) { } ngOnInit(): void { - this.loadResults(this.tagId); + this.loadResults(this.repositoryId, this.tagId); } - showDetail(result: VulnerabilityItem): void { - console.log(result.id); - } - - loadResults(tagId: string): void { - toPromise(this.scanningService.getVulnerabilityScanningResults(tagId)) + loadResults(repositoryId: string, tagId: string): void { + toPromise(this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId)) .then((results: VulnerabilityItem[]) => { - this.scanningResults = results; + this.dataCache = results; + this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== ''); }) .catch(error => { this.errorHandler.error(error) }) } + //TODO: Should query from back-end service filterVulnerabilities(terms: string): void { - console.log(terms); + if (terms.trim() === '') { + this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== ''); + } else { + this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => this._regexpFilter(terms, item.package)); + } } refresh(): void { - this.loadResults(this.tagId); + this.loadResults(this.repositoryId, this.tagId); + } + + severityText(severity: VulnerabilitySeverity): string { + switch (severity) { + case VulnerabilitySeverity.HIGH: + return 'VULNERABILITY.SEVERITY.HIGH'; + case VulnerabilitySeverity.MEDIUM: + return 'VULNERABILITY.SEVERITY.MEDIUM'; + case VulnerabilitySeverity.LOW: + return 'VULNERABILITY.SEVERITY.LOW'; + case VulnerabilitySeverity.UNKNOWN: + return 'VULNERABILITY.SEVERITY.UNKNOWN'; + default: + return 'UNKNOWN'; + } + } + + _regexpFilter(terms: string, testedValue: any): boolean { + let reg = new RegExp('.*' + terms + '.*', 'i'); + return reg.test(testedValue); } } 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 48114db9a..405d1944b 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 @@ -9,6 +9,7 @@ import { ResultTipComponent } from './result-tip.component'; import { SharedModule } from '../shared/shared.module'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; +import { VULNERABILITY_SCAN_STATUS } from '../utils'; describe('ResultTipComponent (inline template)', () => { let component: ResultTipComponent; @@ -16,14 +17,26 @@ 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() + let mockData: VulnerabilitySummary = { + 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 + }] + } }; beforeEach(async(() => { 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 f97078ecc..349b343f3 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 @@ -4,6 +4,7 @@ import { TranslateService } from '@ngx-translate/core'; import { SCANNING_STYLES } from './scanning.css'; import { TIP_COMPONENT_HTML } from './scanning.html'; +import { VULNERABILITY_SCAN_STATUS } from '../utils'; export const MIN_TIP_WIDTH = 5; export const MAX_TIP_WIDTH = 100; @@ -15,24 +16,62 @@ export const MAX_TIP_WIDTH = 100; }) export class ResultTipComponent implements OnInit { _tipTitle: string = ""; + _highCount: number = 0; + _mediumCount: number = 0; + _lowCount: number = 0; + _unknownCount: number = 0; + _noneCount: number = 0; + + totalPackages: number = 0; + packagesWithVul: number = 0; @Input() summary: VulnerabilitySummary = { - total_package: 0, - package_with_none: 0, - complete_timestamp: new Date() + scan_status: VULNERABILITY_SCAN_STATUS.unknown, + severity: VulnerabilitySeverity.UNKNOWN, + update_time: new Date(), + components: { + total: 0, + summary: [] + } }; constructor(private translate: TranslateService) { } ngOnInit(): void { + this.totalPackages = this.summary && this.summary.components ? this.summary.components.total : 0; + if (this.summary && this.summary.components && this.summary.components.summary) { + this.summary.components.summary.forEach(item => { + this.packagesWithVul += item.count + + switch (item.severity) { + case VulnerabilitySeverity.UNKNOWN: + this._unknownCount += item.count; + break; + case VulnerabilitySeverity.NONE: + this._noneCount += 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; + } + }); + } this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE', - { totalVulnerability: this.totalVulnerabilities, totalPackages: this.summary.total_package }) + { totalVulnerability: this.packagesWithVul, totalPackages: this.totalPackages }) .subscribe((res: string) => this._tipTitle = res); } tipWidth(severity: VulnerabilitySeverity): string { let n: number = 0; - let m: number = this.summary ? this.summary.total_package : 0; + let m: number = this.totalPackages; if (m === 0) { return 0 + 'px'; @@ -59,8 +98,8 @@ export class ResultTipComponent implements OnInit { break; } - let width: number = Math.round((n/m)*MAX_TIP_WIDTH); - if(width < MIN_TIP_WIDTH){ + let width: number = Math.round((n / m) * MAX_TIP_WIDTH); + if (width < MIN_TIP_WIDTH) { width = MIN_TIP_WIDTH; } @@ -76,8 +115,8 @@ export class ResultTipComponent implements OnInit { return "VULNERABILITY.SINGULAR"; } - public get totalVulnerabilities(): number { - return this.summary.total_package - this.summary.package_with_none; + public get completeTimestamp(): Date { + return this.summary && this.summary.update_time ? this.summary.update_time : new Date(); } public get hasHigh(): boolean { @@ -105,22 +144,22 @@ export class ResultTipComponent implements OnInit { } public get highCount(): number { - return this.summary && this.summary.package_with_high ? this.summary.package_with_high : 0; + return this._highCount; } public get mediumCount(): number { - return this.summary && this.summary.package_with_medium ? this.summary.package_with_medium : 0; + return this._mediumCount; } public get lowCount(): number { - return this.summary && this.summary.package_With_low ? this.summary.package_With_low : 0; + return this._lowCount; } public get unknownCount(): number { - return this.summary && this.summary.package_with_unknown ? this.summary.package_with_unknown : 0; + return this._unknownCount; } public get noneCount(): number { - return this.summary && this.summary.package_with_none ? this.summary.package_with_none : 0; + return this._noneCount; } public get highSuffix(): string { @@ -144,6 +183,6 @@ export class ResultTipComponent implements OnInit { } public get maxWidth(): string { - return MAX_TIP_WIDTH+"px"; + return (MAX_TIP_WIDTH + 20) + "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 5affed773..76b5f899d 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts @@ -1,6 +1,6 @@ export const SCANNING_STYLES: string = ` .bar-wrapper { - width: 150px; + width: 120px; height: 24px; display: inline-block; } @@ -17,9 +17,9 @@ export const SCANNING_STYLES: string = ` } .tip-wrapper { display: inline-block; - height: 16px; - max-height: 16px; - max-width: 150px; + height: 10px; + max-height: 10px; + max-width: 120px; } .tip-position { margin-left: -4px; 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 f65de9497..a34c46484 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts @@ -1,11 +1,11 @@ export const TIP_COMPONENT_HTML: string = `
-
-
-
-
-
+
+
+
+
+
{{tipTitle}} @@ -34,7 +34,7 @@ export const TIP_COMPONENT_HTML: string = `
{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} - {{summary.complete_timestamp | date}} + {{completeTimestamp | date}}
@@ -58,21 +58,17 @@ export const GRID_COMPONENT_HTML: string = ` {{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}} {{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}} version {{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}} - {{'VULNERABILITY.GRID.COLUMN_LAYER' | translate}} - Description {{'VULNERABILITY.GRID.PLACEHOLDER' | translate}} - - - {{res.id}} - {{res.severity}} + {{severityText(res.severity) | translate}} {{res.package}} {{res.version}} {{res.fixedVersion}} - {{res.layer}} - {{res.description}} + + {{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}} + @@ -87,7 +83,7 @@ export const GRID_COMPONENT_HTML: string = ` export const BAR_CHART_COMPONENT_HTML: string = `
- +
{{'VULNERABILITY.STATE.QUEUED' | translate}} diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index 7db5eb22b..63367ddbf 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -1,72 +1,72 @@ { - "name": "harbor", - "version": "1.1.0", - "description": "Harbor UI with Clarity", - "angular-cli": {}, - "scripts": { - "start": "ng serve --ssl 1 --ssl-key ssl/server.key --ssl-cert ssl/server.crt --host 0.0.0.0 --proxy-config proxy.config.json", - "lint": "tslint \"src/**/*.ts\"", - "test": "ng test --single-run", - "pree2e": "webdriver-manager update", - "e2e": "protractor", - "build": "ngc -p tsconfig-aot.json" - }, - "private": true, - "dependencies": { - "@angular/animations": "^4.1.3", - "@angular/common": "^4.1.3", - "@angular/compiler": "^4.1.3", - "@angular/compiler-cli": "^4.0.2", - "@angular/core": "^4.1.3", - "@angular/forms": "^4.1.3", - "@angular/http": "^4.1.3", - "@angular/platform-browser": "^4.1.3", - "@angular/platform-browser-dynamic": "^4.1.3", - "@angular/router": "^4.1.3", - "@ngx-translate/core": "^6.0.0", - "@ngx-translate/http-loader": "0.0.3", - "@types/jquery": "^2.0.41", - "@webcomponents/custom-elements": "1.0.0-alpha.3", - "clarity-angular": "^0.9.7", - "clarity-icons": "^0.9.7", - "clarity-ui": "^0.9.7", - "core-js": "^2.4.1", - "harbor-ui": "^0.1.85", - "intl": "^1.2.5", - "mutationobserver-shim": "^0.3.2", - "ngx-clipboard": "^8.0.2", - "ngx-cookie": "^1.0.0", - "rxjs": "^5.0.1", - "ts-helpers": "^1.1.1", - "web-animations-js": "^2.2.1", - "zone.js": "^0.8.4" - }, - "devDependencies": { - "@angular/cli": "^1.0.0", - "@angular/compiler-cli": "^4.0.2", - "@types/core-js": "^0.9.34", - "@types/jasmine": "~2.2.30", - "@types/node": "^6.0.42", - "bootstrap": "4.0.0-alpha.5", - "codelyzer": "~2.0.0-beta.4", - "enhanced-resolve": "^3.0.0", - "jasmine-core": "2.4.1", - "jasmine-spec-reporter": "2.5.0", - "karma": "1.2.0", - "karma-cli": "^1.0.1", - "karma-jasmine": "^1.0.2", - "karma-mocha-reporter": "^2.2.1", - "karma-phantomjs-launcher": "^1.0.0", - "karma-remap-istanbul": "^0.2.1", - "protractor": "4.0.9", - "rollup": "^0.41.6", - "rollup-plugin-commonjs": "^8.0.2", - "rollup-plugin-node-resolve": "^3.0.0", - "rollup-plugin-uglify": "^1.0.1", - "ts-node": "1.2.1", - "tslint": "^4.1.1", - "typescript": "~2.2.0", - "typings": "^1.4.0", - "webdriver-manager": "10.2.5" - } + "name": "harbor", + "version": "1.1.0", + "description": "Harbor UI with Clarity", + "angular-cli": {}, + "scripts": { + "start": "ng serve --ssl 1 --ssl-key ssl/server.key --ssl-cert ssl/server.crt --host 0.0.0.0 --proxy-config proxy.config.json", + "lint": "tslint \"src/**/*.ts\"", + "test": "ng test --single-run", + "pree2e": "webdriver-manager update", + "e2e": "protractor", + "build": "ngc -p tsconfig-aot.json" + }, + "private": true, + "dependencies": { + "@angular/animations": "^4.1.3", + "@angular/common": "^4.1.3", + "@angular/compiler": "^4.1.3", + "@angular/compiler-cli": "^4.0.2", + "@angular/core": "^4.1.3", + "@angular/forms": "^4.1.3", + "@angular/http": "^4.1.3", + "@angular/platform-browser": "^4.1.3", + "@angular/platform-browser-dynamic": "^4.1.3", + "@angular/router": "^4.1.3", + "@ngx-translate/core": "^6.0.0", + "@ngx-translate/http-loader": "0.0.3", + "@types/jquery": "^2.0.41", + "@webcomponents/custom-elements": "1.0.0-alpha.3", + "clarity-angular": "^0.9.7", + "clarity-icons": "^0.9.7", + "clarity-ui": "^0.9.7", + "core-js": "^2.4.1", + "harbor-ui": "^0.1.99", + "intl": "^1.2.5", + "mutationobserver-shim": "^0.3.2", + "ngx-clipboard": "^8.0.2", + "ngx-cookie": "^1.0.0", + "rxjs": "^5.0.1", + "ts-helpers": "^1.1.1", + "web-animations-js": "^2.2.1", + "zone.js": "^0.8.4" + }, + "devDependencies": { + "@angular/cli": "^1.0.0", + "@angular/compiler-cli": "^4.0.2", + "@types/core-js": "^0.9.34", + "@types/jasmine": "~2.2.30", + "@types/node": "^6.0.42", + "bootstrap": "4.0.0-alpha.5", + "codelyzer": "~2.0.0-beta.4", + "enhanced-resolve": "^3.0.0", + "jasmine-core": "2.4.1", + "jasmine-spec-reporter": "2.5.0", + "karma": "1.2.0", + "karma-cli": "^1.0.1", + "karma-jasmine": "^1.0.2", + "karma-mocha-reporter": "^2.2.1", + "karma-phantomjs-launcher": "^1.0.0", + "karma-remap-istanbul": "^0.2.1", + "protractor": "4.0.9", + "rollup": "^0.41.6", + "rollup-plugin-commonjs": "^8.0.2", + "rollup-plugin-node-resolve": "^3.0.0", + "rollup-plugin-uglify": "^1.0.1", + "ts-node": "1.2.1", + "tslint": "^4.1.1", + "typescript": "~2.2.0", + "typings": "^1.4.0", + "webdriver-manager": "10.2.5" + } } diff --git a/src/ui_ng/src/i18n/lang/en-us-lang.json b/src/ui_ng/src/i18n/lang/en-us-lang.json index 9e0953ae9..fd9f9f277 100644 --- a/src/ui_ng/src/i18n/lang/en-us-lang.json +++ b/src/ui_ng/src/i18n/lang/en-us-lang.json @@ -459,7 +459,7 @@ "COLUMN_PACKAGE": "Package", "COLUMN_VERSION": "Current version", "COLUMN_FIXED": "Fixed in version", - "COLUMN_LAYER": "Introduced in layer", + "COLUMN_DESCRIPTION": "Description", "FOOT_ITEMS": "Items", "FOOT_OF": "of" }, @@ -488,7 +488,9 @@ "TAG": { "CREATION_TIME_PREFIX": "Create on", "CREATOR_PREFIX": "by", + "ANONYMITY": "anonymity", "IMAGE_DETAILS": "Image Details", + "DOCKER_VERSION": "Docker Version", "ARCHITECTURE": "Architecture", "OS": "OS", "SCAN_COMPLETION_TIME": "Scan Completed", diff --git a/src/ui_ng/src/i18n/lang/es-es-lang.json b/src/ui_ng/src/i18n/lang/es-es-lang.json index 5887d4769..db2c6f1d9 100644 --- a/src/ui_ng/src/i18n/lang/es-es-lang.json +++ b/src/ui_ng/src/i18n/lang/es-es-lang.json @@ -458,7 +458,7 @@ "COLUMN_PACKAGE": "Package", "COLUMN_VERSION": "Current version", "COLUMN_FIXED": "Fixed in version", - "COLUMN_LAYER": "Introduced in layer", + "COLUMN_DESCRIPTION": "Description", "FOOT_ITEMS": "Items", "FOOT_OF": "of" }, @@ -487,7 +487,9 @@ "TAG": { "CREATION_TIME_PREFIX": "Create on", "CREATOR_PREFIX": "by", + "ANONYMITY": "anonymity", "IMAGE_DETAILS": "Image Details", + "DOCKER_VERSION": "Docker Version", "ARCHITECTURE": "Architecture", "OS": "OS", "SCAN_COMPLETION_TIME": "Scan Completed", diff --git a/src/ui_ng/src/i18n/lang/zh-cn-lang.json b/src/ui_ng/src/i18n/lang/zh-cn-lang.json index ef3551f72..3c229e9ef 100644 --- a/src/ui_ng/src/i18n/lang/zh-cn-lang.json +++ b/src/ui_ng/src/i18n/lang/zh-cn-lang.json @@ -459,7 +459,7 @@ "COLUMN_PACKAGE": "组件", "COLUMN_VERSION": "当前版本", "COLUMN_FIXED": "修复版本", - "COLUMN_LAYER": "引入层", + "COLUMN_DESCRIPTION": "简介", "FOOT_ITEMS": "项目", "FOOT_OF": "总共" }, @@ -488,7 +488,9 @@ "TAG": { "CREATION_TIME_PREFIX": "创建时间:", "CREATOR_PREFIX": "创建者:", + "ANONYMITY": "匿名用户", "IMAGE_DETAILS": "镜像详情", + "DOCKER_VERSION": "Docker版本", "ARCHITECTURE": "架构", "OS": "操作系统", "SCAN_COMPLETION_TIME": "扫描完成时间",