| /* |
| * 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()">×</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'); |
| } |
| } |
| |
| |