Commit 7cec6c7b authored by sshijun's avatar sshijun
Browse files

Improve scanner UI


Signed-off-by: default avatarsshijun <sshijun@vmware.com>
parent fc106e21
import { ComponentFixture, TestBed, async, } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement} from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
......@@ -25,7 +24,13 @@ import { ChannelService } from '../channel/index';
import { LabelPieceComponent } from "../label-piece/label-piece.component";
import { LabelDefaultService, LabelService } from "../service/label.service";
import { OperationService } from "../operation/operation.service";
import { ProjectDefaultService, ProjectService, RetagDefaultService, RetagService } from "../service";
import {
ProjectDefaultService,
ProjectService,
RetagDefaultService,
RetagService, ScanningResultDefaultService,
ScanningResultService
} from "../service";
import { UserPermissionDefaultService, UserPermissionService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { of } from "rxjs";
......@@ -158,6 +163,11 @@ describe('RepositoryComponent (inline template)', () => {
let mockHasRetagImagePermission: boolean = true;
let mockHasDeleteImagePermission: boolean = true;
let mockHasScanImagePermission: boolean = true;
let fakedScanningResultService = {
getProjectScanner() {
return of({});
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
......@@ -188,7 +198,8 @@ describe('RepositoryComponent (inline template)', () => {
{ provide: LabelService, useClass: LabelDefaultService},
{ provide: UserPermissionService, useClass: UserPermissionDefaultService},
{ provide: ChannelService},
{ provide: OperationService }
{ provide: OperationService },
{ provide: ScanningResultService, useValue: fakedScanningResultService }
]
});
}));
......
......@@ -315,6 +315,7 @@ export interface VulnerabilitySummary {
}
export interface SeveritySummary {
total: number;
fixable: number;
summary: {[key: string]: number};
}
......
......@@ -73,6 +73,19 @@ export abstract class ScanningResultService {
* @memberOf ScanningResultService
*/
abstract startScanningAll(): Observable<any>;
/**
* Get scanner metadata
* @param uuid
* @memberOf ScanningResultService
*/
abstract getScannerMetadata(uuid: string): Observable<any>;
/**
* Get project scanner
* @param projectId
*/
abstract getProjectScanner(projectId: number): Observable<any>;
}
@Injectable()
......@@ -153,4 +166,14 @@ export class ScanningResultDefaultService extends ScanningResultService {
})
, catchError(error => observableThrowError(error)));
}
getScannerMetadata(uuid: string): Observable<any> {
return this.http.get(`/api/scanners/${uuid}/metadata`)
.pipe(map(response => response as any))
.pipe(catchError(error => observableThrowError(error)));
}
getProjectScanner(projectId: number): Observable<any> {
return this.http.get(`/api/projects/${projectId}/scanner`)
.pipe(map(response => response as any))
.pipe(catchError(error => observableThrowError(error)));
}
}
......@@ -39,11 +39,11 @@
</section>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="vulnerability" [hidden]="hasCve">
<div class="col-md-4 col-sm-6 margin-top-5px">
<div class="vulnerability" [hidden]="hasCve || showStatBar">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="vulnerabilitySummary"></hbr-vulnerability-bar>
</div>
<histogram-chart *ngIf="hasCve" class="margin-top-5px" [metadata]="passMetadataToChart()" [isWhiteBackground]="true"></histogram-chart>
<histogram-chart *ngIf="hasCve" [metadata]="passMetadataToChart()" [isWhiteBackground]="true"></histogram-chart>
</div>
<div *ngIf="!withAdmiral && tagDetails?.labels?.length">
<div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div>
......
......@@ -48,6 +48,7 @@ describe("TagDetailComponent (inline template)", () => {
end_time: new Date(),
summary: {
total: 124,
fixable: 50,
summary: {
"High": 5,
"Low": 5
......
......@@ -56,6 +56,7 @@ export class TagDetailComponent implements OnInit {
hasVulnerabilitiesListPermission: boolean;
hasBuildHistoryPermission: boolean;
@Input() projectId: number;
showStatBar: boolean = true;
constructor(
private tagService: TagService,
public channel: ChannelService,
......@@ -83,6 +84,7 @@ export class TagDetailComponent implements OnInit {
&& tagDetails.scan_overview
&& tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]) {
this.vulnerabilitySummary = tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE];
this.showStatBar = false;
}
}
onBack(): void {
......
......@@ -27,7 +27,12 @@ import { catchError, debounceTime, distinctUntilChanged, finalize, map } from 'r
import { TranslateService } from "@ngx-translate/core";
import { Comparator, Label, State, Tag, TagClickEvent } from "../service/interface";
import { RequestQueryParams, RetagService, TagService, VulnerabilitySeverity } from "../service/index";
import {
RequestQueryParams,
RetagService,
ScanningResultService,
TagService,
} from "../service/index";
import { ErrorHandler } from "../error-handler/error-handler";
import { ChannelService } from "../channel/index";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../shared/shared.const";
......@@ -54,7 +59,6 @@ import { operateChanges, OperateInfo, OperationState } from "../operation/operat
import { OperationService } from "../operation/operation.service";
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
import { errorHandler as errorHandFn } from "../shared/shared.utils";
import { HttpClient } from "@angular/common/http";
import { ClrLoadingState } from "@clr/angular";
export interface LabelState {
......@@ -160,7 +164,7 @@ export class TagComponent implements OnInit, AfterViewInit {
private ref: ChangeDetectorRef,
private operationService: OperationService,
private channel: ChannelService,
private http: HttpClient
private scanningService: ScanningResultService
) { }
ngOnInit() {
......@@ -759,19 +763,27 @@ export class TagComponent implements OnInit, AfterViewInit {
getProjectScanner(): void {
this.hasEnabledScanner = false;
this.scanBtnState = ClrLoadingState.LOADING;
this.http.get(`/api/projects/${this.projectId}/scanner`)
.pipe(map(response => response as any))
.pipe(catchError(error => observableThrowError(error)))
this.scanningService.getProjectScanner(this.projectId)
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disable
&& response.health) {
this.hasEnabledScanner = true;
if (response && "{}" !== JSON.stringify(response) && !response.disabled
&& response.uuid) {
this.getScannerMetadata(response.uuid);
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
this.scanBtnState = ClrLoadingState.SUCCESS;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
getScannerMetadata(uuid: string) {
this.scanningService.getScannerMetadata(uuid)
.subscribe(response => {
this.hasEnabledScanner = true;
this.scanBtnState = ClrLoadingState.SUCCESS;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
handleScanOverview(scanOverview: any) {
if (scanOverview) {
return scanOverview[DEFAULT_SUPPORTED_MIME_TYPE];
......
......@@ -127,6 +127,6 @@ export class HistogramChartComponent implements OnInit, AfterViewInit, DoCheck {
});
}
this.max = count;
this.scale = Math.ceil(count / 4);
this.scale = Math.ceil(count / 40) * 10;
}
}
......@@ -32,6 +32,7 @@ describe('ResultBarChartComponent (inline template)', () => {
end_time: new Date(),
summary: {
total: 124,
fixable: 50,
summary: {
"High": 5,
"Low": 5
......
......@@ -10,7 +10,7 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!hasScanImagePermission" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [clrLoading]="scanBtnState" [disabled]="!hasScanImagePermission || !hasEnabledScanner" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
......
......@@ -10,7 +10,8 @@ import { ChannelService } from "../channel/channel.service";
import { UserPermissionService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from '../utils';
import { finalize } from "rxjs/operators";
import { finalize, map } from "rxjs/operators";
import { ClrLoadingState } from "@clr/angular";
@Component({
......@@ -22,10 +23,13 @@ export class ResultGridComponent implements OnInit {
scanningResults: VulnerabilityItem[] = [];
dataCache: VulnerabilityItem[] = [];
loading: boolean = false;
shouldShowLoading: boolean = true;
@Input() tagId: string;
@Input() repositoryId: string;
@Input() projectId: number;
hasScanImagePermission: boolean;
hasEnabledScanner: boolean = false;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
constructor(
private scanningService: ScanningResultService,
private channel: ChannelService,
......@@ -39,11 +43,41 @@ export class ResultGridComponent implements OnInit {
this.channel.tagDetail$.subscribe(tag => {
this.loadResults(this.repositoryId, this.tagId);
});
if (this.projectId) {
this.getProjectScanner();
}
}
getProjectScanner(): void {
this.hasEnabledScanner = false;
this.scanBtnState = ClrLoadingState.LOADING;
this.scanningService.getProjectScanner(this.projectId)
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disabled
&& response.uuid) {
this.getScannerMetadata(response.uuid);
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
getScannerMetadata(uuid: string) {
this.scanningService.getScannerMetadata(uuid)
.subscribe(response => {
this.hasEnabledScanner = true;
this.scanBtnState = ClrLoadingState.SUCCESS;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
loadResults(repositoryId: string, tagId: string): void {
this.loading = true;
// only show loading for one time
if (this.shouldShowLoading) {
this.loading = true;
this.shouldShowLoading = false;
}
this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId)
.pipe(finalize(() => this.loading = false))
.subscribe((results) => {
......
<div class="tip-wrapper tip-position width-210">
<clr-tooltip>
<div clrTooltipTrigger class="tip-block">
<ng-container *ngIf="!isNone">
<div *ngIf="criticalCount > 0" class="tip-wrapper bar-block-critical shadow-critical width-30">{{criticalCount}}</div>
<div *ngIf="highCount > 0" class="margin-left-5 tip-wrapper bar-block-high shadow-high width-30">{{highCount}}</div>
<div *ngIf="mediumCount > 0" class="margin-left-5 tip-wrapper bar-block-medium shadow-medium width-30">{{mediumCount}}</div>
<div *ngIf="lowCount > 0" class="margin-left-5 tip-wrapper bar-block-low shadow-low width-30">{{lowCount}}</div>
<div *ngIf="negligibleCount > 0" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-30">{{negligibleCount}}</div>
<div *ngIf="unknownCount > 0" class="margin-left-5 tip-wrapper bar-block-unknown shadow-unknown width-30">{{unknownCount}}</div>
</ng-container>
<div *ngIf="!isNone" class="circle-block">
<div class="level-border" [className]="getClass()">{{vulnerabilitySummary?.severity | slice:0:1}}</div>
<div class="black-point margin-left-5"></div>
<span class="margin-left-5">{{total}}</span>
<span class="margin-left-5">{{'SCANNER.TOTAL' | translate}}</span>
<div class="black-point margin-left-10"></div>
<span class="margin-left-5">{{fixableCount}}</span>
<span class="margin-left-5">{{'SCANNER.FIXABLE' | translate}}</span>
</div>
<div *ngIf="isNone" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-150">{{'VULNERABILITY.NO_VULNERABILITY' | translate }}</div>
</div>
<clr-tooltip-content class="w-800" [clrPosition]="'right'" [clrSize]="'lg'" *clrIfOpen>
......@@ -46,6 +47,10 @@
<div class="bar-summary bar-tooltip-fon" *ngIf="!isNone">
<histogram-chart [isWhiteBackground]="false" [metadata]="passMetadataToChart()"></histogram-chart>
</div>
<div>
<span class="bar-scanning-time">{{'SCANNER.DURATION' | translate }}</span>
<span class="margin-left-5">{{duration()}}</span>
</div>
<div>
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
<span>{{completeTimestamp | date:'short'}}</span>
......
......@@ -208,6 +208,9 @@ hr {
.margin-left-5 {
margin-left: 5px;
}
.margin-left-10 {
margin-left: 10px;
}
.width-30 {
width: 30px;
......@@ -220,3 +223,53 @@ hr {
.width-150 {
width: 150px;
}
.circle-block {
color: #575757;
display: flex;
align-items: center;
div:first-child {
display: inline-block;
border-radius: 50%;
height: 20px;
width: 20px;
line-height: 20px;
font-size: 14px;
text-align: center;
}
}
.level-border {
border:1px solid #f8b5b4;
}
.level-critical {
background:red;
color:#621501;
}
.level-high {
background:#e64524;
color:#621501;
}
.level-medium {
background-color: orange;
color:#621501;
}
.level-low {
background: #007CBB;
color:#cab6b1;
}
.level-negligible {
background-color: green;
color:#bad7ba;
}
.level-unknown {
background-color: grey;
color:#bad7ba;
}
.black-point {
display: inline-block;
width: 4px;background-color: #000;
height: 4px;
border-radius: 50%;
}
......@@ -3,6 +3,18 @@ import { VulnerabilitySummary } from "../../service";
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../utils";
import { TranslateService } from "@ngx-translate/core";
const MIN = 60;
const MIN_STR = "min ";
const SEC_STR = "sec";
const CLASS_MAP = {
"CRITICAL": "level-critical",
"HIGH": "level-high",
"MEDIUM": "level-medium",
"LOW": "level-low",
"NEGLIGIBLE": "level-negligible",
"UNKNOWN": "level-unknown"
};
@Component({
selector: 'hbr-result-tip-histogram',
templateUrl: './result-tip-histogram.component.html',
......@@ -52,7 +64,13 @@ export class ResultTipHistogramComponent implements OnInit {
this.vulnerabilitySummary.summary) {
return this.vulnerabilitySummary.summary.total;
}
return 0;
}
get fixableCount() {
if (this.vulnerabilitySummary &&
this.vulnerabilitySummary.summary && this.vulnerabilitySummary.summary.fixable) {
return this.vulnerabilitySummary.summary.fixable;
}
return 0;
}
......@@ -72,7 +90,21 @@ export class ResultTipHistogramComponent implements OnInit {
return 0;
}
duration(): string {
if (this.vulnerabilitySummary && this.vulnerabilitySummary.duration) {
let str = '';
const min = Math.floor(this.vulnerabilitySummary.duration / MIN);
if (min) {
str += min + MIN_STR;
}
const sec = this.vulnerabilitySummary.duration % MIN;
if (sec) {
str += sec + SEC_STR;
}
return str;
}
return '0';
}
get highCount(): number {
if (this.sevSummary) {
return this.sevSummary[VULNERABILITY_SEVERITY.HIGH];
......@@ -139,6 +171,29 @@ export class ResultTipHistogramComponent implements OnInit {
return this.total === 0;
}
getClass(): string {
if (this.vulnerabilitySummary && this.vulnerabilitySummary.severity) {
if (this.isCritical) {
return CLASS_MAP.CRITICAL;
}
if (this.isHigh) {
return CLASS_MAP.HIGH;
}
if (this.isMedium) {
return CLASS_MAP.MEDIUM;
}
if (this.isLow) {
return CLASS_MAP.LOW;
}
if (this.isNegligible) {
return CLASS_MAP.NEGLIGIBLE;
}
if (this.isUnknown) {
return CLASS_MAP.UNKNOWN;
}
}
return null;
}
passMetadataToChart() {
return [
{
......
......@@ -20,6 +20,7 @@ describe('ResultTipComponent (inline template)', () => {
end_time: new Date(),
summary: {
total: 124,
fixable: 50,
summary: {
"High": 5,
"Low": 5
......
......@@ -65,9 +65,12 @@
</clr-dg-cell>
<clr-dg-cell>{{scanner.url}}</clr-dg-cell>
<clr-dg-cell>
<span *ngIf="scanner.health;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
<ng-template #elseBlock>
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
<span *ngIf="scanner.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2"></span>
<ng-template #elseBlockLoading>
<span *ngIf="scanner.metadata;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
<ng-template #elseBlock>
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
</ng-template>
</ng-template>
</clr-dg-cell>
<clr-dg-cell>{{!scanner.disabled}}</clr-dg-cell>
......
......@@ -57,10 +57,27 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
.pipe(finalize(() => this.onGoing = false))
.subscribe(response => {
this.scanners = response;
this.getMetadataForAll();
}, error => {
this.errorHandler.error(error);
});
}
getMetadataForAll() {
if (this.scanners && this.scanners.length > 0) {
this.scanners.forEach((scanner, index) => {
if (scanner.uuid ) {
this.scanners[index].loadingMetadata = true;
this.configScannerService.getScannerMetadata(scanner.uuid)
.pipe(finalize(() => this.scanners[index].loadingMetadata = false))
.subscribe(response => {
this.scanners[index].metadata = response;
}, error => {
this.scanners[index].metadata = null;
});
}
});
}
}
addNewScanner(): void {
this.newScannerDialog.open();
......
......@@ -11,7 +11,7 @@
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
</div>
<clr-control-error *ngIf="!isNameValid">
{{nameTooltip | translate}}
<span id="name-error">{{nameTooltip | translate}}</span>
</clr-control-error>
</div>
</div>
......@@ -33,7 +33,7 @@
<span class="spinner spinner-inline" [hidden]="!checkEndpointOnGoing"></span>
</div>
<clr-control-error *ngIf="!isEndpointValid || showEndpointError">
{{endpointTooltip | translate}}
<span id="endpoint-error">{{endpointTooltip | translate}}</span>
</clr-control-error>
</div>
</div>
......@@ -73,7 +73,7 @@
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="!isPasswordValid">
{{"SCANNER.PASSWORD_REQUIRED" | translate}}
<span id="pwd-error">{{"SCANNER.PASSWORD_REQUIRED" | translate}}</span>
</clr-control-error>
</div>
</div>
......
......@@ -65,9 +65,9 @@ describe('NewScannerFormComponent', () => {
nameInput.blur();
nameInput.dispatchEvent(new Event('blur'));
setTimeout(() => {
let el = fixture.nativeElement.querySelector('clr-control-error');
let el = fixture.nativeElement.querySelector('#name-error');
expect(el).toBeFalsy();
}, 900);
}, 11000);
});
it('endpoint url should be valid', () => {
......@@ -79,9 +79,9 @@ describe('NewScannerFormComponent', () => {
urlInput.blur();
urlInput.dispatchEvent(new Event('blur'));
setTimeout(() => {
let el = fixture.nativeElement.querySelector('