mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-04 16:13:35 +01:00
Provide 'Scan Now' menu in the tag list (#2819)
This commit is contained in:
parent
5c8be3502c
commit
aa681eb018
28
src/ui_ng/lib/src/channel/channel.service.ts
Normal file
28
src/ui_ng/lib/src/channel/channel.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
1
src/ui_ng/lib/src/channel/index.ts
Normal file
1
src/ui_ng/lib/src/channel/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './channel.service';
|
@ -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
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -17,3 +17,4 @@ 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';
|
@ -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>
|
||||||
|
@ -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 }
|
||||||
|
@ -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() {
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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": "推送镜像",
|
||||||
|
Loading…
Reference in New Issue
Block a user