src/app/shared/requests-modal/requests-modal.component.ts
| encapsulation | ViewEncapsulation.None |
| selector | app-requests-modal |
| styleUrls | ./requests-modal.component.scss |
| templateUrl | ./requests-modal.component.html |
Properties |
|
Methods |
|
Inputs |
Outputs |
Accessors |
constructor(sasService: SasService, loggerService: LoggerService, helperService: HelperService)
|
||||||||||||
|
Parameters :
|
| opened | |
Type : boolean
|
|
| openedChange | |
Type : EventEmitter
|
|
| Public cutAppLoc | ||||||
cutAppLoc(link: string)
|
||||||
|
Parameters :
Returns :
any
|
| downloadGeneratedCode | ||||||
downloadGeneratedCode(generatedCode: string)
|
||||||
|
Parameters :
Returns :
void
|
| downloadLog | ||||||
downloadLog(logFile: string)
|
||||||
|
Parameters :
Returns :
void
|
| downloadSourceCode | ||||||
downloadSourceCode(sourceCode: string)
|
||||||
|
Parameters :
Returns :
void
|
| Public formatTimestamp | ||||||
formatTimestamp(timestamp: any)
|
||||||
|
Parameters :
Returns :
any
|
| Public goToLogLine |
goToLogLine(linkingLine: string, requestStackId: string, type: string)
|
|
Returns :
void
|
| Public modalOpenChange | ||||||
modalOpenChange(state: any)
|
||||||
|
Parameters :
Returns :
void
|
| Public modalOpened |
modalOpened()
|
|
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Returns :
void
|
| Public Async parseErrorsAndWarnings | ||||||
parseErrorsAndWarnings(req: any)
|
||||||
|
Parameters :
Returns :
any
|
| Public parseLogTimestamp | ||||||
parseLogTimestamp(timestamp: any)
|
||||||
|
Parameters :
Returns :
string
|
| Public timestampFromNow | ||||||
timestampFromNow(timestamp: any)
|
||||||
|
Parameters :
Returns :
string
|
| Private _opened |
Type : boolean
|
Default value : false
|
| Public sasGeneratedCodeActive |
Type : boolean
|
Default value : false
|
| Public sasjsConfig |
Type : unknown
|
Default value : this.sasService.getSasjsConfig()
|
| Public sasjsRequests |
Type : SASjsRequestExtended[]
|
Default value : []
|
| Public sasLogActive |
Type : boolean
|
Default value : true
|
| Public sasSourceCodeActive |
Type : boolean
|
Default value : false
|
| Public serverType |
Type : string
|
| Public tablesActive |
Type : boolean
|
Default value : false
|
| Public workTables |
Type : any
|
| opened | ||||||
getopened()
|
||||||
setopened(value: boolean)
|
||||||
|
Parameters :
Returns :
void
|
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewEncapsulation
} from '@angular/core'
import { SASjsRequest } from '@sasjs/adapter'
import moment from 'moment'
import { HelperService } from 'src/app/services/helper.service'
import { LoggerService } from '../../services/logger.service'
import { SasService } from '../../services/sas.service'
import { ServerType } from '@sasjs/utils/types/serverType'
interface SASjsRequestExtended extends SASjsRequest {
parsedTimestamp?: string
logErrors?: string[]
logWarnings?: string[]
selectedTable?: string
}
@Component({
selector: 'app-requests-modal',
templateUrl: './requests-modal.component.html',
styleUrls: ['./requests-modal.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: false
})
export class RequestsModalComponent implements OnInit {
private _opened: boolean = false
get opened(): boolean {
return this._opened
}
@Input()
set opened(value: boolean) {
this._opened = value
if (value) this.modalOpened()
this.loggerService.log(this.sasjsRequests)
}
@Output() openedChange = new EventEmitter()
public sasLogActive: boolean = true
public sasSourceCodeActive: boolean = false
public sasGeneratedCodeActive: boolean = false
public tablesActive: boolean = false
public sasjsConfig = this.sasService.getSasjsConfig()
public serverType: string
public sasjsRequests: SASjsRequestExtended[] = []
public workTables: any
constructor(
private sasService: SasService,
private loggerService: LoggerService,
private helperService: HelperService
) {
this.sasjsConfig = this.sasService.getSasjsConfig()
this.serverType = this.sasService.getServerType()
}
ngOnInit(): void {}
public parseLogTimestamp(timestamp: any) {
return `${this.formatTimestamp(timestamp)} ${this.timestampFromNow(
timestamp
)}`
}
public cutAppLoc(link: string) {
return link.replace(this.sasjsConfig.appLoc + '/', '')
}
public formatTimestamp(timestamp: any) {
return moment(timestamp).format()
? moment(timestamp).format('dddd, MMMM Do YYYY, h:mm:ss a')
: timestamp
}
public timestampFromNow(timestamp: any) {
return moment(timestamp).format() ? ` (${moment(timestamp).fromNow()})` : ''
}
public modalOpenChange(state: any) {
this.opened = state
this.openedChange.emit(this.opened)
}
public modalOpened() {
this.sasjsRequests = this.sasService.getSasRequests()
for (let request of this.sasjsRequests) {
this.parseErrorsAndWarnings(request)
request.serviceLink = this.cutAppLoc(request.serviceLink)
request.parsedTimestamp = this.parseLogTimestamp(request.timestamp)
}
}
public goToLogLine(
linkingLine: string,
requestStackId: string,
type: string
) {
const logWrapper: HTMLElement | null = document.querySelector(
`#${requestStackId} .log-wrapper.saslog`
)
if (!logWrapper) return
if (this.serverType === 'SASVIYA') {
// For VIYA (textContent approach)
const logContent = logWrapper.textContent || ''
const logLines = logContent.split('\n')
let lineIndex = -1
// Find the matching line index
for (let i = 0; i < logLines.length; i++) {
if (logLines[i].includes(linkingLine)) {
lineIndex = i
break
}
}
if (lineIndex === -1) return
// Create a temporary div to calculate line height
const tempDiv = document.createElement('div')
tempDiv.className = 'temp-line-height-calc'
tempDiv.textContent = 'X'
logWrapper.appendChild(tempDiv)
const lineHeight = tempDiv.clientHeight
logWrapper.removeChild(tempDiv)
// Scroll to the appropriate position
logWrapper.scrollTop = lineHeight * lineIndex
// Create highlighting overlay at that position
const overlay = document.createElement('div')
overlay.className = `line-highlight-overlay ${type === 'error' ? 'error-highlight' : 'warning-highlight'}`
// We are setting the height using a CSS variable to the HTML element
// this does not trigger CSP errors because it's driven by JS
overlay.classList.add('temp-height-setter')
document.documentElement.style.setProperty(
'--line-height',
`${lineHeight}px`
)
overlay.classList.add('line-position-setter')
document.documentElement.style.setProperty(
'--line-top',
`${lineHeight * lineIndex}px`
)
logWrapper.appendChild(overlay)
setTimeout(() => {
if (logWrapper.contains(overlay)) {
logWrapper.removeChild(overlay)
}
}, 3000)
} else {
// For non-VIYA (innerHTML approach)
const allLines: NodeListOf<Element> = document.querySelectorAll(
`#${requestStackId} .log-wrapper.saslog font`
)
for (let line of Array.from(allLines)) {
if (line.textContent?.includes(linkingLine)) {
logWrapper.scrollTop =
(line as HTMLElement).offsetTop - logWrapper.offsetTop
// Use class instead of inline style
line.classList.add('highlighted-line')
setTimeout(() => {
line.classList.remove('highlighted-line')
}, 3000)
break
}
}
}
}
public async parseErrorsAndWarnings(req: any) {
if (!req || !req.logFile || typeof req.logFile !== 'string') return
if (req['logErrors'] !== undefined || req['logWarnings'] !== undefined)
return
let errorLines = []
let warningLines = []
// Create a safe version of the log content
let logLines = req.logFile.split('\n')
let originalLogLines = [...logLines]
for (let i = 0; i < logLines.length; i++) {
if (/<.*>ERROR/gm.test(logLines[i])) {
let errorLine = logLines[i].substring(
logLines[i].indexOf('E'),
logLines[i].length - 1
)
errorLines.push(errorLine)
} else if (/^ERROR/gm.test(logLines[i])) {
errorLines.push(logLines[i])
if (this.serverType !== 'SASVIYA') {
logLines[i] = '<font class="error-line">' + logLines[i] + '</font>'
}
}
if (/<.*>WARNING/gm.test(logLines[i])) {
let warningLine = logLines[i].substring(
logLines[i].indexOf('W'),
logLines[i].length - 1
)
warningLines.push(warningLine)
} else if (/^WARNING/gm.test(logLines[i])) {
warningLines.push(logLines[i])
if (this.serverType !== 'SASVIYA') {
logLines[i] = '<font class="warning-line">' + logLines[i] + '</font>'
}
}
}
this.loggerService.log(warningLines)
// For VIYA, store both versions of the log
if (this.serverType === 'SASVIYA') {
req.originalLogFile = originalLogLines.join('\n')
req.logFileLineMap = {}
// Create a mapping of error/warning lines to their indices
errorLines.forEach((errorLine) => {
for (let i = 0; i < originalLogLines.length; i++) {
if (originalLogLines[i].includes(errorLine)) {
if (!req.logFileLineMap.errors) req.logFileLineMap.errors = {}
req.logFileLineMap.errors[errorLine] = i
break
}
}
})
warningLines.forEach((warningLine) => {
for (let i = 0; i < originalLogLines.length; i++) {
if (originalLogLines[i].includes(warningLine)) {
if (!req.logFileLineMap.warnings) req.logFileLineMap.warnings = {}
req.logFileLineMap.warnings[warningLine] = i
break
}
}
})
}
req.logFile = logLines.join('\n')
req.logErrors = errorLines
req.logWarnings = warningLines
}
downloadLog(logFile: string) {
const timestamp = new Date().valueOf()
this.helperService.downloadTextFile(`logFile-${timestamp}`, logFile)
}
downloadSourceCode(sourceCode: string) {
const timestamp = new Date().valueOf()
this.helperService.downloadTextFile(`sourceCode-${timestamp}`, sourceCode)
}
downloadGeneratedCode(generatedCode: string) {
const timestamp = new Date().valueOf()
this.helperService.downloadTextFile(
`generatedCode-${timestamp}`,
generatedCode
)
}
}
<clr-modal
[clrModalOpen]="opened"
(clrModalOpenChange)="modalOpenChange($event)"
clrModalSize="xl"
class="requests-modal"
>
<h3 class="modal-title">
Request History
<p class="mt-0 d-inline-block">App Location: {{ sasjsConfig.appLoc }}</p>
</h3>
<div class="modal-body h-70vh">
@if (sasjsRequests.length < 1) {
<p class="no-reqs">No requests made</p>
}
@if (opened && sasjsRequests.length > 0) {
<clr-stack-view>
@for (programLog of sasjsRequests; track programLog; let i = $index) {
<clr-stack-block [id]="'request_' + i" [clrStackViewLevel]="1">
<clr-stack-label>
{{ programLog.serviceLink }}
<span class="ml-10">
@if (programLog.logWarnings?.length! > 0) {
<span>
<clr-icon
class="mb-5 color-green"
shape="exclamation-circle"
></clr-icon>
</span>
}
@if (programLog.logErrors?.length! > 0) {
<span>
<clr-icon
class="mb-5 color-red"
shape="exclamation-triangle"
></clr-icon>
</span>
}
</span>
</clr-stack-label>
<clr-stack-content>{{
programLog.parsedTimestamp
}}</clr-stack-content>
<clr-stack-block class="content">
<clr-stack-content>
<clr-tabs>
<clr-tab>
<button clrTabLink>SAS Log</button>
<ng-template [(clrIfActive)]="sasLogActive">
<clr-tab-content>
<clr-tree>
@if (programLog.logErrors) {
<clr-tree-node class="err-links">
<clr-icon
class="color-red"
shape="exclamation-triangle"
></clr-icon>
Errors ({{ programLog.logErrors.length! }})
@for (
err of programLog.logErrors || [];
track err
) {
<clr-tree-node
(click)="
goToLogLine(err, 'request_' + i, 'error')
"
>
<button class="clr-treenode-link">
{{ err }}
</button>
</clr-tree-node>
}
</clr-tree-node>
}
@if (programLog.logWarnings) {
<clr-tree-node class="err-links">
<clr-icon
class="color-green"
shape="exclamation-circle"
></clr-icon>
Warnings ({{ programLog.logWarnings.length! }})
@for (
warning of programLog.logWarnings || [];
track warning
) {
<clr-tree-node
(click)="
goToLogLine(
warning,
'request_' + i,
'warning'
)
"
>
<button class="clr-treenode-link">
{{ warning }}
</button>
</clr-tree-node>
}
</clr-tree-node>
}
</clr-tree>
@if (serverType !== 'SASVIYA') {
<div
class="log-wrapper saslog"
[innerHTML]="programLog.logFile"
></div>
}
@if (serverType === 'SASVIYA') {
<div
class="log-wrapper saslog viya"
[textContent]="programLog.logFile"
></div>
}
@if (programLog.logFile.length > 0) {
<button
(click)="downloadLog(programLog.logFile)"
type="button"
class="btn btn-link"
>
Download
</button>
}
</clr-tab-content>
</ng-template>
</clr-tab>
<clr-tab>
<button clrTabLink>SAS Source Code</button>
<ng-template [(clrIfActive)]="sasSourceCodeActive">
<clr-tab-content>
<div class="log-wrapper">
{{ programLog.sourceCode }}
</div>
@if (programLog.sourceCode.length > 0) {
<button
(click)="downloadSourceCode(programLog.sourceCode)"
type="button"
class="btn btn-link"
>
Download
</button>
}
</clr-tab-content>
</ng-template>
</clr-tab>
<clr-tab>
<button clrTabLink>SAS Generated Code</button>
<ng-template [(clrIfActive)]="sasGeneratedCodeActive">
<clr-tab-content>
<div class="log-wrapper">
{{ programLog.generatedCode }}
</div>
@if (programLog.generatedCode.length > 0) {
<button
(click)="
downloadGeneratedCode(programLog.generatedCode)
"
type="button"
class="btn btn-link"
>
Download
</button>
}
</clr-tab-content>
</ng-template>
</clr-tab>
@if (programLog.SASWORK) {
<clr-tab>
<clr-dropdown
[clrCloseMenuOnItemClick]="true"
class="work-tables-dropdown"
clrTabLink
>
<button clrDropdownTrigger>
SAS Work Tables
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu *clrIfOpen>
@for (
table of programLog.SASWORK | keyvalue;
track table
) {
<div
(click)="
programLog.selectedTable = $any(table).key
"
[class.selected]="
programLog.selectedTable === table.key
"
class="dropdown-item"
clrDropdownItem
>
{{ table.key }}
</div>
}
</clr-dropdown-menu>
</clr-dropdown>
<ng-template [(clrIfActive)]="tablesActive">
<clr-tab-content>
@if (programLog.selectedTable) {
<p class="mt-10">
<strong
>{{ programLog.selectedTable }} (first 10 rows)
</strong>
</p>
}
<div class="min-h-50vh">
@if (programLog.selectedTable) {
<table class="table mt-10">
<thead>
<tr>
@for (
column of programLog.SASWORK[
programLog.selectedTable
].first10rows[0] | keyvalue;
track column
) {
<th>
{{ column.key }}
</th>
}
</tr>
</thead>
<tbody>
@for (
row of programLog.SASWORK[
programLog.selectedTable
].first10rows;
track row
) {
<tr>
@for (
column of programLog.SASWORK[
programLog.selectedTable
].first10rows[0] | keyvalue;
track column
) {
<td>
{{ row[$any(column).key] }}
</td>
}
</tr>
}
</tbody>
</table>
}
</div>
</clr-tab-content>
</ng-template>
</clr-tab>
}
</clr-tabs>
</clr-stack-content>
</clr-stack-block>
</clr-stack-block>
}
</clr-stack-view>
}
</div>
</clr-modal>
./requests-modal.component.scss