Last active
May 28, 2025 17:54
-
-
Save bmingles/da24e80f27a90bcbd732398483873b5d to your computer and use it in GitHub Desktop.
Deephaven Theme - Postmessage Apis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<link | |
rel="icon" | |
type="image/png" | |
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAA9klEQVRYhe3WMUoDQRSH8e+tkw0bdhtj7wXSmUSIVboUVjYeIIU5hOIVBG8SUppSUBAstDSQFKkEWbOQBLPseoR5KWQt3tQf//nBNCO9y8UbihO14/dGP2n5OqFcjtv1gWYTwAHeUYAiL740bYkk2ssBgn3ivzgGMIABKgc4bVhkxbwUPnxdnGXfq+7VULNZc/mDGvAz275OTsI7X7fpDY9zDuaazV0eXFT+BAYwgAEMIOeP20wTXtdunjrhy6k3LFmko9aZZvMwaq6d1INYE0fBLgT8H04haT7frzSb8A+ewAAGMEDlAAdyqwk/OUqBqTcU0n0Av+DQNksxGrVgAAAAAElFTkSuQmCC" | |
/> | |
<title>Deephaven Iframe Tester</title> | |
<style> | |
html, | |
body { | |
background-color: midnightblue; | |
margin: 0; | |
font-family: sans-serif; | |
} | |
body::before { | |
color: white; | |
font-size: 2rem; | |
position: absolute; | |
top: 0; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
content: 'Parent Container'; | |
width: 100%; | |
height: 100%; | |
} | |
iframe { | |
display: block; | |
position: relative; | |
width: 100vw; | |
height: 100vh; | |
border: none; | |
} | |
#btnUpdateTheme { | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
position: absolute; | |
bottom: 0; | |
padding: 0.5rem 1rem 0.5rem 0.5rem; | |
&:before { | |
content: ''; | |
background: no-repeat left center | |
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAA9klEQVRYhe3WMUoDQRSH8e+tkw0bdhtj7wXSmUSIVboUVjYeIIU5hOIVBG8SUppSUBAstDSQFKkEWbOQBLPseoR5KWQt3tQf//nBNCO9y8UbihO14/dGP2n5OqFcjtv1gWYTwAHeUYAiL740bYkk2ssBgn3ivzgGMIABKgc4bVhkxbwUPnxdnGXfq+7VULNZc/mDGvAz275OTsI7X7fpDY9zDuaazV0eXFT+BAYwgAEMIOeP20wTXtdunjrhy6k3LFmko9aZZvMwaq6d1INYE0fBLgT8H04haT7frzSb8A+ewAAGMEDlAAdyqwk/OUqBqTcU0n0Av+DQNksxGrVgAAAAAElFTkSuQmCC); | |
background-size: cover; | |
width: 20px; | |
height: 20px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<iframe | |
src="http://localhost:10000/iframe/widget/?name=simple_ticking&theme=external-theme&preloadTransparentTheme=true" | |
></iframe> | |
<button id="btnUpdateTheme" onclick="onUpdateClick()">Update Theme</button> | |
<script> | |
// Get the origin of the Deephaven server | |
const dhServerOrigin = new URL(document.querySelector('iframe').src) | |
.origin; | |
let baseThemeKey = 'default-dark'; | |
/** Handle postMessage events from the child iframe */ | |
window.addEventListener( | |
'message', | |
function onMessage({ data, origin, source }) { | |
// Ignore messages that are not from our Deephaven server | |
if (origin !== dhServerOrigin) { | |
return; | |
} | |
// Respond to theme request from Deephaven in the iframe | |
if ( | |
data.message === | |
'io.deephaven.message.ThemeModel.requestExternalTheme' | |
) { | |
// Set a custom background color based on the base theme key | |
const bgColor = | |
baseThemeKey === 'default-dark' ? 'midnightblue' : 'aliceblue'; | |
source.postMessage( | |
{ | |
id: data.id, | |
payload: { | |
name: 'Iframe External Theme', | |
baseThemeKey, | |
cssVars: { | |
'--dh-color-grid-header-bg': bgColor, | |
'--dh-color-grid-bg': `color-mix(in srgb, ${bgColor} 70%, black 50%)`, | |
}, | |
}, | |
}, | |
origin | |
); | |
} | |
} | |
); | |
/** Initiate setting the theme from the parent window */ | |
function onUpdateClick() { | |
const iframeEl = document.querySelector('iframe'); | |
const childWindow = iframeEl.contentWindow; | |
// Base theme key determines whether base colors are for dark or light theme | |
baseThemeKey = | |
baseThemeKey === 'default-dark' ? 'default-light' : 'default-dark'; | |
// Set a custom background color based on the base theme key | |
const bgColor = | |
baseThemeKey === 'default-dark' ? 'midnightblue' : 'aliceblue'; | |
childWindow?.postMessage( | |
{ | |
message: 'io.deephaven.message.ThemeModel.requestSetTheme', | |
payload: { | |
name: 'Iframe External Theme', | |
baseThemeKey, | |
cssVars: { | |
'--dh-color-grid-header-bg': bgColor, | |
'--dh-color-grid-bg': `color-mix(in srgb, ${bgColor} 70%, black 50%)`, | |
}, | |
}, | |
}, | |
dhServerOrigin | |
); | |
} | |
/** Generate a random hex color */ | |
function getRandomHexColor() { | |
return `#${Math.floor(Math.random() * 16777215) | |
.toString(16) | |
.padStart(6, '0')}`; | |
} | |
/** Get light or dark base theme key based on given background hex color */ | |
function getBaseThemeKey(bgHex) { | |
// Remove '#' if present | |
bgHex = bgHex.replace('#', ''); | |
// Parse r, g, b values | |
const r = parseInt(bgHex.substr(0, 2), 16); | |
const g = parseInt(bgHex.substr(2, 2), 16); | |
const b = parseInt(bgHex.substr(4, 2), 16); | |
// Calculate brightness (YIQ formula) | |
const yiq = (r * 299 + g * 587 + b * 114) / 1000; | |
// Return black for light backgrounds, white for dark backgrounds | |
return yiq >= 128 ? 'default-light' : 'default-dark'; | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<link | |
rel="icon" | |
type="image/png" | |
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAA9klEQVRYhe3WMUoDQRSH8e+tkw0bdhtj7wXSmUSIVboUVjYeIIU5hOIVBG8SUppSUBAstDSQFKkEWbOQBLPseoR5KWQt3tQf//nBNCO9y8UbihO14/dGP2n5OqFcjtv1gWYTwAHeUYAiL740bYkk2ssBgn3ivzgGMIABKgc4bVhkxbwUPnxdnGXfq+7VULNZc/mDGvAz275OTsI7X7fpDY9zDuaazV0eXFT+BAYwgAEMIOeP20wTXtdunjrhy6k3LFmko9aZZvMwaq6d1INYE0fBLgT8H04haT7frzSb8A+ewAAGMEDlAAdyqwk/OUqBqTcU0n0Av+DQNksxGrVgAAAAAElFTkSuQmCC" | |
/> | |
<title>Deephaven Iframe Tester</title> | |
<style> | |
html, | |
body { | |
position: relative; | |
background-color: midnightblue; | |
margin: 0; | |
height: 100vh; | |
font-family: sans-serif; | |
} | |
body::before { | |
position: absolute; | |
color: white; | |
font-size: 2rem; | |
top: 0; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
content: 'Loading...'; | |
width: 100%; | |
height: 100%; | |
} | |
#loginForm { | |
display: grid; | |
position: absolute; | |
background-color: midnightblue; | |
color: white; | |
grid-template-rows: auto auto; | |
grid-auto-flow: column; | |
align-content: center; | |
align-items: center; | |
justify-content: center; | |
gap: 6px; | |
height: 100vh; | |
width: 100vw; | |
img { | |
grid-row: span 2; | |
height: 110px; | |
margin: -20px; | |
} | |
input { | |
height: 24px; | |
&:first-of-type { | |
grid-column: span 2; | |
} | |
} | |
button { | |
grid-row: span 2; | |
height: 66px; | |
padding: 0 1rem; | |
} | |
} | |
iframe { | |
display: block; | |
position: relative; | |
width: 100vw; | |
height: 100vh; | |
border: none; | |
} | |
#btnUpdateTheme { | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
position: absolute; | |
bottom: 0; | |
padding: 0.5rem 1rem 0.5rem 0.5rem; | |
&:before { | |
content: ''; | |
background: no-repeat left center | |
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAA9klEQVRYhe3WMUoDQRSH8e+tkw0bdhtj7wXSmUSIVboUVjYeIIU5hOIVBG8SUppSUBAstDSQFKkEWbOQBLPseoR5KWQt3tQf//nBNCO9y8UbihO14/dGP2n5OqFcjtv1gWYTwAHeUYAiL740bYkk2ssBgn3ivzgGMIABKgc4bVhkxbwUPnxdnGXfq+7VULNZc/mDGvAz275OTsI7X7fpDY9zDuaazV0eXFT+BAYwgAEMIOeP20wTXtdunjrhy6k3LFmko9aZZvMwaq6d1INYE0fBLgT8H04haT7frzSb8A+ewAAGMEDlAAdyqwk/OUqBqTcU0n0Av+DQNksxGrVgAAAAAElFTkSuQmCC); | |
background-size: cover; | |
width: 20px; | |
height: 20px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<form id="loginForm"> | |
<img | |
src="data:image/svg+xml;charset=utf-8,%3Csvg%20width%3D%22150%22%20height%3D%22150%22%20viewBox%3D%220%200%20150%20150%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M60%2090H30V120H60V90Z%22%20fill%3D%22%234DCCFA%22%2F%3E%0A%3Cpath%20d%3D%22M90%2030H60V60H90V30Z%22%20fill%3D%22%234DCCFA%22%2F%3E%0A%3Cpath%20d%3D%22M90%2090H60V120H90V90Z%22%20fill%3D%22%23FDD041%22%2F%3E%0A%3Cpath%20d%3D%22M120%2060H90V90H120V60Z%22%20fill%3D%22%23F33666%22%2F%3E%0A%3Cpath%20d%3D%22M30%2030H60V90H30V30Z%22%20fill%3D%22%233D51B3%22%2F%3E%0A%3C%2Fsvg%3E%0A" | |
/> | |
<input | |
autofocus | |
type="text" | |
name="serverUrl" | |
placeholder="Deephaven Server Url" | |
required | |
/> | |
<input type="text" name="username" placeholder="Username" required /> | |
<input type="password" name="password" placeholder="Password" required /> | |
<button type="submit">Login</button> | |
</form> | |
<script type="module"> | |
let dhServerUrl; // will be set at login | |
let baseThemeKey = 'default-dark'; | |
let refreshToken; | |
// Get DOM elements | |
const loginForm = document.getElementById('loginForm'); | |
// Register event listeners | |
loginForm.addEventListener('submit', onLogin); | |
window.addEventListener('message', onMessage); | |
/** Handle login form submission */ | |
async function onLogin(event) { | |
event.preventDefault(); | |
const formData = new FormData(event.target); | |
dhServerUrl = new URL(formData.get('serverUrl')); | |
const username = formData.get('username'); | |
const password = formData.get('password'); | |
// Disable the login form elements | |
Array.from(loginForm.elements).forEach((el) => { | |
el.disabled = true; | |
}); | |
await loadJsapi(dhServerUrl); | |
refreshToken = await getRefreshToken( | |
dhServerUrl.host, | |
username, | |
password | |
); | |
loginForm.remove(); | |
await loadIframe(dhServerUrl); | |
createThemeButton(); | |
} | |
/** Handle postMessage events from the child iframe */ | |
function onMessage({ data, origin, source }) { | |
// Ignore messages that are not from our Deephaven server | |
if (origin !== dhServerUrl.origin) { | |
return; | |
} | |
// Auth token request | |
if ( | |
data.message === 'io.deephaven.message.IframeContent.authTokenRequest' | |
) { | |
source.postMessage({ id: data.id, payload: refreshToken }, origin); | |
return; | |
} | |
// Settings request | |
if ( | |
data.message === 'io.deephaven.message.IframeContent.settingsRequest' | |
) { | |
const newWorkerName = `IframeContentTester - Create Worker - ${crypto.randomUUID()}`; | |
source.postMessage( | |
{ | |
id: data.id, | |
payload: { | |
newWorkerName, | |
settings: { | |
heapSize: 0.5, | |
language: 'python', | |
}, | |
isLegacyWorkerKindSupported: false, | |
showHeader: false, | |
}, | |
}, | |
origin | |
); | |
return; | |
} | |
// Theme request | |
if ( | |
data.message === | |
'io.deephaven.message.ThemeModel.requestExternalTheme' | |
) { | |
// Set a custom background color based on the base theme key | |
const bgColor = | |
baseThemeKey === 'default-dark' ? 'midnightblue' : 'aliceblue'; | |
source.postMessage( | |
{ | |
id: data.id, | |
payload: { | |
name: 'Iframe External Theme', | |
baseThemeKey, | |
cssVars: { | |
'--dh-color-bg': bgColor, | |
}, | |
}, | |
}, | |
origin | |
); | |
return; | |
} | |
console.warn('Unsupported message:', data.message); | |
} | |
/** Dynamically load the jsapi from the Deephaven server */ | |
async function loadJsapi(dhServerUrl) { | |
const { promise, resolve } = Promise.withResolvers(); | |
const scriptUrl = new URL( | |
'irisapi/irisapi.nocache.js', | |
dhServerUrl.origin | |
); | |
const jsApiScript = document.createElement('script'); | |
jsApiScript.src = scriptUrl; | |
jsApiScript.onload = resolve; | |
document.body.appendChild(jsApiScript); | |
return promise; | |
} | |
/** Load Deephaven in an iframe */ | |
async function loadIframe(dhServerUrl) { | |
const { promise, resolve } = Promise.withResolvers(); | |
const iframeUrl = new URL( | |
'iriside/iframecontent/createworker/?theme=external-theme&preloadTransparentTheme=true', | |
dhServerUrl.origin | |
); | |
const iframe = document.createElement('iframe'); | |
iframe.src = iframeUrl.href; | |
iframe.onload = resolve; | |
document.body.prepend(iframe); | |
return promise; | |
} | |
/** Create a button to update the theme */ | |
function createThemeButton() { | |
const button = document.createElement('button'); | |
button.id = 'btnUpdateTheme'; | |
button.innerText = 'Update Theme'; | |
button.addEventListener('click', onUpdateClick); | |
document.body.appendChild(button); | |
} | |
/** Login and get refresh token */ | |
async function getRefreshToken(host, username, token) { | |
const wsUrl = `wss://${host}/socket`; | |
const client = new iris.Client(wsUrl); | |
// Wait for client to connect | |
await new Promise((resolve) => | |
client.addEventListener(iris.Client.EVENT_CONNECT, resolve) | |
); | |
// Register for refresh token updates before loggin in | |
const refreshTokenPromise = new Promise((resolve) => { | |
client.addEventListener( | |
iris.Client.EVENT_REFRESH_TOKEN_UPDATED, | |
(event) => { | |
// Destructure the refresh token so that we have a serializable | |
// object to send to DH | |
const { bytes, expiry } = event.detail; | |
resolve({ bytes, expiry }); | |
} | |
); | |
}); | |
await client.login({ username, token, type: 'password' }); | |
return refreshTokenPromise; | |
} | |
/** Initiate setting the theme from the parent window */ | |
function onUpdateClick() { | |
const iframeEl = document.querySelector('iframe'); | |
const childWindow = iframeEl.contentWindow; | |
// Base theme key determines whether base colors are for dark or light theme | |
baseThemeKey = | |
baseThemeKey === 'default-dark' ? 'default-light' : 'default-dark'; | |
// Set a custom background color based on the base theme key | |
const bgColor = | |
baseThemeKey === 'default-dark' ? 'midnightblue' : 'aliceblue'; | |
childWindow?.postMessage( | |
{ | |
message: 'io.deephaven.message.ThemeModel.requestSetTheme', | |
payload: { | |
name: 'Iframe External Theme', | |
baseThemeKey, | |
cssVars: { | |
'--dh-color-bg': bgColor, | |
}, | |
}, | |
}, | |
dhServerUrl.origin | |
); | |
} | |
/** Generate a random hex color */ | |
function getRandomHexColor() { | |
return `#${Math.floor(Math.random() * 16777215) | |
.toString(16) | |
.padStart(6, '0')}`; | |
} | |
/** Get light or dark base theme key based on given background hex color */ | |
function getBaseThemeKey(bgHex) { | |
// Remove '#' if present | |
bgHex = bgHex.replace('#', ''); | |
// Parse r, g, b values | |
const r = parseInt(bgHex.substr(0, 2), 16); | |
const g = parseInt(bgHex.substr(2, 2), 16); | |
const b = parseInt(bgHex.substr(4, 2), 16); | |
// Calculate brightness (YIQ formula) | |
const yiq = (r * 299 + g * 587 + b * 114) / 1000; | |
// Return black for light backgrounds, white for dark backgrounds | |
return yiq >= 128 ? 'default-light' : 'default-dark'; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment