Skip to content

Commit

Permalink
Custom web component "<adr-report></adr-report>" methods to fetch ADR…
Browse files Browse the repository at this point in the history
… report (#125)

Co-authored-by: Marina Galvagni <[email protected]>
Co-authored-by: U-ANSYS\mgalvagn <[email protected]>
  • Loading branch information
3 people authored Sep 20, 2024
1 parent 7a4c9fb commit ceafe4a
Show file tree
Hide file tree
Showing 2 changed files with 546 additions and 0 deletions.
323 changes: 323 additions & 0 deletions src/ansys/dynamicreporting/core/adr_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,329 @@ def get_url(self, filter: str = "") -> str:
url += build_query_url(self.service.logger, filter)
return url

def get_guid(self) -> str:
"""
Get the guid corresponding to the report.
Returns
-------
str
guid corresponding to the report. If no guid exists, an empty string is returned.
Examples
--------
::
import ansys.dynamicreporting.core as adr
adr_service = adr.Service(ansys_installation = r'C:\\Program Files\\ANSYS Inc\\v232')
ret = adr_service.connect()
my_report = adr_service.get_report(report_name = 'Top report')
report_url = my_report.get_guid()
"""
guid = ""
if self.service is None: # pragma: no cover
self.service.logger.error("No connection to any report")
return guid
if self.service.serverobj is None or self.service.url is None: # pragma: no cover
self.service.logger.error("No connection to any server")
return guid
if self.report:
guid = self.report.guid
else: # pragma: no cover
success = self.__find_report_obj__()
if success:
guid = self.report.guid
else:
self.service.logger.error("Error: can not obtain the report guid")

return guid

def get_report_script(self) -> str:
"""
A block of JavaScript script to define the web component for report fetching.
Note that the function return a block of string that stands for JavaScript codes
and need to be wrapped in a <script>...</script> HTML tag.
Returns
-------
str
JavaScript code to define the report fetching web component (as a block of string)
that will get embedded in the HTML page
Examples
--------
::
import ansys.dynamicreporting.core as adr
adr_service = adr.Service(ansys_installation = r'C:\\Program Files\\ANSYS Inc\\v232')
ret = adr_service.connect()
my_report = adr_service.get_report(report_name = 'Top report')
my_report.get_report_script()
"""
# helper fn to load <script> <link> & <style> on report fetch
loadScript = """
loadDependencies(tgtList, createTgt="", appendTgt = ""){
const promiseQueue = [];
for(let tgt of tgtList){
promiseQueue.push(
(function(){
return new Promise((resolve, reject)=>{
let ele = document.createElement(createTgt);
ele.addEventListener("load", () => {
return resolve(true)
}, {once:true});
if(ele.tagName === "SCRIPT"){
tgt.src ?
ele.setAttribute("src", `${tgt.getAttribute("src")}`)
:
ele.innerHTML = tgt.innerHTML;
ele.type = "text/javascript";
ele.async = false;
}else if(ele.tagName === "LINK"){
ele.type = "text/css";
ele.rel = tgt.rel;
ele.setAttribute("href", `${tgt.getAttribute("href")}`);
}else{
ele.innerHTML = tgt.innerHTML;
}
if(appendTgt){
document.querySelector(appendTgt).appendChild(ele);
}
if(tgt.tagName === "STYLE" || !tgt.src){
resolve(true)
}
ele.addEventListener("error", () => {
return reject(new Error(`${this} failed to load.`))
}, {once:true})
})
})()
)
}
return Promise.all(promiseQueue)
}
"""
# helper fn to remove duplicated <script> on report fetch
removeDuplicates = """
removeScript(root){
return new Promise((resolve, reject)=>{
try{
const scriptList = root.querySelectorAll(" script")
for(let i = 0; i < scriptList.length; i++){
// loop thru the script list and remove <script> one by one
scriptList[i].remove()
if(i === scriptList.length-1){
// once reach and remove the final <script>, then resolve
resolve(true)
}
}
}catch(err){
reject(err)
}
})
}
"""
# report fetch main fn
reportFetch = """
reportFetch(prefix, guid, query){
return new Promise((resolve, reject)=>{
fetch(`/${prefix}/${guid}/${query}`)
.then(res=>{
// get the html text first
return res.text();
})
.then(report=>{
// convert the html text to DOM object (for querySelector... later)
return new DOMParser().parseFromString(report, 'text/html')
})
.then(reportHTML=>{
// get the <adr-report-root>'s children & back_to_top button
// since "return_to_top btn" is not inside <adr-report-root>
const reportRoot = reportHTML.querySelector('adr-report-root');
const returnToTop = reportHTML.querySelector('a#return_to_top');
// async loaded script/link/style (append and load BEFORE report body mounted in the DOM)
const asyncLinks = reportHTML.querySelectorAll('head>link');
const asyncStyles = reportHTML.querySelectorAll('head>style, body>style');
// :not(...) in querySelector is not support in Chrome v87 (Qt Web Engine)
// thus, use .filter() for backward compatibility
// Store all <script> with src path that are not base/report_item/report_display.js
// under <head> & <body>, included nested <script> inside <adr-report-root> as a variable
// (*Note base/report_item/report_display.js depend on report HTML elements, so need to
// append AFTER report body is mounted)
const asyncScripts = [...reportHTML.querySelectorAll(`head>script[src], body script[src]`)].filter(el=>{
return !el.src.endsWith('base.js') &&
!el.src.endsWith('report_item.js') &&
!el.src.endsWith('report_display.js')
});
const asyncScriptsInLine = [...reportHTML.querySelectorAll('head>script')].filter(el=>!el.src);
// defer loaded script (append and load AFTER report body mounted in the DOM)
// Store all inline <script> & base/report_item/report_display.js under <body>,
// included nested script inside <adr-report-root> as a variable.
// (we'll remove these scripts later and then append/load/run all over again
const deferScripts = [...reportHTML.querySelectorAll('body script')].filter(el=>{
return !el.src ||
el.src.endsWith('base.js') ||
el.src.endsWith('report_item.js') ||
el.src.endsWith('report_display.js');
});
// promise chain: ORDER MATTERS!!
Promise.all([
// load async dependencies first
this.loadDependencies(asyncLinks, 'link', 'head'),
this.loadDependencies(asyncStyles, 'style', 'head'),
this.loadDependencies(asyncScripts, 'script', 'head')
]).then(()=>{
// after resolved, then load async inline script
// (*Order matters!! as inline script may depend on the async <script> with src)
return this.loadDependencies(asyncScriptsInLine, 'script', 'head')
}).then(()=>{
// Start handling the report contents...
return new Promise((resolve, reject)=>{
// append core report <section>
try{
// remove all nested <script> first under <adr-report-root> as we'll append all these
// <script> later, load, and run again, thus, need to clean up the duplicated scripts
this.removeScript(reportRoot)
// Once remove the script then append the report body (Now no nested <script>)
.then(()=>this.innerHTML = reportRoot.innerHTML + returnToTop.outerHTML)
.then(()=>{
// once report children > 0, namely...done appending then resolve and return
// the scripts stored initially as a variable (deferScripts) for append & load
if(this.childElementCount > 0){
return resolve(deferScripts)
}
})
}catch(err){
reject(err)
}
})
}).then(deferScripts => {
// append & load all nested <script> inside <adr-report-root>
return this.loadDependencies(deferScripts, 'script', 'adr-report')
})
})
.catch(function (err) {
// There was an error
console.warn('Something went wrong.', err);
reject(err);
});
})
}
"""
# component logic define
component_logic = f"""
class ReportFetchComponent extends HTMLElement {{
constructor() {{
super();
}}
{loadScript}
{removeDuplicates}
{reportFetch}
connectedCallback(){{
const prefix = this.getAttribute('prefix') || "";
const guid = this.getAttribute('guid') || "";
const query = this.getAttribute('query').replaceAll("|", "%7C").replaceAll(";", "%3B") || "";
const reportPath = this.getAttribute('reportURL') || "";
const width = this.getAttribute('width') || "";
const height = this.getAttribute('height') || "";
if(prefix && guid){{
// fetch report
return this.reportFetch(prefix, guid, query);
}}
if(reportPath){{
// use <iframe> instead
const iframeEle = document.createElement('iframe');
iframeEle.src = reportPath;
iframeEle.width = width;
iframeEle.height = height;
return this.appendChild(iframeEle);
}}
}}
}}
customElements.define("adr-report", ReportFetchComponent);
"""
return component_logic

def get_report_component(
self,
prefix: str = "",
filter: str = "",
style_path: str = "",
width: int = 1000,
height: int = 800,
) -> str:
"""
A HTML code of the web component for report fetching. By default, the web
component uses iframe to embed the report. If users have provided additional
configuration settings on their application server or on another proxy server,
the web component will use fetch API to embed the report directly in the
application.
Parameters
----------
prefix : str, optional
A user defined key in the server to reroute and fetch the report from ADR server. If not provided,
the web component will use the default iframe to embed the report in the application.
filter : str, optional
Query string for filtering. The default is ``""``. The syntax corresponds
to the syntax for Ansys Dynamic Reporting. For more information, see
_Query Expressions in the documentation for Ansys Dynamic Reporting.
style_path: str, optional
The hosting app's stylesheet path. The default is ``""``. The syntax is used to overwrite report
styling using an external CSS file.
width : int, optional
Width of the iframe if the web component uses <iframe> to embed report. The default is ``1000``.
height : int, optional
Height of the iframe if the web component uses <iframe> to embed report. The default is ``800``.
Returns
-------
str
The web component HTML code (as string) that will get embedded in the HTML page
Examples
--------
::
import ansys.dynamicreporting.core as adr
adr_service = adr.Service(ansys_installation = r'C:\\Program Files\\ANSYS Inc\\v232')
ret = adr_service.connect()
my_report = adr_service.get_report(report_name = 'Top report')
my_report.get_report_component()
"""
# fetch method using predefined prefix rules in the proxy server OR using traditional <iframe>
# add host-style-path attribute if specified (can only work when prefix is provided)
host_style_path = f'host-style-path="{style_path}"' if style_path else ""
fetch_method = (
f'prefix="{prefix}" guid="{self.get_guid()}" query="{filter}" {host_style_path}'
if prefix
else f'reportURL="{self.get_url()}" width="{width}" height="{height}"'
)
component = f"<adr-report {fetch_method}></adr-report>"
return component

def get_iframe(self, width: int = 1000, height: int = 800, filter: str = ""):
"""
Get the iframe object corresponding to the report.
Expand Down
Loading

0 comments on commit ceafe4a

Please sign in to comment.