import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core' import SASjs, { SASjsConfig } from '@sasjs/adapter' import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings' import { DeployService } from 'src/app/services/deploy.service' import { EventService } from 'src/app/services/event.service' import { LoggerService } from 'src/app/services/logger.service' import { SasViyaService } from 'src/app/services/sas-viya.service' import { SasService } from 'src/app/services/sas.service' import { ViyaApiCurrentUser } from 'src/app/viya-api-explorer/models/viya-api-current-user.model' import { Item, ViyaApiIdentities } from 'src/app/viya-api-explorer/models/viya-api-identities.model' import { ComputeContextDetails } from 'src/app/viya-api-explorer/models/viya-compute-context-details.model' import { ViyaComputeContexts, Item as ComputeContextItem } from 'src/app/viya-api-explorer/models/viya-compute-contexts.model' @Component({ selector: 'app-automatic-deploy', templateUrl: './automatic.component.html', styleUrls: ['./automatic.component.scss'], encapsulation: ViewEncapsulation.None }) export class AutomaticComponent implements OnInit { @Input() sasJs!: SASjs @Input() sasJsConfig: SASjsConfig = new SASjsConfig() @Input() dcAdapterSettings: DcAdapterSettings | undefined @Input() appLoc: string = '' @Input() dcPath: string = '' @Input() selectedAdminGroup: string = '' @Output() onNavigateToHome: EventEmitter = new EventEmitter() public selectedComputeContext: string = '' public makeDataResponse: string = '' public jsonFile: any = null public autodeploying: boolean = false public autodeployDone: boolean = false public recreateDatabaseModal: boolean = false public isSubmittingJson: boolean = false public isJsonSubmitted: boolean = false /** * Default was `false` when deploy was done with frontend and backend separately. * Now we are using only streaming app, so we always want to recreate database (makedata) */ public recreateDatabase: boolean = true public createDatabaseLoading: boolean = false public adminGroupsLoading: boolean = false public currentUserInfoLoading: boolean = false public computeContextsLoading: boolean = false public adminGroups: { id: string; name: string }[] = [] public runningAsUser: string | undefined public currentUserInfo: ViyaApiCurrentUser | null = null public computeContexts: ComputeContextItem[] = [] /** autoDeployStatus * This object presents the status for two steps that we have for deploy. * `deployServicePack` - Creating services based on `viya.json` * `runMakeData` - Running `makedata` service * If any of them is `null` or `false` that means step failed * and will be shown to user on deploy done modal. */ public autoDeployStatus: { deployServicePack: any runMakeData: any } = { deployServicePack: null, runMakeData: null } constructor( private eventService: EventService, private deployService: DeployService, private sasService: SasService, private sasViyaService: SasViyaService, private loggerService: LoggerService ) {} ngOnInit(): void { const promiseGetAadminGroups = this.getAdminGroups() const getCurrentUser = this.getCurrentUser() const getComputeContexts = this.getComputeContexts() Promise.all([ promiseGetAadminGroups, getCurrentUser, getComputeContexts ]).then(() => { setTimeout(() => { if (this.selectedComputeContext) { this.onComputeContextChange(this.selectedComputeContext) } }, 500) }) } public async getComputeContexts() { return new Promise((resolve, reject) => { this.computeContextsLoading = true this.sasViyaService.getComputeContexts().subscribe( (res: ViyaComputeContexts) => { this.computeContextsLoading = false const defaultContext = res.items.find( (item: ComputeContextItem) => item.name === 'SAS Job Execution compute context' ) if (defaultContext) { this.selectedComputeContext = defaultContext.id } this.computeContexts = res.items resolve() }, (err) => { reject(err) } ) }) } public async getCurrentUser() { return new Promise((resolve, reject) => { this.currentUserInfoLoading = true this.sasViyaService.getCurrentUser().subscribe( (res: ViyaApiCurrentUser) => { this.currentUserInfoLoading = false this.currentUserInfo = res this.dcPath = `/export/viya/homes/${res.id}` resolve() }, (err) => { console.error('Error while getting current user', err) reject(err) } ) }) } public async getAdminGroups() { return new Promise((resolve, reject) => { this.adminGroupsLoading = true this.sasViyaService .getAdminGroups() .subscribe((res: ViyaApiIdentities) => { this.adminGroupsLoading = false // Map admin groups with only needed fields this.adminGroups = res.items.map((item: Item) => { return { id: item.id, name: item.name } }) resolve() }), (err: any) => { this.adminGroupsLoading = false this.loggerService.error('Error while getting admin groups', err) this.eventService.showAbortModal('admin groups', err) reject(err) } }) } public async onComputeContextChange(computeContextId: string) { this.sasViyaService .getComputeContextById(computeContextId) .subscribe((res: ComputeContextDetails) => { if (res.attributes && res.attributes.runServerAs) { this.runningAsUser = res.attributes.runServerAs } else { this.runningAsUser = this.currentUserInfo?.id || 'unknown' } }) } public getComputeContextName(id: string): string | undefined { return ( this.computeContexts.find( (context: ComputeContextItem) => context.id === id )?.name || undefined ) } /** * Executes sas.json file to deploy the backend * Method will first try to run the `auto deploy` * If that fails the rest of the code is ignored. * If request is successfull, method will continue to try * to create database if checkbox is toggled on */ public async executeJson() { this.isSubmittingJson = true try { let uploadJsonFile = await this.sasJs.deployServicePack( this.jsonFile, this.dcAdapterSettings?.appLoc, undefined, undefined, true ) this.autoDeployStatus.deployServicePack = true this.isJsonSubmitted = true } catch (ex: any) { let textEx = '' if (typeof ex.message !== 'string') { textEx = JSON.stringify(ex).replace(/\\/gm, '') } else { textEx = ex.message } this.autoDeployStatus.deployServicePack = false this.eventService.showInfoModal( 'Deploy error', `Exception: \n ${textEx !== '' ? textEx : ex}` ) this.autodeploying = false this.autodeployDone = false return } this.isSubmittingJson = false } public async runAutoDeploy(executeJson: boolean = false) { this.autodeploying = true if (executeJson) { this.executeJson() } if (this.recreateDatabase) { this.createDatabase() } else { this.autodeployDone = true } } /** * Runs the `makedata` request sending the ADMIN and DCPATH values */ public createDatabase() { let data = { fromjs: [ { ADMIN: this.selectedAdminGroup, DCPATH: this.dcPath } ] } // Get and run service using the selected context name let selectedComputeContextName = this.sasJsConfig.contextName if (this.selectedComputeContext.length && this.computeContexts.length) { const computeContextName = this.getComputeContextName( this.selectedComputeContext ) if (computeContextName) { selectedComputeContextName = computeContextName } } /** * We are overriding default `sasjsConfig` object fields with this object fields. * Here we want to run this request using original WEB method. * contextName: null is the MUST field for it. */ let overrideConfig = { useComputeApi: null, contextName: selectedComputeContextName, debug: true } this.sasJs .request(`services/admin/makedata`, data, overrideConfig, () => { this.sasService.shouldLogin.next(true) }) .then((res: any) => { this.autodeployDone = true try { this.makeDataResponse = JSON.stringify(res) } catch { this.makeDataResponse = res } if (res.result && res.result.length > 0) { this.autoDeployStatus.runMakeData = true } else { this.autoDeployStatus.runMakeData = false } if (typeof res.sasjsAbort !== 'undefined') { const abortRes = res const abortMsg = abortRes.sasjsAbort[0].MSG const macMsg = abortRes.sasjsAbort[0].MAC this.eventService.showAbortModal('makedata', abortMsg, { SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT, SYSERRORTEXT: abortRes.SYSERRORTEXT, MAC: macMsg }) } this.updateIndexHtmlComputeContext() }) .catch((err: any) => { this.eventService.showAbortModal('makedata', JSON.stringify(err)) this.autoDeployStatus.runMakeData = false this.autodeployDone = true try { this.makeDataResponse = JSON.stringify(err) } catch { this.makeDataResponse = err } }) } /** * Only when on Viya, this method will update the `contextname` in the `DataController.html` on the SAS drive * This is needed to ensure that the DC will use the same compute context `makedata` service used to run against. */ public async updateIndexHtmlComputeContext() { const indexHtmlContent = await this.sasService.getFileContent( `${this.appLoc}/services`, 'DataController.html' ) if (!indexHtmlContent) { this.loggerService.error( `Failed to get DataController.html at ${this.appLoc}/services` ) return } const computeContextName = this.getComputeContextName( this.selectedComputeContext ) if (!computeContextName) { this.loggerService.error( `Compute context name not found for ID: ${this.selectedComputeContext} | List: ${JSON.stringify(this.computeContexts)}` ) return } const updatedContent = indexHtmlContent.replace( /contextname="[^"]*"/g, `contextname="${computeContextName}"` ) await this.sasService .updateFileContent( `${this.appLoc}/services`, 'DataController.html', updatedContent ) .catch((err: any) => { this.loggerService.error(`Failed to update DataController.html: ${err}`) }) } public downloadFile( content: any, filename: string, extension: string = 'txt' ) { this.deployService.downloadFile(content, filename, extension) } public async onJsonFileChange(event: any) { let file = event.target.files[0] this.jsonFile = await this.deployService.readFile(file) } public recreateDatabaseClicked(event: Event) { ;(event.target).checked === true ? (this.recreateDatabaseModal = true) : '' } public clearUploadInput(event: Event) { this.deployService.clearUploadInput(event) } public openSasRequestsModal() { this.eventService.openRequestsModal() } public navigateToHome() { this.onNavigateToHome.emit() } }