Add pagination support to scanner list (#14673)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Will Sun 2021-04-19 10:51:19 +08:00 committed by GitHub
parent 5e75c45873
commit 5e5544cd47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 316 additions and 249 deletions

View File

@ -8,7 +8,6 @@ import { SearchTriggerService } from '../../shared/components/global-search/sear
import { HarborShellComponent } from './harbor-shell.component';
import { ClarityModule } from "@clr/angular";
import { of } from 'rxjs';
import { ConfigScannerService } from "../left-side-nav/interrogation-services/scanner/config-scanner.service";
import { modalEvents } from '../modal-events.const';
import { PasswordSettingComponent } from '../password-setting/password-setting.component';
import { AboutDialogComponent } from '../../shared/components/about-dialog/about-dialog.component';
@ -21,6 +20,10 @@ import { ErrorHandler } from '../../shared/units/error-handler';
import { AccountSettingsModalComponent } from "../account-settings/account-settings-modal.component";
import { InlineAlertComponent } from "../../shared/components/inline-alert/inline-alert.component";
import { AccountSettingsModalService } from "../account-settings/account-settings-modal-service.service";
import { ScannerService } from "../../../../ng-swagger-gen/services/scanner.service";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import { Registry } from "../../../../ng-swagger-gen/models/registry";
import { delay } from "rxjs/operators";
describe('HarborShellComponent', () => {
let component: HarborShellComponent;
@ -71,9 +74,16 @@ describe('HarborShellComponent', () => {
};
}
};
let fakeConfigScannerService = {
getScanners() {
return of(true);
let fakeScannerService = {
listScannersResponse() {
const response: HttpResponse<Array<Registry>> = new HttpResponse<Array<Registry>>({
headers: new HttpHeaders({'x-total-count': [].length.toString()}),
body: []
});
return of(response).pipe(delay(0));
},
listScanners() {
return of([]).pipe(delay(0));
}
};
beforeEach(waitForAsync(() => {
@ -92,7 +102,7 @@ describe('HarborShellComponent', () => {
{ provide: SessionService, useValue: fakeSessionService },
{ provide: SearchTriggerService, useValue: fakeSearchTriggerService },
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: ConfigScannerService, useValue: fakeConfigScannerService },
{ provide: ScannerService, useValue: fakeScannerService },
{ provide: MessageHandlerService, useValue: mockMessageHandlerService },
{ provide: AccountSettingsModalService, useValue: mockAccountSettingsModalService },
{ provide: PasswordSettingService, useValue: mockPasswordSettingService },

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Component, OnInit, ViewChild, OnDestroy, ElementRef, ChangeDetectorRef } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from "rxjs";
import { forkJoin, Observable, Subscription } from "rxjs";
import { AppConfigService } from '../../services/app-config.service';
import { ModalEvent } from '../modal-event';
import { modalEvents } from '../modal-events.const';
@ -23,12 +23,14 @@ import { SessionService } from '../../shared/services/session.service';
import { AboutDialogComponent } from '../../shared/components/about-dialog/about-dialog.component';
import { SearchTriggerService } from '../../shared/components/global-search/search-trigger.service';
import { CommonRoutes } from "../../shared/entities/shared.const";
import { ConfigScannerService, SCANNERS_DOC } from "../left-side-nav/interrogation-services/scanner/config-scanner.service";
import { THEME_ARRAY, ThemeInterface } from "../../services/theme";
import { clone } from "../../shared/units/utils";
import { clone, DEFAULT_PAGE_SIZE } from "../../shared/units/utils";
import { ThemeService } from "../../services/theme.service";
import { AccountSettingsModalComponent } from "../account-settings/account-settings-modal.component";
import { EventService, HarborEvent } from "../../services/event-service/event.service";
import { SCANNERS_DOC } from "../left-side-nav/interrogation-services/scanner/scanner";
import { ScannerService } from "../../../../ng-swagger-gen/services/scanner.service";
import { Project } from "../../../../ng-swagger-gen/models/project";
const HAS_SHOWED_SCANNER_INFO: string = 'hasShowScannerInfo';
const YES: string = 'yes';
@ -76,7 +78,7 @@ export class HarborShellComponent implements OnInit, OnDestroy {
private session: SessionService,
private searchTrigger: SearchTriggerService,
private appConfigService: AppConfigService,
private scannerService: ConfigScannerService,
private scannerService: ScannerService,
public theme: ThemeService,
private event: EventService,
private cd: ChangeDetectorRef
@ -108,7 +110,9 @@ export class HarborShellComponent implements OnInit, OnDestroy {
this.isSearchResultsOpened = false;
});
if (!(localStorage && localStorage.getItem(HAS_SHOWED_SCANNER_INFO) === YES)) {
this.getDefaultScanner();
if (this.isSystemAdmin) {
this.getDefaultScanner();
}
}
// set local in app
if (localStorage) {
@ -131,11 +135,37 @@ export class HarborShellComponent implements OnInit, OnDestroy {
}
getDefaultScanner() {
this.scannerService.getScanners()
.subscribe(scanners => {
if (scanners && scanners.length) {
this.showScannerInfo = scanners.some(scanner => scanner.is_default);
}
this.scannerService.listScannersResponse({
pageSize: DEFAULT_PAGE_SIZE,
page: 1
}).subscribe(res => {
if (res.headers) {
const xHeader: string = res.headers.get("X-Total-Count");
const totalCount = parseInt(xHeader, 0);
let arr = res.body || [];
if (totalCount <= DEFAULT_PAGE_SIZE) { // already gotten all scanners
if (arr && arr.length) {
this.showScannerInfo = arr.some(scanner => scanner.is_default);
}
} else { // get all the scanners in specified times
const times: number = Math.ceil(totalCount / DEFAULT_PAGE_SIZE);
const observableList: Observable<Project[]>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(this.scannerService.listScanners({
page: i,
pageSize: DEFAULT_PAGE_SIZE
}));
}
forkJoin(observableList).subscribe(response => {
if (response && response.length) {
response.forEach(item => {
arr = arr.concat(item);
});
this.showScannerInfo = arr.some(scanner => scanner.is_default);
}
});
}
}
});
}
ngOnDestroy(): void {

View File

@ -17,7 +17,6 @@ import { SharedModule } from "../../../shared/shared.module";
import { NewScannerModalComponent } from "./scanner/new-scanner-modal/new-scanner-modal.component";
import { ScannerMetadataComponent } from "./scanner/scanner-metadata/scanner-metadata.component";
import { NewScannerFormComponent } from "./scanner/new-scanner-form/new-scanner-form.component";
import { ConfigScannerService } from "./scanner/config-scanner.service";
import { RouterModule, Routes } from "@angular/router";
import { ConfigurationScannerComponent } from "./scanner/config-scanner.component";
import { VulnerabilityConfigComponent } from "./vulnerability/vulnerability-config.component";
@ -61,7 +60,6 @@ const routes: Routes = [
VulnerabilityConfigComponent
],
providers: [
ConfigScannerService,
ScanAllRepoService,
{provide: ScanApiRepository, useClass: ScanApiDefaultRepository },
]

View File

@ -8,7 +8,7 @@
</clr-signpost-content>
</clr-signpost>
</h4>
<clr-datagrid [clrDgLoading]="onGoing" [(clrDgSingleSelected)]="selectedRow">
<clr-datagrid (clrDgRefresh)="getScanners($event)" [clrDgLoading]="onGoing" [(clrDgSingleSelected)]="selectedRow">
<clr-dg-action-bar>
<div class="clr-row">
<div class="clr-col-7">
@ -52,7 +52,7 @@
</div>
<div class="clr-col-5">
<div class="action-head-pos">
<span (click)="getScanners()" class="refresh-btn">
<span (click)="refresh()" class="refresh-btn">
<clr-icon shape="refresh" [hidden]="onGoing"></clr-icon>
</span>
</div>
@ -67,7 +67,7 @@
<clr-dg-placeholder>
{{'SCANNER.NO_SCANNER' | translate}}
</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let scanner of scanners" [clrDgItem]="scanner">
<clr-dg-row *ngFor="let scanner of scanners" [clrDgItem]="scanner">
<clr-dg-cell class="position-relative">
<span>{{scanner.name}}</span>
<span *ngIf="scanner.is_default" class="label label-info ml-1">{{'SCANNER.DEFAULT' | translate}}</span>
@ -96,9 +96,10 @@
<scanner-metadata *clrIfExpanded [uid]="scanner.uuid" ngProjectAs="clr-dg-row-detail"></scanner-metadata>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination [clrDgPageSize]="15">
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="page" [clrDgTotalItems]="total">
<clr-dg-page-size [clrPageSizeOptions]="[15,25,50]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size>
<span *ngIf="scanners?.length > 0">1 - {{scanners?.length}} {{'WEBHOOK.OF' | translate}} </span> {{scanners?.length}} {{'WEBHOOK.ITEMS' | translate}}
<span *ngIf="total">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'DESTINATION.OF' | translate}}</span>
{{total}} {{'DESTINATION.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -1,17 +1,15 @@
import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ClarityModule } from "@clr/angular";
import { of } from "rxjs";
import { delay } from "rxjs/operators";
import { ConfigurationScannerComponent } from "./config-scanner.component";
import { ConfigScannerService } from "./config-scanner.service";
import { MessageHandlerService } from "../../../../shared/services/message-handler.service";
import { SharedTestingModule } from "../../../../shared/shared.module";
import { ScannerMetadataComponent } from "./scanner-metadata/scanner-metadata.component";
import { NewScannerModalComponent } from "./new-scanner-modal/new-scanner-modal.component";
import { NewScannerFormComponent } from "./new-scanner-form/new-scanner-form.component";
import { TranslateService } from "@ngx-translate/core";
import { ErrorHandler } from "../../../../shared/units/error-handler";
import { ConfirmationDialogService } from "../../../global-confirmation-dialog/confirmation-dialog.service";
import { ScannerService } from "../../../../../../ng-swagger-gen/services/scanner.service";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import { Registry } from "../../../../../../ng-swagger-gen/models/registry";
import { ClrLoadingState } from "@clr/angular";
describe('ConfigurationScannerComponent', () => {
let mockScannerMetadata = {
@ -35,10 +33,14 @@ describe('ConfigurationScannerComponent', () => {
let fixture: ComponentFixture<ConfigurationScannerComponent>;
let fakedConfigScannerService = {
getScannerMetadata() {
return of(mockScannerMetadata);
return of(mockScannerMetadata).pipe(delay(10));
},
getScanners() {
return of([mockScanner1]);
listScannersResponse() {
const response: HttpResponse<Array<Registry>> = new HttpResponse<Array<Registry>>({
headers: new HttpHeaders({'x-total-count': [mockScanner1].length.toString()}),
body: [mockScanner1]
});
return of(response).pipe(delay(10));
},
updateScanner() {
return of(true);
@ -48,8 +50,6 @@ describe('ConfigurationScannerComponent', () => {
TestBed.configureTestingModule({
imports: [
SharedTestingModule,
BrowserAnimationsModule,
ClarityModule,
],
declarations: [
ConfigurationScannerComponent,
@ -58,11 +58,7 @@ describe('ConfigurationScannerComponent', () => {
NewScannerFormComponent
],
providers: [
ErrorHandler,
MessageHandlerService,
ConfirmationDialogService,
TranslateService,
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
{ provide: ScannerService, useValue: fakedConfigScannerService },
// open auto detect
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
@ -71,9 +67,11 @@ describe('ConfigurationScannerComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ConfigurationScannerComponent);
component = fixture.componentInstance;
component.newScannerDialog.saveBtnState = ClrLoadingState.LOADING;
fixture.detectChanges();
});
it('should create', () => {
it('should create', async () => {
await fixture.whenStable();
expect(component).toBeTruthy();
expect(component.scanners.length).toBe(1);
});

View File

@ -1,14 +1,16 @@
import { Component, ViewChild, OnInit, OnDestroy } from "@angular/core";
import { Scanner } from "./scanner";
import { Scanner, SCANNERS_DOC } from "./scanner";
import { NewScannerModalComponent } from "./new-scanner-modal/new-scanner-modal.component";
import { ConfigScannerService, SCANNERS_DOC } from "./config-scanner.service";
import { finalize } from "rxjs/operators";
import { MessageHandlerService } from "../../../../shared/services/message-handler.service";
import { ErrorHandler } from "../../../../shared/units/error-handler";
import { clone } from "../../../../shared/units/utils";
import { clone, DEFAULT_PAGE_SIZE, getSortingString } from "../../../../shared/units/utils";
import { ConfirmationDialogService } from "../../../global-confirmation-dialog/confirmation-dialog.service";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../../../shared/entities/shared.const";
import { ConfirmationMessage } from "../../../global-confirmation-dialog/confirmation-message";
import { ScannerService } from "../../../../../../ng-swagger-gen/services/scanner.service";
import { ClrDatagridStateInterface } from "@clr/angular";
import { ScannerRegistrationReq } from "../../../../../../ng-swagger-gen/models/scanner-registration-req";
@Component({
selector: 'config-scanner',
@ -18,13 +20,17 @@ import { ConfirmationMessage } from "../../../global-confirmation-dialog/confirm
export class ConfigurationScannerComponent implements OnInit, OnDestroy {
scanners: Scanner[] = [];
selectedRow: Scanner;
onGoing: boolean = false;
onGoing: boolean = true;
@ViewChild(NewScannerModalComponent)
newScannerDialog: NewScannerModalComponent;
deletionSubscription: any;
scannerDocUrl: string = SCANNERS_DOC;
page: number = 1;
pageSize: number = DEFAULT_PAGE_SIZE;
total: number = 0;
state: ClrDatagridStateInterface;
constructor(
private configScannerService: ConfigScannerService,
private configScannerService: ScannerService,
private errorHandler: ErrorHandler,
private msgHandler: MessageHandlerService,
private deletionDialogService: ConfirmationDialogService,
@ -35,17 +41,18 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
if (confirmed &&
confirmed.source === ConfirmationTargets.SCANNER &&
confirmed.state === ConfirmationState.CONFIRMED) {
this.configScannerService.deleteScanners(confirmed.data)
this.configScannerService.deleteScanner({
registrationId: confirmed.data[0].uuid
})
.subscribe(response => {
this.msgHandler.showSuccess("SCANNER.DELETE_SUCCESS");
this.getScanners();
this.refresh();
}, error => {
this.errorHandler.error(error);
});
}
});
}
this.getScanners();
}
ngOnDestroy(): void {
if (this.deletionSubscription) {
@ -53,13 +60,45 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
this.deletionSubscription = null;
}
}
getScanners() {
refresh() {
this.page = 1;
this.selectedRow = null;
this.total = 0;
this.getScanners(this.state);
}
getScanners(state?: ClrDatagridStateInterface) {
this.state = state;
if (state && state.page) {
this.pageSize = state.page.size;
}
let q: string;
if (state && state.filters && state.filters.length) {
q = encodeURIComponent(`${state.filters[0].property}=~${state.filters[0].value}`);
}
let sort: string;
if (state && state.sort && state.sort.by) {
sort = getSortingString(state);
} else { // sort by creation_time desc by default
sort = `-creation_time`;
}
this.onGoing = true;
this.configScannerService.getScanners()
this.configScannerService.listScannersResponse({
page: this.page,
pageSize: this.pageSize,
q: q,
sort: sort
})
.pipe(finalize(() => this.onGoing = false))
.subscribe(response => {
this.scanners = response;
this.getMetadataForAll();
// Get total count
if (response.headers) {
let xHeader: string = response.headers.get("X-Total-Count");
if (xHeader) {
this.total = parseInt(xHeader, 0);
}
}
this.scanners = response.body || [];
this.getMetadataForAll();
}, error => {
this.errorHandler.error(error);
});
@ -69,7 +108,9 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
this.scanners.forEach((scanner, index) => {
if (scanner.uuid ) {
this.scanners[index].loadingMetadata = true;
this.configScannerService.getScannerMetadata(scanner.uuid)
this.configScannerService.getScannerMetadata({
registrationId: scanner.uuid
})
.pipe(finalize(() => this.scanners[index].loadingMetadata = false))
.subscribe(response => {
this.scanners[index].metadata = response;
@ -91,12 +132,15 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
}
changeStat() {
if (this.selectedRow) {
let scanner: Scanner = clone(this.selectedRow);
let scanner: ScannerRegistrationReq = clone(this.selectedRow);
scanner.disabled = !scanner.disabled;
this.configScannerService.updateScanner(scanner)
this.configScannerService.updateScanner({
registrationId: this.selectedRow.uuid,
registration: scanner
})
.subscribe(response => {
this.msgHandler.showSuccess("SCANNER.UPDATE_SUCCESS");
this.getScanners();
this.refresh();
}, error => {
this.errorHandler.error(error);
});
@ -104,10 +148,15 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
}
setAsDefault() {
if (this.selectedRow) {
this.configScannerService.setAsDefault(this.selectedRow.uuid)
this.configScannerService.setScannerAsDefault({
registrationId: this.selectedRow.uuid,
payload: {
is_default: true
}
})
.subscribe(response => {
this.msgHandler.showSuccess("SCANNER.UPDATE_SUCCESS");
this.getScanners();
this.refresh();
}, error => {
this.errorHandler.error(error);
});

View File

@ -1,20 +0,0 @@
import { TestBed, inject } from '@angular/core/testing';
import { SharedTestingModule } from "../../../../shared/shared.module";
import { ConfigScannerService } from "./config-scanner.service";
describe('TagService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SharedTestingModule
],
providers: [
ConfigScannerService
]
});
});
it('should be initialized', inject([ConfigScannerService], (service: ConfigScannerService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -1,82 +0,0 @@
import {Injectable} from "@angular/core";
import {Scanner} from "./scanner";
import { forkJoin, Observable, throwError as observableThrowError } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { HttpClient } from "@angular/common/http";
import { ScannerMetadata } from "./scanner-metadata";
import { CURRENT_BASE_HREF } from "../../../../shared/units/utils";
export const SCANNERS_DOC: string = "https://goharbor.io/blog/harbor-1.10-release/#vulnerability-scanning-with-pluggable-scanners";
@Injectable({
providedIn: 'root',
})
export class ConfigScannerService {
constructor( private http: HttpClient) {}
getScannersByName(name: string): Observable<Scanner[]> {
name = encodeURIComponent(name);
return this.http.get(`${ CURRENT_BASE_HREF }/scanners?ex_name=${name}`)
.pipe(catchError(error => observableThrowError(error)))
.pipe(map(response => response as Scanner[]));
}
getScannersByEndpointUrl(endpointUrl: string): Observable<Scanner[]> {
endpointUrl = encodeURIComponent(endpointUrl);
return this.http.get(`${ CURRENT_BASE_HREF }/scanners?ex_url=${endpointUrl}`)
.pipe(catchError(error => observableThrowError(error)))
.pipe(map(response => response as Scanner[]));
}
testEndpointUrl(testValue: any): Observable<any> {
return this.http.post(`${ CURRENT_BASE_HREF }/scanners/ping`, testValue)
.pipe(catchError(error => observableThrowError(error)));
}
addScanner(scanner: Scanner): Observable<any> {
return this.http.post(CURRENT_BASE_HREF + '/scanners', scanner )
.pipe(catchError(error => observableThrowError(error)));
}
getScanners(): Observable<Scanner[]> {
return this.http.get(CURRENT_BASE_HREF + '/scanners')
.pipe(map(response => response as Scanner[]))
.pipe(catchError(error => observableThrowError(error)));
}
updateScanner(scanner: Scanner): Observable<any> {
return this.http.put(`${ CURRENT_BASE_HREF }/scanners/${scanner.uuid}`, scanner )
.pipe(catchError(error => observableThrowError(error)));
}
deleteScanner(scanner: Scanner): Observable<any> {
return this.http.delete(`${ CURRENT_BASE_HREF }/scanners/${scanner.uuid}`)
.pipe(catchError(error => observableThrowError(error)));
}
deleteScanners(scanners: Scanner[]): Observable<any> {
let observableLists: any[] = [];
if (scanners && scanners.length > 0) {
scanners.forEach(scanner => {
observableLists.push(this.deleteScanner(scanner));
});
return forkJoin(...observableLists);
}
}
getProjectScanner(projectId: number): Observable<Scanner> {
return this.http.get(`${ CURRENT_BASE_HREF }/projects/${projectId}/scanner`)
.pipe(map(response => response as Scanner))
.pipe(catchError(error => observableThrowError(error)));
}
updateProjectScanner(projectId: number , uid: string): Observable<any> {
return this.http.put(`${ CURRENT_BASE_HREF }/projects/${projectId}/scanner` , {uuid: uid})
.pipe(catchError(error => observableThrowError(error)));
}
getScannerMetadata(uid: string): Observable<ScannerMetadata> {
return this.http.get(`${ CURRENT_BASE_HREF }/scanners/${uid}/metadata`)
.pipe(map(response => response as ScannerMetadata))
.pipe(catchError(error => observableThrowError(error)));
}
setAsDefault(uid: string): Observable<any> {
return this.http.patch(`${ CURRENT_BASE_HREF }/scanners/${uid}`, {is_default: true} )
.pipe(catchError(error => observableThrowError(error)));
}
getProjectScanners(projectId: number) {
return this.http.get(`${ CURRENT_BASE_HREF }/projects/${projectId}/scanner/candidates`)
.pipe(map(response => response as Scanner[]))
.pipe(catchError(error => observableThrowError(error)));
}
}

View File

@ -4,10 +4,10 @@ import { FormBuilder } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ClarityModule } from "@clr/angular";
import { SharedTestingModule } from "../../../../../shared/shared.module";
import { ConfigScannerService } from "../config-scanner.service";
import { of } from "rxjs";
import { TranslateService } from "@ngx-translate/core";
import { delay } from "rxjs/operators";
import { ScannerService } from "../../../../../../../ng-swagger-gen/services/scanner.service";
describe('NewScannerFormComponent', () => {
let mockScanner1 = {
@ -19,7 +19,7 @@ describe('NewScannerFormComponent', () => {
let component: NewScannerFormComponent;
let fixture: ComponentFixture<NewScannerFormComponent>;
let fakedConfigScannerService = {
getScannersByName() {
listScanners() {
return of([mockScanner1]).pipe(delay(500));
},
getScannersByEndpointUrl() {
@ -37,7 +37,7 @@ describe('NewScannerFormComponent', () => {
providers: [
FormBuilder,
TranslateService,
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
{ provide: ScannerService, useValue: fakedConfigScannerService },
// open auto detect
{ provide: ComponentFixtureAutoDetect, useValue: true }
]

View File

@ -9,7 +9,7 @@ import {
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { fromEvent } from "rxjs";
import { debounceTime, distinctUntilChanged, filter, finalize, map, switchMap } from "rxjs/operators";
import { ConfigScannerService } from "../config-scanner.service";
import { ScannerService } from "../../../../../../../ng-swagger-gen/services/scanner.service";
@Component({
@ -48,7 +48,7 @@ export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestro
isEdit: boolean;
@ViewChild('name') scannerName: ElementRef;
@ViewChild('endpointUrl') scannerEndpointUrl: ElementRef;
constructor(private fb: FormBuilder, private scannerService: ConfigScannerService) {
constructor(private fb: FormBuilder, private scannerService: ScannerService) {
}
ngAfterViewInit(): void {
if (!this.checkNameSubscribe) {
@ -65,7 +65,9 @@ export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestro
switchMap((name) => {
this.isNameExisting = false;
this.checkOnGoing = true;
return this.scannerService.getScannersByName(name)
return this.scannerService.listScanners({
q: encodeURIComponent(`name=${name}`)
})
.pipe(finalize(() => this.checkOnGoing = false));
})).subscribe(response => {
if (response && response.length > 0) {
@ -94,7 +96,9 @@ export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestro
switchMap((endpointUrl) => {
this.isEndpointUrlExisting = false;
this.checkEndpointOnGoing = true;
return this.scannerService.getScannersByEndpointUrl(endpointUrl)
return this.scannerService.listScanners({
q: encodeURIComponent(`url=${endpointUrl}`)
})
.pipe(finalize(() => this.checkEndpointOnGoing = false));
})).subscribe(response => {
if (response && response.length > 0) {

View File

@ -1,6 +1,5 @@
import { ComponentFixture, ComponentFixtureAutoDetect, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ClrLoadingState } from "@clr/angular";
import { ConfigScannerService } from "../config-scanner.service";
import { NewScannerModalComponent } from "./new-scanner-modal.component";
import { MessageHandlerService } from "../../../../../shared/services/message-handler.service";
import { NewScannerFormComponent } from "../new-scanner-form/new-scanner-form.component";
@ -9,6 +8,7 @@ import { of, Subscription } from "rxjs";
import { delay } from "rxjs/operators";
import { SharedTestingModule } from "../../../../../shared/shared.module";
import { Scanner } from "../scanner";
import { ScannerService } from "../../../../../../../ng-swagger-gen/services/scanner.service";
describe('NewScannerModalComponent', () => {
let component: NewScannerModalComponent;
@ -21,13 +21,13 @@ describe('NewScannerModalComponent', () => {
auth: "",
};
let fakedConfigScannerService = {
getScannersByName() {
listScanners() {
return of([mockScanner1]);
},
testEndpointUrl() {
pingScanner() {
return of(true).pipe(delay(200));
},
addScanner() {
createScanner() {
return of(true).pipe(delay(200));
},
updateScanner() {
@ -45,7 +45,7 @@ describe('NewScannerModalComponent', () => {
],
providers: [
MessageHandlerService,
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
{ provide: ScannerService, useValue: fakedConfigScannerService },
FormBuilder,
// open auto detect
{ provide: ComponentFixtureAutoDetect, useValue: true }

View File

@ -1,12 +1,14 @@
import { Component, EventEmitter, Output, ViewChild } from '@angular/core';
import { Scanner } from "../scanner";
import { NewScannerFormComponent } from "../new-scanner-form/new-scanner-form.component";
import { ConfigScannerService } from "../config-scanner.service";
import { ClrLoadingState } from "@clr/angular";
import { finalize } from "rxjs/operators";
import { MessageHandlerService } from "../../../../../shared/services/message-handler.service";
import { TranslateService } from "@ngx-translate/core";
import { InlineAlertComponent } from "../../../../../shared/components/inline-alert/inline-alert.component";
import { ScannerService } from "../../../../../../../ng-swagger-gen/services/scanner.service";
import { ScannerRegistrationReq } from "../../../../../../../ng-swagger-gen/models/scanner-registration-req";
import { clone } from "../../../../../shared/units/utils";
@Component({
selector: "new-scanner-modal",
@ -29,7 +31,7 @@ export class NewScannerModalComponent {
editScanner: Scanner;
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
constructor(
private configScannerService: ConfigScannerService,
private configScannerService: ScannerService,
private msgHandler: MessageHandlerService,
private translate: TranslateService,
) {}
@ -47,8 +49,8 @@ export class NewScannerModalComponent {
create(): void {
this.onSaving = true;
this.saveBtnState = ClrLoadingState.LOADING;
let scanner: Scanner = new Scanner();
let value = this.newScannerFormComponent.newScannerForm.value;
const scanner: ScannerRegistrationReq = {name: "", url: ""};
const value = this.newScannerFormComponent.newScannerForm.value;
scanner.name = value.name;
scanner.description = value.description;
scanner.url = value.url;
@ -66,7 +68,9 @@ export class NewScannerModalComponent {
}
scanner.skip_certVerify = !!value.skipCertVerify;
scanner.use_internal_addr = !!value.useInner;
this.configScannerService.addScanner(scanner)
this.configScannerService.createScanner({
registration: scanner
})
.pipe(finalize(() => this.onSaving = false))
.subscribe(response => {
this.close();
@ -176,8 +180,8 @@ export class NewScannerModalComponent {
onTestEndpoint() {
this.onTesting = true;
this.checkBtnState = ClrLoadingState.LOADING;
let scanner: Scanner = new Scanner();
let value = this.newScannerFormComponent.newScannerForm.value;
const scanner: ScannerRegistrationReq = {name: "", url: ""};
const value = this.newScannerFormComponent.newScannerForm.value;
scanner.name = value.name;
scanner.description = value.description;
scanner.url = value.url;
@ -195,7 +199,9 @@ export class NewScannerModalComponent {
}
scanner.skip_certVerify = !!value.skipCertVerify;
scanner.use_internal_addr = !!value.useInner;
this.configScannerService.testEndpointUrl(scanner)
this.configScannerService.pingScanner({
settings: scanner
})
.pipe(finalize(() => this.onTesting = false))
.subscribe(response => {
this.inlineAlert.showInlineSuccess({
@ -236,7 +242,11 @@ export class NewScannerModalComponent {
this.editScanner.skip_certVerify = !!value.skipCertVerify;
this.editScanner.use_internal_addr = !!value.useInner;
this.editScanner.uuid = this.uid;
this.configScannerService.updateScanner(this.editScanner)
const scanner: ScannerRegistrationReq = clone(this.editScanner);
this.configScannerService.updateScanner({
registrationId: this.editScanner.uuid,
registration: scanner
})
.pipe(finalize(() => this.onSaving = false))
.subscribe(response => {
this.close();

View File

@ -1,16 +0,0 @@
export class ScannerMetadata {
scanner?: {
name?: string;
vendor?: string;
version?: string;
};
capabilities?: [{
consumes_mime_types?: Array<string>;
produces_mime_types?: Array<string>;
}];
properties?: {
[key: string]: string;
};
constructor() {
}
}

View File

@ -2,10 +2,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ClarityModule } from "@clr/angular";
import { SharedTestingModule } from "../../../../../shared/shared.module";
import { ConfigScannerService } from "../config-scanner.service";
import { of } from "rxjs";
import { ScannerMetadataComponent } from "./scanner-metadata.component";
import { ErrorHandler } from "../../../../../shared/units/error-handler";
import { ScannerService } from "../../../../../../../ng-swagger-gen/services/scanner.service";
describe('ScannerMetadataComponent', () => {
let mockScannerMetadata = {
@ -38,7 +38,7 @@ describe('ScannerMetadataComponent', () => {
],
providers: [
ErrorHandler,
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
{ provide: ScannerService, useValue: fakedConfigScannerService },
]
});
});

View File

@ -2,12 +2,12 @@ import {
Component, Input,
OnInit
} from "@angular/core";
import { ConfigScannerService } from "../config-scanner.service";
import { finalize } from "rxjs/operators";
import { ScannerMetadata } from "../scanner-metadata";
import { DatePipe } from "@angular/common";
import { ErrorHandler } from "../../../../../shared/units/error-handler";
import {DATABASE_NEXT_UPDATE_PROPERTY, DATABASE_UPDATED_PROPERTY} from "../../../../../shared/units/utils";
import { DATABASE_NEXT_UPDATE_PROPERTY, DATABASE_UPDATED_PROPERTY } from "../../../../../shared/units/utils";
import { ScannerService } from "../../../../../../../ng-swagger-gen/services/scanner.service";
import { ScannerAdapterMetadata } from "../../../../../../../ng-swagger-gen/models/scanner-adapter-metadata";
@Component({
selector: 'scanner-metadata',
@ -17,13 +17,15 @@ import {DATABASE_NEXT_UPDATE_PROPERTY, DATABASE_UPDATED_PROPERTY} from "../../..
export class ScannerMetadataComponent implements OnInit {
@Input() uid: string;
loading: boolean = false;
scannerMetadata: ScannerMetadata;
constructor(private configScannerService: ConfigScannerService,
scannerMetadata: ScannerAdapterMetadata;
constructor(private configScannerService: ScannerService,
private errorHandler: ErrorHandler) {
}
ngOnInit(): void {
this.loading = true;
this.configScannerService.getScannerMetadata(this.uid)
this.configScannerService.getScannerMetadata({
registrationId: this.uid
})
.pipe(finalize(() => this.loading = false))
.subscribe(response => {
this.scannerMetadata = response;

View File

@ -1,24 +1,9 @@
import { ScannerMetadata } from "./scanner-metadata";
import { ScannerRegistration } from "../../../../../../ng-swagger-gen/models/scanner-registration";
import { ScannerAdapterMetadata } from "../../../../../../ng-swagger-gen/models/scanner-adapter-metadata";
export class Scanner {
name?: string;
description?: string;
uuid?: string;
url?: string;
auth?: string;
access_credential?: string;
adapter?: string;
disabled?: boolean;
is_default?: boolean;
skip_certVerify?: boolean;
use_internal_addr?: boolean;
create_time?: any;
update_time?: any;
vendor?: string;
version?: string;
metadata?: ScannerMetadata;
export interface Scanner extends ScannerRegistration {
metadata?: ScannerAdapterMetadata;
loadingMetadata?: boolean;
health?: string;
constructor() {
}
}
export const SCANNERS_DOC: string = "https://goharbor.io/blog/harbor-1.10-release/#vulnerability-scanning-with-pluggable-scanners";

View File

@ -3,11 +3,13 @@ import { of } from "rxjs";
import { TranslateService } from "@ngx-translate/core";
import { MessageHandlerService } from "../../../shared/services/message-handler.service";
import { ScannerComponent } from "./scanner.component";
import { ConfigScannerService } from "../../left-side-nav/interrogation-services/scanner/config-scanner.service";
import { SharedTestingModule } from "../../../shared/shared.module";
import { ActivatedRoute } from "@angular/router";
import { Scanner } from "../../left-side-nav/interrogation-services/scanner/scanner";
import { ErrorHandler } from "../../../shared/units/error-handler";
import { ProjectService } from "../../../../../ng-swagger-gen/services/project.service";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import { Registry } from "../../../../../ng-swagger-gen/models/registry";
describe('ScannerComponent', () => {
const mockScanner1: Scanner = {
@ -28,17 +30,21 @@ describe('ScannerComponent', () => {
};
let component: ScannerComponent;
let fixture: ComponentFixture<ScannerComponent>;
let fakedConfigScannerService = {
getProjectScanner() {
let fakedProjectService = {
getScannerOfProject() {
return of(mockScanner1);
},
getScanners() {
listScannerCandidatesOfProject() {
return of([mockScanner1, mockScanner2]);
},
getProjectScanners() {
return of([mockScanner1, mockScanner2]);
listScannerCandidatesOfProjectResponse() {
const response: HttpResponse<Array<Registry>> = new HttpResponse<Array<Registry>>({
headers: new HttpHeaders({'x-total-count': [mockScanner1, mockScanner2].length.toString()}),
body: [mockScanner1, mockScanner2]
});
return of(response);
},
updateProjectScanner() {
setScannerOfProject() {
return of(true);
}
};
@ -63,8 +69,8 @@ describe('ScannerComponent', () => {
TranslateService,
MessageHandlerService,
ErrorHandler,
{provide: ActivatedRoute, useValue: fakedRoute},
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
{ provide: ActivatedRoute, useValue: fakedRoute },
{ provide: ProjectService, useValue: fakedProjectService },
]
})
.compileComponents();

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild } from "@angular/core";
import { ConfigScannerService } from "../../left-side-nav/interrogation-services/scanner/config-scanner.service";
import { Scanner } from "../../left-side-nav/interrogation-services/scanner/scanner";
import { MessageHandlerService } from "../../../shared/services/message-handler.service";
import { ActivatedRoute } from "@angular/router";
@ -22,6 +21,10 @@ import { TranslateService } from "@ngx-translate/core";
import { ErrorHandler } from "../../../shared/units/error-handler";
import { UserPermissionService, USERSTATICPERMISSION } from "../../../shared/services";
import { InlineAlertComponent } from "../../../shared/components/inline-alert/inline-alert.component";
import { ProjectService } from "../../../../../ng-swagger-gen/services/project.service";
import { DEFAULT_PAGE_SIZE } from "../../../shared/units/utils";
import { forkJoin, Observable } from "rxjs";
import { Project } from "../../../../../ng-swagger-gen/models/project";
@Component({
@ -40,12 +43,12 @@ export class ScannerComponent implements OnInit {
onSaving: boolean = false;
hasCreatePermission: boolean = false;
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
constructor( private configScannerService: ConfigScannerService,
private msgHandler: MessageHandlerService,
constructor( private msgHandler: MessageHandlerService,
private errorHandler: ErrorHandler,
private route: ActivatedRoute,
private userPermissionService: UserPermissionService,
private translate: TranslateService
private translate: TranslateService,
private projectService: ProjectService
) {
}
ngOnInit() {
@ -70,7 +73,9 @@ export class ScannerComponent implements OnInit {
}
getScanner(isCheckHealth?: boolean) {
this.loading = true;
this.configScannerService.getProjectScanner(this.projectId)
this.projectService.getScannerOfProject({
projectNameOrId: this.projectId.toString()
})
.pipe(finalize(() => this.loading = false))
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response)) {
@ -89,14 +94,44 @@ export class ScannerComponent implements OnInit {
}
getScanners() {
if (this.projectId) {
this.configScannerService.getProjectScanners(this.projectId)
.subscribe(response => {
if (response && response.length > 0) {
this.scanners = response.filter(scanner => {
return !scanner.disabled;
this.projectService.listScannerCandidatesOfProjectResponse({
projectNameOrId: this.projectId.toString(),
page: 1,
pageSize: DEFAULT_PAGE_SIZE
}).subscribe(response => {
if (response.headers) {
const xHeader: string = response.headers.get("X-Total-Count");
const totalCount = parseInt(xHeader, 0);
let arr = response.body || [];
if (totalCount <= DEFAULT_PAGE_SIZE) { // already gotten all scanners
if (arr && arr.length > 0) {
this.scanners = arr.filter(scanner => {
return !scanner.disabled;
});
}
} else { // get all the scanners in specified times
const times: number = Math.ceil(totalCount / DEFAULT_PAGE_SIZE);
const observableList: Observable<Project[]>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(this.projectService.listScannerCandidatesOfProject({
page: i,
pageSize: DEFAULT_PAGE_SIZE,
projectNameOrId: this.projectId.toString()
}));
}
forkJoin(observableList).subscribe(res => {
if (res && res.length) {
res.forEach(item => {
arr = arr.concat(item);
});
this.scanners = arr.filter(scanner => {
return !scanner.disabled;
});
}
});
}
});
}
});
}
}
close() {
@ -118,8 +153,12 @@ export class ScannerComponent implements OnInit {
}
save() {
this.saveBtnState = ClrLoadingState.LOADING;
this.configScannerService.updateProjectScanner(this.projectId, this.selectedScanner.uuid)
.subscribe(response => {
this.projectService.setScannerOfProject({
projectNameOrId: this.projectId.toString(),
payload: {
uuid: this.selectedScanner.uuid
}
}).subscribe(response => {
this.close();
this.msgHandler.showSuccess('Update Success');
this.getScanner(true);

View File

@ -1,4 +1,5 @@
import { delUrlParam, isSameArrayValue, isSameObject } from "./utils";
import { delUrlParam, getQueryString, getSortingString, isSameArrayValue, isSameObject } from "./utils";
import { ClrDatagridStateInterface } from "@clr/angular";
describe('functions in utils.ts should work', () => {
it('function isSameArrayValue() should work', () => {
@ -30,4 +31,26 @@ describe('functions in utils.ts should work', () => {
expect(delUrlParam('http://test.com', 'param2')).toEqual('http://test.com');
expect(delUrlParam('http://test.com?param2', 'param2')).toEqual('http://test.com');
});
it('function getSortingString() should work', () => {
expect(getSortingString).toBeTruthy();
const state: ClrDatagridStateInterface = {
sort: {
by: 'name',
reverse: true
}
};
expect(getSortingString(state)).toEqual('-name');
});
it('function getQueryString() should work', () => {
expect(getQueryString).toBeTruthy();
const state: ClrDatagridStateInterface = {
filters: [
{property: 'name', value: 'test'},
{property: 'url', value: 'http://test.com'},
]
};
expect(getQueryString(state)).toEqual(encodeURIComponent('name=~test,url=~http://test.com'));
});
});

View File

@ -631,6 +631,10 @@ export function deleteEmptyKey(obj: Object): void {
}
}
/**
* Get sorting string from current state
* @param state
*/
export function getSortingString(state: ClrDatagridStateInterface): string {
if (state && state.sort && state.sort.by) {
let sortString: string;
@ -647,6 +651,32 @@ export function getSortingString(state: ClrDatagridStateInterface): string {
return null;
}
/**
* Get query string from current state, rules as below:
* query string format: q=k=v,k=~v,k=[min~max],k={v1 v2 v3},k=(v1 v2 v3)
* exact match: k=v
* fuzzy match: k=~v
* range: k=[min~max]
* or list: k={v1 v2 v3}
* and list: k=(v1 v2 v3)
* @param state
*/
export function getQueryString(state: ClrDatagridStateInterface): string {
let str: string = '';
if (state && state.filters && state.filters.length) {
state.filters.forEach(item => {
if (str) {
str += `,${item.property}=~${item.value}`;
} else {
str += `${item.property}=~${item.value}`;
}
});
return encodeURIComponent(str);
}
return null;
}
/**
* if two object are the same
* @param a