Skip to content

Commit

Permalink
Merge pull request #30 from microsoft/static-js-filename
Browse files Browse the repository at this point in the history
Static js filename
  • Loading branch information
Chenglong-MS authored Oct 1, 2024
2 parents 3b6c533 + 6ee30bc commit 6b8ba2c
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 0 deletions.
85 changes: 85 additions & 0 deletions embed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Embed Data Formulator

First you'll need to build the bundle:
```
yarn build
```

This puts the complete js file in the `dist` folder.

## Test bundle

Next you can test to see the complete Data Formulator app by opening `/embed/index.html` in your browser. You can do this by double-clicking in your file explorer (this would use the `file://` protocol).

To test cross-frame messaging, launch `postMessageTest.html` which hosts the app in an iframe, and has buttons to send commands such as `load data`.

## Use in Fabric Notebook

You willl need to enable access to your `dist` from the cloud. There are 2 ways to do this:
* Publish the `dist` (e.g. pip, npm, or other)
* Create a tunnel to your localhost

### Tunnel to localhost
One way is to install [local-web-server](https://www.npmjs.com/package/local-web-server). This will serve a local folder as a website on http://localhost:8000. Next, you can set up a tunnel such as [ngrok](https://ngrok.com/download) which can provide a cloud-accesible url proxy to your local server.

Copy the python function in a notebook cell:
```py
def dfviz(df, tableName, serverUrl):
# df is a PySpark DataFrame

import json
from datetime import date, datetime

# Custom function to convert datetime objects to string
def json_serial(obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
raise TypeError ("Type %s not serializable" % type(obj))

# Convert DataFrame rows to dictionaries and collect them into a list
data = [row.asDict() for row in df.collect()]

# Convert list of dictionaries to a single JSON array using the custom function
json_data = json.dumps(data, default=json_serial)

displayHTML(f"""<!DOCTYPE html>
<meta charset="utf-8">
<script>
const table = {json_data};
const embedPromise = new Promise((resolve, reject) => {{
const embedIframe = document.createElement('iframe');
embedIframe.style.height = '700px';
embedIframe.style.width = 'calc(100% - 4px)';
document.body.appendChild(embedIframe);
const closeScriptTag = '</'+'script>';
const htmlContent = `<!DOCTYPE html>
<html><body>
<div id="root"></div>
<script src="{serverUrl}/DataFormulator.js" defer onload="parent.frameLoaded()" onerror="parent.frameError()">${{closeScriptTag}}
</body></html>`;
// Define global functions for onload and onerror events of the script
window.frameLoaded = () => resolve(embedIframe);
window.frameError = () => reject(new Error('Script failed to load'));
// Write the HTML content to the iframe
embedIframe.contentWindow.document.open();
embedIframe.contentWindow.document.write(htmlContent);
embedIframe.contentWindow.document.close();
}});
embedPromise.then((embedIframe) => {{
embedIframe.contentWindow.postMessage({{ actionName: 'setConfig', actionParams: {{ serverUrl: '{serverUrl}', popupConfig: {{ allowPopup: true, jsUrl: '{serverUrl}/DataFormulator.js' }} }} }}, '*');
embedIframe.contentWindow.postMessage({{ actionName: 'loadData', actionParams: {{ tableName: '{tableName}', table }} }}, '*');
}});
</script>
"""
)
```

Get a dataframe and pass it to the `dfviz` function:
```py
df = spark.sql("SELECT * FROM Sample_lakehouse_475.publicholidays LIMIT 100")
display(df)
dfviz(df, 'Holidays', 'https://<your_tunnel_url>')
```

110 changes: 110 additions & 0 deletions embed/dynamic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Formulator embedded dynamically</title>
</head>

<body>
<script>
const table = [
{
"manufacturer": "Toyota",
"model": "Corolla",
"year": 2019,
"price": 20000
},
{
"manufacturer": "Toyota",
"model": "Camry",
"year": 2019,
"price": 25000
},
{
"manufacturer": "Toyota",
"model": "RAV4",
"year": 2019,
"price": 30000
},
{
"manufacturer": "Toyota",
"model": "Highlander",
"year": 2019,
"price": 35000
},
{
"manufacturer": "Toyota",
"model": "4Runner",
"year": 2019,
"price": 40000
},
{
"manufacturer": "Ford",
"model": "Fusion",
"year": 2019,
"price": 20000
},
{
"manufacturer": "Ford",
"model": "Focus",
"year": 2019,
"price": 25000
},
{
"manufacturer": "Ford",
"model": "Escape",
"year": 2019,
"price": 30000
},
{
"manufacturer": "Ford",
"model": "Explorer",
"year": 2019,
"price": 35000
},
{
"manufacturer": "Ford",
"model": "Expedition",
"year": 2019,
"price": 40000
}
];

const embedPromise = new Promise((resolve, reject) => {
const embedIframe = document.createElement('iframe');
embedIframe.style.height = '700px';
embedIframe.style.width = 'calc(100% - 4px)';
document.body.appendChild(embedIframe);

// Prepare the HTML content with the script tag.
const closeScriptTag = '</'+'script>';
const htmlContent = `
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="../dist/DataFormulator.js" defer onload="console.log('my loc'+document.location);parent.frameLoaded()" onerror="parent.frameError()">${closeScriptTag}
</body>
</html>
`;

// Define global functions for onload and onerror events of the script
window.frameLoaded = () => resolve(embedIframe);
window.frameError = () => reject(new Error('Script failed to load'));

// Write the HTML content to the iframe
embedIframe.contentWindow.document.open();
embedIframe.contentWindow.document.write(htmlContent);
embedIframe.contentWindow.document.close();
});
embedPromise.then((embedIframe) => {
embedIframe.contentWindow.postMessage({ actionName: 'setConfig', actionParams: { serverUrl: 'http://localhost:5000' } }, '*');
embedIframe.contentWindow.postMessage({ actionName: 'loadData', actionParams: { tableName: 'FabricTable', table } }, '*');
});

</script>
</body>

</html>
12 changes: 12 additions & 0 deletions embed/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Formulator embedded</title>
</head>
<body>
<div id="root"></div>
<script src="../dist/DataFormulator.js"></script>
</body>
</html>
116 changes: 116 additions & 0 deletions embed/postMessageTest.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Formulator embedded iframe</title>
</head>

<body>
<iframe id="df" src="./index.html" style="width:1500px;height:1000px"></iframe>
<div>
<button onclick="postMessageToIframe()">Post Load Data message to iframe</button>
</div>
<script>
function postMessageToIframe() {
var iframe = document.getElementById('df');

//create 10 rows of fake data objects like cars dataset. make sure not every manufacturer od Toyota
const data = [
{
"manufacturer": "Toyota",
"model": "Corolla",
"year": 2019,
"price": 20000
},
{
"manufacturer": "Toyota",
"model": "Camry",
"year": 2019,
"price": 25000
},
{
"manufacturer": "Toyota",
"model": "RAV4",
"year": 2019,
"price": 30000
},
{
"manufacturer": "Toyota",
"model": "Highlander",
"year": 2019,
"price": 35000
},
{
"manufacturer": "Toyota",
"model": "4Runner",
"year": 2019,
"price": 40000
},
{
"manufacturer": "Ford",
"model": "Fusion",
"year": 2019,
"price": 20000
},
{
"manufacturer": "Ford",
"model": "Focus",
"year": 2019,
"price": 25000
},
{
"manufacturer": "Ford",
"model": "Escape",
"year": 2019,
"price": 30000
},
{
"manufacturer": "Ford",
"model": "Explorer",
"year": 2019,
"price": 35000
},
{
"manufacturer": "Ford",
"model": "Expedition",
"year": 2019,
"price": 40000
}
];

//create a message object to send to the iframe
/* schema is:
interface LoadDataAction extends Action {
actionName: "loadData";
actionParams: {
tableName: string;
table: object[];
}
}
*/

const configMessage = {
actionName: 'setConfig',
actionParams: {
serverUrl: 'http://localhost:5000/',
}
};

iframe.contentWindow.postMessage(configMessage, '*');

const dataMessage = {
actionName: 'loadData',
actionParams: {
tableName: 'cars',
table: data,
}
};

iframe.contentWindow.postMessage(dataMessage, '*');
}
</script>
</body>

</html>
9 changes: 9 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,13 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
build: {
rollupOptions: {
output: {
entryFileNames: `DataFormulator.js`, // specific name for the main JS bundle
chunkFileNames: `assets/[name]-[hash].js`, // keep default naming for chunks
assetFileNames: `assets/[name]-[hash].[ext]` // keep default naming for other assets
}
}
},
});

0 comments on commit 6b8ba2c

Please sign in to comment.