blob: 919799f9a73f76256f179e878b198afa9de42c02 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {Component, OnInit, Inject, HostListener, OnDestroy, ViewChild, ElementRef} from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import {HealthStatusService, AppRoutingService, EndpointService} from '../../core/services';
import {MatTabChangeEvent} from '@angular/material/tabs';
import {Router} from '@angular/router';
import {ConfigurationService} from '../../core/services/configutration.service';
import 'brace';
import 'brace/mode/yaml';
import {ToastrService} from 'ngx-toastr';
import {Observable, Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import { EnvironmentsDataService } from '../management/management-data.service';
import { EnvironmentModel } from '../management/management.model';
@Component({
selector: 'datalab-configuration',
templateUrl: './configuration.component.html',
styleUrls: ['./configuration.component.scss']
})
export class ConfigurationComponent implements OnInit, OnDestroy {
@ViewChild('selfEditor') selfEditor: ElementRef<HTMLElement>;
@ViewChild('provEditor') provEditor: ElementRef<HTMLElement>;
@ViewChild('billingEditor') billingEditor: ElementRef<HTMLElement>;
private unsubscribe$ = new Subject();
private healthStatus: any;
public activeTab = {index: 0};
public activeService: string;
public services = {
'self-service': {label: 'Self-service', selected: false, config: '', serverConfig: '', isConfigChanged: false},
'provisioning': {label: 'Provisioning', selected: false, config: '', serverConfig: '', isConfigChanged: false},
'billing': {label: 'Billing', selected: false, config: '', serverConfig: '', isConfigChanged: false},
};
public messagesStatus = {
success: [],
error: [],
counter: 0
};
public environmentStatuses = {};
public environments = [];
public ingStatuses = ['creating', 'configuring', 'reconfiguring', 'creating image', 'stopping', 'starting', 'terminating'];
private confirmMessages = {
restartService: 'Restarting services will make DataLab unavailable for some time.',
discardChanges: 'Discard all unsaved changes.',
saveChanges: 'After you save changes you need to restart service.',
};
public activeEndpoint: string;
public endpoints: Array<string> | any;
@HostListener('window:keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) &&
event.key === 's' &&
this.activeTab.index !== 0 &&
this.router.url === '/configuration' &&
this.services[this.activeService].config !== this.services[this.activeService].serverConfig
) {
this.action('save');
event.preventDefault();
}
}
constructor(
private healthStatusService: HealthStatusService,
private appRoutingService: AppRoutingService,
private endpointService: EndpointService,
private configurationService: ConfigurationService,
private environmentsDataService: EnvironmentsDataService,
private router: Router,
public dialog: MatDialog,
public toastr: ToastrService,
) { }
ngOnInit() {
this.getEnvironmentHealthStatus();
this.getEndpoints()
.subscribe(endpoints => {
this.endpoints = endpoints;
this.endpoints = this.endpoints.map(endpoint => ({name: endpoint.name, status: endpoint.status }));
this.activeEndpoint = this.endpoints[0].name;
this.getServicesConfig(this.activeEndpoint);
});
this.environmentsDataService.getEnvironmentDataDirect().subscribe((data: any) => {
this.environments = EnvironmentModel.loadEnvironments(data);
this.environments.map(env => {
this.checkResource(env.status, env.endpoint);
if(env.resources?.length > 0) {
env.resources.map(resource => {
this.checkResource(resource.status, env.endpoint);
})
}
});
});
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
public checkResource(resourceStatus: string, endpoint: string) {
if( this.ingStatuses.includes(resourceStatus) ) {
if(!this.environmentStatuses[endpoint]) {
this.environmentStatuses[endpoint] = [];
this.environmentStatuses[endpoint].push(resourceStatus);
} else {
this.environmentStatuses[endpoint].push(resourceStatus);
}
}
}
private getEnvironmentHealthStatus(): void {
this.healthStatusService.getEnvironmentHealthStatus()
.pipe(
takeUntil(this.unsubscribe$)
)
.subscribe((result: any) => {
this.healthStatus = result;
!this.healthStatus.admin && !this.healthStatus.projectAdmin && this.appRoutingService.redirectToHomePage();
}
);
}
private getEndpoints(): Observable<{}> {
return this.endpointService.getEndpointsData();
}
public refreshConfig(): void {
this.getServicesConfig(this.activeEndpoint);
}
public action(action: string): void {
this.dialog.open(SettingsConfirmationDialogComponent, { data: {
action: action,
message: action === 'discard' ? this.confirmMessages.discardChanges : this.confirmMessages.saveChanges,
environmentStatuses: this.environmentStatuses,
activeEndpoint: this.activeEndpoint
}, panelClass: 'modal-sm' })
.afterClosed().subscribe(result => {
if (result && action === 'save') this.setServiceConfig(this.activeService, this.services[this.activeService].config);
if (result && action === 'discard') this.services[this.activeService].config = this.services[this.activeService].serverConfig;
this.configUpdate(this.activeService);
});
}
private getServicesConfig(endpoint): void {
this.configurationService.getServiceSettings(endpoint)
.pipe(
takeUntil(this.unsubscribe$)
)
.subscribe(config => {
for (const service in this.services) {
const file = `${service}.yml`;
this.services[service].config = config[file];
this.services[service].serverConfig = config[file];
this.configUpdate(service);
}
}
);
this.clearSelectedServices();
}
private setServiceConfig(service: string, config: string): void {
this.configurationService.setServiceConfig(service, config, this.activeEndpoint)
.pipe(
takeUntil(this.unsubscribe$)
)
.subscribe(res => {
this.getServicesConfig(this.activeEndpoint);
this.toastr.success('Service configuration saved!', 'Success!');
},
error => this.toastr.error( error.message || 'Service configuration is not saved', 'Oops!')
);
}
refreshServiceEditor(editor: ElementRef<HTMLElement>) {
if(editor) {
editor.nativeElement.children[3].scrollTop = 100;
editor.nativeElement.children[3].scrollTop = 0;
}
}
public tabChanged(tabChangeEvent: MatTabChangeEvent): void {
this.activeTab = tabChangeEvent;
if (this.activeTab.index === 1 && this.activeEndpoint === 'local') {
this.activeService = 'self-service';
this.refreshServiceEditor(this.selfEditor);
} else if ((this.activeEndpoint !== 'local' && this.activeTab.index === 1) ||
(this.activeTab.index === 2 && this.activeEndpoint === 'local')) {
this.activeService = 'provisioning';
this.refreshServiceEditor(this.provEditor);
} else if ((this.activeEndpoint !== 'local' && this.activeTab.index === 2) ||
(this.activeTab.index === 3 && this.activeEndpoint === 'local')) {
this.activeService = 'billing';
this.refreshServiceEditor(this.billingEditor);
} else {
this.activeService = '';
}
if (!!this.activeService) {
if (this.services[this.activeService].config !== this.services[this.activeService].serverConfig) {
this.dialog.open(SettingsConfirmationDialogComponent, { data: {
action: 'Was changed',
environmentStatuses: this.environmentStatuses,
activeEndpoint: this.activeEndpoint
}, panelClass: 'modal-sm' })
.afterClosed().subscribe(result => {
if (result) {
this.services[this.activeService].serverConfig = this.services[this.activeService].config;
} else {
this.services[this.activeService].config = this.services[this.activeService].serverConfig;
}
});
}
}
this.clearSelectedServices();
}
private clearSelectedServices(): void {
Object.keys(this.services).forEach(service => this.services[service].selected = false);
}
public toggleSettings(service): void {
this.services[service].selected = !this.services[service].selected;
}
public restartSingleService(ui: boolean, prov: boolean, billing: boolean, serviceName) {
this.configurationService.restartServices(ui, prov, billing, this.activeEndpoint)
.pipe(
takeUntil(this.unsubscribe$),
)
.subscribe(
res => {
this.messagesStatus.success.push(serviceName);
this.messagesStatus.counter -= 1;
},
error => {
if (serviceName === 'Self-service') {
this.messagesStatus.success.push(serviceName);
} else {
this.messagesStatus.error.push(serviceName);
}
this.messagesStatus.counter -= 1;
}
);
}
public restartServices(): void {
const selectedServices = [];
for (const service in this.services) {
if (this.services[service].selected) {
selectedServices.push(service);
}
}
this.dialog.open(
SettingsConfirmationDialogComponent,
{
data: {
action: 'restart',
services: selectedServices,
environmentStatuses: this.environmentStatuses,
activeEndpoint: this.activeEndpoint
},
panelClass: 'modal-sm'
})
.afterClosed().subscribe(result => {
if (result) {
this.messagesStatus.error = [];
this.messagesStatus.success = [];
if(this.environmentStatuses[this.activeEndpoint] && this.services['provisioning'].selected) {
this.services['provisioning'].selected = false;
}
if(this.services['self-service'].selected) {
this.messagesStatus.counter += 1;
this.restartSingleService(true, false, false, 'Self-service')
}
if(this.services['provisioning'].selected) {
this.messagesStatus.counter += 1;
this.restartSingleService(false, true, false, 'Provisioning service')
}
if(this.services['billing'].selected) {
this.messagesStatus.counter += 1;
this.restartSingleService(false, false, true, 'Billing service')
}
let timer = setInterval(() => {
if(this.messagesStatus.counter === 0) {
for(let key in this.messagesStatus) {
if(key === 'error' && this.messagesStatus[key].length > 0) {
this.toastr.error(`${this.messagesStatus[key].join(', ')} restarting failed`, 'Oops!');
} else if(key === 'success' && this.messagesStatus[key].length > 0) {
this.toastr.success(`${this.messagesStatus[key].join(', ')} restarting started!`, 'Success!');
}
}
clearInterval(timer);
}
}, 200);
this.clearSelectedServices();
} else {
this.clearSelectedServices();
}
});
}
public configUpdate(service: string): void {
this.services[service].isConfigChanged = this.services[service].config !== this.services[service].serverConfig;
}
public isServiceSelected(): boolean {
return Object.keys(this.services).every(service => !this.services[service].selected);
}
public setActiveEndpoint(endpoint) {
this.activeEndpoint = endpoint;
this.getServicesConfig(this.activeEndpoint);
this.activeTab.index = 0;
}
}
@Component({
selector: 'confirm-dialog',
template: `
<div id="dialog-box">
<div class="dialog-header">
<h4 class="modal-title"><span class="capitalize">{{ data.action }}</span> <span *ngIf="data.action === 'save' || data.action === 'discard'"> changes</span><span *ngIf="data.action === 'restart'">
service<span *ngIf="data.services.length > 1">s</span>
</span></h4>
<button type="button" class="close" (click)="dialogRef.close()">&times;</button>
</div>
<div mat-dialog-content class="content">
<ng-template [ngIf]="data.action === 'restart' && !data.environmentStatuses[data.activeEndpoint]?.length" ]>
<span class="strong">{{data.services.join(', ') | titlecase}}</span>
<span class="strong" *ngIf="data.services.length > 1 || (data.services.length === 1 && data.services[0] !== 'self-service')"> service</span>
<span class="strong" [hidden]="(data.services.length < 2) || data.services.length === 2 && data.services[0] === 'self-service'">s</span>: restarting will make DataLab unavailable for some time.
</ng-template>
<ng-template [ngIf]="data.action === 'restart' && data.environmentStatuses[data.activeEndpoint]?.length && filterProvisioning.length" ]>
<span class="strong" >{{filterProvisioning.join(', ') | titlecase}}</span>
<span class="strong" *ngIf="filterProvisioning.length > 1 || (filterProvisioning.length === 1 && filterProvisioning[0] !== 'self-service')"> service</span>
<span [hidden]="(filterProvisioning.length < 2) || filterProvisioning.length === 2 && filterProvisioning[0] === 'self-service'">s</span>: restarting will make DataLab unavailable for some time.
</ng-template>
<ng-template [ngIf]="data.action === 'restart' && data.environmentStatuses[data.activeEndpoint]?.length && (data?.services?.includes('provisioning') || !filterProvisioning.length)">
<div class="warning" [ngStyle]="data?.services?.includes('provisioning') && data.services?.length > 1 && {'margin-top': '10px'}">
<span>Provisioning service: </span>can not be restarted because one of resources is in processing stage. Please try again later.
</div>
</ng-template>
<ng-template [ngIf]="data.action === 'discard'" ]>Discard all unsaved changes.</ng-template>
<ng-template [ngIf]="data.action === 'save'" ]>After you save changes you need to restart service.</ng-template>
</div>
<div class="text-center " *ngIf="!data.environmentStatuses[data.activeEndpoint]?.length || (data.environmentStatuses[data.activeEndpoint]?.length && (!data?.services?.includes('provisioning') || filterProvisioning?.length))">
<p class="strong">Do you want to proceed?</p>
</div>
<div class="text-center m-top-20 pb-25" *ngIf="!data.environmentStatuses[data.activeEndpoint]?.length || (data.environmentStatuses[data.activeEndpoint]?.length && (!data?.services?.includes('provisioning') || filterProvisioning.length))">
<button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">No</button>
<button type="button" class="butt butt-success" mat-raised-button (click)="dialogRef.close(true)">Yes</button>
</div>
<div class="text-center m-top-20 pb-25" *ngIf="data.action === 'restart' && data.environmentStatuses[data.activeEndpoint]?.length && data?.services?.includes('provisioning') && data.services.length === 1">
<button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">Close</button>
</div>
</div>
`,
styles: [
`
.content { color: #718ba6; padding: 20px 50px; font-size: 14px; font-weight: 400; margin: 0; }
header { display: flex; justify-content: space-between; color: #607D8B; }
header h4 i { vertical-align: bottom; }
header a i { font-size: 20px; }
header a:hover i { color: #35afd5; cursor: pointer; }
.content{padding: 35px 30px 30px 30px; text-align: center;}
label{cursor: pointer;}
.warning{color: #EF5C4B;}
.warning span {font-weight: 600;}`
]
})
export class SettingsConfirmationDialogComponent implements OnInit {
filterProvisioning = [];
constructor(
public dialogRef: MatDialogRef<SettingsConfirmationDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
}
ngOnInit() {
this.filterProvisioning = this.data?.services?.filter(service => service !== 'provisioning');
}
}