src/app/lineage/lineage.component.ts
| encapsulation | ViewEncapsulation.None |
| host | { |
| selector | app-lineage |
| styleUrls | ./lineage.component.scss |
| templateUrl | ./lineage.component.html |
No results matching.
Properties |
|
Methods |
|
constructor(licenceService: LicenceService, sasService: SasService, route: ActivatedRoute, router: Router, location: Location, eventService: EventService, loggerService: LoggerService, helperService: HelperService)
|
|||||||||||||||||||||||||||
|
Defined in src/app/lineage/lineage.component.ts:64
|
|||||||||||||||||||||||||||
|
Parameters :
|
| Public buildGraph | ||||||
buildGraph(callback?: any)
|
||||||
|
Defined in src/app/lineage/lineage.component.ts:808
|
||||||
|
Parameters :
Returns :
void
|
| Public cancelRenderingGraph |
cancelRenderingGraph()
|
|
Defined in src/app/lineage/lineage.component.ts:520
|
|
Returns :
void
|
| Public collapseTreeItems |
collapseTreeItems(tree: any, itemToSkip: any)
|
|
Defined in src/app/lineage/lineage.component.ts:146
|
|
Returns :
void
|
| Public columnOnClick |
columnOnClick(libColumn: any, library: any, libTable: any)
|
|
Defined in src/app/lineage/lineage.component.ts:195
|
|
Returns :
void
|
| Public constructName | ||||||
constructName(extension: string)
|
||||||
|
Defined in src/app/lineage/lineage.component.ts:837
|
||||||
|
Parameters :
Returns :
string
|
| Public continueRenderingGraph |
continueRenderingGraph()
|
|
Defined in src/app/lineage/lineage.component.ts:527
|
|
Returns :
void
|
| Public directionText |
directionText()
|
|
Defined in src/app/lineage/lineage.component.ts:351
|
|
Returns :
"FORWARD" | "REVERSE"
|
| downloadCSV |
downloadCSV()
|
|
Defined in src/app/lineage/lineage.component.ts:768
|
|
Returns :
void
|
| downloadDot |
downloadDot()
|
|
Defined in src/app/lineage/lineage.component.ts:797
|
|
Returns :
void
|
| Async downloadPNG |
downloadPNG()
|
|
Defined in src/app/lineage/lineage.component.ts:759
|
|
Returns :
any
|
| downloadSVG |
downloadSVG()
|
|
Defined in src/app/lineage/lineage.component.ts:750
|
|
Returns :
void
|
| Public Async generateGraph | ||||||||||||
generateGraph(urlColumn?: String | undefined, urlDirection?: string, max_depth?: string)
|
||||||||||||
|
Defined in src/app/lineage/lineage.component.ts:593
|
||||||||||||
|
Parameters :
Returns :
Promise<any>
|
| Public Async generateGraphTableLevel |
generateGraphTableLevel(table_id: string, urlDirection?: string, max_depth?: string)
|
|
Defined in src/app/lineage/lineage.component.ts:375
|
|
Returns :
unknown
|
| Private getDotUrl |
getDotUrl()
|
|
Defined in src/app/lineage/lineage.component.ts:791
|
|
Returns :
any
|
| Private getSVGURL |
getSVGURL()
|
|
Defined in src/app/lineage/lineage.component.ts:741
|
|
Returns :
any
|
| Public Async initData |
initData()
|
|
Defined in src/app/lineage/lineage.component.ts:290
|
|
Returns :
any
|
| Public libColumnActive | ||||||
libColumnActive(libColumnUri: string)
|
||||||
|
Defined in src/app/lineage/lineage.component.ts:218
|
||||||
|
Parameters :
Returns :
boolean
|
| Public Async libraryOnClick |
libraryOnClick(libid: string, library: any)
|
|
Defined in src/app/lineage/lineage.component.ts:160
|
|
Returns :
any
|
| Public libraryOnFilter |
libraryOnFilter()
|
|
Defined in src/app/lineage/lineage.component.ts:208
|
|
Returns :
void
|
| Public loadGlobalData |
loadGlobalData()
|
|
Defined in src/app/lineage/lineage.component.ts:242
|
|
Returns :
void
|
| Public loadMoreLibraries |
loadMoreLibraries()
|
|
Defined in src/app/lineage/lineage.component.ts:136
|
|
Returns :
void
|
| Public makeGraphLinkable | ||||||
makeGraphLinkable(idlookup: any)
|
||||||
|
Defined in src/app/lineage/lineage.component.ts:569
|
||||||
|
Parameters :
Returns :
void
|
| ngAfterContentInit |
ngAfterContentInit()
|
|
Defined in src/app/lineage/lineage.component.ts:349
|
|
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Defined in src/app/lineage/lineage.component.ts:266
|
|
Returns :
void
|
| Public onGenerateClick |
onGenerateClick()
|
|
Defined in src/app/lineage/lineage.component.ts:355
|
|
Returns :
void
|
| Public onGenerateGraphTableClick |
onGenerateGraphTableClick()
|
|
Defined in src/app/lineage/lineage.component.ts:366
|
|
Returns :
void
|
| Public onRouteChange |
onRouteChange()
|
|
Defined in src/app/lineage/lineage.component.ts:327
|
|
Returns :
void
|
| Public renderToDownload | ||||||
renderToDownload(type: string)
|
||||||
|
Defined in src/app/lineage/lineage.component.ts:552
|
||||||
|
Parameters :
Returns :
void
|
| Public resubmitWithMaxDepth | ||||||
resubmitWithMaxDepth(maxDepth: number | string)
|
||||||
|
Defined in src/app/lineage/lineage.component.ts:249
|
||||||
|
Parameters :
Returns :
void
|
| Public Async selectLibrary |
selectLibrary($event: any, library?: any)
|
|
Defined in src/app/lineage/lineage.component.ts:173
|
|
Returns :
any
|
| Public Async selectTable |
selectTable($event: any, libTableTree?: any)
|
|
Defined in src/app/lineage/lineage.component.ts:114
|
|
Returns :
any
|
| Public setGlobalData |
setGlobalData()
|
|
Defined in src/app/lineage/lineage.component.ts:232
|
|
Returns :
void
|
| Public showSvg |
showSvg()
|
|
Defined in src/app/lineage/lineage.component.ts:804
|
|
Returns :
void
|
| Public showTableSelect |
showTableSelect()
|
|
Defined in src/app/lineage/lineage.component.ts:77
|
|
Returns :
void
|
| Public Async tableOnClick | ||||||||||||||||||||
tableOnClick(tableuri: string, libTable: any, library: any, startupLoad: boolean)
|
||||||||||||||||||||
|
Defined in src/app/lineage/lineage.component.ts:81
|
||||||||||||||||||||
|
Parameters :
Returns :
any
|
| Public treeNodeClicked |
treeNodeClicked(event: any, item: any, tree: any)
|
|
Defined in src/app/lineage/lineage.component.ts:154
|
|
Returns :
void
|
| Public treeOnFilter |
treeOnFilter(array: any, arrToFilter: string)
|
|
Defined in src/app/lineage/lineage.component.ts:204
|
|
Returns :
void
|
| Public column |
Type : string | undefined
|
|
Defined in src/app/lineage/lineage.component.ts:50
|
| Public columnsList |
Type : Array<any> | undefined
|
|
Defined in src/app/lineage/lineage.component.ts:43
|
| Public currentLineagePathColumn |
Type : string
|
Default value : ''
|
|
Defined in src/app/lineage/lineage.component.ts:57
|
| Public currentLineagePathLibTable |
Type : string
|
Default value : ''
|
|
Defined in src/app/lineage/lineage.component.ts:56
|
| debounceTime |
Type : number
|
Default value : 200
|
|
Defined in src/app/lineage/lineage.component.ts:326
|
| debounceTimeout |
Type : any
|
|
Defined in src/app/lineage/lineage.component.ts:325
|
| Public flatdata |
Type : Array<any>
|
Default value : []
|
|
Defined in src/app/lineage/lineage.component.ts:30
|
| Public forwardLineage |
Type : boolean
|
Default value : false
|
|
Defined in src/app/lineage/lineage.component.ts:29
|
| Public graphContainer |
Type : boolean
|
Default value : false
|
|
Defined in src/app/lineage/lineage.component.ts:34
|
| Public graphLoading |
Type : boolean
|
Default value : false
|
|
Defined in src/app/lineage/lineage.component.ts:32
|
| Public graphRendering |
Type : boolean
|
Default value : false
|
|
Defined in src/app/lineage/lineage.component.ts:33
|
| Public helperService |
Type : HelperService
|
|
Defined in src/app/lineage/lineage.component.ts:74
|
| Public idlookup |
Type : any[] | undefined
|
|
Defined in src/app/lineage/lineage.component.ts:52
|
| Public largeDotFileLimit |
Type : number
|
Default value : 1000
|
|
Defined in src/app/lineage/lineage.component.ts:60
|
| Public largeDotFileLines |
Type : number | null
|
Default value : null
|
|
Defined in src/app/lineage/lineage.component.ts:59
|
| Public lib |
Type : string | null
|
Default value : null
|
|
Defined in src/app/lineage/lineage.component.ts:48
|
| Public librariesPaging |
Type : boolean
|
Default value : false
|
|
Defined in src/app/lineage/lineage.component.ts:37
|
| Public librariesSearch |
Type : string
|
Default value : ''
|
|
Defined in src/app/lineage/lineage.component.ts:40
|
| Public libraryList |
Type : Array<any> | undefined
|
|
Defined in src/app/lineage/lineage.component.ts:39
|
| Public libraryTablesRef |
Type : string
|
Default value : ''
|
|
Defined in src/app/lineage/lineage.component.ts:38
|
| Public limitDotDepth |
Type : boolean
|
Default value : false
|
|
Defined in src/app/lineage/lineage.component.ts:62
|
| Public lineageColumnName |
Type : string
|
Default value : ''
|
|
Defined in src/app/lineage/lineage.component.ts:46
|
| Public lineageTableName |
Type : string
|
Default value : ''
|
|
Defined in src/app/lineage/lineage.component.ts:45
|
| Public pendingRenderDownload |
Type : string | null
|
Default value : null
|
|
Defined in src/app/lineage/lineage.component.ts:64
|
| Public refreshCache |
Type : boolean
|
Default value : false
|
|
Defined in src/app/lineage/lineage.component.ts:54
|
| Public router |
Type : Router
|
|
Defined in src/app/lineage/lineage.component.ts:70
|
| Public switchFlag |
Type : boolean
|
Default value : false
|
|
Defined in src/app/lineage/lineage.component.ts:26
|
| Public table |
Type : string | undefined
|
|
Defined in src/app/lineage/lineage.component.ts:49
|
| Public tableColumnsRef |
Type : string
|
Default value : ''
|
|
Defined in src/app/lineage/lineage.component.ts:41
|
| Public tableDisable |
Type : unknown
|
Default value : false
|
|
Defined in src/app/lineage/lineage.component.ts:51
|
| Public tableFlag |
Type : boolean
|
Default value : true
|
|
Defined in src/app/lineage/lineage.component.ts:27
|
| Public tablesList |
Type : Array<any> | undefined
|
|
Defined in src/app/lineage/lineage.component.ts:42
|
| Public vizInput |
Type : string
|
Default value : ''
|
|
Defined in src/app/lineage/lineage.component.ts:35
|
import { Component, ViewEncapsulation } from '@angular/core'
import { Location } from '@angular/common'
import { globals } from '../_globals'
import * as d3Viz from 'd3-graphviz'
import { ActivatedRoute, Router } from '@angular/router'
import { EventService } from '../services/event.service'
import { HelperService } from '../services/helper.service'
import { SasService } from '../services/sas.service'
import * as saveSvg from 'save-svg-as-png'
import { LoggerService } from '../services/logger.service'
import { LicenceService } from '../services/licence.service'
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
const moment = require('moment')
@Component({
selector: 'app-lineage',
styleUrls: ['./lineage.component.scss'],
templateUrl: './lineage.component.html',
host: {
class: 'content-container'
},
encapsulation: ViewEncapsulation.None,
standalone: false
})
export class LineageComponent {
public switchFlag: boolean = false
public tableFlag: boolean = true
public forwardLineage: boolean = false
public flatdata: Array<any> = []
public graphLoading: boolean = false
public graphRendering: boolean = false
public graphContainer: boolean = false
public vizInput = ''
public librariesPaging: boolean = false
public libraryTablesRef: string = ''
public libraryList: Array<any> | undefined
public librariesSearch: string = ''
public tableColumnsRef: string = ''
public tablesList: Array<any> | undefined
public columnsList: Array<any> | undefined
public lineageTableName: string = ''
public lineageColumnName: string = ''
public lib: string | null = null
public table: string | undefined
public column: string | undefined
public tableDisable = false
public idlookup: any[] | undefined
public refreshCache: boolean = false
public currentLineagePathLibTable: string = ''
public currentLineagePathColumn: string = ''
public largeDotFileLines: number | null = null
public largeDotFileLimit: number = 1000 //default is 1000
public limitDotDepth: boolean = false
public pendingRenderDownload: string | null = null
constructor(
private licenceService: LicenceService,
private sasService: SasService,
private route: ActivatedRoute,
public router: Router,
private location: Location,
private eventService: EventService,
private loggerService: LoggerService,
public helperService: HelperService
) {}
public showTableSelect() {
this.tableFlag = !this.tableFlag
}
public async tableOnClick(
tableuri: string,
libTable: any,
library: any,
startupLoad: boolean = false
) {
if (!libTable['columns']) {
libTable['expanded'] = !libTable['expanded']
libTable['loadingColumns'] = true
this.table = tableuri
this.currentLineagePathLibTable =
libTable.LIBNAME + '.' + libTable.TABLENAME
await this.selectTable(tableuri, libTable)
if (!startupLoad) {
this.onGenerateGraphTableClick()
}
} else {
libTable['expanded'] = !libTable['expanded']
if (libTable['expanded'] === true) {
this.table = tableuri
if (!startupLoad) {
this.onGenerateGraphTableClick()
}
}
}
this.collapseTreeItems(library['tables'], libTable)
}
public async selectTable($event: any, libTableTree?: any) {
this.columnsList = []
let libTable = { SASControlTable: [{ tableuri: $event }] }
await this.sasService
.request('lineage/getmetacols', libTable)
.then((res: RequestWrapperResponse) => {
this.columnsList = res.adapterResponse.metacols
if (this.columnsList && this.columnsList.length > 0) {
// this.column = this.columnsList[0]['COLURI']
libTableTree['columns'] = this.columnsList
libTableTree['expanded'] = true
libTableTree['loadingColumns'] = false
}
})
.catch((err: any) => err)
this.setGlobalData()
}
public loadMoreLibraries() {
if (!this.librariesPaging) {
this.librariesPaging = true
this.helperService.displayLibraries(this.libraryList, true)
this.librariesPaging = false
}
}
public collapseTreeItems(tree: any, itemToSkip: any) {
tree.forEach((item: any) => {
if (JSON.stringify(item) !== JSON.stringify(itemToSkip)) {
item['expanded'] = false
}
})
}
public treeNodeClicked(event: any, item: any, tree: any) {
if (event.target.title === 'Collapse') {
this.collapseTreeItems(tree, item)
}
}
public async libraryOnClick(libid: string, library: any) {
library['inForeground'] = true //When we programatically call this function, we make sure it is visible in the sidebar
if (!library['tables']) {
library['loadingTables'] = true
await this.selectLibrary(libid, library)
} else {
library['expanded'] = !library['expanded']
}
this.collapseTreeItems(this.libraryList, library)
}
public async selectLibrary($event: any, library?: any) {
this.tablesList = []
this.columnsList = []
let libTable = { SASControlTable: [{ liburi: $event }] }
await this.sasService
.request('lineage/getmetatables', libTable)
.then((res: RequestWrapperResponse) => {
this.tablesList = res.adapterResponse.metatables
if (this.tablesList && this.tablesList.length > 0) {
library['tables'] = this.tablesList
library['expanded'] = true
}
this.setGlobalData()
})
.catch((err) => err)
library['loadingTables'] = false
}
public columnOnClick(libColumn: any, library: any, libTable: any) {
this.lib = library.LIBRARYID
this.table = libTable.TABLEURI
this.column = libColumn.COLURI
this.setGlobalData()
this.onGenerateClick()
}
public treeOnFilter(array: any, arrToFilter: string) {
this.helperService.treeOnFilter(array, arrToFilter)
}
public libraryOnFilter() {
this.helperService.libraryOnFilter(
this.libraryList,
this.librariesSearch,
'LIBRARYNAME'
)
globals.lineage.librariesSearch = this.librariesSearch
}
public libColumnActive(libColumnUri: string) {
if (!this.column) {
return false
}
let splitedLibColumnUri = libColumnUri.split('\\')
let splitedColumnUri = this.column.split('\\')
return (
splitedLibColumnUri[splitedLibColumnUri.length - 1] ==
splitedColumnUri[splitedColumnUri.length - 1]
)
}
public setGlobalData() {
globals.lineage.libraryList = this.libraryList
globals.lineage.tablesList = this.tablesList
globals.lineage.columnsList = this.columnsList
globals.lineage.lib = this.lib
globals.lineage.table = this.table
globals.lineage.column = this.column
}
public loadGlobalData() {
this.libraryList = globals.lineage.libraryList
this.tablesList = globals.lineage.tablesList
this.columnsList = globals.lineage.columnsList
this.librariesSearch = globals.lineage.librariesSearch
}
public resubmitWithMaxDepth(maxDepth: number | string) {
this.cancelRenderingGraph()
let queryParams = undefined
if (maxDepth) {
queryParams = {
max_depth: maxDepth
}
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams
})
}
ngOnInit(): void {
globals.viewer.currentSelection = 'view/lineage'
let reload = this.route.snapshot.params['reload']
if (reload !== undefined) {
let url = this.router.url
if (reload === 'reload') {
if (!localStorage.getItem('firstLoad')) {
localStorage['firstLoad'] = true
setTimeout(function () {
window.location.reload()
}, 2000)
} else {
localStorage.removeItem('firstLoad')
this.location.replaceState(url.slice(0, url.lastIndexOf('/')))
}
} else {
this.location.replaceState(url.slice(0, url.lastIndexOf('/')))
}
}
this.initData()
}
public async initData() {
let coluri = this.route.snapshot.params['coluri']
let direction = this.route.snapshot.params['direction']
let table_id = this.route.snapshot.params['tableid']
if (globals.lineage.libraryList) {
if (globals.lineage.libraryList.length > 0) {
this.loadGlobalData()
} else {
await this.sasService
.request('public/viewlibs', null)
.then((res: RequestWrapperResponse) => {
this.libraryList = res.adapterResponse.saslibs
this.helperService.displayLibraries(this.libraryList)
if (this.libraryList) {
if (this.libraryList.length > 0) {
this.lib = this.libraryList[0]['LIBRARYID']
}
}
this.setGlobalData()
})
.catch((err: any) => err)
}
this.route.queryParams.subscribe((queryParams) => {
this.onRouteChange()
})
this.route.params.subscribe((params) => {
this.onRouteChange()
})
}
}
debounceTimeout: any
debounceTime: number = 200 //ms
public onRouteChange() {
clearTimeout(this.debounceTimeout)
this.debounceTimeout = setTimeout(() => {
const coluri = this.route.snapshot.params['coluri']
const direction = this.route.snapshot.params['direction']
const table_id = this.route.snapshot.params['tableid']
const max_depth = this.route.snapshot.queryParams['max_depth']
if (coluri && direction) {
this.forwardLineage = direction === 'FORWARD'
this.column = coluri
this.generateGraph(coluri, direction, max_depth)
}
if (!this.router.url.includes('column') && table_id && direction) {
this.forwardLineage = direction === 'FORWARD'
this.table = table_id
this.generateGraphTableLevel(table_id, direction, max_depth)
}
}, this.debounceTime)
}
ngAfterContentInit(): void {}
public directionText() {
return this.forwardLineage ? 'FORWARD' : 'REVERSE'
}
public onGenerateClick() {
let directionText = this.directionText()
let parsedColumnUrl = this.column
? this.column.substring(this.column.indexOf('\\') + 1)
: null
this.router.navigateByUrl(
'/view/lineage/column/' + parsedColumnUrl + '/' + directionText
)
}
public onGenerateGraphTableClick() {
let directionText = this.directionText()
let table_id = this.table!.includes('\\')
? this.table!.split('\\')[1]
: this.table
this.router.navigateByUrl('/view/lineage/' + table_id + '/' + directionText)
}
public async generateGraphTableLevel(
table_id: string,
urlDirection?: string,
max_depth?: string
) {
let libTable: any = {
SASControlTable: [
{
table_id: table_id,
direction: urlDirection ? urlDirection : this.directionText()
}
]
}
if (libTable.SASControlTable[0].table_id === 'undefined')
this.eventService.showAbortModal(
'generateGraphTableLevel()',
'table_id is undefined.',
undefined,
'Frontend error'
)
if (max_depth) libTable.SASControlTable[0].max_depth = max_depth
this.tableFlag = false
this.switchFlag = true
this.graphContainer = true
this.graphLoading = true
this.vizInput = ''
return new Promise<void>((resolve, reject) => {
this.sasService
.request('lineage/fetchtablelineage', libTable)
.then(async (res: RequestWrapperResponse) => {
if (res.adapterResponse.flatdata.length > 0) {
if (this.licenceService.checkLineageLimit()) {
this.eventService.showInfoModal(
'Notice',
`You have reached daily maximum of lineage diagram renderings. To unlock additional diagrams, contact [email protected]`
)
this.router.navigateByUrl('/view/lineage')
return
}
}
if (typeof res === 'string') {
this.vizInput = 'digraph G {SAS Error}'
this.buildGraph()
return
}
this.lineageTableName =
res.adapterResponse.info[0].LIBREF +
'.' +
res.adapterResponse.info[0].TABLENAME
let dotArray = res.adapterResponse.finalfinal
let vizTmp: string = ''
for (let i = 0; i < dotArray.length; i++) {
vizTmp += unescape(dotArray[i].LINE) + '\n'
}
this.flatdata = res.adapterResponse.flatdata
if (this.libraryList) {
let libraryToSelect = this.libraryList.find((library: any) =>
res.adapterResponse.info[0].LIBURI.toUpperCase().includes(
library.LIBRARYID.toUpperCase()
)
)
if (libraryToSelect) {
let tableToSelect
await this.libraryOnClick(
libraryToSelect.LIBRARYID,
libraryToSelect
)
if (libraryToSelect['tables']) {
tableToSelect = libraryToSelect['tables'].find((table: any) =>
table.TABLEURI.toUpperCase().includes(
res.adapterResponse.info[0].TABLEID.toUpperCase()
)
)
if (tableToSelect) {
this.table = tableToSelect.TABLEURI
if (this.table) {
const query = this.table.replace('\\', '\\\\')
setTimeout(() => {
let tablePElement = document.querySelector(
`[id='${query}']`
)
if (tablePElement) {
tablePElement.scrollIntoView()
}
}, 1000)
}
this.tableOnClick(
tableToSelect.TABLEURI,
tableToSelect,
libraryToSelect,
urlDirection !== undefined
)
}
}
if (libraryToSelect) {
libraryToSelect['expanded'] = true
}
if (tableToSelect) {
tableToSelect['expanded'] = true
}
}
}
this.vizInput = vizTmp ? vizTmp : 'digraph G {No Lineage Available}'
this.vizInput = this.vizInput
.replace(/\sds:/g, '\nds:')
.replace(/\s\n/g, '\n')
this.idlookup = res.adapterResponse.idlookup
if (res.adapterResponse.finalfinal.length > this.largeDotFileLimit) {
this.largeDotFileLines = res.adapterResponse.finalfinal.length
} else {
this.buildGraph()
}
resolve()
})
.catch((err: any) => {
this.graphLoading = false
this.graphContainer = false
})
})
}
public cancelRenderingGraph() {
this.vizInput = 'digraph G {No Lineage Available}'
this.largeDotFileLines = null
this.buildGraph()
}
public continueRenderingGraph() {
this.largeDotFileLines = null
this.buildGraph(() => {
if (this.pendingRenderDownload !== null) {
switch (this.pendingRenderDownload) {
case 'PNG': {
this.downloadPNG()
break
}
case 'SVG': {
this.downloadSVG()
break
}
}
this.pendingRenderDownload = null
this.vizInput = 'digraph G {No Lineage Available}'
this.buildGraph()
}
})
}
public renderToDownload(type: string) {
switch (type) {
case 'PNG': {
this.pendingRenderDownload = 'PNG'
this.continueRenderingGraph()
break
}
case 'SVG': {
this.pendingRenderDownload = 'SVG'
this.continueRenderingGraph()
break
}
}
}
public makeGraphLinkable(idlookup: any) {
let graphNodes: any = document.querySelectorAll('#graph .node')
for (let node of graphNodes) {
let metaid = node.querySelector('title').innerHTML
let meta = idlookup.find((x: any) => x.METAID === metaid)
let href: string
if (meta) {
if (meta.METATYPE === 'TABLE') {
href = '/view/data/' + meta.METANAME
} else {
href = '/view/metadata/object/' + meta.METAID
}
node.classList.add('cursor-pointer')
node.addEventListener('click', (event: MouseEvent) => {
this.router.navigateByUrl(href)
})
}
}
}
public async generateGraph(
urlColumn?: String | undefined,
urlDirection?: string,
max_depth?: string
): Promise<any> {
let libTable: any = {
SASControlTable: [
{
coluri: urlColumn ? urlColumn : this.column,
direction: urlDirection ? urlDirection : this.directionText(),
refresh: this.refreshCache ? 1 : 0
}
]
}
if (libTable.SASControlTable[0].coluri === 'undefined')
this.eventService.showAbortModal(
'generateGraph()',
'coluri is undefined.',
undefined,
'Frontend error'
)
if (max_depth) libTable.SASControlTable[0].max_depth = max_depth
this.tableFlag = false
this.switchFlag = true
this.graphContainer = true
this.graphLoading = true
this.vizInput = ''
return new Promise<void>((resolve, reject) => {
this.sasService
.request('lineage/fetchcollineage', libTable)
.then(async (res: RequestWrapperResponse) => {
if (res.adapterResponse.flatdata.length > 0) {
if (this.licenceService.checkLineageLimit()) {
this.eventService.showInfoModal(
'Notice',
`You have reached daily maximum of lineage diagram renderings. To unlock additional diagrams, contact [email protected]`
)
this.router.navigateByUrl('/view/lineage')
return
}
}
if (typeof res.adapterResponse === 'string') {
this.vizInput = 'digraph G {SAS Error}'
this.buildGraph()
return
}
this.lineageTableName =
res.adapterResponse.info[0].LIBREF +
'.' +
res.adapterResponse.info[0].TABNAME
this.lineageColumnName = res.adapterResponse.info[0].COLNAME
this.idlookup = res.adapterResponse.idlookup
let dotArray = res.adapterResponse.fromsas
let vizTmp: string = ''
for (let i = 0; i < dotArray.length; i++) {
vizTmp += unescape(dotArray[i].STRING) + '\n'
}
this.vizInput = vizTmp ? vizTmp : 'digraph G {No Lineage Available}'
this.vizInput = this.vizInput
.replace(/\sds:/g, '\nds:')
.replace(/\s\n/g, '\n')
this.flatdata = res.adapterResponse.flatdata
if (this.libraryList) {
let libraryToSelect = this.libraryList.find((library: any) =>
res.adapterResponse.info[0]?.LIBURI?.toUpperCase()?.includes(
library?.LIBRARYID?.toUpperCase()
)
)
let tableToSelect: any
if (libraryToSelect) {
await this.libraryOnClick(
libraryToSelect.LIBRARYID,
libraryToSelect
)
if (libraryToSelect['tables']) {
tableToSelect = libraryToSelect['tables'].find(
(table: any) =>
table.TABLEURI === res.adapterResponse.info[0].TABURI
)
if (tableToSelect) {
this.tableOnClick(
tableToSelect.TABLEURI,
tableToSelect,
libraryToSelect,
true
).then(() => {
let tableUri = tableToSelect.TABLEURI
if (tableUri) {
const query = tableUri.replace('\\', '\\\\')
let tablePElement = document.querySelector(
`[id='${query}']`
)
setTimeout(() => {
if (tablePElement) {
this.loggerService.log(
'libraryToSelect',
libraryToSelect
)
tablePElement.scrollIntoView()
}
}, 1000)
}
})
}
}
if (libraryToSelect) {
libraryToSelect['expanded'] = true
}
if (tableToSelect) {
tableToSelect['expanded'] = true
}
}
}
if (res.adapterResponse.fromsas.length > this.largeDotFileLimit) {
this.largeDotFileLines = res.adapterResponse.fromsas.length
} else {
this.buildGraph()
}
resolve()
})
.catch((err: any) => {
this.graphLoading = false
this.graphContainer = false
})
})
}
private getSVGURL() {
let svg: any = document.getElementById('graph')
let serializer = new XMLSerializer()
let svg_blob = new Blob([serializer.serializeToString(svg)], {
type: 'image/svg+xml'
})
return URL.createObjectURL(svg_blob)
}
downloadSVG() {
d3Viz.graphviz('#graph').resetZoom()
let downloadLink = document.createElement('a')
downloadLink.href = this.getSVGURL()
downloadLink.download = this.constructName('svg')
downloadLink.click()
}
async downloadPNG() {
d3Viz.graphviz('#graph').resetZoom()
saveSvg.saveSvgAsPng(
document.querySelector('#graph svg'),
this.constructName('png')
)
}
downloadCSV() {
let data = this.flatdata
const replacer = (key: any, value: any) => (value === null ? '' : value)
const header = Object.keys(data[0])
let csv = data.map((row: any) =>
header
.map((fieldName) => JSON.stringify(row[fieldName], replacer))
.join(',')
)
csv.unshift(header.join(','))
let csvArray = csv.join('\r\n')
var a = document.createElement('a')
var blob = new Blob([csvArray], { type: 'text/csv' })
var url = window.URL.createObjectURL(blob)
a.href = url
a.download = this.constructName('csv')
a.click()
window.URL.revokeObjectURL(url)
}
private getDotUrl() {
let data = this.vizInput
let dot_blob = new Blob([data], { type: 'text/plain' })
return window.URL.createObjectURL(dot_blob)
}
downloadDot() {
let downloadLink = document.createElement('a')
downloadLink.href = this.getDotUrl()
downloadLink.download = this.constructName('txt')
downloadLink.click()
}
public showSvg() {
window.open(this.getSVGURL(), '_blank')
}
public buildGraph(callback?: any) {
this.eventService.closeSidebar()
this.graphLoading = false
this.graphRendering = true
setTimeout(() => {
d3Viz
.graphviz('#graph')
.zoom(true)
.addImage(
'https://datacontroller.io/wp-content/uploads/2020/01/fc1.png',
'30px',
'30px'
)
.renderDot(this.vizInput, () => {
this.graphRendering = false
if (!!this.idlookup) {
this.makeGraphLinkable(this.idlookup)
}
if (callback) {
callback()
}
})
}, 100)
}
public constructName(extension: string) {
let libraryName = ''
let tableName = ''
let columnName = ''
let date = moment().format('YYMMDD_HHmm')
if (this.libraryList && this.libraryList.length) {
let library: any = this.libraryList.find(
(e: any) => e.LIBRARYID == this.lib
)
if (library) {
libraryName = `_${library.LIBRARYNAME}`
}
}
if (this.tablesList && this.tablesList.length) {
let table: any = this.tablesList.find(
(e: any) => e.TABLEURI == this.table
)
if (table) {
tableName = `_${table.TABLENAME}`
}
}
if (this.columnsList && this.columnsList.length) {
let column: any = this.columnsList.find(
(e: any) => e.COLURI == this.column
)
if (column) {
columnName = `_${column.COLNAME}`
}
}
return `${this.directionText()}${libraryName}${tableName}${columnName}_${date}.${extension}`
}
}
<app-sidebar (scrolledToBottom)="loadMoreLibraries()">
<clr-tree>
@if (libraryList) {
<clr-tree-node class="search-node">
<div class="tree-search-wrapper">
<input
appStealFocus
clrInput
#searchLibTreeInput
placeholder="Libraries"
name="input"
[(ngModel)]="librariesSearch"
(keyup)="libraryOnFilter()"
autocomplete="off"
/>
@if (searchLibTreeInput.value.length < 1) {
<clr-icon shape="search"></clr-icon>
}
@if (searchLibTreeInput.value.length > 0) {
<clr-icon
(click)="librariesSearch = ''; libraryOnFilter()"
shape="times"
></clr-icon>
}
</div>
</clr-tree-node>
}
@for (library of libraryList; track library) {
@if (!library['hidden'] && library['inForeground']) {
<clr-tree-node
(click)="treeNodeClicked($event, library, libraryList)"
[(clrExpanded)]="library['expanded']"
[clrLoading]="library['loadingTables'] && !library.tables"
[class.clr-expanded]="library['expanded']"
>
<p
(click)="
lib = library.LIBRARYID; libraryOnClick(lib || '', library)
"
class="m-0 cursor-pointer"
>
<clr-icon shape="rack-server"></clr-icon>
{{ library.LIBRARYNAME }}
</p>
@if (library['tables']) {
<clr-tree-node class="search-node">
<div class="tree-search-wrapper">
<input
appStealFocus
clrInput
#searchTreeInput
placeholder="Tables"
name="input"
[(ngModel)]="library['searchString']"
(keyup)="treeOnFilter(library, 'tables.TABLENAME')"
autocomplete="off"
/>
@if (searchTreeInput.value.length < 1) {
<clr-icon shape="search"></clr-icon>
}
@if (searchTreeInput.value.length > 0) {
<clr-icon
(click)="
searchTreeInput.value = '';
library['searchString'] = '';
treeOnFilter(library, 'tables.TABLENAME')
"
shape="times"
></clr-icon>
}
</div>
</clr-tree-node>
}
@for (libTable of library['tables']; track libTable) {
<clr-tree-node
(click)="treeNodeClicked($event, libTable, library['tables'])"
[(clrExpanded)]="libTable['expanded']"
[clrLoading]="libTable['loadingColumns'] && !libTable.columns"
[class.clr-expanded]="libTable['expanded']"
>
<p
(click)="tableOnClick(libTable.TABLEURI, libTable, library)"
[id]="libTable.TABLEURI"
class="m-0 cursor-pointer"
>
<clr-icon shape="table"></clr-icon>
{{ libTable.TABLENAME }}
</p>
@if (libTable['columns']) {
<clr-tree-node class="search-node">
<div class="tree-search-wrapper">
<input
appStealFocus
clrInput
#searchTreeInput
placeholder="Columns"
name="input"
[(ngModel)]="libTable['searchString']"
(keyup)="treeOnFilter(libTable, 'columns.COLNAME')"
autocomplete="off"
/>
@if (searchTreeInput.value.length < 1) {
<clr-icon shape="search"></clr-icon>
}
@if (searchTreeInput.value.length > 0) {
<clr-icon
(click)="
searchTreeInput.value = '';
libTable['searchString'] = '';
treeOnFilter(libTable, 'columns.COLNAME')
"
shape="times"
></clr-icon>
}
</div>
</clr-tree-node>
}
@for (libColumn of libTable['columns']; track libColumn) {
<clr-tree-node>
<button
(click)="columnOnClick(libColumn, library, libTable)"
class="clr-treenode-link"
[class.column-active]="libColumnActive(libColumn.COLURI)"
>
<clr-icon shape="objects"></clr-icon>
{{ libColumn.COLNAME }}
</button>
</clr-tree-node>
}
</clr-tree-node>
}
</clr-tree-node>
}
}
</clr-tree>
@if (librariesPaging) {
<div class="w-100 text-center">
<span class="spinner spinner-sm"> Loading... </span>
</div>
}
</app-sidebar>
<div class="content-area">
<div class="card">
@if (!column && !table) {
<div class="no-table-selected">
<clr-icon
shape="warning-standard"
size="60"
class="is-info icon-dc-fill"
></clr-icon>
<p class="text-center color-gray mt-10" cds-text="section">
Please select a column or table
</p>
</div>
}
@if (column || table) {
@if (!graphContainer) {
<div class="card-header d-flex flex-column justify-content-center">
@if (!currentLineagePathColumn && !currentLineagePathLibTable) {
<h3 class="text-center pb-10">
{{
currentLineagePathColumn
? currentLineagePathLibTable + '.' + currentLineagePathColumn
: currentLineagePathLibTable
}}
</h3>
}
</div>
}
<div class="card-block">
<section
class="form-block sw position-relative d-flex align-items-center"
>
<div
class="linage-title-wrapper d-flex align-items-center font-weight-bold position-absolute"
>
@if (lineageTableName.length > 0) {
<span>
{{ lineageTableName.split('.')[0] }}.<a
[routerLink]="'/view/data/' + lineageTableName"
>{{ lineageTableName.split('.')[1] }}</a
>{{
lineageColumnName.length > 0 ? '.' + lineageColumnName : ''
}}
</span>
}
</div>
@if (graphContainer) {
<div class="clr-col-md-12 text-center d-flex justify-content-end">
<button
(click)="limitDotDepth = true"
type="button"
class="btn btn-outline mr-5"
>
Limit depth
</button>
<!-- <button class="btn btn-outline" (click)='showSvg()'> Open in New Tab </button> -->
<div class="btn-group direction d-block">
<div
class="radio btn"
(click)="
forwardLineage = false;
router.url.includes('column')
? onGenerateClick()
: onGenerateGraphTableClick()
"
>
<input
type="radio"
name="btn-group-demo-radios"
[checked]="!forwardLineage"
/>
<label>Backward</label>
</div>
<div
class="radio btn"
(click)="
forwardLineage = true;
router.url.includes('column')
? onGenerateClick()
: onGenerateGraphTableClick()
"
>
<input
type="radio"
name="btn-group-demo-radios"
[checked]="forwardLineage"
/>
<label>Forward</label>
</div>
</div>
<clr-dropdown class="mr-10">
<button
class="btn btn-info-outline"
clrDropdownTrigger
[disabled]="!column && !table"
>
Download
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div (click)="downloadSVG()" clrDropdownItem>SVG</div>
<div (click)="downloadPNG()" clrDropdownItem>PNG</div>
<div (click)="downloadDot()" clrDropdownItem>Dot</div>
@if (flatdata) {
<div (click)="downloadCSV()" clrDropdownItem>CSV</div>
}
</clr-dropdown-menu>
</clr-dropdown>
@if (column) {
<clr-checkbox-wrapper class="d-flex align-items-center">
<input
type="checkbox"
(change)="generateGraph()"
clrCheckbox
name="refreshCache"
[(ngModel)]="refreshCache"
/>
<label>Refresh Cache</label>
</clr-checkbox-wrapper>
}
</div>
}
</section>
@if (graphContainer) {
<div [class.mt-2]="tableFlag">
<div class="text-center">
@if (graphLoading) {
<span class="spinner"> Loading... </span>
}
@if (!graphLoading) {
<div class="position-relative">
<div class="graph-render-spinner">
@if (graphRendering) {
<span class="spinner spinner-sm"></span>
}
</div>
<div id="graph"></div>
</div>
}
</div>
</div>
}
</div>
}
</div>
</div>
<clr-modal [clrModalOpen]="limitDotDepth" [clrModalClosable]="false">
<h3 class="modal-title">Limit Depth</h3>
<div class="modal-body">
<p>Enter the depth number you would like to limit DOT rendering to:</p>
</div>
<div class="modal-footer d-flex flex-column">
<div class="biglineage-row mb-0i">
<input
#maxDepthDot
type="number"
min="1"
placeholder="Max depth value"
class="clr-input max-depth-input"
/>
</div>
<clr-control-helper class="mb-20"
>To render with unlimited depth, leave the input empty</clr-control-helper
>
<div class="biglineage-row">
<button
type="button"
class="btn btn-sm btn-primary"
(click)="limitDotDepth = false"
>
Cancel
</button>
<button
type="button"
class="btn btn-sm btn-primary"
(click)="resubmitWithMaxDepth(maxDepthDot.value); limitDotDepth = false"
>
Resubmit
</button>
</div>
</div>
</clr-modal>
<clr-modal
[clrModalOpen]="largeDotFileLines !== null"
[clrModalClosable]="false"
>
<h3 class="modal-title">Warning</h3>
<div class="modal-body">
<p>
The generated dot file has {{ largeDotFileLines }} lines. Rendering may
take a long time, how would you like to proceed?
</p>
</div>
<div class="modal-footer d-flex flex-column">
<div class="biglineage-row">
<p>Render anyway (this may take a long time)</p>
<button
type="button"
class="btn btn-sm btn-primary"
(click)="continueRenderingGraph()"
>
Render
</button>
</div>
<div class="biglineage-row">
<p>Download the result (PNG & SVG require rendering)</p>
<clr-dropdown>
<button
class="btn btn-sm btn-primary mt-3-negative"
clrDropdownTrigger
[disabled]="!column && !table"
>
Download
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div (click)="renderToDownload('SVG')" clrDropdownItem>SVG</div>
<div (click)="renderToDownload('PNG')" clrDropdownItem>PNG</div>
<div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem>
Dot
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<div class="biglineage-row">
<p>Resubmit with a max-depth (specify depth)</p>
<input
#maxDepth
type="number"
min="1"
placeholder="Max depth value"
class="clr-input max-depth-input"
/>
<button
type="button"
class="btn btn-sm btn-primary"
(click)="resubmitWithMaxDepth(maxDepth.value)"
>
Resubmit
</button>
</div>
<div class="biglineage-row">
<p>Cancel this submission</p>
<button
type="button"
class="btn btn-sm btn-primary"
(click)="cancelRenderingGraph()"
>
Cancel
</button>
</div>
</div>
</clr-modal>
./lineage.component.scss