refactor configuration

Signed-off-by: Meina Zhou <meinaz@vmware.com>
This commit is contained in:
Meina Zhou 2018-10-30 15:08:20 +08:00
parent 079059ef0c
commit e3e15dbc65
10 changed files with 508 additions and 331 deletions

View File

@ -38,6 +38,25 @@ export class SystemSettingsComponent {
return this.systemSettingsForm && this.systemSettingsForm.valid;
}
public hasUnsavedChanges(allChanges: any): boolean {
for (let prop in allChanges) {
if (prop === 'token_expiration' || prop === 'read_only') {
return true;
}
}
return false;
}
public getSystemChanges(allChanges: any) {
let changes = {};
for (let prop in allChanges) {
if (prop === 'token_expiration' || prop === 'read_only') {
changes[prop] = allChanges[prop];
}
}
return changes;
}
setRepoReadOnlyValue($event: any) {
this.systemSettings.read_only.value = $event;
}

View File

@ -2,7 +2,7 @@ export * from './harbor-library.module';
export * from './service.config';
export * from './service/index';
export * from './error-handler/index';
// export * from './utils';
export * from './utils';
export * from './log/index';
export * from './filter/index';
export * from './endpoint/index';

View File

@ -295,6 +295,10 @@ export function clone(srcObj: any): any {
return JSON.parse(JSON.stringify(srcObj));
}
export function isEmpty(obj: any): boolean {
return !obj || JSON.stringify(obj) === '{}';
}
export function downloadFile(fileData) {
let url = window.URL.createObjectURL(fileData.data);
let a = document.createElement("a");
@ -306,3 +310,28 @@ export function downloadFile(fileData) {
window.URL.revokeObjectURL(url);
a.remove();
}
export function getChanges(original: any, afterChange: any): { [key: string]: any | any[] } {
let changes: { [key: string]: any | any[] } = {};
if (!afterChange || !original) {
return changes;
}
for (let prop of Object.keys(afterChange)) {
let field = original[prop];
if (field && field.editable) {
if (!compareValue(field.value, afterChange[prop].value)) {
changes[prop] = afterChange[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;
}

View File

@ -214,4 +214,10 @@
</clr-checkbox>
</div>
</section>
</form>
</form>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span id="forTestingLDAP" class="spinner spinner-inline" [hidden]="hideLDAPTestingSpinner"></span>
</div>

View File

@ -11,25 +11,37 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, ViewChild } from '@angular/core';
import { Component, Input, ViewChild, SimpleChanges, OnChanges} from '@angular/core';
import { NgForm } from '@angular/forms';
import { Subscription } from "rxjs";
import { Configuration } from '@harbor/ui';
import { Configuration, clone, isEmpty, getChanges, StringValueItem} from '@harbor/ui';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { confirmUnsavedChanges} from '../config.msg.utils';
import { AppConfigService } from '../../app-config.service';
import { ConfigurationService } from '../config.service';
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
@Component({
selector: 'config-auth',
templateUrl: 'config-auth.component.html',
styleUrls: ['./config-auth.component.scss', '../config.component.scss']
})
export class ConfigurationAuthComponent {
export class ConfigurationAuthComponent implements OnChanges {
changeSub: Subscription;
testingLDAPOnGoing = false;
onGoing = false;
// tslint:disable-next-line:no-input-rename
@Input('allConfig') currentConfig: Configuration = new Configuration();
private originalConfig: Configuration;
@ViewChild('authConfigFrom') authForm: NgForm;
constructor() { }
constructor(
private msgHandler: MessageHandlerService,
private configService: ConfigurationService,
private appConfigService: AppConfigService
) {
}
get checkable() {
return this.currentConfig &&
@ -37,6 +49,12 @@ export class ConfigurationAuthComponent {
this.currentConfig.self_registration.value === true;
}
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["currentConfig"]) {
this.originalConfig = clone(this.currentConfig);
}
}
public get showLdap(): boolean {
return this.currentConfig &&
this.currentConfig.auth_mode &&
@ -59,10 +77,82 @@ export class ConfigurationAuthComponent {
return this.authForm && this.authForm.valid;
}
public hasChanges(): boolean {
return !isEmpty(this.getChanges());
}
setVerifyCertValue($event: any) {
this.currentConfig.ldap_verify_cert.value = $event;
}
public testLDAPServer(): void {
if (this.testingLDAPOnGoing) {
return; // Should not come here
}
let ldapSettings = {};
for (let prop in this.currentConfig) {
if (prop.startsWith('ldap_')) {
ldapSettings[prop] = this.currentConfig[prop].value;
}
}
let allChanges = this.getChanges();
let ldapSearchPwd = allChanges['ldap_search_password'];
if (ldapSearchPwd) {
ldapSettings['ldap_search_password'] = ldapSearchPwd;
} else {
delete ldapSettings['ldap_search_password'];
}
// Fix: Confirm ldap scope is number
ldapSettings['ldap_scope'] = +ldapSettings['ldap_scope'];
this.testingLDAPOnGoing = true;
this.configService.testLDAPServer(ldapSettings)
.then(respone => {
this.testingLDAPOnGoing = false;
this.msgHandler.showSuccess('CONFIG.TEST_LDAP_SUCCESS');
})
.catch(error => {
this.testingLDAPOnGoing = false;
let err = error._body;
if (!err || !err.trim()) {
err = 'UNKNOWN';
}
this.msgHandler.showError('CONFIG.TEST_LDAP_FAILED', { 'param': err });
});
}
public get showLdapServerBtn(): boolean {
return this.currentConfig.auth_mode &&
this.currentConfig.auth_mode.value === 'ldap_auth';
}
public isLDAPConfigValid(): boolean {
return this.isValid() &&
!this.testingLDAPOnGoing;
}
public getChanges() {
let allChanges = getChanges(this.originalConfig, this.currentConfig);
let changes = {};
for (let prop in allChanges) {
if (prop.startsWith('ldap_')
|| prop.startsWith('uaa_')
|| prop === 'auth_mode'
|| prop === 'project_creattion_restriction'
|| prop === 'self_registration') {
changes[prop] = allChanges[prop];
}
}
return changes;
}
public get hideLDAPTestingSpinner(): boolean {
return !this.testingLDAPOnGoing || !this.showLdapServerBtn;
}
disabled(prop: any): boolean {
return !(prop && prop.editable);
}
@ -77,4 +167,81 @@ export class ConfigurationAuthComponent {
}
}
}
/**
*
* Save the changed values
*
* @memberOf ConfigurationComponent
*/
public save(): void {
let changes = this.getChanges();
if (!isEmpty(changes)) {
this.onGoing = true;
this.configService.saveConfiguration(changes)
.then(response => {
this.onGoing = false;
this.retrieveConfig();
// Reload bootstrap option
this.appConfigService.load().catch(error => console.error('Failed to reload bootstrap option with error: ', error));
this.msgHandler.showSuccess('CONFIG.SAVE_SUCCESS');
})
.catch(error => {
this.onGoing = false;
this.msgHandler.handleError(error);
});
} else {
// Inprop situation, should not come here
console.error('Save abort because nothing changed');
}
}
public hasUnsavedChanges(allChanges: any) {
for (let prop in allChanges) {
if (prop.startsWith('ldap_')
|| prop.startsWith('uaa_')
|| prop === 'auth_mode'
|| prop === 'project_creattion_restriction'
|| prop === 'self_registration') {
return true;
}
}
return false;
}
retrieveConfig(): void {
this.onGoing = true;
this.configService.getConfiguration()
.then((configurations: Configuration) => {
this.onGoing = false;
// Add two password fields
configurations.ldap_search_password = new StringValueItem(fakePass, true);
configurations.uaa_client_secret = new StringValueItem(fakePass, true);
this.currentConfig = configurations;
// Keep the original copy of the data
this.originalConfig = clone(configurations);
})
.catch(error => {
this.onGoing = false;
this.msgHandler.handleError(error);
});
}
/**
*
* Discard current changes if have and reset
*
* @memberOf ConfigurationComponent
*/
public cancel(): void {
let changes = this.getChanges();
if (!isEmpty(changes)) {
confirmUnsavedChanges(changes);
} else {
// Invalid situation, should not come here
console.error('Nothing changed');
}
}
}

View File

@ -4,22 +4,28 @@
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<ul id="configTabs" class="nav" role="tablist">
<li role="presentation" class="nav-item">
<button id="config-auth" class="btn btn-link nav-link active" aria-controls="authentication" [class.active]='isCurrentTabLink("config-auth")' type="button" (click)='tabLinkClick("config-auth")'>{{'CONFIG.AUTH' | translate }}</button>
<button id="config-auth" class="btn btn-link nav-link active" aria-controls="authentication" [class.active]='isCurrentTabLink("config-auth")'
type="button" (click)='tabLinkClick("config-auth")'>{{'CONFIG.AUTH' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-email" class="btn btn-link nav-link" aria-controls="email" [class.active]='isCurrentTabLink("config-email")' type="button" (click)='tabLinkClick("config-email")'>{{'CONFIG.EMAIL' | translate }}</button>
<button id="config-email" class="btn btn-link nav-link" aria-controls="email" [class.active]='isCurrentTabLink("config-email")'
type="button" (click)='tabLinkClick("config-email")'>{{'CONFIG.EMAIL' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")' type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button>
<button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")'
type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button>
</li>
<li role="presentation" class="nav-item" *ngIf="!withAdmiral">
<button id="config-label" class="btn btn-link nav-link" aria-controls="system_label" [class.active]='isCurrentTabLink("config-label")' type="button" (click)='tabLinkClick("config-label")'>{{'CONFIG.LABEL' | translate }}</button>
<li role="presentation" class="nav-item" *ngIf="!withAdmiral">
<button id="config-label" class="btn btn-link nav-link" aria-controls="system_label" [class.active]='isCurrentTabLink("config-label")'
type="button" (click)='tabLinkClick("config-label")'>{{'CONFIG.LABEL' | translate }}</button>
</li>
<li role="presentation" class="nav-item" *ngIf="withClair">
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>{{'CONFIG.VULNERABILITY' | translate}}</button>
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")'
type="button" (click)='tabLinkClick("config-vulnerability")'>{{'CONFIG.VULNERABILITY' | translate}}</button>
</li>
<li role="presentation" class="nav-item" *ngIf="hasAdminRole">
<button id="config-gc" class="btn btn-link nav-link" aria-controls="gc" [class.active]='isCurrentTabLink("config-gc")' type="button" (click)='tabLinkClick("config-gc")'>{{'CONFIG.GC' | translate}}</button>
<button id="config-gc" class="btn btn-link nav-link" aria-controls="gc" [class.active]='isCurrentTabLink("config-gc")' type="button"
(click)='tabLinkClick("config-gc")'>{{'CONFIG.GC' | translate}}</button>
</li>
</ul>
<section id="authentication" role="tabpanel" aria-labelledby="config-auth" [hidden]='!isCurrentTabContent("authentication")'>
@ -31,7 +37,8 @@
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>
</section>
<section id="system_label" role="tabpanel" aria-labelledby="config-label" *ngIf="!withAdmiral" [hidden]='!isCurrentTabContent("system_label")' style="padding-top: 16px;">
<section id="system_label" role="tabpanel" aria-labelledby="config-label" *ngIf="!withAdmiral" [hidden]='!isCurrentTabContent("system_label")'
style="padding-top: 16px;">
<hbr-label [scope]="'g'"></hbr-label>
</section>
<section id="vulnerability" *ngIf="withClair" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
@ -40,13 +47,10 @@
<section id="gc" *ngIf="hasAdminRole" role="tabpanel" aria-labelledby="config-gc" [hidden]='!isCurrentTabContent("gc")'>
<gc-config></gc-config>
</section>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span id="forTestingMail" class="spinner spinner-inline" [hidden]="hideMailTestingSpinner"></span>
<span id="forTestingLDAP" class="spinner spinner-inline" [hidden]="hideLDAPTestingSpinner"></span>
</div>
</div>
</div>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
</div>

View File

@ -13,10 +13,12 @@
// limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Subscription } from "rxjs";
import { Configuration, StringValueItem, SystemSettingsComponent, VulnerabilityConfigComponent} from '@harbor/ui';
import { Configuration, StringValueItem, SystemSettingsComponent, VulnerabilityConfigComponent,
isEmpty, clone, getChanges } from '@harbor/ui';
import { ConfirmationTargets, ConfirmationState } from '../shared/shared.const';
import { SessionService } from '../shared/session.service';
import { confirmUnsavedChanges} from './config.msg.utils';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
@ -24,7 +26,7 @@ import { MessageHandlerService } from '../shared/message-handler/message-handler
import { AppConfigService } from '../app-config.service';
import { ConfigurationAuthComponent } from './auth/config-auth.component';
import { ConfigurationEmailComponent } from './email/config-email.component';
import { GcComponent} from './gc/gc.component';
import { GcComponent } from './gc/gc.component';
import { ConfigurationService } from './config.service';
@ -48,10 +50,8 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
onGoing = false;
allConfig: Configuration = new Configuration();
currentTabId = 'config-auth'; // default tab
originalCopy: Configuration;
originalCopy: Configuration = new Configuration();
confirmSub: Subscription;
testingMailOnGoing = false;
testingLDAPOnGoing = false;
@ViewChild(SystemSettingsComponent) systemSettingsConfig: SystemSettingsComponent;
@ViewChild(VulnerabilityConfigComponent) vulnerabilityConfig: VulnerabilityConfigComponent;
@ -91,49 +91,31 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return TabLinkContentMap[this.currentTabId] === contentId;
}
hasUnsavedChangesOfCurrentTab(): any {
let allChanges = this.getChanges();
if (this.isEmpty(allChanges)) {
return null;
hasUnsavedChangesOfCurrentTab(allChanges: any): boolean {
if (isEmpty(allChanges)) {
return false;
}
let properties = [];
switch (this.currentTabId) {
case 'config-auth':
for (let prop in allChanges) {
if (prop.startsWith('ldap_')) {
return allChanges;
}
}
properties = ['auth_mode', 'project_creation_restriction', 'self_registration'];
break;
return this.authConfig.hasUnsavedChanges(allChanges);
case 'config-email':
for (let prop in allChanges) {
if (prop.startsWith('email_')) {
return allChanges;
}
}
return null;
return this.mailConfig.hasUnsavedChanges(allChanges);
case 'config-replication':
properties = ['verify_remote_cert'];
break;
case 'config-system':
properties = ['token_expiration'];
break;
case 'config-vulnerability':
properties = ['scan_all_policy'];
break;
default:
return null;
return this.systemSettingsConfig.hasUnsavedChanges(allChanges);
}
for (let prop in allChanges) {
if (properties.indexOf(prop) !== -1) {
return allChanges;
return true;
}
}
return null;
return false;
}
ngOnInit(): void {
@ -168,68 +150,32 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
}
public isValid(): boolean {
return this.systemSettingsConfig &&
this.systemSettingsConfig.isValid &&
this.mailConfig &&
this.mailConfig.isValid() &&
this.authConfig &&
this.authConfig.isValid() &&
this.isVulnerabiltyValid;
}
public get isVulnerabiltyValid(): boolean {
return !this.appConfigService.getConfig().with_clair ||
(this.vulnerabilityConfig &&
this.vulnerabilityConfig.isValid);
return this.systemSettingsConfig.isValid;
}
public hasChanges(): boolean {
return !this.isEmpty(this.getChanges());
return !isEmpty(this.getSystemChanges());
}
public isMailConfigValid(): boolean {
return this.mailConfig &&
this.mailConfig.isValid() &&
!this.testingMailOnGoing;
}
public get showTestServerBtn(): boolean {
return this.currentTabId === 'config-email';
}
public get showLdapServerBtn(): boolean {
return this.currentTabId === 'config-auth' &&
this.allConfig.auth_mode &&
this.allConfig.auth_mode.value === 'ldap_auth';
}
public get hideBtn(): boolean {
return this.currentTabId === 'config-label' || this.currentTabId === 'config-gc' || this.currentTabId === 'config-vulnerability';
}
public get hideMailTestingSpinner(): boolean {
return !this.testingMailOnGoing || !this.showTestServerBtn;
}
public get hideLDAPTestingSpinner(): boolean {
return !this.testingLDAPOnGoing || !this.showLdapServerBtn;
}
public isLDAPConfigValid(): boolean {
return this.authConfig &&
this.authConfig.isValid() &&
!this.testingLDAPOnGoing;
}
public tabLinkClick(tabLink: string) {
let allChanges = getChanges(this.originalCopy, this.allConfig);
// Whether has unsaved changes in current tab
let changes = this.hasUnsavedChangesOfCurrentTab();
if (!changes) {
let hasChanges = this.hasUnsavedChangesOfCurrentTab(allChanges);
if (!hasChanges) {
this.currentTabId = tabLink;
return;
}
this.confirmUnsavedTabChanges(changes, tabLink);
this.confirmUnsavedTabChanges(allChanges, tabLink);
}
public getSystemChanges() {
let allChanges = getChanges(this.originalCopy, this.allConfig);
if (allChanges) {
return this.systemSettingsConfig.getSystemChanges(allChanges);
}
return null;
}
/**
@ -239,16 +185,8 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
* @memberOf ConfigurationComponent
*/
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);
}
let changes = this.getSystemChanges();
if (!isEmpty(changes)) {
this.onGoing = true;
this.configService.saveConfiguration(changes)
.then(response => {
@ -290,107 +228,17 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
* @memberOf ConfigurationComponent
*/
public cancel(): void {
let changes = this.getChanges();
if (!this.isEmpty(changes)) {
this.confirmUnsavedChanges(changes);
let changes = this.getSystemChanges();
if (!isEmpty(changes)) {
confirmUnsavedChanges(changes);
} else {
// Invalid situation, should not come here
console.error('Nothing changed');
}
}
/**
*
* Test the connection of specified mail server
*
*
* @memberOf ConfigurationComponent
*/
public testMailServer(): void {
if (this.testingMailOnGoing) {
return; // Should not come here
}
let mailSettings = {};
for (let prop in this.allConfig) {
if (prop.startsWith('email_')) {
mailSettings[prop] = this.allConfig[prop].value;
}
}
// Confirm port is number
mailSettings['email_port'] = +mailSettings['email_port'];
let allChanges = this.getChanges();
let password = allChanges['email_password'];
if (password) {
mailSettings['email_password'] = password;
} else {
delete mailSettings['email_password'];
}
this.testingMailOnGoing = true;
this.configService.testMailServer(mailSettings)
.then(response => {
this.testingMailOnGoing = false;
this.msgHandler.showSuccess('CONFIG.TEST_MAIL_SUCCESS');
})
.catch(error => {
this.testingMailOnGoing = false;
let err = error._body;
if (!err) {
err = 'UNKNOWN';
}
this.msgHandler.showError('CONFIG.TEST_MAIL_FAILED', { 'param': err });
});
}
public testLDAPServer(): void {
if (this.testingLDAPOnGoing) {
return; // Should not come here
}
let ldapSettings = {};
for (let prop in this.allConfig) {
if (prop.startsWith('ldap_')) {
ldapSettings[prop] = this.allConfig[prop].value;
}
}
let allChanges = this.getChanges();
let ldapSearchPwd = allChanges['ldap_search_password'];
if (ldapSearchPwd) {
ldapSettings['ldap_search_password'] = ldapSearchPwd;
} else {
delete ldapSettings['ldap_search_password'];
}
// Fix: Confirm ldap scope is number
ldapSettings['ldap_scope'] = +ldapSettings['ldap_scope'];
this.testingLDAPOnGoing = true;
this.configService.testLDAPServer(ldapSettings)
.then(respone => {
this.testingLDAPOnGoing = false;
this.msgHandler.showSuccess('CONFIG.TEST_LDAP_SUCCESS');
})
.catch(error => {
this.testingLDAPOnGoing = false;
let err = error._body;
if (!err || !err.trim()) {
err = 'UNKNOWN';
}
this.msgHandler.showError('CONFIG.TEST_LDAP_FAILED', { 'param': err });
});
}
confirmUnsavedChanges(changes: any) {
let msg = new ConfirmationMessage(
'CONFIG.CONFIRM_TITLE',
'CONFIG.CONFIRM_SUMMARY',
'',
changes,
ConfirmationTargets.CONFIG
);
this.confirmService.openComfirmDialog(msg);
public get hideBtn(): boolean {
return this.currentTabId !== 'config-system';
}
confirmUnsavedTabChanges(changes: any, tabId: string) {
@ -420,7 +268,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
configurations.uaa_client_secret = new StringValueItem(fakePass, true);
this.allConfig = configurations;
// Keep the original copy of the data
this.originalCopy = this.clone(configurations);
this.originalCopy = clone(configurations);
})
.catch(error => {
this.onGoing = false;
@ -428,72 +276,6 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
});
}
/**
*
* Get the changed fields and return a map
*
* @private
* returns {*}
*
* @memberOf ConfigurationComponent
*/
getChanges(): { [key: string]: any | any[] } {
let changes: { [key: string]: any | any[] } = {};
if (!this.allConfig || !this.originalCopy) {
return changes;
}
for (let prop of Object.keys(this.allConfig)) {
let field = this.originalCopy[prop];
if (field && field.editable) {
if (!this.compareValue(field.value, this.allConfig[prop].value)) {
changes[prop] = this.allConfig[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
isEmpty(obj: any): boolean {
return !obj || JSON.stringify(obj) === '{}';
}
/**
*
* Deep clone the configuration object
*
* @private
* ** deprecated param {Configuration} src
* returns {Configuration}
*
* @memberOf ConfigurationComponent
*/
clone(src: Configuration): Configuration {
if (!src) {
return new Configuration(); // Empty
}
return JSON.parse(JSON.stringify(src));
}
/**
*
* Reset the configuration form
@ -504,10 +286,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
* @memberOf ConfigurationComponent
*/
reset(changes: any): void {
if (!this.isEmpty(changes)) {
if (!isEmpty(changes)) {
for (let prop in changes) {
if (this.originalCopy[prop]) {
this.allConfig[prop] = this.clone(this.originalCopy[prop]);
this.allConfig[prop] = clone(this.originalCopy[prop]);
}
}
} else {

View File

@ -0,0 +1,17 @@
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { ConfirmationTargets } from '../shared/shared.const';
export function confirmUnsavedChanges(changes: any) {
let confirmService = new ConfirmationDialogService();
let msg = new ConfirmationMessage(
'CONFIG.CONFIRM_TITLE',
'CONFIG.CONFIRM_SUMMARY',
'',
changes,
ConfirmationTargets.CONFIG
);
confirmService.openComfirmDialog(msg);
}

View File

@ -2,62 +2,58 @@
<section class="form-block">
<div class="form-group">
<label for="mailServer" class="required">{{'CONFIG.MAIL_SERVER' | translate}}</label>
<label for="mailServer" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="mailServerInput.invalid && (mailServerInput.dirty || mailServerInput.touched)">
<input name="mailServer" type="text" #mailServerInput="ngModel" [(ngModel)]="currentConfig.email_host.value"
required
id="mailServer"
size="40" [disabled]="disabled(currentConfig.email_host)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<label for="mailServer" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="mailServerInput.invalid && (mailServerInput.dirty || mailServerInput.touched)">
<input name="mailServer" type="text" #mailServerInput="ngModel" [(ngModel)]="currentConfig.email_host.value" required id="mailServer"
size="40" [disabled]="disabled(currentConfig.email_host)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="emailPort" class="required">{{'CONFIG.MAIL_SERVER_PORT' | translate}}</label>
<label for="emailPort" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="emailPortInput.invalid && (emailPortInput.dirty || emailPortInput.touched)">
<input name="emailPort" type="text" #emailPortInput="ngModel" [(ngModel)]="currentConfig.email_port.value"
required
port
id="emailPort"
size="40" [disabled]="disabled(currentConfig.email_port)">
<span class="tooltip-content">
{{'TOOLTIP.PORT_REQUIRED' | translate}}
</span>
</label>
<label for="emailPort" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="emailPortInput.invalid && (emailPortInput.dirty || emailPortInput.touched)">
<input name="emailPort" type="text" #emailPortInput="ngModel" [(ngModel)]="currentConfig.email_port.value" required port
id="emailPort" size="40" [disabled]="disabled(currentConfig.email_port)">
<span class="tooltip-content">
{{'TOOLTIP.PORT_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="emailUsername">{{'CONFIG.MAIL_USERNAME' | translate}}</label>
<label for="emailUsername" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
<input name="emailUsername" type="text" #emailUsernameInput="ngModel" [(ngModel)]="currentConfig.email_username.value"
id="emailUsername"
size="40" [disabled]="disabled(currentConfig.email_username)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<label for="emailUsername" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="false">
<input name="emailUsername" type="text" #emailUsernameInput="ngModel" [(ngModel)]="currentConfig.email_username.value" id="emailUsername"
size="40" [disabled]="disabled(currentConfig.email_username)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="emailPassword">{{'CONFIG.MAIL_PASSWORD' | translate}}</label>
<label for="emailPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
<input name="emailPassword" type="password" #emailPasswordInput="ngModel" [(ngModel)]="currentConfig.email_password.value"
id="emailPassword"
size="40" [disabled]="disabled(currentConfig.email_password)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<label for="emailPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="false">
<input name="emailPassword" type="password" #emailPasswordInput="ngModel" [(ngModel)]="currentConfig.email_password.value"
id="emailPassword" size="40" [disabled]="disabled(currentConfig.email_password)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="emailFrom" class="required">{{'CONFIG.MAIL_FROM' | translate}}</label>
<label for="emailFrom" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="emailFromInput.invalid && (emailFromInput.dirty || emailFromInput.touched)">
<input name="emailFrom" type="text" #emailFromInput="ngModel" [(ngModel)]="currentConfig.email_from.value"
required
id="emailFrom"
size="40" [disabled]="disabled(currentConfig.email_from)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
<label for="emailFrom" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="emailFromInput.invalid && (emailFromInput.dirty || emailFromInput.touched)">
<input name="emailFrom" type="text" #emailFromInput="ngModel" [(ngModel)]="currentConfig.email_from.value" required id="emailFrom"
size="40" [disabled]="disabled(currentConfig.email_from)">
<span class="tooltip-content">
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="selfReg">{{'CONFIG.MAIL_SSL' | translate}}</label>
@ -70,12 +66,19 @@
</div>
<div class="form-group">
<label for="insecure">{{'CONFIG.MAIL_INSECURE' | translate}}</label>
<clr-checkbox name="emaiInsecure" id="emailInsecure" [clrChecked]="!currentConfig.email_insecure.value" [clrDisabled]="disabled(currentConfig.email_insecure)" (clrCheckedChange)="setInsecureValue($event)">
<clr-checkbox name="emaiInsecure" id="emailInsecure" [clrChecked]="!currentConfig.email_insecure.value" [clrDisabled]="disabled(currentConfig.email_insecure)"
(clrCheckedChange)="setInsecureValue($event)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-7px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.INSECURE_TOOLTIP' | translate}}</span>
</a>
</a>
</clr-checkbox>
</div>
</section>
</form>
</form>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
<span id="forTestingMail" class="spinner spinner-inline" [hidden]="hideMailTestingSpinner"></span>
</div>

View File

@ -11,23 +11,31 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, ViewChild } from '@angular/core';
import { Component, Input, ViewChild, SimpleChanges, OnChanges } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Configuration } from '@harbor/ui';
import { Configuration, clone, isEmpty, getChanges, StringValueItem} from '@harbor/ui';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { confirmUnsavedChanges} from '../config.msg.utils';
import { ConfigurationService } from '../config.service';
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
@Component({
selector: 'config-email',
templateUrl: "config-email.component.html",
styleUrls: ['./config-email.component.scss', '../config.component.scss']
})
export class ConfigurationEmailComponent {
export class ConfigurationEmailComponent implements OnChanges {
// tslint:disable-next-line:no-input-rename
@Input("mailConfig") currentConfig: Configuration = new Configuration();
private originalConfig: Configuration;
testingMailOnGoing = false;
onGoing = false;
@ViewChild("mailConfigFrom") mailForm: NgForm;
constructor() { }
constructor(
private msgHandler: MessageHandlerService,
private configService: ConfigurationService) {
}
disabled(prop: any): boolean {
return !(prop && prop.editable);
@ -40,4 +48,146 @@ export class ConfigurationEmailComponent {
public isValid(): boolean {
return this.mailForm && this.mailForm.valid;
}
public hasChanges(): boolean {
return !isEmpty(this.getChanges());
}
public hasUnsavedChanges(allChanges: any) {
for (let prop in allChanges) {
if (prop.startsWith('email_')) {
return true;
}
}
return false;
}
public getChanges() {
let allChanges = getChanges(this.originalConfig, this.currentConfig);
let changes = {};
for (let prop in allChanges) {
if (prop.startsWith('email_')) {
changes[prop] = allChanges[prop];
}
}
return changes;
}
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["currentConfig"]) {
this.originalConfig = clone(this.currentConfig);
}
}
/**
*
* Test the connection of specified mail server
*
*
* @memberOf ConfigurationComponent
*/
public testMailServer(): void {
if (this.testingMailOnGoing) {
return; // Should not come here
}
let mailSettings = {};
for (let prop in this.currentConfig) {
if (prop.startsWith('email_')) {
mailSettings[prop] = this.currentConfig[prop].value;
}
}
// Confirm port is number
mailSettings['email_port'] = +mailSettings['email_port'];
let allChanges = this.getChanges();
let password = allChanges['email_password'];
if (password) {
mailSettings['email_password'] = password;
} else {
delete mailSettings['email_password'];
}
this.testingMailOnGoing = true;
this.configService.testMailServer(mailSettings)
.then(response => {
this.testingMailOnGoing = false;
this.msgHandler.showSuccess('CONFIG.TEST_MAIL_SUCCESS');
})
.catch(error => {
this.testingMailOnGoing = false;
let err = error._body;
if (!err) {
err = 'UNKNOWN';
}
this.msgHandler.showError('CONFIG.TEST_MAIL_FAILED', { 'param': err });
});
}
public get hideMailTestingSpinner(): boolean {
return !this.testingMailOnGoing;
}
public isMailConfigValid(): boolean {
return this.isValid() &&
!this.testingMailOnGoing;
}
/**
*
* Save the changed values
*
* @memberOf ConfigurationComponent
*/
public save(): void {
let changes = this.getChanges();
if (!isEmpty(changes)) {
this.onGoing = true;
this.configService.saveConfiguration(changes)
.then(response => {
this.onGoing = false;
this.retrieveConfig();
this.msgHandler.showSuccess('CONFIG.SAVE_SUCCESS');
})
.catch(error => {
this.onGoing = false;
this.msgHandler.handleError(error);
});
} else {
// Inprop situation, should not come here
console.error('Save abort because nothing changed');
}
}
retrieveConfig(): void {
this.onGoing = true;
this.configService.getConfiguration()
.then((configurations: Configuration) => {
this.onGoing = false;
// Add two password fields
configurations.email_password = new StringValueItem(fakePass, true);
this.currentConfig = configurations;
// Keep the original copy of the data
this.originalConfig = clone(configurations);
})
.catch(error => {
this.onGoing = false;
this.msgHandler.handleError(error);
});
}
/**
*
* Discard current changes if have and reset
*
* @memberOf ConfigurationComponent
*/
public cancel(): void {
let changes = this.getChanges();
if (!isEmpty(changes)) {
confirmUnsavedChanges(changes);
} else {
// Invalid situation, should not come here
console.error('Nothing changed');
}
}
}