Refactor config component (#16064)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
孙世军 2021-12-01 10:07:46 +08:00 committed by GitHub
parent 43912674b1
commit 846d690b85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 416 additions and 494 deletions

View File

@ -443,9 +443,9 @@
</form> </form>
<div> <div>
<button type="button" id="config_auth_save" class="btn btn-primary" (click)="save()" <button type="button" id="config_auth_save" class="btn btn-primary" (click)="save()"
[disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button> [disabled]="!isValid() || !hasChanges() || inProcess()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" <button type="button" class="btn btn-outline" (click)="cancel()"
[disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button> [disabled]="!isValid() || !hasChanges() || inProcess()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" id="ping-test" class="btn btn-outline" (click)="pingTestServer()" *ngIf="showTestingServerBtn" <button type="button" id="ping-test" class="btn btn-outline" (click)="pingTestServer()" *ngIf="showTestingServerBtn"
[disabled]="!isConfigValidForTesting()">{{(showLdap?'BUTTON.TEST_LDAP':'BUTTON.TEST_OIDC') | translate}}</button> [disabled]="!isConfigValidForTesting()">{{(showLdap?'BUTTON.TEST_LDAP':'BUTTON.TEST_OIDC') | translate}}</button>
<span id="forTestingLDAP" class="spinner spinner-inline" [hidden]="hideTestingSpinner"></span> <span id="forTestingLDAP" class="spinner spinner-inline" [hidden]="hideTestingSpinner"></span>

View File

@ -1,17 +1,16 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { ConfirmMessageHandler } from '../config.msg.utils';
import { AppConfigService } from '../../../../services/app-config.service'; import { AppConfigService } from '../../../../services/app-config.service';
import { ConfigurationService } from '../../../../services/config.service'; import { ConfigurationService } from '../../../../services/config.service';
import { ConfigurationAuthComponent } from './config-auth.component'; import { ConfigurationAuthComponent } from './config-auth.component';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { ErrorHandler } from "../../../../shared/units/error-handler";
import { SystemInfoService } from "../../../../shared/services"; import { SystemInfoService } from "../../../../shared/services";
import { clone } from '../../../../shared/units/utils'; import { clone } from '../../../../shared/units/utils';
import { CONFIG_AUTH_MODE } from '../../../../shared/entities/shared.const'; import { CONFIG_AUTH_MODE } from '../../../../shared/entities/shared.const';
import { ConfigService } from "../config.service";
import { Configuration } from '../config';
import { SharedTestingModule } from "../../../../shared/shared.module";
describe('ConfigurationAuthComponent', () => { describe('ConfigurationAuthComponent', () => {
let component: ConfigurationAuthComponent; let component: ConfigurationAuthComponent;
@ -27,7 +26,25 @@ describe('ConfigurationAuthComponent', () => {
let fakeAppConfigService = { let fakeAppConfigService = {
load: () => of(null) load: () => of(null)
}; };
let fakeConfirmMessageService = null; const fakeConfigService = {
config: new Configuration(),
getConfig() {
return this.config;
},
setConfig(c) {
this.config = c;
},
getOriginalConfig() {
return new Configuration();
},
getLoadingConfigStatus() {
return false;
},
updateConfig() {
},
resetConfig() {
}
};
let fakeSystemInfoService = { let fakeSystemInfoService = {
getSystemInfo: function () { getSystemInfo: function () {
return of({ return of({
@ -39,17 +56,14 @@ describe('ConfigurationAuthComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot(), SharedTestingModule
FormsModule
], ],
declarations: [ConfigurationAuthComponent], declarations: [ConfigurationAuthComponent],
providers: [ providers: [
ErrorHandler,
TranslateService,
{ provide: MessageHandlerService, useValue: fakeMessageHandlerService }, { provide: MessageHandlerService, useValue: fakeMessageHandlerService },
{ provide: ConfigurationService, useValue: fakeConfigurationService }, { provide: ConfigurationService, useValue: fakeConfigurationService },
{ provide: AppConfigService, useValue: fakeAppConfigService }, { provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: ConfirmMessageHandler, useValue: fakeConfirmMessageService }, { provide: ConfigService, useValue: fakeConfigService },
{ provide: SystemInfoService, useValue: fakeSystemInfoService } { provide: SystemInfoService, useValue: fakeSystemInfoService }
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
@ -59,46 +73,10 @@ describe('ConfigurationAuthComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ConfigurationAuthComponent); fixture = TestBed.createComponent(ConfigurationAuthComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
(component as any).originalConfig = clone(component.currentConfig); fixture.detectChanges();
fixture.autoDetectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should save configuration', async () => {
const selfRegInput: HTMLInputElement = fixture.nativeElement.querySelector("#selfReg");
selfRegInput.dispatchEvent(new Event('click'));
component.currentConfig.self_registration.value = true;
await fixture.whenStable();
const configAuthSaveBtn: HTMLButtonElement = fixture.nativeElement.querySelector("#config_auth_save");
component.onGoing = true;
configAuthSaveBtn.dispatchEvent(new Event('click'));
await fixture.whenStable();
expect(component.onGoing).toBeFalsy();
});
it('should select ldap or uaa', () => {
component.handleOnChange({target: {value: 'ldap_auth'}});
expect(component.currentConfig.self_registration.value).toEqual(false);
});
it('should ping test server when ldap', async () => {
component.currentConfig.auth_mode.value = CONFIG_AUTH_MODE.LDAP_AUTH;
component.currentConfig.ldap_scope.value = 123456;
await fixture.whenStable();
const pingTestBtn = fixture.nativeElement.querySelector("#ping-test");
expect(pingTestBtn).toBeTruthy();
pingTestBtn.dispatchEvent(new Event('click'));
await fixture.whenStable();
expect(component.testingOnGoing).toBeFalsy();
});
it('should ping test server when oidc', async () => {
component.currentConfig.auth_mode.value = CONFIG_AUTH_MODE.OIDC_AUTH;
await fixture.whenStable();
const pingTestBtn = fixture.nativeElement.querySelector("#ping-test");
expect(pingTestBtn).toBeTruthy();
pingTestBtn.dispatchEvent(new Event('click'));
await fixture.whenStable();
expect(component.testingOnGoing).toBeFalsy();
});
}); });

View File

@ -11,68 +11,60 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Input, ViewChild, SimpleChanges, OnChanges, OnInit, Output, EventEmitter } from '@angular/core'; import { Component, ViewChild, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
import { Subscription } from "rxjs";
import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { ConfirmMessageHandler } from '../config.msg.utils';
import { AppConfigService } from '../../../../services/app-config.service'; import { AppConfigService } from '../../../../services/app-config.service';
import { ConfigurationService } from '../../../../services/config.service'; import { ConfigurationService } from '../../../../services/config.service';
import { ErrorHandler } from "../../../../shared/units/error-handler";
import { SystemInfoService } from "../../../../shared/services"; import { SystemInfoService } from "../../../../shared/services";
import { clone, isEmpty, getChanges as getChangesFunc } from "../../../../shared/units/utils"; import { isEmpty, getChanges as getChangesFunc } from "../../../../shared/units/utils";
import { CONFIG_AUTH_MODE } from "../../../../shared/entities/shared.const"; import { CONFIG_AUTH_MODE } from "../../../../shared/entities/shared.const";
import { errorHandler } from "../../../../shared/units/shared.utils"; import { errorHandler } from "../../../../shared/units/shared.utils";
import { Configuration } from "../config"; import { Configuration } from "../config";
import { finalize } from "rxjs/operators"; import { finalize } from "rxjs/operators";
const fakePass = 'aWpLOSYkIzJTTU4wMDkx'; import { ConfigService } from "../config.service";
@Component({ @Component({
selector: 'config-auth', selector: 'config-auth',
templateUrl: 'config-auth.component.html', templateUrl: 'config-auth.component.html',
styleUrls: ['./config-auth.component.scss', '../config.component.scss'] styleUrls: ['./config-auth.component.scss', '../config.component.scss']
}) })
export class ConfigurationAuthComponent implements OnChanges, OnInit { export class ConfigurationAuthComponent implements OnInit {
changeSub: Subscription;
testingOnGoing = false; testingOnGoing = false;
onGoing = false; onGoing = false;
redirectUrl: string; redirectUrl: string;
// tslint:disable-next-line:no-input-rename
@Input('allConfig') currentConfig: Configuration = new Configuration();
private originalConfig: Configuration;
@ViewChild('authConfigFrom') authForm: NgForm; @ViewChild('authConfigFrom') authForm: NgForm;
@Output() refreshAllconfig = new EventEmitter<any>();
get currentConfig(): Configuration {
return this.conf.getConfig();
}
set currentConfig(c: Configuration) {
this.conf.setConfig(c);
}
constructor( constructor(
private msgHandler: MessageHandlerService, private msgHandler: MessageHandlerService,
private configService: ConfigurationService, private configService: ConfigurationService,
private appConfigService: AppConfigService, private appConfigService: AppConfigService,
private confirmMessageHandler: ConfirmMessageHandler, private conf: ConfigService,
private systemInfo: SystemInfoService, private systemInfo: SystemInfoService,
private errorHandlerEntity: ErrorHandler,
) { ) {
} }
ngOnInit() { ngOnInit() {
this.conf.resetConfig();
this.getSystemInfo(); this.getSystemInfo();
} }
getSystemInfo(): void { getSystemInfo(): void {
this.systemInfo.getSystemInfo() this.systemInfo.getSystemInfo()
.subscribe(systemInfo => (this.redirectUrl = systemInfo.external_url) .subscribe(systemInfo => (this.redirectUrl = systemInfo.external_url)
, error => this.errorHandlerEntity.error(error)); , error => this.msgHandler.error(error));
} }
get checkable() { get checkable() {
return this.currentConfig && return this.currentConfig &&
this.currentConfig.self_registration && this.currentConfig.self_registration &&
this.currentConfig.self_registration.value === true; this.currentConfig.self_registration.value === true;
} }
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["currentConfig"]) {
this.originalConfig = clone(this.currentConfig);
}
}
public get showLdap(): boolean { public get showLdap(): boolean {
return this.currentConfig && return this.currentConfig &&
this.currentConfig.auth_mode && this.currentConfig.auth_mode &&
@ -99,11 +91,15 @@ export class ConfigurationAuthComponent implements OnChanges, OnInit {
} }
} }
public isValid(): boolean { isValid(): boolean {
return this.authForm && this.authForm.valid; return this.authForm && this.authForm.valid;
} }
public hasChanges(): boolean { inProcess(): boolean {
return this.onGoing || this.conf.getLoadingConfigStatus();
}
hasChanges(): boolean {
return !isEmpty(this.getChanges()); return !isEmpty(this.getChanges());
} }
@ -168,7 +164,7 @@ export class ConfigurationAuthComponent implements OnChanges, OnInit {
this.msgHandler.showSuccess('CONFIG.TEST_OIDC_SUCCESS'); this.msgHandler.showSuccess('CONFIG.TEST_OIDC_SUCCESS');
}, error => { }, error => {
this.testingOnGoing = false; this.testingOnGoing = false;
this.errorHandlerEntity.error(error); this.msgHandler.error(error);
}); });
} }
} }
@ -180,12 +176,15 @@ export class ConfigurationAuthComponent implements OnChanges, OnInit {
} }
public isConfigValidForTesting(): boolean { public isConfigValidForTesting(): boolean {
if (!this.authForm || !this.currentConfig) {
return true;
}
return this.isValid() && return this.isValid() &&
!this.testingOnGoing; !this.testingOnGoing && !this.inProcess();
} }
public getChanges() { public getChanges() {
let allChanges = getChangesFunc(this.originalConfig, this.currentConfig); let allChanges = getChangesFunc(this.conf.getOriginalConfig(), this.currentConfig);
let changes = {}; let changes = {};
for (let prop in allChanges) { for (let prop in allChanges) {
if (prop.startsWith('ldap_') if (prop.startsWith('ldap_')
@ -235,7 +234,7 @@ export class ConfigurationAuthComponent implements OnChanges, OnInit {
this.configService.saveConfiguration(changes) this.configService.saveConfiguration(changes)
.subscribe(response => { .subscribe(response => {
this.onGoing = false; this.onGoing = false;
this.refreshAllconfig.emit(); this.conf.updateConfig();
// Reload bootstrap option // Reload bootstrap option
this.appConfigService.load().subscribe(() => { } this.appConfigService.load().subscribe(() => { }
, error => console.error('Failed to reload bootstrap option with error: ', error)); , error => console.error('Failed to reload bootstrap option with error: ', error));
@ -259,7 +258,7 @@ export class ConfigurationAuthComponent implements OnChanges, OnInit {
public cancel(): void { public cancel(): void {
let changes = this.getChanges(); let changes = this.getChanges();
if (!isEmpty(changes)) { if (!isEmpty(changes)) {
this.confirmMessageHandler.confirmUnsavedChanges(changes); this.conf.confirmUnsavedChanges(changes);
} else { } else {
// Invalid situation, should not come here // Invalid situation, should not come here
console.error('Nothing changed'); console.error('Nothing changed');

View File

@ -1,25 +1,23 @@
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="custom-h2 config-title">{{'CONFIG.TITLE' | translate }}<span class="spinner spinner-inline ml-1 v-mid" [hidden]="inProgress === false"></span></h2> <h2 class="custom-h2 config-title">{{'CONFIG.TITLE' | translate }}<span class="spinner spinner-inline ml-1 v-mid" [hidden]="inProgress === false"></span></h2>
<clr-tabs> <ul class="nav" role="tablist">
<clr-tab> <li role="presentation" class="nav-item" >
<button id="config-auth" clrTabLink>{{'CONFIG.AUTH' | translate}}</button> <button id="config-auth" class="btn btn-link nav-link" type="button"
<clr-tab-content id="authentication" *clrIfActive> routerLink="auth"
<config-auth [allConfig]="allConfig" (refreshAllconfig)="refreshAllconfig()"></config-auth> routerLinkActive="active">{{'CONFIG.AUTH' | translate}}</button>
</clr-tab-content> </li>
</clr-tab> <li role="presentation" class="nav-item" >
<clr-tab> <button id="config-email" class="btn btn-link nav-link" type="button"
<button id="config-email" clrTabLink>{{'CONFIG.EMAIL' | translate }}</button> routerLink="email"
<clr-tab-content id="email" *clrIfActive> routerLinkActive="active">{{'CONFIG.EMAIL' | translate}}</button>
<config-email [mailConfig]="allConfig" (refreshAllconfig)="refreshAllconfig()"></config-email> </li>
</clr-tab-content> <li role="presentation" class="nav-item" >
</clr-tab> <button id="config-system" class="btn btn-link nav-link" type="button"
<clr-tab> routerLink="setting"
<button id="config-system" clrTabLink>{{'CONFIG.SYSTEM' | translate }}</button> routerLinkActive="active">{{'CONFIG.SYSTEM' | translate}}</button>
<clr-tab-content id="system_settings" *clrIfActive> </li>
<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" (reloadSystemConfig)="handleAppConfig($event)" (readOnlyChange)="handleReadyOnlyChange($event)" [hasCAFile]="hasCAFile"></system-settings> </ul>
</clr-tab-content>
</clr-tab>
</clr-tabs>
</div> </div>
</div> </div>
<router-outlet></router-outlet>

View File

@ -1,85 +1,44 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { SessionService } from '../../../shared/services/session.service';
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ClarityModule } from "@clr/angular";
import { AppConfigService } from '../../../services/app-config.service';
import { ConfigurationService } from '../../../services/config.service';
import { ConfigurationComponent } from './config.component'; import { ConfigurationComponent } from './config.component';
import { of } from 'rxjs'; import { SharedTestingModule } from "../../../shared/shared.module";
import { Configuration } from './config'; import { ConfigService } from "./config.service";
import { ConfirmationState, ConfirmationTargets } from "../../../shared/entities/shared.const"; import { Configuration } from "./config";
import { ConfirmationDialogService } from "../../global-confirmation-dialog/confirmation-dialog.service";
import { ConfirmationAcknowledgement } from "../../global-confirmation-dialog/confirmation-state-message";
describe('ConfigurationComponent', () => { describe('ConfigurationComponent', () => {
let component: ConfigurationComponent; let component: ConfigurationComponent;
let fixture: ComponentFixture<ConfigurationComponent>; let fixture: ComponentFixture<ConfigurationComponent>;
let confirmationConfirmFlag = true; const fakeConfigService = {
let confirmationConfirm = () => { getConfig() {
return confirmationConfirmFlag ? of(new ConfirmationAcknowledgement(ConfirmationState.CONFIRMED, {}, ConfirmationTargets.CONFIG)) return new Configuration();
: of(new ConfirmationAcknowledgement(ConfirmationState.CONFIRMED
, {change: { email_password: 'Harbor12345' }, tabId: '1'}, ConfirmationTargets.CONFIG_TAB));
};
let fakeConfirmationDialogService = {
confirmationConfirm$: confirmationConfirm()
};
let mockConfigurationService = {
getConfiguration: () => of(new Configuration()),
confirmationConfirm$: of(new ConfirmationAcknowledgement(ConfirmationState.CONFIRMED, {}, ConfirmationTargets.CONFIG))
};
let fakeSessionService = {
getCurrentUser: function () {
return {
has_admin_role: true,
user_id: 1,
username: 'admin',
email: "",
realname: "admin",
role_name: "admin",
role_id: 1,
comment: "string",
};
}, },
updateAccountSettings: () => of(null), getOriginalConfig() {
renameAdmin: () => of(null), return new Configuration();
},
getLoadingConfigStatus() {
return false;
},
updateConfig() {
},
initConfig() {
}
}; };
let initSpy: jasmine.Spy;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot(), SharedTestingModule
ClarityModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [ConfigurationComponent], declarations: [ConfigurationComponent],
providers: [ providers: [
TranslateService, { provide: ConfigService, useValue: fakeConfigService },
{
provide: SessionService, useValue: {
getCurrentUser: function () {
return "admin";
}
}
},
{ provide: ConfirmationDialogService, useValue: fakeConfirmationDialogService },
{ provide: SessionService, useValue: fakeSessionService },
{ provide: MessageHandlerService, useValue: null },
{
provide: AppConfigService, useValue: {
getConfig: function () {
return { has_ca_root: true };
}
}
},
{
provide: ConfigurationService, useValue: mockConfigurationService
}
] ]
}).compileComponents(); }).compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
initSpy = spyOn(fakeConfigService, "initConfig").and.returnValue(undefined);
fixture = TestBed.createComponent(ConfigurationComponent); fixture = TestBed.createComponent(ConfigurationComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
@ -88,16 +47,8 @@ describe('ConfigurationComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should reset part of allConfig ', async () => { it('should init config', async () => {
confirmationConfirmFlag = false;
component.originalCopy.email_password.value = 'Harbor12345';
component.reset({
email_password: {
value: 'Harbor12345',
editable: true
}
});
await fixture.whenStable(); await fixture.whenStable();
expect(component.allConfig.email_password.value).toEqual('Harbor12345'); expect(initSpy.calls.count()).toEqual(1);
}); });
}); });

View File

@ -11,155 +11,25 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Subscription } from "rxjs"; import { ConfigService } from "./config.service";
import { SessionService } from '../../../shared/services/session.service';
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { AppConfigService } from '../../../services/app-config.service';
import { ConfigurationAuthComponent } from './auth/config-auth.component';
import { ConfigurationEmailComponent } from './email/config-email.component';
import { ConfigurationService } from '../../../services/config.service';
import { Configuration, StringValueItem } from "./config";
import { SystemSettingsComponent } from "./system/system-settings.component";
import { clone, isEmpty } from "../../../shared/units/utils";
import { ConfirmationDialogService } from "../../global-confirmation-dialog/confirmation-dialog.service";
import { ConfirmationState, ConfirmationTargets } from "../../../shared/entities/shared.const";
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
const TabLinkContentMap = {
'config-auth': 'authentication',
'config-replication': 'replication',
'config-email': 'email',
'config-system': 'system_settings',
'config-label': 'system_label',
};
@Component({ @Component({
selector: 'config', selector: 'config',
templateUrl: 'config.component.html', templateUrl: 'config.component.html',
styleUrls: ['config.component.scss'] styleUrls: ['config.component.scss']
}) })
export class ConfigurationComponent implements OnInit, OnDestroy { export class ConfigurationComponent implements OnInit {
allConfig: Configuration = new Configuration(); get inProgress(): boolean {
onGoing = false; return this.conf.getLoadingConfigStatus();
currentTabId = 'config-auth'; // default tab
originalCopy: Configuration = new Configuration();
confirmSub: Subscription;
@ViewChild(SystemSettingsComponent) systemSettingsConfig: SystemSettingsComponent;
@ViewChild(ConfigurationEmailComponent) mailConfig: ConfigurationEmailComponent;
@ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent;
constructor(
private msgHandler: MessageHandlerService,
private configService: ConfigurationService,
private confirmService: ConfirmationDialogService,
private appConfigService: AppConfigService,
private session: SessionService) { }
public get hasAdminRole(): boolean {
return this.session.getCurrentUser() &&
this.session.getCurrentUser().has_admin_role;
} }
public get hasCAFile(): boolean { constructor(private conf: ConfigService) {
return this.appConfigService.getConfig().has_ca_root;
} }
public get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
refreshAllconfig() {
this.retrieveConfig();
}
ngOnInit(): void { ngOnInit(): void {
// First load // First load
// Double confirm the current use has admin role this.conf.initConfig();
let currentUser = this.session.getCurrentUser();
if (currentUser && currentUser.has_admin_role) {
this.retrieveConfig();
}
this.confirmSub = this.confirmService.confirmationConfirm$.subscribe(confirmation => {
if (confirmation &&
confirmation.state === ConfirmationState.CONFIRMED) {
if (confirmation.source === ConfirmationTargets.CONFIG) {
this.reset(confirmation.data);
} else if (confirmation.source === ConfirmationTargets.CONFIG_TAB) {
this.reset(confirmation.data['changes']);
this.currentTabId = confirmation.data['tabId'];
}
}
});
} }
ngOnDestroy(): void {
if (this.confirmSub) {
this.confirmSub.unsubscribe();
}
}
public get inProgress(): boolean {
return this.onGoing;
}
handleReadyOnlyChange(event) {
this.msgHandler.handleReadOnly();
if (!event) {
this.msgHandler.clear();
}
}
handleAppConfig(event) {
// Reload bootstrap option
this.appConfigService.load().subscribe(() => {}
, error => console.error('Failed to reload bootstrap option with error: ', error));
}
retrieveConfig(): void {
this.onGoing = true;
this.configService.getConfiguration()
.subscribe((configurations: Configuration) => {
this.onGoing = false;
// Add two password fields
configurations.email_password = new StringValueItem(fakePass, true);
configurations.ldap_search_password = new StringValueItem(fakePass, true);
configurations.uaa_client_secret = new StringValueItem(fakePass, true);
configurations.oidc_client_secret = new StringValueItem(fakePass, true);
this.allConfig = configurations;
// Keep the original copy of the data
this.originalCopy = clone(configurations);
}, error => {
this.onGoing = false;
this.msgHandler.handleError(error);
});
}
/**
*
* Reset the configuration form
*
* @private
* ** deprecated param {*} changes
*
* @memberOf ConfigurationComponent
*/
reset(changes: any): void {
if (!isEmpty(changes)) {
for (let prop in changes) {
if (this.originalCopy[prop]) {
this.allConfig[prop] = clone(this.originalCopy[prop]);
}
}
} else {
// force reset
this.retrieveConfig();
}
}
disabled(prop: any): boolean {
return !(prop && prop.editable);
}
} }

View File

@ -15,16 +15,35 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { SharedModule } from "../../../shared/shared.module"; import { SharedModule } from "../../../shared/shared.module";
import { ConfigurationComponent } from "./config.component"; import { ConfigurationComponent } from "./config.component";
import { ConfirmMessageHandler } from "./config.msg.utils";
import { ConfigurationAuthComponent } from "./auth/config-auth.component"; import { ConfigurationAuthComponent } from "./auth/config-auth.component";
import { ConfigurationEmailComponent } from "./email/config-email.component"; import { ConfigurationEmailComponent } from "./email/config-email.component";
import { SystemSettingsComponent } from "./system/system-settings.component"; import { SystemSettingsComponent } from "./system/system-settings.component";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { ConfigService } from "./config.service";
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: ConfigurationComponent component: ConfigurationComponent,
children: [
{
path: 'auth',
component: ConfigurationAuthComponent
},
{
path: 'email',
component: ConfigurationEmailComponent
},
{
path: 'setting',
component: SystemSettingsComponent
},
{
path: '',
redirectTo: 'auth',
pathMatch: 'full'
}
]
} }
]; ];
@NgModule({ @NgModule({
@ -39,7 +58,7 @@ const routes: Routes = [
SystemSettingsComponent SystemSettingsComponent
], ],
providers: [ providers: [
ConfirmMessageHandler, ConfigService,
] ]
}) })
export class ConfigurationModule { export class ConfigurationModule {

View File

@ -1,22 +0,0 @@
import { Injectable } from '@angular/core';
import { ConfirmationDialogService } from "../../global-confirmation-dialog/confirmation-dialog.service";
import { ConfirmationTargets } from "../../../shared/entities/shared.const";
import { ConfirmationMessage } from "../../global-confirmation-dialog/confirmation-message";
@Injectable()
export class ConfirmMessageHandler {
constructor(private confirmService: ConfirmationDialogService) {
}
public confirmUnsavedChanges(changes: any) {
let msg = new ConfirmationMessage(
'CONFIG.CONFIRM_TITLE',
'CONFIG.CONFIRM_SUMMARY',
'',
changes,
ConfirmationTargets.CONFIG
);
this.confirmService.openComfirmDialog(msg);
}
}

View File

@ -0,0 +1,41 @@
import { TestBed, inject } from '@angular/core/testing';
import { ConfigureService } from 'ng-swagger-gen/services/configure.service';
import { of } from 'rxjs';
import { SharedTestingModule } from "../../../shared/shared.module";
import { Configuration } from './config';
import { ConfigService } from "./config.service";
describe('ConfigService', () => {
const fakedConfigureService = {
getConfigurations() {
return of(null);
}
};
let getConfigSpy: jasmine.Spy;
beforeEach(() => {
getConfigSpy = spyOn(fakedConfigureService, 'getConfigurations').and.returnValue(of(new Configuration()));
TestBed.configureTestingModule({
imports: [
SharedTestingModule
],
providers: [
ConfigService,
{ provide: ConfigureService, useValue: fakedConfigureService },
]
});
});
it('should be created', inject([ConfigService], (service: ConfigService) => {
expect(service).toBeTruthy();
}));
it('should init config', inject([ConfigService], (service: ConfigService) => {
expect(getConfigSpy.calls.count()).toEqual(0);
service.initConfig();
expect(getConfigSpy.calls.count()).toEqual(1);
// only init once
service.initConfig();
expect(getConfigSpy.calls.count()).toEqual(1);
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,102 @@
import { Injectable } from '@angular/core';
import { ConfirmationDialogService } from "../../global-confirmation-dialog/confirmation-dialog.service";
import { ConfirmationState, ConfirmationTargets } from "../../../shared/entities/shared.const";
import { ConfirmationMessage } from "../../global-confirmation-dialog/confirmation-message";
import { Configuration, StringValueItem } from "./config";
import { ConfigureService } from 'ng-swagger-gen/services/configure.service';
import { clone } from "../../../shared/units/utils";
import { MessageHandlerService } from "../../../shared/services/message-handler.service";
import { finalize } from "rxjs/operators";
import { Subscription } from "rxjs";
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
@Injectable()
export class ConfigService {
private _loadingConfig: boolean = false;
private _hasInit: boolean = false;
private _confirmSub: Subscription;
private _currentConfig: Configuration = new Configuration();
private _originalConfig: Configuration;
constructor(private confirmService: ConfirmationDialogService,
private configureService: ConfigureService,
private msgHandler: MessageHandlerService) {
this._confirmSub = this.confirmService.confirmationConfirm$.subscribe(confirmation => {
if (confirmation &&
confirmation.state === ConfirmationState.CONFIRMED) {
this.resetConfig();
}
});
}
getConfig(): Configuration {
return this._currentConfig;
}
setConfig(c: Configuration) {
this._currentConfig = c;
}
getOriginalConfig(): Configuration {
return this._originalConfig;
}
setOriginalConfig(c: Configuration) {
this._originalConfig = c;
}
getLoadingConfigStatus(): boolean {
return this._loadingConfig;
}
initConfig() {
if (!this._hasInit) {
this.updateConfig();
this._hasInit = true;
}
}
updateConfig() {
this._loadingConfig = true;
this.configureService.getConfigurations()
.pipe(finalize(() => this._loadingConfig = false))
.subscribe(res => {
this._currentConfig = res as Configuration;
// Add password fields
this._currentConfig.email_password = new StringValueItem(fakePass, true);
this._currentConfig.ldap_search_password = new StringValueItem(fakePass, true);
this._currentConfig.uaa_client_secret = new StringValueItem(fakePass, true);
this._currentConfig.oidc_client_secret = new StringValueItem(fakePass, true);
// Keep the original copy of the data
this._originalConfig = clone(this._currentConfig);
// Handle read only
if (this._originalConfig?.read_only?.value) {
this.msgHandler.handleReadOnly();
} else {
this.msgHandler.clear();
}
}, error => {
this.msgHandler.handleError(error);
});
}
resetConfig() {
if (this._originalConfig) {
this._currentConfig = clone(this._originalConfig);
}
}
confirmUnsavedChanges(changes: any) {
let msg = new ConfirmationMessage(
'CONFIG.CONFIRM_TITLE',
'CONFIG.CONFIRM_SUMMARY',
'',
changes,
ConfirmationTargets.CONFIG
);
this.confirmService.openComfirmDialog(msg);
}
}

View File

@ -76,9 +76,9 @@
</form> </form>
<div> <div>
<button type="button" id="config_email_save" class="btn btn-primary" (click)="save()" <button type="button" id="config_email_save" class="btn btn-primary" (click)="save()"
[disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' [disabled]="!isValid() || !hasChanges() || inProgress()">{{'BUTTON.SAVE'
| translate}}</button> | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' <button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges() || inProgress()">{{'BUTTON.CANCEL'
| translate}}</button> | translate}}</button>
<button type="button" id="ping-test" class="btn btn-outline" (click)="testMailServer()" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' <button type="button" id="ping-test" class="btn btn-outline" (click)="testMailServer()" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL'
| translate}}</button> | translate}}</button>

View File

@ -1,13 +1,13 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { ConfirmMessageHandler } from '../config.msg.utils';
import { ConfigurationService } from '../../../../services/config.service'; import { ConfigurationService } from '../../../../services/config.service';
import { ConfigurationEmailComponent } from './config-email.component'; import { ConfigurationEmailComponent } from './config-email.component';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { clone } from '../../../../shared/units/utils'; import { clone } from '../../../../shared/units/utils';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { ConfigService } from "../config.service";
import { SharedTestingModule } from "../../../../shared/shared.module";
import { Configuration } from "../config";
describe('ConfigurationEmailComponent', () => { describe('ConfigurationEmailComponent', () => {
let component: ConfigurationEmailComponent; let component: ConfigurationEmailComponent;
@ -19,17 +19,34 @@ describe('ConfigurationEmailComponent', () => {
let fakeMessageHandlerService = { let fakeMessageHandlerService = {
showSuccess: () => null showSuccess: () => null
}; };
const fakeConfigService = {
config: new Configuration(),
getConfig() {
return this.config;
},
setConfig(c) {
this.config = c;
},
getOriginalConfig() {
return new Configuration();
},
getLoadingConfigStatus() {
return false;
},
updateConfig() {
},
resetConfig() {
}
};
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot(), SharedTestingModule
FormsModule
], ],
declarations: [ConfigurationEmailComponent], declarations: [ConfigurationEmailComponent],
providers: [ providers: [
{ provide: MessageHandlerService, useValue: fakeMessageHandlerService }, { provide: MessageHandlerService, useValue: fakeMessageHandlerService },
TranslateService, { provide: ConfigService, useValue: fakeConfigService },
{ provide: ConfirmMessageHandler, useValue: null },
{ provide: ConfigurationService, useValue: fakeConfigurationService } { provide: ConfigurationService, useValue: fakeConfigurationService }
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
@ -39,7 +56,6 @@ describe('ConfigurationEmailComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ConfigurationEmailComponent); fixture = TestBed.createComponent(ConfigurationEmailComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
(component as any).originalConfig = clone(component.currentConfig);
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -11,33 +11,40 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Input, ViewChild, SimpleChanges, OnChanges, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { ConfirmMessageHandler } from '../config.msg.utils';
import { ConfigurationService } from '../../../../services/config.service'; import { ConfigurationService } from '../../../../services/config.service';
import { Configuration } from "../config"; import { Configuration } from "../config";
import { isEmpty, getChanges as getChangesFunc, clone } from "../../../../shared/units/utils"; import { isEmpty, getChanges as getChangesFunc } from "../../../../shared/units/utils";
import { errorHandler } from "../../../../shared/units/shared.utils"; import { errorHandler } from "../../../../shared/units/shared.utils";
import { ConfigService } from "../config.service";
@Component({ @Component({
selector: 'config-email', selector: 'config-email',
templateUrl: "config-email.component.html", templateUrl: "config-email.component.html",
styleUrls: ['./config-email.component.scss', '../config.component.scss'] styleUrls: ['./config-email.component.scss', '../config.component.scss']
}) })
export class ConfigurationEmailComponent implements OnChanges { export class ConfigurationEmailComponent implements OnInit {
// tslint:disable-next-line:no-input-rename
@Input("mailConfig") currentConfig: Configuration = new Configuration();
@Output() refreshAllconfig = new EventEmitter<any>();
private originalConfig: Configuration;
testingMailOnGoing = false; testingMailOnGoing = false;
onGoing = false; onGoing = false;
@ViewChild("mailConfigFrom", {static: true}) mailForm: NgForm; @ViewChild("mailConfigFrom", {static: true}) mailForm: NgForm;
get currentConfig(): Configuration {
return this.conf.getConfig();
}
set currentConfig(c: Configuration) {
this.conf.setConfig(c);
}
constructor( constructor(
private msgHandler: MessageHandlerService, private msgHandler: MessageHandlerService,
private configService: ConfigurationService, private configService: ConfigurationService,
private confirmMessageHandler: ConfirmMessageHandler) { private conf: ConfigService) {
}
ngOnInit(): void {
this.conf.resetConfig();
} }
disabled(prop: any): boolean { disabled(prop: any): boolean {
@ -48,16 +55,20 @@ export class ConfigurationEmailComponent implements OnChanges {
this.currentConfig.email_insecure.value = !$event; this.currentConfig.email_insecure.value = !$event;
} }
public isValid(): boolean { isValid(): boolean {
return this.mailForm && this.mailForm.valid; return this.mailForm && this.mailForm.valid;
} }
inProgress(): boolean {
return this.onGoing || this.conf.getLoadingConfigStatus();
}
public hasChanges(): boolean { public hasChanges(): boolean {
return !isEmpty(this.getChanges()); return !isEmpty(this.getChanges());
} }
public getChanges() { public getChanges() {
let allChanges = getChangesFunc(this.originalConfig, this.currentConfig); let allChanges = getChangesFunc(this.conf.getOriginalConfig(), this.currentConfig);
let changes = {}; let changes = {};
for (let prop in allChanges) { for (let prop in allChanges) {
if (prop.startsWith('email_')) { if (prop.startsWith('email_')) {
@ -66,13 +77,6 @@ export class ConfigurationEmailComponent implements OnChanges {
} }
return changes; return changes;
} }
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["currentConfig"]) {
this.originalConfig = clone(this.currentConfig);
}
}
/** /**
* *
* Test the connection of specified mail server * Test the connection of specified mail server
@ -121,7 +125,7 @@ export class ConfigurationEmailComponent implements OnChanges {
public isMailConfigValid(): boolean { public isMailConfigValid(): boolean {
return this.isValid() && return this.isValid() &&
!this.testingMailOnGoing; !this.testingMailOnGoing && !this.inProgress();
} }
/** /**
@ -138,7 +142,7 @@ export class ConfigurationEmailComponent implements OnChanges {
.subscribe(response => { .subscribe(response => {
this.onGoing = false; this.onGoing = false;
// refresh allConfig // refresh allConfig
this.refreshAllconfig.emit(); this.conf.updateConfig();
this.msgHandler.showSuccess('CONFIG.SAVE_SUCCESS'); this.msgHandler.showSuccess('CONFIG.SAVE_SUCCESS');
}, error => { }, error => {
this.onGoing = false; this.onGoing = false;
@ -159,7 +163,7 @@ export class ConfigurationEmailComponent implements OnChanges {
public cancel(): void { public cancel(): void {
let changes = this.getChanges(); let changes = this.getChanges();
if (!isEmpty(changes)) { if (!isEmpty(changes)) {
this.confirmMessageHandler.confirmUnsavedChanges(changes); this.conf.confirmUnsavedChanges(changes);
} else { } else {
// Invalid situation, should not come here // Invalid situation, should not come here
console.error('Nothing changed'); console.error('Nothing changed');

View File

@ -1,6 +1,5 @@
<form #systemConfigFrom="ngForm" class="clr-form clr-form-horizontal"> <form #systemConfigFrom="ngForm" class="clr-form clr-form-horizontal">
<section> <section>
<label class="subtitle" *ngIf="showSubTitle">{{'CONFIG.SYSTEM' | translate}}</label>
<clr-select-container> <clr-select-container>
<label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}} <label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}}
<clr-tooltip> <clr-tooltip>
@ -11,8 +10,8 @@
</clr-tooltip> </clr-tooltip>
</label> </label>
<select clrSelect id="proCreation" name="proCreation" <select clrSelect id="proCreation" name="proCreation"
[(ngModel)]="systemSettings.project_creation_restriction.value" [(ngModel)]="currentConfig.project_creation_restriction.value"
[disabled]="disabled(systemSettings.project_creation_restriction)"> [disabled]="disabled(currentConfig.project_creation_restriction)">
<option value="everyone">{{'CONFIG.PRO_CREATION_EVERYONE' | translate }}</option> <option value="everyone">{{'CONFIG.PRO_CREATION_EVERYONE' | translate }}</option>
<option value="adminonly">{{'CONFIG.PRO_CREATION_ADMIN' | translate }}</option> <option value="adminonly">{{'CONFIG.PRO_CREATION_ADMIN' | translate }}</option>
</select> </select>
@ -42,7 +41,7 @@
</clr-tooltip> </clr-tooltip>
</label> </label>
<input clrInput name="robotNamePrefix" type="text" <input clrInput name="robotNamePrefix" type="text"
[(ngModel)]="systemSettings.robot_name_prefix.value" [(ngModel)]="currentConfig.robot_name_prefix.value"
required required
autocomplete="off" autocomplete="off"
id="robotNamePrefix" size="20" [disabled]="!robotNamePrefixEditable()" /> id="robotNamePrefix" size="20" [disabled]="!robotNamePrefixEditable()" />
@ -73,7 +72,7 @@
</clr-tooltip> </clr-tooltip>
<a rel='noopener noreferrer' #certDownloadLink class="cert-down" [href]="downloadLink" target="_blank">{{'CONFIG.ROOT_CERT_LINK' | translate}}</a> <a rel='noopener noreferrer' #certDownloadLink class="cert-down" [href]="downloadLink" target="_blank">{{'CONFIG.ROOT_CERT_LINK' | translate}}</a>
</label> </label>
<clr-checkbox-container *ngIf="!withAdmiral"> <clr-checkbox-container>
<label id="repo_read_only_lbl" for="repoReadOnly">{{'CONFIG.REPO_READ_ONLY' | translate}} <label id="repo_read_only_lbl" for="repoReadOnly">{{'CONFIG.REPO_READ_ONLY' | translate}}
<clr-tooltip> <clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon> <clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
@ -83,8 +82,8 @@
</clr-tooltip> </clr-tooltip>
</label> </label>
<clr-checkbox-wrapper> <clr-checkbox-wrapper>
<input type="checkbox" [disabled]="!systemSettings.read_only.editable" clrCheckbox name="repoReadOnly" id="repoReadOnly" <input type="checkbox" [disabled]="!currentConfig.read_only.editable" clrCheckbox name="repoReadOnly" id="repoReadOnly"
[ngModel]="systemSettings.read_only.value" (ngModelChange)="setRepoReadOnlyValue($event)" /> [ngModel]="currentConfig.read_only.value" (ngModelChange)="setRepoReadOnlyValue($event)" />
</clr-checkbox-wrapper> </clr-checkbox-wrapper>
</clr-checkbox-container> </clr-checkbox-container>
@ -162,7 +161,7 @@
</div> </div>
</div> </div>
</div> </div>
<clr-checkbox-container *ngIf="!withAdmiral"> <clr-checkbox-container>
<label for="webhookNotificationEnabled">{{'CONFIG.WEBHOOK_NOTIFICATION_ENABLED' | translate}} <label for="webhookNotificationEnabled">{{'CONFIG.WEBHOOK_NOTIFICATION_ENABLED' | translate}}
<clr-tooltip> <clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon> <clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
@ -173,9 +172,9 @@
</label> </label>
<clr-checkbox-wrapper> <clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="webhookNotificationEnabled" id="webhookNotificationEnabled" <input type="checkbox" clrCheckbox name="webhookNotificationEnabled" id="webhookNotificationEnabled"
[ngModel]="systemSettings.notification_enable.value" [ngModel]="currentConfig.notification_enable.value"
(ngModelChange)="setWebhookNotificationEnabledValue($event)" (ngModelChange)="setWebhookNotificationEnabledValue($event)"
[disabled]="!systemSettings.notification_enable.editable" /> [disabled]="!currentConfig.notification_enable.editable" />
</clr-checkbox-wrapper> </clr-checkbox-wrapper>
</clr-checkbox-container> </clr-checkbox-container>
</section> </section>
@ -188,4 +187,3 @@
[disabled]="(!isValid() || !hasChanges()) && (!hasAllowlistChanged) || inProgress">{{'BUTTON.CANCEL' [disabled]="(!isValid() || !hasChanges()) && (!hasAllowlistChanged) || inProgress">{{'BUTTON.CANCEL'
| translate}}</button> | translate}}</button>
</div> </div>
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>

View File

@ -3,9 +3,10 @@ import { SystemSettingsComponent } from "./system-settings.component";
import { SystemInfoService } from "../../../../shared/services"; import { SystemInfoService } from "../../../../shared/services";
import { ErrorHandler } from "../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../shared/units/error-handler";
import { of } from "rxjs"; import { of } from "rxjs";
import { StringValueItem } from "../config"; import { Configuration, StringValueItem } from "../config";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { SharedTestingModule } from "../../../../shared/shared.module"; import { SharedTestingModule } from "../../../../shared/shared.module";
import { ConfigService } from "../config.service";
import { AppConfigService } from "../../../../services/app-config.service";
describe('SystemSettingsComponent', () => { describe('SystemSettingsComponent', () => {
let component: SystemSettingsComponent; let component: SystemSettingsComponent;
let fixture: ComponentFixture<SystemSettingsComponent>; let fixture: ComponentFixture<SystemSettingsComponent>;
@ -33,13 +34,43 @@ describe('SystemSettingsComponent', () => {
return null; return null;
} }
}; };
const fakeConfigService = {
config: new Configuration(),
getConfig() {
return this.config;
},
setConfig(c) {
this.config = c;
},
getOriginalConfig() {
return new Configuration();
},
getLoadingConfigStatus() {
return false;
},
confirmUnsavedChanges() {
},
updateConfig() {
},
resetConfig() {
}
};
const fakedAppConfigService = {
getConfig() {
return {};
},
load() {
return of(null);
}
};
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
SharedTestingModule, SharedTestingModule,
BrowserAnimationsModule
], ],
providers: [ providers: [
{ provide: AppConfigService, useValue: fakedAppConfigService },
{ provide: ConfigService, useValue: fakeConfigService },
{ provide: ErrorHandler, useValue: fakedErrorHandler }, { provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: SystemInfoService, useValue: fakedSystemInfoService }, { provide: SystemInfoService, useValue: fakedSystemInfoService },
// open auto detect // open auto detect
@ -53,7 +84,7 @@ describe('SystemSettingsComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SystemSettingsComponent); fixture = TestBed.createComponent(SystemSettingsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.config.auth_mode = new StringValueItem("db_auth", false ); component.currentConfig.auth_mode = new StringValueItem("db_auth", false );
fixture.detectChanges(); fixture.detectChanges();
}); });
@ -61,6 +92,7 @@ describe('SystemSettingsComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('cancel button should works', () => { it('cancel button should works', () => {
const spy: jasmine.Spy = spyOn(fakeConfigService, 'confirmUnsavedChanges').and.returnValue(undefined);
component.systemAllowlist.items.push({cve_id: 'CVE-2019-456'}); component.systemAllowlist.items.push({cve_id: 'CVE-2019-456'});
const readOnly: HTMLElement = fixture.nativeElement.querySelector('#repoReadOnly'); const readOnly: HTMLElement = fixture.nativeElement.querySelector('#repoReadOnly');
readOnly.click(); readOnly.click();
@ -68,7 +100,7 @@ describe('SystemSettingsComponent', () => {
const cancel: HTMLButtonElement = fixture.nativeElement.querySelector('#config_system_cancel'); const cancel: HTMLButtonElement = fixture.nativeElement.querySelector('#config_system_cancel');
cancel.click(); cancel.click();
fixture.detectChanges(); fixture.detectChanges();
expect(component.confirmationDlg.opened).toBeTruthy(); expect(spy.calls.count()).toEqual(1);
}); });
it('save button should works', () => { it('save button should works', () => {
component.systemAllowlist.items[0].cve_id = 'CVE-2019-789'; component.systemAllowlist.items[0].cve_id = 'CVE-2019-789';

View File

@ -1,28 +1,16 @@
import { import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
Component, import { NgForm } from '@angular/forms';
Input, import { Configuration } from '../config';
OnInit, import { clone, compareValue, CURRENT_BASE_HREF, getChanges, isEmpty } from '../../../../shared/units/utils';
Output, import { ErrorHandler } from '../../../../shared/units/error-handler';
EventEmitter, import { ConfirmationState, ConfirmationTargets } from '../../../../shared/entities/shared.const';
ViewChild, import { ConfirmationAcknowledgement } from '../../../global-confirmation-dialog/confirmation-state-message';
OnChanges, import { SystemCVEAllowlist, SystemInfo, SystemInfoService, } from '../../../../shared/services';
SimpleChanges, import { forkJoin } from "rxjs";
ElementRef
} from '@angular/core';
import {NgForm} from '@angular/forms';
import {Configuration, StringValueItem} from '../config';
import { clone, isEmpty, getChanges, compareValue, CURRENT_BASE_HREF } from '../../../../shared/units/utils';
import {ErrorHandler} from '../../../../shared/units/error-handler';
import {ConfirmationMessage} from '../../../global-confirmation-dialog/confirmation-message';
import {ConfirmationDialogComponent} from '../../../../shared/components/confirmation-dialog';
import {ConfirmationState, ConfirmationTargets} from '../../../../shared/entities/shared.const';
import {ConfirmationAcknowledgement} from '../../../global-confirmation-dialog/confirmation-state-message';
import { SystemCVEAllowlist, SystemInfo, SystemInfoService,
} from '../../../../shared/services';
import {forkJoin} from "rxjs";
import { ConfigurationService } from "../../../../services/config.service"; import { ConfigurationService } from "../../../../services/config.service";
import { ConfigService } from "../config.service";
import { AppConfigService } from "../../../../services/app-config.service";
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
const ONE_THOUSAND: number = 1000; const ONE_THOUSAND: number = 1000;
const CVE_DETAIL_PRE_URL = `https://nvd.nist.gov/vuln/detail/`; const CVE_DETAIL_PRE_URL = `https://nvd.nist.gov/vuln/detail/`;
const TARGET_BLANK = "_blank"; const TARGET_BLANK = "_blank";
@ -32,70 +20,58 @@ const TARGET_BLANK = "_blank";
templateUrl: './system-settings.component.html', templateUrl: './system-settings.component.html',
styleUrls: ['./system-settings.component.scss'] styleUrls: ['./system-settings.component.scss']
}) })
export class SystemSettingsComponent implements OnChanges, OnInit { export class SystemSettingsComponent implements OnInit {
config: Configuration = new Configuration();
onGoing = false; onGoing = false;
private originalConfig: Configuration;
downloadLink: string; downloadLink: string;
robotTokenExpiration: string;
systemAllowlist: SystemCVEAllowlist; systemAllowlist: SystemCVEAllowlist;
systemAllowlistOrigin: SystemCVEAllowlist; systemAllowlistOrigin: SystemCVEAllowlist;
cveIds: string; cveIds: string;
showAddModal: boolean = false; showAddModal: boolean = false;
systemInfo: SystemInfo; systemInfo: SystemInfo;
@Output() configChange: EventEmitter<Configuration> = new EventEmitter<Configuration>(); get currentConfig(): Configuration {
@Output() readOnlyChange: EventEmitter<boolean> = new EventEmitter<boolean>(); return this.conf.getConfig();
@Output() reloadSystemConfig: EventEmitter<any> = new EventEmitter<any>();
@Input()
get systemSettings(): Configuration {
return this.config;
} }
set systemSettings(cfg: Configuration) { set currentConfig(cfg: Configuration) {
this.config = cfg; this.conf.setConfig(cfg);
this.configChange.emit(this.config);
} }
@Input() showSubTitle: boolean = false;
@Input() hasAdminRole: boolean = false;
@Input() hasCAFile: boolean = false;
@Input() withAdmiral = false;
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm; @ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
@ViewChild("cfgConfirmationDialog") confirmationDlg: ConfirmationDialogComponent;
@ViewChild('dateInput') dateInput: ElementRef; @ViewChild('dateInput') dateInput: ElementRef;
get editable(): boolean { get editable(): boolean {
return this.systemSettings && return this.currentConfig &&
this.systemSettings.token_expiration && this.currentConfig.token_expiration &&
this.systemSettings.token_expiration.editable; this.currentConfig.token_expiration.editable;
} }
get robotExpirationEditable(): boolean { get robotExpirationEditable(): boolean {
return this.systemSettings && return this.currentConfig &&
this.systemSettings.robot_token_duration && this.currentConfig.robot_token_duration &&
this.systemSettings.robot_token_duration.editable; this.currentConfig.robot_token_duration.editable;
} }
get tokenExpirationValue() { get tokenExpirationValue() {
return this.systemSettings.token_expiration.value; return this.currentConfig.token_expiration.value;
} }
set tokenExpirationValue(v) { set tokenExpirationValue(v) {
// convert string to number // convert string to number
this.systemSettings.token_expiration.value = +v; this.currentConfig.token_expiration.value = +v;
} }
get robotTokenExpirationValue() { get robotTokenExpirationValue() {
return this.systemSettings.robot_token_duration.value; return this.currentConfig.robot_token_duration.value;
} }
set robotTokenExpirationValue(v) { set robotTokenExpirationValue(v) {
// convert string to number // convert string to number
this.systemSettings.robot_token_duration.value = +v; this.currentConfig.robot_token_duration.value = +v;
} }
robotNamePrefixEditable(): boolean { robotNamePrefixEditable(): boolean {
return this.systemSettings && return this.currentConfig &&
this.systemSettings.robot_name_prefix && this.currentConfig.robot_name_prefix &&
this.systemSettings.robot_name_prefix.editable; this.currentConfig.robot_name_prefix.editable;
} }
public isValid(): boolean { public isValid(): boolean {
@ -107,19 +83,13 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
} }
public getChanges() { public getChanges() {
let allChanges = getChanges(this.originalConfig, this.config); let allChanges = getChanges(this.conf.getOriginalConfig(), this.currentConfig);
if (allChanges) { if (allChanges) {
return this.getSystemChanges(allChanges); return this.getSystemChanges(allChanges);
} }
return null; return null;
} }
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["systemSettings"]) {
this.originalConfig = clone(this.config);
}
}
public getSystemChanges(allChanges: any) { public getSystemChanges(allChanges: any) {
let changes = {}; let changes = {};
for (let prop in allChanges) { for (let prop in allChanges) {
@ -132,11 +102,11 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
} }
setRepoReadOnlyValue($event: any) { setRepoReadOnlyValue($event: any) {
this.systemSettings.read_only.value = $event; this.currentConfig.read_only.value = $event;
} }
setWebhookNotificationEnabledValue($event: any) { setWebhookNotificationEnabledValue($event: any) {
this.systemSettings.notification_enable.value = $event; this.currentConfig.notification_enable.value = $event;
} }
disabled(prop: any): boolean { disabled(prop: any): boolean {
@ -144,7 +114,7 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
} }
get canDownloadCert(): boolean { get canDownloadCert(): boolean {
return this.hasAdminRole && this.hasCAFile; return this.appConfigService.getConfig().has_ca_root;
} }
/** /**
@ -172,12 +142,11 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
// To refresh the view, we can clone the original data copy // To refresh the view, we can clone the original data copy
// or force refresh by calling service. // or force refresh by calling service.
// HERE we choose force way // HERE we choose force way
this.retrieveConfig(); this.conf.updateConfig();
if ('read_only' in changes) { // Reload bootstrap option
this.readOnlyChange.emit(changes['read_only']); this.appConfigService.load().subscribe(() => {
} }
, error => console.error('Failed to reload bootstrap option with error: ', error));
this.reloadSystemConfig.emit();
} }
if (!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)) { if (!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)) {
this.systemAllowlistOrigin = clone(this.systemAllowlist); this.systemAllowlistOrigin = clone(this.systemAllowlist);
@ -193,40 +162,10 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
} }
} }
retrieveConfig(): void {
this.onGoing = true;
this.configService.getConfiguration()
.subscribe((configurations: Configuration) => {
this.onGoing = false;
// Add two password fields
configurations.email_password = new StringValueItem(fakePass, true);
this.config = configurations;
// Keep the original copy of the data
this.originalConfig = clone(configurations);
}, error => {
this.onGoing = false;
this.errorHandler.error(error);
});
}
reset(changes: any): void {
if (!isEmpty(changes)) {
for (let prop in changes) {
if (this.originalConfig[prop]) {
this.config[prop] = clone(this.originalConfig[prop]);
}
}
} else {
// force reset
this.retrieveConfig();
}
}
confirmCancel(ack: ConfirmationAcknowledgement): void { confirmCancel(ack: ConfirmationAcknowledgement): void {
if (ack && ack.source === ConfirmationTargets.CONFIG && if (ack && ack.source === ConfirmationTargets.CONFIG &&
ack.state === ConfirmationState.CONFIRMED) { ack.state === ConfirmationState.CONFIRMED) {
let changes = this.getChanges(); this.conf.resetConfig();
this.reset(changes);
if (!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)) { if (!compareValue(this.systemAllowlistOrigin, this.systemAllowlist)) {
this.systemAllowlist = clone(this.systemAllowlistOrigin); this.systemAllowlist = clone(this.systemAllowlistOrigin);
} }
@ -235,7 +174,7 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
public get inProgress(): boolean { public get inProgress(): boolean {
return this.onGoing; return this.onGoing || this.conf.getLoadingConfigStatus();
} }
/** /**
@ -247,28 +186,23 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
public cancel(): void { public cancel(): void {
let changes = this.getChanges(); let changes = this.getChanges();
if (!isEmpty(changes) || !compareValue(this.systemAllowlistOrigin, this.systemAllowlist)) { if (!isEmpty(changes) || !compareValue(this.systemAllowlistOrigin, this.systemAllowlist)) {
let msg = new ConfirmationMessage( this.conf.confirmUnsavedChanges(changes);
'CONFIG.CONFIRM_TITLE',
'CONFIG.CONFIRM_SUMMARY',
'',
{},
ConfirmationTargets.CONFIG
);
this.confirmationDlg.open(msg);
} else { } else {
// Invalid situation, should not come here // Invalid situation, should not come here
console.error('Nothing changed'); console.error('Nothing changed');
} }
} }
constructor( constructor(private appConfigService: AppConfigService,
private configService: ConfigurationService, private configService: ConfigurationService,
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private systemInfoService: SystemInfoService) { private systemInfoService: SystemInfoService,
private conf: ConfigService) {
this.downloadLink = CURRENT_BASE_HREF + "/systeminfo/getcert"; this.downloadLink = CURRENT_BASE_HREF + "/systeminfo/getcert";
} }
ngOnInit() { ngOnInit() {
this.conf.resetConfig();
this.getSystemAllowlist(); this.getSystemAllowlist();
this.getSystemInfo(); this.getSystemInfo();
} }
@ -278,6 +212,7 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
.subscribe(systemInfo => this.systemInfo = systemInfo .subscribe(systemInfo => this.systemInfo = systemInfo
, error => this.errorHandler.error(error)); , error => this.errorHandler.error(error));
} }
getSystemAllowlist() { getSystemAllowlist() {
this.onGoing = true; this.onGoing = true;
this.systemInfoService.getSystemAllowlist() this.systemInfoService.getSystemAllowlist()
@ -298,6 +233,7 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
} }
); );
} }
deleteItem(index: number) { deleteItem(index: number) {
this.systemAllowlist.items.splice(index, 1); this.systemAllowlist.items.splice(index, 1);
} }