A misconfigured Viya computeTasks deployment makes the Compute service return a plain-text "Job error" body instead of JSON. @sasjs/adapter can't parse it and resolves with that raw text as adapterResponse, so both startup and manual deploy validation reported every expected field as "not present" without ever showing the actual cause. Added getMalformedAdapterResponseMessage to detect a non-object adapterResponse and surface its real text instead.
344 lines
8.8 KiB
TypeScript
344 lines
8.8 KiB
TypeScript
import {
|
|
Component,
|
|
Input,
|
|
OnInit,
|
|
Output,
|
|
EventEmitter,
|
|
ViewEncapsulation
|
|
} from '@angular/core'
|
|
import SASjs, { SASjsConfig } from '@sasjs/adapter'
|
|
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
|
|
import { RequestWrapperResponse } from 'src/app/models/request-wrapper/RequestWrapperResponse'
|
|
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 { SasService } from 'src/app/services/sas.service'
|
|
import { getMalformedAdapterResponseMessage } from 'src/app/shared/utils/get-malformed-adapter-response-message'
|
|
|
|
@Component({
|
|
selector: 'app-manual-deploy',
|
|
templateUrl: './manual.component.html',
|
|
styleUrls: ['./manual.component.scss'],
|
|
encapsulation: ViewEncapsulation.None,
|
|
standalone: false
|
|
})
|
|
export class ManualComponent implements OnInit {
|
|
@Input() sasJs!: SASjs
|
|
@Input() sasJsConfig: SASjsConfig = new SASjsConfig()
|
|
@Input() dcAdapterSettings: DcAdapterSettings | undefined
|
|
|
|
@Output() onNavigateToHome: EventEmitter<any> = new EventEmitter<any>()
|
|
|
|
public needsLogin: boolean = false
|
|
public adminGroups: any = []
|
|
public allContexts: any = []
|
|
|
|
public appLoc: string = ''
|
|
public dcPath: string = ''
|
|
public selectedAdminGroup: string = ''
|
|
public selectedContext: string = ''
|
|
public jobLog: string = ''
|
|
public makeDataResponse: string = ''
|
|
public linesOfCode: any = []
|
|
public fileName: string = ''
|
|
public preloadedFile: boolean = true
|
|
public executeSASEnabled: boolean = false
|
|
|
|
public contextsLoading: boolean = false
|
|
public createDatabaseLoading: boolean = false
|
|
public executingScript: boolean = false
|
|
public downloadFileBtn: boolean = false
|
|
public isValidating: boolean = false
|
|
public jsonFile: any = null
|
|
public isSubmittingJson: boolean = false
|
|
public isJsonSubmitted: boolean = false
|
|
public validationState: string = 'none'
|
|
|
|
constructor(
|
|
private sasService: SasService,
|
|
private eventService: EventService,
|
|
private loggerService: LoggerService,
|
|
private deployService: DeployService
|
|
) {}
|
|
|
|
ngOnInit(): void {}
|
|
|
|
/**
|
|
* FIXME: Remove
|
|
*/
|
|
public async executableContext() {
|
|
// getExecutableContexts now need AuthConfig parameter which we don't have on web
|
|
// this.contextsLoading = true
|
|
// let contexts = await this.sasJs.getExecutableContexts()
|
|
// this.allContexts = contexts
|
|
// if (contexts.length === 0) {
|
|
// this.selectedContext = ''
|
|
// } else {
|
|
// this.selectedContext = this.allContexts[0].name
|
|
// }
|
|
// this.contextsLoading = false
|
|
}
|
|
|
|
/**
|
|
* Removes sas.json file attached to the input
|
|
*/
|
|
public clearUploadInput(event: Event) {
|
|
this.deployService.clearUploadInput(event)
|
|
}
|
|
|
|
/**
|
|
* Reads attached SAS file to be sent to sas for execution (backend deploy)
|
|
*/
|
|
public onSasFileChange(event: any) {
|
|
this.preloadedFile = false
|
|
|
|
let file = event.target.files[0]
|
|
this.fileName = file.name
|
|
|
|
let fileReader = new FileReader()
|
|
|
|
fileReader.onload = () => {
|
|
if (fileReader.result) {
|
|
this.linesOfCode = (fileReader.result as string).split('\n')
|
|
this.linesOfCode = this.linesOfCode.filter((el: string) => {
|
|
return el !== '' && el !== null
|
|
})
|
|
|
|
this.executeSASEnabled = true
|
|
|
|
this.addPrecodeLines()
|
|
}
|
|
}
|
|
|
|
fileReader.readAsText(file)
|
|
}
|
|
|
|
/**
|
|
* Reads attached JSON file to be sent to sas for execution (backend deploy)
|
|
*/
|
|
public async onJsonFileChange(event: any) {
|
|
let file = event.target.files[0]
|
|
|
|
this.jsonFile = await this.deployService.readFile(file)
|
|
}
|
|
|
|
/**
|
|
* Appending precode lines to the attached sas or json file for backend deploy
|
|
*/
|
|
public addPrecodeLines() {
|
|
let headerLines = [
|
|
`%let context=${this.selectedContext};`,
|
|
`%let appLoc=${this.appLoc};`,
|
|
`%let admin=${this.selectedAdminGroup};`,
|
|
`%let dcpath=${this.dcPath};`
|
|
]
|
|
|
|
this.linesOfCode.unshift(...headerLines)
|
|
}
|
|
|
|
/**
|
|
* Downloadng file with precode included
|
|
*/
|
|
public downloadSasPrecodeFile() {
|
|
let linesAsText = this.linesOfCode.join('\n')
|
|
let filename = this.fileName.split('.')[0]
|
|
|
|
this.downloadFile(linesAsText, filename, 'sas')
|
|
}
|
|
|
|
/**
|
|
* Used for downloading log and repsonse as a file
|
|
*/
|
|
public downloadFile(
|
|
content: any,
|
|
filename: string,
|
|
extension: string = 'txt'
|
|
) {
|
|
this.deployService.downloadFile(content, filename, extension)
|
|
}
|
|
|
|
/**
|
|
* Saving dcpath to localstorage
|
|
* FIXME: maybe it'snot necessary
|
|
*/
|
|
public saveDcPath() {
|
|
localStorage.setItem('deploy_dc_loc', this.dcPath)
|
|
}
|
|
|
|
/**
|
|
* Send sas.json to be executed (deploying backend)
|
|
*/
|
|
public async executeJson() {
|
|
this.isSubmittingJson = true
|
|
|
|
try {
|
|
let uploadJsonFile = await this.sasJs.deployServicePack(
|
|
this.jsonFile,
|
|
this.dcAdapterSettings?.appLoc || '',
|
|
undefined,
|
|
undefined,
|
|
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.eventService.showInfoModal(
|
|
'Deploy error',
|
|
`Exception: \n ${textEx !== '' ? textEx : ex}`
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
this.isSubmittingJson = false
|
|
}
|
|
|
|
/**
|
|
* Send sas file to be executed (deploying backend)
|
|
*/
|
|
public async executeSAS() {
|
|
this.executingScript = true
|
|
this.jobLog = ''
|
|
this.makeDataResponse = ''
|
|
|
|
try {
|
|
let executeResponse = await this.sasJs.executeScript({
|
|
fileName: this.fileName,
|
|
linesOfCode: this.linesOfCode,
|
|
contextName: this.selectedContext
|
|
})
|
|
|
|
this.loggerService.log(executeResponse)
|
|
|
|
if (typeof executeResponse.log === 'string') {
|
|
executeResponse.log = JSON.parse(executeResponse.log)
|
|
}
|
|
|
|
if (executeResponse.jobStatus === 'error') {
|
|
alert('Error!')
|
|
} else {
|
|
this.jobLog = executeResponse.log.items
|
|
? executeResponse.log.items.map((i: any) => i.line).join('\n')
|
|
: JSON.stringify(executeResponse.log)
|
|
}
|
|
|
|
this.executingScript = false
|
|
} catch (err) {
|
|
this.executingScript = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Running makedata service
|
|
* @param newTab open and run in new tab
|
|
*/
|
|
public createDatabase(newTab: boolean = true) {
|
|
if (newTab) {
|
|
let url =
|
|
this.sasService.getSasjsConfig().serverUrl +
|
|
'/SASJobExecution/?_program=' +
|
|
this.dcAdapterSettings?.appLoc ||
|
|
'' +
|
|
'/admin/makedata' +
|
|
'&ADMIN=' +
|
|
this.selectedAdminGroup +
|
|
'&DCPATH=' +
|
|
this.dcPath +
|
|
this.sasService.getDebugUrlParam()
|
|
|
|
window.open(url, '_blank')
|
|
|
|
return
|
|
}
|
|
|
|
this.createDatabaseLoading = true
|
|
|
|
let data = {
|
|
fromjs: [
|
|
{
|
|
ADMIN: this.selectedAdminGroup,
|
|
DCPATH: this.dcPath
|
|
}
|
|
]
|
|
}
|
|
|
|
/**
|
|
* 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: this.sasJsConfig.contextName,
|
|
debug: true
|
|
}
|
|
|
|
this.sasJs
|
|
.request(`services/admin/makedata`, data, overrideConfig, () => {
|
|
this.sasService.shouldLogin.next(true)
|
|
})
|
|
.then((res: any) => {
|
|
try {
|
|
this.makeDataResponse = JSON.stringify(res)
|
|
} catch {
|
|
this.makeDataResponse = res
|
|
}
|
|
|
|
this.createDatabaseLoading = false
|
|
})
|
|
.catch((err: any) => {
|
|
this.createDatabaseLoading = false
|
|
|
|
try {
|
|
this.makeDataResponse = JSON.stringify(err)
|
|
} catch {
|
|
this.makeDataResponse = err
|
|
}
|
|
})
|
|
}
|
|
|
|
public navigateToHome() {
|
|
this.onNavigateToHome.emit()
|
|
}
|
|
|
|
public validateDeploy() {
|
|
this.isValidating = true
|
|
|
|
this.sasService
|
|
.request('public/startupservice', null)
|
|
.then((res: RequestWrapperResponse) => {
|
|
this.loggerService.log(res.adapterResponse)
|
|
|
|
const malformedMessage = getMalformedAdapterResponseMessage(
|
|
res.adapterResponse
|
|
)
|
|
if (malformedMessage) {
|
|
this.validationState = 'error'
|
|
this.eventService.showInfoModal('Error', malformedMessage)
|
|
} else if (res.adapterResponse.saslibs) {
|
|
this.validationState = 'success'
|
|
} else {
|
|
this.validationState = 'error'
|
|
}
|
|
|
|
this.isValidating = false
|
|
})
|
|
.catch((err: any) => {
|
|
this.isValidating = false
|
|
this.validationState = 'error'
|
|
})
|
|
}
|
|
|
|
public deleteKeys() {
|
|
localStorage.removeItem('deploy_dc_loc')
|
|
}
|
|
}
|