Files
dc/client/src/app/deploy/sections/automatic/automatic.component.ts
2025-06-04 17:37:44 +02:00

421 lines
12 KiB
TypeScript

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<any> = new EventEmitter<any>()
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<void>((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<void>((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<void>((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) {
;(<HTMLInputElement>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()
}
}