Provide 'Scan Now' menu in the tag list (#2819)

This commit is contained in:
Steven Zou 2017-07-20 09:28:00 +08:00 committed by Yan
parent 5c8be3502c
commit aa681eb018
14 changed files with 272 additions and 164 deletions

View File

@ -0,0 +1,28 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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 { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from "rxjs/Observable";
@Injectable()
export class ChannelService {
//Declare for publishing scan event
scanCommandSource = new Subject<string>();
scanCommand$ = this.scanCommandSource.asObservable();
publishScanEvent(tagId: string): void {
this.scanCommandSource.next(tagId);
}
}

View File

@ -0,0 +1 @@
export * from './channel.service';

View File

@ -52,6 +52,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { TranslateServiceInitializer } from './i18n/index'; import { TranslateServiceInitializer } from './i18n/index';
import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from './utils'; import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from './utils';
import { ChannelService } from './channel/index';
/** /**
* Declare default service configuration; all the endpoints will be defined in * Declare default service configuration; all the endpoints will be defined in
@ -203,7 +204,8 @@ export class HarborLibraryModule {
useFactory: initConfig, useFactory: initConfig,
deps: [TranslateServiceInitializer, SERVICE_CONFIG], deps: [TranslateServiceInitializer, SERVICE_CONFIG],
multi: true multi: true
} },
ChannelService
] ]
}; };
} }
@ -221,7 +223,8 @@ export class HarborLibraryModule {
config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService }, config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService },
config.tagService || { provide: TagService, useClass: TagDefaultService }, config.tagService || { provide: TagService, useClass: TagDefaultService },
config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService }, config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService },
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService } config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
ChannelService
] ]
}; };
} }

View File

@ -16,4 +16,5 @@ export * from './i18n/index';
export * from './push-image/index'; export * from './push-image/index';
export * from './third-party/index'; export * from './third-party/index';
export * from './config/index'; export * from './config/index';
export * from './job-log-viewer/index'; export * from './job-log-viewer/index';
export * from './channel/index';

View File

@ -27,8 +27,9 @@ export const TAG_TEMPLATE = `
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder> <clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'> <clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow> <clr-dg-action-overflow>
<button class="action-item" *ngIf="canScanNow(t)" (click)="scanNow(t.name)">{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button class="action-item" *ngIf="hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button> <button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow> </clr-dg-action-overflow>
<clr-dg-cell style="width: 80px;" [ngSwitch]="withClair"> <clr-dg-cell style="width: 80px;" [ngSwitch]="withClair">
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a> <a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a>
@ -36,7 +37,7 @@ export const TAG_TEMPLATE = `
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell style="min-width: 180px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell> <clr-dg-cell style="min-width: 180px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
<clr-dg-cell style="width: 160px;" *ngIf="withClair"> <clr-dg-cell style="width: 160px;" *ngIf="withClair">
<hbr-vulnerability-bar [tagId]="t.name" [summary]="t.scan_overview" (startScanning)="scanTag($event)"></hbr-vulnerability-bar> <hbr-vulnerability-bar [repoName]="repoName" [tagId]="t.name" [summary]="t.scan_overview"></hbr-vulnerability-bar>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="withNotary" [ngSwitch]="t.signature !== null"> <clr-dg-cell style="width: 80px;" *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon> <clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon>

View File

@ -15,6 +15,7 @@ import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
import { FILTER_DIRECTIVES } from '../filter/index' import { FILTER_DIRECTIVES } from '../filter/index'
import { Observable, Subscription } from 'rxjs/Rx'; import { Observable, Subscription } from 'rxjs/Rx';
import { ChannelService } from '../channel/index';
describe('TagComponent (inline template)', () => { describe('TagComponent (inline template)', () => {
@ -52,6 +53,7 @@ describe('TagComponent (inline template)', () => {
], ],
providers: [ providers: [
ErrorHandler, ErrorHandler,
ChannelService,
{ provide: SERVICE_CONFIG, useValue: config }, { provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService }, { provide: TagService, useClass: TagDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService } { provide: ScanningResultService, useClass: ScanningResultDefaultService }

View File

@ -20,14 +20,17 @@ import {
EventEmitter, EventEmitter,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
ElementRef, ElementRef
OnDestroy
} from '@angular/core'; } from '@angular/core';
import { TagService } from '../service/tag.service'; import { TagService } from '../service/tag.service';
import { ErrorHandler } from '../error-handler/error-handler'; import { ErrorHandler } from '../error-handler/error-handler';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../shared/shared.const'; import { ChannelService } from '../channel/index';
import {
ConfirmationTargets,
ConfirmationState,
ConfirmationButtons
} from '../shared/shared.const';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
@ -38,25 +41,23 @@ import { Tag, TagClickEvent } from '../service/interface';
import { TAG_TEMPLATE } from './tag.component.html'; import { TAG_TEMPLATE } from './tag.component.html';
import { TAG_STYLE } from './tag.component.css'; import { TAG_STYLE } from './tag.component.css';
import { toPromise, CustomComparator, VULNERABILITY_SCAN_STATUS } from '../utils'; import {
toPromise,
CustomComparator,
VULNERABILITY_SCAN_STATUS
} from '../utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { State, Comparator } from 'clarity-angular'; 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({ @Component({
selector: 'hbr-tag', selector: 'hbr-tag',
template: TAG_TEMPLATE, template: TAG_TEMPLATE,
styles: [TAG_STYLE], styles: [TAG_STYLE],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TagComponent implements OnInit, OnDestroy { export class TagComponent implements OnInit {
@Input() projectId: number; @Input() projectId: number;
@Input() repoName: string; @Input() repoName: string;
@ -83,11 +84,6 @@ export class TagComponent implements OnInit, OnDestroy {
createdComparator: Comparator<Tag> = new CustomComparator<Tag>('created', 'date'); createdComparator: Comparator<Tag> = new CustomComparator<Tag>('created', 'date');
loading: boolean = false; loading: boolean = false;
stateCheckTimer: Subscription;
tagsInScanning: { [key: string]: any } = {};
scanningTagCount: number = 0;
copyFailed: boolean = false; copyFailed: boolean = false;
@ViewChild('confirmationDialog') @ViewChild('confirmationDialog')
@ -99,8 +95,9 @@ export class TagComponent implements OnInit, OnDestroy {
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private tagService: TagService, private tagService: TagService,
private translateService: TranslateService, private translateService: TranslateService,
private scanningService: ScanningResultService, private ref: ChangeDetectorRef,
private ref: ChangeDetectorRef) { } private channel: ChannelService
) { }
confirmDeletion(message: ConfirmationAcknowledgement) { confirmDeletion(message: ConfirmationAcknowledgement) {
if (message && if (message &&
@ -135,18 +132,6 @@ export class TagComponent implements OnInit, OnDestroy {
} }
this.retrieve(); 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() { retrieve() {
@ -203,7 +188,7 @@ export class TagComponent implements OnInit, OnDestroy {
this.copyFailed = false; this.copyFailed = false;
} }
} }
onTagClick(tag: Tag): void { onTagClick(tag: Tag): void {
if (tag) { if (tag) {
let evt: TagClickEvent = { let evt: TagClickEvent = {
@ -215,49 +200,6 @@ export class TagComponent implements OnInit, OnDestroy {
} }
} }
scanTag(tagId: string): void {
//Double check
if (this.tagsInScanning[tagId]) {
return;
}
toPromise<any>(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<Tag[]>(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);
}
onSuccess($event: any): void { onSuccess($event: any): void {
this.copyFailed = false; this.copyFailed = false;
//Directly close dialog //Directly close dialog
@ -268,8 +210,33 @@ export class TagComponent implements OnInit, OnDestroy {
//Show error //Show error
this.copyFailed = true; this.copyFailed = true;
//Select all text //Select all text
if(this.textInput){ if (this.textInput) {
this.textInput.nativeElement.select(); this.textInput.nativeElement.select();
} }
} }
//Get vulnerability scanning status
scanStatus(t: Tag): string {
if (t && t.scan_overview && t.scan_overview.scan_status) {
return t.scan_overview.scan_status;
}
return VULNERABILITY_SCAN_STATUS.unknown;
}
//Whether show the 'scan now' menu
canScanNow(t: Tag): boolean {
if (!this.withClair) { return false; }
let st: string = this.scanStatus(t);
return st !== VULNERABILITY_SCAN_STATUS.pending &&
st !== VULNERABILITY_SCAN_STATUS.running;
}
//Trigger scan
scanNow(tagId: string): void {
if (tagId) {
this.channel.publishScanEvent(tagId);
}
}
} }

View File

@ -1,17 +1,20 @@
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { DebugElement } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { VulnerabilitySummary } from '../service/index'; import { VulnerabilitySummary } from '../service/index';
import { ResultBarChartComponent, ScanState } from './result-bar-chart.component'; import { ResultBarChartComponent } from './result-bar-chart.component';
import { ResultTipComponent } from './result-tip.component'; import { ResultTipComponent } from './result-tip.component';
import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service'; import {
ScanningResultService,
ScanningResultDefaultService,
TagService,
TagDefaultService
} from '../service/index';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { ErrorHandler } from '../error-handler/index'; import { ErrorHandler } from '../error-handler/index';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { VULNERABILITY_SCAN_STATUS } from '../utils'; import { VULNERABILITY_SCAN_STATUS } from '../utils';
import { ChannelService } from '../channel/index';
describe('ResultBarChartComponent (inline template)', () => { describe('ResultBarChartComponent (inline template)', () => {
let component: ResultBarChartComponent; let component: ResultBarChartComponent;
@ -52,7 +55,10 @@ describe('ResultBarChartComponent (inline template)', () => {
ResultTipComponent], ResultTipComponent],
providers: [ providers: [
ErrorHandler, ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: testConfig } ChannelService,
{ provide: SERVICE_CONFIG, useValue: testConfig },
{ provide: TagService, useValue: TagDefaultService },
{ provide: ScanningResultService, useValue: ScanningResultDefaultService }
] ]
}); });
@ -62,7 +68,7 @@ describe('ResultBarChartComponent (inline template)', () => {
fixture = TestBed.createComponent(ResultBarChartComponent); fixture = TestBed.createComponent(ResultBarChartComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.tagId = "mockTag"; component.tagId = "mockTag";
component.state = ScanState.UNKNOWN; component.summary = mockData;
serviceConfig = TestBed.get(SERVICE_CONFIG); serviceConfig = TestBed.get(SERVICE_CONFIG);
@ -71,30 +77,28 @@ describe('ResultBarChartComponent (inline template)', () => {
it('should be created', () => { it('should be created', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
});
it('should inject the SERVICE_CONFIG', () => {
expect(serviceConfig).toBeTruthy(); expect(serviceConfig).toBeTruthy();
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing"); expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
}); });
it('should show a button if status is PENDING', async(() => { it('should show "not scanned" if status is STOPPED', async(() => {
component.state = ScanState.PENDING; component.summary.scan_status = VULNERABILITY_SCAN_STATUS.stopped;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.scanning-button'); let el: HTMLElement = fixture.nativeElement.querySelector('span');
expect(el).toBeTruthy(); expect(el).toBeTruthy();
expect(el.textContent).toEqual('VULNERABILITY.STATE.STOPPED');
}); });
})); }));
it('should show progress if status is SCANNING', async(() => { it('should show progress if status is SCANNING', async(() => {
component.state = ScanState.SCANNING; component.summary.scan_status = VULNERABILITY_SCAN_STATUS.running;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.progress'); let el: HTMLElement = fixture.nativeElement.querySelector('.progress');
@ -103,10 +107,10 @@ describe('ResultBarChartComponent (inline template)', () => {
})); }));
it('should show QUEUED if status is QUEUED', async(() => { it('should show QUEUED if status is QUEUED', async(() => {
component.state = ScanState.QUEUED; component.summary.scan_status = VULNERABILITY_SCAN_STATUS.pending;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-state'); let el: HTMLElement = fixture.nativeElement.querySelector('.bar-state');
@ -119,11 +123,10 @@ describe('ResultBarChartComponent (inline template)', () => {
})); }));
it('should show summary bar chart if status is COMPLETED', async(() => { it('should show summary bar chart if status is COMPLETED', async(() => {
component.state = ScanState.COMPLETED; component.summary.scan_status = VULNERABILITY_SCAN_STATUS.finished;
component.summary = mockData;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none'); let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');

View File

@ -1,35 +1,40 @@
import { import {
Component, Component,
Input, Input,
Output, OnInit,
EventEmitter, OnDestroy,
OnInit ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core'; } from '@angular/core';
import { VulnerabilitySummary } from '../service/index';
import { SCANNING_STYLES } from './scanning.css'; import { SCANNING_STYLES } from './scanning.css';
import { BAR_CHART_COMPONENT_HTML } from './scanning.html'; import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
import { VULNERABILITY_SCAN_STATUS } from '../utils'; import { VULNERABILITY_SCAN_STATUS } from '../utils';
import { VulnerabilitySeverity } from '../service/index'; import {
VulnerabilitySummary,
VulnerabilitySeverity,
TagService,
ScanningResultService,
Tag
} from '../service/index';
import { ErrorHandler } from '../error-handler/index';
import { toPromise } from '../utils';
import { Observable, Subscription } from 'rxjs/Rx';
import { ChannelService } from '../channel/index';
export enum ScanState { const STATE_CHECK_INTERVAL: number = 2000;//2s
COMPLETED, //Scanning work successfully completed const RETRY_TIMES: number = 3;
ERROR, //Error occurred when scanning
QUEUED, //Scanning job is queued
SCANNING, //Scanning in progress
PENDING, //Scanning not start
UNKNOWN //Unknown status
}
@Component({ @Component({
selector: 'hbr-vulnerability-bar', selector: 'hbr-vulnerability-bar',
styles: [SCANNING_STYLES], styles: [SCANNING_STYLES],
template: BAR_CHART_COMPONENT_HTML template: BAR_CHART_COMPONENT_HTML
}) })
export class ResultBarChartComponent implements OnInit { export class ResultBarChartComponent implements OnInit, OnDestroy {
@Input() repoName: string = "";
@Input() tagId: string = ""; @Input() tagId: string = "";
@Input() state: ScanState = ScanState.PENDING;
@Input() summary: VulnerabilitySummary = { @Input() summary: VulnerabilitySummary = {
scan_status: VULNERABILITY_SCAN_STATUS.unknown, scan_status: VULNERABILITY_SCAN_STATUS.stopped,
severity: VulnerabilitySeverity.UNKNOWN, severity: VulnerabilitySeverity.UNKNOWN,
update_time: new Date(), update_time: new Date(),
components: { components: {
@ -37,63 +42,157 @@ export class ResultBarChartComponent implements OnInit {
summary: [] summary: []
} }
}; };
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>(); onSubmitting: boolean = false;
scanningInProgress: boolean = false; retryCounter: number = 0;
stateCheckTimer: Subscription;
timerHandler: any;
constructor() { } constructor(
private tagService: TagService,
private scanningService: ScanningResultService,
private errorHandler: ErrorHandler,
private channel: ChannelService,
private ref: ChangeDetectorRef
) { }
ngOnInit(): void { ngOnInit(): void {
if (this.summary && this.summary.scan_status) { this.channel.scanCommand$.subscribe((tagId: string) => {
switch (this.summary.scan_status) { if (this.tagId === tagId) {
case VULNERABILITY_SCAN_STATUS.unknown: this.scanNow();
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;
} }
});
}
ngOnDestroy(): void {
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
this.stateCheckTimer = null;
} }
} }
//Get vulnerability scanning status
public get status(): string {
if (this.summary && this.summary.scan_status) {
return this.summary.scan_status;
}
return VULNERABILITY_SCAN_STATUS.unknown;
}
public get completed(): boolean { public get completed(): boolean {
return this.state === ScanState.COMPLETED; return this.status === VULNERABILITY_SCAN_STATUS.finished;
} }
public get error(): boolean { public get error(): boolean {
return this.state === ScanState.ERROR; return this.status === VULNERABILITY_SCAN_STATUS.error;
} }
public get queued(): boolean { public get queued(): boolean {
return this.state === ScanState.QUEUED; return this.status === VULNERABILITY_SCAN_STATUS.pending;
} }
public get scanning(): boolean { public get scanning(): boolean {
return this.state === ScanState.SCANNING; return this.status === VULNERABILITY_SCAN_STATUS.running;
} }
public get pending(): boolean { public get stopped(): boolean {
return this.state === ScanState.PENDING; return this.status === VULNERABILITY_SCAN_STATUS.stopped;
} }
public get unknown(): boolean { public get unknown(): boolean {
return this.state === ScanState.UNKNOWN; return this.status === VULNERABILITY_SCAN_STATUS.unknown;
} }
scanNow(): void { scanNow(): void {
if (this.tagId && this.tagId !== '') { if (this.onSubmitting) {
this.scanningInProgress = true; //Avoid duplicated submitting
this.startScanning.emit(this.tagId); return;
} }
if (!this.repoName || !this.tagId) {
return;
}
this.onSubmitting = true;
toPromise<any>(this.scanningService.startVulnerabilityScanning(this.repoName, this.tagId))
.then(() => {
this.onSubmitting = false;
//Forcely change status to queued after successful submitting
this.summary.scan_status = VULNERABILITY_SCAN_STATUS.pending;
//Forcely refresh view
this.forceRefreshView(1000);
//Start check status util the job is done
if (!this.stateCheckTimer) {
//Avoid duplicated subscribing
this.stateCheckTimer = Observable.timer(STATE_CHECK_INTERVAL, STATE_CHECK_INTERVAL).subscribe(() => {
this.getSummary();
});
}
})
.catch(error => {
this.onSubmitting = false;
this.errorHandler.error(error);
});
}
getSummary(): void {
if (!this.repoName || !this.tagId) {
return
}
toPromise<Tag>(this.tagService.getTag(this.repoName, this.tagId))
.then((t: Tag) => {
//To keep the same summary reference, use value copy.
this.copyValue(t.scan_overview);
//Forcely refresh view
this.forceRefreshView(1000);
if (!this.queued && !this.scanning) {
//Scanning should be done
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
this.stateCheckTimer = null;
}
}
})
.catch(error => {
this.errorHandler.error(error);
this.retryCounter++;
if (this.retryCounter >= RETRY_TIMES) {
//Stop timer
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
this.stateCheckTimer = null;
}
this.retryCounter = 0;
}
});
}
copyValue(newVal: VulnerabilitySummary): void {
if (!newVal || !newVal.scan_status) { return; }
this.summary.scan_status = newVal.scan_status;
this.summary.severity = newVal.severity;
this.summary.components = newVal.components;
this.summary.update_time = newVal.update_time;
}
forceRefreshView(duration: number): void {
//Reset timer
if (this.timerHandler) {
clearInterval(this.timerHandler);
}
this.timerHandler = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => {
if (this.timerHandler) {
clearInterval(this.timerHandler);
this.timerHandler = null;
}
}, duration);
} }
} }

View File

@ -34,7 +34,7 @@ export const TIP_COMPONENT_HTML: string = `
</div> </div>
<div> <div>
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span> <span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
<span>{{completeTimestamp | date}}</span> <span>{{completeTimestamp | date:'MM/dd/y HH:mm:ss'}}</span>
</div> </div>
</clr-tooltip-content> </clr-tooltip-content>
</clr-tooltip> </clr-tooltip>
@ -89,11 +89,11 @@ export const GRID_COMPONENT_HTML: string = `
export const BAR_CHART_COMPONENT_HTML: string = ` export const BAR_CHART_COMPONENT_HTML: string = `
<div class="bar-wrapper"> <div class="bar-wrapper">
<div *ngIf="pending" class="bar-state"> <div *ngIf="stopped" class="bar-state">
<button class="btn btn-link scanning-button" (click)="scanNow()" [disabled]="scanningInProgress">{{'VULNERABILITY.STATE.PENDING' | translate}}</button> <span class="label">{{'VULNERABILITY.STATE.STOPPED' | translate}}</span>
</div> </div>
<div *ngIf="queued" class="bar-state"> <div *ngIf="queued" class="bar-state">
<span>{{'VULNERABILITY.STATE.QUEUED' | translate}}</span> <span class="label label-orange">{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>
</div> </div>
<div *ngIf="error" class="bar-state bar-state-error"> <div *ngIf="error" class="bar-state bar-state-error">
<clr-icon shape="info-circle" class="is-error" size="24"></clr-icon> <clr-icon shape="info-circle" class="is-error" size="24"></clr-icon>
@ -101,9 +101,9 @@ export const BAR_CHART_COMPONENT_HTML: string = `
</div> </div>
<div *ngIf="scanning" class="bar-state bar-state-chart"> <div *ngIf="scanning" class="bar-state bar-state-chart">
<div>{{'VULNERABILITY.STATE.SCANNING' | translate}}</div> <div>{{'VULNERABILITY.STATE.SCANNING' | translate}}</div>
<div class="progress loop" style="height:2px;min-height:2px;"><progress></progress></div> <div class="progress loop" style="height:2px;"><progress></progress></div>
</div> </div>
<div *ngIf="completed" class="bar-state bar-state-chart" style="z-index:1020;"> <div *ngIf="completed" class="bar-state bar-state-chart">
<hbr-vulnerability-summary-chart [summary]="summary"></hbr-vulnerability-summary-chart> <hbr-vulnerability-summary-chart [summary]="summary"></hbr-vulnerability-summary-chart>
</div> </div>
<div *ngIf="unknown" class="bar-state"> <div *ngIf="unknown" class="bar-state">

View File

@ -31,7 +31,7 @@
"clarity-icons": "^0.9.8", "clarity-icons": "^0.9.8",
"clarity-ui": "^0.9.8", "clarity-ui": "^0.9.8",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"harbor-ui": "0.3.2", "harbor-ui": "0.3.18",
"intl": "^1.2.5", "intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2", "mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0", "ngx-cookie": "^1.0.0",

View File

@ -462,7 +462,7 @@
}, },
"VULNERABILITY": { "VULNERABILITY": {
"STATE": { "STATE": {
"PENDING": "SCAN NOW", "STOPPED": "Not Scanned",
"QUEUED": "Queued", "QUEUED": "Queued",
"ERROR": "Error", "ERROR": "Error",
"SCANNING": "Scanning", "SCANNING": "Scanning",
@ -495,7 +495,8 @@
"PLURAL": "Vulnerabilities", "PLURAL": "Vulnerabilities",
"PLACEHOLDER": "Filter Vulnerabilities", "PLACEHOLDER": "Filter Vulnerabilities",
"PACKAGE": "Package with", "PACKAGE": "Package with",
"PACKAGES": "Packages with" "PACKAGES": "Packages with",
"SCAN_NOW": "Scan"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "Push Image", "TITLE": "Push Image",

View File

@ -461,7 +461,7 @@
}, },
"VULNERABILITY": { "VULNERABILITY": {
"STATE": { "STATE": {
"PENDING": "SCAN NOW", "STOPPED": "Not Scanned",
"QUEUED": "Queued", "QUEUED": "Queued",
"ERROR": "Error", "ERROR": "Error",
"SCANNING": "Scanning", "SCANNING": "Scanning",
@ -494,7 +494,8 @@
"PLURAL": "Vulnerabilities", "PLURAL": "Vulnerabilities",
"PLACEHOLDER": "Filter Vulnerabilities", "PLACEHOLDER": "Filter Vulnerabilities",
"PACKAGE": "Package with", "PACKAGE": "Package with",
"PACKAGES": "Packages with" "PACKAGES": "Packages with",
"SCAN_NOW": "Scan"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "Push Image", "TITLE": "Push Image",

View File

@ -466,7 +466,7 @@
}, },
"VULNERABILITY": { "VULNERABILITY": {
"STATE": { "STATE": {
"PENDING": "开始扫描", "STOPPED": "未扫描",
"QUEUED": "已入队列", "QUEUED": "已入队列",
"ERROR": "错误", "ERROR": "错误",
"SCANNING": "扫描中", "SCANNING": "扫描中",
@ -499,7 +499,8 @@
"PLURAL": "缺陷", "PLURAL": "缺陷",
"PLACEHOLDER": "过滤缺陷", "PLACEHOLDER": "过滤缺陷",
"PACKAGE": "个组件有", "PACKAGE": "个组件有",
"PACKAGES": "个组件有" "PACKAGES": "个组件有",
"SCAN_NOW": "扫描"
}, },
"PUSH_IMAGE": { "PUSH_IMAGE": {
"TITLE": "推送镜像", "TITLE": "推送镜像",