diff --git a/src/ui_ng/src/app/config/config.ts b/src/ui_ng/lib/src/config/config.ts similarity index 85% rename from src/ui_ng/src/app/config/config.ts rename to src/ui_ng/lib/src/config/config.ts index b9b55ac8d..a5672b35a 100644 --- a/src/ui_ng/src/app/config/config.ts +++ b/src/ui_ng/lib/src/config/config.ts @@ -41,7 +41,18 @@ export class BoolValueItem { } } +export class ComplexValueItem { + value: any | { [key: string]: any | any[] }; + editable: boolean; + + public constructor(v: any | { [key: string]: any | any[] }, e: boolean) { + this.value = v; + this.editable = e; + } +} + export class Configuration { + [key: string]: any | any[] auth_mode: StringValueItem; project_creation_restriction: StringValueItem; self_registration: BoolValueItem; @@ -63,6 +74,7 @@ export class Configuration { verify_remote_cert: BoolValueItem; token_expiration: NumberValueItem; cfg_expiration: NumberValueItem; + scan_all_policy: ComplexValueItem; public constructor() { this.auth_mode = new StringValueItem("db_auth", true); @@ -83,8 +95,14 @@ export class Configuration { this.email_ssl = new BoolValueItem(false, true); this.email_username = new StringValueItem("", true); this.email_password = new StringValueItem("", true); - this.token_expiration = new NumberValueItem(5, true); + this.token_expiration = new NumberValueItem(30, true); this.cfg_expiration = new NumberValueItem(30, true); this.verify_remote_cert = new BoolValueItem(false, true); + this.scan_all_policy = new ComplexValueItem({ + type: "daily", + parameters: { + daily_time: 0 + } + }, true); } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/index.ts b/src/ui_ng/lib/src/config/index.ts new file mode 100644 index 000000000..4abd60d4b --- /dev/null +++ b/src/ui_ng/lib/src/config/index.ts @@ -0,0 +1,19 @@ +import { Type } from '@angular/core'; + +import { ReplicationConfigComponent } from './replication/replication-config.component'; +import { SystemSettingsComponent } from './system/system-settings.component'; +import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-config.component'; +import { RegistryConfigComponent } from './registry-config.component'; + +export * from './config'; +export * from './replication/replication-config.component'; +export * from './system/system-settings.component'; +export * from './vulnerability/vulnerability-config.component'; +export * from './registry-config.component'; + +export const CONFIGURATION_DIRECTIVES: Type[] = [ + ReplicationConfigComponent, + SystemSettingsComponent, + VulnerabilityConfigComponent, + RegistryConfigComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/registry-config.component.html.ts b/src/ui_ng/lib/src/config/registry-config.component.html.ts new file mode 100644 index 000000000..6402681bd --- /dev/null +++ b/src/ui_ng/lib/src/config/registry-config.component.html.ts @@ -0,0 +1,7 @@ +export const REGISTRY_CONFIG_HTML: string = ` +
+ + + +
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/registry-config.component.spec.ts b/src/ui_ng/lib/src/config/registry-config.component.spec.ts new file mode 100644 index 000000000..63f591789 --- /dev/null +++ b/src/ui_ng/lib/src/config/registry-config.component.spec.ts @@ -0,0 +1,98 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; + +import { SharedModule } from '../shared/shared.module'; +import { ErrorHandler } from '../error-handler/error-handler'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; + +import { ReplicationConfigComponent } from './replication/replication-config.component'; +import { SystemSettingsComponent } from './system/system-settings.component'; +import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-config.component'; +import { RegistryConfigComponent } from './registry-config.component'; + +import { + ConfigurationService, + ConfigurationDefaultService, + ScanningResultService, + ScanningResultDefaultService + } from '../service/index'; +import { Configuration } from './config'; + +describe('RegistryConfigComponent (inline template)', () => { + + let comp: RegistryConfigComponent; + let fixture: ComponentFixture; + let cfgService: ConfigurationService; + let spy: jasmine.Spy; + let saveSpy: jasmine.Spy; + let mockConfig: Configuration = new Configuration(); + mockConfig.token_expiration.value = 90; + mockConfig.verify_remote_cert.value = true; + mockConfig.scan_all_policy.value = { + type: "daily", + parameters: { + daily_time: 0 + } + }; + let config: IServiceConfig = { + configurationEndpoint: '/api/configurations/testing' + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule + ], + declarations: [ + ReplicationConfigComponent, + SystemSettingsComponent, + VulnerabilityConfigComponent, + RegistryConfigComponent + ], + providers: [ + ErrorHandler, + { provide: SERVICE_CONFIG, useValue: config }, + { provide: ConfigurationService, useClass: ConfigurationDefaultService }, + { provide: ScanningResultService, useClass: ScanningResultDefaultService } + ] + }); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RegistryConfigComponent); + comp = fixture.componentInstance; + + cfgService = fixture.debugElement.injector.get(ConfigurationService); + spy = spyOn(cfgService, 'getConfigurations').and.returnValue(Promise.resolve(mockConfig)); + saveSpy = spyOn(cfgService, 'saveConfigurations').and.returnValue(Promise.resolve(true)); + + fixture.detectChanges(); + }); + + it('should render configurations to the view', async(() => { + expect(spy.calls.count()).toEqual(1); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + + let el: HTMLInputElement = fixture.nativeElement.querySelector('input[type="text"]'); + expect(el).toBeTruthy(); + expect(el.value).toEqual('30'); + + let el2: HTMLInputElement = fixture.nativeElement.querySelector('input[type="checkbox"]'); + expect(el2).toBeTruthy(); + expect(el2.value).toEqual('on'); + + let el3: HTMLInputElement = fixture.nativeElement.querySelector('input[type="time"]'); + expect(el3).toBeTruthy(); + expect(el3.value).toEqual("08:00"); + }); + })); + + it('should save the configuration changes', async(() => { + comp.save(); + fixture.detectChanges(); + + expect(saveSpy.calls.any).toBeTruthy(); + })); +}); \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/registry-config.component.ts b/src/ui_ng/lib/src/config/registry-config.component.ts new file mode 100644 index 000000000..f3b21a94c --- /dev/null +++ b/src/ui_ng/lib/src/config/registry-config.component.ts @@ -0,0 +1,110 @@ +import { Component, OnInit, EventEmitter, Output } from '@angular/core'; + +import { Configuration, ComplexValueItem } from './config'; +import { REGISTRY_CONFIG_HTML } from './registry-config.component.html'; +import { ConfigurationService } from '../service/index'; +import { toPromise } from '../utils'; +import { ErrorHandler } from '../error-handler'; + +@Component({ + selector: 'hbr-registry-config', + template: REGISTRY_CONFIG_HTML +}) +export class RegistryConfigComponent implements OnInit { + config: Configuration = new Configuration(); + configCopy: Configuration; + + @Output() configChanged: EventEmitter = new EventEmitter(); + + constructor( + private configService: ConfigurationService, + private errorHandler: ErrorHandler + ) { } + + ngOnInit(): void { + //Initialize + this.load(); + } + + //Load configurations + load(): void { + toPromise(this.configService.getConfigurations()) + .then((config: Configuration) => { + this.configCopy = Object.assign({}, config); + this.config = config; + }) + .catch(error => this.errorHandler.error(error)); + } + + //Save configuration changes + save(): void { + let changes: { [key: string]: any | any[] } = this.getChanges(); + + if (this._isEmptyObject(changes)) { + //Guard code, do nothing + return; + } + + //Fix policy parameters issue + let scanningAllPolicy = changes["scan_all_policy"]; + if (scanningAllPolicy && + scanningAllPolicy.type !== "daily" && + scanningAllPolicy.parameters) { + delete (scanningAllPolicy.parameters); + } + + toPromise(this.configService.saveConfigurations(changes)) + .then(() => { + this.configChanged.emit(changes); + }) + .catch(error => this.errorHandler.error(error)); + } + + reset(): void { + //Reset to the values of copy + let changes: { [key: string]: any | any[] } = this.getChanges(); + for (let prop in changes) { + this.config[prop] = Object.assign({}, this.configCopy[prop]); + } + } + + getChanges(): { [key: string]: any | any[] } { + let changes: { [key: string]: any | any[] } = {}; + if (!this.config || !this.configCopy) { + return changes; + } + + for (let prop in this.config) { + let field = this.configCopy[prop]; + if (field && field.editable) { + if (!this._compareValue(field.value, this.config[prop].value)) { + changes[prop] = this.config[prop].value; + //Number + if (typeof field.value === "number") { + changes[prop] = +changes[prop]; + } + + //Trim string value + if (typeof field.value === "string") { + changes[prop] = ('' + changes[prop]).trim(); + } + } + } + } + + return changes; + } + + //private + _compareValue(a: any, b: any): boolean { + if ((a && !b) || (!a && b)) return false; + if (!a && !b) return true; + + return JSON.stringify(a) === JSON.stringify(b); + } + + //private + _isEmptyObject(obj: any): boolean { + return !obj || JSON.stringify(obj) === "{}"; + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/replication/replication-config.component.html.ts b/src/ui_ng/lib/src/config/replication/replication-config.component.html.ts new file mode 100644 index 000000000..ca6a701f1 --- /dev/null +++ b/src/ui_ng/lib/src/config/replication/replication-config.component.html.ts @@ -0,0 +1,16 @@ +export const REPLICATION_CONFIG_HTML: string = ` +
+
+ +
+ + + + + {{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }} + + +
+
+
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/replication/replication-config.component.ts b/src/ui_ng/lib/src/config/replication/replication-config.component.ts new file mode 100644 index 000000000..60c0dd1f3 --- /dev/null +++ b/src/ui_ng/lib/src/config/replication/replication-config.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core'; +import { NgForm } from '@angular/forms'; + +import { REPLICATION_CONFIG_HTML } from './replication-config.component.html'; +import { Configuration } from '../config'; + +@Component({ + selector: 'replication-config', + template: REPLICATION_CONFIG_HTML +}) +export class ReplicationConfigComponent { + config: Configuration; + @Output() configChange: EventEmitter = new EventEmitter(); + + @Input() + get replicationConfig(): Configuration { + return this.config; + } + set replicationConfig(cfg: Configuration) { + this.config = cfg; + this.configChange.emit(this.config); + } + + @ViewChild("replicationConfigFrom") replicationConfigForm: NgForm; + + get editable(): boolean { + return this.replicationConfig && + this.replicationConfig.verify_remote_cert && + this.replicationConfig.verify_remote_cert.editable; + } + + get isValid(): boolean { + return this.replicationConfigForm && this.replicationConfigForm.valid; + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/system/system-settings.component.html.ts b/src/ui_ng/lib/src/config/system/system-settings.component.html.ts new file mode 100644 index 000000000..d548f3530 --- /dev/null +++ b/src/ui_ng/lib/src/config/system/system-settings.component.html.ts @@ -0,0 +1,24 @@ +export const SYSTEM_SETTINGS_HTML: string = ` +
+
+ +
+ + + + + {{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}} + +
+
+
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/system/system-settings.component.ts b/src/ui_ng/lib/src/config/system/system-settings.component.ts new file mode 100644 index 000000000..f2d174f7c --- /dev/null +++ b/src/ui_ng/lib/src/config/system/system-settings.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core'; +import { NgForm } from '@angular/forms'; + +import { SYSTEM_SETTINGS_HTML } from './system-settings.component.html'; +import { Configuration } from '../config'; + +@Component({ + selector: 'system-settings', + template: SYSTEM_SETTINGS_HTML +}) +export class SystemSettingsComponent { + config: Configuration; + @Output() configChange: EventEmitter = new EventEmitter(); + + @Input() + get systemSettings(): Configuration { + return this.config; + } + set systemSettings(cfg: Configuration) { + this.config = cfg; + this.configChange.emit(this.config); + } + + @ViewChild("systemConfigFrom") systemSettingsForm: NgForm; + + get editable(): boolean { + return this.systemSettings && + this.systemSettings.token_expiration && + this.systemSettings.token_expiration.editable; + } + + get isValid(): boolean { + return this.systemSettingsForm && this.systemSettingsForm.valid; + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.template.ts b/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.template.ts new file mode 100644 index 000000000..67c0fc373 --- /dev/null +++ b/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.template.ts @@ -0,0 +1,32 @@ +export const VULNERABILITY_CONFIG_HTML: string = ` +
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+`; + +export const VULNERABILITY_CONFIG_STYLES: string = ` +.form-group-override { + padding-left: 0px !important; +} + +.section-title { + font-size: 14px !important; + font-weight: 600 !important; +} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.ts b/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.ts new file mode 100644 index 000000000..ee57b9894 --- /dev/null +++ b/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.ts @@ -0,0 +1,165 @@ +import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core'; +import { NgForm } from '@angular/forms'; + +import { Configuration } from '../config'; +import { VULNERABILITY_CONFIG_HTML, VULNERABILITY_CONFIG_STYLES } from './vulnerability-config.component.template'; +import { ScanningResultService } from '../../service/scanning.service'; +import { ErrorHandler } from '../../error-handler'; +import { toPromise } from '../../utils'; +import { TranslateService } from '@ngx-translate/core'; + +const ONE_HOUR_SECONDS: number = 3600; +const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS; + +@Component({ + selector: 'vulnerability-config', + template: VULNERABILITY_CONFIG_HTML, + styles: [VULNERABILITY_CONFIG_STYLES] +}) +export class VulnerabilityConfigComponent { + _localTime: Date = new Date(); + + config: Configuration; + @Output() configChange: EventEmitter = new EventEmitter(); + + @Input() + get vulnerabilityConfig(): Configuration { + return this.config; + } + set vulnerabilityConfig(cfg: Configuration) { + this.config = cfg; + if (this.config.scan_all_policy && + this.config.scan_all_policy.value) { + if (this.config.scan_all_policy.value.type === "daily"){ + if(!this.config.scan_all_policy.value.parameters){ + this.config.scan_all_policy.value.parameters = { + daily_time: 0 + }; + } + } + } + this.configChange.emit(this.config); + } + + //UTC time + get dailyTime(): string { + if (!(this.config && + this.config.scan_all_policy && + this.config.scan_all_policy.value && + this.config.scan_all_policy.value.type === "daily")) { + return "00:00"; + } + + let timeOffset: number = 0;//seconds + if (this.config.scan_all_policy.value.parameters) { + let daily_time = this.config.scan_all_policy.value.parameters.daily_time; + if (daily_time && typeof daily_time === "number") { + timeOffset = +daily_time; + } + } + + //Convert to current time + let timezoneOffset: number = this._localTime.getTimezoneOffset(); + //Local time + timeOffset = timeOffset - timezoneOffset * 60; + if (timeOffset < 0) { + timeOffset = timeOffset + ONE_DAY_SECONDS; + } + + if (timeOffset >= ONE_DAY_SECONDS) { + timeOffset -= ONE_DAY_SECONDS; + } + + //To time string + let hours: number = Math.floor(timeOffset / ONE_HOUR_SECONDS); + let minutes: number = Math.floor((timeOffset - hours * ONE_HOUR_SECONDS) / 60); + + let timeStr: string = "" + hours; + if (hours < 10) { + timeStr = "0" + timeStr; + } + if (minutes < 10) { + timeStr += ":0"; + } else { + timeStr += ":"; + } + timeStr += minutes; + + return timeStr; + } + set dailyTime(v: string) { + if (!v || v === "") { + return; + } + + if (!(this.config && + this.config.scan_all_policy && + this.config.scan_all_policy.value && + this.config.scan_all_policy.value.type === "daily")) { + return; + } + + if (!this.config.scan_all_policy.value.parameters) { + this.config.scan_all_policy.value.parameters = { + daily_time: 0 + }; + } + + let values: string[] = v.split(":"); + if (!values || values.length !== 2) { + return; + } + + let hours: number = +values[0]; + let minutes: number = +values[1]; + //Convert to UTC time + let timezoneOffset: number = this._localTime.getTimezoneOffset(); + let utcTimes: number = hours * ONE_HOUR_SECONDS + minutes * 60; + utcTimes += timezoneOffset * 60; + if (utcTimes < 0) { + utcTimes += ONE_DAY_SECONDS; + } + + if (utcTimes >= ONE_DAY_SECONDS) { + utcTimes -= ONE_DAY_SECONDS; + } + + this.config.scan_all_policy.value.parameters.daily_time = utcTimes; + } + + @ViewChild("systemConfigFrom") systemSettingsForm: NgForm; + + get editable(): boolean { + return this.vulnerabilityConfig && + this.vulnerabilityConfig.scan_all_policy && + this.vulnerabilityConfig.scan_all_policy.editable; + } + + get isValid(): boolean { + return this.systemSettingsForm && this.systemSettingsForm.valid; + } + + get showTimePicker(): boolean { + return this.vulnerabilityConfig && + this.vulnerabilityConfig.scan_all_policy && + this.vulnerabilityConfig.scan_all_policy.value && + this.vulnerabilityConfig.scan_all_policy.value.type === "daily"; + } + + constructor( + private scanningService: ScanningResultService, + private errorHandler: ErrorHandler, + private translate: TranslateService) { } + + scanNow(): void { + toPromise(this.scanningService.startScanningAll()) + .then(() => { + this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_SUCCESS").subscribe((res: string) => { + this.errorHandler.info(res); + }); + //TODO: + //Change button disable status. + }) + .catch(error => this.errorHandler.error(error)) + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index d1cdfded4..cb3123449 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -22,6 +22,7 @@ import { INLINE_ALERT_DIRECTIVES } from './inline-alert/index'; import { DATETIME_PICKER_DIRECTIVES } from './datetime-picker/index'; import { VULNERABILITY_DIRECTIVES } from './vulnerability-scanning/index'; import { PUSH_IMAGE_BUTTON_DIRECTIVES } from './push-image/index'; +import { CONFIGURATION_DIRECTIVES } from './config/index'; import { SystemInfoService, @@ -37,7 +38,9 @@ import { TagService, TagDefaultService, ScanningResultService, - ScanningResultDefaultService + ScanningResultDefaultService, + ConfigurationService, + ConfigurationDefaultService } from './service/index'; import { ErrorHandler, @@ -68,7 +71,8 @@ export const DefaultServiceConfig: IServiceConfig = { langMessageLoader: "local", langMessagePathForHttpLoader: "i18n/langs/", langMessageFileSuffixForHttpLoader: "-lang.json", - localI18nMessageVariableMap: {} + localI18nMessageVariableMap: {}, + configurationEndpoint: "/api/configurations" }; /** @@ -103,7 +107,10 @@ export interface HarborModuleConfig { tagService?: Provider, //Service implementation for vulnerability scanning - scanningService?: Provider + scanningService?: Provider, + + //Service implementation for configuration + configService?: Provider } /** @@ -145,7 +152,8 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co CREATE_EDIT_RULE_DIRECTIVES, DATETIME_PICKER_DIRECTIVES, VULNERABILITY_DIRECTIVES, - PUSH_IMAGE_BUTTON_DIRECTIVES + PUSH_IMAGE_BUTTON_DIRECTIVES, + CONFIGURATION_DIRECTIVES ], exports: [ LOG_DIRECTIVES, @@ -164,6 +172,7 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co DATETIME_PICKER_DIRECTIVES, VULNERABILITY_DIRECTIVES, PUSH_IMAGE_BUTTON_DIRECTIVES, + CONFIGURATION_DIRECTIVES, TranslateModule ], providers: [] @@ -176,13 +185,14 @@ export class HarborLibraryModule { providers: [ config.config || { provide: SERVICE_CONFIG, useValue: DefaultServiceConfig }, config.errorHandler || { provide: ErrorHandler, useClass: DefaultErrorHandler }, - config.systemInfoService || { provide: SystemInfoService,useClass: SystemInfoDefaultService }, + config.systemInfoService || { provide: SystemInfoService, useClass: SystemInfoDefaultService }, config.logService || { provide: AccessLogService, useClass: AccessLogDefaultService }, config.endpointService || { provide: EndpointService, useClass: EndpointDefaultService }, config.replicationService || { provide: ReplicationService, useClass: ReplicationDefaultService }, config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService }, config.tagService || { provide: TagService, useClass: TagDefaultService }, config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService }, + config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }, //Do initializing TranslateServiceInitializer, { @@ -201,13 +211,14 @@ export class HarborLibraryModule { providers: [ config.config || { provide: SERVICE_CONFIG, useValue: DefaultServiceConfig }, config.errorHandler || { provide: ErrorHandler, useClass: DefaultErrorHandler }, - config.systemInfoService || { provide: SystemInfoService,useClass: SystemInfoDefaultService }, + config.systemInfoService || { provide: SystemInfoService, useClass: SystemInfoDefaultService }, config.logService || { provide: AccessLogService, useClass: AccessLogDefaultService }, config.endpointService || { provide: EndpointService, useClass: EndpointDefaultService }, config.replicationService || { provide: ReplicationService, useClass: ReplicationDefaultService }, config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService }, config.tagService || { provide: TagService, useClass: TagDefaultService }, config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService }, + config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService } ] }; } diff --git a/src/ui_ng/lib/src/index.ts b/src/ui_ng/lib/src/index.ts index 39f5e7126..a74986038 100644 --- a/src/ui_ng/lib/src/index.ts +++ b/src/ui_ng/lib/src/index.ts @@ -14,4 +14,5 @@ export * from './replication/index'; export * from './vulnerability-scanning/index'; export * from './i18n/index'; export * from './push-image/index'; -export * from './third-party/index'; \ No newline at end of file +export * from './third-party/index'; +export * from './config/index'; \ No newline at end of file diff --git a/src/ui_ng/lib/src/service.config.ts b/src/ui_ng/lib/src/service.config.ts index 40281feec..07f3bf009 100644 --- a/src/ui_ng/lib/src/service.config.ts +++ b/src/ui_ng/lib/src/service.config.ts @@ -172,4 +172,12 @@ export interface IServiceConfig { * @memberOf IServiceConfig */ localI18nMessageVariableMap?: { [key: string]: any }; + + /** + * The base endpoint of configuration service. + * + * @type {string} + * @memberOf IServiceConfig + */ + configurationEndpoint?: string; } \ No newline at end of file diff --git a/src/ui_ng/lib/src/service/configuration.service.spec.ts b/src/ui_ng/lib/src/service/configuration.service.spec.ts new file mode 100644 index 000000000..f06aa796b --- /dev/null +++ b/src/ui_ng/lib/src/service/configuration.service.spec.ts @@ -0,0 +1,42 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { ConfigurationService, ConfigurationDefaultService } from './configuration.service'; +import { SharedModule } from '../shared/shared.module'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; + +describe('ConfigurationService', () => { + const mockConfig: IServiceConfig = { + configurationEndpoint: "/api/configurations/testing" + }; + + let config: IServiceConfig; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule + ], + providers: [ + ConfigurationDefaultService, + { + provide: ConfigurationService, + useClass: ConfigurationDefaultService + }, { + provide: SERVICE_CONFIG, + useValue: mockConfig + }] + }); + + config = TestBed.get(SERVICE_CONFIG); + }); + + it('should be initialized', inject([ConfigurationDefaultService], (service: ConfigurationService) => { + expect(service).toBeTruthy(); + })); + + it('should inject the right config', () => { + expect(config).toBeTruthy(); + expect(config.configurationEndpoint).toEqual("/api/configurations/testing"); + }); + +}); diff --git a/src/ui_ng/lib/src/service/configuration.service.ts b/src/ui_ng/lib/src/service/configuration.service.ts new file mode 100644 index 000000000..18ae26c1d --- /dev/null +++ b/src/ui_ng/lib/src/service/configuration.service.ts @@ -0,0 +1,69 @@ +import { Observable } from 'rxjs/Observable'; +import { Injectable, Inject } from "@angular/core"; +import 'rxjs/add/observable/of'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; +import { Http } from '@angular/http'; +import { HTTP_JSON_OPTIONS } from '../utils'; +import { Configuration } from '../config/config' + + +/** + * Service used to get and save registry-related configurations. + * + * @export + * @abstract + * @class ConfigurationService + */ +export abstract class ConfigurationService { + + /** + * Get configurations. + * + * @abstract + * @returns {(Observable | Promise | Configuration)} + * + * @memberOf ConfigurationService + */ + abstract getConfigurations(): Observable | Promise | Configuration; + + /** + * Save configurations. + * + * @abstract + * @returns {(Observable | Promise | Configuration)} + * + * @memberOf ConfigurationService + */ + abstract saveConfigurations(changedConfigs: any | { [key: string]: any | any[] }): Observable | Promise | any; +} + +@Injectable() +export class ConfigurationDefaultService extends ConfigurationService { + _baseUrl: string; + + constructor( + private http: Http, + @Inject(SERVICE_CONFIG) private config: IServiceConfig) { + super(); + + this._baseUrl = this.config && this.config.configurationEndpoint ? + this.config.configurationEndpoint : "/api/configurations"; + } + + getConfigurations(): Observable | Promise | Configuration { + return this.http.get(this._baseUrl, HTTP_JSON_OPTIONS).toPromise() + .then(response => response.json() as Configuration) + .catch(error => Promise.reject(error)); + } + + saveConfigurations(changedConfigs: any | { [key: string]: any | any[] }): Observable | Promise | any { + if (!changedConfigs) { + return Promise.reject("Bad argument!"); + } + + return this.http.put(this._baseUrl, JSON.stringify(changedConfigs), HTTP_JSON_OPTIONS) + .toPromise() + .then(() => { }) + .catch(error => Promise.reject(error)); + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/service/index.ts b/src/ui_ng/lib/src/service/index.ts index efa100f82..2b10f7ebd 100644 --- a/src/ui_ng/lib/src/service/index.ts +++ b/src/ui_ng/lib/src/service/index.ts @@ -6,4 +6,5 @@ export * from './replication.service'; export * from './repository.service'; export * from './tag.service'; export * from './RequestQueryParams'; -export * from './scanning.service'; \ No newline at end of file +export * from './scanning.service'; +export * from './configuration.service'; \ No newline at end of file diff --git a/src/ui_ng/lib/src/service/scanning.service.ts b/src/ui_ng/lib/src/service/scanning.service.ts index 4f073f9ff..ab4e2c035 100644 --- a/src/ui_ng/lib/src/service/scanning.service.ts +++ b/src/ui_ng/lib/src/service/scanning.service.ts @@ -53,6 +53,16 @@ export abstract class ScanningResultService { * @memberOf ScanningResultService */ abstract startVulnerabilityScanning(repoName: string, tagId: string): Observable | Promise | any; + + /** + * Trigger the scanning all action. + * + * @abstract + * @returns {(Observable | Promise | any)} + * + * @memberOf ScanningResultService + */ + abstract startScanningAll(): Observable | Promise | any; } @Injectable() @@ -95,4 +105,10 @@ export class ScanningResultDefaultService extends ScanningResultService { .then(() => { return true }) .catch(error => Promise.reject(error)); } + + startScanningAll(): Observable | Promise | any { + return this.http.post(`${this._baseUrl}/scanAll`,{}).toPromise() + .then(() => {return true}) + .catch(error => Promise.reject(error)); + } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts index ce76bd34b..d26651d6b 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts @@ -117,6 +117,10 @@ export class ResultTipComponent implements OnInit { return "VULNERABILITY.SINGULAR"; } + packageText(count: number): string { + return count > 1 ? "VULNERABILITY.PACKAGES" : "VULNERABILITY.PACKAGE"; + } + public get completeTimestamp(): Date { return this.summary && this.summary.update_time ? this.summary.update_time : new Date(); } diff --git a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts index 06aa794df..3018d3079 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts @@ -13,23 +13,23 @@ export const TIP_COMPONENT_HTML: string = `
- {{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{ highSuffix | translate }} + {{highCount}} {{packageText(highCount) | translate }} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{ highSuffix | translate }}
- {{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{ mediumSuffix | translate }} + {{mediumCount}} {{packageText(mediumCount) | translate }} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{ mediumSuffix | translate }}
- {{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{ lowSuffix | translate }} + {{lowCount}} {{packageText(lowCount) | translate }} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{ lowSuffix | translate }}
- {{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{ unknownSuffix | translate }} + {{unknownCount}} {{packageText(unknownCount) | translate }} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{ unknownSuffix | translate }}
- {{noneCount}} {{'VULNERABILITY.SEVERITY.NONE' | translate }} {{ noneSuffix | translate }} + {{noneCount}} {{packageText(noneCount) | translate }} {{'VULNERABILITY.SEVERITY.NONE' | translate }} {{ noneSuffix | translate }}
diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index 7e2090f22..190f33685 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -31,7 +31,7 @@ "clarity-icons": "^0.9.8", "clarity-ui": "^0.9.8", "core-js": "^2.4.1", - "harbor-ui": "^0.2.25", + "harbor-ui": "^0.2.40", "intl": "^1.2.5", "mutationobserver-shim": "^0.3.2", "ngx-cookie": "^1.0.0", diff --git a/src/ui_ng/src/app/config/auth/config-auth.component.ts b/src/ui_ng/src/app/config/auth/config-auth.component.ts index 376e0f1e0..b2a053dda 100644 --- a/src/ui_ng/src/app/config/auth/config-auth.component.ts +++ b/src/ui_ng/src/app/config/auth/config-auth.component.ts @@ -15,7 +15,7 @@ import { Component, Input, ViewChild } from '@angular/core'; import { NgForm } from '@angular/forms'; import { Subscription } from 'rxjs/Subscription'; -import { Configuration } from '../config'; +import { Configuration } from 'harbor-ui'; @Component({ selector: 'config-auth', diff --git a/src/ui_ng/src/app/config/config.component.html b/src/ui_ng/src/app/config/config.component.html index 65aef9879..060a4b21e 100644 --- a/src/ui_ng/src/app/config/config.component.html +++ b/src/ui_ng/src/app/config/config.component.html @@ -15,50 +15,24 @@ +
-
-
-
- - - - - {{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }} - - -
-
-
+
-
-
-
- - - - - {{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}} - -
-
-
+ +
+
+
@@ -67,6 +41,7 @@ +
\ No newline at end of file diff --git a/src/ui_ng/src/app/config/config.component.ts b/src/ui_ng/src/app/config/config.component.ts index 259d9ba82..b99734116 100644 --- a/src/ui_ng/src/app/config/config.component.ts +++ b/src/ui_ng/src/app/config/config.component.ts @@ -13,12 +13,9 @@ // limitations under the License. import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; -import { NgForm } from '@angular/forms'; import { ConfigurationService } from './config.service'; -import { Configuration } from './config'; import { ConfirmationTargets, ConfirmationState } from '../shared/shared.const';; -import { StringValueItem } from './config'; import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service'; import { Subscription } from 'rxjs/Subscription'; import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message' @@ -29,13 +26,22 @@ import { ConfigurationEmailComponent } from './email/config-email.component'; import { AppConfigService } from '../app-config.service'; import { SessionService } from '../shared/session.service'; import { MessageHandlerService } from '../shared/message-handler/message-handler.service'; +import { + Configuration, + StringValueItem, + ComplexValueItem, + ReplicationConfigComponent, + SystemSettingsComponent, + VulnerabilityConfigComponent +} from 'harbor-ui'; const fakePass = "aWpLOSYkIzJTTU4wMDkx"; const TabLinkContentMap = { "config-auth": "authentication", "config-replication": "replication", "config-email": "email", - "config-system": "system_settings" + "config-system": "system_settings", + "config-vulnerability": "vulnerability" }; @Component({ @@ -52,8 +58,9 @@ export class ConfigurationComponent implements OnInit, OnDestroy { testingMailOnGoing: boolean = false; testingLDAPOnGoing: boolean = false; - @ViewChild("repoConfigFrom") repoConfigForm: NgForm; - @ViewChild("systemConfigFrom") systemConfigForm: NgForm; + @ViewChild(ReplicationConfigComponent) replicationConfig: ReplicationConfigComponent; + @ViewChild(SystemSettingsComponent) systemSettingsConfig: SystemSettingsComponent; + @ViewChild(VulnerabilityConfigComponent) vulnerabilityConfig: VulnerabilityConfigComponent; @ViewChild(ConfigurationEmailComponent) mailConfig: ConfigurationEmailComponent; @ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent; @@ -64,6 +71,11 @@ export class ConfigurationComponent implements OnInit, OnDestroy { private appConfigService: AppConfigService, private session: SessionService) { } + consoleTest(): void { + console.log(this.allConfig, this.originalCopy); + console.log("-------------"); + console.log(this.getChanges()); + } isCurrentTabLink(tabId: string): boolean { return this.currentTabId === tabId; } @@ -101,6 +113,9 @@ export class ConfigurationComponent implements OnInit, OnDestroy { case "config-system": properties = ["token_expiration"]; break; + case "config-vulnerability": + properties = ["scan_all_policy"]; + break; default: return null; } @@ -146,10 +161,12 @@ export class ConfigurationComponent implements OnInit, OnDestroy { } public isValid(): boolean { - return this.repoConfigForm && - this.repoConfigForm.valid && - this.systemConfigForm && - this.systemConfigForm.valid && + return this.replicationConfig && + this.replicationConfig.isValid && + this.systemSettingsConfig && + this.systemSettingsConfig.isValid && + this.vulnerabilityConfig && + this.vulnerabilityConfig.isValid && this.mailConfig && this.mailConfig.isValid() && this.authConfig && @@ -191,7 +208,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy { } public tabLinkClick(tabLink: string) { - //Whether has unsave changes in current tab + //Whether has unsaved changes in current tab let changes = this.hasUnsavedChangesOfCurrentTab(); if (!changes) { this.currentTabId = tabLink; @@ -210,6 +227,14 @@ export class ConfigurationComponent implements OnInit, OnDestroy { public save(): void { let changes = this.getChanges(); if (!this.isEmpty(changes)) { + //Fix policy parameters issue + let scanningAllPolicy = changes["scan_all_policy"]; + if (scanningAllPolicy && + scanningAllPolicy.type !== "daily" && + scanningAllPolicy.parameters) { + delete (scanningAllPolicy.parameters); + } + this.onGoing = true; this.configService.saveConfiguration(changes) .then(response => { @@ -247,7 +272,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy { if (!this.isEmpty(changes)) { this.confirmUnsavedChanges(changes); } else { - //Inprop situation, should not come here + //Invalid situation, should not come here console.error("Nothing changed"); } } @@ -260,7 +285,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy { * @memberOf ConfigurationComponent */ public testMailServer(): void { - if(this.testingMailOnGoing){ + if (this.testingMailOnGoing) { return;//Should not come here } let mailSettings = {}; @@ -296,7 +321,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy { } public testLDAPServer(): void { - if(this.testingLDAPOnGoing){ + if (this.testingLDAPOnGoing) { return;//Should not come here } @@ -364,14 +389,14 @@ export class ConfigurationComponent implements OnInit, OnDestroy { retrieveConfig(): void { this.onGoing = true; this.configService.getConfiguration() - .then(configurations => { + .then((configurations: Configuration) => { this.onGoing = false; //Add two password fields configurations.email_password = new StringValueItem(fakePass, true); configurations.ldap_search_password = new StringValueItem(fakePass, true); - this.allConfig = configurations; + this.allConfig = configurations; //Keep the original copy of the data this.originalCopy = this.clone(configurations); }) @@ -390,8 +415,8 @@ export class ConfigurationComponent implements OnInit, OnDestroy { * * @memberOf ConfigurationComponent */ - getChanges(): any { - let changes = {}; + getChanges(): { [key: string]: any | any[] } { + let changes: { [key: string]: any | any[] } = {}; if (!this.allConfig || !this.originalCopy) { return changes; } @@ -399,16 +424,16 @@ export class ConfigurationComponent implements OnInit, OnDestroy { for (let prop in this.allConfig) { let field = this.originalCopy[prop]; if (field && field.editable) { - if (field.value != this.allConfig[prop].value) { + if (!this.compareValue(field.value, this.allConfig[prop].value)) { changes[prop] = this.allConfig[prop].value; - //Fix boolean issue - if (typeof field.value === "boolean") { - changes[prop] = changes[prop] ? "1" : "0"; + //Number + if (typeof field.value === "number") { + changes[prop] = +changes[prop]; } //Trim string value - if(typeof field.value === "string") { - changes[prop] = (''+changes[prop]).trim(); + if (typeof field.value === "string") { + changes[prop] = ('' + changes[prop]).trim(); } } } @@ -417,6 +442,19 @@ export class ConfigurationComponent implements OnInit, OnDestroy { return changes; } + //private + compareValue(a: any, b: any): boolean { + if ((a && !b) || (!a && b)) return false; + if (!a && !b) return true; + + return JSON.stringify(a) === JSON.stringify(b); + } + + //private + isEmpty(obj: any): boolean { + return !obj || JSON.stringify(obj) === "{}"; + } + /** * * Deep clone the configuration object @@ -428,18 +466,11 @@ export class ConfigurationComponent implements OnInit, OnDestroy { * @memberOf ConfigurationComponent */ clone(src: Configuration): Configuration { - let dest = new Configuration(); if (!src) { - return dest;//Empty + return new Configuration();//Empty } - for (let prop in src) { - if (src[prop]) { - dest[prop] = Object.assign({}, src[prop]); //Deep copy inner object - } - } - - return dest; + return JSON.parse(JSON.stringify(src)); } /** @@ -464,14 +495,6 @@ export class ConfigurationComponent implements OnInit, OnDestroy { } } - isEmpty(obj: any) { - for (let key in obj) { - if (obj.hasOwnProperty(key)) - return false; - } - return true; - } - disabled(prop: any): boolean { return !(prop && prop.editable); } diff --git a/src/ui_ng/src/app/config/config.service.ts b/src/ui_ng/src/app/config/config.service.ts index 34d41d0c1..acc380777 100644 --- a/src/ui_ng/src/app/config/config.service.ts +++ b/src/ui_ng/src/app/config/config.service.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { Headers, Http, RequestOptions } from '@angular/http'; import 'rxjs/add/operator/toPromise'; -import { Configuration } from './config'; +import { Configuration } from 'harbor-ui'; const configEndpoint = "/api/configurations"; const emailEndpoint = "/api/email/ping"; diff --git a/src/ui_ng/src/app/config/email/config-email.component.ts b/src/ui_ng/src/app/config/email/config-email.component.ts index 683573448..93f18921c 100644 --- a/src/ui_ng/src/app/config/email/config-email.component.ts +++ b/src/ui_ng/src/app/config/email/config-email.component.ts @@ -14,7 +14,7 @@ import { Component, Input, ViewChild } from '@angular/core'; import { NgForm } from '@angular/forms'; -import { Configuration } from '../config'; +import { Configuration } from 'harbor-ui'; @Component({ selector: 'config-email', diff --git a/src/ui_ng/src/i18n/lang/en-us-lang.json b/src/ui_ng/src/i18n/lang/en-us-lang.json index a324c4573..e241087d7 100644 --- a/src/ui_ng/src/i18n/lang/en-us-lang.json +++ b/src/ui_ng/src/i18n/lang/en-us-lang.json @@ -403,6 +403,15 @@ "UID": "LDAP UID", "SCOPE": "LDAP Scope" }, + "SCANNING": { + "TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!", + "TITLE": "Vulnerability Scanning", + "SCAN_ALL": "Scan All", + "SCAN_NOW": "SCAN NOW", + "NONE_POLICY": "None", + "DAILY_POLICY": "Daily At", + "REFRESH_POLICY": "Upon Refresh" + }, "TEST_MAIL_SUCCESS": "Connection to mail server is verified.", "TEST_LDAP_SUCCESS": "Connection to LDAP server is verified.", "TEST_MAIL_FAILED": "Failed to verify mail server with error: {{param}}.", diff --git a/src/ui_ng/src/i18n/lang/es-es-lang.json b/src/ui_ng/src/i18n/lang/es-es-lang.json index 9e4100c04..8cd8f33a9 100644 --- a/src/ui_ng/src/i18n/lang/es-es-lang.json +++ b/src/ui_ng/src/i18n/lang/es-es-lang.json @@ -404,6 +404,15 @@ "UID": "LDAP UID", "SCOPE": "LDAP Ámbito" }, + "SCANNING": { + "TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!", + "TITLE": "Vulnerability Scanning", + "SCAN_ALL": "Scan All", + "SCAN_NOW": "SCAN NOW", + "NONE_POLICY": "None", + "DAILY_POLICY": "Daily At", + "REFRESH_POLICY": "Upon Refresh" + }, "TEST_MAIL_SUCCESS": "La conexión al servidor de correo ha sido verificada.", "TEST_LDAP_SUCCESS": "La conexión al servidor LDAP ha sido verificada.", "TEST_MAIL_FAILED": "Fallo al verificar el servidor de correo con el error: {{param}}.", diff --git a/src/ui_ng/src/i18n/lang/zh-cn-lang.json b/src/ui_ng/src/i18n/lang/zh-cn-lang.json index b61495d73..1f79be8a7 100644 --- a/src/ui_ng/src/i18n/lang/zh-cn-lang.json +++ b/src/ui_ng/src/i18n/lang/zh-cn-lang.json @@ -403,6 +403,15 @@ "UID": "LDAP用户UID的属性", "SCOPE": "LDAP搜索范围" }, + "SCANNING": { + "TRIGGER_SCAN_ALL_SUCCESS": "成功启动扫描所有镜像任务!", + "TITLE": "缺陷扫描", + "SCAN_ALL": "扫描所有", + "SCAN_NOW": "开始扫描", + "NONE_POLICY": "无", + "DAILY_POLICY": "每日", + "REFRESH_POLICY": "缺陷库刷新后" + }, "TEST_MAIL_SUCCESS": "邮件服务器的连通正常。", "TEST_LDAP_SUCCESS": "LDAP服务器的连通正常。", "TEST_MAIL_FAILED": "验证邮件服务器失败,错误: {{param}}。",